编写 Linux 驱动程序
在尝试深入了解操作系统 (OS) 之前,必须充分理解操作系统的概念。关于 OS 有几种定义
OS 是一组手动和自动程序,允许一组用户以高效的方式共享计算系统。
字典将 OS 定义为管理计算系统的进程并允许正常执行其他作业的程序或程序集。
Tanenbaum 书籍(见资源)中的定义:操作系统是[程序],它控制计算机的所有资源,并为用户开发应用程序提供支持。
区分程序和进程也非常重要。程序是一块数据加上指令,存储在磁盘上的文件中,并准备执行。另一方面,进程是内存中正在执行的程序的映像。这种区别非常重要,因为进程通常在 OS 控制下运行。在这里,我们的程序是 OS,因此我们不能谈论进程。
我们将使用术语内核来指代 OS 的主体,它是一个用 C 语言编写的程序。程序文件可能命名为 vmlinuz、vmlinux 或 zImage,并且与 MS-DOS 文件 COMMAND.COM、MSDOS.SYS 和 IO.SYS 有一些共同之处,尽管它们的功能不同。当我们讨论内核的编译时,我们指的是我们将编辑源文件以生成新内核。
外围或内部设备允许用户与计算机通信。设备的示例包括:键盘、显示器、软盘和硬盘、CD-ROM、打印机、鼠标(串行/并行)、网络、调制解调器等。驱动程序是 OS 中管理与设备通信的部分;因此,它们通常被称为设备驱动程序。

