TCP/IP協議棧在Linux核心中的執行時序分析
網路程式設計調研報告
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 傳送端
- 網路應用呼叫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進行除錯驗證:
剛好符合我們的分析。
5.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打斷點進行驗證:
驗證結果剛好符合我們的調研。
6 傳輸層流程
6.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除錯驗證如下:
6.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: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 棧基本處理過程如下圖所示:
- 首先,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驗證如下:
7.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驗證如下:
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 接收端
簡要過程:
- 一個 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驗證如下:
9 物理層流程
9.1 傳送端
- 物理層在收到傳送請求之後,通過 DMA 將該主存中的資料拷貝至內部RAM(buffer)之中。在資料拷貝中,同時加入符合乙太網協議的相關header,IFG、前導符和CRC。對於乙太網網路,物理層傳送採用CSMA/CD,即在傳送過程中偵聽鏈路衝突。
- 一旦網絡卡完成報文傳送,將產生中斷通知CPU,然後驅動層中的中斷處理程式就可以刪除儲存的 skb 了。
9.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包,進入第三層網路層處理。
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
&n