前言
呵呵 之前曾经看到过 湖光大佬 的 tcp 的流程梳理
呵呵 很高深 有很多不明白的地方, 不光是涉及到 linux 网络处理本身的东西, 还涉及到了 tcp协议 的一些具体的实现, 是非常的复杂
这里之前 在 0voice/linux_kernel_wiki 上面看到了网络协议栈部分的梳理
呵呵 自己也稍微走了一下 流程, 这里稍微记录一下
主要核心的内容包含了, 用户数据传递过来, 数据包的封装, 然后到 数据包发送到驱动层 的这个流程, 当然 是没有上面的 0voice/linux_kernel_wiki 网络协议栈 部分内容详细, 以及准确
记录于 2022.05.02
测试环境 : linux 4.10.14 + qemu 2.5.0
测试用例
测试用例, 拷贝自网络, 但是忘机记录具体的地址信息了, ^_^
udp 服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#define PORT 8082
#define BUFSIZE 512
char buf[BUFSIZE+1];
int main()
{
//第 1 步 创建套接字
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
//第 2 步 设置地址结构体
struct sockaddr_in svraddr;
svraddr.sin_family=AF_INET;//使用 internet 协议
svraddr.sin_port=htons(PORT);
// inet_aton("0.0.0.0",&svraddr.sin_addr);
inet_aton("192.168.0.103",&svraddr.sin_addr);
//第 3 步 绑定
int ret=bind(sockfd,(struct sockaddr*)&svraddr,sizeof(svraddr));
if(ret<0){printf("cannot bind!\r\n");exit(-1);};
while(1)
{
struct sockaddr_in cli;
int len=sizeof(cli);
int z=recvfrom(sockfd,buf,BUFSIZE,0,(struct sockaddr*)&cli,&len);//第 6 步 读取套接字
buf[z]='\0';
printf("%s\r\n",buf);//打印
}
}
udp 客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#define PORT 8082
#define BUFSIZE 512
char buf[BUFSIZE+1];
int main()
{
//第 1 步 创建一个体套接字
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
//第 2 步 设置 addr 结构体
struct sockaddr_in svraddr;
svraddr.sin_family=AF_INET;//使用 internet 协议
svraddr.sin_port=htons(PORT);
inet_aton("0.0.0.0",&svraddr.sin_addr);
//第 3 步 连接服务器
//connect(sockfd,(struct sockaddr*)&svraddr,sizeof(svraddr));
buf[0] = '1';
buf[1] = '2';
buf[2] = '3';
// while(1)
// {
sendto(sockfd,buf,strlen(buf),0,(struct sockaddr*)&svraddr,sizeof(svraddr)); //第 4 步 向套接字中写入字符串
// }
}
如上 客户端发送了 三个字节 的数据
以太网头部, ip头部, udp 头部 合计为 14 + 20 + 8 = 42 字节, 因此 客户端给服务器发送的数据报文长度为 45 字节
//Mac头部,总长度14字节
typedef struct _eth_hdr
{
unsigned char dstmac[6]; //目标mac地址
unsigned char srcmac[6]; //源mac地址
unsigned short eth_type; //以太网类型
} eth_hdr;
//IP头部,总长度20字节
typedef struct _ip_hdr
{
#if LITTLE_ENDIAN
unsigned char ihl:4; //首部长度
unsigned char version:4, //版本
#else
unsigned char version:4, //版本
unsigned char ihl:4; //首部长度
#endif
unsigned char tos; //服务类型
unsigned short tot_len; //总长度
unsigned short id; //标志
unsigned short frag_off; //分片偏移
unsigned char ttl; //生存时间
unsigned char protocol; //协议
unsigned short chk_sum; //检验和
struct in_addr srcaddr; //源IP地址
struct in_addr dstaddr; //目的IP地址
} ip_hdr;
//UDP头部,总长度8字节
typedef struct _udp_hdr
{
unsigned short src_port; //远端口号
unsigned short dst_port; //目的端口号
unsigned short uhl; //udp头部长度
unsigned short chk_sum; //16位udp检验和
} udp_hdr;
这是一段相似的报文, 不过 服务器 和 客户端 主机和我们调试的主机不同
Frame 654: 45 bytes on wire (360 bits), 45 bytes captured (360 bits) on interface en0, id 0
Ethernet II, Src: Apple_c9:48:f9 (38:f9:d3:c9:48:f9), Dst: VMware_4e:80:29 (00:0c:29:4e:80:29)
Destination: VMware_4e:80:29 (00:0c:29:4e:80:29)
Source: Apple_c9:48:f9 (38:f9:d3:c9:48:f9)
Type: IPv4 (0x0800)
Internet Protocol Version 4, Src: 192.168.0.103, Dst: 192.168.0.20
0100 .... = Version: 4
.... 0101 = Header Length: 20 bytes (5)
Differentiated Services Field: 0x00 (DSCP: CS0, ECN: Not-ECT)
Total Length: 31
Identification: 0xaf64 (44900)
Flags: 0x00
Fragment Offset: 0
Time to Live: 64
Protocol: UDP (17)
Header Checksum: 0x499e [validation disabled]
[Header checksum status: Unverified]
Source Address: 192.168.0.103
Destination Address: 192.168.0.20
User Datagram Protocol, Src Port: 53260, Dst Port: 8082
Source Port: 53260
Destination Port: 8082
Length: 11
Checksum: 0x2a3b [unverified]
[Checksum Status: Unverified]
[Stream index: 5]
[Timestamps]
UDP payload (3 bytes)
Data (3 bytes)
Data: 313233
[Length: 3]
udp 发送数据包的流程
用户调用 sendto 系统调用 和 内核进行交互, 传输的 buf 的内容是用户程序中传入的 buf 前三个字节分别为 '1', '2', '3'
我们这里不会关注 整个流程, 我们只会关注一部分的点
1. 用户数据 封装 msghdr 的地方
2. msghdr 封装 skb 的地方
3. 封装 用户数据 的地方
4. 封装 ip 头的地方
5. 封装 udp 头的地方
6. 封装 eth 头的地方
7. 封装好了 skb 之后内核传递数据给 driver 的地方
1. 用户数据 封装 msghdr 的地方
用户程序 调用系统调用传入的输入
buf 为 0x601080, len 为 3, 这三个字节分别对应于 '1', '2', '3'
然后根据传入的 buf 创建了 msghdr 向下交互
(gdb) x/2gx 0x601080
0x601080: 0x0000000000333231 0x0000000000000000
2. msghdr 封装 skb 的地方
from 对应的是传入的 msghdr
getfrag 对应的是将数据填充到 skb 中的函数
3. 封装 用户数据 的地方
将 msghdr 中封装的 buf 的数据拷贝到 skb 的指定位置[预留 ip 头 + udp 头的长度]
这里的偏移为 20 + 8 = 28
这里 拷贝了 '1', '2', '3' 到 skb 中
4. 封装 ip 头的地方
封装了 skb 之后, 会更新 skb 的 iphdr 的相关信息
iphdr 数据填充之后, 我们拆解一下关键的信息项, 注意 dump 出来的内存为小端序
source_ip 在 0xffff88007f44101c 的位置, 值为 0x0100007f = 127.0.0.1
dst_ip 在 0xffff88007f441020 的位置, 值为 0x0100007f = 127.0.0.1
protocol 为 0xffff88007f441019 的位置 值为 0x11 = 17 = udp
ttl 为 0xffff88007f441018 的位置 值为 0x40 = 64
(gdb) x/20gx 0xffff88007f441000
0xffff88007f441000: 0xffff88007f441400 0x0000000000000040
0xffff88007f441010: 0x00406b6500400045 0x0100007f00401140
0xffff88007f441020: 0x000000000100007f 0x00333231000001f8
0xffff88007f441030: 0x0000000000000008 0x0000000400000003
0xffff88007f441040: 0x0000000000000238 0x0000000000400238
0xffff88007f441050: 0x0000000000400238 0x000000000000001c
0xffff88007f441060: 0x000000000000001c 0x0000000000000001
0xffff88007f441070: 0x0000000500000001 0x0000000000000000
0xffff88007f441080: 0x0000000000400000 0x0000000000400000
0xffff88007f441090: 0x0000000000000924 0x0000000000000924
5. 封装 udp 头的地方
封装了 skb 之后会将 skb 从 udp 层传递到 ip 层, 在此之前会先封装 udphdr
udphdr 数据填充之后, 我们拆解一下关键的信息项, 注意 dump 出来的内存为小端序
source_port 在 0xffff88007f441024 的位置, 值为 0x9857 = 38999
dst_port 在 0xffff88007f441026 的位置, 值为 0x1f92 = 8082
length 在 0xffff88007f441028 的位置, 值为 0x00b = 11
checksum 在 0xffff88007f441030 的位置, 目前还没有计算, 值为 0x0000 = 0
(gdb) x/20gx 0xffff88007f441000
0xffff88007f441000: 0xffff88007f441400 0x0000000000000040
0xffff88007f441010: 0x00406b6500400045 0x0100007f00401140
0xffff88007f441020: 0x921f57980100007f 0x0033323100000b00
0xffff88007f441030: 0x0000000000000008 0x0000000400000003
0xffff88007f441040: 0x0000000000000238 0x0000000000400238
0xffff88007f441050: 0x0000000000400238 0x000000000000001c
0xffff88007f441060: 0x000000000000001c 0x0000000000000001
0xffff88007f441070: 0x0000000500000001 0x0000000000000000
0xffff88007f441080: 0x0000000000400000 0x0000000000400000
0xffff88007f441090: 0x0000000000000924 0x0000000000000924
计算了 checksum 之后, 填充到 skb 中
这里的 checksum 为 0xfe1e, 其具体的值不重要
(gdb) x/20gx 0xffff88007f441000
0xffff88007f441000: 0xffff88007f441400 0x0000000000000040
0xffff88007f441010: 0x00406b6500400045 0x0100007f00401140
0xffff88007f441020: 0x921f57980100007f 0x003332311efe0b00
0xffff88007f441030: 0x0000000000000008 0x0000000400000003
0xffff88007f441040: 0x0000000000000238 0x0000000000400238
0xffff88007f441050: 0x0000000000400238 0x000000000000001c
0xffff88007f441060: 0x000000000000001c 0x0000000000000001
0xffff88007f441070: 0x0000000500000001 0x0000000000000000
0xffff88007f441080: 0x0000000000400000 0x0000000000400000
0xffff88007f441090: 0x0000000000000924 0x0000000000000924
6. 封装 eth 头的地方
ip 层处理之后, 路由查询, 然后来到了 数据链路层, 封装 source_mac, dst_mac, 网络层标志 等
ethhdr 数据填充之后, 我们拆解一下关键的信息项, 注意 dump 出来的内存为小端序
source_mac 在 0xffff88007f441002 的位置, 值为 0x000000000000
dst_mac 在 0xffff88007f441008 的位置, 值为 0x000000000000
type 在 0xffff88007f44100e 的位置, 值为 0x000b 为 IPV4
(gdb) x/20gx 0xffff88007f441000
0xffff88007f441000: 0x0000000000000000 0x0008000000000000
0xffff88007f441010: 0x004094681f000045 0x0100007f37d41140
0xffff88007f441020: 0x921f44950100007f 0x003332311efe0b00
0xffff88007f441030: 0x0000000000000008 0x0000000400000003
0xffff88007f441040: 0x0000000000000238 0x0000000000400238
0xffff88007f441050: 0x0000000000400238 0x000000000000001c
0xffff88007f441060: 0x000000000000001c 0x0000000000000001
0xffff88007f441070: 0x0000000500000001 0x0000000000000000
0xffff88007f441080: 0x0000000000400000 0x0000000000400000
0xffff88007f441090: 0x0000000000000924 0x0000000000000924
7. 封装好了 skb 之后内核传递数据给 driver 的地方
这里两种处理方式, 一种是直接将 skb 传递给 drvier, 直接发送 数据包
另外一种是 进入 qdisc 队列, 根据给定的策略从 队列中刷出数据到 drvier 发送数据包, 多了一层缓冲 提高效率
不管是上面哪一种方式, 调用 driver 的入口都是 dev_hard_start_xmit, 调用链路如下
dev_hard_start_xmit - xmit_one - netdev_start_xmit - __netdev_start_xmit - ops->ndo_start_xmit
我这里虚拟机似乎是存在问题? 一直添加到队列, 但是没有 flush 的过程, 呵呵
ops->ndo_start_xmit 就是对应的硬件驱动发送数据包的具体的实现
本地回环 设备对应的实现是在 drivers/net/loopback.c 中, 发送数据包的实现为 loopback_xmit, 具体实现是直接调用 netif_rx 来添加数据包入队
后记
当然 具体的实现还有更多的细节, 这里仅仅是一个 case 来梳理了一些 用户数据 到 封包 到 发送到驱动的流程
完
参考
0voice/linux_kernel_wiki