0%

Linux网络包收发流程

本文记录了Linux网络数据包的接收和发送过程。

Overview

数据包的接收过程

从网卡到内存

我们知道,每个网络设备(网卡)需要有驱动才能工作,驱动需要在内核启动时加载到内核中才能工作。事实上,从逻辑上看,驱动是负责衔接网络设备和内核网络栈的中间模块,每当网络设备接收到新的数据包时,就会触发中断,而对应的中断处理程序正是加载到内核中的驱动程序。

下面这张图详细的展示了数据包如何从网络设备进入内存,并被处于内核中的驱动程序和网络栈处理的:

network-receive-data-1.jpg

  1. 数据包进入物理网卡。如果目的地址不是该网络设备,且该来网络设备没有开启混杂模式,该包会被网络设备丢弃。
  2. 物理网卡将数据包通过DMA的方式写入到指定的内存地址,该地址由网卡驱动分配并初始化。
  3. 物理网卡通过硬件中断(IRQ)通知CPU,有新的数据包到达物理网卡需要处理。
  4. CPU根据中断表,调用已经注册的中断函数,这个中断函数会调到驱动程序(NIC Driver)中相应的函数
  5. 驱动先禁用网卡的中断,表示驱动程序已经知道内存中有数据了,告诉物理网卡下次再收到数据包直接写内存就可以了,不要再通知CPU了,这样可以提高效率,避免CPU不停的被中断。
  6. 启动软中断继续处理数据包。这样的原因是硬中断处理程序执行的过程中不能被中断,所以如果它执行时间过长,会导致CPU没法响应其它硬件的中断,于是内核引入软中断,这样可以将硬中断处理函数中耗时的部分移到软中断处理函数里面来慢慢处理。

内核处理数据包

上一步中网络设备驱动程序会通过软触发内核网络模块中的软中断处理函数,内核处理数据包的流程如下图所示:

network-receive-data-2.jpg

  1. 对于第6步中驱动发出的软中断,内核中的ksoftirqd进程会调用网络模块的相应软中断所对应的处理函数,这里其实就是调用net_rx_action函数。
  2. 接下来net_rx_action调用网卡驱动里的poll函数来一个个地处理数据包。
  3. poll函数会让驱动会读取网卡写到内存中的数据包。事实上,内存中数据包的格式只有驱动知道。
  4. 驱动程序将内存中的数据包转换成内核网络模块能识别的skb(socket buffer)格式,然后调用napi_gro_receive函数
  5. napi_gro_receive会处理GRO相关的内容,也就是将可以合并的数据包进行合并,这样就只需要调用一次协议栈。然后判断是否开启了RPS,如果开启了,将会调用enqueue_to_backlog
  6. enqueue_to_backlog函数会将数据包放入input_pkt_queue结构体中,然后返回。 > Note: 如果input_pkt_queue满了的话,该数据包将会被丢弃,这个queue的大小可以通过net.core.netdev_max_backlog来配置
  7. 接下来CPU会在软中断上下文中处理自己input_pkt_queue里的网络数据(调用__netif_receive_skb_core函数)
  8. 如果没开启RPSnapi_gro_receive会直接调用__netif_receive_skb_core函数。
  9. 紧接着CPU会根据是不是有AF_PACKET类型的socket(原始套接字),如果有的话,拷贝一份数据给它(tcpdump抓包就是抓的这里的包)。
  10. 将数据包交给内核协议栈处理。
  11. 当内存中的所有数据包被处理完成后(poll函数执行完成),重新启用网卡的硬中断,这样下次网卡再收到数据的时候就会通知CPU。

内核协议栈

内核网络协议栈此时接收到的数据包其实是三层(IP网络层)数据包,因此,数据包首先会进入到IP网络层层,然后进入传输层处理。

IP网络层

network-receive-data-3.jpg

  • ip_rcv: ip_rcv函数是IP网络层处理模块的入口函数,该函数首先判断属否需要丢弃该数据包(目的mac地址不是当前网卡,并且网卡设置了混杂模式),如果需要进一步处理就然后调用注册在netfilter中的NF_INET_PRE_ROUTING这条链上的处理函数。
  • NF_INET_PRE_ROUTING: netfilter放在协议栈中的钩子函数,可以通过iptables来注入一些数据包处理函数,用来修改或者丢弃数据包,如果数据包没被丢弃,将继续往下走。 > NF_INET_PRE_ROUTING等netfilter链上的处理逻辑可以通iptables来设置,详情请移步: https://morven.life/notes/the_knowledge_of_iptables/
  • routing: 进行路由处理,如果是目的IP不是本地IP,且没有开启ip forward功能,那么数据包将被丢弃,如果开启了ip forward功能,那将进入ip_forward函数。
  • ip_forward: 该函数会先调用netfilter注册的NF_INET_FORWARD链上的相关函数,如果数据包没有被丢弃,那么将继续往后调用dst_output_sk函数。
  • dst_output_sk: 该函数会调用IP网络层的相应函数将该数据包发送出去,这一步将会在下一章节发送数据包中详细介绍。
  • ip_local_deliver: 如果上面路由处理发现发现目的IP是本地IP,那么将会调用ip_local_deliver函数,该函数先调用NF_INET_LOCAL_IN链上的相关函数,如果通过,数据包将会向下发送到UDP层。

