网络数据包收发流程(三):e1000网卡和DMA

https://i-blog.csdnimg.cn/blog_migrate/1676ace7586c4c0b4f0895ace8f5b28e.jpeg


早就想整理网络数据包收发流程了,一直太懒没动笔。今天下决心写了

一、硬件环境

intel82546:PHY与MAC集成在一起的PCI网卡芯片,很强大
bcm5461:   PHY芯片,与之对应的MAC是TSEC
TSEC:      Three Speed Ethernet Controller,三速以太网控制器,PowerPc 架构CPU里面的MAC模块
            注意,TSEC内部有DMA子模块 

话说现在的CPU越来越牛叉了,什么功能都往里面加,最常见的如MAC功能。
TSEC只是MAC功能模块的一种,其他架构的cpu也有和TSEC类似的MAC功能模块。
这些集成到CPU芯片上的功能模块有个学名,叫平台设备,即 platform device。

二、网络收包原理


网络驱动收包大致有3种情况:

no NAPI:mac每收到一个以太网包,都会产生一个接收中断给cpu,即完全靠中断方式来收包
          缺点是当网络流量很大时,cpu大部分时间都耗在了处理mac的中断。

netpoll:在网络和I/O子系统尚不能完整可用时,模拟了来自指定设备的中断,即轮询收包。
         缺点是实时性差

NAPI: 采用 中断 + 轮询 的方式:mac收到一个包来后会产生接收中断,但是马上关闭。
       直到收够了netdev_max_backlog个包(默认300),或者收完mac上所有包后,才再打开接收中断
       通过sysctl来修改 net.core.netdev_max_backlog
       或者通过proc修改 /proc/sys/net/core/netdev_max_backlog


下面只写内核配置成使用NAPI的情况,只写TSEC驱动。(非NAPI的情况和PCI网卡驱动 以后再说)
内核版本 linux 2.6.24

三、NAPI 相关数据结构

每个网络设备(MAC层)都有自己的net_device数据结构,这个结构上有napi_struct。
每当收到数据包时,网络设备驱动会把自己的napi_struct挂到CPU私有变量上。
这样在软中断时,net_rx_action会遍历cpu私有变量的poll_list,
执行上面所挂的napi_struct结构的poll钩子函数,将数据包从驱动传到网络协议栈。

四、内核启动时的准备工作

4.1 初始化网络相关的全局数据结构,并挂载处理网络相关软中断的钩子函数
start_kernel()
    --> rest_init()
        --> do_basic_setup()
            --> do_initcall
               -->net_dev_init

__init net_dev_init()
{
    //每个CPU都有一个CPU私有变量 _get_cpu_var(softnet_data)
    //_get_cpu_var(softnet_data).poll_list很重要,软中断中需要遍历它的

    for_each_possible_cpu(i) {
        struct softnet_data *queue;
        queue = &per_cpu(softnet_data, i);
        skb_queue_head_init(&queue->input_pkt_queue);
        queue->completion_queue = NULL;
      INIT_LIST_HEAD(&queue->poll_list);
        queue->backlog.poll = process_backlog;
        queue->backlog.weight = weight_p;
    }
    open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);//在软中断上挂网络发送handler
    open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);//在软中断上挂网络接收handler
}
  
4.2 加载网络设备的驱动
NOTE:这里的网络设备是指MAC层的网络设备,即TSEC和PCI网卡(bcm5461是phy)
在网络设备驱动中创建net_device数据结构,并初始化其钩子函数 open(),close() 等
挂载TSEC的驱动的入口函数是 gfar_probe

// 平台设备 TSEC 的数据结构
static struct platform_driver gfar_driver = {
    .probe = gfar_probe,
    .remove = gfar_remove,
    .driver = {
        .name = "fsl-gianfar",
    },
};

int gfar_probe(struct platform_device *pdev)
{
    dev = alloc_etherdev(sizeof (*priv)); // 创建net_device数据结构

    dev->open = gfar_enet_open;
    dev->hard_start_xmit = gfar_start_xmit;
    dev->tx_timeout = gfar_timeout;
    dev->watchdog_timeo = TX_TIMEOUT;
#ifdef CONFIG_GFAR_NAPI
    netif_napi_add(dev, &priv->napi,gfar_poll,GFAR_DEV_WEIGHT);//软中断里会调用poll钩子函数
#endif
#ifdef CONFIG_NET_POLL_CONTROLLER
    dev->poll_controller = gfar_netpoll;
#endif
    dev->stop = gfar_close;
    dev->change_mtu = gfar_change_mtu;
    dev->mtu = 1500;
    dev->set_multicast_list = gfar_set_multi;
    dev->set_mac_address = gfar_set_mac_address;
    dev->ethtool_ops = &gfar_ethtool_ops;
}

五、启用网络设备
5.1 用户调用ifconfig等程序,然后通过ioctl系统调用进入内核
socket的ioctl()系统调用
    --> sock_ioctl()
        --> dev_ioctl()                              //判断SIOCSIFFLAGS
          --> __dev_get_by_name(net, ifr->ifr_name)  //根据名字选net_device
             --> dev_change_flags()                  //判断IFF_UP
                --> dev_open(net_device)             //调用open钩子函数

对于TSEC来说,挂的钩子函数是 gfar_enet_open(net_device)

5.2 在网络设备的open钩子函数里,分配接收bd,挂中断ISR(包括rx、tx、err),对于TSEC来说

