Linux內(nèi)核源代碼:tcp/ip協(xié)議棧的調(diào)用
1 Linux概述
1.1 Linux操作系統(tǒng)架構(gòu)簡(jiǎn)介
Linux操作系統(tǒng)總體上由Linux內(nèi)核和GNU系統(tǒng)構(gòu)成,具體來講由4個(gè)主要部分構(gòu)成,即Linux內(nèi)核、Shell、文件系統(tǒng)和應(yīng)用程序。內(nèi)核、Shell和文件系統(tǒng)構(gòu)成了操作系統(tǒng)的基本結(jié)構(gòu),使得用戶可以運(yùn)行程序、管理文件并使用系統(tǒng)。
內(nèi)核是操作系統(tǒng)的核心,具有很多最基本功能,如虛擬內(nèi)存、多任務(wù)、共享庫、需求加載、可執(zhí)行程序和TCP/IP網(wǎng)絡(luò)功能。我們所調(diào)研的工作,就是在Linux內(nèi)核層面進(jìn)行分析。
1.2 協(xié)議棧簡(jiǎn)介
OSI(Open System Interconnect),即開放式系統(tǒng)互聯(lián)。一般都叫OSI參考模型,是ISO(國際標(biāo)準(zhǔn)化組織)組織在1985年研究的網(wǎng)絡(luò)互連模型。
ISO為了更好的使網(wǎng)絡(luò)應(yīng)用更為普及,推出了OSI參考模型。其含義就是推薦所有公司使用這個(gè)規(guī)范來控制網(wǎng)絡(luò)。這樣所有公司都有相同的規(guī)范,就能互聯(lián)了。
OSI定義了網(wǎng)絡(luò)互連的七層框架(物理層、數(shù)據(jù)鏈路層、網(wǎng)絡(luò)層、傳輸層、會(huì)話層、表示層、應(yīng)用層),即ISO開放互連系統(tǒng)參考模型。如下圖。
每一層實(shí)現(xiàn)各自的功能和協(xié)議,并完成與相鄰層的接口通信。OSI的服務(wù)定義詳細(xì)說明了各層所提供的服務(wù)。某一層的服務(wù)就是該層及其下各層的一種能力,它通過接口提供給更高一層。各層所提供的服務(wù)與這些服務(wù)是怎么實(shí)現(xiàn)的無關(guān)。
osi七層模型已經(jīng)成為了理論上的標(biāo)準(zhǔn),但真正運(yùn)用于實(shí)踐中的是TCP/IP五層模型。
TCP/IP五層協(xié)議和osi的七層協(xié)議對(duì)應(yīng)關(guān)系如下:
在每一層實(shí)現(xiàn)的協(xié)議也各不同,即每一層的服務(wù)也不同.下圖列出了每層主要的協(xié)議。
1.3 Linux內(nèi)核協(xié)議棧
Linux的協(xié)議棧其實(shí)是源于BSD的協(xié)議棧,它向上以及向下的接口以及協(xié)議棧本身的軟件分層組織的非常好。
Linux的協(xié)議;诜謱拥脑O(shè)計(jì)思想,總共分為四層,從下往上依次是:物理層,鏈路層,網(wǎng)絡(luò)層,應(yīng)用層。
物理層主要提供各種連接的物理設(shè)備,如各種網(wǎng)卡,串口卡等;鏈路層主要指的是提供對(duì)物理層進(jìn)行訪問的各種接口卡的驅(qū)動(dòng)程序,如網(wǎng)卡驅(qū)動(dòng)等;網(wǎng)路層的作用是負(fù)責(zé)將網(wǎng)絡(luò)數(shù)據(jù)包傳輸?shù)秸_的位置,最重要的網(wǎng)絡(luò)層協(xié)議當(dāng)然就是IP協(xié)議了,其實(shí)網(wǎng)絡(luò)層還有其他的協(xié)議如ICMP,ARP,RARP等,只不過不像IP那樣被多數(shù)人所熟悉;傳輸層的作用主要是提供端到端,說白一點(diǎn)就是提供應(yīng)用程序之間的通信,傳輸層最著名的協(xié)議非TCP與UDP協(xié)議末屬了;應(yīng)用層,顧名思義,當(dāng)然就是由應(yīng)用程序提供的,用來對(duì)傳輸數(shù)據(jù)進(jìn)行語義解釋的“人機(jī)界面”層了,比如HTTP,SMTP,F(xiàn)TP等等,其實(shí)應(yīng)用層還不是人們最終所看到的那一層,最上面的一層應(yīng)該是“解釋層”,負(fù)責(zé)將數(shù)據(jù)以各種不同的表項(xiàng)形式最終呈獻(xiàn)到人們眼前。
Linux網(wǎng)絡(luò)核心架構(gòu)Linux的網(wǎng)絡(luò)架構(gòu)從上往下可以分為三層,分別是:
用戶空間的應(yīng)用層。
內(nèi)核空間的網(wǎng)絡(luò)協(xié)議棧層。
物理硬件層。
其中最重要最核心的當(dāng)然是內(nèi)核空間的協(xié)議棧層了。
Linux網(wǎng)絡(luò)協(xié)議棧結(jié)構(gòu)Linux的整個(gè)網(wǎng)絡(luò)協(xié)議棧都構(gòu)建與Linux Kernel中,整個(gè)棧也是嚴(yán)格按照分層的思想來設(shè)計(jì)的,整個(gè)棧共分為五層,分別是 :
1,系統(tǒng)調(diào)用接口層,實(shí)質(zhì)是一個(gè)面向用戶空間應(yīng)用程序的接口調(diào)用庫,向用戶空間應(yīng)用程序提供使用網(wǎng)絡(luò)服務(wù)的接口。
2,協(xié)議無關(guān)的接口層,就是SOCKET層,這一層的目的是屏蔽底層的不同協(xié)議(更準(zhǔn)確的來說主要是TCP與UDP,當(dāng)然還包括RAW IP, SCTP等),以便與系統(tǒng)調(diào)用層之間的接口可以簡(jiǎn)單,統(tǒng)一。簡(jiǎn)單的說,不管我們應(yīng)用層使用什么協(xié)議,都要通過系統(tǒng)調(diào)用接口來建立一個(gè)SOCKET,這個(gè)SOCKET其實(shí)是一個(gè)巨大的sock結(jié)構(gòu),它和下面一層的網(wǎng)絡(luò)協(xié)議層聯(lián)系起來,屏蔽了不同的網(wǎng)絡(luò)協(xié)議的不同,只吧數(shù)據(jù)部分呈獻(xiàn)給應(yīng)用層(通過系統(tǒng)調(diào)用接口來呈獻(xiàn))。
3,網(wǎng)絡(luò)協(xié)議實(shí)現(xiàn)層,毫無疑問,這是整個(gè)協(xié)議棧的核心。這一層主要實(shí)現(xiàn)各種網(wǎng)絡(luò)協(xié)議,最主要的當(dāng)然是IP,ICMP,ARP,RARP,TCP,UDP等。這一層包含了很多設(shè)計(jì)的技巧與算法,相當(dāng)?shù)牟诲e(cuò)。
4,與具體設(shè)備無關(guān)的驅(qū)動(dòng)接口層,這一層的目的主要是為了統(tǒng)一不同的接口卡的驅(qū)動(dòng)程序與網(wǎng)絡(luò)協(xié)議層的接口,它將各種不同的驅(qū)動(dòng)程序的功能統(tǒng)一抽象為幾個(gè)特殊的動(dòng)作,如open,close,init等,這一層可以屏蔽底層不同的驅(qū)動(dòng)程序。
5,驅(qū)動(dòng)程序?qū),這一層的目的就很簡(jiǎn)單了,就是建立與硬件的接口層。
可以看到,Linux網(wǎng)絡(luò)協(xié)議棧是一個(gè)嚴(yán)格分層的結(jié)構(gòu),其中的每一層都執(zhí)行相對(duì)獨(dú)立的功能,結(jié)構(gòu)非常清晰。
其中的兩個(gè)“無關(guān)”層的設(shè)計(jì)非常棒,通過這兩個(gè)“無關(guān)”層,其協(xié)議棧可以非常輕松的進(jìn)行擴(kuò)展。在我們自己的軟件設(shè)計(jì)中,可以吸收這種設(shè)計(jì)方法。
2 代碼簡(jiǎn)介
本文采用的測(cè)試代碼是一個(gè)非常簡(jiǎn)單的基于socket的客戶端服務(wù)器程序,打開服務(wù)端并運(yùn)行,再開一終端運(yùn)行客戶端,兩者建立連接并可以發(fā)送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 監(jiān)聽的端口
#define BACKLOG 10 listen的請(qǐng)求接收隊(duì)列長(zhǎng)度
#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); 網(wǎng)絡(luò)字節(jié)順序
sa.sin_addr.s_addr = INADDR_ANY; 自動(dòng)填本機(jī)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; 連接對(duì)方的地址信息
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 一次可以讀的最大字節(jié)數(shù)
int main(int argc, char *argv[])
{
int sockfd, numbytes;
char buf[MAXDATASIZE];
struct hostent *he; 主機(jī)信息
struct sockaddr_in server_addr; 對(duì)方地址信息
if (argc 。 2)
{
fprintf(stderr, "usage: client hostname");
exit(1);
}
get the host info
if ((he = gethostbyname(argv[1])) == NULL)
{
注意:獲取DNS信息時(shí),顯示出錯(cuò)需要用herror而不是perror
herror 在新的版本中會(huì)出現(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。ⅲ 14, 0,
(struct sockaddr *)&server_addr,
sizeof(server_addr))) == -1)
{
perror("sendto");
exit(1);
}
close(sockfd);
return true;
}
簡(jiǎn)單來說,主要流程如下圖所示:
3 應(yīng)用層流程
3.1 發(fā)送端
網(wǎng)絡(luò)應(yīng)用調(diào)用Socket API socket (int family, int type, int protocol) 創(chuàng)建一個(gè) socket,該調(diào)用最終會(huì)調(diào)用 Linux system call socket() ,并最終調(diào)用 Linux Kernel 的 sock_create() 方法。該方法返回被創(chuàng)建好了的那個(gè) socket 的 file descriptor。對(duì)于每一個(gè) userspace 網(wǎng)絡(luò)應(yīng)用創(chuàng)建的 socket,在內(nèi)核中都有一個(gè)對(duì)應(yīng)的 struct socket和 struct sock。其中,struct sock 有三個(gè)隊(duì)列(queue),分別是 rx , tx 和 err,在 sock 結(jié)構(gòu)被初始化的時(shí)候,這些緩沖隊(duì)列也被初始化完成;在收據(jù)收發(fā)過程中,每個(gè) queue 中保存要發(fā)送或者接受的每個(gè) packet 對(duì)應(yīng)的 Linux 網(wǎng)絡(luò)棧 sk_buffer 數(shù)據(jù)結(jié)構(gòu)的實(shí)例 skb。
對(duì)于 TCP socket 來說,應(yīng)用調(diào)用 connect()API ,使得客戶端和服務(wù)器端通過該 socket 建立一個(gè)虛擬連接。在此過程中,TCP 協(xié)議棧通過三次握手會(huì)建立 TCP 連接。默認(rèn)地,該 API 會(huì)等到 TCP 握手完成連接建立后才返回。在建立連接的過程中的一個(gè)重要步驟是,確定雙方使用的 Maxium Segemet Size (MSS)。因?yàn)?UDP 是面向無連接的協(xié)議,因此它是不需要該步驟的。
應(yīng)用調(diào)用 Linux Socket 的 send 或者 write API 來發(fā)出一個(gè) 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ù)。
對(duì)于 TCP ,調(diào)用 tcp_sendmsg 函數(shù)。對(duì)于 UDP 來說,userspace 應(yīng)用可以調(diào)用 send()/sendto()/sendmsg() 三個(gè) system call 中的任意一個(gè)來發(fā)送 UDP message,它們最終都會(huì)調(diào)用內(nèi)核中的 udp_sendmsg() 函數(shù)。
下面我們具體結(jié)合Linux內(nèi)核源碼進(jìn)行一步步仔細(xì)分析:
根據(jù)上述分析可知,發(fā)送端首先創(chuàng)建socket,創(chuàng)建之后會(huì)通過send發(fā)送數(shù)據(jù)。具體到源碼級(jí)別,會(huì)通過send,sendto,sendmsg這些系統(tǒng)調(diào)用來發(fā)送數(shù)據(jù),而上述三個(gè)函數(shù)底層都調(diào)用了sock_sendmsg。見下圖:
我們?cè)偬D(zhuǎn)到__sys_sendto看看這個(gè)函數(shù)干了什么:
我們可以發(fā)現(xiàn),它創(chuàng)建了兩個(gè)結(jié)構(gòu)體,分別是:struct msghdr msg和struct iovec iov,這兩個(gè)結(jié)構(gòu)體根據(jù)命名我們可以大致猜出是發(fā)送數(shù)據(jù)和io操作的一些信息,如下圖:
我們?cè)賮砜纯矗撸遱ys_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進(jìn)行調(diào)試驗(yàn)證:
剛好符合我們的分析。
3.2 接收端
每當(dāng)用戶應(yīng)用調(diào)用 read 或者 recvfrom 時(shí),該調(diào)用會(huì)被映射為/net/socket.c 中的 sys_recv 系統(tǒng)調(diào)用,并被轉(zhuǎn)化為 sys_recvfrom 調(diào)用,然后調(diào)用 sock_recgmsg 函數(shù)。
對(duì)于 INET 類型的 socket,/net/ipv4/af inet.c 中的 inet_recvmsg 方法會(huì)被調(diào)用,它會(huì)調(diào)用相關(guān)協(xié)議的數(shù)據(jù)接收方法。
對(duì) TCP 來說,調(diào)用 tcp_recvmsg。該函數(shù)從 socket buffer 中拷貝數(shù)據(jù)到 user buffer。
對(duì) UDP 來說,從 user space 中可以調(diào)用三個(gè) system call recv()/recvfrom()/recvmsg() 中的任意一個(gè)來接收 UDP package,這些系統(tǒng)調(diào)用最終都會(huì)調(diào)用內(nèi)核中的 udp_recvmsg 方法。

