17.5. 報文傳送

2018-02-24 15:50 更新

17.5.?報文傳送

網(wǎng)絡接口進行的最重要任務是數(shù)據(jù)發(fā)送和接收. 我們從發(fā)送開始, 因為它稍微易懂一些.

傳送指的是通過一個網(wǎng)絡連接發(fā)送一個報文的行為. 無論何時內(nèi)核需要傳送一個數(shù)據(jù)報文, 它調(diào)用驅(qū)動的 hard_start_stransmit 方法將數(shù)據(jù)放在外出隊列上. 每個內(nèi)核處理的報文都包含在一個 socket 緩存結構( 結構 sk_buff )里, 定義見<linux/skbuff.h>. 這個結構從 Unix 抽象中得名, 用來代表一個網(wǎng)絡連接, socket. 如果接口與 socket 沒有關系, 每個網(wǎng)絡報文屬于一個網(wǎng)絡高層中的 socket, 并且任何 socket 輸入/輸出緩存是結構 struct sk_buff 的列表. 同樣的 sk_buff 結構用來存放網(wǎng)絡數(shù)據(jù)歷經(jīng)所有 Linux 網(wǎng)絡子系統(tǒng), 但是對于接口來說, 一個 socket 緩存只是一個報文.

sk_buff 的指針通常稱為 skb, 我們在例子代碼和文本里遵循這個做法.

socket 緩存是一個復雜的結構, 內(nèi)核提供了一些函數(shù)來操作它. 在"Socket 緩存"一節(jié)中描述這些函數(shù); 現(xiàn)在, 對我們來說一個基本的關于 sk_buff 的事實就足夠來編寫一個能工作的驅(qū)動.

傳給 hard_start_xmit 的 socket 緩存包含物理報文, 它應當出現(xiàn)在媒介上, 以傳輸層的頭部結束. 接口不需要修改要傳送的數(shù)據(jù). skb->data 指向要傳送的報文, skb->len 是以字節(jié)計的長度. 如果你的驅(qū)動能夠處理發(fā)散/匯聚 I/O, 情形會稍稍復雜些; 我們在"發(fā)散/匯聚 I/O"一節(jié)中說它.

snull 報文傳送代碼如下; 網(wǎng)絡傳送機制隔離在另外一個函數(shù)里, 因為每個接口驅(qū)動必須根據(jù)特定的在驅(qū)動的硬件來實現(xiàn)它:


int snull_tx(struct sk_buff *skb, struct net_device *dev)
{
    int len;
    char *data, shortpkt[ETH_ZLEN];
    struct snull_priv *priv = netdev_priv(dev);
    data = skb->data;
    len = skb->len;
    if (len < ETH_ZLEN) {
        memset(shortpkt, 0, ETH_ZLEN);
        memcpy(shortpkt, skb->data, skb->len);
        len = ETH_ZLEN;
        data = shortpkt;
    }
    dev->trans_start = jiffies; /* save the timestamp */
    /* Remember the skb, so we can free it at interrupt time */
    priv->skb = skb;

    /* actual deliver of data is device-specific, and not shown here */ snull_hw_tx(data, len, dev);
    return 0; /* Our simple device can not fail */
}

傳送函數(shù), 因此, 只對報文進行一些合理性檢查并通過硬件相關的函數(shù)傳送數(shù)據(jù). 注意, 但是, 要小心對待傳送的報文比下面的媒介(對于 snull, 是我們虛擬的"以太網(wǎng)")支持的最小長度要短的情況. 許多 Linux 網(wǎng)絡驅(qū)動( 其他操作系統(tǒng)的也是 )已被發(fā)現(xiàn)在這種情況下泄漏數(shù)據(jù). 不是產(chǎn)生那種安全漏洞, 我們拷貝短報文到一個單獨的數(shù)組, 這樣我們可以清楚地零填充到足夠的媒介要求的長度. (我們可以安全地在堆棧中放數(shù)據(jù), 因為最小長度 -- 60 字節(jié) -- 是太小了).

hard_start_xmit 的返回值應當為 0 在成功時; 此時, 你的驅(qū)動已經(jīng)負責起報文, 應當盡全力保證發(fā)送成功, 并且必須在最后釋放 skb. 非 0 返回值指出報文這次不能發(fā)送; 內(nèi)核將稍后重試. 這種情況下, 你的驅(qū)動應當停止隊列直到已經(jīng)解決導致失敗的情況.