gfar_enet_open
    --> 给Rx Tx Bd 分配一致性DMA内存
    --> 把Rx Bd的“EA地址”赋给数据结构,物理地址赋给TSEC寄存器
    --> 把Tx Bd的“EA地址”赋给数据结构,物理地址赋给TSEC寄存器
    --> 给 tx_skbuff 指针数组 分配内存,并初始化为NULL
    --> 给 rx_skbuff 指针数组 分配内存,并初始化为NULL

    --> 初始化Tx Bd
    --> 初始化Rx Bd,提前分配存储以太网包的skb,这里使用的是一次性dma映射
       (注意:#define DEFAULT_RX_BUFFER_SIZE  1536保证了skb能存一个以太网包)
        rxbdp = priv->rx_bd_base;
        for (i = 0; i < priv->rx_ring_size; i++) {
            struct sk_buff *skb = NULL;
            rxbdp->status = 0;
            //这里真正分配skb,并且初始化rxbpd->bufPtr, rxbdpd->length
           skb = gfar_new_skb(dev, rxbdp);   
            priv->rx_skbuff[i] = skb;

            rxbdp++;
        }
        rxbdp--;
        rxbdp->status |= RXBD_WRAP; // 给最后一个bd设置标记WRAP标记
       
    --> 注册TSEC相关的中断handler: 错误,接收,发送
        request_irq(priv->interruptError, gfar_error, 0, "enet_error", dev)
        request_irq(priv->interruptTransmit, gfar_transmit, 0, "enet_tx", dev)//包发送完
        request_irq(priv->interruptReceive, gfar_receive, 0, "enet_rx", dev) //包接收完

    -->gfar_start(net_device)
        // 使能Rx、Tx
        // 开启TSEC的 DMA 寄存器
        // Mask 掉我们不关心的中断event


最终,TSEC相关的Bd等数据结构应该是下面这个样子的

六、中断里接收以太网包

TSEC的RX已经使能了,网络数据包进入内存的流程为:
    网线 --> Rj45网口 --> MDI 差分线
         --> bcm5461(PHY芯片进行数模转换) --> MII总线
         --> TSEC的DMA Engine 会自动检查下一个可用的Rx bd
         --> 把网络数据包 DMA 到 Rx bd 所指向的内存,即skb->data


接收到一个完整的以太网数据包后,TSEC会根据event mask触发一个 Rx 外部中断。
cpu保存现场,根据中断向量,开始执行外部中断处理函数do_IRQ()

do_IRQ 伪代码
{
   上半部处理硬中断
       查看中断源寄存器,得知是网络外设产生了外部中断
       执行网络设备的rx中断handler(设备不同,函数不同,但流程类似,TSEC是gfar_receive
          1. mask 掉 rx event,再来数据包就不会产生rx中断
          2. 给napi_struct.state加上 NAPI_STATE_SCHED 状态
          3. 挂网络设备自己的napi_struct结构到cpu私有变量_get_cpu_var(softnet_data).poll_list
          4. 触发网络接收软中断
    下半部处理软中断
        依次执行所有软中断handler,包括timer,tasklet等等
        执行网络接收的软中断handler net_rx_action
          1. 遍历cpu私有变量_get_cpu_var(softnet_data).poll_list
          2. 取出poll_list上面挂的napi_struct 结构,执行钩子函数napi_struct.poll()
             (设备不同,钩子函数不同,流程类似,TSEC是gfar_poll)
          3. 若poll钩子函数处理完所有包,则打开rx event mask,再来数据包的话会产生rx中断
          4. 调用napi_complete(napi_struct *n)
             把napi_struct 结构从_get_cpu_var(softnet_data).poll_list 上移走
             同时去掉 napi_struct.state 的 NAPI_STATE_SCHED 状态
}

6.1 TSEC的接收中断处理函数
gfar_receive
{
#ifdef CONFIG_GFAR_NAPI
    // test_and_set当前net_device的napi_struct.state 为 NAPI_STATE_SCHED
    // 在软中断里调用 net_rx_action 会检查状态 napi_struct.state

    if (netif_rx_schedule_prep(dev, &priv->napi)) { 
        tempval = gfar_read(&priv->regs->imask);           
        tempval &= IMASK_RX_DISABLED; //mask掉rx,不再产生rx中断
        gfar_write(&priv->regs->imask, tempval);   
        // 将当前net_device的 napi_struct.poll_list 挂到
        // CPU私有变量__get_cpu_var(softnet_data).poll_list 上,并触发软中断
        // 所以,在软中断中调用 net_rx_action 的时候,就会执行当前net_device的
        // napi_struct.poll()钩子函数,即 gfar_poll()

        __netif_rx_schedule(dev, &priv->napi);  
    }
#else
    gfar_clean_rx_ring(dev, priv->rx_ring_size);
#endif
}

6.2 网络接收软中断net_rx_action
net_rx_action()
{
    struct list_head *list = &__get_cpu_var(softnet_data).poll_list;   
    //通过 napi_struct.poll_list, 将N多个 napi_struct 链接到一条链上
    //通过 CPU私有变量,我们找到了链头,然后开始遍历这个链


    int budget = netdev_budget; //这个值就是 net.core.netdev_max_backlog,通过sysctl来修改

    while (!list_empty(list)) {
        struct napi_struct *n;
        int work, weight;
        local_irq_enable();
        //从链上取一个 napi_struct 结构(接收中断处理函数里加到链表上的,如gfar_receive)
        n = list_entry(list->next, struct napi_struct, poll_list);
        weight = n->weight;
        work = 0;
        if (test_bit(NAPI_STATE_SCHED, &n->state)) //检查状态标记,此标记在接收中断里加上的  
            work = n->poll(n, weight); //使用NAPI的话,使用的是网络设备自己的napi_struct.poll
                                       //对于TSEC是,是gfar_poll
        WARN_ON_ONCE(work > weight);
        budget -= work;
        local_irq_disable();

        if (unlikely(work == weight)) {
            if (unlikely(napi_disable_pending(n)))
                __napi_complete(n); //操作napi_struct,把去掉NAPI_STATE_SCHED状态,从链表中删去
            else
                list_move_tail(&n->poll_list, list);
        }
        netpoll_poll_unlock(have);
    }
out:
    local_irq_enable();
}

static int gfar_poll(struct napi_struct *napi, int budget)
{
    struct gfar_private *priv = container_of(napi, struct gfar_private, napi);
    struct net_device *dev = priv->dev;  //TSEC对应的网络设备
    int howmany; 
    //根据dev的rx bd,获取skb并送入协议栈,返回处理的skb的个数,即以太网包的个数
    howmany = gfar_clean_rx_ring(dev, budget);
   // 下面这个判断比较有讲究的
    // 收到的包的个数小于budget,代表我们在一个软中断里就全处理完了,所以打开 rx中断
    // 要是收到的包的个数大于budget,表示一个软中断里处理不完所有包,那就不打开rx 中断,
    // 待到下一个软中断里再接着处理,直到把所有包处理完(即howmany<budget),再打开rx 中断

    if (howmany < budget) {       
        netif_rx_complete(dev, napi);
        gfar_write(&priv->regs->rstat, RSTAT_CLEAR_RHALT);
        //打开 rx 中断,rx 中断是在gfar_receive()中被关闭的
        gfar_write(&priv->regs->imask, IMASK_DEFAULT);
    }
    return howmany;
}         

gfar_clean_rx_ring(dev, budget)
{
    bdp = priv->cur_rx;
    while (!((bdp->status & RXBD_EMPTY) || (--rx_work_limit < 0))) {
        rmb();
       skb = priv->rx_skbuff[priv->skb_currx];//从rx_skbugg[]中获取skb
        howmany++;
        dev->stats.rx_packets++;
        pkt_len = bdp->length - 4;  //从length中去掉以太网包的FCS长度
        gfar_process_frame(dev, skb, pkt_len);
        dev->stats.rx_bytes += pkt_len;
        dev->last_rx = jiffies;
        bdp->status &= ~RXBD_STATS;  //清rx bd的状态
   
        skb = gfar_new_skb(dev, bdp); // Add another skb for the future
        priv->rx_skbuff[priv->skb_currx] = skb;

        if (bdp->status & RXBD_WRAP)  //更新指向bd的指针
            bdp = priv->rx_bd_base;   //bd有WARP标记,说明是最后一个bd了,需要“绕回来”
        else
            bdp++;
        priv->skb_currx = (priv->skb_currx + 1) & RX_RING_MOD_MASK(priv->rx_ring_size);
    }
    priv->cur_rx = bdp; /* Update the current rxbd pointer to be the next one */
    return howmany;
}
       
gfar_process_frame() 
    -->RECEIVE(skb) //调用netif_receive_skb(skb)进入协议栈


#ifdef CONFIG_GFAR_NAPI
#define RECEIVE(x) netif_receive_skb(x)
#else
#define RECEIVE(x) netif_rx(x)
#endif

------------------------------------ 华丽的分割线 ---------------------------------------


上一篇讲的是内核配置成NAPI的情况,那也是绝大多数内核使用的配置
现在讲讲内核不配置成NAPI时的情况

一、no NAPI 数据结构

不配置NAPI的时候,网络设备不使用自己的napi_struct结构,
所有网络设备驱动都使用同一个napi_struct,即cpu私有变量__get_cpu_var(softnet_data).backlog

每当收到数据包时,网络设备驱动会把__get_cpu_var(softnet_data).backlog挂到__get_cpu_var(softnet_data).poll_list上面。

所以软中断里net_rx_action遍历cpu私有变量__get_cpu_var(softnet_data).poll_list时,
上面挂的napi_struct只有一个

二、内核启动时的准备工作

也是在net_dev_init中,初始化了cpu私有变量的napi_struct,即所有网络设备驱动使用的napi_struct

__init net_dev_init()
{
    //每个CPU都有一个私有变量 _get_cpu_var(softnet_data)
    //
_get_cpu_var(softnet_data).poll_list很重要,软中断中需要遍历它的
    for_each_possible_cpu(i) {
        struct softnet_data *queue;
        queue = &per_cpu(softnet_data, i);
        skb_queue_head_init(&queue->input_pkt_queue);// 不配置NAPI时,才使用这个接收队列
        queue->completion_queue = NULL;
     INIT_LIST_HEAD(&queue->poll_list);
        queue->backlog.poll = process_backlog;       // poll钩子函数初始化
        queue->backlog.weight = weight_p;             //

    }
    open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL); //在软中断上挂网络接收handler
    open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL); //在软中断上挂网络发送handler

}

