訂閱
糾錯(cuò)
加入自媒體

Linux內(nèi)核源代碼:tcp/ip協(xié)議棧的調(diào)用

1 Linux概述

1.1 Linux操作系統(tǒng)架構(gòu)簡介

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é)議棧簡介

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é)議對應(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)卡,串口卡等;鏈路層主要指的是提供對物理層進(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)用程序提供的,用來對傳輸數(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)用層之間的接口可以簡單,統(tǒng)一。簡單的說,不管我們應(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ū)樱@一層的目的就很簡單了,就是建立與硬件的接口層。
  可以看到,Linux網(wǎng)絡(luò)協(xié)議棧是一個(gè)嚴(yán)格分層的結(jié)構(gòu),其中的每一層都執(zhí)行相對獨(dú)立的功能,結(jié)構(gòu)非常清晰。
  其中的兩個(gè)“無關(guān)”層的設(shè)計(jì)非常棒,通過這兩個(gè)“無關(guān)”層,其協(xié)議?梢苑浅]p松的進(jìn)行擴(kuò)展。在我們自己的軟件設(shè)計(jì)中,可以吸收這種設(shè)計(jì)方法。

2 代碼簡介

本文采用的測試代碼是一個(gè)非常簡單的基于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的請求接收隊(duì)列長度
#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;  連接對方的地址信息
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;  對方地址信息
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;

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

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。對于每一個(gè) userspace 網(wǎng)絡(luò)應(yīng)用創(chuàng)建的 socket,在內(nèi)核中都有一個(gè)對應(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 對應(yīng)的 Linux 網(wǎng)絡(luò)棧 sk_buffer 數(shù)據(jù)結(jié)構(gòu)的實(shí)例 skb。

對于 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ù)。

對于 TCP ,調(diào)用 tcp_sendmsg 函數(shù)。對于 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。見下圖:

我們再跳轉(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操作的一些信息,如下圖:

我們再來看看__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進(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ù)。

對于 INET 類型的 socket,/net/ipv4/af inet.c 中的 inet_recvmsg 方法會(huì)被調(diào)用,它會(huì)調(diào)用相關(guān)協(xié)議的數(shù)據(jù)接收方法。

對 TCP 來說,調(diào)用 tcp_recvmsg。該函數(shù)從 socket buffer 中拷貝數(shù)據(jù)到 user buffer。

對 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 方法。

1  2  3  4  下一頁>  
聲明: 本文由入駐維科號(hào)的作者撰寫,觀點(diǎn)僅代表作者本人,不代表OFweek立場。如有侵權(quán)或其他問題,請聯(lián)系舉報(bào)。

發(fā)表評論

0條評論,0人參與

請輸入評論內(nèi)容...

請輸入評論/評論長度6~500個(gè)字

您提交的評論過于頻繁,請輸入驗(yàn)證碼繼續(xù)

  • 看不清,點(diǎn)擊換一張  刷新

暫無評論

暫無評論

人工智能 獵頭職位 更多
掃碼關(guān)注公眾號(hào)
OFweek人工智能網(wǎng)
獲取更多精彩內(nèi)容
文章糾錯(cuò)
x
*文字標(biāo)題:
*糾錯(cuò)內(nèi)容:
聯(lián)系郵箱:
*驗(yàn) 證 碼:

粵公網(wǎng)安備 44030502002758號(hào)