分析某环境中ss结果中Send-Q为0的原因

2024-05-20

某个环境中ss结果里的Send-Q为0,跟踪代码分析原因。

1. 背景

上一篇文章(TCP半连接全连接(一) – 全连接队列相关过程)中提到ss结果里的Send-Q是全连接队列的长度,也提到ssnetstat实现有差异。

在某环境中查看信息时,ss -lt发现所有监听端口的Send-Q都是0,本篇文章进行流程跟踪和原因定位。

2. 现象和问题定位

2.1. 现象

找几个环境对比现象,有一个环境会出现上述问题,其他环境正常。内核版本也各有区别,3.x、4.x、5.10等,和高低无直接关系。

出现上述问题的环境:

1
2
3
[root@rabbitmq2 iproute2-5.15.0]# ./misc/ss  -lt
State       Recv-Q     Send-Q        Local Address:Port  Peer Address:Port     Process
LISTEN      0          0             0.0.0.0:12345       0.0.0.0:*

正常环境:

1
2
3
State       Recv-Q     Send-Q      Local Address:Port     Peer Address:Port     Process
LISTEN      0          1024        0.0.0.0:12345          0.0.0.0:*
LISTEN      0          1024        0.0.0.0:34567          0.0.0.0:*

2.2. 先ltrace跟踪

ltrace ss -lt,看一下大概流程。可以看到创建socket,sendmsg等流程,但是最后又打开了/proc/net/tcp解析(只读打开)

2.3. 源码流程分析

1、下载源码:

ss --version结果里可以看到其属于iproute2

下载源码进行编译。为了gdb调试,config.mk中添加-g

2、源码流程分析

ss代码在:iproute2-5.15.0\iproute2-5.15.0\misc\ss.c

我们查看-lt,所以关注获取tcp相关信息流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// iproute2-5.15.0\iproute2-5.15.0\misc\ss.c
int main(int argc, char *argv[])
{
    ...
    // 解析参数:
    case 't':
        filter_db_set(&current_filter, TCP_DB, true);
        break;
    ...
    // tcp处理:
    if (current_filter.dbs & (1<<TCP_DB))
        tcp_show(&current_filter);
    ...
}

static int tcp_show(struct filter *f)
{
    ...
    dg_proto = TCP_PROTO;

    // 下面ltrace跟踪可知,返回 NULL
    if (getenv("TCPDIAG_FILE"))
        return tcp_show_netlink_file(f);

    // inet_show_netlink 会创建socket并请求
    // 下面ltrace跟踪可知,PROC_NET_TCP、PROC_ROOT均返回 NULL,所以会走inet_show_netlink逻辑,但是有的环境下最后返回-1
    if (!getenv("PROC_NET_TCP") && !getenv("PROC_ROOT")
        && inet_show_netlink(f, NULL, IPPROTO_TCP) == 0)
        // 此处通过socket请求成功后,就不往下了
        return 0;
    
    ...
    // 下面都是打开本地文件 /proc/net/tcp 或 net/tcp6。上述有的环境inet_show_netlink返回-1,所以继续读本地文件
    if (f->families & FAMILY_MASK(AF_INET)) {
        if ((fp = net_tcp_open()) == NULL)
            goto outerr;

        setbuffer(fp, buf, bufsize);
        if (generic_record_read(fp, tcp_show_line, f, AF_INET))
            goto outerr;
        fclose(fp);
    }
    ...
}


static int inet_show_netlink(struct filter *f, FILE *dump_fp, int protocol)
{
    int err = 0;
    struct rtnl_handle rth, rth2;
    ...
    // 创建了一个原始套接字,可以用于发送和接收包含NETLINK(内核和用户空间进程之间的通信)消息的原始网络数据包。
    if (rtnl_open_byproto(&rth, 0, NETLINK_SOCK_DIAG))
        return -1;
    ...
again:
    // 发送请求
    if ((err = sockdiag_send(family, rth.fd, protocol, f)))
        goto Exit;

    // 接收应答,show_one_inet_sock 是过滤信息的函数指针
    if ((err = rtnl_dump_filter(&rth, show_one_inet_sock, &arg))) {
        if (family != PF_UNSPEC) {
            family = PF_UNSPEC;
            goto again;
        }
        goto Exit;
    }
    ...
Exit:
    ...
    rtnl_close(&rth);
    // 在此处加gdb断点,最后看是会返回-1
    return err;
}


