Linux 网络:调试、追踪、调优

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 网络状态观察

本节描述对网络硬件、网络子系统进行观察的方式方法和工具。内容的组织形式是:先给出对应目标的观察方法,然后分析其相关的源码实现(包括用户空间内核空间两部分)。

2.1 硬件:网络硬件 调试观察

2.1.1 网络 PHY 芯片 调试观察

2.1.1.1 观察方法

调试观察 网络 PHY 芯片,主要是查看和修改 PHY 芯片的寄存器。有一些开源工具,如 mii-tool,mdio-toolsphytool 等等,都可对 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 writephytool 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 网卡 性能评估

我们经常使用 iperf 来进行基础的网卡性能评估。
(未完待续)

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
2.2.1.2 源码实现

本小节分析 netstat -ant 命令的 用户空间内核空间 相关的代码实现细节。

2.2.1.2.1 用户空间部分

netstatnet-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));
	...
}

3. 后记

本文将持续更新。

相关推荐

  1. Linux 网络调试追踪

    2024-03-13 07:00:02       30 阅读
  2. Linux相关命令汇总

    2024-03-13 07:00:02       50 阅读
  3. linux内核常用参数

    2024-03-13 07:00:02       31 阅读
  4. Linux内核常用参数

    2024-03-13 07:00:02       32 阅读
  5. Linux 日志管理与系统补充

    2024-03-13 07:00:02       27 阅读
  6. 【OceanBase诊断 】—— 索引

    2024-03-13 07:00:02       33 阅读
  7. jvm 参数

    2024-03-13 07:00:02       46 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-03-13 07:00:02       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-03-13 07:00:02       100 阅读
  3. 在Django里面运行非项目文件

    2024-03-13 07:00:02       82 阅读
  4. Python语言-面向对象

    2024-03-13 07:00:02       91 阅读

热门阅读

  1. 使用cuBLAS做行优先矩阵的矩阵乘

    2024-03-13 07:00:02       41 阅读
  2. go语言tcp协议实现文件上传

    2024-03-13 07:00:02       44 阅读
  3. 指针数组与数组指针

    2024-03-13 07:00:02       43 阅读
  4. 使用go开发的小tips

    2024-03-13 07:00:02       34 阅读
  5. 如何使用 CSS 中的 :root 伪类选择器

    2024-03-13 07:00:02       41 阅读
  6. SpringCloud-实现基于RabbitMQ的消息队列

    2024-03-13 07:00:02       45 阅读
  7. Linux纯命令行查看文本文件

    2024-03-13 07:00:02       40 阅读