图 1. 软件/硬件方案
图 1 显示了用户程序、OS 和设备之间的关系。此方案中清楚地说明了软件和硬件之间的区别。在左侧,用户程序可以通过一组高级库函数与设备(例如,硬盘)进行交互。例如,我们可以调用 C 库函数 fopen、fprintf 和 close 来打开硬盘上的文件并写入数据
FILE *fid=fopen("filename", "w"); fprintf(fid, "Hello, world!"); fclose(fid);
用户还可以从 OS shell 中使用诸如以下命令来写入文件(或另一个设备,例如打印机)
echo "Hello, world!" > echo "Hello, world!" > /dev/lp为了执行此命令,shell 和库函数都会调用 OS 的低级函数,例如,open()、write() 或 close()
fid = open("/dev/lp", O_WRONLY); write(fid, "Hello, world!"); close(fid);每个设备都可以称为名为 /dev/* 的特殊文件。在内部,OS 由一组驱动程序组成,这些驱动程序是执行与每个设备低级通信的软件片段。在此执行级别,内核调用驱动程序函数,例如 lp_open() 或 lp_write()。
在图 1 的右侧,硬件由设备(视频显示器或以太网链路)加上接口(VGA 卡或网卡)组成。最后,设备驱动程序是软件和硬件之间的物理接口。驱动程序通过端口(硬件物理链接的内存地址)使用内部函数 out_p 和 in_p 从硬件读取和写入硬件。
out_p(0x3a, 0x1f); data = in_p(0x3b);
请注意,这些函数对用户不可用。由于 Linux 内核在保护模式下运行,因此端口地址所在的低内存地址用户无法访问。与低级函数 in 和 out 等效的函数在高层库中不存在,就像在 MS-DOS 等其他操作系统中一样。
驱动程序的主要特性是
它执行输入/输出 (I/O) 管理。
它提供透明的设备管理,避免了低级编程(端口)。
它提高了 I/O 速度,因为它通常已经过优化。
它包括软件和硬件错误管理。
它允许多个进程并发访问硬件。
驱动程序有四种类型:字符驱动程序、块驱动程序、终端驱动程序和流驱动程序。字符驱动程序以字节为单位(参见图 2)将信息从用户传输到设备(或反之亦然)。两个示例是打印机 /dev/lp 和内存(是的,内存也是一种设备)/dev/mem。

图 2. 字符驱动程序
块驱动程序(参见图 3)以块为单位传输信息。这意味着传入的数据(来自用户或来自设备)存储在缓冲区中,直到缓冲区已满。发生这种情况时,缓冲区内容将物理发送到设备或用户。这就是为什么当用户程序崩溃时,所有打印的消息都不会出现在屏幕上(缓冲区中的消息丢失了),或者当用户写入文件时,软盘驱动器指示灯并非总是亮起的原因。此类驱动程序最明显的示例是磁盘:软盘 (/dev/fd0)、IDE 硬盘 (/dev/hda) 和 SCSI 硬盘 (/dev/sd1)。

图 3. 块驱动程序
终端驱动程序(参见图 4)构成了一组用于用户通信的特殊字符驱动程序。例如,开放窗口环境中的命令工具、X 终端或控制台都是需要特殊功能的设备,例如,用于命令缓冲区管理器的向上和向下箭头或 bash shell 中的制表符。块驱动程序的示例包括 /dev/tty0 或 /dev/ttya(串行端口)。在这两种情况下,内核都包含特殊例程,驱动程序包含特殊过程,以应对所有特定功能。

图 4. 终端驱动程序
流驱动程序是最新的驱动程序(参见图 5),专为非常高速的数据流而设计。内核和驱动程序都包含多个协议层。这种类型的最佳示例是网络驱动程序。

图 5. 流驱动程序
正如我们所说,驱动程序是一段程序。它由一组 C 函数组成,其中一些是强制性的。例如,对于打印机设备,一些仅由内核调用的典型函数可能是
lp_init():初始化驱动程序,仅在启动时调用。
lp_open():打开与设备的连接。
lp_read():从设备读取数据。
lp_write():写入设备。
lp_ioctl():执行设备配置操作。
lp_release():中断与设备的连接。
lp_irqhandler():设备调用的特定函数,用于处理中断。
一些附加函数可用于特定应用程序,例如 *_lseek()、*_readdir()、*_select() 和 *_mmap()。您可以在 Michael Johnson 的 黑客指南(参见资源)中找到有关它们的更多信息。
编写我们自己的设备驱动程序有几个原因
解决当两个或多个进程尝试同时访问设备时的并发问题。
使用硬件中断:由于内核在保护模式下运行,用户无法直接从程序管理中断。
处理其他不寻常的应用程序,例如管理虚拟设备(RAM 磁盘或设备模拟器)。
获得作为程序员的满足感:编写驱动程序可以提高个人动力以及对计算机的控制。
了解系统的内部部件。
相反,也有几个理由不编写我们自己的驱动程序
这需要大量的心理准备。
这需要低级编程,即直接管理端口和中断处理程序。
在调试过程中,内核很容易挂起,并且无法使用调试器或 C 库函数(例如 printf)。
为了理解以下解释,您必须了解 C 编程语言、基本 I/O 过程、PC 内部架构的最低限度知识,并且在 Unix 系统的软件应用程序开发方面具有一些经验。
最后,我们必须补充一点,只有当设备制造商不为我们的 OS 提供驱动程序,或者当我们希望为我们已有的驱动程序添加额外功能时,才需要编写我们自己的设备驱动程序。
我们回答的第一个问题是:为什么使用 Linux 作为如何编写驱动程序的示例?答案是双重的:所有源文件都在 Linux 中可用,并且我在西班牙 UPM-DISAM 的实验室中有一个可用的示例。
但是,目录结构和驱动程序与内核的接口都与操作系统相关。实际上,从一个版本或发行版到下一个版本,可能会出现小的更改。例如,从 Linux 1.2.x 到 Linux 2.0.x,发生了一些变化,例如驱动程序函数的原型、内核配置方法和内核编译的 Makefile。
我们为解释选择的设备是美国公司 Denning-Brach International Robotics 的 MRV-4 移动机器人。尽管该机器人使用带有特定板卡用于硬件接口(电机/声纳卡)的 PC,但该公司不提供 Linux 驱动程序。然而,控制机器人通过电机/声纳卡的所有软件的源文件都以 C 语言提供用于 MS-DOS。解决方案是为 Linux 编写驱动程序。在示例中,我们使用内核版本 2.0.24,尽管它也适用于以后的版本,只需进行少量修改。
移动平台由一组与两个电机(驱动和转向)耦合的车轮、一组充当障碍物检测的接近传感器的 24 个声纳以及一组检测碰撞的保险杠组成。我们需要实现一个驱动程序,至少具有以下服务(init、open 和 release 是强制性的)
write:发送线性和角速度命令
read:读取声纳测量值和编码器值
三个中断处理程序:当接收到声纳回声时存储声纳测量值,当保险杠检测到碰撞时实施紧急停止,以及当车轮位于 0(零)度并且 返回原位 标志处于活动状态时停止转向电机
ioctl 命令:返回原位,它向车轮发送恒定的角速度并激活 返回原位 标志;以及电机和声纳的配置
返回原位 服务允许用户将车轮停止在始终相同的初始位置(0 度)。来自声纳和编码器的传入值以及速度命令可能是机器人控制程序主循环的一部分。
回到初始方案(图 1),设备是 MRV-4 机器人,硬件接口是电机/声纳卡,驱动程序的源文件将是 mrv4.c,我们将生成的新内核将是 vmlinuz,用于内核测试的用户程序将是 mrv4test.c,设备将是 /dev/mrv4(参见图 6)。

图 6. mrv4hard MRV-4 方案
要构建驱动程序,请按照以下步骤操作
编写驱动程序源文件,特别注意内核接口。
将驱动程序集成到内核中,包括在内核源代码中调用驱动程序函数。
配置和编译新内核。
测试驱动程序,编写用户程序。
Linux 源文件的目录结构可以描述如下:/usr/src 包含子目录,例如 /xview 和 /linux。在 /linux 目录内,内核的不同部分分为子目录:init、kernel、ipc、drivers 等。目录 /usr/src/linux/drivers/ 包含驱动程序源文件,分为 block、char、net 等类别。
另一个有趣的目录是 /usr/include,其中包含主要的头文件,例如 stdio.h。它包含两个特殊的子目录
/usr/include/system/,其中包含系统头文件,例如 types.h
/usr/include/linux/,其中包含 Linux 内核头文件,例如 lp.h、serial.h、mem.h 和 mrv4.h。
编程驱动程序源文件时的首要任务是选择一个名称来唯一标识它,例如 hd、sd、fd、lp 等。在我们的例子中,我们决定使用 mrv4。我们的驱动程序将是一个字符驱动程序,因此我们将源文件写入文件 /usr/src/linux/drivers/char/mrv4.c,并将其头文件写入 /usr/include/linux/mrv4.h。
第二个任务是实现驱动程序 I/O 函数。在我们的例子中,mrv4_open()、mrv4_read()、mrv4_write()、mrv4_ioctl() 和 mrv4_release()。
在编程驱动程序时必须特别小心,因为存在以下限制
标准库函数不可用。
某些浮点运算不可用。
堆栈大小有限。
无法等待事件,因为内核以及所有进程都已停止。
内核级别支持的 OS 函数当然只是在其中编程的那些函数
kmalloc()、kfree():内存管理
cli()、sti():启用/禁用中断
add_timer()、init_timer()、del_timer():定时管理
request_irq()、free_irq():irq 管理
inb_p()、outb_p():端口管理
memcpy_*fs():数据管理
printk():输入/输出
register_*dev()、unregister_*dev():设备管理
*sleep_on()、wake_up*():进程管理
有关这些函数的详细信息,请参见 Johnson 的 指南(参见资源),甚至在内核源文件中。
通过低内存寻址提供对硬件接口(卡)的访问。卡的 I/O 寄存器(我们可以在其中读取/写入信息)物理连接到 PC 的内存地址(即端口)。例如,MRV-4 移动机器人的电机/声纳卡与地址 0x1b0 关联。此卡中使用了十六个寄存器,因此端口映射包括从 0x1b0 到 0x1be 的地址。表 1 显示了典型的端口寻址列表。
必须找到一个空闲地址区域来为新卡分配端口。在表 1 中,从 1b0 到 1be 的地址是空闲的。源代码示例 foo.c 可在 SSC FTP 站点上获得(参见文章末尾),并包含对系统函数的调用,该函数允许我们查看以前的地址表。最后,通过函数 inb_p 和 outb_p 授予对端口的访问权限。
中断是谈论低级编程和硬件控制时的另一个主要主题。中断处理与轮询相比的主要优点是硬件通常很慢。在打印机完成作业之前,我们无法停止计算机中的所有进程。相反,我们可以继续正常工作,直到打印机完成,然后发送一个中断信号,该信号由特定函数处理。
继续我们的示例,我们需要三个处理程序,每个处理程序对应于卡可以生成的硬件中断之一:声纳处理程序(在 irq 0x0a 处)、原位处理程序(在 irq 0x0b 处)和保险杠处理程序(在 irq 0x0c 处)。作为源代码必须执行的操作的示例,我们展示了 sonar_irq_hdlr 函数的结构。每次接收到来自声纳的回声时,它都必须
禁用硬件中断。
从其端口读取声纳值并将其存储在驱动程序内部变量中。
再次启用中断。
如果用户程序想要读取来自声纳的传入数据,它必须执行 mrv4_read 操作,该操作返回存储在驱动程序内部变量中的数据。
尽管我们将解释实现每个驱动程序函数的指南,但在编程您自己的驱动程序时,最好使用与您的驱动程序最相似的驱动程序作为示例。在我们的例子中,mrv4.c 和 mrv4.h 的模型分别是 lp.c 和 lp.h。
文件 mrv4.c 包括初始化和 I/O 函数。初始化函数 mrv4_init 必须遵循以下步骤(参见文件 foo.c 中的指南)
检查设备。
获取端口寻址的空闲区域。
测试硬件是否存在。
测试 irq 编号是否空闲。
初始化驱动程序内部变量。
返回 OK 状态。
如果在任何这些步骤中检测到错误,则必须撤消所有先前的操作并返回错误状态。为了实现 I/O 函数,必须在 mrv4.c 中定义和初始化以下结构(或类似的结构)
static struct file_operations mrv4_fops = { NULL, /* mrv4_lseek */ mrv4_read, /* mrv4_read */ mrv4_write, /* mrv4_write */ NULL, /* mrv4_readdir */ NULL, /* mrv4_select */ mrv4_ioctl, /* mrv4_ioctl */ NULL, /* mrv4_mmap */ mrv4_open, /* mrv4_open */ mrv4_release /* mrv4_release */ };
所有现有 I/O 函数的指针必须在此结构中设置。然后,可以实现 I/O 函数代码,遵循侧栏中显示的指南。
可用命令在文件 mrv4.h 中定义(参见 FTP 站点上提供的文件 foo.h 中的指南)
#define MRV4_MAGIC, 0x07 #define MRV4_RESET _IO(MRV4_MAGIC, 0x01 #define MRV4_GOTOHOME _IO(MRV4_MAGIC, 0x02 #define MRV4_RESETHOME _IO(MRV4_MAGIC, 0x03 #define MRV4_JOYSTICK _IOW(MRV4_MAGIC, 0x04, unsigned int #define MRV4_PREPMOVE _IOW(MRV4_MAGIC, 0x05, unsigned int #define MRV4_INITODOM _IO(MRV4_MAGIC, 0x06 #define MRV4_SONTOFIRE _IOW(MRV4_MAGIC, 0x07, unsigned int
_IO 宏用于没有参数的命令。_IOW 用于带有输入参数的命令。在这种情况下,宏需要参数类型,例如指针可能是 unsigned int 类型。魔术数字必须由程序员选择。尝试选择一个系统未保留的数字(参见 /usr/include/linux 中的其他头文件)。常量在文件 /usr/include/linux/mrv4.h 中定义,驱动程序 (mrv4.c) 和用户程序都必须包含该文件。通常,mrv4.h 文件可以包括
常量和宏定义
ioctl 命令
端口名称
类型定义
驱动程序和用户之间交换的数据结构
mrv4_init() 函数原型
将驱动程序集成到内核中的任务包括几个步骤
插入对新驱动程序的内核调用。
将驱动程序添加到驱动程序列表。
修改编译脚本。
重新编译驱动程序。
OS 调用 mrv4_init() 的插入在 /usr/src/linux/kernel/mem.c 文件中完成。其他驱动程序函数调用(open、read、write、ioctl、release 等)对用户是透明的。它们通过 file_operations 结构执行。必须将驱动程序主编号添加到位于 /usr/include/linux/major.h 的列表中。搜索空闲驱动程序编号;例如,如果编号 62 是空闲的,则必须根据 Linux 版本将以下一行或两行添加到文件中
/dev/mrv4 62 #define MRV4_MAJOR 62
每个设备都由一个主编号和一个次编号引用。主编号表示驱动程序的编号。次编号区分由同一设备控制的几个设备(例如,由同一 IDE 驱动程序控制的几个硬盘:hd0、hd1、hd2)。
下一步是创建一个逻辑设备来访问驱动程序。您必须以这种方式使用命令 mknod
mknod -m og+rw /dev/mrv4 c 62 0
其中 62 是主编号,0 是次编号(只有一个物理设备),c 表示字符设备。根据需要设置权限,尽管您稍后可以使用命令 chmod 修改它们。例如,如果您想允许所有用户访问设备,请启用 rw
crw-rw-rw-2 bin bin 62, 0 Mar 12 1997 /dev/mrv4
为了允许在内核中编译驱动程序,必须将以下行添加到脚本文件 /usr/src/linux/arch/i386/config.in
comment 'MRV 4' bool 'MRV 4 card support' CONFIG_MRV4
并将以下行添加到 /usr/src/linux/drivers/char/Makefile
ifdef CONFIG_MRV4 L_OBJS += mrv4.o endif建议在链接内核之前单独编译驱动程序。此方法将节省测试语法错误的时间
cd /usr/src/linux/drivers/char gcc -c mrv4.c -Wall -D__KERNEL__当一切顺利时,删除目标文件
rm -f mrv4.o接下来,通过键入以下命令来配置内核
cd /usr/src/linux make config当脚本询问您是否安装 MRV-4 驱动程序时,回答 yes(这会设置常量 CONFIG_MRV4)。最后,插入一张空软盘并通过键入以下命令重新构建内核
make zdisk # generate a bootable # floppy disk dev -R /dev/fd0 1 # disable writes to<\n> # floppy一旦您确定内核可以正常工作,您就可以使用新内核覆盖文件 vmlinuz。要测试新内核,请重新启动系统(键入 reboot)...祝你好运!没有可用的调试器。
如果内核似乎可以工作,您可以编写一个或多个用户程序(即 mrv4test.c)来测试驱动程序,这些程序调用驱动程序函数
fid = open("/dev/mrv4", ...); read(fid, ...); write(fid, ...); ioctl(fid, ...); close(fid);
您可以在 sunsite.unc.edu(参见资源)获得特权文档。但当然,您永远无法仅使用本文的一般指南来编写自己的驱动程序。为了方便此任务,我们为 Linux 2.0.24 提供了一个虚拟驱动程序的源文件,它是字符驱动程序开发的模型。它模拟方程 y = a x,并包含中断管理示例(由于它未与任何硬件关联,因此不起作用)。它的名称是 foo,因为 Linux 已经有一个名为 dummy 的驱动程序。这些文件是
README:安装说明摘要
foo.c:驱动程序源文件
foo.h:驱动程序头文件
footest.c:用于驱动程序测试的程序
您可以通过匿名 FTP 在 ftp://ftp.linuxjournal.com/pub/lj/listings/issue48/2476.tgz 获取这些文件。
Fernando Matía 是马德里理工大学 (UPM) 的副教授。他于 1966 年出生于西班牙马德里。他于 1990 年在 UPM 成为工业工程师,并于 1994 年在 UPM 获得控制工程领域的博士学位。他在系统和自动控制工程部门 (DISAM) 工作。他的主要活动是智能控制、模糊控制、机器人技术和计算机科学。可以通过 matia@disam.upm.es 与他联系。