深度探索套接字緩衝區 sk_buff skb

http://blog.csdn.net/aaa6695798/article/details/4879271

    套接字緩衝區用結構體struct sk_buff表示,它用於在網絡子系統中的各層之間傳遞數據,處於一個核心地位,非常之重要。它包含了一組成員數據用於承載網絡數據,同時,也定義了在這些數據上操作的一組函數。下面是其完整的定義:
    struct sk_buff {
        struct sk_buff      *next;
        struct sk_buff      *prev;

        struct sock     *sk;
        struct skb_timeval  tstamp;
        struct net_device   *dev;
        struct net_device   *input_dev;

        union{
            struct tcphdr   *th;
            struct udphdr   *uh;
            struct icmphdr  *icmph;
            struct igmphdr  *igmph;
            struct iphdr    *ipiph;
            struct ipv6hdr  *ipv6h;
            unsigned char   *raw;
        }h;

        union{
            struct iphdr    *iph;
            struct ipv6hdr  *ipv6h;
            struct arphdr   *arph;
            unsigned char   *raw;
        }nh;
        union{
            unsigned char   *raw;
        }mac;

        struct  dst_entry   *dst;
        struct  sec_path    *sp;

        char            cb[48];

        unsigned int        len,
                            data_len,
                            mac_len,
                            csum;
        __u32           priority;
        __u8            local_df:1,
                        cloned:1,
                        ip_summed:2,
                        nohdr:1,
                        nfctinfo:3;
        __u8            pkt_type:3,
                        fclone:2,
                        ipvs_property:1;
        __be16          protocol;

        void            (*destructor)(struct sk_buff *skb);
#ifdef CONFIG_NETFILTER
        __u32           nfmark;
        struct nf_conntrack *nfct;
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
        struct sk_buff      *nfct_reasm;
#endif
#ifdef CONFIG_BRIDGE_NETFILTER
        struct nf_bridge_info   *nf_bridge;
#endif
#endif /* CONFIG_NETFILTER */
#ifdef CONFIG_NET_SCHED
        __u16           tc_index;
#ifdef CONFIG_NET_CLS_ACT
        __u16           tc_verd;
#endif
#endif


        unsigned int    truesize;
        atomic_t        users;
        unsigned char   *head,
                        *data,
                        *tail,
                        *end;
    };
    這是一個比較寵大的結構體,爲了便於理解,我們分成多塊進行分析。
    爲了使用套接字緩衝區,內核創建了兩個後備高速緩存(looaside cache),它們分別是skbuff_head_cache和skbuff_fclone_cache,協議棧中所使用到的所有的sk_buff結構都是從這兩個後備高速緩存中分配出來的。兩者的區別在於skbuff_head_cache在創建時指定的單位內存區域的大小是sizeof(struct sk_buff),可以容納任意數目的struct sk_buff,而skbuff_fclone_cache在創建時指定的單位內存區域大小是2*sizeof(struct sk_buff)+sizeof(atomic_t),它的最小區域單位是一對strcut sk_buff和一個引用計數,這一對sk_buff是克隆的,即它們指向同一個數據緩衝區,引用計數值是0,1或2,表示這一對中有幾個sk_buff已被使用。
    創建一個套接字緩衝區,最常用的操作是alloc_skb,它在skbuff_head_cache中創建一個struct sk_buff,如果要在skbuff_fclone_cache中創建,可以調用__alloc_skb,通過特定參數進行。
    struct sk_buff的成員head指向一個已分配的空間的頭部,該空間用於承載網絡數據,end指向該空間的尾部,這兩個成員指針從空間創建之後,就不能被修改。data指向分配空間中數據的頭部,tail指向數據的尾部,這兩個值隨着網絡數據在各層之間的傳遞、修改,會被不斷改動。所以,這四個指針指向共同的一塊內存區域的不同位置,該內存區域由__alloc_skb在創建緩衝區時創建,四個指針間存在如下關係:
        head <= data <= tail < end
    那指向的這塊內存區域有多大呢?一般由外部根據需要傳入。外部設定這個大小時,會根據實際數據量加上各層協議的首部,再加15(爲了處理對齊)傳入,在__alloc_skb中根據各平臺不同進行長度向上對齊。但是,我們另外還要加上一個存放結構體struct skb_shared_info的空間,也就是說end並不真正指向內存區域的尾部,在end後面還有一個結構體struct skb_shared_info,下面是其定義:
        struct skb_shared_info{
            atomic_t        dataref;    //引用計數。
            unsigned short  nr_frags;   //數據片段的數量。
            unsigned short  tso_size;
            unsigned short  tso_segs;
            unsigned short  ufo_size;
            unsigned int    ip6_frag_id;
            struct sk_buff  *frag_list;     //數據片段的鏈表。
            skb_frag_t  frags[MAX_SKB_FRAGS];   //每一個數據片段的長度。
        };
    這個結構體存放分隔存儲的數據片段,將數據分解爲多個數據片段是爲了使用分散/聚集I/O。
    如果是在skbuff_fclone_cache中創建,則創建一個struct sk_buff後,還要把緊鄰它的一個struct sk_buff的fclone成員置標誌SKB_FCLONE_UNAVAILABLE,表示該緩衝區還沒有被創建出來,同時置自己的fclone爲SKB_FCLONE_ORIG,表示自己可以被克隆。最後置引用計數爲1。
    最後,truesize表示緩存區的整體長度,置爲sizeof(struct sk_buff)+傳入的長度,不包括結構struct skb_shared_info的長度。

 

