共计 5091 个字符,预计需要花费 13 分钟才能阅读完成。
本篇文章为大家展示了 Linux 网络中数据包的接收过程是怎样的,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。
下面将介绍在 Linux 系统中,数据包是如何一步一步从网卡传到进程手中的。
如果英文没有问题,强烈建议阅读后面参考里的两篇文章,里面介绍的更详细。
丸趣 TV 小编只讨论以太网的物理网卡,不涉及虚拟设备,并且以一个 UDP 包的接收过程作为示例.
示例里列出的函数调用关系来自于 kernel 3.13.0,如果你的内核不是这个版本,函数名称和相关路径可能不一样,但背后的原理应该是一样的(或者有细微差别)
网卡到内存
网卡需要有驱动才能工作,驱动是加载到内核中的模块,负责衔接网卡和内核的网络模块,驱动在加载的时候将自己注册进网络模块,当相应的网卡收到数据包时,网络模块会调用相应的驱动程序处理数据。
下图展示了数据包 (packet) 如何进入内存,并被内核的网络模块开始处理:
+-----+ | | Memroy +--------+ 1 | | 2 DMA +--------+--------+--------+--------+ | Packet |-------- | NIC |------------ | Packet | Packet | Packet | ...... | +--------+ | | +--------+--------+--------+--------+ | | --------+ +-----+ | | +---------------+ | | 3 | Raise IRQ | Disable IRQ | 5 | | | darr; | +-----+ +------------+ | | Run IRQ handler | | | CPU |------------------ | NIC Driver | | | 4 | | +-----+ +------------+ | 6 | Raise soft IRQ | darr;
1:数据包从外面的网络进入物理网卡。如果目的地址不是该网卡,且该网卡没有开启混杂模式,该包会被网卡丢弃。
2:网卡将数据包通过 DMA 的方式写入到指定的内存地址,该地址由网卡驱动分配并初始化。注:老的网卡可能不支持 DMA,不过新的网卡一般都支持。
3:网卡通过硬件中断 (IRQ) 通知 CPU,告诉它有数据来了
4:CPU 根据中断表,调用已经注册的中断函数,这个中断函数会调到驱动程序 (NIC Driver) 中相应的函数
5: 驱动先禁用网卡的中断,表示驱动程序已经知道内存中有数据了,告诉网卡下次再收到数据包直接写内存就可以了,不要再通知 CPU 了,这样可以提高效率,避免 CPU 不停的被中断。
6: 启动软中断。这步结束后,硬件中断处理函数就结束返回了。由于硬中断处理程序执行的过程中不能被中断,所以如果它执行时间过长,会导致 CPU 没法响应其它硬件的中断,于是内核引入软中断,这样可以将硬中断处理函数中耗时的部分移到软中断处理函数里面来慢慢处理。
内核的网络模块
软中断会触发内核网络模块中的软中断处理函数,后续流程如下
+-----+ 14 | | +----------- | NIC | | | | |Enable IRQ +-----+ | | +------------+ Memroy | | Read +--------+--------+--------+--------+ +--------------- | NIC Driver | --------------------- | Packet | Packet | Packet | ...... | | | | 9 +--------+--------+--------+--------+ | +------------+ | | | skb Poll | 8 Raise softIRQ | 6 +-----------------+ | | 10 | | darr; darr; +---------------+ Call +-----------+ +------------------+ | net_rx_action | -------| ksoftirqd | | napi_gro_receive | +---------------+ 7 +-----------+ +------------------+ | | 11 darr; +--------------------------+ 12 +------------------------+ | __netif_receive_skb_core |----------- | packet taps(AF_PACKET) | +--------------------------+ +------------------------+ | | 13 darr; +-----------------+ | protocol layers | +-----------------+
7: 内核中的 ksoftirqd 进程专门负责软中断的处理,当它收到软中断后,就会调用相应软中断所对应的处理函数,对于上面第 6 步中是网卡驱动模块抛出的软中断,ksoftirqd 会调用网络模块的 net_rx_action 函数
8:net_rx_action 调用网卡驱动里的 poll 函数来一个一个的处理数据包
9:在 pool 函数中,驱动会一个接一个的读取网卡写到内存中的数据包,内存中数据包的格式只有驱动知道
10:驱动程序将内存中的数据包转换成内核网络模块能识别的 skb 格式,然后调用 napi_gro_receive 函数
11: napi_gro_receive 会处理 GRO 相关的内容,也就是将可以合并的数据包进行合并,这样就只需要调用一次协议栈,接着调用__netif_receive_skb_core
12: 看是不是有 AF_PACKET 类型的 socket(也就是我们常说的原始套接字),如果有的话,拷贝一份数据给它。tcpdump 抓包就是抓的这里的包。
13:调用协议栈相应的函数,将数据包交给协议栈处理。
14:待内存中的所有数据包被处理完成后(即 poll 函数执行完成),启用网卡的硬中断,这样下次网卡再收到数据的时候就会通知 CPU
协议栈
IP 层
由于是 UDP 包,所以 *** 步会进入 IP 层,然后一级一级的函数往下调:
| | darr; promiscuous mode +--------+ PACKET_OTHERHOST (set by driver) +-----------------+ | ip_rcv |-------------------------------------- | drop this packet| +--------+ +-----------------+ | | darr; +---------------------+ | NF_INET_PRE_ROUTING | +---------------------+ | | darr; +---------+ | | enabled ip forword +------------+ +----------------+ | routing |-------------------- | ip_forward |------- | NF_INET_FOWARD | | | +------------+ +----------------+ +---------+ | | | | destination IP is local darr; darr; +---------------+ +------------------+ | dst_output_sk | | ip_local_deliver | +---------------+ +------------------+ | | darr; +------------------+ | NF_INET_LOCAL_IN | +------------------+ | | darr; +-----------+ | UDP layer | +-----------+
ip_rcv: ip_rcv 函数是 IP 模块的入口函数,在该函数里面,*** 件事就是将垃圾数据包 (目的 mac 地址不是当前网卡,但由于网卡设置了混杂模式而被接收进来) 直接丢掉,然后调用注册在 NF_INET_PRE_ROUTING 上的函数
NF_INET_PRE_ROUTING: netfilter 放在协议栈中的钩子,可以通过 iptables 来注入一些数据包处理函数,用来修改或者丢弃数据包,如果数据包没被丢弃,将继续往下走
routing:进行路由,如果是目的 IP 不是本地 IP,且没有开启 ip forward 功能,那么数据包将被丢弃,如果开启了 ip forward 功能,那将进入 ip_forward 函数
ip_forward: ip_forward 会先调用 netfilter 注册的 NF_INET_FORWARD 相关函数,如果数据包没有被丢弃,那么将继续往后调用 dst_output_sk 函数
dst_output_sk:该函数会调用 IP 层的相应函数将该数据包发送出去,同下一篇要介绍的数据包发送流程的后半部分一样。
ip_local_deliver:如果上面 routing 的时候发现目的 IP 是本地 IP,那么将会调用该函数,在该函数中,会先调用 NF_INET_LOCAL_IN 相关的钩子程序,如果通过,数据包将会向下发送到 UDP 层
UDP 层
| | darr; +---------+ +-----------------------+ | udp_rcv |----------- | __udp4_lib_lookup_skb | +---------+ +-----------------------+ | | darr; +--------------------+ +-----------+ | sock_queue_rcv_skb |----- | sk_filter | +--------------------+ +-----------+ | | darr; +------------------+ | __skb_queue_tail | +------------------+ | | darr; +---------------+ | sk_data_ready | +---------------+
udp_rcv: udp_rcv 函数是 UDP 模块的入口函数,它里面会调用其它的函数,主要是做一些必要的检查,其中一个重要的调用是__udp4_lib_lookup_skb,该函数会根据目的 IP 和端口找对应的 socket,如果没有找到相应的 socket,那么该数据包将会被丢弃,否则继续
sock_queue_rcv_skb:主要干了两件事,一是检查这个 socket 的 receive buffer 是不是满了,如果满了的话,丢弃该数据包,然后就是调用 sk_filter 看这个包是否是满足条件的包,如果当前 socket 上设置了 filter,且该包不满足条件的话,这个数据包也将被丢弃(在 Linux 里面,每个 socket 上都可以像 tcpdump 里面一样定义 filter,不满足条件的数据包将会被丢弃)
__skb_queue_tail:将数据包放入 socket 接收队列的末尾
sk_data_ready:通知 socket 数据包已经准备好
调用完 sk_data_ready 之后,一个数据包处理完成,等待应用层程序来读取,上面所有函数的执行过程都在软中断的上下文中。
socket
应用层一般有两种方式接收数据,一种是 recvfrom 函数阻塞在那里等着数据来,这种情况下当 socket 收到通知后,recvfrom 就会被唤醒,然后读取接收队列的数据; 另一种是通过 epoll 或者 select 监听相应的 socket,当收到通知后,再调用 recvfrom 函数去读取接收队列的数据。两种情况都能正常的接收到相应的数据包。
上述内容就是 Linux 网络中数据包的接收过程是怎样的,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注丸趣 TV 行业资讯频道。