TCP/IP協議棧在Linux內核中的運行時序分析

 TCP/IP協議棧在Linux內核中的運行時序分析

 0.要求

- 在深入理解Linux內核任務調度(中斷處理、softirg、tasklet、wq、內核線程等)機制的基礎上,分析梳理send和recv過程中TCP/IP協議棧相關的運行任務實體及相互協作的時序分析。
- 編譯、部署、運行、測評、原理、源代碼分析、跟蹤調試等
- 應該包括時序圖

1.TCP/IP協議棧總覽
​    在TCP/IP網絡分層模型裏,整個協議棧被分成了物理層、鏈路層、網絡層,傳輸層和應用層。物理層對應的是網卡和網線,應用層對應的是我們常見的FTP,HTTP等等各種應用。Linux實現的是鏈路層、網絡層和傳輸層這三層。

​    在Linux內核實現中,鏈路層協議靠網卡驅動來實現,內核協議棧來實現網絡層和傳輸層。內核對更上層的應用層提供socket接口來供用戶進程訪問。我們用Linux的視角來看到的TCP/IP網絡分層模型應該是下面這個樣子的。



​    在Linux的源代碼中,網絡設備驅動對應的邏輯位於driver/net/ethernet, 其中intel系列網卡的驅動在driver/net/ethernet/intel目錄下。協議棧模塊代碼位於kernel和net目錄。

​    內核和網絡設備驅動是通過中斷的方式來處理的。當設備上有數據到達的時候,會給CPU的相關引腳上觸發一個電壓變化,以通知CPU來處理數據。Linux中斷處理函數是分上半部和下半部的。上半部是隻進行最簡單的工作,快速處理然後釋放CPU,接着CPU就可以允許其它中斷進來。剩下將絕大部分的工作都放到下半部中,可以慢慢從容處理。2.4以後的內核版本採用的下半部實現方式是軟中斷,由ksoftirqd內核線程全權處理。

​    那麼內核收一個包應該是怎樣的呢?如下給出簡單示意圖。



2.linux內核啓動

​    Linux內核協議棧等模塊在具備接收網卡數據包之前,要做很多的準備工作纔行。比如要提前創建好ksoftirqd內核線程,要註冊好各個協議對應的處理函數,網絡設備子系統要提前初始化好,網卡要啓動好。只有這些都準備之後,我們才能真正開始接收數據包。那麼我們現在來看看這些準備工作都是怎麼做的。


2.1 創建ksoftirqd內核線程

​    Linux的軟中斷都是在專門的內核線程(ksoftirqd)中進行的,因此我們非常有必要看一下這些進程是怎麼初始化的,這樣我們才能在後面更準確地瞭解收包過程。該進程數量不是1個,而是N個,其中N等於你的機器的核數。

​    系統初始化的時候在kernel/smpboot.c中調用了smpboot_register_percpu_thread, 該函數進一步會執行到spawn_ksoftirqd(位於kernel/softirq.c)來創建出softirqd進程。

​    相關代碼如下:

 1 //file: kernel/softirq.c
 2 
 3 static struct smp_hotplug_thread softirq_threads = {
 4 
 5     .store          = &ksoftirqd,
 6     .thread_should_run  = ksoftirqd_should_run,
 7     .thread_fn      = run_ksoftirqd,
 8     .thread_comm        = "ksoftirqd/%u",};
 9 static __init int spawn_ksoftirqd(void){
10     register_cpu_notifier(&cpu_nfb);
11 
12     BUG_ON(smpboot_register_percpu_thread(&softirq_threads));
13     return 0;
14 
15 }
16 
17 early_initcall(spawn_ksoftirqd);


​    當ksoftirqd被創建出來以後,它就會進入自己的線程循環函數ksoftirqd_should_run和run_ksoftirqd了。不停地判斷有沒有軟中斷需要被處理。

2.2網絡子系統初始化

​    linux內核通過調用subsys_initcall來初始化各個子系統,在源代碼目錄裏你可以grep出許多對這個函數的調用。這裏我們要說的是網絡子系統的初始化,會執行到net_dev_init函數。

//file: net/core/dev.c

static int __init net_dev_init(void){

    ......

    for_each_possible_cpu(i) {
        struct softnet_data *sd = &per_cpu(softnet_data, i);

        memset(sd, 0, sizeof(*sd));
        skb_queue_head_init(&sd->input_pkt_queue);
        skb_queue_head_init(&sd->process_queue);
        sd->completion_queue = NULL;
        INIT_LIST_HEAD(&sd->poll_list);
        ......
    }
    ......
    open_softirq(NET_TX_SOFTIRQ, net_tx_action);
    open_softirq(NET_RX_SOFTIRQ, net_rx_action);

}

