Linux设备驱动程序的作用
简介
设备 驱动程序就像一个个的“黑盒子”,使某个特定硬件响应一个定义良好的内部编程接口,这些操作完全隐藏了设备的工作细节。用户的操作通过一组标准化的调用执行,而这些调用独立于特定的驱动程序。将这些调用映射到作用于实际硬件的设备特有操作上,则是设备驱动程序的任务。
大多数编程可以分为两个部分:
机制:需要提供什么功能 |
策略:如何使用这些功能 |
内核的功能划分:
进程管理,内存管理,文件系统,设备控制,网络功能
字符设备是个能像字节流(类似文件)一样被访问的设备,由字符设备驱动来实现这种特性。字符设备驱动通常至少实现open,close,read,write系统调用。
一个块设备驱动程序主要通过传输固定大小的数据来访问设备。块设备和字符设备的区别仅仅在于内核管理数据的方式,也就是内核及驱动程序之间的软件接口,而这些不同对用户来讲是透明的。在内核中,和字符取得相比,块设备具有完全不同的接口。
任何网络事物都经过一个网络接口形成,即一个能够和其它主机交换数据的设备。它可以 是个硬件设备,但也可能是个纯软件设备。访问网络接口的方法仍然是给它们分配一个唯一的名字(比如eth0),但这个名字在文件系统中不存在对应的节点。 内核和网络设备驱动程序间的通信,完全不同于内核和字符以及块驱动程序之间的通信,内核调用一套和数据包传输相关的函数而不是read、write等。
1.任何从内核中得到的内存,都必须在提供给用户进程或者设备之前清零或者以其它方式初始化,否则就可能发生信息泄露(如数据和密码泄露等)。
2.一个经过恶意修改过的内核可能会允许任何人装载内核,所有,下载内核的地址应该选择正规网站。
设置测试系统
1.在kernel.org的镜像网站上获得一个“主线”内核。
2.准备好一个内核源代码树。
2.6内核的模块要和内核源代码树中的目标文件链接,通过这种方式,构造一个更加健壮的模块装载器。
Hello World模块
#include #inlcude MODULE_LICENSE("Dual BSD/GPL"); static int hello_init(void) { printk(KERN_ALERT "hello,world\n"); return 0; } static void hello_exit(void) { printk(KERN_ALERT "Goodbye,cruel world\n"); } module_init(hello_init); module_exit(hello_exit); |
涉及知识点:
module_init(init_function),module_exit(cleanup_function),
MODULE_LICENSE,
printk,KERN_ALERT等,
insmod,modprobe,rmmod,
makefile,make,
根据系统传递消息行机制的不同,读者得到的输出结果可能不一样。需要特别指出的是, 上面的屏幕输出是在文本控制台上得到的;如果读者在某个与运行在windows系统下的终端仿真器中运行insmod,rmmod,则不会在屏幕上看到任 何输出。实际上,他可能输出到某个系统日志文件中,比如:/var/log/messages
核心模块与应用程序的对比
应用程序 vs 核心模块 vs 事件驱动的应用程序
用户空间 vs 内核空间
cpu在被设计时,有保护系统软件不被应用程序破坏的功能。且这种保护功能分为不同级别,当
cpu中存在多个级别时,unix通常使用最高级和最低级,即:超级用户级和用户级,也即内核空间和用户空间。
内核中的并发
常见引起并发原 因:1.linux系统中通常正在运行多个并发进程,并且可能有多个进程同时使用我们的驱动程序2.大多数设备能够中断处理器,而中断处理程序异步运行, 而且可能在驱动程序正试图处理其他任务时被调用。3.linux可以运行在多处理器上,因此可能同时有多个处理器在使用该进程。
当前进程
Current 在
Current指针指向当前正在运行的进程;
在open,read等系统调用的执行过程中,当前进程指的是调用这些系统调用的进程。
struct task_struct *current;
current->id :当前进程的id
current->comm. :当前进程的命令名
其他细节
1. 如果我们需要大的结构,应该调用动态分配该结构,而不是声明大的自动变量。
2. 常见函数前加有__两个下划线,这种函数通常是接口的底层组件,实际上,双下划线是告诉程序员:谨慎使用,后果自负
3. 内核代码不支持浮点数运算。
编译与装载
编译模块
1. 确保安装了正确版本的编译器,模块工具,和其它必要的工具,内核文档Documentation/Changes文件列出了需要的工具版本。
2. makefile:obj-m:由内核构造系统使用的makefile符号,用来确定当前目录中应构造哪些模块。
如果已经构造了KERNELRELEASE,则说明是从内核构造系统调用的,因此可以利用其内建语句。
ifneq ($(KERNELRELEASE),)
obj-m :=hello.o
否则,是直接从命令行调用的,这时要调用内核构造系统。
else
KERNELDIR ?=/lib/modules/$(shell uname -r)/build\
PWD :=$(shell pwd)
default:
$(MAKE) –C $(KERNELDIR) M=$(PWD) modules
endif
装载和卸载
1. 只有系统调用函数的名字前边带有sys_前缀。
2. modprob区别于insmod :modprob会考虑要装载的模块是否引用了一些当前内核不存在的符号,如果是存在,modprob回查找,而insmod会失败,并在系统日志文件中记录”unresolved symbols”消息。
3. lsmod列出当前装载到内核中的所有模块。
版本依赖
UTS_RELEASE:一个描述内核版本的字符串,例如:2.6.10 LINUX_VERSION_CODE:内核版本的二进制表示,版本中每一部分对应一个字节。如2.6.10对应的LINUX_VERSION_CODE是132618 KERNEL_VERSION(major,minor,release):创建参数版本,这个宏在我们需要将当前版本和一个已知的检查点比较时非常有用。 |
平台依赖
EXPORT_SYMBOL(name) EXPORT_SYMBOL_GPL(name) 这两个宏均用于将给定的符号导出到模块外部。_GPL版本使得要导出的模块只能被GPL许可证下的模块使用。 |
预备知识
MODULE_LICENSE( ),MODULE_AUTHOR( ),MODULE_DESCRIPTION( ),
MODULE_VERSION( ),MODULE_ALIAS( ),MODULE_DEVICE_TABLE( ),
初始化和关闭
__init,__initdata
__devinit,__devinitdata,
大部分的注册函数都带有register_前缀
__exit,__exitdata
初始化函数:
Static int __init initialization_function(void) { /*初始化代码*/ } Module_init(initialization_function); |
清除函数:
Static void __exit cleanup_function(void){ /*清除代码*/ } Module_exit(cleanup_function); |
初始化过程中的错误处理
举例:该段代码注册了三个设备,在出错的时候使用goto语句,它将只撤销出错时刻以前所成功注册的那些设施。
Int __init my_init_function(void) { Int err; /*使用指针和名称注册*/ Err = register_this(ptr1,"skull"); If(err) goto faile_this; Err = register_that(ptr2,"skull"); If(err) goto faile_that; Err = register_those(ptr3,"skull") If(err) goto faile_those; Return 0; /*成功*/
Faile_those:unregister_that(ptr2,"skull"); Faile_that:unregister_this(ptr1,"skull"); Faile_this:return err; /*返回错误*/ } |
模块装载竞争
在支持某个设施的所有内部初始化完成之前,不要注册任何设施。
模块参数
#include Module_param(variable,type,perm); Module_param_array(name,type,num,perm); |
用来创建模块参数的宏,用户可在装载模块时(或者对内建代码引导时)调整这些参数的值,其中的类型可以是:bool,charp,int,invbool,long,short,ushort,uint,ulong,intarray
在用户空间编写驱动程序
源代码:
#include #inlcude MODULE_LICENSE("Dual BSD/GPL");
static int hello_init(void) {
}
static void hello_exit(void) {
}
module_init(hello_init); module_exit(hello_exit); |
makefile:
ifneq ($(KERNELRELEASE),) obj-m :=hello.o else KERNELDIR ?=/lib/modules/$(shell uname -r)/build\ PWD :=$(shell pwd) default: $(MAKE) –C $(KERNELDIR) M=$(PWD) modules endif |
编译模块
#make
清除
#make clean
-----------
为了能够在终端显示信息,要修改
/lib/modules/2.6.10/build/include/linux/kernel.h
文 件的KERN_ALERT宏。
#define KERN_ALERT "<1>"
修改为
#define KERN_ALERT "<0>"
实际操作并未成功,不知道原因在哪
------------
安装模块
#insmod hello.ko
终端 显示
hello module init
查看已安装的模块
#lsmod
卸载模块
#rmmod hello
终 端显示
hello module exit
-----------
有以下几点要注意:
1,hello.c文件中调用的头 文件
init.h中的module_init(),module_exit()
kernel.h中的 printk(),KERN_ALERT
module.h中的MODULE_LICENSE()
2,Makefile文件中的核心是
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
1),-C $(KERNELDIR)
表示 在$(KERNELDIR)目录下执行make命令。
2),M=$(PWD)
表示包含$(PWD)下的Makefile文件。
3),modules
表 示模块编译。
4), 用到了ifneq...else...endif语句
由于开始还没定义KERNELRELEASE,所以只能执行 else分支。
而在执行
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
后,会在内核的 Makefile中定义KERNELRELEASE,当进入本Makefile时,
则只会执行ifneq的第一个分支,即
obj-m := hello.o
这一句话是非常重要的。事实上,这个Makefile做的本份工作就是它。
我们也可以用命令行的方式来编译:
在 Makefile中的内容写为:
obj-m := hello.o
然后在终端敲入:
#make -C /lib/modules/2.6.10/build M=/home/tmp modules
主设备号,次设备号
主设备号表示设备对应的驱动程序;次设备号由内核使用,用于正确确定设备文件所指的设备。
内核用dev_t类型(
在实际使用中,是通过
(dev_t)-->主设备号、次设备号 | MAJOR(dev_t dev) MINOR(dev_t dev) |
主设备号、次设备号-->(dev_t) | MKDEV(int major,int minor) |
分配和释放设备编号
#include
静态指定 | int register_chrdev_region(dev_t first,unsigned int count,char *name); |
动态分配 | int alloc_chrdev_region(dev *dev,unsigned int firstminor,unsigned int count,char *name); |
释放设备号 | void unregister_chrdev_region(dev_t first,unsigned int count); |
分配之设备号的最佳方式是:默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的余地。
以下是在scull.c中用来获取主设备好的代码:
if(scull_major) {
}else {
} if(result=0) {
} |
在这部分中,比较重要的是在用函数获取设备编号后,其中的参数name是和该编号范围关联的设备名称,它将出现在/proc/devices和sysfs中。
看到这里,就可以理解为什么mdev和udev可以动态、自动地生成当前系统需要的设备文件。udev就是通过读取sysfs下的信息来识别硬件设备的.
一些重要的数据结构
file_operations,file,inode
大多数设备驱动程序都会用到的三个重要数据结构。file_operations结构保存了字符驱动程序的方法,struct file表示一个打开的文件,而struct inode表示一个磁盘上的文件。
字符设备的注册
1.获取一个cdev设备
struct cdev *my_dev = cdev_alloc();
my_cdev->ops = &my_fops;
2.初始化分配到的结构
void cdev_init(struct cdev *cdev,struct file_operation *fops);
另外还有一个struct cdev的字段需要初始化。和file_operations结构类似,struct cdev也有一个所有者字段,应被设置为THIS_MODULE。
cdev.owner = THIS_MODULE
3.cdev设置好之后,最后的步骤是通过下面的调用告诉内核该结构的信息:
int cdev_add(struct cdev *dev,dev_t num,unsigend int count);
dev是cdev结构,num是该设备对应的第一个设备 编号,count是应该和设备关联的设备编号的数量。
4.移除一个字符设备
void cdev_del(struct cdev *dev);
以下为scull中的设备注册
struct scull_dev{ struct scull_qset *data; int quantum; int qset; unsigned long size; unsigend int access_key; struct semaphore sem; struct cdev cdev; } |
static void scull_setup_cdev(struct scull_dev *dev,int index) { int err,devno=MKDEV(scull_major,scull_minor+index); cdev_init(&dev->cdev,&scull_fops); dev->cdev.owner = THIS_MODULE; dev->cdev.ops = &scull_fops; err = cdev_add(&dev->cev,devno,1); /*faile gracefully if need be*/ if(err) printk(KERN_NOTICE "error %d adding scull %d",err,index); } |
open方法提供给驱动程序以初始化的能力,从而为以后的操作完成初始化做准备。在大部分驱动程序中,open应完成以下工作:
1.检查设备特定的错误(诸如设备未就绪,或类似的硬件问题)
2.如果设备是首次打开,则对其进行初始化
3.如有必要,更新f_op指针
4.分配并填写置于filp->private_data里的数据结构
open方法原型:int (*open)(struct inode *inode,struct file *filp)
其中的inode参数在其i_cdev字段中包含了我们所需要的信息,即我们先前设置的cdev结构。唯一的问题是,我们通常不需要cdev结构本身,而是希望得到包含cdev结构的scull_dev结构。c语言可帮助程序员通过一些技巧完成这类转换,但不应滥用这类技巧。幸运的是,在这种情况下我们已经实现了这类技巧,它通过定义在
container_of(pointer,container_type,container_field);
这个宏要一个container_field字段的指针,该字段包含在container_type类型的结构中,然后返回包含该字段的结构指针。
在scull_open中,这个宏用来找到适当的设备:
struct scull_dev *dev; dev = container_of(inode->i_cdev,struct scull_dev,cdev); filp->private_data = dev; /*for other methods*/ |
而根据scull的实际情况,他的open函数只要完成第四步(将初始化过的struct scull_dev dev的指针传递到filp->private_data里,以备后用)就好了,所以open函数很简单。但是其中用到了定义在
|
其实从源码可以看出,其作用就是:通过指针ptr,获得包含ptr所指向数据(是member结构体)的type结构体的指针。即是用指针得到另外一个指针。
release方法提供释放内存,关闭设备的功能。应完成的工作如下:
(1)释放由open分配的、保存在file->private_data中的所有内容;
(2)在最后一次关闭操作时关闭设备。
由于前面定义了scull是一个全局且持久的内存区,所以他的release什么都不做。
read和write
read和write方法的主要作用就是实现内核与用户空间之间的数据拷贝。因为Linux的内核空间和用户空间隔离的,所以要实现数据拷贝就必须使用在
|
而值得一提的是以上两个函数和
|
之间的关系:通过源码可知,前者调用后者,但前者在调用前对用户空间指针进行了检查。
至于read和write 的具体函数比较简单,就在实验中验证好了。
|
|
|
|
|
|
|
|
int test_and_set_bit(nr,void*addr); |
|
|
这个类型的锁常常用在保护某种简单计算,读存取通过在进入临界区入口获取一个(无符号的)整数序列来工作. 在退出时, 那个序列值与当前值比较; 如果不匹配, 读存取必须重试.读者代码形式:
|
如果你的 seqlock 可能从一个中断处理里存取, 你应当使用 IRQ 安全的版本来代替:
|
写者必须获取一个排他锁来进入由一个 seqlock 保护的临界区,写锁由一个自旋锁实现, 调用:
|
因为自旋锁用来控制写存取, 所有通常的变体都可用:
|
(5)读取-复制-更新
读取-拷贝-更新(RCU) 是一个高级的互斥方法, 在合适的情况下能够有高效率. 它在驱动中的使用很少。
ioctl
驱动程序可以使用ioctl执行硬件控制。
两种原型:
1.在用户空间
int ioctl(int fd,unsigned long cmd,...);
fd:文件描述符
cmd:控制命令
,,,:可选参数:插入*argp,具体内容依赖于cmd
2.驱动程序
int (*ioctl) (struct inode *inode,struct file *filp,unsigned int cmd,unsigned long arg);
inode与filp两个指针对应于应用程序传递的文件描述符fd,这和传递open方法的参数一样。
cmd 由用户空间直接不经修改的传递给驱动程序
arg 可选
cmd:
四个位字段:type,number,direction,size
type:幻数,8位
number:序数,8位
direction:涉及内容包括_IOC_NONE(无数据传输),_IOC_READ(从设备中读),_IOC_WRITE,_IOC_READ|_IOC_WRITE(双向数据传输)
size:表示所涉及的用户数据大小,通常为13位或是14位,具体可通过宏_IOC_SIZEBITS找到针对特定体系结构的具体数值。内核不会检查这个位字段,对该字段的检查可以帮助我们检测用户空间的错误。
另外,
_IOR(type,nr,datetype) 构造从驱动程序中读取数据的命令
_IO(type,nr) 用于构造无参数的命令编号
_IOW(type,nr,datetype) 用于写入命令的编号
_IOWR(type,nr,datatype) 双向传输
type,number通过参数传入,size通过对datatype参数取sizeof获取
还有一些解开位字段的宏:_IOC_DIR(nr),_IOC_TYPE(nr),_IOC_NR(nr),_IOC_SIZE(nr)
对非法的ioctl命令一般会返回-EINVAL
预定义命令
在使用ioctl命令编号时,一定要避免与预定义命令重复,否则,命令冲突,设备不会响应
下列ioctl命令对任何文件(包括设备特定文件)都是预定义的:
FIOCTLX 设置执行时关闭标志
FIONCLEX 清除执行时关闭标志
FIOASYNC 设置或复位文件异步通知
FIOQSIZE 返回文件或目录大小
FIONBIO 文件非阻塞型IO,file ioctl non-blocking i/o
如何使用ioctl的附加参数:arg
1:arg是个整数,那简单,直接用
2:arg是个指针,麻烦点,需检测后才能用
分析:使用指针,首先得保证指针指向的地址合法。因此,在使用这个指针之前,我们应该使用
来验证地址的合法性。
type: VERIFY_READ 或是 VERIFY_WRITE,取决于是读取还是写入用户空间内存区。
addr: 一个用户空间的地址
size: 如果要读取或写入一个int型数据,则为sizeof(int)
如果在该地址处既要读取,又要写入,则应该用:VERIFY_WRITE,因为它是VERIFY_READ的超集
注意:首先, access_ok不做校验内存存取的完整工作; 它只检查内存引用是否在这个进程有合理权限的内存范围中,且确保这个地址不指向内核空间内存。其次,大部分驱动代码不需要真正调用 access_ok,而直接使用put_user(datum, ptr)和get_user(local, ptr),它们带有校验的功能,确保进程能够写入给定的内存地址,成功时返回 0, 并且在错误时返回 -EFAULT.。
使用举例:
int err=0,tmp; int retval; /*抽取类型和编号位字段,并拒绝错误的命令号:在调用access_ok之前返回ENOTTY(不恰当的ioctl)*/ if(_IOC_TYPE(cmd)!=SCULL_IOC_MAGIC) return -ENOTTY; if(_IOC_NR(cmd)>SCULL_IOC_MAXNR) return -ENOTTY;
/*方向是一个位掩码,而VERIFY_WRITE用于R/W*传输。“类型”是针对用户空间而言,而access_ok面向内核,因此,读取和写入,恰好相反*/ if(_IOC_DIR(cmd) & _IOC_READ) err=!access_ok(VERIFY_WRITE,(void __user *)arg,_IOC_SIZE(cmd)); else if(_IOC_DIR(cmd) & _IOC_WRITE) err=!access_ok(VERIFY_READ,(void __user *)arg,_IOC_SIZE(cmd)); if(err) return -EFAULT; |
put_user(datum,ptr);
__put_user(datum,ptr);
使用时,速度快,不做类型检查,使用时可以给ptr传递任意类型的指针参数,只要是个用户空间的地址就行,传递的数据大小依赖于ptr参数的类型。
put_user vs __put_user:使用前做的检查,put_user多些,__put_user少些,
一般用法:实现一个读取方法时,可以调用__put_user来节省几个时钟周期,或者在复制多项数据之前调用一次access_ok,像上面代码一样。
get_user(datum.ptr);
__get_user(datum,ptr);
接收的数据被保存在局部变量local中,返回值说明其是否正确。同样,__get_user应该在操作地址被access_ok后使用。
权能与受限操作
来由:驱动程序必须进行附加的检查以确认用户是否有权进行请求的操作
权能作用:基于权能的系统抛弃了那种要么全有,要么全无的特权分配方式,而是把特权操作划分成了独立的组。
int capable(int capability);
在执行一项特权之前,应先检查其是否具有这个权利
if (! capable (CAP_SYS_ADMIN))
return -EPERM;
|
switch(cmd) {
case SCULL_IOCRESET:
scull_quantum = SCULL_QUANTUM;
scull_qset = SCULL_QSET;
break;
case SCULL_IOCSQUANTUM: /* Set: arg points to the value */
if (! capable (CAP_SYS_ADMIN)) //capable
return -EPERM;
retval = __get_user(scull_quantum, (int __user *)arg);//_get_userbreak;
case SCULL_IOCTQUANTUM: /* Tell: arg is the value */
if (! capable (CAP_SYS_ADMIN))
return -EPERM;
scull_quantum = arg;
break;case SCULL_IOCGQUANTUM: /* Get: arg is pointer to result */
retval = __put_user(scull_quantum, (int __user *)arg);
break;case SCULL_IOCQQUANTUM: /* Query: return it (it's positive) */
return scull_quantum;case SCULL_IOCXQUANTUM: /* eXchange: use arg as pointer */
if (! capable (CAP_SYS_ADMIN))
return -EPERM;
tmp = scull_quantum;
retval = __get_user(scull_quantum, (int __user *)arg);
if (retval == 0)
retval = __put_user(tmp, (int __user *)arg);
break;case SCULL_IOCHQUANTUM: /* sHift: like Tell + Query */
if (! capable (CAP_SYS_ADMIN))
return -EPERM;
tmp = scull_quantum;
scull_quantum = arg;
return tmp;
case SCULL_IOCSQSET:
if (! capable (CAP_SYS_ADMIN))
return -EPERM;
retval = __get_user(scull_qset, (int __user *)arg);
break;case SCULL_IOCTQSET:
if (! capable (CAP_SYS_ADMIN))
return -EPERM;
scull_qset = arg;
break;case SCULL_IOCGQSET:
retval = __put_user(scull_qset, (int __user *)arg);
break;case SCULL_IOCQQSET:
return scull_qset;case SCULL_IOCXQSET:
if (! capable (CAP_SYS_ADMIN))
return -EPERM;
tmp = scull_qset;
retval = __get_user(scull_qset, (int __user *)arg);
if (retval == 0)
retval = put_user(tmp, (int __user *)arg);
break;case SCULL_IOCHQSET:
if (! capable (CAP_SYS_ADMIN))
return -EPERM;
tmp = scull_qset;
scull_qset = arg;
return tmp;
/*
* The following two change the buffer size for scullpipe.
* The scullpipe device uses this same ioctl method, just to
* write less code. Actually, it's the same driver, isn't it?
*/
}case SCULL_P_IOCTSIZE:
scull_p_buffer = arg;
break;case SCULL_P_IOCQSIZE:
return scull_p_buffer;
default: /* redundant(???), as cmd was checked against MAXNR */
return -ENOTTY;
}
return retval;
提出问题:若驱动程序无法立即满足请求,该如何响应? 比如:当数据不可用时调用read,或是在缓冲区已满时,调用write
解决问题:驱动程序应该(默认)该阻塞进程,将其置入休眠状态直到请求可继续。
休眠:
当一个进程被置入休眠时,它会被标记为一种特殊状态并从调度器运行队列中移走,直到某些情况下修改了这个状态,才能运行该进程。
安全进入休眠两原则:
1.永远不要在原子上下文中进入休眠。(原子上下文:在执行多个步骤时,不能有任何的并发访问。这意味着,驱动程序不能再拥有自旋锁,seqlock,或是RCU锁时,休眠)
2.对唤醒之后的状态不能做任何假定,因此必须检查以确保我们等待的条件真正为真
临界区 vs 原子上下文
原子上下本:一般说来,具体指在中断,软中断,或是拥有自旋锁的时候。
临界区:每次只允许一个进程进入临界区,进入后不允许其它进程访问。
other question:
要休眠进程,必须有一个前提:有人能唤醒进程,而起这个人必须知道在哪儿能唤醒进程,这里,就引入了“等待队列”这个概念。
等待队列:就是一个进程链表(我的理解:是一个休眠进程链表),其中包含了等待某个特定事件的所有进程。
等待队列头:wait_queue_head_t,定义在
定义方法:静态 DECLARE_QUEUE_HEAD(name)
动态 wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
简单休眠
linux最简单的休眠方式是wait_event(queue,condition)及其变种,在实现休眠的同时,它也检查进程等待的条件。四种wait_event形式如下:
wait_event(queue,condition);/*不可中断休眠,不推荐*/
wait_event_interruptible(queue,condition);/*推荐,返回非零值意味着休眠被中断,且驱动应返回-ERESTARTSYS*/
wait_event_timeout(queue,condition,timeout);
wait_event_interruptible_timeout(queue,conditon,timeout);/*有限的时间的休眠,若超时,则不管条件为何值返回0*/
唤醒休眠进程的函数:wake_up
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head *queue);
惯例:用wake_up唤醒wait_event,用wake_up_interruptible唤醒wait_event_interruptible |
休眠与唤醒 实例分析:
本例实现效果为:任何从该设备上读取的进程均被置于休眠。只要某个进程向给设备写入,所有休眠的进程就会被唤醒。
static DECLARE_WAIT_QUEUE_HEAD(wq); static int flag =0; ssize_t sleepy_read(struct file *filp,char __user *buf,size_t count,loff_t *pos) {
}
ssize_t sleepy_write(struct file *filp,const char __user *buf,size_t count,loff_t *pos) {
} |
阻塞与非阻塞类操作
小知识点:
操作系统中睡眠、阻塞、挂起的区别形象解释 | |||
|
全功能的 read 和 write 方法涉及到进程可以决定是进行非阻塞 I/O还是阻塞 I/O操作。明确的非阻塞 I/O 由 filp->f_flags 中的 O_NONBLOCK 标志来指示(定义再
其实不一定只有read 和 write 方法有阻塞操作,open也可以有阻塞操作。
1.如果指定了O_NONBLOCK标志,read和write的行为就会有所不同。如果在数据没有就绪时调用read或是在缓冲区没有空间时调用write,则该调用简单的返回-EAGAIN。
2.非阻塞型操作会立即返回,使得应用程序可以查询数据。在处理非阻塞型文件时,应用程序调用stdio函数必须非常小心,因为很容易就把一个非阻塞返回误认为EOF,所以必须始终检查errno。
3.有些驱动程序还为O_NONBLOCK实现了特殊的语义。例如,在磁带还没有插入时打开一个磁带设备通常会阻塞,如果磁带驱动程序使用O_NONBLOCK打开的,则不管磁带在不在,open都会立即成功返回。
4.只有read,write,open文件操作受非阻塞标志的影响。
read负责管理阻塞型和非阻塞型输入,如下所示:
static ssize_t scull_p_read (struct file *filp,char __user *buf,size_t count,loff_t *f_pos) {
} |
代码分析:
while循环在拥有设备信号量时测试缓冲区。如果其中有数据,则可以立即将数据返回给用户而不需要休眠,这样,整个循环体就被跳 过了。相反,如果缓冲区为空,则必须休眠。但在休眠之前必须释放设备信号量,因为如果在拥有该信号量时休眠,任何写入者都没有机会来唤醒。在释放信号量之 后,快速检测用户请求的是否是非阻塞I/O,如果是,则返回,否则调用wait_event_interruptible。
高级休眠:
进程休眠步骤:
1.分配并初始化一个wait_queue_t结构,然后将其加入到对应的等待队列
2.设置进程的状态,将其标记为休眠在
2.6 内核的驱动代码通常不需要直接操作进程状态。但如果需要这样做使用的代码是:
void set_current_state(int new_state); |
在老的代码中, 你常常见到如此的东西:current->state = TASK_INTERRUPTIBLE; 但是象这样直接改变 current 是不推荐的,当数据结构改变时这样的代码将会失效。通过改变 current 状态,只改变了调度器对待进程的方式,但进程还未让出处理器。
3.最后一步:释放处理器。但之前我们必须首先检查休眠等待的条件。如果不做这个检查,可能会引入竞态:如果在忙于上面的这个过程时有其他的线程刚刚试图唤醒你,你可能错过唤醒且长时间休眠。因此典型的代码下:
|
如果代码只是从 schedule 返回,则进程处于TASK_RUNNING 状态。 如果不需睡眠而跳过对 schedule 的调用,必须将任务状态重置为 TASK_RUNNING,还必要从等待队列中去除这个进程,否则它可能被多次唤醒
手工休眠:
1.建立并初始化一个等待队列入口。
方法1: DEFINE_WAIT(my_wait); 方法2: wait_queue_t my_wait; init_wait(&my_wait); |
2.将我们的等待队列入口添加到队列中,并设置进程的状态。
void prepare_to_wait(wait_queue_head_t *queue, wait_queue_t *wait, int state); queue和wait分别是等待队列头和进城入口,state是进程的新状态,它应该是TASK_INTERRUPTIBLE(可中断休眠)或者TASK_UNINTERRUPTIBLE(不可中断休眠) |
3.在调用prepaer_to_wait之后,进程即可调用schedule,当然在这之前,应确保仍有必有等待。一旦schedule返回,就到了清理时间了。
void finesh_wait(wait_queue_head_t *queue,wait_queue_t *wait); |
独占等待
当一个进程调用 wake_up 在等待队列上,所有的在这个队列上等待的进程被置为可运行的。 这在许多情况下是正确的做法。但有时,可能只有一个被唤醒的进程将成功获得需要的资源,而其余的将再次休眠。这时如果等待队列中的进程数目大,这可能严重 降低系统性能。为此,内核开发者增加了一个“独占等待”选项。它与一个正常的睡眠有 2 个重要的不同:
1.当等待队列入口设置了 WQ_FLAG_EXCLUSEVE 标志,它被添加到等待队列的尾部;否则,添加到头部。
2.当 wake_up 被在一个等待队列上调用, 它在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止唤醒.但内核仍然每次唤醒所有的非独占等待。
采用独占等待要满足 2 个条件:
(1)希望对资源进行有效竞争;
(2)当资源可用时,唤醒一个进程就足够来完全消耗资源。
使一个进程进入独占等待,可调用:
|
注意:无法使用 wait_event 和它的变体来进行独占等待.
唤醒的相关函数
很少会需要调用wake_up_interruptible 之外的唤醒函数,但为完整起见,这里是整个集合:
wake_up_nr(wait_queue_head_t *queue, int nr); wake_up_interruptible_nr(wait_queue_head_t *queue, int nr); /*这些函数类似 wake_up, 除了它们能够唤醒多达 nr 个独占等待者, 而不只是一个. 注意传递 0 被解释为请求所有的互斥等待者都被唤醒*/
wake_up_all(wait_queue_head_t*queue);
wake_up_interruptible_sync(wait_queue_head_t *queue); /* 一个被唤醒的进程可能抢占当前进程, 并且在 wake_up 返回之前被调度到处理器。 但是, 如果你需要不要被调度出处理器时,可以使用 wake_up_interruptible 的"同步"变体. 这个函数最常用在调用者首先要完成剩下的少量工作,且不希望被调度出处理器时。*/
|
poll and select
当应用程序需要进行对多文件读写时,若某个文件没有准备好,则系统会 处于读写阻塞的状态下,并影响了其它文件的读写。为了避免这种情况的发生,则必须使用多输入输出流又不想阻塞在他们任何一个上的应用程序常将非阻塞的 I/O和poll(system V),select(BSD Unix),epoll(linux2.5.45开始)系统调用配合使用。当poll函数返回时,会给出一个文件是否可读写的标志,应用程序根据不同的标 识读写相应的文件,实现非阻塞的读写。这些系统调用功能相同:允许进程来决定它是否可读或写一个或多个文件而不阻塞。这些调用也可阻塞进程直到任何一个给 定集合的文件描述符可用来读或写。这些调用都需要来自设备驱动中的poll方法的支持。poll返回不同的标识,告诉主进程文件是否可以读写,其原型:
unsigned int (*poll) (struct file *filp,poll_table *wait); |
实现这个设备方法分两步:
1.在一个或多个可指示查询状态变化的等待队列上调用poll_wait,如果没有文件描述符可用来执行I/O,内核使这个进程在等待队列上等待所有的传递给系统调用的文件描述符,驱动通过调用函数poll_wait增加一个队列到poll_wait结构,原型:
void poll_wait(struct file *,wait_queue_head_t *,poll_table*); |
2.返回一个位掩码:描述可能不必阻塞就立刻进行的操作,几个标志(通过
标志 |
含义 |
POLLIN |
如果设备无阻塞的读,就返回该值 |
POLLRDNORM |
通常的数据已经准备好,可以读了,就返回该值。通常的做法是会返回(POLLLIN|POLLRDNORA) |
POLLRDBAND |
如果可以从设备读出带外数据,就返回该值,它只可在linux内核的某些网络代码中使用,通常不用在设备驱动程序中 |
POLLPRI |
如果可以无阻塞的读取高优先级(带外)数据,就返回该值,返回该值会导致select报告文件发生异常,以为select八带外数据当作异常处理 |
POLLHUP |
当读设备的进程到达文件尾时,驱动程序必须返回该值,依照select的功能描述,调用select的进程被告知进程时可读的。 |
POLLERR |
如果设备发生错误,就返回该值。 |
POLLOUT |
如果设备可以无阻塞地些,就返回该值 |
POLLWRNORM |
设备已经准备好,可以写了,就返回该值。通常地做法是(POLLOUT|POLLNORM) |
POLLWRBAND |
于POLLRDBAND类似 |
使用举例:
|
与read与write的交互
正确实现poll调用的规则:
从设备读取数据:
(1)如果在输入缓冲中有数据,read 调用应当立刻返回,即便数据少于应用程序要求的,并确保其他的数据会很快到达。 如果方便,可一直返回小于请求的数据,但至少返回一个字节。在这个情况下,poll 应当返回 POLLIN|POLLRDNORM。
(2)如果在输入缓冲中无数据,read默认必须阻塞直到有一个字节。若O_NONBLOCK 被置位,read 立刻返回 -EAGIN 。在这个情况下,poll 必须报告这个设备是不可读(清零POLLIN|POLLRDNORM)的直到至少一个字节到达。
(3)若处于文件尾,不管是否阻塞,read 应当立刻返回0,且poll 应该返回POLLHUP。
向设备写数据
(1)若输出缓冲有空间,write 应立即返回。它可接受小于调用所请求的数据,但至少必须接受一个字节。在这个情况下,poll应返回 POLLOUT|POLLWRNORM。
(2) 若输出缓冲是满的,write默认阻塞直到一些空间被释放。若 O_NOBLOCK 被设置,write 立刻返回一个 -EAGAIN。在这些情况下, poll 应当报告文件是不可写的(清零POLLOUT|POLLWRNORM). 若设备不能接受任何多余数据, 不管是否设置了 O_NONBLOCK,write 应返回 -ENOSPC("设备上没有空间")。
(3)永远不要让write在返回前等待数据的传输结束,即使O_NONBLOCK 被清除。若程序想保证它加入到输出缓冲中的数据被真正传送, 驱动必须提供一个 fsync 方法。
永远不要让write调用在返回 前等待数据传输的结束,即使O_NONBLOCK标志被清除。这是因为很多应用程序使用select来检查write是否会阻塞。如果报告设备可以写入, 调用就不能被阻塞。如果使用设备的程序需要保证输出缓冲区中的数据确实已经被传送出去,驱动程序就必须提供一个fsync方法。
刷新待处理输出
若一些应用程序需要确保数据被发送到设备,就必须实现fsync 方法。对 fsync 的调用只在设备被完全刷新时(即输出缓冲为空)才返回,不管 O_NONBLOCK 是否被设置,即便这需要一些时间。其原型是:
|
底层的数据结构
|
异步通知
通过使用异步通知,应用程序可以在数据可用时收到一个信号,而无需不停地轮询。
启用步骤:
(1)它们指定一个进程作为文件的拥有者:使用 fcntl 系统调用发出 F_SETOWN 命令,这个拥有者进程的 ID 被保存在 filp->f_owner。目的:让内核知道信号到达时该通知哪个进程。
(2)使用 fcntl 系统调用,通过 F_SETFL 命令设置 FASYNC 标志。
内核操作过程
1.F_SETOWN被调用时filp->f_owner被赋值。
2. 当 F_SETFL 被执行来打开 FASYNC, 驱动的 fasync 方法被调用.这个标志在文件被打开时缺省地被清除。
3. 当数据到达时,所有的注册异步通知的进程都会被发送一个 SIGIO 信号。
Linux 提供的通用方法是基于一个数据结构和两个函数,定义在
数据结构:
|
驱动调用的两个函数的原型:
|
当一个打开的文件的FASYNC标志被修改时,调用 fasync_helper 来从相关的进程列表中添加或去除文件。除了最后一个参数, 其他所有参数都时被提供给 fasync 方法的相同参数并被直接传递。 当数据到达时,kill_fasync 被用来通知相关的进程,它的参数是被传递的信号(常常是 SIGIO)和 band(几乎都是 POLL_IN)。
这是 scullpipe 实现 fasync 方法的:
|
当数据到达, 下面的语句必须被执行来通知异步读者. 因为对 sucllpipe 读者的新数据通过一个发出 write 的进程被产生, 这个语句出现在 scullpipe 的 write 方法中:
|
当文件被关闭时必须 调用fasync 方法,来从活动的异步读取进程列表中删除该文件。尽管这个调用仅当 filp->f_flags 被设置为 FASYNC 时才需要,但不管什么情况,调用这个函数不会有问题,并且是普遍的实现方法。 以下是 scullpipe 的 release 方法的一部分:
|
异步通知使用的数据结构和 struct wait_queue 几乎相同,因为他们都涉及等待事件。区别异步通知用 struct file 替代 struct task_struct. 队列中的 file 用获取 f_owner, 一边给进程发送信号。
度量时间差
概念:
时钟中断:由系统定时硬件以周期性的间隔产生
hz:上述间隔由hz的值设定,hz是一个与体系结构相关的常数
计数器:发生中断一次,计数器加一,这个计数器的值(只有)在系统引导时被初始化为0
jiffies变量:unsigned long 型变量,要么与jiffies_64相同,要么取其低32位
使用jiffies计数器
包含在
jiffies与jiffies_64均应被看做只读变量
jiffies变量应被声明为volatile
使用举例:
#include unsigned long j,stamp_1,stamp_half,stamp_n; j=jiffies; //read the current value stamp_1=j+HZ; //1second in the future stamp_half=j+HZ/2; //0.5second in the future stamp_n=j+n*HZ/1000; // n milliseconds |
比较缓存值(例如上述的stamp_1)与当前值:
#include int time_after(unsigned long a,unsigned long b); int time_before(unsigned long a,unsigned long b); int time_after_eq(unsigned long a,unsigned long b); int time _before_eq(unsigned long a,unsigned long b); |
上述几个宏会将计数器值转换为signed long,相减,然后比较结果。如果需要以安全的方式计算两个jiffies实例之间的差,如下:
diff = (long) t2 - (long) t1;
而通过下面的方法,可将两个jiffies的差转换为毫秒值:
msec = diff *1000/HZ;
用户空间和内核空间的时间表述方法的转换:
用户空间方法:timeval,timespec
内核空间方法:jiffies
#include struct timespec { unsigned long timespec_to_jiffies(struct timespec *value); void jiffies_to_timespec(unsigned long jiffies,struct timespec *value); unsigned long timeval_to_jiffies(struct timeval *value); void jiffies_to _timeval(unsigned long jiffies,struct timeval *value); |
读取64为jiffies:jiffies_64
#include
u64 get_jiffies_64(void);
处理器特定的寄存器
如果需要精度很高的计时,jiffies已不可满足需要,这时就引入了一种技术就是CPU包含一个随时钟周期不断递增的计数寄存器。这是完成高分辨率计时任务的唯一可靠途径。
1.不管该寄存器是否置0,我们都强烈建议不要重置它。
2.TSC:这是一个64位寄存器,记录CPU的时钟周期数,从内核空间和用户空间都可以读取它。
以下宏是与体系结构相关的,上述头文件是x86专用头文件
rdtsc(low32,high32);
rdtscl(low32);
rdtscll(var64);
第一个宏原子性的把64位变量读到两个32位的变量中。
第二个读取低32位,废弃高32位。
第三个把64值读到一个long long型变量中。
举例:
下面代码完成测量指令自身运行时间
unsigned long ini,end; rdtscl(ini); rdtscl(end); printk("time lapse:%li\n",end-ini); |
现提供一个与体系结构无关的函数,可以替代rdtsc
cycles_t get_cycles(void);
在各种平台上都可以使用这个函数,在没有时钟周期计数寄存器的平台上它总是返回0。cycles_t类型是能装入读取值的合适的无符号类型。
获取当前时间
jiffies用来测量时间间隔
墙钟时间-->jiffies时间:
#include
unsigned long mktime(unsigned int year,unsigned int month,
unsigned int day, unsigned int hour,
unsigned int minute,unsigned int second);
为了处理绝对时间,
|
以上两个函数在ARM平台都是通过 xtime 变量得到数据的。
全局变量xtime:它是一个timeval结构类型的变量,用来表示当前时间距UNIX时间基准1970-01-01 00:00:00的相对秒数值。
结构timeval是Linux内核表示时间的一种格式(Linux内核对时间的表示有多种格式,每种格式都有不同的时间精度),其时间精度是微秒。该结构是内核表示时间时最常用的一种格式,它定义在头文件include/linux/time.h中,如下所示:
struct timeval {
time_t tv_sec; /* seconds */
SUSEconds_t tv_usec; /* microseconds */
};
其中,成员tv_sec表示当前时间距UNIX时间基准的秒数值,而成员tv_usec则表示一秒之内的微秒值,且1000000>tv_usec>=0。
Linux内核通过timeval结构类型的全局变量xtime来维持当前时间,该变量定义在kernel/timer.c文件中,如下所示:
/* The current time */
volatile struct timeval xtime __attribute__ ((aligned (16)));
但是,全局变量xtime所维持的当前时间通常是供用户来检索和设 置的,而其他内核模块通常很少使用它(其他内核模块用得最多的是jiffies),因此对xtime的更新并不是一项紧迫的任务,所以这一工作通常被延迟 到时钟中断的底半部(bottom half)中来进行。由于bottom half的执行时间带有不确定性,因此为了记住内核上一次更新xtime是什么时候,Linux内核定义了一个类似于jiffies的全局变量 wall_jiffies,来保存内核上一次更新xtime时的jiffies值。时钟中断的底半部分每一次更新xtime的时侯都会将 wall_jiffies更新为当时的jiffies值。全局变量wall_jiffies定义在kernel/timer.c文件中:
/* jiffies at the most recent update of wall time */
unsigned long wall_jiffies;
延迟
长延迟
忙等待
若想延迟执行若干个时钟嘀哒,精度要求不高。最容易的( 尽管不推荐 ) 实现是一个监视 jiffy 计数器的循环。这种忙等待实现的代码如下:
|
对 cpu_relex 的调用将以体系相关的方式执行,在许多系统中它根本不做任何事,这个方法应当明确地避免。对于ARM体系来说:
|
也就是说在ARM上运行忙等待相当于:
|
这种忙等待严重地降低了系统性能。如果未配置内核为抢占式, 这个循环在延时期间完全锁住了处理器,计算机直到时间 j1 到时会完全死掉。如果运行一个可抢占的内核时会改善一点,但是忙等待在可抢占系统中仍然是浪费资源的。更糟的是, 当进入循环时如果中断碰巧被禁止, jiffies 将不会被更新, 并且 while 条件永远保持真,运行一个抢占的内核也不会有帮助, 唯一的解决方法是重启。
让出处理器
忙等待加重了系统负载,必须找出一个更好的技术:不需要CPU时释放CPU 。 这可通过调用schedule函数实现(在
|
在计算机空闲时运行空闲任务(进程号 0, 由于历史原因也称为swapper)可减轻处理器工作负载、降低温度、增加寿命。
超时
实现延迟的最好方法应该是让内核为我们完成相应的工作。
(1)若驱动使用一个等待队列来等待某些其他事件,并想确保它在一个特定时间段内运行,可使用:
|
(2)为了实现进程在超时到期时被唤醒而又不等待特定事件(避免声明和使用一个多余的等待队列头),内核提供了 schedule_timeout 函数:
|
短延迟
当一个设备驱动需要处理硬件的延迟(latency潜伏期), 涉及到的延时通常最多几个毫秒,在这个情况下, 不应依靠时钟嘀哒,而是内核函数 ndelay, udelay和 mdelay ,他们分别延后执行指定的纳秒数, 微秒数或者毫秒数,定义在
|
重要的是记住这 3 个延时函数是忙等待; 其他任务在时间流失时不能运行。每个体系都实现 udelay, 但是其他的函数可能未定义; 如果它们没有定义,
为避免在循环计算中整数溢出, 传递给udelay 和 ndelay的值有一个上限,如果你的模块无法加载和显示一个未解决的符号:__bad_udelay, 这意味着你调用 udleay时使用太大的参数。
作为一个通用的规则:若试图延时几千纳秒, 应使用 udelay 而不是 ndelay; 类似地, 毫秒规模的延时应当使用 mdelay 完成而不是一个更细粒度的函数。
有另一个方法获得毫秒(和更长)延时而不用涉及到忙等待的方法是使用以下函数(在
|
若能够容忍比请求的更长的延时,应使用 schedule_timeout, msleep 或 ssleep。
内核定时器
当需要调度一个以后发生的动作, 而在到达该时间点时不阻塞当前进程, 则可使用内核定时器。内核定时器用来调度一个函数在将来一个特定的时间(基于时钟嘀哒)执行,从而可完成各类任务。
内核定时器是一个数据结构, 它告诉内核在一个用户定义的时间点使用用户定义的参数执行一个用户定义的函数,函数位于
(1)不允许访问用户空间;
(2)current 指针在原子态没有意义;
(3)不能进行睡眠或者调度. 例如:调用 kmalloc(..., GFP_KERNEL) 是非法的,信号量也不能使用因为它们可能睡眠。
通过调用函数 in_interrupt()能够告知是否它在中断上下文中运行,无需参数并如果处理器当前在中断上下文运行就返回非零。
通过调用函数 in_atomic()能够告知调度是否被禁止,若调度被禁止返回非零; 调度被禁止包含硬件和软件中断上下文以及任何持有自旋锁的时候。
在后一种情况, current 可能是有效的,但是访问用户空间是被禁止的,因为它能导致调度发生. 当使用 in_interrupt()时,都应考虑是否真正该使用的是 in_atomic 。他们都在
内核定时器的另一个重要特性是任务可以注册它本身在后面时间重新运行,因为每个 timer_list 结构都会在运行前从激活的定时器链表中去连接,因此能够立即链入其他的链表。一个重新注册它自己的定时器一直运行在同一个 CPU.
即便在一个单处理器系统,定时器是一个潜在的态源,这是异步运行直接结果。因此任何被定时器函数访问的数据结构应当通过原子类型或自旋锁被保护,避免并发访问。
定时器 API
内核提供给驱动许多函数来声明、注册以及删除内核定时器:
|
内核定时器的实现《LDD3》介绍的比较笼统,以后看《ULK3》的时候再细细研究。
一个内核定时器还远未完善,因为它受到 jitter 、硬件中断,还有其他定时器和其他异步任务的影响。虽然一个简单数字 I/O关联的定时器对简单任务是足够的,但不合适在工业环境中的生产系统,对于这样的任务,你将最可能需要实时内核扩展(RT-Linux).
tasklet对中断处理例程来说尤其有用。中断处理例程必须尽可能快的管理硬件中断,而大部分数据管理则可以安全的延迟到其后的时间。
实际上,与内核定时器类似,tasklet也会在“软件中断”上下文以原子模式执行。软件中断指打开硬件中断的同时执行某些异步任务的内核机制。
tasklet以数据结构形式存在,并在使用前必须初始化。调用特定的函数或者使用特定的宏来声明该结构,即可完成tasklet的初始化:
#include struct tasklet_struct{ /*......*/ void(*func)(unsigned long); unsigned long data; }; void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long),unsigned long data); #define DECLARE_TASKLET(name, func, data) \
|
tasklet特性:
1.一个tasklet可在稍后被禁止或者重新启用;只有启用的次数和禁止的次数相同时,tasklet才会被执行。
2.和定时器类似,tasklet可以自己注册自己。
3.tasklet可被调度以在通常的优先级或者高优先级执行。高优先级的tasklet总会优先执行。
4.如果系统负荷不重,则tasklet会立即执行,但始终不会晚于下一个定时器滴答
5.一个tasklet可以和其它tasklet并发,但对自身来讲是严格串行处理的,也就是说,同一tasklet永远不会在多个处理器上同时运行:tasklet始终会调度自己在同一CPU上运行;
表面来看,工作队列类似于tasklet:允许内核代码请求某个函数在将来的时间被调用。
但其实还是有很多不同:
1.tasklet在软中断上下文中运行,因此,所有的tasklet代码都是原子的。相反,工作队列函数在一个特殊的内核进程上下文中运行,因此他们有更好的灵活性
尤其是,工作队列可以休眠!
2.tasklet始终运行在被初始提交的统一处理器上,但这只是工作队列的默认方式
3.内核代码可以请求工作队列函数的执行延迟给定的时间间隔
4.tasklet 执行的很快, 短时期, 并且在原子态, 而工作队列函数可能是长周期且不需要是原子的,两个机制有它适合的情形。
两者的关键区别:tasklet会在很短的时间内很快执行,并且以原子模式执行,而工作队列函数可以具有更长的延迟并且不必原子化。两种机制有各自适合的情形。
工作队列有 struct workqueue_struct 类型,在
struct workqueue_struct *create_workqueue(const char *name); |
每个工作队列有一个或多个专用的进程("内核线程"), 这些进程运行提交给这个队列的函数。 若使用 create_workqueue, 就得到一个工作队列它在系统的每个处理器上有一个专用的线程。在很多情况下,过多线程对系统性能有影响,如果单个线程就足够则使用 create_singlethread_workqueue 来创建工作队列。
提交一个任务给一个工作队列,在这里《LDD3》介绍的内核2.6.10和我用的新内核2.6.22.2已经有不同了,老接口已经不能用了,编译会出错。这里我只讲2.6.22.2的新接口,至于老的接口我想今后内核不会再有了。从这一点我们可以看出内核发展。
/*需要填充work_struct或delayed_work结构,可以在编译时完成, 宏如下: */ struct work_struct { |
在将来的某个时间, 这个工作函数将被传入给定的 data 值来调用。这个函数将在工作线程的上下文运行, 因此它可以睡眠 (你应当知道这个睡眠可能影响提交给同一个工作队列的其他任务) 工作函数不能访问用户空间,因为它在一个内核线程中运行, 完全没有对应的用户空间来访问。
取消一个挂起的工作队列入口项可以调用:
int cancel_delayed_work(struct delayed_work *work); |
如果这个入口在它开始执行前被取消,则返回非零。内核保证给定入口的执行不会在调用 cancel_delay_work 后被初始化. 如果 cancel_delay_work 返回 0, 但是, 这个入口可能已经运行在一个不同的处理器, 并且可能仍然在调用 cancel_delayed_work 后在运行. 要绝对确保工作函数没有在 cancel_delayed_work 返回 0 后在任何地方运行, 你必须跟随这个调用来调用:
void flush_workqueue(struct workqueue_struct *queue); |
在 flush_workqueue 返回后, 没有在这个调用前提交的函数在系统中任何地方运行。
而cancel_work_sync会取消相应的work,但是如果这个work已经在运行那么cancel_work_sync会阻塞,直到work完成并取消相应的work。
当用完一个工作队列,可以去掉它,使用:
void destroy_workqueue(struct workqueue_struct *queue); |
共享队列
在许多情况下, 设备驱动不需要它自己的工作队列。如果你只偶尔提交任务给队列, 简单地使用内核提供的共享的默认的队列可能更有效。若使用共享队列,就必须明白将和其他人共享它,这意味着不应当长时间独占队列(不能长时间睡眠), 并且可能要更长时间才能获得处理器。
使用的顺序:
(1) 建立 work_struct 或 delayed_work
static struct work_struct jiq_work; |
(2)提交工作
int schedule_work(&jiq_work);/*对于work_struct结构*/ /*返回值的定义和 queue_work 一样*/ |
若需取消一个已提交给工作队列入口项, 可以使用 cancel_delayed_work和cancel_work_sync, 但刷新共享队列需要一个特殊的函数:
void flush_scheduled_work(void); |
因为不知道谁可能使用这个队列,因此不可能知道 flush_schduled_work 返回需要多长时间。
kmalloc函数
#include |
1.不会对所申请的内存清零,保留原有数据
2.参数:size:分配大小
flags:kmalloc行为
3.flags:GFP_KERNEL :内核内存通常的分配方法,可能引起休眠
GFP_ATOMIC :用于在中断处理例程或其它运行于进程上下文之外的代码中分配内存,不会休眠
GFP_USER:用于为用户空间分配内存,可能会引起休眠
GFP_HIGHUSER:类似于GFP_USER,不过如果有高端内存的话就从那里分配
GFP_NOIO:在GFP_KERNEL的基础上,禁止任何I/O的初始化
GFG_NOFS:在GFP_KERNEL的基础上,不允许执行任何文件系统的调用
另外有一些分配标志与上述“或”起来使用
__GFP_DMA:
__GFP_HIGHMEM:
__GFP_COLD:
__GFP_NOWARN:
__GFP_HIGH:
__GFP_REPEAT:
__GFP_NOFAIL:
__GFP_NORETRY:
4.内存区段
linux通常把内存分成三个区段:
可用于DMA内存 | 存在于特别的地址范围 |
常规内存 | |
高端内存 | 32位平台为访问(相对)大量内存而存在的一种机制 |
5.size
linux处理内存分配:创建一系列的内存对象池,每个池中的内存块大小是固定一致的。处理分配请求时,就直接在包含有足够大的内存块的池中传递一个整块给请求者。
内核只能分配一些预定义的,固定大小的字节数组。若申请任意数量的内存,则得到的可能会多一些,最多可以得到申请数量的两倍。
下限 | 32或者64,取决于当前体系 |
上限 | 128kb,使用的页面的大小 |
后备高速缓存
驱动程序常常需要反复分配许多相同大小内存块的情况,增加了一些特殊的内存池,称为后备高速缓存(lookaside cache)。 设备驱动程序通常不会涉及后备高速缓存,但是也有例外:在 Linux 2.6 中 USB 和 SCSI 驱动。Linux 内核的高速缓存管理器有时称为“slab 分配器”,相关函数和类型在
1.实现过程如下:
|
参数*name: 一个指向 name 的指针,name和这个后备高速缓存相关联,功能是管理信息以便追踪问题;通常设置为被缓存的结构类型的名字,不能包含空格。
参数size:每个内存区域的大小。
参数offset:页内第一个对象的偏移量;用来确保被分配对象的特殊对齐,0 表示缺省值。
参数flags:控制分配方式的位掩码:
2.一旦某个对象的高速缓存被创建,就可以调用kmem_cache_alloc从中分配内存对象:
viod *kmem_cache_alloc(kmem_cache_t *cache,int flags); cache是之前创建的高速缓存,flags和传递给kmalloc的相同,并且当需要分配更多的内存来满足kmem_cache_alloc时,高速缓存还会利用这个参数 |
3.释放一个内存对象,使用kmem_cache_free:
void kmem_cache_free(kmem_cache_t *cache,const void *obj); |
4.如果驱动程序代码中和高速缓存有关的部分已经处理完了(典型情况:模块被卸载的时候),这时驱动程序应该释放它的高速缓存:
int kmem_cache_destroy(kmem_cache_t *cache); /*只在从这个缓存中分配的所有的对象都已返时才成功。因此,应检查 kmem_cache_destroy 的返回值:失败指示模块存在内存泄漏*/ |
内存池
为了确保在内存分配不允许失败情况下成功分配内存,内核提供了称为内存池( "mempool" )的抽象,它其实是某种后备高速缓存。它为了紧急情况下的使用,尽力一直保持空闲内存。所以使用时必须注意: mempool 会分配一些内存块,使其空闲而不真正使用,所以容易消耗大量内存 。而且不要使用 mempool 处理可能失败的分配。应避免在驱动代码中使用 mempool。
内存池的类型为 mempool_t ,在
1.创建mempool
/*min_nr 参数是内存池应当一直保留的最小数量的分配对象*/ /*实际的分配和释放对象由 alloc_fn 和 free_fn 处理,原型:*/ |
你可编写特殊用途的函数来处理 mempool 的内存分配,但通常只需使用 slab 分配器为你处理这个任务:mempool_alloc_slab 和 mempool_free_slab的原型和上述内存池分配原型匹配,并使用 kmem_cache_alloc 和 kmem_cache_free 处理内存的分配和释放。
典型的设置内存池的代码如下:
|
(2)创建内存池后,分配和释放对象:
|
可用一下函数重定义mempool预分配对象的数量:
|
(3)若不再需要内存池,则返回给系统:
|
get_free_page与相关函数
1.如果模块需要分配大块的内存,使用面向页的分配技术会更好一些,就是整页的分配。
|
2.get_order 函数可以用来从一个整数参数 size(必须是 2 的幂) 中提取 order,函数也很简单:
|
3.通过/proc/buddyinfo 可以知道系统中每个内存区段上的每个 order 下可获得的数据块数目。
4.。当程序不需要页面时,它可用下列函数之一来释放它们。
它们的关系是: |
alloc_pages 接口
1.struct page 是一个描述一个内存页的内部内核结构,定义在
2.
Linux 页分配器的核心是称为 alloc_pages_node 的函数:
|
参数nid 是要分配内存的 NUMA 节点 ID,
参数flags 是 GFP_ 分配标志,
参数order 是分配内存的大小.
返回值是一个指向第一个(可能返回多个页)page结构的指针, 失败时返回NULL。
alloc_pages 通过在当前 NUMA 节点分配内存( 它使用 numa_node_id 的返回值作为 nid 参数调用 alloc_pages_node)简化了alloc_pages_node调用。alloc_pages 省略了 order 参数而只分配单个页面。
释放分配的页:
|
vmalloc 和 ioremap
vmalloc 是一个基本的 Linux 内存分配机制,它在虚拟内存空间分配一块连续的内存区,尽管这些页在物理内存中不连续 (使用一个单独的 alloc_page 调用来获得每个页),但内核认为它们地址是连续的。 应当注意的是:vmalloc 在大部分情况下不推荐使用。因为在某些体系上留给 vmalloc 的地址空间相对小,且效率不高。函数原型如下:
|
kmalloc 和 _get_free_pages 返回的内存地址也是虚拟地址,其实际值仍需 MMU 处理才能转为物理地址。vmalloc和它们在使用硬件上没有不同,不同是在内核如何执行分配任务上:kmalloc 和 __get_free_pages 使用的(虚拟)地址范围和物理内存是一对一映射的, 可能会偏移一个常量 PAGE_OFFSET 值,无需修改页表。
而vmalloc 和 ioremap 使用的地址范围完全是虚拟的,且每次分配都要通过适当地设置页表来建立(虚拟)内存区域。 vmalloc 可获得的地址在从 VMALLOC_START 到 VAMLLOC_END 的范围中,定义在
ioremap 也要建立新页表,但它实际上不分配任何内存,其返回值是一个特殊的虚拟地址可用来访问特定的物理地址区域。
为了保持可移植性,不应当像访问内存指针一样直接访问由 ioremap 返回的地址,而应当始终使用 readb 和 其他 I/O 函数。
ioremap 和 vmalloc 是面向页的(它们会修改页表),重定位的或分配的空间都会被上调到最近的页边界。ioremap 通过将重映射的地址下调到页边界,并返回第一个重映射页内的偏移量来模拟一个非对齐的映射。
per-CPU变量
per-CPU 变量是一个有趣的 2.6 内核特性,定义在
在编译时间创建一个per-CPU变量使用如下宏定义:
|
虽然操作per-CPU变量几乎不必使用锁定机制。 但是必须记住 2.6 内核是可抢占的,所以在修改一个per-CPU变量的临界区中可能被抢占。并且还要避免进程在对一个per-CPU变量访问时被移动到另一个处理器上运 行。所以必须显式使用 get_cpu_var 宏来访问当前处理器的变量副本, 并在结束后调用 put_cpu_var。 对 get_cpu_var 的调用返回一个当前处理器变量版本的 lvalue ,并且禁止抢占。又因为返回的是lvalue,所以可被直接赋值或操作。例如:
|
当要访问另一个处理器的变量副本时, 使用:
|
|
|
获得大的缓冲区
大量连续内存缓冲的分配是容易失败的。到目前止执行大 I/O 操作的最好方法是通过离散/聚集操作 。
|
I/O端口 VS I/O内存
知识点:
1.每种外设都是通过读写寄存器进行控制。
2.在硬件层,内存区域和I/O区域没有概念上的区别:他们都通过像地址总线和控制总线发送电平信号进行访问,在通过数据总线读写数据。
3.
4.
I/O寄存器 VS 常规内存
1.I/O寄存器与RAM的最主要区别就是I/O操作具有边际效应(个人理解就是副作用):读取某个地址时可能导致这个地址的内容发生变化,比如很多中断寄存器的值一经读取,便自动清零。
2.编译器可以将数据缓存在CPU寄存器而不用写入内存,即使存储数据,读写操作也都能在高速缓存中进行而不用访问物理内存。
3.常规内存可以优化,但是对寄存器进行优化可能导致致命错误。驱动程序一定要保证不适用那个高速缓存,并且,在访问寄存器时不发生读或写指令的重新排序。
4.由硬件自身缓存引起的问题很好解决:只要在把底层硬件配置成(自动配置或是linux初始化代码完成)在访问I/O区域(不管是内存还是端口)时禁止硬件缓存即可。
5.由编译器优化和硬件重新排序引起的问题的解决办法:对硬件或其他处理器必须以特定顺序执行的操作之间设置内存屏障。linux有四个宏解决所有可能的排序问题。
#include void barrier(void); 对barrier的调用可防止在屏障前后的编译器优化,但硬件能完成自己的重新排序。 |
#include void rmb(void); /*保证任何出现于屏障前的读在执行任何后续的读之前完成*/ |
典型的应用:
|
内存屏障影响性能,所以应当只在确实需要它们的地方使用。不同的类型对性能的影响也不同,因此要尽可能地使用需要的特定类型。值得注意的是大部分处理同步的内核原语,例如自旋锁和atomic_t,也可作为内存屏障使用。
某些体系允许赋值和内存屏障组合,以提高效率。它们定义如下:
|
使用do...while 结构来构造宏是标准 C 的惯用方法,它保证了扩展后的宏可在所有上下文环境中被作为一个正常的 C 语句执行。
对I/O端口的使用:申请,访问,释放
1.申请:
#include struct resource *request_region(unsigned long first,unsigned long n,const char *name); 该函数通知内核我们要使用起始于first的n个端口。name应该是设备的名称。若申请成功,返回非NULL,失败,返回NULL |
所有的端口分配可从/proc/ioports中得到,如果我们无法分配到需要的端口集合,则可以从这个文件中得知哪个驱动程序已经分配了这个端口。
2.访问:
|
unsigned inw(unsigned port); |
unsigned inl(unsigned port); |
3.释放:
void release_region(unsigned long start,unsigned long n); 当不再使用I/O端口,或者卸载模块时应使用该函数将这些端口返回给系统。 |
int check_region(unsigned long first,unsigned long n); 该函数用来检测给定的I/O端口是否可用。 |
以上函数主要用在驱动程序的使用上的,但他们也可以在用户空间使用,至少在PC类计算机上可以使用。GNU的C库在
程序必须使用 -O 选项编译来强制扩展内联函数 |
必须用ioperm 和 iopl 系统调用(#include |
程序以 root 来调用 ioperm 和 iopl,或是其父进程必须以 root 获得端口操作权限。(x86 特有的) |
若平台没有 ioperm 和 iopl 系统调用,用户空间可以仍然通过使用 /dev/prot 设备文件访问 I/O 端口。注意:这个文件的定义是体系相关的,并且I/O 端口必须先被注册。
除了一次传递一个数据的I/O操作,linux还提供了一次传递一个数据序列的特殊指令,序列中的数据单位可以是字节、字或双字,这是所谓的串操作 指令。它们完成任务比一个 C 语言循环更快。下列宏定义实现了串I/O,它们有的通过单个机器指令实现;但如果目标处理器没有进行串 I/O 的指令,则通过执行一个紧凑的循环实现。 有的体系的原型如下:
字节端口读写 |
void insw(unsigned port, void *addr, unsigned long count); 16位端口读写 |
void insl(unsigned port, void *addr, unsigned long count); 32位端口读写,即使在64位的体系结构上,端口地址也只使用最大32位的数据通路 |
暂停式I/O
为了匹配低速外设的速度,有时若 I/O 指令后面还紧跟着另一个类似的I/O指令,就必须在 I/O 指令后面插入一个小延时。在 这种情况下,可以使用暂停式的I/O函数代替通常的I/O函数,它们的名字以 _p 结尾,如 inb_p、outb_p等等。 这些函数定义被大部分体系支持,尽管它们常常被扩展为与非暂停式I/O 同样的代码。因为如果体系使用一个合理的现代外设总线,就没有必要额外暂停。细节可参考平台的 asm 子目录的 io.h 文件。以下是include\asm-arm\io.h中的宏定义:
|
由此可见,由于ARM使用内部总线,就没有必要额外暂停,所以暂停式的I/O函数被扩展为与非暂停式I/O 同样的代码。
平台相关性
由于自身的特性,I/O 指令与处理器密切相关的,非常难以隐藏系统间的不同。所以大部分的关于端口 I/O 的源码是平台依赖的。以下是x86和ARM所使用函数的总结:
除了 x86上普遍使用的I/O 端口外,和设备通讯另一种主要机制是通过使用映射到内存的寄存器或设备内存,统称为 I/O 内存。因为寄存器和内存之间的区别对软件是透明的。I/O 内存仅仅是类似 RAM 的一个区域,处理器通过总线访问这个区域,以实现设备的访问。
根据平台和总线的不同,I/O 内存可以就是否通过页表访问分类。若通过页表访问,内核必须首先安排物理地址使其对设备驱动程序可见,在进行任何 I/O 之前必须调用 ioremap。若不通过页表,I/O 内存区域就类似I/O 端口,可以使用适当形式的函数访问它们。因为“side effect”的影响,
不管是否需要 ioremap ,都不鼓励直接使用 I/O 内存的指针。而使用专用的 I/O 内存操作函数,不仅在所有平台上是安全,而且对直接使用指针操作 I/O 内存的情况进行了优化。
I/O 内存分配和映射
I/O 内存区域使用前必须先分配,函数接口在
|
然后必须设置一个映射,由 ioremap 函数实现,此函数专门用来为I/O 内存区域分配虚拟地址。经过ioremap 之后,设备驱动即可访问任意的 I/O 内存地址。注意:ioremap 返回的地址不应当直接引用;应使用内核提供的 accessor 函数。以下为函数定义:
|
访问I/O 内存
访问I/O 内存的正确方式是通过一系列专用于此目的的函数(在
|
一些硬件有一个有趣的特性:一些版本使用 I/O 端口,而其他的使用 I/O 内存。为了统一编程接口,使驱动程序易于编写,2.6 内核提供了一个ioport_map函数:
|
中断仅仅就是一个信号,当硬件需要获得处理器对它的关注时,就可以发送这个信号。 Linux 处理中断的方式非常类似在用户空间处理信号的方式。大多数情况下,一个驱动只需要为它的设备的中断注册一个处理例程,并当中断到来时进行正确的处理。本质上来讲,中断处理例程和其他的代码并行运行。因此,它们不可避免地引起并发问题,并竞争数据结构和硬件。 透彻地理解并发控制技术对中断来讲非常重要。
中断信号线
内核维护了一个中断信号线的注册表,改注册表类似于I/O端口的注册表。模块在使用中断之前要先请求一个中断通道(或者中断请求IRQ),然后在使用后释放该通道。
int request_irq(unsigned long irq, irqreturn_t (*handler)(int,void *,struct pt_regs *), unsigned long flags, const char *dev_name, void *dev_id); 表示注册中断,返回值: 0 指示成功,或返回一个负的错误码,如 -EBUSY 表示另一个驱动已经占用了你所请求的中断线。 参数: unsigned int irq:要申请的中断号 irqreturn_t (*handler)(int,void *,struct pt_regs *):要安装的中断处理函数的指针 unsigned long flags:与中断管理相关的位掩码选项 const char *dev_name:传递给request_irq的字符串,用来在/proc/interrupts显示中断的拥有者 void *dev_id:用于共享的中断信号线,它是唯一的标识,在中断线空闲时可以使用它,驱动程序也可以用它来指向自己的私有数据区(来标识哪个设备产生中断)。若中断没有被共享,dev_id 可以设置为 NULL,但推荐用它指向设备的数据结构。 flags: void free_irq(unsigned int irq,void *dev_id); 释放中断 |
中断处理例程可在驱动初始化时或在设备第一次打开时安装。推荐在设备第一次打开、硬件被告知产生中断前时申请中断,因为可以共享有限的中断资源。这 样调用 free_irq 的位置是设备最后一次被关闭、硬件被告知不用再中断处理器之后。但这种方式的缺点是必须为每个设备维护一个打开计数。
以下是中断申请的示例(并口):
|
i386 和 x86_64 体系定义了一个函数来查询一个中断线是否可用:
|
x86中断处理内幕
这个描述是从 2.6 内核 arch/i386/kernel/irq.c, arch/i386/kernel/ apic.c, arch/i386/kernel/entry.S, arch/i386/kernel/i8259.c, 和 include/asm-i386/hw_irq.h 中得出,尽管基本概念相同,硬件细节与其他平台上不同。
底层中断处理代码在汇编语言文件 entry.S。在所有情况下,这个代码将中断号压栈并且跳转到一个公共段,公共段会调用 do_IRQ(在 irq.c 中定义)。do_IRQ 做的第一件事是应答中断以便中断控制器能够继续其他事情。它接着获取给定 IRQ 号的一个自旋锁,阻止其他 CPU 处理这个 IRQ,然后清除几个状态位(包括IRQ_WAITING )然后查找这个 IRQ 的处理例程。若没有找到,什么也不做;释放自旋锁,处理任何待处理的软件中断,最后 do_IRQ 返回。从中断中返回的最后一件事可能是一次处理器的重新调度。
IRQ的探测是通过为每个缺乏处理例程的IRQ设 置 IRQ_WAITING 状态位来完成。当中断发生, 因为没有注册处理例程,do_IRQ 清除这个位并且接着返回。 当probe_irq_off被一个函数调用,只需搜索没有设置 IRQ_WAITING 的 IRQ。
快速和慢速处理例程
快速中断是那些能够很快处理的中断,而处理慢速中断会花费更长的时间。在处理慢速中断时处理器重新使能中断,避免快速中断被延时过长。在现代内核 中,快速和慢速中断的区别已经消失,剩下的只有一个:快速中断(使用 SA_INTERRUPT )执行时禁止所有在当前处理器上的其他中断。注意:其他的处理器仍然能够处理中断。
除非你充足的理由在禁止其他中断情况下来运行中断处理例程,否则不应当使用SA_INTERRUPT.
当硬件中断到达处理器时, 内核提供的一个内部计数器会递增,产生的中断报告显示在文件 /proc/interrupts中。这一方法可以用来检查设备是否按预期地工作。此文件只显示当前已安装处理例程的中断的计数。若以前request_irq的一个中断,现在已经free_irq了,那么就不会显示在这个文件中,但是它可以显示终端共享的情况。
/proc/stat记录了几个关于系统活动的底层统计信息, 包括(但不仅限于)自系统启动以来收到的中断数。stat 的每一行以一个字符串开始, 是该行的关键词:intr 标志是中断计数。第一个数是所有中断的总数, 而其他每一个代表一个单独的中断线的计数, 从中断 0 开始(包括当前没有安装处理例程的中断),无法显示终端共享的情况。
以上两个文件的一个不同是:/proc/interrupts几乎不依赖体系,而/proc/stat的字段数依赖内核下的硬件中断,其定义在
|
自动检测 IRQ 号
驱动初始化时最迫切的问题之一是决定设备要使用的IRQ 线,驱动需要信息来正确安装处理例程。自动检测中断号对驱动的可用性来说是一个基本需求。有时自动探测依赖一些设备具有的默认特性,以下是典型的并口中断探测程序:
|
有的驱动允许用户在加载时覆盖默认值:
|
当目标设备有能力告知驱动它要使用的中断号时,自动探测中断号只是意味着探测设备,无需做额外的工作探测中断。
但不是每个设备都对程序员友好,对于他们还是需要一些探测工作。这个工作技术上非常简单: 驱动告知设备产生中断并且观察发生了什么。如果一切顺利,则只有一个中断信号线被激活。尽管探l测在理论上简单,但实现可能不简单。有 2 种方法来进行探测中断:调用内核定义的辅助函数和DIY探测。
内核帮助下的探测:
linux提供了一个底层设施来探测中断号。它只能在非共享中断的模式下工作,但是大多数硬件有能力工作在共享中断的模式下,并可提供更好的找到配置中断号的方法,内核提供的这一设施由两个函数组成,在头文件
unsigned long probe_irq_on(void); /*返回一个未分配中断的位掩码。驱动必须保留返回的位掩码,并在后边传递给probe_irq_off,在调用它之后,驱动程序应当至少安排它的设备产生一次中断*/ int probe_irq_off(unsigned long); /*在请求设备产生一次中断后,驱动调用这个函数,并将probe_irq_on产生的位掩码作为参数传递给 probe_irq_off,probe_irq_off返回在probe_on之后发生的中断号。如果没有中断发生,返回0,如果产生了多次中断,返回 一个负值。*/ |
程序员应当注意在调用 probe_irq_on 之后启用设备上的中断, 并在调用 probe_irq_off 前禁用。此外还必须记住在 probe_irq_off 之后服务设备中待处理的中断。
以下是LDD3中的并口示例代码,(并口的管脚 9 和 10 连接在一起,探测五次失败后放弃):
|
最好只在模块初始化时探测中断线一次。
大部分体系定义了这两个函数( 即便是空的 )来简化设备驱动的移植。
DIY探测:
DIY探测与前面原理相同: 使能所有未使用的中断, 接着等待并观察发生什么。我们对设备的了解:通常一个设备能够使用3或4个IRQ 号中的一个来进行配置,只探测这些 IRQ 号使我们能不必测试所有可能的中断就探测到正确的IRQ 号。
下面的LDD3中的代码通过测试所有"可能的"中断并且察看发生的事情来探测中断。 trials 数组列出要尝试的中断, 以 0 作为结尾标志; tried 数组用来跟踪哪个中断号已经被这个驱动注册。
|
以下是handler的源码:
|
若事先不知道"可能的" IRQ ,就需要探测所有空闲的中断,所以不得不从 IRQ 0 探测到 IRQ NR_IRQS-1
1.中断处理例程是在中断时间内运行的,因此行为会受到一些限制。这些限制跟我们在内核定时器中看到的一样。
处理例程不能向用户空间发送或者接收数据,因为它不是在任何进程上下文中执行的
处理例程也不能做任何可能发生休眠的操作,例如调用wait_event,使用不带GFP_ATOMIC标志的内存分配操作,或者锁住一个信号量等等
处理例程不能调用schdule函数
2.中断处理理财的功能就是将有关中断接收的信息反馈给设备,并根据正在服务的终端的不同含义对对数据进行相应的读或写。中断处理例程第一步常常包 括清除设备的一个中断标志位,大部分硬件设备在清除"中断挂起"位前不会再产生中断。这也要根据硬件的工作原理决定, 这一步也可能需要在最后做而不是开始; 这里没有通用的规则。一些设备不需要这步, 因为它们没有一个"中断挂起"位; 这样的设备是少数。
3.中断处理的一个典型 任务:如果中断通知进程所等待的事件已经发生,比如新的数据已经到达,就会唤醒在设备上休眠的进程。
不管是快速或慢速处理例程,程序员应编写执行时间尽可能短的处理例程。 如果需要进行长时间计算, 最好的方法是使用 tasklet 或者 workqueue 在一个更安全的时间来调度计算任务。
启用和禁止中断
有时设备驱动必须在一段时间(希望较短)内阻塞中断发生。并必须在持有一个自旋锁时阻塞中断,以避免死锁系统。注意:应尽量少禁止中断,即使是在设备驱动中,且这个技术不应当用于驱动中的互斥机制。
|
|
中断处理需要很快完成,并且不需要阻塞太长,所以中断处理的一个主要问题就是中断处理例程中完成耗时的任务。
linux通过将中断处理分成两部分来完成这个任务:
1.顶半部:实际响应中断的例程(request_irq注册的那个例程)
2.底半部:被顶半部调用并在稍后更安全的一个时间里执行的函数。
他们最大的不同在底半部处理例程执行时,所有中断都是打开的(这就是所谓的在更安全的时间内运行)。典型的情况是:顶半部保存设备数据到一个设备特定的缓存并调度它的底半部,最后退出:这个操作非常快。底半部接着进行任何其他需要的工作。这种方式的好处是在底半部工作期间,顶半部仍然可以继续为新中断服务。
Linux 内核有 2 个不同的机制可用来实现底半部处理:
(1) tasklet (首选机制),它非常快, 但是所有的 tasklet 代码必须是原子的;
(2)工作队列,它可能有更高的延时,但允许休眠。
tasklet和工作队列在《时间、延迟及延缓操作》已经介绍过,具体的实现代码请看实验源码!
Linux内核支持在所有总线上中断共享。
安装共享的处理例程
通过 request_irq来安装共享中断与非共享中断有2点不同:
(1)当request_irq时,flags中必须指定SA_SHIRQ位; (2)dev_id必须唯一。任何指向模块地址空间的指针都行,但dev_id绝不能设置为NULL。 |
内核为每个中断维护一个中断共享处理例程列表,dev_id就是区别不同处理例程的签名。释放处理例程通过执行free_irq实现。 dev_id用来从这个中断的共享处理例程列表中选择正确的处理例程来释放,这就是为什么dev_id必须是唯一的.
请求一个共享的中断时,如果满足下列条件之一,则request_irq成功:
(1)中断线空闲; (2)所有已经注册该中断信号线的处理例程也标识了IRQ是共享。 |
一个共享的处理例程必须能够识别自己的中断,并且在自己的设备没有被中断时快速退出(返回IRQ_NONE)。
共享处理例程没有探测函数可用,但使用的中断信号线是空闲时标准的探测机制才有效。
一个使用共享处理例程的驱动需要小心:不能使用enable_irq或disable_irq,否则,对其他共享这条线的设备就无法正常工作了。即便短时间禁止中断,另一设备也可能产生延时而为设备和其用户带来问题。所以程序员必须记住:他的驱动并不是独占这个IRQ,它的行为应当比独占这个中断线更加"社会化"。
当与驱动程序管理的硬件间的数据传送可能因为某种原因而延迟,驱动编写者应当实现缓存。一个好的缓存机制需采用中断驱动的I/O,一个输入缓存在中断时被填充,并由读取设备的进程取走缓冲区的数据,一个输出缓存由写设备的进程填充,并在中断时送出数据。
为正确进行中断驱动的数据传送,硬件应能够按照下列语义产生中断:
输入:当新数据到达时并处理器准备好接受时,设备中断处理器。
输出:当设备准备好接受新数据或确认一个成功的数据传送时,设备产生中断。
以《LDD3》的说法:Linux设备模型这部分内容可以认为是高级教材,对于多数程序作者来说是不必要的。但是我个人认为:对于一个嵌入式Linux的底层程序员来说,这部分内容是很重要的。以我学习的ARM9为例,有很多总线(如SPI、IIC、IIS等等)在Linux下已经被编写成了子系统,无需自己写驱动;而这些总线又不像PCI、USB等在《LDD3》上有教程,有时还要自己研究它的子系统构架,甚至要自己添加一个新的总线类型。
在 这部分的学习中,将会先研究linux设备模型的每个元素,最后将其一步一步整合,至底向上地分析。一开始会比较摸不着头脑,到了整合阶段就柳暗花明了。 我之所以没有先介绍整体,再分析每个部分是因为如果不对每个元素做认真分析,看了整体也会云里雾里(我试过了,恕小生愚钝)。所以一开始要耐着性子看,到 整合阶段就会豁然开朗。
Linux设备模型的目的是:为内核建立起一个统一的设备模型,从而有一个对系统结构的一般性抽象描述。
现在内核使用设备模型支持多种不同的任务:
电源管理和系统关机 :这些需要对系统结构的理解,设备模型使OS能以正确顺序遍历系统硬件。
与用户空间的通讯 :sysfs 虚拟文件系统的实现与设备模型的紧密相关, 并向外界展示它所表述的结构。向用户空间提供系统信息、改变操作参数的接口正越来越多地通过 sysfs , 也就是设备模型来完成。
热插拔设备
设备类型:设备模型包括了将设备分类的机制,在一个更高的功能层上描述这些设备, 并使设备对用户空间可见。
对象生命周期:设备模型的实现需要创建一系列机制来处理对象的生命周期、对象间的关系和对象在用户空间的表示。
Linux 设备模型是一个复杂的数据结构。但对模型的大部分来说, Linux 设备模型代码会处理好这些关系, 而不是把他们强加于驱动作者。模型隐藏于交互的背后,与设备模型的直接交互通常由总线级的逻辑和其他的内核子系统处理。所以许多驱动作者可完全忽略设备模 型, 并相信设备模型能处理好他所负责的事。
在此之前请先了解一下sysfs,请看 Linux那些事儿之我是Sysfs(1)sysfs初探 我就不在这里废话了!这里还建议先看看 sysfs 的内核文档\Documentation\filesystems\sysfs.txt,我将其翻译好做成PDF,下载地址:http://blogimg.chinaunix.net/blog/upfile2/071229162826.pdf
如有错误欢迎指正!
Kobject、Kset 和 Subsystem
Kobjects
kobject是一种数据结构,定义在
|
对象的引用计数 :跟踪对象生命周期的一种方法是使用引用计数。当没有内核代码持有该对象的引用时, 该对象将结束自己的有效生命期并可被删除。
sysfs 表述:在 sysfs 中出现的每个对象都对应一个 kobject, 它和内核交互来创建它的可见表述。
数据结构关联:整体来看, 设备模型是一个极端复杂的数据结构,通过其间的大量链接而构成一个多层次的体系结构。kobject 实现了该结构并将其聚合在一起。
热插拔事件处理 :kobject 子系统将产生的热插拔事件通知用户空间。
一个kobject对自身并不感兴趣,它存在的意义在于把高级对象连接到设备模型上。因此内核代码很少(甚至不知道)创 建一个单独的 kobject;而kobject 被用来控制对大型域(domain)相关对象的访问,所以kobject 被嵌入到其他结构中。kobject 可被看作一个最顶层的基类,其他类都它的派生产物。 kobject 实现了一系列方法,对自身并没有特殊作用,而对其他对象却非常有效。
对于给定的kobject指针,可使用container_of宏得到包含它的结构体的指针。
kobject的初始化较为复杂,但是必须的步骤如下:
(1)将整个kobject清零,通常使用memset函数。
(2)调用kobject_init()函数,设置结构内部一些成员。所做的一件事情是设置kobject的引用计数为1。具体的源码如下:
|
(3)设置kobject的名字
|
(4)直接或间接设置其它成员:ktype、kset和parent。 (重要)
对引用计数的操作
kobject 的一个重要函数是为包含它的结构设置引用计数。只要对这个对象的引用计数存在, 这个对象( 和支持它的代码) 必须继续存在。底层控制 kobject 的引用计数的函数有:
|
注意:kobject _init 设置这个引用计数为 1,因此创建一个 kobject时, 当这个初始化引用不再需要,应当确保采取 kobject_put 调用。同理:struct cdev 的引用计数实现如下:
|
创建一个对 cdev 结构的引用时,还需要创建包含它的模块的引用。因此, cdev_get 使用 try_module_get 来试图递增这个模块的使引用计数。如果这个操作成功, kobject_get 被同样用来递增 kobject 的引用计数。kobject_get 可能失败, 因此这个代码检查 kobject_get 的返回值,如果调用失败,则释放它的对模块的引用计数。
release 函数和 kobject 类型
引用计数不由创建 kobject 的代码直接控制,当 kobject 的最后引用计数消失时,必须异步通知,而后kobject中ktype所指向的kobj_type结构体包含的release函数会被调用。通常原型如下:
|
每个 kobject 必须有一个release函数, 并且这个 kobject 必须在release函数被调用前保持不变( 稳定状态 ) 。这样,每一个 kobject 需要有一个关联的 kobj_type 结构,指向这个结构的指针能在 2 个不同的地方找到:
(1)kobject 结构自身包含一个成员(ktype)指向kobj_type ;
(2)如果这个 kobject 是一个 kset 的成员, kset 会提供kobj_type 指针。
|
以下宏用以查找指定kobject的kobj_type 指针:
|
这个函数其实就是从以上提到的这两个地方返回kobj_type指针,源码如下:
|
|
kobject 层次结构、kset和子系统
内核通常用kobject 结构将各个对象连接起来组成一个分层的结构体系,与模型化的子系统相匹配。有 2 个独立的机制用于连接: parent 指针和 kset。
parent 是指向另外一个kobject 结构(分层结构中上一层的节点)的指针,主要用途是在 sysfs 层次中定位对象.
kset
kset 象 kobj_type 结构的扩展; 一个 kset 是嵌入到相同类型结构的 kobject 的集合。但 struct kobj_type 关注的是对象的类型,而struct kset 关心的是对象的聚合和集合,其主要功能是包容,可认为是kobjects 的顶层容器类。每个 kset 在内部包含自己的 kobject, 并可以用多种处理kobject 的方法处理kset。 ksets 总是在 sysfs 中出现; 一旦设置了 kset 并把它添加到系统中, 将在 sysfs 中创建一个目录;kobjects 不必在 sysfs 中表示, 但kset中的每一个 kobject 成员都要在sysfs中表述。
增加 kobject 到 kset 中去,通常是在kobject 创建时完成,其过程分为2步:
(1)完成kobject的初始化,特别注意mane和parent和初始化。
(2)把kobject 的 kset 成员指向目标kset。
(3)将kobject 传递给下面的函数:
|
内核提供了一个组合函数:
|
|
|
这样的函数,所以请根据你使用的内核版本自己研究了.
kset 在一个标准的内核链表中保存了它的子节点,在大部分情况下, 被包含的 kobjects 在它们的 parent 成员中保存指向 kset内嵌的 kobject的指针,关系如下:
图表中所有被包含的 kobjects 实际上被嵌入在一些其他类型中, 甚至可能是其他的 kset。
kset 上的操作
ksets 有类似于kobjects初始化和设置接口:
|
ksets 还有一个指针指向 kobj_type 结构来描述它包含的 kobject,这个类型优先于 kobject 自身中的 ktype 。因此在典型的应用中, 在 struct kobject 中的 ktype 成员被设为 NULL, 而 kset 中的ktype是实际被使用的。
在新的内核里, kset 不再包含一个子系统指针struct subsystem * subsys, 而且subsystem已经被kset取代。
子系统
子系统是对整个内核中一些高级部分的表述。子系统通常(但不一定)出现在 sysfs分层结构中的顶层,内核子系统包括 block_subsys(/sys/block 块设备)、 devices_subsys(/sys/devices 核心设备层)以及内核已知的用于各种总线的特定子系统。
对于新的内核已经不再有subsystem数据结构了,用kset代替了。每个 kset 必须属于一个子系统,子系统成员帮助内核在分层结构中定位 kset 。
|
在新的内核里连以上的subsystem的函数都已经被取消了,完全被kset取代了。其原因可能是因为原来的subsystem的函数根本就是kset函数的简单包装,而在Linux世界里简单就是美,多余的东西被剔出了。
底层sysfs操作
kobject 是在 sysfs 虚拟文件系统后的机制。对每个在 sysfs 中的目录, 在内核中都会有一个 kobject 与之对应。每个 kobject 都输出一个或多个属性, 它在 kobject 的 sysfs 目录中以文件的形式出现, 其中的内容由内核产生。
在 sysfs 中创建kobject的入口是kobject_add的工作的一部分,只要调用 kobject_add 就会在sysfs 中显示,还有些知识值得记住:
(1)kobjects 的 sysfs 入口始终为目录, kobject_add 的调用将在sysfs 中创建一个目录,这个目录包含一个或多个属性(文件);
(2) 分配给 kobject 的名字( 用 kobject_set_name ) 是 sysfs 中的目录名,出现在 sysfs 层次的相同部分的 kobjects 必须有唯一的名字. 分配给 kobjects 的名字也应当是合法的文件名字: 它们不能包含非法字符(如:斜线)且不推荐使用空白。
(3)sysfs 入口位置对应 kobject 的 parent 指针。若 parent 是 NULL ,则它被设置为嵌入到新 kobject 的 kset 中的 kobject;若 parent 和 kset 都是 NULL, 则sysfs 入口目录在顶层,通常不推荐。
当创建kobject 时, 每个 kobject 都被给定一系列默认属性。这些属性保存在 kobj_type 结构中:
|
sysfs 读写这些属性是由 kobj_type->sysfs_ops 成员中的函数完成的:
|
当用户空间读取一个属性时,内核会使用指向 kobject 的指针(kobj)和正确的属性结构(*attr)来调用show 方法,该方法将给定属性值编码进缓冲(buffer)(注意不要越界( PAGE_SIZE 字节)), 并返回实际数据长度。sysfs 的约定要求每个属性应当包含一个单个人眼可读值; 若返回大量信息,需将它分为多个属性.
也可对所有 kobject 关联的属性使用同一个 show 方法,用传递到函数的 attr 指针来判断所请求的属性。有的 show 方法包含对属性名字的检查。有的show 方法会将属性结构嵌入另一个结构, 这个结构包含需要返回属性值的信息,这时可用container_of 获得上层结构的指针以返回属性值的信息。
store 方法将存在缓冲(buffer)的数据( size 为数据的长度,不能超过 PAGE_SIZE )解码并保存新值到属性(*attr), 返回实际解码的字节数。store 方法只在拥有属性的写权限时才能被调用。此时注意:接收来自用户空间的数据一定要验证其合法性。如果到数据不匹配, 返回一个负的错误值。
非默认属性虽然 kobject 类型的 default_attrs 成员描述了所有的 kobject 会拥有的属性,倘若想添加新属性到 kobject 的 sysfs 目录属性只需简单地填充一个attribute结构并传递到以下函数:
|
若要删除属性,调用:
|
二进制属性
sysfs 通常要求所有属性都只包含一个可读文本格式的值,很少需要创建能够处理大量二进制数据的属性。但当在用户空间和设备间传递不可改变的数据时(如上传固件到设备)就需要这个特性。二进制属性使用一个 bin_attribute 结构来描述:
|
符号链接
sysfs 文件系统具有树型结构, 反映 kobject之间的组织层次关系。为了表示驱动程序和所管理的设备间的关系,需要额外的指针,其在 sysfs 中通过符号链接实现。
|
一个热插拔事件是一个从内核空间发送到用户空间的通知, 表明系统配置已经改变. 无论 kobject 被创建或删除,都会产生这种事件。热插拔事件会导致对 /sbin/hotplug 的调用, 它通过加载驱动程序, 创建设备节点, 挂载分区或其他正确动作响应事件。
热插拔事件的实际控制是通过一套存储于 kset_uevent_ops (《LDD3》中介绍的struct kset_hotplug_ops * hotplug_ops;在2.6.22.2中已经被kset_uevent_ops 结构体替换)结构的方法完成:
|
可以在 kset 结构的uevent_ops 成员中找到指向kset_uevent_ops结构的指针。
若在 kobject 中不包含指定的 kset , 内核将通过 parent 指针在分层结构中进行搜索,直到发现一个包含有kset的 kobject ; 接着使用这个 kset 的热插拔操作。
|
(2) 当调用用户空间的热插拔程序时,相关子系统的名字将作为唯一的参数传递给它。name 函数负责返回合适的字符串传递给用户空间的热插拔程序。
(3)热插拔脚本想得到的任何其他参数都通过环境变量传递。uevent 函数的作用是在调用热插拔脚本之前将参数添加到环境变量中。函数原型:
|
热插拔事件的产生通常是由在总线驱动程序层的逻辑所控制。
以上是Linux设备模型的底层原理简介,具体的细节应该参阅内核源码和《ULK3》。
Linux 2.6内核的一个重要特色是提供了统一的内核设备模型。随着技术的不断进步,系统的拓扑结构越来越复杂,对智能电源管理、热插拔以及plug and play的支持要求也越来越高,2.4内核已经难以满足这些需求。为适应这种形势的需要,2.6内核开发了全新的设备模型。
1. Sysfs文件系统
Sysfs文件系统是一个类似于proc文件系统的特殊文件系统,用于将系统中的设备组织成层次结构,并向用户模式程序提供详细的内核数据结构信息。其顶层目录主要有: