Linux網絡系統原理筆記

一、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隊列。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章