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

  • 2021 年 1 月 17 日
  • 筆記

 

網路程式設計調研報告

 

 

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

 

 

姓名:柴浩宇

學號:SA20225105

班級:軟設1班

 

 

2021年1月

 

調研要求

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

 

目錄

1 調研要求

2 目錄

3 Linux概述

  3.1 Linux作業系統架構簡介

  3.2 協議棧簡介

  3.3 Linux內核協議棧的實現

4 本次調研採取的程式碼簡介

5 應用層流程

  5.1 發送端

  5.2 接收端

6 傳輸層流程

  6.1 發送端

  6.2 接收端

7 IP層流程

  7.1 發送端

  7.2 接收端

8 數據鏈路層流程

  8.1 發送端

  8.2 接收端

9 物理層流程

  9.1 發送端

  9.2 接收端

10 時序圖展示和總結

11 參考資料

 

正文

 

3 Linux概述

  3.1 Linux作業系統架構簡介

Linux作業系統總體上由Linux內核和GNU系統構成,具體來講由4個主要部分構成,即Linux內核、Shell、文件系統和應用程式。內核、Shell和文件系統構成了作業系統的基本結構,使得用戶可以運行程式、管理文件並使用系統。

內核是作業系統的核心,具有很多最基本功能,如虛擬記憶體、多任務、共享庫、需求載入、可執行程式和TCP/IP網路功能。我們所調研的工作,就是在Linux內核層面進行分析。

 

 

 

 

  3.2 協議棧簡介

  OSI(Open System Interconnect),即開放式系統互聯。 一般都叫OSI參考模型,是ISO(國際標準化組織)組織在1985年研究的網路互連模型。
        ISO為了更好的使網路應用更為普及,推出了OSI參考模型。其含義就是推薦所有公司使用這個規範來控制網路。這樣所有公司都有相同的規範,就能互聯了。
OSI定義了網路互連的七層框架(物理層、數據鏈路層、網路層、傳輸層、會話層、表示層、應用層),即ISO開放互連繫統參考模型。如下圖。

 

 

        每一層實現各自的功能和協議,並完成與相鄰層的介面通訊。OSI的服務定義詳細說明了各層所提供的服務。某一層的服務就是該層及其下各層的一種能力,它通過介面提供給更高一層。各層所提供的服務與這些服務是怎麼實現的無關。
  osi七層模型已經成為了理論上的標準,但真正運用於實踐中的是TCP/IP五層模型。
  TCP/IP五層協議和osi的七層協議對應關係如下:

 

 

在每一層實現的協議也各不同,即每一層的服務也不同.下圖列出了每層主要的協議。

 

 

  3.3 Linux內核協議棧

  Linux的協議棧其實是源於BSD的協議棧,它向上以及向下的介面以及協議棧本身的軟體分層組織的非常好。 
  Linux的協議棧基於分層的設計思想,總共分為四層,從下往上依次是:物理層,鏈路層,網路層,應用層。
  物理層主要提供各種連接的物理設備,如各種網卡,串口卡等;鏈路層主要指的是提供對物理層進行訪問的各種介面卡的驅動程式,如網卡驅動等;網路層的作用是負責將網路數據包傳輸到正確的位置,最重要的網路層協議當然就是IP協議了,其實網路層還有其他的協議如ICMP,ARP,RARP等,只不過不像IP那樣被多數人所熟悉;傳輸層的作用主要是提供端到端,說白一點就是提供應用程式之間的通訊,傳輸層最著名的協議非TCP與UDP協議末屬了;應用層,顧名思義,當然就是由應用程式提供的,用來對傳輸數據進行語義解釋的「人機介面」層了,比如HTTP,SMTP,FTP等等,其實應用層還不是人們最終所看到的那一層,最上面的一層應該是「解釋層」,負責將數據以各種不同的表項形式最終呈獻到人們眼前。
  Linux網路核心架構Linux的網路架構從上往下可以分為三層,分別是:
  用戶空間的應用層。
  內核空間的網路協議棧層。
  物理硬體層。
  其中最重要最核心的當然是內核空間的協議棧層了。
  Linux網路協議棧結構Linux的整個網路協議棧都構建與Linux Kernel中,整個棧也是嚴格按照分層的思想來設計的,整個棧共分為五層,分別是 :
  1,系統調用介面層,實質是一個面向用戶空間應用程式的介面調用庫,向用戶空間應用程式提供使用網路服務的介面。
  2,協議無關的介面層,就是SOCKET層,這一層的目的是屏蔽底層的不同協議(更準確的來說主要是TCP與UDP,當然還包括RAW IP, SCTP等),以便與系統調用層之間的介面可以簡單,統一。簡單的說,不管我們應用層使用什麼協議,都要通過系統調用介面來建立一個SOCKET,這個SOCKET其實是一個巨大的sock結構,它和下面一層的網路協議層聯繫起來,屏蔽了不同的網路協議的不同,只吧數據部分呈獻給應用層(通過系統調用介面來呈獻)。
  3,網路協議實現層,毫無疑問,這是整個協議棧的核心。這一層主要實現各種網路協議,最主要的當然是IP,ICMP,ARP,RARP,TCP,UDP等。這一層包含了很多設計的技巧與演算法,相當的不錯。
  4,與具體設備無關的驅動介面層,這一層的目的主要是為了統一不同的介面卡的驅動程式與網路協議層的介面,它將各種不同的驅動程式的功能統一抽象為幾個特殊的動作,如open,close,init等,這一層可以屏蔽底層不同的驅動程式。
  5,驅動程式層,這一層的目的就很簡單了,就是建立與硬體的介面層。
  可以看到,Linux網路協議棧是一個嚴格分層的結構,其中的每一層都執行相對獨立的功能,結構非常清晰。
  其中的兩個「無關」層的設計非常棒,通過這兩個「無關」層,其協議棧可以非常輕鬆的進行擴展。在我們自己的軟體設計中,可以吸收這種設計方法。

 

 

 

 

