之前兩篇文章給這篇做了鋪墊,分別講了。
不了解的朋友可以先回顧一下,不回顧也沒關係,跟着本文思路,如果實在順不下去,再了解也不遲,我們開始吧。這個數據包從很遙遠的另一台計算機發出,經歷重重艱難險阻,到達了這台計算機的網卡,這個過程可以閱讀《如果讓你來設計網絡》,總之現在這個數據包已經過來了。這個數據包來了之後,現在只是一堆電信號,離內核程序的處理還十萬八千里呢,需要先經歷網卡這個硬件的折磨。最開始就是我們常見的網線插口,之後會經過信號轉換的模塊 PHY,再經過 MAC 模塊,最後到達網卡內的一個緩衝區,注意哦這個是網卡這個硬件設備內的緩衝區,此時和內核代碼還一點關係也沒有。總之這個過程,實質上就是把網線中的高低電平,轉換到網卡上的一個緩衝區中存儲着。上一步,數據到達了網卡這個硬件的緩衝區中,現在要把它弄到內存中的緩衝區,簡單一張圖就是這樣。而且,這個過程完全不需要 CPU 參與,只需要 DMA 這個硬件設備,配合網卡這個硬件設備即可完成。當然,這個過程的前提是,網卡驅動需要在內存中申請一個緩衝區叫 sk_buffer,然後把這個 sk_buffer 的地址告訴網卡,這樣 DMA 才知道等網卡的緩衝區有數據到來時,把它拷貝到內存的什麼位置上。之前的部分就不展示代碼了,比較繁瑣,且對主流程的理解幫助不大。總之現在,這份數據包,已經從網卡內的緩衝區,然後通過 DMA 的方式,拷貝到了內存中的 sk_buffer 這個結構中。由於這個過程完全是由硬件完成的,所以下一步網卡該做的最後一件事,就是通知內核,讓內核去處理這個數據。網卡向 CPU 發起中斷信號,CPU 打斷當前的程序,根據中斷號找到中斷處理程序,開始執行。那我們主要去看,這個網卡收包這個中斷處理程序是什麼,以及它是如何註冊到中斷向量表中的。由於各個類型的網卡驅動程序是不同的,這裡我們拿 e1000 這個網卡驅動來舉例。我們在 e1000_main.c 中找到了這樣一行代碼。request_irq(netdev->irq,&e1000_intr,...);這段代碼的作用就是,當數據包從網卡緩衝區到內存中的 sk_buffer 後發出中斷,將會執行到 e1000_intr 這個中斷處理函數。drivers\net\e1000\e1000_main.c//註冊的硬中斷處理函數staticirqreturn_te1000_intr(intirq,void*data,structpt_regs*regs){__netif_rx_schedule(netdev);}include\linux\netdevice.hstaticinlinevoid__netif_rx_schedule(structnet_device*dev){list_add_tail(&dev->poll_list,&__get_cpu_var(softnet_data).poll_list);//發出軟中斷__raise_softirq_irqoff(NET_RX_SOFTIRQ);}沒錯,幾乎啥也沒幹,將網卡設備 dev 放入 poll_list 里,然後立刻發起了一次軟中斷,然後就結束了。軟中斷原理在《認認真真聊聊軟中斷》講過,其實就是修改 pending 的某個標誌位,然後內核中有一個線程不斷輪詢這組標誌位,看哪個是 1 了,就去軟中斷向量表里,尋找這個標誌位對應的處理程序,然後執行它。這是為了儘快響應硬中斷,以便計算機可以儘快處理下一個硬中斷,畢竟鼠標點擊、鍵盤敲擊等需要響應特別及時。而像網絡包到來後的拷貝和解析過程,在硬中斷面前優先級沒那麼高,所以就觸發一個軟中斷等着內核線程去執行就好了。剛剛代碼中我們就觸發了一個值為 NET_RX_SOFTIRQ 的軟中斷,那這個軟中斷會執行到哪個軟中斷處理函數呢?內核早在網絡子系統初始化的過程中,把這個軟中斷對應的處理函數註冊好了。staticint__initnet_dev_init(void){open_softirq(NET_TX_SOFTIRQ,net_tx_action,NULL);open_softirq(NET_RX_SOFTIRQ,net_rx_action,NULL);}//transmit發送staticvoidnet_tx_action(structsoftirq_action*h){...}//receive接收staticvoidnet_rx_action(structsoftirq_action*h){...}這個 open_softirq 就是註冊一個軟中斷函數,很簡單,就是把這個函數賦值給軟中斷向量表中對應位置的 action 上。還是上面的圖。這裡註冊了兩個軟中斷,一個發送,一個接收。我們這次是接收,所以軟中斷觸發後,就執行到了 net_rx_action 這個函數。staticvoidnet_rx_action(structsoftirq_action*h){structsoftnet_data*queue=&__get_cpu_var(softnet_data);while(!list_empty(&queue->poll_list)){structnet_devicedev=list_entry(queue->poll_list.next,structnet_device,poll_list); dev->poll(dev, &budget);}}遍歷 poll_list 取出一個個的設備 dev,然後調用其 poll 函數。還記得我們發起軟中斷前的一行代碼吧?正是把當前有數據包到來的這個網卡設備 dev 放入了這個 poll_list,現在又取出來了。由於要調用該網卡相應驅動的 poll 函數,那網卡初始化時,e1000 這款網卡的 poll 函數被附上了這個函數地址。netdev->poll=&e1000_clean;所以,接下來就看這個函數就好了,聽名字就知道是清理這個網卡的數據包的工作。drivers\net\e1000\e1000_main.cstaticinte1000_clean(structnet_device*netdev,int*budget){structe1000_adapter*adapter=netdev->priv;e1000_clean_tx_irq(adapter);e1000_clean_rx_irq(adapter,&work_done,work_to_do);}由於本講我們只看讀數據的過程,所以就看 rx 部分就好了。//drivers\net\e1000\e1000_main.ce1000_clean_rx_irq(structe1000_adapter*adapter){...netif_receive_skb(skb);...}//net\core\dev.cintnetif_receive_skb(structsk_buff*skb){...list_for_each_entry_rcu(ptype,&ptype_base[ntohs(type)&15],list){...deliver_skb(skb,ptype,0);...}...}static__inline__intdeliver_skb(structsk_buff*skb,structpacket_type*pt_prev,intlast){...returnpt_prev->func(skb,skb->dev,pt_prev);}我們看到,一路跟來,執行了 pt_prev 的 func 函數。這個函數是幹嘛的呢?或者先問,這個函數具體的實現指向的是哪個函數呢?這就涉及到協議棧的註冊。//net\ipv4\ip_output.cstaticstructpacket_typeip_packet_type={.type=__constant_htons(ETH_P_IP),.func=ip_rcv,};void__initip_init(void){dev_add_pack(&ip_packet_type);}//net\core\dev.cvoiddev_add_pack(structpacket_type*pt){if(pt->type==htons(ETH_P_ALL)){list_add_rcu(&pt->list,&ptype_all);}else{hash=ntohs(pt->type)&15;list_add_rcu(&pt->list,&ptype_base[hash]);}}我們看到,func 被賦值為了 ip_rcv,那上一步自然就執行到了這個函數,其實就是網絡層交給誰來負責解析的意思。那我們順便把傳輸層的協議註冊也看了吧,不難想到,ip_rcv 這個函數處理完必然交給傳輸層繼續處理。module_init(inet_init);staticstructinet_protocoltcp_protocol={.handler=tcp_v4_rcv,.err_handler=tcp_v4_err,.no_policy=1,};staticstructinet_protocoludp_protocol={.handler=udp_rcv,.err_handler=udp_err,.no_policy=1,};staticint__initinet_init(void){inet_add_protocol(&udp_protocol,IPPROTO_UDP);inet_add_protocol(&tcp_protocol,IPPROTO_TCP);ip_init();tcp_init();}非常直觀明了,記住上面兩個 handler 分別是 tcp_v4_rcv 和 udp_rcv。//net\ipv4\ip_input.cintip_rcv(structsk_buff*skb,structnet_device*dev,structpacket_type*pt){...returnNF_HOOK(PF_INET,NF_IP_PRE_ROUTING,skb,dev,NULL,ip_rcv_finish);}staticinlineintip_rcv_finish(structsk_buff*skb){...if(skb->dst==NULL){if(ip_route_input(skb,iph->daddr,iph->saddr,iph->tos,dev))gotodrop;}...returndst_input(skb);}//include\net\dst.h//rth->u.dst.input=ip_local_deliver;staticinlineintdst_input(structsk_buff*skb){...skb->dst->input(skb);...}//net\ipv4\ip_input.cintip_local_deliver(structsk_buff*skb){...returnNF_HOOK(PF_INET,NF_IP_LOCAL_IN,skb,skb->dev,NULL,ip_local_deliver_finish);}staticinlineintip_local_deliver_finish(structsk_buff*skb){...ipprot=inet_protos[hash];ipprot->handler(skb);...}OK,大功告成!最後執行了這個 handler,還記得上一節協議棧中註冊的吧?staticstructinet_protocoltcp_protocol={.handler=tcp_v4_rcv,.err_handler=tcp_v4_err,.no_policy=1,};staticstructinet_protocoludp_protocol={.handler=udp_rcv,.err_handler=udp_err,.no_policy=1,};由於網絡層解析到的傳輸層協議是 tcp,所以 handler 就指向了處理 tcp 協議的函數,tcp_v4_rcv!再往後就是 tcp 協議的處理流程,解析出來的數據,由應用程序去接受和處理,就是我們的 socket bind listen read 的流程了。這塊又是一片新天地,我還沒有研究,就寫到這吧!不過對於 TCP 的原理,可以讀這篇文章形象地了解下,《你管這破玩意叫 TCP》。你看,我們常說協議棧不斷去掉頭部,交給上層協議棧處理,這句話在代碼層面其實就是網絡層協議解析的方法 ip_rcv 里的末尾調用了傳輸層協議解析的方法 tcp_v4_rcv,僅此而已。而說 Linux 處理中斷是分上半部和下半部的方案,代碼層面就是硬中斷處理函數的代碼里,直接發起一個軟中斷,然後便返回,僅此而已。好啦,大家好好學習!不要再想為什麼不 ban 猛獁這個問題了。
鑽石舞台 發表在 痞客邦 留言(0) 人氣()