如何學習 Linux 內核網絡協議棧
準備工作
對於沒有學習過 Linux 內核網絡的人來說,可能會對這個它產生嚮往,也有可能產生恐懼。但當你深入理解並且驗證後得到正反饋時,那種豁然開朗的感覺,會讓你感到滿足,信可樂也。
回想當初自己進入這個主題時,我產生過以下疑問:
Q1:內核網絡子系統這麼大,我應該從何處開始?會不會栽到裏面就暈了?Q2:內核網絡代碼更新地那麼快,應該從哪個版本開始學習?
Q3:有沒有什麼好的資料,教程?
Q4:如何驗證自己的理解是否正確?
那麼現在,我可以簡單主觀回答下:
Q1:內核網絡子系統這麼大,我應該從何處開始?會不會栽到裏面就暈了?
內核網絡子系統的代碼雖然看着多,但核心流程和旁支還是分離的。並且,我認爲它的水平是超過一大票開源代碼的。註釋的地方夠用,炫技的地方寥寥,每一處修改都能從社區 GIT 倉庫找到修改的原因,這已經很好了。
Q2:內核網絡代碼更新地那麼快,應該從哪個版本開始學習?
需要哪個版本就用哪個版本。如果工作中指定了版本, 那麼就選擇它。如果教程書籍中是基於某個版本,那麼就選擇它。如果只是自己研究,那麼我建議預先幾個版本的代碼:比如 2.6、3.7、4.4、4.9、5.3 (這些版本號是我拍腦袋寫的)。
Q3:有沒有什麼好的資料,教程?
我是沒有見到過完整的教程,我覺得可能是內容太多太雜的原因。不過,幾乎所有方面互聯網上都有相關內容的討論和分析。
Q4:如何驗證自己的理解是否正確?
驗證是十分重要的,否則你怎麼知道是不是對的呢。除了使用 printk 加調試信息重新編譯內核這種原始手段外,有一些更聰明的工具可以幫上忙。
-
Systemtap:幾乎無所不能,可以在內核放置探測點,然後執行自己的代碼。
-
kprobe:簡單的工具,可以快速檢驗某個函數是否被執行到
-
packetdrill:用於驗證 TCP 協議的行爲很有用
【文章福利】小編自己整理了一些個人覺得比較好的學習書籍、視頻資料有需要的可以私信回覆【內核】自行免費領取哦!!【騰訊文檔】Linux 內核源碼技術學習路線 + 視頻教程代碼資料
docs.qq.com/doc/DWmNMckNQc21ZbENE
協議棧的細節
下面將介紹一些內核網絡協議棧中常常涉及到的概念.
sk_buff
內核顯然需要一個數據結構來表示報文,這個結構就是 sk_buff (socket buffer 的簡稱),它等同於在 < TCP/IP 詳解 卷 2 > 中描述的 BSD 內核中的 mbuf。
sk_buff 結構自身並不存儲報文內容,它通過多個指針指向真正的報文內存空間:
sk_buff 是一個貫穿整個協議棧層次的結構,在各層間傳遞時,內核只需要調整 sk_buff 中的指針位置就行。
net_device
內核使用 net_device 表示網卡。網卡可以分爲物理網卡和虛擬網卡。物理網卡是指真正能把報文發出本機的網卡,包括真實物理機的網卡以及 VM 虛擬機的網卡,而像 tun/tap,vxlan、veth pair 這樣的則屬於虛擬網卡的範疇。
如下圖所示,每個網卡都有兩端,一端是協議棧 (IP、TCP、UDP),另一端則有所區別,對物理網卡來說,這一端是網卡生產廠商提供的設備驅動程序,而對虛擬網卡來說差別就大了,正是由於虛擬網卡的存在,內核才能支持各種隧道封裝、容器通信等功能。
socket & sock
用戶空間通過 socket()、bind()、listen()、accept() 等庫函數進行網絡編程。而這裏提到的 socket 和 sock 是內核中的兩個數據結構,其中 socket 向上面向用戶,而 sock 向下面向協議棧。
如下圖所示,這兩個結構實際上是一一對應的.
注意到,這兩個結構上都有一個叫 ops 的指針, 但它們的類型不同。socket 的 ops 是一個指向 struct proto_ops 的指針,sock 的 ops 是一個指向 struct proto 的指針, 它們在結構被創建時確定
回憶網絡編程中 socket() 函數的原型
#include <sys/socket.h>
sockfd = socket(int socket_family, int socket_type, int protocol);
實際上, socket->ops 和 sock->ops 由前兩個參數 socket_family 和 socket_type 共同確定。
如果 socket_family 是最常用的 PF_INET 協議簇, 則 socket->ops 和 sock->ops 的取值就記錄在 INET 協議開關表中
static struct inet_protosw inetsw_array[] =
{
{
.type = SOCK_STREAM,
.protocol = IPPROTO_TCP,
.prot = &tcp_prot, // 對應 sock->ops
.ops = &inet_stream_ops, // 對應 socket->ops
.flags = INET_PROTOSW_PERMANENT |
INET_PROTOSW_ICSK,
},
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_UDP,
.prot = &udp_prot, // 對應 sock->ops
.ops = &inet_dgram_ops, // 對應 socket->ops
.flags = INET_PROTOSW_PERMANENT,
},
}
.......
L3->L4
我們知道網絡協議棧是分層的,但實際上,具體到實現,內核協議棧的分層只是邏輯上的,本質還是函數調用。發送流程 (上層調用下層) 通常是直接調用(因爲沒有不確定性,比如 TCP 知道下面一定 IP),但接收過程不一樣了,比如報文在 IP 層時,它上面可能是 TCP,也可能是 UDP,或者是 ICMP 等等,所以接收過程使用的是註冊 - 回調機制。
還是以 INET 協議簇爲例,註冊接口是
int inet_add_protocol(const struct net_protocol *prot, unsigned char protocol);
在內核網絡子系統初始化時,L4 層協議 (如下面的 TCP 和 UDP) 會被註冊
static struct net_protocol tcp_protocol = {
......
.handler = tcp_v4_rcv,
......
};
static struct net_protocol udp_protocol = {
.....
.handler = udp_rcv,
.....
};
而在 IP 層,查詢過路由後,如果該報文是需要上送本機的,則會根據報文的 L4 協議,送給不同的 L4 處理
static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
......
ipprot = rcu_dereference(inet_protos[protocol]);
......
ret = ipprot->handler(skb);
......
}
L2->L3
L2->L3 如出一轍。只不過註冊接口變成了
void dev_add_pack(struct packet_type *pt)
誰會註冊呢?顯然至少 IP 會
static struct packet_type ip_packet_type = {
.type = cpu_to_be16(ETH_P_IP),
.func = ip_rcv,
}
而在報文接收過程中,設備驅動程序會將報文的 L3 類型設置到 skb->protocol,然後在內核 netif_receive_skb 收包時,會根據這個 protocol 調用不同的回調函數
__netif_receive_skb(struct sk_buff *skb)
{
......
type = skb->protocol;
......
ret = pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
}
NetfilterLinux 內核源碼技術學習路線 + 視頻教程代碼資料 Netfilter
Netfilter 是報文在內核協議棧必然會通過的路徑,我們從下面這張圖就可以看到,Netfilter 在內核的 5 個地方設置了 HOOK 點,用戶可以通過配置 iptables 規則,在 HOOK 點對報文進行過濾、修改等操作。
在內核代碼中,我們時常可一件 NF_HOOK 這樣的調用。我的建議是,如果你暫時不考慮 Netfilter,那麼就直接跳過, 跟蹤 okfn 就行。
static inline int
NF_HOOK(uint8_t pf, unsigned int hook, struct net *net, struct sock *sk, struct sk_buff *skb,
struct net_device *in, struct net_device *out,
int (*okfn)(struct net *, struct sock *, struct sk_buff *))
{
int ret = nf_hook(pf, hook, net, sk, skb, in, out, okfn);
if (ret == 1)
ret = okfn(net, sk, skb);
return ret;
}
dst_entry
內核需要確定收到的報文是應該本地上送 (local deliver) 還是轉發 (forward), 對本機發送(local out) 的報文需要確定是從哪個網卡發送出去,這都是內核通過查詢 fib (forward information base, 轉發信息表) 確定。fib 可以理解爲一個數據庫,數據來源是用戶配置或者內核自動生成的路由。
fib 查詢的輸入是報文 sk_buff,輸出是 dst_entry. dst_entry 會被設置到 skb 上
static inline void skb_dst_set(struct sk_buff *skb, struct dst_entry *dst)
{
skb->_skb_refdst = (unsigned long)dst;
}
而 dst_entry 中最重要的是一個 input 指針和 output 指針
struct dst_entry {
......
int (*input)(struct sk_buff *);
int (*output)(struct net *net, struct sock *sk, struct sk_buff *skb);
......
}
- 對於需要本機上送的報文
rth->dst.input = ip_local_deliver;
- 對需要轉發的報文
rth->dst.input = ip_forward;
- 對本機發送的報文
rth->dst.output = ip_output;
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/4haBiI6OEpILsX6eWk9RNg