"硬件相關"的傳送函數(shù)( snull_hw_tx )這里忽略了, 因為它完全是來實現(xiàn)了 snull 設備的戲法, 包括假造源和目的地址, 對于真正的網(wǎng)絡驅(qū)動作者沒有任何吸引力. 當然, 它呈現(xiàn)在例子源碼里, 給那些想進入并看看它如何工作的人.

17.5.1.?控制發(fā)送并發(fā)

hard_start_xmit 函數(shù)由一個 net_device 結構中的自旋鎖(xmit_lock)來保護避免并發(fā)調(diào)用. 但是, 函數(shù)一返回, 它有可能被再次調(diào)用. 當軟件完成指導硬件報文發(fā)送的事情, 但是硬件傳送可能還沒有完成. 對 snull 這不是問題, 它使用 CPU 完成它所有的工作, 因此報文發(fā)送在傳送函數(shù)返回前就完成了.

真實的硬件接口, 另一方面, 異步發(fā)送報文并且具備有限的內(nèi)存來存放外出的報文. 當內(nèi)存耗盡(對某些硬件, 會發(fā)生在一個單個要發(fā)送的外出報文上), 驅(qū)動需要告知網(wǎng)絡系統(tǒng)不要再啟動發(fā)送直到硬件準備好接收新的數(shù)據(jù).

這個通知通過調(diào)用 netif_stop_queue 來實現(xiàn), 這個前面介紹過的函數(shù)來停止隊列. 一旦你的驅(qū)動已停止了它的隊列, 它必須安排在以后某個時間重啟隊列, 當它又能夠接受報文來發(fā)送了. 為此, 它應當調(diào)用:


void netif_wake_queue(struct net_device *dev); 

這個函數(shù)如同 netif_start_queue, 除了它還刺探網(wǎng)絡系統(tǒng)來使它又啟動發(fā)送報文.

大部分現(xiàn)代的網(wǎng)絡硬件維護一個內(nèi)部的有多個發(fā)送報文的隊列; 以這種方式, 它可以從網(wǎng)絡上獲得最好的性能. 這些設備的網(wǎng)絡驅(qū)動必須支持在如何給定時間有多個未完成的發(fā)送, 但是設備內(nèi)存能夠填滿不管硬件是否支持多個未完成發(fā)送. 任何時候當設備內(nèi)存填充到?jīng)]有空間給最大可能的報文時, 驅(qū)動應當停止隊列直到有空間可用.

如果你必須禁止如何地方的報文傳送, 除了你的 hard_start_xmit 函數(shù)( 也許, 響應一個重新配置請求 ), 你想使用的函數(shù)是:


void netif_tx_disable(struct net_device *dev); 

這個函數(shù)非常象 netif_stop_queue, 但是它還保證, 當它返回時, 你的 hard_start_xmit 方法沒有在另一個 CPU 上運行. 隊列能夠用 netif_wake_queue 重啟, 如常.

17.5.2.?傳送超時

與真實硬件打交道的大部分驅(qū)動不得不預備處理硬件偶爾不能響應. 接口可能忘記它們在做什么, 或者系統(tǒng)可能丟失中斷. 設計在個人機上運行的設備, 這種類型的問題是平常的.

許多驅(qū)動通過設置定時器來處理這個問題; 如果在定時器到期時操作還沒結束, 有什么不對了. 網(wǎng)絡系統(tǒng), 本質(zhì)上是一個復雜的由大量定時器控制的狀態(tài)機的組合體. 因此, 網(wǎng)絡代碼是一個合適的位置來檢測發(fā)送超時, 作為它正常操作的一部分.

因此, 網(wǎng)絡驅(qū)動不需要擔心自己去檢測這樣的問題. 相反, 它們只需要設置一個超時值, 在 net_device 結構的 watchdog_timeo 成員. 這個超時值, 以 jiffy 計, 應當足夠長以容納正常的發(fā)送延遲(例如網(wǎng)絡媒介擁塞引起的沖突).

如果當前系統(tǒng)時間超過設備的 trans_start 時間至少 time-out 值, 網(wǎng)絡層最終調(diào)用驅(qū)動的 tx_timeout 方法. 這個方法的工作是是進行清除問題需要的工作并且保證任何已經(jīng)開始的發(fā)送正確地完成. 特別地, 驅(qū)動沒有丟失追蹤任何網(wǎng)絡代碼委托給它的 socket 緩存.

