數據報文的封裝與分用

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

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

Linux 內核網路協議棧
協議棧的全景圖

協議棧的分層結構


邏輯抽象層級:
物理層:主要提供各種連接的物理設備,如各種網卡,串口卡等。
鏈路層:主要提供對物理層進行訪問的各種介面卡的驅動程序,如網卡驅動等。
網路層:是負責將網路數據包傳輸到正確的位置,最重要的網路層協議是 IP 協議,此外還有如 ICMP,ARP,RARP 等協議。
傳輸層:為應用程序之間提供端到端連接,主要為 TCP 和 UDP 協議。
應用層:顧名思義,主要由應用程序提供,用來對傳輸數據進行語義解釋的 「人機交互界面層」,比如 HTTP,SMTP,FTP 等協議。
【文章福利】需要C/C++ Linux伺服器架構師學習資料加群812855908(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等)

協議棧實現層級:
硬體層(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)應用程序的介面調用庫,向用戶空間應用程序提供使用網路服務的介面。

協議棧的數據結構

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.019n");
/*
* 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
}
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 提供的系統調用介面。


協議棧初始化完成後再執行 dev_init(),繼續設備的初始化。
Socket 創建流程

協議棧收包流程概述
硬體層與設備無關層:硬體監聽物理介質,進行數據的接收,當接收的數據填滿了緩衝區,硬體就會產生中斷,中斷產生後,系統會轉向中斷服務子程序。在中斷服務子程序中,數據會從硬體的緩衝區複製到內核的空間緩衝區,並包裝成一個數據結構(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 的收發包流程總覽

內核中斷收包流程

UDP 收包流程

UDP 發包流程

原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/281311.html
微信掃一掃
支付寶掃一掃