传输层

network-receive-data-4.jpg

  • udp_rcv: 该函数是UDP处理层模块的入口函数,它首先调用__udp4_lib_lookup_skb函数,根据目的IP和端口找对应的socket,如果没有找到相应的socket,那么该数据包将会被丢弃,否则继续。
  • sock_queue_rcv_skb: 该函数一是负责检查这个socket的receive buffer是不是满了,如果满了的话就丢弃该数据包;二是调用sk_filter看这个包是否是满足条件的包,如果当前socket上设置了filter,且该包不满足条件的话,这个数据包也将被丢弃。
  • __skb_queue_tail: 该函数将数据包放入socket接收队列的末尾。
  • sk_data_ready: 通知socket数据包已经准备好。
  • 调用完sk_data_ready之后,一个数据包处理完成,等待应用层程序来读取。

Note: 上面所述的所有执行过程都在软中断的上下文中执行。


数据包的发送过程

从逻辑上看,Linux网络数据包的发送过程和接收过程正好相反,我们仍旧以一个UDP数据包通过物理网卡发送的过程为例来讲解:

应用层

应用层处理过程的起点是应用程序调用Linux网络接口创建socket(所谓socket基本就是ip+端口组成的基本结构体),下面这张图详细的展示了应用层如何构建socket并发送给下层UDP:

network-send-data-1.jpg

  • socket(…): 调用该函数来创建一个socket结构体,并初始化相应的操作函。
  • sendto(sock, …): 应用层程序调用该函数开始发送数据包,该函数数会调用后面的inet_sendmsg
  • inet_sendmsg: 该函数主要是检查当前socket有没有绑定源端口,如果没有的话,调用inet_autobind分配一个,然后调用UDP层的函数。
  • inet_autobind: 该函数会调用socket上绑定的get_port函数获取一个可用的端口。

传输层

network-send-data-2.jpg

  • udp_sendmsg: 该函数是UDP传输层模块发送数据包的入口。该函数中先调用ip_route_output_flow获取路由信息(主要包括源IP和网卡),然后调用ip_make_skb构造skb结构体,最后将网卡的信息和该skb关联。
  • ip_route_output_flow: 该函数主要处理路由信息,它会根据路由表和目的IP,找到这个数据包应该从哪个设备发送出去,如果该socket没有绑定源IP,该函数还会根据路由表找到一个最合适的源IP给它。 如果该socket已经绑定了源IP,但根据路由表,从这个源IP对应的网卡没法到达目的地址,则该包会被丢弃,于是数据发送失败将返回错误。该函数最后会将找到的设备和源IP塞进flowi4结构体并返回给udp_sendmsg
  • ip_make_skb: 该函数的功能是构造skb包,构造好的skb包里面已经分配了IP包头(包括源IP信息),同时该函数会调用__ip_append_dat,如果需要分片的话,会在__ip_append_data函数中进行分片,同时还会在该函数中检查socket的send buffer是否已经用光,如果被用光的话,返回ENOBUFS。
  • udp_send_skb(skb, fl4): 该函数主要是往skb里面填充UDP的包头,同时处理checksum,然后交给IP网络层层的相应函数。

IP网络层

network-send-data-3.jpg

  • ip_send_skb: IP网络层模块发送数据包的入口,该函数主要是调用后面的一些列函数。
  • __ip_local_out_sk: 用来设置IP报文头的长度和checksum,然后调用下面netfilter的钩子链NF_INET_LOCAL_OUT
  • NF_INET_LOCAL_OUT: netfilter的钩子函数,可以通过iptables来配置处理函数链;如果该数据包没被丢弃,则继续往下走。
  • dst_output_sk: 该函数根据skb里面的信息,调用相应的output函数ip_output
  • ip_output: 将上一层udp_sendmsg得到的网卡信息写入skb,然后调用 NF_INET_POST_ROUTING的钩子链。
  • NF_INET_POST_ROUTING: 在这一步主要在配置了SNAT,从而导致该skb的路由信息发生变化。
  • ip_finish_output: 这里会判断经过了上一步后,路由信息是否发生变化,如果发生变化的话,需要重新调用dst_output_sk(重新调用这个函数时,可能就不会再走到ip_output,而是走到被netfilter指定的output函数里,这里有可能是xfrm4_transport_output),否则接着往下走。
  • ip_finish_output2: 根据目的IP到路由表里面找到下一跳(nexthop)的地址,然后调用__ipv4_neigh_lookup_noref去arp表里面找下一跳的neigh信息,没找到的话会调用__neigh_create构造一个空的neigh结构体。
  • dst_neigh_output: 该函数调用neigh_resolve_output获取neigh信息,并将neigh信息里面的mac地址填到skb中,然后调用dev_queue_xmit发送数据包。
  • neigh_resolve_output: 该函数里面会发送arp请求,得到下一跳的mac地址,然后将mac地址填到skb中并调用dev_queue_xmit