// 处理 recvmsg收到的信息
static int show_one_inet_sock(struct nlmsghdr *h, void *arg)
{
    ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// iproute2-5.15.0\iproute2-5.15.0\include\libnetlink.h
#define rtnl_dump_filter(rth, filter, arg) rtnl_dump_filter_nc(rth, filter, arg, 0)

// iproute2-5.15.0\iproute2-5.15.0\lib\libnetlink.c
int rtnl_dump_filter_nc(struct rtnl_handle *rth,
            rtnl_filter_t filter,
            void *arg1, __u16 nc_flags)
{
    const struct rtnl_dump_filter_arg a[] = {
        {
            .filter = filter, .arg1 = arg1,
            .nc_flags = nc_flags,
        },
        { },
    };

    return rtnl_dump_filter_l(rth, a);
}

static int rtnl_dump_filter_l(struct rtnl_handle *rth,
                  const struct rtnl_dump_filter_arg *arg)
{
    char *buf;
    ...
    while (1) {
        const struct rtnl_dump_filter_arg *a;
        ...
        // 数据接收在 buf
        status = rtnl_recvmsg(rth->fd, &msg, &buf);
        // 遍历过滤规则列表 arg是头
        for (a = arg; a->filter; a++) {
            struct nlmsghdr *h = (struct nlmsghdr *)buf;

            while(h列表循环){
                ...
                // 过滤信息,filter是传入的函数指针,inet_show_netlink调用rtnl_dump_filter时,会传入相应函数指针(ss -lnt是 show_one_inet_sock)
                err = a->filter(h, a->arg1);
                ...
            }
        }
    }
}

3. gdb调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
gdb ./ss

# 先调试ss.c流程,设置断点,可以如下查看对应的代码上下行
# 3657行是 inet_show_netlink 中return前一行
(gdb) b ss.c:3657

(gdb) r -lt

# 每次断点都打印下返回值,最后会返回-1(调试发现失败后,重试一次还是失败,所以最后还是走了读取本地tcp文件解析)
(gdb) p err
$5 = -1

...

# 继续调试内部为什么会失败。先delete 清理之前的breakpoints,再打新断点
# 891行如下代码可看到是判断 h->nlmsg_type,基于其成功失败走不同处理
(gdb) b libnetlink.c:891

...

# 可看到结果是2,即NLMSG_ERROR。即每次 rtnl_recvmsg的内容中都是失败的
(gdb) p h->nlmsg_type
$15 = 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(gdb) l ss.c:3642
3637    again:
3638            if ((err = sockdiag_send(family, rth.fd, protocol, f)))
3639                    goto Exit;
3640
3641            if ((err = rtnl_dump_filter(&rth, show_one_inet_sock, &arg))) {
3642                    if (family != PF_UNSPEC) {
3643                            family = PF_UNSPEC;
3644                            goto again;
3645                    }
3646                    goto Exit;
(gdb) 
3647            }
3648            if (family == PF_INET && preferred_family != PF_INET) {
3649                    family = PF_INET6;
3650                    goto again;
3651            }
3652
3653    Exit:
3654            rtnl_close(&rth);
3655            if (arg.rth)
3656                    rtnl_close(arg.rth);
(gdb) 
3657            return err;
3658    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
(gdb) l libnetlink.c:870
865                     int msglen = 0;
866
867                     status = rtnl_recvmsg(rth->fd, &msg, &buf);
868                     if (status < 0)
869                             return status;
870
871                     if (rth->dump_fp)
872                             fwrite(buf, 1, NLMSG_ALIGN(status), rth->dump_fp);
873
874                     for (a = arg; a->filter; a++) {
(gdb) 
875                             struct nlmsghdr *h = (struct nlmsghdr *)buf;
876
877                             msglen = status;
878
879                             while (NLMSG_OK(h, msglen)) {
880                                     int err = 0;
881
882                                     h->nlmsg_flags &= ~a->nc_flags;
883
884                                     if (nladdr.nl_pid != 0 ||
(gdb) 
885                                         h->nlmsg_pid != rth->local.nl_pid ||
886                                         h->nlmsg_seq != rth->dump)
887                                             goto skip_it;
888
889                                     if (h->nlmsg_flags & NLM_F_DUMP_INTR)
890                                             dump_intr = 1;
891
892                                     if (h->nlmsg_type == NLMSG_DONE) {
893                                             err = rtnl_dump_done(h, a);
894                                             if (err < 0) {
(gdb) 
895                                                     free(buf);
896                                                     return -1;
897                                             }
898
899                                             found_done = 1;
900                                             break; /* process next filter */
901                                     }
902

通过gdb调试过程,发现有问题的环境里,recvmsg收到的结果中内容就是失败状态,重试后也是失败。

下一步定位思路:摘抄ss.c里发送接收的参数,写个简单demo自行发送请求,接收内容。

查看发送请求相关的代码中,有部分bpf关键字,于是思路转变成会不会类似bpf有内核模块没起来。

查看之后,确实如此。

  • 正常环境(ss -lt正常)
1
2
3
[root@localhost misc]# lsmod |grep tcp_diag
tcp_diag               16384  0
inet_diag              28672  4 tcp_diag,sctp_diag,raw_diag,udp_diag
  • 异常环境:没有起tcp_diag模块
1
[root@rabbitmq2 iproute2-5.15.0]# lsmod |grep tcp_diag
  • 3.x内核环境(ss -lt正常):
1
2
3
[root@localhost ~]# lsmod |grep tcp_diag
tcp_diag               12591  0 
inet_diag              18949  2 tcp_diag,udp_diag

所以是ss依赖的tcp_diag模块没有加载,导致正常方式获取失败后,还是通过老方式去解析/proc/net/tcp文件内容了。

而为什么/proc/net/tcp里(netstat就是解析这个内容)不展示全连接队列长度,待定,后续分析netstat流程时再单独分析。

4. 小结

通过代码梳理和gdb调试,定位到ss在不同环境下Send-Q表现不同的原因。

虽然结果很简单,就是tcp_diag内核模块没加载,但定位过程挺有收获。

5. 参考

1、ss源码

2、gpt



Comments