數(shù)據(jù)包從網(wǎng)卡到nginx
本文將研究一個(gè)數(shù)據(jù)包從被網(wǎng)卡接收到流出應(yīng)用層到底經(jīng)歷了什么,并探究在應(yīng)用層nginx的處理流程。**注:**本文只討論物理網(wǎng)卡,暫不涉及虛擬網(wǎng)卡。
從網(wǎng)卡到內(nèi)存
1: 數(shù)據(jù)包從外面的網(wǎng)絡(luò)進(jìn)入物理網(wǎng)卡。如果目的地址不是該網(wǎng)卡,且該網(wǎng)卡沒有開啟混雜模式,該包會(huì)被網(wǎng)卡丟棄。
2: 網(wǎng)卡將數(shù)據(jù)包通過DMA的方式寫入到指定的內(nèi)存地址,該地址由網(wǎng)卡驅(qū)動(dòng)分配并初始化。注: 老的網(wǎng)卡可能不支持DMA,不過新的網(wǎng)卡一般都支持。
3: 網(wǎng)卡通過硬件中斷(IRQ)通知CPU,告訴它有數(shù)據(jù)來了
4: CPU根據(jù)中斷表,調(diào)用已經(jīng)注冊的中斷函數(shù),這個(gè)中斷函數(shù)會(huì)調(diào)到驅(qū)動(dòng)程序(NIC Driver)中相應(yīng)的函數(shù)
5: 驅(qū)動(dòng)先禁用網(wǎng)卡的中斷,表示驅(qū)動(dòng)程序已經(jīng)知道內(nèi)存中有數(shù)據(jù)了,告訴網(wǎng)卡下次再收到數(shù)據(jù)包直接寫內(nèi)存就可以了,不要再通知CPU了,這樣可以提高效率,避免CPU不停的被中斷。
6: 啟動(dòng)軟中斷。這步結(jié)束后,硬件中斷處理函數(shù)就結(jié)束返回了。由于硬中斷處理程序執(zhí)行的過程中不能被中斷,所以如果它執(zhí)行時(shí)間過長,會(huì)導(dǎo)致CPU沒法響應(yīng)其它硬件的中斷,于是內(nèi)核引入軟中斷,這樣可以將硬中斷處理函數(shù)中耗時(shí)的部分移到軟中斷處理函數(shù)里面來慢慢處理。
如下圖:
內(nèi)存-網(wǎng)絡(luò)模塊-協(xié)議棧
RPS實(shí)現(xiàn)了數(shù)據(jù)流的hash歸類,并把軟中斷的負(fù)載均衡分到各個(gè)cpu
7: 內(nèi)核中的ksoftirqd進(jìn)程專門負(fù)責(zé)軟中斷的處理,當(dāng)它收到軟中斷后,就會(huì)調(diào)用相應(yīng)軟中斷所對應(yīng)的處理函數(shù),對于上面第6步中是網(wǎng)卡驅(qū)動(dòng)模塊拋出的軟中斷,ksoftirqd會(huì)調(diào)用網(wǎng)絡(luò)模塊的net_rx_action函數(shù)
8: net_rx_action調(diào)用網(wǎng)卡驅(qū)動(dòng)里的poll函數(shù)來一個(gè)一個(gè)的處理數(shù)據(jù)包
9: 在pool函數(shù)中,驅(qū)動(dòng)會(huì)一個(gè)接一個(gè)的讀取網(wǎng)卡寫到內(nèi)存中的數(shù)據(jù)包,內(nèi)存中數(shù)據(jù)包的格式只有驅(qū)動(dòng)知道
10: 驅(qū)動(dòng)程序?qū)?nèi)存中的數(shù)據(jù)包轉(zhuǎn)換成內(nèi)核網(wǎng)絡(luò)模塊能識(shí)別的skb格式,然后調(diào)用napi_gro_receive函數(shù)
11: napi_gro_receive會(huì)處理GRO相關(guān)的內(nèi)容,也就是將可以合并的數(shù)據(jù)包進(jìn)行合并,這樣就只需要調(diào)用一次協(xié)議棧。然后判斷是否開啟了RPS,如果開啟了,將會(huì)調(diào)用enqueue_to_backlog
12: 在enqueue_to_backlog函數(shù)中,會(huì)將數(shù)據(jù)包放入CPU的softnet_data結(jié)構(gòu)體的input_pkt_queue中,然后返回,如果input_pkt_queue滿了的話,該數(shù)據(jù)包將會(huì)被丟棄,queue的大小可以通過
net.core.netdev_max_backlog來配置
13: CPU會(huì)接著在自己的軟中斷上下文中處理自己input_pkt_queue里的網(wǎng)絡(luò)數(shù)據(jù)(調(diào)用__netif_receive_skb_core)
14: 如果沒開啟RPS,napi_gro_receive會(huì)直接調(diào)用__netif_receive_skb_core
15: 看是不是有AF_PACKET類型的socket(也就是我們常說的原始套接字),如果有的話,拷貝一份數(shù)據(jù)給它。tcpdump抓包就是抓的這里的包。
16: 調(diào)用協(xié)議棧相應(yīng)的函數(shù),將數(shù)據(jù)包交給協(xié)議棧處理。
17: 待內(nèi)存中的所有數(shù)據(jù)包被處理完成后(即poll函數(shù)執(zhí)行完成),啟用網(wǎng)卡的硬中斷,這樣下次網(wǎng)卡再收到數(shù)據(jù)的時(shí)候就會(huì)通知CPU
如下圖:
下面,數(shù)據(jù)包將交給相應(yīng)的協(xié)議棧函數(shù)處理,進(jìn)入第三層網(wǎng)絡(luò)層。
IP 層的入口函數(shù)在 ip_rcv 函數(shù)。該函數(shù)首先會(huì)做包括 package checksum 在內(nèi)的各種檢查,如果需要的話會(huì)做 IP defragment(將多個(gè)分片合并),然后 packet 調(diào)用已經(jīng)注冊的 Pre-routing netfilter hook ,完成后最終到達(dá) ip_rcv_finish 函數(shù)。
ip_rcv_finish 函數(shù)會(huì)調(diào)用 ip_router_input 函數(shù),進(jìn)入路由處理環(huán)節(jié)。它首先會(huì)調(diào)用 ip_route_input 來更新路由,然后查找 route,決定該 package 將會(huì)被發(fā)到本機(jī)還是會(huì)被轉(zhuǎn)發(fā)還是丟棄:
1、如果是發(fā)到本機(jī)的話,調(diào)用 ip_local_deliver 函數(shù),可能會(huì)做 de-fragment(合并多個(gè) IP packet),然后調(diào)用 ip_local_deliver 函數(shù)。該函數(shù)根據(jù) package 的下一個(gè)處理層的 protocal number,調(diào)用下一層接口,包括 tcp_v4_rcv (TCP), udp_rcv (UDP),icmp_rcv (ICMP),igmp_rcv(IGMP)。對于 TCP 來說,函數(shù) tcp_v4_rcv 函數(shù)會(huì)被調(diào)用,從而處理流程進(jìn)入 TCP 棧。
2、如果需要轉(zhuǎn)發(fā) (forward),則進(jìn)入轉(zhuǎn)發(fā)流程。該流程需要處理 TTL,再調(diào)用 dst_input 函數(shù)。該函數(shù)會(huì)
(1)處理 Netfilter Hook
(2)執(zhí)行 IP fragmentation
(3)調(diào)用 dev_queue_xmit,進(jìn)入鏈路層處理流程。
如下圖:
在上圖中,
- ip_rcv: ip_rcv函數(shù)是IP模塊的入口函數(shù),在該函數(shù)里面,第一件事就是將垃圾數(shù)據(jù)包(目的mac地址不是當(dāng)前網(wǎng)卡,但由于網(wǎng)卡設(shè)置了混雜模式而被接收進(jìn)來)直接丟掉,然后調(diào)用注冊在NF_INET_PRE_ROUTING上的函數(shù)
- NF_INET_PRE_ROUTING: netfilter放在協(xié)議棧中的鉤子,可以通過iptables來注入一些數(shù)據(jù)包處理函數(shù),用來修改或者丟棄數(shù)據(jù)包,如果數(shù)據(jù)包沒被丟棄,將繼續(xù)往下走
- routing: 進(jìn)行路由,如果是目的IP不是本地IP,且沒有開啟ip forward功能,那么數(shù)據(jù)包將被丟棄,如果開啟了ip forward功能,那將進(jìn)入ip_forward函數(shù)
- ip_forward: ip_forward會(huì)先調(diào)用netfilter注冊的NF_INET_FORWARD相關(guān)函數(shù),如果數(shù)據(jù)包沒有被丟棄,那么將繼續(xù)往后調(diào)用dst_output_sk函數(shù)
- dst_output_sk: 該函數(shù)會(huì)調(diào)用IP層的相應(yīng)函數(shù)將該數(shù)據(jù)包發(fā)送出去,同下一篇要介紹的數(shù)據(jù)包發(fā)送流程的后半部分一樣。
- ip_local_deliver:如果上面routing的時(shí)候發(fā)現(xiàn)目的IP是本地IP,那么將會(huì)調(diào)用該函數(shù),在該函數(shù)中,會(huì)先調(diào)用NF_INET_LOCAL_IN相關(guān)的鉤子程序,如果通過,數(shù)據(jù)包將會(huì)向下發(fā)送到傳輸層
傳輸層
1、傳輸層 TCP包的 處理入口在 tcp_v4_rcv 函數(shù)(位于 linux/net/ipv4/tcp ipv4.c 文件中),它會(huì)做 TCP header 檢查等處理。
2、調(diào)用 _tcp_v4_lookup,查找該 package 的 open socket。如果找不到,該 package 會(huì)被丟棄。接下來檢查 socket 和 connection 的狀態(tài)。
3、如果socket 和 connection 一切正常,調(diào)用 tcp_prequeue 使 package 從內(nèi)核進(jìn)入 user space,放進(jìn) socket 的 receive queue。然后 socket 會(huì)被喚醒,調(diào)用 system call,并最終調(diào)用 tcp_recvmsg 函數(shù)去從 socket recieve queue 中獲取 segment。
應(yīng)用層
1、每當(dāng)用戶應(yīng)用調(diào)用 read 或者 recvfrom 時(shí),該調(diào)用會(huì)被映射為/net/socket.c 中的 sys_recv 系統(tǒng)調(diào)用,并被轉(zhuǎn)化為 sys_recvfrom 調(diào)用,然后調(diào)用 sock_recgmsg 函數(shù)。
2、對于 INET 類型的 socket,/net/ipv4/af inet.c 中的 inet_recvmsg 方法會(huì)被調(diào)用,它會(huì)調(diào)用相關(guān)協(xié)議的數(shù)據(jù)接收方法。
3、對 TCP 來說,調(diào)用 tcp_recvmsg。該函數(shù)從 socket buffer 中拷貝數(shù)據(jù)到 user buffer。
4、對 UDP 來說,從 user space 中可以調(diào)用三個(gè) system call recv()/recvfrom()/recvmsg() 中的任意一個(gè)來接收 UDP package,這些系統(tǒng)調(diào)用最終都會(huì)調(diào)用內(nèi)核中的 udp_recvmsg 方法。
整個(gè)報(bào)文接收的過程如下:
分層:
1、socket 位于傳輸層協(xié)議之上,屏蔽了不同網(wǎng)絡(luò)協(xié)議之間的差異
2、socket 是網(wǎng)絡(luò)編程的入口,它提供了大量的系統(tǒng)調(diào)用,構(gòu)成了網(wǎng)絡(luò)程序的主體
3、在Linux系統(tǒng)中,socket 屬于文件系統(tǒng)的一部分,網(wǎng)絡(luò)通信可以被看作是對文件的讀取,使得我們對網(wǎng)絡(luò)的控制和對文件的控制一樣方便
nginx處理socket套接字的流程
nginx解析用戶配置,在所有端口創(chuàng)建socket并啟動(dòng)監(jiān)聽。
nginx解析配置文件是由各個(gè)模塊分擔(dān)處理的,每個(gè)模塊注冊并處理自己關(guān)心的配置,通過模塊結(jié)構(gòu)體ngx_module_t的字段ngx_command_t *commands實(shí)現(xiàn)。
main方法會(huì)調(diào)用ngx_init_cycle,其完成了服務(wù)器初始化的大部分工作,其中就包括啟動(dòng)監(jiān)聽(
ngx_open_listening_sockets)
假設(shè)nginx使用epoll處理所有socket事件,ngx_event_core_module模塊是事件處理核心模塊,初始化此模塊時(shí)會(huì)執(zhí)行ngx_event_process_init函數(shù),包括將監(jiān)聽事件添加到epoll
- 結(jié)構(gòu)體ngx_connection_t存儲(chǔ)socket連接相關(guān)信息;nginx預(yù)先創(chuàng)建若干個(gè)ngx_connection_t對象,存儲(chǔ)在全局變量ngx_cycle->free_connections,稱之為連接池;當(dāng)新生成socket時(shí),會(huì)嘗試從連接池中獲取空閑connection連接,如果獲取失敗,則會(huì)直接關(guān)閉此socket。指令worker_connections用于配置連接池最大連接數(shù)目,配置在events指令塊中,由ngx_event_core_module解析
- 結(jié)構(gòu)體ngx_http_request_t存儲(chǔ)整個(gè)HTTP請求處理流程所需的所有信息,字段非常多
- ngx_http_request.c文件中定義了所有的HTTP頭部,存儲(chǔ)在ngx_http_headers_in數(shù)組,數(shù)組的每個(gè)元素是一個(gè)ngx_http_header_t結(jié)構(gòu)體,解析后的請求頭信息都存儲(chǔ)在ngx_http_headers_in_t結(jié)構(gòu)體中
- 從ngx_http_headers_in數(shù)組中查找請求頭對應(yīng)ngx_http_header_t對象時(shí),需要遍歷,每個(gè)元素都需要進(jìn)行字符串比較,效率低下。因此nginx將ngx_http_headers_in數(shù)組轉(zhuǎn)換為哈希表,哈希表的鍵即為請求頭的key,方法ngx_http_init_headers_in_hash實(shí)現(xiàn)了數(shù)組到哈希表的轉(zhuǎn)換
1、在創(chuàng)建socket啟動(dòng)監(jiān)聽時(shí),會(huì)添加可讀事件到epoll,事件處理函數(shù)為ngx_event_accept,用于接收socket連接,分配connection連接,并調(diào)用ngx_listening_t對象的處理函數(shù)(ngx_http_init_connection)
2、socket連接成功后,nginx會(huì)等待客戶端發(fā)送HTTP請求,默認(rèn)會(huì)有60秒的超時(shí)時(shí)間,即60秒內(nèi)沒有接收到客戶端請求時(shí),斷開此連接,打印錯(cuò)誤日志。函數(shù)ngx_http_init_connection用于設(shè)置讀事件處理函數(shù),以及超時(shí)定時(shí)器。
3、函數(shù)
ngx_http_wait_request_handler為解析HTTP請求的入口函數(shù)
4、函數(shù)ngx_http_create_request創(chuàng)建并初始化ngx_http_request_t對象
5、解析完成請求行與請求頭,nginx就開始處理HTTP請求,并沒有等到解析完請求體再處理。處理請求入口為ngx_http_process_request。
下面進(jìn)入nginx http請求處理的11個(gè)階段
絕大多數(shù)HTTP模塊都會(huì)將自己的handler添加到某個(gè)階段(將handler添加到全局唯一的數(shù)組ngx_http_phases中),注意其中有4個(gè)階段不能添加自定義handler,nginx處理HTTP請求時(shí)會(huì)挨個(gè)調(diào)用每個(gè)階段的handler
typedef enum {
NGX_HTTP_POST_READ_PHASE = 0, //第一個(gè)階段,目前只有realip模塊會(huì)注冊handler,但是該模塊默認(rèn)不會(huì)運(yùn)行(nginx作為代理服務(wù)器時(shí)有用,后端以此獲取客戶端原始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, //檢查是否發(fā)生了url重寫,如果有,重新回到FIND_CONFIG階段;不能自定義handler;
NGX_HTTP_PREACCESS_PHASE, //訪問控制,比如限流模塊會(huì)注冊handler到此階段
NGX_HTTP_ACCESS_PHASE, //訪問權(quán)限控制,比如基于ip黑白名單的權(quán)限控制,基于用戶名密碼的權(quán)限控制等
NGX_HTTP_POST_ACCESS_PHASE, //根據(jù)訪問權(quán)限控制階段做相應(yīng)處理;不能自定義handler;
NGX_HTTP_TRY_FILES_PHASE, //只有配置了try_files指令,才會(huì)有此階段;不能自定義handler;
NGX_HTTP_CONTENT_PHASE, //內(nèi)容產(chǎn)生階段,返回響應(yīng)給客戶端
NGX_HTTP_LOG_PHASE //日志記錄
} ngx_http_phases;
nginx 在ngx_http_block函數(shù)中初始化11個(gè)階段的ngx_http_phases數(shù)組,把http模塊注冊到相應(yīng)的階段去。注意多個(gè)模塊可能注冊到同一個(gè)階段,因此phases是一個(gè)二維數(shù)組
- nginx使用結(jié)構(gòu)體ngx_module_s表示一個(gè)模塊,其中字段ctx,是一個(gè)指向模塊上下文結(jié)構(gòu)體的指針(上下文結(jié)構(gòu)體的字段都是一些函數(shù)指針)
- postconfiguration,負(fù)責(zé)注冊本模塊的handler到某個(gè)處理階段
使用GDB調(diào)試,斷點(diǎn)到ngx_http_block方法執(zhí)行所有HTTP模塊注冊handler之后,打印phases數(shù)組:
p cmcf- >phases[*].handlers
p *(ngx_http_handler_pt*)cmcf- >phases[*].handlers.elts
11個(gè)階段(7個(gè)階段可注冊)以及模塊注冊的handler如下圖:
處理請求的過程
1、HTTP請求的處理入口函數(shù)是ngx_http_process_request,其主要調(diào)用ngx_http_core_run_phases實(shí)現(xiàn)11個(gè)階段的執(zhí)行流程
2、ngx_http_core_run_phases遍歷預(yù)先設(shè)置好的cmcf->phase_engine.handlers數(shù)組,調(diào)用其checker函數(shù)
3、checker內(nèi)部就是調(diào)用handler,并設(shè)置下一步要執(zhí)行handler的索引
所以綜上看來,nginx處理請求的過程可以歸納為:
- 初始化 HTTP Request(讀取來自客戶端的數(shù)據(jù),生成 HTTP Request 對象,該對象含有該請求所有的信息)。
- 處理請求頭。
- 處理請求體。
- 如果有的話,調(diào)用與此請求(URL 或者 Location)關(guān)聯(lián)的 handler。
- 依次調(diào)用各 phase handler 進(jìn)行處理。
-
網(wǎng)卡
+關(guān)注
關(guān)注
4文章
307瀏覽量
27374 -
驅(qū)動(dòng)程序
+關(guān)注
關(guān)注
19文章
831瀏覽量
48026 -
數(shù)據(jù)包
+關(guān)注
關(guān)注
0文章
260瀏覽量
24385 -
nginx
+關(guān)注
關(guān)注
0文章
149瀏覽量
12170
發(fā)布評(píng)論請先 登錄
相關(guān)推薦
評(píng)論