4 本次調研採取的程式碼簡介

本文採用的測試程式碼是一個非常簡單的基於socket的客戶端伺服器程式,打開服務端並運行,再開一終端運行客戶端,兩者建立連接並可以發送hello\hi的資訊,server端程式碼如下:

#include <stdio.h>     /* perror */
#include <stdlib.h>    /* exit    */
#include <sys/types.h> /* WNOHANG */
#include <sys/wait.h>  /* waitpid */
#include <string.h>    /* memset */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netdb.h> /* gethostbyname */

#define true        1
#define false       0

#define MYPORT      3490    /* 監聽的埠 */
#define BACKLOG     10      /* listen的請求接收隊列長度 */
#define BUF_SIZE    1024

int main()
{
    int sockfd;
    if ((sockfd = socket(PF_INET, SOCK_DGRAM, 0)) == -1)
    {
        perror("socket");
        exit(1);
    }

    struct sockaddr_in sa;         /* 自身的地址資訊 */
    sa.sin_family = AF_INET;
    sa.sin_port = htons(MYPORT);     /* 網路位元組順序 */
    sa.sin_addr.s_addr = INADDR_ANY; /* 自動填本機IP */
    memset(&(sa.sin_zero), 0, 8);    /* 其餘部分置0 */

    if (bind(sockfd, (struct sockaddr *)&sa, sizeof(sa)) == -1)
    {
        perror("bind");

        exit(1);
    }

    struct sockaddr_in their_addr; /* 連接對方的地址資訊 */
    unsigned int sin_size = 0;
    char buf[BUF_SIZE];
    int ret_size = recvfrom(sockfd, buf, BUF_SIZE, 0, (struct sockaddr *)&their_addr, &sin_size);
    if(ret_size == -1)
    {
        perror("recvfrom");
        exit(1);
    }
    buf[ret_size] = '\0';
    printf("recvfrom:%s", buf); 
}

client端程式碼如下:

#include <stdio.h>     /* perror */
#include <stdlib.h>    /* exit    */
#include <sys/types.h> /* WNOHANG */
#include <sys/wait.h>  /* waitpid */
#include <string.h>    /* memset */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netdb.h> /* gethostbyname */

#define true 1
#define false 0

#define PORT 3490       /* Server的埠 */
#define MAXDATASIZE 100 /* 一次可以讀的最大位元組數 */

int main(int argc, char *argv[])
{
    int sockfd, numbytes;
    char buf[MAXDATASIZE];
    struct hostent *he;            /* 主機資訊 */
    struct sockaddr_in server_addr; /* 對方地址資訊 */
    if (argc != 2)
    {
        fprintf(stderr, "usage: client hostname\n");
        exit(1);
    }

    /* get the host info */
    if ((he = gethostbyname(argv[1])) == NULL)
    {
        /* 注意:獲取DNS資訊時,顯示出錯需要用herror而不是perror */
        /* herror 在新的版本中會出現警告,已經建議不要使用了 */
        perror("gethostbyname");
        exit(1);
    }

    if ((sockfd = socket(PF_INET, SOCK_DGRAM, 0)) == -1)
    {
        perror("socket");
        exit(1);
    }

    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT); /* short, NBO */
    server_addr.sin_addr = *((struct in_addr *)he->h_addr_list[0]);
    memset(&(server_addr.sin_zero), 0, 8); /* 其餘部分設成0 */
 
    if ((numbytes = sendto(sockfd, 
                           "Hello, world!\n", 14, 0, 
                           (struct sockaddr *)&server_addr, 
                           sizeof(server_addr))) == -1)
    {
        perror("sendto");
        exit(1);
    }

    close(sockfd);

    return true;
}

簡單來說,主要流程如下圖所示:

 

 

5 應用層流程

  5.1 發送端

  1. 網路應用調用Socket API socket (int family, int type, int protocol) 創建一個 socket,該調用最終會調用 Linux system call socket() ,並最終調用 Linux Kernel 的 sock_create() 方法。該方法返回被創建好了的那個 socket 的 file descriptor。對於每一個 userspace 網路應用創建的 socket,在內核中都有一個對應的 struct socket和 struct sock。其中,struct sock 有三個隊列(queue),分別是 rx , tx 和 err,在 sock 結構被初始化的時候,這些緩衝隊列也被初始化完成;在收據收發過程中,每個 queue 中保存要發送或者接受的每個 packet 對應的 Linux 網路棧 sk_buffer 數據結構的實例 skb。
  2. 對於 TCP socket 來說,應用調用 connect()API ,使得客戶端和伺服器端通過該 socket 建立一個虛擬連接。在此過程中,TCP 協議棧通過三次握手會建立 TCP 連接。默認地,該 API 會等到 TCP 握手完成連接建立後才返回。在建立連接的過程中的一個重要步驟是,確定雙方使用的 Maxium Segemet Size (MSS)。因為 UDP 是面向無連接的協議,因此它是不需要該步驟的。
  3. 應用調用 Linux Socket 的 send 或者 write API 來發出一個 message 給接收端
  4. sock_sendmsg 被調用,它使用 socket descriptor 獲取 sock struct,創建 message header 和 socket control message
  5. _sock_sendmsg 被調用,根據 socket 的協議類型,調用相應協議的發送函數。
    1. 對於 TCP ,調用 tcp_sendmsg 函數。
    2. 對於 UDP 來說,userspace 應用可以調用 send()/sendto()/sendmsg() 三個 system call 中的任意一個來發送 UDP message,它們最終都會調用內核中的 udp_sendmsg() 函數。

  

 

 下面我們具體結合Linux內核源碼進行一步步仔細分析:

