Linux如何接收和发送网络包?

接收

网卡负责接收和发送网络包,当网卡接收到一个网络包后会通过DMA将包写入指定内存也就是RingBuffer中,接着网卡向 CPU 发起硬件中断, CPU 中断请求后,根据中断表,调用已经注册的中断处理函数。

怎么告诉操作系统这个网络包已经到达?

NAPI机制:混合中断和轮询的方式来接收网络包。核心概念就是不采用中断方式读,而是首先采用中断唤醒数据接收的服务程序,然后 poll 去轮询数据。

func-> 先「暂时屏蔽中断」,表示已经知道内存中有数据了,告诉网卡下次再收到数据包直接写内存就可以了,不要再通知 CPU 了(避免 CPU 不停的被中断)。然后发起软中断,然后恢复中断。

软中断的处理:内核中会有一个线程专门负责软中断处理,当该内核线程收到软中断后,就会来轮询处理数据。首先会从 Ring Buffer 中获取一个数据帧,作为一个网络包交给网络协议栈进行逐层处理。

网络协议栈的处理:首先会先进到网络接口层,这一层检查报文合法性,不合法丢弃,合法会找出该包的上层协议类型(IPv4/IPv6),接着再去掉帧头帧尾交给网络层。

到了网络层,取出 IP 包,判断包下一步走向(交给上层处理/转发出去)。当确认这个网络包要发送给本机后,就会从 IP 头里看看上一层协议类型是TCP还是UDP,接着去掉 IP 头,然后交给传输层。

传输层取出 TCP/UDP 头,根据四元组「源 IP、源端口、目的 IP、目的端口」 标识找出对应Socket,并把数据放到 Socket 的接收缓冲区。

最后,应用层程序调用 Socket 接口,将内核的 Socket 接收缓冲区的数据「拷贝」到应用层的缓冲区,然后唤醒用户进程。

发送

程序调socket接口,系统调用从用户态陷入内核态的socket层,内核申请一个buffer内存,将待发送的数据拷贝到buffer中并加入到发送缓冲区。

网络协议栈从socket发送缓冲区中取出buffer,按照TCP/IP协议栈从上到下处理。

如果是TCP,先拷贝一个buffer副本,每次调用网卡发送时传递的是拷贝而不是本身(因为buffer后续调用网络层最后到达网卡发送完成时,这个buffer会被释放。而TCP支持重传,所以在对方回复ACK之前,buffer不能无)

并且在层级之间传递时用的都是一个结构体sk_buff来描述所有网络包,是为了不发生拷贝,以免多次拷贝造成CPU效率下降。(网络包头的剥离用的是一个data指针)

接着对buffer填充TCP头,然后交给网络层:选路由(确认下一 IP)、填 IP 头、过滤、对超过 MTU 大小的数据包分片。

处理完交给网络接口层处理:通过 ARP 协议获得下一跳的 MAC 地址,然后对 buffer 填充帧头帧尾,接着将 buffer 放到网卡发送队列中。

over!触发「软中断」告诉网卡驱动程序这里有新网络包需要发,程序会从发送队列中读取 buffer,挂到 RingBuffer 中,接着将 buffer数据映射到网卡可访问的内存 DMA 区域,最后触发真实的发送。

发完后,网卡触发一个硬中断释放buffer内存和清理 RingBuffer 内存

最后收到ACK,传输层释放原始buffer。

发送网络数据的时候,涉及几次内存拷贝操作?

1、调系统调用时,内核会申请一个内核态buffer内存,将用户数据拷贝到 buffer 内存,并将其加入到发送缓冲区。

2、使用 TCP 情况下,从传输层进入网络层的时候,每一个 buffer 都会被克隆一个新的副本。

3、当 IP 层发现 buffer 大于 MTU 时才需要进行。会再申请额外的 buffer,并将原来的 buffer 拷贝为多个小的 buffer