三、中断里接受以太网包

TSEC的接收中断处理函数

gfar_receive

{
    gfar_write(&priv->regs->ievent, IEVENT_RX_MASK);
#ifdef CONFIG_GFAR_NAPI
    // test_and_set当前net_device的napi_struct.state 为 NAPI_STATE_SCHED
    // 在软中断里调用 net_rx_action 会检查状态 napi_struct.state
    if (netif_rx_schedule_prep(dev, &priv->napi)) { 
        tempval = gfar_read(&priv->regs->imask);           
        tempval &= IMASK_RX_DISABLED;
        gfar_write(&priv->regs->imask, tempval);   
        // 将当前net_device的 napi_struct.poll_list 挂到
        // CPU私有变量 &__get_cpu_var(softnet_data).poll_list 上,并触发软中断
        // 所以,在软中断中调用 net_rx_action 的时候,就会执行当前net_device的
        // napi_struct.poll()钩子函数,即 gfar_poll()
        __netif_rx_schedule(dev, &priv->napi);  
    }

#else
    gfar_clean_rx_ring(dev, priv->rx_ring_size);
#endif
}

gfar_clean_rx_ring  
  
-->gfar_process_frame 
      -->初始化了skb->dev,这样在软中断里才能判断这个数据包来自哪里
      -->RECEIVE(skb) // 调用netif_rx(skb)


#ifdef CONFIG_GFAR_NAPI
#define RECEIVE(x) netif_receive_skb(x)
#else
#define RECEIVE(x) netif_rx(x)
#endif

netif_rx(skb)
{
   queue = &__get_cpu_var(softnet_data);
   __skb_queue_tail(&queue->input_pkt_queue, skb); //将skb放到接收队列(在net_dev_init初始化)中
   napi_schedule(&queue->backlog); //将cpu私有变量的的napi_struct挂到cpu私有变量的poll_list上
                                   //test_and_set napi_struct.state为 NAPI_STATE_SCHED
                                   //触发网络接收软中断
}

软中断net_rx_action中调用poll钩子函数

虽说软中断里也遍历cpu私有变量的poll_list,事实上poll_list现在只挂一个napi_struct结构
即cpu私有变量的backlog成员(它在net_dev_init中初始化),所以现在调用的poll钩子函数就是process_backlog了

static int process_backlog(struct napi_struct *napi, int quota)
{
    struct softnet_data *queue = &__get_cpu_var(softnet_data);
    napi->weight = weight_p;
    do {
        struct sk_buff *skb;
        struct net_device *dev;

        local_irq_disable();
        skb = __skb_dequeue(&queue->input_pkt_queue); //从接收队列中取出skb,
        if (!skb) {                                   //这些skb是在netif_rx中进入队列的
            __napi_complete(napi);
            local_irq_enable();
            break;
        }
        local_irq_enable();
        dev = skb->dev;
        netif_receive_skb(skb);     //进入协议协议栈
        dev_put(dev);
    } while (++work < quota && jiffies == start_time);
    return work;
}
http://blog.chinaunix.net/space.php?uid=24148050&do=blog&id=473352

一、硬件布局

每个网卡(MAC)都有自己的专用DMA Engine,如上图的 TSEC 和 e1000 网卡intel82546。
上图中的红色线就是以太网数据流,DMA与DDR打交道需要其他模块的协助,如TSEC,PCI controller
以太网数据在 TSEC<-->DDR  PCI_Controller<-->DDR 之间的流动,CPU的core是不需要介入的
只有在数据流动结束时(接收完、发送完),DMA Engine才会以外部中断的方式告诉CPU的core

二、DMA Engine

上面是DMA Engine的框图,以接收为例:
1.在System memory中为DMA开辟一端连续空间,用来BD数组  (一致性dma内存)
  BD是给DMA Engine使用的,所以不同的设备,BD结构不同,但是大致都有状态、长度、指针3个成员。

2.初始化BD数组,status为E,length为0
   在System memory中再开辟一块一块的内存,可以不连续,用来存放以太网包
   将这些内存块的总线地址赋给buf(dma映射)

3.当MAC接收以太网数据流,放在了Rx FIFO中

4.当一个以太网包接收完全后,DMA engine依次做以下事情
    fetch bd:开始一个个的遍历BD数组,直到当前BD状态为Empty为止
    update bd:更新BD状态为Ready
    move data:把数据从Rx FIFO中搬移到System Memory中dma映射的部分
    generate interrupt:数据搬移完了,产生外部中断给cpu core

5.cpu core处理外部中断,此时以太网数据已经在System memory中dma映射的部分了
    解除dma映射,更新bd状态为Empty
    再开辟一端内存,将这块内存的总线地址赋给bd的指针字段

三、内核中DMA相关API

void *dma_alloc_cohrent(struct device *dev, size_t size, dma_addr_t *dma_handle, int flag);
功能:分配一致性dma内存,返回这块内存的虚拟地址EA, 这块内存的物理地址保存在 dma_handle
dev:  NULL也行
size: 分配空间的大小
dma_handle: 用来保存内存的总线地址(物理地址)

