17.5. 報(bào)文傳送

2018-02-24 15:50 更新

17.5.?報(bào)文傳送

網(wǎng)絡(luò)接口進(jìn)行的最重要任務(wù)是數(shù)據(jù)發(fā)送和接收. 我們從發(fā)送開始, 因?yàn)樗晕⒁锥恍?

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

sk_buff 的指針通常稱為 skb, 我們?cè)诶哟a和文本里遵循這個(gè)做法.

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

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

snull 報(bào)文傳送代碼如下; 網(wǎng)絡(luò)傳送機(jī)制隔離在另外一個(gè)函數(shù)里, 因?yàn)槊總€(gè)接口驅(qū)動(dòng)必須根據(jù)特定的在驅(qū)動(dòng)的硬件來實(shí)現(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ù), 因此, 只對(duì)報(bào)文進(jìn)行一些合理性檢查并通過硬件相關(guān)的函數(shù)傳送數(shù)據(jù). 注意, 但是, 要小心對(duì)待傳送的報(bào)文比下面的媒介(對(duì)于 snull, 是我們虛擬的"以太網(wǎng)")支持的最小長(zhǎng)度要短的情況. 許多 Linux 網(wǎng)絡(luò)驅(qū)動(dòng)( 其他操作系統(tǒng)的也是 )已被發(fā)現(xiàn)在這種情況下泄漏數(shù)據(jù). 不是產(chǎn)生那種安全漏洞, 我們拷貝短報(bào)文到一個(gè)單獨(dú)的數(shù)組, 這樣我們可以清楚地零填充到足夠的媒介要求的長(zhǎng)度. (我們可以安全地在堆棧中放數(shù)據(jù), 因?yàn)樽钚¢L(zhǎng)度 -- 60 字節(jié) -- 是太小了).

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

"硬件相關(guān)"的傳送函數(shù)( snull_hw_tx )這里忽略了, 因?yàn)樗耆莵韺?shí)現(xiàn)了 snull 設(shè)備的戲法, 包括假造源和目的地址, 對(duì)于真正的網(wǎng)絡(luò)驅(qū)動(dòng)作者沒有任何吸引力. 當(dāng)然, 它呈現(xiàn)在例子源碼里, 給那些想進(jìn)入并看看它如何工作的人.

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

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

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

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


void netif_wake_queue(struct net_device *dev); 

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

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

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


void netif_tx_disable(struct net_device *dev); 

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

17.5.2.?傳送超時(shí)

與真實(shí)硬件打交道的大部分驅(qū)動(dòng)不得不預(yù)備處理硬件偶爾不能響應(yīng). 接口可能忘記它們?cè)谧鍪裁? 或者系統(tǒng)可能丟失中斷. 設(shè)計(jì)在個(gè)人機(jī)上運(yùn)行的設(shè)備, 這種類型的問題是平常的.

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

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

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

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


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

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

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

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


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;
}

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

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

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

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


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

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


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

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

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)