編寫Linux網絡設備驅動(上)
本文介紹基於Realtek 8139芯片PCI接口的網卡驅動程序。我選擇了Realtek芯片有兩個原因:首先,Realtek提供免費的芯片技術手冊; 第二,芯片相當便宜。
本文介紹的驅動程序是最基本的,它只有發送和接收數據包功能,和做一些簡單的統計。對於一個全面和專業級的驅動程序,請參閱Linux源碼。
本文代碼是基於Linux2.4.18上測試的,建議編譯一個內核,此內核沒有任何形式RealTek8139驅動程序,以避免有莫名的BUG。最後,你將網卡插入PCI插槽,我們可以開始了。
目錄
網絡設備驅動程序的開發,分解成以下步驟:
上:
- 1.檢測設備
- 2.啓用設備
- 3.認識網絡設備
- 4.總線無關的設備訪問
- 5.理解PCI配置空間
- 6.初始化網絡設備(net_device)
中:
- 7.RTL8139收發原理
下:
- 8.編寫網絡設備的發包功能
- 9.編寫網絡設備的收包功能
1.設備檢測
第一步,我們需要檢測的網卡設備。 Linux內核提供了豐富的API檢測PCI總線上的設備,我們這隻用其中最簡單的一個API——pci_find_device。
#define REALTEK_VENDER_ID 0x10EC #define REALTEK_DEVICE_ID 0x8139 #include <linux/kernel.h> #include <linux/module.h> #include <linux/stddef.h> #include <linux/pci.h> int init_module(void) { struct pci_dev *pdev; pdev = pci_find_device(REALTEK_VENDER_ID, REALTEK_DEVICE_ID, NULL); if(!pdev) printk("<1>Device not found\n"); else printk("<1>Device found\n"); return 0; }
Table 1: Detecting the device
PCI標準爲每個供應商分配一個唯一的Vendor ID,供應商會爲每一個特定類型的設備分配一個唯一的Device ID。宏REALTEK_VENDER_ID、REALTEK_DEVICE_ID表示這些ID。你可以在RealTek8139規範的“PCI配置空間表”找到這些值。
2.設備啓用(Enabling)
檢測到設備後,我們使用設備之前,我必須先激活設備,這個步驟稱爲[啓用設備]。表2所示的代碼片段是[設備檢測]和[設備啓用]合併的代碼。
#define REALTEK_VENDER_ID 0x10EC #define REALTEK_DEVICE_ID 0X8139 static struct pci_dev* probe_for_realtek8139(void) { struct pci_dev *pdev = NULL; /* Ensure we are not working on a non-PCI system * if(!pci_present( )) { LOG_MSG("<1>pci not present\n"); return pdev; } /* Look for RealTek 8139 NIC */ pdev = pci_find_device(REALTEK_VENDER_ID, REALTEK_DEVICE_ID, NULL); if(pdev) { /* device found, enable it */ if(pci_enable_device(pdev)) { LOG_MSG("Could not enable the device\n"); return NULL; } else LOG_MSG("Device enabled\n"); } else { LOG_MSG("device not found\n"); return pdev; } return pdev; } int init_module(void) { struct pci_dev *pdev; pdev = probe_for_realtek8139(); if(!pdev) return 0; return 0; }
在表2,函數probe_for_realtek8139執行以下任務:
- 確保系統支持PCI總線
- 檢測Realtek8139設備
- 如果發現設備,則啓用的設備(通過調用pci_enable_device)
現在,爲了更好地理解代碼,我們先暫停一下驅動程序代碼的研究,轉而看一下Linux內核是怎樣[處理]設備和設備驅動的。我們將着眼於[網絡設備的定義],內存映射I/O和獨立端口I/O之間的差異,還有PCI配置空間的概念。
3.理解何爲網絡設備
我們是檢測到了PCI設備,並啓用它,但它只是一支硬件設備(網卡設備),而Linux的網絡協議棧只認得[網絡設備]。[網絡設備]是一支邏輯設備,由結構net_device表徵。也就是說,網絡協議棧向[網絡設備]發出命令,而[網絡設備]的驅動將這些命令傳遞到PCI[網卡設備]。表3列出了結構net_device的一些重要數據域,這將在本文稍後使用。
struct net_device { char *name; unsigned long base_addr; unsigned char addr_len; unsigned char dev_addr[MAX_ADDR_LEN]; unsigned char broadcast[MAX_ADDR_LEN]; unsigned short hard_header_len; unsigned char irq; int (*open) (struct net_device *dev); int (*stop) (struct net_device *dev); int (*hard_start_xmit) (struct sk_buff *skb, struct net_device *dev); struct net_device_stats* (*get_stats)(struct net_device *dev); void *priv; };
Table 3: Structure net_device
上表只列出C結構net_device部分成員,不過,對於我們最小驅動程序,這些成員已經足夠。以下簡介這些成員的用途:
- name – 設備的名稱。如果名稱的第一個字符是null,那麼register_netdev分配給它取名爲“ethN”,其中N是合適的數字。例如,如果您的系統已經有eth0和eth1,您的設備將被命名的eth2。
- base_addr – I/O基地址。 I/O地址在本文後面,我們將更深入的討論。
- addr_len – 硬件地址(MAC地址)的長度。以太網接口地址長度爲6字節。
- dev_addr – 硬件地址(以太網地址或MAC地址)
- broadcast – 設備的廣播地址。以太網接口的廣播地址是FF:FF:FF:FF:FF:FF
- hard_header_len – “硬件頭的長度”是數據包硬件頭的八位位組(octets)的數量。 以太網接口的hard_header_len的值是14
- IRQ – 分配的中斷號
- open – 這是打開設備函數的指針。這個函數在用ifconfig命令激活設備時被調用,例如“ifconfig eth0 up”。 open函數負責向系統申請所需的系統資源需求(I/O端口,IRQ,DMA等),啓用硬件和遞增模塊的使用計數。
- stop – 這是停止設備函數的指針。這個函數在用ifconfig命令停用設備時被調用,例如“ifconfig eth0 down”。 stop函數釋放所有open函數獲得的資源。
- hard_start_xmit – 此函數在傳輸線路上發送一個給定的數據包。該函數的第一個參數是指向結構sk_buff指針。結構sk_buff的是通過Linux網絡協議棧的數據包。本文並不需要詳細瞭解有關的sk_buff的結構的細節,你可以在下網址獲得更多的結構sk_buff的信息:http://www.tldp.org/LDP/khg/HyperNews/get/net/net-intro.html。
- get_stats – 此函數提供了接口統計信息。命令“ifconfig eth0”的很多輸出內容來自get_stats。
- priv – 驅動程序的私有數據域。驅動程序擁有這一數據域,並可以使用它。我們稍後會看到,我們的驅動程序使用這一數據域保存與PCI設備相關的數據。
請特別注意,net_device沒有接收數據包的成員函數,這是因爲接收數據包是由設備的[中斷處理程序]負責的,我們將在本文後面看到。
4.總線無關的設備訪問(Bus-Independent)
注:本小節摘自Alan Cox的《Bus-Independent Device Accesses》http://tali.admingilde.org/linux-docbook/deviceiobook.pdf
Linux提供了一個API集(下文稱爲[設備操作API]),抽象所有總線和設備的I/O操作,使設備驅動程序的編寫獨立於總線類型。
4.1 內存映射的I/O
最廣泛支持的I/O的操作是[內存映射I/O]。[內存映射I/O]是指,部分的CPU地址空間被解釋爲訪問設備,而不是訪問內存。一些體系結構爲[內存映射I/O]的設備定義了固定的地址,但大多數體系提供了檢測設備地址的方法。 PCI總線是很好的例子。本文不教你如何獲得一個設備地址,假設你已經知道設備地址。
物理地址是unsigned long類型,你不能直接使用這些地址。你應該調用ioremap,來獲得一個適合(傳遞給下面函數)的虛擬地址。當你使用完的設備(比如模塊卸載),必須調用iounmap以返還虛擬地址給內核。
4.2 訪問設備
在Linux提供[設備操作API]中,驅動程序最常用的接口是訪問的設備寄存器的讀和寫函數。 Linux提供了讀取和寫入8位,16位,32位和64位量的函數,分別爲 byte, word, long, 和 quad,函數命名readb,readw,readl,readq,writeb,writew,writel和writeq。
有些設備(如幀緩衝)更傾向一次內發起超過8個字節的傳輸。對於這些設備,可使用memcpy_toio,memcpy_fromio和memset_io功能。不要使用memset或memcpy對I/O地址操作,因爲它們不能保證[按順序]複製數據。
[設備操作API]中的讀寫函數是假設嚴格[按照源碼字面順序]執行的,編譯器不能對它進行亂序優化。如果希望設備讀寫有一定的優化,可使用原始的__readb函數(等原始無抽象的函數)。 但是要非常小心,要在適當的地方插入內存屏障指令——rmb()/wmb()。
4.3 獨立端口IO
另外一種常用的IO操作是[獨立端口IO]。端口IO的地址是獨立於內存地址空間的,端口IO的訪問速度不如內存映射I/O,地址空間也小很多。不過,不像內存映射I/O,訪問端口IO的設備相對直觀,不需要考慮以上提到的一些問題。
[設備操作API]中提供了訪問端口IO的函數,分別操作字節(byte)、雙字(word)和四字(long):inb, inw, inl, outb, outw 和 outl。
以上函數還有提供給慢速設備的變種:後加一“_p”;還有類似memcpy功能的ins 和 outs。
5.理解PCI配置空間
RTL8139是一支PCI接口設備,PCI是一種通用的擴展總線,而非與CPU體系相關的本地總線(local bus),從而CPU不能直接對RTL8139尋址訪問,必須經PCI總線控制器轉譯。PCI總線設計實現的核心是PCI總線控制器(有的地方譯爲PIC主橋,PCI Host Bridge),它將整個系統劃分兩個數字通信域,兩個域獨立編址。一個是原來CPU與內存和設備通信的[CPU域],一個是PCI總線控制器的(因它本身就是一CPU),這裏稱爲[PCI總線域]。爲了跨越兩域通信,系統將CPU域劃出一個“window”——將CPU部分尋址空間劃給PCI總線用。PCI總線控制器對這部分地址進行管理,實現即插即用等一些現代總線功能。而所謂的[配置空間]只是配合PCI總線控制器實現地址管理提供必要的狀態信息[注]。
注:個人覺得[配置空間]用“空間”一詞欠佳,容易混淆其它地址空間概念,增加理解PCI總線原理的難度。
[配置空間]是每支PCI設備(包括PCI橋)集成一集寄存器,[配置空間]是面向PCI總線控制器而言的,此空間的基地址是PCI設備的拓撲位置(總線號/設備號/功能號)。PCI定義每支PCI設備的[配置空間]爲256字節,如下圖,其中最前面的64個字節已由標準定義,餘下的空間由設備自定義。
圍繞[配置空間]有兩種事務和三種操作角色,事務是[配置]和[使用配置],角色有靜態配置的廠商和動態配置的操作系統,還有使用配置的設備驅動。靜態配置的例子,如廠商在設備生產時配置其Vendor ID和Device ID;動態配置的例子,如操作系統初始化代碼根據PCI設備的拓撲位置,配置設備的基地址(Base Address0~5)[注]。
注:這個地址屬於PCI總線域的地址,而不是CPU域的地址。
使用配置的例子,如設備驅動的初始接口函數讀取基地址寄存器(Base Address Registers),確定設備接口的基地址,下面的RTL8139設備初始化時你可以看到具體例子。
6.初始化net_device
現在我們回到驅動程序代碼的開發上來。剛纔我們已經討論了設備驅動模塊初始化中的設備檢測和啓用的任務,還有網絡設備的表徵結構,接下來我們先看看邏輯設備的初始化任務。
6.1 rtl8139_private
首先,作爲一支特殊的網絡設備,除了有標準的net_device表徵,8139有其特殊數據,這是由C結構rtl8139_private 表徵,由net_device->priv指向。rtl8139_private的定義如下:
struct rtl8139_private { struct pci_dev *pci_dev; /* PCI device */ void *mmio_addr; /* memory mapped I/O addr */ unsigned long regs_len; /* length of I/O or MMI/O region */ };
Table 4: rtl8139_private structure
6.2 init_module
現在我們擴展init_module 函數,添加邏輯設備的初始化的任務。先看代碼:
int init_module(void) { struct pci_dev *pdev; unsigned long mmio_start, mmio_end, mmio_len, mmio_flags; void *ioaddr; struct rtl8139_private *tp; int i; pdev = probe_for_realtek8139( ); if(!pdev) return 0; if(rtl8139_init(pdev, &rtl8139_dev)) { LOG_MSG("Could not initialize device\n"); return 0; } tp = rtl8139_dev->priv; /* rtl8139 private information */
首先probe_for_realtek8139函數檢測和啓用設備後返回一個PCI設備——pdev,然後rtl8139_init用pdev初始化rtl8139_private,轉而初始化網絡設備rtl8139_dev。
我們下一個目標是得到(初始化)設備的基地址——net_device的base_addr域。這是設備寄存器的內存映射的起始地址。本設備驅動程序只使用內存映射IO。
/* get PCI memory mapped I/O space base address from BAR1 */ mmio_start = pci_resource_start(pdev, 1); mmio_end = pci_resource_end(pdev, 1); mmio_len = pci_resource_len(pdev, 1); mmio_flags = pci_resource_flags(pdev, 1); /* make sure above region is MMI/O */ if(!(mmio_flags & I/ORESOURCE_MEM)) { LOG_MSG("region not MMI/O region\n"); goto cleanup1; } /* get PCI memory space */ if(pci_request_regions(pdev, DRIVER)) { LOG_MSG("Could not get PCI region\n"); goto cleanup1; } pci_set_master(pdev);
爲了取得基地址,我們利用了內核PCI總線子系統提供的API:pci_resource_start, pci_resource_end, pci_resource_len, pci_resource_flags。注意這些API函數的第二個參數——BAR號1。PCI規定PCI設備最多可以申請6個PCI總線地址區,這些空間區的基地址分別保存在6個BAR裏。在RealTek8139手冊定義裏,RTL只申請了兩個區,第一個BAR(編號爲0)是I/OAR,第二個 BAR(編號爲1)是MEMAR。由於本設備驅動程序只使用內存映射IO,故BAR選用1。
現在,在使用這些地址之前,我們還有兩件事要做。
/* ioremap MMI/O region */ ioaddr = ioremap(mmio_start, mmio_len); if(!ioaddr) { LOG_MSG("Could not ioremap\n"); goto cleanup2; } rtl8139_dev->base_addr = (long)ioaddr; tp->mmio_addr = ioaddr; tp->regs_len = mmio_len;
這兩個事就是,第一,爲設備驅動保留這些地址(調用pci_request_regions函數),以免被誤用;第二,將這些物理地址重映射(remap);這在前面“內存映射的I/O”小節已經提到,驅動代碼不能用直接使用物理地址。重映射後的地址io_addr填入 net_device的base_addr域後,我們可以讀定設備的寄存器了。
剩下的代碼比較直觀和易理解了。
/* UPDATE NET_DEVICE */ for(i = 0; i < 6; i++) { /* Hardware Address */ rtl8139_dev->dev_addr[i] = readb(rtl8139_dev->base_addr+i); rtl8139_dev->broadcast[i] = 0xff; } rtl8139_dev->hard_header_len = 14; memcpy(rtl8139_dev->name, DRIVER, sizeof(DRIVER)); /* Device Name */ rtl8139_dev->irq = pdev->irq; /* Interrupt Number */ rtl8139_dev->open = rtl8139_open; rtl8139_dev->stop = rtl8139_stop; rtl8139_dev->hard_start_xmit = rtl8139_start_xmit; rtl8139_dev->get_stats = rtl8139_get_stats; /* register the device */ if(register_netdev(rtl8139_dev)) { LOG_MSG("Could not register netdevice\n"); goto cleanup0; } return 0; }
我們用了一個for循環來讀取設備的硬件地址和廣播地址(注意這回是直接用內核[設備操作API]的readb,而不是 PCI總線系統API),設備的硬件地址位於基地址的最前面。另外值得注意的是幾個網絡設備的接口函數指針,如open,hard_start_xmit 等,它們指向還沒有實現的函數。爲了編譯驅動模塊並進行測試,到此暫時爲這些接口函數寫一些Dummy測試代碼。
6.3 邏輯設備的其它接口
static int rtl8139_open(struct net_device *dev) { LOG_MSG("rtl8139_open iscalled\n"); return 0; } static int rtl8139_stop(struct net_device *dev) { LOG_MSG("rtl8139_open is called\n"); return 0; } static int rtl8139_start_xmit(struct sk_buff *skb, struct net_device *dev) { LOG_MSG("rtl8139_start_xmit is called\n"); return 0; } static struct net_device_stats* rtl8139_get_stats(struct net_device *dev) { LOG_MSG("rtl8139_get_stats is called\n"); return 0; }
Table 6: Dummy functions
6.4 註銷函數
最後是註銷函數:
void cleanup_module(void) { struct rtl8139_private *tp; tp = rtl8139_dev->priv; iounmap(tp->mmio_addr); pci_release_regions(tp->pci_dev); unregister_netdev(rtl8139_dev); pci_disable_device(tp->pci_dev); return; }
Table 7: Function cleanup_module
6.5 編譯測試
到此,一支完整的8139網卡設備驅動基本完成了,當然目前還只是一個模板,沒有實質性的功能。我們可以編譯並安裝它了。
$ insmod rtl8139.o
Table 8: Compiling the driver
安裝不出問題的話,我們可以用SHELL命令進行測試:”ifconfig”, “ifconfig – a”, “ifconfig rtl8139 up”, “ifconfig” 和 “ifconfig rtl8139 down”。如無意外,”ifconfig – a” 會列出設備rtl8139;執行 “ifconfig rtl8139 up”會返回消息”function rtl8139_open called”等等……
好了,通過測試後,下一步是實現網絡設備真正的數據收發了。爲了更好理解實現代碼,我們還是需要一些背景知識——理解RTL8139收發原理。