99久久全国免费观看_国产一区二区三区四区五区VM_久久www人成免费看片中文_国产高清在线a视频大全_深夜福利www_日韩一级成人av

一口Linux
認證:優質創作者
所在專題目錄 查看專題
【粉絲問答7】局域網內終端是如何訪問外網?答案在最后
教你如何抓取網絡中的數據包!黑客必備技能
1萬字30張圖說清TCP協議
TCP/IP協議漏洞如此之多?
TCP/IP協議棧在Linux內核中的運行時序分析
linux服務器編程之網絡連接斷掉之后,究竟會發生什么···
作者動態 更多
某通信公司筆試題,你會做幾道?
5天前
10種初學者最常見的c語言段錯誤實例及原因分析
05-30 12:13
linux系統監控工具小神器:btop
05-17 17:37
有沒有權貴開后門讓子女做軟件開發人員?
05-10 23:36
一文包你學會網絡數據抓包
03-15 09:26

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

1 Linux概述

  1.1 Linux操作系統架構簡介

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

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

1.2 協議棧簡介

  OSI(Open System Interconnect),即開放式系統互聯。一般都叫OSI參考模型,是ISO(國際標準化組織)組織在1985年研究的網絡互連模型。

ISO為了更好的使網絡應用更為普及,推出了OSI參考模型。其含義就是推薦所有公司使用這個規范來控制網絡。這樣所有公司都有相同的規范,就能互聯了。

OSI定義了網絡互連的七層框架(物理層、數據鏈路層、網絡層、傳輸層、會話層、表示層、應用層),即ISO開放互連系統參考模型。如下圖。

每一層實現各自的功能和協議,并完成與相鄰層的接口通信。OSI的服務定義詳細說明了各層所提供的服務。某一層的服務就是該層及其下各層的一種能力,它通過接口提供給更高一層。各層所提供的服務與這些服務是怎么實現的無關。

  osi七層模型已經成為了理論上的標準,但真正運用于實踐中的是TCP/IP五層模型。

  TCP/IP五層協議和osi的七層協議對應關系如下:

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

  1.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網絡協議棧是一個嚴格分層的結構,其中的每一層都執行相對獨立的功能,結構非常清晰。  其中的兩個“無關”層的設計非常棒,通過這兩個“無關”層,其協議棧可以非常輕松的進行擴展。在我們自己的軟件設計中,可以吸收這種設計方法。

2 代碼簡介

本文采用的測試代碼是一個非常簡單的基于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    1024int 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;}

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

3 應用層流程

  3.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。

對于 TCP socket 來說,應用調用 connect()API ,使得客戶端和服務器端通過該 socket 建立一個虛擬連接。在此過程中,TCP 協議棧通過三次握手會建立 TCP 連接。默認地,該 API 會等到 TCP 握手完成連接建立后才返回。在建立連接的過程中的一個重要步驟是,確定雙方使用的 Maxium Segemet Size (MSS)。因為 UDP 是面向無連接的協議,因此它是不需要該步驟的。

應用調用 Linux Socket 的 send 或者 write API 來發出一個 message 給接收端sock_sendmsg 被調用,它使用 socket descriptor 獲取 sock struct,創建 message header 和 socket control message_sock_sendmsg 被調用,根據 socket 的協議類型,調用相應協議的發送函數。

對于 TCP ,調用 tcp_sendmsg 函數。對于 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進行調試驗證:

剛好符合我們的分析。

  3.2 接收端

每當用戶應用調用 read 或者 recvfrom 時,該調用會被映射為/net/socket.c 中的 sys_recv 系統調用,并被轉化為 sys_recvfrom 調用,然后調用 sock_recgmsg 函數。

對于 INET 類型的 socket,/net/ipv4/af inet.c 中的 inet_recvmsg 方法會被調用,它會調用相關協議的數據接收方法。

對 TCP 來說,調用 tcp_recvmsg。該函數從 socket buffer 中拷貝數據到 user buffer。

對 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打斷點進行驗證:

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

4 傳輸層流程

  4.1 發送端

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

TCP 棧簡要過程:

tcp_sendmsg 函數會首先檢查已經建立的 TCP connection 的狀態,然后獲取該連接的 MSS,開始 segement 發送流程。

構造 TCP 段的 playload:它在內核空間中創建該 packet 的 sk_buffer 數據結構的實例 skb,從 userspace buffer 中拷貝 packet 的數據到 skb 的 buffer。

構造 TCP header。

計算 TCP 校驗和(checksum)和 順序號 (sequence number)。

