詳解Linux核心網路協議棧資料報文的封裝與分用

資料報文的封裝與分用

封裝:當應用程式用 TCP 協議傳送資料時,資料首先進入核心網路協議棧中,然後逐一透過 TCP/IP 協議族的每層直到被當作一串位元流送入網路。對於每一層而言,對收到的資料都會封裝相應的協議首部資訊(有時還會增加尾部資訊)。TCP 協議傳給 IP 協議的資料單元稱作 TCP 報文段,或簡稱 TCP 段(TCP segment)。IP 傳給資料鏈路層的資料單元稱作 IP 資料報(IP datagram),最後透過乙太網傳輸的位元流稱作幀(Frame)

分用:當目的主機收到一個乙太網資料幀時,資料就開始從核心網路協議棧中由底向上升,同時去掉各層協議加上的報文首部。每層協議都會檢查報文首部中的協議標識,以確定接收資料的上層協議。這個過程稱作分用。

Linux 核心網路協議棧

協議棧的分層結構

邏輯抽象層級:

物理層:主要提供各種連線的物理裝置,如各種網絡卡,串列埠卡等。

鏈路層:主要提供對物理層進行訪問的各種介面卡的驅動程式,如網絡卡驅動等。

網路層:是負責將網路資料包傳輸到正確的位置,最重要的網路層協議是 IP 協議,此外還有如 ICMP,ARP,RARP 等協議。

傳輸層:為應用程式之間提供端到端的連線,主要為 TCP 和 UDP 協議。

應用層:顧名思義,主要由應用程式提供,用來對傳輸資料進行語義解釋的 “人機互動介面層”,比如說 HTTP,SMTP,FTP 等協議。

協議棧實現層級:

硬體層(Physical device hardware):又稱驅動程式層,提供連線硬體裝置的介面。

裝置無關層(Device agnostic interface):又稱裝置介面層,提供與具體裝置無關的驅動程式抽象介面。這一層的目的主要是為了統一不同的介面卡的驅動程式與網路協議層的介面,它將各種不同的驅動程式的功能統一抽象為幾個特殊的動作,如 open,close,init 等,這一層可以遮蔽底層不同的驅動程式。

網路協議層(Network protocols):對應 IP layer 和 Transport layer。毫無疑問,這是整個核心網路協議棧的核心。這一層主要實現了各種網路協議,最主要的當然是 IP,ICMP,ARP,RARP,TCP,UDP 等。

協議無關層(Protocol agnostic interface),又稱協議介面層,本質就是 SOCKET 層。這一層的目的是遮蔽網路協議層中諸多型別的網路協議(主要是 TCP 與 UDP 協議,當然也包括 RAW IP, SCTP 等等),以便提供簡單而統一的介面給上面的系統呼叫層呼叫。簡單地說,不管我們應用層使用什麼協議,都要透過系統呼叫介面來建立一個 SOCKET,這個 SOCKET 其實是一個巨大的 sock 結構體,它和下面的網路協議層聯絡起來,遮蔽了不同的網路協議,透過系統呼叫介面只把資料部分呈現給應用層。

BSD(Berkeley Software Distribution)socket:BSD Socket 層,提供統一的 SOCKET 操作介面,與 socket 結構體關係緊密。

INET(指一切支援 IP 協議的網路) socket:INET socket 層,呼叫 IP 層協議的統一介面,與 sock 結構體關係緊密。

系統呼叫介面層(System call interface),實質是一個面向使用者空間(User Space)應用程式的介面呼叫庫,向用戶空間應用程式提供使用網路服務的介面。

核心學習網站:

Linux核心原始碼/記憶體調優/檔案系統/程序管理/裝置驅動/網路協議棧-學習影片教程-騰訊課堂

協議棧的資料結構

msghdr:描述了從應用層傳遞下來的訊息格式,包含有使用者空間地址,訊息標記等重要資訊。

iovec:描述了使用者空間地址的起始位置。

file:描述檔案屬性的結構體,與檔案描述符一一對應。

