哈嘍大家好,我是鹹魚 我們在跟別人網上聊天的時候,有沒有想過你發送的信息是怎麼傳到對方的電腦上的 又或者我們在上網衝浪的時候,有沒有想過 HTML 頁面是怎麼顯示在我們的電腦屏幕上的 無論是我們跟別人聊天還是上網衝浪,其實都依靠於電腦網路這項技術 > 電腦網路是指將多台電腦通過通信設備和傳輸介 ...
哈嘍大家好,我是鹹魚
我們在跟別人網上聊天的時候,有沒有想過你發送的信息是怎麼傳到對方的電腦上的
又或者我們在上網衝浪的時候,有沒有想過 HTML 頁面是怎麼顯示在我們的電腦屏幕上的
無論是我們跟別人聊天還是上網衝浪,其實都依靠於電腦網路這項技術
電腦網路是指將多台電腦通過通信設備和傳輸介質連接在一起,使得它們之間能夠相互通信、資源共用和協同工作
而電腦之間是通過數據包來實現信息傳輸和信息交換的,數據包是電腦網路中傳輸數據的基本單位
今天鹹魚將以 Linux 為例來給大家介紹一下 Linux 是如何實現網路接收數據包
網路協議棧&網路架構
在正文開始之前,我們先來瞭解一下 Linux 中的網路協議模型和網路子系統
- 網路協議模型(網路協議棧)
在 Linux 中,Linux 網路協議棧分成了五層
其中:
-
應用層提供 socket 介面來供用戶進程訪問內核空間的網路協議棧
-
傳輸層、網路層協議由 Linux 內核網路協議棧實現
-
鏈路層協議靠網卡驅動來實現
-
物理層協議由硬體網卡實現
-
網路子系統(網路架構)
網路子系統是 Linux 內核中的一部分,由多個模塊和驅動程式組成,它負責管理和控制系統的網路功能以實現網路通信
通過 Linux 網路子系統(網路架構)來實現上述網路協議模型
其中
- System call interface:為應用程式獲取內核的網路系統提供了介面,例如 socket
- Protocol agnostic interface:為和各種傳輸層協議的網路交互提供的一層公共介面
- Network protocals:對各種傳輸層協議的實現,如 TCP、UDP、IP 等
- Device agnostic interface:為各種底層網路設備抽象出的公共介面,與各種網路設備驅動連接在一起
- Device drivers:與各種網路設備交互的驅動
收包過程
當 Linux 接收一個數據包的時候,這個包是怎麼經過 Linux 的內核從而被應用程式拿到的呢?
- 到達網卡(NIC,Network Interface Card)
首先數據包到達網卡之後,網卡會校驗接收到的數據包中的目的 MAC 地址是不是自己的 MAC 地址,如果不是的話通常就會丟棄掉
這種只接受發送給自己的數據包(其餘的扔掉)的工作模式稱為非混雜模式(Non-Promiscuous Mode)
混雜模式(Promiscuous Mode)則是網卡會接收通過網路傳輸的所有數據包,而不僅僅是發送給它自己的數據包
非混雜模式是網卡預設的工作模式,可以儘可能的保護網路安全和減少網路負載
網卡在校驗完 MAC 地址之後還會校驗數據幀(Data Frame)中校驗欄位 FCS 來一次確保接收到的數據包是正確的
- 網卡硬體緩衝區 ——> 系統記憶體(ring buffer)
當網卡接收到數據包時,它將數據包的內容存儲在硬體緩衝區中,然後通過 DMA 將接收到的數據從硬體緩衝區傳輸到系統記憶體中的指定位置,這個位置通常是一個環形緩衝區( ring buffer)
DMA(直接記憶體訪問,Direct Memory Access)
DMA是一種數據傳輸技術,允許外設(如網卡、硬碟控制器、顯卡等)直接訪問電腦記憶體,而無需經過 CPU
通過 DMA 可以大大提高數據傳輸的效率,減輕 CPU 的負擔
- 觸發硬中斷
當網卡將數據包 DMA 到用於接收的環形緩衝區(rx_ring)之後,就會觸發一個硬中斷來告訴 CPU 數據包收到了
什麼時候會觸發一個硬中斷,可以通過下麵的參數來進行配置:
- rx-usecs:當過這麼長時間過後,一個中斷就會被產生
- rx-frames:當累計接收到這麼多個數據幀後,一個中斷就會被產生
上面的參數配置可以通過下麵的命令來查看
# 以 CentOS 7 為例
ethtool -c <網卡名稱>
當 ring buffer 滿了之後,新來的數據包將給丟棄
ifconfig 查看網卡的時候,可以裡面有個 overruns,表示因為環形隊列滿而被丟棄的包
CPU 收到硬中斷之後就會停止手中的活,保存上下文,然後去調用網卡驅動註冊的硬中斷處理函數
為數據包分配 skb_buff
,並將接收到的數據拷貝到 skb_buff
緩衝區中
當一個數據包經過了網卡引起中斷之後,每一個包都會在記憶體中分配一塊區域,稱為
sk_buff
(套接字緩存,socket buffer )
sk_buff
是 Linux 網路的一個核心數據結構
- 觸發軟中斷
網卡的硬中斷處理函數處理完之後驅動先 disable 硬中斷,然後 enable 軟中斷
ps:待 ring buffer 中的所有數據包被處理完成後,enable 網卡的硬中斷,這樣下次網卡再收到數據的時候就會通知 CPU
內核負責軟中斷進程 ksoftirqd
發現有軟中斷請求到來,進行下麵的一些操作
# 查看軟中斷進程
[root@localhost ~]# ps -ef | grep ksoftirqd
調用 net_rx_action
函數
它會通過 poll
函數去 rx_ring
中拿數據幀,獲取的時候順便把 rx_ring
上的數據給刪除
static void net_rx_action(struct softirq_action *h)
{
struct softnet_data *sd = &__get_cpu_var(softnet_data);
unsigned long time_limit = jiffies + 2;
int budget = netdev_budget;
void *have;
local_irq_disable();
while (!list_empty(&sd->poll_list)) {
......
n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);
work = 0;
if (test_bit(NAPI_STATE_SCHED, &n->state)) {
work = n->poll(n, weight);
trace_napi_poll(n);
}
budget -= work;
}
}
除此之外,poll
函數會把 ring buffer
中的數據包轉換成內核網路模塊能夠識別的 skb 格式(即 socket kernel buffer
)
socket kernel buffer (skb) 是 Linux 內核網路棧處理網路包(packets)所使用的 buffer,它的類型是 sk_buffer
3、最後進入 netif _receive_skb
處理流程,它是數據鏈路層接收數據幀的最後一關
根據註冊在全局數組 ptype_all
和 ptype_base
里的網路層數據幀類型去調用第三層協議的接收函數處理
例如對於 ip 包來講,就會進入到
ip_rcv
;如果是 arp 包的話,會進入到arp_rcv
- 到達網路層(以 IP 協議為例)
IP 層的入口函數在 ip_rcv
函數,調用 ip_rcv
函數進入三層協議棧
首先會對數據包進行各種檢查(檢查 IP Header),然後調用 netfilter
中的鉤子函數: NF_INET_PRE_ROUTING
netfilter: 是 Linux 內核中進行數據包過濾,連接跟蹤(Connect Track),網路地址轉換(NAT)等功能的主要實現框架
該框架在網路協議棧處理數據包的關鍵流程中定義了一系列鉤子點(Hook 點),併在這些鉤子點中註冊一系列函數對數據包進行處理
這些註冊在鉤子點的函數即為設置在網路協議棧內的數據包通行策略,也就意味著,這些函數可以決定內核是接受還是丟棄某個數據包
NF_INET_PRE_ROUTING
會根據預設的規則對數據包進行判斷並根據判斷結果做相關的處理(修改或者丟棄數據包)
處理完成後,數據包交由 ip_rcv_finish
處理,該函數根據路由判決結果,決定數據包是交由本機上層應用處理,還是需要進行轉發
如果是交由本機處理,則會交由 ip_local_deliver
本地上交流程;如果需要轉發,則交由 ip_forward
函數走轉發流程
- 到達傳輸層(以 TCP 為例)
傳輸層 TCP 處理入口在 tcp_v4_rcv
函數,首先檢查數據包的 TCP 頭部等信息,確保數據包的完整性和正確性
然後去查找該數據包對應的已經打開的 socket ,如果找不到匹配的 socket,表示該數據包不屬於任何一個已建立的連接,因此該數據包會被丟棄
如果找到了匹配的 socket,TCP 會進一步檢查該 socket 和連接的狀態,如果狀態正常,TCP 會將數據包從內核傳輸到用戶空間,放入 socket 的接收緩衝區(socket receive buffer)
- 應用層獲取數據
當數據包到達操作系統內核的傳輸層時,應用程式可以從套接字的接收緩衝區(socket receive buffer)中讀取數據包
一般有兩種方式讀取數據,一種是 recvfrom
函數阻塞在那裡等著數據來,這種情況下當 socket 收到通知後,recvfrom
就會被喚醒,然後讀取接收隊列的數據
另一種是通過 epoll 或者 select 監聽相應的 socket,當收到通知後,再調用 recvfrom
函數去讀取接收隊列的數據
總結
網路模塊可以說是 Linux 內核中最複雜的模塊了
看起來一個簡簡單單的收包過程就涉及到許多內核組件之間的交互,如網卡驅動、協議棧,內核ksoftirqd 線程等
鹹魚原本打算把收包和發包的流程都寫上的,但是光是寫收包流程就就要了我半條命了,等下次有機會把發包的流程也寫一下
總結一下 Linux 網路收包流程:
- 數據到達網卡之後,網卡通過 DMA 將數據放到記憶體分配好的一塊
ring buffer
中,然後觸發硬中斷 - CPU 收到硬中斷之後簡單的處理了一下(分配
skb_buffer
),然後觸發軟中斷 - 軟中斷進程
ksoftirqd
執行一系列操作(例如把數據幀從ring ruffer
上取下來)然後將數據送到三層協議棧中 - 在三層協議棧中數據被進一步處理髮送到四層協議棧
- 在四層協議棧中,數據會從內核拷貝到用戶空間,供應用程式讀取
- 最後被處在應用層的應用程式去讀取