1. 程式人生 > >從網絡卡到應用層nginx,一個經歷了什麼?

從網絡卡到應用層nginx,一個經歷了什麼?

資料包從網絡卡到nginx

本文將研究一個數據包從被網絡卡接收到流出應用層到底經歷了什麼,並探究在應用層nginx的處理流程。**注:**本文只討論物理網絡卡,暫不涉及虛擬網絡卡。

從網絡卡到記憶體

1: 資料包從外面的網路進入物理網絡卡。如果目的地址不是該網絡卡,且該網絡卡沒有開啟混雜模式,該包會被網絡卡丟棄。
2: 網絡卡將資料包通過DMA的方式寫入到指定的記憶體地址,該地址由網絡卡驅動分配並初始化。注: 老的網絡卡可能不支援DMA,不過新的網絡卡一般都支援。
3: 網絡卡通過硬體中斷(IRQ)通知CPU,告訴它有資料來了
4: CPU根據中斷表,呼叫已經註冊的中斷函式,這個中斷函式會調到驅動程式(NIC Driver)中相應的函式
5: 驅動先禁用網絡卡的中斷,表示驅動程式已經知道記憶體中有資料了,告訴網絡卡下次再收到資料包直接寫記憶體就可以了,不要再通知CPU了,這樣可以提高效率,避免CPU不停的被中斷。
6: 啟動軟中斷。這步結束後,硬體中斷處理函式就結束返回了。由於硬中斷處理程式執行的過程中不能被中斷,所以如果它執行時間過長,會導致CPU沒法響應其它硬體的中斷,於是核心引入軟中斷,這樣可以將硬中斷處理函式中耗時的部分移到軟中斷處理函式裡面來慢慢處理。
如下圖:

                   +-----+
                   |     |                            Memroy
+--------+   1     |     |  2  DMA     +--------+--------+--------+--------+
| Packet |-------->| NIC |------------>| Packet | Packet | Packet | ...... |
+--------+         |     |             +--------+--------+--------+--------+
                   |     |<--------+
                   +-----+         |
                      |            +---------------+
                      |                            |
                    3 | Raise IRQ                  | Disable IRQ
                      |                          5 |
                      |                            |
                      ↓                            |
                   +-----+                   +------------+
                   |     |  Run IRQ handler  |            |
                   | CPU |------------------>| NIC Driver |
                   |     |       4           |            |
                   +-----+                   +------------+
                                                   |
                                                6  | Raise soft IRQ
                                                   |
                                                   ↓

記憶體-網路模組-協議棧

RPS實現了資料流的hash歸類,並把軟中斷的負載均衡分到各個cpu
7: 核心中的ksoftirqd程序專門負責軟中斷的處理,當它收到軟中斷後,就會呼叫相應軟中斷所對應的處理函式,對於上面第6步中是網絡卡驅動模組丟擲的軟中斷,ksoftirqd會呼叫網路模組的net_rx_action函式
8: net_rx_action呼叫網絡卡驅動裡的poll函式來一個一個的處理資料包
9: 在pool函式中,驅動會一個接一個的讀取網絡卡寫到記憶體中的資料包,記憶體中資料包的格式只有驅動知道
10: 驅動程式將記憶體中的資料包轉換成核心網路模組能識別的skb格式,然後呼叫napi_gro_receive函式
11: napi_gro_receive會處理GRO相關的內容,也就是將可以合併的資料包進行合併,這樣就只需要呼叫一次協議棧。然後判斷是否開啟了RPS,如果開啟了,將會呼叫enqueue_to_backlog
12: 在enqueue_to_backlog函式中,會將資料包放入CPU的softnet_data結構體的input_pkt_queue中,然後返回,如果input_pkt_queue滿了的話,該資料包將會被丟棄,queue的大小可以通過net.core.netdev_max_backlog來配置
13: CPU會接著在自己的軟中斷上下文中處理自己input_pkt_queue裡的網路資料(呼叫__netif_receive_skb_core)
14: 如果沒開啟RPS,napi_gro_receive會直接呼叫__netif_receive_skb_core
15: 看是不是有AF_PACKET型別的socket(也就是我們常說的原始套接字),如果有的話,拷貝一份資料給它。tcpdump抓包就是抓的這裡的包。
16: 呼叫協議棧相應的函式,將資料包交給協議棧處理。
17: 待記憶體中的所有資料包被處理完成後(即poll函式執行完成),啟用網絡卡的硬中斷,這樣下次網絡卡再收到資料的時候就會通知CPU
如下圖:

                                                     +-----+
                                             17      |     |
                                        +----------->| NIC |
                                        |            |     |
                                        |Enable IRQ  +-----+
                                        |
                                        |
                                  +------------+                                      Memroy
                                  |            |        Read           +--------+--------+--------+--------+
                 +--------------->| NIC Driver |<--------------------- | Packet | Packet | Packet | ...... |
                 |                |            |          9            +--------+--------+--------+--------+
                 |                +------------+
                 |                      |    |        skb
            Poll | 8      Raise softIRQ | 6  +-----------------+
                 |                      |             10       |
                 |                      ↓                      ↓
         +---------------+  Call  +-----------+        +------------------+        +--------------------+  12  +---------------------+
         | net_rx_action |<-------| ksoftirqd |        | napi_gro_receive |------->| enqueue_to_backlog |----->| CPU input_pkt_queue |
         +---------------+   7    +-----------+        +------------------+   11   +--------------------+      +---------------------+
                                                               |                                                      | 13
                                                            14 |        + - - - - - - - - - - - - - - - - - - - - - - +
                                                               ↓        ↓
                                                    +--------------------------+    15      +------------------------+
                                                    | __netif_receive_skb_core |----------->| packet taps(AF_PACKET) |
                                                    +--------------------------+            +------------------------+
                                                               |
                                                               | 16
                                                               ↓
                                                      +-----------------+
                                                      | protocol layers |
                                                      +-----------------+