file_operations:檔案操作相關結構體,包括 read()、write()、open()、ioctl() 等。

socket:嚮應用層提供的 BSD socket 操作結構體,協議無關,主要作用是為應用層提供統一的 Socket 操作。

sock:網路層 sock,定義與協議無關操作,是網路層的統一的結構,傳輸層在此基礎上實現了 inet_sock。

sock_common:最小網路層表示結構體。

inet_sock:表示層結構體,在 sock 上面做的擴充套件,用於在網路層之上表示 inet 協議族的的傳輸層公共結構體。

udp_sock:傳輸層 UDP 協議專用 sock 結構,在傳輸層 inet_sock 上擴充套件。

proto_ops:BSD socket 層到 inet_sock 層介面,主要用於操作 socket 結構。

proto:inet_sock 層到傳輸層操作的統一介面,主要用於操作 sock 結構。

net_proto_family:用於標識和註冊協議族,常見的協議族有 IPv4、IPv6。

softnet_data:核心為每個 CPU 每人分配一個這樣的 softnet_data 資料空間。每個 CPU 都有一個這樣的佇列,用於接收資料包。

sk_buff:描述一個幀結構的屬性,包含 socket、到達時間、到達裝置、各層首部大小、下一站路由入口、幀長度、校驗和等等。

sk_buff_head:資料包佇列結構。

net_device:這個巨大的結構體描述一個網路裝置的所有屬性,資料等資訊。

inet_protosw:向 IP 層註冊 socket 層的呼叫操作介面。

inetsw_array:socket 層呼叫 IP 層操作介面都在這個陣列中註冊。

sock_type:socket 型別。

IPPROTO:傳輸層協議型別 ID。

net_protocol:用於傳輸層協議向 IP 層註冊收包的介面。

packet_type:乙太網資料幀的結構,包括了乙太網幀型別、處理方法等。

rtable:路由表結構,描述一個路由表的完整形態。

rt_hash_bucket:路由表快取。

dst_entry:包的去向介面,描述了包的去留,下一跳等路由關鍵資訊。

napi_struct:NAPI 排程的結構。NAPI 是 Linux 上採用的一種提高網路處理效率的技術,它的核心概念就是不採用中斷的方式讀取資料,而代之以首先採用中斷喚醒資料接收服務,然後採用 poll 的方法來輪詢資料。NAPI 技術適用於高速率的短長度資料包的處理。

網路協議棧初始化流程

這需要從核心啟動流程說起。當核心完成自解壓過程後進入核心啟動流程,這一過程先在 arch/mips/kernel/head。S 程式中,這個程式負責資料區(BBS)、中斷描述表(IDT)、段描述表(GDT)、頁表和暫存器的初始化,程式中定義了核心的入口函式 kernel_entry()、kernel_entry() 函式是體系結構相關的彙編程式碼,它首先初始化核心堆疊段為建立系統中的第一過程進行準備,接著用一段迴圈將核心映像的未初始化的資料段清零,最後跳到 start_kernel() 函式中初始化硬體相關的程式碼,完成 Linux Kernel 環境的建立。

start_kenrel() 定義在 init/main。c 中,真正的核心初始化過程就是從這裡才開始。函式 start_kerenl() 將會呼叫一系列的初始化函式,如:平臺初始化,記憶體初始化,陷阱初始化,中斷初始化,程序排程初始化,緩衝區初始化,完成核心本身的各方面設定,目的是最終建立起基本完整的 Linux 核心環境。

start_kernel() 中主要函式及呼叫關係如下:

start_kernel() 的過程中會執行 socket_init() 來完成協議棧的初始化,實現如下:

void sock_init(void)//網路棧初始化{ int i; printk(“Swansea University Computer Society NET3。019\n”); /* * Initialize all address (protocol) families。 */ for (i = 0; i < NPROTO; ++i) pops[i] = NULL; /* * Initialize the protocols module。 */ proto_init(); #ifdef CONFIG_NET /* * Initialize the DEV module。 */ dev_init(); /* * And the bottom half handler */ bh_base[NET_BH]。routine= net_bh; enable_bh(NET_BH);#endif }

