access 网站数据库,护肤品网站建设环境分析,网站移动端优化工具,wordpress个人博客实战哈喽大家好#xff0c;我是咸鱼
之前咸鱼在《Linux 网络收包流程》一文中介绍了 Linux 是如何实现网络接收数据包的
简单回顾一下#xff1a;
数据到达网卡之后#xff0c;网卡通过 DMA 将数据放到内存分配好的一块 ring buffer 中#xff0c;然后触发硬中断CPU 收到硬中…哈喽大家好我是咸鱼
之前咸鱼在《Linux 网络收包流程》一文中介绍了 Linux 是如何实现网络接收数据包的
简单回顾一下
数据到达网卡之后网卡通过 DMA 将数据放到内存分配好的一块 ring buffer 中然后触发硬中断CPU 收到硬中断之后简单的处理了一下分配 skb_buffer然后触发软中断软中断进程 ksoftirqd 执行一系列操作例如把数据帧从 ring ruffer上取下来然后将数据送到三层协议栈中在三层协议栈中数据被进一步处理发送到四层协议栈在四层协议栈中数据会从内核拷贝到用户空间供应用程序读取最后被处在应用层的应用程序去读取
当 Linux 要发送一个数据包的时候这个包是怎么从应用程序再到 Linux 的内核最后由网卡发送出去的呢
那么今天咸鱼将会为大家介绍 Linux 是如何实现网络发送数据包
发包流程
假设我们的网卡已经启动好分配和初始化 RingBuffer 且 server 和 client 已经建立好 socket
这里需要注意的是网卡在启动过程中申请分配的 RingBuffer 是有两个
igb_tx_buffer 数组这个数组是内核使用的用于存储要发送的数据包描述信息通过 vzalloc 申请的e1000_adv_tx_desc 数组这个数组是网卡硬件使用的用于存储要发送的数据包网卡硬件可以通过 DMA 直接访问这块内存通过 dma_alloc_coherent分配 igb_tx_buffer 数组中的每个元素都有一个指针指向 e1000_adv_tx_desc 这样内核就可以把要发送的数据填充到 e1000_adv_tx_desc 数组上 然后网卡硬件会直接从 e1000_adv_tx_desc 数组中读取实际数据并将数据发送到网络上 拷贝到内核
socket 系统调用将数据拷贝到内核
应用程序首先通过 socket 提供的接口实现系统调用
我们在用户态使用的 send 函数和 sendto 函数其实都是 sendto 系统调用实现的
send/sendto 函数 只是为了用户方便封装出来的一个更易于调用的方式而已
/* sendto 系统调用 省略了一些代码 */
SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,unsigned int, flags, struct sockaddr __user *, addr,int, addr_len)
{...sock sockfd_lookup_light(fd, err, fput_needed);...err sock_sendmsg(sock, msg, len);...
}在 sendto 系统调用内部首先 sockfd_lookup_light 函数会查找与给定文件描述符fd关联的 socket
接着调用 sock_sendmsg 函数sock_sendmsg __sock_sendmsg __sock_sendmsg_nosec
其中 sock-ops-sendmsg 函数实际执行的是 inet_sendmsg 协议栈函数
/*
__sock_sendmsg_nosec 函数iocb指向与 I/O 操作相关的结构体 kiocb
sock: 指向要执行发送操作的套接字结构体
msg: 指向存储要发送数据的消息头结构体 msghdr
size: 要发送的数据大小*/
static inline int __sock_sendmsg_nosec(struct kiocb *iocb, struct socket *sock,struct msghdr *msg, size_t size)
{...return sock-ops-sendmsg(iocb, sock, msg, size);
}这时候内核会去找 socket 上对应的具体协议发送函数
以 TCP 为例具体协议发送函数为 tcp_sendmsg tcp_sendmsg 会去申请一个内核态内存 skbsk_buff 然后挂到发送队列上发送队列是由 skb 组成的一个链表 接着把用户待发送的数据拷贝到 skb 中拷贝之后会触发【发送】操作 这里说的发送是指在当前上下文中待发送数据从 socket 层发送到传输层 需要注意的是这时候不一定开始真正发送因为还要进行一些条件判断比如说发送队列中的数据已经超过了窗口大小的一半
只有满足了条件才能够发送如果没有满足条件这次系统调用就可能直接返回了
网络协议栈处理
传输层处理
接着数据来到了传输层
传输层主要看 tcp_write_xmit 函数这个函数处理了传输层的拥塞控制、滑动窗口相关的工作 该函数会根据发送窗口和最大段大小等因素计算出本次发送的数据大小然后将数据封装成 TCP 段并发送出去 如果满足窗口要求设置 TCP 头然后将数据传到更低的网络层进行处理
在传输层中内核主要做了两件事
复制一份数据skb
为什么要复制一份出来呢因为网卡发送完成之后skb 会被释放掉但 TCP 协议是支持丢失重传的
所以在收到对方的 ACK 之前必须要备份一个 skb 去为重传做准备 实际上一开始发送的是 skb 的拷贝版收到了对方的 ACK 之后系统才会把真正的 skb 删除掉 封装 TCP 头
系统会根据实际情况添加 TCP 头封装成 TCP 段
这里需要知道的是每个 skb 内部包含了网络协议中的所有头部信息例如 MAC 头、IP 头、TCP/UDP 头等
在设置这些头部时内核会通过调整指针的位置来填充相应的字段而不是频繁申请和拷贝内存 比如说在设置 TCP 头的时候只是把指针指向 skb 的合适位置。后面再设置 IP 头的时候在把指针挪一挪就行 这种方式利用了 skb 数据结构的链表特性可以避免内存分配和数据拷贝所带来的性能开销从而提高数据传输的效率
网络层处理
数据离开了传输层之后就来到了网络层
网络层主要做下面的事情
路由项查找
根据目标 IP 地址查找路由表确定数据包的下一跳 ip_queue_xmit 函数
IP 头设置
根据路由表查找的结果设置 IP 头中的源和目标 IP 地址、TTL生存时间、IP 协议等字段
netfilter 过滤
netfilter 是 Linux 内核中的一个框架用于实现数据包的过滤和修改
在网络层netfilter 可以用于对数据包进行过滤、NAT网络地址转换等操作
skb 切分
如果数据包的大小超过了 MTU最大传输单元需要将数据包进行切分成多个片段以适应网络传输每个片段会被封装成单独的 skb
数据链路层处理
当数据来到了数据链路层之后会有两个子系统协同工作确保数据包在发送和接收过程中能够正确地对数据进行封装、解析和传输
邻居子系统
管理和维护主机或路由器与其它设备之间的邻居关系
邻居子系统里会发送 arp 请求找邻居然后把邻居信息存在邻居缓存表里用于存储目标主机的 MAC 地址
当需要发送数据包到某个目标主机时数据链路层会首先查询邻居缓存表以获取目标主机的 MAC 地址从而正确地封装数据包封装 MAC 头
网络设备子系统
网络设备子系统负责处理与物理网络接口相关的操作包括数据包的封装和发送以及从物理接口接收数据包并进行解析 网络设备子系统不但处理数据包的格式转换如在以太网中添加帧头和帧尾以及从帧中提取数据 还负责处理硬件相关的操作如发送和接收数据包的时钟同步、物理层错误检测等 到达网卡发送队列
接着网络设备子系统会选择一个合适的网卡发送队列并把 skb 添加到队列中绕过软中断处理程序
然后内核会调用网卡驱动的入口函数 dev_hard_start_xmit 来触发数据包的发送 在一些情况下邻居子系统还会将 skb 数据包添加到软中断队列softnet_data上并触发软中断NET_TX_SOFTIRQ 这个过程是为了将 skb 数据包交给软中断处理程序进行进一步处理和发送。软中断处理程序会负责实际的数据包发送 这就是为什么一般服务器上查看 /proc/softirqs一般 NET_RX 都要比 NET_TX 大的多的原因之一
即对于收包来说都是要经过 NET_RX 软中断而对于发包来说只有某些情况下才触发 NET_TX 软中断
网卡驱动发送
驱动程序从发送队列中读取 skb 的描述信息将其挂到 RingBuffer 上前面提到的igb_tx_buffer 数组
接着将 skb 的描述信息映射到网卡可访问的内存 DMA 区域中前面提到的e1000_adv_tx_desc 数组
网卡会直接从 e1000_adv_tx_desc 数组中根据描述信息读取实际数据并将数据发送到网络。这样就完成了数据包的发送过程
收尾工作
当数据发送完成后网卡设备会触发一个硬件中断NET_RX_SOFTIRQ这个硬中断通常称为“发送完成中断”或者“发送队列清理中断”
这个硬中断的主要作用是执行发送完成的清理工作包括释放之前为数据包分配的内存即释放 skb 内存和 RingBuffer 内存
最后当收到这个 TCP 报文的 ACK 应答时传输层就会释放原始的 skb前面有讲到发送的其实是 skb 的拷贝版 可以看到当数据发送完成以后通过硬中断的方式来通知驱动发送完毕而这个中断类型是 NET_RX_SOFTIRQ 前面我们讲到过网卡收到一个网络包的时候会触发 NET_RX_SOFTIRQ中断去告诉 CPU 有数据要处理 也就是说无论是网卡接收一个网络包还是发送网络包结束之后触发的都是 NET_RX_SOFTIRQ 总结
最后总结一下在 Linux 系统中发送网络数据包的流程
应用程序通过 socket 提供的接口进行系统调用将数据从用户态拷贝到内核态的 socket 缓冲区中网络协议栈从 socket 缓冲区中拿取数据并按照 TCP/IP 协议栈从上到下逐层处理 传输层处理以 TCP 为例在传输层中会复制一份数据为了丢失重传然后为数据封装 TCP 头网络层处理选取路由确认下一跳的 IP、填充 IP 头、netfilter 过滤、对超过 MTU 大小的数据包进行分片等操作邻居子系统和网络设备子系统处理在这里数据会被进一步处理和封装然后被添加到网卡的发送队列中 驱动程序从发送队列中读取 skb 的描述信息然后挂在 RingBuffer 上接着将 skb 的描述信息映射到网卡可访问的内存 DMA 区域中网卡将数据发送到网络当数据发送完成后触发硬中断释放 skb 内存和 RingBuffer 内存