下面,資料包將交給相應的協議棧函式處理,進入第三層網路層。

  1. IP 層的入口函式在 ip_rcv 函式。該函式首先會做包括 package checksum 在內的各種檢查,如果需要的話會做 IP defragment(將多個分片合併),然後 packet 呼叫已經註冊的 Pre-routing netfilter hook ,完成後最終到達 ip_rcv_finish 函式。
  2. ip_rcv_finish 函式會呼叫 ip_router_input 函式,進入路由處理環節。它首先會呼叫 ip_route_input 來更新路由,然後查詢 route,決定該 package 將會被髮到本機還是會被轉發還是丟棄:
    • 如果是發到本機的話,呼叫 ip_local_deliver 函式,可能會做 de-fragment(合併多個 IP packet),然後呼叫 ip_local_deliver 函式。該函式根據 package 的下一個處理層的 protocal number,呼叫下一層介面,包括 tcp_v4_rcv (TCP), udp_rcv (UDP),icmp_rcv (ICMP),igmp_rcv(IGMP)。對於 TCP 來說,函式 tcp_v4_rcv 函式會被呼叫,從而處理流程進入 TCP 棧。
    • 如果需要轉發 (forward),則進入轉發流程。該流程需要處理 TTL,再呼叫 dst_input 函式。該函式會
      • (1)處理 Netfilter Hook
      • (2)執行 IP fragmentation
      • (3)呼叫 dev_queue_xmit,進入鏈路層處理流程。
        如下圖:
          |
          |
          ↓         promiscuous mode &&
      +--------+    PACKET_OTHERHOST (set by driver)   +-----------------+
      | ip_rcv |-------------------------------------->| drop this packet|
      +--------+                                       +-----------------+
          |
          |
          ↓
+---------------------+
| NF_INET_PRE_ROUTING |
+---------------------+
          |
          |
          ↓
      +---------+
      |         | enabled ip forword  +------------+        +----------------+
      | routing |-------------------->| ip_forward |------->| NF_INET_FORWARD |
      |         |                     +------------+        +----------------+
      +---------+                                                   |
          |                                                         |
          | destination IP is local                                 ↓
          ↓                                                 +---------------+
 +------------------+                                       | dst_output_sk |
 | ip_local_deliver |                                       +---------------+
 +------------------+
          |
          |
          ↓
 +------------------+
 | NF_INET_LOCAL_IN |
 +------------------+
          |
          |
          ↓
    +-------------------+
    | tcp_v4_rcv (TCP) |
    +-------------------+