根據上述分析可知,發送端首先創建socket,創建之後會通過send發送數據。具體到源碼級別,會通過send,sendto,sendmsg這些系統調用來發送數據,而上述三個函數底層都調用了sock_sendmsg。見下圖:

                

 

 我們再跳轉到__sys_sendto看看這個函數幹了什麼:

 

 

 我們可以發現,它創建了兩個結構體,分別是:struct msghdr msg和struct iovec iov,這兩個結構體根據命名我們可以大致猜出是發送數據和io操作的一些資訊,如下圖:

                                    

 

 

 我們再來看看__sys_sendto調用的sock_sendmsg函數執行了什麼內容:

 

 發現調用了sock_sendmsg_nosec函數:

 

 發現調用了inet_sendmsg函數:

 

 至此,發送端調用完畢。我們可以通過gdb進行調試驗證:

 

 剛好符合我們的分析。

  5.2 接收端

  1. 每當用戶應用調用  read 或者 recvfrom 時,該調用會被映射為/net/socket.c 中的 sys_recv 系統調用,並被轉化為 sys_recvfrom 調用,然後調用 sock_recgmsg 函數。
  2. 對於 INET 類型的 socket,/net/ipv4/af inet.c 中的 inet_recvmsg 方法會被調用,它會調用相關協議的數據接收方法。
  3. 對 TCP 來說,調用 tcp_recvmsg。該函數從 socket buffer 中拷貝數據到 user buffer。
  4. 對 UDP 來說,從 user space 中可以調用三個 system call recv()/recvfrom()/recvmsg() 中的任意一個來接收 UDP package,這些系統調用最終都會調用內核中的 udp_recvmsg 方法。

 

我們結合源碼進行仔細分析:

接收端調用的是__sys_recvfrom函數:

 

 

__sys_recvfrom函數具體如下:

 

 

 發現它調用了sock_recvmsg函數:

 

 發現它調用了sock_recvmsg_nosec函數:

 

 

發現它調用了inet_recvmsg函數:

 

 最後調用的是tcp_recvmsg這個系統調用。至此接收端調用分析完畢。

下面用gdb打斷點進行驗證:

 

 驗證結果剛好符合我們的調研。

6 傳輸層流程

  6.1 發送端

傳輸層的最終目的是向它的用戶提供高效的、可靠的和成本有效的數據傳輸服務,主要功能包括 (1)構造 TCP segment (2)計算 checksum (3)發送回復(ACK)包 (4)滑動窗口(sliding windown)等保證可靠性的操作。TCP 協議棧的大致處理過程如下圖所示:

 

 

TCP 棧簡要過程:

  1. tcp_sendmsg 函數會首先檢查已經建立的 TCP connection 的狀態,然後獲取該連接的 MSS,開始 segement 發送流程。
  2. 構造 TCP 段的 playload:它在內核空間中創建該 packet 的 sk_buffer 數據結構的實例 skb,從 userspace buffer 中拷貝 packet 的數據到 skb 的 buffer。
  3. 構造 TCP header。
  4. 計算 TCP 校驗和(checksum)和 順序號 (sequence number)。
    1. TCP 校驗和是一個端到端的校驗和,由發送端計算,然後由接收端驗證。其目的是為了發現TCP首部和數據在發送端到接收端之間發生的任何改動。如果接收方檢測到校驗和有差錯,則TCP段會被直接丟棄。TCP校驗和覆蓋 TCP 首部和 TCP 數據。
    2. TCP的校驗和是必需的
  5. 發到 IP 層處理:調用 IP handler 句柄 ip_queue_xmit,將 skb 傳入 IP 處理流程。

UDP 棧簡要過程:

  1. UDP 將 message 封裝成 UDP 數據報
  2. 調用 ip_append_data() 方法將 packet 送到 IP 層進行處理。

 下面我們結合程式碼依次分析:

根據我們對應用層的追查可以發現,傳輸層也是先調用send()->sendto()->sys_sento->sock_sendmsg->sock_sendmsg_nosec,我們看下sock_sendmsg_nosec這個函數:

 

在應用層調用的是inet_sendmsg函數,在傳輸層根據後面的斷點可以知道,調用的是sock->ops-sendmsg這個函數。而sendmsg為一個宏,調用的是tcp_sendmsg,如下;

