内核协议栈数据包转发目录1 NAPI流程与非NAPI1.1NAPI驱动流程1.2非NAPI流程1.3NAPI和非NAPI的区别2内核接受数据2.1数据接收过程2.2 采取DMA技术实现3 e100采用NAPI接收数据过程3.1 e100_open 启动e100网卡3.2 e100_rx_alloc_list 建立环形缓冲区3.3 e100_rx_alloc_skb 分配skb缓存3.4 e100_poll 轮询函数3.5 e100_rx_clean 数据包的接收和传输3.6 e100_rx_indicate4 队列层4.1、软中断与下半部4.2、队列层5采用非NAPI接收数据过程5.1netif_rx5.2轮询与中断调用netif_rx_schedule不同点5.3 netif_rx_schedule5.4 net_rx_action5.5 process_backlog6数据包进入网络层6.1 netif_receive_skb():6.2 ip_rcv():6.3 ip_rcv_finish():6.4 dst_input():6.5本地流程ip_local_deliver:6.6转发流程ip_forward():1 NAPI流程与非NAPI1.1NAPI驱动流程:中断发生-->确定中断原因是数据接收完毕(中断原因也可能是发送完毕,DMA完毕,甚至是中断通道上的其他设备中断)-->通过netif_rx_schedule将驱动自己的napi结构加入softnet_data的poll_list 链表,禁用网卡中断,并发出软中断NET_RX_SOFTIRQ-->中断返回时触发软中断调用相应的函数net_rx_action,从softnet_data的poll_list上取下刚挂入的napi结构,并且调用其 poll函数,这个poll函数也是驱动自己提供的,比如e100网卡驱动中的e100_poll等。
-->在poll函数中进行轮询,直到接受完所有的数据或者预算(budget)耗尽。
每接收一个报文要分配skb,用eth_type_trans处理并交给netif_receive_skb。
-->如果数据全部接收完(预算没有用完),则重新使能中断并将napi从链表中取下。
如果数据没接收完,则什么也不作,等待下一次poll函数被调度。
1.2非NAPI流程:中断发生-->确定中断发生的原因是接收完毕。
分配skb,读入数据,用eth_type_trans处理并且将skb交给netif_rx-->在netif_rx中,将packet加入到softnet_data的input_pkt_queue末尾(NAPI 驱动不使用这个 input_pkt_queue),再通过napi_schedule将softnet_data中的backlog(这也是个napi结构)加入 softnet_data的poll_list,最后发出软中断 -->软中断net_rx_action从poll_list上取下softnet_data的backlog,调用其poll 函数,这个poll函数是内核提供的process_backlog-->函数process_backlog从softnet_data的input_pkt_queue末尾取下skb,并且直接交给netif_receive_skb处理。
-->如果input_pkt_queue中所有skb都处理完则将backlog从队列中除去(注意input_pkt_queue中可能有多个网卡加入的报文,因为它是每cpu公用的)并退出循环;如果预算用完后也跳出循环。
最后返回接受到的包数1.3 NAPI和非NAPI的区别NAPI和非NAPI的区别1.NAPI使用中断+轮询的方式,中断产生之后暂时关闭中断然后轮询接收完所有的数据包,接着再开中断。
而非NAPI采用纯粹中断的方式,一个中断接收一个数据包2.NAPI都有自己的struct napi结构,非NAPI没有3.NAPI有自己的poll函数,而且接收数据都是在软中断调用poll函数时做的,而非NAPI使用公共的process_backlog函数作为其poll函数,接收数据是在硬件中断中做的4.NAPI在poll函数中接收完数据之后直接把skb发给netif_receive_skb,而非NAPI 在硬件中断中接收了数据通过 netif_rx把skb挂到公共的input_pkt_queue上,最后由软中断调用的process_backlog函数来将其发送给 netif_receive_skb驱动以及软中断这块对skb仅仅做了以下简单处理:1.调用skb_reserve预留出2个字节的空间,这是为了让ip首部对齐,因为以太网首部是14字节2.调用skb_put将tail指向数据末尾3.调用eth_type_trans进行如下处理:(1)将skb->dev指向接收设备(2)将skb->mac_header指向data(此时data就是指向mac起始地址)(3)调用skb_pull(skb, ETH_HLEN)将skb->data后移14字节指向ip首部(4)通过比较目的mac地址判断包的类型,并将skb->pkt_type赋值PACKET_BROADCAST或PACKET_MULTICAST或者PACKET_OTHERHOST,因为PACKET_HOST为0,所以是默认值(5)最后判断协议类型,并返回(大部分情况下直接返回eth首部的protocol 字段的值),这个返回值被存在skb->protocol字段中总结,结束后,skb->data指向ip首部,skb->mac_header指向 mac首部,skb->protocol 储存L3的协议代码,skb->pkt_type已被设置,skb->len等于接收到的报文长度减去eth 首部长度,也就是整个ip报文的总长。
其余字段基本上还是默认值。
2 内核接受数据2.1数据接收过程内核从网卡接受数据,传统的经典过程:1、数据到达网卡;2、网卡产生一个中断给内核;3、内核使用I/O指令,从网卡I/O区域中去读取数据;就是大流量的数据来到,网卡会产生大量的中断,内核在中断上下文中,会浪费大量的资源来处理中断本身。
这就是no NAPI方式。
no NAPI:mac每收到一个以太网包,都会产生一个接收中断给cpu,即完全靠中断方式来收包,收包缺点是当网络流量很大时,cpu大部分时间都耗在了处理mac的中断。
NAPI:采用中断+ 轮询的方式:mac收到一个包来后会产生接收中断,但是马上关闭。
直到收够了netdev_max_backlog个包(默认300),或者收完mac上所有包后,才再打开接收中断。
通过sysctl来修改dev_max_backlog或者通过proc修改/proc/sys/net/core/netdev_max_backlog2.2 DMA技术实现从网卡的I/O区域,包括I/O寄存器或I/O内存中去读取数据,这都要CPU去读,也要占用CPU资源,“CPU从I/O区域读,然后把它放到内存(这个内存指的是系统本身的物理内存,跟外设的内存不相干,也叫主内存)中”。
Linux使用DMA技术——让网卡直接从主内存之间读写它们的I/O数据,就不关CPU的事。
1、首先,内核在主内存中为收发数据建立一个环形的缓冲队列(通常叫DMA环形缓冲区)。
2、内核将这个缓冲区通过DMA映射,把这个队列交给网卡;3、网卡收到数据,就直接放进这个环形缓冲区了——也就是直接放进主内存了;然后,向系统产生一个中断;4、内核收到这个中断,就取消DMA映射,这样,内核就直接从主内存中读取数据;这一个过程比传统的过程少了不少工作,因为设备直接把数据放进了主内存,不需要CPU的干预,效率提高了.对应以上4步,来看它的具体实现:1、分配环形DMA缓冲区Linux内核中,用skb来描述一个缓存,所谓分配,就是建立一定数量的skb,然后把它们组织成一个双向链表;2、建立DMA映射内核通过调用dma_map_single(struct device *dev,void *buffer,size_t size,enum dma_data_direction direction)建立映射关系。
struct device *dev,描述一个设备;buffer:把哪个地址映射给设备;也就是某一个skb——要映射全部,当然是做一个双向链表的循环即可;size:缓存大小;direction:映射方向——谁传给谁:一般来说,是“双向”映射,数据在设备和内存之间双向流动;对于PCI设备而言(网卡一般是PCI的),通过另一个包裹函数pci_map_single,这样,就把buffer交给设备了!设备可以直接从里边读/取数据。
3、这一步由硬件完成;4、取消映射dma_unmap_single,对PCI而言,大多调用它的包裹函数pci_unmap_single,不取消的话,缓存控制权还在设备手里,要调用它,把主动权掌握在CPU手里——因为我们已经接收到数据了,应该由CPU把数据交给上层网络栈;当然,不取消之前,通常要读一些状态位信息,诸如此类,一般是调用dma_sync_single_for_cpu()让CPU在取消映射前,就可以访问DMA缓冲区中的内容。
每个网卡(MAC)都有自己的专用DMA Engine,如上图的TSEC 和e1000 网卡intel82546。
上图中的红色线就是以太网数据流,DMA与DDR打交道需要其他模块的协助,如TSEC,PCI controller。
以太网数据在TSEC<-->DDR PCI_Controller<-->DDR 之间的流动,CPU的core是不需要介入的,只有在数据流动结束时(接收完、发送完),DMA Engine才会以外部中断的方式告诉CPU的core3 e100接收数据过程3.1 e100_open 启动e100网卡e100_open(struct net_device *dev),调用e100_up,就是环形缓冲区的建立,这一步,是通过e100_rx_alloc_list函数调用完成的。
3.2e100_rx_alloc_list 建立环形缓冲区static int e100_rx_alloc_list(struct nic *nic){struct rx *rx;unsigned int i, count = nic->params.rfds.count;nic->rx_to_use = nic->rx_to_clean = NULL;nic->ru_running = RU_UNINITIALIZED;/*结构struct rx用来描述一个缓冲区节点,这里分配了count个*/if(!(nic->rxs = kmalloc(sizeof(struct rx) * count, GFP_ATOMIC)))return -ENOMEM;memset(nic->rxs, 0, sizeof(struct rx) * count);/*虽然是连续分配的,不过还是遍历它,建立双向链表,然后为每一个rx的skb指针分员分配空间skb用来描述内核中的一个数据包,呵呵,说到重点了*/for(rx = nic->rxs, i = 0; i < count; rx++, i++) {rx->next = (i + 1 < count) ? rx + 1 : nic->rxs;rx->prev = (i == 0) ? nic->rxs + count - 1 : rx - 1;if(e100_rx_alloc_skb(nic, rx)) { /*分配缓存*/e100_rx_clean_list(nic);return -ENOMEM;}}nic->rx_to_use = nic->rx_to_clean = nic->rxs;nic->ru_running = RU_SUSPENDED;return 0;}3.3e100_rx_alloc_skb 分配skb缓存static inline int e100_rx_alloc_skb(struct nic *nic, struct rx *rx){/*skb缓存的分配,是通过调用系统函数dev_alloc_skb来完成的,它同内核栈中通常调用alloc_skb的区别在于,它是原子的,所以,通常在中断上下文中使用*/if(!(rx->skb = dev_alloc_skb(RFD_BUF_LEN + NET_IP_ALIGN)))return -ENOMEM;/*初始化必要的成员*/rx->skb->dev = nic->netdev;skb_reserve(rx->skb, NET_IP_ALIGN);/*这里在数据区之前,留了一块sizeof(struct rfd) 这么大的空间,该结构的一个重要作用,用来保存一些状态信息,比如,在接收数据之前,可以先通过它,来判断是否真有数据到达等,诸如此类*/memcpy(rx->skb->data, &nic->blank_rfd, sizeof(struct rfd));/*这是最关键的一步,建立DMA映射,把每一个缓冲区rx->skb->data都映射给了设备,缓存区节点rx利用dma_addr保存了每一次映射的地址,这个地址后面会被用到*/rx->dma_addr = pci_map_single(nic->pdev, rx->skb->data,RFD_BUF_LEN, PCI_DMA_BIDIRECTIONAL);if(pci_dma_mapping_error(rx->dma_addr)) {dev_kfree_skb_any(rx->skb);rx->skb = 0;rx->dma_addr = 0;return -ENOMEM;}/* Link the RFD to end of RFA by linking previous RFD to* this one, and clearing EL bit of previous. */if(rx->prev->skb) {struct rfd *prev_rfd = (struct rfd *)rx->prev->skb->data;/*put_unaligned(val,ptr);用到把var放到ptr指针的地方,它能处理处理内存对齐的问题prev_rfd是在缓冲区开始处保存的一点空间,它的link成员,也保存了映射后的地址*/ put_unaligned(cpu_to_le32(rx->dma_addr),(u32 *)&prev_rfd->link);wmb();prev_rfd->command &= ~cpu_to_le16(cb_el);pci_dma_sync_single_for_device(nic->pdev, rx->prev->dma_addr,sizeof(struct rfd), PCI_DMA_TODEVICE);}return 0;}e100_rx_alloc_list函数在一个循环中,建立了环形缓冲区,并调用e100_rx_alloc_skb为每个缓冲区分配了空间,并做了DMA映射。