内核处理数据包

network-send-data-4.jpg

  • dev_queue_xmit: 内核模块开始处理发送数据包的入口函数,该函数会先获取设备对应的qdisc,如果没有的话(如loopback或者IP tunnels),就直接调用dev_hard_start_xmit,否则数据包将经过traffic control模块进行处理。
  • traffic control:该模块主要对数据包进行过滤和排序,如果队列满了的话,数据包会被丢掉,详情请参考: http://tldp.org/HOWTO/Traffic-Control-HOWTO/intro.html
  • dev_hard_start_xmit: 该函数先拷贝一份skb给“packet taps”(tcpdump的数据就从来自于此),然后调用ndo_start_xmit函数。如果dev_hard_start_xmit返回错误的话,调用它的函数会把skb放到一个地方,然后抛出软中断NET_TX_SOFTIRQ,然后交给软中断处理程序net_tx_action稍后重试。
  • ndo_start_xmit:该函数绑定到具体驱动发送数据的处理函数。

Note: ndo_start_xmit会指向具体网卡驱动的发送数据包的函数,这一步之后,数据包发送任务就交给网络设备驱动了,不同的网络设备驱动有不同的处理方式,但是大致流程基本一致:

  1. 将skb放入网卡自己的发送队列
  2. 通知网卡发送数据包
  3. 网卡发送完成后发送中断给CPU
  4. 收到中断后进行skb的清理工作

总结

理解了Linux网络数据包的接收和发送流程,我们就可以知道在哪些地方监控和修改数据包,哪些情况下数据包可能被丢弃,特别是了解了netfilter中相应钩子函数的位置,对于了解iptables的用法有一定的帮助,同时也会帮助我们更好的理解Linux下的网络虚拟设备。

网卡驱动层

网络硬件

网卡工作在物理层和数据链路层,主要由PHY/MAC芯片、Tx/Rx FIFO、DMA等组成,其中网线通过变压器接PHY芯片、PHY芯片通过MII接MAC芯片、MAC芯片接PCI总线

  • PHY芯片主要负责:
    • CSMA/CD、模数转换、编解码、串并转换
  • MAC芯片主要负责:
    • 比特流和帧的转换:7字节的前导码Preamble和1字节的帧首定界符SFD
    • CRC校验
    • Packet Filtering:L2 Filtering、VLAN Filtering、Manageability / Host Filtering

Intel的千兆网卡以82575/82576为代表、万兆网卡以82598/82599为代表

网卡驱动

网卡驱动ixgbe初始化

网卡驱动为每个新的接口在一个全局的网络设备列表里插入一个数据结构。每个接口由一个结构 net_device 项来描述, 它在<linux/netdevice.h>里定义。该结构必须动态分配。

每个网卡,无论是物理还是虚拟的网卡,都必须有一个:net_device,这个struct是在网卡驱动中分配创建的,不同的网卡,对应厂商不同的驱动,那么看看ixgbe的驱动初始化; 创建net_device 的函数是: alloc_etherdev, 或者: alloc_etherdev_mq

https://www.cnblogs.com/lidp/archive/2009/05/13/1697981.html

pci设备:

在内核中,一个PCI设备,使用struct pci_driver结构来描述, 因为在系统引导的时候,PCI设备已经被识别,当内核发现一个已经检测到的设备同驱动注册的id_table中的信息相匹配时,
它就会触发驱动的probe函数,

比如,看看ixgbe 驱动:

1
2
3
4
5
6
7
8
9
10
11
static struct pci_driver ixgb_driver = {
.name = ixgb_driver_name,
.id_table = ixgb_pci_tbl,
.probe = ixgb_probe,
.remove = ixgb_remove,
.err_handler = &ixgb_err_handler
};
# vim drivers/net/ethernet/intel/ixgbe/ixgbe_main.c
module_init
ixgbe_init_module
pci_register_driver

probe函数被调用,证明已经发现了我们所支持的网卡,这样,就可以调用register_netdev函数向内核注册网络设备了,注册之前,一般会调用alloc_etherdev分配一个net_device,然后初始化它的重要成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ixgbe_probe  
struct net_device *netdev;
struct pci_dev *pdev;
pci_enable_device_mem(pdev);
pci_request_mem_regions(pdev, ixgbe_driver_name);
pci_set_master(pdev);
pci_save_state(pdev);
netdev = alloc_etherdev_mq(sizeof(struct ixgbe_adapter), indices);// 这里分配struct net_device
alloc_etherdev_mqs
alloc_netdev_mqs(sizeof_priv, "eth%d", NET_NAME_UNKNOWN, ether_setup, txqs, rxqs);
ether_setup // Initial struct net_device

SET_NETDEV_DEV(netdev, &pdev->dev);
adapter = netdev_priv(netdev);

