RTL8139网卡驱动程序
独孤求真
www.osplay.org
Email: Addylee2004@163.com
OSPlay 原创文章 转载请注明出处。
c° 2007-7-23
根据sinister的建议,在接收部分加⼊了对NAPI和
非NAPI方式的分析。 在此对sinister⼤虾表示感谢。
1
CONTENTS 欢迎访问 OSPlay www.osplay.org
Contents
1 预备知识 3
2 驱动的初始化 4
3 中断处理 18
4 软中断请求 20
4.1 NAPI方式 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
4.2 非NAPI方式 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
5 网卡接收操作 26
6 网卡发送操作 31
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结够如下:
1 // 该结构相当于 Windows 中的功能驱动程序对象
2 static struct pci driver rtl8139 pci driver = f
3 . name = DRV NAME ,
4 . id table = rtl8139 pci tbl ,
5 . probe = rtl8139 init one ,
6 . remove = devexit p ( rtl8139 remove one ) ,
7 #ifdef CONFIG PM
8 . suspend = rtl8139 suspend ,
9 . resume = rtl8139 resume ,
10 #endif /¤ CONFIG PM ¤/
11 g ;
12
13 // 最重要的参数。 rtl8139 pci tbl
14 static struct pci device id rtl8139 pci tbl [ ] = f
15 f0 x10ec , 0x8139 , PCI ANY ID , PCI ANY ID , 0 , 0 ,
RTL8139 g ,
16 f0 x10ec , 0x8138 , PCI ANY ID , PCI ANY ID , 0 , 0 ,
RTL8139 g ,
17 f0 x1 1 13 , 0 x121 1 , PCI ANY ID , PCI ANY ID , 0 , 0 ,
RTL8139 g ,
18 . . . . . .
19 g ;
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。
1 static int devinit rtl8139 init one ( struct pci dev
¤pdev , const struct pci device id ¤ent )
2 f
3 struct net device ¤dev = NULL ;
4 struct rtl8139 private ¤tp ;
5 int i , addr len , option ;
6 void iomem ¤ ioaddr ;
7 static int board idx = �1;
8 u8 pci rev ;
9
10 . . . . . .
11 i = rtl8139 init board ( pdev , &dev ) ;
12 . . . . . .
13 g
rtl8139_init_one的参数就是PCI总线驱动程序在设备枚举过程中创
建的。首先调用rtl8139_init_board。
1 static int devinit rtl8139 init board ( struct
pci dev ¤pdev , struct net device ¤¤ dev out )
5
欢迎访问 OSPlay www.osplay.org
2 f
3 void iomem ¤ ioaddr ;
4 struct net device ¤dev ;
5 struct rtl8139 private ¤tp ;
6 u8 tmp8 ;
7 int rc , disable dev on err = 0 ;
8 unsigned int i ;
9 unsigned long pio start , pio end , pio flags , pio len
;
10 unsigned long mmio start , mmio end , mmio flags ,
mmio len ;
11 u32 version ;
12
13 assert ( pdev ! = NULL ) ;
14
15 ¤ dev out = NULL ;
16
17 /¤ dev and priv zeroed in alloc etherdev ¤/
18 /¤ 每⼀个网络设备驱动程序为了设备管理方便,统⼀接⼝等目的,
需要创建自⼰的⼀个设备对象来维护设备信息,在 Windows 系
统中,该对象称为功能设备对象。 ¤/
19 /¤ 每⼀个设备对象是标准的结构,但是不同的驱动程序可能都要维
护不同的私有信息,所以在分配 net dev 结构的同时可以多分
配出 rtl8139 private 结构来。比如应用程序可能会经常查
询网卡地址,虽然驱动程序可以通过访问网卡上的存储空间来获
取网卡地址,但是驱动程序可不希望每次都通过慢速的 IO 访问
来获取这些信息,通常驱动程序会为这些信息维护内存中的数据
结构中,这些信息都可以放在 tp 中。¤/
20 dev = alloc etherdev ( sizeof (¤ tp ) ) ;
21 if ( dev == NULL ) f
22 dev err (& pdev�>dev , "Unable to alloc new net
device\n" ) ;
6
欢迎访问 OSPlay www.osplay.org
23 return �ENOMEM ;
24 g
25 SET MODULE OWNER ( dev ) ;
26 SET NETDEV DEV ( dev , &pdev�>dev ) ;
27
28 tp = netdev priv ( dev ) ;
29 tp�>pci dev = pdev ;
30
31 /¤ enable device ( incl . PCI PM wakeup and hotplug
setup ) ¤/
32 /¤ 启用设备的 memory/Io 译码,如果设备处于休眠状态,则唤醒
设备。在启用设备之前,虽然设备的基址寄存器中设置了值,但
是当总线有主设备对该地址进行寻址的时候,设备是不会响应
的。具体启用操作由总线驱动程序封装,这里通过调用 pci 总线
驱动程序提供的函数来完成该任务,在 windows 中某些写操作
由功能驱动向下层的总线驱动发送 IRP 完成同样的任务。 ¤/
33 rc = pci enable device ( pdev ) ;
34 if ( rc )
35 goto err out ;
36
37 /¤ ⼤多数设备上有自⼰的板载存储空间,其中包括 memory 空间
和 IO 空间,在 X86 这样的 IO 与 Memory 分立编址的系统
上,他们的区别就在于独立的 IO 访问指令,独立的地址译码,
在多数统⼀编址的系统上 memeor 空间和 IO 空间没有本质区
别。 CPU 给出的指令中的地址落在哪里,设备在电路级别会访问
到对应地址的板载存储空间。 PCI 总线驱动程序已经为设备分配
地址空间,并配置了基址寄存器,通常 BIOS 已经为这些设备配
置了互不冲突的地址,系统的 PCI 总线驱动程序可以直接扫
描 PCI 设备,并从基址寄存器中读出各个设备的基地址,也可以
推倒从新统⼀分配。这里功能驱动程序需要知道自⼰如何访问到
目标设备,所以从总线物理设备对象中读取基址信息。 ¤/
38 pio start = pci resource start ( pdev , 0 ) ;
39 pio end = pci resource end ( pdev , 0 ) ;
40 pio flags = pci resource flags ( pdev , 0 ) ;
41 pio len = pci resource len ( pdev , 0 ) ;
7
欢迎访问 OSPlay www.osplay.org
42
43 mmio start = pci resource start ( pdev , 1 ) ;
44 mmio end = pci resource end ( pdev , 1 ) ;
45 mmio flags = pci resource flags ( pdev , 1 ) ;
46 mmio len = pci resource len ( pdev , 1 ) ;
47
48 /¤
49 set this immediately , we need to know before
50 we talk to the chip directly
51 ¤/
52 DPRINTK ( "PIO region size == 0x%02X\n" , pio len ) ;
53 DPRINTK ( "MMIO region size == 0x%02lX\n" , mmio len ) ;
54
55 /¤ 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 /¤ make sure PCI base addr 0 is PIO ¤/
59 if ( ! ( pio flags & IORESOURCE IO ) ) f
60 dev err (& pdev�>dev , "region #0 not a PIO resource ,
aborting\n" ) ;
8
欢迎访问 OSPlay www.osplay.org
61 rc = �ENODEV ;
62 goto err out ;
63 g
64 /¤ check for weird/broken PCI region reporting ¤/
65 if ( pio len < RTL MIN IO SIZE ) f
66 dev err (& pdev�>dev , "Invalid PCI I/O region size(s
), aborting\n" ) ;
67 rc = �ENODEV ;
68 goto err out ;
69 g
70 #else
71 /¤ make sure PCI base addr 1 is MMIO ¤/
72 if ( ! ( mmio flags & IORESOURCE MEM ) ) f
73 dev err (& pdev�>dev , "region #1 not an MMIO
resource , aborting\n" ) ;
74 rc = �ENODEV ;
75 goto err out ;
76 g
77 if ( mmio len < RTL MIN IO SIZE ) f
78 dev err (& pdev�>dev , "Invalid PCI mem region size(s
), aborting\n" ) ;
79 rc = �ENODEV ;
80 goto err out ;
81 g
82 #endif
83
84 /¤ PCI 设备的地址是由软件配置的,那么必然需要⼀种机制来防止
地址冲突,系统维护
了 io resource 和 memory resource ,分别登记了已经分
配出去的地址空间,当为新设备分配地址的时候,必须根据登记
9
欢迎访问 OSPlay www.osplay.org
的信息找到空闲的地址空间,这里就是在系统中登记,表示改设
备要使用这个地址空间了,具体的地址在总线设备对
象 pdev 结构中指定。注意把地址写⼊到设备的基址寄存器早
就已经由 bios 或者内核完成了。这里只是进行登记。 ¤/
85 rc = pci request regions ( pdev , DRV NAME ) ;
86 if ( rc )
87 goto err out ;
88 disable dev on err = 1 ;
89
90 /¤ enable PCI bus�mastering ¤/
91 pci set master ( pdev ) ;
92
93 #ifdef USE IO OPS
94 ioaddr = ioport map ( pio start , pio len ) ;
95 if ( ! ioaddr ) f
96 dev err (& pdev�>dev , "cannot map PIO, aborting\n" ) ;
97 rc = �EIO ;
98 goto err out ;
99 g
100 dev�>base addr = pio start ;
101 tp�>mmio addr = ioaddr ;
102 tp�>regs len = pio len ;
103 #else
104 /¤ ioremap MMIO region ¤/
105 /¤ 映射设备地址,这里需要搞清楚 3 种地址:
106 1 . 虚拟地址:经过页表页目录映射,访问的时候通
过 CPU 的 MMU 转换得到物理地址。
107 2 . 物理地址:在实模式下使用的或保护模式下经
过 CPU 的 MMU 转换后的地址。
108 3 . 总线地址:经过各种总线桥接器转换后在出现在总线的地址线上
的地址。 ¤/
109
10
欢迎访问 OSPlay www.osplay.org
110 /¤ 通常程序指令中访问的是虚拟地址, CPU 在指令执行的时候通
过 MMU 把虚拟地址转换成物理地址,这个地址被送到北桥,北桥
根据自⼰地址空间判断出这个地址是在内存上还是在外部总线上
的设备上面,如果在外部设备上面,北桥可能要根据总线地址空
间的分配情况将这个物理地址变换成总线地址,再发送到总线上
面去。总线上的 Host � PCI 以及 PCI � PCI 都可以做这样
的变换,⼤多数情况下总线地址和物理地址是⼀样的,这取决于
系统设计。需要注意的是写⼊ PCI 设备基址寄存器中的值必须
总线地址,才能够匹配到出现在总线上的地址,从而响应寻址操
作。而 CPU 则要记住物理地址,并根据物理地址映射虚拟地
址。 ioaddr 是映射后的虚拟地址,以后程序中将通
过 ioaddr 访问设备。 ¤/
111
112 ioaddr = pci iomap ( pdev , 1 , 0 ) ;
113 if ( ioaddr == NULL ) f
114 dev err (& pdev�>dev , "cannot remap MMIO , aborting\n
" ) ;
115 rc = �EIO ;
116 goto err out ;
117 g
118 dev�>base addr = ( long ) ioaddr ;
119 tp�>mmio addr = ioaddr ;
120 tp�>regs len = mmio len ;
121 #endif /¤ USE IO OPS ¤/
122
123 . . . . . .
124
125 // reset 就是向命令寄存器写个 reset 命令。
126 rtl8139 chip reset ( ioaddr ) ;
127
128 ¤ dev out = dev ;
129 return 0 ;
130
131 err out :
11
欢迎访问 OSPlay www.osplay.org
132 rtl8 139 cleanup dev ( dev ) ;
133 if ( disable dev on err )
134 pci disable device ( pdev ) ;
135 return rc ;
136 g
回到rtl8139_init_one:
1 static int devinit rtl8139 init one ( struct pci dev
¤pdev , const struct pci device id ¤ent )
2 f
3 . . . . . .
4
5 i = rtl8139 init board ( pdev , &dev ) ;
6
7 . . . . . .
8
9 tp = netdev priv ( dev ) ;
10 ioaddr = tp�>mmio addr ;
11
12 // 从 ioaddr 中读出 Mac 地址
13 addr len = read eeprom ( ioaddr , 0 , 8 ) == 0 x8129 ? 8
: 6 ;
14 for ( i = 0 ; i < 3 ; i ++)
15 ( ( u16 ¤ ) ( dev�>dev addr ) ) [ i ] = le 16 to cpu (
read eeprom ( ioaddr , i + 7 , addr len ) ) ;
16
17 memcpy ( dev�>perm addr , dev�>dev addr , dev�>addr len )
;
18
12
欢迎访问 OSPlay www.osplay.org
19 /¤ The Rtl8139�specific entries in the device
structure . ¤/
20 /¤ 设置功能驱动程序对象的函数指针集 ¤/
21 dev�>open = rtl8139 open ;
22 dev�>hard start xmit = rtl8139 start xmit ;
23 dev�>poll = rtl8139 poll ;
24 dev�>weight = 64 ;
25 dev�>stop = rtl8139 close ;
26 dev�>get stats = rtl8139 get stats ;
27 dev�>set multicast list = rtl8139 set rx mode ;
28 dev�>do ioctl = netdev ioctl ;
29 dev�>ethtool ops = &rtl8139 ethtool ops ;
30 dev�>tx timeout = rtl8139 tx timeout ;
31 dev�>watchdog timeo = TX TIMEOUT ;
32 #ifdef CONFIG NET POLL CONTROLLER
33 dev�>poll controller = rtl8139 poll controller ;
34 #endif
35
36 . . . . . .
37
38 /¤ Put the chip into low�power mode . ¤/
39 /¤ ' R ' would leave the clock running . ¤/
40 if ( rtl chip info [ tp�>chipset ] . flags & HasHltClk )
41 RTL W8 ( HltClk , 'H' ) ;
42
43 return 0 ;
44
45 err out :
46 rtl8 139 cleanup dev ( dev ) ;
13
欢迎访问 OSPlay www.osplay.org
47 pci disable device ( pdev ) ;
48 return i ;
49 g
8139网卡的有⼀个接收缓冲寄存器,用于存放接收缓存的首地址,网
卡⼀边把网线上的发出的数据放到内部FIFO,⼀边从FIFO中把数据通过
DMA传送到由接收寄存器指定的内存地址中,接收到的数据依次排放,当
长度超过默认的缓冲区长度时,会回过头来放到开始的地方,所以接收
缓冲区被称为环形缓冲区。发送方面:8139有四个发送地址寄存器,CPU
将要发送的数据在内存中的地址写⼊这四个寄存器中的任何⼀个,网卡
就会通过DMA操作把数据发送出去。当发送或者接送完成后,网卡会发出
中断,中断处理程序通过读取网卡的中断状态寄存器来识别出是发送完
成发出的中断,接收到数据包的中断,还是错误中断。
当运行ifconfig ethx up的时候,rtl8139_open得到调用。该函数的
任务就是分配,初始化接收,发送缓冲区,分配中断号等。
1 static int rtl8139 open ( struct net device ¤dev )
2 f
3 struct rtl8139 private ¤tp = netdev priv ( dev ) ;
4 int retval ;
5 void iomem ¤ ioaddr = tp�>mmio addr ;
6
7 // 为网卡申请中断,当中断到来时 rtl8139 interrupt 会被调
用。
8 retval = request irq ( dev�>irq , rtl8139 interrupt ,
IRQF SHARED , dev�>name , dev ) ;
9 if ( retval )
10 return retval ;
11 // 分配接收,发送缓冲区, DMA 没有 CPU 的 MMU 单元,因此只
能使用物理地址上连续的内存空间。
14
欢迎访问 OSPlay www.osplay.org
12 tp�>tx bufs = pci alloc consistent ( tp�>pci dev ,
TX BUF TOT LEN , &tp�>tx bufs dma ) ;
13 tp�>rx ring = pci alloc consistent ( tp�>pci dev ,
RX BUF TOT LEN , &tp�>rx ring dma ) ;
14 if ( tp�>tx bufs == NULL j j tp�>rx ring == NULL )
15 f
16 free irq ( dev�>irq , dev ) ;
17
18 if ( tp�>tx bufs )
19 pci free consistent ( tp�>pci dev , TX BUF TOT LEN ,
tp�>tx bufs , tp�>tx bufs dma ) ;
20 if ( tp�>rx ring )
21 pci free consistent ( tp�>pci dev , RX BUF TOT LEN ,
tp�>rx ring , tp�>rx ring dma ) ;
22 return �ENOMEM ;
23 g
24
25 tp�>mii . full duplex = tp�>mii . force media ;
26 tp�>tx flag = ( TX FIFO THRESH << 1 1 ) & 0x003f0000 ;
27
28 /¤ 初始化接送发送缓冲区,由于有四个发送地址寄存器,因此把发
送缓冲区分成 4 组,以后发送请求到来的时候,将待发送内容拷
贝到第⼀组,再将第⼀组的地址写⼊寄存器,之后依次轮流使用
第⼆,三,四,⼀组。 . . . ¤/
29 rtl8139 init ring ( dev ) ;
30 /¤ 对网卡硬件进行相关的初始化。 ¤/
31 rtl8139 hw start ( dev ) ;
32
33 return 0 ;
34 g
15
欢迎访问 OSPlay www.osplay.org
rtl8139_hw_start主要是对网卡芯片进行初始化,主要就是根据网卡
的硬件手册,Programming guide向⼀些寄存器写⼊某些值。接下来我们
将看到⼤量类似的操作。
1 static void rtl8139 hw start ( struct net device ¤dev )
2 f
3 struct rtl8139 private ¤tp = netdev priv ( dev ) ;
4 /¤ ioaddr 就是访问设备上的存储空间的基址。 ¤/
5 void iomem ¤ ioaddr = tp�>mmio addr ;
6
7 . . . . . .
8 /¤ 向网卡命令寄存器写⼊⼀个 RESET 命令,这样网卡的各个寄存
器恢复到默认状态。 ¤/
9 rtl8139 chip reset ( ioaddr ) ;
10
11 /¤ unlock Config [ 0 1 2 3 4 ] and BMCR register writes ¤/
12 RTL W8 F ( Cfg9346 , Cfg9346 Unlock ) ;
13 /¤ MAC0 被定义成 0 ,在 8139 网卡 PCI 空间基址偏移为的个
自节是用于存放网卡06 MAC 地址的,现在把之前从 EEPROM 中
读出来的 MAC 地址写⼊这个地址,将来网卡在收包的时候,就会
根据这个寄存器中的值来确定自⼰的 MAC 地址。现在⼤家该明白
为什么我们平时能改 MAC 地址了吧。 ¤/
14 /¤ Restore our idea of the MAC address . ¤/
15 RTL W32 F ( MAC0 + 0 , cpu to le32 ( ¤ ( u32 ¤ ) ( dev�>
dev addr + 0) ) ) ;
16 RTL W32 F ( MAC0 + 4 , cpu to le32 ( ¤ ( u32 ¤ ) ( dev�>
dev addr + 4) ) ) ;
17
18 /¤ Must enable Tx/Rx before setting transfer
thresholds ! ¤/
19 /¤ 向命令写⼊命令,允许发送和接送。 ¤/
20 RTL W8 ( ChipCmd , CmdRxEnb j CmdTxEnb ) ;
16
欢迎访问 OSPlay www.osplay.org
21
22 /¤ 向接收配置寄存器写⼊配置,以后该网卡只接收广播帧和目
的 MAC 地址是自⼰的帧。设为混杂模式的时候,则是向这个寄存
器写⼊。 AcceptAllPhys ¤/
23 tp�>rx config = rtl8139 rx config j AcceptBroadcast
j AcceptMyPhys ;
24 RTL W32 ( RxConfig , tp�>rx config ) ;
25 RTL W32 ( TxConfig , rtl8139 tx config ) ;
26
27 . . . . . .
28 /¤ 向中断屏蔽寄存器写⼊中断允许位。默认允许接送,发送,错误
等等中断。如果屏蔽了接送中断,那么当网卡接收到帧的时候就
不会发出中断了, NAPI 就是通过屏蔽这里的接收中断,而通
过轮询接收状态寄存器来查看是不是有帧收到了来减少中断次
数,提高效率的。由于网卡接收⼩包的速度快,如果按常规处理
流程,中断 �> CPU 保存⼀堆寄存器然后中断处
理 �> CPU 恢复⼀堆寄存器 �> 调度决策�> . . . . . . 在⼩包
高速发送的环境下,尤其是在测试的时候,很可能在刚⼀恢复线
程上下文,中断⼜来了。⼜得重新保存上下文进⼊中断处理,关
闭中断进行轮询就是要节省这⼏个NAPI CPU 时钟周期。对⼤包
来说,⼀个包收的要慢⼀点,很可能在执行 poll 轮询的时候,
第⼀次检测网卡状态寄存器发现有包了, CPU Copy 出来处理,
之后再检测那个寄存器的时候,下⼀个⼤包的接收还没完成,于
是开了中断,结束⼀次,然后恢线程上下文,然后过了很⼩的⼀
段时间,中断⼜来了,所以⼤包省不了⼏个中断切换的时钟周
期,效果不明显。poll¤/
29 RTL W16 ( IntrMask , rtl8139 intr mask ) ;
以上都是向某个地址写⼊⼀些值,基地址是PCI总线驱动程序配置
的,某个偏移位置的地址代表什么意思,该写⼊什么值是功能设备的
芯片逻辑规定的。以接送配置寄存器为例,RxConfig被被定义为0x44,
则该寄存器地址偏移是0x44,AcceptAllPhys被定义为0x01, AcceptMyPhys
被定义为0x02,就是说该寄存器的最低位为1时,网卡会进⼊混杂模式
接收所有的帧,第⼀位为0,第⼆位为1时,只接收目的MAC地址为自⼰的
帧。彻底的搞清楚每⼀个寄存器的每个BIT代表什么实在是没有必要,
不同的网卡芯片都是不⼀样的。所有这里我将略去细节的分析,⼒求从
主线上把握就可以了,如果确实需要,可以查阅相关芯片的Datasheet和
17
欢迎访问 OSPlay www.osplay.org
Programming guide。
3 中断处理
当网卡收到数据,发送数据完成,或收发出错都可能发出中断,在中
断处理中根据网卡中断状态寄存器的值来判断是什么情况的中断,然后
调用相应的处理函数。
1 static irqreturn t rtl8139 interrupt ( int irq , void ¤
dev instance )
2 f
3 struct net device ¤dev = ( struct net device ¤ )
dev instance ;
4 struct rtl8139 private ¤tp = netdev priv ( dev ) ;
5 void iomem ¤ ioaddr = tp�>mmio addr ;
6 u16 status , ackstat ;
7 int link changed = 0 ; /¤ avoid bogus " uninit "
warning ¤/
8 int handled = 0 ;
9
10 spin lock (&tp�>lock ) ;
11 /¤ 读取中断状态寄存器的值。 ¤/
12 status = RTL R16 ( IntrStatus ) ;
13
14 . . . . . .
15
16 /¤ Receive packets are processed by poll routine . If
not running start it now . ¤/
17 /¤ 如果状态寄存器的接收位置 1 ,则进⼊接收处理函数。根
据 NAPI 机制。这里先向中断屏蔽寄存器中写
⼊ rtl8139 norx intr mask , 关闭接收中
18
欢迎访问 OSPlay www.osplay.org
断, netif rx shedule prep 检查网卡是不是处于 up 状
态,然后在 dev�>state 上设
置 LINK STATE RX SCHED 标记,然后通过把接收
的 netif rx schedule poll 函数加⼊软中断队列。将来
软中断调度的时候,会调用 rtl8139 poll , 进行轮询。轮询完
成的时候,会清除 dev�>state 上
的 LINK STATE RX SCHED 标记。这主要是避免软中断队列
中出现多余的 poll 请求。我们都知道中断的优先级比较高,
如果直接在这里用 netif rx schedule 把 poll 请求加⼊
软中断队列中,那么很可能在软中断还没被调度的时候,⼜来了
⼀次接收中断,于是⼜有⼀个 poll 请求被加⼊队列中。等软中
断被调度的时候,很可能在第⼀次 poll 的时候就处理完成了所
有的接收,而后来的那些中断所收到的数据也被第⼀个处理
了。poll¤/
18 if ( status & RxAckBits ) f
19 if ( netif rx schedule prep ( dev ) ) f
20 RTL W16 F ( IntrMask , rtl8139 norx intr mask ) ;
21 netif rx schedule ( dev ) ;
22 g
23 g
24
25 /¤ Check uncommon events with one test . ¤/
26 /¤ 如果状态寄存器的相关错误位置 1 ,则进⼊错误处理函数。 ¤/
27 if ( unlikely ( status & ( PCIErr j PCSTimeout j
RxUnderrun j RxErr ) ) )
28 rtl8139 weird interrupt ( dev , tp , ioaddr , status ,
link changed ) ;
29
30 /¤ 如果状态寄存器的发送位置 1 ,则进⼊发送中断的处理函
数。 ¤/
31 if ( status & ( TxOK j TxErr ) ) f
32 rtl8139 tx interrupt ( dev , tp , ioaddr ) ;
33 if ( status & TxErr )
34 RTL W16 ( IntrStatus , TxErr ) ;
35 g
19
欢迎访问 OSPlay www.osplay.org
36
37 out :
38 spin unlock (&tp�>lock ) ;
39
40 DPRINTK ( "%s: exiting interrupt , intr_status=%#4.4x
.\n" , dev�>name , RTL R16 ( IntrStatus ) ) ;
41 return IRQ RETVAL ( handled ) ;
这里有必要说明⼏种关中断的方式以免读者混淆。
² CPU指令执行要经过取指令,指令译码,执行,中断检查等过程,
通过CPU标志寄存器的IF位,可以让CPU在中断检查的时候忽略可屏
蔽中断信号。
² 8259A 等中断控制器上面可以设置中断屏蔽位,当外设发出中断请
求时,8259A先检测该中断是否被屏蔽,如果没被屏蔽,8259A才会
让CPU的intr管脚有效,从而向CPU通知中断。
² 每⼀个可中断的外设有⼀个中断屏蔽寄存器,来指明哪种情况下需
要向8259A中断控制器发出中断信号。
显然,这里的NAPI关闭接收中断是最后⼀种情况。在它关闭中断进行
轮询处理过程中,随时都可能被系统上的其它设备中断。
4 软中断请求
4.1 NAPI方式
__netif_rx_schedule(dev)是把poll函数加⼊软中断调度队列。 ;
1 void netif rx schedule ( struct net device ¤dev )
2 f
3 unsigned long flags ;
20
4.1 NAPI方式 欢迎访问 OSPlay www.osplay.org
4
5 local irq save ( flags ) ;
6 dev hold ( dev ) ;
7 /¤ 每⼀个 CPU 有⼀个软中断调度队列,这里 poll list 只是⼀
个双向链表结构,没别的意思,当软中断调度的时候,它会循环
处理队列中的调度请求,然后利用 list move tail ,直到队
列为空。 ¤/
8 list add tail (& dev�>poll list , & get cpu var (
softnet data ) . poll list ) ;
9 if ( dev�>quota < 0)
10 dev�>quota += dev�>weight ;
11 else
12 dev�>quota = dev�>weight ;
13 /¤ 系统启动的时候,通过 open softirq ( NET RX SOFTIRQ ,
net rx action , NULL ) ; 把 NET RX SOFTIRQ 的处理函数
设置为 net rx action ,触发软中断
后, net rx action 在适当的时候会被调用。
14 raise softirq irqoff ( NET RX SOFTIRQ ) ;
15 local irq restore ( flags ) ;
16 g
再看看net_rx_action。
1 static void net rx action ( struct softirq action ¤h )
2 f
3 struct softnet data ¤queue = & get cpu var (
softnet data ) ;
4 unsigned long start time = jiffies ;
5 int budget = netdev budget ;
6 void ¤have ;
7
8 local irq disable ( ) ;
21
4.1 NAPI方式 欢迎访问 OSPlay www.osplay.org
9
10 /¤ 循环处理软中断队列。 ¤/
11 while ( ! list empty (& queue�>poll list ) ) f
12 struct net device ¤dev ;
13
14 /¤ ⼀次软中断轮询时间不能过长。 ¤/
15 if ( budget <= 0 j j jiffies � start time > 1 )
16 goto softnet break ;
17
18 local irq enable ( ) ;
19
20 dev = list entry ( queue�>poll list . next , struct
net device , poll list ) ;
21 have = netpoll poll lock ( dev ) ;
22
23 /¤ 调用 poll 函数进⼊轮询处理。这里
是 rtl8139 poll 。¤/
24 if ( dev�>quota <= 0 j j dev�>poll ( dev , &budget ) ) f
25 netpoll poll unlock ( have ) ;
26 local irq disable ( ) ;
27 list move tail (& dev�>poll list , &queue�>
poll list ) ;
28 if ( dev�>quota < 0)
29 dev�>quota += dev�>weight ;
30 else
31 dev�>quota = dev�>weight ;
32 g else f
33 netpoll poll unlock ( have ) ;
34 dev put ( dev ) ;
22
4.1 NAPI方式 欢迎访问 OSPlay www.osplay.org
35 local irq disable ( ) ;
36 g
37 g
38 out :
39 #ifdef CONFIG NET DMA
40 /¤
41 ¤ There may not be any more sk buffs coming right
now , so push
42 ¤ any pending DMA copies to hardware
43 ¤/
44 if ( net dma client ) f
45 struct dma chan ¤chan ;
46 rcu read lock ( ) ;
47 list for each entry rcu ( chan , &net dma client
�>channels , client node )
48 dma async memcpy issue pending ( chan ) ;
49 rcu read unlock ( ) ;
50 g
51 #endif
52 local irq enable ( ) ;
53 return ;
54
55 softnet break :
56 get cpu var ( netdev rx stat ) . time squeeze ++;
57 raise softirq irqoff ( NET RX SOFTIRQ ) ;
58 goto out ;
59 g
23
4.2 非NAPI方式 欢迎访问 OSPlay www.osplay.org
4.2 非NAPI方式
在2.6的内核中,许多驱动默认就直接使用NAPI方式了,在没使用
NAPI方式的情况下,网卡驱动在接收中断的时候,调用netif_rx而不
是netif_rx_action。
1 int netif rx ( struct sk buff ¤skb )
2 f
3 struct softnet data ¤queue ;
4 unsigned long flags ;
5
6 /¤ if netpoll wants it , pretend we never saw it ¤/
7 /¤ 注意这个 netpoll 和我们说的轮询那个 poll 无关,在内核调
试过程中,用 printk 输出东西的时候,有时还来不急显示,
就 panic 了,可以通过配置 netpoll 把 printk 的数据发
送到另外⼀台机器,从而可以在 panic 看的时候看到正确的输
出。 ¤/
8 if ( netpoll rx ( skb ) )
9 return NET RX DROP ;
10
11 if ( ! skb�>tstamp . off sec )
12 net timest