發(fā)表評(píng)論
請(qǐng)輸入評(píng)論內(nèi)容...
請(qǐng)輸入評(píng)論/評(píng)論長(zhǎng)度6~500個(gè)字
最新活動(dòng)更多
-
3月27日立即報(bào)名>> 【工程師系列】汽車電子技術(shù)在線大會(huì)
-
4月30日立即下載>> 【村田汽車】汽車E/E架構(gòu)革新中,新智能座艙挑戰(zhàn)的解決方案
-
5月15-17日立即預(yù)約>> 【線下巡回】2025年STM32峰會(huì)
-
即日-5.15立即報(bào)名>>> 【在線會(huì)議】安森美Hyperlux™ ID系列引領(lǐng)iToF技術(shù)革新
-
5月15日立即下載>> 【白皮書】精確和高效地表征3000V/20A功率器件應(yīng)用指南
-
5月16日立即參評(píng) >> 【評(píng)選啟動(dòng)】維科杯·OFweek 2025(第十屆)人工智能行業(yè)年度評(píng)選
推薦專題
- 1 UALink規(guī)范發(fā)布:挑戰(zhàn)英偉達(dá)AI統(tǒng)治的開始
- 2 北電數(shù)智主辦酒仙橋論壇,探索AI產(chǎn)業(yè)發(fā)展新路徑
- 3 降薪、加班、裁員三重暴擊,“AI四小龍”已折戟兩家
- 4 “AI寒武紀(jì)”爆發(fā)至今,五類新物種登上歷史舞臺(tái)
- 5 國產(chǎn)智駕迎戰(zhàn)特斯拉FSD,AI含量差幾何?
- 6 光計(jì)算迎來商業(yè)化突破,但落地仍需時(shí)間
- 7 東陽光:2024年扭虧、一季度凈利大增,液冷疊加具身智能打開成長(zhǎng)空間
- 8 地平線自動(dòng)駕駛方案解讀
- 9 封殺AI“照騙”,“淘寶們”終于不忍了?
- 10 優(yōu)必選:營(yíng)收大增主靠小件,虧損繼續(xù)又逢關(guān)稅,能否乘機(jī)器人東風(fēng)翻身?