refs: https://blog.csdn.net/shallnet/article/details/25470775

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
alloc_etherdev_mqs() -> ether_setup()
void ether_setup(struct net_device *dev)
{
dev->header_ops = &eth_header_ops;
dev->type = ARPHRD_ETHER;
dev->hard_header_len = ETH_HLEN;
dev->min_header_len = ETH_HLEN;
dev->mtu = ETH_DATA_LEN;
dev->addr_len = ETH_ALEN;
dev->tx_queue_len = 1000; /* Ethernet wants good queues */
dev->flags = IFF_BROADCAST|IFF_MULTICAST;
dev->priv_flags |= IFF_TX_SKB_SHARING;

eth_broadcast_addr(dev->broadcast);

}
EXPORT_SYMBOL(ether_setup);
static struct pci_driver ixgbe_driver = {
.name = ixgbe_driver_name,
.id_table = ixgbe_pci_tbl,
.probe = ixgbe_probe, // 系统探测到ixgbe网卡后调用ixgbe_probe()
.remove = ixgbe_remove,
#ifdef CONFIG_PM
.suspend = ixgbe_suspend,
.resume = ixgbe_resume,
#endif
.shutdown = ixgbe_shutdown,
.sriov_configure = ixgbe_pci_sriov_configure,
.err_handler = &ixgbe_err_handler
};

static int __init ixgbe_init_module(void)
{
...
ret = pci_register_driver(&ixgbe_driver); // 注册ixgbe_driver
...
}

module_init(ixgbe_init_module);

static void __exit ixgbe_exit_module(void)
{
...
pci_unregister_driver(&ixgbe_driver); // 注销ixgbe_driver
...
}

module_exit(ixgbe_exit_module);

中断注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */

NR_SOFTIRQS
};

内核初始化期间,softirq_init会注册TASKLET_SOFTIRQ以及HI_SOFTIRQ相关联的处理函数。

1
2
3
4
5
6
7
void __init softirq_init(void)
{
......

open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

网络子系统分两种soft IRQ。NET_TX_SOFTIRQNET_RX_SOFTIRQ,分别处理发送数据包和接收数据包。这两个soft IRQ在net_dev_init函数(net/core/dev.c)中注册:

1
2
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);

收发数据包的软中断处理函数被注册为net_rx_actionnet_tx_action
其中open_softirq实现为:

1
2
3
4
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}

重要结构体初始化

每个cpu都有队列来处理接收到的帧,都有其数据结构来处理入口和出口流量,因此,不同cpu之间没有必要使用上锁机制。此队列数据结构为softnet_data(定义在include/linux/netdevice.h中):

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* Incoming packets are placed on per-cpu queues so that
* no locking is needed.
*/
struct softnet_data
{
struct Qdisc *output_queue;
struct sk_buff_headinput_pkt_queue;//有数据要传输的设备列表
struct list_headpoll_list; //双向链表,其中的设备有输入帧等着被处理。
struct sk_buff*completion_queue;//缓冲区列表,其中缓冲区已成功传输,可以释放掉

struct napi_structbacklog;
}

softnet_data 是在start_kernel 中创建的, 并且,每个cpu一个 softnet_data 变量, 这个变量中,最重要的是poll_list , 每当收到数据包时,网络设备驱动会把自己的napi_struct挂到CPU私有变量softnet_data->poll_list上, 这样在软中断时,net_rx_action会遍历cpu私有变量的softnet_data->poll_list, 执行上面所挂的napi_struct结构的poll钩子函数,将数据包从驱动传到网络协议栈。

内核初始化流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
start_kernel()
--> rest_init()
--> do_basic_setup()
--> do_initcall
-->net_dev_init

__init net_dev_init(){
//每个CPU都有一个CPU私有变量 _get_cpu_var(softnet_data)
//_get_cpu_var(softnet_data).poll_list很重要,软中断中需要遍历它的
for_each_possible_cpu(i) {
struct softnet_data *queue;
queue = &per_cpu(softnet_data, i);
skb_queue_head_init(&queue->input_pkt_queue);
queue->completion_queue = NULL;
INIT_LIST_HEAD(&queue->poll_list);
queue->backlog.poll = process_backlog;
queue->backlog.weight = weight_p;
}
//在软中断上挂网络发送handler
open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
//在软中断上挂网络接收handler
open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);
}

收发包过程图

ixgbe_adapter包含ixgbe_q_vector数组(一个ixgbe_q_vector对应一个中断),ixgbe_q_vector包含napi_struct:

硬中断函数把napi_struct加入CPU的poll_list,软中断函数net_rx_action()遍历poll_list,执行poll函数这里写图片描述

发包过程

这里写图片描述

1、网卡驱动创建tx descriptor ring(一致性DMA内存),将tx descriptor ring的总线地址写入网卡寄存器TDBA

2、协议栈通过dev_queue_xmit()将sk_buff下送网卡驱动

3、网卡驱动将sk_buff放入tx descriptor ring,更新TDT

4、DMA感知到TDT的改变后,找到tx descriptor ring中下一个将要使用的descriptor

5、DMA通过PCI总线将descriptor的数据缓存区复制到Tx FIFO

6、复制完后,通过MAC芯片将数据包发送出去

7、发送完后,网卡更新TDH,启动硬中断通知CPU释放数据缓存区中的数据包

