本文通过学习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先跳过一些简单的和不用关心的代码,从以下的地方開始看。

可见,假设接收port是一个bond的成员口,则skb中的接收portskb->dev须要换成接口port的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进入睡眠的线程。

转载于:https://www.cnblogs.com/jzdwajue/p/7294074.html

代码学习-Linux内核网卡收包过程(NAPI)相关推荐

  1. linux内核网络收包过程—硬中断与软中断

    目录 硬中断处理 软中断处理 数据通过网络发送过来 硬中断处理 数据帧首先到达网卡的接收队列,分配RingBuffer DMA把数据搬运到网卡关联的内存 网卡向CPU发起硬中断,通知CPU有数据 调用 ...

  2. 如何快速优化 Linux 内核 UDP 收包效率? | CSDN 博文精选

    作者 | dog250 责编 | 郭芮 出品 | CSDN 博客 现在很多人都在诟病Linux内核协议栈收包效率低,不管他们是真的懂还是一点都不懂只是听别人说的,反正就是在一味地怼Linux内核协议栈 ...

  3. Linux内核UDP收包为什么效率低?能做什么优化?

    现在很多人都在诟病Linux内核协议栈收包效率低,不管他们是真的懂还是一点都不懂只是听别人说的,反正就是在一味地怼Linux内核协议栈,他们的武器貌似只有DPDK. 但是,即便Linux内核协议栈收包 ...

  4. 网卡收包流程分析(一)

    由于本人工作内容主要集中于kernel的网络子系统,刚接触这个模块,于是想梳理一下网卡驱动的收包过程,以下内容为个人理解,如有不对,希望大家能够多多指正,相互成长~ 后续会持续更新有关kernel网络 ...

  5. linux网络收包过程

    记录一下linux数据包从网卡进入协议栈的过程,不涉及驱动,不涉及其他层的协议处理. 内核是如何知道网卡收到数据的,这就涉及到网卡和内核的交互方式: 轮询(poll):内核周期性的检查网卡,查看是否收 ...

  6. Linux网络协议栈:网卡收包分析

    Table of Contents 网卡收包 一,框架 二,初始化 三,驱动收包 四,内核处理 参考文章 推荐阅读 网卡收包 内核网络模块如何初始化? 内核如何通过网卡驱动收发数据包? 驱动收到的数据 ...

  7. Linux内核网络数据包发送(一)

    Linux内核网络数据包发送(一) 1. 前言 2. 数据包发送宏观视角 3. 协议层注册 4. 通过 socket 发送网络数据 4.1 `sock_sendmsg`, `__sock_sendms ...

  8. Linux内核网络数据包处理流程

    Linux内核网络数据包处理流程 from kernel-4.9: 0. Linux内核网络数据包处理流程 - 网络硬件 网卡工作在物理层和数据链路层,主要由PHY/MAC芯片.Tx/Rx FIFO. ...

  9. DPDK 网卡收包流程

    Table of Contents 1.Linux网络收发包流程 1.1 网卡与liuux驱动交互 1.2  linux驱动与内核协议栈交互 题外1: 中断处理逻辑 题外2:中断的弊端 2.linux ...

最新文章

  1. IE这回在css flex中扳回一局?
  2. leetcode_two sum()
  3. 为什么 SAP 电商云 Spartacus UI SSR 模式下的客户端应用,不会发起 product 请求
  4. 【原创】开源Math.NET基础数学类库使用(05)C#解析Delimited Formats数据格式
  5. TCP/IP和HTTP的不同之处
  6. 计算机六年级基础知识,六年级计算机试题
  7. 微服务架构的服务与发现-Spring Cloud
  8. 鸿蒙开发之拨打电话号码
  9. 变频器的技术应用:接线与参数设置
  10. python matrix用法_numpy中matrix使用方法
  11. linux基础——信号阻塞及未决信号
  12. iMac恢复出厂设置及安装
  13. 大数据新时代依然需要古老的磁带存储技术
  14. 我对说话人识别/声纹识别的研究综述
  15. H - Unloaded Die
  16. Linux 实现ssh免密登录--设置后不生效的处理办法
  17. kali2020之快速搜索文件工具——安装篇
  18. Linux内存 匿名页,学点linux之四:内存
  19. 详解笔记本屏幕的那点事儿
  20. 神舟服务器安装系统,神舟UT47笔记本一键u盘装系统win10教程

热门文章

  1. mpls工作原理通俗解释_马自达3 压燃上市的关头,解释X发动机的工作原理
  2. python主流编程语言_目前主流的编程语言有哪些?
  3. android 多线程 加锁,android 多线程 — 从一个小例子再次品位多线程
  4. 数据结构堆栈 内存堆栈_了解堆栈数据结构
  5. wordpress创建_如何创建WordPress儿童主题
  6. rstudio创建矩阵_R中的矩阵
  7. linux中awk命令_Linux / Unix中的AWK命令
  8. 实现视图示例_AngularJS控制器,范围和视图教程示例
  9. 如何在Ubuntu 18.04上安装Elasticsearch Logstash Kibana(Elastic Stack)
  10. C#使用SetWindowsHookEx时报错“类型的已垃圾回收委托进行了回调”