在上圖中,

  • ip_rcv: ip_rcv函式是IP模組的入口函式,在該函式裡面,第一件事就是將垃圾資料包(目的mac地址不是當前網絡卡,但由於網絡卡設定了混雜模式而被接收進來)直接丟掉,然後呼叫註冊在NF_INET_PRE_ROUTING上的函式
  • NF_INET_PRE_ROUTING: netfilter放在協議棧中的鉤子,可以通過iptables來注入一些資料包處理函式,用來修改或者丟棄資料包,如果資料包沒被丟棄,將繼續往下走
  • routing: 進行路由,如果是目的IP不是本地IP,且沒有開啟ip forward功能,那麼資料包將被丟棄,如果開啟了ip forward功能,那將進入ip_forward函式
  • ip_forward: ip_forward會先呼叫netfilter註冊的NF_INET_FORWARD相關函式,如果資料包沒有被丟棄,那麼將繼續往後呼叫dst_output_sk函式
  • dst_output_sk: 該函式會呼叫IP層的相應函式將該資料包傳送出去,同下一篇要介紹的資料包傳送流程的後半部分一樣。
  • ip_local_deliver:如果上面routing的時候發現目的IP是本地IP,那麼將會呼叫該函式,在該函式中,會先呼叫NF_INET_LOCAL_IN相關的鉤子程式,如果通過,資料包將會向下傳送到傳輸層

傳輸層

  1. 傳輸層 TCP包的 處理入口在 tcp_v4_rcv 函式(位於 linux/net/ipv4/tcp ipv4.c 檔案中),它會做 TCP header 檢查等處理。
  2. 呼叫 _tcp_v4_lookup,查詢該 package 的 open socket。如果找不到,該 package 會被丟棄。接下來檢查 socket 和 connection 的狀態。
  3. 如果socket 和 connection 一切正常,呼叫 tcp_prequeue 使 package 從核心進入 user space,放進 socket 的 receive queue。然後 socket 會被喚醒,呼叫 system call,並最終呼叫 tcp_recvmsg 函式去從 socket recieve queue 中獲取 segment。

應用層

  1. 每當使用者應用呼叫 read 或者 recvfrom 時,該呼叫會被對映為/net/socket.c 中的 sys_recv 系統呼叫,並被轉化為 sys_recvfrom 呼叫,然後呼叫 sock_recgmsg 函式。
  2. 對於 INET 型別的 socket,/net/ipv4/af inet.c 中的 inet_recvmsg 方法會被呼叫,它會呼叫相關協議的資料接收方法。
  3. 對 TCP 來說,呼叫 tcp_recvmsg。該函式從 socket buffer 中拷貝資料到 user buffer。
  4. 對 UDP 來說,從 user space 中可以呼叫三個 system call recv()/recvfrom()/recvmsg() 中的任意一個來接收 UDP package,這些系統呼叫最終都會呼叫核心中的 udp_recvmsg 方法。

整個報文接收的過程如下:
nic2user

參考

分層:
在這裡插入圖片描述

  1. socket 位於傳輸層協議之上,遮蔽了不同網路協議之間的差異
  2. socket 是網路程式設計的入口,它提供了大量的系統呼叫,構成了網路程式的主體
  3. 在Linux系統中,socket 屬於檔案系統的一部分,網路通訊可以被看作是對檔案的讀取,使得我們對網路的控制和對檔案的控制一樣方便

nginx處理socket套接字的流程

nginx解析使用者配置,在所有埠建立socket並啟動監聽。
nginx解析配置檔案是由各個模組分擔處理的,每個模組註冊並處理自己關心的配置,通過模組結構體ngx_module_t的欄位ngx_command_t *commands實現。

main方法會呼叫ngx_init_cycle,其完成了伺服器初始化的大部分工作,其中就包括啟動監聽(ngx_open_listening_sockets)

假設nginx使用epoll處理所有socket事件,ngx_event_core_module模組是事件處理核心模組,初始化此模組時會執行ngx_event_process_init函式,包括將監聽事件新增到epoll

  • 結構體ngx_connection_t儲存socket連線相關資訊;nginx預先建立若干個ngx_connection_t物件,儲存在全域性變數ngx_cycle->free_connections,稱之為連線池;當新生成socket時,會嘗試從連線池中獲取空閒connection連線,如果獲取失敗,則會直接關閉此socket。指令worker_connections用於配置連線池最大連線數目,配置在events指令塊中,由ngx_event_core_module解析

  • 結構體ngx_http_request_t儲存整個HTTP請求處理流程所需的所有資訊,欄位非常多

  • ngx_http_request.c檔案中定義了所有的HTTP頭部,儲存在ngx_http_headers_in陣列,陣列的每個元素是一個ngx_http_header_t結構體,解析後的請求頭資訊都儲存在ngx_http_headers_in_t結構體中

  • 從ngx_http_headers_in陣列中查詢請求頭對應ngx_http_header_t物件時,需要遍歷,每個元素都需要進行字串比較,效率低下。因此nginx將ngx_http_headers_in陣列轉換為雜湊表,雜湊表的鍵即為請求頭的key,方法ngx_http_init_headers_in_hash實現了陣列到雜湊表的轉換

  1. 在建立socket啟動監聽時,會新增可讀事件到epoll,事件處理函式為ngx_event_accept,用於接收socket連線,分配connection連線,並呼叫ngx_listening_t物件的處理函式(ngx_http_init_connection)
  2. socket連線成功後,nginx會等待客戶端傳送HTTP請求,預設會有60秒的超時時間,即60秒內沒有接收到客戶端請求時,斷開此連線,列印錯誤日誌。函式ngx_http_init_connection用於設定讀事件處理函式,以及超時定時器。
  3. 函式ngx_http_wait_request_handler為解析HTTP請求的入口函式
  4. 函式ngx_http_create_request建立並初始化ngx_http_request_t物件
  5. 解析完成請求行與請求頭,nginx就開始處理HTTP請求,並沒有等到解析完請求體再處理。處理請求入口為ngx_http_process_request。