注意:一致性DMA映射BD所占内存就是靠dma_alloc_cohrent来分配的。

dma_addr_t *dma_map_single(struct device *dev, void *buffer, size_t size, enum dma_data_direction);
功能:将一块连续的内存 buffer 映射为DMA内存来使用。映射后,CPU不能再操作这块 buffer
返回:这块buffer的总线地址(物理地址)
dev: NULL也行
buffer: 一块连续内存的虚拟地址EA
size: 连续内存的大小
dma_data_direction: dma数据流的方向

注意:流式DMA映射以太网包所占内存先通过kmalloc来分配,然后通过dma_map_single来映射给bd的

四、e1000驱动中的DMA

网卡驱动中使用DMA的套路差不多都一样,以e1000驱动为例讲一下(
TSEC驱动的dma见这里

4.1 加载e1000网卡驱动

e1000_probe(){                        //主要是初始化钩子函数
     netdev = alloc_etherdev(sizeof(struct e1000_adapter));
    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;
    e1000_set_ethtool_ops(netdev);
    netdev->tx_timeout = &e1000_tx_timeout;
    netdev->watchdog_timeo = 5 * HZ;
#ifdef CONFIG_E1000_NAPI
    netif_napi_add(netdev, &adapter->napi, e1000_clean, 64); //重要
#endif
}

4.1 启动e1000网卡

   e1000_open() //当用户敲ifconfig up命令时,最终调用网卡驱动的open函数
     -->e1000_setup_all_rx_resources(adapter)
         -->e1000_setup_rx_resources(adapter, &adapter->rx_ring[i])
               //给rx bd分配一致性dma内存
               rxdr->desc = pci_alloc_consistent(pdev, rxdr->size, &rxdr->dma);
     -->e1000_configure(adapter)
         -->e1000_configure_rx(adapter)
               adapter->clean_rx = e1000_clean_rx_irq;
               adapter->alloc_rx_buf = e1000_alloc_rx_buffers;
         -->调用 adapter->alloc_rx_buf钩子函数,即 e1000_alloc_rx_buffers
                --> skb = netdev_alloc_skb(netdev, bufsz); //调用kmalloc新建一个skb
                    buffer_info->dma = pci_map_single(pdev,
                        skb->data,
                        adapter->rx_buffer_len,
                        PCI_DMA_FROMDEVICE);               //给skb->data建立DMA映射
                    rx_desc->buffer_addr = cpu_to_le64(buffer_info->dma);//初始化bd的buf指针
     -->e1000_request_irq(adapter);
        //挂rx 中断ISR函数为 e1000_intr()

最终bd数据结构应该是下面这个样子
      
4.2 e1000的中断

注意:e1000产生rx中断时,以太网数据包已经在系统内存中,即在skb->data里面
下面的中断处理过程就简略了,详细的看这里
do_IRQ()
{
    中断上半部
       调用e1000网卡的rx中断函数 e1000_intr()
          触发软中断 (使用NAPI的话)
    中断下半部
       依次调用软中断的所有handler
       在net_rx_action中最终调用e1000的napi_struct.poll()钩子函数,即e1000_clean
       e1000_clean()最终调用 e1000_clean_rx_irq()
}

e1000_clean_rx_irq()
{
     rx_desc = E1000_RX_DESC(*rx_ring, i); //获取rx bd
     status = rx_desc->status;
     skb = buffer_info->skb;
     buffer_info->skb = NULL;

     pci_unmap_single(pdev,                //解除skb->data的DMA映射
                   buffer_info->dma,
                   buffer_info->length,
                   PCI_DMA_FROMDEVICE);
     length = le16_to_cpu(rx_desc->length);
     length -= 4;                          //以太网包的FCS校验就不要了
     skb_put(skb, length);
     skb->protocol = eth_type_trans(skb, netdev);
     netif_receive_skb(skb);               //skb进入协议栈
}


进入函数netif_receive_skb()后,skb正式开始协议栈之旅。
先上图,协议栈大致过程如下所示:

跟OSI七层模型不同,linux根据包结构对网络进行分层。
比如,arp头和ip头都是紧跟在以太网头后面的,所以在linux协议栈中arp和ip地位相同(如上图)
但是在OSI七层模型中,arp属于链路层,ip属于网络层.....
这里就不死抠概念,我们就说arp,ip都属于第二层。下面是网络第二层的处理流程

一、相关数据结构
内核处理网络第二层,有下面2个重要list_head变量 (文件linux_2_6_24/net/core/dev.c)
list_head 链表上挂了很多packet_type数据结构

static struct list_head ptype_base[16] __read_mostly;   /* 16 way hashed list */
static struct list_head ptype_all __read_mostly;        /* Taps */

struct packet_type {
    __be16 type;                /* This is really htons(ether_type).*/
    struct net_device   *dev;   /* NULL is wildcarded here       */
    int     (*func) (struct sk_buff *,
                     struct net_device *,
                     struct packet_type *,
                     struct net_device *);
    struct sk_buff    *(*gso_segment)(struct sk_buff *skb, int features);
    int    (*gso_send_check)(struct sk_buff *skb);
    void   *af_packet_priv;
    struct list_head    list;
};

type 成员保存了二层协议类型,ETH_P_IP、ETH_P_ARP等等
func 成员就是钩子函数了,如 ip_rcv()、arp_rcv()等等

二、操作packet_type的API
//把packet_type结构挂在与type对应的list_head上面
void dev_add_pack(struct packet_type *pt){
    int hash;
    spin_lock_bh(&ptype_lock);
    if (pt->type == htons(ETH_P_ALL))        //type为ETH_P_ALL时,挂在ptype_all上面
        list_add_rcu(&pt->list, &ptype_all);
    else {
        hash = ntohs(pt->type) & 15;         //否则,挂在ptype_base[type&15]上面
        list_add_rcu(&pt->list, &ptype_base[hash]);
    }
    spin_unlock_bh(&ptype_lock);
}

//把packet_type从list_head上删除
void dev_remove_pack(struct packet_type *pt){
    __dev_remove_pack(pt);
    synchronize_net();
}
void __dev_remove_pack(struct packet_type *pt){
    struct list_head *head;
    struct packet_type *pt1;
    spin_lock_bh(&ptype_lock);
    if (pt->type == htons(ETH_P_ALL))
        head = &ptype_all;                        //找到链表头
    else
        head = &ptype_base[ntohs(pt->type) & 15]; //

    list_for_each_entry(pt1, head, list) {
        if (pt == pt1) {
            list_del_rcu(&pt->list);
            goto out;
        }
    }
    printk(KERN_WARNING "dev_remove_pack: %p not found.\n", pt);
out:
    spin_unlock_bh(&ptype_lock);
}

三、进入二层协议处理函数
int netif_receive_skb(struct sk_buff *skb)
{
   //略去一些代码
    rcu_read_lock();
    //第一步:先处理 ptype_all 上所有的 packet_type->func()           
    //所有包都会调func,对性能影响严重!内核默认没挂任何钩子函数

    list_for_each_entry_rcu(ptype, &ptype_all, list) { //遍历ptye_all链表
        if (!ptype->dev || ptype->dev == skb->dev) {    //上面的paket_type.type 为 ETH_P_ALL
            if (pt_prev)                                //对所有包调用paket_type.func()
                ret = deliver_skb(skb, pt_prev, orig_dev); //此函数最终调用paket_type.func()
            pt_prev = ptype;
        }
    }
    //第二步:若编译内核时选上BRIDGE,下面会执行网桥模块
    //调用函数指针 br_handle_frame_hook(skb), 在动态模块 linux_2_6_24/net/bridge/br.c中
    //br_handle_frame_hook = br_handle_frame;
    //所以实际函数 br_handle_frame。
    //注意:在此网桥模块里初始化 skb->pkt_type 为 PACKET_HOST、PACKET_OTHERHOST

    skb = handle_bridge(skb, &pt_prev, &ret, orig_dev);
    if (!skb) goto out;

    //第三步:编译内核时选上MAC_VLAN模块,下面才会执行
    //调用 macvlan_handle_frame_hook(skb), 在动态模块linux_2_6_24/drivers/net/macvlan.c中
    //macvlan_handle_frame_hook = macvlan_handle_frame;
    //所以实际函数为 macvlan_handle_frame。
    //注意:此函数里会初始化 skb->pkt_type 为 PACKET_BROADCAST、PACKET_MULTICAST、PACKET_HOST

    skb = handle_macvlan(skb, &pt_prev, &ret, orig_dev);
    if (!skb)  goto out;

    //第四步:最后 type = skb->protocol; &ptype_base[ntohs(type)&15]
    //处理ptype_base[
ntohs(type)&15]上的所有的 packet_type->func()
    //根据第二层不同协议来进入不同的钩子函数,重要的有:ip_rcv() arp_rcv()
    type = skb->protocol;
    list_for_each_entry_rcu(ptype, &ptype_base[ntohs(type)&15], list) {
        if (ptype->type == type &&                      //遍历包type所对应的链表
            (!ptype->dev || ptype->dev == skb->dev)) {  //调用链表上所有pakcet_type.func()
            if (pt_prev)
                ret = deliver_skb(skb, pt_prev, orig_dev); //就这里!arp包会调arp_rcv()
            pt_prev = ptype;                               //        ip包会调ip_rcv()
        }
    }
    if (pt_prev) {
        ret = pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
    } else {               //下面就是数据包从协议栈返回来了
        kfree_skb(skb);    //注意这句,若skb没进入socket的接收队列,则在这里被释放
        ret = NET_RX_DROP; //若skb进入接收队列,则系统调用取包时skb释放,这里skb引用数减一而已
    }
out:
    rcu_read_unlock();
    return ret;
}

