RTL8139网卡驱动程序分析独孤求真@163.comOSPlay原创文章转载请注明出处。c⃝2007-7-23Contents1预备知识22驱动的初始化33中断处理164数据接收处理19Abstract对多数驱动程序开发的学习者来说,总是感觉很难⼊门,不能从整体上把握驱动程序是如何驱动硬件设备⼯作的。本文以Linux内核中8139网卡驱动为例,对驱动程序的⼯作过程进行详细的分析,为初学者拨开迷雾,走出雾里看花的迷茫。本文虽然以Linux驱动为例,但是技术总是相通的,为了给Windows驱动初学者同样的启发,我有意的借用了许多Windows驱动中的名词,同时顺便略述了Windows驱动中的⼀些容易让初学者感到迷惑的概念。本⼈水平有限,纰漏之处在所难免,希望读者海涵,并不吝赐教。1欢迎访问OSPlay是⼀个PCI网卡,老式的设备地址是固定的,对设备的扩充通常通过跳线等方式来更改地址以避免地址冲突,例如通过跳线来设置IED主盘,从盘。PCI总线设备可以通过软件编程灵活的设置各个设备的地址。在操作系统启动的时候,系统根据PCI总线协议规范对主板上的PCI进行扫描,同时为发现的设备配置相关资源,包括中断请求号,地址空间等。每⼀个PCI设备上有⼀个配置空间,配置空间中包含了设备的基本信息,例如设备类别ID,⼚商ID,设备板载存储空间等信息,操作系统在扫描所有的PCI设备后,可以根据这些信息统⼀分配地址资源以避免地址冲突。系统通过在PCI设备中的基地址寄存器中写⼊⼀个分配到的基地址,之后CPU在指令执行的时候给出⼀个地址,这个地址首先送到Host-PCI桥,就是我们通常所说的北桥,北桥判断出这个地址是落在内存或是PCI等设备的地址空间上1,如果落在PCI空间地址中,则北桥通过PCI总线仲裁申请,把地址送到PCI总线上,总线上的每⼀个PCI设备会根据自⼰的存储空间⼤⼩以及基地址寄存器中的值来比较,如果被寻址的地址落在自⼰的地址空间范围内,则该设备会作为PCI从设备响应完成数据传输。这就是说,系统上的设备都有自⼰的地址空间,以总线带宽为32位的系统为例,可以容纳的地址空间为4G,CPU在这4G的地址空间⼀部分划分到内存空间,还有以部分很可能划分给PCI等外部设备,这通常取决于硬件系统设计者。PCI设备灵活的配置方式也不可避免的带来了复杂性。PCI协议对设备的枚举,检测,配置的过程是复杂的,通常操作系统提供了PCI总线协议驱动程序,并在启动的时候完成了这⼀复杂的过程,这样⼤⼤减⼩了PCI设备驱动程序开发者的⼯作量。这就好像平时⼤家做网络程序开发的时候都没必要自⼰实现TCP/IP协议⼀样。用Windows的术语来说,对PCI设备的枚举由总线驱动程序完成,而具体的对PCI设备的控制是功能驱动程序的⼯作。本文要描述的是Rtl8139网卡功能驱动程序。对PCI协议有⼀定了解是必要的。关于PCI总线驱动程序的知识可以1新的集成内存控制器的CPU自⼰能判断对内存的寻址。2欢迎访问OSPlay阅读《Linux内核情景分析》。2驱动的初始化驱动程序的⼊⼝函数rtl8139_init_module调用了pci_register_driver(&rtl8139_pci_driver);其中参数rtl8139_pci_driver结够如下:1//该结构相当于Windows中的功能驱动程序对象2staticstructpcidriverrtl8139pcidriver={3.name=DRVNAME,4.idtable=rtl8139pcitbl,5.probe=rtl8139initone,6.remove=devexitp(rtl8139removeone),7#ifdefCONFIGPM8.suspend=rtl8139suspend,9.resume=rtl8139resume,10#endif/∗CONFIGPM∗/11};1213//最重要的参数。rtl8139pcitbl14staticstructpcideviceidrtl8139pcitbl[]={15{0x10ec,0x8139,PCIANYID,PCIANYID,0,0,RTL8139},16{0x10ec,0x8138,PCIANYID,PCIANYID,0,0,RTL8139},17{0x1113,0x1211,PCIANYID,PCIANYID,0,0,RTL8139},18......19};3欢迎访问OSPlay,DeviceID等组成,表示该设备驱动程序可以控制的设备,每⼀个PCI设备在配置空间中固化了自⼰的基本信息,前面说过内核启动的时候PCI总线驱动程序会扫描PCI总线上的设备,并且把这些信息收集起来,并且每⼀个设备的信息由⼀个专门的结构保存起来,保存在⼀个pci_dev结构中。pci_register_driver会根据rtl8139_pci_tbl中的信息和内核中扫描到的对比,如果有匹配的话,就把功能驱动程序和目标设备对应起来了。在Windows系统中维护设备信息的那个结构被称为物理设备对象,该对象由总线驱动程序创建管理。之后pci_register_driver进行必要的初始化后,调用参数指定的rtl8139_init_one。1staticintdevinitrtl8139initone(structpcidev∗pdev,conststructpcideviceid∗ent)2{3structnetdevice∗dev=NULL;4structrtl8139private∗tp;5inti,addrlen,option;6voidiomem∗ioaddr;7staticintboardidx=−1;8u8pcirev;910......11i=rtl8139initboard(pdev,&dev);12......13}rtl8139_init_one的参数就是PCI总线驱动程序在设备枚举过程中创建的。首先调用rtl8139_init_board。1staticintdevinitrtl8139initboard(structpcidev∗pdev,structnetdevice∗∗devout)4欢迎访问OSPlay{3voidiomem∗ioaddr;4structnetdevice∗dev;5structrtl8139private∗tp;6u8tmp8;7intrc,disabledevonerr=0;8unsignedinti;9unsignedlongpiostart,pioend,pioflags,piolen;10unsignedlongmmiostart,mmioend,mmioflags,mmiolen;11u32version;1213assert(pdev!=NULL);1415∗devout=NULL;1617/∗devandprivzeroedinallocetherdev∗/18/∗每⼀个网络设备驱动程序为了设备管理方便,统⼀接⼝等目的,需要创建自⼰的⼀个设备对象来维护设备信息,在Windows系统中,该对象称为功能设备对象。∗/19/∗每⼀个设备对象是标准的结构,但是不同的驱动程序可能都要维护不同的私有信息,所以在分配netdev结构的同时可以多分配出rtl8139private结构来。比如应用程序可能会经常查询网卡地址,虽然驱动程序可以通过访问网卡上的存储空间来获取网卡地址,但是驱动程序可不希望每次都通过慢速的IO访问来获取这些信息,通常驱动程序会为这些信息维护内存中的数据结构中,这些信息都可以放在tp中。∗/20dev=allocetherdev(sizeof(∗tp));21if(dev==NULL){22deverr(&pdev−dev,Unabletoallocnewnetdevice\n);5欢迎访问OSPlay−ENOMEM;24}25SETMODULEOWNER(dev);26SETNETDEVDEV(dev,&pdev−dev);2728tp=netdevpriv(dev);29tp−pcidev=pdev;3031/∗enabledevice(incl.PCIPMwakeupandhotplugsetup)∗/32/∗启用设备的memory/Io译码,如果设备处于休眠状态,则唤醒设备。在启用设备之前,虽然设备的基址寄存器中设置了值,但是当总线有主设备对该地址进行寻址的时候,设备是不会响应的。具体启用操作由总线驱动程序封装,这里通过调用pci总线驱动程序提供的函数来完成该任务,在windows中某些写操作由功能驱动向下层的总线驱动发送IRP完成同样的任务。∗/33rc=pcienabledevice(pdev);34if(rc)35gotoerrout;3637/∗⼤多数设备上有自⼰的板载存储空间,其中包括memory空间和IO空间,在X86这样的IO与Memory分立编址的系统上,他们的区别就在于独立的IO访问指令,独立的地址译码,在多数统⼀编址的系统上memeor空间和IO空间没有本质区别。CPU给出的指令中的地址落在哪里,设备在电路级别会访问到对应地址的板载存储空间。PCI总线驱动程序已经为设备分配地址空间,并配置了基址寄存器,通常BIOS已经为这些设备配置了互不冲突的地址,系统的PCI总线驱动程序可以直接扫描PCI设备,并从基址寄存器中读出各个设备的基地址,也可以推倒从新统⼀分配。这里功能驱动程序需要知道自⼰如何访问到目标设备,所以从总线物理设备对象中读取基址信息。∗/38piostart=pciresourcestart(pdev,0);39pioend=pciresourceend(pdev,0);40pioflags=pciresourceflags(pdev,0);41piolen=pciresourcelen(pdev,0);6欢迎访问OSPlay=pciresourcestart(pdev,1);44mmioend=pciresourceend(pdev,1);45mmioflags=pciresourceflags(pdev,1);46mmiolen=pciresourcelen(pdev,1);4748/∗49setthisimmediately,weneedtoknowbefore50wetalktothechipdirectly51∗/52DPRINTK(PIOregionsize==0x%02X\n,piolen);53DPRINTK(MMIOregionsize==0x%02lX\n,mmiolen);5455/∗PCI设备板载存储空间可以通过IO或者Memory方式来访问,PCI设备上分别有IO和Memory基地址寄存器,在X86上的有独立的IO指令,对于某些统⼀编址的体系结构则只有通过Memory方式来访问,CPU执行IO/Memory指令时,其地址送到总线上,PCI设备会根据IO/Memory基址寄存器判断出目标设备是不是自⼰。无论是IO方式还是Memory方式,访问到通常是板载存储空间上的同⼀个地方。由被寻址的从设备内部处理。例如:假设8139网卡IO/Memory基址寄存器的值分别是X/Y,则使用IO指令(IN/OUT)访问X,以及使用Memory指令(MOV)访问Y,结果是⼀样的。这样保证了设备在统⼀编址和独立编址的体系结构下的兼容性,为什么通过MOV访问的地址没有访问到内存上去呢?通常CPU给出⼀条访存指令,地址被发到北桥,北桥会根据地址空间的划分情况判别出该地址是落在内存空间上还是其它总线上的设备空间上。如