1. 程式人生 > >網路資料包收發流程(一):從驅動到協議棧

網路資料包收發流程(一):從驅動到協議棧

早就想整理網路資料包收發流程了,一直太懶沒動筆。今天下決心寫了
一、硬體環境

intel82546:PHY與MAC整合在一起的PCI網絡卡晶片,很強大
bcm5461:   PHY晶片,與之對應的MAC是TSEC
TSEC:      Three Speed Ethernet Controller,三速乙太網控制器,PowerPc 架構CPU裡面的MAC模組
            注意,TSEC內部有DMA子模組  

話說現在的CPU越來越牛叉了,什麼功能都往裡面加,最常見的如MAC功能。
TSEC只是MAC功能模組的一種,其他架構的cpu也有和TSEC類似的MAC功能模組。
這些整合到CPU晶片上的功能模組有個學名,叫平臺裝置,即 platform device。



二、網路收包原理


網路驅動收包大致有3種情況:

no NAPI:mac每收到一個乙太網包,都會產生一個接收中斷給cpu,即完全靠中斷方式來收包
          缺點是當網路流量很大時,cpu大部分時間都耗在了處理mac的中斷。

netpoll:在網路和I/O子系統尚不能完整可用時,模擬了來自指定裝置的中斷,即輪詢收包。
         缺點是實時性差

NAPI: 採用 中斷 + 輪詢 的方式:mac收到一個包來後會產生接收中斷,但是馬上關閉。
       直到收夠了netdev_max_backlog個包(預設300),或者收完mac上所有包後,才再開啟接收中斷
通過sysctl來修改 net.core.netdev_max_backlog
       或者通過proc修改 /proc/sys/net/core/netdev_max_backlog


下面只寫核心配置成使用NAPI的情況,只寫TSEC驅動。(非NAPI的情況和PCI網絡卡驅動 以後再說)
核心版本 linux 2.6.24

三、NAPI 相關資料結構

每個網路裝置(MAC層)都有自己的net_device資料結構,這個結構上有napi_struct。
每當收到資料包時,網路裝置驅動會把自己的napi_struct掛到CPU私有變數上。
這樣在軟中斷時,net_rx_action會遍歷cpu私有變數的poll_list,
執行上面所掛的napi_struct結構的poll鉤子函式,將資料包從驅動傳到網路協議棧。

四、核心啟動時的準備工作

4.1 初始化網路相關的全域性資料結構,並掛載處理網路相關軟中斷的鉤子函式

start_kernel()
    --> rest_init()
        --> do_basic_setup()
            --> do_initcall
               -->net_dev_init

__init net_dev_init()
{
    //每個CPU都有一個CPU私有變數 _get_cpu_var(softnet_data)
    //_get_cpu_var(softnet_data).poll_list很重要,軟中斷中需要遍歷它的

    for_each_possible_cpu(i) {
        struct softnet_data *queue;
        queue = &per_cpu(softnet_data, i);
        skb_queue_head_init(&queue->input_pkt_queue);
        queue->completion_queue = NULL;
 INIT_LIST_HEAD(&queue->poll_list);
        queue->backlog.poll = process_backlog;
        queue->backlog.weight = weight_p;
    }
    open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL); //在軟中斷上掛網路傳送handler
    open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL); //在軟中斷上掛網路接收handler
}
4.2 載入網路裝置的驅動
NOTE:這裡的網路裝置是指MAC層的網路裝置,即TSEC和PCI網絡卡(bcm5461是phy)
在網路裝置驅動中建立net_device資料結構,並初始化其鉤子函式 open(),close() 等
掛載TSEC的驅動的入口函式是 gfar_probe

// 平臺裝置 TSEC 的資料結構
static struct platform_driver gfar_driver = {
    .probe = gfar_probe,
    .remove = gfar_remove,
    .driver = {
        .name = "fsl-gianfar",
    },
};

int gfar_probe(struct platform_device *pdev)
{
    dev = alloc_etherdev(sizeof (*priv)); // 建立net_device資料結構

    dev->open = gfar_enet_open;
    dev->hard_start_xmit = gfar_start_xmit;
    dev->tx_timeout = gfar_timeout;
    dev->watchdog_timeo = TX_TIMEOUT;
#ifdef CONFIG_GFAR_NAPI
    netif_napi_add(dev, &priv->napi,gfar_poll,GFAR_DEV_WEIGHT); //軟中斷裡會呼叫poll鉤子函式
#endif
#ifdef CONFIG_NET_POLL_CONTROLLER
    dev->poll_controller = gfar_netpoll;
#endif
    dev->stop = gfar_close;
    dev->change_mtu = gfar_change_mtu;
    dev->mtu = 1500;
    dev->set_multicast_list = gfar_set_multi;
    dev->set_mac_address = gfar_set_mac_address;
    dev->ethtool_ops = &gfar_ethtool_ops;
}

