## 17.5.?報文傳送
網絡接口進行的最重要任務是數據發送和接收. 我們從發送開始, 因為它稍微易懂一些.
傳送指的是通過一個網絡連接發送一個報文的行為. 無論何時內核需要傳送一個數據報文, 它調用驅動的 hard_start_stransmit 方法將數據放在外出隊列上. 每個內核處理的報文都包含在一個 socket 緩存結構( 結構 sk_buff )里, 定義見<linux/skbuff.h>. 這個結構從 Unix 抽象中得名, 用來代表一個網絡連接, socket. 如果接口與 socket 沒有關系, 每個網絡報文屬于一個網絡高層中的 socket, 并且任何 socket 輸入/輸出緩存是結構 struct sk_buff 的列表. 同樣的 sk_buff 結構用來存放網絡數據歷經所有 Linux 網絡子系統, 但是對于接口來說, 一個 socket 緩存只是一個報文.
sk_buff 的指針通常稱為 skb, 我們在例子代碼和文本里遵循這個做法.
socket 緩存是一個復雜的結構, 內核提供了一些函數來操作它. 在"Socket 緩存"一節中描述這些函數; 現在, 對我們來說一個基本的關于 sk_buff 的事實就足夠來編寫一個能工作的驅動.
傳給 hard_start_xmit 的 socket 緩存包含物理報文, 它應當出現在媒介上, 以傳輸層的頭部結束. 接口不需要修改要傳送的數據. skb->data 指向要傳送的報文, skb->len 是以字節計的長度. 如果你的驅動能夠處理發散/匯聚 I/O, 情形會稍稍復雜些; 我們在"發散/匯聚 I/O"一節中說它.
snull 報文傳送代碼如下; 網絡傳送機制隔離在另外一個函數里, 因為每個接口驅動必須根據特定的在驅動的硬件來實現它:
~~~
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 */
}
~~~
傳送函數, 因此, 只對報文進行一些合理性檢查并通過硬件相關的函數傳送數據. 注意, 但是, 要小心對待傳送的報文比下面的媒介(對于 snull, 是我們虛擬的"以太網")支持的最小長度要短的情況. 許多 Linux 網絡驅動( 其他操作系統的也是 )已被發現在這種情況下泄漏數據. 不是產生那種安全漏洞, 我們拷貝短報文到一個單獨的數組, 這樣我們可以清楚地零填充到足夠的媒介要求的長度. (我們可以安全地在堆棧中放數據, 因為最小長度 -- 60 字節 -- 是太小了).
hard_start_xmit 的返回值應當為 0 在成功時; 此時, 你的驅動已經負責起報文, 應當盡全力保證發送成功, 并且必須在最后釋放 skb. 非 0 返回值指出報文這次不能發送; 內核將稍后重試. 這種情況下, 你的驅動應當停止隊列直到已經解決導致失敗的情況.
"硬件相關"的傳送函數( snull_hw_tx )這里忽略了, 因為它完全是來實現了 snull 設備的戲法, 包括假造源和目的地址, 對于真正的網絡驅動作者沒有任何吸引力. 當然, 它呈現在例子源碼里, 給那些想進入并看看它如何工作的人.
### 17.5.1.?控制發送并發
hard_start_xmit 函數由一個 net_device 結構中的自旋鎖(xmit_lock)來保護避免并發調用. 但是, 函數一返回, 它有可能被再次調用. 當軟件完成指導硬件報文發送的事情, 但是硬件傳送可能還沒有完成. 對 snull 這不是問題, 它使用 CPU 完成它所有的工作, 因此報文發送在傳送函數返回前就完成了.
真實的硬件接口, 另一方面, 異步發送報文并且具備有限的內存來存放外出的報文. 當內存耗盡(對某些硬件, 會發生在一個單個要發送的外出報文上), 驅動需要告知網絡系統不要再啟動發送直到硬件準備好接收新的數據.
這個通知通過調用 netif_stop_queue 來實現, 這個前面介紹過的函數來停止隊列. 一旦你的驅動已停止了它的隊列, 它必須安排在以后某個時間重啟隊列, 當它又能夠接受報文來發送了. 為此, 它應當調用:
~~~
void netif_wake_queue(struct net_device *dev);
~~~
這個函數如同 netif_start_queue, 除了它還刺探網絡系統來使它又啟動發送報文.
大部分現代的網絡硬件維護一個內部的有多個發送報文的隊列; 以這種方式, 它可以從網絡上獲得最好的性能. 這些設備的網絡驅動必須支持在如何給定時間有多個未完成的發送, 但是設備內存能夠填滿不管硬件是否支持多個未完成發送. 任何時候當設備內存填充到沒有空間給最大可能的報文時, 驅動應當停止隊列直到有空間可用.
如果你必須禁止如何地方的報文傳送, 除了你的 hard_start_xmit 函數( 也許, 響應一個重新配置請求 ), 你想使用的函數是:
~~~
void netif_tx_disable(struct net_device *dev);
~~~
這個函數非常象 netif_stop_queue, 但是它還保證, 當它返回時, 你的 hard_start_xmit 方法沒有在另一個 CPU 上運行. 隊列能夠用 netif_wake_queue 重啟, 如常.
### 17.5.2.?傳送超時
與真實硬件打交道的大部分驅動不得不預備處理硬件偶爾不能響應. 接口可能忘記它們在做什么, 或者系統可能丟失中斷. 設計在個人機上運行的設備, 這種類型的問題是平常的.
許多驅動通過設置定時器來處理這個問題; 如果在定時器到期時操作還沒結束, 有什么不對了. 網絡系統, 本質上是一個復雜的由大量定時器控制的狀態機的組合體. 因此, 網絡代碼是一個合適的位置來檢測發送超時, 作為它正常操作的一部分.
因此, 網絡驅動不需要擔心自己去檢測這樣的問題. 相反, 它們只需要設置一個超時值, 在 net_device 結構的 watchdog_timeo 成員. 這個超時值, 以 jiffy 計, 應當足夠長以容納正常的發送延遲(例如網絡媒介擁塞引起的沖突).
如果當前系統時間超過設備的 trans_start 時間至少 time-out 值, 網絡層最終調用驅動的 tx_timeout 方法. 這個方法的工作是是進行清除問題需要的工作并且保證任何已經開始的發送正確地完成. 特別地, 驅動沒有丟失追蹤任何網絡代碼委托給它的 socket 緩存.
snull 有能力模仿發送器上鎖, 由 2 個加載時參數控制的:
~~~
static int lockup = 0;
module_param(lockup, int, 0);
static int timeout = SNULL_TIMEOUT;
module_param(timeout, int, 0);
~~~
如果驅動使用參數 lockup=n 加載, 則模擬一個上鎖, 一旦每 n 個報文傳送了, 并且 watchdog_timeo 成員設為給定的時間值. 當模擬上鎖時, snull 也調用 netif_stop_queue 來阻止其他的發送企圖發生.
snull 發送超時處理看來如此:
~~~
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;
}
~~~
當發生傳送超時, 驅動必須在接口統計量中標記這個錯誤, 并安排設備被復位到一個干凈的能發送新報文的狀態. 當一個超時發生在 snull, 驅動調用 snull_interrupt 來填充"丟失"的中斷并用 netif_wake_queue 重啟隊列.
### 17.5.3.?發散/匯聚 I/O
網絡中創建一個發送報文的過程包括組合多個片. 報文數據必須從用戶空間拷貝, 由網絡協議棧各層使用的頭部必須同時加上. 這個組合可能要求相當數量的數據拷貝. 但是, 如果注定要發送報文的網絡接口能夠進行發散/匯聚 I/O, 報文就不需要組裝成一個單個塊, 大量的拷貝可以避免. 發散/匯聚 I/O 也從用戶空間啟動"零拷貝"網絡發送.
內核不傳遞發散的報文給你的 hard_start_xmit 方法除非 NETIF_F_SG 位已經設置到你的設備結構的特性成員中. 如果你已設置了這個標志, 你需要查看一個特殊的 skb 中的"shard info"成員來確定是否報文由一個單個片段或者多個組成, 并且如果需要就找出發散的片段. 一個特殊的宏定義來存取這個信息; 它是 skb_shinfo. 發送潛在的分片報文的第一步常常是看來如此的東東:
~~~
if (skb_shinfo(skb)->nr_frags == 0) {
/* Just use skb->data and skb->len as usual */
}
~~~
nr_frags 成員告知多少片要用來建立這個報文. 如果它是 0, 報文存于一個單個片中, 可以如常使用 data 成員來存取. 但是, 如果它是非 0, 你的驅動必須歷經并安排發送每一個單獨的片. skb 結構的 data 成員方便地指向第一個片(在不分片情況下, 指向整個報文). 片的長度必須通過從 skb->len ( 仍然含有整個報文的長度 ) 中減去 skb->data_len 計算得來. 剩下的片會在稱為 frags 的數組中找到, frags 在共享的信息結構中; frags 中每個入口是一個 skb_frag_struct 結構:
~~~
struct skb_frag_struct { struct page *page;
__u16 page_offset;
__u16 size;
};
~~~
如你所見, 我們又一次遇到 page 結構, 不是內核虛擬地址. 你的驅動應當遍歷這些分片, 為 DMA 傳送映射每一個, 并且不要忘記第一個分片, 它由 skb 直接指著. 你的硬件, 當然, 必須組裝這些分片并作為一個單個報文發送它們. 注意, 如果你已經設置了NETIF_F_HIGHDMA 特性標志, 一些或者全部分片可能位于高端內存.
- Linux設備驅動第三版
- 第 1 章 設備驅動簡介
- 1.1. 驅動程序的角色
- 1.2. 劃分內核
- 1.3. 設備和模塊的分類
- 1.4. 安全問題
- 1.5. 版本編號
- 1.6. 版權條款
- 1.7. 加入內核開發社團
- 1.8. 本書的內容
- 第 2 章 建立和運行模塊
- 2.1. 設置你的測試系統
- 2.2. Hello World 模塊
- 2.3. 內核模塊相比于應用程序
- 2.4. 編譯和加載
- 2.5. 內核符號表
- 2.6. 預備知識
- 2.7. 初始化和關停
- 2.8. 模塊參數
- 2.9. 在用戶空間做
- 2.10. 快速參考
- 第 3 章 字符驅動
- 3.1. scull 的設計
- 3.2. 主次編號
- 3.3. 一些重要數據結構
- 3.4. 字符設備注冊
- 3.5. open 和 release
- 3.6. scull 的內存使用
- 3.7. 讀和寫
- 3.8. 使用新設備
- 3.9. 快速參考
- 第 4 章 調試技術
- 4.1. 內核中的調試支持
- 4.2. 用打印調試
- 4.3. 用查詢來調試
- 4.4. 使用觀察來調試
- 4.5. 調試系統故障
- 4.6. 調試器和相關工具
- 第 5 章 并發和競爭情況
- 5.1. scull 中的缺陷
- 5.2. 并發和它的管理
- 5.3. 旗標和互斥體
- 5.4. Completions 機制
- 5.5. 自旋鎖
- 5.6. 鎖陷阱
- 5.7. 加鎖的各種選擇
- 5.8. 快速參考
- 第 6 章 高級字符驅動操作
- 6.1. ioctl 接口
- 6.2. 阻塞 I/O
- 6.3. poll 和 select
- 6.4. 異步通知
- 6.5. 移位一個設備
- 6.6. 在一個設備文件上的存取控制
- 6.7. 快速參考
- 第 7 章 時間, 延時, 和延后工作
- 7.1. 測量時間流失
- 7.2. 獲知當前時間
- 7.3. 延后執行
- 7.4. 內核定時器
- 7.5. Tasklets 機制
- 7.6. 工作隊列
- 7.7. 快速參考
- 第 8 章 分配內存
- 8.1. kmalloc 的真實故事
- 8.2. 后備緩存
- 8.3. get_free_page 和其友
- 8.4. 每-CPU 的變量
- 8.5. 獲得大量緩沖
- 8.6. 快速參考
- 第 9 章 與硬件通訊
- 9.1. I/O 端口和 I/O 內存
- 9.2. 使用 I/O 端口
- 9.3. 一個 I/O 端口例子
- 9.4. 使用 I/O 內存
- 9.5. 快速參考
- 第 10 章 中斷處理
- 10.1. 準備并口
- 10.2. 安裝一個中斷處理
- 10.3. 前和后半部
- 10.4. 中斷共享
- 10.5. 中斷驅動 I/O
- 10.6. 快速參考
- 第 11 章 內核中的數據類型
- 11.1. 標準 C 類型的使用
- 11.2. 安排一個明確大小給數據項
- 11.3. 接口特定的類型
- 11.4. 其他移植性問題
- 11.5. 鏈表
- 11.6. 快速參考
- 第 12 章 PCI 驅動
- 12.1. PCI 接口
- 12.2. 回顧: ISA
- 12.3. PC/104 和 PC/104+
- 12.4. 其他的 PC 總線
- 12.5. SBus
- 12.6. NuBus 總線
- 12.7. 外部總線
- 12.8. 快速參考
- 第 13 章 USB 驅動
- 13.1. USB 設備基礎知識
- 13.2. USB 和 sysfs
- 13.3. USB 的 Urbs
- 13.4. 編寫一個 USB 驅動
- 13.5. 無 urb 的 USB 傳送
- 13.6. 快速參考
- 第 14 章 Linux 設備模型
- 14.1. Kobjects, Ksets 和 Subsystems
- 14.2. 低級 sysfs 操作
- 14.3. 熱插拔事件產生
- 14.4. 總線, 設備, 和驅動
- 14.5. 類
- 14.6. 集成起來
- 14.7. 熱插拔
- 14.8. 處理固件
- 14.9. 快速參考
- 第 15 章 內存映射和 DMA
- 15.1. Linux 中的內存管理
- 15.2. mmap 設備操作
- 15.3. 進行直接 I/O
- 15.4. 直接內存存取
- 15.5. 快速參考
- 第 16 章 塊驅動
- 16.1. 注冊
- 16.2. 塊設備操作
- 16.3. 請求處理
- 16.4. 一些其他的細節
- 16.5. 快速參考
- 第 17 章 網絡驅動
- 17.1. snull 是如何設計的
- 17.2. 連接到內核
- 17.3. net_device 結構的詳情
- 17.4. 打開與關閉
- 17.5. 報文傳送
- 17.6. 報文接收
- 17.7. 中斷處理
- 17.8. 接收中斷緩解
- 17.9. 連接狀態的改變
- 17.10. Socket 緩存
- 17.11. MAC 地址解析
- 17.12. 定制 ioctl 命令
- 17.13. 統計信息
- 17.14. 多播
- 17.15. 幾個其他細節
- 17.16. 快速參考
- 第 18 章 TTY 驅動
- 18.1. 一個小 TTY 驅動
- 18.2. tty_driver 函數指針
- 18.3. TTY 線路設置
- 18.4. ioctls 函數
- 18.5. TTY 設備的 proc 和 sysfs 處理
- 18.6. tty_driver 結構的細節
- 18.7. tty_operaions 結構的細節
- 18.8. tty_struct 結構的細節
- 18.9. 快速參考