struct proto tcp_prot = {
    .name            = "TCP",
    .owner            = THIS_MODULE,
    .close            = tcp_close,
    .pre_connect        = tcp_v4_pre_connect,
    .connect        = tcp_v4_connect,
    .disconnect        = tcp_disconnect,
    .accept            = inet_csk_accept,
    .ioctl            = tcp_ioctl,
    .init            = tcp_v4_init_sock,
    .destroy        = tcp_v4_destroy_sock,
    .shutdown        = tcp_shutdown,
    .setsockopt        = tcp_setsockopt,
    .getsockopt        = tcp_getsockopt,
    .keepalive        = tcp_set_keepalive,
    .recvmsg        = tcp_recvmsg,
    .sendmsg        = tcp_sendmsg,
    ......

而tcp_sendmsg實際上調用的是

int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)

這個函數如下:

int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
{
    struct tcp_sock *tp = tcp_sk(sk);/*進行了強制類型轉換*/
    struct sk_buff *skb;
    flags = msg->msg_flags;
    ......
        if (copied)
            tcp_push(sk, flags & ~MSG_MORE, mss_now,
                 TCP_NAGLE_PUSH, size_goal);
}

在tcp_sendmsg_locked中,完成的是將所有的數據組織成發送隊列,這個發送隊列是struct sock結構中的一個域sk_write_queue,這個隊列的每一個元素是一個skb,裡面存放的就是待發送的數據。然後調用了tcp_push()函數。結構體struct sock如下:

struct sock{
    ...
    struct sk_buff_head    sk_write_queue;/*指向skb隊列的第一個元素*/
    ...
    struct sk_buff    *sk_send_head;/*指向隊列第一個還沒有發送的元素*/
}

在tcp協議的頭部有幾個標誌欄位:URG、ACK、RSH、RST、SYN、FIN,tcp_push中會判斷這個skb的元素是否需要push,如果需要就將tcp頭部欄位的push置一,置一的過程如下:

static void tcp_push(struct sock *sk, int flags, int mss_now,
             int nonagle, int size_goal)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct sk_buff *skb;

    skb = tcp_write_queue_tail(sk);
    if (!skb)
        return;
    if (!(flags & MSG_MORE) || forced_push(tp))
        tcp_mark_push(tp, skb);

    tcp_mark_urg(tp, flags);

    if (tcp_should_autocork(sk, skb, size_goal)) {

        /* avoid atomic op if TSQ_THROTTLED bit is already set */
        if (!test_bit(TSQ_THROTTLED, &sk->sk_tsq_flags)) {
            NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPAUTOCORKING);
            set_bit(TSQ_THROTTLED, &sk->sk_tsq_flags);
        }
        /* It is possible TX completion already happened
         * before we set TSQ_THROTTLED.
         */
        if (refcount_read(&sk->sk_wmem_alloc) > skb->truesize)
            return;
    }

    if (flags & MSG_MORE)
        nonagle = TCP_NAGLE_CORK;

    __tcp_push_pending_frames(sk, mss_now, nonagle);
}

首先struct tcp_skb_cb結構體存放的就是tcp的頭部,頭部的控制位為tcp_flags,通過tcp_mark_push會將skb中的cb,也就是48個位元組的數組,類型轉換為struct tcp_skb_cb,這樣位於skb的cb就成了tcp的頭部。tcp_mark_push如下:

static inline void tcp_mark_push(struct tcp_sock *tp, struct sk_buff *skb)
{
    TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH;
    tp->pushed_seq = tp->write_seq;
}

...
#define TCP_SKB_CB(__skb)    ((struct tcp_skb_cb *)&((__skb)->cb[0]))
...

struct sk_buff {
    ...    
    char            cb[48] __aligned(8);
    ...
struct tcp_skb_cb {
    __u32        seq;        /* Starting sequence number    */
    __u32        end_seq;    /* SEQ + FIN + SYN + datalen    */
    __u8        tcp_flags;    /* tcp頭部標誌,位於第13個位元組tcp[13])    */
    ......
};

然後,tcp_push調用了__tcp_push_pending_frames(sk, mss_now, nonagle);函數發送數據:

void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,
                   int nonagle)
{

    if (tcp_write_xmit(sk, cur_mss, nonagle, 0,
               sk_gfp_mask(sk, GFP_ATOMIC)))
        tcp_check_probe_timer(sk);
}

發現它調用了tcp_write_xmit函數來發送數據:

