1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 网络状态观察
本节描述对网络硬件、网络子系统进行观察的方式方法和工具
。内容的组织形式是:先给出对应目标的观察方法
,然后分析其相关的源码实现
(包括用户空间
和内核空间
两部分)。
2.1 硬件:网络硬件 调试观察
2.1.1 网络 PHY 芯片 调试观察
2.1.1.1 观察方法
调试观察 网络 PHY 芯片,主要是查看和修改 PHY 芯片的寄存器。有一些开源工具,如 mii-tool,mdio-tools,phytool 等等,都可对 PHY 芯片进行调试和观察。本文只对 phytool
进行分析,没什么原因,因为它最简单易用,可直接读写 PHY 寄存器。
来看下如何为 arm32 架构交叉编译 phytool,从前面给出的地址下载源码,解压后切换到源码目录,将交叉编译器的路径扩展到 PATH
,然后运行下面的命令:
make CC=arm-linux-gnueabihf-gcc # CC 修改为你实际使用的交叉编译器
然后会生成一个名为 phytool
可执行文件。接下来,示范下通过 phytool
如何读写 PHY 寄存器:
# 下面示范对 YT8531C PHY 寄存器读写操作
# 写寄存器 0xa0
$ phytool write eth0/1/0x1e 0xa0 # 写的扩展寄存器,先要选择寄存器 page
$ phytool write eth0/1/0x1f 0xa8d0 # 寄存器 0xa0 写入值 0xa8d0
# 读寄存 0xa3
$ phytool write eth0/1/0x1e 0xa4
$ phytool read eth0/1/0x1f
0x00a6
phytool
语法有点独特,phy write
后的参数格式:网络接口/PHY地址/寄存器地址 写入值
。相对于 phytool write
,phytool read
不需要最后一个参数。phytool
还有几个其它的指令,都相对简单,感兴趣的读者,可自行研究。
2.1.1.2 源码实现
phytool
的核心逻辑
是:通过网卡驱动的 ioctl(SIOCGMIIREG)
读取 PHY 寄存器,通过 ioctl(SIOCSMIIREG)
写 PHY 寄存器。
2.1.1.2.1 用户空间部分
/* phytool/phytool.c */
/* 读操作 */
main()
phytool_read()
phytool_parse_loc() /* 参数解析 */
val = phy_read (&loc);
int err = __phy_op(loc, &val, SIOCGMIIREG);
static int sd = -1;
struct ifreq ifr;
struct mii_ioctl_data* mii = (struct mii_ioctl_data *)(&ifr.ifr_data);
int err;
if (sd < 0)
sd = socket(AF_INET, SOCK_DGRAM, 0);
...
strncpy(ifr.ifr_name, loc->ifnam, sizeof(ifr.ifr_name));
mii->phy_id = loc->phy_id; /* PHY 地址 */
mii->reg_num = loc->reg; /* PHY 寄存器地址 */
mii->val_in = *val; /* 写操作: 要写入的值 */
mii->val_out = 0;
/*
* 读寄存器:发送 SIOCGMIIREG 命令
* 写寄存器:发送 SIOCSMIIREG 命令
*/
err = ioctl(sd, cmd, &ifr);
*val = mii->val_out; /* 读操作: 读回的 PHY 寄存器值 */
...
...
printf("%#.4x\n", val); /* 打印读回来的值 */
/* 写操作 */
main()
phytool_write()
phytool_parse_loc() /* 参数解析 */
...
val = strtoul(argv[1], NULL, 0); /* 要写入的值 */
err = phy_write (&loc, val);
int err = __phy_op(loc, &val, SIOCSMIIREG);
/* 参考读操作解析 */
...
2.1.1.2.2 内核空间部分
sock_ioctl() /* net/socket.c */
sock_do_ioctl()
dev_ioctl() /* net/core/dev_ioctl.c */
...
switch (cmd) {
...
case SIOCGMIIPHY:
case SIOCGMIIREG: /* 读取 PHY 寄存器 */
case SIOCSIFNAME:
...
ret = dev_ifsioc(net, &ifr, cmd);
...
struct net_device *dev = __dev_get_by_name(net, ifr->ifr_name);
const struct net_device_ops *ops;
// &stmmac_netdev_ops
ops = dev->netdev_ops; /* 网卡驱动接口,以 STMicro 的 MAC 为例 */
switch (cmd) {
...
default:
if ((cmd >= SIOCDEVPRIVATE &&
...
cmd == SIOCGMIIREG ||
cmd == SIOCSMIIREG ||
...)
if (ops->ndo_do_ioctl) {
...
ops->ndo_do_ioctl(dev, ifr, cmd) = stmmac_ioctl()
switch (cmd) {
case SIOCGMIIPHY:
case SIOCGMIIREG: /* 读 PHY 寄存器 */
case SIOCSMIIREG: /* 写 PHY 寄存器 */
...
ret = phy_mii_ioctl(dev->phydev, rq, cmd);
break;
...
}
}
}
...
...
case SIOCSMIIREG: /* 写 PHY 寄存器 */
...
...
ret = dev_ifsioc(net, &ifr, cmd);
/* 后续路径同 读操作 一样 */
...
return ret;
}
// 上接 phy_mii_ioctl()
phy_mii_ioctl()
switch (cmd) {
case SIOCGMIIPHY:
mii_data->phy_id = phydev->mdio.addr;
/* fall through */
case SIOCGMIIREG:
/* 通过 MDIO 接口读 PHY 寄存器 */
mii_data->val_out = mdiobus_read(phydev->mdio.bus,
mii_data->phy_id,
mii_data->reg_num);
case SIOCSMIIREG:
...
/* 通过 MDIO 接口写 PHY 寄存器 */
mdiobus_write(phydev->mdio.bus, mii_data->phy_id,
mii_data->reg_num, val);
...
...
}
2.1.2 网卡调试观察
2.1.2.1 网卡性能评估
2.1.2.1.1 网卡性能评估方法
网卡性能的评估,通常会用 iperf 分别测试 TCP
和 UDP
通信的带宽。iperf
是一个 服务端/客户端 模式
的测试工具:一台机器运行 iperf 服务端(通过 -s
参数指定),一台机器运行 iperf 客户端(通过 -c
参数指定)。默认情形下,客户端负责发送数据,服务端负责接收数据,因此此时重点测试的是服务端的 RX
和 客户端的 TX
,虽然各自的另外一个方向也有数据传输,但数据量很小。如果要测试各自另一个传输方向,调换彼此的角色即可:服务端变客户端,客户端变服务端。
TCP
方式是可靠连接,其测试的是可靠通信的带宽
。TCP
方式下,数据中间经过各式各样的缓冲,以及数据滑动窗口、数据拥塞控制,传输随可靠但有延迟,但排除交换机、路由器等中间设备的影响后(可以用网线直连测试的机器),正常情况下,测试的带宽速度应接近网卡本身的带宽。看一个测试 TCP
可靠连接下带宽的例子:
上例中测试的是一个 1000Mbps
网卡的情形,由于存在一些问题,可以看到 TCP
可靠通信的带宽,远远低于 1000Mbps
,平均只有 420Mbps
的带宽。
UDP
方式(通过 -u
参考指定)是非可靠方式,会出现丢包的情况,通过 -b
参数去指定请求的带宽速度(当然最大无法指定超过网卡的带宽速度),然后看丢包的比例。当然,同样要排除交换机、路由器等中间设备的影响后(可以用网线直连测试的机器)。看一个例子:
还是上面这个有问题的网卡,看到了吗?平均 76% 的丢包率
,相当恐怖。
2.1.2.1.2 代码实现
iperf
的核心逻辑:对于 TCP
可靠通信的带宽测试,就是 send()
和 recv()
,然后统计收到的数据总数,每隔1秒(默认,可通过 -i
参数修改该间隔)报告一次一个间隔周期内的带宽(间隔周期内收到的数据总数 / 间隔周期时长
),测试结束也会报告一次带宽(收到的数据总数 / 测试时长
);对于 UDP
通信的带宽测试,就是 sendto()
和 recvfrom()
,然后统计收到的数据报总数,每隔1秒(默认,可通过 -i
参数修改该间隔)报告一次一个间隔周期内的丢包率
,测试结束也会报告一次总的丢包率
。至于 UDP 是如何统计丢包率的?也不复杂,就是在通信数据报内内置了包数量的信息(包括发送和接收的),收发双方都去对比收发的数量,然后计算丢包率。
除了以上提到的核心逻辑,iperf
的代码值得注意的就是 UDP
带宽测试下的 -b
参数,它指定了请求的带宽:
// iperf3 -b 1000M
setsockopt(s, SOL_SOCKET, SO_MAX_PACING_RATE, &rate, sizeof(rate)); // 流量控制
2.1.2.2 网卡 观察 和 调优
2.1.2.2.1 网卡收发统计数据观察
可以通过两种方式来观察网卡收发统计数据:一是通过网卡驱动导出统计数据 /proc/net/dev
;二是通过 ethtool
来观察网卡驱动接口 struct ethtool_ops
导出的统计数据。
先看一下通过网卡驱动导出的统计数据的观察,可以直接读 /proc/net/dev
文件,也可以通过 ifconfig
导出:
$ cat /proc/net/dev
Inter-| Receive | Transmit
face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
lo: 51067 680 0 0 0 0 0 0 51067 680 0 0 0 0 0 0
ens33: 762481 1415 0 0 0 0 0 0 14367 124 0 0 0 0 0 0
$ ifconfig ens33
ens33 Link encap:Ethernet HWaddr 00:0c:29:4f:b1:e7
inet addr:192.168.0.4 Bcast:192.168.0.255 Mask:255.255.255.0
inet6 addr: fe80::bbc7:b835:be2a:a578/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:3057 errors:0 dropped:0 overruns:0 frame:0
TX packets:146 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:1661083 (1.6 MB) TX bytes:16653 (16.6 KB)
/proc/net/dev
文件导出内容的细节,可以阅读内核源码文件 net/core/net-procfs.c
中的函数 dev_seq_show()
。而 ifconfig
的实现就是读取解析 /proc/net/dev
以及其它相关文件内容。
再看通过 ethtool 命令 ethtool -S <IFACE>
观察网卡的收发统计数据,<IFACE>
为网卡名。
$ ethtool -S eth0
# ethtool -S eth0
NIC statistics:
Good Rx Frames: 10079353
Broadcast Rx Frames: 75797
Multicast Rx Frames: 96406
Pause Rx Frames: 0
Rx CRC Errors: 0
Rx Align/Code Errors: 0
Oversize Rx Frames: 0
Rx Jabbers: 0
Undersize (Short) Rx Frames: 0
Rx Fragments: 0
Rx Octets: 1021757198
Good Tx Frames: 749537
Broadcast Tx Frames: 6
Multicast Tx Frames: 15
Pause Tx Frames: 0
Deferred Tx Frames: 0
Collisions: 0
Single Collision Tx Frames: 0
Multiple Collision Tx Frames: 0
Excessive Collisions: 0
Late Collisions: 0
Tx Underrun: 0
Carrier Sense Errors: 0
Tx Octets: 1117837490
Rx + Tx 64 Octet Frames: 250017
Rx + Tx 65-127 Octet Frames: 68086
Rx + Tx 128-255 Octet Frames: 34404
Rx + Tx 256-511 Octet Frames: 6330
Rx + Tx 512-1023 Octet Frames: 1307359
Rx + Tx 1024-Up Octet Frames: 9162694
Net Octets: 2139594688
Rx Start of Frame Overruns: 2998092
Rx Middle of Frame Overruns: 0
Rx DMA Overruns: 2998092
Rx DMA chan 0: head_enqueue: 1
Rx DMA chan 0: tail_enqueue: 6988545
Rx DMA chan 0: pad_enqueue: 0
Rx DMA chan 0: misqueued: 571
Rx DMA chan 0: desc_alloc_fail: 0
Rx DMA chan 0: pad_alloc_fail: 0
Rx DMA chan 0: runt_receive_buf: 0
Rx DMA chan 0: runt_transmit_bu: 0
Rx DMA chan 0: empty_dequeue: 0
Rx DMA chan 0: busy_dequeue: 505265
Rx DMA chan 0: good_dequeue: 6984450
Rx DMA chan 0: requeue: 446
Rx DMA chan 0: teardown_dequeue: 0
Tx DMA chan 0: head_enqueue: 32715
Tx DMA chan 0: tail_enqueue: 716822
Tx DMA chan 0: pad_enqueue: 0
Tx DMA chan 0: misqueued: 1284
Tx DMA chan 0: desc_alloc_fail: 0
Tx DMA chan 0: pad_alloc_fail: 0
Tx DMA chan 0: runt_receive_buf: 0
Tx DMA chan 0: runt_transmit_bu: 11816
Tx DMA chan 0: empty_dequeue: 32712
Tx DMA chan 0: busy_dequeue: 472547
Tx DMA chan 0: good_dequeue: 749537
Tx DMA chan 0: requeue: 22009
Tx DMA chan 0: teardown_dequeue: 0
具体数据的含义,每个网卡驱动都有可能不同,需要查看网卡驱动的 struct ethtool_ops
。
2.1.2.2.1 网卡调优
调优之前,要确定目标:吞吐量(更高延迟) 或 高响应(更小延迟)。按目标需求,可以在如下方面进行优化:
. 中断聚合,即调整中断次数,将多帧数据在一个中断内提取。这要求硬件支持,也要求更大的硬件缓冲。
用户空间可通过
ethtool -C <IFACE> rx-usecs N # 每 N 微妙 生成一次 RX 中端
ethtool -C <IFACE> rx-frames N # 每 N 帧 数据生成一次 RX 中端
ethtool -C <IFACE> tx-usecs N # 每 N 微妙 向网卡提交一次数据
ethtool -C <IFACE> tx-frames N # 聚合 N 帧 向网卡提交一次数据
等命令来进行配置:如果注重吞吐量,则将 N 调大;否则,N 应尽量小。
. 调整收发缓冲大小
ethtool -rx N -tx N ...
. 调整 收发队列个数,使用 DMA 传送网卡数据时,收发队列队列个数通常对应 DMA 通道数。
对于收发队列支持独立设置的情形,使用命令 ethtool -L rx N tx N 独立设置收发队列个数
对于收发队列不支持独立设置的情形,使用命令 ethtool -L combined N 同时设置收发队列个数
. 如果网卡驱动使用 NAPI 方式提取接收的数据,则要按需求适当调整 poll weight 。
. 支持硬件多队列的网卡,均匀的各队列中断分配到各个 CPU
. 启用 TSO(TCP Segmentation Offload), ...
......
2.1.2.2.3 相关代码实现
这里仅列举通过 ethtool -S <IFACE>
来进行网卡收发统计数据观察的代码实现主干流程(经过修剪改动)。先看用户空间部分
:
/*
* 网卡有一列的统计数据,内置在网卡驱动的 ethtool 支持实现中。
*/
const char *ifname = "eth0"; // 用想要观察的网卡名称代替
int fd;
unsigned int n_stats, i;
struct ifreq ifr;
struct {
struct ethtool_sset_info hdr;
unsigned int buf[1];
} sset_info;
struct ethtool_gstrings *strings; // 统计数据字段名称
struct ethtool_stats *stats; // 统计数据
unsigned int sz_stats;
int ret;
fd = socket(AF_INET, SOCK_DGRAM, 0);
/*
* 1. 应用程序首先通过 ioctl(fd, SIOCETHTOOL, ETHTOOL_GSSET_INFO) 来获取这些
* 统计数据字段的列表长度。
*/
sset_info.hdr.cmd = ETHTOOL_GSSET_INFO;
sset_info.hdr.reserved = 0;
sset_info.hdr.sset_mask = 1ULL << ETH_SS_STATS;
memset(&ifr, 0, sizeof(ifr));
strcpy(ifr.ifr_name, ifname);
ifr.ifr_data = (void *)&sset_info;
ret = ioctl(fd, SIOCETHTOOL, &ifr);
...
/*
* 2. 应用程序首先通过 ioctl(fd, SIOCETHTOOL, ETHTOOL_GSTRINGS) 来获取这些
* 统计数据字段名称的列表。
*/
strings = calloc(1, sizeof(*strings) + n_stats * ETH_GSTRING_LEN);
...
strings->cmd = ETHTOOL_GSTRINGS;
strings->string_set = ETH_SS_STATS;
strings->len = n_stats;
memset(&ifr, 0, sizeof(ifr));
strcpy(ifr.ifr_name, ifname);
ifr.ifr_data = (void *)strings;
ret = ioctl(fd, SIOCETHTOOL, &ifr);
...
/*
* 3. 应用程序首先通过 ioctl(fd, SIOCETHTOOL, ETHTOOL_GSTATS) 来获取这些
* 统计数据字段的值。
*/
sz_stats = n_stats * sizeof(unsigned long long);
stats = calloc(1, sz_stats + sizeof(struct ethtool_stats));
stats->cmd = ETHTOOL_GSTATS;
stats->n_stats = n_stats;
memset(&ifr, 0, sizeof(ifr));
strcpy(ifr.ifr_name, ifname);
ifr.ifr_data = (void *)stats;
ret = ioctl(fd, SIOCETHTOOL, &ifr);
/*
* 打印个统计字段的值。
*/
for (i = 0, p = strings->data; i < n_stats; i++, p += ETH_GSTRING_LEN)
printf("%s: %llu\n", p, stats->data[i]);
再看内核空间部分(只描述获取统计数据命令 ETHTOOL_GSTATS 的流程,其余部分类似,感兴趣的读者可自行阅读源码):
sys_ioctl()
...
sock_ioctl() /* net/socket.c */
sock_do_ioctl(net, sock, cmd, arg)
dev_ioctl(net, cmd, argp) /* net/core/dev_ioctl.c */
...
switch (cmd) {
...
case SIOCETHTOOL:
...
ret = dev_ethtool(net, &ifr);
switch (ethcmd) {
...
case ETHTOOL_GSTATS:
rc = ethtool_get_stats(dev, useraddr); /* net/core/ethtool.c */
n_stats = ops->get_sset_count(dev, ETH_SS_STATS);
...
/*
* 调用网卡驱动的统计数据获取接口获取数据,
* 如 stmmac_get_ethtool_stats() 。
*/
ops->get_ethtool_stats(dev, &stats, data);
...
break;
...
}
...
return ret;
...
}
2.2 软件:网络协议栈 调试观察
2.2.1 查看 TCP 套接字信息数据
2.2.1.1 观察方法
内核通过 /proc/net/tcp
(IPv4
) 和 /proc/net/tcp6
(IPv6
) 导出系统中 TCP 套接字的信息:
$ cat /proc/net/tcp # 导出 IPv4 TCP 套接字信息
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 00000000:C875 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 31122 1 0000000000000000 100 0 0 10 0
1: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 25241 1 0000000000000000 100 0 0 10 0
2: 0100007F:0277 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 40284 1 0000000000000000 100 0 0 10 0
3: 00000000:0801 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 28384 1 0000000000000000 100 0 0 10 0
4: 00000000:C887 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 31106 1 0000000000000000 100 0 0 10 0
5: 00000000:9C0D 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 28398 1 0000000000000000 100 0 0 10 0
6: 00000000:D00F 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 31114 1 0000000000000000 100 0 0 10 0
7: 00000000:006F 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 31028 1 0000000000000000 100 0 0 10 0
$ cat /proc/net/tcp6 # 导出 IPv6 TCP 套接字信息
sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 00000000000000000000000000000000:0016 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 25243 1 0000000000000000 100 0 0 10 0
1: 00000000000000000000000001000000:0277 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 40283 1 0000000000000000 100 0 0 10 0
2: 00000000000000000000000000000000:BCF9 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 31110 1 0000000000000000 100 0 0 10 0
3: 00000000000000000000000000000000:0801 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 28395 1 0000000000000000 100 0 0 10 0
4: 00000000000000000000000000000000:9087 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 31126 1 0000000000000000 100 0 0 10 0
5: 00000000000000000000000000000000:8D2D 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 28400 1 0000000000000000 100 0 0 10 0
6: 00000000000000000000000000000000:A2ED 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 31118 1 0000000000000000 100 0 0 10 0
7: 00000000000000000000000000000000:006F 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 31031 1 0000000000000000 100 0 0 10 0
简要说明下 /proc/net/tcp
和 /proc/net/tcp6
的输出格式:
sl: socket 的一个索引编号,没有太多意义。
local_address: socket 本地地址,用 ip:port 的形式描述,数字都是十六进制,ip 需要手工转换为点分十进制格式。
remote_address: socket 远端地址,用 ip:port 的形式描述,数字都是十六进制,ip 需要手工转换为点分十进制格式。
st: 套接字状态,用十六进制数字描述,如 0A 表示 LISTEN 状态。
这个和内核 include/net/tcp_states.h 中定义状态对应,如 0x0A 对应 TCP_LISTEN 。
tx_queue rx_queue:这两个信息的两个十六进制数据以 XXXXXXXX:XXXXXXXX 形式放在一起。
对于处于不同状态的套接字,它们表示的含义有所不同。对于 LISTEN 状态的套接字(譬如 server
的监听套接字),rx_queue 表示 SYNC 队列长度(半连接);而对于其它状态的套接字,rx_queue 表示
套接字接收缓冲队列长度,tx_queque 表示套接字发送缓冲队列长度。
tr tm->when: 套接字的定时器信息,如重传定时器。这两个信息的两个十六进制数据以 XX:XXXXXXXX 形式放在一起。
tr 不为零表示套接字当前有某类型的定时器激活,tm->when 表示激活的定时剩余的超时时间。
retrnsmt: 超时重传次数。
uid: 套接字所属用户的 UID 。
timeout: 未应答的窗口探测包次数。
inode: 套接字对应的 inode 。
其它剩余信息:见后面对应的内核源码分析。
这些数字,很不直观,难于理解,可以通过 netstat -ant
命令来翻译下这些信息:
$ netstat -ant # 命令通过读取 /proc/net/tcp 和 /proc/net/tcp6,将其中的部分信息转换为人类友好格式
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:51317 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:2049 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:51335 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:39949 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:53263 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN
tcp6 0 0 :::22 :::* LISTEN
tcp6 0 0 ::1:631 :::* LISTEN
tcp6 0 0 :::48377 :::* LISTEN
tcp6 0 0 :::2049 :::* LISTEN
tcp6 0 0 :::36999 :::* LISTEN
tcp6 0 0 :::36141 :::* LISTEN
tcp6 0 0 :::41709 :::* LISTEN
tcp6 0 0 :::111 :::* LISTEN
另外 netstat -s
命令可以按照协议类型,导出各协议类型套接字的收发等统计数据:
$ netstat -s
Ip:
4523 total packets received
1 with invalid addresses
0 forwarded
0 incoming packets discarded
4515 incoming packets delivered
775 requests sent out
159 outgoing packets dropped
Icmp:
320 ICMP messages received
0 input ICMP message failed.
ICMP input histogram:
destination unreachable: 320
320 ICMP messages sent
0 ICMP messages failed
ICMP output histogram:
destination unreachable: 320
IcmpMsg:
InType3: 320
OutType3: 320
Tcp:
4 active connections openings
0 passive connection openings
2 failed connection attempts
0 connection resets received
0 connections established
12 segments received
16 segments send out
0 segments retransmited
0 bad segments received.
2 resets sent
Udp:
1176 packets received
320 packets to unknown port received.
0 packet receive errors
439 packets sent
IgnoredMulti: 2689
UdpLite:
TcpExt:
2 TCP sockets finished time wait in fast timer
2 packet headers predicted
2 acknowledgments not containing data payload received
IPReversePathFilter: 7
TCPOrigDataSent: 4
IpExt:
InMcastPkts: 40
OutMcastPkts: 42
InBcastPkts: 3759
OutBcastPkts: 12
InOctets: 2072287
OutOctets: 60692
InMcastOctets: 4760
OutMcastOctets: 4840
InBcastOctets: 2005401
OutBcastOctets: 752
InNoECTPkts: 4520
InECT0Pkts: 3
netstat -s
命令实现的核心逻辑是读取 /proc/net/sockstat
,/proc/net/netstat
,/proc/net/snmp
等文件内容,解析其数据输出,内核相关代码为 net/ipv4/proc.c
中 sockstat_seq_show()
,netstat_seq_show()
,snmp_seq_show()
等函数,后文不再对此做细致分析。
2.2.1.2 源码实现
本小节分析 netstat -ant
命令的 用户空间
和 内核空间
相关的代码实现细节。
2.2.1.2.1 用户空间部分
netstat
是 net-tools 网络工具包中的其中一个,由该源码包中 netstat.c
实现。现在我们来简要分析下 netstat -ant
命令的 IPv4
协议相关部分:
/* net-tools/netstat.c */
int main(int argc, char *argv[])
{
...
if (!flag_arg || flag_tcp) {
i = tcp_info();
if (i)
return (i);
}
...
}
static int tcp_info(void)
{
// net-tools/lib/pathnames.h:
//
// #define _PATH_PROCNET_TCP "/proc/net/tcp"
// #define _PATH_PROCNET_TCP6 "/proc/net/tcp6"
INFO_GUTS6(_PATH_PROCNET_TCP, _PATH_PROCNET_TCP6, "AF INET (tcp)",
tcp_do_one, "tcp", "tcp6");
}
static void tcp_do_one(int lnr, const char *line, const char *prot)
{
...
/*
* 提取文件 /proc/net/tcp 或 /proc/net/tcp6 每一行内容的相关域。
* 文件 /proc/net/tcp 或 /proc/net/tcp6 的 每一行记录了一个 套接字 的信息。
*/
num = sscanf(line,
"%d: %64[0-9A-Fa-f]:%X %64[0-9A-Fa-f]:%X %X %lX:%lX %X:%lX %lX %d %d %lu %*s\n",
&d, local_addr, &local_port, rem_addr, &rem_port, &state,
&txq, &rxq, &timer_run, &time_len, &retr, &uid, &timeout, &inode);
...
/*
* 打印一个 套接字 的信息。
* 输出内容参考 2.1.1 小节中 netstat -ant 的输出。
*/
printf("%-4s %6ld %6ld %-*s %-*s %-11s",
prot, rxq, txq, (int)netmax(23,strlen(local_addr)), local_addr,
(int)netmax(23,strlen(rem_addr)), rem_addr, _(tcp_state[state]));
...
}
到此,对 netstat -ant
命令用户空间部分源码分析到此结束。
2.2.1.2.2 内核部分
上接 2.2.2.1
小节对 netstat -ant
源码的分析,这里对相关的内核部分做出简要分析。这里仅分析 IPv4
协议相关部分,对 IPv6
部分感兴趣的读者请自行阅读相关源码(tcp6_seq_show()
)。
/* net/ipv4/tcp_ipv4.c */
static struct pernet_operations tcp4_net_ops = {
.init = tcp4_proc_init_net,
.exit = tcp4_proc_exit_net,
};
/* net/ipv4/af_inet.c */
inet_init()
ipv4_proc_init()
tcp4_proc_init()
register_pernet_subsys(&tcp4_net_ops); /* 触发 tcp4_proc_init_net() 回调 */
fs_initcall(inet_init);
/* net/ipv4/tcp_ipv4.c */
static struct tcp_seq_afinfo tcp4_seq_afinfo = {
.name = "tcp",
.family = AF_INET,
.seq_fops = &tcp_afinfo_seq_fops,
.seq_ops = {
.show = tcp4_seq_show,
},
};
static int __net_init tcp4_proc_init_net(struct net *net)
{
return tcp_proc_register(net, &tcp4_seq_afinfo); /* 创建 /proc/net/tcp 文件 */
}
/*
* 读取 /proc/net/tcp 内容(如 netstat -ant 和 cat /proc/net/tcp),
* 触发 tcp4_seq_show() 调用。
*/
static int tcp4_seq_show(struct seq_file *seq, void *v)
{
...
if (sk->sk_state == TCP_TIME_WAIT) /* TIME-WAIT 态套接字信息 */
get_timewait4_sock(v, seq, st->num);
else if (sk->sk_state == TCP_NEW_SYN_RECV) /* 服务端 SYN-RECEIVED 状态套接字信息 */
get_openreq4(v, seq, st->num);
else
get_tcp4_sock(v, seq, st->num);
out:
seq_pad(seq, '\n');
...
}
/* 本文只关注调用 get_tcp4_sock() 情形,对其它情形感兴趣的读者请自行阅读源码 */
static void get_tcp4_sock(struct sock *sk, struct seq_file *f, int i)
{
...
/* 套接字上 定时器 相关数据信息,如 超时重传定时器 信息 */
if (icsk->icsk_pending == ICSK_TIME_RETRANS ||
icsk->icsk_pending == ICSK_TIME_REO_TIMEOUT ||
icsk->icsk_pending == ICSK_TIME_LOSS_PROBE) {
/* 重传定时器 处于激活状态 */
timer_active = 1;
timer_expires = icsk->icsk_timeout;
} else if (icsk->icsk_pending == ICSK_TIME_PROBE0) {
/* 零窗口探测 定时器 处于激活状态 */
timer_active = 4;
timer_expires = icsk->icsk_timeout;
} else if (timer_pending(&sk->sk_timer)) {
/* 延迟 ACK 或 keepallive 定时器 处于激活状态 */
timer_active = 2;
timer_expires = sk->sk_timer.expires;
} else {
/* 套接字上没有激活的定时器 */
timer_active = 0;
timer_expires = jiffies;
}
state = sk_state_load(sk);
if (state == TCP_LISTEN) /* 如果是 server 监听套接字, */
rx_queue = sk->sk_ack_backlog; /* @rx_queue 赋值为 SYN 队列(半连接队列) 长度 */
else
/* Because we don't lock the socket,
* we might find a transient negative value.
*/
rx_queue = max_t(int, tp->rcv_nxt - tp->copied_seq, 0);
seq_printf(f, "%4d: %08X:%04X %08X:%04X %02X %08X:%08X %02X:%08lX "
"%08X %5u %8d %lu %d %pK %lu %lu %u %u %d",
i, src, srcp, dest, destp, state,
tp->write_seq - tp->snd_una, /* 发送缓冲 */
rx_queue,
timer_active,
jiffies_delta_to_clock_t(timer_expires - jiffies),
icsk->icsk_retransmits, /* 重传次数 */
from_kuid_munged(seq_user_ns(f), sock_i_uid(sk)),
icsk->icsk_probes_out,
sock_i_ino(sk), /* socket inode */
refcount_read(&sk->sk_refcnt), sk,
jiffies_to_clock_t(icsk->icsk_rto),
jiffies_to_clock_t(icsk->icsk_ack.ato),
(icsk->icsk_ack.quick << 1) | icsk->icsk_ack.pingpong,
tp->snd_cwnd, /* 发送窗口大小 */
state == TCP_LISTEN ?
fastopenq->max_qlen :
(tcp_in_initial_slowstart(tp) ? -1 : tp->snd_ssthresh));
...
}
2.2.2 协议栈调优
. 启用 GRO(Generic Segmentation Offload), RSS, RPS, RFS, XPS, ...
2.3 故障排查
(未完待续)
ping: 检查常见网络故障,测试 RTT,TTL 等
ip: 网络配置
iptables: 网络包过滤、防火墙
tcpdump: 网络抓包
iftop, nethog: 流量观察
......