int deliver_skb(struct sk_buff *skb,struct packet_type *pt_prev, struct net_device *orig_dev){
    atomic_inc(&skb->users);
    return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);//调函数ip_rcv() arp_rcv()等
}

这里只是将大致流程,arp_rcv(), ip_rcv() 什么的具体流程,以后再写。

四、网络抓包tcpdump
tcpdump也是在二层抓包的,用的是libpcap库,它的基本原理是
1.先创建socket,内核dev_add_packet()挂上自己的钩子函数
2.然后在钩子函数中,把skb放到自己的接收队列中,
3.接着系统调用recv取出skb来,把数据包skb->data拷贝到用户空间
4.最后关闭socket,内核dev_remove_packet()删除自己的钩子函数

下面是一些重要的数据结构,用到的钩子函数都在这里初始化好了
static const struct proto_ops packet_ops = {
    .family =    PF_PACKET,
    .owner =    THIS_MODULE,
    .release =    packet_release,   //关闭socket的时候调这个
    .bind =        packet_bind,
    .connect =    sock_no_connect,
    .socketpair =    sock_no_socketpair,
    .accept =    sock_no_accept,
    .getname =    packet_getname,
    .poll =        packet_poll,
    .ioctl =    packet_ioctl,
    .listen =    sock_no_listen,
    .shutdown =    sock_no_shutdown,
    .setsockopt =    packet_setsockopt,
    .getsockopt =    packet_getsockopt,
    .sendmsg =    packet_sendmsg,
    .recvmsg =    packet_recvmsg,   //socket收包的时候调这个
    .mmap =        packet_mmap,
    .sendpage =    sock_no_sendpage,
};

static struct net_proto_family packet_family_ops = {
    .family =    PF_PACKET,
    .create =    packet_create,    //创建socket的时候调这个
    .owner    =    THIS_MODULE,
};

至于系统调用 socket、recv、close是如何调到这些内核钩子函数的,以后再讲。这里只关注packet_type

4.1 系统调用socket

libpcap系统调用socket,内核最终调用 packet_create
static int packet_create(struct net *net, struct socket *sock, int protocol){
    po->prot_hook.func = packet_rcv;  //初始化钩子函数指针
    po->prot_hook.af_packet_priv = sk;
    if (protocol) {
        po->prot_hook.type = protocol;  //类型是系统调用socket形参指定的
       dev_add_pack(&po->prot_hook);//关键!!
        sock_hold(sk);
        po->running = 1;
    }
    return(0);
}

4.2 钩子函数 packet_rcv 将skb放入到接收队列
文件 linux_2_6_24/net/packet/af_packet.c
简单来说,packet_rcv中,skb越过了整个协议栈,直接进入队列

4.3 系统调用recv
系统调用recv、read、recvmsg,内核最终会调用packet_recvmsg
从接收队列中取出skb,将数据包内容skb->data拷贝到用户空间

4.4 系统调用close
内核最终会调用packet_release
static int packet_release(struct socket *sock){
    struct sock *sk = sock->sk;
    struct packet_sock *po;
    if (!sk)  return 0;
    po = pkt_sk(sk);
    write_lock_bh(&packet_sklist_lock);
    sk_del_node_init(sk);
    write_unlock_bh(&packet_sklist_lock);
    // Unhook packet receive handler.
    if (po->running) {
        dev_remove_pack(&po->prot_hook);  //就是这句!!把packet_type从链表中删除
        po->running = 0;
        po->num = 0;
        __sock_put(sk);
    }
    packet_flush_mclist(sk);
     // Now the socket is dead. No more input will appear.
    sock_orphan(sk);
    sock->sk = NULL;
    /* Purge queues */
    skb_queue_purge(&sk->sk_receive_queue);
    sk_refcnt_debug_release(sk);
    sock_put(sk);
    return 0;
}

----------------------------------------------------------------------------------------------


