Linux内核学习之网络设备

  • 字符设备、块设备、网络设备是linux中对设备的三种分类。字符设备、块设备在/dev下是有设备节点的,而块设备是没有的。对块设备的操作是通过一种叫socket的API进行的,这些操作包括了收包(读)、发包(写)、设置IP地址等等(IOCTL)。

    • 网络设备的注册

    分配net_device空间,该数据类型表示一个网络设备

    struct net_device *alloc_netdev(int sizeof_priv, const char *name, void (*setup)(struct net_device *));

    如果是以太网设备,可以这样分配

    struct net_device *alloc_etherdev(int sizeof_priv);

    它会调用ether_setup函数来初始化一些net_device的成员。

    priv是一个私有设备,其中可以定义自己的数据类型

    struct snull_priv *priv = netdev_priv(dev);
    struct snull_priv {
        struct net_device_stats stats;
        int status;
        struct snull_packet *ppool;
        struct snull_packet *rx_queue;  /* List of incoming packets */
        int rx_int_enabled;
        int tx_packetlen;
        u8 *tx_packetdata;
        struct sk_buff *skb;
        spinlock_t lock;
    };

    网络设备的注册

    int register_netdev(struct net_device *dev)
    net_device数据类型中的成员:
    dev->name//名称“eth0”“eth1”,每次注册序号递增1
    dev->irq//中断号
    dev->netdev_ops//与字符设备的file_operations一样,网络设备有net_device_ops结构体定义与系统的函数接口(如open、xmit、ioctl等)
    dev->dev_addr//14字节的MAC地址
    dev->broadcast
    dev->mtu
    dev->tx_queue_len//发包队列上可以缓存多少个包,默认ether_setup设置为1000
    dev->flags

    IFF_UP—kernel控制,变化会调用open或close方法
    IFF_BROADCAST—kernel控制,NIC支持广播
    IFF_MULTICAST—driver控制,默认ether_setup会开启,若是NIC不支持,需要手动禁用该标记
    IFF_ALLMULTI—kernel控制,表明接受所有的多播
    IFF_PROMISC—kernel控制,NIC支持混杂。默认NIC支持自己MAC地址的单播和广播,但是在tcpdump情况下要接受其它MAC地址的单播,就需要开启。当多播和混杂标记改变时,会调用driver的  set_multicast_list,用以设置NIC的硬件filter

    dev->features

    NETIF_F_SG//NIC支持scatter/gather数据传送,前提是NIC能够做硬件checksum
    NETIF_F_IP_CSUM//NIC支持硬件做IP的checksum
    NETIF_F_IPV6_CSUM
    NETIF_F_NO_CSUM//不需要做checksum,loopback的设备就是如此
    NETIF_F_HW_CSUM//NIC支持硬件做所有包的checksum
    NETIF_F_HIGHDMA//支持HIGHMEMORY的DMA
    NETIF_F_HW_VLAN_TX
    NETIF_F_HW_VLAN_RX
    NETIF_F_HW_VLAN_FILTER
    NETIF_F_TSO
    NETIF_F_UFO

     

    • 网络设备的打开

    通过ifconfig来打开,比如:

    ifconfig eth0 192.168.1.3 netmask 255.255.255.0

    上述ifconfig有两步设置:SIOCSIFADDR设置IP地址,SIOCSIFFLAGS设置IFF_UP。后者会调用driver中的open方法。open方法中初始化NIC设备,初始化net_device中的成员,注册中断号,并通过以下函数告知kernel可以开始收发包。

    void netif_start_queue(struct net_device *dev);

     

    • 网络设备的发包

    上层对driver的发包接口是hard_start_xmit,传入的包用skb(sk_buff数据类型)表示,sk->data指向包头,sk->len是包的长度,已经经过封装,driver不需要对包进行处理。

    hard_start_xmit返回0表示包被成功发送,skb所占的内存空间需要被释放掉;返回非0,表示包未能成功发送,内核会过段时间继续尝试发送(详情见generic dev的实现/net/core/dev.c)。


     

    01. int snull_tx(struct sk_buff *skb, struct net_device *dev)
    02. {
    03.     int len;
    04.     char *data, shortpkt[ETH_ZLEN];
    05.     struct snull_priv *priv = netdev_priv(dev);
    06.     data = skb->data;
    07.     len = skb->len;
    08.     if (len < ETH_ZLEN) {
    09.         memset(shortpkt, 0, ETH_ZLEN);
    10.         memcpy(shortpkt, skb->data, skb->len);
    11.         len = ETH_ZLEN;
    12.         data = shortpkt;
    13.     }
    14.     dev->trans_start = jiffies; /* save the timestamp */
    15.     /* Remember the skb, so we can free it at interrupt time */
    16.     priv->skb = skb;
    17.     /* actual deliver of data is device-specific, and not shown here */
    18.     snull_hw_tx(data, len, dev);
    19.     return 0; /* Our simple device can not fail */
    20. }

    snull_hw_tx(data, len, dev)是和NIC的硬件操作息息相关的,它一般是通过触发DMA传输将skb从内存拷贝到NIC的缓存中去。然而硬件的DMA传输需要一定的时间,软件不应该等待DMA完成之后再继续执行,而是应该在触发DMA传输后继续执行,DMA传输完毕通过中断来通知软件,软件在中断服务程序中释放skb。
    NIC的缓存容量有限,但在发包程序中检测到NIC缓存不够时,就不应该触发DMA传输,而是应该通过netif_stop_queue(与netif_start_queue对应)来暂停发送队列(队列由/net/core/dev.c控制)。当中断服务程序中检测到NIC的缓存有空的时候,调用netif_wake_queue,该函数与netif_start_queue区别在于,它不仅使能了发送队列,而且会让内核重新调用hard_start_xmit来发包。

    另一个调用netif_wake_queue的地方是timeout方法(netif_stop_queue之后会timeout),网络设备驱动可以设置timeout方法,当超时发生时,timeout方法被调用,其中复位硬件,并wake up queue。

    若NIC的feature支持NETIF_F_SG的话,在发包程序中,需要检测一下skb是否是离散存放的,也就是skb是否由多个frags组成:

    if (skb_shinfo(skb)->nr_frags == 0) {
        /* Just use skb->data and skb->len as usual */
    }

         skb->data仍然表示包头,指向第一个frag的起始;skb->len仍然是整个包的长度,skb->datalen是减去第一个frag的长度(第一个frag包含在skb中,详细看sk_buff)。每个frag表示为:

    struct skb_frag_struct {
        struct page *page;//用page表示,并不是用virtual address来表示
        __u16 page_offset;
        __u16 size;
    };

    其实很多NIC驱动都是用描述符来控制硬件的,这些描述符组成ring buffer的形式,所以ring buffer的大小就限制了一次可以发包的数量。每个描述符中都有一个地址指针指向skb->data在内存中的位置。在调用driver的xmit方法之前,generic dev层(/net/core/dev.c)对skb会进行入队和出队,这是qdisc(QoS)的内容。

     

    \
     

    • 网络设备的收包

     

    01. void snull_rx(struct net_device *dev, struct snull_packet *pkt)
    02. {
    03.     struct sk_buff *skb;
    04.     struct snull_priv *priv = netdev_priv(dev);
    05.   
    06.     /*
    07.      * The packet has been retrieved from the transmission
    08.      * medium. Build an skb around it, so upper layers can handle it
    09.      */
    10.   
    11.     skb = dev_alloc_skb(pkt->datalen + 2);
    12.     if (!skb) {
    13.         if (printk_ratelimit())
    14.             printk(KERN_NOTICE "snull rx: low on mem - packet dropped\n");
    15.         priv->stats.rx_dropped++;
    16.         goto out;
    17.     }
    18.     memcpy(skb_put(skb, pkt->datalen), pkt->data, pkt->datalen);
    19.     /* Write metadata, and then pass to the receive level */
    20.     skb->dev = dev;
    21.     skb->protocol = eth_type_trans(skb, dev);
    22.     skb->ip_summed = CHECKSUM_UNNECESSARY; /* don't check it */
    23.     priv->stats.rx_packets++;
    24.     priv->stats.rx_bytes += pkt->datalen;
    25.     netif_rx(skb);
    26.   out:
    27.     return;
    28. }

    snull_rx从中断处理程序中调用,pkt指示了从NIC缓存中拷贝到内存的一个网络包,snull_rx再为其分配skb空间,设置skb的某些成员,并提交协议栈。
    上述过程有两次拷贝,第一次是从NIC缓存到内存(pkt),第二次是pkt到skb。为了提高性能,很多NIC的driver中通常是先分配skb,但有包来临后,操作DMA直接将NIC缓存中的数据包拷贝至skb,这样就只有一次拷贝。但是由于skb是事先分配的,所以是以最大以太网中的大小来分配的。

    收包中的skb->ip_summed有以下几种:

    CHECKSUM_HW—NIC硬件已经做了checksum,软件不用再做
    CHECKSUM_NONE—需要软件做checksum
    CHECKSUM_UNNECESSARY—没有必要做checksum

    这里的checksum设置和之前在feature中的设置的区别在于:feature指明的是发包情况,而这里指的是收包我情况。

    netif_rx(skb)将skb提交至协议栈,但其实过程没那么简单:它将skb加入一个称为backlog的queue,然后触发NET_RX_SOFTIRQ,在NET_RX_SOFTIRQ的handler中处理这个skb。netif_rx(skb)可能返回值NET_RX_DROP,表示backlog的queue已经满从而丢包,这是generic dev层(/net/core/dev.c)的内容。

    与TX一样,很多NIC的收包也是通过描述符控制的,这些描述符组成ring buffer的形式。driver预先分配收包的内存(最大包长),并将分配的内存地址填入可用描述符,硬件收到包之后检查有没有可用的描述符,若有的话将包DMA到描述符里记录的内存地址,然后出发中断,否则就丢包。驱动在ISR中判断需要接受多少包,每次将包提交到协议栈之后,就更新可用描述符的数量。这里驱动判断出有多少包可收方法是很tricky的。注意,收包时,包的内存是由驱动分配,协议栈释放的;而发包时,包的内存是由协议栈分配,驱动释放的。

     

    \
     

    • 网络设备的中断服务程序

     

    01. static void snull_regular_interrupt(int irq, void *dev_id, struct pt_regs *regs)
    02. {
    03.     int status<A class=keylink href="http://www.it165.net/edu/ebg/" target=_blank>word</A>;
    04.     struct snull_priv *priv;
    05.     struct snull_packet *pkt = NULL;
    06.     /* As usual, check the "device" pointer to be sure it is
    07.      * really interrupting.
    08.      * Then assign "struct device *dev" */
    09.     struct net_device *dev = (struct net_device *)dev_id;
    10.     /* ... and check with hw if it's really ours */
    11.     /* paranoid */
    12.     if (!dev)
    13.         return;
    14.     /* Lock the device */
    15.     priv = netdev_priv(dev);
    16.     spin_lock(&priv->lock);
    17.     /* retrieve status<A class=keylink href="http://www.it165.net/edu/ebg/" target=_blank>word</A>: real netdevices use I/O instructions */
    18.     statusword = priv->status;
    19.     priv->status = 0;
    20.     if (statusword & SNULL_RX_INTR) {
    21.         /* send it to snull_rx for handling */
    22.         pkt = priv->rx_queue;
    23.         if (pkt) {
    24.             priv->rx_queue = pkt->next;
    25.             snull_rx(dev, pkt);
    26.         }
    27.     }
    28.     if (statusword & SNULL_TX_INTR) {
    29.         /* a transmission is over: free the skb */
    30.         priv->stats.tx_packets++;
    31.         priv->stats.tx_bytes += priv->tx_packetlen;
    32.         dev_kfree_skb(priv->skb);
    33.     }
    34.     /* Unlock the device and we are done */
    35.     spin_unlock(&priv->lock);
    36.     if (pkt) snull_release_buffer(pkt); /* Do this outside the */
    37.     return;
    38. }

    如前所述,TX方法中只是触发DMA传送给NIC缓存,真正数据包的拷贝完成在ISR中得到通知,之后需要将skb释放掉。包的来临在ISR中得到通知,并调用RX方法。
    除此以外,ISR中还会处理例如link状态变化等其它事情。

    一般来说,NIC有以下几种类型的中断:

    收到包

    发送失败

    DMA传输完成(参考3c59x.c和3c509.c,前者使用DMA,后者不使用DMA)

    NIC有空间接受包发送

     

    • NAPI


    通常每来一个包就会触发中断,所以如果包来临的很快,上一个包还没有处理完的情况下,就可能会被下一个包的中断打断。由于前面的包一直得不到处理,也就得不到释放,协议栈的缓存空间用完之后,就收不了后面的包了。因此,解决问题的关键是如果包来临的很快,就不要产生中断,而是存放在NIC的缓存,等协议栈将先前的包处理完之后,再处理NIC缓存的包。

    ××××××××××××××××ULNI中的说明:××××××××××××××××

    9.2.2. Interrupts

    Here the device driver, on behalf of the kernel, instructs the device to generate a hardware interrupt when specific events occur. The kernel, interrupted from its other activities, will then invoke a handler registered by the driver to take care of the device's needs. When the event is the reception of a frame, the handler queues the frame somewhere and notifies the kernel about it. This technique, which is quite common, still represents the best option under low traffic loads. Unfortunately, it does not perform well under high traffic loads: forcing an interrupt for each frame received can easily make the CPU waste all of its time handling interrupts.

    The code that takes care of an input frame is split into two parts: first the driver copies the frame into an input queue accessible by the kernel, and then the kernel processes it (usually passing it to a handler dedicated to the associated protocol such as IP). The first part is executed in interrupt context and can preempt the execution of the second part. This means that the code that accepts input frames and copies them into the queue has higher priority than the code that actually processes the frames.

    Under a high traffic load, the interrupt code would keep preempting the processing code. The consequence is obvious: at some point the input queue will be full, but since the code that is supposed to dequeue and process those frames does not have a chance to run due to its lower priority, the system collapses. New frames cannot be queued since there is no space, and old frames cannot be processed because there is no CPU available for them. This condition is called receive-livelock in the literature.

    In summary, this technique has the advantage of very low latency between the reception of the frame and its processing, but does not work well under high loads. Most network drivers use interrupts, and a large section later in this chapter will discuss how they work.

    10.4.1. Introduction to the New API (NAPI)

    The main idea behind NAPI is simple: instead of using a pure interrupt-driven model, it uses a mix of interrupts and polling. If new frames are received when the kernel has not finished handling the previous ones yet, there is no need for the driver to generate other interrupts: it is just easier to have the kernel keep processing whatever is in the device input queue (with interrupts disabled for the device), and re-enable interrupts once the queue is empty. This way, the driver reaps the advantages of  both interrupts and polling.

    ×××××××××××××××××××××××××××××××××××××××

    总的来说,NAPI的好处有:

    1.增大吞吐量,因为协议栈在处理先前包的时候是屏蔽中断的,后面来的包不产生中断,但是缓存在NIC中或者driver维护的队列里。

    2.CPU占用率低,因为中断次数减少了,触发的NET_RX_SOFTIRQ软中断也少了。

    NAPI很像是中断+polling的结合。为使用NAPI,NIC必须能够缓存多个包。使用NAPI,驱动与上层的接口也要有所修改(poll方法);需要适时的屏蔽与开启中断,中断指示缓存中有包来临时屏蔽中断并触发NET_RX_SOFTIRQ,然后generic dev层会调用driver提供的poll方法提交包,等这些包处理完毕之后才开启中断。可以参看Documentation/networking/NAPI_HOWTO.txt

    若使用NAPI的话,在初始化的时候需要告知内核:www.it165.net

    if (use_napi) {

        dev->poll        = snull_poll;//提供poll函数

        dev->weight      = 16;

    }

    weight,该值要设为比NIC能够缓存的包的数目还要大,一般10M以太网设为16,100M以太网设为64。Poll的方法为:


     

    01. static int snull_poll(struct net_device *dev, int *budget)
    02. {
    03.     int npackets = 0, quota = min(dev->quota, *budget);
    04.     struct sk_buff *skb;
    05.     struct snull_priv *priv = netdev_priv(dev);
    06.     struct snull_packet *pkt;
    07.     while (npackets < quota && priv->rx_queue) {
    08.         pkt = snull_dequeue_buf(dev);
    09.         skb = dev_alloc_skb(pkt->datalen + 2);
    10.         if (! skb) {
    11.             if (printk_ratelimit())
    12.                 printk(KERN_NOTICE "snull: packet dropped\n");
    13.             priv->stats.rx_dropped++;
    14.             snull_release_buffer(pkt);
    15.             continue;
    16.         }
    17.         memcpy(skb_put(skb, pkt->datalen), pkt->data, pkt->datalen);
    18.         skb->dev = dev;
    19.         skb->protocol = eth_type_trans(skb, dev);
    20.         skb->ip_summed = CHECKSUM_UNNECESSARY; /* don't check it */
    21.         netif_receive_skb(skb);
    22.         /* Maintain stats */
    23.         npackets++;
    24.         priv->stats.rx_packets++;
    25.         priv->stats.rx_bytes += pkt->datalen;
    26.         snull_release_buffer(pkt);
    27.     }
    28.     /* If we processed all packets, we're done; tell the kernel and reenable ints */
    29.     *budget -= npackets;
    30.     dev->quota -= npackets;
    31.     if (! priv->rx_queue) {
    32.         netif_rx_complete(dev);
    33.         snull_rx_ints(dev, 1);
    34.         return 0;
    35.     }
    36.     /* We couldn't process everything. */
    37.     return 1;
    38. }

    上述poll方法中的budget是这个CPU上每次能够提交给协议栈的包的数量,dev->quota是这个NIC每次能够提交给协议栈的数量,真实提交包的数量是两者中的最小值。poll方法返回1表示还有包可以提交,返回0表示提交完毕。generic dev中调用的驱动的poll方法,具体查看/net/core/dev.c。

    \
     

    • 链路变化


    链路变化用以下方法来通知内核:

    void netif_carrier_off(struct net_device *dev);

    void netif_carrier_on(struct net_device *dev);

     

    • Skb


    内核中表示网络包的数据结构,其中的一些成员为:

    union { /* ... */ } h;//skb->h.tp就是访问TCP头

    union { /* ... */ } nh;

    union { /*... */} mac;

    unsigned char *head;

    unsigned char *data;

    unsigned char *tail;

    unsigned char *end;//可用的空间是end-head,已用的空间是tail-data

    unsigned int len;

    unsigned int data_len;//frag_list和frags的长度

    unsigned char ip_summed;

    unsigned char pkt_type;

    shinfo(struct sk_buff *skb);

    unsigned int shinfo(skb)->nr_frags;

    skb_frag_t shinfo(skb)->frags;

     

    关于skb的几种操作方法:

    struct sk_buff *alloc_skb(unsigned int len, int priority);//能在进程上下文进行,skb->head与skb->end之间分配空间,skb->data和skb->tail都设置为skb->head

    struct sk_buff *dev_alloc_skb(unsigned int len);//能在中断上下文进行(并在skb->head和skb->data之间预留空间?)

    dev_kfree_skb(struct sk_buff *skb);//能在进程上下文进行

    dev_kfree_skb_irq(struct sk_buff *skb);//只能在中断上下文进行

    dev_kfree_skb_any(struct sk_buff *skb);//任何情况都能进行

    unsigned char *skb_put(struct sk_buff *skb, int len);

    unsigned char *__skb_put(struct sk_buff *skb, int len);

    unsigned char *skb_push(struct sk_buff *skb, int len);

    unsigned char *__skb_push(struct sk_buff *skb, int len);

    put增加tail和len,push减少data增加len。带“__”不检查空间有效性。

    int skb_tailroom(struct sk_buff *skb);

    tail与end之间的大小,有多少数据可以put

    int skb_headroom(struct sk_buff *skb);

    data与head之间的大小,有多少数据可以push

    void skb_reserve(struct sk_buff *skb, int len);

    同时增大data与tail,等于为data之前预留空间,一般都会在data之间留2个字节用于存放MAC头,IP头从data开始。

    unsigned char *skb_pull(struct sk_buff *skb, int len);

    增大data,减少len

    int skb_is_nonlinear(struct sk_buff *skb);

    int skb_headlen(struct sk_buff *skb);

    第一个frag中的长度(==len-data_len)

    void *kmap_skb_frag(skb_frag_t *frag);

    void kunmap_skb_frag(void *vaddr);

    将frag的page转化为virtual address。

     

    • IOCTL


    int (*do_ioctl)(struct net_device *dev, struct ifreq *ifr, int cmd);

    用户空间传入的数据结构为struct ifreq

    系统已经定义了很多命令,如ifconfig中的SIOCSIFADDR和SIOCSIFFLAGS,但这两个貌似不传递到driver中来,在协议栈中的IOCTL已经解决。用户自定义的命令为SIOCDEVPRIVATE到SIOCDEVPRIVATE+15。

     

    • 统计信息


    收包和发包方法中都会更新统计信息,driver中有与系统交互统计信息的接口:

    struct net_device_stats *snull_stats(struct net_device *dev){

        struct snull_priv *priv = netdev_priv(dev);

        return &priv->stats;

    }

    常用的统计信息有:

    unsigned long rx_packets;

    unsigned long tx_packets;

    unsigned long rx_bytes;

    unsigned long tx_bytes;

    unsigned long rx_errors;

    unsigned long tx_errors;

    unsigned long rx_dropped;

    unsigned long tx_dropped;

    unsigned long collisions;

    unsigned long multicast;

     

    • 多播


    按照多播的接收方式NIC可以分为三类:

    NIC设置为混杂模式

    NIC设置为接受所有多播

    NIC可为多播设置过滤条件

    内核与driver管理NIC多播的方式:

    void (*dev->set_multicast_list)(struct net_device *dev);

    每次网络设备加入或者离开一个多播组的时候调用,在dev->flag有变化的时候也会调用(比如开启混杂或者多播功能等),该方法用于设置NIC的多播过滤表

    struct dev_mc_list *dev->mc_list;

    int dev->mc_count;

    软件维护的多播表和数量

    dev->flag三个与多播有关的标志:IFF_MULTICAST、IFF_ALLMULTI、IFF_PROMISC

     

    01. 一个能够接受多播过滤的set_multicast_list的例子:
    02. void ff_set_multicast_list(struct net_device *dev)
    03. {
    04.     struct dev_mc_list *mcptr;
    05.     if (dev->flags & IFF_PROMISC) {
    06.         ff_get_all_packets();
    07.         return;
    08.     }
    09.   
    10.     /* If there's more addresses than we handle, get all multicast
    11.     packets and sort them out in software. */
    12.     if (dev->flags & IFF_ALLMULTI || dev->mc_count > FF_TABLE_SIZE) {
    13.         ff_get_all_multicast_packets();
    14.         return;
    15.     }
    16.   
    17.     /* No multicast? Just get our own stuff */
    18.     if (dev->mc_count == 0) {
    19.         ff_get_only_own_packets();
    20.         return;
    21.     }
    22.     /* Store all of the multicast addresses in the hardware filter */
    23.     ff_clear_mc_list();
    24.     for (mc_ptr = dev->mc_list; mc_ptr; mc_ptr = mc_ptr->next)
    25.         ff_store_mc_address(mc_ptr->dmi_addr);
    26.     ff_get_packets_in_multicast_list();
    27. }
    28.   
    29. NIC没有多播过滤功能的例子
    30. void nf_set_multicast_list(struct net_device *dev)
    31. {
    32.     if (dev->flags & IFF_PROMISC)
    33.         nf_get_all_packets();
    34.     else
    35.         nf_get_only_own_packets();
    36. }

    • 其他


    MDIO读写支持

    int (*mdio_read) (struct net_device *dev, int phy_id, int location);

    void (*mdio_write) (struct net_device *dev, int phy_id, int location, int val);

    Ethtool支持

    mii_ethtool_gset、mii_ethtool_sset可以实现get_settings和set_settings,见 <linux/ethtool.h>

     

    • 参考


    LDD和ULNI

    以前写的笔记,写的不够精简,没有加入generic dev层的记录(net/core/dev.c)

    generic dev层和NIC的驱动是紧密配合的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值