static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
               int push_one, gfp_t gfp)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct sk_buff *skb;
    unsigned int tso_segs, sent_pkts;
    int cwnd_quota;
    int result;
    bool is_cwnd_limited = false, is_rwnd_limited = false;
    u32 max_segs;
    /*統計已發送的報文總數*/
    sent_pkts = 0;
    ......

    /*若發送隊列未滿,則準備發送報文*/
    while ((skb = tcp_send_head(sk))) {
        unsigned int limit;

        if (unlikely(tp->repair) && tp->repair_queue == TCP_SEND_QUEUE) {
            /* "skb_mstamp_ns" is used as a start point for the retransmit timer */
            skb->skb_mstamp_ns = tp->tcp_wstamp_ns = tp->tcp_clock_cache;
            list_move_tail(&skb->tcp_tsorted_anchor, &tp->tsorted_sent_queue);
            tcp_init_tso_segs(skb, mss_now);
            goto repair; /* Skip network transmission */
        }

        if (tcp_pacing_check(sk))
            break;

        tso_segs = tcp_init_tso_segs(skb, mss_now);
        BUG_ON(!tso_segs);
        /*檢查發送窗口的大小*/
        cwnd_quota = tcp_cwnd_test(tp, skb);
        if (!cwnd_quota) {
            if (push_one == 2)
                /* Force out a loss probe pkt. */
                cwnd_quota = 1;
            else
                break;
        }

        if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) {
            is_rwnd_limited = true;
            break;
        ......
        limit = mss_now;
        if (tso_segs > 1 && !tcp_urg_mode(tp))
            limit = tcp_mss_split_point(sk, skb, mss_now,
                            min_t(unsigned int,
                              cwnd_quota,
                              max_segs),
                            nonagle);

        if (skb->len > limit &&
            unlikely(tso_fragment(sk, TCP_FRAG_IN_WRITE_QUEUE,
                      skb, limit, mss_now, gfp)))
            break;

        if (tcp_small_queue_check(sk, skb, 0))
            break;

        if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
            break;
    ......

tcp_write_xmit位於tcpoutput.c中,它實現了tcp的擁塞控制,然後調用了tcp_transmit_skb(sk, skb, 1, gfp)傳輸數據,實際上調用的是__tcp_transmit_skb:

static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
                  int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
{
    
    skb_push(skb, tcp_header_size);
    skb_reset_transport_header(skb);
    ......
    /* 構建TCP頭部和校驗和 */
    th = (struct tcphdr *)skb->data;
    th->source        = inet->inet_sport;
    th->dest        = inet->inet_dport;
    th->seq            = htonl(tcb->seq);
    th->ack_seq        = htonl(rcv_nxt);

    tcp_options_write((__be32 *)(th + 1), tp, &opts);
    skb_shinfo(skb)->gso_type = sk->sk_gso_type;
    if (likely(!(tcb->tcp_flags & TCPHDR_SYN))) {
        th->window      = htons(tcp_select_window(sk));
        tcp_ecn_send(sk, skb, th, tcp_header_size);
    } else {
        /* RFC1323: The window in SYN & SYN/ACK segments
         * is never scaled.
         */
        th->window    = htons(min(tp->rcv_wnd, 65535U));
    }
    ......
    icsk->icsk_af_ops->send_check(sk, skb);

    if (likely(tcb->tcp_flags & TCPHDR_ACK))
        tcp_event_ack_sent(sk, tcp_skb_pcount(skb), rcv_nxt);

    if (skb->len != tcp_header_size) {
        tcp_event_data_sent(tp, sk);
        tp->data_segs_out += tcp_skb_pcount(skb);
        tp->bytes_sent += skb->len - tcp_header_size;
    }

    if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq)
        TCP_ADD_STATS(sock_net(sk), TCP_MIB_OUTSEGS,
                  tcp_skb_pcount(skb));

    tp->segs_out += tcp_skb_pcount(skb);
    /* OK, its time to fill skb_shinfo(skb)->gso_{segs|size} */
    skb_shinfo(skb)->gso_segs = tcp_skb_pcount(skb);
    skb_shinfo(skb)->gso_size = tcp_skb_mss(skb);

    /* Leave earliest departure time in skb->tstamp (skb->skb_mstamp_ns) */

    /* Cleanup our debris for IP stacks */
    memset(skb->cb, 0, max(sizeof(struct inet_skb_parm),
                   sizeof(struct inet6_skb_parm)));

    err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);
    ......
}

tcp_transmit_skb是tcp發送數據位於傳輸層的最後一步,這裡首先對TCP數據段的頭部進行了處理,然後調用了網路層提供的發送介面icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);實現了數據的發送,自此,數據離開了傳輸層,傳輸層的任務也就結束了。

gdb調試驗證如下:

 

 

  6.2 接收端

  1. 傳輸層 TCP 處理入口在 tcp_v4_rcv 函數(位於 linux/net/ipv4/tcp ipv4.c 文件中),它會做 TCP header 檢查等處理。
  2. 調用 _tcp_v4_lookup,查找該 package 的 open socket。如果找不到,該 package 會被丟棄。接下來檢查 socket 和 connection 的狀態。
  3. 如果socket 和 connection 一切正常,調用 tcp_prequeue 使 package 從內核進入 user space,放進 socket 的 receive queue。然後 socket 會被喚醒,調用 system call,並最終調用 tcp_recvmsg 函數去從 socket recieve queue 中獲取 segment。

對於傳輸層的程式碼階段,我們需要分析recv函數,這個與send類似,調用的是__sys_recvfrom,整個函數的調用路徑與send非常類似:

int __sys_recvfrom(int fd, void __user *ubuf, size_t size, unsigned int flags,
           struct sockaddr __user *addr, int __user *addr_len)
{
    ......
    err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter);
    if (unlikely(err))
        return err;
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    .....
    msg.msg_control = NULL;
    msg.msg_controllen = 0;
    /* Save some cycles and don't copy the address if not needed */
    msg.msg_name = addr ? (struct sockaddr *)&address : NULL;
    /* We assume all kernel code knows the size of sockaddr_storage */
    msg.msg_namelen = 0;
    msg.msg_iocb = NULL;
    msg.msg_flags = 0;
    if (sock->file->f_flags & O_NONBLOCK)
        flags |= MSG_DONTWAIT;
    err = sock_recvmsg(sock, &msg, flags);

    if (err >= 0 && addr != NULL) {
        err2 = move_addr_to_user(&address,
                     msg.msg_namelen, addr, addr_len);
    .....
}

__sys_recvfrom調用了sock_recvmsg來接收數據,整個函數實際調用的是sock->ops->recvmsg(sock, msg, msg_data_left(msg), flags);,同樣,根據tcp_prot結構的初始化,調用的其實是tcp_rcvmsg