搜一下内核源代码,二层协议还真是多。。。
drivers/net/wan/hdlc.c: dev_add_pack(&hdlc_packet_type);  //ETH_P_HDLC    hdlc_rcv
drivers/net/wan/lapbether.c:
            dev_add_pack(&lapbeth_packet_type);         //ETH_P_DEC       lapbeth_rcv
drivers/net/wan/syncppp.c:
            dev_add_pack(&sppp_packet_type);            //ETH_P_WAN_PPP   sppp_rcv
drivers/net/bonding/bond_alb.c:  dev_add_pack(pk_type); //ETH_P_ARP       rlb_arp_recv
drivers/net/bonding/bond_main.c:dev_add_pack(pk_type);  //PKT_TYPE_LACPDU bond_3ad_lacpdu_recv
drivers/net/bonding/bond_main.c:dev_add_pack(pt);       //ETH_P_ARP       bond_arp_rcv
drivers/net/pppoe.c: dev_add_pack(&pppoes_ptype);       //ETH_P_PPP_SES   pppoe_rcv
drivers/net/pppoe.c: dev_add_pack(&pppoed_ptype);       //ETH_P_PPP_DISC  pppoe_disc_rcv
drivers/net/hamradio/bpqether.c:
                    dev_add_pack(&bpq_packet_type);     //ETH_P_BPQ       bpq_rcv
net/ipv4/af_inet.c:  dev_add_pack(&ip_packet_type);     //ETH_P_IP       ip_rcv
net/ipv4/arp.c:    dev_add_pack(&arp_packet_type);      //ETH_P_ARP       arp_rcv
net/ipv4/ipconfig.c:  dev_add_pack(&rarp_packet_type);  //ETH_P_RARP      ic_rarp_recv
net/ipv4/ipconfig.c:  dev_add_pack(&bootp_packet_type); //ETH_P_IP        ic_bootp_recv
net/llc/llc_core.c: dev_add_pack(&llc_packet_type);     //ETH_P_802_2     llc_rcv
net/llc/llc_core.c: dev_add_pack(&llc_tr_packet_type);  //ETH_P_TR_802_2  llc_rcv
net/x25/af_x25.c:  dev_add_pack(&x25_packet_type);    //ETH_P_X25      x25_lapb_receive_frame
net/8021q/vlan.c:  dev_add_pack(&vlan_packet_type);     //ETH_P_8021Q     vlan_skb_recv

这些不同协议的packet_type,有些是linux系统启动时挂上去的
比如处理ip协议的pakcet_type,就是在 inet_init()时挂上去的
还有些驱动模块加载的时候才加上去的。
http://blog.chinaunix.net/space.php?uid=24148050&do=blog&id=1994898









这个标题起得比较纠结,之前熟知的PPPOE是作为PPP协议的底层载体,而实际上它也是一个完整的协议,不过它的实现比较简单,由它出发,可以很容易理清楚Linux网络栈的实现方式。

1.总述

    Linux中用户空间的网络编程,是以socket为接口,一般创建一个sockfd = socket(family,type,protocol),之后以该sockfd为参数,进行各种系统调用来实现网络通信功能。其中family指明使用哪种协议域(如INET、UNIX等),protocol指明该协议域中具体哪种协议(如INET中的TCP、UDP等),type表明该接口的类型(如STREAM、DGRAM等),一般设protocol=0,那么就会用该family中该type类型的默认协议(如INET中的STREAM默认就是TCP协议)。

    Linux中利用module机制,层次分明地实现了这套协议体系,并具有很好的扩展性,其基本模块构成如下:

    先看右边,顶层的socket模块提供一个sock_register()函数,供各个协议域模块使用,在全局的net_family[]数组中增加一项;各个协议域模块也提供一个类似的register_xx_proto()函数,供各个具体的协议使用,在该协议域私有的xx_proto[]数组中增加一项。这两个数组中的存放的都是指针,指向的数据结构如下图所示:

    很明显它们是用来创建不同类型的socket接口的,且是一种分层次的创建过程,可想而知,顶层socket_create()完成一些共有的操作,如分配内存等,然后调用下一层create;协议域内的create()完成一些该协议域内共有的初始化工作;最后具体协议中的create()完成协议特有的初始化。具体的下一节讲。

    再来看上图右边的,也是顶层socket模块提供的4个函数,前两个一般由具体协议模块调用,由于协议栈与应用层的交互,具体的后面会讲到。后两个一般有协议域模块调用,用于底层设备与协议栈间的交互。但这也不绝对,如在PPPOE协议中,这4个函数都由具体协议模块调用,这是因为PPPOX协议域内的共有部分不多,各个协议间几乎独立。这4个函数的功能及所用到的数据结构,在后面具体用到时会详细说明。

2.socket插口创建

    首先来看一下最终创建好的socket插口由哪些部分组成,该结构是相当庞大的,这里只给出框架:

  1. 基本属性有state(listen、accept等),flags标志(blocked等),type类型,这里family和protocol都没有了,因为它们再创建时使用过了,已经被融入到socket结构中。
  2. File指针指向一个file结构,在Linux中一个socket也被抽象为一个文件,所以在应用层一般通过标准的文件操作来操作它。
  3. Ops指向一个struct proto_ops结构,它是每种协议特有的,应用层的系统调用,最终映射到网络栈中具体协议的操作方法。
  4. Sk指向一个struct sock结构,而该结构在分配空间时,多分配了一点以作为该协议的私有部分,这里包含了该协议的具体信息,内容相当多。首先是一个struct sock_common结构,包含了协议的基本信息;然后是一个sk_prot_create指针,指向一个struct proto结构体,该结构体就是第一节中所述的,用proto_regsiter()注册到内核中的,它包含应用层到协议栈的交互操作和信息(也可以说成是Appà transport layer的交互信息);然后还有一个sk_backlog_rcv函数指针,所指函数在协议栈处理完接收到的包之后调用,一般仅是把数据包放到该socket的接收队列中,等待APP读取;最后协议的私有部分里存放该协议的私有信息,如pppoe的sessionID、daddr,tcp的连接4元组等,这些信息很重要,利用它们来区分同一个协议中的多个socket。

 

创建的总体过程,第一节已讲过了,下面以pppoe为例,描述一个socket插口的具体创建过程:

之前所述的关键点这里几乎都涉及到了,要注意的是这里的struct proto结构非常简单,因为PPPOE协议几乎没有传输层,所以不需要有太多的中间操作,仅需要一个obj_size来指明struct sock结构后需分配的私有结构大小,关于私有结构的内容,一般在connect操作时才能初始化。

创建好socket之后,其中的fops,proto_ops,sk_backlog_rcv等操作是如何作用,来实现网络通信的功能?这是后面要讲述的内容。