Tx Ring Buffer

SW将sk_buff挂载到从next_to_use开始的N个descriptor,next_to_use += N,tail = next_to_use(写网卡寄存器TDT)

HW使用DMA读从head开始的M个descriptor的sk_buff,发送成功后回写DD(Descriptor Done),head += M

SW将从next_to_clean的开始的L个sk_buff移出Tx Ring Buffer并清理,next_to_clean += L

注意:每次挂载完sk_buff后,tail和next_to_use指向同一个descriptor

在这里插入图片描述

收包过程

这里写图片描述

1、网卡驱动创建rx descriptor ring(一致性DMA内存),将rx descriptor ring的总线地址写入网卡寄存器RDBA

2、网卡驱动为每个descriptor分配sk_buff和数据缓存区,流式DMA映射数据缓存区,将数据缓存区的总线地址保存到descriptor

3、网卡接收数据包,将数据包写入Rx FIFO

4、DMA找到rx descriptor ring中下一个将要使用的descriptor

5、整个数据包写入Rx FIFO后,DMA通过PCI总线将Rx FIFO中的数据包复制到descriptor的数据缓存区

6、复制完后,网卡启动硬中断通知CPU数据缓存区中已经有新的数据包了,CPU执行硬中断函数:

  • NAPI(以e1000网卡为例):e1000_intr() -> napi_schedule() -> raise_softirq_irqoff(NET_RX_SOFTIRQ)
  • 非NAPI(以dm9000网卡为例):dm9000_interrupt() -> dm9000_rx() -> netif_rx() -> napi_schedule() -> napi_schedule() -> raise_softirq_irqoff(NET_RX_SOFTIRQ)

7、ksoftirqd执行软中断函数net_rx_action():

  • NAPI(以e1000网卡为例):net_rx_action() -> e1000_clean() -> e1000_clean_rx_irq() -> e1000_receive_skb() -> netif_receive_skb()
  • 非NAPI(以dm9000网卡为例):net_rx_action() -> process_backlog() -> netif_receive_skb()

8、网卡驱动通过netif_receive_skb()将sk_buff上送协议栈

Rx Ring Buffer

SW向从next_to_use开始的N个descriptor补充sk_buff,next_to_use += N,tail = next_to_use(写网卡寄存器RDT)

HW写Frame到从head开始的M个descriptor的sk_buff,写完后回写EOP(End of Packet),head += M

SW将从next_to_clean开始的L个sk_buff移出Rx Ring Buffer并上送协议栈,next_to_clean += L,向从next_to_use开始的L个descriptor补充sk_buff,next_to_use += L,tail = next_to_use

注意:每次补充完sk_buff后,tail和next_to_use指向同一个sk_buff

在这里插入图片描述

中断上下部

这里写图片描述

do_IRQ()是CPU处理硬中断的总入口

1
2
3
4
5
6
7
8
9
10
11
12
13
// 在e1000_request_irq()中注册硬中断,中断函数为e1000_intr()
irq_handler_t handler = e1000_intr;
err = request_irq(adapter->pdev->irq, handler, irq_flags, netdev->name,
netdev);

// 在net_dev_init()中注册软中断,中断函数为net_rx_action()
open_softirq(NET_RX_SOFTIRQ, net_rx_action);

// 在e1000_probe()中注册napi的poll函数为e1000_clean()
netif_napi_add(netdev, &adapter->napi, e1000_clean, 64);

// 在net_dev_init()中注册非napi的poll函数为process_backlog()
queue->backlog.poll = process_backlog;

netif_rx()

在netif_rx()中把skb加入CPU的softnet_data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int netif_rx(struct sk_buff *skb)
{
struct softnet_data *queue;
unsigned long flags;

/* if netpoll wants it, pretend we never saw it */
if (netpoll_rx(skb))
return NET_RX_DROP;

if (!skb->tstamp.tv64)
net_timestamp(skb);

/*
* The code is rearranged so that the path is the most
* short when CPU is congested, but is still operating.
*/
local_irq_save(flags);
queue = &__get_cpu_var(softnet_data); // 得到CPU的softnet_data

__get_cpu_var(netdev_rx_stat).total++;
if (queue->input_pkt_queue.qlen <= netdev_max_backlog) { // 若队列长度不大于netdev_max_backlog
if (queue->input_pkt_queue.qlen) { // 若队列长度非0,表示queue->backlog已被加入poll_list
enqueue:
__skb_queue_tail(&queue->input_pkt_queue, skb); // 将skb加入队列尾部
local_irq_restore(flags);
return NET_RX_SUCCESS;
}

napi_schedule(&queue->backlog); // 调度queue->backlog
goto enqueue; // 将skb加入队列尾部
}

__get_cpu_var(netdev_rx_stat).dropped++;
local_irq_restore(flags);

kfree_skb(skb);
return NET_RX_DROP;
}

