Linux內核源代碼:tcp/ip協議棧的調用
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的客戶端服務器程序,打開服務端并運行,再開一終端運行客戶端,兩者建立連接并可以發送hellohi的信息,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] = '';
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");
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!", 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 方法。
請輸入評論內容...
請輸入評論/評論長度6~500個字
最新活動更多
-
11月7日立即參評>> 【評選】維科杯·OFweek 2025(第十屆)物聯網行業年度評選
-
11月20日立即報名>> 【免費下載】RISC-V芯片發展現狀與測試挑戰-白皮書
-
即日-11.25立即下載>>> 費斯托白皮書《柔性:汽車生產未來的關鍵》
-
11月27日立即報名>> 【工程師系列】汽車電子技術在線大會
-
11月28日立即下載>> 【白皮書】精準洞察 無線掌控——283FC智能自檢萬用表
-
12月18日立即報名>> 【線下會議】OFweek 2025(第十屆)物聯網產業大會


分享













