Linux網卡驅動分析

來源:互聯網
上載者:User

 

原文地址 http://www.linuxforum.net/forum/showflat.php?Cat=&Board=driver&Number=635688&page=0&view=collapsed&s
學習應該是一個先把問題簡單化,在把問題複雜化的過程。一開始就著手處理複雜的問題,難免讓人有心驚膽顫,捉襟見肘的感覺。讀Linux網卡驅動也是一樣。那長長的源碼夾雜著那些我們陌生的變數和符號,望而生畏便是理所當然的了。不要擔心,事情總有解決的辦法,先把一些我們管不著的代碼切割出去,留下必須的部分,把架構掌握了,哪其他的事情自然就水到渠成了,這是筆者的心得。
一般在使用的Linux網卡驅動代碼動輒3000行左右,這個代碼量以及它所表達出來的知識量無疑是龐大的,我們有沒有辦法縮短一下這個代碼量,使我們的學習變的簡單些呢,經過筆者的不懈努力,在仍然能夠使網路裝置正常工作的前提下,把它縮減到了600多行,我們把暫時還用不上的功能先割出去。這樣一來,事情就簡單多了,真的就剩下一個架構了(欲索取者請通過 xhbbs@tom.com 聯絡我)。下面我們就來剖析這個可以執行的架構。
限於篇幅,以下分析用到的所有涉及到核心中的函數代碼,我都不予列出,但給出在哪個具體檔案中,請讀者自行查閱。
首先,我們來看看裝置的初始化。當我們正確編譯完我們的程式後,我們就需要把產生的目標檔案載入到核心中去,我們會先ifconfig eth0 down和rmmod 8139too來卸載正在使用的網卡驅動,然後insmod 8139too.o把我們的驅動載入進去(其中8139too.o是我們編譯產生的目標檔案)。就像C程式有主函數main()一樣,模組也有第一個執行的函數,即module_init(rtl8139_init_module);在我們的程式中,rtl8139_init_module()在 insmod之後首先執行,它的代碼如下:
static int __init rtl8139_init_module (void)
{
return pci_module_init (&rtl8139_pci_driver);
}
它直接調用了pci_module_init(),這個函數代碼在Linux/drivers/net/eepro100.c中,並且把 rtl8139_pci_driver(這個結構是在我們的驅動代碼裡定義的,它是驅動程式和PCI裝置聯絡的紐帶)的地址作為參數傳給了它。 rtl8139_pci_driver定義如下:
static struct pci_driver rtl8139_pci_driver = {
name: MODNAME,
id_table: rtl8139_pci_tbl,
probe: rtl8139_init_one,
remove: rtl8139_remove_one,
};
pci_module_init()在驅動代碼裡沒有定義,你一定想到了,它是Linux核心提供給模組是一個標準介面,那麼這個介面都幹了些什麼,筆者跟蹤了這個函數。裡面調用了pci_register_driver(),這個函數代碼在Linux/drivers/pci/pci.c 中,pci_register_driver做了三件事情。
①是把帶過來的參數rtl8139_pci_driver在核心中進行了註冊,核心中有一個PCI裝置的大的鏈表,這裡負責把這個PCI驅動掛到裡面去。
②是查看匯流排上所有PCI裝置(網卡裝置屬於PCI裝置的一種)的配置空間如果發現標識資訊與rtl8139_pci_driver中的id_table相同即rtl8139_pci_tbl,而它的定義如下:
static struct pci_device_id rtl8139_pci_tbl[] __devinitdata = {
{0x10ec, 0x8129, PCI_ANY_ID, PCI_ANY_ID, 0, 0, 1},
{PCI_ANY_ID, 0x8139, 0x10ec, 0x8139, 0, 0,0 },
{0,}
};
,那麼就說明這個驅動程式就是用來驅動這個裝置的,於是調用rtl8139_pci_driver中的probe函數即 rtl8139_init_one,這個函數是在我們的驅動程式中定義了的,它是用來初始化整個裝置和做一些準備工作。這裡需要注意一下 pci_device_id是核心定義的用來辨別不同PCI裝置的一個結構,例如在我們這裡0x10ec代表的是Realtek公司,我們掃描PCI裝置配置空間如果發現有Realtek公司製造的裝置時,兩者就對上了。當然對上了公司號後還得看其他的裝置號什麼的,都對上了才說明這個驅動是可以為這個裝置服務的。
③是把這個rtl8139_pci_driver結構掛在這個裝置的資料結構(pci_dev)上,表示這個裝置從此就有了自己的驅動了。而驅動也找到了它服務的對象了。
PCI是一個匯流排標準,PCI匯流排上的裝置就是PCI裝置,這些裝置有很多類型,當然也包括網卡裝置,每一個PCI裝置在核心中抽象為一個資料結構pci_dev,它描述了一個PCI裝置的所有的特性,具體請查詢相關文檔,本文限於篇幅無法詳細描述。但是有幾個地方和驅動程式的關係特別大,必須予以說明。PCI裝置都遵守PCI標準,這個部分所有的PCI裝置都是一樣的,每個PCI裝置都有一段寄存器儲存著配置空間,這一部分格式是一樣的,比如第一個寄存器總是生產商號碼,如Realtek就是10ec,而Intel則是另一個數字,這些都是商家像標準組織申請的,是肯定不同的。我就可以通過配置空間來辨別其生產商,裝置號,不論你什麼平台,x86也好,ppc也好,他們都是同一的標準格式。當然光有這些PCI配置空間的統一格式還是不夠的,比如說人類,都有鼻子和眼睛,但並不是所有人的鼻子和眼睛都長的一樣的。網卡裝置是PCI裝置必須遵守規則,在裝置裡整合了PCI配置空間,但它是一個網卡就必須同時整合能控制網卡工作的寄存器。而寄存器的訪問就成了一個問題。在Linux裡面我們是把這些寄存器映射到主存虛擬空間上的,換句話說我們的CPU 訪存指令就可以訪問到這些處於外設中的控制寄存器。總結一下PCI裝置主要包括兩類空間,一個是配置空間,它是作業系統或BIOS控制外設的統一格式的空間,CPU指令不能訪問,訪問這個空間要藉助BIOS功能,事實上Linux的訪問配置空間的函數是通過CPU指令驅使BIOS來完成讀寫訪問的。而另一類是普通的控制寄存器空間,這一部分映射完後CPU可以訪問來控制裝置工作。
現在我們回到上面pci_register_driver的第二步,如果找到相關裝置和我們的pci_device_id結構數組對上號了,說明我們找到服務物件了,則調用rtl8139_init_one,它主要做了七件事:
① 建立net_device結構,讓它在核心中代表這個網路裝置。但是讀者可能會問,pci_dev也是代表著這個裝置,那麼兩者有什麼區別呢,正如我們上面討論的,網卡裝置既要遵循PCI規範,也要擔負起其作為網卡裝置的職責,於是就分了兩塊,pci_dev用來負責網卡的PCI規範,而這裡要說的 net_device則是負責網卡的網路裝置這個職責。
dev = init_etherdev (NULL, sizeof (*tp));
if (dev == NULL) {
printk ("unable to alloc new ethernet\n");
return -ENOMEM;
}
tp = dev->priv;
init_etherdev函數在Linux/drivers/net/net_init.c中,在這個函數中分配了net_device的記憶體並進行了初步的初始化。這裡值得注意的是net_device中的一個成員priv,它代表著不同網卡的私人資料,比如Intel的網卡和Realtek 的網卡在核心中都是以net_device來代表。但是他們是有區別的,比如Intel和Realtek實現同一功能的方法不一樣,這些都是靠著priv 來體現。所以這裡把拿出來同net_device相提並論。分配記憶體時,net_device中除了priv以外的成員都是固定的,而priv的大小是可以任意的,所以分配時要把priv的大小傳過去。
②開啟這個裝置(其實是開啟了裝置的寄存器映射到記憶體的功能)
rc = pci_enable_device (pdev);
if (rc)
goto err_out;
pci_enable_device也是一個核心開發出來的介面,代碼在drivers/pci/pci.c中,筆者跟蹤發現這個函數主要就是把 PCI配置空間的Command域的0位和1位置成了1,從而達到了開啟裝置的目的,因為rtl8139的官方datasheet中,說明了這兩位的作用就是開啟記憶體映射和I/O映射,如果不開的話,那我們以上討論的把控制寄存器空間映射到記憶體空間的這一功能就被屏蔽了,這對我們是非常不利的,除此之外,pci_enable_device還做了些中斷開啟工作。
③獲得各項資源
mmio_start = pci_resource_start (pdev, 1);
mmio_end = pci_resource_end (pdev, 1);
mmio_flags = pci_resource_flags (pdev, 1);
mmio_len = pci_resource_len (pdev, 1);
讀者也許疑問我們的寄存器被映射到記憶體中的什麼地方是什麼時候有誰決定的呢。是這樣的,在硬體加電初始化時,BIOS韌體同一檢查了所有的PCI 裝置,並統一為他們分配了一個和其他互不衝突的地址,讓他們的驅動程式可以向這些地址映射他們的寄存器,這些地址被BIOS寫進了各個裝置的配置空間,因為這個活動是一個PCI的標準的活動,所以自然寫到各個裝置的配置空間裡而不是他們風格各異的控制寄存器空間裡。當然只有BIOS可以訪問配置空間。當作業系統初始化時,他為每個PCI裝置分配了pci_dev結構,並且把BIOS獲得的並寫到了配置空間中的地址讀出來寫到了pci_dev中的 resource欄位中。這樣以後我們在讀這些地址就不需要在訪問配置空間了,直接跟pci_dev要就可以了,我們這裡的四個函數就是直接從 pci_dev讀出了相關資料,代碼在include/linux/pci.h中。定義如下:
#define pci_resource_start(dev,bar) ((dev)->resource[(bar)].start)
#define pci_resource_end(dev,bar) ((dev)->resource[(bar)].end)
這裡需要說明一下,每個PCI裝置有0-5一共6個地址空間,我們通常只使用前兩個,這裡我們把參數1傳給了bar就是使用記憶體映射的地址空間。
④把得到的地址進行映射
ioaddr = ioremap (mmio_start, mmio_len);
if (ioaddr == NULL) {
printk ("cannot remap MMIO, aborting\n");
rc = -EIO;
goto err_out_free_res;
}
ioremap是核心提供的用來映射外設寄存器到主存的函數,我們要映射的地址已經從pci_dev中讀了出來(上一步),這樣就水到渠成的成功映射了而不會和其他地址有衝突。映射完了有什麼效果呢,我舉個例子,比如某個網卡有100個寄存器,他們都是連在一塊的,位置是固定的,加入每個寄存器占 4個位元組,那麼一共400個位元組的空間被映射到記憶體成功後,ioaddr就是這段地址的開頭(注意ioaddr是虛擬位址,而mmio_start是物理地址,它是BIOS得到的,肯定是物理地址,而保護模式下CPU不認物理地址,只認虛擬位址),ioaddr+0就是第一個寄存器的地址,ioaddr+4就是第二個寄存器地址(每個寄存器佔4個位元組),以此類推,我們就能夠在記憶體中訪問到所有的寄存器進而操控他們了。
⑤重啟網卡裝置
重啟網卡裝置是初始化網卡裝置的一個重要部分,它的原理就是向寄存器中寫入命令就可以了(注意這裡寫寄存器,而不是配置空間,因為跟PCI沒有什麼關係),代碼如下:
writeb ((readb(ioaddr+ChipCmd) & ChipCmdClear) | CmdReset,ioaddr+ChipCmd);
是我們看到第二參數ioaddr+ChipCmd,ChipCmd是一個位移,使地址剛好對應的就是ChipCmd哪個寄存器,讀者可以查閱官方 datasheet得到這個位移量,我們在程式中定義的這個值為:ChipCmd = 0x37;與datasheet是吻合的。我們把這個命令寄存器中相應位(RESET)置1就可以完成操作。
⑥獲得MAC地址,並把它儲存到net_device中。
for(i = 0; i < 6; i++) { /* Hardware Address */
dev->dev_addr[i] = readb(ioaddr+i);
dev->broadcast[i] = 0xff;
}
我們可以看到讀的地址是ioaddr+0到ioaddr+5,讀者查看官方datasheet會發現寄存器地址空間的開頭6個位元組正好存的是這個網卡裝置的MAC地址,MAC地址是網路中標識網卡的物理地址,這個地址在今後的收發資料包時會用的上。
⑦向net_device中登記一些主要的函數
dev->open = rtl8139_open;
dev->hard_start_xmit = rtl8139_start_xmit;
dev->stop = rtl8139_close;
由於dev(net_device)代表著裝置,把這些函數註冊完後,rtl8139_open就是用於開啟這個裝置,rtl8139_start_xmit就是當應用程式要通過這個裝置往外面發資料時被調用,具體的其實這個函數是在網路通訊協定層中調用的,這就涉及到 Linux網路通訊協定棧的內容,不再我們討論之列,我們只是負責實現它。rtl8139_close用來關掉這個裝置。
好了,到此我們把rtl8139_init_one函數介紹完了,初始化個裝置完了之後呢,我們通過ifconfig eth0 up命令來把我們的裝置啟用。這個命令直接導致了我們剛剛註冊的rtl8139_open的調用。這個函數啟用了裝置。這個函數主要做了三件事。
①註冊這個裝置的中斷處理函數。當網卡發送資料完成或者接收到資料時,是用中斷的形式來告知的,比如有資料從網線傳來,中斷也通知了我們,那麼必須要有一個處理這個中斷的函數來完成資料的接收。關於Linux的中斷機制不是我們詳細講解的範疇,有興趣的可以參考《Linux核心原始碼情景分析》,但是有個非常重要的資源我們必須注意,那就是中斷號的分配,和記憶體位址映射一樣,中斷號也是BIOS在初始化階段分配並寫入裝置的配置空間的,然後 Linux在建立pci_dev時從配置空間讀出這個中斷號然後寫入pci_dev的irq成員中,所以我們註冊中斷程式需要中斷號就是直接從 pci_dev裡取就可以了。
retval = request_irq (dev->irq, rtl8139_interrupt, SA_SHIRQ, dev->name, dev);
if (retval) {
return retval;
}
我們註冊的中斷處理函數是rtl8139_interrupt,也就是說當網卡發生中斷(如資料到達)時,中斷控制器8259A把中斷號發給 CPU,CPU根據這個中斷號找到處理常式,這裡就是rtl8139_interrupt,然後執行。rtl8139_interrupt也是在我們的程式中定義好了的,這是驅動程式的一個重要的義務,也是一個基本的功能。request_irq的代碼在arch/i386/kernel/irq.c中。
②分配發送和接收的緩衝空間
根據官方文檔,發送一個資料包的過程是這樣的:先從應用程式中把資料包拷貝到一段連續的記憶體中(這段記憶體就是我們這裡要分配的緩衝),然後把這段記憶體的地址寫進網卡的資料發送地址寄存器(TSAD)中,這個寄存器的位移量是TxAddr0 = 0x20。在把這個資料包的長度寫進另一個寄存器(TSD)中,它的位移量是TxStatus0 = 0x10。然後就把這段記憶體的資料發送到網卡內部的發送緩衝中(FIFO),最後由這個發送緩衝區把資料發送到網線上。
好了現在建立這麼一個發送和接收緩衝記憶體的目的已經很顯然了。
tp->tx_bufs = pci_alloc_consistent(tp->pci_dev, TX_BUF_TOT_LEN,
&tp->tx_bufs_dma);
tp->rx_ring = pci_alloc_consistent(tp->pci_dev, RX_BUF_TOT_LEN,
&tp->rx_ring_dma);
tp是net_device的priv的指標,tx_bufs是發送緩衝記憶體的首地址,rx_ring是接收緩衝記憶體的首地址,他們都是虛擬位址,而最後一個參數tx_bufs_dma和rx_ring_dma均是這一段記憶體的物理地址。為什麼同一個事物,既用虛擬位址來表示它還要用物理地址呢,是這樣的,CPU執行程式用到這個地址時,用虛擬位址,而網卡裝置向這些記憶體中存取資料時用的是物理地址(因為網卡相對CPU屬於頭腦比較簡單型的)。pci_alloc_consistent的代碼在Linux/arch/i386/kernel/pci-dma.c中。
③發送和接收緩衝區初始化和網卡開始工作的操作
RTL8139有4個發送描述符(包括4個發送緩衝區的基地址寄存器(TSAD0-TSAD3)和4個發送狀態寄存器(TSD0-TSD3)。也就是說我們分配的緩衝區要分成四個等分並把這四個空間的地址都寫到相關寄存器裡去,下面這段程式碼完成了這個操作。
for (i = 0; i < NUM_TX_DESC; i++)
((struct rtl8139_private*)dev->priv)->tx_buf[i] =
&((struct rtl8139_private*)dev->priv)->tx_bufs[i * TX_BUF_SIZE];
上面這段代碼負責把發送緩衝區虛擬空間進行了分割。
for (i = 0; i < NUM_TX_DESC; i++)
{
writel(tp->tx_bufs_dma+(tp->tx_buf[i]tp->tx_bufs),ioaddr+TxAddr0+(i*4));
readl(ioaddr+TxAddr0+(i * 4));
}
上面這段代碼負責把發送緩衝區物理空間進行了分割,並把它寫到了相關寄存器中,這樣在網卡開始工作後就能夠迅速定位和找到這些記憶體並存取他們的資料。
writel(tp->rx_ring_dma,ioaddr+RxBuf);
上面這行代碼是把接收緩衝區的物理地址寫到了相關寄存器中,這樣網卡接收到資料後就能準確的把資料從網卡中搬運到這些記憶體空間中,等待CPU來領走他們。
writeb((readb(ioaddr+ChipCmd) & ChipCmdClear) |
CmdRxEnb | CmdTxEnb,ioaddr+ChipCmd);
重新RESET裝置後,我們要啟用裝置的發送和接收的功能,上面這行代碼就是向相關寄存器中寫入相應值,啟用了裝置的這些功能。
writel ((TX_DMA_BURST << TxDMAShift),ioaddr+TxConfig);
上面這行代碼是向網卡的TxConfig(位移是0x44)寄存器中寫入TX_DMA_BURST << TxDMAShift這個值,翻譯過來就是6<<8,就是把第8到第10這三位置成110,查閱管法文檔發現6就是110代表著一次DMA的資料量為1024位元組。
另外在這個階段設定了接收資料的模式,和開啟中斷等等,限於篇幅由讀者自行研究。
下面進入資料收發階段:
當一個網路應用程式要向網路發送資料時,它要利用Linux的網路通訊協定棧來解決一系列問題,找到網卡裝置的代表net_device,由這個結構來找到並控制這個網卡裝置來完成資料包的發送,具體是調用net_device的hard_start_xmit成員函數,這是一個函數指標,在我們的驅動程式裡它指向的是rtl8139_start_xmit,正是由它來完成我們的發送工作的,下面我們就來剖析這個函數。它一共做了四件事。
①檢查這個要發送的資料包的長度,如果它達不到乙太網路幀的長度,必須採取措施進行填充。
if( skb->len < ETH_ZLEN ){//if data_len < 60
if( (skb->data + ETH_ZLEN) <= skb->end ){
memset( skb->data + skb->len, 0x20, (ETH_ZLEN - skb->len) );
skb->len = (skb->len >= ETH_ZLEN) ? skb->len : ETH_ZLEN;}
else{
printk("%s:(skb->data+ETH_ZLEN) > skb->end\n",__FUNCTION__);
}
}
skb->data和skb->end就決定了這個包的內容,如果這個包本身總共的長度(skb->end- skb->data)都達不到要求,那麼想填也沒地方填,就出錯返回了,否則的話就填上。
②把包的資料拷貝到我們已經建立好的發送緩衝中。
memcpy (tp->tx_buf[entry], skb->data, skb->len);
其中skb->data就是資料包資料的地址,而tp->tx_buf[entry]就是我們的發送緩衝地址,這樣就完成了拷貝,忘記了這些內容的回頭看看前面的介紹。
③光有了地址和資料還不行,我們要讓網卡知道這個包的長度,才能保證資料不多不少精確的從緩衝中截取出來搬運到網卡中去,這是靠寫發送狀態寄存器(TSD)來完成的。
writel(tp->tx_flag | (skb->len >= ETH_ZLEN ? skb->len : ETH_ZLEN),ioaddr+TxStatus0+(entry * 4));
我們把這個包的長度和一些控制資訊一起寫進了狀態寄存器,使網卡的工作有了依據。
④判斷髮送緩衝是否已經滿了,如果滿了在發就覆蓋資料了,要停發。
if ((tp->cur_tx - NUM_TX_DESC) == tp->dirty_tx)
netif_stop_queue (dev);
談完了發送,我們開始談接收,當有資料從網線上過來時,網卡產生一個中斷,調用的中斷服務程式是rtl8139_interrupt,它主要做了三件事。
①從網卡的中斷狀態寄存器中讀出狀態值進行分析,status = readw(ioaddr+IntrStatus);
if ((status &(PCIErr | PCSTimeout | RxUnderrun | RxOverflow |
RxFIFOOver | TxErr | TxOK | RxErr | RxOK)) == 0)
goto out;
上面代碼說明如果上面這9種情況均沒有的表示沒什麼好處理的了,退出。
② if (status & (RxOK | RxUnderrun | RxOverflow | RxFIFOOver))/* Rx interrupt */
rtl8139_rx_interrupt (dev, tp, ioaddr);
如果是以上4種情況,屬於接收訊號,調用rtl8139_rx_interrupt進行接收處理。
③ if (status & (TxOK | TxErr)) {
spin_lock (&tp->lock);
rtl8139_tx_interrupt (dev, tp, ioaddr);
spin_unlock (&tp->lock);
}
如果是傳輸完成的訊號,就調用rtl8139_tx_interrupt進行發送善後處理。
下面我們先來看看接收中斷處理函數rtl8139_rx_interrupt,在這個函數中主要做了下面四件事
①這個函數是一個大迴圈,迴圈條件是只要接收緩衝不為空白就還可以繼續讀取資料,迴圈不會停止,讀空了之後就跳出。
int ring_offset = cur_rx % RX_BUF_LEN;
rx_status = le32_to_cpu (*(u32 *) (rx_ring + ring_offset));
rx_size = rx_status >> 16;
上面三行代碼是計算出要接收的包的長度。
②根據這個長度來分配包的資料結構
skb = dev_alloc_skb (pkt_size + 2);
③如果分配成功就把資料從接收緩衝中拷貝到這個包中
eth_copy_and_sum (skb, &rx_ring[ring_offset + 4], pkt_size, 0);
這個函數在include/linux/etherdevice.h中,實質還是調用了memcpy()。
static inline void eth_copy_and_sum(struct sk_buff*dest, unsigned char *src, int len, int base)
{
memcpy(dest->data, src, len);
}
現在我們已經熟知,&rx_ring[ring_offset + 4]就是接收緩衝,也是源地址,而skb->data就是包的資料地址,也是目的地址,一目瞭然。
④把這個包送到Linux協議棧去進行下一步處理
skb->protocol = eth_type_trans (skb, dev);
netif_rx (skb);
在netif_rx()函數執行完後,這個包的資料就脫離了網卡驅動範疇,而進入了Linux網路通訊協定棧裡面,把這些資料包的乙太網路幀頭,IP 頭,TCP頭都脫下來,最後把資料送給了應用程式,不過協議棧不再本文討論範圍內。netif_rx函數在net/core/dev.c,中。
而rtl8139_remove_one則基本是rtl8139_init_one的逆過程。
到此,本文已經將Linux驅動程式的架構勾勒了出來,如果有什麼疑問聯絡我。QQ:591970467.

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.