subsys_initcall(net_dev_init);

​    在這個函數裏,會爲每個CPU都申請一個softnet_data數據結構,在這個數據結構裏的poll_list是等待驅動程序將其poll函數註冊進來,稍後網卡驅動初始化的時候我們可以看到這一過程。

 2.3協議棧註冊

​    內核實現了網絡層的ip協議,也實現了傳輸層的tcp協議和udp協議。這些協議對應的實現函數分別是ip_rcv(),tcp_v4_rcv()和udp_rcv()。和我們平時寫代碼的方式不一樣的是,內核是通過註冊的方式來實現的。Linux內核中的fs_initcall和subsys_initcall類似,也是初始化模塊的入口。fs_initcall調用inet_init後開始網絡協議棧註冊。通過inet_init,將這些函數註冊到了inet_protos和ptype_base數據結構中了。

 2.4網卡驅動初始化與網卡啓動

​    這一步主要是初始化對應網卡的驅動程序,啓動網卡。驅動向內核註冊了 structure net_device_ops 變量,它包含着網卡啓用、發包、設置mac 地址等回調函數(函數指針)。當啓用一個網卡時(例如,通過 ifconfig eth0 up),net_device_ops 中的 igb_open方法會被調用,之後分配隊列內存,註冊中斷處理函數,然後打開硬中斷等包進來。

3.包的接收(收到socket接收隊列)

3.1硬中斷處理

​    首先當數據幀從網線到達網卡上的時候,第一站是網卡的接收隊列。網卡在分配給自己的RingBuffer中尋找可用的內存位置,找到後DMA引擎會把數據DMA到網卡之前關聯的內存裏,這個時候CPU都是無感的。當DMA操作完成以後,網卡會像CPU發起一個硬中斷,通知CPU有數據到達。

​    Linux在硬中斷裏只完成簡單必要的工作,剩下的大部分的處理都是轉交給軟中斷的。只是記錄了一個寄存器,修改了一下下CPU的poll_list,然後發出個軟中斷。

 3.2 ksoftirqd內核線程處理軟中斷

​    簡單講就是系統會循環執行ksoftirqd內核線程,判斷softirq_pengding標誌根據軟中斷類型執行不同函數,之後調用驅動註冊的poll函數,取下數據包

然後送給協議棧。

 3.3 網絡協議棧處理

 3.3.1網絡層之前的處理

​    netif_receive_skb函數會根據包的協議,假如是udp包,會將包依次送到ip_rcv(),udp_rcv()協議處理函數中進行處理。(如果是tcp,則送到arp_rcv(),tcp_rcv())。

//file: net/core/dev.c

int netif_receive_skb(struct sk_buff *skb){

    //RPS處理邏輯,先忽略    ......
    return __netif_receive_skb(skb);

}

static int __netif_receive_skb(struct sk_buff *skb){

    ......  
    ret = __netif_receive_skb_core(skb, false);}static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc){
    ......

    //pcap邏輯,這裏會將數據送入抓包點。tcpdump就是從這個入口獲取包的    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, orig_dev);
            pt_prev = ptype;
        }
    }
    ......
    list_for_each_entry_rcu(ptype,
            &ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {
        if (ptype->type == type &&
            (ptype->dev == null_or_dev || ptype->dev == skb->dev ||
             ptype->dev == orig_dev)) {
            if (pt_prev)
                ret = deliver_skb(skb, pt_prev, orig_dev);
            pt_prev = ptype;
        }
    }

}

​    接着netif_receive_skb_core取出protocol,它會從數據包中取出協議信息,然後遍歷註冊在這個協議上的回調函數列表。ptype_base 是一個 hash table,ip_rcv 函數地址就是存在這個 hash table中的。

//file: net/core/dev.c

static inline int deliver_skb(struct sk_buff *skb,

                  struct packet_type *pt_prev,
                  struct net_device *orig_dev){
    ......
    return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);

}

​    pt_prev->func這一行就調用到了協議層註冊的處理函數了。對於ip包來講,就會進入到ip_rcv(如果是arp包的話,會進入到arp_rcv)。

3.3.2網絡層處理

​    我們再來大致看一下linux在ip協議層都做了什麼,包又是怎麼樣進一步被送到udp或tcp協議處理函數中的。