TCP 校驗和是一個端到端的校驗和,由發送端計算,然后由接收端驗證。其目的是為了發現TCP首部和數據在發送端到接收端之間發生的任何改動。如果接收方檢測到校驗和有差錯,則TCP段會被直接丟棄。TCP校驗和覆蓋 TCP 首部和 TCP 數據。

TCP的校驗和是必需的

發到 IP 層處理:調用 IP handler 句柄 ip_queue_xmit,將 skb 傳入 IP 處理流程。

UDP 棧簡要過程:

UDP 將 message 封裝成 UDP 數據報

調用 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調試驗證如下:

  4.2 接收端

傳輸層 TCP 處理入口在 tcp_v4_rcv 函數(位于 linux/net/ipv4/tcp ipv4.c 文件中),它會做 TCP header 檢查等處理。

調用 _tcp_v4_lookup,查找該 package 的 open socket。如果找不到,該 package 會被丟棄。接下來檢查 socket 和 connection 的狀態。

如果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);    

這里共維護了三個隊列:prequeue、backlog、receive_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:19901990    {(gdb) cContinuing.Breakpoint 2, sock_recvmsg (sock=0xffff888006df1900, msg=0xffffc900001f7e28,     flags=0) at net/socket.c:891891    {(gdb) cContinuing.Breakpoint 3, tcp_recvmsg (sk=0xffff888006479100, msg=0xffffc900001f7e28,     len=1024, nonblock=0, flags=0, addr_len=0xffffc900001f7df4)    at net/ipv4/tcp.c:19331933    {(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:19901990    {(gdb) cContinuing.Breakpoint 2, sock_recvmsg (sock=0xffff888006df1900, msg=0xffffc900001f7e28,     flags=0) at net/socket.c:891891    {(gdb) cContinuing.Breakpoint 3, tcp_recvmsg (sk=0xffff888006479100, msg=0xffffc900001f7e28,     len=1024, nonblock=0, flags=0, addr_len=0xffffc900001f7df4)    at net/ipv4/tcp.c:19331933    {(gdb) cContinuing.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:414414    {

符合我們之前的分析。

5 IP層流程

  5.1 發送端

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

IP 頭:

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

首先,ip_queue_xmit(skb)會檢查skb->dst路由信息。如果沒有,比如套接字的第一個包,就使用ip_route_output()選擇一個路由。

接著,填充IP包的各個字段,比如版本、包頭長度、TOS等。

中間的一些分片等,可參閱相關文檔。基本思想是,當報文的長度大于mtu,gso的長度不為0就會調用 ip_fragment 進行分片,否則就會調用ip_finish_output2把數據發送出去。ip_fragment 函數中,會檢查 IP_DF 標志位,如果待分片IP數據包禁止分片,則調用 icmp_send()向發送方發送一個原因為需要分片而設置了不分片標志的目的不可達ICMP報文,并丟棄報文,即設置IP狀態為分片失敗,釋放skb,返回消息過長錯誤碼。

接下來就用 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驗證如下:

  5.2 接收端

IP 層的入口函數在 ip_rcv 函數。該函數首先會做包括 package checksum 在內的各種檢查,如果需要的話會做 IP defragment(將多個分片合并),然后 packet 調用已經注冊的 Pre-routing netfilter hook ,完成后最終到達 ip_rcv_finish 函數。

ip_rcv_finish 函數會調用 ip_router_input 函數,進入路由處理環節。它首先會調用 ip_route_input 來更新路由,然后查找 route,決定該 package 將會被發到本機還是會被轉發還是丟棄:

如果是發到本機的話,調用 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 棧。

如果需要轉發 (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驗證如下:

6 數據鏈路層流程

  6.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調試如下:

  6.2 接收端

簡要過程:

一個 package 到達機器的物理網絡適配器,當它接收到數據幀時,就會觸發一個中斷,并將通過 DMA 傳送到位于 linux kernel 內存中的 rx_ring。

網卡發出中斷,通知 CPU 有個 package 需要它處理。中斷處理程序主要進行以下一些操作,包括分配 skb_buff 數據結構,并將接收到的數據幀從網絡適配器I/O端口拷貝到skb_buff 緩沖區中;

從數據幀中提取出一些信息,并設置 skb_buff 相應的參數,這些參數將被上層的網絡協議使用,例如skb->protocol;

終端處理程序經過簡單處理后,發出一個軟中斷(NET_RX_SOFTIRQ),通知內核接收到新的數據幀。

內核 2.5 中引入一組新的 API 來處理接收的數據幀,即 NAPI。所以,驅動有兩種方式通知內核:(1) 通過以前的函數netif_rx;(2)通過NAPI機制。該中斷處理程序調用 Network device的 netif_rx_schedule 函數,進入軟中斷處理流程,再調用 net_rx_action 函數。

該函數關閉中斷,獲取每個 Network device 的 rx_ring 中的所有 package,最終 pacakage 從 rx_ring 中被刪除,進入 netif _receive_skb 處理流程。

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驗證如下:

7 物理層流程

  7.1 發送端

物理層在收到發送請求之后,通過 DMA 將該主存中的數據拷貝至內部RAM(buffer)之中。在數據拷貝中,同時加入符合以太網協議的相關header,IFG、前導符和CRC。對于以太網網絡,物理層發送采用CSMA/CD,即在發送過程中偵聽鏈路沖突。

一旦網卡完成報文發送,將產生中斷通知CPU,然后驅動層中的中斷處理程序就可以刪除保存的 skb 了。

  7.2 接收端

一個 package 到達機器的物理網絡適配器,當它接收到數據幀時,就會觸發一個中斷,并將通過 DMA 傳送到位于 linux kernel 內存中的 rx_ring。

網卡發出中斷,通知 CPU 有個 package 需要它處理。中斷處理程序主要進行以下一些操作,包括分配 skb_buff 數據結構,并將接收到的數據幀從網絡適配器I/O端口拷貝到skb_buff 緩沖區中;從數據幀中提取出一些信息,并設置 skb_buff 相應的參數,這些參數將被上層的網絡協議使用,例如skb->protocol;

終端處理程序經過簡單處理后,發出一個軟中斷(NET_RX_SOFTIRQ),通知內核接收到新的數據幀。

內核 2.5 中引入一組新的 API 來處理接收的數據幀,即 NAPI。所以,驅動有兩種方式通知內核:(1) 通過以前的函數netif_rx;(2)通過NAPI機制。該中斷處理程序調用 Network device的 netif_rx_schedule 函數,進入軟中斷處理流程,再調用 net_rx_action 函數。

該函數關閉中斷,獲取每個 Network device 的 rx_ring 中的所有 package,最終 pacakage 從 rx_ring 中被刪除,進入 netif _receive_skb 處理流程。

netif_receive_skb 是鏈路層接收數據報的最后一站。它根據注冊在全局數組 ptype_all 和 ptype_base 里的網絡層數據報類型,把數據報遞交給不同的網絡層協議的接收函數(INET域中主要是ip_rcv和arp_rcv)。該函數主要就是調用第三層協議的接收函數處理該skb包,進入第三層網絡層處理。

8 時序圖展示和總結

時序圖如下:

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

聲明:本內容為作者獨立觀點,不代表電子星球立場。未經允許不得轉載。授權事宜與稿件投訴,請聯系:editor@netbroad.com
覺得內容不錯的朋友,別忘了一鍵三連哦!
贊 5
收藏 4
關注 181
成為作者 賺取收益
全部留言
0/200
成為第一個和作者交流的人吧
主站蜘蛛池模板: 99免费在线视频观看 | 最新亚洲人成无码WWW | 成人片黄网站色大片免费毛片 | 四虎国产精品永久在线无码 | 欧美熟妇色ⅹxxx欧美妇 | 亚洲日日摸夜夜夜夜夜爽小说 | 婷婷影院在线综合免费视频 | 中国老妇女50xxxxhd | 久久不见久久见中文字幕免费 | 欧美疯狂做受xxxx | 人妻熟妇乱又伦精品视频 | 成人勉费视频 | 亚洲三级黄色片 | 中文无码一区二区视频在线播放 | 国产精品久久久久9999 | 亚洲人成网站在线观看妞妞网 | 少妇人妻偷人精品无码视频 | 果冻传媒2021精品一区 | 亚洲欧美国产一区二区三区 | 亚洲人成无码网站在线观看 | 欧美高清69 | 国产性AV在线 | 精品久久久久免费极品大片 | 产精品视频在线观看免费 | 伊人久久精品亚洲午夜 | 龙珠z免费观看国语版 | 久久精品无码专区免费东京热 | 柠檬福利第一导航在线 | 国产一区二区精品在线观看 | 亚洲福利国产精品17p | 中文字幕成人网 | 少妇又色又爽又高潮极品 | www.国产日本| 人成在线免费网站 | 狠狠操图片 | 亚洲国产亚洲 | 波多野结衣在线视频一区二区三区 | 亚洲国产精品毛片AV不卡在线 | 国产精品成人VA在线播放 | 黄色av| yp在线观看视频网址入口 |