硬中断中的netif_rx()函数:把skb加入CPU的softnet_data-> input_pkt_queue队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
netif_rx(skb);  // 在 硬中断中,处理skb
netif_rx_internal(skb);
trace_netif_rx(skb);
preempt_disable();
rcu_read_lock();
cpu = get_rps_cpu(skb->dev, skb, &rflow); // 通过rps,获得cpu id
enqueue_to_backlog(skb, cpu, &rflow->last_qtail);
struct softnet_data *sd;
sd = &per_cpu(softnet_data, cpu); // 根据cpu id,获得sd
rps_lock(sd);
__skb_queue_tail(&sd->input_pkt_queue, skb); // enqueue 动作
input_queue_tail_incr_save(sd, qtail);
rps_unlock(sd);
local_irq_restore(flags)
return NET_RX_SUCCESS
rcu_read_unlock();
preempt_enable();
static int netif_rx_internal(struct sk_buff *skb)
{
int ret;

net_timestamp_check(netdev_tstamp_prequeue, skb);

trace_netif_rx(skb);
#ifdef CONFIG_RPS
if (static_key_false(&rps_needed)) {
struct rps_dev_flow voidflow, *rflow = &voidflow;
int cpu;

preempt_disable(); // 关闭抢占
rcu_read_lock();

cpu = get_rps_cpu(skb->dev, skb, &rflow);
if (cpu < 0)
cpu = smp_processor_id();

ret = enqueue_to_backlog(skb, cpu, &rflow->last_qtail); // 加入队列

rcu_read_unlock();
preempt_enable();
} else
#endif
{
unsigned int qtail;
ret = enqueue_to_backlog(skb, get_cpu(), &qtail);
put_cpu();
}
return ret;
}

enqueue_to_backlog()主要工作,就是将skb挂到一个cpu下的softnet_data-> input_pkt_queue队列里,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
static int enqueue_to_backlog(struct sk_buff *skb, int cpu,
unsigned int *qtail)
{
struct softnet_data *sd;
unsigned long flags;
unsigned int qlen;

sd = &per_cpu(softnet_data, cpu);

local_irq_save(flags);

rps_lock(sd);
if (!netif_running(skb->dev))
goto drop;
qlen = skb_queue_len(&sd->input_pkt_queue);
if (qlen <= netdev_max_backlog && !skb_flow_limit(skb, qlen)) {
if (qlen) {
enqueue:
__skb_queue_tail(&sd->input_pkt_queue, skb); // 将skb加入到sd-> input_pkt_queue队列
input_queue_tail_incr_save(sd, qtail);
rps_unlock(sd);
local_irq_restore(flags);
return NET_RX_SUCCESS;
}

/* Schedule NAPI for backlog device
* We can use non atomic operation since we own the queue lock
*/
if (!__test_and_set_bit(NAPI_STATE_SCHED, &sd->backlog.state)) {
if (!rps_ipi_queued(sd))
____napi_schedule(sd, &sd->backlog); // napi方式处理skb
}
goto enqueue;
}

drop:
sd->dropped++;
rps_unlock(sd);

local_irq_restore(flags);

atomic_long_inc(&skb->dev->rx_dropped);
kfree_skb(skb);
return NET_RX_DROP;
}
____napi_schedule
list_add_tail(&napi->poll_list, &sd->poll_list);

上述,就是硬中断需要做的工作,然后, 软中断net_rx_action()会遍历这个list,进行进一步操作。

中断处理上,处理skb,包含两种方式:

硬中断就是上半部,在上半部,有netif_rx 中对napi进行判断,在下半部的softirq (net_rx_action()) 中,同样对napi和非napi进行了判断 !

  • 非NAPI
    • 非NAPI设备驱动会为其所接收的每一个帧产生一个中断事件,在高流量负载下,会花掉大量时间处理中断事件,造成资源浪费。而NAPI驱动混合了中断事件和轮询,在高流量负载下其性能会比旧方法要好。
  • NAPI
    • NAPI主要思想是混合使用中断事件和轮询,而不是仅仅使用中断事件驱动模型。当收到新的帧时,关中断,再一次处理完所有入口队列。从内核观点来看,NAPI方法因为中断事件少了,减少了cpu负载。

默认是napi?还是非napi?

在初始化时,默认是非napi的模式,poll函数默认是: process_backlog,如下:

1
2
3
4
net_dev_init
for_each_possible_cpu(i) {
sd->backlog.poll = process_backlog;
}

net_rx_action中将会调用设备的poll函数, 如果没有, 就是默认的process_backlog函数
process_backlog函数里面将skb出队列之后, netif_receive_skb处理此skb

软中断中,使用net_rx_action(),处理skb:

1
2
3
4
5
6
7、ksoftirqd执行软中断函数`net_rx_action()`:

* NAPI(以e1000网卡为例):`net_rx_action() -> e1000_clean() -> e1000_clean_rx_irq() -> e1000_receive_skb() -> netif_receive_skb()`
* 非NAPI(以dm9000网卡为例):`net_rx_action() -> process_backlog() -> netif_receive_skb()`

8、网卡驱动通过`netif_receive_skb()`将`sk_buff`上送协议栈

最后,通过netif_receive_skb(), 将skb送上协议栈;

软中断中,对napi和非napi的处理: process_backlog