接受函數比發送函數要複雜得多,因為數據接收不僅僅只是接收,tcp的三次握手也是在接收函數實現的,所以收到數據後要判斷當前的狀態,是否正在建立連接等,根據發來的資訊考慮狀態是否要改變,在這裡,我們僅僅考慮在連接建立後數據的接收。

tcp_rcvmsg函數如下:

int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
        int flags, int *addr_len)
{
    ......
    if (sk_can_busy_loop(sk) && skb_queue_empty(&sk->sk_receive_queue) &&
        (sk->sk_state == TCP_ESTABLISHED))
        sk_busy_loop(sk, nonblock);

    lock_sock(sk);
    .....
        if (unlikely(tp->repair)) {
        err = -EPERM;
        if (!(flags & MSG_PEEK))
            goto out;

        if (tp->repair_queue == TCP_SEND_QUEUE)
            goto recv_sndq;

        err = -EINVAL;
        if (tp->repair_queue == TCP_NO_QUEUE)
            goto out;
    ......
        last = skb_peek_tail(&sk->sk_receive_queue);
        skb_queue_walk(&sk->sk_receive_queue, skb) {
            last = skb;
    ......
            if (!(flags & MSG_TRUNC)) {
            err = skb_copy_datagram_msg(skb, offset, msg, used);
            if (err) {
                /* Exception. Bailout! */
                if (!copied)
                    copied = -EFAULT;
                break;
            }
        }

        *seq += used;
        copied += used;
        len -= used;

        tcp_rcv_space_adjust(sk);
    

這裡共維護了三個隊列:prequeuebacklogreceive_queue,分別為預處理隊列,後備隊列和接收隊列,在連接建立後,若沒有數據到來,接收隊列為空,進程會在sk_busy_loop函數內循環等待,知道接收隊列不為空,並調用函數數skb_copy_datagram_msg將接收到的數據拷貝到用戶態,實際調用的是__skb_datagram_iter,這裡同樣用了struct msghdr *msg來實現。__skb_datagram_iter函數如下:

int __skb_datagram_iter(const struct sk_buff *skb, int offset,
            struct iov_iter *to, int len, bool fault_short,
            size_t (*cb)(const void *, size_t, void *, struct iov_iter *),
            void *data)
{
    int start = skb_headlen(skb);
    int i, copy = start - offset, start_off = offset, n;
    struct sk_buff *frag_iter;

    /* 拷貝tcp頭部 */
    if (copy > 0) {
        if (copy > len)
            copy = len;
        n = cb(skb->data + offset, copy, data, to);
        offset += n;
        if (n != copy)
            goto short_copy;
        if ((len -= copy) == 0)
            return 0;
    }

    /* 拷貝數據部分 */
    for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
        int end;
        const skb_frag_t *frag = &skb_shinfo(skb)->frags[i];

        WARN_ON(start > offset + len);

        end = start + skb_frag_size(frag);
        if ((copy = end - offset) > 0) {
            struct page *page = skb_frag_page(frag);
            u8 *vaddr = kmap(page);

            if (copy > len)
                copy = len;
            n = cb(vaddr + frag->page_offset +
                offset - start, copy, data, to);
            kunmap(page);
            offset += n;
            if (n != copy)
                goto short_copy;
            if (!(len -= copy))
                return 0;
        }
        start = end;
    }

拷貝完成後,函數返回,整個接收的過程也就完成了。
用一張函數間的相互調用圖可以表示:

 

 通過gdb調試驗證如下:

Breakpoint 1, __sys_recvfrom (fd=5, ubuf=0x7ffd9428d960, size=1024, flags=0, 
    addr=0x0 <fixed_percpu_data>, addr_len=0x0 <fixed_percpu_data>)
    at net/socket.c:1990
1990    {
(gdb) c
Continuing.

Breakpoint 2, sock_recvmsg (sock=0xffff888006df1900, msg=0xffffc900001f7e28, 
    flags=0) at net/socket.c:891
891    {
(gdb) c
Continuing.

Breakpoint 3, tcp_recvmsg (sk=0xffff888006479100, msg=0xffffc900001f7e28, 
    len=1024, nonblock=0, flags=0, addr_len=0xffffc900001f7df4)
    at net/ipv4/tcp.c:1933
1933    {
(gdb) c
Breakpoint 1, __sys_recvfrom (fd=5, ubuf=0x7ffd9428d960, size=1024, flags=0, 
    addr=0x0 <fixed_percpu_data>, addr_len=0x0 <fixed_percpu_data>)
    at net/socket.c:1990
1990    {
(gdb) c
Continuing.

Breakpoint 2, sock_recvmsg (sock=0xffff888006df1900, msg=0xffffc900001f7e28, 
    flags=0) at net/socket.c:891
891    {
(gdb) c
Continuing.

Breakpoint 3, tcp_recvmsg (sk=0xffff888006479100, msg=0xffffc900001f7e28, 
    len=1024, nonblock=0, flags=0, addr_len=0xffffc900001f7df4)
    at net/ipv4/tcp.c:1933
1933    {
(gdb) c
Continuing.

Breakpoint 4, __skb_datagram_iter (skb=0xffff8880068714e0, offset=0, 
    to=0xffffc900001efe38, len=2, fault_short=false, 
    cb=0xffffffff817ff860 <simple_copy_to_iter>, data=0x0 <fixed_percpu_data>)
    at net/core/datagram.c:414
414    {

符合我們之前的分析。

7 IP層流程

  7.1 發送端

網路層的任務就是選擇合適的網間路由和交換結點, 確保數據及時傳送。網路層將數據鏈路層提供的幀組成數據包,包中封裝有網路層包頭,其中含有邏輯地址資訊- -源站點和目的站點地址的網路地址。其主要任務包括 (1)路由處理,即選擇下一跳 (2)添加 IP header(3)計算 IP header checksum,用於檢測 IP 報文頭部在傳播過程中是否出錯 (4)可能的話,進行 IP 分片(5)處理完畢,獲取下一跳的 MAC 地址,設置鏈路層報文頭,然後轉入鏈路層處理。

  IP 頭:

 

 IP 棧基本處理過程如下圖所示:

 

 

  1. 首先,ip_queue_xmit(skb)會檢查skb->dst路由資訊。如果沒有,比如套接字的第一個包,就使用ip_route_output()選擇一個路由。
  2. 接著,填充IP包的各個欄位,比如版本、包頭長度、TOS等。
  3. 中間的一些分片等,可參閱相關文檔。基本思想是,當報文的長度大於mtu,gso的長度不為0就會調用 ip_fragment 進行分片,否則就會調用ip_finish_output2把數據發送出去。ip_fragment 函數中,會檢查 IP_DF 標誌位,如果待分片IP數據包禁止分片,則調用 icmp_send()向發送方發送一個原因為需要分片而設置了不分片標誌的目的不可達ICMP報文,並丟棄報文,即設置IP狀態為分片失敗,釋放skb,返回消息過長錯誤碼。 
  4. 接下來就用 ip_finish_ouput2 設置鏈路層報文頭了。如果,鏈路層報頭快取有(即hh不為空),那就拷貝到skb里。如果沒,那麼就調用neigh_resolve_output,使用 ARP 獲取。

具體程式碼分析如下:

入口函數是ip_queue_xmit,函數如下:

 

 發現調用了__ip_queue_xmit函數:

 

 

發現調用了skb_rtable函數,實際上是開始找路由快取,繼續看:

 

 

 發現調用ip_local_out進行數據發送:

 

 發現調用__ip_local_out函數:

 

 發現返回一個nf_hook函數,裡面調用了dst_output,這個函數實質上是調用ip_finish__output函數:

 

 發現調用__ip_finish_output函數:

 

 如果分片就調用ip_fragment,否則就調用IP_finish_output2函數:

 

 

 在構造好 ip 頭,檢查完分片之後,會調用鄰居子系統的輸出函數 neigh_output 進行輸 出。neigh_output函數如下:

 

 輸出分為有二層頭快取和沒有兩種情況,有快取時調用 neigh_hh_output 進行快速輸 出,沒有快取時,則調用鄰居子系統的輸出回調函數進行慢速輸出。這個函數如下:

 

 最後調用dev_queue_xmit函數進行向鏈路層發送包,到此結束。gdb驗證如下:

 

 

 

 

  7.2 接收端

  1. IP 層的入口函數在 ip_rcv 函數。該函數首先會做包括 package checksum 在內的各種檢查,如果需要的話會做 IP defragment(將多個分片合併),然後 packet 調用已經註冊的 Pre-routing netfilter hook ,完成後最終到達 ip_rcv_finish 函數。
  2. ip_rcv_finish 函數會調用 ip_router_input 函數,進入路由處理環節。它首先會調用 ip_route_input 來更新路由,然後查找 route,決定該 package 將會被發到本機還是會被轉發還是丟棄:
    1. 如果是發到本機的話,調用 ip_local_deliver 函數,可能會做 de-fragment(合併多個 IP packet),然後調用 ip_local_deliver 函數。該函數根據 package 的下一個處理層的 protocal number,調用下一層介面,包括 tcp_v4_rcv (TCP), udp_rcv (UDP),icmp_rcv (ICMP),igmp_rcv(IGMP)。對於 TCP 來說,函數 tcp_v4_rcv 函數會被調用,從而處理流程進入 TCP 棧。
    2. 如果需要轉發 (forward),則進入轉發流程。該流程需要處理 TTL,再調用 dst_input 函數。該函數會 (1)處理 Netfilter Hook (2)執行 IP fragmentation (3)調用 dev_queue_xmit,進入鏈路層處理流程。

接收相對簡單,入口在ip_rcv,這個函數如下:

 

 裡面調用ip_rcv_finish函數:

 

 發現調用dst_input函數,實際上是調用ip_local_deliver函數:

 

 如果分片,就調用ip_defrag函數,沒有則調用ip_local_deliver_finish函數:

 

 發現調用ip_protocol_deliver_rcu函數:

 

 調用完畢之後進入tcp棧,調用完畢,通過gdb驗證如下:

 

 

8 數據鏈路層流程

  8.1 發送端

功能上,在物理層提供比特流服務的基礎上,建立相鄰結點之間的數據鏈路,通過差錯控制提供數據幀(Frame)在信道上無差錯的傳輸,並進行各電路上的動作系列。數據鏈路層在不可靠的物理介質上提供可靠的傳輸。該層的作用包括:物理地址定址、數據的成幀、流量控制、數據的檢錯、重發等。在這一層,數據的單位稱為幀(frame)。數據鏈路層協議的代表包括:SDLC、HDLC、PPP、STP、幀中繼等。

   實現上,Linux 提供了一個 Network device 的抽象層,其實現在 linux/net/core/dev.c。具體的物理網路設備在設備驅動中(driver.c)需要實現其中的虛函數。Network Device 抽象層調用具體網路設備的函數。

 

 發送端調用dev_queue_xmit,這個函數實際上調用__dev_queue_xmit:

 

 發現它調用了dev_hard_start_xmit函數:

 

 調用xmit_one:

 

 調用trace_net_dev_start_xmit,實際上調用__net_dev_start_xmit函數:

 

 到此,調用鏈結束。gdb調試如下:

 

 

  8.2 接收端

簡要過程:

  1. 一個 package 到達機器的物理網路適配器,當它接收到數據幀時,就會觸發一個中斷,並將通過 DMA 傳送到位於 linux kernel 記憶體中的 rx_ring。
  2. 網卡發出中斷,通知 CPU 有個 package 需要它處理。中斷處理程式主要進行以下一些操作,包括分配 skb_buff 數據結構,並將接收到的數據幀從網路適配器I/O埠拷貝到skb_buff 緩衝區中;從數據幀中提取出一些資訊,並設置 skb_buff 相應的參數,這些參數將被上層的網路協議使用,例如skb->protocol;
  3. 終端處理程式經過簡單處理後,發出一個軟中斷(NET_RX_SOFTIRQ),通知內核接收到新的數據幀。
  4. 內核 2.5 中引入一組新的 API 來處理接收的數據幀,即 NAPI。所以,驅動有兩種方式通知內核:(1) 通過以前的函數netif_rx;(2)通過NAPI機制。該中斷處理程式調用 Network device的 netif_rx_schedule 函數,進入軟中斷處理流程,再調用 net_rx_action 函數。
  5. 該函數關閉中斷,獲取每個 Network device 的 rx_ring 中的所有 package,最終 pacakage 從 rx_ring 中被刪除,進入 netif _receive_skb 處理流程。
  6. netif_receive_skb 是鏈路層接收數據報的最後一站。它根據註冊在全局數組 ptype_all 和 ptype_base 里的網路層數據報類型,把數據報遞交給不同的網路層協議的接收函數(INET域中主要是ip_rcv和arp_rcv)。該函數主要就是調用第三層協議的接收函數處理該skb包,進入第三層網路層處理。

入口函數是net_rx_action:

 

 發現調用napi_poll,實質上調用napi_gro_receive函數:

 

 napi_gro_receive 會直接調用 netif_receive_skb_core。而它會調用__netif_receive_skb_one_core,將數據包交給上層 ip_rcv 進行處理。

 

 調用結束之後,通過軟中斷通知CPU,至此,調用鏈結束。gdb驗證如下:

 

 

9 物理層流程

  9.1 發送端

  1. 物理層在收到發送請求之後,通過 DMA 將該主存中的數據拷貝至內部RAM(buffer)之中。在數據拷貝中,同時加入符合乙太網協議的相關header,IFG、前導符和CRC。對於乙太網網路,物理層發送採用CSMA/CD,即在發送過程中偵聽鏈路衝突。
  2. 一旦網卡完成報文發送,將產生中斷通知CPU,然後驅動層中的中斷處理程式就可以刪除保存的 skb 了。

 

 

  9.2 接收端

  1. 一個 package 到達機器的物理網路適配器,當它接收到數據幀時,就會觸發一個中斷,並將通過 DMA 傳送到位於 linux kernel 記憶體中的 rx_ring。
  2. 網卡發出中斷,通知 CPU 有個 package 需要它處理。中斷處理程式主要進行以下一些操作,包括分配 skb_buff 數據結構,並將接收到的數據幀從網路適配器I/O埠拷貝到skb_buff 緩衝區中;從數據幀中提取出一些資訊,並設置 skb_buff 相應的參數,這些參數將被上層的網路協議使用,例如skb->protocol;
  3. 終端處理程式經過簡單處理後,發出一個軟中斷(NET_RX_SOFTIRQ),通知內核接收到新的數據幀。
  4. 內核 2.5 中引入一組新的 API 來處理接收的數據幀,即 NAPI。所以,驅動有兩種方式通知內核:(1) 通過以前的函數netif_rx;(2)通過NAPI機制。該中斷處理程式調用 Network device的 netif_rx_schedule 函數,進入軟中斷處理流程,再調用 net_rx_action 函數。
  5. 該函數關閉中斷,獲取每個 Network device 的 rx_ring 中的所有 package,最終 pacakage 從 rx_ring 中被刪除,進入 netif _receive_skb 處理流程。
  6. netif_receive_skb 是鏈路層接收數據報的最後一站。它根據註冊在全局數組 ptype_all 和 ptype_base 里的網路層數據報類型,把數據報遞交給不同的網路層協議的接收函數(INET域中主要是ip_rcv和arp_rcv)。該函數主要就是調用第三層協議的接收函數處理該skb包,進入第三層網路層處理。

 

10 時序圖展示和總結


時序圖如下:

 

 

 

本次實驗主要是通過分析Linux內核源程式碼,一步步的通過gdb進行調試函數調用鏈,最終清楚了tcp/ip協議棧的調用過程。因為時間有限,部分細節可能會有錯誤,希望讀者多加指正。

11 參考資料

1 《庖丁解牛Linux內核分析》

2  //www.cnblogs.com/myguaiguai/p/12069485.html

3  //www.cnblogs.com/jmilkfan-fanguiju/p/12789808.html#Linux__23