代码学习-Linux内核网卡收包过程(NAPI)【转】
作者:互联网
转自:https://blog.csdn.net/crazycoder8848/article/details/46333761
版权声明:本文没有任何版权限制,任何人可以以任何方式使用本文。 https://blog.csdn.net/crazycoder8848/article/details/46333761
本文通过学习RealTek8169/8168/8101网卡的驱动代码(drivers/net/r8169.c),梳理一下Linux下网卡的收包过程。
在下水平相当有限,有不当之处,还请大家斧正^_^
驱动的初始化
如下的rtl8169_init_module函数是此驱动的初始化代码,此函数只干了一件事,就是向内核注册一个pci驱动rtl8169_pci_driver。
static int __init rtl8169_init_module(void)
{
returnpci_register_driver(&rtl8169_pci_driver);
}
rtl8169_pci_driver驱动的定义如下。
static struct pci_driver rtl8169_pci_driver= {
.name = MODULENAME,
.id_table = rtl8169_pci_tbl,
.probe = rtl8169_init_one,
.remove = __devexit_p(rtl8169_remove_one),
.shutdown = rtl_shutdown,
.driver.pm = RTL8169_PM_OPS,
};
.id_table成员是一个驱动程序支持的全部设备列表。对于rtl8169_pci_driver,id_tabl就是b rtl8169_pci_tbl了,其内容如下。可见此驱动支持多种不同型号的网卡芯片。
static struct pci_device_idrtl8169_pci_tbl[] = {
{PCI_DEVICE(PCI_VENDOR_ID_REALTEK, 0x8129),0, 0, RTL_CFG_0 },
{PCI_DEVICE(PCI_VENDOR_ID_REALTEK, 0x8136),0, 0, RTL_CFG_2 },
{PCI_DEVICE(PCI_VENDOR_ID_REALTEK, 0x8167),0, 0, RTL_CFG_0 },
{PCI_DEVICE(PCI_VENDOR_ID_REALTEK, 0x8168),0, 0, RTL_CFG_1 },
{PCI_DEVICE(PCI_VENDOR_ID_REALTEK, 0x8169),0, 0, RTL_CFG_0 },
{PCI_DEVICE(PCI_VENDOR_ID_DLINK, 0x4300),0, 0, RTL_CFG_0 },
{PCI_DEVICE(PCI_VENDOR_ID_AT, 0xc107),0, 0, RTL_CFG_0 },
{PCI_DEVICE(0x16ec, 0x0116),0, 0, RTL_CFG_0 },
{PCI_VENDOR_ID_LINKSYS, 0x1032, PCI_ANY_ID, 0x0024, 0, 0, RTL_CFG_0 },
{0x0001, 0x8168, PCI_ANY_ID, 0x2410, 0, 0, RTL_CFG_2 },
{0,},
};
需要注意到,驱动中还有如下一行代码。
MODULE_DEVICE_TABLE(pci, rtl8169_pci_tbl);
这个宏貌似是给rtl8169_pci_tbl变量起了一个别名__mod_pci_device_table。可见__mod_pci_device_table是pci设备驱动中的一个统一的符号名。他包含了此驱动支持的全部设备的列表。
这是干什么的呢?这时解释一下。
如果此驱动被编译到了内核中,或者此驱动已经被加载到内核中。那么这一句话就没什么作用了。因为内核随时可以根据rtl8169_pci_driver中的信息,来判断某一设备是否匹配此驱动。代码见pci_match_device函数。
但是,如果此驱动被编译成了一个模块文件r8169.ko,并且没有被加载到内核中(正常情况下,大量的设备驱动都应该是被编译成模块的,并且都是不加载到内核中的。机器上电时,根据扫描到的设备,动态加载相应的驱动模块。不然的话,如果各种驱动都加载到内核中,那内核就太臃肿了)。此时,如果内核扫描到了一个pci设备,就得加载相应的驱动模块文件。但内核只掌握了此设备的类似Vendorand device ID这样的信息,如何将这种信息对应到具体的驱动模块文件r8169.ko呢。这时候MODULE_DEVICE_TABLE这句话就发挥作用了。具体细节,可以参考udev与modprobe等相关知识。
这里顺便多说两句,当一个pci驱动被加载到内核中时(见调用链pci_register_driver ->__pci_register_driver -> driver_register ->bus_add_driver ->driver_attach),或者当内核发现一个新设备时(见调用链device_add->bus_probe_device->device_attach),都会做一次驱动与设备的匹配操作。
probe一块网卡rtl8169_init_one
当某一块网卡匹配了rtl8169_pci_driver时,rtl8169_pci_driver. probe函数(即rtl8169_init_one)即被调用,此函数针对此网卡做一些初始化操作,然后此网卡就可用了。
这里顺便说一下,一个设备,如何与业务流程关联起来。不同的设备,可能是不一样的。
例如,有些设备(如看门狗设备,块设备),是在文件系统中创建一个文件(如/dev/ watchdog)。业务通过打开设备文件,操作/读/写设备文件,就将设备用起来了。
而网卡设备,则不是这样。网卡设备是向内核注册一个struct net_device结构。注册以后,ifconfig命令就能看到此网卡了。内核协议栈及路由系统也就与此net_device结构关联起来了。struct net_device结构,是内核对网络设备的一种抽象,他使得内核可以用统一的方式操作一切网络设备。
下面看看rtl8169_init_one的主要任务:
l 将网卡配置寄存器区间映射到内核虚存空间
l 执行硬件初始化
l 构建一个net_device结构,注册到内核中
这里需要多说的是net_device结构的构建。net_device结构类似于面向对象编程中的多态。前面说过,struct net_device结构,是内核对网络设备的一种抽象,他使得内核可以用统一的方式操作一切网络设备。具体的网卡驱动,如何各自以不同的方法实现自己的功能呢。每个net_device结构上,除了通用的内容外,还有一片私有空间用于保存各个网卡的私有数据。通过netdev_priv函数即可得到一个net_device结构的私有空间。R8169驱动就在这个私有空间中保存了一个struct rtl8169_private结构,用于保存R8169系列网卡的私有数据。这里就不详细说明了,但后面会根据需要提到其中的某些成分。
net_device结构中包含一个指针netdev_ops,指向一个struct net_device_ops结构,此结构中包含了指向网卡的各种操作的函数指针。这种设计使得内核可以对于任何网卡,看到一个统一的操作界面。不同的网卡驱动,将自己实现的各种操作的函数指针填到一个net_device_ops结构中,然后将此结构的地址填到net_device结构的netdev_ops指针中即可。
对于R8169驱动,这个net_device_ops结构就是rtl8169_netdev_ops。
打开网卡rtl8169_open
当用户执行ifconfig eth0 up命令启动一个网卡时,网卡对应的net_device的netdev_ops->ndo_open函数被调用(调用链:sys_ioctl->do_vfs_ioctl->vfs_ioctl->sock_ioctl->dev_ioctl->dev_ifsioc ->dev_change_flags->dev_open),对于R8169驱动来说就是rtl8169_open函数。
这里为理解收包过程,列出rtl8169_open中的部分操作:
1) 申请一个struct RxDesc类型的数组空间,地址保存到rtl8169_private结构的RxDescArray成员中。
struct RxDesc结构用于描述一个buffer,主要是包含一个buffer的物理地址与长度。
rtl8169_private结构的RxDescArray成员就存放了RxDesc数组的起始物理地址。接下来,代码会预先申请一些buffer(rtl8169_rx_fill函数中实现,最终是调用__alloc_skb分配的buffer。Tcp发送数据时,最终也是调用__alloc_skb分配buffer的,可以参考tcp_sendmsg函数),然后将这些buffer的物理地址及长度记录到RxDesc数组中,以供硬件收包使用。
2) 申请一个struct sk_buff *类型的数组空间,地址保存到rtl8169_private.Rx_skbuff成员中。
上面提到的预先申请的那么buffer,其内核态虚拟地址均记录到此数组中。这样的话,硬件将报文输出到buffer中后,驱动能够获取到相应的buffer地址,将报文传入内核协议栈。从代码来看,buffer存放一个报文。
3) 注册中断处理函数rtl8169_interrupt
当网卡收到报文时,内核的框架代码最终会调用到这里注册的中断处理函数。
4) enable网卡的napi
5) 启动网卡
这里涉及诸多硬件操作,我们的主要关注点是,1)中提到的物理地址通过rtl_set_rx_tx_desc_registers函数(调用链rtl8169_open->rtl_hw_start->rtl_hw_start_8169->rtl_set_rx_tx_desc_registers)写给了硬件。
这样一来,这就等于通过RxDesc数组,等于向硬件提供了一组buffer的信息。从而让硬件将收到的报文输出到这些buffer中
中断处理
当中断发生时,硬件已经将报文输出到了前面所说的预先申请的buffer中了。此时,系统的中断处理机制最终会调用rtl8169_interrupt进行中断处理。
这里为理解收包过程,列出rtl8169_interrupt所做的部分操作:
l 处理中断硬件层面相关工作
l 调用__napi_schedule将网卡的rtl8169_private.napi结构挂入__get_cpu_var(softnet_data).poll_list链表。
l 调用__raise_softirq_irqoff(NET_RX_SOFTIRQ);让软中断处理线程ksoftirqd被调度执行,此线程将负责完成报文的接收。
软中断处理线程ksoftirqd
前面说了,网卡中断发生后,会触发软中断处理线程ksoftirqd被调度执行,而此线程将会负责完成报文的接收。那么此线程是个什么东东呢?这里先简单介绍一下。
每个核上,都会创建一个ksoftirqd线程,专门负责处理软中断。
如果没有配置CONFIG_PREEMPT_SOFTIRQS,则ksoftirqd 线程是在cpu_callback中通过如下代码创建的。可见这种情况下,线程的处理函数就是ksoftirqd。
kthread_create(ksoftirqd, hcpu,"ksoftirqd/%d", hotcpu);
通过如下命令,可以查看当前机器上ksoftirqd线程的创建情况。
[root@A22770684 VMB]# ps -ef | grep irq
root 4 2 0 May18 ? 00:00:00 [ksoftirqd/0]
root 9 2 0 May18 ? 00:00:00 [ksoftirqd/1]
ksoftirqd软中断处理线程并不是专门负责网卡设备的软中断处理,他还负责其他各种设备的软中断处理。
内核的各个子系统,通过open_softirq注册相应的软中断处理条目。
下面是网络系统与块设备系统注册软中断处理条目的代码。
open_softirq(BLOCK_IOPOLL_SOFTIRQ,blk_iopoll_softirq);
open_softirq(BLOCK_SOFTIRQ,blk_done_softirq);
open_softirq(NET_TX_SOFTIRQ,net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
open_softirq的代码如下。由此可见,每个条目,其实就是一个软中断处理函数。那么网卡收包软中断就对应net_rx_action函数了。
void open_softirq(int nr, void(*action)(struct softirq_action *))
{
softirq_vec[nr].action= action;
}
Ksoftirqd最终调用__do_softirq中完成各种软中断任务的处理。
__do_softirq 遍历softirq_vec数组,执行每个条目的action。
对网卡收包来说,action就是net_rx_action函数了。
网卡收包
前面说了,网卡中断发生后,会触发软中断处理线程ksoftirqd被调度执行,而此线程将会负责完成报文的接收。具体如何接收呢,从前面的介绍可以知道,对ksoftirqd来说,其实就是调用net_rx_action函数而已。
下面看看net_rx_action函数的工作:
前面说过,中断来了,网卡驱动将自己的napi结构挂到了__get_cpu_var(softnet_data).poll_list链表中。那么net_rx_action的核心工作,就是从链表中一一取出其中的napi结构,执行napi结构中的poll成员所指向的函数。为什么是一一取区呢?因为可能不止一块网卡在发生了中断后,将自己的napi结构挂进了链表。
对于R8169驱动来说,其napi结构poll成员指向的函数就是rtl8169_poll。这是在rtl8169_init_one中设置好的。实际上,rtl8169_poll中既做报文接收工作又做报文发送完成后的善后工作。从代码来看,rtl8169_start_xmit负责发送工作,代码中将要发送的报文的buffer信息填入rtl8169_private.TxDescArray数组中,然后写寄存器(RTL_W8(TxPoll, NPQ);)通知硬件发包。硬件完成发送后,同样会上报中断。然后rtl8169_poll调用rtl8169_tx_interrupt对rtl8169_private.TxDescArray中的buffer描述信息置空,以供未来新的报文发送使用。因此,不管是收,还是发,最终都是产生中断,然后由rtl8169_interrupt中断处理将流程转入软中断处理线程ksoftirqd,再由软中断进入rtl8169_poll函数处理。
这里,我们只看接收相关的代码。很明显,接收工作是由rtl8169_rx_interrupt函数完成的。
我们这里不看硬件相关的代码,只分析纯粹的收包相关的代码。
前面提到,为了收包,预先申请了一批buffer。这些buffer的信息,存在了如下两个数组中。
第一个是给硬件看的,第二个是给驱动看的。
rtl8169_private.RxDescArray
rtl8169_private.Rx_skbuff
rtl8169_private.Rx_skbuff就是一个环型数组,每个元素就是一个指向struct sk_buff结构的指针。rtl8169_rx_interrupt遍历此数组,取出其中的一个个报文,调用协议栈报文接收函数netif_receive_skb即可。
从代码实现来看,驱动总是先尝试重新申请一个sk_buff,将硬件接收buffer中的报文拷出来。但是,如果拷贝失败,那就不拷了,直接将硬件接收buffer中的报文转入协议栈接收流程。代码这样做,可能是不想重新申请buffer给硬件接收使用。
当拷贝失败时,由于代码直接将硬件接收buffer中的报文转入协议栈接收流程。这样的话,这个buffer就不能再继续用作硬件接收buffer了。因此对于这种情况,代码就将rtl8169_private.Rx_skbuff[idx]置成NULL。这样的话,可用的硬件接收buffer就变少了。为了应对这种情况,rtl8169_rx_interrupt函数尾部会调用rtl8169_rx_fill尝试重新将接收buffer补满。
内核协议栈对报文的接收
前面看到,网卡驱动调用netif_receive_skb,将处理流程转入内核协议栈。
netif_receive_skb先跳过一些简单的和不用关心的代码,从下面的地方开始看。
可见,如果接收端口是一个bond的成员口,则skb中的接收端口skb->dev需要换成接口端口的master,即bond口。但也未必总是会换,因为有时候成员口还未起来,但是收到一些杂包,这时候这些杂包不属于bond口的流量,因此不换。
null_or_orig= NULL;
orig_dev= skb->dev;
if(orig_dev->master) {
if(skb_bond_should_drop(skb))
null_or_orig = orig_dev; /*deliver only exact match */
else
skb->dev= orig_dev->master;
}
接下来,先通过如下代码将报文送达可能存在的raw socket(PF_PACKET协议族)。
list_for_each_entry_rcu(ptype,&ptype_all, list) {
if(ptype->dev == null_or_orig || ptype->dev == skb->dev ||
ptype->dev == orig_dev) {
if(pt_prev)
ret= deliver_skb(skb, pt_prev, orig_dev);
pt_prev= ptype;
}
}
这些报文接收条目是通过dev_add_pack注册的。
接下来,将报文传递给bridge处理。如果这里一步返回了0,报文就不往下走了。
skb= handle_bridge(skb, &pt_prev, &ret, orig_dev);
if(!skb)
gotoout;
否则,通过如下代码,将报文传达给各个协议处理。
type= skb->protocol;
list_for_each_entry_rcu(ptype,
&ptype_base[ntohs(type)& PTYPE_HASH_MASK], list) {
if(ptype->type == type &&
(ptype->dev == null_or_orig ||ptype->dev == skb->dev ||
ptype->dev == orig_dev)) {
if(pt_prev)
ret= deliver_skb(skb, pt_prev, orig_dev);
pt_prev= ptype;
}
}
这里的各个接收条目,也是通过dev_add_pack注册的。看看其代码,报文接收条目有两种,一种是全接收,一种是单收。
void dev_add_pack(struct packet_type *pt)
{
inthash;
spin_lock_bh(&ptype_lock);
if(pt->type == htons(ETH_P_ALL))
list_add_rcu(&pt->list,&ptype_all);
else{
hash= ntohs(pt->type) & PTYPE_HASH_MASK;
list_add_rcu(&pt->list,&ptype_base[hash]);
}
spin_unlock_bh(&ptype_lock);
}
来看看IP协议的接收条目的定义:
static struct packet_type ip_packet_type__read_mostly = {
.type= cpu_to_be16(ETH_P_IP),
.func= ip_rcv,
.gso_send_check= inet_gso_send_check,
.gso_segment= inet_gso_segment,
.gro_receive= inet_gro_receive,
.gro_complete= inet_gro_complete,
};
顺便也看看arp协议的接收条目定义(arp的学习就是通过arp_rcv完成的, arp的查找则是通过neigh_lookup接口):
static struct packet_type arp_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_ARP),
.func = arp_rcv,
};
从ip_packet_type可知,IP报文接收的入口是ip_rcv
如果是本机接收,主处理调用链如下:
ip_rcv->ip_rcv_finish-> dst_input->skb_dst(skb)->input (即ip_local_deliver)
ip_local_deliver主要是根据协议,选择一个协议来处理。
hash = protocol & (MAX_INET_PROTOS -1);
ipprot =rcu_dereference(inet_protos[hash]);
ipprot->handler(skb);
这些协议是通过inet_add_protocol注册的。例如,UDP协议的注册通过如下代码。
inet_add_protocol(&udp_protocol,IPPROTO_UDP)
udp_protocol的定义如下:
static const struct net_protocoludp_protocol = {
.handler= udp_rcv,
.err_handler= udp_err,
.gso_send_check= udp4_ufo_send_check,
.gso_segment= udp4_ufo_fragment,
.no_policy= 1,
.netns_ok= 1,
};
可见UDP的接收函数是udp_rcv
如果是一般的UDP,接收过程如下:
sock_queue_rcv_skb
调用udp_rcv ->__udp4_lib_rcv->udp_queue_rcv_skb ->__udp_queue_rcv_skb ->sock_queue_rcv_skb ->sk->sk_data_ready(即sock_def_readable)
最后一个函数sock_def_readable用于唤醒因读取socket进入睡眠的线程。
---------------------
作者:孙明保
来源:CSDN
原文:https://blog.csdn.net/crazycoder8848/article/details/46333761
版权声明:本文为博主原创文章,转载请附上博文链接!
标签:rtl8169,收包,报文,Linux,dev,网卡,skb,net 来源: https://www.cnblogs.com/sky-heaven/p/10438967.html