詳解Linux核心網路協議棧資料報文的封裝與分用

sock_init() 包含了核心協議棧的初始化工作:

sock_init:Initialize sk_buff SLAB cache,註冊 SOCKET 檔案系統。

net_inuse_init:為每個 CPU 分配快取。

proto_init:在 /proc/net 域下建立 protocols 檔案,註冊相關檔案操作函式。

net_dev_init:建立 netdevice 在 /proc/sys 相關的資料結構,並且開啟網絡卡收發中斷;為每個 CPU 初始化一個數據包接收佇列(softnet_data),包接收的回撥;註冊本地迴環操作,註冊預設網路裝置操作。

inet_init:註冊 INET 協議族的 SOCKET 建立方法,註冊 TCP、UDP、ICMP、IGMP 介面基本的收包方法。為 IPv4 協議族建立 proc 檔案。此函式為協議棧主要的註冊函式:

rc = proto_register(&udp_prot, 1);:註冊 INET 層 UDP 協議,為其分配快速快取。

(void)sock_register(&inet_family_ops);:向 static const struct net_proto_family *net_families[NPROTO] 結構體註冊 INET 協議族的操作集合(主要是 INET socket 的建立操作)。

inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0;:向 externconst struct net_protocol *inet_protos[MAX_INET_PROTOS] 結構體註冊傳輸層 UDP 的操作集合。

static struct list_head inetsw[SOCK_MAX]; for (r = &inetsw[0]; r < &inetsw[SOCK_MAX];++r) INIT_LIST_HEAD(r);:初始化 SOCKET 型別陣列,其中儲存了這是個連結串列陣列,每個元素是一個連結串列,連線使用同種 SOCKET 型別的協議和操作集合。

for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q):

inet_register_protosw(q);:向 sock 註冊協議的的呼叫操作集合。

arp_init();:啟動 ARP 協議支援。

ip_init();:啟動 IP 協議支援。

udp_init();:啟動 UDP 協議支援。

dev_add_pack(&ip_packet_type);:向 ptype_base[PTYPE_HASH_SIZE]; 註冊 IP 協議的操作集合。

socket。c 提供的系統呼叫介面。

詳解Linux核心網路協議棧資料報文的封裝與分用

詳解Linux核心網路協議棧資料報文的封裝與分用

協議棧初始化完成後再執行 dev_init(),繼續裝置的初始化。

Socket 建立流程

詳解Linux核心網路協議棧資料報文的封裝與分用

協議棧收包流程概述

硬體層與裝置無關層:硬體監聽物理介質,進行資料的接收,當接收的資料填滿了緩衝區,硬體就會產生中斷,中斷產生後,系統會轉向中斷服務子程式。在中斷服務子程式中,資料會從硬體的緩衝區複製到核心的空間緩衝區,幷包裝成一個數據結構(sk_buff),然後呼叫對驅動層的介面函式 netif_rx() 將資料包傳送給裝置無關層。該函式的實現在 net/inet/dev。c 中,採用了 bootom half 技術,該技術的原理是將中斷處理程式人為的分為兩部分,上半部分是實時性要求較高的任務,後半部分可以稍後完成,這樣就可以節省中斷程式的處理時間,整體提高了系統的效能。

NOTE:在整個協議棧實現中 dev。c 檔案的作用重大,它銜接了其下的硬體層和其上的網路協議層,可以稱它為鏈路層模組,或者裝置無關層的實現。

