網絡卡驅動的資料包傳送接收
阿新 • • 發佈:2019-01-01
我們在許多網絡卡驅動中,都可以在網絡卡的中斷函式中見到這一過程。
但是,這一種方法,有一種重要的問題,就是大流量的資料來到,網絡卡會產生大量的中斷,核心在中斷上下文中,會浪費大量的資源來處理中斷本身。所以,一個問題是,“可不可以不使用中斷”,這就是輪詢技術,所謂NAPI技術,說來也不神祕,就是說,核心遮蔽中斷,然後隔一會兒就去問網絡卡,“你有沒有資料啊?”……
從這個描述本身可以看到,哪果資料量少,輪詢同樣佔用大量的不必要的CPU資源,大家各有所長吧,呵呵……
OK,另一個問題,就是從網絡卡的I/O區域,包括I/O暫存器或I/O記憶體中去讀取資料,這都要CPU去讀,也要佔用CPU資源,“CPU從I/O區域讀,然後把它放到記憶體(這個記憶體指的是系統本身的實體記憶體,跟外設的記憶體不相干,也叫主記憶體)中”。於是自然地,就想到了DMA技術——讓網絡卡直接從主記憶體之間讀寫它們的I/O資料,CPU,這兒不干你事,自己找樂子去:
引用 1、首先,核心在主記憶體中為收發資料建立一個環形的緩衝佇列(通常叫DMA環形緩衝區)。
2、核心將這個緩衝區通過DMA對映,把這個佇列交給網絡卡;
3、網絡卡收到資料,就直接放進這個環形緩衝區了——也就是直接放進主記憶體了;然後,向系統產生一箇中斷;
4、核心收到這個中斷,就取消DMA對映,這樣,核心就直接從主記憶體中讀取資料;
——呵呵,這一個過程比傳統的過程少了不少工作,因為裝置直接把資料放進了主記憶體,不需要CPU的干預,效率是不是提高不少?
對應以上4步,來看它的具體實現:
1、分配環形DMA緩衝區
Linux核心中,用skb來描述一個快取,所謂分配,就是建立一定數量的skb,然後把它們組織成一個雙向連結串列;
2、建立DMA對映
核心通過呼叫
dma_map_single(struct device *dev,void *buffer,size_t size,enum dma_data_direction direction)
建立對映關係。
struct device *dev,描述一個裝置;
buffer:把哪個地址對映給裝置;也就是某一個skb——要對映全部,當然是做一個雙向連結串列的迴圈即可;
size:快取大小;
direction:對映方向——誰傳給誰:一般來說,是“雙向”對映,資料在裝置和記憶體之間雙向流動;
對於PCI裝置而言(網絡卡一般是PCI的),通過另一個包裹函式pci_map_single,這樣,就把buffer交給裝置了!裝置可以直接從裡邊讀/取資料。
3、這一步由硬體完成;
4、取消對映
dma_unmap_single,對PCI而言,大多呼叫它的包裹函式pci_unmap_single,不取消的話,快取控制權還在裝置手裡,要呼叫它,把主動權掌握在CPU手裡——因為我們已經接收到資料了,應該由CPU把資料交給上層網路棧;
當然,不取消之前,通常要讀一些狀態位資訊,諸如此類,一般是呼叫
dma_sync_single_for_cpu()
讓CPU在取消對映前,就可以訪問DMA緩衝區中的內容。
關於DMA對映的更多內容,可以參考《Linux裝置驅動程式》“記憶體對映和DMA”章節相關內容!
OK,有了這些知識,我們就可以來看e100的程式碼了,它跟上面講的步驟基本上一樣的——繞了這麼多圈子,就是想繞到e100上面了,呵呵!
在e100_open函式中,呼叫e100_up,我們前面分析它時,略過了一個重要的東東,就是環形緩衝區的建立,這一步,是通過
e100_rx_alloc_list函式呼叫完成的:
e100_rx_alloc_list函式在一個迴圈中,建立了環形緩衝區,並呼叫e100_rx_alloc_skb為每個緩衝區分配了空間,並做了
DMA對映。這樣,我們就可以來看接收資料的過程了。
前面我們講過,中斷函式中,呼叫netif_rx_schedule,表明使用輪詢技術,系統會在未來某一時刻,呼叫裝置的poll函式:
目前,我們只關心rx,所以,e100_rx_clean函式就成了我們關注的對像,它用來從緩衝佇列中接收全部資料(這或許是取名為clean的原因吧!):
但是,這一種方法,有一種重要的問題,就是大流量的資料來到,網絡卡會產生大量的中斷,核心在中斷上下文中,會浪費大量的資源來處理中斷本身。所以,一個問題是,“可不可以不使用中斷”,這就是輪詢技術,所謂NAPI技術,說來也不神祕,就是說,核心遮蔽中斷,然後隔一會兒就去問網絡卡,“你有沒有資料啊?”……
從這個描述本身可以看到,哪果資料量少,輪詢同樣佔用大量的不必要的CPU資源,大家各有所長吧,呵呵……
OK,另一個問題,就是從網絡卡的I/O區域,包括I/O暫存器或I/O記憶體中去讀取資料,這都要CPU去讀,也要佔用CPU資源,“CPU從I/O區域讀,然後把它放到記憶體(這個記憶體指的是系統本身的實體記憶體,跟外設的記憶體不相干,也叫主記憶體)中”。於是自然地,就想到了DMA技術——讓網絡卡直接從主記憶體之間讀寫它們的I/O資料,CPU,這兒不干你事,自己找樂子去:
引用 1、首先,核心在主記憶體中為收發資料建立一個環形的緩衝佇列(通常叫DMA環形緩衝區)。
2、核心將這個緩衝區通過DMA對映,把這個佇列交給網絡卡;
3、網絡卡收到資料,就直接放進這個環形緩衝區了——也就是直接放進主記憶體了;然後,向系統產生一箇中斷;
4、核心收到這個中斷,就取消DMA對映,這樣,核心就直接從主記憶體中讀取資料;
——呵呵,這一個過程比傳統的過程少了不少工作,因為裝置直接把資料放進了主記憶體,不需要CPU的干預,效率是不是提高不少?
對應以上4步,來看它的具體實現:
1、分配環形DMA緩衝區
Linux核心中,用skb來描述一個快取,所謂分配,就是建立一定數量的skb,然後把它們組織成一個雙向連結串列;
2、建立DMA對映
核心通過呼叫
dma_map_single(struct device *dev,void *buffer,size_t size,enum dma_data_direction direction)
建立對映關係。
struct device *dev,描述一個裝置;
buffer:把哪個地址對映給裝置;也就是某一個skb——要對映全部,當然是做一個雙向連結串列的迴圈即可;
size:快取大小;
direction:對映方向——誰傳給誰:一般來說,是“雙向”對映,資料在裝置和記憶體之間雙向流動;
對於PCI裝置而言(網絡卡一般是PCI的),通過另一個包裹函式pci_map_single,這樣,就把buffer交給裝置了!裝置可以直接從裡邊讀/取資料。
3、這一步由硬體完成;
4、取消對映
dma_unmap_single,對PCI而言,大多呼叫它的包裹函式pci_unmap_single,不取消的話,快取控制權還在裝置手裡,要呼叫它,把主動權掌握在CPU手裡——因為我們已經接收到資料了,應該由CPU把資料交給上層網路棧;
當然,不取消之前,通常要讀一些狀態位資訊,諸如此類,一般是呼叫
dma_sync_single_for_cpu()
讓CPU在取消對映前,就可以訪問DMA緩衝區中的內容。
關於DMA對映的更多內容,可以參考《Linux裝置驅動程式》“記憶體對映和DMA”章節相關內容!
OK,有了這些知識,我們就可以來看e100的程式碼了,它跟上面講的步驟基本上一樣的——繞了這麼多圈子,就是想繞到e100上面了,呵呵!
在e100_open函式中,呼叫e100_up,我們前面分析它時,略過了一個重要的東東,就是環形緩衝區的建立,這一步,是通過
e100_rx_alloc_list函式呼叫完成的:
- staticint e100_rx_alloc_list(struct nic *nic)
- {
- struct rx *rx;
- unsigned int i, count = nic->params.rfds.count;
- nic->rx_to_use = nic->rx_to_clean = NULL;
- nic->ru_running = RU_UNINITIALIZED;
- /*結構struct rx用來描述一個緩衝區節點,這裡分配了count個*/
- if(!(nic->rxs = kmalloc(sizeof(struct rx) * count, GFP_ATOMIC)))
- return -ENOMEM;
- memset(nic->rxs, 0, sizeof(struct rx) * count);
- /*雖然是連續分配的,不過還是遍歷它,建立雙向連結串列,然後為每一個rx的skb指標分員分配空間
- skb用來描述核心中的一個數據包,呵呵,說到重點了*/
- for(rx = nic->rxs, i = 0; i < count; rx++, i++) {
- rx->next = (i + 1 < count) ? rx + 1 : nic->rxs;
- rx->prev = (i == 0) ? nic->rxs + count - 1 : rx - 1;
- if(e100_rx_alloc_skb(nic, rx)) { /*分配快取*/
- e100_rx_clean_list(nic);
- return -ENOMEM;
- }
- }
- nic->rx_to_use = nic->rx_to_clean = nic->rxs;
- nic->ru_running = RU_SUSPENDED;
- return 0;
- }
- #define RFD_BUF_LEN (sizeof(struct rfd) + VLAN_ETH_FRAME_LEN)
- staticinlineint e100_rx_alloc_skb(struct nic *nic, struct rx *rx)
- {
- /*skb快取的分配,是通過呼叫系統函式dev_alloc_skb來完成的,它同核心棧中通常呼叫alloc_skb的區別在於,
- 它是原子的,所以,通常在中斷上下文中使用*/
- if(!(rx->skb = dev_alloc_skb(RFD_BUF_LEN + NET_IP_ALIGN)))
- return -ENOMEM;
- /*初始化必要的成員 */
- rx->skb->dev = nic->netdev;
- skb_reserve(rx->skb, NET_IP_ALIGN);
- /*這裡在資料區之前,留了一塊sizeof(struct rfd) 這麼大的空間,該結構的
- 一個重要作用,用來儲存一些狀態資訊,比如,在接收資料之前,可以先通過
- 它,來判斷是否真有資料到達等,諸如此類*/
- memcpy(rx->skb->data, &nic->blank_rfd, sizeof(struct rfd));
- /*這是最關鍵的一步,建立DMA對映,把每一個緩衝區rx->skb->data都對映給了裝置,快取區節點
- rx利用dma_addr儲存了每一次對映的地址,這個地址後面會被用到*/
- rx->dma_addr = pci_map_single(nic->pdev, rx->skb->data,
- RFD_BUF_LEN, PCI_DMA_BIDIRECTIONAL);
- if(pci_dma_mapping_error(rx->dma_addr)) {
- dev_kfree_skb_any(rx->skb);
- rx->skb = 0;
- rx->dma_addr = 0;
- return -ENOMEM;
- }
- /* Link the RFD to end of RFA by linking previous RFD to
- * this one, and clearing EL bit of previous. */
- if(rx->prev->skb) {
- struct rfd *prev_rfd = (struct rfd *)rx->prev->skb->data;
- /*put_unaligned(val,ptr);用到把var放到ptr指標的地方,它能處理處理記憶體對齊的問題
- prev_rfd是在緩衝區開始處儲存的一點空間,它的link成員,也儲存了對映後的地址*/
- put_unaligned(cpu_to_le32(rx->dma_addr),
- (u32 *)&prev_rfd->link);
- wmb();
- prev_rfd->command &= ~cpu_to_le16(cb_el);
- pci_dma_sync_single_for_device(nic->pdev, rx->prev->dma_addr,
- sizeof(struct rfd), PCI_DMA_TODEVICE);
- }
- return 0;
- }
e100_rx_alloc_list函式在一個迴圈中,建立了環形緩衝區,並呼叫e100_rx_alloc_skb為每個緩衝區分配了空間,並做了
DMA對映。這樣,我們就可以來看接收資料的過程了。
前面我們講過,中斷函式中,呼叫netif_rx_schedule,表明使用輪詢技術,系統會在未來某一時刻,呼叫裝置的poll函式:
- staticint e100_poll(struct net_device *netdev, int *budget)
- {
- struct nic *nic = netdev_priv(netdev);
- unsigned int work_to_do = min(netdev->quota, *budget);
- unsigned int work_done = 0;
- int tx_cleaned;
- e100_rx_clean(nic, &work_done, work_to_do);
- tx_cleaned = e100_tx_clean(nic);
- /* If no Rx and Tx cleanup work was done, exit polling mode. */
- if((!tx_cleaned && (work_done == 0)) || !netif_running(netdev)) {
- netif_rx_complete(netdev);
- e100_enable_irq(nic);
- return 0;
- }
- *budget -= work_done;
- netdev->quota -= work_done;
- return 1;
- }
目前,我們只關心rx,所以,e100_rx_clean函式就成了我們關注的對像,它用來從緩衝佇列中接收全部資料(這或許是取名為clean的原因吧!):
- staticinlinevoid e100_rx_clean(struct nic *nic, unsigned int *work_done,
- unsigned int work_to_do)
- {
- struct rx *rx;
- int restart_required = 0;
- struct rx *rx_to_start = NULL;
- /* are we already rnr? then pay attention!!! this ensures that
- * the state machine progression never allows a start with a
- * partially cleaned list, avoiding a race between hardware
- * and rx_to_clean when in NAPI mode */
- if(RU_SUSPENDED == nic->ru_running)
- restart_required = 1;
- /* 函式最重要的工作,就是遍歷環形緩衝區,接收資料*/
- for(rx = nic->rx_to_clean; rx->skb; rx = nic->rx_to_clean = rx->next) {
- int err = e100_rx_indicate(nic, rx, work_done, work_to_do);
- if(-EAGAIN == err) {
- /* hit quota so have more work to do, restart once
- * cleanup is complete */
- restart_required = 0;
- break;
- } elseif(-ENODATA == err)
- break; /* No more to clean */
- }
- /* save our starting point as the place we'll restart the receiver */
- if(restart_required)
- rx_to_start = nic->rx_to_clean;
- /* Alloc new skbs to refill list */
- for(rx = nic->rx_to_use; !rx->skb; rx = nic->rx_to_use = rx->next) {
- if(unlikely(e100_rx_alloc_skb(nic, rx)))
- break; /* Better luck next time (see watchdog) */
- }
- if(restart_required) {
- // ack the rnr?
- writeb(stat_ack_rnr, &nic->csr->scb.stat_ack);
- e100_start_receiver(nic, rx_to_start);
- if(work_done)
- (*work_done)++;
- }
- }
- staticinlineint e100_rx_indicate(struct nic *nic, struct rx *rx,
- unsigned int *work_done, unsigned int work_to_do)
- {
- struct sk_buff *skb = rx->skb;
- struct rfd *rfd = (struct rfd *)skb->data;
- u16 rfd_status, actual_size;
- if(unlikely(work_done && *work_done >= work_to_do))
- return -EAGAIN;
- /* 讀取資料之前,也就是取消DMA對映之前,需要先讀取cb_complete 狀態位,
- 以確定資料是否真的準備好了,並且,rfd的actual_size中,也包含了真實的資料大小
- pci_dma_sync_single_for_cpu函式前面已經介紹過,它讓CPU在取消DMA對映之前,具備
- 訪問DMA快取的能力*/
- pci_dma_sync_single_for_cpu(nic->pdev, rx->dma_addr,
- sizeof(struct rfd), PCI_DMA_FROMDEVICE);
- rfd_status = le16_to_cpu(rfd->status);
- DPRINTK(RX_STATUS, DEBUG, "status=0x%04X\n", rfd_status);
- /* If data isn't ready, nothing to indicate */
- if(unlikely(!(rfd_status & cb_complete)))
- return -ENODATA;
- /* Get actual data size */
- actual_size = le16_to_cpu(rfd->actual_size) & 0x3FFF;
- if(unlikely(actual_size > RFD_BUF_LEN - sizeof(struct rfd)))
- actual_size = RFD_BUF_LEN - sizeof(struct rfd);
- /* 取消對映,因為通過DMA,網絡卡已經把資料放在了主記憶體中,這裡一取消,也就意味著,
- CPU可以處理主記憶體中的資料了 */
- pci_unmap_single(nic->pdev, rx->dma_addr,
- RFD_BUF_LEN, PCI_DMA_FROMDEVICE);
- /* this allows for a fast restart without re-enabling interrupts */
- if(le16_to_cpu(rfd->command) & cb_el)
- nic->ru_running = RU_SUSPENDED;
- /*正確地設定data指標,因為最前面有一個sizeof(struct rfd)大小區域,跳過它*/
- skb_reserve(skb, sizeof(struct rfd));
- /*更新skb的tail和len指標,也是就更新接收到這麼多資料的長度*/
- skb_put(skb, actual_size);
- /*設定協議位*/
- skb->protocol = eth_type_trans(skb, nic->netdev);
- if(unlikely(!(rfd_status & cb_ok))) {
- /* Don't indicate if hardware indicates errors */
- nic->net_stats.rx_dropped++;