1
2
3
4
net_rx_action
process_backlog
__netif_receive_skb
__netif_receive_skb_core

非NAPI vs NAPI

  • (1) 支持NAPI的网卡驱动必须提供轮询方法poll()
  • (2) 非NAPI的内核接口为netif_rx()
    NAPI的内核接口为napi_schedule()
  • (3) 非NAPI使用共享的CPU队列softnet_data->input_pkt_queue
    NAPI使用设备内存(或者设备驱动程序的接收环)。

img

Example

Data Structures

arch/cris/drivers/ethernet.c
1
2
3
4
5
#define NBR_OF_RX_DESC     64
#define NBR_OF_TX_DESC 256
static etrax_eth_descr RxDescList[NBR_OF_RX_DESC] __attribute__ ((aligned(32)));
...
static etrax_eth_descr TxDescList[NBR_OF_TX_DESC] __attribute__ ((aligned(32)));
arch/cris/drivers/ethernet.c
1
2
3
4
5
typedef struct etrax_eth_descr
{
etrax_dma_descr descr;
struct sk_buff* skb;
} etrax_eth_descr;

要记住,对于DMA而言,其数据传输不会经过MMU,因此需要一个真实的物理地址,而不是一个虚拟地址。所以,在 extrax_dma_descr 结构体中的 buf/next 指针字段都是 unsigned long 类型,而不是 void *类型。

include/asm-cris/svinto.h
1
2
3
4
5
6
7
8
9
typedef struct etrax_dma_descr {
unsigned short sw_len; /* 0-1 */
unsigned short ctrl; /* 2-3 */
unsigned long next; /* 4-7 */
unsigned long buf; /* 8-11 */
unsigned short hw_len; /* 12-13 */
unsigned char status; /* 14 */
unsigned char fifo_len; /* 15 */
} etrax_dma_descr;

关于这几个字段的含义解释如下:

  • sw_len:表明这个DMA描述符指向的DMA Buffer的空间大小
  • ctrl:包含了这个DMA Channel的控制信息
  • next:指向在DMA RingBuffer List中的下一个DMA描述符
  • buf:指向这个DMA描述符拥有的DMA Buffer的开始地址,这是数据在接收和发送所在的实际地址
  • hw_len:表明这个DMA描述符指向的DMA Buffer的物理空间大小,它与sw_len大小不一样,因为它还会包含一些控制字段
  • status:状态/控制字段,比如可以为 d_eop表明这个DMA描述符是最后一个packet。
1
2
3
4
5
6
7
8
9
10
11
12
enum {                          /* Available in:  */
d_eol = (1 << 0), /* flags */
d_eop = (1 << 1), /* flags & status */
d_wait = (1 << 2), /* flags */
d_int = (1 << 3), /* flags */
d_txerr = (1 << 4), /* flags */
d_stop = (1 << 4), /* status */
d_ecp = (1 << 4), /* flags & status */
d_pri = (1 << 5), /* flags & status */
d_alignerr = (1 << 6), /* status */
d_crcerr = (1 << 7) /* status */
};

Initilization

作为一个网络设备,etrax 的初始化方法与其他网络设备基本一致,具体可以参考 Linux 网络设备 。具体而言,其通过 etrax_ethernet_init 函数来初始化设备。首先通过调用 ether_setup 来设置与以太网协议相关的参数。接下来需要初始化DMA描述符中的接收与发送 Ring Buffer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
static int __init
etrax_ethernet_init(struct net_device *dev)
{
...
ether_setup(dev);
...
dev->open = e100_open;
dev->hard_start_xmit = e100_send_packet;
dev->stop = e100_close;
dev->get_stats = e100_get_stats;
...
/* Initialise receive descriptors */
for (i = 0; i < NBR_OF_RX_DESC; i++) {
RxDescList[i].skb = dev_alloc_skb(MAX_MEDIA_DATA_SIZE);
RxDescList[i].descr.ctrl = 0;
RxDescList[i].descr.sw_len = MAX_MEDIA_DATA_SIZE;
RxDescList[i].descr.next = virt_to_phys(&RxDescList[i + 1]);
RxDescList[i].descr.buf = virt_to_phys(RxDescList[i].skb->data);
RxDescList[i].descr.status = 0;
RxDescList[i].descr.hw_len = 0;

prepare_rx_descriptor(&RxDescList[i].descr);
}

RxDescList[NBR_OF_RX_DESC - 1].descr.ctrl = d_eol;
RxDescList[NBR_OF_RX_DESC - 1].descr.next = virt_to_phys(&RxDescList[0]);
rx_queue_len = 0;

/* Initialize transmit descriptors */
for (i = 0; i < NBR_OF_TX_DESC; i++) {
TxDescList[i].descr.ctrl = 0;
TxDescList[i].descr.sw_len = 0;
TxDescList[i].descr.next = virt_to_phys(&TxDescList[i + 1].descr);
TxDescList[i].descr.buf = 0;
TxDescList[i].descr.status = 0;
TxDescList[i].descr.hw_len = 0;
TxDescList[i].skb = 0;
}

TxDescList[NBR_OF_TX_DESC - 1].descr.ctrl = d_eol;
TxDescList[NBR_OF_TX_DESC - 1].descr.next = virt_to_phys(&TxDescList[0].descr);

/* Initialise initial pointers */

myNextRxDesc = &RxDescList[0];
myLastRxDesc = &RxDescList[NBR_OF_RX_DESC - 1];
myPrevRxDesc = &RxDescList[NBR_OF_RX_DESC - 1];
myFirstTxDesc = &TxDescList[0];
myNextTxDesc = &TxDescList[0];
myLastTxDesc = &TxDescList[NBR_OF_TX_DESC - 1];
...
return 0;
}
Initialization of DMA Transmit Ring Buffers
Initialization of DMA Receive Ring Buffers