3.主动过程

    主动过程即在应用层中通过系统调用,触发socket完成某种动作,有些系统调用和标准的文件操作类似,因此可以直接用sockfd的fops来描述,如read、write、ioctl等,有些则是socket接口特有的,需重新定义系统调用接口,Linux中用SYSCALL_DEFINEn()宏来定义系统调用接口,如bind、accept等。这些系统调用一般都很简单,最终都会去调用socket内部proto_ops中的接口函数。

    如下图所示,在socket层,并不是所有的文件操作都适用于socket,因此其特有的socket_file_ops中只指定了部分函数;另外还封装了几个系统调用,是我们熟悉的bind、listen、connect、accept。这些系统调用接口都是静态的,它们一般经过简单的处理,就调用具体socket中的proto_ops操作。

    在协议栈中,主要是socket特有的proto_ops操作,但对于一些复杂的协议,如TCP,还需要其它一些操作来支持,这些接口都放在struct sock中的struct proto中。PPPOE协议比较简单,不需要struct proto的操作来支持,但其中的obj_size仍然重要,如前所述。

 

    如上图所示,PPPOE协议中,并不是所有协议操作都需要,如bind、accept等,下面选几个来详细看一下socket的主动过程的工作。

    Ioctl系统调用:ioctl是通过标准的文件操作来调用的,具体如下图所示:

其中顶层sock_ioctl中,对于一些特殊情况,如VLAN、BRIDGE等,它们并不是要对socket插口本身操作,而是要调用VLAN、BRIDGE模块中的创建函数,这看起来有点格格不入,但为了操作方便,且保证网络相关的操作都封装在socket中,这么做也是不得已。

在pppoe_ioctl中,根据cmd进行相应操作,其中有一个值得注意的,就是PPPIOCGCHAN选项,它使得该pppoe_socket成为一个特殊的channel,这主要是pppoe为了给ppp协议提供服务而特有的,与网络协议栈关系不大,以后会具体看。

Read系统调用:read也是标准的文件操作,但要注意,在网络栈中,read并不是接收过程,而仅是从该sock的接收队列中取出skb,提交给应用层,如下图所示。而这些skb是如何获得的,那是一个复杂的被动过程,下面再讲。

Connect系统调用:connect是socket.c中封装的一个系统特用,其代码也很简单,最终调用协议栈中的pppoe_connect接口,该接口函数是pppoe协议中一个非常重要的操作,具体如下图所示:

    首先先一下通配地址的问题,这是network programming中一个基本问题,因为各个协议用到的地址结构不同,在应用层,为了方便可读性,可以用协议特有的地址结构,只要符标准的模式即可(即第一个元素为family),然后强制转换成sockaddr*类型,传递给通用的系统调用接口。在最终调用协议模块中的接口函数时,再转换回来。

    再看pppoe_connect中,首先由sock结构指针得到pn指针,它们是分配在一起的(如前所述),这很容易得到,同时还得到pppoe_net结构的指针(它是该协议中全局共有的)。然后把用户传递进来的addr的数据放到socket中来,并且执行一个set_item函数,该函数主要根据addr信息,把该socket指针放到协议全局的pppoe_net结构中(这一步对接收过程很重要,后面会细讲)。最后初始化了该socket中特有的chan结构,并调用ppp_register_net_channel(),这主要为ppp服务,以后再看。

4.发送流程

    这也是一个主动过程,在协议体系中,它是一个比较重要的过程,所以单独列出来。Socket框架中,发送过程是通过标准的文件操作write完成的, socket的write操作为sock_aio_write(),最终会调用proto_ops->sendmsg()函数,即pppoe模块中的pppoe_sendmsg(),如下图所示:

    首先从sock中获得相关信息,最重要的当然是dev设备,因为pppoe的设备是选定的(由useraddr提供),而有些协议如IP,则会根据协议地址,有协议栈自动选择dev。然后分配skb,并准备其中的package,这是每个协议的关键,由于pppoe协议很简单,只需要设置好一个pppoe header即可。最后直接调用dev_queue_xmit(skb),通过设备将该package发送出去。

5.网络协议栈结构小结

    这里想讲一下的是,pppoe到底是什么层的协议,链路层。而通过上面的描述,更准确的说法应该是,pppoe是一个完整的协议,是从应用层到设备之间的协议模块,从这个意义上来讲,它和INET域中的协议是等价的。如下图所示:

    这里讲的协议是从应用层往下直到物理设备的完整过程,有些协议具有一定的相似性,(如TCP、UDP,还包括裸IP等都以IP协议为基础),则把它们归为一个协议域内。至于协议分层,则是概念上的,如PPPOE协议的主要功能体现在链路层,则一般称它为链路层协议, 而狭义上称TCP、UDP为传输层协议(而前面讲的广义上的TCP、UDP则是包括传输层、以IP为基础的网络层、链路层的完整协议)。

    有点饶人,不过没关系,只要理解协议栈的功能就是从socket接口得到数据,封装成一定的包结构,最终由物理设备发送出去(接收过程反过来)。至于具体的实现,则是由具体协议的特点决定的,对于一些复杂协议,分层方式则是一种比较好的选择。

    而其中有些协议会比较特殊,如之前讲的VLAN,它甚至从来都不会进入到协议栈,仅在设备驱动层,就被转化成以太网协议,协议栈中根本不需要为它准备处理接口。再如比较典型的ICMP协议,它既可以是一个完整的协议,被应用层调用(如典型的Ping程序),也可以只作为TCP的附属协议(只被TCP处理,对应用层不可见)。这里的PPPOE与此很类似,本文讲述了其作为完整协议的工作方式,另外它也可以作为PPP协议的底层基础,在下一篇中会讲述其具体的实现方法。

6.被动过程-接收流程

    接收过程是一个被动过程,在屋里设备层,它往往是由中断触发,其实现的复杂度也较发送过程高很多。在协议栈中,其实现也同样与发送过程很不对称。因为发送时,本身主机拥有控制权,而接收时,是一个数据包对多个接收模块(一对多),只能从数据包中的信息中一点一点分析,并去寻找接收模块。

    先给出接收流程的框架,再逐步去分析其实现。如下图所示:

    先不看橙色部分,一个接收流程由物理设备的中断触发,设备驱动程序进行相应处理,得到协议栈中标准的数据结构sk_buff(简称skb),并根据一个特殊的全局数据结构packet_type,将数据交给相应的协议;协议根据自身设计特点对skb数据进行处理,并通过全局变量xx_net_id和各个协议私有的特殊数据结构xx_net,寻找到该数据包对应的应用层socket插口,并将其放在该socket插口的接收队列中;最后应用层在某个时刻会通过read系统调用读取该数据(如第3节所讲)。

