精华内容
下载资源
问答
  • linux 内核网络协议栈

    千次阅读 2015-02-22 14:58:06
    Linux网络协议栈之数据包处理过程  1前言 本来是想翻译《The journey of a packet through the linux 2.4 network stack》这篇文章的。但在查阅相关的资料时,发现需要补充一些技术细节,才使得我这种菜鸟理解...

    Linux网络协议栈之数据包处理过程 


    1前言

    本来是想翻译The journey of a packet through the linux 2.4 network stack这篇文章的。但在查阅相关的资料时,发现需要补充一些技术细节,才使得我这种菜鸟理解更加深刻,所以综合了上面两篇文档,在加上自己的裁减和罗嗦,就有了下面的文字。我不知道这是否侵犯了作者权益。如果有的话,请告知,我会及时删除这篇拼凑起来的文档。

    引用作者Harald Welte的话:我毫无疑问不是内核导师级人物,也许此文档的信息是错误的。所以不要对此期望太高了,我也感激你们的批评和指正。

    这篇文档是基于x86体系结构和转发IP分组的。

    1

    数据包在Linux内核链路层路径

    数据包在Linux内核链路层路径

    2 接收分组

    2.1 接收中断

    如果网卡收到一个和自己MAC地址匹配或链路层广播的以太网帧,它就会产生一个中断。此网卡的驱动程序会处理此中断:

    • 从DMA/PIO或其他得到分组数据,写到内存里去;
    •   接着,会分配一个新的套接字缓冲区skb,并调用与协议无关的、网络设备均支持的通用网络接收处理函数netif_rx(skb)。netif_rx()函数让内核准备进一步处理skb。
    • 然后,skb会进入到达队列以便CPU处理(对于多核CPU而言,每个CPU维护一个队列)。如果FIFO队列已满,就会丢弃此分组。在skb排队后,调用__cpu_raise_softirq()标记NET_RX_SOFTIRQ软中断,等待CPU执行。
    •   至此,netif_rx()函数调用结束,返回调用者状况信息(成功还是失败等)。此时,中断上下文进程完成任务,数据分组继续被上层协议栈处理。

    2.2 softirq 和 bottom half

    内核2.4以后,整个协议栈不再使用bottom half(下半文,没找到好的翻译),而是被软中断softirq取代。软中断softirq优势明显,可以同时在多个CPU上执行;而bottom half一次只能在一个CPU上执行,即在多个CPU执行时严格保持串行。

    中断服务程序往往都是在CPU关中断的条件下执行的,以避免中断嵌套而使控制复杂化。但是CPU关中断的时间不能太长,否则容易丢失中断信号。为此,Linux将中断服务程序一分为二,各称作“Top Half”和“Bottom Half”。前者通常对时间要求较为严格,必须在中断请求发生后立即或至少在一定的时间限制内完成。因此为了保证这种处理能原子地完成,Top Half通常是在CPU关中断的条件下执行的。具体地说,Top Half的范围包括:从在IDT中登记的中断入口函数一直到驱动程序注册在中断服务队列中的ISR。而Bottom Half则是Top Half根据需要来调度执行的,这些操作允许延迟到稍后执行,它的时间要求并不严格,因此它通常是在CPU开中断的条件下执行的,比如网络底层操作就是这样,由于某些原因,中断并没有立刻响应,而是先记录下来,等到可以处理这些中断的时候就一块处理了。但是,Linux的这种Bottom Half(以下简称BH)机制有两个缺点,也即:(1)在任意一时刻,系统只能有一个CPU可以执行Bottom Half代码,以防止两个或多个CPU同时来执行Bottom Half函数而相互干扰。因此BH代码的执行是严格“串行化”的。(2)BH函数不允许嵌套。这两个缺点在单CPU系统中是无关紧要的,但在SMP系统中却是非常致命的。因为BH机制的严格串行化执行显然没有充分利用SMP系统的多CPU特点。为此,Linux2.4内核在BH机制的基础上进行了扩展,这就是所谓的“软中断请求”(softirq)机制。Linux的softirq机制是与SMP紧密不可分的。为此,整个softirq机制的设计与实现中自始自终都贯彻了一个思想:“谁触发,谁执行”(Who marks,Who runs),也即触发软中断的那个CPU负责执行它所触发的软中断,而且每个CPU都由它自己的软中断触发与控制机制。这个设计思想也使得softirq 机制充分利用了SMP系统的性能和特点。

    2.3 NET_RX_SOFTIRQ 网络接收软中断

    这两篇文档描述的各不相同,侧重不一。在这里,只好取重避轻。

    这一阶段会根据协议的不同来处理数据分组。CPU开始处理软中断do_softirq(),,接着net_rx_action()处理前面标记的NET_RX_SOFTIRQ,把出对列的skb送入相应列表处理(根据协议不同到不同的列表)。比如,IP分组交给ip_rcv()处理,ARP分组交给arp_rcv()处理等。

    基于网络设备处理协议类型

    2.4 处理IPv4分组

    下面以IPv4为例,讲解IPv4分组在高层的处理。

    linux内核协议栈之网络层

    linux内核协议栈之网络层

    linux内核协议栈之网络层2

    以上两个图都是一个意思,可以对比着看。

    ip_rcv()函数验证IP分组,比如目的地址是否本机地址,校验和是否正确等。若正确,则交给netfilter的NF_IP_PRE_ROUTING钩子(关于netfilter细节可以参考Hacking the Linux Kernel Network Stack);否则,丢弃。

    到了ip_rcv_finish()函数,数据包就要根据skb结构的目的或路由信息各奔东西了。

    • 判断数据包的去向,ip_local_deliver() 处理到本机的数据分组、ip_forward() 处理需要转发的数据分组、ip_mr_input() 转发组播数据包。如果是转发的数据包,还需要找出出口设备和下一跳。

    • 分析和处理IP选项。(并不是处理所有的IP选项)。

    具体来说,从skb->nh(IP头,由netif_receive_skb初始化)结构得到IP地址:struct net_device *dev = skb->dev; struct iphdr *iph = skb->nh.iph;

    而skb->dst或许包含了数据分组到达目的地的路由信息,如果没有,则需要查找路由,如果最后结果显示目的地不可达,那么就丢弃该数据包:

    if (skb->dst == NULL) {

      if (ip_route_input(skb, iph->daddr, iph->saddr, iph->tos, dev))

         goto drop;

    }

    ip_rcv_finish()函数最后执行dst_input,决定数据包的下一步的处理。

    2.4.1 转发数据包

    转发数据包主要包括一下步骤:

    • l  处理IP头选项。如果需要的话,会记录本地IP地址和时间戳;
    • l  确认分组可以被转发;
    • l  将TTL减一,如果TTL为0,则丢弃分组;
    • l  根据MTU大小和路由信息,对数据分组进行分片,如果需要的话;
    • l  将数据分组送往外出设备。

    如果由于某种原因,数据分组不能被转发,那么就回应ICMP消息来说明不能转发的原因。在对转发的分组进行各种检查无误后,执行ip_forward_finish,准备发送。然后执行dst_output(skb)。无论是转发的分组,还是本地产生的分组,都要经过dst_output(skb)到达目的主机。IP头在此时已经完成就绪。dst_output(skb)函数要执行虚函数output(单播的话为ip_output,多播为ip_mc_output)。最后,ip_finish_output进入邻居子系统。

    下图是转发数据包的流程图:

    转发数据包

    2.4.1 本地处理

    int ip_local_deliver(struct sk_buff *skb)

    {

        if (skb->nh.iph->frag_off & htons(IP_MF|IP_OFFSET)) {

            skb = ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER);

            if (!skb)

                return 0;

        }

        return NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL,

                   ip_local_deliver_finish);

    }

    最后执行ip_local_deliver_finish。

    以下属ip_local_deliver_finish函数流程图:

    本地处理

    在L4协议中,TCP和UDP是运行在内核空间的,而RAW则可以运行在用户空间中。

    TCP处理见下图:

    TCP

    UDP处理略。

    数据分组的发送:

    ip_queue_xmit检查socket结构体中是否含有路由信息,如果没有则执行ip_route_output_flow查找,并存储到sk数据结构中。如果找不到,则丢弃数据包。

    至此,数据分组的接受和处理工作就告一段落了,至于于此相对的数据分组的发送,我就贴个图吧,具体细节可参考The Linux® Networking Architecture: Design and Implementation of Network Protocols in the Linux Kernel  Prentice Hall   August 01, 2004

    dev_queue_xmit()处理发送分组

    dev_queue_xmit()处理发送分组

    附一张Linux 2.4 核的netfilter框架下分组的走向图:

    netfilter

    来自链接:   
    http://ftp.gnumonks.org/pub/doc/packet-journey-2.4.html
    http://m.linuxjournal.com/article/4852

    这篇文档描述了网络分组在linux内核2.4协议栈的处理过程。

     

     

    内核的路由部分是是网络中重要部分,目前在Linux内核中默认的路由查找算法使用的是Hash查找,所以你会看到很多的数据结构是XXX_hash什么之类(例如fn_hash)。Linux内核从2.1开始就支持基于策略的路由,那么什么是基于策略的路由呢?我们一般的最基本的路由转发是考虑IP包的目的地址,但是有些时候不仅仅是这些,还有例如IP协议,传输端口等之类的考虑因素,所以采用所谓基于策略的路由。

          或许这样理解更好,Linux默认有三种策略路由:本地路由,主路由和默认路由,那么与之对应的就是三张路由表:本地路由表,主路由表和默认路由表。

          那么我们需要理解是什么呢?当然是路由怎么转的过程。在这之前,先看看所涉及数据结构有哪些。

          介绍下面之前我们首先需要知道内核常用的结构之间的操作手法。说道这里不得不先说一下内核的链表结构。

          内核的链表结构主要是用来表示连接关系的

    1. struct hlist_head { 
    2.          struct hlist_node *first; 
    3. }; 
    4.   
    5. struct hlist_node { 
    6.          struct hlist_node *next, **pprev;  // 看这个你就知道,内核链表一般是双向链表(其实还是循环链表) 
    7. }; 
    struct hlist_head {
             struct hlist_node *first;
     };
     
     struct hlist_node {
             struct hlist_node *next, **pprev;   // 看这个你就知道,内核链表一般是双向链表(其实还是循环链表)
     };


     

         那么下面的很多结构之间的链接都是通过这样的链表的!但是就算我通过一个结构找到另一个结构的链表字段的时候,怎么确定结构真正的首地址呢?其实我们都不用担心,内核采取container_of这个宏定义来处理的!

    1. #define container_of(ptr, type, member) ({                      \ 
    2.          const typeof( ((type *)0)->member ) *__mptr = (ptr);    \ 
    3.          (type *)( (char *)__mptr - offsetof(type,member) );}) 
    #define container_of(ptr, type, member) ({                      \
             const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
             (type *)( (char *)__mptr - offsetof(type,member) );})


    很简单,其实就是通过偏移来做的,很easy、

    1. struct fib_table { 
    2.          struct hlist_node tb_hlist;// hash节点(通过ipv4的hlist_head可以得到属于自己的路由信息表FIB,这个就是链接字段) 
    3.          u32             tb_id;     // 标识符(例如:本地路由,主路由,默认路由) 
    4.          unsigned        tb_stamp;  // 时间戳 
    5.          int             tb_default;// 路由信息结构队列序号 
    6.          int             (*tb_lookup)(struct fib_table *tb,conststruct flowi *flp,struct fib_result *res);// 查找函数 
    7.          int             (*tb_insert)(struct fib_table *, struct fib_config *);// 插入函数 
    8.          int             (*tb_delete)(struct fib_table *,struct fib_config *);// 删除路由函数 
    9.          int             (*tb_dump)(struct fib_table *table, struct sk_buff *skb,  
    10.                                       struct netlink_callback *cb);           // 用于路由转发 
    11.          int             (*tb_flush)(struct fib_table *table);                 // 移除路由信息结构 
    12.          void            (*tb_select_default)(struct fib_table *table,        // 设置默认路由 
    13.                                               const struct flowi *flp,struct fib_result *res); 
    14.   
    15.          unsigned char   tb_data[0];  // 注意这个特殊字段,标识结构的结尾,分配fib_table同时分配fn_hash结构 
    16. };                                    // 也就是fib_table之后就是fn_hash结构 
    struct fib_table {
             struct hlist_node tb_hlist;// hash节点(通过ipv4的hlist_head可以得到属于自己的路由信息表FIB,这个就是链接字段)
             u32             tb_id;     // 标识符(例如:本地路由,主路由,默认路由)
             unsigned        tb_stamp;  // 时间戳
             int             tb_default;// 路由信息结构队列序号
             int             (*tb_lookup)(struct fib_table *tb, const struct flowi *flp, struct fib_result *res);// 查找函数
             int             (*tb_insert)(struct fib_table *, struct fib_config *);// 插入函数
             int             (*tb_delete)(struct fib_table *, struct fib_config *);// 删除路由函数
             int             (*tb_dump)(struct fib_table *table, struct sk_buff *skb, 
                                          struct netlink_callback *cb);            // 用于路由转发
             int             (*tb_flush)(struct fib_table *table);                 // 移除路由信息结构
             void            (*tb_select_default)(struct fib_table *table,         // 设置默认路由
                                                  const struct flowi *flp, struct fib_result *res);
     
             unsigned char   tb_data[0];   // 注意这个特殊字段,标识结构的结尾,分配fib_table同时分配fn_hash结构
     };                                    // 也就是fib_table之后就是fn_hash结构
    


    // 先介绍一下“路由区”定义:fn_zone,举个例子,子网掩码长度相同的认为是相同的路由区(ok)

    1. struct fn_hash {    // 路由区结构体的数组( 包含所有的额路由区的情况 ) 
    2.          struct fn_zone  *fn_zones[33];// 路由区分成33份,why?仔细想想,子网掩码长度是1~32,0长度掩码代表网关,那么加起来就是33,即:fn_zone[0]的掩码是0.0.0.0,fn_zone[1]是10000000.00000000.00000000.0000000这一类  等等 
    3.          struct fn_zone  *fn_zone_list;// 指向第一个活动的路由区 
    4. }; 
    struct fn_hash {     // 路由区结构体的数组( 包含所有的额路由区的情况 )
             struct fn_zone  *fn_zones[33];// 路由区分成33份,why?仔细想想,子网掩码长度是1~32,0长度掩码代表网关,那么加起来就是33,即:fn_zone[0]的掩码是0.0.0.0,fn_zone[1]是10000000.00000000.00000000.0000000这一类  等等
             struct fn_zone  *fn_zone_list;// 指向第一个活动的路由区
     };


    1. struct fn_zone {     // 路由区结构体(所有的子网长度相等的被分在同一个路由区) 
    2.          struct fn_zone          *fz_next;      // 指向下一个不为空的路由区结构,那么所有的路由区就能链接起来 
    3.          struct hlist_head       *fz_hash;      // 有一个hash数组,<span style="font-family: Arial, Helvetica, sans-serif;">用来hash得到一个hlist_head,是很多的fib_node通过自己的字段连接在这个队列中,那么通过这个fz_hahs字段可以找到fib_node所在的队列的头hlist_head,进而找到对应的fib_node ( 注意:上面说的hash数组的长度是fz_divisor长度)</span> 
    4.          int                     fz_nent;       // 包含的路由节总数 
    5.          int                     fz_divisor;    // hash头数量(上面说了) 
    6.          u32                     fz_hashmask;    // 确定hash头的掩码 
    7. #define FZ_HASHMASK(fz)         ((fz)->fz_hashmask) 
    8.   
    9.          int                     fz_order;      // 子网掩码位数 
    10.          __be32                  fz_mask;        // 子网掩码 
    11. #define FZ_MASK(fz)             ((fz)->fz_mask)  // 获取子网掩码的宏定义 
    12. }; 
    struct fn_zone {      // 路由区结构体(所有的子网长度相等的被分在同一个路由区)
             struct fn_zone          *fz_next;       // 指向下一个不为空的路由区结构,那么所有的路由区就能链接起来
             struct hlist_head       *fz_hash;       // 有一个hash数组,<span style="font-family:Arial, Helvetica, sans-serif;">用来hash得到一个hlist_head,是很多的fib_node通过自己的字段连接在这个队列中,那么通过这个fz_hahs字段可以找到fib_node所在的队列的头hlist_head,进而找到对应的fib_node ( 注意:上面说的hash数组的长度是fz_divisor长度)</span>
             int                     fz_nent;        // 包含的路由节总数
             int                     fz_divisor;     // hash头数量(上面说了)
             u32                     fz_hashmask;    // 确定hash头的掩码
     #define FZ_HASHMASK(fz)         ((fz)->fz_hashmask)
     
             int                     fz_order;       // 子网掩码位数
             __be32                  fz_mask;        // 子网掩码
    #define FZ_MASK(fz)             ((fz)->fz_mask)  // 获取子网掩码的宏定义
    };


    1. struct fib_node {   // 路由节点结构体( 子网相等的路由被分在一起 ) 
    2.          struct hlist_node       fn_hash;// 链接到hash表节点( 注意到我们上面所说的fn_zone中的fz_hash了吗?fz_hash哈希之后得到的结果就是fib_node的这个字段,所以这个字段同样仅仅是作为链接作用而已 ) 
    3.          struct list_head        fn_alias;// 别名?其实更好的理解是这样的:虽然现在所有的路由都是同一个子网了,但是路由之间还会有其他的信息不同例如tos,路由类型,等等。所以依然存在不同的路由,所以这些都是通过fn_alias来区分。 
    4.          __be32                  fn_key;  // 路由别名队列:即这个node下面所有的具体路由(不同的fn_alias的)都在这个队列中 
    5.          struct fib_alias        fn_embedded_alias;// 分配路由节点的时候同时也分配一个路由别名,所以称为嵌入式的~ 
    6. }; 
    struct fib_node {    // 路由节点结构体( 子网相等的路由被分在一起 )
             struct hlist_node       fn_hash; // 链接到hash表节点( 注意到我们上面所说的fn_zone中的fz_hash了吗?fz_hash哈希之后得到的结果就是fib_node的这个字段,所以这个字段同样仅仅是作为链接作用而已 )
             struct list_head        fn_alias;// 别名?其实更好的理解是这样的:虽然现在所有的路由都是同一个子网了,但是路由之间还会有其他的信息不同例如tos,路由类型,等等。所以依然存在不同的路由,所以这些都是通过fn_alias来区分。
             __be32                  fn_key;  // 路由别名队列:即这个node下面所有的具体路由(不同的fn_alias的)都在这个队列中
             struct fib_alias        fn_embedded_alias; // 分配路由节点的时候同时也分配一个路由别名,所以称为嵌入式的~
     };


    1. struct fib_alias {     // 路由别名结构,这个结构基本就是最后一次路由筛选了 
    2.          struct list_head        fa_list; // 这个是用于链接到fib_node节点中的,看上面的结构体的第二个字段的类型你就懂了~~~~~~ 
    3.          struct fib_info         *fa_info;// 这是很重要的字段:顾名思义,就是具体怎么处置这个数据包的操作等 
    4.          u8                      fa_tos;   // 服务类型TOS 
    5.          u8                      fa_type;  // 路由类型 
    6.          u8                      fa_scope; // 路由范围 
    7.          u8                      fa_state; // 路由状态 
    8. #ifdef CONFIG_IP_FIB_TRIE 
    9.          struct rcu_head         rcu; 
    10. #endif 
    11. }; 
    struct fib_alias {      // 路由别名结构,这个结构基本就是最后一次路由筛选了
             struct list_head        fa_list;  // 这个是用于链接到fib_node节点中的,看上面的结构体的第二个字段的类型你就懂了~~~~~~
             struct fib_info         *fa_info; // 这是很重要的字段:顾名思义,就是具体怎么处置这个数据包的操作等
             u8                      fa_tos;   // 服务类型TOS
             u8                      fa_type;  // 路由类型
             u8                      fa_scope; // 路由范围
             u8                      fa_state; // 路由状态
     #ifdef CONFIG_IP_FIB_TRIE
             struct rcu_head         rcu;
     #endif
     };



     

    1. struct fib_info {     // 具体怎么路由这个数据包的信息 
    2.          struct hlist_node       fib_hash;     // 链接到fib_info_hash队列 
    3.          struct hlist_node       fib_lhash;    // 链接到fib_hash_laddrhash队列 
    4.          struct net              *fib_net;     // 所属网络空间 
    5.          int                     fib_treeref;  // 路由信息结构使用计数器 
    6.          atomic_t                fib_clntref;   // 释放路由信息结构(fib)计数器 
    7.          int                     fib_dead;     // 标志路由被删除了 
    8.          unsigned                fib_flags;     // 标识位 
    9.          int                     fib_protocol; // 安装路由协议 
    10.          __be32                  fib_prefsrc;   // 指定源IP,源地址和目的地址组成一个路由 
    11.          u32                     fib_priority;  // 路由优先级 
    12.          u32                     fib_metrics[RTAX_MAX]; // 保存负载值(例如MTU,MSS) 
    13. #define fib_mtu fib_metrics[RTAX_MTU-1]        // MTU值 
    14. #define fib_window fib_metrics[RTAX_WINDOW-1]  // 窗口值 
    15. #define fib_rtt fib_metrics[RTAX_RTT-1]        // RTT值 
    16. #define fib_advmss fib_metrics[RTAX_ADVMSS-1]  // MSS值(对外公开的) 
    17.          int                     fib_nhs;      // 倒数第二个字段即:跳转结构的数组个数 
    18. #ifdef CONFIG_IP_ROUTE_MULTIPATH 
    19.          int                     fib_power;    // 支持多路径时候使用 
    20. #endif 
    21.          struct fib_nh           fib_nh[0];    // 跳转结构(就是该怎么路由) 
    22. #define fib_dev         fib_nh[0].nh_dev       
    23. }; 
    struct fib_info {      // 具体怎么路由这个数据包的信息
             struct hlist_node       fib_hash;      // 链接到fib_info_hash队列
             struct hlist_node       fib_lhash;     // 链接到fib_hash_laddrhash队列
             struct net              *fib_net;      // 所属网络空间
             int                     fib_treeref;   // 路由信息结构使用计数器
             atomic_t                fib_clntref;   // 释放路由信息结构(fib)计数器
             int                     fib_dead;      // 标志路由被删除了
             unsigned                fib_flags;     // 标识位
             int                     fib_protocol;  // 安装路由协议
             __be32                  fib_prefsrc;   // 指定源IP,源地址和目的地址组成一个路由
             u32                     fib_priority;  // 路由优先级
             u32                     fib_metrics[RTAX_MAX]; // 保存负载值(例如MTU,MSS)
     #define fib_mtu fib_metrics[RTAX_MTU-1]        // MTU值
     #define fib_window fib_metrics[RTAX_WINDOW-1]  // 窗口值
     #define fib_rtt fib_metrics[RTAX_RTT-1]        // RTT值
     #define fib_advmss fib_metrics[RTAX_ADVMSS-1]  // MSS值(对外公开的)
             int                     fib_nhs;       // 倒数第二个字段即:跳转结构的数组个数
     #ifdef CONFIG_IP_ROUTE_MULTIPATH
             int                     fib_power;     // 支持多路径时候使用
     #endif
             struct fib_nh           fib_nh[0];     // 跳转结构(就是该怎么路由)
     #define fib_dev         fib_nh[0].nh_dev       
     };


    对于上面的fib_nh[0],这样的操作手法在内核中也是常见的。代表会有这个字段的存在,但是具体是几个并不知道,因为可能是动态的,所以需要一个计数表示,也就是fib_power


    OK,主要的数据结构已经介绍,后面的结构会边说边介绍,下面我们根据路由转发的顺序来梳理一下思路:


    数据包的路由是通过函数ip_route_input来处理的,看这个函数:

    1. externint  ip_route_input(struct sk_buff*, __be32 dst, __be32 src, u8 tos,struct net_device *devin); 
    extern int  ip_route_input(struct sk_buff*, __be32 dst, __be32 src, u8 tos, struct net_device *devin);
    


    参数有5个:

          skb: IP包缓冲区,

          dst: IP包的目的地址,

          src: IP包源地址,

          tos: 服务类型,

          devin: 输入的网络设备。


    怎么运行的呢?首先这个函数需要查路由缓存(cache),如果找到了那么它给skb->dst赋值并返回,如是没找到,它会调用ip_route_input_slow去查询路由数据库。


    这里我们需要理解几个问题: 首先路由缓存到底是什么结构,怎么查找,这个我们马上就会说到。再次我们需要知道所谓路由就是最终找到这个路由条目,得到目的地址(吓一跳),然后赋值给skb->dst,然后通过skb->dst->input(skb)就可以进行操作。第三需要注意,这里的操作分成两类:第一类是投到本地,即数据是发到本机的,那么调用ip_local_deliver将数据包发送给上一层进行处理;第二类是转发,调用ip_forward函数进行处理,转发出去!最后注意:当路由缓冲找不到所需要的路由项,那么最终需要再次到fib中去查找,也就是完整的一个查找过程。


    下面具体看看路由缓存问题:

    首先是怎么建立这个缓存的呢?其实这个问题不需要特意来说,因为后面肯定会说到,为什么呢?缓存总是由不存在到存在的,当不存在的时候只能使用查询路由信息库来处理,但是同时需要注意:更新缓存cache、这个时候就是建立cache的时候。所以在后面说到的路由信息库查询和cache的建立是一样的,先不说这个,先直接看在cache中处理。

    cache的结构定义为:

    1. staticstruct rt_hash_bucket    *rt_hash_table; 
    static struct rt_hash_bucket    *rt_hash_table;


     

    rt_hash_table就是路由cache,它是rt_hash_bucket结构。

    1. struct rt_hash_bucket { 
    2.          struct rtable   *chain; 
    struct rt_hash_bucket {
             struct rtable   *chain;
    }

    注意chain是一个rtable结构,看下面:

    1. struct rtable 
    2.          union 
    3.          { 
    4.                  struct dst_entry        dst;  // 这是目的地址 
    5.          } u; 
    6.   
    7.          /* Cache lookup keys */ 
    8.          struct flowi            fl;           // 注意在cache中的查找主要是通过路由键值和下面的信息 
    9.   
    10.          struct in_device        *idev;        // 设备 
    11.           
    12.          int                     rt_genid;     // 路由id 
    13.          unsigned                rt_flags;      // 标识 
    14.          __u16                   rt_type;       // 路由类型 
    15.   
    16.          __be32                  rt_dst;        // 目的地址 
    17.          __be32                  rt_src;        // 源地址 
    18.          int                     rt_iif;       // 入端口 
    19.   
    20.          /* Info on neighbour */ 
    21.          __be32                  rt_gateway;    // 网关 
    22.   
    23.          /* Miscellaneous cached information */ 
    24.          __be32                  rt_spec_dst;  /* RFC1122 specific destination */ 
    25.          struct inet_peer        *peer;       /* long-living peer info */ 
    26. }; 
    struct rtable
     {
             union
             {
                     struct dst_entry        dst;   // 这是目的地址
             } u;
     
             /* Cache lookup keys */
             struct flowi            fl;            // 注意在cache中的查找主要是通过路由键值和下面的信息
     
             struct in_device        *idev;         // 设备
             
             int                     rt_genid;      // 路由id
             unsigned                rt_flags;      // 标识
             __u16                   rt_type;       // 路由类型
     
             __be32                  rt_dst;        // 目的地址
             __be32                  rt_src;        // 源地址
             int                     rt_iif;        // 入端口
     
             /* Info on neighbour */
             __be32                  rt_gateway;    // 网关
     
             /* Miscellaneous cached information */
             __be32                  rt_spec_dst;  /* RFC1122 specific destination */
             struct inet_peer        *peer;        /* long-living peer info */
     };


    我们看一下查询的一小段代码:

    1. 2048         for (rth = rcu_dereference(rt_hash_table[hash].chain); rth; 
    2. 2049              rth = rcu_dereference(rth->u.dst.rt_next)) { 
    3. 2050                 if (rth->fl.fl4_dst == daddr && 
    4. 2051                     rth->fl.fl4_src == saddr && 
    5. 2052                     rth->fl.iif == iif && 
    6. 2053                     rth->fl.oif == 0 && 
    7. 2054                     rth->fl.mark == skb->mark && 
    8. 2055                     rth->fl.fl4_tos == tos && 
    9. 2056                     rth->u.dst.dev->nd_net == net && 
    10. 2057                     rth->rt_genid == atomic_read(&rt_genid)) { 
    11. 2058                         dst_use(&rth->u.dst, jiffies); 
    12. 2059                         RT_CACHE_STAT_INC(in_hit); 
    13. 2060                         rcu_read_unlock(); 
    14. 2061                         skb->dst = (struct dst_entry*)rth; 
    15. 2062                         return 0; 
    16. 2063                 } 
    17. 2064                 RT_CACHE_STAT_INC(in_hlist_search); 
    18. 2065         } 
    2048         for (rth = rcu_dereference(rt_hash_table[hash].chain); rth;
    2049              rth = rcu_dereference(rth->u.dst.rt_next)) {
    2050                 if (rth->fl.fl4_dst == daddr &&
    2051                     rth->fl.fl4_src == saddr &&
    2052                     rth->fl.iif == iif &&
    2053                     rth->fl.oif == 0 &&
    2054                     rth->fl.mark == skb->mark &&
    2055                     rth->fl.fl4_tos == tos &&
    2056                     rth->u.dst.dev->nd_net == net &&
    2057                     rth->rt_genid == atomic_read(&rt_genid)) {
    2058                         dst_use(&rth->u.dst, jiffies);
    2059                         RT_CACHE_STAT_INC(in_hit);
    2060                         rcu_read_unlock();
    2061                         skb->dst = (struct dst_entry*)rth;
    2062                         return 0;
    2063                 }
    2064                 RT_CACHE_STAT_INC(in_hlist_search);
    2065         }


    所以很清晰的看到匹配的所有字段。下面看看我们构造一下在cache中查找的结构图:


    首先通过hash找到这个队列首部的chain,然后在chain的队列中进行匹配,如果匹配到那么OK,否则进行完整的查询。


    OK,假如现在在缓存cache中并没有找到,那么执行ip_route_input_slow函数进行完整查询。


    我们知道Linux最多可以支持255张路由表,默认有三张路由表,即本地路由表,主路由表和默认路由表,三个优先级递减(数字越大优先级越小),也就是查询顺序递减。我们先需要知道怎么样得到这三张路由表先。三张路由表就是三个规则,所以需要看看下面的路由信息结构规则结构体。

            表255: 本地路由表(local ) 本地接口地址,广播地址,已及NAT地址都放在这个表。该路由表由系统自动维护,管理员不能直接修改。
      表254: 主路由表(main ) 如果没有指明路由所属的表,所有的路由都默认都放在这个表里,一般来说,旧的路由工具(如route)所添加的路由都会加到这个表。一般是普通的路由。
      表253: 默认路由表 (default ) 一般来说默认的路由都放在这张表。
      表 0 :保留

    看一下它们是怎么被初始化的:

    1. staticint fib_default_rules_init(struct fib_rules_ops *ops) 
    2.          int err; 
    3.   
    4.          err = fib_default_rule_add(ops, 0, RT_TABLE_LOCAL, FIB_RULE_PERMANENT); // 本地路由规则(本地路由表) 
    5.          if (err < 0) 
    6.                  return err; 
    7.          err = fib_default_rule_add(ops, 0x7FFE, RT_TABLE_MAIN, 0);              // 主路由规则(主路由表) 
    8.          if (err < 0) 
    9.                  return err; 
    10.          err = fib_default_rule_add(ops, 0x7FFF, RT_TABLE_DEFAULT, 0);           // 默认路由规则(默认路由表) 
    11.          if (err < 0) 
    12.                  return err; 
    13.          return 0; 
    static int fib_default_rules_init(struct fib_rules_ops *ops)
     {
             int err;
     
             err = fib_default_rule_add(ops, 0, RT_TABLE_LOCAL, FIB_RULE_PERMANENT);  // 本地路由规则(本地路由表)
             if (err < 0)
                     return err;
             err = fib_default_rule_add(ops, 0x7FFE, RT_TABLE_MAIN, 0);               // 主路由规则(主路由表)
             if (err < 0)
                     return err;
             err = fib_default_rule_add(ops, 0x7FFF, RT_TABLE_DEFAULT, 0);            // 默认路由规则(默认路由表)
             if (err < 0)
                     return err;
             return 0;
     }


     

    1. // 本地规则local_rule 
    2. staticstruct fib_rule local_rule = { 
    3. r_next: &main_rule,            //下一条规则是主规则 
    4. r_clntref: ATOMIC_INIT(2), 
    5. r_table: RT_TABLE_LOCAL,       // 指向本地路由表 
    6. r_action: RTN_UNICAST,         // 动作是返回路由 
    7. }; 
    // 本地规则local_rule
    static struct fib_rule local_rule = {
    r_next: &main_rule,            //下一条规则是主规则
    r_clntref: ATOMIC_INIT(2),
    r_table: RT_TABLE_LOCAL,       // 指向本地路由表
    r_action: RTN_UNICAST,         // 动作是返回路由
    };
    1. // 主规则main_rule 
    2. staticstruct fib_rule main_rule = { 
    3. r_next: &default_rule,         // 下一条规则是默认规则 
    4. r_clntref: ATOMIC_INIT(2), 
    5. r_preference: 0x7FFE,          // 默认规则的优先级32766 
    6. r_table: RT_TABLE_MAIN,        // 指向主路由表 
    7. r_action: RTN_UNICAST,         // 动作是返回路由 
    8. }; 
    // 主规则main_rule
    static struct fib_rule main_rule = {
    r_next: &default_rule,         // 下一条规则是默认规则
    r_clntref: ATOMIC_INIT(2),
    r_preference: 0x7FFE,          // 默认规则的优先级32766
    r_table: RT_TABLE_MAIN,        // 指向主路由表 
    r_action: RTN_UNICAST,         // 动作是返回路由
    };
    1. // 默认规则default rule 
    2. staticstruct fib_rule default_rule = { 
    3. r_clntref: ATOMIC_INIT(2), 
    4. r_preference: 0x7FFF,          // 默认规则的优先级32767 
    5. r_table: RT_TABLE_DEFAULT,     // 指默认路由表 
    6. r_action: RTN_UNICAST,         // 动作是返回路由 
    7. }; 
    // 默认规则default rule
    static struct fib_rule default_rule = {
    r_clntref: ATOMIC_INIT(2),
    r_preference: 0x7FFF,          // 默认规则的优先级32767
    r_table: RT_TABLE_DEFAULT,     // 指默认路由表
    r_action: RTN_UNICAST,         // 动作是返回路由
    };

    注意:规则链的链头指向本地规则。


    下面我们需要看看这个结构体:

    1. struct fib_rule   // 规则结构体(在初始化的时候,会注册上面的三种规则,生成默认的三张表) 
    2.          struct list_head        list;             // 用来链入路由规则函数队列中(fib_rules_ops,下面介绍) 
    3.          atomic_t                refcnt;            // 计数器 
    4.          int                     ifindex;          // 网络设备id 
    5.          char                    ifname[IFNAMSIZ]; // 设备名称 
    6.          u32                     mark;              // 用于过滤作用 
    7.          u32                     mark_mask;         // 掩码 
    8.          u32                     pref;              // 优先级(例如上面代码中分别是0,0x7FEE,0x7FFF) 
    9.          u32                     flags;             // 标识位 
    10.          u32                     table;             // 路由函数表id(例如本地LOCAL,主路由MAIN...) 
    11.          u8                      action;            // 动作,即怎么去处理这个数据包 
    12.          u32                     target; 
    13.          struct fib_rule *       ctarget;          // 当前规则 
    14.          struct rcu_head         rcu; 
    15.          struct net *            fr_net;           // 网络空间结构指针 
    16. }; 
    struct fib_rule    // 规则结构体(在初始化的时候,会注册上面的三种规则,生成默认的三张表)
     {
             struct list_head        list;              // 用来链入路由规则函数队列中(fib_rules_ops,下面介绍)
             atomic_t                refcnt;            // 计数器
             int                     ifindex;           // 网络设备id
             char                    ifname[IFNAMSIZ];  // 设备名称
             u32                     mark;              // 用于过滤作用
             u32                     mark_mask;         // 掩码
             u32                     pref;              // 优先级(例如上面代码中分别是0,0x7FEE,0x7FFF)
             u32                     flags;             // 标识位
             u32                     table;             // 路由函数表id(例如本地LOCAL,主路由MAIN...)
             u8                      action;            // 动作,即怎么去处理这个数据包
             u32                     target;
             struct fib_rule *       ctarget;           // 当前规则
             struct rcu_head         rcu;
             struct net *            fr_net;            // 网络空间结构指针
     };


    同时看一下rule的规则函数:

    1. struct fib_rules_ops 
    2.         int                     family;             // 协议族ID 
    3.         struct list_head        list;               // 用于链接到网络空间队列中 
    4.         int                     rule_size;          // 规则结构大小 
    5.         int                     addr_size;          // 地址大小 
    6.         int                     unresolved_rules; 
    7.         int                     nr_goto_rules; 
    8.  
    9.         int                     (*action)(struct fib_rule *,        // 动作函数指针 
    10.                                           struct flowi *, int
    11.                                           struct fib_lookup_arg *);  
    12.         int                     (*match)(struct fib_rule *,          // 匹配函数指针 
    13.                                          struct flowi *,int); 
    14.         int                     (*configure)(struct fib_rule *,      // 配置函数指针 
    15.                                              struct sk_buff *, 
    16.                                              struct nlmsghdr *, 
    17.                                              struct fib_rule_hdr *, 
    18.                                              struct nlattr **); 
    19.         int                     (*compare)(struct fib_rule *,       // 对比函数指针 
    20.                                            struct fib_rule_hdr *, 
    21.                                            struct nlattr **); 
    22.         int                     (*fill)(struct fib_rule *, struct sk_buff *, 
    23.                                         struct nlmsghdr *,          // 填写函数指针 
    24.                                         struct fib_rule_hdr *); 
    25.         u32                     (*default_pref)(struct fib_rules_ops *ops); // 查找优先级函数指针 
    26.         size_t                  (*nlmsg_payload)(struct fib_rule *); // 统计负载数据能力函数指针 
    27.  
    28.         /* Called after modifications to the rules set, must flush
    29.          * the route cache if one exists. */ 
    30.         void                    (*flush_cache)(void);    // 修改规则之后刷新缓存函数指针 
    31.  
    32.         int                     nlgroup;     // 路由netlink组划分标识 
    33.         conststruct nla_policy *policy;     // netlink属性优先级 
    34.         struct list_head        rules_list;  // 路由规则队列 
    35.         struct module           *owner;      //  
    36.         struct net              *fro_net;    // 网络空间结构指针 
    37. }; 
     struct fib_rules_ops
     {
             int                     family;              // 协议族ID
             struct list_head        list;                // 用于链接到网络空间队列中
             int                     rule_size;           // 规则结构大小
             int                     addr_size;           // 地址大小
             int                     unresolved_rules;
             int                     nr_goto_rules;
     
             int                     (*action)(struct fib_rule *,         // 动作函数指针
                                               struct flowi *, int,
                                               struct fib_lookup_arg *); 
             int                     (*match)(struct fib_rule *,          // 匹配函数指针
                                              struct flowi *, int);
             int                     (*configure)(struct fib_rule *,      // 配置函数指针
                                                  struct sk_buff *,
                                                  struct nlmsghdr *,
                                                  struct fib_rule_hdr *,
                                                  struct nlattr **);
             int                     (*compare)(struct fib_rule *,        // 对比函数指针
                                                struct fib_rule_hdr *,
                                                struct nlattr **);
             int                     (*fill)(struct fib_rule *, struct sk_buff *,
                                             struct nlmsghdr *,           // 填写函数指针
                                             struct fib_rule_hdr *);
             u32                     (*default_pref)(struct fib_rules_ops *ops);  // 查找优先级函数指针
             size_t                  (*nlmsg_payload)(struct fib_rule *); // 统计负载数据能力函数指针
     
             /* Called after modifications to the rules set, must flush
              * the route cache if one exists. */
             void                    (*flush_cache)(void);     // 修改规则之后刷新缓存函数指针
     
             int                     nlgroup;      // 路由netlink组划分标识
             const struct nla_policy *policy;      // netlink属性优先级
             struct list_head        rules_list;   // 路由规则队列
             struct module           *owner;       // 
             struct net              *fro_net;     // 网络空间结构指针
     };


    现在我们从宏观上应该有一个认识,当我们进入策略查找的时候,根据优先级,分别查找本地路由表->主路由表->默认路由表。

    OK,我们需要看一下结构直接的关系:


    ok,我们由规则找到了我们需要的三张表,三张表按照优先级的顺序进行查询,现在就以Local表为例进行下面具体的查询,看下图:


    从图中我们可以看到四个等级查询:fib_table ---> fn_zone ---> fib_node ---> fib_info

    > fib_table结构后面紧接着就是fn_hash数组,里面是33个数组元素,fn_hash[0]代表网关,fn_hash[1]代表子网掩码长度为一的情况... 为什么需要这样划分,因为我们知道,在匹配地址的时候遵循最长掩码优先原则,所以,精确度递减。  同时注意fn_zone_list指向第一个活动的路由区,将所有的路由区都链接在一起,从而提高查找的效率。fn_zone结构中最重要的就是fz_hash域了,它指向了一个hash table,这个hash table组织了这个区域下的所有路由项。( 一个fn_zone其实就是所有掩码长度相等的路由聚集在一起... )


    > fn_zone路由区通过再次计算hash值,可以获得和自己相关的fib_node节点,fib_node节点是所有的子网相等的路由聚集在一起。

    fn_key子网地址,也就hash查找的关键字;fn_type表示路由类型,即到底要怎处理数据,例如:单播转发,本地,丢弃,NAT等等对于大多数情况,路由项都是单播转发类型的;fn_info就是保存下一跳的信息,它指向一个fib_info结构。


    > 需要注意的是,一个fib_node对应着很多fib_info,因为即使是子网相等,也不一定是相等的路由,还有很多其他的因素。fib_info结构被组织成一个双向链表,表头为fib_info_list。下一跳的具体信息是fib_nh[]数组,它表示一个下一跳动作可以对应着多个物理的下一跳,这是linux支持的一个MULITPATH功能。


    说到这来,大致的印象应该是有的,下面需要做的就是深入代码细节。



    待续...  后面会介绍相关的代码...

     

     

    NAPI 是 Linux 上采用的一种提高网络处理效率的技术,它的核心概念就是不采用中断的方式读取数据,而代之以首先采用中断唤醒数据接收的服务程序,然后 POLL 的方法来轮询数据。随着网络的接收速度的增加,NIC 触发的中断能做到不断减少,目前 NAPI 技术已经在网卡驱动层和网络层得到了广泛的应用,驱动层次上已经有 E1000 系列网卡,RTL8139 系列网卡,3c50X 系列等主流的网络适配器都采用了这个技术,而在网络层次上,NAPI 技术已经完全被应用到了著名的 netif_rx 函数中间,并且提供了专门的 POLL 方法--process_backlog 来处理轮询的方法;根据实验数据表明采用NAPI技术可以大大改善短长度数据包接收的效率,减少中断触发的时间。
     
    但是 NAPI 存在一些比较严重的缺陷:
    1.               对于上层的应用程序而言,系统不能在每个数据包接收到的时候都可以及时地去处理它,而且随着传输速度增加,累计的数据包将会耗费大量的内存,经过实验表明在 Linux 平台上这个问题会比在 FreeBSD 上要严重一些;
    2.               另外一个问题是对于大的数据包处理比较困难,原因是大的数据包传送到网络层上的时候耗费的时间比短数据包长很多(即使是采用 DMA 方式),所以正如前面所说的那样,NAPI 技术适用于对高速率的短长度数据包的处理。
     
    使用 NAPI 先决条件:
    驱动可以继续使用老的 2.4 内核的网络驱动程序接口,NAPI 的加入并不会导致向前兼容性的丧失,但是 NAPI 的使用至少要得到下面的保证:
    1.          要使用 DMA 的环形输入队列(也就是 ring_dma,这个在 2.4 驱动中关于 Ethernet 的部分有详细的介绍),或者是有足够的内存空间缓存驱动获得的包。
    2.          在发送/接收数据包产生中断的时候有能力关断 NIC 中断的事件处理,并且在关断 NIC 以后,并不影响数据包接收到网络设备的环形缓冲区(以下简称 rx-ring)处理队列中。
    NAPI 对数据包到达的事件的处理采用轮询方法,在数据包达到的时候,NAPI 就会强制执行dev->poll 方法。而和不像以前的驱动那样为了减少包到达时间的处理延迟,通常采用中断的方法来进行。
     
    E1000网卡驱动程序对NAPI的支持:
    上面已经介绍过了,使用NAPI需要在编译内核的时候选择打开相应网卡设备的NAPI支持选项,对于E1000网卡来说就是CONFIG_E1000_NAPI宏。
    E1000网卡的初始化函数,也就是通常所说的probe方法,定义为e1000_probe():
    static int __devinit e1000_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
    {
           struct net_device *netdev;
           struct e1000_adapter *adapter;
           static int cards_found = 0;
           unsigned long mmio_start;
           int mmio_len;
           int pci_using_dac;
           int i;
           int err;
           uint16_t eeprom_data;
     
           if((err = pci_enable_device(pdev)))
                  return err;
           /*
           在这里设置PCI设备的DMA掩码,如果这个设备支持DMA传输,则掩码置位。
           */
           if(!(err = pci_set_dma_mask(pdev, PCI_DMA_64BIT))) {
                  pci_using_dac = 1;
           } else {
                  if((err = pci_set_dma_mask(pdev, PCI_DMA_32BIT))) {
                         E1000_ERR("No usable DMA configuration, aborting\n");
                         return err;
                  }
                  pci_using_dac = 0;
           }
     
           if((err = pci_request_regions(pdev, e1000_driver_name)))
                  return err;
     
           pci_set_master(pdev);
           /*
           为e1000网卡对应的net_device结构分配内存。
           */
           netdev = alloc_etherdev(sizeof(struct e1000_adapter));
           if(!netdev) {
                  err = -ENOMEM;
                  goto err_alloc_etherdev;
           }
     
           SET_MODULE_OWNER(netdev);
     
           pci_set_drvdata(pdev, netdev);
           adapter = netdev->priv;
           adapter->netdev = netdev;
           adapter->pdev = pdev;
           adapter->hw.back = adapter;
     
           mmio_start = pci_resource_start(pdev, BAR_0);
           mmio_len = pci_resource_len(pdev, BAR_0);
     
           adapter->hw.hw_addr = ioremap(mmio_start, mmio_len);
           if(!adapter->hw.hw_addr) {
                  err = -EIO;
                  goto err_ioremap;
           }
     
           for(i = BAR_1; i <= BAR_5; i++) {
                  if(pci_resource_len(pdev, i) == 0)
                         continue;
                  if(pci_resource_flags(pdev, i) & IORESOURCE_IO) {
                         adapter->hw.io_base = pci_resource_start(pdev, i);
                         break;
                  }
           }
           /*
           将e1000网卡驱动程序的相应函数注册到net_device结构的成员函数上。这里值得注意的是如果定义了设备的CONFIG_E1000_NAPI宏,则设备对应的poll方法被注册为e1000_clean。
           在网络设备 初始化时(net_dev_init()函数)将所有的设备的poll方法注册为系统默认函数process_backlog(),该函数的处理方法就是 从CPU相关队列softnet_data的输入数据包队列中读取skb,然后调用netif_receive_skb()函数提交给上层协议继续处理。 设备的poll方法是在软中断处理函数中调用的。
           */
           netdev->open = &e1000_open;
           netdev->stop = &e1000_close;
           netdev->hard_start_xmit = &e1000_xmit_frame;
           netdev->get_stats = &e1000_get_stats;
           netdev->set_multicast_list = &e1000_set_multi;
           netdev->set_mac_address = &e1000_set_mac;
           netdev->change_mtu = &e1000_change_mtu;
           netdev->do_ioctl = &e1000_ioctl;
           netdev->tx_timeout = &e1000_tx_timeout;
           netdev->watchdog_timeo = 5 * HZ;
    #ifdef CONFIG_E1000_NAPI
           netdev->poll = &e1000_clean;
           netdev->weight = 64;
    #endif
           netdev->vlan_rx_register = e1000_vlan_rx_register;
           netdev->vlan_rx_add_vid = e1000_vlan_rx_add_vid;
           netdev->vlan_rx_kill_vid = e1000_vlan_rx_kill_vid;
           /*
           这些就是利用ifconfig能够看到的内存起始地址,以及基地址。
           */
           netdev->irq = pdev->irq;
           netdev->mem_start = mmio_start;
           netdev->mem_end = mmio_start + mmio_len;
           netdev->base_addr = adapter->hw.io_base;
     
           adapter->bd_number = cards_found;
     
           if(pci_using_dac)
                  netdev->features |= NETIF_F_HIGHDMA;
     
           /* MAC地址是存放在网卡设备的EEPROM上的,现在将其拷贝出来。 */
           e1000_read_mac_addr(&adapter->hw);
           memcpy(netdev->dev_addr, adapter->hw.mac_addr, netdev->addr_len);
           if(!is_valid_ether_addr(netdev->dev_addr)) {
                  err = -EIO;
                  goto err_eeprom;
           }
           /*
           这里初始化三个定时器列表,以后对内核Timer的实现进行分析,这里就不介绍了。
           */
           init_timer(&adapter->tx_fifo_stall_timer);
           adapter->tx_fifo_stall_timer.function = &e1000_82547_tx_fifo_stall;
           adapter->tx_fifo_stall_timer.data = (unsigned long) adapter;
     
           init_timer(&adapter->watchdog_timer);
           adapter->watchdog_timer.function = &e1000_watchdog;
           adapter->watchdog_timer.data = (unsigned long) adapter;
     
           init_timer(&adapter->phy_info_timer);
           adapter->phy_info_timer.function = &e1000_update_phy_info;
           adapter->phy_info_timer.data = (unsigned long) adapter;
     
           INIT_TQUEUE(&adapter->tx_timeout_task,
                  (void (*)(void *))e1000_tx_timeout_task, netdev);
           /*
           这里调用网络设备注册函数将当前网络设备注册到系统的dev_base[]设备数组当中,并且调用设备的probe函数,对于以太网来说,就是ethif_probe()函数。相关的说明见内核网络设备操作部分的分析。
           调用关系:register_netdev ()->register_netdevice()
           */
           register_netdev(netdev);
     
           netif_carrier_off(netdev);
           netif_stop_queue(netdev);
     
    e1000_check_options(adapter);
    }
     
    在分析网卡接收数据包的过程中,设备的open方法是值得注意的,因为在这里对网卡设备的各种数据结构进行了初始化,特别是环形缓冲区队列。E1000网卡驱动程序的open方法注册为e1000_open():
    static int e1000_open(struct net_device *netdev)
    {
           struct e1000_adapter *adapter = netdev->priv;
           int err;
     
           /* allocate transmit descriptors */
     
           if((err = e1000_setup_tx_resources(adapter)))
                  goto err_setup_tx;
     
           /* allocate receive descriptors */
     
           if((err = e1000_setup_rx_resources(adapter)))
                  goto err_setup_rx;
     
           if((err = e1000_up(adapter)))
                  goto err_up;
    }
           事 实上e1000_open()函数调用了e1000_setup_rx_resources()函数为其环形缓冲区分配资源。e1000设备的接收方式是 一种缓冲方式,能显著的降低CPU接收数据造成的花费,接收数据之前,软件需要预先分配一个 DMA 缓冲区,一般对于传输而言,缓冲区最大为 8Kbyte 并且把物理地址链接在描述符的 DMA 地址描述单元,另外还有两个双字的单元表示对应的 DMA 缓冲区的接收状态。
    在 /driver/net/e1000/e1000/e1000.h 中对于环形缓冲队列描述符的数据单元如下表示:
    struct e1000_desc_ring {     
           void *desc;            /* 指向描述符环状缓冲区的指针。*/      
           dma_addr_t dma;  /* 描述符环状缓冲区物理地址,也就是DMA缓冲区地址*/
           unsigned int size;    /* 描述符环状缓冲区的长度(用字节表示)*/      
           unsigned int count; /* 缓冲区内描述符的数量,这个是系统初始化时规定好的,它决定该环形缓冲区有多少描述符(或者说缓冲区)可用*/
           unsigned int next_to_use; /* 下一个要使用的描述符。*/
           unsigned int next_to_clean; /* 下一个待删除描述符。*/ 
           struct e1000_buffer *buffer_info; /* 缓冲区信息结构数组。*/
    };
     
    static int e1000_setup_rx_resources(struct e1000_adapter *adapter)
    {
           /*将环形缓冲区取下来*/
           struct e1000_desc_ring *rxdr = &adapter->rx_ring;
           struct pci_dev *pdev = adapter->pdev;
           int size;
     
           size = sizeof(struct e1000_buffer) * rxdr->count;
           /*
           为每一个描述符缓冲区分配内存,缓冲区的数量由count决定。
           */
           rxdr->buffer_info = kmalloc(size, GFP_KERNEL);
           if(!rxdr->buffer_info) {
                  return -ENOMEM;
           }
           memset(rxdr->buffer_info, 0, size);
     
           /* Round up to nearest 4K */
     
           rxdr->size = rxdr->count * sizeof(struct e1000_rx_desc);
           E1000_ROUNDUP(rxdr->size, 4096);
           /*
           调用pci_alloc_consistent()函数为系统分配DMA缓冲区。
           */
           rxdr->desc = pci_alloc_consistent(pdev, rxdr->size, &rxdr->dma);
     
           if(!rxdr->desc) {
                  kfree(rxdr->buffer_info);
                  return -ENOMEM;
           }
           memset(rxdr->desc, 0, rxdr->size);
     
           rxdr->next_to_clean = 0;
           rxdr->next_to_use = 0;
     
           return 0;
    }
    在e1000_up()函数中,调用request_irq()向系统申请irq中断号,然后将e1000_intr()中断处理函数注册到系统当中,系统有一个中断向量表irq_desc[](?)。然后使能网卡的中断。
    接下来就是网卡处于响应中断的模式,这里重要的函数是 e1000_intr()中断处理函数,关于这个函数的说明在内核网络设备操作笔记当中,这里就不重复了,但是重点强调的是中断处理函数中对NAPI部分 的处理方法,因此还是将该函数的源码列出,不过省略了与NAPI无关的处理过程:
    static irqreturn_t e1000_intr(int irq, void *data, struct pt_regs *regs)
    {
           struct net_device *netdev = data;
           struct e1000_adapter *adapter = netdev->priv;
           uint32_t icr = E1000_READ_REG(&adapter->hw, ICR);
    #ifndef CONFIG_E1000_NAPI
           unsigned int i;
    #endif
     
           if(!icr)
                  return IRQ_NONE; /* Not our interrupt */
     
    #ifdef CONFIG_E1000_NAPI
           /*
           如果定义了采用NAPI模式接收数据包,则进入这个调用点。
    首先调用netif_rx_schedule_prep(dev),确定设备处于运行,而且设备还没有被添加到网络层的 POLL 处理队列中,在调用 netif_rx_schedule之前会调用这个函数。
    接下来调用 __netif_rx_schedule(dev),将设备的 POLL 方法添加到网络层次的 POLL 处理队列中去,排队并且准备接收数据包,在使用之前需要调用 netif_rx_reschedule_prep,并且返回的数为 1,并且触发一个 NET_RX_SOFTIRQ 的软中断通知网络层接收数据包。
    处理完成。
           */
           if(netif_rx_schedule_prep(netdev)) {
     
                  /* Disable interrupts and register for poll. The flush
                   of the posted write is intentionally left out.
                  */
     
                  atomic_inc(&adapter->irq_sem);
                  E1000_WRITE_REG(&adapter->hw, IMC, ~0);
                  __netif_rx_schedule(netdev);
           }
    #else
           /*
           在中断模式下,就会调用net_if()函数将数据包插入接收队列中,等待软中断处理。
           */
           for(i = 0; i < E1000_MAX_INTR; i++)
                  if(!e1000_clean_rx_irq(adapter) &
                     !e1000_clean_tx_irq(adapter))
                         break;
    #endif
     
           return IRQ_HANDLED;
    }
    下面介绍一下__netif_rx_schedule(netdev)函数的作用:
    static inline void __netif_rx_schedule(struct net_device *dev)
    {
           unsigned long flags;
           /*    获取当前CPU。   */
    int cpu = smp_processor_id();
     
           local_irq_save(flags);
           dev_hold(dev);
           /*将当前设备加入CPU相关全局队列softnet_data的轮询设备列表中,不过值得注意的是,这个列表中的设备不一定都执行轮询接收数据包,这里的poll_list只是表示当前设备需要接收数据,具体采用中断还是轮询的方式,取决于设备提供的poll方法。*/
           list_add_tail(&dev->poll_list, &softnet_data[cpu].poll_list);
           if (dev->quota < 0)
    /*对于e1000网卡的轮询机制,weight(是权,负担的意思)这个参数是64。而quota的意思是配额,限额。这两个参数在随后的轮询代码中出现频繁。*/
                  dev->quota += dev->weight;
           else
                  dev->quota = dev->weight;
           /*
           调用函数产生网络接收软中断。也就是系统将运行net_rx_action()处理网络数据。
           */
           __cpu_raise_softirq(cpu, NET_RX_SOFTIRQ);
           local_irq_restore(flags);
    }
    在内核网络设备操作阅读笔记当中已经介绍过net_rx_action()这个重要的网络接收软中断处理函数了,不过这里为了清楚的分析轮询机制,需要再次分析这段代码:
    static void net_rx_action(struct softirq_action *h)
    {
           int this_cpu = smp_processor_id();
           /*获取当前CPU的接收数据队列。*/
           struct softnet_data *queue = &softnet_data[this_cpu];
           unsigned long start_time = jiffies;
    /*呵呵,这里先做个预算,限定我们只能处理这么多数据(300个)。*/
           int budget = netdev_max_backlog;
     
           br_read_lock(BR_NETPROTO_LOCK);
           local_irq_disable();
           /*
           进入一个循环,因为软中断处理函数与硬件中断并不是同步的,因此,我们此时并不知道数据包属于哪个设备,因此只能采取逐个查询的方式,遍历整个接收设备列表。
           */
           while (!list_empty(&queue->poll_list)) {
                  struct net_device *dev;
                  /*如果花费超过预算,或者处理时间超过1秒,立刻从软中断处理函数跳出,我想这可能是系统考虑效率和实时性,一次不能做过多的工作或者浪费过多的时间。*/
                  if (budget <= 0 || jiffies - start_time > 1)
                         goto softnet_break;
     
                  local_irq_enable();
    /*从当前列表中取出一个接收设备。并根据其配额判断是否能够继续接收数据,如果配额不足(<=0),则立刻将该设备从设备列表中删除。并且再次插入队列当中,同时为该设备分配一定的配额,允许它继续处理数据包。
    如果此时配额足够,则调用设备的 poll方法,对于e1000网卡来说,如果采用中断方式处理数据,则调用系统默认poll方法process_backlog(),而对于采用NAPI 来说,则是调用e1000_clean()函数了。记住这里第一次传递的预算是300 ^_^。*/
                  dev = list_entry(queue->poll_list.next, struct net_device, poll_list);
     
                  if (dev->quota <= 0 || dev->poll(dev, &budget)) {
                         local_irq_disable();
                         list_del(&dev->poll_list);
                         list_add_tail(&dev->poll_list, &queue->poll_list);
                         if (dev->quota < 0)
                                dev->quota += dev->weight;
                         else
                                dev->quota = dev->weight;
                  } else {
                         dev_put(dev);
                         local_irq_disable();
                  }
           }
     
           local_irq_enable();
           br_read_unlock(BR_NETPROTO_LOCK);
           return;
     
    softnet_break:
           netdev_rx_stat[this_cpu].time_squeeze++;
           /*再次产生软中断,准备下一次数据包处理。*/
           __cpu_raise_softirq(this_cpu, NET_RX_SOFTIRQ);
     
           local_irq_enable();
           br_read_unlock(BR_NETPROTO_LOCK);
    }
    下面介绍一下e1000网卡的轮询poll处理函数e1000_clean(),这个函数只有定义了NAPI宏的情况下才有效:
    #ifdef CONFIG_E1000_NAPI
    static int e1000_clean(struct net_device *netdev, int *budget)
    {
           struct e1000_adapter *adapter = netdev->priv;
           /*计算一下我们要做的工作量,取系统给定预算(300)和我们网卡设备的配额之间的最小值,这样做同样是为了效率和实时性考虑,不能让一个设备在接收设备上占用太多的资源和时间。*/
           int work_to_do = min(*budget, netdev->quota);
           int work_done = 0;
           /*处理网卡向外发送的数据,这里我们暂时不讨论。*/
           e1000_clean_tx_irq(adapter);
           /*处理网卡中断收到的数据包,下面详细讨论这个函数的处理方法。*/
           e1000_clean_rx_irq(adapter, &work_done, work_to_do);
           /*从预算中减掉我们已经完成的任务,预算在被我们支出,^_^。同时设备的配额也不断的削减。*/
           *budget -= work_done;
           netdev->quota -= work_done;
           /*如果函 数返回时,完成的工作没有达到预期的数量,表明接收的数据包并不多,很快就全部处理完成了,我们就彻底完成了这次轮询任务,调用 netif_rx_complete(),把当前指定的设备从 POLL 队列中清除(注意如果在 POLL 队列处于工作状态的时候是不能把指定设备清除的,否则将会出错),然后使能网卡中断。*/
           if(work_done < work_to_do) {
                  netif_rx_complete(netdev);
                  e1000_irq_enable(adapter);
           }
           /*如果完成的工作大于预期要完成的工作,则表明存在问题,返回1,否则正常返回0。*/
           return (work_done >= work_to_do);
    }
     
    设备轮询接收机制中最重要的函数就是下面这个函数,当然它同时也可以为中断接收机制所用,只不过处理过程有一定的差别。
    static boolean_t
    #ifdef CONFIG_E1000_NAPI
    e1000_clean_rx_irq(struct e1000_adapter *adapter, int *work_done,
                       int work_to_do)
    #else
    e1000_clean_rx_irq(struct e1000_adapter *adapter)
    #endif
    {
           /*这里很清楚,获取设备的环形缓冲区指针。*/
           struct e1000_desc_ring *rx_ring = &adapter->rx_ring;
           struct net_device *netdev = adapter->netdev;
           struct pci_dev *pdev = adapter->pdev;
           struct e1000_rx_desc *rx_desc;
           struct e1000_buffer *buffer_info;
           struct sk_buff *skb;
           unsigned long flags;
           uint32_t length;
           uint8_t last_byte;
           unsigned int i;
           boolean_t cleaned = FALSE;
           /*把i置为下一个要清除的描述符索引,因为在环形缓冲区队列当中,我们即使已经处理完一个缓冲区描述符,也不是将其删除,而是标记为已经处理,这样如果有新的数据需要使用缓冲区,只是将已经处理的缓冲区覆盖而已。*/
           i = rx_ring->next_to_clean;
           rx_desc = E1000_RX_DESC(*rx_ring, i);
           /*如果i对应的描述符状态是已经删除,则将这个缓冲区取出来给新的数据使用*/
           while(rx_desc->status & E1000_RXD_STAT_DD) {
                  buffer_info = &rx_ring->buffer_info[i];
     
    #ifdef CONFIG_E1000_NAPI
           /*在配置了NAPI的情况下,判断是否已经完成的工作?,因为是轮询机制,所以我们必须自己计算我们已经处理了多少数据。*/
                  if(*work_done >= work_to_do)
                         break;
     
                  (*work_done)++;
    #endif
     
                  cleaned = TRUE;
                  /*这个是DMA函数,目的是解除与DMA缓冲区的映射关系,这样我们就可以访问这个缓冲区,获取通过DMA传输过来的数据包(skb)。驱动程序在分配环形缓冲区的时候就将缓冲区与DMA进行了映射。*/
                  pci_unmap_single(pdev,
                                   buffer_info->dma,
                                   buffer_info->length,
                                   PCI_DMA_FROMDEVICE);
     
                  skb = buffer_info->skb;
                  length = le16_to_cpu(rx_desc->length);
                  /*对接收的数据包检查一下正确性。确认是一个正确的数据包以后,将skb的数据指针进行偏移。*/
                  skb_put(skb, length - ETHERNET_FCS_SIZE);
     
                  /* Receive Checksum Offload */
                  e1000_rx_checksum(adapter, rx_desc, skb);
                  /*获取skb的上层协议类型。这里指的是IP层的协议类型。*/
                  skb->protocol = eth_type_trans(skb, netdev);
    #ifdef CONFIG_E1000_NAPI     
    /*调用函数直接将skb向上层协议处理函数递交,而不是插入什么队列等待继续处理,因此这里可能存在一个问题,如果数据包比较大,处理时间相对较长,则可能造成系统效率的下降。*/
                         netif_receive_skb(skb);
          
    #else /* CONFIG_E1000_NAPI */
                  /*如果采用中断模式,则调用netif_rx()将数据包插入队列中,在随后的软中断处理函数中调用netif_receive_skb(skb)向上层协议处理函数递交。这里就体现出了中断处理机制和轮询机制之间的差别。*/     
                         netif_rx(skb);
    #endif /* CONFIG_E1000_NAPI */
                  /*用全局时间变量修正当前设备的最后数据包接收时间。*/
                  netdev->last_rx = jiffies;
     
                  rx_desc->status = 0;
                  buffer_info->skb = NULL;
                  /*这 里是处理环形缓冲区达到队列末尾的情况,因为是环形的,所以到达末尾的下一个就是队列头,这样整个环形队列就不断的循环处理。然后获取下一个描述符的状 态,看看是不是处于删除状态。如果处于这种状态就会将新到达的数据覆盖旧的的缓冲区,如果不处于这种状态跳出循环。并且将当前缓冲区索引号置为下一次查询 的目标。*/
                  if(++i == rx_ring->count) i = 0;
     
                  rx_desc = E1000_RX_DESC(*rx_ring, i);
           }
     
           rx_ring->next_to_clean = i;
           /*为下一次接收skb做好准备,分配sk_buff内存。出于效率的考虑,如果下一个要使用的缓冲区的sk_buff还没有分配,就分配,如果已经分配,则可以重用。*/
           e1000_alloc_rx_buffers(adapter);
     
           return cleaned;
    }
    下面分析的这个函数有助于我们了解环形接收缓冲区的结构和工作原理:
    static void e1000_alloc_rx_buffers(struct e1000_adapter *adapter)
    {
           struct e1000_desc_ring *rx_ring = &adapter->rx_ring;
           struct net_device *netdev = adapter->netdev;
           struct pci_dev *pdev = adapter->pdev;
           struct e1000_rx_desc *rx_desc;
           struct e1000_buffer *buffer_info;
           struct sk_buff *skb;
           int reserve_len = 2;
           unsigned int i;
           /*接收队列中下一个用到的缓冲区索引,初始化是0。并且获取该索引对应的缓冲区信息结构指针buffer_info。*/
           i = rx_ring->next_to_use;
           buffer_info = &rx_ring->buffer_info[i];
           /*如果该缓冲区还没有为sk_buff分配内存,则调用dev_alloc_skb函数分配内存,默认的e1000网卡的接收缓冲区长度是2048字节加上保留长度。
    注 意:在e1000_open()->e1000_up()中已经调用了这个函数为环形缓冲区队列中的每一个缓冲区分配了sk_buff内存,但是如 果接收到数据以后,调用netif_receive_skb (skb)向上层提交数据以后,这段内存将始终被这个skb占用(直到上层处理完以后才会调用__kfree_skb释放,但已经跟这里没有关系了),换 句话说,就是当前缓冲区必须重新申请分配sk_buff内存,为了下一个数据做准备。*/
           while(!buffer_info->skb) {
                  rx_desc = E1000_RX_DESC(*rx_ring, i);
     
                  skb = dev_alloc_skb(adapter->rx_buffer_len + reserve_len);
     
                  if(!skb) {
                         /* Better luck next round */
                         break;
                  }
                  skb_reserve(skb, reserve_len);
     
                  skb->dev = netdev;
                  /*映射DMA缓冲区,DMA通道直接将收到的数据写到我们提供的这个缓冲区内,每次必须将缓冲区与DMA通道解除映射关系,才能读取缓冲区内容。*/
                  buffer_info->skb = skb;
                  buffer_info->length = adapter->rx_buffer_len;
                  buffer_info->dma =
                         pci_map_single(pdev,
                                        skb->data,
                                        adapter->rx_buffer_len,
                                        PCI_DMA_FROMDEVICE);
     
                  rx_desc->buffer_addr = cpu_to_le64(buffer_info->dma);
     
                  if(++i == rx_ring->count) i = 0;
                  buffer_info = &rx_ring->buffer_info[i];
           }
           rx_ring->next_to_use = i;
    }

     

     

    展开全文
  • 如何学习linux内核网络协议栈

    如何学习linux内核网络协议栈?

    展开全文
  • Linux内核网络协议栈笔记

    千次阅读 2018-04-13 17:14:42
    Linux内核网络协议栈笔记0:序言(附参考书籍)转自:http://www.th7.cn/system/lin/2011/08/11/18810.shtml自己是研究网络的,但实际上对Linux中网络协议栈的实现知之甚少。最近看完《深入理解Linux内核》前几章...

    Linux内核网络协议栈笔记0:序言(附参考书籍)

    转自:http://www.th7.cn/system/lin/2011/08/11/18810.shtml

    自己是研究网络的,但实际上对Linux中网络协议栈的实现知之甚少。最近看完《深入理解Linux内核》前几章之后(特别是与网络子系统密切相关的软中断),觉得可以而且应该看一下网络协议栈了。这部分网上的文章大部分都没有什么结构和思路,很少有能够条分缕析的把协议栈讲述明白的。当然,个人水平有限,还是希望朋友们能够批评指正。
    参考书籍《Understanding Linux Network Internals》以及《The Linux Networking Architecture Design and Implementation of Network Protocols in the Linux Kernel》,在我的Skydrive里(点这里)可以下到英文chm版。
    先大体说说这两本巨著吧。前者确实是一本关于internals的书,前三个part:General Background/System Initialization/Transmission and Reception以及第5个part:IPv4比较有用,而且思路也与本文所采用的吻合:从系统初始化到数据包的发送与接收。而后者也确实是一本architecture的书,采用了与TCP/IP协议栈(不是OSI 7层)一样的5层架构自底向上讲述了Linux内核的相关内容。

    全系列文章都基于Linux内核2.6.11版本,如果最新版本(当前是2.6.30)有较大变化,也会给与标出。


      

    Linux内核网络协议栈笔记1:协议栈分层/层次结构

    大家都知道TCP/IP协议栈现在是世界上最流行的网络协议栈,恐怕它的普及的最重要的原因就是其清晰的层次结构以及清晰定义的原语和接口。不仅使得上层应用开发者可以无需关心下层架构或者内部机制,从而相对透明的操作网络。这个明显的层次结构也可以在Linux内核的网络协议栈中观察到。

    主要的参考文献是:Linux网络栈剖析(中文版)/Anatomy of Linux networking stack(英文原版)by Tim Jones.

    以及:Linux内核2.4.x的网络接口结构

    另外一些参考资料可以从这个页面找到:http://www.ecsl.cs.sunysb.edu/elibrary/linux/network/ (纽约州立大学石溪分校的页面)

    Linux内核网络协议栈采用了如下的层次结构:

    内核中的五层分别是(从上到下):

    系统调用接口(详见Jones的另一篇文章:使用Linux系统调用的内核命令

    协议无关接口(BSD socket层)

    网络协议(或者简称网络层。这是一个协议的集合,从链路层到传输层的协议都包括在内。不同的协议在/net文件夹下除core以外的子目录下,例如基于IP的协议簇都在/net/ipv4目录下,以太网协议在/net/ethernet目录下)

    驱动无关接口(又称通用设备层--generic device layer/驱动接口层/设备操作层--device handling layer。属于网络协议栈最核心的部分,文件位于内核/net/core文件夹下,所以又叫网络核心层。其中包括了核心的数据结构skbuff文件中的sk_buff/dev.c文件中net_device,这些数据结构将在下篇文章中介绍)

    设备驱动程序(在/driver/net文件夹内)

    不像OSI或者TCP/IP协议栈,事实上并没有一个命名标准,因此在这里,这些层次的名称并不是通用的,但是其语义是清晰的,而且在大多数其他的文章里只是个别字上的差别。分层详细介绍可以参考Jones的文章。

     

    Linux内核网络协议栈笔记2:初始化

    参考文献《Understanding Linux Network Internals》中用了整整一章(part II)来介绍system initialization。本文只提供一个简单的概述,如果需要详细信息,还请看参考文献。

    我们这里所说的初始化过程指的是从硬件加电启动,到可以从网络接收或发送数据包之前的过程。在Linux系统中,网卡拥有双重身份:struct pci_dev和struct net_device。pci_dev对象代表通用硬件的性质,是作为一个标准的PCI的设备插入了PCI的卡槽,由驱动程序进行管理;另一方面,net_device对象代表网络传输的性质,与内核的网络协议栈交互,进行数据传输。因此我们也必须通过两个方面来进行初始化,但是后者是我们的重点。而且我们并不关心内核与硬件的交互细节,例如寄存器读写与I/O映射等。

    内核在初始化时,也会初始化一些与网络相关的数据结构;而且对应我们前面的日志所提及的内核网络协议栈层次结构(点这里),内核也需要一定的初始化工作来建立这种层次结构。

    笔者认为初始化最重要的就是重要数据结构(通常用粗体标注)。因此也希望读者能够记住重要的数据结构的作用。

    下面我们将分层,自底向上的分析整个初始化过程:

    (一)驱动程序层

    本文中以一个realtek 8139系列网卡作为例子,因为其驱动只有一个c文件(/drivers/net/8139too.c),比较容易分析。读者也可以参考e1000网卡的另一篇文章(点这里)。内核版本基于2.6.11。

    驱动程序加载/注册主要包括以下的步骤:

    (a)将设备驱动程序(pci_driver)添加到内核驱动程序链表中;

    (b)调用每个驱动中的probe函数(其中重要一步就是初始化net_device对象)。

    下面进行详细分解。

    通常,在Linux中使用insmod命令加载一个驱动程序模块,例如8139too.o目标文件。加载之后,Linux会默认执行模块中的module_init(rtl8139_init_module)宏函数,其中的参数rtl8139_init_module是一个函数指针,指向在具体的驱动程序8139too.o中声明的rtl8139_init_module函数。这个函数定义如下:

    static int __init rtl8139_init_module (void )
    return pci_module_init (&rtl8139_pci_driver); }

    pci_module_init是一个宏定义,实际上就等于pci_register_driver函数。(在2.6.30内核版本中,直接变成了return pci_register_driver(&rtl8139_pci_driver) )。pci_register_driver函数的注释说明了它的作用:register a new pci driver.Adds the driver structure to the list of registered drivers。也就是把如下的这样一个驱动程序(pci_driver类型)挂到系统的驱动程序链表中:

    static struct pci_driver rtl8139_pci_driver =  {
    .name   
    =
     DRV_NAME,
    .id_table 
    =
     rtl8139_pci_tbl,
    .probe   
    =
     rtl8139_init_one,
    .remove   
    =
     __devexit_p(rtl8139_remove_one),
    #ifdef CONFIG_PM
    .suspend 
    =
     rtl8139_suspend,
    .resume   
    =
     rtl8139_resume,
    #endif /* CONFIG_PM */

    };

    这一步我们应该这样理解(熟悉面向对象编程的读者):所有的pci_driver应该提供一致的接口(比如remove卸载/suspend挂起);但是这些接口的每个具体实现是不同的(pci声卡和pci显卡的挂起应该是不同的),所以采用了这样的函数指针结构。这个pci_driver结构其中最重要的就是probe函数指针,指向rtl8139_init_one,具体后面会解释。
    但是pci_register_driver并不仅仅完成了注册驱动这个任务,它内部调用了driver_register函数(/drivers/base/driver.c中):

    int driver_register(struct device_driver *  drv)
    {
    INIT_LIST_HEAD(
    &drv->
    devices);
    init_MUTEX_LOCKED(
    &drv->
    unload_sem);
    return
     bus_add_driver(drv);
    }

    前两个就是实现了添加到链表的功能,bus_add_driver才是主要的函数(/drivers/base/bus.c中),内部又调用了driver_attach函数,这个函数的主体是一个list_for_each循环,对链表中的每一个成员调用driver_probe_device函数(哈哈,出现了probe!),这个函数就调用了drv->probe(dev)(drv就是pci_driver类型的对象)!这样也就调用了驱动程序中的probe函数指针,也就是调用了rtl8139_init_one函数。

    函数rtl8139_init_one的主要作用就是给net_device对象分配空间(分配空间由函数rtl8139_init_board完成)并初始化。分配空间主要是内存空间。分配的资源包括I/O端口,内存映射(操作系统基本概念,请自行google)的地址范围以及IRQ中断号等。而初始化主要是设置net_device对象中的各个成员变量及成员函数,其中比较重要的是hard_start_xmit(通过硬件发送数据)/poll(轮训)/open(启动)等函数(粗体标注),代码如下:

    static int __devinit rtl8139_init_one (struct pci_dev *pdev, const structpci_device_id * ent)
    {
        
    struct net_device *dev =
     NULL;
         rtl8139_init_board (pdev, 
    &
    dev);
    /* The Rtl8139-specific entries in the device structure. */

             dev
    ->open =  rtl8139_open;
             dev
    ->hard_start_xmit =
     rtl8139_start_xmit;
             dev
    ->poll =
     rtl8139_poll;
             dev
    ->stop =
     rtl8139_close;
             dev
    ->do_ioctl =
     netdev_ioctl;
    }

    整个的调用链如下:pci_register_driver ==> driver_register ==> bus_add_driver ==> driver_attach ==> driver_probe_device ==> drv->probe ==> rtl8139_init_one(生成net_device)

    一个简单的net_device生命周期示意图如下(左边为初始化,右边为卸载):

    这个net_device数据结构的生成,标志着网络硬件和驱动程序层初始化完毕。也意味着,网络协议栈与硬件之间的纽带已经建立起来。

    (二)设备无关层/网络协议层/协议无关接口socket层

    Linux内核在启动后所执行的一些内核函数如下图所示:

    系统初始化的过程中会调用do_basic_setup函数进行一些初始化操作。其中2.6.11内核中就直接包括了driver_init()驱动程序初始化,以及sock_init函数初始化socket层。然后do_initcalls()函数调用一组前缀为__init类型(这个宏就表示为需要在系统初始化时执行)的函数。与网络相关的以__init宏标记的函数有:net_dev_init初始化设备无关层;inet_init初始化网络协议层。

    (fs_initcall和module_init这两个宏也具有类似的作用。由于这一阶段处于系统初始化,宏定义比较多,欲详细了解各种宏的使用的读者请参阅参考文献《Understanding Linux Network Internals》Part II Chapter 7)

    我们下面详细介绍一下这三个初始化函数都进行了哪些工作。

    (a)net_dev_init(在文件/net/core/dev.c中):设备操作层

    static int __init net_dev_init(void )
    {
        
    if
     (dev_proc_init())
        
    if
     (netdev_sysfs_init())
         INIT_LIST_HEAD(
    &
    ptype_all);
        
        
    for (i = 0; i < 16; i++

             INIT_LIST_HEAD(
    &
    ptype_base[i]);
        
    for (i = 0; i < ARRAY_SIZE(dev_name_head); i++
    )
             INIT_HLIST_HEAD(
    &
    dev_name_head[i]);
        
    for (i = 0; i < ARRAY_SIZE(dev_index_head); i++
    )
             INIT_HLIST_HEAD(
    &
    dev_index_head[i]);

        
    //Initialise the packet receive queues.

        for (i = 0; i < NR_CPUS; i++ ) {
            
    struct softnet_data *
    queue;

             queue 
    = &
    per_cpu(softnet_data, i);
             skb_queue_head_init(
    &queue->
    input_pkt_queue);
             queue
    ->throttle = 0
    ;
             queue
    ->cng_level = 0
    ;
             queue
    ->avg_blog = 10/* arbitrary non-zero */

             queue
    ->completion_queue =  NULL;
             INIT_LIST_HEAD(
    &queue->
    poll_list);
             set_bit(__LINK_STATE_START, 
    &queue->
    backlog_dev.state);
             queue
    ->backlog_dev.weight =
     weight_p;
             queue
    ->backlog_dev.poll =
     process_backlog;
             atomic_set(
    &queue->backlog_dev.refcnt, 1
    );
         }

         open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
         open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);
    }

    这个函数所做的具体工作主要包括:初始化softnet_data这个数据结构(每个CPU都有一个这样的队列,表示要交给此CPU处理的数据包);注册网络相关软中断(参见我关于软中断的文章,点这里)。

    (b)inet_init(在文件/net/ipv4/af_inet.c中):网络层

    由于各种网络协议是按照协议族(protocol family,PF或者address family,AF)为单位组织起来的。我们在这里仅以Internet协议族(AF_INET或者PF_INET,在内核中这二者是等价的)为例。

    有时候这一层又被称为INET socket层(对应的数据结构为struct sock),请注意与BSD socket层区别(对应数据结构为struct socket):BSD socket层提供一组统一的接口,与协议无关;但具体到网络层就必须与协议相关了,因此操作也会有一些不同。

    代码如下(有删节):

    static int __init inet_init(void )
    {
        
    struct sk_buff *
    dummy_skb;
        
    struct inet_protosw *
    q;
        
    struct list_head *
    r;

         rc 
    = sk_alloc_slab(&tcp_prot, "tcp_sock"
    );
         rc 
    = sk_alloc_slab(&udp_prot, "udp_sock"
    );
         rc 
    = sk_alloc_slab(&raw_prot, "raw_sock"
    );

        
    //Tell SOCKET that we are alive 

           (void)sock_register(& inet_family_ops);

        
    //Add all the base protocols.

        if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0 );
        
    if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0
    );

        
    /* Register the socket-side information for inet_create. */

        
    for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++ r)
             INIT_LIST_HEAD(r);
        
    for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++
    q)
            inet_register_protosw(q);

        
    //Set the ARP module up

         arp_init();
          
    //Set the IP module up

         ip_init();
         tcp_v4_init(
    &
    inet_family_ops);
        
    /* Setup TCP slab cache for open requests. */

         tcp_init();
        
        
    //dev_add_pack(&ip_packet_type);
    }
    module_init(inet_init);

    这个函数中包括的主要函数包括:

    sock_register:在以前的文章中说过,Linux内核网络协议栈采用了分层结构,所以层与层之间肯定是松耦合的。上层的socket层并不知道下面的网络协议层都具体包括哪些协议。因此本函数有两个作用:(a)周知INET协议族;(b)注册到socket模块上。

    inet_add_protocol:在协议族中添加具体的协议。

    inet_register_protosw:各种inet协议族中的协议在内核中是保存在inetsw_array[]数组,其中元素为inet_protosw类型(/include/net/protocol.h文件中),每一个元素对应一个协议。每一个协议又有两个数据结构:struct proto/struct proto_ops。这两个结构中都是一些函数操作,但proto表示每个协议独特的操作,而proto_ops是通用的socket操作(包含在struct socket中);这种区别就类似于INET socket和BSD socket的区别。

    (c)sock_init(在文件/net/socket.c中):BSD socket层

    定义如下(代码有删节):

    void __init sock_init(void )
    {
        
    //Initialize sock SLAB cache.

         sk_init();
        
    //Initialize skbuff SLAB cache 

         skb_init();
        
    //Initialize the protocols module. 

         init_inodecache();
         register_filesystem(
    &
    sock_fs_type);
         sock_mnt 
    = kern_mount(&
    sock_fs_type);
        
    //The real protocol initialization is performed when do_initcalls is run.  

         netfilter_init();
    }

    此函数主要执行的工作是:初始化socksk_buff数据结构(这些重要的数据结构在后面的文章中还会涉及);由于sock属于linux文件系统的一部分,因此要注册成为文件系统;以及如果使用netfilter,也要进行初始化。

    Linux内核网络协议栈笔记3:重要数据结构sk_buff

    sk_buff,全称socket buffers,简称skb,中文名字叫套接字缓存。它可以称得上是Linux内核网络协议栈中最重要的数据结构,没有之一。它作为网络数据包的存放地点,使得协议栈中每个层都可以对数据进行操作,从而实现了数据包自底向上的传递。

    主要参考资料是《Understanding Linux Network Internals》Part I--Chapter 2.1中关于sk_buff的介绍。由于这个结构比较庞大,笔者主要选择比较重要的部分进行介绍,其余的还请看参考资料。

    (1)一些重要字段

    struct sock *sk;

        指向拥有这个缓存的那个sock(注意,这里是sock,不是socket,缓存是由具体协议拥有的)。

    struct net_device *dev;

        接收或者发送这个数据包队列的网络设备。参见笔者前面所写的文章(点这里)。

    (2)sk_buff的创建和销毁

    dev_alloc_skb/dev_kfree_skb函数负责sk_buff的创建和销毁,其内部分别调用了alloc_skb/kfree_skb。其内部实现就不再介绍了,但关于销毁和资源释放有一点需要注意:由于同一个sk_buff有可能被很多对象使用,所以使用了引用计数(reference count),当没有人使用之后,才能释放其资源。

    (3)布局(Layout)

    sk_buff的实现方式是双向链表。不仅每个节点有next指针指向下一元素,prev指针指向前一元素,又包含一个指向头结点的指针;同时链表的头结点比较特殊,包括了链表长度和一个自旋锁用于同步,因此形成了如下的结构:

    关于布局,sk_buff实现的另一个重要功能就是:当数据包从最底层的数据链路层向上传递到传输层时,进行一些处理。例如,IP层把数据包交给传输层时,就需要把IP头去掉。下面的图演示了一个UDP的数据包在发送时添加UDP报头和IP报头的过程:

    这个功能还需要另外三个成员变量,分别指向了传输层的头部(h),网络层的头部(nh=network header),数据链路层的头部(mac):

    union {...} h

    union {...} nh

    union {...} mac

    在2.6.30内核中这三个字段改为了:

    sk_buff_data_t          transport_header;
    sk_buff_data_t          network_header;
    sk_buff_data_t          mac_header;

    下面的例子表示了在经过mac层的传输过程中data字段的变化,被mac层接收之后,data指向了IP包头开始的地方(请回忆相关的网络知识):

    (4)管理函数

    skb_reserve, skb_put, skb_push, and skb_pull等函数可以用于缓存的空间管理。例如,在数据包从上层向下传输时,因为要添加下层协议的报头,这个时候就可以根据最大可能添加的长度预留空间,这样就无需每经过一层分配一次空间,降低了系统开销。

    还有另外一些以skb_queue开头的函数,表示对双向链表操作的函数。



    Linux内核网络协议栈笔记4:接收网络数据包详细过程

    网络数据接收过程,从数据包到达网卡的物理接口开始,然后由网卡的驱动程序交给网络协议栈,最后经过协议栈的一层层处理之后交给应用程序。大致上是这样的过程,但实际上有更多的细节。本文中主要介绍第一个和第二个步骤。

    我们本文中依然以一个Realtek 8139网卡为例(驱动程序为/drivers/net/8139too.c)。请注意在内核代码中receive都是用rx简写的。

    (1)注册与激活软中断

    在生成net_device对象及初始化的函数rtl8139_init_one中已经初始化dev->open方法为rtl8139_open函数(在本系列文章2:初始化中的net_device对象中已经介绍,点这里查看)。在rtl8139_open函数(这个函数在网卡启动时被调用)中注册了一个中断函数rtl8139_interrupt:

    retval = request_irq (dev->irq, rtl8139_interrupt, SA_SHIRQ, dev->name, dev);

    所以只要当网卡开启后(状态为up),当网络数据包到达时,都会产生一个硬件中断(这不同于后面的软中断)。这个硬件中断由内核调用中断处理程序rtl8139_interrupt函数处理。这个函数比较重要,网卡发送或者接收数据时内核都会调用这个函数处理中断,而中断的类型是根据网卡状态寄存器的不同而确定的。本文中仅涉及接收数据的中断,因此只给出了接收的代码:

    static irqreturn_t rtl8139_interrupt (int irq, void *dev_instance, struct pt_regs* regs)
    {
        
    if (status &
     RxAckBits){
            
    if
     (netif_rx_schedule_prep(dev))
                     __netif_rx_schedule (dev);
         }
    }

    主要函数为__netif_rx_schedule(函数名意为:network interface receive schedule,即网络接口接收调度),因为当网卡接收到数据包之后,马上告知CPU在合适的时间去启动调度程序,轮询(poll)网卡。

    请注意:Linux接收网络数据实际上有两种方式。
    (a)中断。每个数据包到达都会产生一个中断,然后由内核调用中断处理程序处理。
    (b)NAPI(New API)。Linux内核2.6版本之后加入的新机制,核心方法是:不采用中断的方式读取数据,而代之以首先采用中断唤醒数据接收的服务程序,然后以POLL的方法来轮询数据。
    因此本文中只介绍NAPI的接收方式。我们不再详细介绍这种机制,网上可找到比较多的资料,可以参考IBM的技术文章:NAPI 技术在 Linux 网络驱动上的应用和完善。

    __netif_rx_schedule函数的定义如下:

    static inline void __netif_rx_schedule(struct net_device * dev)
    {
             local_irq_save(flags);
    //
    disable interrupt
        
    //Add interface to tail of rx poll list

             list_add_tail(&dev->poll_list, & __get_cpu_var(softnet_data).poll_list);
        
    //activate network rx softirq

             __raise_softirq_irqoff(NET_RX_SOFTIRQ);
             local_irq_restore(flags);
    }

    这个函数最核心的就是三步:

    (a)local_irq_save:禁用中断

    (b)list_add_tail:将设备添加到softnet_data的poll_list中。

    (c)激活一个软中断NET_RX_SOFTIRQ。

    ======================================

    说到这里我们必须介绍一个关键数据结构softnet_data,每个CPU都拥有一个这样的网络数据队列(所以函数中使用了__get_cpu_var函数取得),定义如下:

    struct  softnet_data
    {
        
    int             throttle;    /*为 1 表示当前队列的数据包被禁止*/

        
    int             cng_level;    /*表示当前处理器的数据包处理拥塞程度*/
        
    int             avg_blog;    /*某个处理器的平均拥塞度*/
        
    struct sk_buff_head     input_pkt_queue;    /*接收缓冲区的sk_buff队列*/
        
    struct list_head             poll_list;    /*POLL设备队列头*/
        
    struct net_device              output_queue;     /*网络设备发送队列的队列头*/
        
    struct sk_buff         completion_queue; /*完成发送的数据包等待释放的队列*/
        struct net_device     backlog_dev;    /*表示当前参与POLL处理的网络设备*/
    };

    大致说明一下这个数据结构的意义。某个网卡产生中断之后,内核就把这个网卡挂载到轮询列表(poll_list)中。一个CPU会轮询自己的列表中的每一个网卡,看看它们是不是有新的数据包可以处理。我们需要先用一个比喻说明这个数据结构与轮询的关系:网卡就是佃户,CPU就是地主。佃户有自己种的粮食(网络数据包),但地主家也有粮仓(softnet_data)。地主要收粮的时候,就会挨家挨户的去催佃户交粮,放到自己的粮仓里。

    =======================================

    (2)软中断处理

    我们知道:激活软中断之后,并不是马上会被处理的。只有当遇到软中断的检查点时,系统才会调用相应的软中断处理函数。

    所有的网络接收数据包的软中断处理函数都是net_rx_action。这个函数的详细注释可以看IBM的那篇技术文章。其核心语句就是一个轮询的函数:

    dev->poll

    就调用了相应设备的poll函数。也就是说,当CPU处理软中断时,才去轮询网卡,把数据放入softnet_data中。

    下面是整个中断和轮询过程的一个示意图:

    下面我们解释一下poll函数具体干了什么事情。

    而我们知道,在Realtek 8139网卡的net_device对象中我们已经注册了一个poll函数:

    dev->poll = rtl8139_poll

    那么一次poll就表示从网卡缓冲区取出一定量的数据。而rtl8139_poll函数中调用的主要函数就是rtl8139_rx函数。这个函数是完成从网卡取数据,分配skb缓冲区的核心函数。其核心代码如下:

    static int rtl8139_rx(struct net_device *dev, struct rtl8139_private *tp, int budget)
    {
         skb 
    = dev_alloc_skb (pkt_size + 2
    );
         eth_copy_and_sum (skb, 
    &rx_ring[ring_offset + 4], pkt_size, 0);//memcpy

         skb->protocol =  eth_type_trans (skb, dev);
         netif_receive_skb (skb);
    }

    工作主要分为4部分:

    (a)给sk_buff数据结构(skb)分配空间。

    (b)从网卡的环形缓冲区rx_ring中拷贝出网络数据包放到sk_buff对象skb中。这个函数实质上就是一个memcpy函数。

    (c)在skb中标识其协议为以太网帧。

    (d)调用netif_receice_skb函数。
    netif_receive_skb函数相对比较重要。函数主体是两个循环:

    list_for_each_entry_rcu(ptype, & ptype_all, list) 
    {
        
    if (!ptype->dev || ptype->dev == skb->
    dev) 
         {
            
    if
     (pt_prev) 
                 ret 
    =
     deliver_skb(skb, pt_prev);
             pt_prev 
    =
     ptype;
             }
    }

    list_for_each_entry_rcu(ptype, 
    &ptype_base[ntohs(type)&15
    ], list) {
        
    if (ptype->type == type && (!ptype->dev || ptype->dev == skb->
    dev)) 
         {
            
    if
     (pt_prev) 
                 ret 
    =
     deliver_skb(skb, pt_prev);
             pt_prev 
    =
     ptype;
         }
    }

    两个循环分别遍历了两个链表:ptype_all和ptype_base。前者是内核中注册的sniffer,后者则是注册到内核协议栈中的网络协议类型。如果skb中的协议类型type与ptype_base中的类型一致,那么使用deliver_skb函数发送给这个协议一份,定义如下:

    static __inline__ int deliver_skb(struct sk_buff *skb, struct packet_type* pt_prev)
    {
             atomic_inc(
    &skb->
    users);
            
    return pt_prev->func(skb, skb->
    dev, pt_prev);
    }

    这个函数只是一个封装函数,实际上调用了每个packet type结构中注册的处理函数func。

    struct  packet_type {
             unsigned 
    short           type;   

            
    struct net_device               *dev;   
             int                      (*func) (struct sk_buff *
                              struct net_device *struct packet_type *
    );
            
    void                    *
    af_packet_priv;
            
    struct
     list_head         list;
    };

    例如:IP包类型的处理函数就是ip_rcv(定义在/net/ipv4/ip_output.c文件中),定义如下:

    static struct packet_type ip_packet_type =  {
             .type 
    =
     __constant_htons(ETH_P_IP),
             .func 
    =
     ip_rcv,
    };

    这个包的类型是在ip_init协议初始化时添加到全局的ptype_base哈希数组中的:

    void __init ip_init(void )
    {
             dev_add_pack(
    &
    ip_packet_type);
    }
    展开全文
  • 准备将readme.md作为笔记,记录linux内核网络协议栈源码剖析的过程 天2,2016年11月18日20:24:07 增加了对sys_socket(), sock_create, __sock_create(), inet_create()的完全注释 socket的创建过程已经完成,接下来...
  • Linux内核网络协议栈:udp数据包发送(源码解读)》 目录 LINUX内核网络数据包发送(一) 1. 前言 2. 数据包发送宏观视角 3. 协议层注册 4. 通过 socket 发送网络数据 4.1sock_sendmsg,__sock_sendmsg,__...

    监视和调整Linux网络协议栈:接收数据

    监控和调整Linux网络协议栈的图解指南:接收数据

    Linux网络 - 数据包的接收过程

    Linux网络协议栈:网络包接收过程

    Linux内核网络协议栈:udp数据包发送(源码解读)

    目录

    LINUX内核网络数据包发送(一)

    1. 前言

    2. 数据包发送宏观视角

    3. 协议层注册

    4. 通过 socket 发送网络数据

    4.1 sock_sendmsg, __sock_sendmsg, __sock_sendmsg_nosec

    4.2 inet_sendmsg

    5. 总结

    Linux网络 - 数据包的发送过程

    LINUX内核网络数据包发送(二)——UDP协议层分析

    1. 前言

    2. udp_sendmsg

    2.1 UDP corking

    2.2 获取目的 IP 地址和端口

    2.3 Socket 发送:bookkeeping 和打时间戳

    2.4 辅助消息(Ancillary messages)

    2.5 设置自定义 IP 选项

    2.6 多播或单播(Multicast or unicast)

    2.7 路由

    2.8 MSG_CONFIRM: 阻止 ARP 缓存过期

    2.9 uncorked UDP sockets 快速路径:准备待发送数据

    2.9.1 ip_make_skb

    2.9.2 发送数据

    2.10 没有被 cork 的数据时的慢路径

    2.10.1 ip_append_data

    2.10.2 __ip_append_data

    2.10.3 Flushing corked sockets

    2.11 Error accounting

    3. udp_send_skb

    4. 监控:UDP 层统计

    4.1 /proc/net/snmp

    4.2 /proc/net/udp

    5. 调优:socket 发送队列内存大小

    6. 总结


    LINUX内核网络数据包发送(一)

    http://kerneltravel.net/blog/2020/network_ljr11/


    1. 前言


    本文首先从宏观上概述了数据包发送的流程,接着分析了协议层注册进内核以及被socket的过程,最后介绍了通过 socket 发送网络数据的过程。

     

    2. 数据包发送宏观视角


    从宏观上看,一个数据包从用户程序到达硬件网卡的整个过程如下:

    1. 使用系统调用(如 sendtosendmsg 等)写数据
    2. 数据穿过socket 子系统,进入socket 协议族(protocol family)系统
    3. 协议族处理:数据穿过协议层,这一过程(在许多情况下)会将数据(data)转换成数据包(packet)
    4. 数据穿过路由层,这会涉及路由缓存和 ARP 缓存的更新;如果目的 MAC 不在 ARP 缓存表中,将触发一次 ARP 广播来查找 MAC 地址
    5. 穿过协议层,packet 到达设备无关层(device agnostic layer)
    6. 使用 XPS(如果启用)或散列函数选择发送队列
    7. 调用网卡驱动的发送函数
    8. 数据传送到网卡的 qdisc(queue discipline,排队规则)
    9. qdisc 会直接发送数据(如果可以),或者将其放到队列,下次触发NET_TX 类型软中断(softirq)的时候再发送
    10. 数据从 qdisc 传送给驱动程序
    11. 驱动程序创建所需的DMA 映射,以便网卡从 RAM 读取数据
    12. 驱动向网卡发送信号,通知数据可以发送了
    13. 网卡从 RAM 中获取数据并发送
    14. 发送完成后,设备触发一个硬中断(IRQ),表示发送完成
    15. 硬中断处理函数被唤醒执行。对许多设备来说,这会触发 NET_RX 类型的软中断,然后 NAPI poll 循环开始收包
    16. poll 函数会调用驱动程序的相应函数,解除 DMA 映射,释放数据

     

    3. 协议层注册


    协议层分析我们将关注 IP 和 UDP 层,其他协议层可参考这个过程。我们首先来看协议族是如何注册到内核,并被 socket 子系统使用的。

    当用户程序像下面这样创建 UDP socket 时会发生什么?

    sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
    

    简单来说,内核会去查找由 UDP 协议栈导出的一组函数(其中包括用于发送和接收网络数据的函数),并赋给 socket 的相应字段。准确理解这个过程需要查看 AF_INET 地址族的代码。

    内核初始化的很早阶段就执行了 inet_init 函数,这个函数会注册 AF_INET 协议族 ,以及该协议族内的各协议栈(TCP,UDP,ICMP 和 RAW),并调用初始化函数使协议栈准备好处理网络数据。inet_init 定义在net/ipv4/af_inet.c 。

    AF_INET 协议族导出一个包含 create 方法的 struct net_proto_family 类型实例。当从用户程序创建 socket 时,内核会调用此方法:

    static const struct net_proto_family inet_family_ops = {
        .family = PF_INET,
        .create = inet_create,
        .owner  = THIS_MODULE,
    };
    

    inet_create 根据传递的 socket 参数,在已注册的协议中查找对应的协议:

    /* Look for the requested type/protocol pair. */
    lookup_protocol:
            err = -ESOCKTNOSUPPORT;
            rcu_read_lock();
            list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
    
                    err = 0;
                    /* Check the non-wild match. */
                    if (protocol == answer->protocol) {
                            if (protocol != IPPROTO_IP)
                                    break;
                    } else {
                            /* Check for the two wild cases. */
                            if (IPPROTO_IP == protocol) {
                                    protocol = answer->protocol;
                                    break;
                            }
                            if (IPPROTO_IP == answer->protocol)
                                    break;
                    }
                    err = -EPROTONOSUPPORT;
            }
    

    然后,将该协议的回调方法(集合)赋给这个新创建的 socket:

    sock->ops = answer->ops;
    

    可以在 af_inet.c 中看到所有协议的初始化参数。下面是TCP 和 UDP的初始化参数:

    /* Upon startup we insert all the elements in inetsw_array[] into
     * the linked list inetsw.
     */
    static struct inet_protosw inetsw_array[] =
    {
            {
                    .type =       SOCK_STREAM,
                    .protocol =   IPPROTO_TCP,
                    .prot =       &tcp_prot,
                    .ops =        &inet_stream_ops,
                    .no_check =   0,
                    .flags =      INET_PROTOSW_PERMANENT |
                                  INET_PROTOSW_ICSK,
            },
    
            {
                    .type =       SOCK_DGRAM,
                    .protocol =   IPPROTO_UDP,
                    .prot =       &udp_prot,
                    .ops =        &inet_dgram_ops,
                    .no_check =   UDP_CSUM_DEFAULT,
                    .flags =      INET_PROTOSW_PERMANENT,
           },
    
                /* .... more protocols ... */
    

    IPPROTO_UDP 协议类型有一个 ops 变量,包含很多信息,包括用于发送和接收数据的回调函数:

    const struct proto_ops inet_dgram_ops = {
    	.family          = PF_INET,
    	.owner           = THIS_MODULE,
    	
    	/* ... */
    	
    	.sendmsg     = inet_sendmsg,
    	.recvmsg     = inet_recvmsg,
    	
    	/* ... */
    };
    EXPORT_SYMBOL(inet_dgram_ops);
    

    prot 字段指向一个协议相关的变量(的地址),对于 UDP 协议,其中包含了 UDP 相关的回调函数。UDP 协议对应的 prot 变量为 udp_prot,定义在 net/ipv4/udp.c:

    struct proto udp_prot = {
    	.name        = "UDP",
    	.owner           = THIS_MODULE,
    	
    	/* ... */
    	
    	.sendmsg     = udp_sendmsg,
    	.recvmsg     = udp_recvmsg,
    	
    	/* ... */
    };
    EXPORT_SYMBOL(udp_prot);
    

    现在,让我们转向发送 UDP 数据的用户程序,看看 udp_sendmsg 是如何在内核中被调用的。

     

    4. 通过 socket 发送网络数据


    用户程序想发送 UDP 网络数据,因此它使用 sendto 系统调用:

    ret = sendto(socket, buffer, buflen, 0, &dest, sizeof(dest));
    

    该系统调用穿过Linux 系统调用(system call)层,最后到达net/socket.c中的这个函数:

    /*
     *      Send a datagram to a given address. We move the address into kernel
     *      space and check the user space data area is readable before invoking
     *      the protocol.
     */
    
    SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
                    unsigned int, flags, struct sockaddr __user *, addr,
                    int, addr_len)
    {
        /*  ... code ... */
    
        err = sock_sendmsg(sock, &msg, len);
    
        /* ... code  ... */
    }
    

    SYSCALL_DEFINE6 宏会展开成一堆宏,后者经过一波复杂操作创建出一个带 6 个参数的系统调用(因此叫 DEFINE6)。作为结果之一,会看到内核中的所有系统调用都带 sys_前缀。

    sendto 代码会先将数据整理成底层可以处理的格式,然后调用 sock_sendmsg。特别地, 它将传递给 sendto 的地址放到另一个变量(msg)中:

    iov.iov_base = buff;
    iov.iov_len = len;
    msg.msg_name = NULL;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = NULL;
    msg.msg_controllen = 0;
    msg.msg_namelen = 0;
    if (addr) {
            err = move_addr_to_kernel(addr, addr_len, &address);
            if (err < 0)
                    goto out_put;
            msg.msg_name = (struct sockaddr *)&address;
            msg.msg_namelen = addr_len;
    }
    

    这段代码将用户程序传入到内核的(存放待发送数据的)地址,作为 msg_name 字段嵌入到 struct msghdr 类型变量中。这和用户程序直接调用 sendmsg 而不是 sendto 发送数据差不多,这之所以可行,是因为 sendto 和 sendmsg 底层都会调用 sock_sendmsg

     

    4.1 sock_sendmsg__sock_sendmsg__sock_sendmsg_nosec


    sock_sendmsg 做一些错误检查,然后调用__sock_sendmsg;后者做一些自己的错误检查 ,然后调用__sock_sendmsg_nosec__sock_sendmsg_nosec 将数据传递到 socket 子系统的更深处:

    static inline int __sock_sendmsg_nosec(struct kiocb *iocb, struct socket *sock,
                                           struct msghdr *msg, size_t size)
    {
        struct sock_iocb *si =  ....
    
        /* other code ... */
    
        return sock->ops->sendmsg(iocb, sock, msg, size);
    }
    

    通过前面介绍的 socket 创建过程,可以知道注册到这里的 sendmsg 方法就是 inet_sendmsg

     

    4.2 inet_sendmsg


    从名字可以猜到,这是 AF_INET 协议族提供的通用函数。此函数首先调用 sock_rps_record_flow 来记录最后一个处理该(数据所属的)flow 的 CPU; Receive Packet Steering 会用到这个信息。接下来,调用 socket 的协议类型(本例是 UDP)对应的 sendmsg 方法:

    int inet_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,
                     size_t size)
    {
          struct sock *sk = sock->sk;
    
          sock_rps_record_flow(sk);
    
          /* We may need to bind the socket. */
          if (!inet_sk(sk)->inet_num && !sk->sk_prot->no_autobind && inet_autobind(sk))
                  return -EAGAIN;
    
          return sk->sk_prot->sendmsg(iocb, sk, msg, size);
    }
    EXPORT_SYMBOL(inet_sendmsg);
    

    本例是 UDP 协议,因此上面的 sk->sk_prot->sendmsg 指向的是之前看到的(通过 udp_prot 导出的)udp_sendmsg 函数。

    sendmsg()函数作为分界点,处理逻辑从 AF_INET 协议族通用处理转移到具体的 UDP 协议的处理。

     

    5. 总结


    了解Linux内核网络数据包发送的详细过程,有助于我们进行网络监控和调优。本文只分析了协议层的注册和通过 socket 发送数据的过程,数据在传输层和网络层的详细发送过程将在下一篇文章中分析。

    参考链接:

    [1] https://blog.packagecloud.io/eng/2017/02/06/monitoring-tuning-linux-networking-stack-sending-data

    [2] https://segmentfault.com/a/1190000008926093

     

    Linux网络 - 数据包的发送过程

    https://segmentfault.com/a/1190000008926093


     

     

    LINUX内核网络数据包发送(二)——UDP协议层分析

    http://kerneltravel.net/blog/2020/network_ljr12/


    1. 前言


    本文分享了Linux内核网络数据包发送在UDP协议层的处理,主要分析了udp_sendmsgudp_send_skb函数,并分享了UDP层的数据统计和监控以及socket发送队列大小的调优。

     

    2. udp_sendmsg


    这个函数定义在 net/ipv4/udp.c,函数很长,分段来看。

     

    2.1 UDP corking


    在变量声明和基本错误检查之后,udp_sendmsg 所做的第一件事就是检查 socket 是否“ 塞住”了(corked)。UDP corking 是一项优化技术,允许内核将多次数据累积成单个数据报发送。在用户程序中有两种方法可以启用此选项:

    • 使用 setsockopt 系统调用设置 socket 的 UDP_CORK 选项

    • 程序调用 sendsendto 或 sendmsg 时,带 MSG_MORE 参数

    udp_sendmsg 代码检查 up->pending 以确定 socket 当前是否已被塞住(corked),如果是, 则直接跳到 do_append_data 进行数据追加(append)。

    int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
                    size_t len)
    {
    
        /* variables and error checking ... */
    
      fl4 = &inet->cork.fl.u.ip4;
      if (up->pending) {
              /*
               * There are pending frames.
               * The socket lock must be held while it's corked.
               */
              lock_sock(sk);
              if (likely(up->pending)) {
                      if (unlikely(up->pending != AF_INET)) {
                              release_sock(sk);
                              return -EINVAL;
                      }
                      goto do_append_data;
              }
              release_sock(sk);
      }
    

    2.2 获取目的 IP 地址和端口


    接下来获取目的 IP 地址和端口,有两个可能的来源:

    • 如果之前 socket 已经建立连接,那 socket 本身就存储了目标地址

    • 地址通过辅助结构(struct msghdr)传入,正如我们在 sendto 的内核代码中看到的那样

    具体逻辑:

    /*
     *      Get and verify the address.
     */
      if (msg->msg_name) {
              struct sockaddr_in *usin = (struct sockaddr_in *)msg->msg_name;
              if (msg->msg_namelen < sizeof(*usin))
                      return -EINVAL;
              if (usin->sin_family != AF_INET) {
                      if (usin->sin_family != AF_UNSPEC)
                              return -EAFNOSUPPORT;
              }
    
              daddr = usin->sin_addr.s_addr;
              dport = usin->sin_port;
              if (dport == 0)
                      return -EINVAL;
      } else {
              if (sk->sk_state != TCP_ESTABLISHED)
                      return -EDESTADDRREQ;
              daddr = inet->inet_daddr;
              dport = inet->inet_dport;
              /* Open fast path for connected socket.
                 Route will not be used, if at least one option is set.
               */
              connected = 1;
      }
    

    UDP 代码中出现了 TCP_ESTABLISHED!UDP socket 的状态使用了 TCP 状态来描述。上面的代码显示了内核如何解析该变量以便设置 daddr 和 dport

    如果没有 struct msghdr 变量,内核函数到达 udp_sendmsg 函数时,会从 socket 本身检索目的地址和端口,并将 socket 标记为“已连接”。

     

    2.3 Socket 发送:bookkeeping 和打时间戳


    接下来,获取存储在 socket 上的源地址、设备索引(device index)和时间戳选项(例如SOCK_TIMESTAMPING_TX_HARDWARESOCK_TIMESTAMPING_TX_SOFTWARESOCK_WIFI_STATUS):

    ipc.addr = inet->inet_saddr;
    
    ipc.oif = sk->sk_bound_dev_if;
    
    sock_tx_timestamp(sk, &ipc.tx_flags);
    

    2.4 辅助消息(Ancillary messages)


    除了发送或接收数据包之外,sendmsg 和 recvmsg 系统调用还允许用户设置或请求辅助数据。用户程序可以通过将请求信息组织成 struct msghdr 类型变量来利用此辅助数据。一些辅助数据类型记录在IP man page中 。

    辅助数据的一个常见例子是 IP_PKTINFO。对于 sendmsgIP_PKTINFO 允许程序在发送数据时设置一个 in_pktinfo 变量。程序可以通过填写 struct in_pktinfo 变量中的字段来指定要在 packet 上使用的源地址。如果程序是监听多个 IP 地址的服务端程序,那这是一个很有用的选项。在这种情况下,服务端可能想使用客户端连接服务端的那个 IP 地址来回复客户端,IP_PKTINFO 非常适合这种场景。

    setsockopt 可以在socket 级别设置发送包的 IP_TTL和 IP_TOS。而辅助消息允许在每个数据包级别设置 TTL 和 TOS 值。Linux 内核会使用一个数组将 TOS 转换为优先级,后者会影响数据包如何以及何时从 qdisc 中发送出去。

    可以看到内核如何在 UDP socket 上处理 sendmsg 的辅助消息:

    if (msg->msg_controllen) {
            err = ip_cmsg_send(sock_net(sk), msg, &ipc,
                               sk->sk_family == AF_INET6);
            if (err)
                    return err;
            if (ipc.opt)
                    free = 1;
            connected = 0;
    }
    

    解析辅助消息的工作是由 ip_cmsg_send 完成的,定义在 net/ipv4/ip_sockglue.c 。传递一个未初始化的辅助数据,将会把这个 socket 标记为“未建立连接的”。

     

    2.5 设置自定义 IP 选项


    接下来,sendmsg 将检查用户是否通过辅助消息设置了的任何自定义 IP 选项。如果设置了 ,将使用这些自定义值;如果没有,那就使用 socket 中(已经在用)的参数:

    if (!ipc.opt) {
            struct ip_options_rcu *inet_opt;
    
            rcu_read_lock();
            inet_opt = rcu_dereference(inet->inet_opt);
            if (inet_opt) {
                    memcpy(&opt_copy, inet_opt,
                           sizeof(*inet_opt) + inet_opt->opt.optlen);
                    ipc.opt = &opt_copy.opt;
            }
            rcu_read_unlock();
    }
    

    接下来,该函数检查是否设置了源记录路由(source record route, SRR)IP 选项。SRR 有两种类型:宽松源记录路由和严格源记录路由。如果设置了此选项,则会记录第一跳地址并将其保存到 faddr,并将 socket 标记为“未连接”。这将在后面用到:

    ipc.addr = faddr = daddr;
    
    if (ipc.opt && ipc.opt->opt.srr) {
            if (!daddr)
                    return -EINVAL;
            faddr = ipc.opt->opt.faddr;
            connected = 0;
    }
    

    处理完 SRR 选项后,将处理 TOS 选项,这可以从辅助消息中获取,或者从 socket 当前值中获取。然后检查:

    1. 是否(使用 setsockopt)在 socket 上设置了 SO_DONTROUTE,或
    2. 是否(调用 sendto 或 sendmsg 时)指定了 MSG_DONTROUTE 标志,或
    3. 是否已设置了 is_strictroute,表示需要严格的 SRR 任何一个为真,tos 字段的 RTO_ONLINK 位将置 1,并且 socket 被视为“未连接”:
    tos = get_rttos(&ipc, inet);
    if (sock_flag(sk, SOCK_LOCALROUTE) ||
        (msg->msg_flags & MSG_DONTROUTE) ||
        (ipc.opt && ipc.opt->opt.is_strictroute)) {
            tos |= RTO_ONLINK;
            connected = 0;
    }
    

    2.6 多播或单播(Multicast or unicast)


    接下来代码开始处理 multicast。这有点复杂,因为用户可以通过 IP_PKTINFO 辅助消息 来指定发送包的源地址或设备号,如前所述。

    如果目标地址是多播地址:

    1. 将多播设备(device)的索引(index)设置为发送(写)这个 packet 的设备索引,并且

    2. packet 的源地址将设置为 multicast 源地址

    如果目标地址不是一个组播地址,则发送 packet 的设备制定为 inet->uc_index(单播), 除非用户使用 IP_PKTINFO 辅助消息覆盖了它。

    if (ipv4_is_multicast(daddr)) {
            if (!ipc.oif)
                    ipc.oif = inet->mc_index;
            if (!saddr)
                    saddr = inet->mc_addr;
            connected = 0;
    } else if (!ipc.oif)
            ipc.oif = inet->uc_index;
    

    2.7 路由


    现在开始路由,UDP 层中处理路由的代码以快速路径(fast path)开始。如果 socket 已连接,则直接尝试获取路由:

    if (connected)
            rt = (struct rtable *)sk_dst_check(sk, 0);
    

    如果 socket 未连接,或者虽然已连接,但路由辅助函数 sk_dst_check 认定路由已过期,则代码将进入慢速路径(slow path)以生成一条路由记录。首先调用 flowi4_init_output 构造一个描述此 UDP 流的变量:

    if (rt == NULL) {
            struct net *net = sock_net(sk);
    
            fl4 = &fl4_stack;
            flowi4_init_output(fl4, ipc.oif, sk->sk_mark, tos,
                               RT_SCOPE_UNIVERSE, sk->sk_protocol,
                               inet_sk_flowi_flags(sk)|FLOWI_FLAG_CAN_SLEEP,
                               faddr, saddr, dport, inet->inet_sport);
    

    然后,socket 及其 flow 实例会传递给安全子系统,这样 SELinux 或 SMACK 这样的系统就可以在 flow 实例上设置安全 ID。接下来,ip_route_output_flow 将调用 IP 路由代码,创建一个路由实例:

    security_sk_classify_flow(sk, flowi4_to_flowi(fl4));
    rt = ip_route_output_flow(net, fl4, sk);
    

    如果创建路由实例失败,并且返回码是 ENETUNREACH, 则 OUTNOROUTES 计数器将会加 1。

    if (IS_ERR(rt)) {
      err = PTR_ERR(rt);
      rt = NULL;
      if (err == -ENETUNREACH)
        IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES);
      goto out;
    }
    

    这些统计计数器所在的源文件、其他可用的计数器及其含义,将在下面的 UDP 监控部分分享。

    接下来,如果是广播路由,但 socket 的 SOCK_BROADCAST 选项未设置,则处理过程终止。如果 socket 被视为“已连接”,则路由实例将缓存到 socket 上:

    err = -EACCES;
    if ((rt->rt_flags & RTCF_BROADCAST) &&
        !sock_flag(sk, SOCK_BROADCAST))
            goto out;
    if (connected)
            sk_dst_set(sk, dst_clone(&rt->dst));
    

    2.8 MSG_CONFIRM: 阻止 ARP 缓存过期


    如果调用 sendsendto 或 sendmsg 的时候指定了 MSG_CONFIRM 参数,UDP 协议层将会如下处理:

      if (msg->msg_flags&MSG_CONFIRM)
              goto do_confirm;
    back_from_confirm:
    

    该标志提示系统去确认一下 ARP 缓存条目是否仍然有效,防止其被垃圾回收。 do_confirm 标签位于此函数末尾处:

    do_confirm:
            dst_confirm(&rt->dst);
            if (!(msg->msg_flags&MSG_PROBE) || len)
                    goto back_from_confirm;
            err = 0;
            goto out;
    

    dst_confirm 函数只是在相应的缓存条目上设置一个标记位,稍后当查询邻居缓存并找到 条目时将检查该标志,我们后面一些会看到。此功能通常用于 UDP 网络应用程序,以减少不必要的 ARP 流量。此代码确认缓存条目然后跳回 back_from_confirm 标签。一旦 do_confirm 代码跳回到 back_from_confirm(或者之前就没有执行到 do_confirm ),代码接下来将处理 UDP cork 和 uncorked 情况。

     

    2.9 uncorked UDP sockets 快速路径:准备待发送数据


    如果不需要 corking,数据就可以封装到一个 struct sk_buff 实例中并传递给 udp_send_skb,离 IP 协议层更进了一步。这是通过调用 ip_make_skb 来完成的。

    先前通过调用 ip_route_output_flow 生成的路由条目也会一起传进来, 它将保存到 skb 里。

    /* Lockless fast path for the non-corking case. */
    if (!corkreq) {
            skb = ip_make_skb(sk, fl4, getfrag, msg->msg_iov, ulen,
                              sizeof(struct udphdr), &ipc, &rt,
                              msg->msg_flags);
            err = PTR_ERR(skb);
            if (!IS_ERR_OR_NULL(skb))
                    err = udp_send_skb(skb, fl4);
            goto out;
    }
    

    ip_make_skb 函数将创建一个 skb,其中需要考虑到很多的事情,例如:

    1. MTU

    2. UDP corking(如果启用)

    3. UDP Fragmentation Offloading(UFO)

    4. Fragmentation(分片):如果硬件不支持 UFO,但是要传输的数据大于 MTU,需要软件做分片

    大多数网络设备驱动程序不支持 UFO,因为网络硬件本身不支持此功能。我们来看下这段代码,先看 corking 禁用的情况。

     

    2.9.1 ip_make_skb


    定义在net/ipv4/ip_output.c,这个函数有点复杂。

    构建 skb 的时候,ip_make_skb 依赖的底层代码需要使用一个 corking 变量和一个 queue 变量 ,skb 将通过 queue 变量传入。如果 socket 未被 cork,则会传入一个假的 corking 变量和一个空队列。

    现在来看看假 corking 变量和空队列是如何初始化的:

    struct sk_buff *ip_make_skb(struct sock *sk, /* more args */)
    {
            struct inet_cork cork;
            struct sk_buff_head queue;
            int err;
    
            if (flags & MSG_PROBE)
                    return NULL;
    
            __skb_queue_head_init(&queue);
    
            cork.flags = 0;
            cork.addr = 0;
            cork.opt = NULL;
            err = ip_setup_cork(sk, &cork, /* more args */);
            if (err)
                    return ERR_PTR(err);
    

    如上所示,cork 和 queue 都是在栈上分配的,ip_make_skb 根本不需要它。 ip_setup_cork 初始化 cork 变量。接下来,调用__ip_append_data 并传入 cork 和 queue 变 量:

    err = __ip_append_data(sk, fl4, &queue, &cork,
                           &current->task_frag, getfrag,
                           from, length, transhdrlen, flags);
    

    我们将在后面看到这个函数是如何工作的,因为不管 socket 是否被 cork,最后都会执行它。

    现在,我们只需要知道__ip_append_data 将创建一个 skb,向其追加数据,并将该 skb 添加 到传入的 queue 变量中。如果追加数据失败,则调用__ip_flush_pending_frame 丢弃数据 并向上返回错误(指针类型):

    if (err) {
            __ip_flush_pending_frames(sk, &queue, &cork);
            return ERR_PTR(err);
    }
    

    最后,如果没有发生错误,__ip_make_skb 将 skb 出队,添加 IP 选项,并返回一个准备好传递给更底层发送的 skb:

    return __ip_make_skb(sk, fl4, &queue, &cork);
    

    2.9.2 发送数据


    如果没有错误,skb 就会交给 udp_send_skb,后者会继续将其传给下一层协议,IP 协议:

    err = PTR_ERR(skb);
    if (!IS_ERR_OR_NULL(skb))
            err = udp_send_skb(skb, fl4);
    goto out;
    

    如果有错误,错误计数就会有相应增加。后面的“错误计数”部分会详细介绍。

     

    2.10 没有被 cork 的数据时的慢路径


    如果使用了 UDP corking,但之前没有数据被 cork,则慢路径开始:

    1. 对 socket 加锁
    2. 检查应用程序是否有 bug:已经被 cork 的 socket 是否再次被 cork
    3. 设置该 UDP flow 的一些参数,为 corking 做准备
    4. 将要发送的数据追加到现有数据

    udp_sendmsg 代码继续向下看,就是这一逻辑:

      lock_sock(sk);
      if (unlikely(up->pending)) {
              /* The socket is already corked while preparing it. */
              /* ... which is an evident application bug. --ANK */
              release_sock(sk);
    
              LIMIT_NETDEBUG(KERN_DEBUG pr_fmt("cork app bug 2\n"));
              err = -EINVAL;
              goto out;
      }
      /*
       *      Now cork the socket to pend data.
       */
      fl4 = &inet->cork.fl.u.ip4;
      fl4->daddr = daddr;
      fl4->saddr = saddr;
      fl4->fl4_dport = dport;
      fl4->fl4_sport = inet->inet_sport;
      up->pending = AF_INET;
    
    do_append_data:
      up->len += ulen;
      err = ip_append_data(sk, fl4, getfrag, msg->msg_iov, ulen,
                           sizeof(struct udphdr), &ipc, &rt,
                           corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);
    

    2.10.1 ip_append_data


    这个函数简单封装了__ip_append_data,在调用后者之前,做了两件重要的事情:

    1. 检查是否从用户传入了 MSG_PROBE 标志。该标志表示用户不想真正发送数据,只是做路径探测(例如,确定PMTU)

    2. 检查 socket 的发送队列是否为空。如果为空,意味着没有 cork 数据等待处理,因此调用 ip_setup_cork 来设置 corking

    一旦处理了上述条件,就调用__ip_append_data 函数,该函数包含用于将数据处理成数据包的大量逻辑。

     

    2.10.2 __ip_append_data


    如果 socket 是 corked,则从 ip_append_data 调用此函数;如果 socket 未被 cork,则从 ip_make_skb 调用此函数。在任何一种情况下,函数都将分配一个新缓冲区来存储传入的数据,或者将数据附加到现有数据中。这种工作的方式围绕 socket 的发送队列。等待发送的现有数据(例如,如果 socket 被 cork) 将在队列中有一个对应条目,可以被追加数据。

    这个函数很复杂,它执行很多计算以确定如何构造传递给下面的网络层的 skb。

    该函数的重点包括:

    1. 如果硬件支持,则处理 UDP Fragmentation Offload(UFO)。绝大多数网络硬件不支持 UFO。如果你的网卡驱动程序支持它,它将设置 NETIF_F_UFO 标记位
    2. 处理支持分散/收集( scatter/gather)IO 的网卡。许多 卡都支持此功能,并使用 NETIF_F_SG 标志进行通告。支持该特性的网卡可以处理数据 被分散到多个 buffer 的数据包;内核不需要花时间将多个缓冲区合并成一个缓冲区中。避 免这种额外的复制会提升性能,大多数网卡都支持此功能
    3. 通过调用 sock_wmalloc 跟踪发送队列的大小。当分配新的 skb 时,skb 的大小由创建它 的 socket 计费(charge),并计入 socket 发送队列的已分配字节数。如果发送队列已经 没有足够的空间(超过计费限制),则 skb 并分配失败并返回错误。我们将在下面的调优部分中看到如何设置 socket 发送队列大小(txqueuelen)
    4. 更新错误统计信息。此函数中的任何错误都会增加“discard”计数。我们将在下面的监控部分中看到如何读取此值

    函数执行成功后返回 0,以及一个适用于网络设备传输的 skb。

    • 在 unorked 情况下,持有 skb 的 queue 被作为参数传递给上面描述的__ip_make_skb,在那里 它被出队并通过 udp_send_skb 发送到更底层。

    • 在 cork 的情况下,__ip_append_data 的返回值向上传递。数据位于发送队列中,直到 udp_sendmsg 确定是时候调用 udp_push_pending_frames 来完成 skb,后者会进一步调用 udp_send_skb

     

    2.10.3 Flushing corked sockets


    现在,udp_sendmsg 会继续,检查__ip_append_skb 的返回值(错误码):

    if (err)
            udp_flush_pending_frames(sk);
    else if (!corkreq)
            err = udp_push_pending_frames(sk);
    else if (unlikely(skb_queue_empty(&sk->sk_write_queue)))
            up->pending = 0;
    release_sock(sk);
    

    我们来看看每个情况:

    1. 如果出现错误(错误为非零),则调用 udp_flush_pending_frames,这将取消 cork 并从 socket 的发送队列中删除所有数据

    2. 如果在未指定 MSG_MORE 的情况下发送此数据,则调用 udp_push_pending_frames,它将数据传递到更下面的网络层

    3. 如果发送队列为空,请将 socket 标记为不再 cork

    如果追加操作完成并且有更多数据要进入 cork,则代码将做一些清理工作,并返回追加数据的长度:

    ip_rt_put(rt);
    if (free)
            kfree(ipc.opt);
    if (!err)
            return len;
    

    这就是内核如何处理 corked UDP sockets 的。

     

    2.11 Error accounting


    如果:

    1. non-corking 快速路径创建 skb 失败,或 udp_send_skb 返回错误,或
    2. ip_append_data 无法将数据附加到 corked UDP socket,或
    3. 当 udp_push_pending_frames 调用 udp_send_skb 发送 corked skb 时后者返回错误

    仅当返回的错误是 ENOBUFS(内核无可用内存)或 socket 已设置 SOCK_NOSPACE(发送队列已满)时,SNDBUFERRORS 统计信息才会增加:

    /*
     * ENOBUFS = no kernel mem, SOCK_NOSPACE = no sndbuf space.  Reporting
     * ENOBUFS might not be good (it's not tunable per se), but otherwise
     * we don't have a good statistic (IpOutDiscards but it can be too many
     * things).  We could add another new stat but at least for now that
     * seems like overkill.
     */
    if (err == -ENOBUFS || test_bit(SOCK_NOSPACE, &sk->sk_socket->flags)) {
            UDP_INC_STATS_USER(sock_net(sk),
                            UDP_MIB_SNDBUFERRORS, is_udplite);
    }
    return err;
    

    我们接下来会在后面的数据监控里看到如何读取这些计数。

     

    3. udp_send_skb


    udp_sendmsg 通过调用 udp_send_skb 函数将 skb 送到下一网络层,在本文中是 IP 协议层。这个函数做了一些重要的事情:

    1. 向 skb 添加 UDP 头
    2. 处理校验和:软件校验和,硬件校验和或无校验和(如果禁用)
    3. 调用 ip_send_skb 将 skb 发送到 IP 协议层
    4. 更新发送成功或失败的统计计数器

    首先,创建 UDP 头:

    static int udp_send_skb(struct sk_buff *skb, struct flowi4 *fl4)
    {
                    /* useful variables ... */
    
            /*
             * Create a UDP header
             */
            uh = udp_hdr(skb);
            uh->source = inet->inet_sport;
            uh->dest = fl4->fl4_dport;
            uh->len = htons(len);
            uh->check = 0;
    

    接下来,处理校验和。有几种情况:

    1. 首先处理 UDP-Lite 校验和
    2. 接下来,如果 socket 校验和选项被关闭(setsockopt 带 SO_NO_CHECK 参数),它将被标记为校 验和关闭
    3. 接下来,如果硬件支持 UDP 校验和,则将调用 udp4_hwcsum 来设置它。请注意,如果数 据包是分段的,内核将在软件中生成校验和,可以在 udp4_hwcsum 的源代码中看到这一点
    4. 最后,通过调用 udp_csum 生成软件校验和
    if (is_udplite)                                  /*     UDP-Lite      */
            csum = udplite_csum(skb);
    
    else if (sk->sk_no_check == UDP_CSUM_NOXMIT) {   /* UDP csum disabled */
    
            skb->ip_summed = CHECKSUM_NONE;
            goto send;
    
    } else if (skb->ip_summed == CHECKSUM_PARTIAL) { /* UDP hardware csum */
    
            udp4_hwcsum(skb, fl4->saddr, fl4->daddr);
            goto send;
    
    } else
            csum = udp_csum(skb);
    

    接下来,添加了伪头 :

    uh->check = csum_tcpudp_magic(fl4->saddr, fl4->daddr, len,
                                  sk->sk_protocol, csum);
    if (uh->check == 0)
            uh->check = CSUM_MANGLED_0;
    

    如果校验和为 0,则根据 RFC 768,校验为全 1( transmitted as all ones (the equivalent in one’s complement arithmetic))。最后,将 skb 传递给 IP 协议层并增加统计计数:

    send:
      err = ip_send_skb(sock_net(sk), skb);
      if (err) {
              if (err == -ENOBUFS && !inet->recverr) {
                      UDP_INC_STATS_USER(sock_net(sk),
                                         UDP_MIB_SNDBUFERRORS, is_udplite);
                      err = 0;
              }
      } else
              UDP_INC_STATS_USER(sock_net(sk),
                                 UDP_MIB_OUTDATAGRAMS, is_udplite);
      return err;
    

    如果 ip_send_skb 成功,将更新 OUTDATAGRAMS 统计。如果 IP 协议层报告错误,并且错误 是 ENOBUFS(内核缺少内存)而且错误 queue(inet->recverr)没有启用,则更新 SNDBUFERRORS

    接下来看看如何在 Linux 内核中监视和调优 UDP 协议层。

     

    4. 监控:UDP 层统计


    两个非常有用的获取 UDP 协议统计文件:

    • /proc/net/snmp
    • /proc/net/udp

     

    4.1 /proc/net/snmp


    监控 UDP 协议层统计:

    cat /proc/net/snmp | grep Udp\:
    

    要准确地理解这些计数,需要仔细地阅读内核代码。一些类型的错误计数并不是只出现在一种计数中,而可能是出现在多个计数中。

    • InDatagrams: Incremented when recvmsg was used by a userland program to read datagram. Also incremented when a UDP packet is encapsulated and sent back for processing.
    • NoPorts: Incremented when UDP packets arrive destined for a port where no program is listening.
    • InErrors: Incremented in several cases: no memory in the receive queue, when a bad checksum is seen, and if sk_add_backlog fails to add the datagram.
    • OutDatagrams: Incremented when a UDP packet is handed down without error to the IP protocol layer to be sent.
    • RcvbufErrors: Incremented when sock_queue_rcv_skb reports that no memory is available; this happens if sk->sk_rmem_alloc is greater than or equal to sk->sk_rcvbuf.
    • SndbufErrors: Incremented if the IP protocol layer reported an error when trying to send the packet and no error queue has been setup. Also incremented if no send queue space or kernel memory are available.
    • InCsumErrors: Incremented when a UDP checksum failure is detected. Note that in all cases I could find, InCsumErrors is incremented at the same time as InErrors. Thus, InErrors - InCsumErros should yield the count of memory related errors on the receive side.

    UDP 协议层发现的某些错误会出现在其他协议层的统计信息中。一个例子:路由错误 。 udp_sendmsg 发现的路由错误将导致 IP 协议层的 OutNoRoutes 统计增加。

     

    4.2 /proc/net/udp


    监控 UDP socket 统计:

    cat /proc/net/udp
    

    每一列的意思:

    • sl: Kernel hash slot for the socket
    • local_address: Hexadecimal local address of the socket and port number, separated by :.
    • rem_address: Hexadecimal remote address of the socket and port number, separated by :.
    • st: The state of the socket. Oddly enough, the UDP protocol layer seems to use some TCP socket states. In the example above, 7 is TCP_CLOSE.
    • tx_queue: The amount of memory allocated in the kernel for outgoing UDP datagrams.
    • rx_queue: The amount of memory allocated in the kernel for incoming UDP datagrams.
    • tr, tm->when, retrnsmt: These fields are unused by the UDP protocol layer.
    • uid: The effective user id of the user who created this socket.
    • timeout: Unused by the UDP protocol layer.
    • inode: The inode number corresponding to this socket. You can use this to help you determine which user process has this socket open. Check /proc/[pid]/fd, which will contain symlinks to socket[:inode].
    • ref: The current reference count for the socket.
    • pointer: The memory address in the kernel of the struct sock.
    • drops: The number of datagram drops associated with this socket. Note that this does not include any drops related to sending datagrams (on corked UDP sockets or otherwise); this is only incremented in receive paths as of the kernel version examined by this blog post.

    打印这些计数的代码在net/ipv4/udp.c。

     

    5. 调优:socket 发送队列内存大小


    发送队列(也叫“写队列”)的最大值可以通过设置 net.core.wmem_max sysctl 进行修改。

    $ sudo sysctl -w net.core.wmem_max=8388608
    

    sk->sk_write_queue 用 net.core.wmem_default 初始化, 这个值也可以调整。

    调整初始发送 buffer 大小:

    $ sudo sysctl -w net.core.wmem_default=8388608
    

    也可以通过从应用程序调用 setsockopt 并传递 SO_SNDBUF 来设置 sk->sk_write_queue 。通过 setsockopt 设置的最大值是 net.core.wmem_max

    不过,可以通过 setsockopt 并传递 SO_SNDBUFFORCE 来覆盖 net.core.wmem_max 限制, 这需要 CAP_NET_ADMIN 权限。

    每次调用__ip_append_data 分配 skb 时,sk->sk_wmem_alloc 都会递增。正如我们所看到 的,UDP 数据报传输速度很快,通常不会在发送队列中花费太多时间。

     

    6. 总结


    本文重点分析了数据包在传输层(UDP协议)的发送过程,并进行了监控和调优,后面数据包将到达 IP 协议层,下次再分享,感谢阅读。

    Reference:https://blog.packagecloud.io/eng/2017/02/06/monitoring-tuning-linux-networking-stack-sending-data

     

    展开全文
  • Linux 内核网络协议栈源码剖析】系统网络协议栈初始化及数据传输通道建立过程
  • Linux内核网络协议栈笔记1:协议栈分层/层次结构 大家都知道TCP/IP协议栈现在是世界上最流行的网络协议栈,恐怕它的普及的最重要的原因就是其清晰的层次结构以及清晰定义的原语和接口。不仅使得上层应用开发者可以...
  • 深入浅出Linux内核网络协议栈 视频讲解如下,点击观看: 深入浅出Linux内核网络协议栈|结构sk C/C++Linux服务器开发高级架构师知识点精彩内容包括:C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,MongoDB,ZK,流...
  • DDD#阅读和理解Linux内核网络协议栈 nginx高并发设计优秀思想替代其他高并发代理中间件: redis,nginx,memcache,twemproxy,mongodb等更多中间件,分布式系统,高效服务端核心思想实现博客: ==================...
  • linux内核网络协议栈架构分析,全流程分析-干货

    万次阅读 多人点赞 2017-07-22 00:29:23
    内核协议栈架构分析,全流程分析,干货
  • Linux内核网络协议栈代码分析

    千次阅读 2014-12-23 17:24:02
    一.linux内核网络栈代码的准备知识   1. linux内核ipv4网络部分分层结构:   BSD socket层: 这一部分处理BSD socket相关操作,每个socket在内核中以struct socket结构体现。这一部分的文件   主要有:/...
  • https://blog.csdn.net/zxorange321/article/details/75676063
  • linux 内核网络协议栈阅读理解--带详尽注释以及相关流程调用注释,对理解内核协议栈源码很有帮助对理解阅读 linux 协议栈源码很用帮助github 地址:... 转载于:https://www.cnb...
  • 深度剖析网络协议栈中的 socket 函数,可以说是把前面介绍的串联起来,将网络协议栈各层关联起来。
  • 5、内核收包流程细化(中断收包) 6、应用层收包流程 7、UDP发包流程 原文链接:...
  • 内核的路由部分是是网络中重要部分,目前在Linux内核中默认的路由查找算法使用的是Hash查找,所以你会看到很多的数据结构是XXX_hash什么之类(例如fn_hash)。Linux内核从2.1开始就支持基于策略的路由,那么什么是基于...
  • Linux版本: 3.10.103 网卡驱动: ixgbev 报文收发简单流程 网卡驱动默认采用的是NAPI的报文处理方式。即中断+轮询的方式,网卡收到一个报文之后会产生接收中断,并且屏蔽中断,直到收够了netdev_max_backlog个报文...
  • linux内核网络协议栈学习笔记(3)

    千次阅读 2012-08-07 22:42:09
    这篇主题是内核二层包的接受发送,先来看接收: 首先关注下几个状态值 __QUEUE_STATE_FROZEN:发送队列被锁 __QUEUE_STATE_XOFF:发送队列发送功能关闭 __LINK_STATE_START:设备是否开启 __LINK_STATE_PRESENT:...
  • (1)路由:跨越从源主机到目标主机的一个互联网络来转发数据包的过程; (2)路由器:能够将数据包转发到正确的目的地,并在转发过程中选择最佳路径的设备; (3)路由表:在路由器中维护的路由条目,路由器根据...
  • 网卡需要有驱动才能工作,驱动是加载到内核中的模块,负责衔接网卡和内核网络模块,驱动在加载的时候将自己注册进网络模块,当相应的网卡收到数据包时,网络模块会调用相应的驱动程序处理数据。
  • ARP地址解析协议理论前篇参见ARP与RARP,这里则通过源码(Linux kernel 1.2.13;net\inet\arp.c)来剖析其内部原理及实现过程。
  • //free=0,表示对数据包进行缓存,一旦发生丢弃的情况,进行数据包重传(可靠性数据传输协议) if (!free) { unsigned long flags; /* The socket now has more outstanding blocks */ sk->packets_out++;//...
  • listen 函数仅供服务器端调用,把一个未连接的套接字转换为一个被动套接字,指示内核应接受指向该套接字的连接请求。 1、应用层——listen 函数 #include int listen(int sockfd, int backlog); /*sockfd是bind之后...
  • 服务器端中调用recvfrom函数,并未指定发送端(客户端)的地址,换句话说这个函数是一个被动函数,有点类似于tcp协议中服务器listen 之后阻塞,等待客户端connect。这里则是服务器端recvfrom后,等待客户端
  • linux内核网络协议栈学习笔记:vlan

    千次阅读 2012-10-02 01:38:36
    这篇是我临时加的,本来不打算放在整个内核协议栈分析的系列里的,但我现在觉得vlan还是蛮重要的,而且讨论vlan源码的文章很少,不知道我这篇算不算第一篇 :D vlan的代码都在net/8021q/的内核目录下,首先我们来看...
  •  作为内核网络协议部分最重要的数据结构SKB,有很多值得仔细推敲的问题。   SKB这种说法实际包含了两部分,即skb描述符和skb数据。 Skb描述符即内核中的skbuff结构体,里面含有大量的指针变量,运用指针的好处是...
  • 前面介绍的函数基本上都是TCP协议的,如listen,connect,accept 等函数,这都是为可靠传输协议TCP定制的。对于另一个不可靠udp协议(通信系统其可靠性交由上层应用层负责),则主要由两个函数完成,sendto 和 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 39,001
精华内容 15,600
关键字:

linux内核网络协议栈

linux 订阅