下面進入nginx http請求處理的11個階段

絕大多數HTTP模組都會將自己的handler新增到某個階段(將handler新增到全域性唯一的陣列ngx_http_phases中),注意其中有4個階段不能新增自定義handler,nginx處理HTTP請求時會挨個呼叫每個階段的handler

typedef enum {
    NGX_HTTP_POST_READ_PHASE = 0, //第一個階段,目前只有realip模組會註冊handler,但是該模組預設不會執行(nginx作為代理伺服器時有用,後端以此獲取客戶端原始ip)
  
    NGX_HTTP_SERVER_REWRITE_PHASE,  //server塊中配置了rewrite指令,重寫url
  
    NGX_HTTP_FIND_CONFIG_PHASE,   //查詢匹配的location配置;不能自定義handler;
    NGX_HTTP_REWRITE_PHASE,       //location塊中配置了rewrite指令,重寫url
    NGX_HTTP_POST_REWRITE_PHASE,  //檢查是否發生了url重寫,如果有,重新回到FIND_CONFIG階段;不能自定義handler;
  
    NGX_HTTP_PREACCESS_PHASE,     //訪問控制,比如限流模組會註冊handler到此階段
  
    NGX_HTTP_ACCESS_PHASE,        //訪問許可權控制,比如基於ip黑白名單的許可權控制,基於使用者名稱密碼的許可權控制等
    NGX_HTTP_POST_ACCESS_PHASE,   //根據訪問許可權控制階段做相應處理;不能自定義handler;
  
    NGX_HTTP_TRY_FILES_PHASE,     //只有配置了try_files指令,才會有此階段;不能自定義handler;
    NGX_HTTP_CONTENT_PHASE,       //內容產生階段,返回響應給客戶端
  
    NGX_HTTP_LOG_PHASE            //日誌記錄
} ngx_http_phases;

nginx 在ngx_http_block函式中初始化11個階段的ngx_http_phases陣列,把http模組註冊到相應的階段去。注意多個模組可能註冊到同一個階段,因此phases是一個二維陣列

  • nginx使用結構體ngx_module_s表示一個模組,其中欄位ctx,是一個指向模組上下文結構體的指標(上下文結構體的欄位都是一些函式指標)
  • postconfiguration,負責註冊本模組的handler到某個處理階段

使用GDB除錯,斷點到ngx_http_block方法執行所有HTTP模組註冊handler之後,列印phases陣列:

p cmcf->phases[*].handlers
p *(ngx_http_handler_pt*)cmcf->phases[*].handlers.elts

11個階段(7個階段可註冊)以及模組註冊的handler如下圖:
在這裡插入圖片描述

處理請求的過程

  1. HTTP請求的處理入口函式是ngx_http_process_request,其主要呼叫ngx_http_core_run_phases實現11個階段的執行流程
  2. ngx_http_core_run_phases遍歷預先設定好的cmcf->phase_engine.handlers陣列,呼叫其checker函式
  3. checker內部就是呼叫handler,並設定下一步要執行handler的索引

所以綜上看來,nginx處理請求的過程可以歸納為:

  1. 初始化 HTTP Request(讀取來自客戶端的資料,生成 HTTP Request 物件,該物件含有該請求所有的資訊)。
  2. 處理請求頭。
  3. 處理請求體。
  4. 如果有的話,呼叫與此請求(URL 或者 Location)關聯的 handler。
  5. 依次呼叫各 phase handler 進行處理。