前面一篇文章分析了套接字緩衝區sk_buff的創建過程,但一般來講,一個套接字緩衝區總是屬於一個套接字,所以,除了調用sk_buff本身的alloc_skb函數創建一個套接字緩衝區,套接字本身還要對sk_buff進行一些操作,以及設置自身的一些成員值。下面我們來分析這個過程。
    如果檢查到待發送數據報沒有傳輸層協議頭(不是傳輸層的tcp或udp數據報),套接字創建緩衝區的函數是sock_alloc_send_skb,它的函數原型是:
    struct sk_buff *sock_alloc_send_skb(struct sock *sk, unsigned long size,
            int noblock, int *errcode)
    它直接調用函數:
    static struct sk_buff *sock_alloc_send_pskb(struct sock *sk,
            unsigned long header_len,
            unsigned long data_len,
            int noblock, int *errcode)
    參數sk是要創建緩衝區的那個套接字,header_len是sk_buff中,成員data指向的那塊數據區的長度,而data_len則是指除那塊數據區以外的被分片的數據的總長。noblock指示是否阻塞模式。對於非傳輸層協議包,不使用分散/聚集IO,所以,置data_len爲0。
    網絡層代表一個套接字的結構體struct sock有兩個成員sk_wmem_alloc和sk_sndbuf,sk_wmem_alloc表示在這個套接字上已經分配的寫緩衝區(發送緩衝區)的總長,每次分配完一個屬於它的寫sk_buff,這個值總是加上sk_buff->truesize。而sk_sndbuf則是這個socket所允許的最大發送緩衝區。它的值在系統初始化的時候設爲變量sysctl_wmem_max的值,可以通過系統調用進行修改。其缺省值sysctl_wmem_max爲107520字節,因爲它的計算長度還包括了struct sk_buff,所以,一般認爲其缺省值是64K數據。
    而對於傳輸層協議包,我們使用sock_wmalloc創建套接字緩衝區,這是一個更爲簡單的創建函數,沒有超時、出錯判斷機制,直接通過調用alloc_skb創建一個sk_buff並返回。但對於傳輸層協議有一個不同點就是sk_wmem_alloc最大可以達到兩倍sk_sndbuf,即缺省的發送緩衝區可以達到128K。
    到這裏,我們就不難理解struct sk_buff中另外兩個成員的含義了:
    len是指數據包全部數據的長度,包括data指向的數據和end後面的分片的數據的總長,而data_len只包括分片的數據的長度。而truesize的最終值是len+sizeof(struct sk_buff)。

 

 結構體struct sk_buff中共有三個聯合體,分別是h, nh和mac,它們都是一些指針,指向協議棧各層協議的首部。從含有的首部類型來看,nh是h的子集,而mac是nh的子集。《Linux設備驅動程序》第三版第522頁這樣介紹這三個聯合體:h中包含有傳輸層的報文頭,nh中包含有網絡層的報文頭,而mac中包含的是鏈路層的報文頭。
    光靠這樣的一個解釋可能過於抽象,讓我們來看一個UDP數據報是怎麼樣穿過數千公里長的網線來到我們的網卡,通過網卡的驅動程序層層向上來到協議棧的上層的。
    當網卡驅動程序收到一個UDP數據報後,它創建一個結構體struct sk_buff,確保data成員指向的空間足夠存放收到的數據(對於數據報分片的情況,因爲比較複雜,我們暫時忽略,我們假設一次收到的是一個完整的UDP數據報)。把收到的數據全部拷貝到data指向的空間,然後,把skb->mac.raw指向data,此時,數據報的開始位置是一個以太網頭,所以skb->mac.raw指向鏈路層的以太網頭。然後通過調用skb_pull剝掉以太網頭,所謂剝掉以太網頭,只是把data加上sizeof(struct ethhdr),同時len減去這個值,這樣,在邏輯上,skb已經不包含以太網頭了,但通過skb->mac.raw還能找到它。這就是我們通常所說的,IP數據報被收到後,在鏈路層被剝去以太網頭。
    在繼續往上層的過程中,一直到我們的my_inet域的函數myip_local_deliver_finish中,我們通過 __skb_pull剝去IP首部,同樣,我們可以通過skb->nh.raw找到它。最後,skb->h.raw指向data,即udp首部,udp首部其實到最後都沒有被剝去,應用程序在調用recv接收數據時,直接從skb->data+sizeof(struc udphdr)的位置開始拷貝。    
    我們可以看到,從網卡驅動開始,通過協議棧層層往上傳送數據報時,通過增加skb->data的值,來逐步剝離協議首部,但通過h,nh,mac這三個聯合指針,我們可以訪問到這些協議首部,從而利用其提供的有效信息。
    但必須指出的是,《Linux設備驅動程序》中的解釋並不完全準確,mac中包含鏈路層報文頭,這是毫無疑問的,nh中包含義網絡層的報文頭,也沒有問題,因爲ARP協議也屬於網絡層協議,nh中包含IP首部或者ARP首部。當我們接收到一個icmp數據報時,在myip_local_deliver_finish中剝去IP首部後,skb->h.raw指向的是icmp首部,但icmp顯然不是傳輸層協議,它是網絡層的一個附屬協議。igmp也是相同的情況,我想這也是爲什麼sk_buff的三個聯合體不命名爲th, nh, mac的原因,因爲th(transprot header)不能準確反映它的內容。
    正確的理解應該是三個聯合體是按TCP/IP數據報的協議首部的排列順序來制定的。排在最前面的是以太網頭,包含在mac中,第二是網絡層協議首部,包括IP和ARP,包含在nh中,第三包括傳輸層協議頭(TCP, UDP)、ICMP, IGMP。
    另外,再選擇兩個重要的數據成員作個簡短介紹。
    pkt_type,數據報的類型。這個值在網卡驅動程序中由函數eth_type_trans通過判斷目的以太網地址來確定。如果目的地址是FF:FF:FF:FF:FF:FF,則爲廣播地址,pkt_type=PACKET_BROADCAST,如果最高位爲1,則爲組播地址,pkt_type=PACKET_MULTICAST,如果目的mac地址跟本機mac地址不相等,則不是發給本機的數據報,pkt_type=PACKET_OTHERHOST,否則就是缺省值PACKET_HOST。
    protocol, 它的值是以太網首部的第三個成員,即幀類型,對於IP數據來講,就是ETH_P_IP(0x8000),對ARP數據報來講,就是ETH_P_ARP(0x8086)。
    sk_buff還有一組操作函數,在理解sk_buff本身的基礎上,理解這些函數並不困難,這裏不再作分析。關於套接字緩衝區的分析就到這裏結束。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章