snull 有能力模仿發(fā)送器上鎖, 由 2 個加載時參數(shù)控制的:


static int lockup = 0;
module_param(lockup, int, 0);

static int timeout = SNULL_TIMEOUT;
module_param(timeout, int, 0);

如果驅(qū)動使用參數(shù) lockup=n 加載, 則模擬一個上鎖, 一旦每 n 個報文傳送了, 并且 watchdog_timeo 成員設為給定的時間值. 當模擬上鎖時, snull 也調(diào)用 netif_stop_queue 來阻止其他的發(fā)送企圖發(fā)生.

snull 發(fā)送超時處理看來如此:


void snull_tx_timeout (struct net_device *dev)
{
    struct snull_priv *priv = netdev_priv(dev);
    PDEBUG("Transmit timeout at %ld, latency %ld\n", jiffies, jiffies - dev->trans_start);
    /* Simulate a transmission interrupt to get things moving */
    priv->status = SNULL_TX_INTR;
    snull_interrupt(0, dev, NULL);
    priv->stats.tx_errors++;
    netif_wake_queue(dev);
    return;
}

當發(fā)生傳送超時, 驅(qū)動必須在接口統(tǒng)計量中標記這個錯誤, 并安排設備被復位到一個干凈的能發(fā)送新報文的狀態(tài). 當一個超時發(fā)生在 snull, 驅(qū)動調(diào)用 snull_interrupt 來填充"丟失"的中斷并用 netif_wake_queue 重啟隊列.

17.5.3.?發(fā)散/匯聚 I/O

網(wǎng)絡中創(chuàng)建一個發(fā)送報文的過程包括組合多個片. 報文數(shù)據(jù)必須從用戶空間拷貝, 由網(wǎng)絡協(xié)議棧各層使用的頭部必須同時加上. 這個組合可能要求相當數(shù)量的數(shù)據(jù)拷貝. 但是, 如果注定要發(fā)送報文的網(wǎng)絡接口能夠進行發(fā)散/匯聚 I/O, 報文就不需要組裝成一個單個塊, 大量的拷貝可以避免. 發(fā)散/匯聚 I/O 也從用戶空間啟動"零拷貝"網(wǎng)絡發(fā)送.

內(nèi)核不傳遞發(fā)散的報文給你的 hard_start_xmit 方法除非 NETIF_F_SG 位已經(jīng)設置到你的設備結構的特性成員中. 如果你已設置了這個標志, 你需要查看一個特殊的 skb 中的"shard info"成員來確定是否報文由一個單個片段或者多個組成, 并且如果需要就找出發(fā)散的片段. 一個特殊的宏定義來存取這個信息; 它是 skb_shinfo. 發(fā)送潛在的分片報文的第一步常常是看來如此的東東:


if (skb_shinfo(skb)->nr_frags == 0) {
    /* Just use skb->data and skb->len as usual */
}

nr_frags 成員告知多少片要用來建立這個報文. 如果它是 0, 報文存于一個單個片中, 可以如常使用 data 成員來存取. 但是, 如果它是非 0, 你的驅(qū)動必須歷經(jīng)并安排發(fā)送每一個單獨的片. skb 結構的 data 成員方便地指向第一個片(在不分片情況下, 指向整個報文). 片的長度必須通過從 skb->len ( 仍然含有整個報文的長度 ) 中減去 skb->data_len 計算得來. 剩下的片會在稱為 frags 的數(shù)組中找到, frags 在共享的信息結構中; frags 中每個入口是一個 skb_frag_struct 結構:


struct skb_frag_struct { struct page *page;
    __u16 page_offset;
    __u16 size;
};

如你所見, 我們又一次遇到 page 結構, 不是內(nèi)核虛擬地址. 你的驅(qū)動應當遍歷這些分片, 為 DMA 傳送映射每一個, 并且不要忘記第一個分片, 它由 skb 直接指著. 你的硬件, 當然, 必須組裝這些分片并作為一個單個報文發(fā)送它們. 注意, 如果你已經(jīng)設置了NETIF_F_HIGHDMA 特性標志, 一些或者全部分片可能位于高端內(nèi)存.

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號