Linux网络-数据包的接收流程(基于RTL8139网卡驱动程序)
本文將介紹Linux系統中,基于RTL8139網卡驅動程序,是如何一步一步將接收到的數據包傳送到內核的網絡協議棧的。
下圖展示了數據包(packet)如何進入內存,并被內核的網絡模塊開始處理:
+-----+| | 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|↓1.數據包從外部網絡傳送到物理網卡。如果目的地址不是該網卡,且該網卡沒有開啟混雜模式,該包會被網卡丟棄。
2.網卡將數據包以DMA的方式寫進指定的內存地址。該地址是由網卡驅動程序分配并初始化的。
3.數據來了之后,網卡產生一個硬件中斷(IRQ)告訴CPU。
4.CPU會根據中斷向量表,調用中斷處理函數,這個中斷處理函數會調用網卡驅動程序中的中斷函數。
5.驅動先禁用網卡的中斷,因為驅動程序已經知道內存中有數據了,告訴網卡驅動程序下次再有數據,直接寫進內存中,不用再通知CPU了,這樣可以提高效率。避免CPU被不停的中斷打擾。
6.啟用軟中斷。這步結束后,硬件中斷就結束返回了。由于硬中斷在執行程序的過程中,不能被中斷,所以如果執行時間過長,會導致CPU沒法響應其他硬件的中斷,于是引入了軟中斷,這樣可以將硬中斷處理函數中比較耗時部分,交給軟中斷處理函數里慢慢處理。
軟中斷會觸發內核網絡模塊中的軟中斷處理函數,流程如下:
+-----+17 | |+----------->| NIC || | ||Enable IRQ +-----+||+------------+ Memroy| | Read +--------+--------+--------+--------++--------------->| NIC Driver |<--------------------- | Packet | Packet | Packet | ...... || | | 9 +--------+--------+--------+--------+| +------------+| | | skbPoll | 8 Raise softIRQ | 6 +-----------------+| | 10 || ↓ ↓+---------------+ Call +-----------+ +------------------+ +--------------------+ 12 +---------------------+| net_rx_action |<-------| ksoftirqd | | napi_gro_receive |------->| enqueue_to_backlog |----->| CPU input_pkt_queue |+---------------+ 7 +-----------+ +------------------+ 11 +--------------------+ +---------------------+| | 1314 | + - - - - - - - - - - - - - - - - - - - - - - +↓ ↓+--------------------------+ 15 +------------------------+| __netif_receive_skb_core |----------->| packet taps(AF_PACKET) |+--------------------------+ +------------------------+|| 16↓+-----------------+| protocol layers |+-----------------+7 內核中的ksoftirqd進程專門負責軟中斷的處理,當它收到軟中斷后,就會調用相應軟中斷所對應的處理函數,對于上面第6步中是網卡驅動模塊拋出的軟中斷,ksoftirqd會調用網絡模塊的net_rx_action函數
8 net_rx_action函數會調用網卡驅動程序里的poll函數來一個一個處理數據包。
9 在poll函數中,網卡驅動程序一個接一個讀取網卡寫到內存中的數據包,內存中的數據包的格式只有驅動程序知道。
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會接著在自己的軟中斷上下文中調用__netif_receive_skb_core函數處理自己input_pkt_queue里的網絡數據。
14 如果沒開啟RPS,napi_gro_receive會直接調用__netif_receive_skb_core
15 看是不是有AF_PACKET類型的socket(也就是我們常說的原始套接字),如果有的話,拷貝一份數據給它。tcpdump抓包就是抓的這里的包。
16 調用協議棧相應的函數,將數據包交給協議棧處理。
17 待內存中的所有數據包被處理完成后(即poll函數執行完成),啟用網卡的硬中斷,這樣下次網卡再收到數據的時候就會通知CPU。
以上分析參考地址:
點擊查看原文
以上是大致分析了一下數據包是如何從網卡傳送到內核網絡協議棧的。那么下面我們就分析網卡驅動程序里面的相關函數:
一.中斷函數
首先是中斷函數rtl8139_interrupt(),當硬件中斷后,CPU會調用網卡驅動程序的該函數,rtl8139_interrupt函數是在rtl8139_open函數中注冊的:
中斷函數處理的中斷事件可以大致分為幾類:
A 數據包到達產生的中斷(RxAckBits = RxFIFOOver | RxOverflow | RxOK);
B 異常事件,通常都是出錯的情況(RxAckBits = RxFIFOOver | RxOverflow | RxOK)
C發送完成事件(TxOK | TxErr)
我們先來看中斷函數:
/* The interrupt handler does all of the Rx thread work and cleans upafter the Tx thread. */ static irqreturn_t rtl8139_interrupt (int irq, void *dev_instance) {/* 參數dev_instance是在上面注冊中斷處理函數的時候傳入的 */struct net_device *dev = (struct net_device *) dev_instance;/* tp 為網卡驅動自定義的驅動特有的數據,和dev一起分配的 */struct rtl8139_private *tp = netdev_priv(dev);void __iomem *ioaddr = tp->mmio_addr;u16 status, ackstat;int link_changed = 0; /* avoid bogus "uninit" warning */int handled = 0;/* 對驅動數據加鎖*/spin_lock (&tp->lock);/*讀中斷狀態寄存器,獲取中斷狀態*/status = RTL_R16 (IntrStatus);/* shared irq? *//* 這時由共享此中斷號的其它設備產生的中斷 */if (unlikely((status & rtl8139_intr_mask) == 0))goto out;handled = 1;/* h/w no longer present (hotplug?) or major error, bail *//* 硬件錯誤 */if (unlikely(status == 0xFFFF))goto out;/* close possible race's with dev_close *//* 設備已關閉*/if (unlikely(!netif_running(dev))) {/* 屏蔽所有中斷*/RTL_W16 (IntrMask, 0);goto out;}/* Acknowledge all of the current interrupt sources ASAP, butan first get an additional status bit from CSCR. */if (unlikely(status & RxUnderrun))link_changed = RTL_R16 (CSCR) & CSCR_LinkChangeBit;ackstat = status & ~(RxAckBits | TxErr);if (ackstat)RTL_W16 (IntrStatus, ackstat);/* Receive packets are processed by poll routine.If not running start it now. *//* 下一步處理數據包到達事件 */if (status & RxAckBits){if (napi_schedule_prep(&tp->napi)) {RTL_W16_F (IntrMask, rtl8139_norx_intr_mask);/* 這個函數的分析在下面 */__napi_schedule(&tp->napi); //這里采用了NAPI新機制,暫時不詳細說明這個機制,會新開一篇文章,詳細講解一下}}/* 以下都是一些檢查,在此不做分析 *//* Check uncommon events with one test. */if (unlikely(status & (PCIErr | PCSTimeout | RxUnderrun | RxErr)))rtl8139_weird_interrupt (dev, tp, ioaddr,status, link_changed);if (status & (TxOK | TxErr)) {/* 發送完成事件處理 ,下面會分析*/rtl8139_tx_interrupt (dev, tp, ioaddr);if (status & TxErr)RTL_W16 (IntrStatus, TxErr);}out:spin_unlock (&tp->lock);netdev_dbg(dev, "exiting interrupt, intr_status=%#4.4x\n",RTL_R16(IntrStatus));return IRQ_RETVAL(handled); }__napi_schedule()函數的分析:
void __napi_schedule(struct napi_struct *n) {unsigned long flags;local_irq_save(flags); //這里應該是保存中斷標志位____napi_schedule(&__get_cpu_var(softnet_data), n); //去這個函數看看local_irq_restore(flags); //回復中斷標志位 }____napi_schedule()看看這個函數:
static inline void ____napi_schedule(struct softnet_data *sd,struct napi_struct *napi) {list_add_tail(&napi->poll_list, &sd->poll_list); //將當前設備加入CPU相關全局隊列softnet_data的輪詢設備列表中/* 調用函數產生網絡接收軟中斷。 */__raise_softirq_irqoff(NET_RX_SOFTIRQ); }二. 發送完成事件處理
下面我們分析rtl8139_tx_interrupt函數:
三.軟中斷處理函數
由于在前面的中斷處理程序中調用了__raise_softirq_irqoff(NET_RX_SOFTIRQ),CPU會在中斷處理完成后的適當的時候調用軟中斷處理函數,也就是我們在系統初始化的過程中注冊的net_rx_action函數。
static void net_rx_action(struct softirq_action *h) {/*獲取每個CPU的softnet_data結構,然后取得其poll_list */struct softnet_data *sd = &__get_cpu_var(softnet_data);unsigned long time_limit = jiffies + 2;int budget = netdev_budget;void *have;local_irq_disable();/* 處理poll_list上關聯的每一個設備*/while (!list_empty(&sd->poll_list)) {struct napi_struct *n;int work, weight;/* If softirq window is exhuasted then punt.* Allow this to run for 2 jiffies since which will allow* an average latency of 1.5/HZ.*/if (unlikely(budget <= 0 || time_after(jiffies, time_limit))) //如果收到數據的總數到了300個goto softnet_break;local_irq_enable();/* Even though interrupts have been re-enabled, this* access is safe because interrupts can only add new* entries to the tail of this list, and only ->poll()* calls can remove this head entry from the list.*/n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);have = netpoll_poll_lock(n);weight = n->weight;/* This NAPI_STATE_SCHED test is for avoiding a race* with netpoll's poll_napi(). Only the entity which* obtains the lock and sees NAPI_STATE_SCHED set will* actually make the ->poll() call. Therefore we avoid* accidentally calling ->poll() when NAPI is not scheduled.*/work = 0;/* 調用每個設備的pool方法接收數據*/if (test_bit(NAPI_STATE_SCHED, &n->state)) {work = n->poll(n, weight);trace_napi_poll(n);}WARN_ON_ONCE(work > weight);budget -= work; //更新budget,這樣能控制總收到的數據local_irq_disable();/* Drivers must not modify the NAPI state if they* consume the entire weight. In such cases this code* still "owns" the NAPI instance and therefore can* move the instance around on the list at-will.*/if (unlikely(work == weight)) {/* 設備運行出錯,或自己退出poll_list,就刪除它*/if (unlikely(napi_disable_pending(n))) {local_irq_enable();napi_complete(n);local_irq_disable();} else/* 該設備還有要接收的數據沒被處理,因為輪詢算法被移動到poll_llst尾部等待處理*/list_move_tail(&n->poll_list, &sd->poll_list);}netpoll_poll_unlock(have);} out:net_rps_action_and_irq_enable(sd);#ifdef CONFIG_NET_DMA/** There may not be any more sk_buffs coming right now, so push* any pending DMA copies to hardware*/dma_issue_pending_all(); #endifreturn;softnet_break:sd->time_squeeze++;__raise_softirq_irqoff(NET_RX_SOFTIRQ);goto out; }通常,在網卡收發數據的時候,需要維護一個緩沖區隊列,來緩存可能存在的突發數據,類似于前面的DMA環形緩沖區。隊列層中,包含了一個叫做struct softnet_data:
/** Incoming packets are placed on per-cpu queues*/ struct softnet_data {struct Qdisc *output_queue;struct Qdisc **output_queue_tailp;struct list_head poll_list;struct sk_buff *completion_queue;struct sk_buff_head process_queue;/* stats */unsigned int processed;unsigned int time_squeeze;unsigned int cpu_collision;unsigned int received_rps;#ifdef CONFIG_RPSstruct softnet_data *rps_ipi_list;/* Elements below can be accessed between CPUs for RPS */struct call_single_data csd ____cacheline_aligned_in_smp;struct softnet_data *rps_ipi_next;unsigned int cpu;unsigned int input_queue_head;unsigned int input_queue_tail; #endifunsigned dropped;struct sk_buff_head input_pkt_queue;struct napi_struct backlog; };下一步進入設備的poll函數。需要注意的是,如果是NAPI的網卡驅動的話,poll函數是在驅動中注冊的,驅動實現的;如果是非 NAPI的話,就是內核定義的process_backlog函數,至于process_backlog是如何添加到poll_list中的,這里暫時不 管,先看看8139驅動的poll 函數是如何實現的。
四.8139 poll函數實現
static int rtl8139_poll(struct napi_struct *napi, int budget) {struct rtl8139_private *tp = container_of(napi, struct rtl8139_private, napi);struct net_device *dev = tp->dev;void __iomem *ioaddr = tp->mmio_addr;int work_done;spin_lock(&tp->rx_lock);work_done = 0;/* 在 rtl8139_rx中將接送到的數據拷貝出來并傳遞給上層協議驅動。*/if (likely(RTL_R16(IntrStatus) & RxAckBits))work_done += rtl8139_rx(dev, tp, budget);/*說明沒有多余的數據到達,則恢復接收中斷,并把此設備從poll_list中清除*/if (work_done < budget) {unsigned long flags;/** Order is important since data can get interrupted* again when we think we are done.先關中斷,在寫中斷屏蔽位*/spin_lock_irqsave(&tp->lock, flags);__napi_complete(napi);RTL_W16_F(IntrMask, rtl8139_intr_mask);spin_unlock_irqrestore(&tp->lock, flags);}spin_unlock(&tp->rx_lock);return work_done; }從rtl8139_rx的代碼也可以看出,當數據包接收出錯或者是沒有更多的數據包可以接收時,work_done才不會達到budget,這時,應該讓網卡重新回到中斷的狀態,以等待數據包的到來。另外一種情況就是work_done等于budget,很可能是因為還有數據包要接收,所以在net_rx_action函數中,只是把該網卡設備移到隊列的尾部,以期待在下次循環中再次調用其poll函數。
下面看rtl8139_rx的實現
五.rtl8139_rx的實現
最后就是netif_receive_skb了,數據包從此離開鏈路層,提交給上層。
六.netif_receive_skb
netif_receive_skb(skb) 這是一個輔助函數,用于在poll中處理接收到的幀。它主要是向各個已注冊的協議處理例程發送一個SKB。
總結一下:
netif_rx是舊的收包函數
比如在某個網卡收到一個包后,首先就是調用這個函數
netif_rx把包放入一個每CPU隊列:
__skb_queue_tail(&queue->input_pkt_queue, skb);
并且raise軟中斷NET_RX_SOFTIRQ,讓它進一步處理包,因為收包是在網卡驅動的中斷中
最后軟中斷處理函數 net_rx_action會得到運行,這個函數會對每個收到包的設備調用其設備的出隊列函數, 把包從上面的隊列中拿出來(process_backlog函數),拿出來之后就會調用netif_receive_skb開始靠近協議棧,有很多人可能要處理它,比如PF_PACKET(tcpdump),bridge,等,如果最后包還在,那么就會進入協議棧的3層,對ipv4的包,調用了ipv4的包接收函數,ip_rcv,這個函數在簡單的校驗之后會到netfilter,如果還幸存,那就復雜了, 比如典型的最后就到tcp或者udp的收包程序,它們檢查有沒有socket需要,不需要就扔掉等。
通俗的講:
在netif_rx函數中會調用netif_rx_schedule, 然后該函數又會去調用__netif_rx_schedule
在函數__netif_rx_schedule中會去觸發軟中斷NET_RX_SOFTIRQ, 也即是去調用net_rx_action.
然后在net_rx_action函數中會去調用設備的poll函數, 它是設備自己注冊的.
在設備的poll函數中, 會去調用neif_receive_skb函數, 在該函數中有下面一條語句 pt_prev->func, 此處的func為一個函數指針, 在之前的注冊中設置為ip_rcv.
因此, 就完成了數據包從鏈路層上傳到網絡層的這一個過程了.
我們就分析到這里就行了,已經知道驅動程序是如何把數據傳送給內核了,至于再往后的操作,這里暫時不管了,以后有機會再分析。
想一起探討以及獲得各種學習資源加我(有我博客中寫的代碼的原稿):
qq:1126137994
微信:liu1126137994
可以共同交流關于嵌入式,操作系統,C++語言,C語言,數據結構等技術問題。
總結
以上是生活随笔為你收集整理的Linux网络-数据包的接收流程(基于RTL8139网卡驱动程序)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Eigen+suitesparse f
- 下一篇: gmssl编译linux,linux 编