某个环境中ss结果里的Send-Q
为0,跟踪代码分析原因。
1. 背景
上一篇文章(TCP半连接全连接(一) – 全连接队列相关过程)中提到ss
结果里的Send-Q
是全连接队列的长度,也提到ss
和netstat
实现有差异。
在某环境中查看信息时,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(¤t_filter, TCP_DB, true);
break;
...
// tcp处理:
if (current_filter.dbs & (1<<TCP_DB))
tcp_show(¤t_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