网卡的基础知识
网卡本身是有内存的,每个网卡一般都有4k以上的内存,用来发送、接受数据。数据从主内存搬到网卡之后,不是立即就能被发送出去的,而是要先在网卡自身的内存中排队,再按先后顺序发送,同样的,数据从以太网传递到网卡时,网卡也是先把数据存储到自身的内存中,等到收到一帧数据了,再经过中断的方式,告诉CPU把网卡内存的数据读走(现在网卡大都支持DMA方式直接从网卡内存拷贝被内核内存),而读走后的内存,又被清空,再次被用来接收新的数据。

蓝色部分为发送数据用的页面总和,总共只有6个页面用于发送数据(40h-45h);剩余的46h-80h都是接收数据用的,而在接收数据内存中,只有红色部分是有数据的,当接收新的数据时,是向红色部分前面的绿色中的256字节写入数据,同时“把当前指针”移动到+256字节的后面(网卡自动完成),而现在要读的数据,是在“边界指针”那里开始的256字节(紫色部分),下一个要读的数据,是在“下一包指针”的位置开始的256字节,当256字节被读出来了,就变成了重新可以使用的内存,即绿色所表示,而接收数据,就是把可用的内存拿来用,即变成了红色,当数据写到了0x80h后,又从0x46h开始写数据,这样循环,如果数据满了,则网卡就不能再接收数据,必须等待数据被读出去了,才能再继续接收。
网卡到内存
网卡需要有驱动才能工作,驱动是加载到内核中的模块,负责衔接网卡和内核的网络模块,驱动在加载的时候将自己注册进网络模块,当相应的网卡收到数据包时,网络模块会调用相应的驱动程序处理数据。
下图展示了数据包(packet)如何进入内存,并被内核的网络模块开始处理:

- 数据包从外面的网络进入物理网卡。如果目的地址不是该网卡,并且该网卡没有开启混杂模式,该包会被网卡丢弃。
- 网卡将数据包通过DMA的方式写入到指定的内存地址,该地址由网卡驱动分配。
- 网卡通过硬件中断(IRQ)告知cpu有数据来了。
- cpu根据中断表,调用已经注册的中断函数,这个中断函数会调动驱动程序中相应的函数
- 驱动先禁用网卡的中断,表示驱动程序已经知道内存中有数据了,告诉网卡下次再收到数据包直接写内存就可以了,不要再通知cpu了,这样可以提高效率,避免cpu不停的被中断
- 启动软中断。这步结束后,硬件中断处理函数就结束返回了,由于硬中断处理程序执行的过程中不能被中断,所以如果它执行时间过长,会导致cpu无法响应其他硬件的中断,于是内核引入软中断,这样可以将硬中断处理函数中耗时的部分移到软中断处理函数里面来慢慢处理。
内核的网络模块
软中断会触发内核网络模块中的软中断处理函数,后续流程如下:
+-----+
17 | |
+----------->| NIC |
| | |
|Enable IRQ +-----+
|
|
+------------+ Memroy
| | Read +--------+--------+--------+--------+
+--------------->| NIC Driver |<--------------------- | Packet | Packet | Packet | ...... |
| | | 9 +--------+--------+--------+--------+
| +------------+
| | | skb
Poll | 8 Raise softIRQ | 6 +-----------------+
| | 10 |
| ↓ ↓
+---------------+ Call +-----------+ +------------------+ +--------------------+ 12 +---------------------+
| net_rx_action |<-------| ksoftirqd | | napi_gro_receive |------->| enqueue_to_backlog |----->| CPU input_pkt_queue |
+---------------+ 7 +-----------+ +------------------+ 11 +--------------------+ +---------------------+
| | 13
14 | + - - - - - - - - - - - - - - - - - - - - - - +
↓ ↓
+--------------------------+ 15 +------------------------+
| __netif_receive_skb_core |----------->| packet taps(AF_PACKET) |
+--------------------------+ +------------------------+
|
| 16
↓
+-----------------+
| protocol layers |
+-----------------+
- 内核中的ksoftirqd进程专门负责软中断的处理,当它收到软中断后,就会调用相应软中断所对应的处理函数,对于上面第六步中网卡驱动模块抛出的软中断,ksoftirqd会调用网络模块的net_rx_action函数
- net_rx_action调用网卡驱动里的poll函数来一个一个的处理数据包
- 在pool函数中,驱动会一个接一个的读取网卡写到内存中的数据包,内存中数据包的格式只有驱动知道
- 驱动程序将内存中的数据包转换成内核网络模块能识别的skb格式,然后调用napi_gro_receive函数
- napi_gro_receive会处理GRO相关的内容,也就是将可以合并的数据包进行合并,这样就只需要调用一次协议栈。然后判断是否开启了RPS,如果开启了,将会调用enqueue_to_backlog
- 在enqueue_to_backlog函数中,会将数据包放入CPU的softnet_data结构体的input_pkt_queue中,然后返回,如果input_pkt_queue满了的话,该数据包将会被丢弃,queue的大小可以通过net.core.netdev_max_backlog来配置
- CPU会接着在自己的软中断上下文中处理自己input_pkt_queue里的网络数据(调用__netif_receive_skb_core)
- 如果没开启RPS,napi_gro_receive会直接调用__netif_receive_skb_core
- 看是不是有AF_PACKET类型的socket(也就是我们常说的原始套接字),如果有的话,拷贝一份数据给它。tcpdump抓包就是抓的这里的包。
- 调用协议栈相应的函数,将数据包交给协议栈处理。
- 待内存中的所有数据包被处理完成后(即poll函数执行完成),启用网卡的硬中断,这样下次网卡再收到数据的时候就会通知CPU
协议栈
IP层
|
|
↓ promiscuous mode &&
+--------+ PACKET_OTHERHOST (set by driver) +-----------------+
| ip_rcv |-------------------------------------->| drop this packet|
+--------+ +-----------------+
|
|
↓
+---------------------+
| NF_INET_PRE_ROUTING |
+---------------------+
|
|
↓
+---------+
| | enabled ip forword +------------+ +----------------+
| routing |-------------------->| ip_forward |------->| NF_INET_FORWARD |
| | +------------+ +----------------+
+---------+ |
| |
| destination IP is local ↓
↓ +---------------+
+------------------+ | dst_output_sk |
| ip_local_deliver | +---------------+
+------------------+
|
|
↓
+------------------+
| NF_INET_LOCAL_IN |
+------------------+
|
|
↓
+-----------+
| 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函数
- 该函数会调用IP层的相应函数将该数据包发送出去
- ip_local_deliver:如果上面routing的时候发现目的IP是本地IP,那么将会调用该函数,在该函数中,会先调用NF_INET_LOCAL_IN相关的钩子程序,如果通过,数据包将会向下发送到UDP层
UDP层
|
|
↓
+---------+ +-----------------------+
| udp_rcv |----------->| __udp4_lib_lookup_skb |
+---------+ +-----------------------+
|
|
↓
+--------------------+ +-----------+
| sock_queue_rcv_skb |----->| sk_filter |
+--------------------+ +-----------+
|
|
↓
+------------------+
| __skb_queue_tail |
+------------------+
|
|
↓
+---------------+
| 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函数去读取接收队列的数据。两种情况都能正常的接收到相应的数据包。
网友评论