一、Socket網絡協議基本原理
1. 假設這裏就涉及三臺機器。Linux服務器A和B處於不同的網段,通過中間的Linux服務器作爲路由器進行轉發,如下圖所示:
說到網絡協議,還需要簡要介紹一下兩種網絡協議模型,一種是OSI標準七層模型,一種是業界標準的TCP/IP模型,它們的對應關係如下圖所示:
爲什麼網絡要分層呢?因爲網絡環境過於複雜,不是一個能夠集中控制的體系。全球數以億記的服務器和設備各有各的體系,但是都可以通過同一套網絡協議棧通過切分成多個層次和組合,來滿足不同服務器和設備的通信需求。這裏簡單介紹一下網絡協議的幾個層次。第三層網絡層也叫IP層,通常看到的IP地址都是這個樣子的:192.168.1.100/24,斜槓前面是IP地址,這個地址被點分隔爲四個部分,每個部分8位,總共是32位,斜線後面24的意思是32位中,前24位是網絡號,後8位是主機號(C類地址)。
IP地址類似互聯網上的郵寄地址,是有全局定位功能的。從第三層往下看,第二層是數據鏈路層。有時候簡稱MAC 層,所謂MAC就是每個網卡都有的唯一的硬件地址(不絕對唯一,相對大概率唯一即可,類比UUID)。這雖然也是一個地址,但是MAC地址是沒有全局定位功能的。MAC 地址的定位功能侷限在同一個網絡裏面,即同一個網絡號下的IP地址之間,可以通過MAC進行定位和通信。從IP地址獲取MAC地址要通過ARP協議,是通過在本地發送廣播包獲得的MAC地址。
2. 由於同一個網絡內的機器數量有限,通過MAC地址的好處就是簡單,匹配上MAC地址就接收,匹配不上就不接收,沒有什麼所謂路由協議這樣複雜的協議。當然壞處就是MAC地址的作用範圍不能出本地網絡,所以一旦跨網絡通信,雖然IP地址保持不變,但是MAC地址每經過一個路由器就要換一次。看上面的圖中,服務器A發送網絡包給服務器B,原IP地址始終是192.168.1.100,目標IP地址始終是192.168.2.100,但是在網絡1裏面源MAC地址是MAC1,目標MAC地址是路由器的MAC2,路由器轉發之後,源MAC地址是路由器的MAC3,目標MAC地址是MAC4。
所以第二層乾的事情,就是網絡包在本地網絡中的服務器之間定位及通信的機制。第一層物理層就是物理設備,例如連着電腦的網線,能連上的WiFi,這一層不打算進行分析。從第三層往上看,第四層是傳輸層裏面有兩個著名的協議TCP和UDP。在IP層的代碼邏輯中,僅僅負責數據從一個IP地址發送給另一個IP地址,丟包、亂序、重傳、擁塞,這些IP層都不管。處理這些問題的代碼邏輯寫在了傳輸層的TCP協議裏面。常稱TCP是可靠傳輸協議,因爲從第一層到第三層都不可靠,網絡包說丟就丟,是TCP這一層通過各種編號、重傳等機制,讓本來不可靠的網絡對於更上層來講,變得“看起來”可靠。
傳輸層再往上就是應用層,例如在瀏覽器裏面輸入的HTTP,Java服務端寫的Servlet都是這一層的。二層到四層都是在Linux內核裏面處理的,應用層例如瀏覽器、Nginx、Tomcat都是用戶態的。內核裏面對於網絡包的處理是不區分應用的,從四層再往上就需要區分網絡包發給哪個應用。
在傳輸層的TCP和UDP協議裏面都有端口的概念,不同的應用監聽不同的端口,例如服務端Nginx監聽80、Tomcat監聽8080;再如客戶端瀏覽器監聽一個隨機端口,FTP客戶端監聽另外一個隨機端口。應用層和內核互通的機制就是通過Socket系統調用,所以面試會問Socket屬於哪一層,其實它哪一層都不屬於,它屬於操作系統的概念,而非網絡協議分層的概念。只不過操作系統選擇對於網絡協議的實現模式是,二到四層的處理代碼在內核裏面,七層的處理代碼讓應用自己去做,兩者需要跨內核態和用戶態通信,就需要一個系統調用完成這個銜接,這就是Socket。
3. 網絡分完層之後,對於數據包的發送就是層層封裝的過程。就像下面的圖中所示:
在服務器B上部署的服務端Nginx和Tomcat,都是通過Socket監聽80和8080端口。這個時候內核的數據結構就知道了。如果遇到發送到這兩個端口的,就發送給這兩個進程。在服務器A上的客戶端打開一個瀏覽器連接Ngnix。也是通過Socket,客戶端會被分配一個隨機端口12345。同理打開另一個瀏覽器連接Tomcat,同樣通過Socket分配隨機端口12346。
客戶端瀏覽器將請求封裝爲HTTP協議,通過Socket發送到內核。在內核的網絡協議棧裏面, TCP層創建用於維護連接、序列號、重傳、擁塞控制的數據結構,將HTTP包加上TCP頭,發送給IP層,IP層加上IP頭髮送給MAC層,MAC層加上MAC頭從硬件網卡發出去。
網絡包會先到達網絡1的交換機。常稱交換機爲二層設備,這是因爲交換機只會處理到第二層,然後它會將網絡包的MAC頭拿下來,發現目標MAC是在自己右面的網口,於是就從這個網口發出去。網絡包會到達中間的路由器,它左面的網卡會收到網絡包,發現MAC地址匹配,就交給IP層,在IP層根據IP頭中的信息在路由表中查找下一跳在哪裏,應該從哪個網口發出去。在這個例子中最終會從右面的網口發出去。路由器被稱爲三層設備,因爲它只會處理到第三層。從路由器右面的網口發出的包會到網絡2的交換機,還是會經歷一次二層的處理,轉發到交換機右面的網口。
最終網絡包會被轉發到服務器B,它發現MAC地址匹配就將MAC頭取下來,交給上一層。IP層發現IP地址匹配,將IP頭取下來交給上一層。TCP層會根據TCP頭中的序列號等信息,發現它是一個正確的網絡包,就會將網絡包緩存起來,等待應用層的讀取。應用層通過Socket監聽某個端口,因而讀取的時候內核會根據TCP頭中的端口號,將網絡包發給相應的應用。HTTP層的頭和正文,是應用層來解析的,通過解析應用層知道了客戶端的請求,例如購買一個商品還是請求一個網頁。當應用層處理完HTTP的請求,會將結果仍然封裝爲HTTP的網絡包,通過Socket接口發送給內核。
內核會經過層層封裝,從物理網口發送出去,經過網絡2的交換機和路由器到達網絡1,經過網絡1的交換機到達服務器A。在服務器A上經過層層解封裝,通過socket接口根據客戶端的隨機端口號,發送給客戶端的應用程序即瀏覽器,於是瀏覽器就能夠顯示出一個頁面了。
二、Socket通信
4. socket接口大多數情況下操作的是傳輸層,更底層的協議不用它來操心,這就是分層的好處。在傳輸層有兩個主流的協議TCP和UDP,所以socket程序設計也是主要操作這兩個協議。這兩個協議的區別通常答案是下面這樣的:
(1)TCP是面向連接的,UDP是面向無連接的。
(2)TCP 提供可靠交付,無差錯、不丟失、不重複、並且按序到達;UDP不提供可靠交付,不保證不丟失,不保證按順序到達。
(3)TCP是面向字節流的,發送時發的是一個流,沒頭沒尾;UDP是面向數據報的,一個一個地發送。
(4)TCP是可以提供流量控制和擁塞控制的,既防止對端被壓垮,也防止網絡被壓垮。
但從本質上來講,所謂的建立連接,其實是爲了在客戶端和服務端維護連接,而建立一定的數據結構來維護雙方交互的狀態,並用這樣的數據結構來保證面向連接的特性。TCP無法左右中間的任何通路,也沒有什麼虛擬的連接,中間的通路根本意識不到兩端使用了TCP還是UDP,所謂的連接就是兩端數據結構狀態的協同,兩邊的狀態能夠對得上。符合TCP協議的規則,就認爲連接存在;兩面狀態對不上,連接就算斷了。
流量控制和擁塞控制其實就是根據收到的對端的網絡包,調整兩端數據結構的狀態。TCP協議的設計理論上認爲,這樣調整了數據結構的狀態,就能進行流量控制和擁塞控制了,其實在中間通路上是不是真的做到了,誰也管不着。所謂的可靠,也是兩端的數據結構做的事情。不丟失其實是數據結構在“點名”,順序到達其實是數據結構在“排序”,面向數據流其實是數據結構將零散的包,按照順序捏成一個流發給應用層。總而言之,“連接”兩個字讓人誤以爲功夫在通路,其實功夫在兩端。
當然,無論是用socket操作TCP,還是UDP,首先都要調用socket函數,如下所示:
int socket(int domain, int type, int protocol);
socket函數用於創建一個socket的文件描述符,唯一標識一個socket,這裏叫作文件描述符是因爲在內核中,會創建類似文件系統的數據結構,並且後續的操作都有用到它。socket函數有三個參數:
(1)domain表示使用什麼IP層協議,AF_INET表示IPv4,AF_INET6表示IPv6;
(2)type表示socket類型,SOCK_STREAM就是TCP面向流的,SOCK_DGRAM就是UDP面向數據報的,SOCK_RAW可以直接操作IP層,或者非TCP和UDP的協議,例如 ICMP;
(3)protocol表示協議,包括IPPROTO_TCP、IPPTOTO_UDP。
通信結束後,還要像關閉文件一樣關閉socket。
5. 接下來看針對TCP應該如何編程,如下所示:
TCP的服務端要先監聽一個端口,一般是先調用bind函數,給這個socket賦予一個端口和IP地址,如下所示:
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family */
__be16 sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
/* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
struct in_addr {
__be32 s_addr;
};
其中sockfd是上面創建的socket文件描述符,在sockaddr_in結構中sin_family設置爲AF_INET表示IPv4;sin_port是端口號;sin_addr是IP地址。服務端所在的服務器可能有多個網卡、多個地址,可以選擇監聽在一個地址,也可以監聽0.0.0.0表示所有的地址都監聽。服務端一般要監聽在一個衆所周知的端口上,例如Nginx一般是 80,Tomcat一般是8080。客戶端要訪問服務端肯定事先要知道服務端的端口。仔細觀察會會發現客戶端不需要bind,因爲瀏覽器隨機分配一個端口就可以了,只有用戶主動去連接別人,別人不會主動連接自己,沒有人關心客戶端監聽到了哪個端口。
上面代碼中的數據結構,裏面的變量名稱都有“be”兩個字母,代表的意思是“big-endian”。如果在網絡上傳輸超過1 Byte的類型,就要區分大端(Big Endian)和小端(Little Endian)。假設要在32位4 Bytes的一個空間存放整數1,很顯然只要1 Byte放1其他3 Bytes放0就可以了。問題是1作爲最低位,應該放在32位的最後一個位置,還是放在第一個位置?最低位放在最後一個位置叫作小端,最低位放在第一個位置叫作大端。TCP/IP棧是按照大端來設計的,而x86機器多按照小端來設計,因而發出去時需要做一個轉換,這就是__be的作用。
6. 接下來就要建立TCP的連接了,也就是著名的三次握手,其實就是將客戶端和服務端的狀態通過三次網絡交互,達到初始狀態是協同的狀態。下圖就是三次握手的序列圖以及對應的狀態轉換:
接下來服務端要調用listen()進入LISTEN狀態,等待客戶端進行連接,如下所示:
int listen(int sockfd, int backlog);
連接的建立過程即三次握手,是TCP層的動作,是在內核完成的應用層不需要參與。接着服務端只需要調用accept,等待內核完成了至少一個連接的建立才返回。如果沒有一個連接完成了三次握手,accept就一直等待;如果有多個客戶端發起連接,並且在內核裏面完成了多個三次握手,建立了多個連接,這些連接會被放在一個隊列裏面,accept會從隊列裏面取出一個來進行處理。如果想進一步處理其他連接,需要調用多次accept,所以accept往往在一個循環裏面,如下所示:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
接下來,客戶端可以通過connect函數發起連接,如下所示:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
先在參數中指明要連接的IP地址和端口號,然後發起三次握手。內核會給客戶端分配一個臨時的端口,一旦握手成功,服務端的accept就會返回另一個socket。這裏需要注意的是,監聽的socket和真正用來傳送數據的socket是兩個socket,一個叫作監聽socket,一個叫作已連接socket。成功連接建立之後,雙方開始通過read和write函數來讀寫數據,就像往一個文件流裏面寫東西一樣。
7. 接下來看針對UDP應該如何編程,如下圖所示:
UDP是沒有連接的,所以不需要三次握手,也就不需要調用listen和connect,但是UDP的交互仍然需要IP地址和端口號,因而也需要bind。對於UDP來講沒有所謂的連接維護,也沒有所謂的連接的發起方和接收方,甚至都不存在客戶端和服務端的概念,大家就都是客戶端,也同時都是服務端。只要有一個socket,多臺機器就可以任意通信,不存在哪兩臺機器是屬於一個連接的概念,因此每一個UDP的socket都需要 bind。每次通信時調用sendto和recvfrom,都要傳入IP地址和端口,如下所示:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
在操作系統範圍內,需要重點關注TCP協議的系統調用。socket系統調用是用戶態和內核態的接口,網絡協議的四層以下都是在內核中的。關於TCP協議的socket調用的過程,按照這個順序來總結一下這些系統調用到內核都做了什麼:
(1)服務端和客戶端都調用socket,得到文件描述符;
(2)服務端調用listen進行監聽;
(3)服務端調用accept等待客戶端連接;
(4)客戶端調用connect連接服務端;
(5)服務端accept返回用於傳輸的socket的文件描述符;
(6)客戶端調用write寫入數據;
(7)服務端調用read讀取數據。
三、Socket內核數據結構
8. 上面講了Socket在TCP和UDP場景下的調用流程。這裏沿着這個流程到內核裏面,看看都創建了哪些數據結構,做了哪些事情,先從Socket系統調用開始,如下所示:
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
int retval;
struct socket *sock;
int flags;
......
if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
retval = sock_create(family, type, protocol, &sock);
......
retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
......
return retval;
}
Socket系統調用會調用sock_create創建一個struct socket結構,然後通過sock_map_fd和文件描述符對應起來。在創建Socket的時候有三個參數,一個是family表示地址族,不是所有的Socket都要通過IP進行通信,還有其他的通信方式,例如下面的定義中,domain sockets就是通過本地文件進行通信的,不需要IP地址,只不過通過IP地址是最常用的模式,所以這裏着重分析這種模式:
#define AF_UNIX 1/* Unix domain sockets */
#define AF_INET 2/* Internet IP Protocol */
第二個參數是type即Socket的類型,類型比較少。第三個參數是protocol是協議,協議數目是比較多的,也就是說多個協議會屬於同一種類型。常用的Socket類型有三種,分別是SOCK_STREAM、SOCK_DGRAM和SOCK_RAW,如下所示:
enum sock_type {
SOCK_STREAM = 1,
SOCK_DGRAM = 2,
SOCK_RAW = 3,
......
}
SOCK_STREAM是面向數據流的,協議IPPROTO_TCP屬於這種類型。SOCK_DGRAM是面向數據報的,協議IPPROTO_UDP屬於這種類型。如果在內核裏面看的話,IPPROTO_ICMP也屬於這種類型。SOCK_RAW是原始的IP包,IPPROTO_IP屬於這種類型。這裏重點看SOCK_STREAM類型和IPPROTO_TCP協議。爲了管理family、type、protocol這三個分類層次,內核會創建對應的數據結構。接下來打開sock_create函數看一下,它會調用__sock_create:
int __sock_create(struct net *net, int family, int type, int protocol,
struct socket **res, int kern)
{
int err;
struct socket *sock;
const struct net_proto_family *pf;
......
sock = sock_alloc();
......
sock->type = type;
......
pf = rcu_dereference(net_families[family]);
......
err = pf->create(net, sock, protocol, kern);
......
*res = sock;
return 0;
}
這裏先是分配了一個struct socket結構,接下來要用到family參數,這裏有一個net_families數組,可以以family參數爲下標,找到對應的struct net_proto_family,如下所示:
/* Supported address families. */
#define AF_UNSPEC 0
#define AF_UNIX 1 /* Unix domain sockets */
#define AF_LOCAL 1 /* POSIX name for AF_UNIX */
#define AF_INET 2 /* Internet IP Protocol */
......
#define AF_INET6 10 /* IP version 6 */
......
#define AF_MPLS 28 /* MPLS */
......
#define AF_MAX 44 /* For now.. */
#define NPROTO AF_MAX
struct net_proto_family __rcu *net_families[NPROTO] __read_mostly;
這裏可以找到net_families的定義,每一個地址族在這個數組裏面都有一項,裏面的內容是net_proto_family。每一種地址族都有自己的net_proto_family,IP地址族的net_proto_family定義如下,裏面最重要的就是create函數指向 inet_create:
//net/ipv4/af_inet.c
static const struct net_proto_family inet_family_ops = {
.family = PF_INET,
.create = inet_create,//這個用於socket系統調用創建
......
}
回到函數__sock_create,接下來在這裏面,這個inet_create會被調用,如下所示:
static int inet_create(struct net *net, struct socket *sock, int protocol, int kern)
{
struct sock *sk;
struct inet_protosw *answer;
struct inet_sock *inet;
struct proto *answer_prot;
unsigned char answer_flags;
int try_loading_module = 0;
int err;
/* Look for the requested type/protocol pair. */
lookup_protocol:
list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
err = 0;
/* Check the non-wild match. */
if (protocol == answer->protocol) {
if (protocol != IPPROTO_IP)
break;
} else {
/* Check for the two wild cases. */
if (IPPROTO_IP == protocol) {
protocol = answer->protocol;
break;
}
if (IPPROTO_IP == answer->protocol)
break;
}
err = -EPROTONOSUPPORT;
}
......
sock->ops = answer->ops;
answer_prot = answer->prot;
answer_flags = answer->flags;
......
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
......
inet = inet_sk(sk);
inet->nodefrag = 0;
if (SOCK_RAW == sock->type) {
inet->inet_num = protocol;
if (IPPROTO_RAW == protocol)
inet->hdrincl = 1;
}
inet->inet_id = 0;
sock_init_data(sock, sk);
sk->sk_destruct = inet_sock_destruct;
sk->sk_protocol = protocol;
sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv;
inet->uc_ttl = -1;
inet->mc_loop = 1;
inet->mc_ttl = 1;
inet->mc_all = 1;
inet->mc_index = 0;
inet->mc_list = NULL;
inet->rcv_tos = 0;
if (inet->inet_num) {
inet->inet_sport = htons(inet->inet_num);
/* Add to protocol hash chains. */
err = sk->sk_prot->hash(sk);
}
if (sk->sk_prot->init) {
err = sk->sk_prot->init(sk);
}
......
}
在inet_create中,先會看到一個循環list_for_each_entry_rcu,在這裏第二個參數type開始起作用,因爲循環查看的是inetsw[sock->type],這裏的inetsw也是一個數組,type作爲下標,裏面的內容是struct inet_protosw是協議,即inetsw數組對於每個類型有一項,這一項裏面是屬於這個類型的協議,如下所示:
static struct list_head inetsw[SOCK_MAX];
static int __init inet_init(void)
{
......
/* Register the socket-side information for inet_create. */
for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)
INIT_LIST_HEAD(r);
for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
inet_register_protosw(q);
......
}
inetsw數組是在系統初始化的時候初始化的,就像上面代碼裏面實現的一樣。首先,一個循環會將inetsw數組的每一項都初始化爲一個鏈表。前面說了一個type類型會包含多個protocol,因而需要一個鏈表。接下來一個循環,是將inetsw_array註冊到inetsw數組裏面去,inetsw_array的定義如下,這個數組裏面的內容很重要,後面會用到它們:
static struct inet_protosw inetsw_array[] =
{
{
.type = SOCK_STREAM,
.protocol = IPPROTO_TCP,
.prot = &tcp_prot,
.ops = &inet_stream_ops,
.flags = INET_PROTOSW_PERMANENT |
INET_PROTOSW_ICSK,
},
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_UDP,
.prot = &udp_prot,
.ops = &inet_dgram_ops,
.flags = INET_PROTOSW_PERMANENT,
},
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_ICMP,
.prot = &ping_prot,
.ops = &inet_sockraw_ops,
.flags = INET_PROTOSW_REUSE,
},
{
.type = SOCK_RAW,
.protocol = IPPROTO_IP, /* wild card */
.prot = &raw_prot,
.ops = &inet_sockraw_ops,
.flags = INET_PROTOSW_REUSE,
}
}
回到inet_create的list_for_each_entry_rcu循環中,到這裏就好理解了,這是在inetsw數組中,根據type找到屬於這個類型的列表,然後依次比較列表中的struct inet_protosw的protocol是不是用戶指定的protocol;如果是就得到了符合用戶指定的family->type->protocol中三項的struct inet_protosw *answer對象。
接下來struct socket *sock的ops成員變量,被賦值爲answer的ops,對於TCP來講就是inet_stream_ops。後面任何用戶對於這個socket的操作,都是通過 inet_stream_ops 進行的。接下來,我們創建一個 struct sock *sk 對象。這裏比較讓人困惑。socket 和 sock 看起來幾乎一樣,容易讓人混淆,這裏需要說明一下,socket 是用於負責對上給用戶提供接口,並且和文件系統關聯。而 sock,負責向下對接內核網絡協議棧。
在sk_alloc函數中,struct inet_protosw *answer結構的tcp_prot賦值給了struct sock *sk的sk_prot成員。tcp_prot的定義如下,裏面定義了很多的函數,都是sock之下內核協議棧的動作:
struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
.close = tcp_close,
.connect = tcp_v4_connect,
.disconnect = tcp_disconnect,
.accept = inet_csk_accept,
.ioctl = tcp_ioctl,
.init = tcp_v4_init_sock,
.destroy = tcp_v4_destroy_sock,
.shutdown = tcp_shutdown,
.setsockopt = tcp_setsockopt,
.getsockopt = tcp_getsockopt,
.keepalive = tcp_set_keepalive,
.recvmsg = tcp_recvmsg,
.sendmsg = tcp_sendmsg,
.sendpage = tcp_sendpage,
.backlog_rcv = tcp_v4_do_rcv,
.release_cb = tcp_release_cb,
.hash = inet_hash,
.get_port = inet_csk_get_port,
......
}
在inet_create函數中,接下來創建一個struct inet_sock結構,這個結構一開始就是struct sock,然後擴展了一些其他的信息,剩下的代碼就填充這些信息,這一幕會經常看到,將一個結構放在另一個結構的開始位置,然後擴展一些成員,通過對於指針的強制類型轉換,來訪問這些成員。socket的創建至此結束。
9. 接下來看bind函數,如下所示:
SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
struct socket *sock;
struct sockaddr_storage address;
int err, fput_needed;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
err = move_addr_to_kernel(umyaddr, addrlen, &address);
if (err >= 0) {
err = sock->ops->bind(sock,
(struct sockaddr *)
&address, addrlen);
}
fput_light(sock->file, fput_needed);
}
return err;
}
在bind中,sockfd_lookup_light會根據fd文件描述符找到struct socket結構。然後將sockaddr從用戶態拷貝到內核態,然後調用struct socket結構裏面ops的bind函數。根據前面創建socket時候的設定,調用的是inet_stream_ops 的bind函數,也即調用inet_bind,如下所示:
int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;
struct sock *sk = sock->sk;
struct inet_sock *inet = inet_sk(sk);
struct net *net = sock_net(sk);
unsigned short snum;
......
snum = ntohs(addr->sin_port);
......
inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;
/* Make sure we are allowed to bind here. */
if ((snum || !inet->bind_address_no_port) &&
sk->sk_prot->get_port(sk, snum)) {
......
}
inet->inet_sport = htons(inet->inet_num);
inet->inet_daddr = 0;
inet->inet_dport = 0;
sk_dst_reset(sk);
}
bind裏面會調用sk_prot的get_port函數,即inet_csk_get_port來檢查端口是否衝突,是否可以綁定。如果允許則會設置struct inet_sock的本方地址inet_saddr和本方端口inet_sport,對方地址inet_daddr和對方端口inet_dport都初始化爲0。bind的邏輯相對比較簡單,就到這裏了。
10. 接下來看listen,如下所示:
SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
struct socket *sock;
int err, fput_needed;
int somaxconn;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
if ((unsigned int)backlog > somaxconn)
backlog = somaxconn;
err = sock->ops->listen(sock, backlog);
fput_light(sock->file, fput_needed);
}
return err;
}
在listen中還是通過sockfd_lookup_light根據fd文件描述符,找到struct socket結構。接着調用struct socket結構裏ops的listen函數。根據前面創建socket時的設定,調用的是inet_stream_ops的listen函數,即調用inet_listen,如下所示:
int inet_listen(struct socket *sock, int backlog)
{
struct sock *sk = sock->sk;
unsigned char old_state;
int err;
old_state = sk->sk_state;
/* Really, if the socket is already in listen state
* we can only allow the backlog to be adjusted.
*/
if (old_state != TCP_LISTEN) {
err = inet_csk_listen_start(sk, backlog);
}
sk->sk_max_ack_backlog = backlog;
}
如果這個socket還不在TCP_LISTEN狀態,會調用inet_csk_listen_start進入監聽狀態,如下所示:
int inet_csk_listen_start(struct sock *sk, int backlog)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct inet_sock *inet = inet_sk(sk);
int err = -EADDRINUSE;
reqsk_queue_alloc(&icsk->icsk_accept_queue);
sk->sk_max_ack_backlog = backlog;
sk->sk_ack_backlog = 0;
inet_csk_delack_init(sk);
sk_state_store(sk, TCP_LISTEN);
if (!sk->sk_prot->get_port(sk, inet->inet_num)) {
......
}
......
}
這裏面建立了一個新的結構 inet_connection_sock,這個結構一開始是struct inet_sock,inet_csk其實做了一次強制類型轉換擴大了結構,這裏又是這個層層套嵌的套路。struct inet_connection_sock結構比較複雜,如果打開它能看到處於各種狀態的隊列,各種超時時間、擁塞控制等字眼,TCP是面向連接的,就是客戶端和服務端都是有一個結構維護連接的狀態,就是指這個結構。這裏先不詳細分析裏面的變量,因爲太多了,後面遇到一個分析一個。
首先遇到的是icsk_accept_queue,它是幹什麼的呢?在TCP的狀態裏面有一個listen狀態,當調用listen函數之後就會進入這個狀態,雖然寫程序的時候,一般要等待服務端調用accept後,等待在那的時候讓客戶端發起連接。其實服務端一旦處於listen狀態不用accept,客戶端也能發起連接。
其實TCP的狀態中,沒有一個是否被accept的狀態,那accept函數的作用是什麼呢?在內核中爲每個Socket維護兩個隊列,一個是已經建立了連接的隊列,這時候連接三次握手已經完畢處於established狀態;一個是還沒有完全建立連接的隊列,這個時候三次握手還沒完成處於syn_rcvd的狀態,服務端調用accept函數,其實是在established隊列中拿出一個已經完成的連接進行處理,如果還沒有完成就阻塞等待。上面代碼的icsk_accept_queue就是第一個隊列。
初始化完之後,將TCP的狀態設置爲TCP_LISTEN,再次調用get_port判斷端口是否衝突。至此listen的邏輯就結束了。
11. 接下來,解析服務端調用accept,如下所示:
SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr,
int __user *, upeer_addrlen)
{
return sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0);
}
SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,
int __user *, upeer_addrlen, int, flags)
{
struct socket *sock, *newsock;
struct file *newfile;
int err, len, newfd, fput_needed;
struct sockaddr_storage address;
......
sock = sockfd_lookup_light(fd, &err, &fput_needed);
newsock = sock_alloc();
newsock->type = sock->type;
newsock->ops = sock->ops;
newfd = get_unused_fd_flags(flags);
newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name);
err = sock->ops->accept(sock, newsock, sock->file->f_flags, false);
if (upeer_sockaddr) {
if (newsock->ops->getname(newsock, (struct sockaddr *)&address, &len, 2) < 0) {
}
err = move_addr_to_user(&address,
len, upeer_sockaddr, upeer_addrlen);
}
fd_install(newfd, newfile);
......
}
accept函數的實現印證了socket原理中說的那樣,原來的socket是監聽socket,這裏會找到原來的struct socket,並基於它去創建一個新的newsock,這纔是連接socket。除此之外還會創建一個新的struct file和fd,並關聯到socket,這裏面還會調用struct socket的sock->ops->accept,即會調用inet_stream_ops的accept函數,也就是inet_accept,如下所示:
int inet_accept(struct socket *sock, struct socket *newsock, int flags, bool kern)
{
struct sock *sk1 = sock->sk;
int err = -EINVAL;
struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err, kern);
sock_rps_record_flow(sk2);
sock_graft(sk2, newsock);
newsock->state = SS_CONNECTED;
}
inet_accept會調用struct sock的sk1->sk_prot->accept,即tcp_prot的accept函數,就是inet_csk_accept函數,如下所示:
/*
* This will accept the next outstanding connection.
*/
struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct request_sock_queue *queue = &icsk->icsk_accept_queue;
struct request_sock *req;
struct sock *newsk;
int error;
if (sk->sk_state != TCP_LISTEN)
goto out_err;
/* Find already established connection */
if (reqsk_queue_empty(queue)) {
long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
error = inet_csk_wait_for_connect(sk, timeo);
}
req = reqsk_queue_remove(queue, sk);
newsk = req->sk;
......
}
/*
* Wait for an incoming connection, avoid race conditions. This must be called
* with the socket locked.
*/
static int inet_csk_wait_for_connect(struct sock *sk, long timeo)
{
struct inet_connection_sock *icsk = inet_csk(sk);
DEFINE_WAIT(wait);
int err;
for (;;) {
prepare_to_wait_exclusive(sk_sleep(sk), &wait,
TASK_INTERRUPTIBLE);
release_sock(sk);
if (reqsk_queue_empty(&icsk->icsk_accept_queue))
timeo = schedule_timeout(timeo);
sched_annotate_sleep();
lock_sock(sk);
err = 0;
if (!reqsk_queue_empty(&icsk->icsk_accept_queue))
break;
err = -EINVAL;
if (sk->sk_state != TCP_LISTEN)
break;
err = sock_intr_errno(timeo);
if (signal_pending(current))
break;
err = -EAGAIN;
if (!timeo)
break;
}
finish_wait(sk_sleep(sk), &wait);
return err;
}
inet_csk_accept的實現印證了上面講的兩個隊列的邏輯。如果icsk_accept_queue爲空,則調用inet_csk_wait_for_connect進行等待;等待的時候調用schedule_timeout讓出CPU,並且將進程狀態設置爲TASK_INTERRUPTIBLE即可被信號量打斷。如果再次CPU醒來會接着判斷icsk_accept_queue是否爲空,同時也會調用signal_pending看有沒有信號可以處理,一旦icsk_accept_queue不爲空,就從inet_csk_wait_for_connect中返回,在隊列中取出一個struct sock對象賦值給newsk。
12. 什麼情況下icsk_accept_queue纔不爲空呢?當然是三次握手結束纔可以,接下來分析三次握手的過程,如下圖所示:
三次握手一般是由客戶端調用connect發起,如下所示:
SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
int, addrlen)
{
struct socket *sock;
struct sockaddr_storage address;
int err, fput_needed;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
err = move_addr_to_kernel(uservaddr, addrlen, &address);
err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen, sock->file->f_flags);
}
connect函數的實現一開始應該很眼熟,還是通過sockfd_lookup_light根據fd文件描述符,找到struct socket結構。接着會調用struct socket結構裏面ops的connect函數,根據前面創建socket時的設定,調用inet_stream_ops的connect函數,即調用inet_stream_connect,如下所示:
/*
* Connect to a remote host. There is regrettably still a little
* TCP 'magic' in here.
*/
int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
int addr_len, int flags, int is_sendmsg)
{
struct sock *sk = sock->sk;
int err;
long timeo;
switch (sock->state) {
......
case SS_UNCONNECTED:
err = -EISCONN;
if (sk->sk_state != TCP_CLOSE)
goto out;
err = sk->sk_prot->connect(sk, uaddr, addr_len);
sock->state = SS_CONNECTING;
break;
}
timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);
if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
......
if (!timeo || !inet_wait_for_connect(sk, timeo, writebias))
goto out;
err = sock_intr_errno(timeo);
if (signal_pending(current))
goto out;
}
sock->state = SS_CONNECTED;
}
在__inet_stream_connect裏面,如果socket處於SS_UNCONNECTED狀態,那就調用struct sock的sk->sk_prot->connect,即tcp_prot的connect函數——tcp_v4_connect函數進行連接,如下所示:
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
struct inet_sock *inet = inet_sk(sk);
struct tcp_sock *tp = tcp_sk(sk);
__be16 orig_sport, orig_dport;
__be32 daddr, nexthop;
struct flowi4 *fl4;
struct rtable *rt;
......
orig_sport = inet->inet_sport;
orig_dport = usin->sin_port;
rt = ip_route_connect(fl4, nexthop, inet->inet_saddr,
RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,
IPPROTO_TCP,
orig_sport, orig_dport, sk);
......
tcp_set_state(sk, TCP_SYN_SENT);
err = inet_hash_connect(tcp_death_row, sk);
sk_set_txhash(sk);
rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,
inet->inet_sport, inet->inet_dport, sk);
/* OK, now commit destination to socket. */
sk->sk_gso_type = SKB_GSO_TCPV4;
sk_setup_caps(sk, &rt->dst);
if (likely(!tp->repair)) {
if (!tp->write_seq)
tp->write_seq = secure_tcp_seq(inet->inet_saddr,
inet->inet_daddr,
inet->inet_sport,
usin->sin_port);
tp->tsoffset = secure_tcp_ts_off(sock_net(sk),
inet->inet_saddr,
inet->inet_daddr);
}
rt = NULL;
......
err = tcp_connect(sk);
......
}
在tcp_v4_connect函數中,ip_route_connect其實是做一個路由的選擇,因爲三次握手馬上就要發送一個SYN包了,這就要湊齊源地址、源端口、目標地址、目標端口。目標地址和目標端口是服務端的,已經知道源端口是客戶端隨機分配的,源地址應該用哪一個呢?這時候要選擇一條路由,看從哪個網卡出去,就應該填寫哪個網卡的IP地址。接下來在發送SYN之前,先將客戶端socket的狀態設置爲TCP_SYN_SENT。然後初始化TCP的seq num即write_seq,然後調用tcp_connect進行發送,如下所示:
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
struct inet_sock *inet = inet_sk(sk);
struct tcp_sock *tp = tcp_sk(sk);
__be16 orig_sport, orig_dport;
__be32 daddr, nexthop;
struct flowi4 *fl4;
struct rtable *rt;
......
orig_sport = inet->inet_sport;
orig_dport = usin->sin_port;
rt = ip_route_connect(fl4, nexthop, inet->inet_saddr,
RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,
IPPROTO_TCP,
orig_sport, orig_dport, sk);
......
tcp_set_state(sk, TCP_SYN_SENT);
err = inet_hash_connect(tcp_death_row, sk);
sk_set_txhash(sk);
rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,
inet->inet_sport, inet->inet_dport, sk);
/* OK, now commit destination to socket. */
sk->sk_gso_type = SKB_GSO_TCPV4;
sk_setup_caps(sk, &rt->dst);
if (likely(!tp->repair)) {
if (!tp->write_seq)
tp->write_seq = secure_tcp_seq(inet->inet_saddr,
inet->inet_daddr,
inet->inet_sport,
usin->sin_port);
tp->tsoffset = secure_tcp_ts_off(sock_net(sk),
inet->inet_saddr,
inet->inet_daddr);
}
rt = NULL;
......
err = tcp_connect(sk);
......
}
在tcp_v4_connect函數中,ip_route_connect其實是做一個路由的選擇,因爲三次握手馬上就要發送一個SYN包了,這就要湊齊源地址、源端口、目標地址、目標端口。目標地址和目標端口是服務端的,已經知道源端口是客戶端隨機分配的,源地址應該用哪一個呢?這時候要選擇一條路由,看從哪個網卡出去,就應該填寫哪個網卡的IP地址。接下來在發送SYN之前,先將客戶端socket的狀態設置爲TCP_SYN_SENT。然後初始化TCP的seq num即write_seq,然後調用tcp_connect進行發送,如下所示:
/* Build a SYN and send it off. */
int tcp_connect(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *buff;
int err;
......
tcp_connect_init(sk);
......
buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true);
......
tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);
tcp_mstamp_refresh(tp);
tp->retrans_stamp = tcp_time_stamp(tp);
tcp_connect_queue_skb(sk, buff);
tcp_ecn_send_syn(sk, buff);
/* Send off SYN; include data in Fast Open. */
err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
......
tp->snd_nxt = tp->write_seq;
tp->pushed_seq = tp->write_seq;
buff = tcp_send_head(sk);
if (unlikely(buff)) {
tp->snd_nxt = TCP_SKB_CB(buff)->seq;
tp->pushed_seq = TCP_SKB_CB(buff)->seq;
}
......
/* Timer for repeating the SYN until an answer. */
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
return 0;
}
在tcp_connect中有一個新的結構struct tcp_sock,如果打開它,會發現它是struct inet_connection_sock的一個擴展,struct inet_connection_sock在struct tcp_sock開頭的位置,通過強制類型轉換訪問,擴展套嵌故伎重演。struct tcp_sock裏面維護了更多的TCP的狀態,後面遇到了再分析。接下來tcp_init_nondata_skb初始化一個SYN包,tcp_transmit_skb將SYN包發送出去,inet_csk_reset_xmit_timer設置了一個timer,如果SYN發送不成功則再次發送。發送網絡包的過程放到後面再說。這裏姑且認爲SYN已經發送出去了。
13. 回到__inet_stream_connect函數,在調用sk->sk_prot->connect之後,inet_wait_for_connect會一直等待客戶端收到服務端的ACK。而服務端在accept之後也是在等待中。網絡包是如何接收的呢?對於接收的詳細過程後面會講解,這裏爲了解析三次握手,先簡單的看網絡包接收到TCP層做的部分事情,如下所示:
static struct net_protocol tcp_protocol = {
.early_demux = tcp_v4_early_demux,
.early_demux_handler = tcp_v4_early_demux,
.handler = tcp_v4_rcv,
.err_handler = tcp_v4_err,
.no_policy = 1,
.netns_ok = 1,
.icmp_strict_tag_validation = 1,
}
通過struct net_protocol結構中的handler進行接收,調用的函數是tcp_v4_rcv,接下來的調用鏈爲tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_state_process。tcp_rcv_state_process顧名思義是用來處理接收一個網絡包後引起狀態變化的,如下所示:
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
const struct tcphdr *th = tcp_hdr(skb);
struct request_sock *req;
int queued = 0;
bool acceptable;
switch (sk->sk_state) {
......
case TCP_LISTEN:
......
if (th->syn) {
acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0;
if (!acceptable)
return 1;
consume_skb(skb);
return 0;
}
......
}
目前服務端是處於TCP_LISTEN狀態的,而且發過來的包是SYN,因而就有了上面的代碼,調用icsk->icsk_af_ops->conn_request函數。struct inet_connection_sock對應的操作是inet_connection_sock_af_ops,按照下面的定義其實調用的是tcp_v4_conn_request:
const struct inet_connection_sock_af_ops ipv4_specific = {
.queue_xmit = ip_queue_xmit,
.send_check = tcp_v4_send_check,
.rebuild_header = inet_sk_rebuild_header,
.sk_rx_dst_set = inet_sk_rx_dst_set,
.conn_request = tcp_v4_conn_request,
.syn_recv_sock = tcp_v4_syn_recv_sock,
.net_header_len = sizeof(struct iphdr),
.setsockopt = ip_setsockopt,
.getsockopt = ip_getsockopt,
.addr2sockaddr = inet_csk_addr2sockaddr,
.sockaddr_len = sizeof(struct sockaddr_in),
.mtu_reduced = tcp_v4_mtu_reduced,
};
tcp_v4_conn_request會調用tcp_conn_request,這個函數也比較長裏面調用了send_synack,但實際調用的是tcp_v4_send_synack。具體發送的過程不去管它,看註釋能知道這是收到了SYN後回覆一個SYN-ACK,回覆完畢後服務端處於TCP_SYN_RECV,如下所示:
int tcp_conn_request(struct request_sock_ops *rsk_ops,
const struct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)
{
......
af_ops->send_synack(sk, dst, &fl, req, &foc,
!want_cookie ? TCP_SYNACK_NORMAL :
TCP_SYNACK_COOKIE);
......
}
/*
* Send a SYN-ACK after having received a SYN.
*/
static int tcp_v4_send_synack(const struct sock *sk, struct dst_entry *dst,
struct flowi *fl,
struct request_sock *req,
struct tcp_fastopen_cookie *foc,
enum tcp_synack_type synack_type)
{......}
這個時候輪到客戶端接收網絡包了。都是TCP協議棧,所以過程和服務端沒有太多區別,還是會走到tcp_rcv_state_process函數的,只不過由於客戶端目前處於TCP_SYN_SENT狀態,就進入了下面的代碼分支:
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
const struct tcphdr *th = tcp_hdr(skb);
struct request_sock *req;
int queued = 0;
bool acceptable;
switch (sk->sk_state) {
......
case TCP_SYN_SENT:
tp->rx_opt.saw_tstamp = 0;
tcp_mstamp_refresh(tp);
queued = tcp_rcv_synsent_state_process(sk, skb, th);
if (queued >= 0)
return queued;
/* Do step6 onward by hand. */
tcp_urg(sk, skb, th);
__kfree_skb(skb);
tcp_data_snd_check(sk);
return 0;
}
......
}
tcp_rcv_synsent_state_process會調用tcp_send_ack發送一個ACK-ACK,發送後客戶端處於TCP_ESTABLISHED狀態。又輪到服務端接收網絡包了,還是歸tcp_rcv_state_process函數處理。由於服務端目前處於狀態TCP_SYN_RECV狀態,因而又走了另外的分支。當收到這個網絡包的時候,服務端也處於TCP_ESTABLISHED狀態,三次握手結束,如下所示:
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
const struct tcphdr *th = tcp_hdr(skb);
struct request_sock *req;
int queued = 0;
bool acceptable;
......
switch (sk->sk_state) {
case TCP_SYN_RECV:
if (req) {
inet_csk(sk)->icsk_retransmits = 0;
reqsk_fastopen_remove(sk, req, false);
} else {
/* Make sure socket is routed, for correct metrics. */
icsk->icsk_af_ops->rebuild_header(sk);
tcp_call_bpf(sk, BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB);
tcp_init_congestion_control(sk);
tcp_mtup_init(sk);
tp->copied_seq = tp->rcv_nxt;
tcp_init_buffer_space(sk);
}
smp_mb();
tcp_set_state(sk, TCP_ESTABLISHED);
sk->sk_state_change(sk);
if (sk->sk_socket)
sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);
tp->snd_una = TCP_SKB_CB(skb)->ack_seq;
tp->snd_wnd = ntohs(th->window) << tp->rx_opt.snd_wscale;
tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);
break;
......
}
14. 除了網絡包的接收和發送,其他的系統調用都分析到了。可以看出它們有一個統一的數據結構和流程。具體如下圖所示:
首先Socket系統調用會有三級參數family、type、protocal,通過這三級參數分別在net_proto_family表中找到type鏈表,在type鏈表中找到protocal對應的操作。這個操作分爲兩層,對於TCP協議來講,第一層是inet_stream_ops層,第二層是tcp_prot層,於是接下來的系統調用規律就都一樣了:
(1)bind第一層調用inet_stream_ops的inet_bind函數,第二層調用tcp_prot的inet_csk_get_port函數;
(2)listen第一層調用inet_stream_ops的inet_listen函數,第二層調用tcp_prot的inet_csk_get_port函數;
(3)accept第一層調用inet_stream_ops的inet_accept函數,第二層調用tcp_prot的inet_csk_accept函數;
(4)connect第一層調用inet_stream_ops的inet_stream_connect函數,第二層調用tcp_prot的tcp_v4_connect函數。
四、發送網絡包
15. socket對於用戶來講是一個文件一樣的存在,擁有一個文件描述符。因而對於網絡包的發送,可以使用對於socket文件的寫入系統調用,也就是write系統調用。write系統調用對於一個文件描述符的操作,大致過程都是類似的。對於每一個打開的文件都有一struct file 結構,write系統調用會最終調用stuct file結構指向的file_operations操作。對於socket來講,它的file_operations定義如下:
static const struct file_operations socket_file_ops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.read_iter = sock_read_iter,
.write_iter = sock_write_iter,
.poll = sock_poll,
.unlocked_ioctl = sock_ioctl,
.mmap = sock_mmap,
.release = sock_close,
.fasync = sock_fasync,
.sendpage = sock_sendpage,
.splice_write = generic_splice_sendpage,
.splice_read = sock_splice_read,
};
按照文件系統的寫入流程,調用的是sock_write_iter,如下所示:
static ssize_t sock_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
struct file *file = iocb->ki_filp;
struct socket *sock = file->private_data;
struct msghdr msg = {.msg_iter = *from,
.msg_iocb = iocb};
ssize_t res;
......
res = sock_sendmsg(sock, &msg);
*from = msg.msg_iter;
return res;
}
在sock_write_iter中通過VFS中的struct file,將創建好的socket結構拿出來,然後調用sock_sendmsg,而sock_sendmsg會調用sock_sendmsg_nosec,如下所示:
static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)
{
int ret = sock->ops->sendmsg(sock, msg, msg_data_left(msg));
......
}
這裏調用了socket的ops的sendmsg,其實就是inet_stream_ops,根據它的定義這裏調用的是inet_sendmsg,如下所示:
int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
struct sock *sk = sock->sk;
......
return sk->sk_prot->sendmsg(sk, msg, size);
}
16. 根據tcp_prot的定義,調用的是tcp_sendmsg,如下所示:
int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb;
int flags, err, copied = 0;
int mss_now = 0, size_goal, copied_syn = 0;
long timeo;
......
/* Ok commence sending. */
copied = 0;
restart:
mss_now = tcp_send_mss(sk, &size_goal, flags);
while (msg_data_left(msg)) {
int copy = 0;
int max = size_goal;
skb = tcp_write_queue_tail(sk);
if (tcp_send_head(sk)) {
if (skb->ip_summed == CHECKSUM_NONE)
max = mss_now;
copy = max - skb->len;
}
if (copy <= 0 || !tcp_skb_can_collapse_to(skb)) {
bool first_skb;
new_segment:
/* Allocate new segment. If the interface is SG,
* allocate skb fitting to single page.
*/
if (!sk_stream_memory_free(sk))
goto wait_for_sndbuf;
......
first_skb = skb_queue_empty(&sk->sk_write_queue);
skb = sk_stream_alloc_skb(sk,
select_size(sk, sg, first_skb),
sk->sk_allocation,
first_skb);
......
skb_entail(sk, skb);
copy = size_goal;
max = size_goal;
......
}
/* Try to append data to the end of skb. */
if (copy > msg_data_left(msg))
copy = msg_data_left(msg);
/* Where to copy to? */
if (skb_availroom(skb) > 0) {
/* We have some space in skb head. Superb! */
copy = min_t(int, copy, skb_availroom(skb));
err = skb_add_data_nocache(sk, skb, &msg->msg_iter, copy);
......
} else {
bool merge = true;
int i = skb_shinfo(skb)->nr_frags;
struct page_frag *pfrag = sk_page_frag(sk);
......
copy = min_t(int, copy, pfrag->size - pfrag->offset);
......
err = skb_copy_to_page_nocache(sk, &msg->msg_iter, skb,
pfrag->page,
pfrag->offset,
copy);
......
pfrag->offset += copy;
}
......
tp->write_seq += copy;
TCP_SKB_CB(skb)->end_seq += copy;
tcp_skb_pcount_set(skb, 0);
copied += copy;
if (!msg_data_left(msg)) {
if (unlikely(flags & MSG_EOR))
TCP_SKB_CB(skb)->eor = 1;
goto out;
}
if (skb->len < max || (flags & MSG_OOB) || unlikely(tp->repair))
continue;
if (forced_push(tp)) {
tcp_mark_push(tp, skb);
__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
} else if (skb == tcp_send_head(sk))
tcp_push_one(sk, mss_now);
continue;
......
}
......
}
tcp_sendmsg的實現還是很複雜的,這裏面做了這樣幾件事情。msg是用戶要寫入的數據,這個數據要拷貝到內核協議棧裏面去發送;在內核協議棧裏面,網絡包的數據都是由struct sk_buff維護的,因而第一件事情就是找到一個空閒的內存空間,將用戶要寫入的數據拷貝到struct sk_buff的管轄範圍內。而第二件事情就是發送struct sk_buff。在tcp_sendmsg中首先通過強制類型轉換,將sock結構轉換爲struct tcp_sock,這個是維護TCP連接狀態的重要數據結構。
接下來是tcp_sendmsg的第一件事情,把數據拷貝到struct sk_buff。先聲明一個變量copied初始化爲0,這表示拷貝了多少數據,緊接着是一個循環,while (msg_data_left(msg))即如果用戶的數據沒有發送完畢,就一直循環。循環裏聲明瞭一個copy變量,表示這次拷貝的數值,在循環的最後有copied += copy,將每次拷貝的數量都加起來,這裏只需要看一次循環做了哪些事情:
(1)第一步,tcp_write_queue_tail從TCP寫入隊列sk_write_queue中拿出最後一個struct sk_buff,在這個寫入隊列中排滿了要發送的 struct sk_buff,爲什麼要拿最後一個呢?這裏面只有最後一個,可能會因爲上次用戶給的數據太少,而沒有填滿。
(2)第二步,tcp_send_mss會計算MSS即Max Segment Size。這個意思是說,在網絡上傳輸的網絡包的大小是有限制的,而這個限制在最底層開始就有。MTU(Maximum Transmission Unit,最大傳輸單元)是二層的一個定義,以以太網爲例MTU爲1500個Byte,前面有6個Byte的目標MAC地址,6個Byt的源 MAC地址,2個Byte的類型,後面有4個Byte的CRC校驗,共1518個Byte。在IP層,一個IP數據報在以太網中傳輸,如果它的長度大於該MTU值,就要進行分片傳輸。
在 TCP 層有個MSS,等於MTU 減去IP頭,再減去TCP頭,也就是在不分片的情況下,TCP裏面放的最大內容。在這裏max是struct sk_buff的最大數據長度,skb->len是當前已經佔用的skb的數據長度,相減得到當前skb的剩餘數據空間。
(3)第三步,如果copy小於0,說明最後一個struct sk_buff已經沒地方存放了,需要調用sk_stream_alloc_skb重新分配struct sk_buff,然後調用skb_entail,將新分配的sk_buf放到隊列尾部。struct sk_buff 是存儲網絡包的重要數據結構,在應用層數據包叫data,在TCP層稱爲segment,在IP層叫packet,在數據鏈路層稱爲frame。在struct sk_buff,首先是一個鏈表將struct sk_buff結構串起來。
接下來從headers_start開始,到headers_end結束,裏面都是各層次的頭的位置。這裏面有二層的mac_header、三層的network_header和四層的transport_header。sk_buff的實現如下所示:
struct sk_buff {
union {
struct {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;
......
};
struct rb_node rbnode; /* used in netem & tcp stack */
};
......
/* private: */
__u32 headers_start[0];
/* public: */
......
__u32 priority;
int skb_iif;
__u32 hash;
__be16 vlan_proto;
__u16 vlan_tci;
......
union {
__u32 mark;
__u32 reserved_tailroom;
};
union {
__be16 inner_protocol;
__u8 inner_ipproto;
};
__u16 inner_transport_header;
__u16 inner_network_header;
__u16 inner_mac_header;
__be16 protocol;
__u16 transport_header;
__u16 network_header;
__u16 mac_header;
/* private: */
__u32 headers_end[0];
/* public: */
/* These elements must be at the end, see alloc_skb() for details. */
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char *head,
*data;
unsigned int truesize;
refcount_t users;
};
最後幾項, head指向分配的內存塊起始地址,data這個指針指向的位置是可變的,它有可能隨着報文所處的層次而變動。當接收報文時,從網卡驅動開始,通過協議棧層層往上傳送數據報,通過增加skb->data的值,來逐步剝離協議首部。而要發送報文時,各協議會創建sk_buff{},在經過各下層協議時,通過減少skb->data的值來增加協議首部。tail指向數據的結尾,end指向分配的內存塊的結束地址。要分配這樣一個結構,sk_stream_alloc_skb會最終調用到__alloc_skb,在這個函數裏面除了分配一個sk_buff結構之外,還要分配sk_buff指向的數據區域,這段數據區域分爲下面這幾個部分:
第一部分是連續的數據區域,緊接着是第二部分,即一個struct skb_shared_info結構,這個結構是對於網絡包發送過程的一個優化,因爲傳輸層之上就是應用層了。按照TCP的定義,應用層感受不到下面網絡層的IP包是一個個獨立包的存在的,反正就是一個流往裏寫就是了,可能一下子寫多了超過了一個IP包的承載能力,就會出現上面MSS的定義,拆分成一個個的Segment放在一個個的IP包裏面,也可能一次寫一點,這樣數據是分散的,在IP層還要通過內存拷貝合成一個IP包。
爲了減少內存拷貝的代價,有的網絡設備支持分散聚合(Scatter/Gather)I/O,顧名思義就是IP層沒必要通過內存拷貝進行聚合,讓散的數據零散的放在原處,在設備層進行聚合。如果使用這種模式,網絡包的數據就不會放在連續的數據區域,而是放在struct skb_shared_info結構裏面指向的離散數據,skb_shared_info的成員變量skb_frag_t frags[MAX_SKB_FRAGS]會指向一個數組的頁面,就不能保證連續了。
(4)於是就有了第四步。在註釋 /* Where to copy to? */ 後面有個if-else分支,if分支就是skb_add_data_nocache將數據拷貝到連續的數據區域,else分支就是skb_copy_to_page_nocache將數據拷貝到struct skb_shared_info結構指向的不需要連續的頁面區域。
(5)第五步,就是要發送網絡包了。第一種情況是積累的數據報數目太多了,因而需要通過調用__tcp_push_pending_frames發送網絡包。第二種情況是這是第一個網絡包,需要馬上發送,調用tcp_push_one。無論__tcp_push_pending_frames還是tcp_push_one,都會調用tcp_write_xmit發送網絡包。至此,tcp_sendmsg解析完了。
17. 接下來看,tcp_write_xmit是如何發送網絡包的,如下所示:
static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle, int push_one, gfp_t gfp)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb;
unsigned int tso_segs, sent_pkts;
int cwnd_quota;
......
max_segs = tcp_tso_segs(sk, mss_now);
while ((skb = tcp_send_head(sk))) {
unsigned int limit;
......
tso_segs = tcp_init_tso_segs(skb, mss_now);
......
cwnd_quota = tcp_cwnd_test(tp, skb);
......
if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) {
is_rwnd_limited = true;
break;
}
......
limit = mss_now;
if (tso_segs > 1 && !tcp_urg_mode(tp))
limit = tcp_mss_split_point(sk, skb, mss_now, min_t(unsigned int, cwnd_quota, max_segs), nonagle);
if (skb->len > limit &&
unlikely(tso_fragment(sk, skb, limit, mss_now, gfp)))
break;
......
if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
break;
repair:
/* Advance the send_head. This one is sent out.
* This call will increment packets_out.
*/
tcp_event_new_data_sent(sk, skb);
tcp_minshall_update(tp, mss_now, skb);
sent_pkts += tcp_skb_pcount(skb);
if (push_one)
break;
}
......
}
這裏面主要的邏輯是一個循環,用來處理髮送隊列,只要隊列不空就會發送。在一個循環中,涉及TCP層的很多傳輸算法,來一一解析。第一個概念是TSO(TCP Segmentation Offload),如果發送的網絡包非常大,就像上面說的一樣要進行分段。分段這個事情可以由協議棧代碼在內核做,但是缺點是比較費CPU,另一種方式是延遲到硬件網卡去做,需要網卡支持對大數據包進行自動分段,可以降低CPU負載。
在代碼中,tcp_init_tso_segs會調用tcp_set_skb_tso_segs。這裏面有這樣的語句:DIV_ROUND_UP(skb->len, mss_now),也就是sk_buff的長度除以mss_now,應該分成幾個段。如果算出來要分成多個段,接下來就是要看是在協議棧的代碼裏面分好,還是等待到了底層網卡再分,於是調用函數tcp_mss_split_point開始計算切分的limit,這裏面會計算max_len = mss_now * max_segs,根據現在不切分來計算limit,所以下一步的判斷中,大部分情況下tso_fragment不會被調用,等待到了底層網卡來切分。
第二個概念是擁塞窗口(cwnd,congestion window),也就是說爲了避免拼命發包把網絡塞滿了,定義一個窗口的概念,在這個窗口之內的才能發送,超過這個窗口的就不能發送,來控制發送的頻率。那窗口大小是多少呢?就是遵循下面這個著名的擁塞窗口變化圖:
一開始的窗口只有一個mss大小叫作slow start(慢啓動)。一開始的增長速度是很快的,翻倍增長。一旦到達一個臨界值ssthresh,就變成線性增長,就稱爲擁塞避免。什麼時候算真正擁塞呢?就是出現了丟包。一旦丟包,一種方法是馬上降回到一個mss,然後重複先翻倍再線性對的過程。如果覺得太過激進也可以有第二種方法,就是降到當前cwnd的一半,然後進行線性增長。
在代碼中,tcp_cwnd_test會將當前的snd_cwnd,減去已經在窗口裏面尚未發送完畢的網絡包,那就是剩下的窗口大小cwnd_quota,即就能發送這麼多了。
第三個概念就是接收窗口rwnd的概念(receive window),也叫滑動窗口。如果說擁塞窗口是爲了怕把網絡塞滿,在出現丟包的時候減少發送速度,那麼滑動窗口就是爲了怕把接收方塞滿,而控制發送速度。滑動窗口,其實就是接收方主動告訴發送方自己的網絡包的接收能力,超過這個能力就受不了了。因爲滑動窗口的存在,將發送方的緩存分成了四個部分:
(1)發送了並且已經確認的。這部分是已經發送完畢的網絡包,沒有用了可以回收。
(2)發送了但尚未確認的。這部分發送方要等待,萬一發送不成功,還要重新發送,所以不能刪除。
(3)沒有發送,但是已經等待發送的。這部分是接收方空閒的能力,可以馬上發送,接收方受得了。
(4)沒有發送,並且暫時還不會發送的。這部分已經超過了接收方的接收能力,再發送接收方就受不了了。
因爲滑動窗口的存在,接收方的緩存也要分成了三個部分,如下圖所示:
(1)接受並且確認過的任務。這部分完全接收成功了,可以交給應用層了。
(2)還沒接收,但是馬上就能接收的任務。這部分有的網絡包到達了,但是還沒確認,不算完全完畢,有的還沒有到達,那就是接收方能夠接受的最大的網絡包數量。
(3)還沒接收,也沒法接收的任務。這部分已經超出接收方能力。
在網絡包的交互過程中,接收方會將第二部分的大小,作爲AdvertisedWindow發送給發送方,發送方就可以根據它來調整發送速度了。在tcp_snd_wnd_test函數中,會判斷sk_buff中的end_seq和tcp_wnd_end(tp)之間的關係,即這個sk_buff是否在滑動窗口的允許範圍之內,如果不在範圍內說明發送要受限制了,就要把is_rwnd_limited設置爲 true。接下來,tcp_mss_split_point函數要被調用了,如下所示:
static unsigned int tcp_mss_split_point(const struct sock *sk,
const struct sk_buff *skb,
unsigned int mss_now,
unsigned int max_segs,
int nonagle)
{
const struct tcp_sock *tp = tcp_sk(sk);
u32 partial, needed, window, max_len;
window = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq;
max_len = mss_now * max_segs;
if (likely(max_len <= window && skb != tcp_write_queue_tail(sk)))
return max_len;
needed = min(skb->len, window);
if (max_len <= needed)
return max_len;
......
return needed;
}
這裏面除了會判斷上面講的是否會因爲超出mss而分段,還會判斷另一個條件,就是是否在滑動窗口的運行範圍之內,如果小於窗口的大小也需要分段,即需要調用tso_fragment。在一個循環的最後是調用tcp_transmit_skb,真的去發送一個網絡包,如下所示:
static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
gfp_t gfp_mask)
{
const struct inet_connection_sock *icsk = inet_csk(sk);
struct inet_sock *inet;
struct tcp_sock *tp;
struct tcp_skb_cb *tcb;
struct tcphdr *th;
int err;
tp = tcp_sk(sk);
skb->skb_mstamp = tp->tcp_mstamp;
inet = inet_sk(sk);
tcb = TCP_SKB_CB(skb);
memset(&opts, 0, sizeof(opts));
tcp_header_size = tcp_options_size + sizeof(struct tcphdr);
skb_push(skb, tcp_header_size);
/* Build TCP header and checksum it. */
th = (struct tcphdr *)skb->data;
th->source = inet->inet_sport;
th->dest = inet->inet_dport;
th->seq = htonl(tcb->seq);
th->ack_seq = htonl(tp->rcv_nxt);
*(((__be16 *)th) + 6) = htons(((tcp_header_size >> 2) << 12) |
tcb->tcp_flags);
th->check = 0;
th->urg_ptr = 0;
......
tcp_options_write((__be32 *)(th + 1), tp, &opts);
th->window = htons(min(tp->rcv_wnd, 65535U));
......
err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);
......
}
tcp_transmit_skb這個函數比較長,主要做了兩件事情,第一件事情就是填充TCP頭,如果對着TCP頭的格式來看:
這裏面有源端口設置爲inet_sport,有目標端口設置爲inet_dport;有序列號設置爲tcb->seq;有確認序列號設置爲tp->rcv_nxt。把所有的flags設置爲tcb->tcp_flags,設置選項爲opts,設置窗口大小爲tp->rcv_wnd。全部設置完畢之後,就會調用icsk_af_ops的queue_xmit方法,icsk_af_ops指向ipv4_specific,即調用的是ip_queue_xmit函數,到了IP層,如下所示:
const struct inet_connection_sock_af_ops ipv4_specific = {
.queue_xmit = ip_queue_xmit,
.send_check = tcp_v4_send_check,
.rebuild_header = inet_sk_rebuild_header,
.sk_rx_dst_set = inet_sk_rx_dst_set,
.conn_request = tcp_v4_conn_request,
.syn_recv_sock = tcp_v4_syn_recv_sock,
.net_header_len = sizeof(struct iphdr),
.setsockopt = ip_setsockopt,
.getsockopt = ip_getsockopt,
.addr2sockaddr = inet_csk_addr2sockaddr,
.sockaddr_len = sizeof(struct sockaddr_in),
.mtu_reduced = tcp_v4_mtu_reduced,
};
18. 上面解析了發送一個網絡包的一部分過程(到IP層),如下圖所示:
這個過程分成幾個層次:
(1)VFS層:write系統調用找到struct file,根據裏面file_operations的定義調用sock_write_iter函數。sock_write_iter函數調用sock_sendmsg函數。
(2)Socket層:從struct file裏面的private_data得到struct socket,根據裏面ops的定義調用inet_sendmsg函數。
(3)Sock層:從struct socket裏面的sk得到struct sock,根據裏面sk_prot的定義調用tcp_sendmsg函數。
(4)TCP層:tcp_sendmsg函數會調用tcp_write_xmit函數,tcp_write_xmit函數會調用tcp_transmit_skb,在這裏實現了TCP層面向連接的邏輯。
(5)IP層:擴展struct sock得到struct inet_connection_sock,根據裏面icsk_af_ops的定義調用ip_queue_xmit函數。
19. 上面講網絡包的發送講了上半部分,即從VFS層一直到IP層,這裏接着看IP層和MAC層是如何發送數據的。從ip_queue_xmit函數開始,就要進入IP層的發送邏輯了:
int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl)
{
struct inet_sock *inet = inet_sk(sk);
struct net *net = sock_net(sk);
struct ip_options_rcu *inet_opt;
struct flowi4 *fl4;
struct rtable *rt;
struct iphdr *iph;
int res;
inet_opt = rcu_dereference(inet->inet_opt);
fl4 = &fl->u.ip4;
rt = skb_rtable(skb);
/* Make sure we can route this packet. */
rt = (struct rtable *)__sk_dst_check(sk, 0);
if (!rt) {
__be32 daddr;
/* Use correct destination address if we have options. */
daddr = inet->inet_daddr;
......
rt = ip_route_output_ports(net, fl4, sk,
daddr, inet->inet_saddr,
inet->inet_dport,
inet->inet_sport,
sk->sk_protocol,
RT_CONN_FLAGS(sk),
sk->sk_bound_dev_if);
if (IS_ERR(rt))
goto no_route;
sk_setup_caps(sk, &rt->dst);
}
skb_dst_set_noref(skb, &rt->dst);
packet_routed:
/* OK, we know where to send it, allocate and build IP header. */
skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0));
skb_reset_network_header(skb);
iph = ip_hdr(skb);
*((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));
if (ip_dont_fragment(sk, &rt->dst) && !skb->ignore_df)
iph->frag_off = htons(IP_DF);
else
iph->frag_off = 0;
iph->ttl = ip_select_ttl(inet, &rt->dst);
iph->protocol = sk->sk_protocol;
ip_copy_addrs(iph, fl4);
/* Transport layer set skb->h.foo itself. */
if (inet_opt && inet_opt->opt.optlen) {
iph->ihl += inet_opt->opt.optlen >> 2;
ip_options_build(skb, &inet_opt->opt, inet->inet_daddr, rt, 0);
}
ip_select_ident_segs(net, skb, sk,
skb_shinfo(skb)->gso_segs ?: 1);
/* TODO : should we use skb->sk here instead of sk ? */
skb->priority = sk->sk_priority;
skb->mark = sk->sk_mark;
res = ip_local_out(net, sk, skb);
......
}
在 ip_queue_xmit 即 IP 層的發送函數裏面,有三部分邏輯:
(1)第一部分,選取路由,即要發送這個包應該從哪個網卡出去。這件事情主要由ip_route_output_ports函數完成。接下來的調用鏈爲:ip_route_output_ports->ip_route_output_flow->__ip_route_output_key->ip_route_output_key_hash->ip_route_output_key_hash_rcu,如下所示:
struct rtable *ip_route_output_key_hash_rcu(struct net *net, struct flowi4 *fl4, struct fib_result *res, const struct sk_buff *skb)
{
struct net_device *dev_out = NULL;
int orig_oif = fl4->flowi4_oif;
unsigned int flags = 0;
struct rtable *rth;
......
err = fib_lookup(net, fl4, res, 0);
......
make_route:
rth = __mkroute_output(res, fl4, orig_oif, dev_out, flags);
......
}
ip_route_output_key_hash_rcu先會調用fib_lookup,FIB(Forwarding Information Base,轉發信息表)其實就是常說的路由表,如下所示:
static inline int fib_lookup(struct net *net, const struct flowi4 *flp, struct fib_result *res, unsigned int flags)
{ struct fib_table *tb;
......
tb = fib_get_table(net, RT_TABLE_MAIN);
if (tb)
err = fib_table_lookup(tb, flp, res, flags | FIB_LOOKUP_NOREF);
......
}
路由表可以有多個,一般會有一個主表,RT_TABLE_MAIN,然後fib_table_lookup函數在這個表裏面進行查找。路由就是在Linux服務器上的路由表裏面配置的一條一條規則,這些規則大概是這樣的:想訪問某個網段,從某個網卡出去,下一跳是某個IP。之前講過一個簡單的拓撲圖,裏面三臺Linux機器的路由表都可以通過ip route命令查看,如下所示:
# Linux服務器A
default via 192.168.1.1 dev eth0
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.100 metric 100
# Linux服務器B
default via 192.168.2.1 dev eth0
192.168.2.0/24 dev eth0 proto kernel scope link src 192.168.2.100 metric 100
# Linux服務器做路由器
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.1
192.168.2.0/24 dev eth1 proto kernel scope link src 192.168.2.1
可以看到對於兩端的服務器來講,沒有太多路由可以選,但是對於中間Linux路由器來講有兩條路可以選,一個是往左面轉發,一個是往右面轉發,就需要路由表的查找。fib_table_lookup的代碼邏輯比較複雜,好在註釋比較清楚。因爲路由表要按照前綴進行查詢,希望找到最長匹配的那一個,例如192.168.2.0/24和192.168.0.0/16都能匹配192.168.2.100/24,但是應該使用192.168.2.0/24的這一條。爲了更方面做這個事情,使用了Trie樹這種結構(能比較好的查詢最長前綴)。比如有一系列的字符串:{bcs#, badge#, baby#, back#, badger#, badness#},之所以每個字符串都加上#,是希望不要一個字符串成爲另外一個字符串的前綴,然後把它們放在Trie樹中,如下圖所示:
對於將IP地址轉成二進制放入trie樹也是同樣的道理,可以很快進行路由的查詢。找到了路由,就知道了應該從哪個網卡發出去。然後ip_route_output_key_hash_rcu會調用__mkroute_output,創建一個struct rtable,表示找到的路由表項,這個結構是由rt_dst_alloc函數分配的,如下所示:
struct rtable *rt_dst_alloc(struct net_device *dev,
unsigned int flags, u16 type,
bool nopolicy, bool noxfrm, bool will_cache)
{
struct rtable *rt;
rt = dst_alloc(&ipv4_dst_ops, dev, 1, DST_OBSOLETE_FORCE_CHK,
(will_cache ? 0 : DST_HOST) |
(nopolicy ? DST_NOPOLICY : 0) |
(noxfrm ? DST_NOXFRM : 0));
if (rt) {
rt->rt_genid = rt_genid_ipv4(dev_net(dev));
rt->rt_flags = flags;
rt->rt_type = type;
rt->rt_is_input = 0;
rt->rt_iif = 0;
rt->rt_pmtu = 0;
rt->rt_gateway = 0;
rt->rt_uses_gateway = 0;
rt->rt_table_id = 0;
INIT_LIST_HEAD(&rt->rt_uncached);
rt->dst.output = ip_output;
if (flags & RTCF_LOCAL)
rt->dst.input = ip_local_deliver;
}
return rt;
}
最終返回struct rtable實例,第一部分也就完成了,知道了怎麼發。
(2)第二部分,就是準備IP層的頭,往裏面填充內容。這就要對着IP層的頭的格式進行理解,如下圖所示:
在這裏面服務類型設置爲tos,標識位裏面設置是否允許分片frag_off,如果不允許而遇到MTU太小過不去的情況,就發送ICMP報錯。TTL是這個包的存活時間,爲了防止一個IP包迷路以後一直存活下去,每經過一個路由器TTL都減一,減爲零則被丟棄。設置protocol指的是更上層的協議,這裏是TCP。源地址和目標地址由ip_copy_addrs設置。最後設置options。
(3)第三部分,就是調用ip_local_out發送IP包,如下所示:
int ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
int err;
err = __ip_local_out(net, sk, skb);
if (likely(err == 1))
err = dst_output(net, sk, skb);
return err;
}
int __ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct iphdr *iph = ip_hdr(skb);
iph->tot_len = htons(skb->len);
skb->protocol = htons(ETH_P_IP);
return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT,
net, sk, skb, NULL, skb_dst(skb)->dev,
dst_output);
}
ip_local_out先是調用__ip_local_out,然後裏面調用了nf_hook。這是什麼呢?nf的意思是Netfilter,這是Linux內核的一個機制,用於在網絡發送和轉發的關鍵節點上加上hook函數,這些函數可以截獲數據包,對數據包進行干預。一個著名的實現就是內核模塊ip_tables,在用戶態還有一個客戶端程序iptables,用該命令行來干預內核的規則,如下圖所示:
iptables有表和鏈的概念,最重要的是兩個表:
a. filter表處理過濾功能,主要包含以下三個鏈:INPUT鏈過濾所有目標地址是本機的數據包;FORWARD鏈過濾所有路過本機的數據包;OUTPUT鏈過濾所有由本機產生的數據包。
b. nat表主要處理網絡地址轉換,可以進行SNAT(改變源地址)、DNAT(改變目標地址),包含以下三個鏈:PREROUTING鏈可以在數據包到達時改變目標地址;OUTPUT 鏈可以改變本地產生的數據包的目標地址;POSTROUTING 鏈在數據包離開時改變數據包的源地址。改變的方式如下圖所示:
在這裏網絡包馬上就要發出去了,因而是NF_INET_LOCAL_OUT即ouput鏈,如果用戶曾經在iptables裏面寫過某些規則,就會在nf_hook這個函數裏面起作用。ip_local_out再調用dst_output,就是真正的發送數據,如下所示:
/* Output packet to network from transport. */
static inline int dst_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
return skb_dst(skb)->output(net, sk, skb);
}
這裏調用的就是struct rtable成員dst的ouput函數。在rt_dst_alloc中可以看到,output函數指向的是ip_output,如下所示:
int ip_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct net_device *dev = skb_dst(skb)->dev;
skb->dev = dev;
skb->protocol = htons(ETH_P_IP);
return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,
net, sk, skb, NULL, dev,
ip_finish_output,
!(IPCB(skb)->flags & IPSKB_REROUTED));
}
在ip_output裏面又看到了熟悉的NF_HOOK,這一次是NF_INET_POST_ROUTING即POSTROUTING鏈,處理完之後調用ip_finish_output。
20. 從ip_finish_output函數開始,發送網絡包的邏輯由第三層到達第二層。ip_finish_output最終調用ip_finish_output2,如下所示:
static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct dst_entry *dst = skb_dst(skb);
struct rtable *rt = (struct rtable *)dst;
struct net_device *dev = dst->dev;
unsigned int hh_len = LL_RESERVED_SPACE(dev);
struct neighbour *neigh;
u32 nexthop;
......
nexthop = (__force u32) rt_nexthop(rt, ip_hdr(skb)->daddr);
neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
if (unlikely(!neigh))
neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);
if (!IS_ERR(neigh)) {
int res;
sock_confirm_neigh(skb, neigh);
res = neigh_output(neigh, skb);
return res;
}
......
}
在ip_finish_output2中,先找到struct rtable路由表裏面的下一跳,下一跳一定和本機在同一個局域網中,可以通過二層進行通信,因而通過__ipv4_neigh_lookup_noref,查找如何通過二層訪問下一跳:
static inline struct neighbour *__ipv4_neigh_lookup_noref(struct net_device *dev, u32 key)
{
return ___neigh_lookup_noref(&arp_tbl, neigh_key_eq32, arp_hashfn, &key, dev);
}
__ipv4_neigh_lookup_noref是從本地的ARP表中查找下一跳的MAC地址。ARP表的定義如下:
struct neigh_table arp_tbl = {
.family = AF_INET,
.key_len = 4,
.protocol = cpu_to_be16(ETH_P_IP),
.hash = arp_hash,
.key_eq = arp_key_eq,
.constructor = arp_constructor,
.proxy_redo = parp_redo,
.id = "arp_cache",
......
.gc_interval = 30 * HZ,
.gc_thresh1 = 128,
.gc_thresh2 = 512,
.gc_thresh3 = 1024,
};
如果在ARP表中沒有找到相應的項,則調用__neigh_create進行創建ARP項,如下所示:
struct neighbour *__neigh_create(struct neigh_table *tbl, const void *pkey, struct net_device *dev, bool want_ref)
{
u32 hash_val;
int key_len = tbl->key_len;
int error;
struct neighbour *n1, *rc, *n = neigh_alloc(tbl, dev);
struct neigh_hash_table *nht;
memcpy(n->primary_key, pkey, key_len);
n->dev = dev;
dev_hold(dev);
/* Protocol specific setup. */
if (tbl->constructor && (error = tbl->constructor(n)) < 0) {
......
}
......
if (atomic_read(&tbl->entries) > (1 << nht->hash_shift))
nht = neigh_hash_grow(tbl, nht->hash_shift + 1);
hash_val = tbl->hash(pkey, dev, nht->hash_rnd) >> (32 - nht->hash_shift);
for (n1 = rcu_dereference_protected(nht->hash_buckets[hash_val],
lockdep_is_held(&tbl->lock));
n1 != NULL;
n1 = rcu_dereference_protected(n1->next,
lockdep_is_held(&tbl->lock))) {
if (dev == n1->dev && !memcmp(n1->primary_key, pkey, key_len)) {
if (want_ref)
neigh_hold(n1);
rc = n1;
goto out_tbl_unlock;
}
}
......
rcu_assign_pointer(n->next,
rcu_dereference_protected(nht->hash_buckets[hash_val],
lockdep_is_held(&tbl->lock)));
rcu_assign_pointer(nht->hash_buckets[hash_val], n);
......
}
__neigh_create先調用neigh_alloc創建一個struct neighbour結構,用於維護MAC地址和ARP相關的信息。大家都是在一個局域網裏面,可以通過MAC地址訪問到,當然是鄰居了,如下所示:
static struct neighbour *neigh_alloc(struct neigh_table *tbl, struct net_device *dev)
{
struct neighbour *n = NULL;
unsigned long now = jiffies;
int entries;
......
n = kzalloc(tbl->entry_size + dev->neigh_priv_len, GFP_ATOMIC);
if (!n)
goto out_entries;
__skb_queue_head_init(&n->arp_queue);
rwlock_init(&n->lock);
seqlock_init(&n->ha_lock);
n->updated = n->used = now;
n->nud_state = NUD_NONE;
n->output = neigh_blackhole;
seqlock_init(&n->hh.hh_lock);
n->parms = neigh_parms_clone(&tbl->parms);
setup_timer(&n->timer, neigh_timer_handler, (unsigned long)n);
NEIGH_CACHE_STAT_INC(tbl, allocs);
n->tbl = tbl;
refcount_set(&n->refcnt, 1);
n->dead = 1;
......
}
在neigh_alloc中,先分配一個struct neighbour結構並且初始化。這裏面比較重要的有兩個成員,一個是arp_queue,即上層想通過ARP獲取MAC地址的任務,都放在這個隊列裏面。另一個是timer定時器,設置成過一段時間就調用neigh_timer_handler,來處理這些ARP任務。__neigh_create然後調用了arp_tbl的constructor函數,即調用了arp_constructor,在這裏面定義了ARP的操作arp_hh_ops,如下所示:
static int arp_constructor(struct neighbour *neigh)
{
__be32 addr = *(__be32 *)neigh->primary_key;
struct net_device *dev = neigh->dev;
struct in_device *in_dev;
struct neigh_parms *parms;
......
neigh->type = inet_addr_type_dev_table(dev_net(dev), dev, addr);
parms = in_dev->arp_parms;
__neigh_parms_put(neigh->parms);
neigh->parms = neigh_parms_clone(parms);
......
neigh->ops = &arp_hh_ops;
......
neigh->output = neigh->ops->output;
......
}
static const struct neigh_ops arp_hh_ops = {
.family = AF_INET,
.solicit = arp_solicit,
.error_report = arp_error_report,
.output = neigh_resolve_output,
.connected_output = neigh_resolve_output,
};
21. 上面__neigh_create最後是將創建的struct neighbour結構放入一個哈希表,從前面的代碼邏輯容易看出,這是一個數組加鏈表的鏈式哈希表,先計算出哈希值hash_val得到相應的鏈表,然後循環這個鏈表找到對應的項,如果找不到就在最後插入一項。回到ip_finish_output2,在__neigh_create之後,會調用neigh_output發送網絡包,如下所示:
static inline int neigh_output(struct neighbour *n, struct sk_buff *skb)
{
......
return n->output(n, skb);
}
按照上面對於struct neighbour的操作函數arp_hh_ops的定義,output調用的是neigh_resolve_output,如下所示:
int neigh_resolve_output(struct neighbour *neigh, struct sk_buff *skb)
{
if (!neigh_event_send(neigh, skb)) {
......
rc = dev_queue_xmit(skb);
}
......
}
在neigh_resolve_output裏面,首先neigh_event_send觸發一個事件看能否激活ARP,如下所示:
int __neigh_event_send(struct neighbour *neigh, struct sk_buff *skb)
{
int rc;
bool immediate_probe = false;
if (!(neigh->nud_state & (NUD_STALE | NUD_INCOMPLETE))) {
if (NEIGH_VAR(neigh->parms, MCAST_PROBES) +
NEIGH_VAR(neigh->parms, APP_PROBES)) {
unsigned long next, now = jiffies;
atomic_set(&neigh->probes,
NEIGH_VAR(neigh->parms, UCAST_PROBES));
neigh->nud_state = NUD_INCOMPLETE;
neigh->updated = now;
next = now + max(NEIGH_VAR(neigh->parms, RETRANS_TIME),
HZ/2);
neigh_add_timer(neigh, next);
immediate_probe = true;
}
......
} else if (neigh->nud_state & NUD_STALE) {
neigh_dbg(2, "neigh %p is delayed\n", neigh);
neigh->nud_state = NUD_DELAY;
neigh->updated = jiffies;
neigh_add_timer(neigh, jiffies +
NEIGH_VAR(neigh->parms, DELAY_PROBE_TIME));
}
if (neigh->nud_state == NUD_INCOMPLETE) {
if (skb) {
.......
__skb_queue_tail(&neigh->arp_queue, skb);
neigh->arp_queue_len_Bytes += skb->truesize;
}
rc = 1;
}
out_unlock_bh:
if (immediate_probe)
neigh_probe(neigh);
.......
}
在__neigh_event_send中,激活 ARP 分兩種情況,第一種情況是馬上激活即immediate_probe。另一種情況是延遲激活則僅僅設置一個timer,到時機了再激活。然後將ARP包放在arp_queue上,如果馬上激活就直接調用neigh_probe;如果延遲激活,則定時器到了就會觸發neigh_timer_handler,在這裏面還是會調用neigh_probe。來看neigh_probe的實現,在這裏面會從arp_queue中拿出ARP包來,然後調用struct neighbour的solicit操作,如下所示:
static void neigh_probe(struct neighbour *neigh)
__releases(neigh->lock)
{
struct sk_buff *skb = skb_peek_tail(&neigh->arp_queue);
......
if (neigh->ops->solicit)
neigh->ops->solicit(neigh, skb);
......
}
按照上面對於struct neighbour的操作函數arp_hh_ops的定義,solicit調用的是arp_solicit,在這裏可以找到對於arp_send_dst的調用,創建併發送一個arp包,得到結果放在struct dst_entry裏面,如下所示:
static void arp_send_dst(int type, int ptype, __be32 dest_ip,
struct net_device *dev, __be32 src_ip,
const unsigned char *dest_hw,
const unsigned char *src_hw,
const unsigned char *target_hw,
struct dst_entry *dst)
{
struct sk_buff *skb;
......
skb = arp_create(type, ptype, dest_ip, dev, src_ip,
dest_hw, src_hw, target_hw);
......
skb_dst_set(skb, dst_clone(dst));
arp_xmit(skb);
}
22. 再回到上面neigh_resolve_output中,當ARP發送完畢(知道MAC地址了),就可以調用dev_queue_xmit發送二層網絡包了,如下所示:
/**
* __dev_queue_xmit - transmit a buffer
* @skb: buffer to transmit
* @accel_priv: private data used for L2 forwarding offload
*
* Queue a buffer for transmission to a network device.
*/
static int __dev_queue_xmit(struct sk_buff *skb, void *accel_priv)
{
struct net_device *dev = skb->dev;
struct netdev_queue *txq;
struct Qdisc *q;
......
txq = netdev_pick_tx(dev, skb, accel_priv);
q = rcu_dereference_bh(txq->qdisc);
if (q->enqueue) {
rc = __dev_xmit_skb(skb, q, dev, txq);
goto out;
}
......
}
就像硬盤塊設備,每個塊設備都有隊列用於將內核的數據放到隊列裏面,然後設備驅動從隊列裏面取出後,將數據根據具體設備的特性發送給設備。網絡設備也是類似的,對於發送來說有一個發送隊列struct netdev_queue *txq,這裏還有另一個變量叫做struct Qdisc,它的意思是如果在一臺Linux機器上運行ip addr,能看到對於一個網卡都有下面的輸出:
# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1400 qdisc pfifo_fast state UP group default qlen 1000
link/ether fa:16:3e:75:99:08 brd ff:ff:ff:ff:ff:ff
inet 10.173.32.47/21 brd 10.173.39.255 scope global noprefixroute dynamic eth0
valid_lft 67104sec preferred_lft 67104sec
inet6 fe80::f816:3eff:fe75:9908/64 scope link
valid_lft forever preferred_lft forever
這裏面有個關鍵字qdisc pfifo_fast是什麼意思呢?qdisc全稱是queueing discipline叫排隊規則,內核如果需要通過某個網絡接口發送數據包,都需要按照爲這個接口配置的qdisc(排隊規則)把數據包加入隊列。最簡單的qdisc是pfifo,它不對進入的數據包做任何處理,數據包採用先入先出的方式通過隊列。pfifo_fast稍複雜一些,它的隊列包括三個波段(band),在每個波段裏面使用先進先出規則。
三個波段的優先級也不相同。band 0的優先級最高,band 2的最低。如果band 0裏面有數據包,系統就不會處理band 1裏面的數據包,band 1和band 2之間也是一樣。數據包是按照服務類型(Type of Service,TOS)被分配到三個波段裏面的。TOS是IP頭裏面的一個字段,代表了當前的包是高優先級的還是低優先級的。pfifo_fast分爲三個先入先出的隊列,稱爲三個Band。根據網絡包裏面的TOS,看這個包到底應該進入哪個隊列。TOS總共四位,每一位表示的意思不同,總共十六種類型,如下圖所示:
通過命令行tc qdisc show dev eth0可以輸出結果priomap,也是十六個數字,在0到2之間,和TOS的十六種類型對應起來。不同的TOS對應不同的隊列。其中Band 0優先級最高,發送完畢後才輪到Band 1發送,最後纔是Band 2,如下所示:
# tc qdisc show dev eth0
qdisc pfifo_fast 0: root refcnt 2 bands 3 priomap 1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
接下來,__dev_xmit_skb開始進行網絡包發送,如下所示:
static inline int __dev_xmit_skb(struct sk_buff *skb, struct Qdisc *q,
struct net_device *dev,
struct netdev_queue *txq)
{
......
rc = q->enqueue(skb, q, &to_free) & NET_XMIT_MASK;
if (qdisc_run_begin(q)) {
......
__qdisc_run(q);
}
......
}
void __qdisc_run(struct Qdisc *q)
{
int quota = dev_tx_weight;
int packets;
while (qdisc_restart(q, &packets)) {
/*
* Ordered by possible occurrence: Postpone processing if
* 1. we've exceeded packet quota
* 2. another process needs the CPU;
*/
quota -= packets;
if (quota <= 0 || need_resched()) {
__netif_schedule(q);
break;
}
}
qdisc_run_end(q);
}
__dev_xmit_skb會將請求放入隊列,然後調用__qdisc_run處理隊列中的數據。qdisc_restart用於數據的發送,這段註釋很重要,qdisc的另一個功能是用於控制網絡包的發送速度,因而如果超過速度就需要重新調度,則會調用__netif_schedule,__netif_schedule又會調用__netif_reschedule,如下所示:
static void __netif_reschedule(struct Qdisc *q)
{
struct softnet_data *sd;
unsigned long flags;
local_irq_save(flags);
sd = this_cpu_ptr(&softnet_data);
q->next_sched = NULL;
*sd->output_queue_tailp = q;
sd->output_queue_tailp = &q->next_sched;
raise_softirq_irqoff(NET_TX_SOFTIRQ);
local_irq_restore(flags);
}
這裏會發起一個軟中斷NET_TX_SOFTIRQ。之前提到設備驅動程序時說過,設備驅動程序處理中斷分兩個過程,一個是屏蔽中斷的關鍵處理邏輯,一個是延遲處理邏輯,工作隊列是延遲處理邏輯的處理方案,軟中斷也是一種方案。在系統初始化時,會定義軟中斷的處理函數,例如NET_TX_SOFTIRQ的處理函數是net_tx_action,用於發送網絡包。還有一個NET_RX_SOFTIRQ的處理函數是net_rx_action,用於接收網絡包,如下所示:
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
23. 這裏來解析一下net_tx_action,如下所示:
static __latent_entropy void net_tx_action(struct softirq_action *h)
{
struct softnet_data *sd = this_cpu_ptr(&softnet_data);
......
if (sd->output_queue) {
struct Qdisc *head;
local_irq_disable();
head = sd->output_queue;
sd->output_queue = NULL;
sd->output_queue_tailp = &sd->output_queue;
local_irq_enable();
while (head) {
struct Qdisc *q = head;
spinlock_t *root_lock;
head = head->next_sched;
......
qdisc_run(q);
}
}
}
會發現net_tx_action還是調用了qdisc_run,然後會調用__qdisc_run,再調用qdisc_restart發送網絡包。來看一下qdisc_restart的實現,如下所示:
static inline int qdisc_restart(struct Qdisc *q, int *packets)
{
struct netdev_queue *txq;
struct net_device *dev;
spinlock_t *root_lock;
struct sk_buff *skb;
bool validate;
/* Dequeue packet */
skb = dequeue_skb(q, &validate, packets);
if (unlikely(!skb))
return 0;
root_lock = qdisc_lock(q);
dev = qdisc_dev(q);
txq = skb_get_tx_queue(dev, skb);
return sch_direct_xmit(skb, q, dev, txq, root_lock, validate);
}
qdisc_restart將網絡包從Qdisc的隊列中拿下來,然後調用sch_direct_xmit進行發送,如下所示:
int sch_direct_xmit(struct sk_buff *skb, struct Qdisc *q,
struct net_device *dev, struct netdev_queue *txq,
spinlock_t *root_lock, bool validate)
{
int ret = NETDEV_TX_BUSY;
if (likely(skb)) {
if (!netif_xmit_frozen_or_stopped(txq))
skb = dev_hard_start_xmit(skb, dev, txq, &ret);
}
......
if (dev_xmit_complete(ret)) {
/* Driver sent out skb successfully or skb was consumed */
ret = qdisc_qlen(q);
} else {
/* Driver returned NETDEV_TX_BUSY - requeue skb */
ret = dev_requeue_skb(skb, q);
}
......
}
在sch_direct_xmit中,調用dev_hard_start_xmit進行發送,如果發送不成功會返回NETDEV_TX_BUSY,這說明網卡很忙,於是就調用dev_requeue_skb重新放入隊列。dev_hard_start_xmit的實現如下所示:
struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev, struct netdev_queue *txq, int *ret)
{
struct sk_buff *skb = first;
int rc = NETDEV_TX_OK;
while (skb) {
struct sk_buff *next = skb->next;
rc = xmit_one(skb, dev, txq, next != NULL);
skb = next;
if (netif_xmit_stopped(txq) && skb) {
rc = NETDEV_TX_BUSY;
break;
}
}
......
}
在dev_hard_start_xmit中是一個 while 循環,每次在隊列中取出一個sk_buff,調用xmit_one發送。接下來的調用鏈爲:xmit_one->netdev_start_xmit->__netdev_start_xmit,如下所示:
static inline netdev_tx_t __netdev_start_xmit(const struct net_device_ops *ops, struct sk_buff *skb, struct net_device *dev, bool more)
{
skb->xmit_more = more ? 1 : 0;
return ops->ndo_start_xmit(skb, dev);
}
這個時候已經到了設備驅動層了,能看到drivers/net/ethernet/intel/ixgb/ixgb_main.c裏面有對於這個網卡的操作定義,如下所示:
static const struct net_device_ops ixgb_netdev_ops = {
.ndo_open = ixgb_open,
.ndo_stop = ixgb_close,
.ndo_start_xmit = ixgb_xmit_frame,
.ndo_set_rx_mode = ixgb_set_multi,
.ndo_validate_addr = eth_validate_addr,
.ndo_set_mac_address = ixgb_set_mac,
.ndo_change_mtu = ixgb_change_mtu,
.ndo_tx_timeout = ixgb_tx_timeout,
.ndo_vlan_rx_add_vid = ixgb_vlan_rx_add_vid,
.ndo_vlan_rx_kill_vid = ixgb_vlan_rx_kill_vid,
.ndo_fix_features = ixgb_fix_features,
.ndo_set_features = ixgb_set_features,
};
在這裏面可以找到對於ndo_start_xmit的定義,即調用ixgb_xmit_frame,如下所示:
static netdev_tx_t
ixgb_xmit_frame(struct sk_buff *skb, struct net_device *netdev)
{
struct ixgb_adapter *adapter = netdev_priv(netdev);
......
if (count) {
ixgb_tx_queue(adapter, count, vlan_id, tx_flags);
/* Make sure there is space in the ring for the next send. */
ixgb_maybe_stop_tx(netdev, &adapter->tx_ring, DESC_NEEDED);
}
......
return NETDEV_TX_OK;
}
在ixgb_xmit_frame中會得到這個網卡對應的適配器(adapter),然後將包放入硬件網卡的隊列中。至此整個發送纔算結束。
24. 上面接着解析了發送一個網絡包的過程,整個發送過程的總結如下所示:
(1)VFS層:write系統調用找到struct file,根據裏面file_operations的定義調用sock_write_iter函數,sock_write_iter 函數調用sock_sendmsg函數。
(2)Socket層:從struct file裏面的private_data得到struct socket,根據裏面ops的定義調用inet_sendmsg函數。
(3)Sock層:從struct socket裏面的sk得到struct sock,根據裏面sk_prot的定義調用tcp_sendmsg函數。、
(4)TCP層:tcp_sendmsg函數會調用tcp_write_xmit函數,tcp_write_xmit函數會調用tcp_transmit_skb,在這裏實現了TCP層面向連接的邏輯。
(5)IP層:擴展struct sock得到struct inet_connection_sock,根據裏面icsk_af_ops的定義調用ip_queue_xmit函數。然後ip_route_output_ports函數裏面會調用fib_lookup查找FIB路由表。在IP層裏要做的另外兩個事情是填寫IP層的頭,和通過iptables規則。
(6)MAC層:IP層調用ip_finish_output進入MAC層。MAC層需要ARP獲得MAC地址,因而要調用__neigh_lookup_noref查找屬於同一個網段的鄰居,它會調用neigh_probe發送 ARP。有了MAC地址,就可以調用dev_queue_xmit發送二層網絡包了,它會調用__dev_xmit_skb會將請求放入隊列。
(7)設備層:網絡包的發送會觸發一個軟中斷NET_TX_SOFTIRQ來處理隊列中的數據,這個軟中斷的處理函數是net_tx_action。在軟中斷處理函數中會將網絡包從隊列上拿下來,調用網絡設備的傳輸函數ixgb_xmit_frame,將網絡包發到設備的隊列上去。
五、接收網絡包
25. 如果說網絡包的發送是從應用層開始層層調用,一直到網卡驅動程序的話,網絡包的接收過程就是一個反過來的過程,不能從應用層的讀取開始,而應該從網卡接收到一個網絡包開始。這裏先從硬件網卡解析到IP層,後面再從IP層解析到Socket層。
網卡作爲一個硬件接收到網絡包,應該怎麼通知操作系統這個網絡包到達了呢?雖然可以觸發一箇中斷。但是這裏有個問題,就是網絡包的到來往往是很難預期的。網絡吞吐量比較大的時候,網絡包的到達會十分頻繁。這個時候如果非常頻繁地去觸發中斷,會導致這樣的後果:比如CPU正在做某個事情,一些網絡包來了觸發了中斷,CPU停下手裏的事情去處理這些網絡包,處理完畢按照中斷處理的邏輯,應該回去繼續處理其他事情;這個時候另一些網絡包又來了又觸發了中斷,CPU手裏的事情還沒捂熱,又要停下來去處理網絡包。
因此必須另想辦法,可以有一種機制,就是當一些網絡包到來觸發了中斷,內核處理完這些網絡包之後,可以先進入主動輪詢poll網卡的方式,主動去接收到來的網絡包。如果一直有就一直處理,等處理告一段落,就返回幹其他的事情。當再有下一批網絡包到來的時候,再中斷再輪詢poll。這樣就會大大減少中斷的數量,提升網絡處理的效率,這種處理方式稱爲NAPI。
爲了瞭解設備驅動層的工作機制,還是以上面發送網絡包時的網卡的drivers/net/ethernet/intel/ixgb/ixgb_main.c爲例子進行解析:
static struct pci_driver ixgb_driver = {
.name = ixgb_driver_name,
.id_table = ixgb_pci_tbl,
.probe = ixgb_probe,
.remove = ixgb_remove,
.err_handler = &ixgb_err_handler
};
MODULE_AUTHOR("Intel Corporation, <[email protected]>");
MODULE_DESCRIPTION("Intel(R) PRO/10GbE Network Driver");
MODULE_LICENSE("GPL");
MODULE_VERSION(DRV_VERSION);
/**
* ixgb_init_module - Driver Registration Routine
*
* ixgb_init_module is the first routine called when the driver is
* loaded. All it does is register with the PCI subsystem.
**/
static int __init
ixgb_init_module(void)
{
pr_info("%s - version %s\n", ixgb_driver_string, ixgb_driver_version);
pr_info("%s\n", ixgb_copyright);
return pci_register_driver(&ixgb_driver);
}
module_init(ixgb_init_module);
在網卡驅動程序初始化的時候會調用ixgb_init_module,註冊一個驅動ixgb_driver,並且調用它的probe函數ixgb_probe,如下所示:
static int
ixgb_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
struct net_device *netdev = NULL;
struct ixgb_adapter *adapter;
......
netdev = alloc_etherdev(sizeof(struct ixgb_adapter));
SET_NETDEV_DEV(netdev, &pdev->dev);
pci_set_drvdata(pdev, netdev);
adapter = netdev_priv(netdev);
adapter->netdev = netdev;
adapter->pdev = pdev;
adapter->hw.back = adapter;
adapter->msg_enable = netif_msg_init(debug, DEFAULT_MSG_ENABLE);
adapter->hw.hw_addr = pci_ioremap_bar(pdev, BAR_0);
......
netdev->netdev_ops = &ixgb_netdev_ops;
ixgb_set_ethtool_ops(netdev);
netdev->watchdog_timeo = 5 * HZ;
netif_napi_add(netdev, &adapter->napi, ixgb_clean, 64);
strncpy(netdev->name, pci_name(pdev), sizeof(netdev->name) - 1);
adapter->bd_number = cards_found;
adapter->link_speed = 0;
adapter->link_duplex = 0;
......
}
在ixgb_probe中會創建一個struct net_device表示這個網絡設備,並且netif_napi_add函數爲這個網絡設備註冊一個輪詢poll函數ixgb_clean,將來一旦出現網絡包的時候就是要通過它來輪詢了。當一個網卡被激活的時候,會調用函數ixgb_open->ixgb_up,在這裏面註冊一個硬件的中斷處理函數,如下所示:
int
ixgb_up(struct ixgb_adapter *adapter)
{
struct net_device *netdev = adapter->netdev;
......
err = request_irq(adapter->pdev->irq, ixgb_intr, irq_flags,
netdev->name, netdev);
......
}
/**
* ixgb_intr - Interrupt Handler
* @irq: interrupt number
* @data: pointer to a network interface device structure
**/
static irqreturn_t
ixgb_intr(int irq, void *data)
{
struct net_device *netdev = data;
struct ixgb_adapter *adapter = netdev_priv(netdev);
struct ixgb_hw *hw = &adapter->hw;
......
if (napi_schedule_prep(&adapter->napi)) {
IXGB_WRITE_REG(&adapter->hw, IMC, ~0);
__napi_schedule(&adapter->napi);
}
return IRQ_HANDLED;
}
如果一個網絡包到來觸發了硬件中斷,就會調用ixgb_intr,這裏面會調用__napi_schedule,如下所示:
/**
* __napi_schedule - schedule for receive
* @n: entry to schedule
*
* The entry's receive function will be scheduled to run.
* Consider using __napi_schedule_irqoff() if hard irqs are masked.
*/
void __napi_schedule(struct napi_struct *n)
{
unsigned long flags;
local_irq_save(flags);
____napi_schedule(this_cpu_ptr(&softnet_data), n);
local_irq_restore(flags);
}
static inline void ____napi_schedule(struct softnet_data *sd,
struct napi_struct *napi)
{
list_add_tail(&napi->poll_list, &sd->poll_list);
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
__napi_schedule是處於中斷處理的關鍵部分,在它被調用的時候中斷是暫時關閉的,但是處理網絡包是個複雜的過程,需要到延遲處理部分,所以____napi_schedule將當前設備放到struct softnet_data結構的poll_list裏面,說明在延遲處理部分可以接着處理這個poll_list裏面的網絡設備。然後____napi_schedule觸發一個軟中斷NET_RX_SOFTIRQ,通過軟中斷觸發中斷處理的延遲處理部分,也是常用的手段。
26. 上面提到過,軟中斷NET_RX_SOFTIRQ對應的中斷處理函數是net_rx_action,如下所示:
static __latent_entropy void net_rx_action(struct softirq_action *h)
{
struct softnet_data *sd = this_cpu_ptr(&softnet_data);
LIST_HEAD(list);
list_splice_init(&sd->poll_list, &list);
......
for (;;) {
struct napi_struct *n;
......
n = list_first_entry(&list, struct napi_struct, poll_list);
budget -= napi_poll(n, &repoll);
}
......
}
在net_rx_action中會得到struct softnet_data結構,這個結構在發送的時候也遇到過,當時它的output_queue用於網絡包的發送,這裏的poll_list用於網絡包的接收。softnet_data結構如下所示:
struct softnet_data {
struct list_head poll_list;
......
struct Qdisc *output_queue;
struct Qdisc **output_queue_tailp;
......
}
在net_rx_action中接下來是一個循環,在poll_list裏面取出網絡包到達的設備,然後調用napi_poll來輪詢這些設備,napi_poll會調用最初設備初始化時註冊的poll函數,對於ixgb_driver對應的函數是ixgb_clean,ixgb_clean會調用ixgb_clean_rx_irq,如下所示:
static bool
ixgb_clean_rx_irq(struct ixgb_adapter *adapter, int *work_done, int work_to_do)
{
struct ixgb_desc_ring *rx_ring = &adapter->rx_ring;
struct net_device *netdev = adapter->netdev;
struct pci_dev *pdev = adapter->pdev;
struct ixgb_rx_desc *rx_desc, *next_rxd;
struct ixgb_buffer *buffer_info, *next_buffer, *next2_buffer;
u32 length;
unsigned int i, j;
int cleaned_count = 0;
bool cleaned = false;
i = rx_ring->next_to_clean;
rx_desc = IXGB_RX_DESC(*rx_ring, i);
buffer_info = &rx_ring->buffer_info[i];
while (rx_desc->status & IXGB_RX_DESC_STATUS_DD) {
struct sk_buff *skb;
u8 status;
status = rx_desc->status;
skb = buffer_info->skb;
buffer_info->skb = NULL;
prefetch(skb->data - NET_IP_ALIGN);
if (++i == rx_ring->count)
i = 0;
next_rxd = IXGB_RX_DESC(*rx_ring, i);
prefetch(next_rxd);
j = i + 1;
if (j == rx_ring->count)
j = 0;
next2_buffer = &rx_ring->buffer_info[j];
prefetch(next2_buffer);
next_buffer = &rx_ring->buffer_info[i];
......
length = le16_to_cpu(rx_desc->length);
rx_desc->length = 0;
......
ixgb_check_copybreak(&adapter->napi, buffer_info, length, &skb);
/* Good Receive */
skb_put(skb, length);
/* Receive Checksum Offload */
ixgb_rx_checksum(adapter, rx_desc, skb);
skb->protocol = eth_type_trans(skb, netdev);
netif_receive_skb(skb);
......
/* use prefetched values */
rx_desc = next_rxd;
buffer_info = next_buffer;
}
rx_ring->next_to_clean = i;
......
}
在網絡設備的驅動層有一個用於接收網絡包的rx_ring,它是一個環,從網卡硬件接收的包會放在這個環裏面。這個環裏面的buffer_info[]是一個數組,存放的是網絡包的內容,i和j是這個數組的下標,在ixgb_clean_rx_irq裏面的while循環中,依次處理環裏面的數據,在這裏面看到了i和j加一之後,如果超過了數組的大小就跳回下標0,就說明這是一個環。ixgb_check_copybreak函數將buffer_info裏面的內容拷貝到struct sk_buff *skb,從而可以作爲一個網絡包進行後續的處理,然後調用netif_receive_skb。
27. 從netif_receive_skb函數開始,就進入了內核的網絡協議棧。接下來的調用鏈爲:netif_receive_skb->netif_receive_skb_internal->__netif_receive_skb->__netif_receive_skb_core。在__netif_receive_skb_core中先是處理了二層的一些邏輯,例如對於VLAN的處理,接下來要想辦法交給第三層,如下所示:
static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
struct packet_type *ptype, *pt_prev;
......
type = skb->protocol;
......
deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
&orig_dev->ptype_specific);
if (pt_prev) {
ret = pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
}
......
}
static inline void deliver_ptype_list_skb(struct sk_buff *skb,
struct packet_type **pt,
struct net_device *orig_dev,
__be16 type,
struct list_head *ptype_list)
{
struct packet_type *ptype, *pt_prev = *pt;
list_for_each_entry_rcu(ptype, ptype_list, list) {
if (ptype->type != type)
continue;
if (pt_prev)
deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
*pt = pt_prev;
}
在網絡包struct sk_buff裏面,二層的頭裏面有一個protocol表示裏面一層,即三層是什麼協議。deliver_ptype_list_skb在一個協議列表中逐個匹配,如果能夠匹配到就返回。這些協議的註冊在網絡協議棧初始化的時候, inet_init函數調用dev_add_pack(&ip_packet_type)添加IP協議,協議被放在一個鏈表裏面,如下所示:
void dev_add_pack(struct packet_type *pt)
{
struct list_head *head = ptype_head(pt);
list_add_rcu(&pt->list, head);
}
static inline struct list_head *ptype_head(const struct packet_type *pt)
{
if (pt->type == htons(ETH_P_ALL))
return pt->dev ? &pt->dev->ptype_all : &ptype_all;
else
return pt->dev ? &pt->dev->ptype_specific : &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}
假設這個時候的網絡包是一個IP包,則在這個鏈表裏面一定能夠找到ip_packet_type,在__netif_receive_skb_core中會調用ip_packet_type的func函數,如下所示:
static struct packet_type ip_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_IP),
.func = ip_rcv,
};
從上面的定義可以看出,接下來ip_rcv會被調用。
28. 從ip_rcv函數開始,處理邏輯就從二層到了三層即IP層,如下所示:
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
const struct iphdr *iph;
struct net *net;
u32 len;
......
net = dev_net(dev);
......
iph = ip_hdr(skb);
len = ntohs(iph->tot_len);
skb->transport_header = skb->network_header + iph->ihl*4;
......
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
net, NULL, skb, dev, NULL,
ip_rcv_finish);
......
}
在ip_rcv中得到IP頭,然後又遇到了見過多次的NF_HOOK,這次因爲是接收網絡包,第一個hook點是NF_INET_PRE_ROUTING,也就是iptables的PREROUTING鏈。如果裏面有規則則執行規則,然後調用ip_rcv_finish,如下所示:
static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
const struct iphdr *iph = ip_hdr(skb);
struct net_device *dev = skb->dev;
struct rtable *rt;
int err;
......
rt = skb_rtable(skb);
.....
return dst_input(skb);
}
static inline int dst_input(struct sk_buff *skb)
{
return skb_dst(skb)->input(skb);
ip_rcv_finish得到網絡包對應的路由表然後調用dst_input,在dst_input中調用的是struct rtable成員的dst的input函數。在rt_dst_alloc中可以看到,input函數指向的是ip_local_deliver,如下所示:
int ip_local_deliver(struct sk_buff *skb)
{
/*
* Reassemble IP fragments.
*/
struct net *net = dev_net(skb->dev);
if (ip_is_fragment(ip_hdr(skb))) {
if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER))
return 0;
}
return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
net, NULL, skb, skb->dev, NULL,
ip_local_deliver_finish);
}
在ip_local_deliver函數中,如果IP層進行了分段則進行重新的組合。接下來就是熟悉的NF_HOOK,hook點在NF_INET_LOCAL_IN,對應iptables裏面的INPUT鏈,在經過iptables規則處理完畢後調用ip_local_deliver_finish,如下所示:
static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
__skb_pull(skb, skb_network_header_len(skb));
int protocol = ip_hdr(skb)->protocol;
const struct net_protocol *ipprot;
ipprot = rcu_dereference(inet_protos[protocol]);
if (ipprot) {
int ret;
ret = ipprot->handler(skb);
......
}
......
}
在IP頭中有一個字段protocol,用於指定裏面一層的協議,在這裏應該是TCP協議。於是從inet_protos數組中,找出TCP協議對應的處理函數,這個數組的定義如下,裏面的內容是struct net_protocol:
struct net_protocol __rcu *inet_protos[MAX_INET_PROTOS] __read_mostly;
int inet_add_protocol(const struct net_protocol *prot, unsigned char protocol)
{
......
return !cmpxchg((const struct net_protocol **)&inet_protos[protocol],
NULL, prot) ? 0 : -1;
}
static int __init inet_init(void)
{
......
if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
pr_crit("%s: Cannot add UDP protocol\n", __func__);
if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
pr_crit("%s: Cannot add TCP protocol\n", __func__);
......
}
static struct net_protocol tcp_protocol = {
.early_demux = tcp_v4_early_demux,
.early_demux_handler = tcp_v4_early_demux,
.handler = tcp_v4_rcv,
.err_handler = tcp_v4_err,
.no_policy = 1,
.netns_ok = 1,
.icmp_strict_tag_validation = 1,
};
static struct net_protocol udp_protocol = {
.early_demux = udp_v4_early_demux,
.early_demux_handler = udp_v4_early_demux,
.handler = udp_rcv,
.err_handler = udp_err,
.no_policy = 1,
.netns_ok = 1,
};
在系統初始化的時候,網絡協議棧的初始化調用的是inet_init,它會調用inet_add_protocol,將TCP協議對應的處理函數tcp_protocol、UDP協議對應的處理函數udp_protocol,放到inet_protos數組中。在上面的網絡包的接收過程中,會取出TCP協議對應的處理函數tcp_protocol,然後調用handler函數即tcp_v4_rcv函數。這裏IP層就結束了,後面就到傳輸層了。
29. 上面講了接收網絡包的上半部分,分以下幾個層次:
(1)硬件網卡接收到網絡包之後,通過DMA技術將網絡包放入Ring Buffer。
(2)硬件網卡通過中斷通知CPU新的網絡包的到來。
(3)網卡驅動程序會註冊中斷處理函數ixgb_intr。
(4)中斷處理函數處理完需要暫時屏蔽中斷的核心流程之後,通過軟中斷NET_RX_SOFTIRQ觸發接下來的處理過程。
(5)NET_RX_SOFTIRQ軟中斷處理函數net_rx_action,net_rx_action會調用napi_poll,進而調用ixgb_clean_rx_irq,從Ring Buffer中讀取數據到內核struct sk_buff。
(6)調用netif_receive_skb進入內核網絡協議棧,進行一些關於VLAN的二層邏輯處理後,調用ip_rcv進入第三層IP層。
(7)在IP層,會處理iptables規則,然後調用ip_local_deliver交給更上層TCP層。
(8)在TCP層調用tcp_v4_rcv。
30. 上面解析了網絡包接收的上半部分,即從硬件網卡到IP層。這裏接着來解析TCP層和Socket層都做了哪些事情。從tcp_v4_rcv函數開始,處理邏輯就從IP層到了TCP層,如下所示:
int tcp_v4_rcv(struct sk_buff *skb)
{
struct net *net = dev_net(skb->dev);
const struct iphdr *iph;
const struct tcphdr *th;
bool refcounted;
struct sock *sk;
int ret;
......
th = (const struct tcphdr *)skb->data;
iph = ip_hdr(skb);
......
TCP_SKB_CB(skb)->seq = ntohl(th->seq);
TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin + skb->len - th->doff * 4);
TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);
TCP_SKB_CB(skb)->tcp_flags = tcp_flag_byte(th);
TCP_SKB_CB(skb)->tcp_tw_isn = 0;
TCP_SKB_CB(skb)->ip_dsfield = ipv4_get_dsfield(iph);
TCP_SKB_CB(skb)->sacked = 0;
lookup:
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source, th->dest, &refcounted);
process:
if (sk->sk_state == TCP_TIME_WAIT)
goto do_time_wait;
if (sk->sk_state == TCP_NEW_SYN_RECV) {
......
}
......
th = (const struct tcphdr *)skb->data;
iph = ip_hdr(skb);
skb->dev = NULL;
if (sk->sk_state == TCP_LISTEN) {
ret = tcp_v4_do_rcv(sk, skb);
goto put_and_return;
}
......
if (!sock_owned_by_user(sk)) {
if (!tcp_prequeue(sk, skb))
ret = tcp_v4_do_rcv(sk, skb);
} else if (tcp_add_backlog(sk, skb)) {
goto discard_and_relse;
}
......
}
在tcp_v4_rcv中得到TCP的頭之後,就可以開始處理TCP層的事情。因爲TCP層是分狀態的,狀態被維護在數據結構struct sock裏面,因而要根據IP地址以及TCP頭裏面的內容,在tcp_hashinfo中找到這個包對應的struct sock,從而得到這個包對應連接的狀態,接下來就根據不同的狀態做不同的處理。例如上面代碼中的TCP_LISTEN、TCP_NEW_SYN_RECV狀態屬於連接建立過程中,再比如TCP_TIME_WAIT狀態是連接結束時的狀態,這個暫時可以不用看。
31. 接下來分析最主流的網絡包接收過程,這裏面涉及三個隊列:backlog隊、prequeue隊列和sk_receive_queue隊列。爲什麼接收網絡包的過程需要在這三個隊列裏面倒騰來去呢?這是因爲同樣一個網絡包要在三個主體之間交接:(1)第一個主體是軟中斷的處理過程。在執行tcp_v4_rcv函數的時候依然處於軟中斷的處理邏輯裏,所以必然會佔用這個軟中斷。
(2)第二個主體就是用戶態進程。如果用戶態觸發系統調用read讀取網絡包,也要從隊列裏面找。
(3)第三個主體就是內核協議棧。哪怕用戶進程沒有調用read讀取網絡包,當網絡包來的時候也得有一個地方收着。
這時候就能夠了解上面代碼中sock_owned_by_user的意思了,其實就是說當前這個sock是否正有一個用戶態進程等着讀數據,如果沒有則內核協議棧也調用tcp_add_backlog暫存在backlog隊列中,並且抓緊離開軟中斷的處理過程。
如果有一個用戶態進程等待讀取數據,就會先調用tcp_prequeue即趕緊放入prequeue隊列,並且離開軟中斷的處理過程。在這個函數裏面,會看到對於sysctl_tcp_low_latency的判斷,即是否要低時延地處理網絡包,如果把sysctl_tcp_low_latency設置爲 0,那就要放在prequeue隊列中暫存,這樣不用等待網絡包處理完畢就可以離開軟中斷的處理過程,但是會造成比較長的時延。如果把sysctl_tcp_low_latency設置爲1就會是低時延,還是會調用tcp_v4_do_rcv,如下所示:
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
struct sock *rsk;
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
struct dst_entry *dst = sk->sk_rx_dst;
......
tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len);
return 0;
}
......
if (tcp_rcv_state_process(sk, skb)) {
......
}
return 0;
......
}
在tcp_v4_do_rcv中分兩種情況,一種情況是連接已經建立處於TCP_ESTABLISHED狀態,調用tcp_rcv_established。另一種情況就是其他的狀態,調用tcp_rcv_state_process,如下所示:
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
const struct tcphdr *th = tcp_hdr(skb);
struct request_sock *req;
int queued = 0;
bool acceptable;
switch (sk->sk_state) {
case TCP_CLOSE:
......
case TCP_LISTEN:
......
case TCP_SYN_SENT:
......
}
......
switch (sk->sk_state) {
case TCP_SYN_RECV:
......
case TCP_FIN_WAIT1:
......
case TCP_CLOSING:
......
case TCP_LAST_ACK:
......
}
/* step 7: process the segment text */
switch (sk->sk_state) {
case TCP_CLOSE_WAIT:
case TCP_CLOSING:
case TCP_LAST_ACK:
......
case TCP_FIN_WAIT1:
case TCP_FIN_WAIT2:
......
case TCP_ESTABLISHED:
......
}
}
在tcp_rcv_state_process中,如果對着TCP的狀態圖進行比對,能看到對於TCP所有狀態的處理,其中和連接建立相關的狀態前面已經分析過,釋放連接相關的狀態暫不分析,這裏重點關注連接狀態下的工作模式,如下圖所示:
在連接狀態下會調用tcp_rcv_established。在這個函數裏面會調用tcp_data_queue,將其放入sk_receive_queue隊列進行處理,如下所示:
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
bool fragstolen = false;
......
if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
if (tcp_receive_window(tp) == 0)
goto out_of_window;
/* Ok. In sequence. In window. */
if (tp->ucopy.task == current &&
tp->copied_seq == tp->rcv_nxt && tp->ucopy.len &&
sock_owned_by_user(sk) && !tp->urg_data) {
int chunk = min_t(unsigned int, skb->len,
tp->ucopy.len);
__set_current_state(TASK_RUNNING);
if (!skb_copy_datagram_msg(skb, 0, tp->ucopy.msg, chunk)) {
tp->ucopy.len -= chunk;
tp->copied_seq += chunk;
eaten = (chunk == skb->len);
tcp_rcv_space_adjust(sk);
}
}
if (eaten <= 0) {
queue_and_out:
......
eaten = tcp_queue_rcv(sk, skb, 0, &fragstolen);
}
tcp_rcv_nxt_update(tp, TCP_SKB_CB(skb)->end_seq);
......
if (!RB_EMPTY_ROOT(&tp->out_of_order_queue)) {
tcp_ofo_queue(sk);
......
}
......
return;
}
if (!after(TCP_SKB_CB(skb)->end_seq, tp->rcv_nxt)) {
/* A retransmit, 2nd most common case. Force an immediate ack. */
tcp_dsack_set(sk, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq);
out_of_window:
tcp_enter_quickack_mode(sk);
inet_csk_schedule_ack(sk);
drop:
tcp_drop(sk, skb);
return;
}
/* Out of window. F.e. zero window probe. */
if (!before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt + tcp_receive_window(tp)))
goto out_of_window;
tcp_enter_quickack_mode(sk);
if (before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) {
/* Partial packet, seq < rcv_next < end_seq */
tcp_dsack_set(sk, TCP_SKB_CB(skb)->seq, tp->rcv_nxt);
/* If window is closed, drop tail of packet. But after
* remembering D-SACK for its head made in previous line.
*/
if (!tcp_receive_window(tp))
goto out_of_window;
goto queue_and_out;
}
tcp_data_queue_ofo(sk, skb);
}
在tcp_data_queue中,對於收到的網絡包要分情況進行處理。第一種情況是seq == tp->rcv_nxt,說明來的網絡包正是服務端期望的下一個網絡包,這個時候判斷sock_owned_by_user,即用戶進程也是正在等待讀取,這種情況下就直接skb_copy_datagram_msg,將網絡包拷貝給用戶進程就可以了。如果用戶進程沒有正在等待讀取,或者因爲內存原因沒有能夠拷貝成功,tcp_queue_rcv裏面還是將網絡包放入sk_receive_queue隊列。
接下來,tcp_rcv_nxt_update將tp->rcv_nxt設置爲end_seq,即當前的網絡包接收成功後,更新下一個期待的網絡包。這個時候,還會判斷一下另一個隊列out_of_order_queue,也看看亂序隊列的情況,看看亂序隊列裏面的包,會不會因爲這個新的網絡包的到來,也能放入到sk_receive_queue隊列中。
例如,客戶端發送的網絡包序號爲5、6、7、8、9。在5還沒有到達的時候,服務端的rcv_nxt應該是 5,即期望下一個網絡包是5。但是由於中間網絡通路的問題,5、6還沒到達服務端,7、8已經到達了服務端了,這就出現了亂序。亂序的包不能進入sk_receive_queue隊列,因爲一旦進入到這個隊列意味着可以發送給用戶進程。然而按照TCP的定義,用戶進程應該是按順序收到包的,沒有排好序就不能給用戶進程。
所以,7、8不能進入sk_receive_queue隊列,只能暫時放在out_of_order_queue亂序隊列中。當5、6到達的時候,5、6先進入sk_receive_queue隊列,這個時候再來看out_of_order_queue亂序隊列中的7、8,發現能夠接上,於是7、8也能進入sk_receive_queue隊列了,上面tcp_ofo_queue函數就是做這個事情的。至此第一種情況處理完畢。
32. 第二種情況,end_seq不大於rcv_nxt,即服務端期望網絡包5,但是來了一個網絡包3,怎樣纔會出現這種情況呢?肯定是服務端早就收到了網絡包3,但是ACK沒有到達客戶端中途丟了,那客戶端就認爲網絡包3沒有發送成功,於是又發送了一遍,這種情況下要趕緊給客戶端再發送一次ACK,表示早就收到了。
第三種情況,seq不小於rcv_nxt + tcp_receive_window,這說明客戶端發送得太猛了。本來seq肯定應該在接收窗口裏面的,這樣服務端纔來得及處理,結果現在超出了接收窗口,說明客戶端一下子把服務端給塞滿了。這種情況下,服務端不能再接收數據包了,只能發送ACK了,在ACK中會將接收窗口爲0的情況告知客戶端,客戶端就知道不能再發送了。這個時候雙方只能交互窗口探測數據包,直到服務端因爲用戶進程把數據讀走了,空出接收窗口,才能在ACK裏面再次告訴客戶端,又有窗口了又能發送數據包了。
第四種情況,seq小於rcv_nxt但是end_seq大於rcv_nxt,這說明從seq到rcv_nxt這部分網絡包原來的ACK客戶端沒有收到,所以重新發送了一次,從rcv_nxt到end_seq時新發送的,可以放入sk_receive_queue隊列。
當前四種情況都排除掉了,說明網絡包一定是一個亂序包了。這裏有點難理解,還是用上面那個亂序的例子仔細分析一下rcv_nxt=5,假設tcp_receive_window也是5,即超過10服務端就接收不了了。當前來的這個網絡包既不在rcv_nxt之前(不是3這種),也不在rcv_nxt + tcp_receive_window之後(不是11這種),說明這正在期望的接收窗口裏面,但是又不是rcv_nxt(不是馬上期望的網絡包 5),這正是上面例子中網絡包7、8的情況。
對於網絡包7、8,只好調用tcp_data_queue_ofo進入out_of_order_queue亂序隊列,但是沒有關係,當網絡包5、6到來的時候,會走上面第一種情況,把7、8拿出來放到sk_receive_queue隊列中。至此,網絡協議棧的處理過程就結束了。
33. 當接收的網絡包進入各種隊列之後,接下來就要等待用戶進程去讀取它們了。讀取一個socket就像讀取一個文件一樣,讀取socket的文件描述符,通過read系統調用,它對於一個文件描述符的操作大致過程都是類似的,最終它會調用到用來表示一個打開文件的結構stuct file所指向的file_operations操作。對socket來講,它的file_operations定義如下:
static const struct file_operations socket_file_ops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.read_iter = sock_read_iter,
.write_iter = sock_write_iter,
.poll = sock_poll,
.unlocked_ioctl = sock_ioctl,
.mmap = sock_mmap,
.release = sock_close,
.fasync = sock_fasync,
.sendpage = sock_sendpage,
.splice_write = generic_splice_sendpage,
.splice_read = sock_splice_read,
};
按照文件系統的讀取流程,調用的是sock_read_iter,如下所示:
static ssize_t sock_read_iter(struct kiocb *iocb, struct iov_iter *to)
{
struct file *file = iocb->ki_filp;
struct socket *sock = file->private_data;
struct msghdr msg = {.msg_iter = *to,
.msg_iocb = iocb};
ssize_t res;
if (file->f_flags & O_NONBLOCK)
msg.msg_flags = MSG_DONTWAIT;
......
res = sock_recvmsg(sock, &msg, msg.msg_flags);
*to = msg.msg_iter;
return res;
}
在sock_read_iter中,通過VFS中的struct file,將創建好的socket結構拿出來,然後調用sock_recvmsg,sock_recvmsg會調用sock_recvmsg_nosec,如下所示:
static inline int sock_recvmsg_nosec(struct socket *sock, struct msghdr *msg, int flags)
{
return sock->ops->recvmsg(sock, msg, msg_data_left(msg), flags);
}
這裏調用了socket的ops的recvmsg,這個遇到好幾次了。根據inet_stream_ops的定義,這裏調用的是inet_recvmsg,如下所示:
int inet_recvmsg(struct socket *sock, struct msghdr *msg, size_t size,
int flags)
{
struct sock *sk = sock->sk;
int addr_len = 0;
int err;
......
err = sk->sk_prot->recvmsg(sk, msg, size, flags & MSG_DONTWAIT,
flags & ~MSG_DONTWAIT, &addr_len);
......
}
這裏面從socket結構,可以得到更底層的sock結構,然後調用sk_prot的recvmsg方法。這個同樣遇到好幾次了,根據tcp_prot的定義調用的是tcp_recvmsg,如下所示:
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
int flags, int *addr_len)
{
struct tcp_sock *tp = tcp_sk(sk);
int copied = 0;
u32 peek_seq;
u32 *seq;
unsigned long used;
int err;
int target; /* Read at least this many bytes */
long timeo;
struct task_struct *user_recv = NULL;
struct sk_buff *skb, *last;
.....
do {
u32 offset;
......
/* Next get a buffer. */
last = skb_peek_tail(&sk->sk_receive_queue);
skb_queue_walk(&sk->sk_receive_queue, skb) {
last = skb;
offset = *seq - TCP_SKB_CB(skb)->seq;
if (offset < skb->len)
goto found_ok_skb;
......
}
......
if (!sysctl_tcp_low_latency && tp->ucopy.task == user_recv) {
/* Install new reader */
if (!user_recv && !(flags & (MSG_TRUNC | MSG_PEEK))) {
user_recv = current;
tp->ucopy.task = user_recv;
tp->ucopy.msg = msg;
}
tp->ucopy.len = len;
/* Look: we have the following (pseudo)queues:
*
* 1. packets in flight
* 2. backlog
* 3. prequeue
* 4. receive_queue
*
* Each queue can be processed only if the next ones
* are empty.
*/
if (!skb_queue_empty(&tp->ucopy.prequeue))
goto do_prequeue;
}
if (copied >= target) {
/* Do not sleep, just process backlog. */
release_sock(sk);
lock_sock(sk);
} else {
sk_wait_data(sk, &timeo, last);
}
if (user_recv) {
int chunk;
chunk = len - tp->ucopy.len;
if (chunk != 0) {
len -= chunk;
copied += chunk;
}
if (tp->rcv_nxt == tp->copied_seq &&
!skb_queue_empty(&tp->ucopy.prequeue)) {
do_prequeue:
tcp_prequeue_process(sk);
chunk = len - tp->ucopy.len;
if (chunk != 0) {
len -= chunk;
copied += chunk;
}
}
}
continue;
found_ok_skb:
/* Ok so how much can we use? */
used = skb->len - offset;
if (len < used)
used = len;
if (!(flags & MSG_TRUNC)) {
err = skb_copy_datagram_msg(skb, offset, msg, used);
......
}
*seq += used;
copied += used;
len -= used;
tcp_rcv_space_adjust(sk);
......
} while (len > 0);
......
}
tcp_recvmsg這個函數比較長,裏面邏輯也很複雜,好在裏面有一段註釋概括了這裏面的邏輯。註釋裏面提到了三個隊列,即receive_queue隊列、prequeue隊列和backlog隊列。這裏面需要把前一個隊列處理完畢,才處理後一個隊列。tcp_recvmsg的整個邏輯也是這樣執行的:這裏面有一個while循環,不斷地讀取網絡包,這裏會先處理sk_receive_queue隊列,如果找到了網絡包,就跳到found_ok_skb這裏,這裏會調用skb_copy_datagram_msg,將網絡包拷貝到用戶進程中,然後直接進入下一層循環。
循環直到sk_receive_queue隊列處理完畢,纔到了sysctl_tcp_low_latency判斷。如果不需要低時延,則會有prequeue隊列,於是能就跳到do_prequeue這裏,調用tcp_prequeue_process進行處理。如果sysctl_tcp_low_latency設置爲1,即沒有prequeue隊列,或者prequeue隊列爲空,則需要處理backlog隊列,在release_sock函數中處理。release_sock會調用__release_sock,這裏面會依次處理隊列中的網絡包,如下所示:
void release_sock(struct sock *sk)
{
......
if (sk->sk_backlog.tail)
__release_sock(sk);
......
}
static void __release_sock(struct sock *sk)
__releases(&sk->sk_lock.slock)
__acquires(&sk->sk_lock.slock)
{
struct sk_buff *skb, *next;
while ((skb = sk->sk_backlog.head) != NULL) {
sk->sk_backlog.head = sk->sk_backlog.tail = NULL;
do {
next = skb->next;
prefetch(next);
skb->next = NULL;
sk_backlog_rcv(sk, skb);
cond_resched();
skb = next;
} while (skb != NULL);
}
......
}
34. 上面講完了接收網絡包的過程,這裏來從頭串一下,整個過程可以分成以下幾個層次:
(1)硬件網卡接收到網絡包之後,通過DMA技術將網絡包放入Ring Buffer;
(2)硬件網卡通過中斷通知CPU新的網絡包的到來;
(3)網卡驅動程序會註冊中斷處理函數ixgb_intr;中斷處理函數處理完需要暫時屏蔽中斷的核心流程之後,通過軟中斷NET_RX_SOFTIRQ觸發接下來的處理過程;
(4)NET_RX_SOFTIRQ軟中斷處理函數調用net_rx_action,net_rx_action會調用napi_poll,進而調用ixgb_clean_rx_irq,從Ring Buffer中讀取數據到內核struct sk_buff;
(5)調用netif_receive_skb進入內核網絡協議棧,進行一些關於VLAN的二層邏輯處理後,調用ip_rcv進入三層IP層;
(6)在IP層會處理iptables規則,然後調用ip_local_deliver交給更上層TCP層;
(7)在TCP層調用tcp_v4_rcv,這裏面有三個隊列需要處理,如果當前的Socket不是正在被讀取,則放入backlog隊列,如果正在被讀取不需要很實時的話,則放入prequeue隊列,其他情況調用tcp_v4_do_rcv;
(8)在tcp_v4_do_rcv中,如果是處於TCP_ESTABLISHED狀態調用tcp_rcv_established,其他的狀態調用tcp_rcv_state_process;
(9)在 tcp_rcv_established中調用tcp_data_queue,如果序列號能夠接的上,則放入sk_receive_queue隊列;如果序列號接不上,則暫時放入out_of_order_queue隊列,等序列號能夠接上的時候再放入sk_receive_queue隊列。
至此內核接收網絡包的過程到此結束,接下來就是用戶態讀取網絡包的過程,這個過程也分成幾個層次:
(1)VFS層:read系統調用找到struct file,根據裏面file_operations的定義調用sock_read_iter函數。sock_read_iter 函數調用 sock_recvmsg 函數。
(2)Socket層:從struct file裏面的private_data得到struct socket,根據裏面ops的定義調用inet_recvmsg函數。
(3)Sock層:從struct socket裏面的sk得到struct sock,根據裏面sk_prot的定義調用tcp_recvmsg函數。
(4)TCP層:tcp_recvmsg函數會依次讀取receive_queue隊列、prequeue隊列和backlog隊列。