中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static void
e100rx_interrupt(int irq, void *dev_id, struct pt_regs * regs)
{
struct net_device *dev = (struct net_device *)dev_id;
unsigned long irqbits = *R_IRQ_MASK2_RD;

if (irqbits & IO_STATE(R_IRQ_MASK2_RD, dma1_eop, active)) {
/* acknowledge the eop interrupt */

*R_DMA_CH1_CLR_INTR = IO_STATE(R_DMA_CH1_CLR_INTR, clr_eop, do);

/* check if one or more complete packets were indeed received */

while (*R_DMA_CH1_FIRST != virt_to_phys(myNextRxDesc)) {
/* Take out the buffer and give it to the OS, then
* allocate a new buffer to put a packet in.
*/
e100_rx(dev);
((struct net_local *)dev->priv)->stats.rx_packets++;
/* restart/continue on the channel, for safety */
*R_DMA_CH1_CMD = IO_STATE(R_DMA_CH1_CMD, cmd, restart);
/* clear dma channel 1 eop/descr irq bits */
*R_DMA_CH1_CLR_INTR =
IO_STATE(R_DMA_CH1_CLR_INTR, clr_eop, do) |
IO_STATE(R_DMA_CH1_CLR_INTR, clr_descr, do);

/* now, we might have gotten another packet
so we have to loop back and check if so */
}
}
}

Rx Ring Buffer Initialized

arch/cris/drivers/ethernet.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
static void
e100_rx(struct net_device *dev)
{
struct sk_buff *skb;
int length = 0;
...
length = myNextRxDesc->descr.hw_len - 4;
...
if (length < RX_COPYBREAK) {
/* Small packet, copy data */
skb = dev_alloc_skb(length - ETHER_HEAD_LEN);
...
skb_put(skb, length - ETHER_HEAD_LEN); /* allocate room for the packet body */
skb_data_ptr = skb_push(skb, ETHER_HEAD_LEN); /* allocate room for the header */
...
memcpy(skb_data_ptr, phys_to_virt(myNextRxDesc->descr.buf), length);
}
else {
/* Large packet, send directly to upper layers and allocate new memory */
skb = myNextRxDesc->skb;
skb_put(skb, length);
myNextRxDesc->skb = dev_alloc_skb(MAX_MEDIA_DATA_SIZE);
myNextRxDesc->descr.buf = virt_to_phys(myNextRxDesc->skb->data);
}
...
skb->protocol = eth_type_trans(skb, dev);
/* Send the packet to the upper layers */
netif_rx(skb);
/* Prepare for next packet */
myNextRxDesc->descr.status = 0;
myPrevRxDesc = myNextRxDesc;
myNextRxDesc = phys_to_virt(myNextRxDesc->descr.next);

rx_queue_len++;

/* Check if descriptors should be returned */
if (rx_queue_len == RX_QUEUE_THRESHOLD) {
flush_etrax_cache();
myPrevRxDesc->descr.ctrl |= d_eol;
myLastRxDesc->descr.ctrl &= ~d_eol;
myLastRxDesc = myPrevRxDesc;
rx_queue_len = 0;
}
}

Reception of Packets

Transmission of Packets

IP层

INET Socket层

BSD Socket层

参考资料

82599:
https://www.intel.com/content/www/us/en/embedded/products/networking/82599-10-gbe-controller-datasheet.html
网卡:
http://blog.csdn.net/tao546377318/article/details/51602298
http://blog.csdn.net/Just_Do_IT_Ye/article/details/47000383
DMA:
http://www.wowotech.net/memory_management/DMA-Mapping-api.html
http://blog.csdn.net/phunxm/article/details/9452575
http://blog.chinaunix.net/uid-1858380-id-3261817.html
http://www.elecfans.com/book/232/
协议栈收发包过程:
https://segmentfault.com/a/1190000008836467
https://segmentfault.com/a/1190000008926093
NAPI:
http://blog.csdn.net/zhangskd/article/details/21627963

https://blog.packagecloud.io/eng/2016/06/22/monitoring-tuning-linux-networking-stack-receiving-data/
https://blog.packagecloud.io/eng/2017/02/06/monitoring-tuning-linux-networking-stack-sending-data/
https://blog.packagecloud.io/eng/2016/10/11/monitoring-tuning-linux-networking-stack-receiving-data-illustrated/

参考

参考资料