6.1设备驱动层的处理

    设备驱动层的接收过程在之前的篇章中已经讲过了,一般是由硬件中断触发,然后或是采用中断模式、或是采用NAPI模式,总之其根本任务就是:根据设备的特点(先验知识,如以太网设备驱动事先就是知道以太网帧的基本结构的),将接收到的裸数据转换成协议栈所认识的标准结构skb(从而实现底层设备对上层的透明性),然后提交给相应的协议。很明显问题有两个,skb是什么样的,要准备什么?怎么知道提交给谁?

    准备skb结构。首先来看一下sk_buff的构成,如下图所示。Skb只是一个控制结构,实际的数据放在一个data_buf中,并由skb中一些列参数索引,具体见下图右所示,这之中有些参数是在分配data_buf、copy数据时就决定的,如head、end、data、tail等;有些则要经过一定的识别才能得到,如mac_header一般在设备驱动中得到,而network_header、transport_header则要到协议栈中才知道,且各个协议的处理各不相同,如PPPOE协议根本不需要只需要指明network_header,而TCP协议则有复杂的头部信息。最终由skb->data指针和头部长可得到app_data的位置,因此应用层可以只读取应用数据即可。

    Skb中另外一些参数也相当重要,如vlan_tci用于指明vlan的id,其用法在前面已讲过。dev参数则是要贯穿整个流程的,因为该庞大结构中的多个信息会在整个网络系统中用到,要注意的是该参数由设备驱动程序决定,一般就是接收的物理设备,但在Linux中,网络设备是由net_device结构指示的,一个物理设备可有多个协议设备,这在VLAN、BRIDGE中很明显,其实在PPP协议中,这也是一个关键点,后面会讲到。Sk参数指示了该数据包属于哪个应用层socket插口,它由具体协议根据特定方法得到,后面会讲到。Protocol参数是本节的重点,它由mac_header中的字节决定。

    设备驱动程序只关心mac_header,即数据包最初始的部分。前面也提到了,这需要一定的先验知识,如ethernet设备驱动,它先验的指导以太网头部由DMAC、SMAC和两字节的协议构成,下面是一个RTL8012驱动的接收片段(~/dev/net/Ethernet/realtek/apt.c):

 

    提交协议栈。主要就是根据skb->protocol参数,当然还需要另一个重要的数据结构packet_type。

    设备驱动中最后提交过程有netif_skb_receive()函数完成,它会遍历系统中所有的packet_type,找到protocol和dev(这个是啥意思)都相同,就调用该ptype中的func函数,如ip_packet中的func为ip_rcv()函数,这样skb就到了协议栈中。

    系统中所有全局的packet_type构成一个list,并由全局变量ptype_all索引,另外还提供ptype_base[]全局数组,将type相同的packet_type单独成链,为遍历提供方便。

    这些全局的packet_type结构是从哪来的,这就要看第一节图中,左边4个函数中的一个dev_add_packet(struct packet_type*)。协议模块在加载时,调用该函数,将自己特有的packet_type结构注册进内核中,而其中的(*func)则有协议自己定义。

    最后要注意的是,打开if_ether.h文件,可以看到现在已定义的协议protocol有_P_IP、_P_ARP、_P_8021Q、_P_PPP_SES、_P_PPP_DIS等,如果根据传统的分层协议来看它们,会觉得很乱,有网络层的、链路层的、甚至同一种协议还有两个,但如果用第5节的概念来看,则很容易理解。再看由什么模块注册,TCP、UDP都是以IP协议为基础,只要有INET协议域模块注册一个即可,而ARP虽然也属于INET域,但它却必须自己有一个packet_type,PPPOE协议虽然只是一个协议,但却有两个阶段,所以它有两个不同的packet_tpye。可见这种实现是很灵活的,根据具体协议的特点决定。

6.2协议栈接收处理

    协议栈的处理由各协议决定,如TCP协议的处理过程是相当复杂的,而这里的pppoe的处理却非常简单,但由它却可以避开细节,更清楚地看到流程的梗概,如下图所示:

    可以看到pppoe协议的处理过程几乎没有,仅是设置了skb的network_header, transport_header,然后就利用get_item()函数找到它所属的socket插口,直接把它提交给上层。如上图右所示,是典型的TCP接收流程,是相当复杂的,其中TCP与IP的接头处还需用到额外的私有数据结构。

    提交函数sk_backlog_rcv,即这里的pppoe_rcv_core(sk,skb)函数,首先判断是否为ppp通道的数据,若是则提交给ppp协议。一般正常情况下,直接用sock_queue_rcv_skb(sk,skb)函数将它放在socket的接收队列中。

    匹配应用层接口:协议栈在对数据包进行处理后,需要确定该包属于哪个socket插口,这个过程在内核中有一套完整的机制来完成,其框架如下图所示:

    首先内核有个全局结构net_generic,其中一个最重要的元素是指针数组。然后每个协议module加载时,会调用register_pernet_device(struct pernet_operations*)(见第一节图),pernet_operations结构中最关键的两个参数,一个是size,它指示内核为该模块分配一个私有数据结构(如pppoe即为struct pppoe_net),另一个是xx_net_id,它指示由net_generic.ptr[xx_net_id]来指向该数据结构,这样每个协议模块中,就可以根据自己的xx_net_id很容易寻找到内核分配给自己的私有结构。最后协议的私有模块中一般也有一个指针数组,用以索引属于它的各个socket。

    工作流程就很清楚了,具体的工作方式还要看两个函数,

就不看细节了,仅看两个函数的原型就能明白,其中pn参数就是上面所述的用全局结构net_generic和各协议私有xx_net_id获得的。Set_item()函数在connect时调用(参见第3节),它根据pppox_sock中的sessionID、remoteMAC(这两个参数由*useraddr传入,详见下一篇协议分析),根据一个hash算法得到一个hashInt值,然后用pn->hash_ptr[hashInt]指向该socket结构。那反过来,接收时由这两个参数(由数据包的协议头中获得)得到hashInt,便能很容易找到对应的socket了。

    各个协议使用的方法及参数都不同,但思路都一样,就是依据协议本身特有的参数(如TCP中的连接4元组),在socket创建、或连接的时候(接收数据之前),根据一定的算法,将它的指针放在该协议私有的xx_net中,这样接收时就可以由数据报的协议参数找到它了。

6.3命名空间namespace

上述的socket索引方法有个绕弯的地方:就是每个协议私有的xx_net结构可以直接由协议模块本身分配,索引起来也方便,不要用到全局的net_generic。而目前内核所用的方法,其实是为了另外的目的,那就是命名空间namespace。也就是虚拟多用户的一套机制,具体的也没细看,好像目前内核整个namespace还没有全部完成。

network的命名空间问题主要在于,每个协议模块的xx_net私有结构不仅是一个,而是由内核全局决定的,即每注册一个新的用户(有点像虚拟机机制),就分配一个新的xx_net结构,这样多用户间可以用参数相同的socket连接,但却指向不同的socket。

    可以看到前面所述的很多内容中,都会有个net参数,就是为了这个作用,主要实现函数在namespace.c中。

7.总结

    主要结合pppoe协议,学习了Linux中网络栈的实现。由于pppoe协议本身很简单,代码量少,更容易抓住协议实现的梗概。Linux网络栈,继承Unix,采用socket插口作为主线,主要包括创建、协议连接、主动过程、匹配机制、被动过程等内容。

    要注意的是,实际应用中,很少有直接利用pppoe协议通信的,而是把它作为ppp协议的底层基础来用,而这需要协议实现中的一些技巧来支持,下一篇中讲述。


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值