Linux 网络设备驱动程序设计
刘文涛
(武汉理工大学计算机科学与技术学院)
摘 要 Linux 网络设备驱动程序是 Linux 网络应用的重要组成部分。本文详细分析了 Linux
内核中网络设备驱动程序的运行机理,并着重介绍了实现 Linux 网络驱动程序的关键过程,
最后给出了一种实现模式和具体实例。
关键词 Linux 操作系统;网络设备驱动程序;内核;模块
1 引言
Linux 网络设备驱动程序是 Linux 操作系统网络应用中的一个重要的组成部分,分析其
运行机理,对于设计 Linux 网络应用程序是很有帮助的。我们可以在网络驱动程序这一级做
一些与应用相关联的特殊事情,例如,在设计 Linux 防火墙和网络入侵检测系统时可以在网
络驱动程序的基础上拦截网络数据包,继而对其进行分析。由于 Linux 是开放源代码的,这
给我们提供了一个绝好的机会来分析和改造网络驱动程序使其满足自己的特殊应用。本文就
Linux 内核中的网络驱动程序部分进行了详细的讨论,并给出了实现 Linux 网络驱动程序的
重要过程及一种实现模式和具体实例。
2 运行机理
1) 体系结构
dev_queue_x
mit()
netif_rx()
struct device
数据包发送
hard_start_x
mit()
中 断 处 理
( 数 据 包
接收)
网络物理设备媒介
网络协议
接口层
网络设备
接口层
设备驱动
功能层
设备媒
介层
图 1 Linux 网络驱动程序体系结构
Linux 网络驱动程序的体系结构如图 1 所示。可以划分为四层,从上到下分别为协议接
口层,网络设备接口层,再就是提供实际功能的设备驱动功能层,以及网络设备和网络媒介
层。我们在设计网络驱动程序时,最主要的工作就是完成设备驱动功能层,使其满足我们自
己所需的功能。在 Linux 中对所有网络设备都抽象为一个接口,这个接口提供了对所有网络
设备的操作集合。由数据结构 struct device 来表示网络设备在内核中的运行情况,即网络设
备接口,它既包括纯软件网络设备接口,如环路(Loopback),也可以包括硬件网络设备接
口,如以太网卡。而由以 dev_base 为头指针的设备链表来集体管理所有网络设备,该设备
链表中的每个元素代表一个网络设备接口。数据结构 device 中有很多供系访问和协议层调
用的设备方法,包括供设备初始化和往系统注册用的 init 函数,打开和关闭网络设备的 open
和 stop 函数,处理数据包发送的函数 hard_start_xmit,以及中断处理函数等。有关 device 数
据结构(在内核中也就是 net_device)的详细内容,请参看/linux/include/linux/netdevice.h。
1
2) 初始化
网络设备的初始化主要是由 device 数据结构中的 init 函数指针所指的初始化函数来完成
的,当内核启动或加载网络驱动模块的时候,就会调用初始化过程。在这其中将首先检测网
络物理设备是否存在,这是通过检测物理设备的硬件特征来完成,然后再对设备进行资源配
置,这些完成之后就要构造设备的 device 数据结构,把检测到的数值来对 device 中的变量
初始化,这一步很重要。最后向 Linux 内核中注册该设备并申请内存空间。
3) 数据包的发送与接收
数据包的发送和接收是实现 Linux 网络驱动程序中两个最关键的过程,对这两个过程处
理的好坏将直接影响到驱动程序的整体运行质量。图 1 中也很明确地说明了网络数据包的传
输过程。首先在网络设备驱动加载时,通过 device 域中的 init 函数指针调用网络设备的初始
化函数对设备进行初始化,如果操作成功就可以通过 device 域中的 open 函数指针调用网络
设备的打开函数打开设备,再通过 device 域中的建立硬件包头函数指针 hard_header 来建立
硬件包头信息。最后通过协议接口层函数 dev_queue_xmit(详见/linux/net/core/dev.c)来调
用 device 域中的 hard_start_xmit 函数指针来完成数据包的发送。该函数将把存放在套接字缓
冲区中的数据发送到物理设备,该缓冲区是由数据结构 sk_buff (详见/linux/include/linux/
sk_buff.h)来表示的。
数据包的接收是通过中断机制来完成的,当有数据到达时,就产生中断信号,网络设备
驱动功能层就调用中断处理程序,即数据包接收程序来处理数据包的接收,然后网络协议接
口层调用 netif_rx 函数(详见/linux/net/core/dev.c)把接收到的数据包传输到网络协议的上层
进行处理。
3 实现模式
实现 Linux 网络设备驱动功能主要有两种形式,一是通过内核来进行加载,当内核启动
的时候,就开始加载网络设备驱动程序,内核启动完成之后,网络驱动功能也随即实现了,
再就是通过模块加载的形式。比较两者,第二种形式更加灵活,在此着重对模块加载形式进
行讨论。
模块设计是 Linux 中特有的技术,它使 Linux 内核功能更容易扩展。采用模块来设计
Linux 网络设备驱动程序会很轻松,并且能够形成固定的模式,任何人只要依照这个模式去
设计,都能设计出优良的网络驱动程序。先简要概述一下基于模块加载的网络驱动程序的设
计步骤,后面还结合具体实例来讲解。首先通过模块加载命令 insmod 来把网络设备驱动程
序插入到内核之中。然后 insmod 将调用 init_module()函数首先对网络设备的 init 函数指针初
始化,再通过调用 register_netdev()函数在 Linux 系统中注册该网络设备,如果成功,再调用
init 函数指针所指的网络设备初始化函数来对设备初始化,将设备的 device 数据结构插入到
dev_base 链表的末尾。最后可以通过执行模块卸栽命令 rmmod 来调用网络驱动程序中的
cleanup_module()函数来对网络驱动程序模块卸载。具体实现过程见图 2 所示。
通过模块初始化网络接口是在编译内核时标记为编译为模块,系统在启动时并不知道该
接口的存在,需要用户在/etc/rc.d/目录中定义的初始启动脚本中写入命令或手动将模块插入
内核空间来激活网络接口。这也给我们在何时加载网络设备驱动程序提供了灵活性。
2
insmod 命令
init_module()
register_netdev()
init 指针调用初始化函数
打开网络接口设备
数据包发送与接收操作
关闭网络接口设备
模块卸载
图 2 Linux 网络设备驱动程序实现模式
4 应用实例
我们以 ne2000 兼容网卡为例,来具体介绍基于模块的网络驱动程序的设计过程。可以
参考文件 linux/drivers/net/ne.c 和 linux/drivers/net/8390.c。
1)模块加载和卸载
ne2000 网卡的模块加载功能由 init_module()函数完成,具体过程及解释如下:
int init_module(void)
{
int this_dev, found = 0;
//循环检测 ne2000 类型的网络设备接口
for (this_dev = 0; this_dev < MAX_NE_CARDS; this_dev++)
{
//获得网络接口对应的 net-device 结构指针
struct net_device *dev = &dev_ne[this_dev];
dev->irq = irq[this_dev];
dev->mem_end = bad[this_dev];
dev->base_addr = io[this_dev];
dev->init = ne_probe;
//调用 registre_netdevice()向系统登记网络接口,在这个函数中将分配给网络接口在
系统中惟一的名称。并且将该网络接口设备添加到系统管理的链表 dev-base 中进行管理。
//初始化该接口的中断请求号
//初始化接收缓冲区的终点位置
//初始化网络接口的 I/O 基地址
//初始化 init 为 ne_probe,后面介绍此函数
if (register_netdev(dev) == 0) {
found++;
continue; }
… //省略
}
return 0;}
模块卸载功能由 cleanup_module()函数来实现。如下所示:
void cleanup_module(void)
{
int this_dev;
3
//遍历整个 dev-ne 数组
for (this_dev = 0; this_dev < MAX_NE_CARDS; this_dev++) {
//获得 net-device 结构指针
struct net_device *dev = &dev_ne[this_dev];
if (dev->priv != NULL) {
idev->deactivate(idev);
void *priv = dev->priv;
struct pci_dev *idev = (struct pci_dev *)ei_status.priv;
//调用函数指针 idev->deactive 将已经激活的网卡关闭使用
if (idev)
free_irq(dev->irq, dev);
//调用函数 release_region()释放该网卡占用的 I/O 地址空间
release_region(dev->base_addr, NE_IO_EXTENT);
//调用 unregister_netdev()注销 这个 net_device()结构
unregister_netdev(dev);
kfree(priv);
//释放 priv 空间
}
}
}
2)网络接口初始化
实现此功能是由 ne_probe()函数来完成的,前面已经提到过,在 init_module()函数中用
它来初始化 init 函数指针。它主要对网卡进行检测,并且初始化系统中网络设备信息用于后
面的网络数据的发送和接收。具体过程及解释如下:
int __init ne_probe(struct net_device *dev)
{
unsigned int base_addr = dev->base_addr;
//初始化 dev-owner 成员,因为使用模块类型驱动,会将 dev-owner 指向对象 modules 结构
指针。
SET_MODULE_OWNER(dev);
//检测 dev->base_addr 是否合法,是则执行 ne-probe1()函数检测过程。不是,则需要自
动检测。
if (base_addr > 0x1ff)
return ne_probe1(dev, base_addr);
else if (base_addr != 0)
return -ENXIO;
//如果有 ISAPnP 设备,则调用 ne_probe_isapnp()检测这种类型的网卡。
if (isapnp_present() && (ne_probe_isapnp(dev) == 0))
return 0;
…//省略
return -ENODEV;
}
这其中两个函数 ne_probe_isapnp()和 ne_probe19()的区别在于检测中断号上面。PCI 方
式只需指定 I/O 基地址就可以自动获得 irq,是由 BIOS 自动分配的。而 ISA 方式需要获得
空闲的中断资源才能分配。
4
3)网络接口设备打开和关闭
网络接口设备打开就是激活网络接口,使它能接收来自网络的数据并且传递到网络协议
栈的上面,也可以将数据发送到网络上。设备关闭就是停止操作。
在 ne2000 网络驱动程序中网络设备打开由 dev_open()和 ne_open()完成。设备关
闭有 dev_close()和 ne_close()完成。它们相应调用底层函数 ei_open()和 ei_close()
来完成。其实现过程相对简单,不再赘述。
4)数据包接收和发送
在驱动程序层次上的发送和接收数据都是通过低层对硬件的读写来完成的。当网络上的
数据到来时,将触发硬件中断,根据注册的中断向量表确定处理函数,进入中断向量处理程
序,将数据送到上层协议进行处理。
对 ne 网卡的数据接收过程是在 ne_probe()函数中的中断处理函数 ei_interrupt 来完成
的,在进入 ei_interrupt()之后再通过 ei_receive()从 8309 的接收缓冲区获得数据,并组
合成 sk_buff 结构,再通过 netif_rx()函数将接收到的数据存放在系统的饿接收队列之中。
Ei-interrupt()的函数原型如下:void ei_interrupt(int irq,void *dev_id, struct
pt_regs *regs)
其中 irq 为中断号,dev-id 是表示产生中断的网络接口设备对应的结构指针,regs 表示当
前的寄存器内容。
数据发送是由 dev_dev_start_xmit 函数指针对应的函数为 ei_start_xmit 函数,由它来完
成数据包的发送。在函数 ethdev_init()把 net_device 结构的 hard_start_xmit 指针初始化为
ei_start_xmit。
5 小结
设计 Linux 网络设备驱动程序有一定的模式,遵循这个模式,将会大大减轻我们设计程
序的工作量。本文分析了网络驱动程序在内核中的工作机理,并提出了设计网络驱动程序的
一种模式并给出了实例,这对设计复杂的网络驱动程序是很有帮助的。
参考文献
[1] Alessandro Rubini 著. LISOLEG 译.Linux 设备驱动程序. 北京:中国电力出版社. 2000
[2] 雷澍等编著. Linux 的内核与编程. 北京:机械工业出版社,2000
[3] 李善平,刘文蜂等. Linux 内核 2.4 版源代码分析大全. 北京:机械工业出版社,2002
5