目錄
文章目錄
前文列表
《計算機網絡基礎 — 以太網》
《計算機網絡基礎 — 物理網絡》
《計算機網絡基礎 — TCP/IP 網絡模型》
前言
本文主要記錄 Linux 內核網絡協議棧的運行原理,為學習記錄,僅做參考,大量內容來自網絡,詳見參考文章列表。
NOTE:本文涉及到兩個 Linux kernel 版本 1.2.13 以及 2.6.32。
數據報文的封裝與分用
封裝:當應用程序用 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)應用程序的接口調用庫,向用戶空間應用程序提供使用網絡服務的接口。
協議棧的數據結構
- 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
}
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 到用戶空間緩沖區。數據就完整的從硬件中傳輸到用戶空間。這樣也完成了一次完整的從下到上的傳輸。
協議棧發包流程概述
- 應用層可以通過系統調用接口層或文件操作來調用內核函數,BSD socket 層的
sock_write()
會調用 INET socket 層的inet_wirte()
。INET socket 層會調用具體傳輸層協議的 write 函數,該函數是通過調用本層的inet_send()
來實現的,inet_send()
的 UDP 協議對應的函數為udp_write()
。 - 在傳輸層
udp_write()
調用本層的udp_sendto()
完成功能。udp_sendto()
完成 sk_buff 結構體相應的設置和報頭的填寫后會調用udp_send()
來發送數據。而在udp_send()
中,最后會調用ip_queue_xmit()
將數據包下放的網絡層。 - 在網絡層,函數
ip_queue_xmit()
的功能是將數據包進行一系列復雜的操作,比如是檢查數據包是否需要分片,是否是多播等一系列檢查,最后調用dev_queue_xmit()
發送數據。 - 在鏈路層中,函數調用會調用具體設備提供的發送函數來發送數據包,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://blog.csdn.net/zxorange321/article/details/75676063
https://blog.csdn.net/geekcome/article/details/8333011