RTL8139网卡驱动程序分析
独孤求真
www.osplay.org
Email: Addylee2004@163.com
OSPlay 原创文章 转载请注明出处。
c⃝ 2007-7-23
根据sinister的建议,在接收部分加⼊了对NAPI和
非NAPI方式的分析。 在此对sinister⼤虾表示感谢。
1
欢迎访问 OSPlay www.osplay.org
3
4
18
20
. 20
. 24
26
31
CONTENTS
Contents
1 预备知识
2 驱动的初始化
3 中断处理
4 软中断请求
NAPI方式 .
4.1
.
4.2 非NAPI方式 .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
5 网卡接收操作
6 网卡发送操作
Abstract
对多数驱动程序开发的学习者来说,总是感觉很难⼊门,不能从
整体上把握驱动程序是如何驱动硬件设备⼯作的。本文以Linux内
核中8139网卡驱动为例,对驱动程序的⼯作过程进行详细的分析,
为初学者拨开迷雾,走出雾里看花的迷茫。本文虽然以Linux驱动
为例,但是技术总是相通的,为了给Windows驱动初学者同样的启
发,我有意的借用了许多Windows驱动中的名词,同时顺便略述了
Windows驱动中的⼀些容易让初学者感到迷惑的概念。
本⼈水平有限,纰漏之处在所难免,希望读者海涵,并不吝赐
教。
2
欢迎访问 OSPlay www.osplay.org
1 预备知识
Rtl8139是⼀个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自⼰能判断对内存的寻址。
3
欢迎访问 OSPlay www.osplay.org
阅读《Linux内核情景分析》。
2 驱动的初始化
驱动程序的⼊⼝函数rtl8139_init_module调用了pci_register_driver
(&rtl8139_pci_driver); 其中参数rtl8139_pci_driver结够如下:
d e v e x i t p ( r t l 8 1 3 9 r e m o v e o n e ) ,
. name
. id table
. probe
. remove
1 // 该结构相当于 Windows 中的功能驱动程序对象
2 static struct p c i d r i v e r r t l 8 1 3 9 p c i d r i v e r = {
= DRV NAME ,
3
= r t l 8 1 3 9 p c i t b l ,
4
= r t l 8 1 3 9 i n i t o n e ,
5
6
=
7 #ifdef CONFIG PM
8
9
10 #endif /∗ CONFIG PM ∗/
11 } ;
12
13
14 static struct p c i d e v i c e i d r t l 8 1 3 9 p c i t b l [ ] = {
{0 x10ec , 0 x8139 , PCI ANY ID , PCI ANY ID , 0 , 0 ,
15
// 最重要的参数。 r t l 8 1 3 9 p c i t b l
. suspend
. resume
= rtl8139 suspend ,
= rtl8139 resume ,
{0 x10ec , 0 x8138 , PCI ANY ID , PCI ANY ID , 0 , 0 ,
{0 x 1 1 1 3 , 0 x 1 2 1 1 , PCI ANY ID , PCI ANY ID , 0 , 0 ,
16
17
RTL8139 } ,
RTL8139 } ,
RTL8139 } ,
. . . . . .
18
19 } ;
4
欢迎访问 OSPlay www.osplay.org
rtl8139_pci_tbl中是由VerdonID,DeviceID等组成,表示该设备驱动
程序可以控制的设备,每⼀个PCI设备在配置空间中固化了自⼰的基本
信息,前面说过内核启动的时候PCI总线驱动程序会扫描PCI总线上的
设备,并且把这些信息收集起来,并且每⼀个设备的信息由⼀个专门
的结构保存起来,保存在⼀个pci_dev结构中。pci_register_driver会
根据rtl8139_pci_tbl中的信息和内核中扫描到的对比,如果有匹配的
话,就把功能驱动程序和目标设备对应起来了。在Windows系统中维护
设备信息的那个结构被称为物理设备对象,该对象由总线驱动程序创
建管理。之后pci_register_driver进行必要的初始化后,调用参数指定
的rtl8139_init_one。
∗ pdev , const struct p c i d e v i c e i d ∗ ent )
1 static int
d e v i n i t r t l 8 1 3 9 i n i t o n e ( struct p c i d e v
2 {
3
4
5
6
7
8
9
10
11
12
13 }
struct net device ∗ dev = NULL ;
struct r t l 8 1 3 9 p r i v a t e ∗tp ;
int i , addr len , option ;
void
static int board idx = −1;
u8 p c i r e v ;
iomem ∗ ioaddr ;
. . . . . .
i = r t l 8 1 3 9 i n i t b o a r d ( pdev , &dev ) ;
. . . . . .
rtl8139_init_one的参数就是PCI总线驱动程序在设备枚举过程中创
建的。首先调用rtl8139_init_board。
1 static int
d e v i n i t r t l 8 1 3 9 i n i t b o a r d ( struct
p c i d e v ∗ pdev , struct net device ∗∗ dev out )
5
2 {
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
欢迎访问 OSPlay www.osplay.org
iomem ∗ ioaddr ;
void
struct net device ∗ dev ;
struct r t l 8 1 3 9 p r i v a t e ∗tp ;
u8 tmp8 ;
int rc , d i s a b l e d e v o n e r r = 0 ;
unsigned int i ;
unsigned long pio start , pio end , pio flags , pio len
;
unsigned long mmio start , mmio end , mmio flags ,
mmio len ;
u32 version ;
assert ( pdev ! = NULL ) ;
∗ dev out = NULL ;
/∗ dev and priv zeroed in alloc etherdev ∗/
/∗ 每⼀个网络设备驱动程序为了设备管理方便,统⼀接⼝等目的,
需要创建自⼰的⼀个设备对象来维护设备信息,在 Windows 系
统中,该对象称为功能设备对象。 ∗/
/∗ 每⼀个设备对象是标准的结构,但是不同的驱动程序可能都要维
护不同的私有信息,所以在分配 net dev 结构的同时可以多分
配出 r t l 8 1 3 9 p r i v a t e 结构来。比如应用程序可能会经常查
询网卡地址,虽然驱动程序可以通过访问网卡上的存储空间来获
取网卡地址,但是驱动程序可不希望每次都通过慢速的 IO 访问
来获取这些信息,通常驱动程序会为这些信息维护内存中的数据
结构中,这些信息都可以放在 tp 中。∗/
dev = alloc etherdev ( sizeof (∗ tp ) ) ;
if ( dev == NULL ) {
dev err (& pdev−>dev , "Unable to alloc new net
device\n" ) ;
6
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
欢迎访问 OSPlay www.osplay.org
return −ENOMEM ;
}
SET MODULE OWNER ( dev ) ;
SET NETDEV DEV ( dev , &pdev−>dev ) ;
tp = netdev priv ( dev ) ;
tp−>p c i d e v = pdev ;
/∗ enable device ( incl . PCI PM wakeup and hotplug
setup ) ∗/
/∗ 启用设备的 memory/ Io 译码,如果设备处于休眠状态,则唤醒
设备。在启用设备之前,虽然设备的基址寄存器中设置了值,但
是当总线有主设备对该地址进行寻址的时候,设备是不会响应
的。具体启用操作由总线驱动程序封装,这里通过调用 pci 总线
驱动程序提供的函数来完成该任务,在 windows 中某些写操作
由功能驱动向下层的总线驱动发送 IRP 完成同样的任务。 ∗/
rc = p c i e n a b l e d e v i c e ( pdev ) ;
if ( rc )
goto err out ;
/∗ ⼤多数设备上有自⼰的板载存储空间,其中包括 memory 空间
和 IO 空间,在 X86 这样的 IO 与 Memory 分立编址的系统
上,他们的区别就在于独立的 IO 访问指令,独立的地址译码,
在多数统⼀编址的系统上 memeor 空间和 IO 空间没有本质区
别。 CPU 给出的指令中的地址落在哪里,设备在电路级别会访问
到对应地址的板载存储空间。 PCI 总线驱动程序已经为设备分配
地址空间,并配置了基址寄存器,通常 BIOS 已经为这些设备配
置了互不冲突的地址,系统的 PCI 总线驱动程序可以直接扫
描 PCI 设备,并从基址寄存器中读出各个设备的基地址,也可以
推倒从新统⼀分配。这里功能驱动程序需要知道自⼰如何访问到
目标设备,所以从总线物理设备对象中读取基址信息。 ∗/
pio start = p c i r e s o u r c e s t a r t ( pdev , 0 ) ;
pio end = p c i r e s o u r c e e n d ( pdev , 0 ) ;
pio flags = p c i r e s o u r c e f l a g s ( pdev , 0 ) ;
pio len = p c i r e s o u r c e l e n ( pdev , 0 ) ;
7
42
43
44
45
46
47
48
49
50
51
52
53
54
55
欢迎访问 OSPlay www.osplay.org
mmio start = p c i r e s o u r c e s t a r t ( pdev ,
mmio end = p c i r e s o u r c e e n d ( pdev ,
1 ) ;
mmio flags = p c i r e s o u r c e f l a g s ( pdev ,
mmio len = p c i r e s o u r c e l e n ( pdev ,
1 ) ;
1 ) ;
1 ) ;
/∗
set this immediately , we need to know before
we talk to the chip directly
∗/
DPRINTK ( "PIO region size == 0x%02X\n" , pio len ) ;
DPRINTK ( "MMIO region size == 0x%02lX\n" , mmio len ) ;
/∗ 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 给出⼀条访存指令,地址被
发到北桥,北桥会根据地址空间的划分情况判别出该地址是落在
内存空间上还是其它总线上的设备空间上。如果是内存的话,则
向内存控制器发起访问操作,如果是 PCI 空间的话,则同样的
向 PCI 总线总裁提出申请。 ∗/
56
57 #ifdef USE IO OPS
58
59
60
/∗ make sure PCI base addr 0 is PIO ∗/
if ( ! ( pio flags & IORESOURCE IO ) ) {
dev err (& pdev−>dev , "region #0 not a PIO resource ,
aborting\n" ) ;
8