Linux網路底層收發探究
一、基本框架
簡單看了一下p1020核心中,網路底層的資料收發
先看一下linux核心中網路的層次結構
也是基本按照7層來構造
由於從ip層(網路層)往上就比較統一了,這裡主要分析硬體層和鏈路層
二、名詞解釋
1.NAPI
CPU資料接收靠中斷和輪詢的配合,達到較高的收發效率。
CPU接收外部資料時一般採用中斷的方式,中斷的好處是響應及時,如果資料量較小,則不會佔用太多的CPU事件;缺點是資料量大時,會產生過多中斷,
而每個中斷都要消耗不少的CPU時間,從而導致效率反而不如輪詢高。輪詢方式與中斷方式相反,它更適合處理大量資料,因為每次輪詢不需要消耗過多的CPU時間;缺點是即使只接收很少資料或不接收資料時,也要佔用CPU時間。
NAPI是兩者的結合,資料量低時採用中斷,資料量高時採用輪詢。平時是中斷方式,當有資料到達時,會觸發中斷,處理函式會關閉中斷開始處理。如果此時有資料到達,則沒必要再觸發中斷了,因為中斷處理函式會輪詢處理資料,直到沒有新資料時才打開中斷。
很明顯,資料量很低與很高時,NAPI可以發揮中斷與輪詢方式的優點,效能較好。
//如果資料量不穩定,且說高不高說低不低,則NAPI則會在兩種方式切換上消耗不少時間,效率反而較低一些。
2.CPU私有變數
通過struct softnet_data __get_cpu_var(softnet_data)獲取到的CPU的私有變數,硬中斷中將資料掛上,軟中斷中取出來處理
三、驅動分析
P1020的物理層驅動(TSEC驅動)主要在/drivers/net/gianfar.c檔案中
鏈路層驅動主要在/net/core/dev.c
1.初始化
gianfar.c函式gfar_probe執行了初始化的操作。比較重要的就是napi的註冊,
呼叫函式netif_napi_add將gfar_poll註冊進NAPI中
netif_napi_add(dev, &priv->gfargrp[i].napi, gfar_poll, GFAR_DEV_WEIGHT);
另一段比較重要的初始化在gfar_enet_open中,先來看一下gfar裝置的檔案操作符可以使用的函式
static const struct net_device_opsgfar_netdev_ops = {
.ndo_open= gfar_enet_open,
.ndo_start_xmit= gfar_start_xmit,
.ndo_stop= gfar_close,
.ndo_change_mtu= gfar_change_mtu,
.ndo_set_multicast_list= gfar_set_multi,
.ndo_tx_timeout= gfar_timeout,
.ndo_do_ioctl= gfar_ioctl,
.ndo_get_stats= gfar_get_stats,
.ndo_vlan_rx_register= gfar_vlan_rx_register,
.ndo_set_mac_address= eth_mac_addr,
.ndo_validate_addr= eth_validate_addr,
。。。
};
當通過IOCTL操作gfar這個網路裝置時,呼叫gfar_enet_open函式執行初始化
static int gfar_enet_open(struct net_device*dev)
{
enable_napi(priv); //使能NAPI
gfar_set_mac_address(dev); //設定mac
err= init_phy(dev); //初始化phy
err= startup_gfar(dev); //…
netif_tx_start_all_queues(dev); //初始化佇列
device_set_wakeup_enable(&priv->ofdev->dev,priv->wol_en);
。。。
}
其中startup_gfar又進行了一系列初始化,然後重要的來了
它呼叫了函式register_grp_irqs進行IRQ的註冊
static int register_grp_irqs(structgfar_priv_grp *grp)
{
request_irq(grp->interruptError,gfar_error,0, grp->int_name_er,grp));
request_irq (grp->interruptTransmit,gfar_transmit, 0, grp->int_name_tx, grp);
request_irq(grp->interruptReceive,gfar_receive, 0, grp->int_name_rx, grp);
request_irq (grp->interruptTransmit,gfar_interrupt,0, grp->int_name_tx, grp));
}
收發報文的中斷已經註冊好,初始化細節不再扣,主要看看報文的流向
2.報文流向——報文接收
(1).簡析
網路資料包進入核心的流程為:
Rj45網口à PHY --> MII à TSEC的DMA Engine à把網路資料包 DMA 到記憶體
à完整報文觸發Rx IRQ à gfar_receiveà net_rx_action ànetif_receive_skb這裡就是協議棧了
前面DMA部分配置好後,收到的資料報文會自動搬移到記憶體中,產生中斷後,中斷處理會到指定的地址尋找,後續再分析DMA部分的細節,下面是比較重要的處理函式解釋。
IRQ處理函式:
gfar_receive
1.關閉RX中斷 //這裡是NAPI的重點,關閉中斷,後續到來的資料不會產生中斷
2.置NAPI_STATE_SCHED狀態
3.將napi_struct掛到CPU私有變量表上 //報文DMA後的記憶體地址等,中斷下半段處理報文
4.觸發軟中斷
軟中斷處理函式:
net_rx_action
1.遍歷CPU私有變量表,取出napi_struct結構
2.執行napi_struct上的poll函式,迴圈處理DMA到記憶體的資料包 //前面中斷關閉了,poll函式會遍歷所有地址查詢資料包,也就是輪詢模式
3.若資料包處理完或達到netdev_max_backlog個報文則開RX中斷 //資料包處理完或達到這個閾值(一般為300)就會結束輪詢,重新進入中斷模式
4.呼叫napi_complete函式將napi_struct結構從CPU私有變數移走並去掉NAPI_STATE_SCHED狀態
(2).程式碼詳細
直接從gfar_receive入手
硬中斷部分
gfar_receive
à gfar_write(&grp->regs->ievent,IEVENT_RX_MASK); //關閉收中斷
à napi_schedule_prep //置NAPI_STATE_SCHED狀態
à__napi_schedule(&grp->napi);
à____napi_schedule(&__get_cpu_var(softnet_data), n); //將napi_struct掛到cpu var上
à__raise_softirq_irqoff(NET_RX_SOFTIRQ); //觸發軟中斷
具體分析
gfar_receive
{
/*
* Clear IEVENT, so interrupts aren't calledagain
* because of the packets that have alreadyarrived.
*/
gfar_write(&grp->regs->ievent,IEVENT_RX_MASK); //關閉收中斷
if(napi_schedule_prep(&grp->napi)) { //見下
tempval= gfar_read(&grp->regs->imask);
tempval&= IMASK_RX_DISABLED;
gfar_write(&grp->regs->imask, tempval); //關RX mask
__napi_schedule(&grp->napi); //見下
…
}
static inline int napi_schedule_prep(structnapi_struct *n)
{
return!napi_disable_pending(n) &&
!test_and_set_bit(NAPI_STATE_SCHED,&n->state); //置NAPI_STATE_SCHED狀態
}
void __napi_schedule(structnapi_struct *n)
{
local_irq_save(flags);
____napi_schedule(&__get_cpu_var(softnet_data),n); //將napi_struct掛到cpu var上,詳見下面
local_irq_restore(flags);
}
static inline void ____napi_schedule(struct softnet_data *sd,
struct napi_struct *napi)
{
list_add_tail(&napi->poll_list,&sd->poll_list); //掛變數
__raise_softirq_irqoff(NET_RX_SOFTIRQ); //觸發軟中斷
}
軟中斷部分
net_rx_action
àwork = n->poll(n, weight);
àgfar_clean_rx_ring //遍歷記憶體資料包地址,獲取報文
àgfar_process_frame //對報文進行校驗和乙太網型別判斷
ànetif_receive_skb //Send the packet up to the stack
ànext_bd //下一條報文
ànapi_complete(n); //輪詢完成,恢復中斷
具體分析
net_rx_action
{
structsoftnet_data *sd = &__get_cpu_var(softnet_data); //獲取CPU私有變數
while(!list_empty(&sd->poll_list)) { //遍歷poll_list
if(test_bit(NAPI_STATE_SCHED, &n->state)) {
work =n->poll(n, weight); //執行驅動POLL方法,見下
trace_napi_poll(n);
}
}
}
關於這個POLL方法,是否還記得初始化時註冊的gfar_poll ? 就是這個函式
gfar_poll
{
while(num_queues && left_over_budget) {
rx_cleaned_per_queue= gfar_clean_rx_ring(rx_queue, //報文獲取,詳見下
budget_per_queue);
}
if(rx_cleaned < budget) { //判斷當前處理的報文數量是否已經達到閾值,達到則關閉NAPI輪詢模式,開啟Rx中斷
napi_complete(napi);
/* Clear the halt bit inRSTAT */
gfar_write(®s->rstat,gfargrp->rstat);
gfar_write(®s->imask,IMASK_DEFAULT);
}
}
gfar_clean_rx_ring
{
skb= rx_queue->rx_skbuff[rx_queue->skb_currx]; //獲取報文
pkt_len= bdp->length - ETH_FCS_LEN; //去掉校驗
/*Remove the FCS from the packet length */
skb_put(skb,pkt_len);
gfar_process_frame(dev, skb, amount_pull); //報文處理,詳見下
/*Add another skb for the future */
newskb= gfar_new_skb(dev); //為下一條報文分配空間
/*Setup the new bdp */
gfar_new_rxbdp(rx_queue,bdp, newskb); //DMA報文記憶體地址NEXT
/*Update to the next pointer */
bdp= next_bd(bdp, base, rx_queue->rx_ring_size);
}
gfar_process_frame
{
if (priv->rx_csum_enable) //報文校驗
gfar_rx_checksum(skb,fcb);
/*Tell the skb what kind of packet this is */
skb->protocol= eth_type_trans(skb, dev); //獲取乙太網型別
/*Send the packet up the stack */
if(unlikely(priv->vlgrp && (fcb->flags & RXFCB_VLN)))
ret= vlan_hwaccel_receive_skb(skb, priv->vlgrp, fcb->vlctl);
else
ret =netif_receive_skb(skb); //傳送到上層協議棧
}
到上層協議棧基本就告別了P1020的TSEC驅動部分
以前的平臺就在netif_receive_skb裡面添加了一些協議報文的處理,以後也可以繼續新增
本文永久更新連結: http://embeddedlinux.org.cn/emb-linux/system-development/201903/21-8594.html