1. 程式人生 > >Linux核心學習之網路裝置

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= 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的驅動是緊密配合的。