1 Linux概述
1.1 Linux操作系統(tǒng)架構(gòu)簡介
Linux操作系統(tǒng)總體上由Linux內(nèi)核和GNU系統(tǒng)構(gòu)成,具體來講由4個主要部分構(gòu)成,即Linux內(nèi)核、Shell、文件系統(tǒng)和應(yīng)用程序。內(nèi)核、Shell和文件系統(tǒng)構(gòu)成了操作系統(tǒng)的基本結(jié)構(gòu),使得用戶可以運行程序、管理文件并使用系統(tǒng)。
內(nèi)核是操作系統(tǒng)的核心,具有很多最基本功能,如虛擬內(nèi)存、多任務(wù)、共享庫、需求加載、可執(zhí)行程序和TCP/IP網(wǎng)絡(luò)功能。我們所調(diào)研的工作,就是在Linux內(nèi)核層面進行分析。
1.2 協(xié)議棧簡介
OSI(Open System Interconnect),即開放式系統(tǒng)互聯(lián)。一般都叫OSI參考模型,是ISO(國際標準化組織)組織在1985年研究的網(wǎng)絡(luò)互連模型。
ISO為了更好的使網(wǎng)絡(luò)應(yīng)用更為普及,推出了OSI參考模型。其含義就是推薦所有公司使用這個規(guī)范來控制網(wǎng)絡(luò)。這樣所有公司都有相同的規(guī)范,就能互聯(lián)了。
OSI定義了網(wǎng)絡(luò)互連的七層框架(物理層、數(shù)據(jù)鏈路層、網(wǎng)絡(luò)層、傳輸層、會話層、表示層、應(yīng)用層),即ISO開放互連系統(tǒng)參考模型。如下圖。
每一層實現(xiàn)各自的功能和協(xié)議,并完成與相鄰層的接口通信。OSI的服務(wù)定義詳細說明了各層所提供的服務(wù)。某一層的服務(wù)就是該層及其下各層的一種能力,它通過接口提供給更高一層。各層所提供的服務(wù)與這些服務(wù)是怎么實現(xiàn)的無關(guān)。
osi七層模型已經(jīng)成為了理論上的標準,但真正運用于實踐中的是TCP/IP五層模型。
TCP/IP五層協(xié)議和osi的七層協(xié)議對應(yīng)關(guān)系如下:
在每一層實現(xiàn)的協(xié)議也各不同,即每一層的服務(wù)也不同.下圖列出了每層主要的協(xié)議。
1.3 Linux內(nèi)核協(xié)議棧
Linux的協(xié)議棧其實是源于BSD的協(xié)議棧,它向上以及向下的接口以及協(xié)議棧本身的軟件分層組織的非常好。
Linux的協(xié)議?;诜謱拥脑O(shè)計思想,總共分為四層,從下往上依次是:物理層,鏈路層,網(wǎng)絡(luò)層,應(yīng)用層。
物理層主要提供各種連接的物理設(shè)備,如各種網(wǎng)卡,串口卡等;鏈路層主要指的是提供對物理層進行訪問的各種接口卡的驅(qū)動程序,如網(wǎng)卡驅(qū)動等;網(wǎng)路層的作用是負責將網(wǎng)絡(luò)數(shù)據(jù)包傳輸?shù)秸_的位置,最重要的網(wǎng)絡(luò)層協(xié)議當然就是IP協(xié)議了,其實網(wǎng)絡(luò)層還有其他的協(xié)議如ICMP,ARP,RARP等,只不過不像IP那樣被多數(shù)人所熟悉;傳輸層的作用主要是提供端到端,說白一點就是提供應(yīng)用程序之間的通信,傳輸層最著名的協(xié)議非TCP與UDP協(xié)議末屬了;應(yīng)用層,顧名思義,當然就是由應(yīng)用程序提供的,用來對傳輸數(shù)據(jù)進行語義解釋的“人機界面”層了,比如HTTP,SMTP,F(xiàn)TP等等,其實應(yīng)用層還不是人們最終所看到的那一層,最上面的一層應(yīng)該是“解釋層”,負責將數(shù)據(jù)以各種不同的表項形式最終呈獻到人們眼前?! inux網(wǎng)絡(luò)核心架構(gòu)Linux的網(wǎng)絡(luò)架構(gòu)從上往下可以分為三層,分別是: 用戶空間的應(yīng)用層?! ?nèi)核空間的網(wǎng)絡(luò)協(xié)議棧層?! ∥锢碛布??! ∑渲凶钪匾詈诵牡漠斎皇莾?nèi)核空間的協(xié)議棧層了。 Linux網(wǎng)絡(luò)協(xié)議棧結(jié)構(gòu)Linux的整個網(wǎng)絡(luò)協(xié)議棧都構(gòu)建與Linux Kernel中,整個棧也是嚴格按照分層的思想來設(shè)計的,整個棧共分為五層,分別是 : 1,系統(tǒng)調(diào)用接口層,實質(zhì)是一個面向用戶空間應(yīng)用程序的接口調(diào)用庫,向用戶空間應(yīng)用程序提供使用網(wǎng)絡(luò)服務(wù)的接口?! ?,協(xié)議無關(guān)的接口層,就是SOCKET層,這一層的目的是屏蔽底層的不同協(xié)議(更準確的來說主要是TCP與UDP,當然還包括RAW IP, SCTP等),以便與系統(tǒng)調(diào)用層之間的接口可以簡單,統(tǒng)一。簡單的說,不管我們應(yīng)用層使用什么協(xié)議,都要通過系統(tǒng)調(diào)用接口來建立一個SOCKET,這個SOCKET其實是一個巨大的sock結(jié)構(gòu),它和下面一層的網(wǎng)絡(luò)協(xié)議層聯(lián)系起來,屏蔽了不同的網(wǎng)絡(luò)協(xié)議的不同,只吧數(shù)據(jù)部分呈獻給應(yīng)用層(通過系統(tǒng)調(diào)用接口來呈獻)?! ?,網(wǎng)絡(luò)協(xié)議實現(xiàn)層,毫無疑問,這是整個協(xié)議棧的核心。這一層主要實現(xiàn)各種網(wǎng)絡(luò)協(xié)議,最主要的當然是IP,ICMP,ARP,RARP,TCP,UDP等。這一層包含了很多設(shè)計的技巧與算法,相當?shù)牟诲e。 4,與具體設(shè)備無關(guān)的驅(qū)動接口層,這一層的目的主要是為了統(tǒng)一不同的接口卡的驅(qū)動程序與網(wǎng)絡(luò)協(xié)議層的接口,它將各種不同的驅(qū)動程序的功能統(tǒng)一抽象為幾個特殊的動作,如open,close,init等,這一層可以屏蔽底層不同的驅(qū)動程序。 5,驅(qū)動程序?qū)樱@一層的目的就很簡單了,就是建立與硬件的接口層?! 】梢钥吹?,Linux網(wǎng)絡(luò)協(xié)議棧是一個嚴格分層的結(jié)構(gòu),其中的每一層都執(zhí)行相對獨立的功能,結(jié)構(gòu)非常清晰?! ∑渲械膬蓚€“無關(guān)”層的設(shè)計非常棒,通過這兩個“無關(guān)”層,其協(xié)議??梢苑浅]p松的進行擴展。在我們自己的軟件設(shè)計中,可以吸收這種設(shè)計方法。
2 代碼簡介
本文采用的測試代碼是一個非常簡單的基于socket的客戶端服務(wù)器程序,打開服務(wù)端并運行,再開一終端運行客戶端,兩者建立連接并可以發(fā)送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 /* 監(jiān)聽的端口 */#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); /* 網(wǎng)絡(luò)字節(jié)順序 */ 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 /* 一次可以讀的最大字節(jié)數(shù) */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 在新的版本中會出現(xiàn)警告,已經(jīng)建議不要使用了 */ 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); /* 其余部分設(shè)成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 應(yīng)用層流程
3.1 發(fā)送端
網(wǎng)絡(luò)應(yīng)用調(diào)用Socket API socket (int family, int type, int protocol) 創(chuàng)建一個 socket,該調(diào)用最終會調(diào)用 Linux system call socket() ,并最終調(diào)用 Linux Kernel 的 sock_create() 方法。該方法返回被創(chuàng)建好了的那個 socket 的 file descriptor。對于每一個 userspace 網(wǎng)絡(luò)應(yīng)用創(chuàng)建的 socket,在內(nèi)核中都有一個對應(yīng)的 struct socket和 struct sock。其中,struct sock 有三個隊列(queue),分別是 rx , tx 和 err,在 sock 結(jié)構(gòu)被初始化的時候,這些緩沖隊列也被初始化完成;在收據(jù)收發(fā)過程中,每個 queue 中保存要發(fā)送或者接受的每個 packet 對應(yīng)的 Linux 網(wǎng)絡(luò)棧 sk_buffer 數(shù)據(jù)結(jié)構(gòu)的實例 skb。
對于 TCP socket 來說,應(yīng)用調(diào)用 connect()API ,使得客戶端和服務(wù)器端通過該 socket 建立一個虛擬連接。在此過程中,TCP 協(xié)議棧通過三次握手會建立 TCP 連接。默認地,該 API 會等到 TCP 握手完成連接建立后才返回。在建立連接的過程中的一個重要步驟是,確定雙方使用的 Maxium Segemet Size (MSS)。因為 UDP 是面向無連接的協(xié)議,因此它是不需要該步驟的。
應(yīng)用調(diào)用 Linux Socket 的 send 或者 write API 來發(fā)出一個 message 給接收端sock_sendmsg 被調(diào)用,它使用 socket descriptor 獲取 sock struct,創(chuàng)建 message header 和 socket control message_sock_sendmsg 被調(diào)用,根據(jù) socket 的協(xié)議類型,調(diào)用相應(yīng)協(xié)議的發(fā)送函數(shù)。
對于 TCP ,調(diào)用 tcp_sendmsg 函數(shù)。對于 UDP 來說,userspace 應(yīng)用可以調(diào)用 send()/sendto()/sendmsg() 三個 system call 中的任意一個來發(fā)送 UDP message,它們最終都會調(diào)用內(nèi)核中的 udp_sendmsg() 函數(shù)。
下面我們具體結(jié)合Linux內(nèi)核源碼進行一步步仔細分析:
根據(jù)上述分析可知,發(fā)送端首先創(chuàng)建socket,創(chuàng)建之后會通過send發(fā)送數(shù)據(jù)。具體到源碼級別,會通過send,sendto,sendmsg這些系統(tǒng)調(diào)用來發(fā)送數(shù)據(jù),而上述三個函數(shù)底層都調(diào)用了sock_sendmsg。見下圖:
我們再跳轉(zhuǎn)到__sys_sendto看看這個函數(shù)干了什么:
我們可以發(fā)現(xiàn),它創(chuàng)建了兩個結(jié)構(gòu)體,分別是:struct msghdr msg和struct iovec iov,這兩個結(jié)構(gòu)體根據(jù)命名我們可以大致猜出是發(fā)送數(shù)據(jù)和io操作的一些信息,如下圖:
我們再來看看__sys_sendto調(diào)用的sock_sendmsg函數(shù)執(zhí)行了什么內(nèi)容:
發(fā)現(xiàn)調(diào)用了sock_sendmsg_nosec函數(shù):
發(fā)現(xiàn)調(diào)用了inet_sendmsg函數(shù):
至此,發(fā)送端調(diào)用完畢。我們可以通過gdb進行調(diào)試驗證:
剛好符合我們的分析。
3.2 接收端
每當用戶應(yīng)用調(diào)用 read 或者 recvfrom 時,該調(diào)用會被映射為/net/socket.c 中的 sys_recv 系統(tǒng)調(diào)用,并被轉(zhuǎn)化為 sys_recvfrom 調(diào)用,然后調(diào)用 sock_recgmsg 函數(shù)。
對于 INET 類型的 socket,/net/ipv4/af inet.c 中的 inet_recvmsg 方法會被調(diào)用,它會調(diào)用相關(guān)協(xié)議的數(shù)據(jù)接收方法。
對 TCP 來說,調(diào)用 tcp_recvmsg。該函數(shù)從 socket buffer 中拷貝數(shù)據(jù)到 user buffer。
對 UDP 來說,從 user space 中可以調(diào)用三個 system call recv()/recvfrom()/recvmsg() 中的任意一個來接收 UDP package,這些系統(tǒng)調(diào)用最終都會調(diào)用內(nèi)核中的 udp_recvmsg 方法。
我們結(jié)合源碼進行仔細分析:
接收端調(diào)用的是__sys_recvfrom函數(shù):
__sys_recvfrom函數(shù)具體如下:
發(fā)現(xiàn)它調(diào)用了sock_recvmsg函數(shù):
發(fā)現(xiàn)它調(diào)用了sock_recvmsg_nosec函數(shù):
發(fā)現(xiàn)它調(diào)用了inet_recvmsg函數(shù):
最后調(diào)用的是tcp_recvmsg這個系統(tǒng)調(diào)用。至此接收端調(diào)用分析完畢。
下面用gdb打斷點進行驗證:
驗證結(jié)果剛好符合我們的調(diào)研。
4 傳輸層流程
4.1 發(fā)送端
傳輸層的最終目的是向它的用戶提供高效的、可靠的和成本有效的數(shù)據(jù)傳輸服務(wù),主要功能包括 (1)構(gòu)造 TCP segment (2)計算 checksum (3)發(fā)送回復(fù)(ACK)包 (4)滑動窗口(sliding windown)等保證可靠性的操作。TCP 協(xié)議棧的大致處理過程如下圖所示:
TCP 棧簡要過程:
tcp_sendmsg 函數(shù)會首先檢查已經(jīng)建立的 TCP connection 的狀態(tài),然后獲取該連接的 MSS,開始 segement 發(fā)送流程。
構(gòu)造 TCP 段的 playload:它在內(nèi)核空間中創(chuàng)建該 packet 的 sk_buffer 數(shù)據(jù)結(jié)構(gòu)的實例 skb,從 userspace buffer 中拷貝 packet 的數(shù)據(jù)到 skb 的 buffer。
構(gòu)造 TCP header。
計算 TCP 校驗和(checksum)和 順序號 (sequence number)。
TCP 校驗和是一個端到端的校驗和,由發(fā)送端計算,然后由接收端驗證。其目的是為了發(fā)現(xiàn)TCP首部和數(shù)據(jù)在發(fā)送端到接收端之間發(fā)生的任何改動。如果接收方檢測到校驗和有差錯,則TCP段會被直接丟棄。TCP校驗和覆蓋 TCP 首部和 TCP 數(shù)據(jù)。
TCP的校驗和是必需的
發(fā)到 IP 層處理:調(diào)用 IP handler 句柄 ip_queue_xmit,將 skb 傳入 IP 處理流程。
UDP 棧簡要過程:
UDP 將 message 封裝成 UDP 數(shù)據(jù)報
調(diào)用 ip_append_data() 方法將 packet 送到 IP 層進行處理。
下面我們結(jié)合代碼依次分析:
根據(jù)我們對應(yīng)用層的追查可以發(fā)現(xiàn),傳輸層也是先調(diào)用send()->sendto()->sys_sento->sock_sendmsg->sock_sendmsg_nosec,我們看下sock_sendmsg_nosec這個函數(shù):
在應(yīng)用層調(diào)用的是inet_sendmsg函數(shù),在傳輸層根據(jù)后面的斷點可以知道,調(diào)用的是sock->ops-sendmsg這個函數(shù)。而sendmsg為一個宏,調(diào)用的是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實際上調(diào)用的是
int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
這個函數(shù)如下:
int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size){ struct tcp_sock *tp = tcp_sk(sk);/*進行了強制類型轉(zhuǎn)換*/ 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中,完成的是將所有的數(shù)據(jù)組織成發(fā)送隊列,這個發(fā)送隊列是struct sock結(jié)構(gòu)中的一個域sk_write_queue,這個隊列的每一個元素是一個skb,里面存放的就是待發(fā)送的數(shù)據(jù)。然后調(diào)用了tcp_push()函數(shù)。結(jié)構(gòu)體struct sock如下:
struct sock{ ... struct sk_buff_head sk_write_queue;/*指向skb隊列的第一個元素*/ ... struct sk_buff *sk_send_head;/*指向隊列第一個還沒有發(fā)送的元素*/}
在tcp協(xié)議的頭部有幾個標志字段: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結(jié)構(gòu)體存放的就是tcp的頭部,頭部的控制位為tcp_flags,通過tcp_mark_push會將skb中的cb,也就是48個字節(jié)的數(shù)組,類型轉(zhuǎn)換為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個字節(jié)tcp[13]) */ ......};
然后,tcp_push調(diào)用了__tcp_push_pending_frames(sk, mss_now, nonagle);函數(shù)發(fā)送數(shù)據(jù):
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);}
發(fā)現(xiàn)它調(diào)用了tcp_write_xmit函數(shù)來發(fā)送數(shù)據(jù):
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; /*統(tǒng)計已發(fā)送的報文總數(shù)*/ sent_pkts = 0; ...... /*若發(fā)送隊列未滿,則準備發(fā)送報文*/ 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); /*檢查發(fā)送窗口的大小*/ 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中,它實現(xiàn)了tcp的擁塞控制,然后調(diào)用了tcp_transmit_skb(sk, skb, 1, gfp)傳輸數(shù)據(jù),實際上調(diào)用的是__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); ...... /* 構(gòu)建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發(fā)送數(shù)據(jù)位于傳輸層的最后一步,這里首先對TCP數(shù)據(jù)段的頭部進行了處理,然后調(diào)用了網(wǎng)絡(luò)層提供的發(fā)送接口icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);實現(xiàn)了數(shù)據(jù)的發(fā)送,自此,數(shù)據(jù)離開了傳輸層,傳輸層的任務(wù)也就結(jié)束了。
gdb調(diào)試驗證如下:
4.2 接收端
傳輸層 TCP 處理入口在 tcp_v4_rcv 函數(shù)(位于 linux/net/ipv4/tcp ipv4.c 文件中),它會做 TCP header 檢查等處理。
調(diào)用 _tcp_v4_lookup,查找該 package 的 open socket。如果找不到,該 package 會被丟棄。接下來檢查 socket 和 connection 的狀態(tài)。
如果socket 和 connection 一切正常,調(diào)用 tcp_prequeue 使 package 從內(nèi)核進入 user space,放進 socket 的 receive queue。然后 socket 會被喚醒,調(diào)用 system call,并最終調(diào)用 tcp_recvmsg 函數(shù)去從 socket recieve queue 中獲取 segment。
對于傳輸層的代碼階段,我們需要分析recv函數(shù),這個與send類似,調(diào)用的是__sys_recvfrom,整個函數(shù)的調(diào)用路徑與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調(diào)用了sock_recvmsg來接收數(shù)據(jù),整個函數(shù)實際調(diào)用的是sock->ops->recvmsg(sock, msg, msg_data_left(msg), flags);,同樣,根據(jù)tcp_prot結(jié)構(gòu)的初始化,調(diào)用的其實是tcp_rcvmsg
接受函數(shù)比發(fā)送函數(shù)要復(fù)雜得多,因為數(shù)據(jù)接收不僅僅只是接收,tcp的三次握手也是在接收函數(shù)實現(xiàn)的,所以收到數(shù)據(jù)后要判斷當前的狀態(tài),是否正在建立連接等,根據(jù)發(fā)來的信息考慮狀態(tài)是否要改變,在這里,我們僅僅考慮在連接建立后數(shù)據(jù)的接收。
tcp_rcvmsg函數(shù)如下:
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,分別為預(yù)處理隊列,后備隊列和接收隊列,在連接建立后,若沒有數(shù)據(jù)到來,接收隊列為空,進程會在sk_busy_loop函數(shù)內(nèi)循環(huán)等待,知道接收隊列不為空,并調(diào)用函數(shù)數(shù)skb_copy_datagram_msg將接收到的數(shù)據(jù)拷貝到用戶態(tài),實際調(diào)用的是__skb_datagram_iter,這里同樣用了struct msghdr *msg來實現(xiàn)。__skb_datagram_iter函數(shù)如下:
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; } /* 拷貝數(shù)據(jù)部分 */ 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; }
拷貝完成后,函數(shù)返回,整個接收的過程也就完成了。用一張函數(shù)間的相互調(diào)用圖可以表示:
通過gdb調(diào)試驗證如下:
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 發(fā)送端
網(wǎng)絡(luò)層的任務(wù)就是選擇合適的網(wǎng)間路由和交換結(jié)點, 確保數(shù)據(jù)及時傳送。網(wǎng)絡(luò)層將數(shù)據(jù)鏈路層提供的幀組成數(shù)據(jù)包,包中封裝有網(wǎng)絡(luò)層包頭,其中含有邏輯地址信息- -源站點和目的站點地址的網(wǎng)絡(luò)地址。其主要任務(wù)包括 (1)路由處理,即選擇下一跳 (2)添加 IP header(3)計算 IP header checksum,用于檢測 IP 報文頭部在傳播過程中是否出錯 (4)可能的話,進行 IP 分片(5)處理完畢,獲取下一跳的 MAC 地址,設(shè)置鏈路層報文頭,然后轉(zhuǎn)入鏈路層處理。
IP 頭:
IP 棧基本處理過程如下圖所示:
首先,ip_queue_xmit(skb)會檢查skb->dst路由信息。如果沒有,比如套接字的第一個包,就使用ip_route_output()選擇一個路由。
接著,填充IP包的各個字段,比如版本、包頭長度、TOS等。
中間的一些分片等,可參閱相關(guān)文檔?;舅枷胧?,當報文的長度大于mtu,gso的長度不為0就會調(diào)用 ip_fragment 進行分片,否則就會調(diào)用ip_finish_output2把數(shù)據(jù)發(fā)送出去。ip_fragment 函數(shù)中,會檢查 IP_DF 標志位,如果待分片IP數(shù)據(jù)包禁止分片,則調(diào)用 icmp_send()向發(fā)送方發(fā)送一個原因為需要分片而設(shè)置了不分片標志的目的不可達ICMP報文,并丟棄報文,即設(shè)置IP狀態(tài)為分片失敗,釋放skb,返回消息過長錯誤碼。
接下來就用 ip_finish_ouput2 設(shè)置鏈路層報文頭了。如果,鏈路層報頭緩存有(即hh不為空),那就拷貝到skb里。如果沒,那么就調(diào)用neigh_resolve_output,使用 ARP 獲取。
具體代碼分析如下:
入口函數(shù)是ip_queue_xmit,函數(shù)如下:
發(fā)現(xiàn)調(diào)用了__ip_queue_xmit函數(shù):
發(fā)現(xiàn)調(diào)用了skb_rtable函數(shù),實際上是開始找路由緩存,繼續(xù)看:
發(fā)現(xiàn)調(diào)用ip_local_out進行數(shù)據(jù)發(fā)送:
發(fā)現(xiàn)調(diào)用__ip_local_out函數(shù):
發(fā)現(xiàn)返回一個nf_hook函數(shù),里面調(diào)用了dst_output,這個函數(shù)實質(zhì)上是調(diào)用ip_finish__output函數(shù):
發(fā)現(xiàn)調(diào)用__ip_finish_output函數(shù):
如果分片就調(diào)用ip_fragment,否則就調(diào)用IP_finish_output2函數(shù):
在構(gòu)造好 ip 頭,檢查完分片之后,會調(diào)用鄰居子系統(tǒng)的輸出函數(shù) neigh_output 進行輸 出。neigh_output函數(shù)如下:
輸出分為有二層頭緩存和沒有兩種情況,有緩存時調(diào)用 neigh_hh_output 進行快速輸 出,沒有緩存時,則調(diào)用鄰居子系統(tǒng)的輸出回調(diào)函數(shù)進行慢速輸出。這個函數(shù)如下:
最后調(diào)用dev_queue_xmit函數(shù)進行向鏈路層發(fā)送包,到此結(jié)束。gdb驗證如下:
5.2 接收端
IP 層的入口函數(shù)在 ip_rcv 函數(shù)。該函數(shù)首先會做包括 package checksum 在內(nèi)的各種檢查,如果需要的話會做 IP defragment(將多個分片合并),然后 packet 調(diào)用已經(jīng)注冊的 Pre-routing netfilter hook ,完成后最終到達 ip_rcv_finish 函數(shù)。
ip_rcv_finish 函數(shù)會調(diào)用 ip_router_input 函數(shù),進入路由處理環(huán)節(jié)。它首先會調(diào)用 ip_route_input 來更新路由,然后查找 route,決定該 package 將會被發(fā)到本機還是會被轉(zhuǎn)發(fā)還是丟棄:
如果是發(fā)到本機的話,調(diào)用 ip_local_deliver 函數(shù),可能會做 de-fragment(合并多個 IP packet),然后調(diào)用 ip_local_deliver 函數(shù)。該函數(shù)根據(jù) package 的下一個處理層的 protocal number,調(diào)用下一層接口,包括 tcp_v4_rcv (TCP), udp_rcv (UDP),icmp_rcv (ICMP),igmp_rcv(IGMP)。對于 TCP 來說,函數(shù) tcp_v4_rcv 函數(shù)會被調(diào)用,從而處理流程進入 TCP 棧。
如果需要轉(zhuǎn)發(fā) (forward),則進入轉(zhuǎn)發(fā)流程。該流程需要處理 TTL,再調(diào)用 dst_input 函數(shù)。該函數(shù)會
(1)處理 Netfilter Hook
(2)執(zhí)行 IP fragmentation
(3)調(diào)用 dev_queue_xmit,進入鏈路層處理流程。
接收相對簡單,入口在ip_rcv,這個函數(shù)如下:
里面調(diào)用ip_rcv_finish函數(shù):
發(fā)現(xiàn)調(diào)用dst_input函數(shù),實際上是調(diào)用ip_local_deliver函數(shù):
如果分片,就調(diào)用ip_defrag函數(shù),沒有則調(diào)用ip_local_deliver_finish函數(shù):
發(fā)現(xiàn)調(diào)用ip_protocol_deliver_rcu函數(shù):
調(diào)用完畢之后進入tcp棧,調(diào)用完畢,通過gdb驗證如下:
6 數(shù)據(jù)鏈路層流程
6.1 發(fā)送端
功能上,在物理層提供比特流服務(wù)的基礎(chǔ)上,建立相鄰結(jié)點之間的數(shù)據(jù)鏈路,通過差錯控制提供數(shù)據(jù)幀(Frame)在信道上無差錯的傳輸,并進行各電路上的動作系列。
數(shù)據(jù)鏈路層在不可靠的物理介質(zhì)上提供可靠的傳輸。
該層的作用包括:物理地址尋址、數(shù)據(jù)的成幀、流量控制、數(shù)據(jù)的檢錯、重發(fā)等。在這一層,數(shù)據(jù)的單位稱為幀(frame)。數(shù)據(jù)鏈路層協(xié)議的代表包括:SDLC、HDLC、PPP、STP、幀中繼等。
實現(xiàn)上,Linux 提供了一個 Network device 的抽象層,其實現(xiàn)在 linux/net/core/dev.c。具體的物理網(wǎng)絡(luò)設(shè)備在設(shè)備驅(qū)動中(driver.c)需要實現(xiàn)其中的虛函數(shù)。Network Device 抽象層調(diào)用具體網(wǎng)絡(luò)設(shè)備的函數(shù)。
發(fā)送端調(diào)用dev_queue_xmit,這個函數(shù)實際上調(diào)用__dev_queue_xmit:
發(fā)現(xiàn)它調(diào)用了dev_hard_start_xmit函數(shù):
調(diào)用xmit_one:
調(diào)用trace_net_dev_start_xmit,實際上調(diào)用__net_dev_start_xmit函數(shù):
到此,調(diào)用鏈結(jié)束。gdb調(diào)試如下:
6.2 接收端
簡要過程:
一個 package 到達機器的物理網(wǎng)絡(luò)適配器,當它接收到數(shù)據(jù)幀時,就會觸發(fā)一個中斷,并將通過 DMA 傳送到位于 linux kernel 內(nèi)存中的 rx_ring。
網(wǎng)卡發(fā)出中斷,通知 CPU 有個 package 需要它處理。中斷處理程序主要進行以下一些操作,包括分配 skb_buff 數(shù)據(jù)結(jié)構(gòu),并將接收到的數(shù)據(jù)幀從網(wǎng)絡(luò)適配器I/O端口拷貝到skb_buff 緩沖區(qū)中;
從數(shù)據(jù)幀中提取出一些信息,并設(shè)置 skb_buff 相應(yīng)的參數(shù),這些參數(shù)將被上層的網(wǎng)絡(luò)協(xié)議使用,例如skb->protocol;
終端處理程序經(jīng)過簡單處理后,發(fā)出一個軟中斷(NET_RX_SOFTIRQ),通知內(nèi)核接收到新的數(shù)據(jù)幀。
內(nèi)核 2.5 中引入一組新的 API 來處理接收的數(shù)據(jù)幀,即 NAPI。所以,驅(qū)動有兩種方式通知內(nèi)核:(1) 通過以前的函數(shù)netif_rx;(2)通過NAPI機制。該中斷處理程序調(diào)用 Network device的 netif_rx_schedule 函數(shù),進入軟中斷處理流程,再調(diào)用 net_rx_action 函數(shù)。
該函數(shù)關(guān)閉中斷,獲取每個 Network device 的 rx_ring 中的所有 package,最終 pacakage 從 rx_ring 中被刪除,進入 netif _receive_skb 處理流程。
netif_receive_skb 是鏈路層接收數(shù)據(jù)報的最后一站。它根據(jù)注冊在全局數(shù)組 ptype_all 和 ptype_base 里的網(wǎng)絡(luò)層數(shù)據(jù)報類型,把數(shù)據(jù)報遞交給不同的網(wǎng)絡(luò)層協(xié)議的接收函數(shù)(INET域中主要是ip_rcv和arp_rcv)。該函數(shù)主要就是調(diào)用第三層協(xié)議的接收函數(shù)處理該skb包,進入第三層網(wǎng)絡(luò)層處理。
入口函數(shù)是net_rx_action:
發(fā)現(xiàn)調(diào)用napi_poll,實質(zhì)上調(diào)用napi_gro_receive函數(shù):
napi_gro_receive 會直接調(diào)用 netif_receive_skb_core。而它會調(diào)用__netif_receive_skb_one_core,將數(shù)據(jù)包交給上層 ip_rcv 進行處理。
調(diào)用結(jié)束之后,通過軟中斷通知CPU,至此,調(diào)用鏈結(jié)束。gdb驗證如下:
7 物理層流程
7.1 發(fā)送端
物理層在收到發(fā)送請求之后,通過 DMA 將該主存中的數(shù)據(jù)拷貝至內(nèi)部RAM(buffer)之中。在數(shù)據(jù)拷貝中,同時加入符合以太網(wǎng)協(xié)議的相關(guān)header,IFG、前導(dǎo)符和CRC。對于以太網(wǎng)網(wǎng)絡(luò),物理層發(fā)送采用CSMA/CD,即在發(fā)送過程中偵聽鏈路沖突。
一旦網(wǎng)卡完成報文發(fā)送,將產(chǎn)生中斷通知CPU,然后驅(qū)動層中的中斷處理程序就可以刪除保存的 skb 了。
7.2 接收端
一個 package 到達機器的物理網(wǎng)絡(luò)適配器,當它接收到數(shù)據(jù)幀時,就會觸發(fā)一個中斷,并將通過 DMA 傳送到位于 linux kernel 內(nèi)存中的 rx_ring。
網(wǎng)卡發(fā)出中斷,通知 CPU 有個 package 需要它處理。中斷處理程序主要進行以下一些操作,包括分配 skb_buff 數(shù)據(jù)結(jié)構(gòu),并將接收到的數(shù)據(jù)幀從網(wǎng)絡(luò)適配器I/O端口拷貝到skb_buff 緩沖區(qū)中;從數(shù)據(jù)幀中提取出一些信息,并設(shè)置 skb_buff 相應(yīng)的參數(shù),這些參數(shù)將被上層的網(wǎng)絡(luò)協(xié)議使用,例如skb->protocol;
終端處理程序經(jīng)過簡單處理后,發(fā)出一個軟中斷(NET_RX_SOFTIRQ),通知內(nèi)核接收到新的數(shù)據(jù)幀。
內(nèi)核 2.5 中引入一組新的 API 來處理接收的數(shù)據(jù)幀,即 NAPI。所以,驅(qū)動有兩種方式通知內(nèi)核:(1) 通過以前的函數(shù)netif_rx;(2)通過NAPI機制。該中斷處理程序調(diào)用 Network device的 netif_rx_schedule 函數(shù),進入軟中斷處理流程,再調(diào)用 net_rx_action 函數(shù)。
該函數(shù)關(guān)閉中斷,獲取每個 Network device 的 rx_ring 中的所有 package,最終 pacakage 從 rx_ring 中被刪除,進入 netif _receive_skb 處理流程。
netif_receive_skb 是鏈路層接收數(shù)據(jù)報的最后一站。它根據(jù)注冊在全局數(shù)組 ptype_all 和 ptype_base 里的網(wǎng)絡(luò)層數(shù)據(jù)報類型,把數(shù)據(jù)報遞交給不同的網(wǎng)絡(luò)層協(xié)議的接收函數(shù)(INET域中主要是ip_rcv和arp_rcv)。該函數(shù)主要就是調(diào)用第三層協(xié)議的接收函數(shù)處理該skb包,進入第三層網(wǎng)絡(luò)層處理。
8 時序圖展示和總結(jié)
時序圖如下:
本次實驗主要是通過分析Linux內(nèi)核源代碼,一步步地通過gdb進行調(diào)試函數(shù)調(diào)用鏈,最終清楚了tcp/ip協(xié)議棧的調(diào)用過程。因為時間有限,部分細節(jié)可能會有錯誤,希望讀者多加指正。