五、啟用網路裝置
5.1 使用者呼叫ifconfig等程式,然後通過ioctl系統呼叫進入核心
socket的ioctl()系統呼叫
    --> sock_ioctl()
        --> dev_ioctl()                              //判斷SIOCSIFFLAGS
          --> __dev_get_by_name(net, ifr->ifr_name)  //根據名字選net_device
             --> dev_change_flags()                  //判斷IFF_UP
                --> dev_open(net_device)             //呼叫open鉤子函式 

 對於TSEC來說,掛的鉤子函式是 gfar_enet_open(net_device)

5.2 在網路裝置的open鉤子函式裡,分配接收bd,掛中斷ISR(包括rx、tx、err),對於TSEC來說

gfar_enet_open
    --> 給Rx Tx Bd 分配一致性DMA記憶體 
    --> 把Rx Bd的“EA地址”賦給資料結構,實體地址賦給TSEC暫存器
    --> 把Tx Bd的“EA地址”賦給資料結構,實體地址賦給TSEC暫存器
    --> 給 tx_skbuff 指標陣列 分配記憶體,並初始化為NULL
    --> 給 rx_skbuff 指標陣列 分配記憶體,並初始化為NULL

    --> 初始化Tx Bd
    --> 初始化Rx Bd,提前分配儲存乙太網包的skb,這裡使用的是一次性dma對映
       (注意:#define DEFAULT_RX_BUFFER_SIZE  1536保證了skb能存一個乙太網包)
        rxbdp = priv->rx_bd_base;
        for (i = 0; i < priv->rx_ring_size; i++) {
            struct sk_buff *skb = NULL;
            rxbdp->status = 0;
          //這裡真正分配skb,並且初始化rxbpd->bufPtr, rxbdpd->length
 skb = gfar_new_skb(dev, rxbdp); 
priv->rx_skbuff[i] = skb;
            rxbdp++;
        }
        rxbdp--;
        rxbdp->status |= RXBD_WRAP; // 給最後一個bd設定標記WRAP標記

    --> 註冊TSEC相關的中斷handler: 錯誤,接收,傳送
        request_irq(priv->interruptError, gfar_error, 0, "enet_error", dev)
        request_irq(priv->interruptTransmit, gfar_transmit, 0, "enet_tx", dev)//包傳送完
        request_irq(priv->interruptReceive, gfar_receive, 0, "enet_rx", dev)  //包接收完

    -->gfar_start(net_device)
        // 使能Rx、Tx
        // 開啟TSEC的 DMA 暫存器
        // Mask 掉我們不關心的中斷event


最終,TSEC相關的Bd等資料結構應該是下面這個樣子的

六、中斷裡接收乙太網包

TSEC的RX已經使能了,網路資料包進入記憶體的流程為:
    網線 --> Rj45網口 --> MDI 差分線
         --> bcm5461(PHY晶片進行數模轉換) --> MII匯流排 
         --> TSEC的DMA Engine 會自動檢查下一個可用的Rx bd 
         --> 把網路資料包 DMA 到 Rx bd 所指向的記憶體,即skb->data

接收到一個完整的乙太網資料包後,TSEC會根據event mask觸發一個 Rx 外部中斷。
cpu儲存現場,根據中斷向量,開始執行外部中斷處理函式do_IRQ()

do_IRQ 虛擬碼
{
   上半部處理硬中斷
       檢視中斷源暫存器,得知是網路外設產生了外部中斷
       執行網路裝置的rx中斷handler(裝置不同,函式不同,但流程類似,TSEC是gfar_receive
          1. mask 掉 rx event,再來資料包就不會產生rx中斷
          2. 給napi_struct.state加上 NAPI_STATE_SCHED 狀態
          3. 掛網路裝置自己的napi_struct結構到cpu私有變數_get_cpu_var(softnet_data).poll_list
          4. 觸發網路接收軟中斷
    下半部處理軟中斷
        依次執行所有軟中斷handler,包括timer,tasklet等等
        執行網路接收的軟中斷handler  net_rx_action
          1. 遍歷cpu私有變數_get_cpu_var(softnet_data).poll_list 
          2. 取出poll_list上面掛的napi_struct 結構,執行鉤子函式napi_struct.poll()
(裝置不同,鉤子函式不同,流程類似,TSEC是gfar_poll)
          3. 若poll鉤子函式處理完所有包,則開啟rx event mask,再來資料包的話會產生rx中斷
          4. 呼叫napi_complete(napi_struct *n)
             把napi_struct 結構從_get_cpu_var(softnet_data).poll_list 上移走
             同時去掉 napi_struct.state 的 NAPI_STATE_SCHED 狀態
}

6.1 TSEC的接收中斷處理函式
gfar_receive
{
#ifdef CONFIG_GFAR_NAPI
    // test_and_set當前net_device的napi_struct.state 為 NAPI_STATE_SCHED
    // 在軟中斷裡呼叫 net_rx_action 會檢查狀態 napi_struct.state

    if (netif_rx_schedule_prep(dev, &priv->napi)) {  
        tempval = gfar_read(&priv->regs->imask);            
        tempval &= IMASK_RX_DISABLED; //mask掉rx,不再產生rx中斷
        gfar_write(&priv->regs->imask, tempval);    
        // 將當前net_device的 napi_struct.poll_list 掛到
        // CPU私有變數__get_cpu_var(softnet_data).poll_list 上,並觸發軟中斷
        // 所以,在軟中斷中呼叫 net_rx_action 的時候,就會執行當前net_device的
        // napi_struct.poll()鉤子函式,即 gfar_poll()

__netif_rx_schedule(dev, &priv->napi);   
    } 
#else
    gfar_clean_rx_ring(dev, priv->rx_ring_size);
#endif
}

6.2 網路接收軟中斷net_rx_action
net_rx_action()
{
    struct list_head *list = &__get_cpu_var(softnet_data).poll_list;    
    //通過 napi_struct.poll_list, 將N多個 napi_struct 連結到一條鏈上 
    //通過 CPU私有變數,我們找到了鏈頭,然後開始遍歷這個鏈


    int budget = netdev_budget; //這個值就是 net.core.netdev_max_backlog,通過sysctl來修改

    while (!list_empty(list)) {
        struct napi_struct *n;
        int work, weight;
        local_irq_enable();
//從鏈上取一個 napi_struct 結構(接收中斷處理函式里加到連結串列上的,如gfar_receive)
        n = list_entry(list->next, struct napi_struct, poll_list);
        weight = n->weight;
        work = 0;
        if (test_bit(NAPI_STATE_SCHED, &n->state)) //檢查狀態標記,此標記在接收中斷里加上的
            work = n->poll(n, weight); //使用NAPI的話,使用的是網路裝置自己的napi_struct.poll
                                       //對於TSEC是,是gfar_poll
        WARN_ON_ONCE(work > weight);
        budget -= work;
        local_irq_disable();

        if (unlikely(work == weight)) {
            if (unlikely(napi_disable_pending(n)))
                __napi_complete(n); //操作napi_struct,把去掉NAPI_STATE_SCHED狀態,從連結串列中刪去
            else
                list_move_tail(&n->poll_list, list);
        }
        netpoll_poll_unlock(have);
    }
out:
    local_irq_enable();
}

static int gfar_poll(struct napi_struct *napi, int budget)
{
    struct gfar_private *priv = container_of(napi, struct gfar_private, napi);
    struct net_device *dev = priv->dev;  //TSEC對應的網路裝置
    int howmany;  
//根據dev的rx bd,獲取skb並送入協議棧,返回處理的skb的個數,即乙太網包的個數
    howmany = gfar_clean_rx_ring(dev, budget);
   // 下面這個判斷比較有講究的
    // 收到的包的個數小於budget,代表我們在一個軟中斷裡就全處理完了,所以開啟 rx硬中斷
    // 要是收到的包的個數大於budget,表示一個軟中斷裡處理不完所有包,那就不開啟 rx硬中斷
    // 此次軟中斷的下一輪迴圈裡再接著處理,直到包處理完(即howmany<budget),再開啟 rx硬中斷

    if (howmany < budget) {        
        netif_rx_complete(dev, napi);
        gfar_write(&priv->regs->rstat, RSTAT_CLEAR_RHALT);
//開啟 rx 硬中斷,rx 硬中斷是在gfar_receive()中被關閉的
        gfar_write(&priv->regs->imask, IMASK_DEFAULT); 
    }
    return howmany;
}          

gfar_clean_rx_ring(dev, budget)
{
    bdp = priv->cur_rx;