//file: net/ipv4/ip_input.c

int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev){

    ......
    return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
               ip_rcv_finish);

}

​    這裏NF_HOOK是一個鉤子函數,當執行完註冊的鉤子後就會執行到最後一個參數指向的函數ip_rcv_finish。

static int ip_rcv_finish(struct sk_buff *skb){
    ......
    if (!skb_dst(skb)) {
        int err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
                           iph->tos, skb->dev);
        ...
    }
    ......
    return dst_input(skb);

}


​    跟蹤ip_route_input_noref 後看到它又調用了 ip_route_input_mc。在ip_route_input_mc中,函數ip_local_deliver被賦值給了dst.input, 如下:

//file: net/ipv4/route.c

static int ip_route_input_mc(struct sk_buff *skb, __be32 daddr, __be32 saddr,u8 tos, struct net_device *dev, int our){

    if (our) {
        rth->dst.input= ip_local_deliver;
        rth->rt_flags |= RTCF_LOCAL;
    }

}

​    所以回到ip_rcv_finish中的return dst_input(skb)。

/* Input packet from network to transport.  */

static inline int dst_input(struct sk_buff *skb){

    return skb_dst(skb)->input(skb);

}

​    skb_dst(skb)->input調用的input方法就是路由子系統賦的ip_local_deliver。

//file: net/ipv4/ip_input.c

int ip_local_deliver(struct sk_buff *skb){

    /*     *  Reassemble IP fragments.     */
    if (ip_is_fragment(ip_hdr(skb))) {
        if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))
            return 0;
    }

    return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL,
               ip_local_deliver_finish);

}

static int ip_local_deliver_finish(struct sk_buff *skb){

    ......
    int protocol = ip_hdr(skb)->protocol;
    const struct net_protocol *ipprot;

    ipprot = rcu_dereference(inet_protos[protocol]);
    if (ipprot != NULL) {
        ret = ipprot->handler(skb);
    }

}


​    這裏將會根據包中的協議類型選擇進行分發,在這裏skb包將會進一步被派送到更上層的協議中,udp和tcp。

3.3.3傳輸層處理

​    以udp爲例,這裏的處理函數是udp_rcv()。

//file: net/ipv4/udp.c

int udp_rcv(struct sk_buff *skb){

    return __udp4_lib_rcv(skb, &udp_table, IPPROTO_UDP);

}
int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable,

           int proto){
    sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);

    if (sk != NULL) {
        int ret = udp_queue_rcv_skb(sk, skb
    }
    icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);

}


​    __udp4_lib_lookup_skb是根據skb來尋找對應的socket,當找到以後將數據包放到socket的緩存隊列裏。如果沒有找到,則發送一個目標不可達的icmp包。

//file: net/ipv4/udp.c

int udp_queue_rcv_skb(struct sock *sk, struct sk_buff *skb){  

    ......
    if (sk_rcvqueues_full(sk, skb, sk->sk_rcvbuf))
        goto drop;

    rc = 0;

    ipv4_pktinfo_prepare(skb);
    bh_lock_sock(sk);
    if (!sock_owned_by_user(sk))
        rc = __udp_queue_rcv_skb(sk, skb);
    else if (sk_add_backlog(sk, skb, sk->sk_rcvbuf)) {
        bh_unlock_sock(sk);
        goto drop;
    }
    bh_unlock_sock(sk);
    return rc;

}

​    sock_owned_by_user判斷的是用戶是不是正在這個socker上進行系統調用(socket被佔用),如果沒有,那就可以直接放到socket的接收隊列中。如果有,那就通過sk_add_backlog把數據包添加到backlog隊列。當用戶釋放的socket的時候,內核會檢查backlog隊列,如果有數據再移動到接收隊列中。

​    sk_rcvqueues_full接收隊列如果滿了的話,將直接把包丟棄。接收隊列大小受內核參數net.core.rmem_max和net.core.rmem_default影響。

4.recvfrom系統調用

​    上面我們說完了整個Linux內核對數據包的接收和處理過程,最後把數據包放到socket的接收隊列中了。那麼我們再回頭看用戶進程調用recvfrom後是發生了什麼。我們在代碼裏調用的recvfrom是一個glibc的庫函數,該函數在執行後會將用戶進行陷入到內核態,進入到Linux實現的系統調用sys_recvfrom。



​    以上一個包在linux內核下的接收讀取便成功了。

5.時序圖







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