Linux下 8019网卡驱动程序
福建鑫诺通信有限公司 陈光平(chenggp_fj@163.com)
本文以 S3C44B0 的 CPU 为例,详细解析了 linux 下 RTL8019 网卡驱动程序工作原理,其
间知识大多来源互联网络,特别是浙大潘纲的论文,在此不一一列出,此文目的只是让嵌入
式 linux 爱好者得到更多网卡驱动的资料,并获得交流机会,不足之处请指正
(一)、硬件相关部份
1、CPU与网卡的连接方式
A8 A0
(s3c44B0 CPU) (RTL 8019 网卡)
CPU 与网卡接线图
上图为 S3c44b0CPU 和网卡的接线图,此接法并非固定,如接法不同,则牵涉到很多相应的
改动,下面会详细分析不同之处
从硬件部门得到:网卡在 CPU 的存储空间上接 BANK4,即 0x08000000(看 44B0 手册)
外部中断号为 :EXTINT3 (irqs.h 文件获得值为 22)
上面两个值可以查 CPU 手册,或询问硬件设计人员
由上图可以知道以下数据:
(1)、网卡与 CPU 地址线连接错开 8 位(A0 接 A8)
(2)、总共连线,其实 4 根就足够用了,因为每根线可以译码 4 个地址空间,总共是 16 个
地址空间,每个地址空间对应一个寄存器地址,而 8019 总共就是 16 个寄存器
(3)、一般是跳线模式,不使用 9346 芯片
1-1 基地址算法
首先 8019 的基地址是 300H(见 RTL8019 芯片资料:选择 IO 总线地址),但是有些硬件已在
芯片中做过了偏移,比如我们的网卡已做了处理,基址已偏移到 0x08000000,
那么因为网卡 A0 接 CPU 的 A8,
示基地址左移 8 位,下一个寄存器 reg0 的地址就是:
0x08000100(0000,0000,0001Æ0001,0000,0000)
还不理解的话我们看另一种接法:
A9 A1
A10 A2
A11 A3
A12 A4
A1 A0
A2 A1
A3 A2
A4 A3
A5 A4
(S3C44B0 CPU) (RTL8019 芯片)
这种接法地址线只错开一位,我们来看(假如仍使用 BANK4):
A1ÆA0,为何 A0 不能接 A0,因为 8019 是共用数据线和地址线的,如果 A0 接 A0 的话,
无法配置 8 位和 16 位方式
现在可以看到,基地址为 300H 的话,左移一位(A0->A1),就是 600H,于是算出基地址为:
0x08000600,下一个寄存器的地址就是 0x08000602,因为都左移一位,相当于 x2 了
2、8019网卡的工作原理
本节主要讨论一下 8019 是怎么工作的,如下图:
RSAR0,1
RBCR0,1
CRDA0,1
TPSR
TBCR0,1
PSTART
PSTOP
16K
CURR 双口 RAM
BNRY
CLDA0,1
(本地 DMA) (远端 DMA)
(图 2 与 DMA 有关的寄存器 )
2-1 远端 DMA和本地 DMA
首先解释一下远端 DMA 和本地 DMA 的区别,以发送为例,网络发送数据是下面这样一个
流程:
CPU 将数据先发送到网卡上的 16KRAM 中,这之间必须要有一个数据通道,我们称为远端
DMA,而数据从网卡的 RAM 中发出给链路,RTL8019 控制器与 RAM 之间的通道称为本地
DMA
2-2 CPU读写数据到网络芯片
先解释一下上面提到的几个重要的寄存器,我们从上面分阶段说明:
RBCR0,RBCR1:存放要读写数据的长度
RSAR0,RSAR1:存放数据到网络芯片(RAM)中存放的起始地址(而不是页号,但通常
还是以某页的 00 为开始,如 0x4000)
CR:向命令寄存器发出 Remote DMA 开始指令
上面的流程后面会详细解说
2-3 网络芯片发数据到以太网
CPU 把数据用 Remote DMA 发到网络芯片之后,就可以用让网络芯片用 Local DMA 向
外数据了,需要设置如下的控制器:
TPSR:网络芯片要发送的数据在网络芯片 RAM 中的起始页号,所以发送的数据只能从某
页的开头存放。
TBCR0,TBCR1:要发送的数据总长度
CR:向命令寄存器发出发送数据包的指令 CMD_XMIT
然后程序就可以返回了,网络芯片会自动用 Local DMA 发数据包
2-4 网络芯片从以太网读数据
网络芯片在从以太网读数据过程中,会用到的寄存器如下:
PSTART,PSTOP:网络芯片接收数据缓冲区的起始和终止页号。形成一个接收缓冲环,每
页 256 字节
CURR:接收缓冲环写页指针,初始化=PSTART
BNRY:接收缓冲环读页指针,初始化=PSTART
这四个寄存器在 init 函数里初始化,以后有数据包来到时,网络芯片会自动判断是否发给本
机,是则用 Local DMA 存入数据,并自动修改读写指针
RTL8019 接收包帧结构:
下 一 页
指针
类型/长度 源 IP 地址
SA
校验 FCS 填充 PAD数 据 域
DATA
目的 IP
地址
以太网帧
长度
接收状
态
具体流程如下:
有了上面的收发包的格式,如何发送和接收数据包呢?看下图:
TPSR 发首地址
TBCR0
TBCR1
发送数目
图 3 发送缓冲区
先将待发送的数据包存入芯片 RAM,给出发送缓冲区首地址和数据包长度(写入 TPSR,
TBCR0,1),启动发送命令(CR=3E),即可实现 8019 发送功能.8019 会自动按以太网协议
完成发送并将结果写入状态寄存器。
那么 8019 是怎样接收数据的呢?请看下图:
PSTOP(FIFO 停止页)
BNRY(读页指针)
CURR(写页指针)
PSTART(FIFO 开始页)
RSAR0,1(远端 DMA 首地址)
双口 RAM
RBCR0,1(远端 DMA 数据字节数)
接收缓冲区构成一个循环 FIFO 队列,PSTART、PSTOP 两个寄存器限定了循环队列的开始
和结束页,CURR 为写入指针,受芯片控制,BNRY 为读出指针,由主机程序控制,根据
CURR=BNRY+1?可以判断是否收到新的包,新收到的数据包按下图的格式存于以 CURR
指出的地址为首址的 RAM 中
当 CURR=BNRY 时芯片停止接收数据包
总的说来,数据包是这样被主机写入芯片和从芯片 RAM 读出来的:主机设置好远端 DMA
开始地址(RSAR0,1)和远端 DMA 数据字节数(RBCR0,1),并在 CR 中设置读写,就
可以从远端 DMA 口寄存器读出芯片 RAM 里的数据或把数据写入 RAM 芯片,用个示例来说
明一下(以发送为例):
首先了解 RBCR0,1 中的远端 DMA 字节数在寄存器高低位上是怎么存储的?比如长度为
600 字节,如下:(十进制 600=十六进制 258)
PG0W_RBCR0 = 600 & 0xff; //0x58
PG0W_RBCR1 = 600 >> 8; //0x2
同样道理写入地址:
PG0W_RSAR0 = XMIT_START & 0xff;
PG0W_RSAR1 = XMIT_START >> 8;
然后发送:
NIC_CR = (CR_PAGE0 | CR_DMA_WRITE | CR_START);
此时 CPU 通过远端 DMA 发送了网卡 RAM 中的指定区域数据
2-5网卡 RAM的空间分配
网卡 RAM 的空间分配(包括收缓冲区、发缓冲区、数据存储区),RAM 空间原则上是随意
分配的,看了资料很多的分配
都是如下:
RAM 总共 16k,可以用来存储数据的地址是 0x4000-0x7FFF,为什么呢?看一下 RAM 的结
构
图 7 网卡 RAM 结构图
因为 256 个字节称为一页,所以 0x4000-0x7FFF 可以用页表示为 0x40---0x7F,下面采用的
也基本都是用页的地址,如:
0x4000-0x40ff 是一页,我们称该页为第 0x40 页(或页 0x40),页码也就是 16 位地址的高 8 位.
0x4100-0x41ff 是一页,称为第 0x41 页.
0x4000-0x7fff 共 16k 字节的 ram 是网卡接收和发送数据包用的,该 16k 字节的 ram 实际上
是双端口的 ram,可以同时被网卡读/写和用户读/写,相互之间不影响,网卡读/写比用户读写的
优先级高,
举个例子:
假如网卡收到一个数据包,需要放到 0x5000 开始的缓冲区,那么需要写入第 0x50 页.
这时用户同时读取旧的数据包,该数据包放在 0x4000 开始的一段缓冲区,那么需要读第 0x40
页.
网卡优先,它先写入一个字节,然后才读取一个字节到 ISA 总线上.内部总线的速度很快,是
20Mhz,而 ISA 总线只有 8Mhz, 网卡足够快进行同时读写数据,互不影响.
PROM
0000H
不使用
4000H
8K*16 缓冲区双口 RAM
7FFFH
不使用
既然如此,我们做如下定义:0x40---0x4B 共 12 页做为网卡的发送缓冲区,刚好可以存储两
个最大的网络包(约 1.5k 一个最大网络包),另外使用 0x4C---0x7F 为网卡的接收缓冲区,
共 52 页,如下定义:
Reg04=0x40;//参看 RTL8019 手册,04 在寄存器页 0 发状态为 TPSR,即发送首地址
Reg01=0x4C;//参看 RTL8019 手册,01 在寄存器页 1 写状态为 PSTART,即接收首地址
Reg02=0x80;//参看 RTL8019 手册,01 在寄存器页 2 写状态为 PSTOP,即接收尾地址
2-6关于网卡MAC地址:
0x00e0-0x00ff:52525454ABAB3D3D8E8E2C2C545446464C4C49494E4E404022222020575757
57 ,那么该网卡的地址是:5254AB3D8E2C,单地址和双地址的内容是重复的.一般使用偶数
地址的内容,Prom 是网卡在复位的时候从 93C46 里读出来的.如果你没有使用 93C46,那么就
不要使用 Prom.,那么如何获得网卡的地址,有两种方法,一是直接读 93C46,二是读 Prom.
网卡在工作的时候的网卡地址是由寄存器 MAR0,MAR1,MAR2,MAR3,MAR4,MAR5 决
定,而不是 93C46,也不是 Prom. 而这几个寄存器的内容需要用户自己编写程序写入,一般可
以读出 Prom里的网卡地址,然后写入到这 6 个寄存器里.如果你没有使用 93C46,那么 Prom也
是不可以使用的,这时要由你的程序自己指定一个网卡地址, 网卡地址不能跟别的网卡地址
相同,写入到 MAR0-MAR5 里.
例如使:
MAR0=00
MAR1=00
MAR2=00
MAR3=00
MAR4=00
MAR5=08
那么网卡的地址就是:
00:00:00:00:00:08
所有发给 00:00:00:00:00:08 地址的数据包就可以被网卡收到.
发送数据包的时候你的程序也要使用 MAR0-MAR5 的值作为发送的源地址.
2-7 关于 IO端口
几乎每一种外设都是通过读写设备上的寄存器来进行的。外设寄存器也称为“I/O 端口”,
通常包括:控制寄存器、状态寄存器和数据寄存器三大类,而且一个外设的寄存器通常被连
续地编址。CPU 对外设 IO 端口物理地址的编址方式有两种:一种是 I/O 映射方式(I/O-
mapped),另一种是内存映射方式(Memory-mapped)。而具体采用哪一种则取决于
CPU 的体系结构。
有些体系结构的 CPU(如,PowerPC、m68k 等)通常只实现一个物理地址空间(RAM)。
在这种情况下,外设 I/O 端口的物理地址就被映射到 CPU 的单一物理地址空间中,而成为
内存的一部分。此时,CPU 可以象访问一个内存单元那样访问外设 I/O 端口,而不需要设
立专门的外设 I/O 指令。这就是所谓的“内存映射方式”(Memory-mapped)。
而另外一些体系结构的 CPU(典型地如 X86)则为外设专门实现了一个单独地地址空间,
称为“I/O 地址空间”或者“I/O 端口空间”。这是一个与 CPU 的 RAM 物理地址空间不同
的地址空间,所有外设的 I/O 端口均在这一空间中进行编址。CPU 通过设立专门的 I/O 指
令(如 X86 的 IN 和 OUT 指令,linux 的 outb,inb 等,后面代码有用到)来访问这一空
间中的地址单元(也即 I/O 端口)。这就是所谓的“I/O 映射方式”(I/O-mapped)。与
RAM物理地址空间相比,I/O地址空间通常都比较小,如x86 CPU的 I/O空间就只有64KB
(0-0xffff)。这是“I/O 映射方式”的一个主要缺点。
Linux 采用内存映射方式。
我们通过上面的知识联系一下网卡的驱动:也就是说网卡的 IO 端口的物理地址被映射到
CPU 的物理地址上,即上面说的“网卡在 CPU 的存储空间上接 BANK4,即 0x08000000”
(三)、linux下网卡的驱动简介
3-1 linux下网卡驱动的基本概述
在 Linux 中,为了简化对设备的管理,将所有的外围设备都归结为三类:字符设备(如键盘,
鼠标等)、块设备(如硬盘、软驱等)和网络设备(如网卡、串口等等)。为了将网络环境中
的物理网络设备的多样性屏蔽,Linux 对所有的网络物理设备抽象并且定义了一个统一的概
念:接口(Interface)。对于所有的网络硬件的访问都是通过接口进行访问的,接口实际上提
供了一个对于所有类型的网络硬件的一致化的操作集合,用来处理对数据的发送和接收。对
于每一个已经驱动了的网络设备,都用一个 struct device 的数据结构表示。在内核启动或者
驱动模块插入时,通过网络驱动程序,向系统注册检测到的网络设备。在进行网络数据传输
的时候,网络驱动程序需要负责通过标准的接口将数据发送到相应的网络层,或者向网络发
送数据包,而由以 dev_base 为头指针的设备链表来集体管理所有网络设备,该设备链表中
的每个元素代表一个网络设备接口。数据结构 device 中有很多供系统访问和协议层调用的
设备方法,包括供设备初始化和系统注册用的 init 函数,打开和关闭网络设备的 open 和 stop
函数,处理数据包发送的 hard_start_xmit 函数,以及中断处理函数等
网络设备作为其中的三类设备之一,它有其非常特殊的地方。它与字符设备及块设备都有很
大的不同:
(1)、网络接口不存在于 Linux 的文件系统中,而是在核心中用一个 device 数据结构表示的。
每一个字符设备或块设备则在文件系统中都存在一个相应的特殊设备文件来表示该设备,如
/dev/hda1、/dev/sda1、/dev/tty1 等。网络设备在做数据包发送和接收时,直接通过接口访问,
不需要进行文件的操作;而对字符设备和块设备的访问都需通过文件操作界面。
(2)、网络接口是在系统初始化时实时生成的,对于核心支持的但不存在的物理网络设备,
将不可能有与之相对应的 device 结构。而对于字符设备和块设备,即使该物理设备不存在,
在/dev 下也必定有相应的特殊文件与之相对应。且在系统初始化时,核心将会对所有内核支
持的字符设备和块设备进行登记,初始化该设备的文件操作界面(struct file_operations),而
不管该设备在物理上是否存在。
以上两点是网络设备与其他设备之间存在的最主要的不同。然而,它们之间又有一些共同之
处,如在系统中一个网络设备的角色和一个安装的块设备相似。一个块设备在 blk_dev 数组
及核心其他的数据结构中登记自己,然后根据请求,通过自己的 request_function 函数“发
送”和“接收”数据块。相似地,为了能与外面世界进行数据交流,一个网络接口也必须在
一个特殊的数据结构中登记自己。
在系统内核中,存在字符设备管理表 chardevs 和块设备管理表 blkdevs,这两张保存着指向
file_operations 结构的指针的设备管理表,分别用来描述各种字符驱动程序和块设备驱动程
序。
类似地,在内核中也存在着一张网络接口管理表 dev_base,但与前两张表不同,dev_base
是指向 device 结构的指针,因为网络设备是通过 device 数据结构来表示的。dev_base 实际
上是一条 device 结构链表的表头,在系统初始化完成以后,系统检测到的网络设备将自动
地保存在这张链表中,其中每一个链表单元表示一个存在的物理网络设备。当要发送数据时,
网络子系统将根据系统路由表选择相应的网络接口进行数据传输,而当接收到数据包时,通
过驱动程序登记的中断服务程序进行数据的接收处理(软件网络接口除外)。以下是网络设
备工作原理图:
dev_queue_xmit ()
上层下传数据
netif_rx ()
向上层传送数据
DEVICE 结构的变量和方法
(设备接口)
mydev_interrupt ()
(中断服务程序)
从硬件接收数据
hard_start_xmit ()
向硬件发送数据
初始化
程序
网络物理设备和媒介
图一 Linux 网络设备工作原理图
每一个具体的网络接口都应该有一个名字,以在系统中能唯一标识一个网络接口。通常一个
名字仅表明该接口的类型。Linux 对网络设备命名有以下约定:(其中 N 为一个非负整数)
ethN 以太网接口,包括 10Mbps 和 100Mbps;
trN 令牌环接口;
slN SLIP 网络接口;
pppN PPP 网络接口,包括同步和异步;
plipN PLIP 网络接口,其中 N 与打印端口号相同;
tunlN IPIP 压缩频道网络接口;
nrN NetROM 虚拟设备接口;
isdnN ISDN 网络接口;
dummyN 空设备;
lo 回送网络接口。
3-2 重要数据结构——struct device
结构 device 存储一个网络接口的重要信息,是网络驱动程序的核心。在逻辑上,它可
以分割为两个部分:可见部分和隐藏部分。可见部分是由外部赋值;隐藏部分的域段仅面向
系统内部,它们可以随时被改变。下面我们将对之进行详细的分析和解剖。
/* from include/linux/netdevice.h */
struct device
{
(1)、属性
char *name;
设备的名字。如果第一字符为 NULL(即’\0’),register_netdev (drivers/net/net_init.c)将
会赋给它一个 n 最小的可用网络设备名 ethn。
unsigned long rmem_end; /* shmem "recv" end */
unsigned long rmem_start; /* shmem "recv" start */
unsigned long mem_end; /* shared mem end */
unsigned long mem_start; /* shared mem start */
这些域段标识被设备使用的共享内存的首地址及尾地址。如果设备用来接收和发送的内
存块不同,则 mem 域段用来标识发送的内存位置,rmem 用来标识接收的内存位置。
mem_start 和 mem_end 可在系统启动时用内核的命令行指定,用 ifconfig 可以查看它们
的值。rmem 域段从来不被驱动程序以外的程序所引用。
unsigned long base_addr; /* device I/O address */
unsigned char irq; /* device IRQ number */
I/O 基地址和中断号。它们都是在设备检测期间被赋值的,但也可以在系统启动时指定
传入(如传给 LILO)。ifconfig 命令可显示及修改他们的当前值。
volatile unsigned char start; /* start an operation */
volatile unsigned char interrupt; /* interrupt arrived */
这是两个二值的低层状态标志。通常在设备打开时置 start 标志,在设备关闭时清 start
标志。当 interrupt 置位时,表示有一个中断已到达且正在进行中断服务程序理。
unsigned long tbusy; /* transmitter busy must be long for bitops */
标识“发送忙”。在驱动程序不能接受一个新的需传输的包时,该域段应该为非零。
struct device *next;
指向下一个网络设备,用于维护链表。
unsigned char if_port;
记录哪个硬件 I/O 端口正在被接口所用,如 BNC,AUI,TP 等(drivers/net/de4x5.h)。
unsigned char dma;
设备用的 DMA 通道。
一些设备可能需要以上两个域段,但非必需的。
unsigned long trans_start; /* Time (in jiffies) of last Tx */
上次传输的时间点(in jiffies)
unsigned long last_rx; /* Time of last Rx */
上次接收的时间点(in jiffies)。如 trans_start 可用来帮助内核检测数据传输的死锁
(lockup)。
unsigned short flags; /* interface flags (a la BSD) */
该域描述了网络设备的能力和特性。它包括以下 flags:(include/linux/if.h)
IFF_UP
表示接口在运行中。当接口被激活时,内核将置该标志位。
IFF_BROADCAST
表示设备中的广播地址时有效的。以太网支持广播。
IFF_DEBUG
调试模式,表示设备调试打开。当想控制 printk 及其他一些基于调试目的的信息显
示时,可利用这个标志位。虽然当前没有正式的驱动程序使用它,但它可以在程序
中通过 ioctl 来设置从而使用它。
IFF_LOOPBACK
表示这是一个回送(loopback)设备,回送接口应该置该标志位。核心是通过检查
此标志位来判断设备是否是回送设备的,而不是看设备的名字是否是 lo。
IFF_POINTTOPOINT
表示这是一个点对点链接(SLIP and PPP),点对点接口必须置该标志位。Ifconfig
也可以置此标志位及清除它。若置上该标志位,则 dev->dstaddr 应也相应的置为链
接对方的地址。
IFF_MASTER /* master of a load balancer */
IFF_SLAVE /* slave of a load balancer */
此两个标志位在装入平等化中要用到。
IFF_NOARP
表示不支持 ARP 协议。通常的网络接口能传输 ARP 包,如果想让接口不执行 ARP,
可置上该标志位。如点对点接口不需要运行 ARP。
IFF_PROMISC
全局接受模式。在该模式下,设备将接受所有的包,而不关这些包是发给谁的。在
缺省情况下,以太网接口会使用硬件过滤,以保证只接受广播包及发给本网络接口
的包。Sniff 的原理就是通过设置网络接口为全局接受模式,接受所有到达本接口
媒介的包,来“偷听”本子网的“秘密”。
IFF_MULTICAST
能接收多点传送的 IP 包,具有多点传输的能力。ether_setup 缺省是置该标志位的,
故若不想支持多点传送,必须在初始化时清除该标志位。
IFF_ALLMULTI
接收所有多点传送的 IP 包。
IFF_NOTRAILERS /*无网络 TRAILER*/
IFF_RUNNING /*资源被分配*/
此标志在 Linux 中没什么用,只是为了与 BSD 兼容。
unsigned short family; /* address family ID (AF_INET) */
该域段标识本设备支持的协议地址簇。大部分为 AF_INET(英特网 IP 协议),接口通
常不需要用这个域段或赋值给它。
unsigned short metric; /* routing metric (not used) */
unsigned short mtu;
不包括数据链路层帧首帧尾的最大传输单位(Maximum Transfer Unit)。网络层在包传
输时要用到。对以太网而言,该域段为 1500,不包括 MAC 帧的帧首和帧尾(MAC 帧
格式稍后所示)。
unsigned short type; /* interface hardware type */
接口的硬件类型,描述了与该网络接口绑在一起的媒介类型。Linux 网络设备支持
许多不同种类的媒介,如以太网,X.25,令牌环,SLIP,PPP,Apple Localtalk 等。ARP
在判定接口支持哪种类型的物理地址时要用到该域段。若是以太网接口,则在
ether_setup 中将之设为 ARPHRD_ETHER(Ethernet 10Mbps)。
unsigned short hard_header_len; /* hardware hdr length */
在被传送的包中 IP 头之前的字节数。对于以太网接口,该域段为 14(ETH_HLEN,
include\linux\if_ether.h),这个值可由 MAC 帧的格式得出:
MAC 帧格式:
目的地址(6 字节)+ 源地址(6 字节)+ 数据长度(2 字节)+ 数据(46~~1500)+FCS
void *priv; /* pointer to private data */
该指针指向私有数据,通常该数据结构中包括 struct enet_statistics。类似于 struct file 的
private_data 指针,但 priv 指针是在设备初始化时被分配内存空间的(而不是在设备打
开时),因为该指针指向的内容包括设备接口的统计数据,而这些数据即使在接口卸下
(down)时也应可以得到的,如用户通过 ifconfig 查看。
unsigned char pad; /* make dev_addr aligned to 8 bytes */
unsigned char broadcast[MAX_ADDR_LEN]; /* hw bcast add */
广播地址由六个 0xff 构成,即表示 255.255.255.255。
memset(dev->broadcast,0xFF, ETH_ALEN); (drivers/net/net_init.c)
unsigned char dev_addr[MAX_ADDR_LEN]; /* hw address */
设备的物理地址。当包传送给驱动程序传输时,要用物理地址来产生正确的帧首。
unsigned char addr_len; /* hardware address length */
物理地址的长度。以太网网卡的物理地址为 6 字节(ETH_ALEN)。
unsigned long pa_addr; /* protocol address */
unsigned long pa_brdaddr; /* protocol broadcast addr */
unsigned long pa_mask; /* protocol netmask */
该三个域段分别描述接口的协议地址、协议广播地址和协议的网络掩码。若 dev->family
为 AF_INET,则它们即为 IP 地址。这些域段可用 ifconfig 赋值。
unsigned short pa_alen; /* protocol address length */
协议地址的长度。AF_INET 的为 4。
unsigned long pa_dstaddr; /* protocol P-P other side addr */
点对点协议接口(如 SLIP、PPP)用这个域记录连接另一边的 IP 值。
struct dev_mc_list *mc_list; /* Multicast mac addresses */
int mc_count; /* Number of installed mcasts*/
struct ip_mc_list *ip_mc_list; /* IP multicast filter chain */
这三个域段用于处理多点传输。其中 mc_count 表示 mc_list 中的项目数。
__u32 tx_queue_len; /* Max frames per queue allowed */
一个设备的传输队列能容纳的最大的帧数。对以太网,缺省为 100;而 plip 则为节省系
统资源,仅设为 10。
/* For load balancing driver pair support */
unsigned long pkt_queue; /* Packets queued */
struct device *slave; /* Slave device */
struct net_alias_info *alias_info; /* main dev alias info */
struct net_alias *my_alias; /* alias devs */
struct sk_buff_head buffs[DEV_NUMBUFFS];
指向网络接口缓冲区的指针。
3-3 服务处理程序
以下是一些对网络接口的操作,类似于字符设备和块设备。网络接口操作可以分为两部
分,一部分为基本操作,即每个网络接口都必须有的操作;另一部分是可选操作。
/* 基本操作 */
int (*init) (struct device *dev); /* Called only once. */
初始化函数的指针,仅被调用一次。当登记一个设备时,核心一般会让驱动程序初
始化该设备。初始化函数功能包括以下内容:检测设备是否存在;自动检测该设备的
I/O 端口和中断号;填写该设备 device 结构的大部分域段;用 kmalloc 分配所需的内存
空间等。若初始化失败,该设备的 device 结构就不会被链接到全局的网络设备表上。
在系统启动时,每个驱动程序都试图登记自己,当只有那些实际存在的设备才会登记成
功。这与用主设备号及次设备号索引的字符设备和块设备不同。
int (*open) (struct device *dev);
打开网络接口。每当接口被 ifconfig 激活时,网络接口都要被打开。Open 操作做
以下工作:登记一些需要的系统资源,如 IRQ、DMA、I/O 端口等;打开硬件;将 module
使用计数器加一。
int (*stop) (struct device *dev);
停止网络接口。操作内容与 open 相逆。
int (*hard_start_xmit) (struct sk_buff *skb, struct device *dev);
硬件开始传输。这个操作请求对一个包的传输,这个包原保存在一个 socket 缓冲
区结构中(sk_buff)。
int (*hard_header) (struct sk_buff *skb, struct device *dev, unsigned short type,
void *daddr, void *saddr, unsigned len);
这个函数可根据先前得到的源物理地址和目的物理地址建立硬件头(hardware
header)。以太网接口的缺省函数是 eth_header。
int (*rebuild_header)(void *eth, struct device *dev, unsigned long raddr, struct sk_buff
*skb);
在一个包被发送之前重建硬件头。对于以太网设备,若有未知的信息,缺省函数将
使用 ARP 填写。
struct enet_statistics* (*get_stats)(struct device *dev);
当一个应用程序需要知道网络接口的一些统计数据时,可调用该函数,如 ifconfig、
netstat 等。
/* 可选操作 */
void (*set_multicast_list)(struct device *dev);
设置多点传输的地址链表(*mc_list)。
int (*set_mac_address)(struct device *dev, void *addr);
改变硬件的物理地址。如果网络接口支持改变它的硬件物理地址,就可用这个操作。
许多硬件不支持该功能。
int (*do_ioctl)(struct device *dev, struct ifreq *ifr, int cmd);
执行依赖接口的 ioctl 命令。
int (*set_config)(struct device *dev, struct ifmap *map);
改变接口配置。设备的 I/O 地址和中断号可以通过该函数进行实时修改。
void (*header_cache_bind)(struct hh_cache **hhp, struct device *dev,
unsigned short htype, __u32 daddr);
void (*header_cache_update)(struct hh_cache *hh, struct device *dev, unsigned char *
haddr);
int (*change_mtu) (struct device *dev, int new_mtu);
这个函数负责使接口 MTU 改变后生效。如果当 MTU 改变时驱动程序要作一些特
殊的事情,就应该写这个函数。
struct iw_statistics* (*get_wireless_stats) (struct device *dev);
};
(四)、网卡驱动程序初始化
网卡初始化是网卡驱动程序中最重要的部份,它有两种初始化模式,即“模块初始化模式”
和“起动初始化模式”
1、“模块初始化模式”的分析
(a)、概述
insmod命令将调用相应模块的init_module(),装载模块。init_module函数在
初始化dev->init函数指针后,将调用register_netdev()在系统登记该设备。若登
记成功,则模块装载成功,否则返回出错信息。register_netdev首先检查设备名
是否已确定,若没赋值则给它一个缺省的值ethN,N为最小的可用以太网设备号
注;然后,网络设备自己的init_function,即刚在init_module中赋值的dev->init,
将被调用,用来实现对网络接口的实际的初始化工作。若初始化成功,则将该网
络接口加到网络设备管理表dev_base的尾部。整个函数调用关系图如下所示。下
面我们以用得最广泛以太网卡之一——NE2000 兼容网卡为例子进行分析。
NE2000 网卡的主要驱动程序在文件drivers/net/ne.c中。
insmod init_module( ) register_netdev( ) dev->init ( )
图二 “模块初始化模式”的函数调用关系图
(b)、init_module
init_module---模块初始化函数,当装载模块时,核心将自动调用该函数。在此函数中一
般处理以下内容:
1.处理用户可能传入的参数 name、ports 及 irq 的值。若有,则赋给相应的接口(注
意:未登记);
2.对 dev->init 函数指针进行赋值,对于任何网络设备这一步必不可少!!因为在
register_netdev 中要用到该函数指针;
3.调用 register_netdev,完成检测、初始化及设备登记等工作。
/* from drivers/net/ne.c */
init_module(void)
{
int this_dev, found = 0;
/* 对所有可能存在的以太网接口进行检测并试图去登记,MAX_NE_CARDS 为 4,
* 即最多可以使用 4 块 NE2000 兼容网卡。 */
for (this_dev = 0; this_dev < MAX_NE_CARDS; this_dev++) {
struct device *dev = &dev_ne[this_dev];
/* 可能有用户传入的参数:指定的 name、ports 及 irq 的值 */
dev->name = namelist+(NAMELEN*this_dev);
dev->irq = irq[this_dev];
dev->base_addr = io[this_dev];
dev->init = ne_probe; /* NE2000 的检测和初始化函数 */
dev->mem_end = bad[this_dev];
if (register_netdev(dev) == 0) { /* 试图登记该设备 */
found++;
continue; /* 设备登记成功,继续登记下一个设备 */
}
/* 第一次发生登记不成功事件 */
if (found != 0) /* 在这之前没有成功登记 NE2000 接口,返回 */
return 0;
/* 显示出错信息 */
if (io[this_dev] != 0)
printk(KERN_WARNING "ne.c: No NE*000 card found at i/o = %#x\n",
io[this_dev]);
else
printk(KERN_NOTICE "ne.c: No PCI cards found. Use \"io=0xNNN\" value(s) for
…………
(c)、register_netdev
该函数实现对网络接口的登记功能。其实现步骤如下:
1.首先检查设备名是否已确定,若没赋值则以以太网设备待之并给它一个缺省的
值 ethN,N 为最小的可用以太网设备号;
2.然后,网络设备自己的 init_function,即刚在 init_module 中赋值的 dev->init,将
被调用,用来实现对网络接口的实际的初始化工作。
3.若初始化成功,则将该网络接口加到网络设备管理表 dev_base 的尾部
/* from drivers/net/net_init.c */
int register_netdev(struct device *dev)
{
struct device *d = dev_base; /* 取得网络设备管理表的表头指针 */
…………
if (dev && dev->init) {
/*若设备名字没确定,则将之看作是以太网设备!!*/
if (dev->name &&
((dev->name[0] == '\0') || (dev->name[0] == ' '))) {
/* 找到下一个最小的空闲可用以太网设备名字 */
for (i = 0; i < MAX_ETH_CARDS; ++i)
if (ethdev_index[i] == NULL) {
sprintf(dev->name, "eth%d", i);
printk("loading device '%s'...\n", dev->name);
ethdev_index[i] = dev;
break;
}
}
…………
/* 调用初始化函数进行设备的初始化 */
if (dev->init(dev) != 0) {
…………
/* 将设备加到网络设备管理表中,加在最后 */
if (dev_base) {
/* 找到链表尾部 */
while (d->next)
d = d->next;
d->next = dev;
}
else
dev_base = dev;
dev->next = NULL;
…………
(d)、init_function
函数原型:int init_function (struct device *dev);
当系统登记一个网络设备时,核心一般会请求该设备的驱动程序初始化自己。初始化函
数功能包括以下内容:
1.检测设备是否存在,一般和第二步一起作;
2.自动检测该设备的 I/O 地址和中断号;
对于可以与其他共享中断号的设备,我们应尽量避免在初始化函数中登记 I/O 地
址和中断号,I/O 地址和中断号的登记最好在设备被打开的时候,因为中断号有可
能被其他设备所共享。若不准备和其他设备共享,则可在此调用 request_irq 和
request_region 马上向系统登记。
3.填写传入的该设备 device 结构的大部分域段;
对于以太网接口,device 结构中许多有关网络接口信息都是通过调用 ether_setup
函数(driver/net/net_init.c)统一来设置的,因为以太网卡有很好的共性。对于非以
太网接口,也有一些类似于 ether_setup 的函数,如 tr_setup(令牌网),fddi_setup。
若添加的网络设备都不属于这些类型,就需要自己填写 device 结构的各个分量。
4.kmalloc 需要的内存空间。
若初始化失败,该设备的 device 结构就不会被链接到全局的网络设备表上。在系统启
动时,每个驱动程序都试图登记自己,当只有那些实际存在的设备才会登记成功。这与用主
设备号及次设备号索引的字符设备和块设备不同。
物理设备 NE2000 兼容网卡的初始化函数是由 ne_probe 和 ne_probe1 及 ethdev_init 共同
实现。
/* from drivers/net/ne.c */
int ne_probe(struct device *dev)
{
…………
int base_addr = dev ? dev->base_addr : 0;
/* I/O 地址. User knows best.
*/
if (base_addr > 0x1ff) /* I/O 地址有指定值 */
return ne_probe1(dev, base_addr); /* 这个函数在下面分析 */
else if (base_addr != 0) /* 不自动检测 I/O */
return ENXIO;
…………
/* base_addr=0,自动检测,若有第二块 ISA 网卡则是一个冒险!☺
* 对所有 NE2000 可能的 I/O 地址都进行检测,可能的 I/O 地址在存在
* netcard_portlist 数组中:
* static unsigned int netcard_portlist[]={ 0x300, 0x280, 0x320, 0x340, 0x360, 0};
*/
for (i = 0; netcard_portlist[i]; i++) {
int ioaddr = netcard_portlist[i];
if (check_region(ioaddr, NE_IO_EXTENT))
continue;
/* 检测到一个 I/O 端口地址 */
if (ne_probe1(dev, ioaddr) == 0)
return 0;
…………
/* from drivers/net/ne.c */
s