深入浅出Linux 设备驱动编程
Posted yxtxiaotian
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入浅出Linux 设备驱动编程相关的知识,希望对你有一定的参考价值。
【转自】https://www.cnblogs.com/zhang-xiao/archive/2013/01/02/2842351.html
目录
1.引言
目前,Linux 软件工程师大致可分为两个层次:
(1)Linux 应用软件工程师(Application Software Engineer):主要利用C 库函数和Linux API 进行应用软件的编写;
(2)Linux 固件工程师(Firmware Engineer):主要进行Bootloader、Linux 的移植及Linux 设备驱动程序的设计。
一般而言,固件工程师的要求要高于应用软件工程师的层次,而其中的Linux 设备驱动编程又是Linux 程序设计中比较复杂的部分,究其原因,主要包括如下几个方面:
(1)设备驱动属于Linux 内核的部分,编写Linux 设备驱动需要有一定的Linux 操作系统内核基础;
(2)编写Linux 设备驱动需要对硬件的原理有相当的了解,大多数情况下我们是针对一个特定的嵌入式硬件平台编写驱动的;
(3)Linux 设备驱动中广泛涉及到多进程并发的同步、互斥等控制,容易出现bug;
(4)由于属于内核的一部分,Linux 设备驱动的调试也相当复杂。
目前,市面上的Linux 设备驱动程序参考书籍非常稀缺,少有的经典是由Linux 社区的三位领导者Jonathan Corbet、Alessandro Rubini、Greg Kroah-Hartman 编写的《Linux Device Drivers》(目前该书已经出版到第3 版,中文译本由中国电力出版社出版)。该书将Linux 设备驱动编写技术进行了较系统的展现,但是该书所列举实例的背景过于复杂,使得读者需要将过多的精力投放于对例子背景的理解上,很难完全集中精力于Linux 驱动程序本身。往往需要将此书翻来覆去地研读许多遍,才能有较深的体会。
本文将仍然秉承《Linux Device Drivers》一书以实例为主的风格,但是实例的背景将非常简单,以求使读者能将精力集中于Linux 设备驱动本身,理解Linux 内核模块、Linux 设备驱动的结构、Linux 设备驱动中的并发控制等内容。另外,与《Linux Device Drivers》所不同的是,针对设备驱动的实例,本文还给出了用户态的程序来访问该设备,展现设备驱动的运行情况及用户态和内核态的交互。相信阅读完本文将为您领悟《Linux Device Drivers》一书中的内容打下很好的基础。
本文中的例程除引用的以外皆由笔者亲自调试通过,主要基于的内核版本为Linux 2.4,例子要在其他内核上运行只需要做少量的修改。构建本文例程运行平台的一个较好方法是:在 Windows 平台上安装VMWare 虚拟机,并在VMWare 虚拟机上安装Red Hat。注意安装的过程中应该选中“开发工具”和“内核开发”二项(如果本文的例程要在特定的嵌入式系统中运行,还应安装相应的交叉编译器,并包含相应的Linux 源代码):
2.Linux 内核模块
Linux 设备驱动属于内核的一部分,Linux 内核的一个模块可以以两种方式被编译和加载:
(1)直接编译进Linux 内核,随同Linux 启动时加载;
(2)编译成一个可加载和删除的模块,使用insmod 加载(modprobe 和insmod 命令类似,但依赖于相关的配置文件),rmmod 删除。这种方式控制了内核的大小,而模块一旦被插入内核,它就和内核其他部分一样。
下面我们给出一个内核模块的例子:
#include <linux/module.h> //所有模块都需要的头文件
#include <linux/init.h> // init&exit 相关宏
MODULE_LICENSE("GPL");
static int __init hello_init (void)
printk("Hello module init\\n");
return 0;
static void __exit hello_exit (void)
printk("Hello module exit\\n");
module_init(hello_init);
module_exit(hello_exit);
分析上述程序,发现一个Linux 内核模块需包含模块初始化和模块卸载函数,前者在insmod 的时候运行,后者在rmmod 的时候运行。初始化与卸载函数必须在宏module_init和module_exit 使用前定义,否则会出现编译错误。
程序中的 MODULE_LICENSE("GPL")用于声明模块的许可证。
如果要把上述程序编译为一个运行时加载和删除的模块,则编译命令为:
gcc –D__KERNEL__ -DMODULE –DLINUX –I /usr/local/src/linux2.4/include -c –o hello.o hello.c
由此可见,Linux 内核模块的编译需要给gcc 指示–D__KERNEL__ -DMODULE–DLINUX 参数。-I 选项跟着Linux 内核源代码中Include 目录的路径。
下列命令将加载 hello 模块:
insmod ./hello.o
下列命令完成相反过程:
rmmod hello
如果要将其直接编译入Linux 内核,则需要将源代码文件拷贝入Linux 内核源代码的相应路径里,并修改Makefile。我们有必要补充一下 Linux 内核编程的一些基本知识:
(1)内存 kmalloc()和kfree()
在 Linux 内核模式下,我们不能使用用户态的malloc()和free()函数申请和释放内存。进行内核编程时,最常用的内存申请和释放函数为在include/linux/kernel.h 文件中声明的kmalloc()和kfree(),其原型为:
void *kmalloc(unsigned int len, int priority);
void kfree(void *__ptr);
kmalloc 的priority 参数通常设置为GFP_KERNEL,如果在中断服务程序里申请内存则要用GFP_ATOMIC 参数,因为使用GFP_KERNEL 参数可能会引起睡眠,不能用于非进程上下文中(在中断中是不允许睡眠的)。
由于内核态和用户态使用不同的内存定义,所以二者之间不能直接访问对方的内存。而应该使用Linux 中的用户和内核态内存交互函数(这些函数在include/asm/uaccess.h 中被声明):
unsigned long copy_from_user(void *to, const void *from, unsigned long n);
unsigned long copy_to_user (void * to, void * from, unsigned long len);
copy_from_user、copy_to_user 函数返回不能被复制的字节数,因此,如果完全复制成功,返回值为0。
include/asm/uaccess.h 中定义的put_user 和get_user 用于内核空间和用户空间的单值交互(如char、int、long)。
这里给出的仅仅是关于内核中内存管理的皮毛,关于 Linux 内存管理的更多细节知识,我们会在本文第9 节《内存与I/O 操作》进行更加深入地介绍。
(2)输出 printk()
在内核编程中,我们不能使用用户态 C 库函数中的printf()函数输出信息,而只能使用printk()。但是,内核中printk()函数的设计目的并不是为了和用户交流,它实际上是内核的一种日志机制,用来记录下日志信息或者给出警告提示。
每个printk 都会有个优先级,内核一共有8 个优先级,它们都有对应的宏定义。如果未指定优先级,内核会选择默认的优先级DEFAULT_MESSAGE_LOGLEVEL。如果优先级数字比int console_loglevel 变量小的话,消息就会打印到控制台上。如果syslogd 和klogd 守护进程在运行的话,则不管是否向控制台输出,消息都会被追加进/var/log/messages 文件。klogd
只处理内核消息,syslogd 处理其他系统消息,比如应用程序。
(3)模块参数
2.4 内核下,include/linux/module.h 中定义的宏MODULE_PARM(var,type) 用于向模块传递命令行参数。var 为接受参数值的变量名, type 为采取如下格式的字符串[min[-max]]b,h,i,l,s。min 及max 用于表示当参数为数组类型时,允许输入的数组元素的个
数范围;b:byte;h:short;i:int;l:long;s:string。在装载内核模块时,用户可以向模块传递一些参数:
insmod modname var=value
如果用户未指定参数,var 将使用模块内定义的缺省值。
3.字符设备驱动程序
Linux 下的设备驱动程序被组织为一组完成不同任务的函数的集合,通过这些函数使得设备操作犹如文件一般。在应用程序看来,硬件设备只是一个设备文件,应用程序可以像操作普通文件一样对硬件设备进行操作,如open ()、close ()、read ()、write () 等。
Linux 主要将设备分为二类:字符设备和块设备。字符设备是指设备发送和接收数据以字符的形式进行;而块设备则以整个数据缓冲区的形式进行。字符设备的驱动相对比较简单。
下面我们来假设一个非常简单的虚拟字符设备:这个设备中只有一个4 个字节的全局变量int global_var,而这个设备的名字叫做“gobalvar”。对“gobalvar”设备的读写等操作即是对其中全局变量global_var 的操作。
驱动程序是内核的一部分,因此我们需要给其添加模块初始化函数,该函数用来完成对所控设备的初始化工作,并调用register_chrdev() 函数注册字符设备:
static int __init gobalvar_init(void)
if (register_chrdev(MAJOR_NUM, " gobalvar ", &gobalvar_fops))
//…注册失败
else
//…注册成功
其中,register_chrdev 函数中的参数,MAJOR_NUM 为主设备号, “gobalvar”为设备名,gobalvar_fops 为包含基本函数入口点的结构体,类型为file_operations。当gobalvar 模块被加载时,gobalvar_init 被执行,它将调用内核函数register_chrdev,把驱动程序的基本入口点指针存放在内核的字符设备地址表中,在用户进程对该设备执行系统调用时提供入口地址。
与模块初始化函数对应的就是模块卸载函数,需要调用 register_chrdev()的“反函数”
unregister_chrdev():
static void __exit gobalvar_exit(void)
if (unregister_chrdev(MAJOR_NUM, " gobalvar "))
//…卸载失败
else
//…卸载成功
随着内核不断增加新的功能,file_operations 结构体已逐渐变得越来越大,但是大多数的驱动程序只是利用了其中的一部分。对于字符设备来说,要提供的主要入口有:open ()、release ()、read ()、write ()、ioctl ()、llseek()、poll()等。
open()函数 对设备特殊文件进行open()系统调用时,将调用驱动程序的open () 函数:
int (*open)(struct inode * ,struct file *);
其中参数inode 为设备特殊文件的inode (索引结点) 结构的指针,参数file 是指向这一设备的文件结构的指针。open()的主要任务是确定硬件处在就绪状态、验证次设备号的合法性(次设备号可以用MINOR(inode-> i - rdev) 取得)、控制使用设备的进程数、根据执行情况返回状态码(0 表示成功,负数表示存在错误) 等;
release()函数 当最后一个打开设备的用户进程执行close ()系统调用时,内核将调用驱动程序的release () 函数:
void (*release) (struct inode * ,struct file *) ;
release 函数的主要任务是清理未结束的输入/输出操作、释放资源、用户自定义排他标志的复位等。
read()函数 当对设备特殊文件进行read() 系统调用时,将调用驱动程序read() 函数:
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
用来从设备中读取数据。当该函数指针被赋为NULL 值时,将导致read 系统调用出错并返回-EINVAL(“Invalid argument,非法参数”)。函数返回非负值表示成功读取的字节数(返回值为“signed size”数据类型,通常就是目标平台上的固有整数类型)。
globalvar_read 函数中内核空间与用户空间的内存交互需要借助第2 节所介绍的函数:
static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)
…
copy_to_user(buf, &global_var, sizeof(int));
…
write( ) 函数 当设备特殊文件进行write () 系统调用时,将调用驱动程序的write () 函数:
ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
向设备发送数据。如果没有这个函数,write 系统调用会向调用程序返回一个-EINVAL。如果返回值非负,则表示成功写入的字节数。globalvar_write 函数中内核空间与用户空间的内存交互需要借助第2 节所介绍的函数:
static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t
*off)
…
copy_from_user(&global_var, buf, sizeof(int));
…
ioctl() 函数 该函数是特殊的控制函数,可以通过它向设备传递控制信息或从设备取得状态信息,函数原型为:
int (*ioctl) (struct inode * ,struct file * ,unsigned int ,unsigned long);
unsigned int 参数为设备驱动程序要执行的命令的代码,由用户自定义,unsigned long参数为相应的命令提供参数,类型可以是整型、指针等。如果设备不提供ioctl 入口点,则对于任何内核未预先定义的请求,ioctl 系统调用将返回错误(-ENOTTY,“No such ioctlfordevice,该设备无此ioctl 命令”)。如果该设备方法返回一个非负值,那么该值会被返回给调用程序以表示调用成功。
llseek()函数该函数用来修改文件的当前读写位置,并将新位置作为(正的)返回值返回,原型为:
loff_t (*llseek) (struct file *, loff_t, int);
poll()函数: poll 方法是poll 和select 这两个系统调用的后端实现,用来查询设备是否可读或可写,或是否处于某种特殊状态,原型为:
unsigned int (*poll) (struct file *, struct poll_table_struct *);
我们将在“设备的阻塞与非阻塞操作”一节对该函数进行更深入的介绍。
设备“gobalvar”的驱动程序的这些函数应分别命名为gobalvar_open、gobalvar_ release、gobalvar_read、gobalvar_write、gobalvar_ioctl,因此设备“gobalvar”的基本入口点结构变量gobalvar_fops 赋值如下:
struct file_operations gobalvar_fops =
read: gobalvar_read,
write: gobalvar_write,
;
上述代码中对gobalvar_fops 的初始化方法并不是标准C 所支持的,属于GNU 扩展语法。
完整的 globalvar.c 文件源代码如下:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
MODULE_LICENSE("GPL");
#define MAJOR_NUM 254 //主设备号
static ssize_t globalvar_read(struct file *, char *, size_t, loff_t*);
static ssize_t globalvar_write(struct file *, const char *, size_t, loff_t*);
//初始化字符设备驱动的file_operations 结构体
struct file_operations globalvar_fops =
read: globalvar_read,
write: globalvar_write,
;
static int global_var = 0; //“globalvar”设备的全局变量
static int __init globalvar_init(void)
int ret;
//注册设备驱动
ret = register_chrdev(MAJOR_NUM, "globalvar", &globalvar_fops);
if (ret)
printk("globalvar register failure");
else
printk("globalvar register success");
return ret;
static void __exit globalvar_exit(void)
int ret;
//注销设备驱动
ret = unregister_chrdev(MAJOR_NUM, "globalvar");
if (ret)
printk("globalvar unregister failure");
else
printk("globalvar unregister success");
static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)
//将global_var 从内核空间复制到用户空间
if (copy_to_user(buf, &global_var, sizeof(int)))
return - EFAULT;
return sizeof(int);
static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t *off)
//将用户空间的数据复制到内核空间的global_var
if (copy_from_user(&global_var, buf, sizeof(int)))
return - EFAULT;
return sizeof(int);
module_init(globalvar_init);
module_exit(globalvar_exit);
运行
gcc –D__KERNEL__ -DMODULE –DLINUX –I /usr/local/src/linux2.4/include -c –o globalvar.o globalvar.c
编译代码,运行
inmod globalvar.o
加载globalvar 模块,再运行
cat /proc/devices
发现其中多出了“254 globalvar”一行,如下图:
接着我们可以运行:
mknod /dev/globalvar c 254 0
创建设备节点,用户进程通过/dev/globalvar 这个路径就可以访问到这个全局变量虚拟设备了。我们写一个用户态的程序globalvartest.c 来验证上述设备:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
main()
int fd, num;
//打开“/dev/globalvar”
fd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR);
if (fd != -1 )
//初次读globalvar
read(fd, &num, sizeof(int));
printf("The globalvar is %d\\n", num);
//写globalvar
printf("Please input the num written to globalvar\\n");
scanf("%d", &num);
write(fd, &num, sizeof(int));
//再次读globalvar
read(fd, &num, sizeof(int));
printf("The globalvar is %d\\n", num);
//关闭“/dev/globalvar”
close(fd);
else
printf("Device open failure\\n");
编译上述文件:
gcc –o globalvartest.o globalvartest.c
运行
./globalvartest.o
可以发现“globalvar”设备可以正确的读写。
4.设备驱动中的并发控制
在驱动程序中,当多个线程同时访问相同的资源时(驱动程序中的全局变量是一种典型的共享资源),可能会引发“竞态”,因此我们必须对共享资源进行并发控制。Linux 内核中解决并发控制的最常用方法是自旋锁与信号量(绝大多数时候作为互斥锁使用)。
自旋锁与信号量“类似而不类”,类似说的是它们功能上的相似性,“不类”指代它们在本质和实现机理上完全不一样,不属于一类。
自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环查看是否该自旋锁的保持者已经释放了锁,“自旋”就是“在原地打转”。而信号量则引起调用者睡眠,它把进程从运行队列上拖出去,除非获得锁。这就是它们的“不类”。
但是,无论是信号量,还是自旋锁,在任何时刻,最多只能有一个保持者,即在任何时刻最多只能有一个执行单元获得锁。这就是它们的“类似”。
鉴于自旋锁与信号量的上述特点,一般而言,自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用;信号量适合于保持时间较长的情况,且只能在进程上下文使用(导致睡眠)。如果被保护的共享资源只在进程上下文访问,则可以以信号量来保护该共享资源,如果对共享资源的访问时间非常短,自旋锁也是好的选择。但是,如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。
与信号量相关的 API 主要有:
// 定义信号量
struct semaphore sem;
// 初始化信号量,该函数初始化信号量,并设置信号量sem 的值为val
void sema_init (struct semaphore *sem, int val);
//该函数用于初始化一个互斥锁,即它把信号量sem 的值设置为1,等同于sema_init (struct semaphore *sem, 1);
void init_MUTEX (struct semaphore *sem);
//该函数也用于初始化一个互斥锁,但它把信号量sem 的值设置为0,等同于sema_init (struct semaphore *sem, 0);
void init_MUTEX_LOCKED (struct semaphore *sem);
//获得信号量,该函数用于获得信号量sem,它会导致睡眠,因此不能在中断上下文使用;
void down(struct semaphore * sem);
//该函数功能与down 类似,不同之处为,down 不能被信号打断,但down_interruptible能被信号打断;
int down_interruptible(struct semaphore * sem);
//该函数尝试获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0,否则,返回非0 值。它不会导致调用者睡眠,可以在中断上下文使用。
int down_trylock(struct semaphore * sem);
//释放信号量,该函数释放信号量sem,唤醒等待者。
void up(struct semaphore * sem);
与自旋锁相关的 API 主要有:
//定义自旋锁
spinlock_t spin;
//初始化自旋锁,该宏用于动态初始化自旋锁lock
spin_lock_init(lock)
//获得自旋锁,该宏用于获得自旋锁lock,如果能够立即获得锁,它就马上返回,否则,它将自旋在那里,直到该自旋锁的保持者释放;
spin_lock(lock)
//该宏尝试获得自旋锁lock,如果能立即获得锁,它获得锁并返回真,否则立即返回假,实际上不再“在原地打转”;
spin_trylock(lock)
//释放自旋锁,该宏释放自旋锁lock,它与spin_trylock 或spin_lock 配对使用;
spin_unlock(lock)
除此之外,还有一组自旋锁使用于中断情况下的 API。
下面进入对并发控制的实战。首先,在 globalvar 的驱动程序中,我们可以通过信号量来控制对int global_var 的并发访问,下面给出源代码:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <asm/semaphore.h>
MODULE_LICENSE("GPL");
#define MAJOR_NUM 254
static ssize_t globalvar_read(struct file *, char *, size_t, loff_t*);
static ssize_t globalvar_write(struct file *, const char *, size_t, loff_t*);
struct file_operations globalvar_fops =
read: globalvar_read,
write: globalvar_write,
;
static int global_var = 0;
static struct semaphore sem;
static int __init globalvar_init(void)
int ret;
ret = register_chrdev(MAJOR_NUM, "globalvar", &globalvar_fops);
if (ret)
printk("globalvar register failure");
else
printk("globalvar register success");
init_MUTEX(&sem);
return ret;
static void __exit globalvar_exit(void)
int ret;
ret = unregister_chrdev(MAJOR_NUM, "globalvar");
if (ret)
printk("globalvar unregister failure");
else
printk("globalvar unregister success");
static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)
//获得信号量
if (down_interruptible(&sem))
return - ERESTARTSYS;
//将global_var 从内核空间复制到用户空间
if (copy_to_user(buf, &global_var, sizeof(int)))
up(&sem);
return - EFAULT;
//释放信号量
up(&sem);
return sizeof(int);
ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t *off)
//获得信号量
if (down_interruptible(&sem))
return - ERESTARTSYS;
//将用户空间的数据复制到内核空间的global_var
if (copy_from_user(&global_var, buf, sizeof(int)))
up(&sem);
return - EFAULT;
//释放信号量
up(&sem);
return sizeof(int);
module_init(globalvar_init);
module_exit(globalvar_exit);
接下来,我们给globalvar 的驱动程序增加open()和release()函数,并在其中借助自旋锁来保护对全局变量int lobalvar_count(记录打开设备的进程数)的访问来实现设备只能被一个进程打开(必须确保globalvar_count 最多只能为1):
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <asm/semaphore.h>
MODULE_LICENSE("GPL");
#define MAJOR_NUM 254
static ssize_t globalvar_read(struct file *, char *, size_t, loff_t*);
static ssize_t globalvar_write(struct file *, const char *, size_t, loff_t*);
static int globalvar_open(struct inode *inode, struct file *filp);
static int globalvar_release(struct inode *inode, struct file *filp);
struct file_operations globalvar_fops =
read: globalvar_read,
write: globalvar_write,
open: globalvar_open,
release: globalvar_release,
;
static int global_var = 0;
static int globalvar_count = 0;
static struct semaphore sem;
static spinlock_t spin = SPIN_LOCK_UNLOCKED;
static int __init globalvar_init(void)
int ret;
ret = register_chrdev(MAJOR_NUM, "globalvar", &globalvar_fops);
if (ret)
printk("globalvar register failure");
else
printk("globalvar register success");
init_MUTEX(&sem);
return ret;
static void __exit globalvar_exit(void)
int ret;
ret = unregister_chrdev(MAJOR_NUM, "globalvar");
if (ret)
printk("globalvar unregister failure");
else
printk("globalvar unregister success");
static int globalvar_open(struct inode *inode, struct file *filp)
//获得自选锁
spin_lock(&spin);
//临界资源访问
if (globalvar_count)
spin_unlock(&spin);
return - EBUSY;
globalvar_count++;
//释放自选锁
spin_unlock(&spin);
return 0;
static int globalvar_release(struct inode *inode, struct file *filp)
globalvar_count--;
return 0;
static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)
if (down_interruptible(&sem))
return - ERESTARTSYS;
if (copy_to_user(buf, &global_var, sizeof(int)))
up(&sem);
return - EFAULT;
up(&sem);
return sizeof(int);
static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t *off)
if (down_interruptible(&sem))
return - ERESTARTSYS;
if (copy_from_user(&global_var, buf, sizeof(int)))
up(&sem);
return - EFAULT;
up(&sem);
return sizeof(int);
module_init(globalvar_init);
module_exit(globalvar_exit);
为了上述驱动程序的效果,我们启动两个进程分别打开/dev/globalvar。在两个终端中调用./globalvartest.o 测试程序,当一个进程打开/dev/globalvar 后,另外一个进程将打开失败,输出“device open failure”。
5.设备的阻塞与非阻塞操作
阻塞操作是指,在执行设备操作时,若不能获得资源,则进程挂起直到满足可操作的条件再进行操作。非阻塞操作的进程在不能进行设备操作时,并不挂起。被挂起的进程进入sleep 状态,被从调度器的运行队列移走,直到等待的条件被满足。
在 Linux 驱动程序中,我们可以使用等待队列(wait queue)来实现阻塞操作。wait queue很早就作为一个基本的功能单位出现在Linux 内核里了,它以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现核心的异步事件通知机制。等待队列可以用来同步对系统资源的访问,上节中所讲述Linux 信号量在内核中也是由等待队列来实现的。
下面我们重新定义设备“globalvar”,它可以被多个进程打开,但是每次只有当一个进程写入了一个数据之后本进程或其它进程才可以读取该数据,否则一直阻塞。
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/wait.h>
#include <asm/semaphore.h>
MODULE_LICENSE("GPL");
#define MAJOR_NUM 254
static ssize_t globalvar_read(struct file *, char *, size_t, loff_t*);
static ssize_t globalvar_write(struct file *, const char *, size_t, loff_t*);
struct file_operations globalvar_fops =
read: globalvar_read,
write: globalvar_write,
;
static int global_var = 0;
static struct semaphore sem;
static wait_queue_head_t outq;
static int flag = 0;
static int __init globalvar_init(void)
int ret;
ret = register_chrdev(MAJOR_NUM, "globalvar", &globalvar_fops);
if (ret)
printk("globalvar register failure");
else
printk("globalvar register success");
init_MUTEX(&sem);
init_waitqueue_head(&outq);
return ret;
static void __exit globalvar_exit(void)
int ret;
ret = unregister_chrdev(MAJOR_NUM, "globalvar");
if (ret)
printk("globalvar unregister failure");
else
printk("globalvar unregister success");
static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)
//等待数据可获得
if (wait_event_interruptible(outq, flag != 0))
return - ERESTARTSYS;
if (down_interruptible(&sem))
return - ERESTARTSYS;
flag = 0;
if (copy_to_user(buf, &global_var, sizeof(int)))
up(&sem);
return - EFAULT;
up(&sem);
return sizeof(int);
static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t *off)
if (down_interruptible(&sem))
return - ERESTARTSYS;
if (copy_from_user(&global_var, buf, sizeof(int)))
up(&sem);
return - EFAULT;
up(&sem);
flag = 1;
//通知数据可获得
wake_up_interruptible(&outq);
return sizeof(int);
module_init(globalvar_init);
module_exit(globalvar_exit);
编写两个用户态的程序来测试,第一个用于阻塞地读/dev/globalvar,另一个用于写/dev/globalvar。只有当后一个对/dev/globalvar 进行了输入之后,前者的read 才能返回。
读的程序为:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
main()
int fd, num;
fd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR);
if (fd != - 1)
while (1)
read(fd, &num, sizeof(int)); //程序将阻塞在此语句,除非有针对globalvar 的输入
printf("The globalvar is %d\\n", num);
//如果输入是0,则退出
if (num == 0)
close(fd);
break;
else
printf("device open failure\\n");
写的程序为:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
main()
int fd, num;
fd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR);
if (fd != - 1)
while (1)
printf("Please input the globalvar:\\n");
scanf("%d", &num);
write(fd, &num, sizeof(int));
//如果输入0,退出
if (num == 0)
close(fd);
break;
else
printf("device open failure\\n");
打开两个终端,分别运行上述两个应用程序,发现当在第二个终端中没有输入数据时,第一个终端没有输出(阻塞),每当我们在第二个终端中给globalvar 输入一个值,第一个终端就会输出这个值:
关于上述例程,我们补充说一点,如果将驱动程序中的 read 函数改为:
static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)
//获取信号量:可能阻塞
if (down_interruptible(&sem))
return - ERESTARTSYS;
//等待数据可获得:可能阻塞
if (wait_event_interruptible(outq, flag != 0))
return - ERESTARTSYS;
flag = 0;
//临界资源访问
if (copy_to_user(buf, &global_var, sizeof(int)))
up(&sem);
return - EFAULT;
//释放信号量
up(&sem);
return sizeof(int);
即交换wait_event_interruptible(outq, flag != 0)和down_interruptible(&sem)的顺序,这个驱动程序将变得不可运行。实际上,当两个可能要阻塞的事件同时出现时,即两个wait_event或down 摆在一起的时候,将变得非常危险,死锁的可能性很大,这个时候我们要特别留意它们的出现顺序。当然,我们应该尽可能地避免这种情况的发生!
还有一个与设备阻塞与非阻塞访问息息相关的论题,即select 和poll,select 和poll 的本质一样,前者在BSD Unix 中引入,后者在System V 中引入。poll 和select 用于查询设备的状态,以便用户程序获知是否能对设备进行非阻塞的访问,它们都需要设备驱动程序中的poll 函数支持。
驱动程序中 poll 函数中最主要用到的一个API 是poll_wait,其原型如下:
void poll_wait(struct file *filp, wait_queue_heat_t *queue, poll_table * wait);
poll_wait 函数所做的工作是把当前进程添加到wait 参数指定的等待列表(poll_table)中。下面我们给globalvar 的驱动添加一个poll 函数:
static unsigned int globalvar_poll(struct file *filp, poll_table *wait)
unsigned int mask = 0;
poll_wait(filp, &outq, wait);
//数据是否可获得?
if (flag != 0)
mask |= POLLIN | POLLRDNORM; //标示数据可获得
return mask;
需要说明的是,poll_wait 函数并不阻塞,程序中poll_wait(filp, &outq, wait)这句话的意思并不是说一直等待outq 信号量可获得,真正的阻塞动作是上层的select/poll 函数中完成的。select/poll 会在一个循环中对每个需要监听的设备调用它们自己的poll 支持函数以使得当前进程被加入各个设备的等待列表。若当前没有任何被监听的设备就绪,则内核进行调度(调用schedule)让出cpu 进入阻塞状态,schedule 返回时将再次循环检测是否有操作可以进行,如此反复;否则,若有任意一个设备就绪,select/poll 都立即返回。
Tip:阻塞/非阻塞/同步/异步和select函数具体介绍参见博文 select与阻塞/非阻塞IO 和 Linux下5种IO模型以及阻塞/非阻塞/同步/异步区别。
我们编写一个用户态应用程序来测试改写后的驱动。程序中要用到BSD Unix 中引入的select 函数,其原型为:
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
其中readfds、writefds、exceptfds 分别是被select()监视的读、写和异常处理的文件描述符集合,numfds 的值是需要检查的号码最高的文件描述符加1。timeout 参数是一个指向struct timeval 类型的指针,它可以使select()在等待timeout 时间后若没有文件描述符准备好则返回。
struct timeval 数据结构为:
struct timeval
int tv_sec; /* seconds */
int tv_usec; /* microseconds */
;
除此之外,我们还将使用下列API:
FD_ZERO(fd_set *set) //清除一个文件描述符集;
FD_SET(int fd,fd_set *set) //将一个文件描述符加入文件描述符集中;
FD_CLR(int fd,fd_set *set) //将一个文件描述符从文件描述符集中清除;
FD_ISSET(int fd,fd_set *set) //判断文件描述符是否被置位。
下面的用户态测试程序等待/dev/globalvar 可读,但是设置了5 秒的等待超时,若超过5秒仍然没有数据可读,则输出“No data within 5 seconds”:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
main()
int fd, num;
fd_set rfds;
struct timeval tv;
fd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR);
if (fd != - 1)
while (1)
//查看globalvar 是否有输入
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
//设置超时时间为5s
tv.tv_sec = 5;
tv.tv_usec = 0;
select(fd + 1, &rfds, NULL, NULL, &tv);
//数据是否可获得?
if (FD_ISSET(fd, &rfds))
read(fd, &num, sizeof(int));
printf("The globalvar is %d\\n", num);
//输入为0,退出
if (num == 0)
close(fd);
break;
else
printf("No data within 5 seconds.\\n");
else
printf("device open failure\\n");
开两个终端,分别运行程序:一个对globalvar 进行写,一个用上述程序对globalvar 进行读。当我们在写终端给globalvar 输入一个值后,读终端立即就能输出该值,当我们连续5秒没有输入时,“No data within 5 seconds”在读终端被输出:
6.设备驱动中的异步通知
结合阻塞与非阻塞访问、poll 函数可以较好地解决设备的读写,但是如果有了异步通知就更方便了。异步通知的意思是:一旦设备就绪,则主动通知应用程序,这样应用程序根本就不需要查询设备状态,这一点非常类似于硬件上“中断”地概念,比较准确的称谓是“信号驱动(SIGIO)的异步I/O”。
我 们 先 来 看 一 个 使 用 信 号 驱 动 的 例 子 , 它 通 过 signal(SIGIO, input_handler) 对STDIN_FILENO 启动信号机制,输入可获得时input_handler 被调用,其源代码如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#define MAX_LEN 100
void input_handler(int num)
char data[MAX_LEN];
int len;
//读取并输出STDIN_FILENO 上的输入
len = read(STDIN_FILENO, &data, MAX_LEN);
data[len] = 0;
printf("input available:%s\\n", data);
main()
int oflags;
//启动信号驱动机制
signal(SIGIO, input_handler);
fcntl(STDIN_FILENO, F_SETOWN, getpid());
oflags = fcntl(STDIN_FILENO, F_GETFL);
fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC);
//最后进入一个死循环,程序什么都不干了,只有信号能激发input_handler 的运行
//如果程序中没有这个死循环,会立即执行完毕
while (1);
为了使设备支持该机制,我们需要在驱动程序中实现 fasync()函数,并在write()函数中当数据被写入时,调用kill_fasync()函数激发一个信号,此部分工作留给读者来完成。
7.设备驱动中的中断处理
与 Linux 设备驱动中中断处理相关的首先是申请与释放IRQ 的API :request_irq()和free_irq();
request_irq()的原型为:
int request_irq(unsigned int irq, void (*handler)(int irq, void *dev_id, struct pt_regs *regs), unsigned long irqflags, const char * devname, void *dev_id);
irq 是要申请的硬件中断号;
handler 是向系统登记的中断处理函数,是一个回调函数,中断发生时,系统调用这个函数,dev_id 参数将被传递;
irqflags 是中断处理的属性,若设置SA_INTERRUPT,标明中断处理程序是快速处理程序,快速处理程序被调用时屏蔽所有中断,慢速处理程序不屏蔽;若设置SA_SHIRQ,则多个设备共享中断,dev_id 在共享中断时会用到,一般设置为这个设备的device 结构本身或者NULL。
free_irq()的原型为:
void free_irq(unsigned int irq,void *dev_id);
另外,与Linux 中断息息相关的一个重要概念是Linux 中断分为两个半部:上半部(tophalf,“登记中断”)和下半部(bottom half,“处理中断”)。上半部的功能是“登记中断”,当一个中断发生时,它进行相应地硬件读写后就把中断例程的下半部挂到该设备的下半部执行队列中去。因此,上半部执行的速度就会很快,可以服务更多的中断请求。但是,仅有“登记中断”是远远不够的,因为中断的事件可能很复杂。因此,Linux 引入了一个下半部,来完成中断事件的绝大多数使命。下半部和上半部最大的不同是下半部是可中断的,而上半部是不可中断的,下半部几乎做了中断处理程序所有的事情,而且可以被新的中断打断!下半部则相对来说并不是非常紧急的,通常还是比较耗时的,因此由系统自行安排运行时机,不在中断服务上下文中执行。
Linux 实现下半部的机制主要有tasklet 和工作队列。
(1)tasklet 基于Linux softirq,其使用相当简单,我们只需要定义tasklet 及其处理函数并将二者关联:
void my_tasklet_func(unsigned long); //定义一个处理函数:
DECLARE_TASKLET(my_tasklet,my_tasklet_func,data); // 定义一个tasklet 结构my_tasklet,与my_tasklet_func(data)函数相关联
然后,在需要调度 tasklet 的时候引用一个简单的API 就能使系统在适当的时候进行调度运行:
tasklet_schedule(&my_tasklet);
此外,Linux 还提供了另外一些其它的控制tasklet 调度与运行的API:
DECLARE_TASKLET_DISABLED(name,function,data); // 与DECLARE_TASKLET 类似,但等待tasklet 被使能
tasklet_enable(struct tasklet_struct *); //使能tasklet
tasklet_disble(struct tasklet_struct *); //禁用tasklet
tasklet_init(struct tasklet_struct *,void (*func)(unsigned long),unsigned long); // 类似DECLARE_TASKLET()
tasklet_kill(struct tasklet_struct *); // 清除指定tasklet 的可调度位,即不允许调度该tasklet
我们先来看一个tasklet 的运行实例,这个实例没有任何实际意义,仅仅为了演示。它的功能是:在globalvar 被写入一次后,就调度一个tasklet,函数中输出“tasklet is executing”:
#include <linux/interrupt.h>
…
//定义与绑定tasklet 函数
void test_tasklet_action(unsigned long t);
DECLARE_TASKLET(test_tasklet, test_tasklet_action, 0);
void test_tasklet_action(unsigned long t)
printk("tasklet is executing\\n");
…
ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t *off)
…
if (copy_from_user(&global_var, buf, sizeof(int)))
return - EFAULT;
//调度tasklet 执行
tasklet_schedule(&test_tasklet);
return sizeof(int);
由于中断与真实的硬件息息相关,脱离硬件而空谈中断是毫无意义的,我们还是来举一个简单的例子。这个例子来源于SAMSUNG S3C2410 嵌入式系统实例,看看其中实时钟的驱动中与中断相关的部分:
static struct fasync_struct *rtc_async_queue;
static int __init rtc_init(void)
misc_register(&rtc_dev);
create_proc_read_entry("driver/rtc", 0, 0, rtc_read_proc, NULL);
#if RTC_IRQ
if (rtc_has_irq == 0)
goto no_irq2;
init_timer(&rtc_irq_timer);
rtc_irq_timer.function = rtc_dropped_irq;
spin_lock_irq(&rtc_lock);
/* Initialize periodic freq. to CMOS reset default, which is 1024Hz */
CMOS_WRITE(((CMOS_READ(RTC_FREQ_SELECT) &0xF0) | 0x06), RTC_FREQ_SELECT);
spin_unlock_irq(&rtc_lock);
rtc_freq = 1024;
no_irq2:
#endif
printk(KERN_INFO "Real Time Clock Driver v" RTC_VERSION "\\n");
return 0;
static void __exit rtc_exit(void)
remove_proc_entry("driver/rtc", NULL);
misc_deregister(&rtc_dev);
release_region(RTC_PORT(0), RTC_IO_EXTENT);
if (rtc_has_irq)
free_irq(RTC_IRQ, NULL);
static void rtc_interrupt(int irq, void *dev_id, struct pt_regs *regs)
/*
* Can be an alarm interrupt, update complete interrupt,
* or a periodic interrupt. We store the status in the
* low byte and the number of interrupts received since
* the last read in the remainder of rtc_irq_data.
*/
spin_lock(&rtc_lock);
rtc_irq_data += 0x100;
rtc_irq_data &= ~0xff;
rtc_irq_data |= (CMOS_READ(RTC_INTR_FLAGS) &0xF0);
if (rtc_status &RTC_TIMER_ON)
mod_timer(&rtc_irq_timer, jiffies + HZ / rtc_freq + 2 * HZ / 100);
spin_unlock(&rtc_lock);
/* Now do the rest of the actions */
wake_up_interruptible(&rtc_wait);
kill_fasync(&rtc_async_queue, SIGIO, POLL_IN);
static int rtc_fasync (int fd, struct file *filp, int on)
return fasync_helper (fd, filp, on, &rtc_async_queue);
static void rtc_dropped_irq(unsigned long data)
unsigned long freq;
spin_lock_irq(&rtc_lock);
/* Just in case someone disabled the timer from behind our back... */
if (rtc_status &RTC_TIMER_ON)
mod_timer(&rtc_irq_timer, jiffies + HZ / rtc_freq + 2 * HZ / 100);
rtc_irq_data += ((rtc_freq / HZ) << 8);
rtc_irq_data &= ~0xff;
rtc_irq_data |= (CMOS_READ(RTC_INTR_FLAGS) &0xF0); /* restart */
freq = rtc_freq;
spin_unlock_irq(&rtc_lock);
printk(KERN_WARNING "rtc: lost some interrupts at %ldHz.\\n", freq);
/* Now we have new data */
wake_up_interruptible(&rtc_wait);
kill_fasync(&rtc_async_queue, SIGIO, POLL_IN);
RTC 中断发生后,激发了一个异步信号,因此本驱动程序提供了对第6 节异步信号的支持。并不是每个中断都需要一个下半部,如果本身要处理的事情并不复杂,可能只有一个上半部,本例中的RTC 驱动就是如此。
8.定时器
Linux 内核中定义了一个timer_list 结构,我们在驱动程序中可以利用之:
struct timer_list
struct list_head list;
unsigned long expires; //定时器到期时间
unsigned long data; //作为参数被传入定时器处理函数
void (*function)(unsigned long);
;
下面是关于timer 的API 函数:
//增加定时器
void add_timer(struct timer_list * timer);
//删除定时器
int del_timer(struct timer_list * timer);
//修改定时器的expire
int mod_timer(struct timer_list *timer, unsigned long expires);
使用定时器的一般流程为:
(1)timer、编写function;
(2)为timer 的expires、data、function 赋值;
(3)调用add_timer 将timer 加入列表;
(4)在定时器到期时,function 被执行;
(5)在程序中涉及timer 控制的地方适当地调用del_timer、mod_timer 删除timer 或修改timer 的expires。
我们可以参考 drivers\\char\\keyboard.c 中键盘的驱动中关于timer 的部分:
…
#include <linux/timer.h>
…
static struct timer_list key_autorepeat_timer =
function: key_callback
;
static void kbd_processkeycode(unsigned char keycode, char up_flag, int autorepeat)
char raw_mode = (kbd->kbdmode == VC_RAW);
if (up_flag)
rep = 0;
if(!test_and_clear_bit(keycode, key_down))
up_flag = kbd_unexpected_up(keycode);
else
rep = test_and_set_bit(keycode, key_down);
/* If the keyboard autorepeated for us, ignore it.
* We do our own autorepeat processing.
*/
if (rep && !autorepeat)
return;
if (kbd_repeatkeycode == keycode || !up_flag || raw_mode)
kbd_repeatkeycode = -1;
del_timer(&key_autorepeat_timer);
…
/*
* Calculate the next time when we have to do some autorepeat
* processing. Note that we do not do autorepeat processing
* while in raw mode but we do do autorepeat processing in
* medium raw mode.
*/
if (!up_flag && !raw_mode)
kbd_repeatkeycode = keycode;
if (vc_kbd_mode(kbd, VC_REPEAT))
if (rep)
key_autorepeat_timer.expires = jiffies + kbd_repeatinterval;
else
key_autorepeat_timer.expires = jiffies + kbd_repeattimeout;
add_timer(&key_autorepeat_timer);
…
9.内存与I/O 操作
对于提供了 MMU(存储管理器,辅助操作系统进行内存管理,提供虚实地址转换等硬件支持)的处理器而言,Linux 提供了复杂的存储管理系统,使得进程所能访问的内存达到4GB。进程的 4GB 内存空间被人为的分为两个部分——用户空间与内核空间。用户空间地址分布从0 到3GB(PAGE_OFFSET,在0x86 中它等于0xC0000000),3GB 到4GB 为内核空间:
内核空间中,从3G 到vmalloc_start 这段地址是物理内存映射区域(该区域中包含了内核镜像、物理页框表mem_map 等等),比如我们使用的VMware 虚拟系统内存是160M,那么3G~3G+160M 这片内存就应该映射物理内存。在物理内存映射区之后,就是vmalloc 区域。对于160M 的系统而言,vmalloc_start 位置应在3G+160M 附近(在物理内存映射区与vmalloc_start 期间还存在一个8M 的gap 来防止跃界),vmalloc_end 的位置接近4G(最后位置系统会保留一片128k 大小的区域用于专用页面映射)。
kmalloc 和get_free_page 申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因此存在较简单的转换关系,virt_to_phys()可以实现内核虚拟地址转化为物理地址:
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
extern inline unsigned long virt_to_phys(volatile void * address)
return __pa(address);
上面转换过程是将虚拟地址减去3G(PAGE_OFFSET=0XC000000)。与之对应的函数为 phys_to_virt(),将内核物理地址转化为虚拟地址:
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
extern inline void * phys_to_virt(unsigned long address)
return __va(address);
virt_to_phys()和phys_to_virt()都定义在include\\asm-i386\\io.h 中。
而 vmalloc 申请的内存则位于vmalloc_start~vmalloc_end 之间,与物理地址没有简单的转换关系,虽然在逻辑上它们也是连续的,但是在物理上它们不要求连续。我们用下面的程序来演示 kmalloc、get_free_page 和vmalloc 的区别:
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
MODULE_LICENSE("GPL");
unsigned char *pagemem;
unsigned char *kmallocmem;
unsigned char *vmallocmem;
int __init mem_module_init(void)
//最好每次内存申请都检查申请是否成功
//下面这段仅仅作为演示的代码没有检查
pagemem = (unsigned char*)get_free_page(0);
printk("<1>pagemem addr=%x", pagemem);
kmallocmem = (unsigned char*)kmalloc(100, 0);
printk("<1>kmallocmem addr=%x", kmallocmem);
vmallocmem = (unsigned char*)vmalloc(1000000);
printk("<1>vmallocmem addr=%x", vmallocmem);
return 0;
void __exit mem_module_exit(void)
free_page(pagemem);
kfree(kmallocmem);
vfree(vmallocmem);
module_init(mem_module_init);
module_exit(mem_module_exit);
我们的系统上有160MB 的内存空间,运行一次上述程序,发现pagemem 的地址在0xc7997000(约3G+121M)、kmallocmem 地址在0xc9bc1380(约3G+155M)、vmallocmem的地址在0xcabeb000(约3G+171M)处,符合前文所述的内存布局。
接下来,我们讨论 Linux 设备驱动究竟怎样访问外设的I/O 端口(寄存器)。
几乎每一种外设都是通过读写设备上的寄存器来进行的,通常包括控制寄存器、状态寄存器和数据寄存器三大类,外设的寄存器通常被连续地编址。根据CPU 体系结构的不同,CPU 对IO 端口的编址方式有两种:
(1)I/O 映射方式(I/O-mapped)
典型地,如 X86 处理器为外设专门实现了一个单独的地址空间,称为“I/O 地址空间”或者“I/O 端口空间”,CPU 通过专门的I/O 指令(如X86 的IN 和OUT 指令)来访问这一空间中的地址单元。
(2)内存映射方式(Memory-mapped)
RISC 指令系统的CPU(如ARM、PowerPC 等)通常只实现一个物理地址空间,外设I/O 端口成为内存的一部分。此时,CPU 可以象访问一个内存单元那样访问外设I/O 端口,而不需要设立专门的外设I/O 指令。
但是,这两者在硬件实现上的差异对于软件来说是完全透明的,驱动程序开发人员可以将内存映射方式的I/O 端口和外设内存统一看作是“I/O 内存”资源。一般来说,在系统运行时,外设的I/O 内存资源的物理地址是已知的,由硬件的设计决定。但是CPU 通常并没有为这些已知的外设I/O 内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O 内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源。Linux 在io.h 头文件中声明了函数ioremap(),用来将I/O 内存资源的物理地址映射到核心虚地址空间(3GB-4GB)中,原型如下:
void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
iounmap 函数用于取消ioremap()所做的映射,原型如下:
void iounmap(v以上是关于深入浅出Linux 设备驱动编程的主要内容,如果未能解决你的问题,请参考以下文章
深入理解Linux RPC - 从Linux RPC到Android Binder