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

 

網絡程序設計調研報告html

 

 

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

 

 

姓名:柴浩宇算法

學號:SA20225105api

班級:軟設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  https://www.cnblogs.com/myguaiguai/p/12069485.html

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

相關文章
相關標籤/搜索