網路協議層:就以 IP 資料報為例,從裝置無關層向網路協議層傳遞時會呼叫 ip_rcv()。該函式會根據 IP 首部中使用的傳輸層協議來呼叫相應協議的處理函式。UDP 對應 udp_rcv()、TCP 對應 tcp_rcv()、ICMP 對應 icmp_rcv()、IGMP 對應 igmp_rcv()。以 tcp_rcv() 為例,所有使用 TCP 協議的套接字對應的 sock 結構體都被掛入 tcp_prot 全域性變量表示的 proto 結構之 sock_array 陣列中,採用以本地埠號為索引的插入方式。所以,當 tcp_rcv() 接收到一個數據包,在完成必要的檢查和處理後,其將以 TCP 協議首部中目的埠號為索引,在 tcp_prot 對應的 sock 結構體之 sock_array 陣列中得到正確的 sock 結構體佇列,再輔之以其他條件遍歷該佇列進行對應 sock 結構體的查詢,在得到匹配的 sock 結構體後,將資料包掛入該 sock 結構體中的快取佇列中(由 sock 結構體中的 receive_queue 欄位指向),從而完成資料包的最終接收。

NOTE:雖然這裡的 ICMP、IGMP 通常被劃分為網路層協議,但是實際上他們都封裝在 IP 協議裡面,作為傳輸層對待。

協議無關層和系統呼叫介面層:當用戶需要接收資料時,首先根據檔案描述符 inode 得到 socket 結構體和 sock 結構體,然後從 sock 結構體中指向的佇列 recieve_queue 中讀取資料包,將資料包 copy 到使用者空間緩衝區。資料就完整的從硬體中傳輸到使用者空間。這樣也完成了一次完整的從下到上的傳輸。

協議棧發包流程概述

1、應用層可以透過系統呼叫介面層或檔案操作來呼叫核心函式,BSD socket 層的 sock_write() 會呼叫 INET socket 層的 inet_wirte()。INET socket 層會呼叫具體傳輸層協議的 write 函式,該函式是透過呼叫本層的 inet_send() 來實現的,inet_send() 的 UDP 協議對應的函式為 udp_write()。

2、在傳輸層 udp_write() 呼叫本層的 udp_sendto() 完成功能。udp_sendto() 完成 sk_buff 結構體相應的設定和報頭的填寫後會呼叫 udp_send() 來發送資料。而在 udp_send() 中,最後會呼叫 ip_queue_xmit() 將資料包下放的網路層。

3、在網路層,函式 ip_queue_xmit() 的功能是將資料包進行一系列複雜的操作,比如是檢查資料包是否需要分片,是否是多播等一系列檢查,最後呼叫 dev_queue_xmit() 傳送資料。

4、在鏈路層中,函式呼叫會呼叫具體裝置提供的傳送函式來發送資料包,e。g。 dev->hard_start_xmit(skb, dev);。具體裝置的傳送函式在協議棧初始化的時候已經設定了。這裡以 8390 網絡卡為例來說明驅動層的工作原理,在 net/drivers/8390。c 中函式 ethdev_init() 的設定如下:

/* Initialize the rest of the 8390 device structure。 */ int ethdev_init(struct device *dev) { if (ei_debug > 1) printk(version); if (dev->priv == NULL) { //申請私有空間 struct ei_device *ei_local; //8390 網絡卡裝置的結構體 dev->priv = kmalloc(sizeof(struct ei_device), GFP_KERNEL); //申請核心記憶體空間 memset(dev->priv, 0, sizeof(struct ei_device)); ei_local = (struct ei_device *)dev->priv; #ifndef NO_PINGPONG ei_local->pingpong = 1; #endif } /* The open call may be overridden by the card-specific code。 */ if (dev->open == NULL) dev->open = &ei_open; // 裝置的開啟函式 /* We should have a dev->stop entry also。 */ dev->hard_start_xmit = &ei_start_xmit; // 裝置的傳送函式,定義在 8390。c 中 dev->get_stats = get_stats; #ifdef HAVE_MULTICAST dev->set_multicast_list = &set_multicast_list; #endif ether_setup(dev); return 0; }

UDP 的收發包流程總覽

核心中斷收包流程

詳解Linux核心網路協議棧資料報文的封裝與分用

UDP 收包流程

詳解Linux核心網路協議棧資料報文的封裝與分用

UDP 發包流程

詳解Linux核心網路協議棧資料報文的封裝與分用