remote0 是在 snullnet0 的主機, 并且它的第 4 octet 與 local1 的相同. 任何發送給 remote0 的封包到達 local1, 在它的網絡位址被接口代碼改變之後. 主機 remote1 屬于 snullnet1, 它的第 4 octet 與 local0 相同.
snull 接口的操作在圖
主機如何看它的接口
中描述, 其中每個接口的關聯的主機名印在接口名的旁邊.
圖 17.1. 主機如何看它的接口
linux kernel網絡驅動 下面是網絡編号的可能值. 一旦你把這些行放進 /etc/networks, 你可以使用名子來調用你的網絡. 這些值選自保留做私人用途的編号範圍.
snullnet0 192.168.0.0
snullnet1 192.168.1.0
下面的是一些可能的主機編号, 可放進 /etc/hosts 裡面:
192.168.0.1 local0
192.168.0.2 remote0
192.168.1.2 local1
192.168.1.1 remote1
這些編号的重要特性是 local0 的主機部分與 remote1 的主機部分相同, local1 的主機部分和 remote0 的主機部分相同. 你可以使用完全不同的編号, 隻要保持着這種關系.
但是要小心, 如果你的計算機以及連接配接到一個網絡上. 你選擇的編号可能是真實的網際網路或者内聯網的編号, 把它們安排給你的接口會阻止和這些真實的主機間的通訊. 例如, 盡管剛剛展示的這些編号不是可以路由的網際網路編号, 它們也可能被你的私有網絡已經在使用.
不管你選擇什麼編号, 你可以正确設定這些接口來操作, 通過發出下面的指令:
ifconfig sn0 local0
ifconfig sn1 local1
你可能需要添加網絡掩碼 255.255.255.0 參數, 如果選擇的位址範圍不是 C 類範圍.
在此, 接口的"遠端"端點能夠到達了. 下面的螢幕拷貝顯示了一個主機如何到達 remote0 和 remote1 的, 通過 snull 接口.
morgana% ping -c 2 remote0
64 bytes from 192.168.0.99: icmp_seq=0 ttl=64 time=1.6 ms
64 bytes from 192.168.0.99: icmp_seq=1 ttl=64 time=0.9 ms
2 packets transmitted, 2 packets received, 0% packet loss
morgana% ping -c 2 remote1
64 bytes from 192.168.1.88: icmp_seq=0 ttl=64 time=1.8 ms
64 bytes from 192.168.1.88: icmp_seq=1 ttl=64 time=0.9 ms
2 packets transmitted, 2 packets received, 0% packet loss
注意, 你不能到達屬于這兩個網絡的任何其他主機, 因為封包被你的計算機丢棄了, 在位址被修改和收到封包之後. 例如, 一個發向 192.168.0.32 的封包将離開 sn0 并以 192.168.1.32 的目的位址出現在 sn1, 這并不是這台主機的本地位址.
17.1.2. 封包的實體傳送
隻考慮資料傳送的話, snull 接口屬于以太網一類的.
snull 模拟以太網是因為大量的現存網絡 -- 至少一個工作站所連接配接的網段 -- 是基于以太網技術的, 它可能是 10base-T, 100base-T, 或者 千兆網. 另外, 核心為以太網裝置提供了一些通用的接口, 沒有理由不用它. 一個以太網裝置的優勢是如此的強以至于 plip 接口( 使用列印機端口的接口 )都聲明自己是一個以太網裝置.
snull 使用以太網設定的最後一個優勢是你可以運作 tcpdump 在接口上來觀察過往的封包. 使用 tcpdump 來觀察接口是得知兩個接口如何工作的有用途徑.
如同我們之前提到的, snull 隻處理 IP 封包. 這個限制來自這樣的事實, snull 監聽封包并且甚至修改它們, 以便使代碼工作. 代碼修改了每個封包的源, 目的和 IP header 的校驗和, 并不檢查它是否實際承載着 IP 資訊.
這種快而髒的資料修改毀壞了非 IP 封包. 如果你想通過 snull 遞交其他協定, 你必須修改子產品的源代碼.
17.2. 連接配接到核心
我們從分析 snull 的源碼來檢視網絡驅動的結構開始. 把幾個驅動的源碼留在手邊, 對于下面的讨論和得知真實世界中的 Linux 網絡驅動如何運作是會有幫助的.
17.2.1. 裝置注冊
當一個驅動子產品加載進一個運作着的核心中, 它請求資源并提供功能; 這裡沒有新内容. 并且在資源是如何請求上也沒有新東西. 驅動應當探測它的裝置和它的硬體位置( I/O 端口和 IRQ 線 ) -- 但是不注冊它們 --如在第 10 章的" 安裝一個中斷處理程式 "中所述. 一個網絡驅動通過它的子產品初始化函數注冊的方式與字元和塊驅動是不同的. 因為沒有對等的主次編号給網絡接口, 一個網絡驅動不請求這樣一個号. 相反, 驅動為每個剛剛探測到的接口在一個全局的網絡裝置清單裡插入一個資料結構.
每個接口由一個結構 net_device 項來描述, 它在 裡定義. snull 驅動留有指向兩個這樣結構的指針, 在一個簡單數組裡.
struct net_device *snull_devs[2];
net_device 結構, 如同許多其他核心結構, 包含一個 kobject, 以及是以它可被引用計數并通過 sysfs 輸出. 如同别的這樣的結構, 它必須動态配置設定. 進行這種配置設定的核心函數是 alloc_netdev, 它有下列原型:
struct net_device *alloc_netdev(int sizeof_priv,
const char *name,
void (*setup)(struct net_device *));
這裡, sizeof_priv 是驅動的的"私有資料"區的大小; 對于網絡驅動, 這個區是同 net_device 結構一起配置設定的. 實際上, 這兩個是是在一個大記憶體塊中一起配置設定的, 但是驅動作者應當假裝不知道這一點. name 是這個接口的名子, 如同使用者空間看到的一樣; 這個名子可以有一個 printf 風格的 %d 在裡面. 核心用下一個可用的接口号來替換這個 %d. 最後, setup 是一個初始化函數的指針, 被調用來設定 net_device 結構的剩餘部分. 我們即将進入這個初始化函數, 但是現在, 為強化起見, snull 以這樣的方式配置設定它的兩個裝置結構:
snull_devs[0] = alloc_netdev(sizeof(struct snull_priv), "sn%d",
snull_init);
snull_devs[1] = alloc_netdev(sizeof(struct snull_priv), "sn%d",
snull_init);
if (snull_devs[0] == NULL || snull_devs[1] == NULL)
goto out;
象通常一樣, 我們必須檢查傳回值來確定配置設定成功.
網絡子系統為各種接口提供了一些幫助函數, 包裹着 alloc_netdev. 最通用的是 alloc_etherdev, 定義在 :
struct net_device *alloc_etherdev(int sizeof_priv);
這個函數配置設定一個網絡裝置使用 eth%d 作為參數 name. 它提供了自己的初始化函數 ( ether_setup )來設定幾個 net_device 字段, 使用對以太網裝置合适的值. 是以, 沒有驅動提供的初始化函數給 alloc_etherdev; 驅動應當隻完成它要求的初始化, 直接在一個成功的配置設定之後. 其他類型驅動的編寫者可能想利用這些幫助函數的其中一個, 例如 alloc_fcdev ( 定義在 ) 為 fiber-channel 裝置, alloc_fddidev () 為 FDDI 裝置, 或者 aloc_trdev () 為令牌環裝置.
snull 可以順利使用 alloc_etherdev; 我們選擇使用 alloc_netdev 來代替, 作為示範低層接口的方式, 并且給我們控制安排給接口的名子.
一旦 net_device 結構完成初始化, 完成這個過程就隻是傳遞這個結構給 register_netdev. 在 snull 中, 調用看來如同這樣:
for (i = 0; i name);
一些經常的注意問題這裡提一下: 在你調用 register_netdev 時, 你的驅動可能會馬上被調用來操作裝置. 是以, 你不應當注冊裝置直到所有東西都已經完全初始化.
17.2.2. 初始化每一個裝置
我們已經看到了 net_device 結構的配置設定和注冊, 但是我們越過了中間的完全初始化這個結構的步驟. 注意 net_device 結構在運作時一直是放在一起; 它不能如同一個 file_operations 或者 block_device_opreations 結構一樣在編譯時設定. 必須在調用 register_netdev 之前完成初始化. net_device 結構又大又複雜; 幸運的是, 核心負責了一些以太網範圍中的預設值, 通過 ether_setup 函數(由 alloc_etherdev 調用).
因為 snull 使用 alloc_netdev, 它有單獨的初始化函數. 該函數的核心( snull_init )如下:
ether_setup(dev);
dev->open = snull_open;
dev->stop = snull_release;
dev->set_config = snull_config;
dev->hard_start_xmit = snull_tx;
dev->do_ioctl = snull_ioctl;
dev->get_stats = snull_stats;
dev->rebuild_header = snull_rebuild_header;
dev->hard_header = snull_header;
dev->tx_timeout = snull_tx_timeout;
dev->watchdog_timeo = timeout;
dev->flags |= IFF_NOARP;
dev->features |= NETIF_F_NO_CSUM;
dev->hard_header_cache = NULL;
上面的代碼是對 net_device 結構的例行初始化; 大部分是存儲我們的各種驅動函數指針. 代碼的單個不尋常的特性是設定 IFF_NOARP 在 flags 裡面. 這個指出該接口不能使用 ARP. ARP 是一個低層以太網協定; 它的工作是将 IP 位址轉變成以太網媒體存取控制 (MAC) 位址. 因為由 snull 模拟的遠端系統并不存在, 就沒人回答對它們的 ARP 請求. 不想因為增加 ARP 實作使 snull 變複雜, 我們選擇辨別接口作為不能處理這個協定. 其中的對 hard_header_cache 指派是同樣理由: 它關閉了這個接口的(不存在的) ARP 回答. 這個主題在本章後面的" MAC 位址解析"一節中詳述.
代碼初始化也設定了幾個和發送逾時的處理有關的幾個變量( tx_timeout 和 watchdog_time ). 我們在"發送逾時"一節完整地涉及這個主題.
我們現在看結構 net_device 的另一個成員, priv. 它的角色近似于我們用在字元驅動上的 private_data 指針. 不同于 fops->private_data, 這個 priv 指針是随 net_device 結構一起配置設定的. 也不鼓勵直接存取 priv 成員, 由于性能和靈活性的原因. 當一個驅動需要存取私有資料指針, 應當使用 netdev_priv 函數. 是以, snull 驅動充滿着這樣的聲明:
struct snull_priv *priv = netdev_priv(dev);
snull 子產品聲明了一個 snull_priv 資料結構來給 priv 使用:
struct snull_priv {
struct net_device_stats stats;
int status;
struct snull_packet *ppool;
struct snull_packet *rx_queue;
int rx_int_enabled;
int tx_packetlen;
u8 *tx_packetdata;
struct sk_buff *skb;
spinlock_t lock;
};
這個結構包括, 還有其他東西, 一個 net_device_stats 結構的執行個體, 這是放置接口統計量的标準地方. 下面的在 snull_init 中的各行配置設定并初始化 dev->priv:
priv = netdev_priv(dev);
memset(priv, 0, sizeof(struct snull_priv));
spin_lock_init(&priv->lock);
snull_rx_ints(dev, 1);
17.2.3. 子產品解除安裝
子產品解除安裝時沒什麼特别的. 子產品的清理函數隻是登出接口, 進行任何需要的内部清理, 釋放 net_device 結構回系統.
void snull_cleanup(void)
{
int i;
for (i = 0; i
對 unregister_netdev 的調用從系統中去除了接口; free_netdev 歸還 net_device 結構給核心. 如果某個地方有對這個結構的引用, 它可能繼續存在, 但是你的驅動不需要關心這個. 一旦你已經登出了接口, 核心不再調用它的方法.
注意我們的内部清理( 在 snull_teardown_pool 裡所做的 )直到已經登出了裝置後才能進行. 它必須, 但是, 在我們傳回 net_device 結構給系統之前進行; 一旦我們已調用了 free_netdev, 我們再不能對這個裝置或者我們的私有資料做任何引用.
17.3. net_device 結構的詳情
net_device 結構處于網絡驅動層的非常核心的位置并且值得完全的描述. 這個清單描述了所有成員, 更多的是提供了一個參考而不是用來備忘. 本章剩下的部分簡要地描述了每個成員, 一旦它用在例子代碼上, 是以你不需要不停地回看這一節.
17.3.1. 全局資訊
結構 net_device 的第一部分是由下面成員組成:
char name[IFNAMSIZ];
裝置名子. 如果名子由驅動設定, 包含一個 %d 格式串, register_netdev 用一個數替換它來形成一個唯一的名子; 配置設定的編号從 0 開始.
unsigned long state;
裝置狀态. 這個成員包括幾個标志. 驅動正常情況下不直接操作這些标志; 相反, 提供了一套實用函數. 這些函數在我們進入驅動操作後馬上讨論這些函數.
struct net_device *next;
全局清單中指向下一個裝置的指針. 這個成員驅動不能動.
int (*init)(struct net_device *dev);
一個初始化函數. 如果設定了這個指針, 這個函數被 register_netdev 調用來完成對 net_device 結構的初始化. 大部分現代的網絡驅動不再使用這個函數; 相反, 初始化在注冊接口前進行.
17.3.2. 硬體資訊
下面的成員包含了相對簡單裝置的低層硬體資訊. 它們是早期 Linux 網絡的延續; 大部分現代驅動确實使用它們(可能的例外是 if_port ). 我們為完整起見在這裡列出.
unsigned long rmem_end;
unsigned long rmem_start;
unsigned long mem_end;
unsigned long mem_start;
裝置記憶體資訊. 這些成員持有裝置使用的共享記憶體的開始和結束位址. 如果裝置有不同的接收和發送記憶體, mem 成員由發送記憶體使用, rmem 成員由接收記憶體使用. rmem 成員在驅動之外從不被引用. 慣例上, 設定 end 成員, 是以 end - start 是可用的闆上記憶體的數量.
unsigned long base_addr;
網絡接口的 I/O 基位址. 這個成員, 如同前面的, 由驅動在裝置探測時指派. ifconfig 目錄可用來顯示或修改目前值. base_addr 可以當系統啟動時在核心指令行中顯式指派( 通過 netdev= 參數), 或者在子產品加載時. 這個成員, 象上面描述過的記憶體成員, 核心不使用它們.
unsigned char irq;
安排的中斷号. 當接口被列出時 ifconfig 列印出 dev->irq 的值. 這個值常常在啟動或者加載時間設定并且在後來由 ifconfig 列印.
unsigned char if_port;
在多端口裝置中使用的端口. 例如, 這個成員用在同時支援同軸線(IF_PORT_10BASE2)和雙絞線(IF_PORT_100BSAET)以太網連接配接. 完整的已知端口類型設定定義在 .
unsigned char dma;
裝置配置設定的 DMA 通道. 這個成員隻在某些外設總線時有意義, 例如 ISA. 它不在裝置驅動自身以外使用, 隻是為了資訊目的( 在 ifconfig ) 中.
17.3.3. 接口資訊
有關接口的大部分資訊由 ether_setup 函數正确設定(或者任何其他對給定硬體類型适合的設定函數). 以太網卡可以依賴這個通用的函數設定大部分這些成員, 但是 flags 和 dev_addr 成員是特定裝置的, 必須在初始化時間明确指定.
一些非以太網接口可以使用類似 ether_setup 的幫助函數. deviers/net/net_init.c 輸出了一些類似的函數, 包括下列:
void ltalk_setup(struct net_device *dev);
設定一個 LocalTalk 裝置的成員
void fc_setup(struct net_device *dev);
初始化光通道裝置的成員
void fddi_setup(struct net_device *dev);
配置一個光纖分布資料接口 (FDDI) 網絡的接口
void hippi_setup(struct net_device *dev);
預備給一個高性能并行接口 (HIPPI) 的高速互連驅動的成員
void tr_setup(struct net_device *dev);
處理令牌環網絡接口的設定
大部分裝置會歸于這些類别中的一類. 如果你的是全新和不同的, 但是, 你需要手工指派下面的成員:
unsigned short hard_header_len;
硬體頭部長度, 就是, 被發送封包前面在 IP 頭之前的位元組數, 或者别的協定資訊. 對于以太網接口 hard_header_len 值是 14 (ETH_HLEN).
unsigned mtu;
最大傳輸單元 (MTU). 這個成員是網絡層用作驅動封包傳輸. 以太網有一個 1500 位元組的 MTU (ETH_DATA_LEN). 這個值可用 ifconfig 改變.
unsigned long tx_queue_len;
裝置發送隊列中可以排隊的最大幀數. 這個值由 ether_setup 設定為 1000, 但是你可以改它. 例如, plip 使用 10 來避免浪費系統記憶體( 相比真實以太網接口, plip 有一個低些的吞吐量).
unsigned short type;
接口的硬體類型. 這個 type 成員由 ARP 用來決定接口支援什麼樣的硬體位址. 對以太網接口正确的值是 ARPHRD_ETHER, 這是由 ether_setup 設定的值. 可認識的類型定義于 .
unsigned char addr_len;
unsigned char broadcast[MAX_ADDR_LEN];
unsigned char dev_addr[MAX_ADDR_LEN];
硬體 (MAC) 位址長度和裝置硬體位址. 以太網位址長度是 6 個位元組( 我們指的是接口闆的硬體 ID ), 廣播位址由 6 個 0xff 位元組組成; ether_setup 安排成正确的值. 裝置位址, 另外, 必須以特定于裝置的方式從接口闆讀出, 驅動應當将它拷貝到 dev_addr. 硬體位址用來産生正确的以太網頭, 在封包傳遞給驅動發送之前. snull 裝置不使用實體接口, 它創造自己的硬體接口.
unsigned short flags;
int features;
接口标志(下面詳述)
這個 flags 成員是一個位掩碼, 包括下面的位值. IFF_ 字首代表 "interface flags". 有些标志由核心管理, 有些由接口在初始化時設定來表明接口的能力和其他特性. 有效的标志, 對應于 , 有:
IFF_UP
對驅動這個标志是隻讀的. 核心打開它當接口激活并準備号傳送封包時.
IFF_BROADCAST
這個标志(由網絡代碼維護)說明接口允許廣播. 以太網闆是這樣.
IFF_DEBUG
這個辨別了調試模式. 這個标志用來控制你的 printk 調用的複雜性或者用于其他調試目的. 盡管目前沒有 in-tree 驅動使用這個标志, 它可以通過 ioctl 來設定和重置, 你的驅動可用它. misc-progs/netifdebug 程式可以用來打開或關閉這個标志.
IFF_LOOPBACK
這個标志應當隻在環回接口中設定. 核心檢查 IFF_LOOPBACK , 以代替硬連線 lo 名子作為一個特殊接口.
IFF_POINTOPOINT
這個标志說明接口連接配接到一個點對點鍊路. 它由驅動設定或者, 有時, 由 ifconfig. 例如, plip 和 PPP 驅動設定它.
IFF_NOARP
這個說明接口不能進行 ARP. 例如, 點對點接口不需要運作 ARP, 它隻能增加額外的流量卻沒有任何有用的資訊. snull 在沒有 ARP 能力的情況下運作, 是以它設定這個标志.
IFF_PROMISC
這個标志設定(由網絡代碼)來激活混雜操作. 預設地, 以太網接口使用硬體過濾器來保證它們隻接收廣播封包和直接到接口硬體位址的封包. 封包嗅探器, 例如 tcpdump, 在接口上設定混雜模式來存取在接口發送媒體上經過的所有封包.
IFF_MULTICAST
驅動設定這個标志來表示接口能夠多點傳播發送. ether_setup 設定 IFF_MULTICAST 預設地, 是以如果你的驅動不支援多點傳播, 必須在初始化時清除這個标志.
IFF_ALLMULTI
這個标志告知接口接收所有的多點傳播封包. 核心在主機進行多點傳播路由時設定它, 前提是 IFF_MULTICAST 置位. IFF_ALLMULTI 對驅動是隻讀的. 多點傳播标志在本章後面的"多點傳播"一節中用到.
IFF_MASTER
IFF_SLAVE
這些标志由負載均衡代碼使用. 接口驅動不需要知道它們.
IFF_PORTSEL
IFF_AUTOMEDIA
這些标志指出裝置可以在多個媒體類型間切換; 例如, 無屏蔽雙絞線 (UTP) 和 同軸以太網電纜. 如果 IFF_AUTOMEDIA 設定了, 裝置自動選擇正确的媒體. 特别地, 核心一個也不使用這 2 個标志.
IFF_DYNAMIC
這個标志, 由驅動設定, 指出接口的位址能夠變化. 目前核心沒有使用.
IFF_RUNNING
這個标志指出接口已啟動并在運作. 它大部分是因為和 BSD 相容; 核心很少用它. 大部分網絡驅動不需要擔心 IFF_RUNNING.
IFF_NOTRAILERS
在 Linux 中不用這個标志, 為了 BSD 相容才存在.
當一個程式改變 IFF_UP, open 或者 stop 裝置方法被調用. 進而, 當 IFF_UP 或者任何别的标志修改了, set_multicast_list 方法被調用. 如果驅動需要進行某些動作來響應标志的修改, 它必須在 set_multicast_list 中采取動作. 例如, 當 IFF_PROMISC 被置位或者複位, set_multicast_list 必須通知闆上的硬體過濾器. 這個裝置方法的責任在"多點傳播"一節中講解.
結構 net_device 的特性成員由驅動設定來告知核心關于任何的接口擁有的特别硬體能力. 我們将談論一些這些特性; 别的就超出了本書範圍. 完整的集合是:
NETIF_F_SG
NETIF_F_FRAGLIST
2 個标志控制發散/彙聚 I/O 的使用. 如果你的接口可以發送一個封包, 它由幾個不同的記憶體段組成, 你應當設定 NETIF_F_SG. 當然, 你不得不實際實作發散/彙聚 I/O( 我們在"發散/彙聚"一節中描述如何做 ). NETIF_F_FRAGLIST 表明你的接口能夠處理分段的封包; 在 2.6 中隻有環回驅動做這一點.
注意核心不對你的裝置進行發散/彙聚 I/O 操作, 如果它沒有同時提供某些校驗和形式. 理由是, 如果核心不得不跨過一個分片的("非線性")的封包來計算校驗和, 它可能也拷貝資料并同時接合封包.
NETIF_F_IP_CSUM
NETIF_F_NO_CSUM
NETIF_F_HW_CSUM
這些标志都是告知核心, 不需要給一些或所有的通過這個接口離開系統的封包進行校驗. 如果你的接口可以校驗 IP 封包但是别的不行, 就設定 NETIF_F_IP_CSUM. 如果這個接口不曾要求校驗和, 就設定 NETIF_F_NO_CSUM. 環回驅動設定了這個标志, snull 也設定; 因為封包隻通過系統記憶體傳送, 對它們來說沒有機會( 1 跳 )被破壞, 沒有必要校驗它們. 如果你的硬體自己做校驗, 設定 NETIF_F_HW_CWSUM.
NETIF_F_HIGHDMA
設定這個标志, 如果你的裝置能夠對高端記憶體進行 DMA. 沒有這個标志, 所有提供給你的驅動的封包在低端記憶體配置設定.
NETIF_F_HW_VLAN_TX
NETIF_F_HW_VLAN_RX
NETIF_F_HW_VLAN_FILTER
NETIF_F_VLAN_CHALLENGED
這些選項描述你的硬體對 802.1q VLAN 封包的支援. VLAN 支援超出我們本章的内容. 如果 VLAN 封包使你的裝置混亂( 其實不應該 ), 設定标志 NETIF_F_VLAN_CHALLENGED.
NETIF_F_TSO
如果你的裝置能夠進行 TCP 分段解除安裝, 設定這個标志. TSO 是一個我們在這不涉及的進階特性.
17.3.4. 裝置方法
如同在字元和塊驅動的一樣, 每個網絡裝置聲明能操作它的函數. 本節列出能夠對網絡接口進行的操作. 有些操作可以留作 NULL, 别的常常是不被觸動的, 因為 ether_setup 給它們安排了合适的方法.
網絡接口的裝置方法可分為 2 組: 基本的和可選的. 基本方法包括那些必需的能夠使用接口的; 可選的方法實作更多進階的不是嚴格要求的功能. 下列是基本方法:
int (*open)(struct net_device *dev);
打開接口. 任何時候 ifconfig 激活它, 接口被打開. open 方法應當注冊它需要的任何系統資源( I/O 口, IRQ, DMA, 等等), 打開硬體, 進行任何别的你的裝置要求的設定.
int (*stop)(struct net_device *dev);
停止接口. 接口停止當它被關閉. 這個函數應當恢複在打開時進行的操作.
int (*hard_start_xmit) (struct sk_buff *skb, struct net_device *dev);
起始封包的發送的方法. 完整的封包(協定頭和所有)包含在一個 socket 緩存區( sk_buff ) 結構. socket 緩存在本章後面介紹.
int (*hard_header) (struct sk_buff *skb, struct net_device *dev, unsigned short type, void *daddr, void *saddr, unsigned len);
用之前取到的源和目的硬體位址來建立硬體頭的函數(在 hard_start_xmit 前調用). 它的工作是将作為參數傳給它的資訊組織成一個合适的特定于裝置的硬體頭. eth_header 是以太網類型接口的預設函數, ether_setup 針對性地對這個成員指派.
int (*rebuild_header)(struct sk_buff *skb);
用來在 ARP 解析完成後但是在封包發送前重建硬體頭的函數. 以太網裝置使用的預設的函數使用 ARP 支援代碼來填充封包缺失的資訊.
void (*tx_timeout)(struct net_device *dev);
由網絡代碼在一個封包發送沒有在一個合理的時間内完成時調用的方法, 可能是丢失一個中斷或者接口被鎖住. 它應當處理這個問題并恢複封包發送.
struct net_device_stats *(*get_stats)(struct net_device *dev);
任何時候當一個應用程式需要擷取接口的統計資訊, 調用這個方法. 例如, 當 ifconfig 或者 netstat -i 運作時. snull 的一個例子實作在"統計資訊"一節中介紹.
int (*set_config)(struct net_device *dev, struct ifmap *map);
改變接口配置. 這個方法是配置驅動的入口點. 裝置的 I/O 位址和中斷号可以在運作時使用 set_config 來改變. 這種能力可由系統管理者在接口沒有探測到時使用. 現代硬體正常的驅動一般不需要實作這個方法.
剩下的裝置操作是可選的:
int weight;
int (*poll)(struct net_device *dev; int *quota);
由适應 NAPI 的驅動提供的方法, 用來在查詢模式下操作接口, 中斷關閉着. NAPI ( 以及 weight 成員) 在"接收中斷緩解"一節中涉及.
void (*poll_controller)(struct net_device *dev);
在中斷關閉的情況下, 要求驅動檢查接口上的事件的函數. 它用于特殊的核心中的網絡任務, 例如遠端控制台和使用網絡的核心調試.
int (*do_ioctl)(struct net_device *dev, struct ifreq *ifr, int cmd);
處理特定于接口的 ioctl 指令. (這些指令的實作在"定制 ioclt 指令"一節中描述)相應的 net_device 結構中的成員可留為 NULL, 如果接口不需要任何特定于接口的指令.
void (*set_multicast_list)(struct net_device *dev);
當裝置的多點傳播清單改變和當标志改變時調用的方法. 詳情見"多點傳播"一節, 以及一個例子實作.
int (*set_mac_address)(struct net_device *dev, void *addr);
如果接口支援改變它的硬體位址的能力, 可以實作這個函數. 很多接口根本不支援這個能力. 其他的使用預設的 eth_mac_adr 實作(在 deivers/net/net_init.c). eth_mac_addr 隻拷貝新位址到 dev->dev_addr, 隻在接口沒有運作時作這件事. 使用 eth_mac_addr 的驅動應當在它們的 open 方法中自 dev->dev_addr 裡設定硬體 MAC 位址.
int (*change_mtu)(struct net_device *dev, int new_mtu);
當接口的最大傳輸單元 (MTU) 改變時動作的函數. 如果使用者改變 MTU 時驅動需要做一些特殊的事情, 它應當聲明它的自己的函數; 否則, 預設的會将事情做對. snull 有對這個函數的一個模闆, 如果你有興趣.
int (*header_cache) (struct neighbour *neigh, struct hh_cache *hh);
header_cache 被調用來填充 hh_cache 結構, 使用一個 ARP 請求的結果. 幾乎全部類似以太網的驅動可以使用預設的 eth_header_cache 實作.
int (*header_cache_update) (struct hh_cache *hh, struct net_device *dev, unsigned char *haddr);
在響應一個變化中, 更新 hh_cache 結構中的目的位址的方法. 以太網裝置使用 eth_header_cache_update.
int (*hard_header_parse) (struct sk_buff *skb, unsigned char *haddr);
hard_header_parse 方法從包含在 skb 中的封包中抽取源位址, 拷貝到 haddr 的緩存區. 函數的傳回值是位址的長度. 以太網裝置通常使用 eth_header_parse.
17.3.5. 公用成員
結構 net_device 剩下的資料成員由接口使用來持有有用的狀态資訊. 有些是 ifconfig 和 netstat 用來提供給使用者關于目前配置的資訊. 是以, 接口應當給這些成員指派:
unsigned long trans_start;
unsigned long last_rx;
儲存一個 jiffy 值的成員. 驅動負責分别更新這些值, 當開始發送和收到一個封包時. trans_start 值被網絡子系統用來探測發送器加鎖. last_rx 目前沒有用到, 但是驅動應當盡量維護這個成員以備将來使用.
int watchdog_timeo;
網絡層認為一個傳送逾時發生前應當過去的最小時間(按 jiffy 計算), 調用驅動的 tx_timeout 函數.
void *priv;
filp->private_data 的對等者. 在現代的驅動裡, 這個成員由 alloc_netdev 設定, 不應當直接存取; 使用 netdev_priv 代替.
struct dev_mc_list *mc_list;
int mc_count;
處理多點傳播發送的成員. mc_count 是 mc_list 中的項數目. 更多細節見"多點傳播"一節.
spinlock_t xmit_lock;
int xmit_lock_owner;
xmit_lock 用來避免對驅動的 hard_start_xmit 函數多個同時調用. xmit_lock_owner 是已獲得 xmit_lock 的CPU号. 驅動應當不改變這些成員的值.
結構 net_device 中有其他的成員, 但是網絡驅動用不着它們.
17.4. 打開與關閉
我們的驅動可以在子產品加載時或者核心啟動時探測接口. 在接口能夠承載封包前, 但是, 核心必須打開它并配置設定一個位址給它. 核心打開或者關閉一個接口對應 ifconfig 指令.
當 ifconfig 用來給接口安排一個位址, 它做 2 個任務. 第一, 它通過 ioctl(SIOCSIFADDR)( Socket I/O Control Set Interface Address) 來安排位址. 接着它設定 dev->flag 的 IFF_UP 位, 通過 ioctl(SIOCSIFFLAGS) ( Socket I/O Control Set Interface Flags) 來打開接口.
目前為止, ioctl(SIOCSIFADDR) 不做任何事. 沒有驅動函數被調用 -- 這個任務是獨立于裝置的, 并且是核心實作它. 後面的指令 (ioctl(SIOCSIFFLAGS)), 但是, 為裝置調用 open 方法.
相似地, 當接口關閉, ifconfig 使用 ioctl(SIOCSIFFLAGS) 來清除 IFF_UP, 并且 stop 方法被調用.
2 個裝置方法都傳回 0 在成功時, 并且出錯時傳回負值.
目前為止的實際代碼, 驅動不得不進行許多與字元和塊驅動同樣的任務. open 請求任何它需要的系統資源并且告知接口啟動; stop 關閉接口并釋放系統資源. 網絡驅動必須進行一些附加的步驟在 open 時, 但是.
第一, 硬體 (MAC) 位址需要從硬體裝置拷貝到 dev->dev_addr, 在接口可以和外部世界通訊之前. 硬體位址接着在 open 時拷貝到裝置. snull 軟體接口在 open 裡面安排它; 它隻是使用了一個長為 ETH_ALEN 的字元串僞造了一個硬體号, ETH_ALEN 是以太網硬體位址長度.
open 方法應當也啟動接口的發送隊列( 允許它接受發送封包 ), 一旦它準備好啟動發送資料. 核心提供了一個函數來啟動隊列:
void netif_start_queue(struct net_device *dev);
snull 的 open 代碼看來如下:
int snull_open(struct net_device *dev)
{
memcpy(dev->dev_addr, "\0SNUL0", ETH_ALEN);
if (dev == snull_devs[1])
dev->dev_addr[ETH_ALEN-1]++;
netif_start_queue(dev);
return 0;
}
如你所見, 在缺乏真實硬體的情況下, 在 open 方法中沒什麼可做. stop 方法也一樣; 它隻是反轉 open 的操作. 是以, 實作 stop 的函數常常稱為 close 或者 release.
int snull_release(struct net_device *dev)
{
netif_stop_queue(dev);
return 0;
}
函數:
void netif_stop_queue(struct net_device *dev);
是 netif_start_queue 的對立面; 它标志裝置為不能再發送任何封包. 這個函數必須在接口關閉( 在 stop 方法中 )時調用, 但以可用于暫時停止發送, 如下一節中解釋的.
17.5. 封包傳送
網絡接口進行的最重要任務是資料發送和接收. 我們從發送開始, 因為它稍微易懂一些.
傳送指的是通過一個網絡連接配接發送一個封包的行為. 無論何時核心需要傳送一個資料封包, 它調用驅動的 hard_start_stransmit 方法将資料放在外出隊列上. 每個核心處理的封包都包含在一個 socket 緩存結構( 結構 sk_buff )裡, 定義見. 這個結構從 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 data, skb->len);
len = ETH_ZLEN;
data = shortpkt;
}
dev->trans_start = jiffies;
priv->skb = skb;
snull_hw_tx(data, len, dev);
return 0;
}
傳送函數, 是以, 隻對封包進行一些合理性檢查并通過硬體相關的函數傳送資料. 注意, 但是, 要小心對待傳送的封包比下面的媒介(對于 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);
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) {
}
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 特性标志, 一些或者全部分片可能位于高端記憶體.
17.6. 封包接收
從網絡上接收封包比發送它要難一些, 因為必須配置設定一個 sk_buff 并從一個原子性上下文中遞交給上層. 網絡驅動可以實作 2 種封包接收的模式: 中斷驅動和查詢. 大部分驅動采用中斷驅動技術, 這是我們首先要涉及的. 有些高帶寬适配卡的驅動也可能采用查詢技術; 我們在"接收中斷緩解"一節中了解這個方法.
snull 的實作将"硬體"細節從裝置獨立的正常事務中分離. 是以, 函數 snull_rx 在硬體收到封包後從 snull 的"中斷"進行中調用, 并且封包現在已經在計算機的記憶體中. snull_rx 收到一個資料指針和封包長度; 它唯一的責任是發走這個封包和運作附加資訊給上層的網絡代碼. 這個代碼獨立于獲得資料指針和長度的方式.
void snull_rx(struct net_device *dev, struct snull_packet *pkt)
{
struct sk_buff *skb;
struct snull_priv *priv = netdev_priv(dev);
skb = dev_alloc_skb(pkt->datalen + 2);
if (!skb) {
if (printk_ratelimit())
printk(KERN_NOTICE "snull rx: low on mem - packet dropped\n"); priv->stats.rx_dropped++; goto out;
}
memcpy(skb_put(skb, pkt->datalen), pkt->data, pkt->datalen);
skb->dev = dev;
skb->protocol = eth_type_trans(skb, dev);
skb->ip_summed = CHECKSUM_UNNECESSARY;
priv->stats.rx_packets++;
priv->stats.rx_bytes += pkt->datalen;
netif_rx(skb);
out:
return;
}
這個函數足夠普通以作為任何網絡驅動的一個模闆, 但是在你有信心重用這個代碼段前需要一些解釋.
第一步是配置設定一個緩存區來儲存封包. 注意緩存配置設定函數 (dev_alloc_skb) 需要知道資料長度. 函數用這些資訊來給緩存區配置設定空間. dev_alloc_skb 使用 atomic 優先級調用 kmalloc , 是以它可以在中斷時間安全使用. 核心提供了其他接口給 socket 緩存配置設定, 但是它們不值得在此介紹; socket 緩存在"socket 緩存"一節中詳細介紹.
當然, dev_alloc_skb 的傳回值必須檢查, snull 這樣做了. 我們調用 printk_ratelimit 在抱怨失敗之前, 但是. 每秒鐘産生成百上千的控制台消息是完全陷死系統和隐藏問題的真正源頭的好方法; printk_ratelimit 幫助阻止這個問題, 通過在有太多輸出到了控制台時傳回 0, 事情需要慢下來一點.
一旦有一個有效的 skb 指針, 通過調用 memcpy, 封包資料被拷貝到緩存區; skb_put 函數更新緩存中的資料末尾指針并傳回指向建立空間的指針.
如果你在編寫一個高性能驅動, 為一個可以進行完全總線占據 I/O 的接口, 一個可能的優化值得在此考慮下. 一些驅動在封包接收前配置設定 sokcet 緩存, 接着使接口将封包資料直接放入 socket 緩存空間. 網絡層通過在可 DMA 的空間( 如果你的裝置設定了 NETIF_F_HIGHDMA 标志, 這個空間有可能在高端記憶體)中配置設定所有 socket 緩存來配合這個政策. 這樣避免了單獨的填充 socket 緩存的拷貝操作, 但是需要小心緩存區的大小, 因為你無法提前知道進來的封包大小. change_mtu 方法的實作在這種情況下也重要, 因為它允許驅動對最大封包大小改變作出響應.
網絡層在搞懂封包的意思前需要清楚一些事情. 為此, dev 和 protocol 成員必須在緩存向上傳遞前指派. 以太網支援代碼輸出一個幫助函數( eth_type_trans ), 它發現一個合适值來賦給 protocol. 接着我們需要指出校驗和要如何進行或者已經在封包上完成( snull 不需要做任何校驗和 ). 對于 skb->ip_summed 可能的政策有:
CHECKSUM_HW
裝置已經在硬體裡做了校驗. 一個硬體校驗的例子使 APARC HME 接口.
CHECKSUM_NONE
校驗和還沒被驗證, 必須由系統軟體來完成這個任務. 這個是預設的, 在新配置設定的緩存中.
CHECKSUM_UNNECESSARY
不要做任何校驗. 這是 snull 和 環回接口的政策.
你可能奇怪為什麼校驗和狀态必須在這裡指定, 當我們已經在我們的 net_device 結構的特性成員中設定了标志. 答案是特性标志告訴核心我們的裝置如何對待外出的封包. 它不用于進入的封包, 相反, 進入封包必須單獨标記.
最後, 驅動更新它的統計計數來記錄收到一個封包。 統計結構由幾個成員組成; 最重要的是 rx_packet, rx_bytes, 和 tx_bytes, 分别含有收到的封包數目, 發送的數目, 和發送的位元組總數. 所有的成員在"統計資訊"一節中完全描述.
封包接收的最後一步由 netif_rx 進行, 它遞交 socket 緩存給上層. 實際上 netif_rx 傳回一個整數; NET_RX_SUCCESS(0) 意思是封包成功接收; 任何其他值訓示錯誤. 有 3 個傳回值 (NET_RX_CN_LOW, NET_RX_CN_MOD, 和 NET_RX_CN_HIGH )指出網絡子系統的遞增的擁塞級别; NET_RX_DROP 意思是封包被丢棄. 一個驅動在擁塞變高時可能使用這些值來停止輸送封包給核心, 但是, 實際上, 大部分驅動忽略從 netif_rx 的傳回值. 如果你在編寫一個高帶寬裝置的驅動, 并且希望正确處理擁塞, 最好的辦法是實作 NAPI, 我們在快速讨論中斷處理後讨論它.
17.7. 中斷處理
大部分硬體接口通過一個中斷處理來控制. 硬體中斷處理器來發出 2 種可能的信号: 一個新封包到了或者一個外出封包的發送完成了. 網絡接口也能夠産生中斷來訓示錯誤, 例如狀态改變, 等等.
通常的中斷過程能夠告知新封包到達中斷和發送完成通知的差別, 通過檢查實體裝置中的狀态寄存器. snull 接口類似地工作, 但是它的狀态字在軟體中實作, 位于 dev->priv. 網絡接口的中斷處理看來如此:
static void snull_regular_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
int statusword;
struct snull_priv *priv;
struct snull_packet *pkt = NULL;
struct net_device *dev = (struct net_device *)dev_id;
if (!dev)
return;
priv = netdev_priv(dev);
spin_lock(&priv->lock);
statusword = priv->status;
priv->status = 0;
if (statusword & SNULL_RX_INTR) {
pkt = priv->rx_queue;
if (pkt) {
priv->rx_queue = pkt->next;
snull_rx(dev, pkt);
}
}
if (statusword & SNULL_TX_INTR) {
priv->stats.tx_packets++;
priv->stats.tx_bytes += priv->tx_packetlen;
dev_kfree_skb(priv->skb);
}
spin_unlock(&priv->lock);
if (pkt) snull_release_buffer(pkt);
return;
}
中斷處理的第一個任務是取一個指向正确 net_device 結構的指針. 這個指針通常來自作為參數收到的 dev_id 指針.
中斷處理的有趣部分處理"發送結束"的情況. 在這個情況下, 統計量被更新, 調用 dev_kfree_skb 來傳回 socket 緩存給系統. 實際上, 有這個函數的 3 個變體可以調用:
dev_kfree_skb(struct sk_buff *skb);
這個版本應當在你知道你的代碼不會在中斷上下文中運作時調用. 因為 snull 沒有實際的硬體中斷, 我們使用這個版本.
dev_kfree_skb_irq(struct sk_buff *skb);
如果你知道會在中斷進行中釋放緩存, 使用這個版本, 它對這個情況做了優化.
dev_kfree_skb_any(struct sk_buff *skb);
如果相關代碼可能在中斷或非中斷上下文運作時, 使用這個版本.
最後, 如果你的驅動已暫時停止了發送隊列, 這常常是用 netif_wake_queue 重新開機它的地方.
封包的接收, 相比于發送, 不需要特别的中斷處理. 調用 snull_rx (我們已經見過)就是全部所需.
17.8. 接收中斷緩解
當一個網絡驅動如我們上面所述編寫出來, 你的接口收到每個封包都中斷處理器. 在許多情況下, 這是希望的操作模式, 它不是個問題. 然而, 高帶寬接口能夠在每秒内收到幾千個封包. 這個樣子的中斷負載下, 系統的整體性能會受損害.
作為一個提高高端 Linux 系統性能的方法, 網絡子系統開發者已建立了一種可選的基于查詢的接口(稱為 NAPI). [
52
]"查詢"可能是一個不妥的字在驅動開發者看來, 他們常常看到查詢是不靈巧和低效的. 查詢是低效的, 但是, 僅僅在接口沒有工作做的時候被查詢. 當系統有一個處理大流量的高速接口時, 會一直有更多的封包來處理. 在這種情況下沒有必要中斷處理器; 時常從接口收集新封包是足夠的.
停止接收中斷能夠減輕相當數量的處理器負載. 适應 NAPI 的驅動能夠被告知不要輸送封包給核心, 如果這些封包隻是在網絡代碼裡因擁塞而被丢棄, 這樣能夠在最需要的時候對性能有幫助. 由于各種理由, NAPI 驅動也比較少可能重排序封包.
不是所有的裝置能夠以 NAPI 模式操作, 但是. 一個 NAPI 适應的接口必須能夠存儲幾個封包( 要麼在接口卡上, 要麼在記憶體内 DMA 環). 接口應當能夠禁止中斷來接收封包, 卻可以繼續因成功發送或其他事件而中斷. 有其他微妙的事情使得編寫一個适應 NAPI 的驅動更有難度; 詳情見核心源碼中的 Documentation/networking/NAPI_HOWTO.txt.
相對少有驅動實作 NAPI 接口. 如果你在編寫一個驅動給一個可能産生大量中斷的接口, 但是, 花點時間來實作 NAPI 會被證明是很值得的.
snull 驅動, 當用非零的 use_napi 參數加載時, 在 NAPI 模式下操作. 在初始化時, 我們不得不建立一對格外的結構 net_device 的成員:
if (use_napi) {
dev->poll = snull_poll;
dev->weight = 2;
}
poll 成員必須設定為你的驅動的查詢函數; 我們簡短看一下 snull_poll. weight 成員描述接口的相對重要性: 有多少流量可以從接口收到, 當資源緊張時. 如何設定 weight 參數沒有嚴格的規則; 依照慣例, 10 MBps 以太網接口設定 weight 為 16, 而快一些的接口使用 64. 你不能設定 weight 為一個超過你的接口能夠存儲的封包數目的值. 在 snull, 我們設定 weight 為 2, 作為一個示範不同封包接收的方法.
建立适應 NAPI 的驅動的下一步是改變中斷處理. 當你的接口(它應當在接收中斷使能下啟動)示意有封包到達, 中斷處理不應當處理這個封包. 相反, 它應當禁止後面的接收中斷并告知核心到時候查詢接口了. 在 snull的"中斷"處理裡, 響應封包接收中斷的代碼已變為如下:
if (statusword & SNULL_RX_INTR) {
snull_rx_ints(dev, 0);
netif_rx_schedule(dev);
}
當接口告訴我們有封包來了, 中斷處理将其留在接口中; 此時需要的所有東西就是調用 netif_rx_schedule, 它使得我們的 poll 方法在後面某個時候被調用.
poll 方法有下面原型:
int (*poll)(struct net_device *dev, int *budget);
snull 的 poll 方法實作看來如此:
static int snull_poll(struct net_device *dev, int *budget)
{
int npackets = 0, quota = min(dev->quota, *budget);
struct sk_buff *skb;
struct snull_priv *priv = netdev_priv(dev);
struct snull_packet *pkt;
while (npackets rx_queue) {
pkt = snull_dequeue_buf(dev);
skb = dev_alloc_skb(pkt->datalen + 2);
if (! skb) {
if (printk_ratelimit())
printk(KERN_NOTICE "snull: packet dropped\n"); priv->stats.rx_dropped++; snull_release_buffer(pkt); continue;
}
memcpy(skb_put(skb, pkt->datalen), pkt->data, pkt->datalen);
skb->dev = dev;
skb->protocol = eth_type_trans(skb, dev);
skb->ip_summed = CHECKSUM_UNNECESSARY;
netif_receive_skb(skb);
npackets++;
priv->stats.rx_packets++;
priv->stats.rx_bytes += pkt->datalen;
snull_release_buffer(pkt);
}
*budget -= npackets;
dev->quota -= npackets;
if (! priv->rx_queue) {
netif_rx_complete(dev);
snull_rx_ints(dev, 1);
return 0;
}
return 1;
}
函數的中心部分是關于建立一個保持封包的 skb; 這部分代碼和我們之前在 snull_rx 中見到的一樣. 但是, 有些東西不一樣:
如果 poll 方法能夠在給定的限制内處理所有的封包, 它應當重新使能接收中斷, 調用 netif_rx_complete 來關閉 查詢, 并且傳回 0. 傳回值 1 訓示有剩下的封包需要處理.
網絡子系統保證任何給定的裝置的 poll 方法不會在多于一個處理器上被同時調用. 但是, poll 調用仍然可以與你的其他裝置方法的調用并發.
[
52
] NAPI 代表"new API"; 網絡黑客們精于建立接口卻疏于給它們起名.
17.9. 連接配接狀态的改變
網絡連接配接, 根據定義, 打交道本地系統之外的世界. 是以, 它們常常受外界事件的影響, 并且它們可能是短暫的東西. 網絡子系統需要知道網絡連接配接的上或下, 它提供了幾個驅動可用來傳達這種資訊的函數.
大部分涉及實際的實體連接配接的網絡技術提供有一個載波狀态; 載波存在說明硬體存在并準備好. 以太網擴充卡, 例如, 在電線上感覺載波信号; 當一個使用者絆倒一根電纜, 載波消失, 連接配接斷開. 預設地, 網絡裝置假設有載波信号存在. 驅動可以明确改變這個狀态, 但是, 使用這些函數:
void netif_carrier_off(struct net_device *dev);
void netif_carrier_on(struct net_device *dev);
如果你的驅動檢測到它的一個裝置載波丢失, 它應當調用 netif_carrier_off 來通知核心這個改變. 當載波回來時, 應當調用 netif_carrier_on. 一些驅動也調用 netif_carrier_off 當進行大的配置改變時(例如媒介類型); 一旦擴充卡已經完成複位它自身, 新載波被檢測并且恢複流量.
一個整數函數也存在:
int netif_carrier_ok(struct net_device *dev);
它可用于測試目前載波狀态( 如同裝置結構中所反映的 );
17.10. Socket 緩存
我們現在已經涵蓋到了大部分關于網絡接口的問題. 還缺乏的是對 sk_buff 結構的較長的描述.這個結構處于 Linux 核心網絡子系統的核心, 我們現在介紹這個結構的重要成員和操作它們的函數.
盡管沒有嚴格要求去了解 sk_buff 的内部, 能夠檢視它的内容的能力在你追蹤問題和試圖優化代碼時是有幫助的. 例如, 如果你看 loopback.c, 你會發現一個基于對 sk_buff 内部了解的優化. 這裡适用的通常的警告是: 如果你編寫利用 sk_buff 結構的知識的代碼, 你應當準備好在以後核心發行中它壞掉. 仍然, 有時性能優勢值得額外的維護開銷.
我們這裡不會描述整個結構, 隻是那些在驅動裡可能用到的. 如果你想看到更多, 你可以檢視 , 那裡定義了結構和函數原型. 關于如何使用這些成員和函數的額外的細節可以通過搜尋核心源碼很容易擷取.
17.10.1. 重要成員變量
這裡介紹的成員是驅動可能需要存取的. 以非特别的順序列出它們.
struct net_device *dev;
接收或發送這個緩存的裝置
union { } h;
union { } nh;
union { } mac;
指向封包中包含的各級的頭的指針. union 中的某個成員都是一個不同資料結構類型的指針. h 含有傳輸層頭部指針(例如, struct tcphdr *th); nh 包含網絡層頭部(例如 struct iphdr *iph); 以及 mac 包含鍊路層頭部指針(例如 struct ethkr * ethernet).
如果你的驅動需要檢視 TCP 封包的源和目的位址, 可以在 skb->h.th 中找到. 看頭檔案來找到全部的可以這樣存取的頭部類型.
注意網絡驅動負責設定進入封包的 mac 指針. 這個任務正常是由 eth_type_trans 處理, 但是 非以太網驅動不得不直接設定 skb->mac.raw, 如同"非以太網頭部"一節所示.
unsigned char *head;
unsigned char *data;
unsigned char *tail;
unsigned char *end;
用來尋址封包中資料的指針. head 指向配置設定記憶體的開始, data 是有效位元組的開始(并且常常稍微比 head 大一些), tail 是有效位元組的結尾, end 指向 tail 能夠到達的最大位址. 檢視它的另一個方法是可用緩存空間是 skb->end - skb->head, 目前使用的空間是 skb->tail - skb->data.
unsigned int len;
unsigned int data_len;
len 是封包中全部資料的長度, 而 data_len 是封包存儲于單個片中的部分的長度. 除非使用發散/彙聚 I/O, data_len 成員的值為 0.
unsigned char ip_summed;
這個封包的校驗和政策. 由驅動在進入封包上設定這個成員, 如在"封包接收"一節中描述的.
unsigned char pkt_type;
在遞送中使用的封包分類. 驅動負責設定它為 PACKET_HOST (封包是給自己的), PACKET_OTHERHOST (不, 這個封包不是給我的), PACKET_BROADCAST, 或者 PACKET_MULTICAST. 以太網驅動不顯式修改 pkt_type, 因為 eth_type_trans 為它們做.
shinfo(struct sk_buff *skb);
unsigned int shinfo(skb)->nr_frags;
skb_frag_t shinfo(skb)->frags;
由于性能的原因, 有些 skb 資訊存儲于一個分開的結構中, 它在記憶體中緊接着 skb. 這個"shared info"(這樣命名是因為它可以在網絡代碼中多個 skb 拷貝中共享)必須通過 shinfo 宏定義來存取. 這個結構中有幾個成員, 但是大部分超出本書的範圍. 我們在"發散/彙聚 I/O"一節中見過 nr_frags 和 frags.
在結構中剩下的成員不是特别有趣. 它們用來維護緩存清單, 來統計 socket 擁有的緩存大小, 等等.
17.10.2. 作用于 socket 緩存的函數
使用一個 sk_buff 結構的網絡驅動利用正式接口函數來操作它. 許多函數操作一個 socket 緩存; 這裡是最有趣的幾個:
struct sk_buff *alloc_skb(unsigned int len, int priority);
struct sk_buff *dev_alloc_skb(unsigned int len);
配置設定一個緩存區. alloc_skb 函數配置設定一個緩存并且将 skb->data 和 skb->tail 都初始化成 skb->head. dev_alloc_skb 函數是使用 GFP_ATOMIC 優先級調用 alloc_skb 的快捷方法, 并且在 skb->head 和 skb->data 之間保留了一些空間. 這個資料空間用在網絡層之間的優化, 驅動不要動它.
void kfree_skb(struct sk_buff *skb);
void dev_kfree_skb(struct sk_buff *skb);
void dev_kfree_skb_irq(struct sk_buff *skb);
void dev_kfree_skb_any(struct sk_buff *skb);
釋放緩存. kfree_skb 調用由核心在内部使用. 一個驅動應當使用一種 dev_kfree_skb 的變體: 在非中斷上下文中使用 dev_kfree_skb, 在中斷上下文中使用 dev_kfree_skb_irq, 或者 dev_kfree_skb_any 在任何 2 種情況下.
unsigned char *skb_put(struct sk_buff *skb, int len);
unsigned char *__skb_put(struct sk_buff *skb, int len);
更新 sk_buff 結構中的 tail 和 len 成員; 它們用來增加資料到緩存的結尾, 每個函數的傳回值是 skb->tail 的前一個值(換句話說, 它指向剛剛建立的資料空間). 驅動可以使用傳回值通過引用 memcpy(skb_put(...), data, len) 來拷貝資料或者一個等同的東東. 兩個函數的差別在于 skb_put 檢查以确認資料适合緩存, 而 __skb_put 省略這個檢查.
unsigned char *skb_push(struct sk_buff *skb, int len);
unsigned char *__skb_push(struct sk_buff *skb, int len);
遞減 skb->data 和遞增 skb->len 的函數. 它們與 skb_put 相似, 除了資料是添加到封包的開始而不是結尾. 傳回值指向剛剛建立的資料空間. 這些函數用來在發送封包之前添加一個硬體頭部. 又一次, __skb_push 不同在它不檢查空間是否足夠.
int skb_tailroom(struct sk_buff *skb);
傳回可以在緩存中放置資料的可用空間數量. 如果驅動放了多于它能持有的資料到緩存中, 系統傻掉. 盡管你可能反對說一個 printk 會足夠來辨別出這個錯誤, 記憶體破壞對系統是非常有害的以至于開發者決定采取确定的動作. 實際中, 你不該需要檢查可用空間, 如果緩存被正确地配置設定了. 因為驅動常常在配置設定緩存前獲知封包的大小, 隻有一個嚴重壞掉的驅動會在緩存中安放太多的資料, 這樣出亂子就可當作一個應得的懲罰.
int skb_headroom(struct sk_buff *skb);
傳回 data 前面的可用空間數量, 就是, 可以 "push" 給緩存多少位元組.
void skb_reserve(struct sk_buff *skb, int len);
遞增 data 和 tail. 這個函數可用來在填充資料前保留白間. 大部分以太網接口保留 2 個位元組在封包的前面; 是以, IP 頭對齊到 16 位元組, 在 14 位元組的以太網頭後面. snull 也這樣做, 盡管沒有在"封包接收"一節中展現這個指令以避免在那時引入過多概念.
unsigned char *skb_pull(struct sk_buff *skb, int len);
從封包的頭部去除資料. 驅動不會需要使用這個函數, 但是為完整而包含在這兒. 它遞減 skb->len 和遞增 skb->data; 這是硬體頭如何從進入封包開始被剝離.
int skb_is_nonlinear(struct sk_buff *skb);
傳回一個真值, 如果這個 skb 分離為多個片為發散/彙聚 I/O.
int skb_headlen(struct sk_buff *skb);
傳回 skb 的第一個片的長度(由 skb->data 指着).
void *kmap_skb_frag(skb_frag_t *frag);
void kunmap_skb_frag(void *vaddr);
如果你必須從核心中的一個非線性 skb 直接存取片, 這些函數為你映射以及去映射它們. 使用一個原子性 kmap, 是以你不能一次映射多于一個片.
核心定義了幾個其他的作用于 socket 緩存的函數, 但是它們是打算用于高層網絡代碼, 驅動不需要它們.
17.11. MAC 位址解析
以太網通訊的一個有趣的方面是如何将 MAC 位址( 接口的唯一硬體 ID )和 IP 編号結合起來. 大部分協定有類似的問題, 但我們這裡集中于類以太網的情況. 我們試圖提供這個問題的完整描述, 是以我們展示三個情形: ARP, 無 ARP 的以太網頭部( 例如 plip), 以及非以太網頭部.
17.11.1. 以太網使用 ARP
處理位址解析的通常方法是使用 Address Resolution Protocol (ARP). 幸運的是, ARP 由核心來管理, 并且一個以太網接口不需要做特别的事情來支援 ARP. 隻要 dev->addr 和 dev->addr_len 在 open 時正确的指派了, 驅動就不需要擔心解決 IP 編号對應于 MAC 位址; ether_setup 安排正确的裝置方法給 dev->hard_header 和 dev_rebuild_header.
盡管通常核心處理位址解析的細節(并且緩存結果), 它需要接口驅動來幫助建立封包. 畢竟, 驅動知道實體層頭部細節, 然而網絡代碼的作者已經試圖隔離核心其他部分. 為此, 核心調用驅動的 hard_header 方法使用 ARP 查詢的結果來布置封包. 正常地, 以太網驅動編寫者不需要知道這個過程 -- 公共的以太網代碼負責了所有事情.
17.11.2. 不考慮 ARP
簡單的點對點網絡接口, 例如 plip, 可能從使用以太網頭部中受益, 而避免來回發送 ARP 封包的開銷. snull 中的例子代碼也屬于這一類的網絡裝置. snull 不能使用 ARP 因為驅動改變發送封包中的 IP 位址, ARP 封包也交換 IP 位址. 盡管我們可能輕易實作了一個簡單 ARP 應答發生器, 更多的是示範性的來展示如何直接處理網絡層頭部.
如果你的裝置想使用通常的硬體頭而不運作 ARP, 你需要重寫預設的 dev->hard_header 方法. 這是 snull 的實作, 作為一個非常短的函數:
int snull_header(struct sk_buff *skb, struct net_device *dev,
unsigned short type, void *daddr, void *saddr,
unsigned int len)
{
struct ethhdr *eth = (struct ethhdr *)skb_push(skb,ETH_HLEN);
eth->h_proto = htons(type);
memcpy(eth->h_source, saddr ? saddr : dev->dev_addr, dev->addr_len);
memcpy(eth->h_dest, daddr ? daddr : dev->dev_addr, dev->addr_len);
eth->h_dest[ETH_ALEN-1] ^= 0x01;
return (dev->hard_header_len);
}
這個函數僅僅用核心提供的資訊并把它格式成标準以太網頭. 它也翻轉目的以太網位址的 1 位, 理由下面叙述.
當接口收到一個封包, eth_type_trans 以幾種方法來使用硬體頭部. 我們已經在 snull_rx 看到這個調用.
skb->protocol = eth_type_trans(skb, dev);
這個函數抽取協定辨別( ETH_P_IP, 在這個情況下 )從以太網頭; 它也指派 skb->mac.raw, 從封包 data (使用 skb_pull)去掉硬體頭部, 并且設定 skb->pkt_type. 最後一項在 skb 配置設定是預設為 PACKET_HOST(訓示封包是發向這個主機的), eth_type_trans 改變它來反映以太網目的位址: 如果這個位址不比對接收它的接口位址, pkt_type 成員被設為 PACKET_OTHERHOST. 結果, 除非接口處于混雜模式或者核心打開了封包轉發, netif_rx 丢棄任何類型為 PACKET_OTHERHOST 的封包. 因為這樣, snull_header 小心地使目的硬體位址比對接收接口.
如果你的接口是點對點連接配接, 你不會想收到不希望的多點傳播封包. 為避免這個問題, 記住, 第一個位元組的最低位(LSB)為 0 的目的位址是方向一個單個主機(即, 要麼 PACKET_HOST, 要麼 PACKET_OTHERHOST). plip 驅動使用 0xfc 作為它的硬體位址的第一個位元組, 而 snull 使用 0x00. 兩個位址都導緻一個工作中的類似以太網的點對點連接配接.
17.11.3. 非以太網頭部
我們剛剛看過硬體頭部除目的位址外包含了一些資訊, 最重要的是通訊協定. 我們現在描述硬體頭部如何用來封裝相關的資訊. 如果你需要知道細節, 你可從核心源碼裡抽取它們或者從特定傳送媒介的技術文檔中. 大部分驅動編寫者能夠忽略這個讨論隻是使用以太網實作.
值得一提的是不是所有資訊都由每個協定提供. 一個點對點連接配接例如 plip 或者 snull 可能在不失去通用性的情況下避免傳送這個以太網頭部. hard_header 裝置方法, 由 snull_header 實作所展示的, 接收自核心的遞交的資訊( 協定級别和硬體位址 ). 它也在 type 參數中接收 16 位協定編号; IP, 例如, 辨別為 ETH_P_IP. 驅動應該正确遞交封包資料和協定編号給接收主機. 一個點對點連接配接可能它的硬體頭部的位址, 隻傳送協定編号, 因為保證遞交是獨立于源和目的位址的. 一個隻有 IP 的連接配接甚至可能不發送任何硬體頭部.
當封包在連接配接的另一端被收到, 接收函數應當正确設定成員 skb->protocol, skb->pkt_type, 和 skb->mac.raw.
skb->mac.raw 是一個字元指針, 由在高層的網絡代碼(例如, net/ipv4/arp.c)所實作的位址解析機制使用. 它必須指向一個比對 dev->type 的機器位址. 裝置類型的可能的值在 中定義; 以太網接口使用 ARPHRD_ETHER. 例如, 這是 eth_type_trans 如何處理收到的封包的以太網頭:
skb->mac.raw = skb->data;
skb_pull(skb, dev->hard_header_len);
在最簡單的情況下( 一個沒有頭的點對點連接配接 ), skb->mac.raw 可指向一個靜态緩存, 包含接口的硬體位址, protocol 可設定為 ETH_P_IP, 并且 packet_type 可讓它是預設的值 PACKET_HOST.
因為每個硬體類型是獨特的, 給出超出已經讨論的特别的裝置是困難的. 核心中滿是例子, 但是. 例如, 可檢視 AppleTalk 驅動( drivers/net/appletalk/cops.c), 紅外驅動(例如, driver/net/irds/smc_ircc.c), 或者 PPP 驅動( drivers/net/ppp_generic.c).
17.12. 定制 ioctl 指令
我們硬體看到給 socket 實作的 ioctl 系統調用; SIOCSIFADDR 和 SIOCSIFMAP 是 "socket ioctls" 的例子. 現在我們看看網絡代碼如何使用這個系統調用的 3 個參數.
當 ioctl 系統調用在一個 socket 上被調用, 指令号是 中定義的符号中的一個, 并且 sock_ioctl 函數直接調用一個協定特定的函數(這裡"協定"指的是使用的主要網絡協定, 例如, IP 或者 AppleTalk).
任何協定層不識别的 ioctl 指令傳遞給裝置層. 這些裝置有關的 ioctl 指令從使用者空間接收一個第 3 個參數, 一個 struct ifreq*. 這個結構定義在 . SIOCSIFADDR 和 SIOCSIFMAP 指令實際上在 ifreq 結構上工作. SIOCSIFMAP 的額外參數, 定義為 ifmap, 隻是 ifreq 的一個成員.
除了使用标準調用, 每個接口可以定義它自己的 ioctl 指令. plip 接口, 例如, 允許接口通過 ioctl 修改它内部的逾時值. socket 的 ioctl 實作認識 16 作為接口私有的個指令: SIOCDEVPRIVATE 到 SIOCDEVPRIVATE+15.[
53
]
當這些指令中的一個被識别, dev->do_ioctl 在相關的接口驅動中被調用. 這個函數接收與通用 ioctl 函數使用的相同的 struct ifreq * 指針.
int (*do_ioctl)(struct net_device *dev, struct ifreq *ifr, int cmd);
ifr 指針指向一個核心空間位址, 這個位址持有使用者傳遞的結構的一個拷貝. 在 do_ioctl 傳回之後, 結構被拷貝回使用者空間; 是以, 驅動可以使用這些私有指令接收和傳回資料.
裝置特定的指令可以選擇使用結構 ifreq 中的成員, 但是它們已經傳達一個标準意義, 并且不可能驅動使這個結構适應自己的需要. 成員 ifr_data 是一個 caddr_t 項( 一個指針 ), 是打算用做裝置特定的需要. 驅動和用來調用它的 ioctl 指令的程式應當一緻地使用 ifr_data. 例如, ppp-stats 使用裝置特定的指令來從 ppp 接口驅動擷取資訊.
這裡不值得展示一個 do_ioctl 的實作, 但是有了本章的資訊和核心例子, 你應當能夠在你需要時編寫一個. 注意, 但是, plip 實作使用 ifr_data 不正确, 不應當作為一個 ioctl 實作的例子.
[
53
] 注意, 根據 , SIOCDEVPRIVATE 指令是被不贊成的. 應當使用什麼來代替它們是不明确的, 但是, 并且不少在目錄樹中的驅動還使用它們.
17.13. 統計資訊
驅動需要的最後一個方法是 get_stats. 這個方法傳回一個指向給裝置的統計的指針. 它的實作非常簡單; 展示過的這個即便在幾個接口由同一個驅動管理時都好用, 因為統計量駐留于裝置資料結構内部.
struct net_device_stats *snull_stats(struct net_device *dev)
{
struct snull_priv *priv = netdev_priv(dev);
return &priv->stats;
}
需要傳回有意義統計的真正工作是分布在整個驅動中的, 有各種成員量被更新. 下列清單展示了最有趣的結構 net_device_stats 中的成員:
unsigned long rx_packets;
unsigned long tx_packets;
接口成功傳送的進入和出去封包的總和.
unsigned long rx_bytes;
unsigned long tx_bytes;
接口接收和發送的位元組數.
unsigned long rx_errors;
unsigned long tx_errors;
接收和發送的錯誤數. 封包發送可能出錯的事情是沒有結束的, net_device_stats 結構包括 6 個計數器給特定的接收錯誤以及有 5 個給發送錯誤. 完整清單看 . 如果可能, 你的驅動調用維護詳細的錯誤統計, 因為它們是對系統管理者試圖追蹤問題的最大幫助.
unsigned long rx_dropped;
unsigned long tx_dropped;
在接收和發送中丢失的封包數目. 當沒有可用記憶體給封包資料時丢棄封包. tx_dropped 極少使用.
unsigned long collisions;
由于媒體擁塞引起的沖突數目.
unsigned long multicast;
收到的多點傳播封包數目.
值得重複一下, get_stats 方法可以在任何時候調用 -- 即便在接口關閉時 -- 是以隻要 net_device 結構存在驅動必須保持統計資訊.
17.14. 多點傳播
一個多點傳播封包是一個會被多個主機接收的網絡封包, 但不是所有主機. 這個功能通過給一組主機配置設定特殊的硬體位址來獲得. 發向一個特殊位址的封包應當被那個組當中的所有主機接收. 在以太網的情況下, 一個多點傳播位址在目的位址的第一個位元組的最低位為 1, 而每個裝置闆在它自己的硬體位址的這一位上為 0.
處理主機組和硬體位址的技巧由應用程式和核心處理, 接口驅動不必處理這個問題.
多點傳播封包的傳送是一個簡單問題, 因為它們看起來就如同其他的封包. 接口發送它們通過通訊媒介, 不檢視目的位址. 核心必須要安排一個正确的硬體目的位址; hard_header 裝置方法, 如果定義了, 不必檢視它安排的資料.
核心來跟蹤在任何給定時間對哪些多點傳播位址感興趣. 這個清單可能經常改變, 因為它是在任何給定時間和按照使用者意願運作的應用程式的功能. 驅動的工作是接收感興趣的多點傳播位址清單并遞交給核心任何發向這些位址的封包. 驅動如何實作多點傳播清單是依賴于底層硬體是如何工作的. 典型地, 在多點傳播的角度上, 硬體屬于 3 類中的 1 種:
可以進行硬體檢測多點傳播位址的接口. 可以傳遞一個多點傳播位址的清單給這些接口, 這些位址的封包接收, 并忽略其他多點傳播位址的封包. 對核心這是優化的情況, 因為它不浪費處理器時間來丢棄接口收到的"不感興趣"的封包.
核心盡力利用進階接口的能力, 通過支援第 3 種裝置類型, 它是最通用的. 是以, 核心通知驅動, 在任何有效多點傳播位址清單發生改變時, 并且它傳遞新的清單給驅動, 是以它能夠根據新的資訊來更新硬體過濾器.
17.14.1. 多點傳播的核心支援
對多點傳播封包的支援有幾項組成:一個裝置方法, 一個資料結構, 以及裝置辨別:
void (*dev->set_multicast_list)(struct net_device *dev);
裝置方法, 在與裝置相關的機器位址改變時調用. 它也在 dev->flags 被修改時調用, 因為一些标志(例如, IFF_PROMISC) 可能也要求你重新程式設計硬體過濾器. 這個方法接收一個 struct net_device 指針作為一個參數, 并傳回 void. 一個對實作這個方法不感興趣的驅動可以聽任它為 NULL.
struct dev_mc_list *dev->mc_list;
所有裝置相關的多點傳播位址的清單. 這個結構的實際定義在本節的末尾介紹.
int dev->mc_count;
連結清單裡的項數. 這個資訊有些重複, 但是用 0 來檢查 mc_count 是檢查這個清單的有用的方法.
IFF_MULTICAST
除非驅動在 dev->flags 中設定這個标志, 接口不會被要求來處理多點傳播封包. 然而, 核心調用驅動的 set_multicast_list 方法, 當 dev->flags 改變時, 因為多點傳播清單可能在接口未激活時改變了.
IFF_ALLMULTI
在 dev->flags 中設定的标志, 網絡軟體來告知驅動從網絡上接收所有多點傳播封包. 這發生在當多點傳播路由激活時. 如果标志設定了, dev->ma_list 不該用來過濾多點傳播封包.
IFF_PROMISC
在 dev->flags 中設定的标志, 當接口在混雜模式下. 接口應當接收每個封包, 不管 dev->ma_list.
驅動開發者需要的最後一點資訊是 struct dev_mc_list 的定義, 在 :
struct dev_mc_list { struct dev_mc_list *next;
__u8 dmi_addr[MAX_ADDR_LEN];
unsigned char dmi_addrlen;
int dmi_users;
int dmi_gusers;
};
因為多點傳播和硬體位址是獨立于真正的封包發送, 這個結構在網絡實作中是可移植的, 每個位址由一個字元串和一個長度辨別, 就像 dev->dev_addr.
17.14.2. 典型實作
描述 set_multicast_list 的設計的最好方法是給你看一些僞碼.
下面的函數是一個典型函數實作在一個全特性(ff)驅動中. 這個驅動是全模式的, 它控制的接口有一個複雜的硬體封包過濾器, 它能夠持有一個主機要接收的多點傳播位址表. 表的最大尺寸是 FF_TABLE_SIZE.
所有以 ff_ 字首的函數是給特定硬體操作的占位者:
void ff_set_multicast_list(struct net_device *dev) { struct dev_mc_list *mcptr;
if (dev->flags & IFF_PROMISC) {
ff_get_all_packets();
return;
}
if (dev->flags & IFF_ALLMULTI || dev->mc_count > FF_TABLE_SIZE) {
ff_get_all_multicast_packets();
return;
}
if (dev->mc_count == 0) {
ff_get_only_own_packets();
return;
}
ff_clear_mc_list();
for (mc_ptr = dev->mc_list; mc_ptr; mc_ptr = mc_ptr->next)
ff_store_mc_address(mc_ptr->dmi_addr);
ff_get_packets_in_multicast_list();
}
這個實作可以簡化, 如果接口不能為進入封包存儲多點傳播表在硬體過濾器中. 這種情況下, FF_TABLE_SIZE 減為 0, 并且代碼的最後 4 行不需要了.
如同前面提過的, 不能處理多點傳播封包的接口不需要實作 set_multicast_list 方法來擷取 dev->flags 改變的通知. 這個辦法可能被稱為一個"非特性的"(nf)實作. 實作非常簡單, 如下面代碼所示:
void nf_set_multicast_list(struct net_device *dev)
{
if (dev->flags & IFF_PROMISC)
nf_get_all_packets();
else
nf_get_only_own_packets();
}
實作 IFF_PROMISC 是非常重要的, 因為不這樣使用者就不能運作 tcpdump 或任何其他網絡分析器. 如果接口運作一個點對點連接配接, 另一方面, 根本沒有必要實作 set_multicast_list, 因為使用者接收每個封包.
17.15. 幾個其他細節
本節涵蓋了幾個其他主題, 對網絡驅動作者感興趣的. 在每種情況, 我們試着簡單指點你正确的方向. 擷取了一個主題的完整描繪可能還需要花費一些時間深入核心源碼.
17.15.1. 獨立于媒介的接口支援
媒介獨立接口(或 MII) 是一個 IEEE 802.3 标準, 描述以太網收發器如何與網絡控制器接口; 很多市場上的産品遵守這個接口. 如果你在編寫一個驅動為一個 MII 相容控制器, 核心輸出了一個通用 MII 支援層, 可能會使你易做一些.
為使用通用 MII 層, 你應當包含 . 你需要填充一個 mii_if_info 結構使用收發器的實體 ID 資訊, 如是否全雙工有效. 還要求 mii_if_info 結構的 2 個方法:
int (*mdio_read) (struct net_device *dev, int phy_id, int location);
void (*mdio_write) (struct net_device *dev, int phy_id, int location, int val);
如你可能預料的, 這些方法應當實作與你的特殊 MII 接口的通訊.
通用的 MII 代碼提供一套函數, 來查詢和改變收發器的操作模式; 許多設計成與 ethtool 工具一起工作( 下一節描述 ). 在 和 drivers/net/mii.c 中檢視細節.
17.15.2. ethtool 支援
ethtool 是一個實用工具, 設計來給系統管理者以大量的控制網絡接口的操作. 用 ethtool, 可能來控制各種接口參數, 包括速度, 媒體類型, 雙工模式, DMA 環設定, 硬體校驗和, LAN 喚醒操作, 等等, 但是隻有當 ethtool 被驅動支援. ethtool 可以從 http://sf.net/projects/gkernel/. 下載下傳.
對 ethtool 支援的相關聲明可在 中找到. 它的核心是一個 ethtool_ops 類型的結構, 裡面包含一個全部 24 個不同方法來支援 ethtool. 大部分這些方法是相對直接地; 細節看 . 如果你的驅動使用 MII 層, 你能使用 mii_ethtool_gset 和 mii_ethtool_sset 來實作 get_settings 和 set_settings 方法, 分别地.
對于和你的裝置一起工作的 ethtool, 你必須放置一個指向你的 ethtool_ops 結構的指針在 net_devcie 結構中. 宏定義 SET_ETHTOOL_OPS( 在 中定義)應當用作這個目的. 注意你的 ethtool 方法可能會在接口關閉時被調用.
Netpoll
17.15.3. netpoll
"netpoll" 是相對遲的增加到網絡協定棧中; 它的目的是使核心能夠發送和接收封包, 在完整的網絡和I/O子系統不可用的情況下. 它用來給如遠端網絡控制台和遠端核心調試等特色使用的. 無論如何, 你的驅動不必支援 netpoll, 但是它可能使你的驅動在某些情況下更有用. 在大部分情況下支援 netpoll 也相對容易.
實作 netpoll 的驅動應當實作 poll_controller 方法. 它的工作是跟上控制器上可能發生的任何東西, 在缺乏裝置中斷時. 幾乎所有的 poll_controller 方法采用下面形式:
void my_poll_controller(struct net_device *dev)
{
disable_device_interrupts(dev);
call_interrupt_handler(dev->irq, dev, NULL);
reenable_device_interrupts(dev);
}
poll_controller 方法, 實際上, 是簡單模拟自給定裝置的中斷.
17.16. 快速參考
本節提供了本章中介紹的概念的參考. 也解釋了每個驅動需要包含的頭檔案的角色. 在 net_device 和 sk_buff 結構中成員的清單, 但是, 這裡沒有重複.
#include
定義 struct net_device 和 struct net_device_stats 的頭檔案, 包含了幾個其他網絡驅動需要的頭檔案.
struct net_device *alloc_netdev(int sizeof_priv, char *name, void (*setup)(struct net_device *);
struct net_device *alloc_etherdev(int sizeof_priv);
void free_netdev(struct net_device *dev);
配置設定和釋放 net_device 結構的函數
int register_netdev(struct net_device *dev);
void unregister_netdev(struct net_device *dev);
注冊和登出一個網絡裝置.
void *netdev_priv(struct net_device *dev);
擷取網絡裝置結構的驅動私有區域的指針的函數.
struct net_device_stats;
持有裝置統計的結構.
netif_start_queue(struct net_device *dev);
netif_stop_queue(struct net_device *dev);
netif_wake_queue(struct net_device *dev);
控制傳送給驅動來發送的封包的函數. 沒有封包被傳送, 直到 netif_start_queue 被調用. netif_stop_queue 挂起發送, netif_wake_queue 重新開機隊列并刺探網絡層重新開機發送封包.
skb_shinfo(struct sk_buff *skb);
宏定義, 提供對封包緩存的"shared info"部分的存取.
void netif_rx(struct sk_buff *skb);
調用來通知核心一個封包已經收到并且封裝到一個 socket 緩存中的函數.
void netif_rx_schedule(dev);
來告訴核心封包可用并且應當啟動查詢接口; 它隻是被 NAPI 相容的驅動使用.
int netif_receive_skb(struct sk_buff *skb);
void netif_rx_complete(struct net_device *dev);
應當隻被 NAPI 相容的驅動使用. netif_receive_skb 是對于 netif_rx 的 NAPI 對等函數; 它遞交一個封包給核心. 當一個 NAPI 相容的驅動已耗盡接收封包的供應, 它應當重開中斷, 并且調用 netif_rx_complete 來停止查詢.
#include
由 netdevice.h 包含, 這個檔案聲明接口标志( IFF_ 宏定義 )和 struct ifmap, 它在網絡驅動的 ioctl 實作中有重要地位.
void netif_carrier_off(struct net_device *dev);
void netif_carrier_on(struct net_device *dev);
int netif_carrier_ok(struct net_device *dev);
前 2 個函數可用來告知核心是否接口上有載波信号. netif_carrier_ok 測試載波狀态, 如同在裝置結構中反映的.
#include
ETH_ALEN
ETH_P_IP
struct ethhdr;
由 netdevice.h 包含, if_ether.h 定義所有的 ETH_ 宏定義, 用來代表位元組長度( 例如位址長度 )以及網絡協定(例如 IP). 它也定義 ethhdr 結構.
#include
struct sk_buff 和相關結構的定義, 以及幾個操作緩存的内聯函數. 這個頭檔案由 netdevice.h 包含.
struct sk_buff *alloc_skb(unsigned int len, int priority);
struct sk_buff *dev_alloc_skb(unsigned int len);
void kfree_skb(struct sk_buff *skb);
void dev_kfree_skb(struct sk_buff *skb);
void dev_kfree_skb_irq(struct sk_buff *skb);
void dev_kfree_skb_any(struct sk_buff *skb);
處理 socket 緩存的配置設定和釋放的函數. 通常驅動應當使用 dev_ 變體, 其意圖就是此目的.
unsigned char *skb_put(struct sk_buff *skb, int len);
unsigned char *__skb_put(struct sk_buff *skb, int len);
unsigned char *skb_push(struct sk_buff *skb, int len);
unsigned char *__skb_push(struct sk_buff *skb, int len);
添加資料到一個 skb 的函數; skb_put 在 skb 的尾部放置資料, 而 skb_push 放在開始. 正常版本進行檢查以確定有足夠的空間; 雙下劃線版本不進行檢查.
int skb_headroom(struct sk_buff *skb);
int skb_tailroom(struct sk_buff *skb);
void skb_reserve(struct sk_buff *skb, int len);
進行 skb 中的空間管理的函數. skb_headroom 和 skb_tailroom 說明在開始和結尾分别有多少空間可用. skb_reserve 可用來保留白間, 在一個必須為空的 skb 開始.
unsigned char *skb_pull(struct sk_buff *skb, int len);
skb_pull "去除" 資料從一個 skb, 通過調整内部指針.
int skb_is_nonlinear(struct sk_buff *skb);
如果這個 skb 是為發散/彙聚 I/O 分隔為幾個片, 函數傳回一個真值.
int skb_headlen(struct sk_buff *skb);
傳回 skb 的第一個片的長度, 由 skb->data 指向.
void *kmap_skb_frag(skb_frag_t *frag);
void kunmap_skb_frag(void *vaddr);
提供對非線性 skb 中的片直接存取的函數.
#include
void ether_setup(struct net_device *dev);
為以太網驅動設定大部分方法為通用實作的函數. 它還設定 dev->flags 和安排下一個可用的 ethx 給 dev->name, 如果名子的第一個字元是一個空格或者 NULL 字元.
unsigned short eth_type_trans(struct sk_buff *skb, struct net_device *dev);
當一個以太網接口收到一個封包, 這個函數被調用來設定 skb->pkt_type. 傳回值是一個協定号, 通常存儲于 skb->protocol.
#include
SIOCDEVPRIVATE
前 16 個 ioctl 指令, 每個驅動可為它們自己的私有用途而實作. 所有的網絡 ioctl 指令都在 sockios.h 中定義.
#include
struct mii_if_info;
聲明和一個結構, 支援實作 MII 标準的裝置的驅動.
#include
struct ethtool_ops;
聲明和結構, 使得裝置與 ethtool 工具一起工作.