數據包從網卡到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 |
+-----------------+
下面,數據包將交給相應的協議棧函數處理,進入第三層網絡層。
- IP 層的入口函數在 ip_rcv 函數。該函數首先會做包括 package checksum 在內的各種檢查,如果需要的話會做 IP defragment(將多個分片合併),然後 packet 調用已經註冊的 Pre-routing netfilter hook ,完成後最終到達 ip_rcv_finish 函數。
- 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相關的鉤子程序,如果通過,數據包將會向下發送到傳輸層
傳輸層
- 傳輸層 TCP包的 處理入口在 tcp_v4_rcv 函數(位於 linux/net/ipv4/tcp ipv4.c 文件中),它會做 TCP header 檢查等處理。
- 調用 _tcp_v4_lookup,查找該 package 的 open socket。如果找不到,該 package 會被丟棄。接下來檢查 socket 和 connection 的狀態。
- 如果socket 和 connection 一切正常,調用 tcp_prequeue 使 package 從內核進入 user space,放進 socket 的 receive queue。然後 socket 會被喚醒,調用 system call,並最終調用 tcp_recvmsg 函數去從 socket recieve queue 中獲取 segment。
應用層
- 每當用戶應用調用 read 或者 recvfrom 時,該調用會被映射爲/net/socket.c 中的 sys_recv 系統調用,並被轉化爲 sys_recvfrom 調用,然後調用 sock_recgmsg 函數。
- 對於 INET 類型的 socket,/net/ipv4/af inet.c 中的 inet_recvmsg 方法會被調用,它會調用相關協議的數據接收方法。
- 對 TCP 來說,調用 tcp_recvmsg。該函數從 socket buffer 中拷貝數據到 user buffer。
- 對 UDP 來說,從 user space 中可以調用三個 system call recv()/recvfrom()/recvmsg() 中的任意一個來接收 UDP package,這些系統調用最終都會調用內核中的 udp_recvmsg 方法。
整個報文接收的過程如下:
參考
https://segmentfault.com/a/1190000008836467
http://www.cnblogs.com/sammyliu/p/5225623.html
分層:
- socket 位於傳輸層協議之上,屏蔽了不同網絡協議之間的差異
- socket 是網絡編程的入口,它提供了大量的系統調用,構成了網絡程序的主體
- 在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實現了數組到哈希表的轉換
- 在創建socket啓動監聽時,會添加可讀事件到epoll,事件處理函數爲ngx_event_accept,用於接收socket連接,分配connection連接,並調用ngx_listening_t對象的處理函數(ngx_http_init_connection)
- socket連接成功後,nginx會等待客戶端發送HTTP請求,默認會有60秒的超時時間,即60秒內沒有接收到客戶端請求時,斷開此連接,打印錯誤日誌。函數ngx_http_init_connection用於設置讀事件處理函數,以及超時定時器。
- 函數ngx_http_wait_request_handler爲解析HTTP請求的入口函數
- 函數ngx_http_create_request創建並初始化ngx_http_request_t對象
- 解析完成請求行與請求頭,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如下圖:
處理請求的過程
- HTTP請求的處理入口函數是ngx_http_process_request,其主要調用ngx_http_core_run_phases實現11個階段的執行流程
- ngx_http_core_run_phases遍歷預先設置好的cmcf->phase_engine.handlers數組,調用其checker函數
- checker內部就是調用handler,並設置下一步要執行handler的索引
所以綜上看來,nginx處理請求的過程可以歸納爲:
- 初始化 HTTP Request(讀取來自客戶端的數據,生成 HTTP Request 對象,該對象含有該請求所有的信息)。
- 處理請求頭。
- 處理請求體。
- 如果有的話,調用與此請求(URL 或者 Location)關聯的 handler。
- 依次調用各 phase handler 進行處理。