Linux设备驱动程序学习笔记

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 在中定义,是一个指向struct task_struct的指针,而task_struct结构在中定义。

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)

{

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);

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是一个32位的数,12位表示主设备号,20为表示次设备号。
在实际使用中,是通过中定义的宏来转换格式。

 (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)

{

dev=MKDEV(scull_major,scull_minor);

register_chrdev_region(dev,scull_nr_devs,"scull");

}else

{

result=alloc_chrdev_region(&dev,scull_minor,scull_nr_devs,"scull");

scull_major=MAJOR(dev);

}

if(result=0)

{

printk(KERN_WARNING "scull:can not get scull major %d",scull_major);

return result;

}

在这部分中,比较重要的是在用函数获取设备编号后,其中的参数name是和该编号范围关联的设备名称,它将出现在/proc/devicessysfs中。

看到这里,就可以理解为什么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和release

open方法

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宏实现:

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函数很简单。但是其中用到了定义在中的container_of宏,源码如下:

#define container_of(ptr, type, member) ({            \
    const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
    (type *)( (char *)__mptr - offsetof(type,member) );})

其实从源码可以看出,其作用就是:通过指针ptr,获得包含ptr所指向数据(是member结构体)type结构体的指针。即是用指针得到另外一个指针。

 

release方法提供释放内存,关闭设备的功能。应完成的工作如下:

(1)释放由open分配的、保存在file->private_data中的所有内容;

(2)在最后一次关闭操作时关闭设备。

由于前面定义了scull是一个全局且持久的内存区,所以他的release什么都不做。

 

 

read和write

 read和write方法的主要作用就是实现内核与用户空间之间的数据拷贝。因为Linux的内核空间和用户空间隔离的,所以要实现数据拷贝就必须使用在中定义的

unsigned long copy_to_user(void __user *to,
                           const void *from,
                           unsigned long count);
unsigned long copy_from_user(void *to,
                             const void __user *from,
                             unsigned long count);

而值得一提的是以上两个函数和

#define __copy_from_user(to,from,n)    (memcpy(to, (void __force *)from, n), 0)
#define __copy_to_user(to,from,n)    (memcpy((void __force *)to, from, n), 0)

之间的关系:通过源码可知,前者调用后者,但前者在调用前对用户空间指针进行了检查。

至于read和write 的具体函数比较简单,就在实验中验证好了。

并发与静态

一、并发及其管理
竞态通常是作为对资源的共享访问结果而产生的。
在设计自己的驱动程序时,第一个要记住的规则是:只要可能,就应该避免资源的共享。若没有并发访问,就不会有竞态。这种思想的最明显的应用是避免使用全局变量。
 
但是,资源的共享是不可避免的 ,如硬件资源本质上就是共享、指针传递等等。
资源共享的硬性规则:
(1)在单个执行线程之外共享硬件或软件资源的任何时候,因为另外一个线程可能产生对该资源的不一致观察,因此必须显示地管理对该资源的访问。--访问管理的常见技术成为“锁定”或者“互斥”:确保一次只有一个执行线程可操作共享资源。
(2)当内核代码创建了一个可能和其他内核部分共享的对象时,该对象必须在还有其他组件引用自己时保持存在(并正确工作)。对象尚不能正确工作时,不能将其对内核可用。
 
二、信号量和互斥体
一个信号量(semaphore: 旗语,信号灯)本质上是一个整数值,它和一对函数联合使用,这一对函数通常称为P和V。希望进入临届区的进程将在相关信号量上调用P;如果信号量的值大于 零,则该值会减小一,而进程可以继续。相反,如果信号量的值为零(或更小),进程必须等待知道其他人释放该信号。对信号量的解锁通过调用V完成;该函数增 加信号量的值,并在必要时唤醒等待的进程。
当信号量用于互斥时(即避免多个进程同是在一个临界区运行),信号量的值应初始化为1。这种信号量在任何给定时刻只能由单个进程或线程拥有。在这种使用模式下,一个信号量有事也称为一个“互斥体(mutex)”,它是互斥(mutual exclusion)的简称。Linux内核中几乎所有的信号量均用于互斥
使用信号量,内核代码必须包含
 
信号量的创建与初始化
1.void sema_init(struct semaphore *sem,int val);
   其中val是赋予一个信号量的初始值
2.DECLARE_MUTEX(name);
   DECLARE_MUTEX_LOCKED(name);
   上面两个宏的结果是:一个称为name的信号量变量被初始化为1(使用DECLARE_MUTEX)或者0  (DECLARE_MUTEX_LOCKED)。
    void init_MUTEX(struct semaphore *sem);
    void init_MUTEX_LOCKED(struct semaphore *sem);
 
P函数为:

void down(struct semaphore*sem);/*不推荐使用,会建立不可杀进程*/
int down_interruptible(struct semaphore*sem);/*推荐使用,使用down_interruptible需要格外小心,若操作被中断,该函数会返回非零值,而调用这不会拥有该信号量。对down_interruptible的正确使用需要始终检查返回值,并做出相应的响应。*/
int down_trylock(struct semaphore*sem);/*带有“_trylock”的永不休眠,若信号量在调用是不可获得,会返回非零值。*/

 
V函数为:

void up(struct semaphore*sem);/*任何拿到信号量的线程都必须通过一次(只有一次)对up的调用而释放该信号量。在出错时,要特别小心;若在拥有一个信号量时发生错误,必须在将错误状态返回前释放信号量。*/

 
 
在scull中使用信号量
其实在之前的实验中已经用到了信号量的代码,在这里提一下应该注意的地方:
在初始化scull_dev的地方:

/* Initialize each device. */
    for (i = 0; i < scull_nr_devs; i++) {
        scull_devices[i].quantum = scull_quantum;
        scull_devices[i].qset = scull_qset;
        init_MUTEX(&scull_devices[i].sem);/* 注意顺序:先初始化好互斥信号量 ,再使scull_devices可用。*/
        scull_setup_cdev(&scull_devices[i], i);
    }

而且要确保在不拥有信号量的时候不会访问scull_dev结构体。

completion


completion是一种轻量级的机制,它允许一个线程告诉另一个线程某个工作已经完成。代码必须包含。使用的代码如下:

 

DECLARE_COMPLETION(my_completion);/* 创建completion(声明+初始化) */

/

struct completion my_completion;/* 动态声明completion 结构体*/
static inline void init_completion(&my_completion);/*动态初始化completion*/

///

void wait_for_completion(struct completion*c);/* 等待completion */
void complete(struct completion*c);/*唤醒一个等待completion的线程*/
void complete_all(struct completion*c);/*唤醒所有等待completion的线程*/

/*如果未使用completion_all,completion可重复使用;否则必须使用以下函数重新初始化completion*/
INIT_COMPLETION(struct completion c);/*快速重新初始化completion*/


  completion的典型应用是模块退出时的内核线程终止。在这种运行中,某些驱动程序的内部工作有一个内核线程在while(1)循环中完成。当内核准备清除该模块时,exit函数会告诉该线程退出并等待completion。为此内核包含了用于这种线程的一个特殊函数:

void complete_and_exit(struct completion*c,long retval);

 
 
三、自旋锁
其实上面介绍的几种信号量和互斥机制,其底层源码对于自身结构体的某些变量的维护也用到了现在我们讲到的自旋锁,但是绝不是自旋锁的再包装。

自旋锁是一个互斥设备,他只能会两个值:“锁定”和“解锁”。它通常实现为某个整数之中的单个位。
“测试并设置”的操作必须以原子方式完成。
任何时候,只要内核代码拥有自旋锁,在相关CPU上的抢占就会被禁止。
适用于自旋锁的核心规则:
(1)任何拥有自旋锁的代码都必须使原子的,除 服务中断外(某些情况下也不能放弃CPU,如中断服务也要获得自旋锁。为了避免这种锁陷阱,需要在拥有自旋锁时禁止中断),不能放弃CPU(如休眠,休眠 可发生在许多无法预期的地方)。否则CPU将有可能永远自旋下去(死机)。
(2)拥有自旋锁的时间越短越好。
自旋锁原语所需包含的文件是 ,以下是自旋锁的内核API: 

spinlock_t my_lock= SPIN_LOCK_UNLOCKED;/* 编译时初始化spinlock*/
void spin_lock_init(spinlock_t*lock);/* 运行时初始化spinlock*/

/* 所有spinlock等待本质上是不可中断的,一旦调用spin_lock,在获得锁之前一直处于自旋状态*/
void spin_lock(spinlock_t*lock);/* 获得spinlock*/
void spin_lock_irqsave(spinlock_t*lock,unsigned long flags);/* 获得spinlock,禁止本地cpu中断,保存中断标志于flags*/
void spin_lock_irq(spinlock_t*lock);/* 获得spinlock,禁止本地cpu中断*/
void spin_lock_bh(spinlock_t*lock)/* 获得spinlock,禁止软件中断,保持硬件中断打开*/

/* 以下是对应的锁释放函数*/
void spin_unlock(spinlock_t*lock);
void spin_unlock_irqrestore(spinlock_t*lock,unsigned long flags);
void spin_unlock_irq(spinlock_t*lock);
void spin_unlock_bh(spinlock_t*lock);

/* 以下非阻塞自旋锁函数,成功获得,返回非零值;否则返回零*/
int spin_trylock(spinlock_t*lock);
int spin_trylock_bh(spinlock_t*lock);


/*新内核的包含了更多函数*/

 
读取者/写入者自旋锁:

rwlock_t my_rwlock= RW_LOCK_UNLOCKED;/* 编译时初始化*/


rwlock_t my_rwlock;
rwlock_init(&my_rwlock);/* 运行时初始化*/


void read_lock(rwlock_t*lock);
void read_lock_irqsave(rwlock_t*lock,unsigned long flags);
void read_lock_irq(rwlock_t*lock);
void read_lock_bh(rwlock_t*lock);


void read_unlock(rwlock_t*lock);
void read_unlock_irqrestore(rwlock_t*lock,unsigned long flags);
void read_unlock_irq(rwlock_t*lock);
void read_unlock_bh(rwlock_t*lock);

/* 新内核已经有了read_trylock*/

void write_lock(rwlock_t*lock);
void write_lock_irqsave(rwlock_t*lock,unsigned long flags);
void write_lock_irq(rwlock_t*lock);
void write_lock_bh(rwlock_t*lock);
int write_trylock(rwlock_t*lock);


void write_unlock(rwlock_t*lock);
void write_unlock_irqrestore(rwlock_t*lock,unsigned long flags);
void write_unlock_irq(rwlock_t*lock);
void write_unlock_bh(rwlock_t*lock);

/*新内核的包含了更多函数*/

 
锁陷阱
锁定模式必须在一开始就安排好,否则其后的改进将会非常困难。
不明确规则:如果某个获得锁的函数要调用其他同样试图获取这个锁的函数,代码就会锁死。(不允许锁的拥有者第二次获得同个锁。)为了锁的正确工作,不得不编写一些函数,这些函数假定调用这已经获得了相关的锁。
锁的顺序规则:再必须获取多个锁时,应始终以相同顺序获取。
若必须获得一个局部锁和一个属于内核更中心位置的锁,应先获得局部锁。
若我们拥有信号量和自旋锁的组合,必须先获得信号量。
不得再拥有自旋锁时调用down。(可导致休眠)
尽量避免需要多个锁的情况。
细颗粒度和粗颗粒度的对比:应该在最初使用粗颗粒度的锁,除非有真正的原因相信竞争会导致问题。
 
四、锁之外的办法
 
(1)免锁算法
经常用于免锁的生产者/消费者任务的数据结构之一是 循环缓冲区。它在设备驱动程序中相当普遍,如以前移植的网卡驱动程序。内核里有一个通用的循环缓冲区的实现在
 
(2)原子变量
完整的锁机制对一个简单的整数来讲显得浪费。内核提供了一种原子的整数类型,称为atomic_t,定义在 原子变量操作是非常快的, 因为它们在任何可能时编译成一条单个机器指令。
以下是其接口函数:

void atomic_set(atomic_t*v,int i);/*设置原子变量 v 为整数值 i.*/
atomic_t v = ATOMIC_INIT(0);  /*编译时使用宏定义 ATOMIC_INIT 初始化原子值.*/

int atomic_read(atomic_t*v);/*返回 v 的当前值.*/

void atomic_add(int i, atomic_t*v);/*由 v 指向的原子变量加 i. 返回值是 void*/
void atomic_sub(int i, atomic_t*v);/*从 *v 减去 i.*/

void atomic_inc(atomic_t*v);
void atomic_dec(atomic_t*v);/*递增或递减一个原子变量.*/

int atomic_inc_and_test(atomic_t*v);
int atomic_dec_and_test(atomic_t*v);
int atomic_sub_and_test(int i, atomic_t*v);
/*进行一个特定的操作并且测试结果; 如果, 在操作后, 原子值是 0, 那么返回值是真; 否则, 它是假. 注意没有 atomic_add_and_test.*/

int atomic_add_negative(int i, atomic_t*v);
/*加整数变量 i 到 v. 如果结果是负值返回值是真, 否则为假.*/

int atomic_add_return(int i, atomic_t*v);
int atomic_sub_return(int i, atomic_t*v);
int atomic_inc_return(atomic_t*v);
int atomic_dec_return(atomic_t*v);
/*像 atomic_add 和其类似函数, 除了它们返回原子变量的新值给调用者.*/

atomic_t 数据项必须通过这些函数存取。 如果你传递一个原子项给一个期望一个整数参数的函数, 你会得到一个编译错误。需要多个 atomic_t 变量的操作仍然需要某种其他种类的加锁。
 
(3)位操作
内核提供了一套函数来原子地修改或测试单个位。原子位操作非常快, 因为它们使用单个机器指令来进行操作, 而在任何时候低层平台做的时候不用禁止中断. 函数是体系依赖的并且在 中声明. 以下函数中的数据是体系依赖的. nr 参数(描述要操作哪个位)在ARM体系中定义为unsigned int

void set_bit(nr,void*addr);/*设置第 nr 位在 addr 指向的数据项中。*/

void clear_bit(nr,void*addr);/*清除指定位在 addr 处的无符号长型数据.*/

void change_bit(nr,void*addr);/*翻转nr位.*/

test_bit(nr, void *addr);/*这个函数是唯一一个不需要是原子的位操作; 它简单地返回这个位的当前值.*/

/*以下原子操作如同前面列出的, 除了它们还返回这个位以前的值.*/

int test_and_set_bit(nr,void*addr);
int test_and_clear_bit(nr,void*addr);
int test_and_change_bit(nr,void*addr);

 
以下是一个使用范例:

/* try to set lock */
while (test_and_set_bit(nr, addr)!= 0)
    wait_for_a_while();

/* do your work */

/* release lock, and check. */
if (test_and_clear_bit(nr, addr)== 0)
    something_went_wrong();/* already released: error */

 
(4)seqlock
2.6内核包含了一对新机制打算来提供快速地, 无锁地存取一个共享资源。 seqlock要保护的资源小, 简单, 并且常常被存取, 并且很少写存取但是必须要快。seqlock 通常不能用在保护包含指针的数据结构。seqlock 定义在

/*两种初始化方法*/
seqlock_t lock1 = SEQLOCK_UNLOCKED;

seqlock_t lock2;
seqlock_init(&lock2);

这个类型的锁常常用在保护某种简单计算,读存取通过在进入临界区入口获取一个(无符号的)整数序列来工作. 在退出时, 那个序列值与当前值比较; 如果不匹配, 读存取必须重试.读者代码形式: 

unsignedint seq;
do {
    seq = read_seqbegin(&the_lock);
    /* Do what you need to do */
} while read_seqretry(&the_lock, seq);

 如果你的 seqlock 可能从一个中断处理里存取, 你应当使用 IRQ 安全的版本来代替:

unsignedint read_seqbegin_irqsave(seqlock_t*lock,unsigned long flags);
int read_seqretry_irqrestore(seqlock_t*lock,unsigned int seq,unsignedlong flags);

写者必须获取一个排他锁来进入由一个 seqlock 保护的临界区,写锁由一个自旋锁实现, 调用:

void write_seqlock(seqlock_t*lock);
void write_sequnlock(seqlock_t*lock);

因为自旋锁用来控制写存取, 所有通常的变体都可用:

void write_seqlock_irqsave(seqlock_t*lock,unsigned long flags);
void write_seqlock_irq(seqlock_t*lock);
void write_seqlock_bh(seqlock_t*lock);

void write_sequnlock_irqrestore(seqlock_t*lock,unsigned long flags);
void write_sequnlock_irq(seqlock_t*lock);
void write_sequnlock_bh(seqlock_t*lock);

 还有一个 write_tryseqlock 在它能够获得锁时返回非零.

(5)读取-复制-更新

读取-拷贝-更新(RCU) 是一个高级的互斥方法, 在合适的情况下能够有高效率. 它在驱动中的使用很少。

高级字符驱动程序操作--ioctl

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是个指针,麻烦点,需检测后才能用

分析:使用指针,首先得保证指针指向的地址合法。因此,在使用这个指针之前,我们应该使用

中声明的  int  access_ok(int type,const void *addr,unsigend long size)  返回值为1(成功)或0(失败),如果返回失败,驱动程序通常返回-EFAULT给调用者。

来验证地址的合法性。

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;

 

CAP_DAC_OVERRIDE/*越过在文件和目录上的访问限制(数据访问控制或 DAC)的能力。*/

CAP_NET_ADMIN /*进行网络管理任务的能力, 包括那些能够影响网络接口的任务*/

CAP_SYS_MODULE /*加载或去除内核模块的能力*/

CAP_SYS_RAWIO /*进行 "raw"(裸)I/O 操作的能力. 例子包括存取设备端口或者直接和 USB 设备通讯*/

CAP_SYS_ADMIN /*截获的能力, 提供对许多系统管理操作的途径*/

CAP_SYS_TTY_CONFIG /*执行 tty 配置任务的能力*/

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_user

   break;

   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;

}

高级字符驱动学习--阻塞型I/0


提出问题:若驱动程序无法立即满足请求,该如何响应? 比如:当数据不可用时调用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)

{

pirntk(KERN_DEBUG "process %i (%s) going to sleep\n",current->pid,current->comm);

wait_event_interruptible(wq,flag!=0);

flag=0;

printk(KERN_DEBUG "awoken %i (%s) \n",current->pid,current->comm);

return 0;

}

 

ssize_t sleepy_write(struct file *filp,const char __user *buf,size_t count,loff_t *pos)

{

printk(KERN_DEBUG "process %i (%s) awakening the readers ...\n",current->pid,current->comm);

flag=1;

wake_up_interruptible(&wq);

return count;  /*成功并避免重试*/

}

 

 

阻塞与非阻塞类操作

小知识点:

操作系统中睡眠、阻塞、挂起的区别形象解释
操作系统中睡眠、阻塞、挂起的区别形象解释
首先这些术语都是对于线程来说的。对线程的控制就好比你控制了一个雇工为你干活。你对雇工的控制是通过编程来实现的。
挂起线程的意思就是你对主动对雇工说:“你睡觉去吧,用着你的时候我主动去叫你,然后接着干活”。
使线程睡眠的意思就是你主动对雇工说:“你睡觉去吧,某时某刻过来报到,然后接着干活”。
线程阻塞的意思就是,你突然发现,你的雇工不知道在什么时候没经过你允许,自己睡觉呢,但是你不能 怪雇工,肯定你这个雇主没注意,本来你让雇工扫地,结果扫帚被偷了或被邻居家借去了,你又没让雇工继续干别的活,他就只好睡觉了。至于扫帚回来后,雇工会 不会知道,会不会继续干活,你不用担心,雇工一旦发现扫帚回来了,他就会自己去干活的。因为雇工受过良好的培训。这个培训机构就是操作系统。

全功能的 read 和 write 方法涉及到进程可以决定是进行非阻塞 I/O还是阻塞 I/O操作。明确的非阻塞 I/O 由 filp->f_flags 中的 O_NONBLOCK 标志来指示(定义再 ,被自动包含)。浏览源码,会发现O_NONBLOCK 的另一个名字:O_NDELAY ,这是为了兼容 System V 代码。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)

{

struct scull_pipe *dev = filp->private_data;

if(down_interruptible(&dev))

     return -ERRESTARTSYS;

while(dev->rp == dev->wp){/*无数据可读*/

up(&dev->sem); /*释放锁*/

if(filp->f_flags & O_NONBLOCK)

       return -EAGAIN;

PDEBUG("\“ %s ”\  reading: going to sleep\n",current->comm);

if(wait_event_interruptible(dev-inq,(dev->rp!=dev-wp)))

        return -ERESTARTSYS;

}

/*数据已就绪,返回*/

if(dev->wp > dev->rp)

    count=min(count,(size_t) (dev->wp - dev->rp));

else  /*写入指针回卷,返回数据直到dev->end*/

    count = min(count,(size_t)(dev->end - dev->rp))

if(copy_to_user(buf,dev->rp,count)){

up(&dev->sem);

return -EFAULT;

}

dev->rp += count;

if(dev->rp == dev->end)

    dev->rp = dev->buffer;/*回卷*/

up(&dev->sem);

/*最后,唤醒所有写入者并返回*/

wake_up_interruptible(&dev->outq);

PDEBUG("\%s\" did read %li bytes \n ",current->comm,(long)count");

return count;

}

 代码分析:

while循环在拥有设备信号量时测试缓冲区。如果其中有数据,则可以立即将数据返回给用户而不需要休眠,这样,整个循环体就被跳 过了。相反,如果缓冲区为空,则必须休眠。但在休眠之前必须释放设备信号量,因为如果在拥有该信号量时休眠,任何写入者都没有机会来唤醒。在释放信号量之 后,快速检测用户请求的是否是非阻塞I/O,如果是,则返回,否则调用wait_event_interruptible。

 

高级休眠:

进程休眠步骤:

1.分配并初始化一个wait_queue_t结构,然后将其加入到对应的等待队列

2.设置进程的状态,将其标记为休眠在 中定义有几个任务状态:TASK_RUNNING意思是进程可运行。有 2 个状态指示一个进程是在睡眠:TASK_INTERRUPTIBLE TASK_UNTINTERRUPTIBLE

 

2.6 内核的驱动代码通常不需要直接操作进程状态。但如果需要这样做使用的代码是:

void set_current_state(int new_state);

在老的代码中, 你常常见到如此的东西:current->state = TASK_INTERRUPTIBLE; 但是象这样直接改变 current 是不推荐的,当数据结构改变时这样的代码将会失效。通过改变 current 状态,只改变了调度器对待进程的方式,但进程还未让出处理器。

3.最后一步:释放处理器。但之前我们必须首先检查休眠等待的条件。如果不做这个检查,可能会引入竞态:如果在忙于上面的这个过程时有其他的线程刚刚试图唤醒你,你可能错过唤醒且长时间休眠。因此典型的代码下:

if (!condition)
    schedule();

如果代码只是从 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)当资源可用时,唤醒一个进程就足够来完全消耗资源。

使一个进程进入独占等待,可调用:

void prepare_to_wait_exclusive(wait_queue_head_t *queue, wait_queue_t *wait, int state);

 注意:无法使用 wait_event 和它的变体来进行独占等待.

 

唤醒的相关函数

很少会需要调用wake_up_interruptible 之外的唤醒函数,但为完整起见,这里是整个集合:

wake_up(wait_queue_head_t*queue);
wake_up_interruptible(wait_queue_head_t *queue);
/*wake_up 唤醒队列中的每个非独占等待进程和一个独占等待进程。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_all(wait_queue_head_t *queue);
/*这种 wake_up 唤醒所有的进程, 不管它们是否进行独占等待(可中断的类型仍然跳过在做不可中断等待的进程)*/
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类似

 

使用举例:

static unsigned int scull_p_poll(struct file *filp, poll_table *wait)
{
        struct scull_pipe *dev = filp->private_data;
        unsigned int mask = 0;
        /*
        * The buffer is circular; it is considered full
        * if "wp" is right behind "rp" and empty if the
        * two are equal.
        */
        down(&dev->sem);
        poll_wait(filp, &dev->inq, wait);
        poll_wait(filp, &dev->outq, wait);
        if (dev->rp != dev->wp)
                mask |= POLLIN | POLLRDNORM; /* readable */
        if (spacefree(dev))
                mask |= POLLOUT | POLLWRNORM; /* writable */
        up(&dev->sem);
        return mask;
}

 

与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 是否被设置,即便这需要一些时间。其原型是:

int(*fsync)(structfile*file,struct dentry*dentry,int datasync);

 

 

底层的数据结构

 

异步通知

通过使用异步通知,应用程序可以在数据可用时收到一个信号,而无需不停地轮询。

启用步骤:

(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 提供的通用方法是基于一个数据结构和两个函数,定义在

 

数据结构:

struct fasync_struct{
    int    magic;
    int    fa_fd;
    struct    fasync_struct    *fa_next;/* singly linked list */
    struct    file         *fa_file;
};

 

 

 驱动调用的两个函数的原型:

int fasync_helper(int fd,structfile*filp,int mode, struct fasync_struct**fa);
void kill_fasync(struct fasync_struct**fa,int sig, int band);

当一个打开的文件的FASYNC标志被修改时,调用 fasync_helper 来从相关的进程列表中添加或去除文件。除了最后一个参数, 其他所有参数都时被提供给 fasync 方法的相同参数并被直接传递。 当数据到达时,kill_fasync 被用来通知相关的进程,它的参数是被传递的信号(常常是 SIGIO)和 band(几乎都是 POLL_IN)。

 

这是 scullpipe 实现 fasync 方法的:

staticint scull_p_fasync(int fd,struct file*filp,int mode)
{
 struct scull_pipe *dev = filp->private_data;
 return fasync_helper(fd, filp, mode,&dev->async_queue);
}

当数据到达, 下面的语句必须被执行来通知异步读者. 因为对 sucllpipe 读者的新数据通过一个发出 write 的进程被产生, 这个语句出现在 scullpipe 的 write 方法中:

       
       
        
  
    
    
    
    

if (dev->async_queue)  kill_fasync(&dev->async_queue, SIGIO, POLL_IN); /* 注意, 一些设备也针对设备可写而实现了异步通知,在这个情况,kill_fasnyc 必须以 POLL_OUT 模式调用.*/

当文件被关闭时必须 调用fasync 方法,来从活动的异步读取进程列表中删除该文件。尽管这个调用仅当 filp->f_flags 被设置为 FASYNC 时才需要,但不管什么情况,调用这个函数不会有问题,并且是普遍的实现方法。 以下是 scullpipe 的 release 方法的一部分:

       
       
        
  
    
    
    
    

/* remove this filp from the asynchronously notified filp's */ scull_p_fasync(-1, filp, 0);

异步通知使用的数据结构和 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 {
    time_t    tv_sec;        /* seconds */
    long    tv_nsec;    /* nanoseconds */
};

struct timeval {
    time_t        tv_sec;        /* seconds */
    SUSEconds_t    tv_usec;    /* microseconds */
};

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);

 

为了处理绝对时间, 导出了 do_gettimeofday 函数,它填充一个指向 struct timeval 的指针变量。绝对时间也可来自 xtime 变量,一个 struct timespec 值,为了原子地访问它,内核提供了函数 current_kernel_time。它们的精确度由硬件决定,原型是:

#include
void do_gettimeofday(structtimeval*tv);
struct timespec current_kernel_time(void);

/*得到的数据都表示当前时间距UNIX时间基准1970-01-01 00:00:00的相对时间*/

以上两个函数在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 计数器的循环。这种忙等待实现的代码如下:

while(time_before(jiffies, j1))
    cpu_relax();

对 cpu_relex 的调用将以体系相关的方式执行,在许多系统中它根本不做任何事,这个方法应当明确地避免。对于ARM体系来说:

#define cpu_relax()            barrier()

也就是说在ARM上运行忙等待相当于:

while(time_before(jiffies, j1)) ;

这种忙等待严重地降低了系统性能。如果未配置内核为抢占式, 这个循环在延时期间完全锁住了处理器,计算机直到时间 j1 到时会完全死掉。如果运行一个可抢占的内核时会改善一点,但是忙等待在可抢占系统中仍然是浪费资源的。更糟的是, 当进入循环时如果中断碰巧被禁止, jiffies 将不会被更新, 并且 while 条件永远保持真,运行一个抢占的内核也不会有帮助, 唯一的解决方法是重启。

 

让出处理器

忙等待加重了系统负载,必须找出一个更好的技术:不需要CPU时释放CPU 。 这可通过调用schedule函数实现(在 中声明):

while(time_before(jiffies, j1)){
    schedule();
}

在计算机空闲时运行空闲任务(进程号 0, 由于历史原因也称为swapper)可减轻处理器工作负载、降低温度、增加寿命。

 

超时

实现延迟的最好方法应该是让内核为我们完成相应的工作。

(1)若驱动使用一个等待队列来等待某些其他事件,并想确保它在一个特定时间段内运行,可使用:

#include
long wait_event_timeout(wait_queue_head_t q, condition,long timeout);
long wait_event_interruptible_timeout(wait_queue_head_t q, condition,long timeout);
/*这些函数在给定队列上睡眠, 但是它们在超时(以 jiffies 表示)到后返回。如果超时,函数返回 0; 如果这个进程被其他事件唤醒,则返回以 jiffies 表示的剩余的延迟实现;返回值从不会是负值*/

(2)为了实现进程在超时到期时被唤醒而又不等待特定事件(避免声明和使用一个多余的等待队列头),内核提供了 schedule_timeout 函数:

 

#include
signed long schedule_timeout(signedlong timeout);

/*timeout 是要延时的 jiffies 数。除非这个函数在给定的 timeout 流失前返回,否则返回值是 0 。schedule_timeout 要求调用者首先设置当前的进程状态。为获得一个不可中断的延迟, 可使用 TASK_UNINTERRUPTIBLE 代替。如果你忘记改变当前进程的状态, 调用 schedule_time 如同调用 shcedule,建立一个不用的定时器。一个典型调用如下:*/
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout (delay);

 

短延迟

当一个设备驱动需要处理硬件的延迟(latency潜伏期), 涉及到的延时通常最多几个毫秒,在这个情况下, 不应依靠时钟嘀哒,而是内核函数 ndelay, udelay和 mdelay ,他们分别延后执行指定的纳秒数, 微秒数或者毫秒数,定义在 ,原型如下:

#include
void ndelay(unsignedlong nsecs);
void udelay(unsignedlong usecs);
void mdelay(unsignedlong msecs);

重要的是记住这 3 个延时函数是忙等待; 其他任务在时间流失时不能运行。每个体系都实现 udelay, 但是其他的函数可能未定义; 如果它们没有定义, 提供一个缺省的基于 udelay 的版本。在所有的情况中, 获得的延时至少是要求的值, 但可能更多。udelay 的实现使用一个软件循环, 它基于在启动时计算的处理器速度和使用整数变量 loos_per_jiffy确定循环次数。

为避免在循环计算中整数溢出, 传递给udelay 和 ndelay的值有一个上限,如果你的模块无法加载和显示一个未解决的符号:__bad_udelay, 这意味着你调用 udleay时使用太大的参数。
作为一个通用的规则:若试图延时几千纳秒, 应使用 udelay 而不是 ndelay; 类似地, 毫秒规模的延时应当使用 mdelay 完成而不是一个更细粒度的函数。

有另一个方法获得毫秒(和更长)延时而不用涉及到忙等待的方法是使用以下函数(在 中声明):

void msleep(unsignedint millisecs);
unsigned long msleep_interruptible(unsignedint millisecs);
void ssleep(unsignedint seconds)

若能够容忍比请求的更长的延时,应使用 schedule_timeout, msleep 或 ssleep。

 

内核定时器

当需要调度一个以后发生的动作, 而在到达该时间点时不阻塞当前进程, 则可使用内核定时器。内核定时器用来调度一个函数在将来一个特定的时间(基于时钟嘀哒)执行,从而可完成各类任务。
内核定时器是一个数据结构, 它告诉内核在一个用户定义的时间点使用用户定义的参数执行一个用户定义的函数,函数位于 和 kernel/timer.c 。被调度运行的函数几乎确定不会在注册它们的进程在运行时运行,而是异步运行。实际上, 内核定时器通常被作为一个"软件中断"的结果而实现。当在进程上下文之外(即在中断上下文)中运行程序时, 必须遵守下列规则:

(1)不允许访问用户空间;
(2)current 指针在原子态没有意义;
(3)不能进行睡眠或者调度. 例如:调用 kmalloc(..., GFP_KERNEL) 是非法的,信号量也不能使用因为它们可能睡眠。


通过调用函数 in_interrupt()能够告知是否它在中断上下文中运行,无需参数并如果处理器当前在中断上下文运行就返回非零。
通过调用函数 in_atomic()能够告知调度是否被禁止,若调度被禁止返回非零; 调度被禁止包含硬件和软件中断上下文以及任何持有自旋锁的时候。

在后一种情况, current 可能是有效的,但是访问用户空间是被禁止的,因为它能导致调度发生. 当使用 in_interrupt()时,都应考虑是否真正该使用的是 in_atomic 。他们都在 中声明。

内核定时器的另一个重要特性是任务可以注册它本身在后面时间重新运行,因为每个 timer_list 结构都会在运行前从激活的定时器链表中去连接,因此能够立即链入其他的链表。一个重新注册它自己的定时器一直运行在同一个 CPU.

即便在一个单处理器系统,定时器是一个潜在的态源,这是异步运行直接结果。因此任何被定时器函数访问的数据结构应当通过原子类型或自旋锁被保护,避免并发访问

定时器 API

内核提供给驱动许多函数来声明、注册以及删除内核定时器:


#include
struct timer_list {
    struct list_head entry;
    unsigned long expires;/*期望定时器运行的绝对 jiffies 值,不是一个 jiffies_64 值,因为定时器不被期望在将来很久到时*/
    void (*function)(unsigned long); /*期望调用的函数*/
    unsigned long data;/*传递给函数的参数,若需要在参数中传递多个数据项,可以将它们捆绑成单个数据结构并且将它的指针强制转换为 unsiged long 的指针传入。这种做法在所有支持的体系上都是安全的并且在内存管理中相当普遍*/
    struct tvec_t_base_s *base;
#ifdef CONFIG_TIMER_STATS
    void *start_site;
    char start_comm[16];
    int start_pid;
#endif
};
/*这个结构必须在使用前初始化,以保证所有的成员被正确建立(包括那些对调用者不透明的初始化):*/
void init_timer(struct timer_list *timer);
struct timer_list TIMER_INITIALIZER(_function, _expires, _data);
/*在初始化后和调用 add_timer 前,可以改变 3 个公共成员:expires、function和data*/
void add_timer(struct timer_list * timer);
int del_timer(struct timer_list * timer);/*在到时前禁止一个已注册的定时器*/
int del_timer_sync(struct timer_list *timer); /*如同 del_timer ,但还保证当它返回时, 定时器函数不在任何 CPU 上运行,以避免在 SMP 系统上竞态, 并且在 单处理器内核中和 del_timer 相同。这个函数应当在大部分情况下优先考虑。 如果它被从非原子上下文调用, 这个函数可能睡眠,但是在其他情况下会忙等待。当持有锁时要小心调用 del_timer_sync ,如果这个定时器函数试图获得同一个锁, 系统会死锁。如果定时器函数重新注册自己, 调用者必须首先确保这个重新注册不会发生; 这通常通过设置一个" 关闭 "标志来实现, 这个标志被定时器函数检查*/
int mod_timer(struct timer_list *timer, unsigned long expires); /*更新一个定时器的超时时间, 常用于超时定时器。也可在正常使用 add_timer时在不活动的定时器上调用mod_timer*/
int timer_pending(const struct timer_list * timer); /*通过调用timer_list结构中一个不可见的成员,返回定时器是否在被调度运行*/

内核定时器的实现《LDD3》介绍的比较笼统,以后看《ULK3》的时候再细细研究。

 一个内核定时器还远未完善,因为它受到 jitter 、硬件中断,还有其他定时器和其他异步任务的影响。虽然一个简单数字 I/O关联的定时器对简单任务是足够的,但不合适在工业环境中的生产系统,对于这样的任务,你将最可能需要实时内核扩展(RT-Linux).

tasklet,工作队列,共享队列

tasklet

tasklet  vs  内核定时器

相同:始终在中断期间运行,始终会在调度他们的同一CPU上运行,而且都接收一个unsigned long参数

不同:不可以要求tasklet在某一给定的时间执行

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) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }

void tasklet_disable(struct tasklet_struct *t);
/*函数暂时禁止给定的 tasklet被 tasklet_schedule 调度,直到这个 tasklet 被再次被enable;若这个 tasklet 当前在运行, 这个函数忙等待直到这个tasklet退出*/


void tasklet_disable_nosync(struct tasklet_struct *t);
/*和tasklet_disable类似,但是tasklet可能仍然运行在另一个 CPU */


void tasklet_enable(struct tasklet_struct *t);
/*使能一个之前被disable的 tasklet;若这个 tasklet 已经被调度, 它会很快运行。 tasklet_enable 和tasklet_disable必须匹配调用, 因为内核跟踪每个 tasklet 的"禁止次数"*/


void tasklet_schedule(struct tasklet_struct *t);
/*调度 tasklet 执行,如果tasklet在运行中被调度, 它在完成后会再次运行; 这保证了在其他事件被处理当中发生的事件受到应有的注意. 这个做法也允许一个 tasklet 重新调度它自己*/


void tasklet_hi_schedule(struct tasklet_struct *t);
/*和tasklet_schedule类似,只是在更高优先级执行。当软中断处理运行时, 它处理高优先级 tasklet 在其他软中断之前,只有具有低响应周期要求的驱动才应使用这个函数, 可避免其他软件中断处理引入的附加周期*/


void tasklet_kill(struct tasklet_struct *t);
/*确 保了 tasklet 不会被再次调度来运行,通常当一个设备正被关闭或者模块卸载时被调用。如果 tasklet 正在运行, 这个函数等待直到它执行完毕。若 tasklet 重新调度它自己,则必须阻止在调用 tasklet_kill 前它重新调度它自己,如同使用 del_timer_sync*/

 

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);
struct workqueue_struct *create_singlethread_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 {
    atomic_long_t data;
#define WORK_STRUCT_PENDING 0        /* T if work item pending execution */
#define WORK_STRUCT_FLAG_MASK (3UL)
#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
    struct list_head entry;
    work_func_t func;
};

struct delayed_work {
    struct work_struct work;
    struct timer_list timer;
};

DECLARE_WORK(n, f)    
/*n 是声明的work_struct结构名称, f是要从工作队列被调用的函数*/
DECLARE_DELAYED_WORK(n, f)
/*n是声明的delayed_work结构名称, f是要从工作队列被调用的函数*/

/*若在运行时需要建立 work_struct 或 delayed_work结构, 使用下面 2 个宏定义:*/
INIT_WORK(struct work_struct *work, void (*function)(void *));
PREPARE_WORK(struct work_struct *work, void (*function)(void *));
INIT_DELAYED_WORK(struct delayed_work *work, void (*function)(void *));
PREPARE_DELAYED_WORK(struct delayed_work *work, void (*function)(void *));

/* INIT_* 做更加全面的初始化结构的工作,在第一次建立结构时使用. PREPARE_* 做几乎同样的工作, 但是它不初始化用来连接 work_struct或delayed_work 结构到工作队列的指针。如果这个结构已经被提交给一个工作队列, 且只需要修改该结构,则使用 PREPARE_* 而不是 INIT_* */

/*有 2 个函数来提交工作给一个工作队列:*/
int queue_work(struct workqueue_struct *queue, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *queue, struct delayed_work *work, unsigned long delay);

/* 每个都添加work到给定的workqueue。如果使用 queue_delay_work, 则实际的工作至少要经过指定的 jiffies 才会被执行。 这些函数若返回 1 则工作被成功加入到队列; 若为0,则意味着这个 work 已经在队列中等待,不能再次加入*/

在将来的某个时间, 这个工作函数将被传入给定的 data 值来调用。这个函数将在工作线程的上下文运行, 因此它可以睡眠 (你应当知道这个睡眠可能影响提交给同一个工作队列的其他任务) 工作函数不能访问用户空间,因为它在一个内核线程中运行, 完全没有对应的用户空间来访问。
取消一个挂起的工作队列入口项可以调用:

int cancel_delayed_work(struct delayed_work *work);
void cancel_work_sync(struct work_struct *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;
static struct delayed_work jiq_work_delay;

    /* this line is in jiq_init() */
INIT_WORK(&jiq_work, jiq_print_wq);
INIT_DELAYED_WORK(&jiq_work_delay, jiq_print_wq);

(2)提交工作

int schedule_work(&jiq_work);/*对于work_struct结构*/
int schedule_delayed_work(&jiq_work_delay, delay);/*对于delayed_work结构*/

/*返回值的定义和 queue_work 一样*/

若需取消一个已提交给工作队列入口项, 可以使用 cancel_delayed_work和cancel_work_sync, 但刷新共享队列需要一个特殊的函数:

void flush_scheduled_work(void);

因为不知道谁可能使用这个队列,因此不可能知道 flush_schduled_work 返回需要多长时间。

分配内存

kmalloc函数

#include

void *kmalloc(size_t size,int flags);

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 分配器”,相关函数和类型在 中声明。slab 分配器实现的高速缓存具有 kmem_cache_t 类型。

1.实现过程如下:

kmem_cache_t *kmem_cache_create(const char *name, size_t size,size_t offset,
                  unsigned long flags,
       void (*constructor)(void *, kmem_cache_t *,unsigned long flags),

      void (*destructor)(void *, kmem_cache_t *, unsigned long flags));
/*创建一个可以容纳任意数目内存区域的、大小都相同的高速缓存对象,这些区域的大小都相同,由size参数决定。*/

参数*name: 一个指向 name 的指针,name和这个后备高速缓存相关联,功能是管理信息以便追踪问题;通常设置为被缓存的结构类型的名字,不能包含空格。

参数size:每个内存区域的大小。

参数offset:页内第一个对象的偏移量;用来确保被分配对象的特殊对齐,0 表示缺省值。

参数flags:控制分配方式的位掩码:

SLAB_NO_REAP        保护缓存在系统查找内存时不被削减,不推荐。 SLAB_HWCACHE_ALIGN  所有数据对象跟高速缓存行对齐,平台依赖,可能浪费内存。 SLAB_CACHE_DMA      每个数据对象在 DMA 内存区段分配.。

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

mempool_t *mempool_create(int min_nr,

                           mempool_alloc_t *alloc_fn,
                           mempool_free_t *free_fn,
                           void *pool_data);

/*min_nr 参数是内存池应当一直保留的最小数量的分配对象*/

/*实际的分配和释放对象由 alloc_fn 和 free_fn 处理,原型:*/
typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data);
typedef void (mempool_free_t)(void *element, void *pool_data);
/*给 mempool_create 最后的参数 *pool_data 被传递给 alloc_fn 和 free_fn */

你可编写特殊用途的函数来处理 mempool 的内存分配,但通常只需使用 slab 分配器为你处理这个任务:mempool_alloc_slab 和 mempool_free_slab的原型和上述内存池分配原型匹配,并使用 kmem_cache_alloc 和 kmem_cache_free 处理内存的分配和释放。

典型的设置内存池的代码如下:

cache = kmem_cache_create(. . .);
pool = mempool_create(MY_POOL_MINIMUM,mempool_alloc_slab, mempool_free_slab, cache);

(2)创建内存池后,分配和释放对象:

void *mempool_alloc(mempool_t *pool, int gfp_mask);
void mempool_free(void *element, mempool_t *pool);

 在 创建mempool时,分配函数将被调用多次来创建预先分配的对象。因此,对 mempool_alloc 的调用是试图用分配函数请求额外的对象,如果失败,则返回预先分配的对象(如果存在)。用 mempool_free 释放对象时,若预分配的对象数目小于最小量,就将它保留在池中,否则将它返回给系统。

可用一下函数重定义mempool预分配对象的数量:

int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask);
/*若成功,内存池至少有 new_min_nr 个对象*/

 

(3)若不再需要内存池,则返回给系统:

void mempool_destroy(mempool_t *pool);
/*在销毁 mempool 之前,必须返回所有分配的对象,否则会产生 oops*/

 

get_free_page与相关函数

1.如果模块需要分配大块的内存,使用面向页的分配技术会更好一些,就是整页的分配。

__get_free_page(unsigned int flags);
/*返回一个指向新页的指针, 未清零该页*/

get_zeroed_page(unsigned int flags);
/*类似于__get_free_page,但用零填充该页*/

__get_free_pages(unsigned int flags, unsigned int order);
/*分配是若干(物理连续的)页面并返回指向该内存区域的第一个字节的指针,该内存区域未清零*/

/*参数flags 与 kmalloc 的用法相同;
参数order 是请求或释放的页数以 2 为底的对数。若其值过大(没有这么大的连续区可用), 则分配失败*/

2.get_order 函数可以用来从一个整数参数 size(必须是 2 的幂) 中提取 order,函数也很简单:

/* Pure 2^n version of get_order */
static __inline__ __attribute_const__ int get_order(unsigned long size)
{
    int order;

    size = (size - 1) >> (PAGE_SHIFT - 1);
    order = -1;
    do {
        size >>= 1;
        order++;
    } while (size);
    return order;
}

3.通过/proc/buddyinfo 可以知道系统中每个内存区段上的每个 order 下可获得的数据块数目。

4.。当程序不需要页面时,它可用下列函数之一来释放它们。

void free_page(unsigned long addr);
void free_pages(unsigned long addr, unsigned long order);

它们的关系是:

#define __get_free_page(gfp_mask) \
        __get_free_pages((gfp_mask),0)

若试图释放和你分配的数目不等的页面,会破坏内存映射关系,系统会出错。

 

alloc_pages 接口

1.struct page 是一个描述一个内存页的内部内核结构,定义在

2.

Linux 页分配器的核心是称为 alloc_pages_node 的函数:

struct page *alloc_pages_node(int nid, unsigned int flags,
 unsigned int order);

/*以下是这个函数的 2 个变体(是简单的宏):*/
struct page *alloc_pages(unsigned int flags, unsigned int order);
struct page *alloc_page(unsigned int flags);

/*他们的关系是:*/
#define alloc_pages(gfp_mask, order) \
        alloc_pages_node(numa_node_id(), gfp_mask, order)
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)

参数nid 是要分配内存的 NUMA 节点 ID,
参数flags 是 GFP_ 分配标志,
参数order 是分配内存的大小.
返回值是一个指向第一个(可能返回多个页)page结构的指针, 失败时返回NULL。

alloc_pages 通过在当前 NUMA 节点分配内存( 它使用 numa_node_id 的返回值作为 nid 参数调用 alloc_pages_node)简化了alloc_pages_node调用。alloc_pages 省略了 order 参数而只分配单个页面。

释放分配的页:

void __free_page(struct page *page);
void __free_pages(struct page *page, unsigned int order);
void free_hot_page(struct page *page);
void free_cold_page(struct page *page);
/*若知道某个页中的内容是否驻留在处理器高速缓存中,可以使用 free_hot_page (对于驻留在缓存中的页) 或 free_cold_page(对于没有驻留在缓存中的页) 通知内核,帮助分配器优化内存使用*/

 

vmalloc 和 ioremap

vmalloc 是一个基本的 Linux 内存分配机制,它在虚拟内存空间分配一块连续的内存区,尽管这些页在物理内存中不连续 (使用一个单独的 alloc_page 调用来获得每个页),但内核认为它们地址是连续的。 应当注意的是:vmalloc 在大部分情况下不推荐使用。因为在某些体系上留给 vmalloc 的地址空间相对小,且效率不高。函数原型如下:

#include <linux/vmalloc.h>
void *vmalloc(unsigned long size);
void vfree(void * addr);
void *ioremap(unsigned long offset, unsigned long size);
void iounmap(void * addr);


kmalloc 和 _get_free_pages 返回的内存地址也是虚拟地址,其实际值仍需 MMU 处理才能转为物理地址。vmalloc和它们在使用硬件上没有不同,不同是在内核如何执行分配任务上:kmalloc 和 __get_free_pages 使用的(虚拟)地址范围和物理内存是一对一映射的, 可能会偏移一个常量 PAGE_OFFSET 值,无需修改页表。

而vmalloc 和 ioremap 使用的地址范围完全是虚拟的,且每次分配都要通过适当地设置页表来建立(虚拟)内存区域。 vmalloc 可获得的地址在从 VMALLOC_STARTVAMLLOC_END 的范围中,定义在 中。vmalloc 分配的地址只在处理器的 MMU 之上才有意义。当驱动需要真正的物理地址时,就不能使用 vmalloc。 调用 vmalloc 的正确场合是分配一个大的、只存在于软件中的、用于缓存的内存区域时。注意:vamlloc 比 __get_free_pages 要更多开销,因为它必须即获取内存又建立页表。因此, 调用 vmalloc 来分配仅仅一页是不值得的。vmalloc 的一个小的缺点在于它无法在原子上下文中使用。因为它内部使用 kmalloc(GFP_KERNEL) 来获取页表的存储空间,因此可能休眠。


ioremap 也要建立新页表,但它实际上不分配任何内存,其返回值是一个特殊的虚拟地址可用来访问特定的物理地址区域。
为了保持可移植性,不应当像访问内存指针一样直接访问由 ioremap 返回的地址,而应当始终使用 readb 和 其他 I/O 函数。

ioremap 和 vmalloc 是面向页的(它们会修改页表),重定位的或分配的空间都会被上调到最近的页边界。ioremap 通过将重映射的地址下调到页边界,并返回第一个重映射页内的偏移量来模拟一个非对齐的映射。

per-CPU变量

per-CPU 变量是一个有趣的 2.6 内核特性,定义在 中。当创建一个per-CPU变量,系统中每个处理器都会获得该变量的副本。其优点是对per-CPU变量的访问(几乎)不需要加锁,因为每个处理器都使 用自己的副本。per-CPU 变量也可存在于它们各自的处理器缓存中,这就在频繁更新时带来了更好性能

在编译时间创建一个per-CPU变量使用如下宏定义:

DEFINE_PER_CPU(type, name);
/*若变量( name)是一个数组,则必须包含类型的维数信息,例如一个有 3 个整数的per-CPU 数组创建如下: */
DEFINE_PER_CPU(int[3], my_percpu_array);

虽然操作per-CPU变量几乎不必使用锁定机制。 但是必须记住 2.6 内核是可抢占的,所以在修改一个per-CPU变量的临界区中可能被抢占。并且还要避免进程在对一个per-CPU变量访问时被移动到另一个处理器上运 行。所以必须显式使用 get_cpu_var 宏来访问当前处理器的变量副本, 并在结束后调用 put_cpu_var。 对 get_cpu_var 的调用返回一个当前处理器变量版本的 lvalue ,并且禁止抢占。又因为返回的是lvalue,所以可被直接赋值或操作。例如:

get_cpu_var(sockets_in_use)++;
put_cpu_var(sockets_in_use);

当要访问另一个处理器的变量副本时, 使用:

per_cpu(variable, int cpu_id);

 

当代码涉及到多处理器的per-CPU变量,就必须实现一个加锁机制来保证访问安全。
 
动态分配per-CPU变量方法如下:

void *alloc_percpu(type);
void *__alloc_percpu(size_t size, size_t align);/*需要一个特定对齐的情况下调用*/
void free_percpu(void *per_cpu_var); /* 将per-CPU 变量返回给系统*/

/*访问动态分配的per-CPU变量通过 per_cpu_ptr 来完成,这个宏返回一个指向给定 cpu_id 版本的per_cpu_var变量的指针。若操作当前处理器版本的per-CPU变量,必须保证不能被切换出那个处理器:*/
per_cpu_ptr(void *per_cpu_var, int cpu_id);

/*通常使用 get_cpu 来阻止在使用per-CPU变量时被抢占,典型代码如下:*/

int cpu;
cpu = get_cpu()
ptr = per_cpu_ptr(per_cpu_var, cpu);
/* work with ptr */
put_cpu();

/*当使用编译时的per-CPU 变量, get_cpu_var 和 put_cpu_var 宏将处理这些细节。动态per-CPU变量需要更明确的保护*/


per-CPU变量可以导出给模块, 但必须使用一个特殊的宏版本:

EXPORT_PER_CPU_SYMBOL(per_cpu_var);
EXPORT_PER_CPU_SYMBOL_GPL(per_cpu_var);


/*要在模块中访问这样一个变量,声明如下:*/
DECLARE_PER_CPU(type, name);

 
注意:在某些体系架构上,per-CPU变量的使用是受地址空间有限的。若在代码中创建per-CPU变量, 应当尽量保持变量较小.

 

 

获得大的缓冲区
大量连续内存缓冲的分配是容易失败的。到目前止执行大 I/O 操作的最好方法是通过离散/聚集操作 。

在引导时获得专用缓冲区
 
若真的需要大块连续的内存作缓冲区,最好的方法是在引导时来请求内存来分配。在引导时分配是获得大量连续内存页(避开 __get_free_pages 对缓冲大小和固定颗粒双重限制)的唯一方法。一个模块无法在引导时分配内存,只有直接连接到内核的驱动才可以。 而且这对普通用户不是一个灵活的选择,因为这个机制只对连接到内核映象中的代码才可用。要安装或替换使用这种分配方法的设备驱动,只能通过重新编译内核并 且重启计算机。

当内核被引导, 它可以访问系统种所有可用物理内存,接着通过调用子系统的初始化函数, 允许初始化代码通过减少留给常规系统操作使用的 RAM 数量来分配私有内存缓冲给自己。
 
在引导时获得专用缓冲区要通过调用下面函数进行:

#include <linux/bootmem.h>
/*分配不在页面边界上对齐的内存区*/
void *alloc_bootmem(unsigned long size);
void *alloc_bootmem_low(unsigned long size); /*分配非高端内存。希望分配到用于DMA操作的内存可能需要,因为高端内存不总是支持DMA*/

/*分配整个页*/
void *alloc_bootmem_pages(unsigned long size);
void *alloc_bootmem_low_pages(unsigned long size);/*分配非高端内存*/

/*很少在启动时释放分配的内存,但肯定不能在之后取回它。注意:以这个方式释放的部分页不返回给系统*/
void free_bootmem(unsigned long addr, unsigned long size);


与硬件通信


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); /*保证任何出现于屏障前的读在执行任何后续的读之前完成*/


void wmb(void); /*保证任何出现于屏障前的写在执行任何后续的写之前完成*/


void mb(void); /*保证任何出现于屏障前的读写操作在执行任何后续的读写操作之前完成*/


void read_barrier_depends(void); /*一种特殊的、弱些的读屏障形式。rmb 阻止屏障前后的所有读指令的重新排序,read_barrier_depends 只阻止依赖于其他读指令返回的数据的读指令的重新排序。区别微小, 且不在所有体系中存在。除非你确切地理解它们的差别, 并确信完整的读屏障会增加系统开销,否则应当始终使用 rmb。*/
/*以上指令是barrier的超集*/


void smp_rmb(void);
void smp_read_barrier_depends(void);
void smp_wmb(void);
void smp_mb(void);
/*仅当内核为 SMP 系统编译时插入硬件屏障; 否则, 它们都扩展为一个简单的屏障调用。*/

典型的应用:

 

writel(dev->registers.addr, io_destination_address);
writel(dev->registers.size, io_size);
writel(dev->registers.operation, DEV_READ);
wmb();/*类似一条分界线,上面的写操作必然会在下面的写操作前完成,但是上面的三个写操作的排序无法保证*/
writel(dev->registers.control, DEV_GO);

内存屏障影响性能,所以应当只在确实需要它们的地方使用。不同的类型对性能的影响也不同,因此要尽可能地使用需要的特定类型。值得注意的是大部分处理同步的内核原语,例如自旋锁和atomic_t,也可作为内存屏障使用。

某些体系允许赋值和内存屏障组合,以提高效率。它们定义如下:

 

#define set_mb(var, value)do {var = value; mb();}while 0
/*以下宏定义在ARM体系中不存在*/
#define set_wmb(var, value)do {var = value; wmb();}while 0
#define set_rmb(var, value)do {var = value; rmb();}while 0

 

使用do...while 结构来构造宏是标准 C 的惯用方法,它保证了扩展后的宏可在所有上下文环境中被作为一个正常的 C 语句执行。

 

使用I/O端口

对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 inb(unsigned port);
void outb(unsigned char byte, unsigned port);


/*读/写字节端口( 8 位宽 )。port 参数某些平台定义为 unsigned long ,有些为 unsigned short 。 inb 的返回类型也体系而不同。*/

unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);


/*访问 16位 端口( 一个字宽 )*/

unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);

 
/*访问 32位 端口。 longword 声明有的平台为 unsigned long ,有的为 unsigned int。*/

3.释放:

 

void release_region(unsigned long start,unsigned long n);

当不再使用I/O端口,或者卸载模块时应使用该函数将这些端口返回给系统。

 

int check_region(unsigned long first,unsigned long n);

该函数用来检测给定的I/O端口是否可用。

 

在用户空间访问I/O端口

以上函数主要用在驱动程序的使用上的,但他们也可以在用户空间使用,至少在PC类计算机上可以使用。GNU的C库在中定义了这些函数。如果要在用户空间代码中使用inb及相关函数,则必须满足下面这些条件:

程序必须使用 -O 选项编译来强制扩展内联函数
必须用ioperm 和 iopl 系统调用(#include ) 来获得对端口 I/O 操作的权限。ioperm 为获取单独端口操作权限,而 iopl 为整个 I/O 空间的操作权限。 (x86 特有的)
程序以 root 来调用 ioperm 和 iopl,或是其父进程必须以 root 获得端口操作权限。(x86 特有的)

若平台没有 ioperm 和 iopl 系统调用,用户空间可以仍然通过使用 /dev/prot 设备文件访问 I/O 端口。注意:这个文件的定义是体系相关的,并且I/O 端口必须先被注册。

 

串操作

除了一次传递一个数据的I/O操作,linux还提供了一次传递一个数据序列的特殊指令,序列中的数据单位可以是字节、字或双字,这是所谓的串操作 指令。它们完成任务比一个 C 语言循环更快。下列宏定义实现了串I/O,它们有的通过单个机器指令实现;但如果目标处理器没有进行串 I/O 的指令,则通过执行一个紧凑的循环实现。 有的体系的原型如下:

void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);

字节端口读写

void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);

16位端口读写

void insl(unsigned port, void *addr, unsigned long count);
void outsl(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中的宏定义:

 

#define outb_p(val,port) outb((val),(port))
#define outw_p(val,port)    outw((val),(port))
#define outl_p(val,port)    outl((val),(port))
#define inb_p(port)        inb((port))
#define inw_p(port)        inw((port))
#define inl_p(port)        inl((port))

#define outsb_p(port,from,len)    outsb(port,from,len)
#define outsw_p(port,from,len)    outsw(port,from,len)
#define outsl_p(port,from,len)    outsl(port,from,len)
#define insb_p(port,to,len)    insb(port,to,len)
#define insw_p(port,to,len)    insw(port,to,len)
#define insl_p(port,to,len)    insl(port,to,len)

由此可见,由于ARM使用内部总线,就没有必要额外暂停,所以暂停式的I/O函数被扩展为与非暂停式I/O 同样的代码。

平台相关性

由于自身的特性,I/O 指令与处理器密切相关的,非常难以隐藏系统间的不同。所以大部分的关于端口 I/O 的源码是平台依赖的。以下是x86和ARM所使用函数的总结:

IA-32 (x86)

x86_64 这个体系支持所有的以上描述的函数,端口号是 unsigned short 类型。
ARM 端口映射到内存,支持所有函数。串操作 用C语言实现。端口是 unsigned int 类型。 使用 I/O 内存

除了 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 内存区域使用前必须先分配,函数接口在 定义:

struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);/* 从 start 开始,分配一个 len 字节的内存区域。成功返回一个非NULL指针,否则返回NULL。所有的 I/O 内存分配情况都 /proc/iomem 中列出。*/

/*I/O内存区域在不再需要时应当释放*/
void release_mem_region(unsigned long start, unsigned long len);

/*一个旧的检查 I/O 内存区可用性的函数,不推荐使用*/
int check_mem_region(unsigned long start, unsigned long len);

然后必须设置一个映射,由 ioremap 函数实现,此函数专门用来为I/O 内存区域分配虚拟地址。经过ioremap 之后,设备驱动即可访问任意的 I/O 内存地址。注意:ioremap 返回的地址不应当直接引用;应使用内核提供的 accessor 函数。以下为函数定义:

#include
void *ioremap(unsigned long phys_addr, unsigned long size);
void *ioremap_nocache(unsigned long phys_addr, unsigned long size);/*如果控制寄存器也在该区域,应使用的非缓存版本,以实现side effect。*/
void iounmap(void * addr);

访问I/O 内存

访问I/O 内存的正确方式是通过一系列专用于此目的的函数(在 中定义的):

/*I/O 内存读函数*/
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
/*addr 是从 ioremap 获得的地址(可能包含一个整型偏移量), 返回值是从给定 I/O 内存读取的值*/

/*对应的I/O 内存写函数*/
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);

/*读和写一系列值到一个给定的 I/O 内存地址,从给定的 buf 读或写 count 个值到给定的 addr */
void ioread8_rep(void *addr, void *buf, unsigned long count);
void ioread16_rep(void *addr, void *buf, unsigned long count);
void ioread32_rep(void *addr, void *buf, unsigned long count);
void iowrite8_rep(void *addr, const void *buf, unsigned long count);
void iowrite16_rep(void *addr, const void *buf, unsigned long count);
void iowrite32_rep(void *addr, const void *buf, unsigned long count);

/*需要操作一块 I/O 地址,使用一下函数*/
void memset_io(void *addr, u8 value, unsigned int count);
void memcpy_fromio(void *dest, void *source, unsigned int count);
void memcpy_toio(void *dest, void *source, unsigned int count);

/*旧函数接口,仍可工作, 但不推荐。*/
unsigned readb(address);
unsigned readw(address);
unsigned readl(address);
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address);

像 I/O 内存一样使用端口

一些硬件有一个有趣的特性:一些版本使用 I/O 端口,而其他的使用 I/O 内存。为了统一编程接口,使驱动程序易于编写,2.6 内核提供了一个ioport_map函数:

void *ioport_map(unsigned long port, unsigned int count);/*重映射 count 个I/O 端口,使其看起来像 I/O 内存。,此后,驱动程序可以在返回的地址上使用 ioread8 和同类函数。其在编程时消除了I/O 端口和I/O 内存的区别。

/*这个映射应当在它不再被使用时撤销:*/
void ioport_unmap(void *addr);

/*注意:I/O 端口仍然必须在重映射前使用 request_region 分配I/O 端口。ARM9不支持这两个函数!*/


中断处理


尽管有些设备仅仅通过控制其寄存器就可以得到控制,但现实中的大部分设备却要比这复杂一些。因为大部分设备的处理时间与处理器不在同一个周期,且一定会比处理器慢的多,这就造成了一种让处理器等待设备的现象,显然这是不行的,而有一种解决方法就是中断操作。

中断仅仅就是一个信号,当硬件需要获得处理器对它的关注时,就可以发送这个信号。 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:

SA_INTERRUPT :快速中断标志。快速中断处理例程运行在当前处理器禁止中断的状态下。 SA_SHIRQ : 在设备间共享中断标志。 SA_SAMPLE_RANDOM :该位 表示产生的中断能对 /dev/random 和 /dev/urandom 使用的熵池(entropy pool)有贡献。 读取这些设备会返回真正的随机数,从而有助于应用程序软件选择用于加密的安全密钥。 若设备以真正随机的周期产生中断,就应当设置这个标志。若设备中断是可预测的,这个标志不值得设置。可能被攻击者影响的设备不应当设置这个标志。更多信息 看 drivers/char/random.c 的注释。

void free_irq(unsigned int irq,void *dev_id);

释放中断

中断处理例程可在驱动初始化时或在设备第一次打开时安装。推荐在设备第一次打开、硬件被告知产生中断前时申请中断,因为可以共享有限的中断资源。这 样调用 free_irq 的位置是设备最后一次被关闭、硬件被告知不用再中断处理器之后。但这种方式的缺点是必须为每个设备维护一个打开计数。

以下是中断申请的示例(并口):

if (short_irq >= 0)
{
        result = request_irq(short_irq, short_interrupt,
                             SA_INTERRUPT, "short", NULL);
        if (result) {
                printk(KERN_INFO "short: can't get assigned irq %i\n",
                       short_irq);

                short_irq = -1;
        } else { /*打开中断硬件的中断能力*/
                outb(0x10,short_base+2);
        }
}

i386 和 x86_64 体系定义了一个函数来查询一个中断线是否可用:

int can_request_irq(unsigned int irq, unsigned long flags);

/*当能够成功分配给定中断,则返回非零值。但注意,在 can_request_irq 和 request_irq 的调用之间给定中断可能被占用*/

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接口

当硬件中断到达处理器时, 内核提供的一个内部计数器会递增,产生的中断报告显示在文件 /proc/interrupts中。这一方法可以用来检查设备是否按预期地工作。此文件只显示当前已安装处理例程的中断的计数。若以前request_irq的一个中断,现在已经free_irq了,那么就不会显示在这个文件中,但是它可以显示终端共享的情况。

/proc/stat记录了几个关于系统活动的底层统计信息, 包括(但不仅限于)自系统启动以来收到的中断数。 stat 的每一行以一个字符串开始, 是该行的关键词:intr 标志是中断计数。第一个数是所有中断的总数, 而其他每一个代表一个单独的中断线的计数, 从中断 0 开始(包括当前没有安装处理例程的中断),无法显示终端共享的情况。

以上两个文件的一个不同是:/proc/interrupts几乎不依赖体系,而/proc/stat的字段数依赖内核下的硬件中断,其定义在中。ARM的定义为:

#define NR_IRQS    128

 

自动检测 IRQ 号

驱动初始化时最迫切的问题之一是决定设备要使用的IRQ 线,驱动需要信息来正确安装处理例程。自动检测中断号对驱动的可用性来说是一个基本需求。有时自动探测依赖一些设备具有的默认特性,以下是典型的并口中断探测程序:

if (short_irq < 0) /* 依靠使并口的端口号,确定中断*/
switch(short_base) {
case 0x378: short_irq = 7; break;
case 0x278: short_irq = 2; break;
case 0x3bc: short_irq = 5; break;
}

有的驱动允许用户在加载时覆盖默认值:

insmod xxxxx.ko irq=x

当目标设备有能力告知驱动它要使用的中断号时,自动探测中断号只是意味着探测设备,无需做额外的工作探测中断。

但不是每个设备都对程序员友好,对于他们还是需要一些探测工作。这个工作技术上非常简单: 驱动告知设备产生中断并且观察发生了什么。如果一切顺利,则只有一个中断信号线被激活。尽管探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 连接在一起,探测五次失败后放弃):

intcount= 0;
do
{
        unsigned long mask;
        mask = probe_irq_on();
        outb_p(0x10,short_base+2);/* enable reporting */
        outb_p(0x00,short_base);/* clear the bit */
        outb_p(0xFF,short_base);/* set the bit: interrupt! */
        outb_p(0x00,short_base+2);/* disable reporting */
        udelay(5);/* give it some time */
        short_irq = probe_irq_off(mask);

        if (short_irq== 0){/* none of them? */
                printk(KERN_INFO "short: no irq reported by probe\n");
                short_irq = -1;
        }
} while (short_irq < 0 && count++< 5);
if (short_irq< 0)
        printk("short: probe failed %i times, giving up\n",count);

最好只在模块初始化时探测中断线一次。
大部分体系定义了这两个函数( 即便是空的 )来简化设备驱动的移植。

 

DIY探测:

DIY探测与前面原理相同: 使能所有未使用的中断, 接着等待并观察发生什么。我们对设备的了解:通常一个设备能够使用3或4个IRQ 号中的一个来进行配置,只探测这些 IRQ 号使我们能不必测试所有可能的中断就探测到正确的IRQ 号。

下面的LDD3中的代码通过测试所有"可能的"中断并且察看发生的事情来探测中断。 trials 数组列出要尝试的中断, 以 0 作为结尾标志; tried 数组用来跟踪哪个中断号已经被这个驱动注册。

int trials[]= {3, 5, 7, 9, 0};
int tried[]={0, 0, 0, 0, 0};
int i, count = 0;


for (i = 0; trials[i]; i++)
        tried[i]= request_irq(trials[i], short_probing,
                               SA_INTERRUPT, "short probe", NULL);
do
{
        short_irq = 0;/* none got, yet */
        outb_p(0x10,short_base+2);/* enable */
        outb_p(0x00,short_base);
        outb_p(0xFF,short_base);/* toggle the bit */
        outb_p(0x00,short_base+2);/* disable */
        udelay(5);/* give it some time */
         /* 等待中断,若在这段时间有中断产生,handler会改变 short_irq */
        /* the value has been set by the handler */
        if (short_irq== 0){/* none of them? */
                printk(KERN_INFO "short: no irq reported by probe\n");
        }
 } while (short_irq <=0&&count++< 5);

/* end of loop, uninstall the handler */
for (i = 0; trials[i]; i++)
        if (tried[i]== 0)
                free_irq(trials[i],NULL);

if (short_irq< 0)
        printk("short: probe failed %i times, giving up\n",count);

以下是handler的源码:

irqreturn_t short_probing(int irq,void*dev_id,struct pt_regs*regs)
{
    if (short_irq== 0) short_irq= irq;/* found */
 if (short_irq!= irq) short_irq=-irq;/* ambiguous */
 return IRQ_HANDLED;
}

若事先不知道"可能的" IRQ ,就需要探测所有空闲的中断,所以不得不从 IRQ 0 探测到 IRQ NR_IRQS-1

实现中断处理例程

1.中断处理例程是在中断时间内运行的,因此行为会受到一些限制。这些限制跟我们在内核定时器中看到的一样。

处理例程不能向用户空间发送或者接收数据,因为它不是在任何进程上下文中执行的

处理例程也不能做任何可能发生休眠的操作,例如调用wait_event,使用不带GFP_ATOMIC标志的内存分配操作,或者锁住一个信号量等等

处理例程不能调用schdule函数

2.中断处理理财的功能就是将有关中断接收的信息反馈给设备,并根据正在服务的终端的不同含义对对数据进行相应的读或写。中断处理例程第一步常常包 括清除设备的一个中断标志位,大部分硬件设备在清除"中断挂起"位前不会再产生中断。这也要根据硬件的工作原理决定, 这一步也可能需要在最后做而不是开始; 这里没有通用的规则。一些设备不需要这步, 因为它们没有一个"中断挂起"位; 这样的设备是少数。

3.中断处理的一个典型 任务:如果中断通知进程所等待的事件已经发生,比如新的数据已经到达,就会唤醒在设备上休眠的进程。

不管是快速或慢速处理例程,程序员应编写执行时间尽可能短的处理例程。 如果需要进行长时间计算, 最好的方法是使用 tasklet 或者 workqueue 在一个更安全的时间来调度计算任务。

 

启用和禁止中断

 

有时设备驱动必须在一段时间(希望较短)内阻塞中断发生。并必须在持有一个自旋锁时阻塞中断,以避免死锁系统。注意:应尽量少禁止中断,即使是在设备驱动中,且这个技术不应当用于驱动中的互斥机制。

禁止单个中断
有时(但是很少!)一个驱动需要禁止一个特定中断。但不推荐这样做,特别是不能禁止共享中断(在现代系统中, 共享的中断是很常见的)。内核提供了 3 个函数,是内核 API 的一部分,声明在 :

void disable_irq(int irq);/*禁止给定的中断, 并等待当前的中断处理例程结束。如果调用 disable_irq 的线程持有任何中断处理例程需要的资源(例如自旋锁), 系统可能死锁*/
void disable_irq_nosync(int irq);/*禁止给定的中断后立刻返回(可能引入竞态)*/
void enable_irq(int irq);

 
调用任一函数可能更新在可编程控制器(PIC)中的特定 irq 的掩码, 从而禁止或使能所有处理器特定的 IRQ。这些函数的调用能够嵌套,即如果 disable_irq 被连续调用 2 次,则需要 2 个 enable_irq 重新使能 IRQ 。可以在中断处理例程中调用这些函数,但在处理某个IRQ时再打开它是不好的做法。
 
禁止所有中断
在 2.6 内核, 可使用下面 2 个函数中的任一个(定义在 )关闭当前处理器上所有中断:

void local_irq_save(unsignedlong flags);/*在保存当前中断状态到 flags 之后禁止中断*/
void local_irq_disable(void);/* 关闭中断而不保存状态*/
/*如果调用链中有多个函数可能需要禁止中断, 应使用 local_irq_save*/
/*打开中断使用:*/
void local_irq_restore(unsignedlong flags); 

void local_irq_enable(void);
/*2.6内核,没有方法全局禁用整个系统上的所有中断*/

 

顶半部和底半部

中断处理需要很快完成,并且不需要阻塞太长,所以中断处理的一个主要问题就是中断处理例程中完成耗时的任务。

linux通过将中断处理分成两部分来完成这个任务:

1.顶半部:实际响应中断的例程(request_irq注册的那个例程)

2.底半部:被顶半部调用并在稍后更安全的一个时间里执行的函数。

他们最大的不同在底半部处理例程执行时,所有中断都是打开的(这就是所谓的在更安全的时间内运行)。典型的情况是:顶半部保存设备数据到一个设备特定的缓存并调度它的底半部,最后退出:这个操作非常快。底半部接着进行任何其他需要的工作。这种方式的好处是在底半部工作期间,顶半部仍然可以继续为新中断服务。

Linux 内核有 2 个不同的机制可用来实现底半部处理:

1 tasklet (首选机制),它非常快, 但是所有的 tasklet 代码必须是原子的;

2)工作队列,它可能有更高的延时,但允许休眠。

tasklet和工作队列在《时间、延迟及延缓操作》已经介绍过,具体的实现代码请看实验源码!

 

中断共享

Linux内核支持在所有总线上中断共享。

安装共享的处理例程

通过 request_irq来安装共享中断与非共享中断有2点不同:

1)当request_irq时,flags中必须指定SA_SHIRQ位;

2dev_id必须唯一。任何指向模块地址空间的指针都行,但dev_id绝不能设置为NULL

内核为每个中断维护一个中断共享处理例程列表,dev_id就是区别不同处理例程的签名。释放处理例程通过执行free_irq实现。 dev_id用来从这个中断的共享处理例程列表中选择正确的处理例程来释放,这就是为什么dev_id必须是唯一的.

 

请求一个共享的中断时,如果满足下列条件之一,则request_irq成功:

1)中断线空闲;

2)所有已经注册该中断信号线的处理例程也标识了IRQ是共享。

一个共享的处理例程必须能够识别自己的中断,并且在自己的设备没有被中断时快速退出(返回IRQ_NONE)。

共享处理例程没有探测函数可用,但使用的中断信号线是空闲时标准的探测机制才有效。

一个使用共享处理例程的驱动需要小心:不能使用enable_irqdisable_irq,否则,对其他共享这条线的设备就无法正常工作了。即便短时间禁止中断,另一设备也可能产生延时而为设备和其用户带来问题。所以程序员必须记住:他的驱动并不是独占这个IRQ,它的行为应当比独占这个中断线更加"社会化"

中断驱动的I/O

当与驱动程序管理的硬件间的数据传送可能因为某种原因而延迟,驱动编写者应当实现缓存。一个好的缓存机制需采用中断驱动的I/O,一个输入缓存在中断时被填充,并由读取设备的进程取走缓冲区的数据,一个输出缓存由写设备的进程填充,并在中断时送出数据。

为正确进行中断驱动的数据传送,硬件应能够按照下列语义产生中断:

输入:当新数据到达时并处理器准备好接受时,设备中断处理器。

输出:当设备准备好接受新数据或确认一个成功的数据传送时,设备产生中断。


Linux设备模型1

以《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是一种数据结构,定义在

struct kobject {
    const char    * k_name;/*kobject 的名字数组(sysfs 入口使用的名字)指针;如果名字数组大小小于KOBJ_NAME_LEN,它指向本数组的name,否则指向另外分配的一个名字数组空间 */
    char            name[KOBJ_NAME_LEN];/*kobject 的名字数组,若名字数组大小不小于KOBJ_NAME_LEN,只储存前KOBJ_NAME_LEN个字符*/
    struct kref        kref;/*kobject 的引用计数*/
    struct list_head    entry;/*kobject 之间的双向链表,与所属的kset形成环形链表*/
    struct kobject        * parent;/*在sysfs分层结构中定位对象,指向上一级kset中的struct kobject kobj*/
    struct kset        * kset;/*指向所属的kset*/
    struct kobj_type    * ktype;/*负责对该kobject类型进行跟踪的struct kobj_type的指针*/
    struct dentry        * dentry;/*sysfs文件系统中与该对象对应的文件节点路径指针*/
    wait_queue_head_t    poll;/*等待队列头*/
};


kobject 是组成设备模型的基本结构,初始它只被作为一个简单的引用计数, 但随时间的推移,其任务越来越多。现在kobject 所处理的任务和支持代码包括:

对象的引用计数 :跟踪对象生命周期的一种方法是使用引用计数。当没有内核代码持有该对象的引用时, 该对象将结束自己的有效生命期并可被删除。

sysfs 表述:在 sysfs 中出现的每个对象都对应一个 kobject, 它和内核交互来创建它的可见表述。

数据结构关联:整体来看, 设备模型是一个极端复杂的数据结构,通过其间的大量链接而构成一个多层次的体系结构。kobject 实现了该结构并将其聚合在一起。

热插拔事件处理 :kobject 子系统将产生的热插拔事件通知用户空间。

一个kobject对自身并不感兴趣,它存在的意义在于把高级对象连接到设备模型上。因此内核代码很少(甚至不知道)创 建一个单独的 kobject;而kobject 被用来控制对大型域(domain)相关对象的访问,所以kobject 被嵌入到其他结构中。kobject 可被看作一个最顶层的基类,其他类都它的派生产物。 kobject 实现了一系列方法,对自身并没有特殊作用,而对其他对象却非常有效。

 对于给定的kobject指针,可使用container_of宏得到包含它的结构体的指针。

kobject 初始化

kobject的初始化较为复杂,但是必须的步骤如下:

(1)将整个kobject清零,通常使用memset函数。

(2)调用kobject_init()函数,设置结构内部一些成员。所做的一件事情是设置kobject的引用计数为1。具体的源码如下:

void kobject_init(struct kobject * kobj)/*inkobject.c*/
{
    if (!kobj)
        return;
    kref_init(&kobj->kref);/*设置引用计数为1*/
    INIT_LIST_HEAD(&kobj->entry);/*初始化kobject 之间的双向链表*/
    init_waitqueue_head(&kobj->poll);/*初始化等待队列头*/
    kobj->kset = kset_get(kobj->kset);/*增加所属kset的引用计数(若没有所属的kset,则返回NULL)*/
}

void kref_init(struct kref *kref)/*in kobject.c*/
{
    atomic_set(&kref->refcount,1);
    smp_mb();
}

static inline struct kset * to_kset(struct kobject * kobj)/*in kobject.h*/
{
    return kobj ? container_of(kobj,struct kset,kobj) : NULL;
}

static inline struct kset * kset_get(struct kset * k)/*in kobject.h*/
{
    return k ? to_kset(kobject_get(&k->kobj)) : NULL;/*增加引用计数*/
}

(3)设置kobject的名字

int kobject_set_name(struct kobject * kobj, const char * fmt, ...);

(4)直接或间接设置其它成员:ktype、kset和parent。 (重要)

 

对引用计数的操作

kobject 的一个重要函数是为包含它的结构设置引用计数。只要对这个对象的引用计数存在, 这个对象( 和支持它的代码) 必须继续存在。底层控制 kobject 的引用计数的函数有:

struct kobject *kobject_get(struct kobject *kobj);/*若成功,递增 kobject 的引用计数并返回一个指向 kobject 的指针,否则返回 NULL。必须始终测试返回值以免产生竞态*/
void kobject_put(struct kobject *kobj);/*递减引用计数并在可能的情况下释放这个对象*/

注意:kobject _init 设置这个引用计数为 1,因此创建一个 kobject时, 当这个初始化引用不再需要,应当确保采取 kobject_put 调用。同理:struct cdev 的引用计数实现如下:

struct kobject *cdev_get(struct cdev *p)
{
 struct module *owner = p->owner;
 struct kobject *kobj;
 if (owner && !try_module_get(owner))
 return NULL;
 kobj = kobject_get(&p->kobj);
 if (!kobj)
 module_put(owner);
 return kobj;
}


创建一个对 cdev 结构的引用时,还需要创建包含它的模块的引用。因此, cdev_get 使用 try_module_get 来试图递增这个模块的使引用计数。如果这个操作成功, kobject_get 被同样用来递增 kobject 的引用计数。kobject_get 可能失败, 因此这个代码检查 kobject_get 的返回值,如果调用失败,则释放它的对模块的引用计数。

release 函数和 kobject 类型

引用计数不由创建 kobject 的代码直接控制,当 kobject 的最后引用计数消失时,必须异步通知,而后kobject中ktype所指向的kobj_type结构体包含的release函数会被调用。通常原型如下:

void my_object_release(struct kobject *kobj)
{
 struct my_object *mine = container_of(kobj, struct my_object, kobj);
/* Perform any additional cleanup on this object, then... */
 kfree(mine);
}

每个 kobject 必须有一个release函数, 并且这个 kobject 必须在release函数被调用前保持不变( 稳定状态 ) 。这样,每一个 kobject 需要有一个关联的 kobj_type 结构,指向这个结构的指针能在 2 个不同的地方找到:

(1)kobject 结构自身包含一个成员(ktype)指向kobj_type ;

(2)如果这个 kobject 是一个 kset 的成员, kset 会提供kobj_type 指针。

struct kset {
    struct kobj_type    * ktype; /*指向该kset对象类型的指针*/
    struct list_head    list;/*用于连接该kset中所有kobject以形成环形链表的链表头*/
    spinlock_t        list_lock;/*用于避免竞态的自旋锁*/
    struct kobject        kobj; /*嵌入的kobject*/
    struct kset_uevent_ops    * uevent_ops;

/*原有的struct kset_hotplug_ops * hotplug_ops;已经不存在,被kset_uevent_ops 结构体替换,在热插拔操作中会介绍*/
};

以下宏用以查找指定kobject的kobj_type 指针:

struct kobj_type *get_ktype(struct kobject *kobj);

这个函数其实就是从以上提到的这两个地方返回kobj_type指针,源码如下:

static inline struct kobj_type * get_ktype(struct kobject * k)
{
    if (k->kset && k->kset->ktype)
        return k->kset->ktype;
    else
        return k->ktype;
}

关于新版本内核的kobj_type :

在新版本的内核中已经在struct kset中去除了   struct kobj_type    * ktype;
也就是说只有struct kobject中存在 kobj_type ,所以get_ktype也相应变为了: 

static inline struct kobj_type * get_ktype(struct kobject * kobj)

{
        return kobj->ktype;

}


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 传递给下面的函数:

int kobject_add(struct kobject *kobj); /*函数可能失败(返回一个负错误码),程序应作出相应地反应*/

内核提供了一个组合函数:

extern int kobject_register(struct kobject *kobj); /*仅仅是一个 kobject_init 和 kobject_add 的结合,其他成员的初始化必须在之前手动完成*/

当把一个 kobject从kset中删除以清除引用时使用:

void kobject_unregister(struct kobject *kobj); /*是 kobject_del 和 kobject_put 的结合*/


但是在新的内核中已经没有" register"和" unregister "类似的函数了,取而代之的是类似

extern int __must_check kobject_init_and_add(struct kobject *kobj,
                     struct kobj_type *ktype,
                     struct kobject *parent,
                     const char *fmt, ...);

这样的函数,所以请根据你使用的内核版本自己研究了.

kset 在一个标准的内核链表中保存了它的子节点,在大部分情况下, 被包含的 kobjects 在它们的 parent 成员中保存指向 kset内嵌的 kobject的指针,关系如下:

一个简单的 kset 层次

图表中所有被包含的 kobjects 实际上被嵌入在一些其他类型中, 甚至可能是其他的 kset。

kset 上的操作

ksets 有类似于kobjects初始化和设置接口:

void kset_init(struct kset *kset);
int kset_add(struct kset *kset);
int kset_register(struct kset *kset);
void kset_unregister(struct kset *kset);

/*管理 ksets 的引用计数:*/
struct kset *kset_get(struct kset *kset);
void kset_put(struct kset *kset);

/* kset 也有一个名字,存储于嵌入的 kobject,因此设置它的名字用:*/
kobject_set_name(&my_set->kobj, "The name");

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 。

/*子系统通常用以下的宏声明:*/
decl_subsys(name, struct kobj_type *type, struct kset_uevent_ops * uevent_ops);

/*子系统的操作函数:*/
void subsystem_init(struct kset *s);
int subsystem_register(struct kset *s);
void subsystem_unregister(struct kset *s);
struct subsystem *subsys_get(struct kset *s)
void subsys_put(struct kset *s);

/*这些函数基本上是kset操作函数的包装,以实现子系统的操作*/


在新的内核里连以上的subsystem的函数都已经被取消了,完全被kset取代了。其原因可能是因为原来的subsystem的函数根本就是kset函数的简单包装,而在Linux世界里简单就是美,多余的东西被剔出了。

底层sysfs操作

kobject 是在 sysfs 虚拟文件系统后的机制。对每个在 sysfs 中的目录, 在内核中都会有一个 kobject 与之对应。每个 kobject 都输出一个或多个属性, 它在 kobject 的 sysfs 目录中以文件的形式出现, 其中的内容由内核产生。   包含 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 结构中:

struct kobj_type {
 void (*release)(struct kobject *);
 struct sysfs_ops *sysfs_ops;/*提供实现以下属性的方法*/
 struct attribute **default_attrs; /*用于保存类型属性列表(指针的指针) */
};

struct attribute {
 char *name;/*属性的名字( 在 kobject 的 sysfs 目录中显示)*/
 struct module *owner;/*指向模块的指针(如果有), 此模块负责实现这个属性*/
 mode_t mode; /*属性的保护位,modes 的宏定义在 :例如S_IRUGO 为只读属性等等*/
}; /*default_attrs 列表中的最后一个元素必须用 0 填充*/

 sysfs 读写这些属性是由 kobj_type->sysfs_ops 成员中的函数完成的:

struct sysfs_ops {
 ssize_t (*show)(struct kobject *kobj, struct attribute *attr, char *buffer);
 ssize_t (*store)(struct kobject *kobj, struct attribute *attr, const char *buffer, size_t size);
};

当用户空间读取一个属性时,内核会使用指向 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结构并传递到以下函数:

int sysfs_create_file(struct kobject *kobj, struct attribute *attr);
/*若成功,文件以attribute结构中的名字创建并返回 0; 否则, 返回负错误码*/
/*注意:内核会调用相同的 show() 和 store() 函数来实现对新属性的操作,所以在添加一个新非默认属性前,应采取必要的步骤确保这些函数知道如何实现这个属性*/

若要删除属性,调用:

int sysfs_remove_file(struct kobject *kobj, struct attribute *attr);
/*调用后, 这个属性不再出现在 kobject 的 sysfs 入口。若一个用户空间进程可能有一个打开的那个属性的文件描述符,在这个属性已经被删除后,show 和 store 仍然可能被调用*/

二进制属性

sysfs 通常要求所有属性都只包含一个可读文本格式的值,很少需要创建能够处理大量二进制数据的属性。但当在用户空间和设备间传递不可改变的数据时(如上传固件到设备)就需要这个特性。二进制属性使用一个 bin_attribute 结构来描述:

struct bin_attribute {
    struct attribute    attr;/*属性结构体*/
    size_t            size;/*这个二进制属性的最大大小(若无最大值则为0)*/
    void            *private;
    ssize_t (*read)(struct kobject *, char *, loff_t, size_t);
    ssize_t (*write)(struct kobject *, char *, loff_t, size_t);
/*read 和 write 方法类似字符驱动的读写方法;,在一次加载中可被多次调用,每次调用最大操作一页数据,且必须能以其他方式判断操作数据的末尾*/
    int (*mmap)(struct kobject *, struct bin_attribute *attr,
         struct vm_area_struct *vma);
};

/*二进制属性必须显式创建,不能以默认属性被创建,创建一个二进制属性调用:*/
int sysfs_create_bin_file(struct kobject *kobj, struct bin_attribute *attr);

/*删除二进制属性调用:*/
int sysfs_remove_bin_file(struct kobject *kobj, struct bin_attribute *attr);

符号链接

sysfs 文件系统具有树型结构, 反映 kobject之间的组织层次关系。为了表示驱动程序和所管理的设备间的关系,需要额外的指针,其在 sysfs 中通过符号链接实现。

/*在 sysfs 创建一个符号链接:*/
int sysfs_create_link(struct kobject *kobj, struct kobject *target, char *name);
/*函数创建一个链接(name)指向target的 sysfs 入口作为 kobj 的一个属性,是一个相对连接,与它在sysfs 系统中的位置无关*/

/*删除符号连接调用:*/
void sysfs_remove_link(struct kobject *kobj, char *name);

热插拔事件产生

一个热插拔事件是一个从内核空间发送到用户空间的通知, 表明系统配置已经改变. 无论 kobject 被创建或删除,都会产生这种事件。热插拔事件会导致对 /sbin/hotplug 的调用, 它通过加载驱动程序, 创建设备节点, 挂载分区或其他正确动作响应事件。
热插拔事件的实际控制是通过一套存储于 kset_uevent_ops (《LDD3》中介绍的struct kset_hotplug_ops * hotplug_ops;在2.6.22.2中已经被kset_uevent_ops 结构体替换结构的方法完成:

struct kset_uevent_ops {
    int (*filter)(struct kset *kset, struct kobject *kobj);
    const char *(*name)(struct kset *kset, struct kobject *kobj);
    int (*uevent)(struct kset *kset, struct kobject *kobj, char **envp,
            int num_envp, char *buffer, int buffer_size);
};

可以在 kset 结构的uevent_ops 成员中找到指向kset_uevent_ops结构的指针。

若在 kobject 中不包含指定的 kset , 内核将通过 parent 指针在分层结构中进行搜索,直到发现一个包含有kset的 kobject ; 接着使用这个 kset 的热插拔操作。

kset_uevent_ops 结构中的三个方法作用如下:
(1) filter 函数让 kset 代码决定是否将事件传递给用户空间。如果 filter 返回 0,将不产生事件。以磁盘的 filter 函数为例,它只允许kobject产生磁盘和分区的事件,源码如下:

static int block_hotplug_filter(struct kset *kset, struct kobject *kobj)
{
 struct kobj_type *ktype = get_ktype(kobj);
    return ((ktype == &ktype_block) || (ktype == &ktype_part));
}

(2) 当调用用户空间的热插拔程序时,相关子系统的名字将作为唯一的参数传递给它。name 函数负责返回合适的字符串传递给用户空间的热插拔程序。

(3)热插拔脚本想得到的任何其他参数都通过环境变量传递。uevent 函数的作用是在调用热插拔脚本之前将参数添加到环境变量中。函数原型:

int (*uevent)(struct kset *kset, struct kobject *kobj, /*产生事件的目标对象*/
 char **envp,/*一个保存其他环境变量定义(通常为 NAME=value 的格式)的数组*/
 int num_envp, /*环境变量数组中包含的变量个数(数组大小)*/
 char *buffer, int buffer_size/*环境变量被编码后放入的缓冲区的指针和字节数(大小)*/
/*若需要添加任何环境变量到 envp, 必须在最后的添加项后加一个 NULL 入口,使内核知道数组的结尾*/
        );
/*返回值正常应当是 0,若返回非零值将终止热插拔事件的产生*/

热插拔事件的产生通常是由在总线驱动程序层的逻辑所控制。

以上是Linux设备模型的底层原理简介,具体的细节应该参阅内核源码和《ULK3》。


Linux设备模型2

Linux 2.6内核的一个重要特色是提供了统一的内核设备模型。随着技术的不断进步,系统的拓扑结构越来越复杂,对智能电源管理、热插拔以及plug and play的支持要求也越来越高,2.4内核已经难以满足这些需求。为适应这种形势的需要,2.6内核开发了全新的设备模型。
1. Sysfs文件系统
Sysfs文件系统是一个类似于proc文件系统的特殊文件系统,用于将系统中的设备组织成层次结构,并向用户模式程序提供详细的内核数据结构信息。其顶层目录主要有:

 

Block目录:包含所有的块设备
Devices目录:包含系统所有的设备,并根据设备挂接的总线类型组织成层次结构
Bus目录:包含系统中所有的总线类型
Drivers目录:包括内核中所有已注册的设备驱动程序
Class目录:系统中的设备类型(如网卡设备,声卡设备等)
2. 内核对象机制关键数据结构
2.1 kobject内核对象
Kobject 是Linux 2.6引入的新的设备管理机制,在内核中由struct kobject表示。通过这个数据结构使所有设备在底层都具有统一的接口,kobject提供基本的对象管理,是构成Linux 2.6设备模型的核心结构,它与sysfs文件系统紧密关联,每个在内核中注册的kobject对象都对应于sysfs文件系统中的一个目录。
Kobject结构定义为:
struct kobject {
char * k_name;    指向设备名称的指针
char name[KOBJ_NAME_LEN];   设备名称
struct kref kref;    对象引用计数
struct list_head entry;   挂接到所在kset中去的单元
struct kobject * parent; 指向父对象的指针
struct kset * kset;    所属kset的指针
struct kobj_type * ktype;   指向其对象类型描述符的指针
struct dentry * dentry; sysfs文件系统中与该对象对应的文件节点路径指针
};
其中的kref域表示该对象引用的计数,内核通过kref实现对象引用计数管理,内核提供两个函数kobject_get()、kobject_put()分别用于增加和减少引用计数,当引用计数为0时,所有该对象使用的资源将被释放。
Ktype 域是一个指向kobj_type结构的指针,表示该对象的类型。Kobj_type数据结构包含三个域:一个release方法用于释放kobject占 用的资源;一个sysfs_ops指针指向sysfs操作表和一个sysfs文件系统缺省属性列表。Sysfs操作表包括两个函数store()和 show()。当用户态读取属性时,show()函数被调用,该函数编码指定属性值存入buffer中返回给用户态;而store()函数用于存储用户态 传入的属性值。
2.2 kset内核对象集合
Kobject通常通过kset组织成层次化的结构,kset是具有相同类型的kobject的集合,在内核中用kset数据结构表示,定义为:
struct kset {
struct subsystem * subsys;   所在的subsystem的指针
struct kobj_type * ktype;   指向该kset对象类型描述符的指针
struct list_head list;      用于连接该kset中所有kobject的链表头
struct kobject kobj;     嵌入的kobject
struct kset_hotplug_ops * hotplug_ops; 指向热插拔操作表的指针
};
包 含在kset中的所有kobject被组织成一个双向循环链表,list域正是该链表的头。Ktype域指向一个kobj_type结构,被该 kset中的所有kobject共享,表示这些对象的类型。Kset数据结构还内嵌了一个kobject对象(由kobj域表示),所有属于这个kset 的kobject对象的parent域均指向这个内嵌的对象。此外,kset还依赖于kobj维护引用计数:kset的引用计数实际上就是内嵌的 kobject对象的引用计数。
2.3 subsystem内核对象子系统
Subsystem是一系列kset的集合,描述系统中某一 类设备子系统,如block_subsys表示所有的块设备,对应于sysfs文件系统中的block目录。类似的,devices_subsys对应于 sysfs中的devices目录,描述系统中所有的设备。Subsystem由struct subsystem数据结构描述,定义为:
struct subsystem {
struct kset kset;       内嵌的kset对象
struct rw_semaphore rwsem; 互斥访问信号量
};
每 个kset必须属于某个subsystem,通过设置kset结构中的subsys域指向指定的subsystem可以将一个kset加入到该 subsystem。所有挂接到同一subsystem的kset共享同一个rwsem信号量,用于同步访问kset中的链表。

Linux设备模型3

3. 内核对象机制主要相关函数
针对内核对象不同层次的数据结构,linux 2.6内核定义了一系列操作函数,定义于lib/kobject.c文件中。
3.1 kobject相关函数
void kobject_init(struct kobject * kobj);
kobject初始化函数。设置kobject引用计数为1,entry域指向自身,其所属kset引用计数加1。
int kobject_set_name(struct kobject *kobj, const char *format, ...);
 

设置指定kobject的名称。
void kobject_cleanup(struct kobject * kobj)和void kobject_release(struct kref *kref);
kobject清除函数。当其引用计数为0时,释放对象占用的资源。
struct kobject *kobject_get(struct kobject *kobj);
将kobj 对象的引用计数加1,同时返回该对象的指针。
void kobject_put(struct kobject * kobj);
将kobj对象的引用计数减1,如果引用计数降为0,则调用kobject_release()释放该kobject对象。
int kobject_add(struct kobject * kobj);
将kobj对象加入Linux设备层次。挂接该kobject对象到kset的list链中,增加父目录各级kobject的引用计数,在其parent指向的目录下创建文件节点,并启动该类型内核对象的hotplug函数。
int kobject_register(struct kobject * kobj);
kobject注册函数。通过调用kobject_init()初始化kobj,再调用kobject_add()完成该内核对象的注册。
void kobject_del(struct kobject * kobj);
从Linux设备层次(hierarchy)中删除kobj对象。
void kobject_unregister(struct kobject * kobj);
kobject注销函数。与kobject_register()相反,它首先调用kobject_del从设备层次中删除该对象,再调用kobject_put()减少该对象的引用计数,如果引用计数降为0,则释放该kobject对象。
3.2 kset相关函数
与kobject 相似,kset_init()完成指定kset的初始化,kset_get()和kset_put()分别增加和减少kset对象的引用计数。 Kset_add()和kset_del()函数分别实现将指定keset对象加入设备层次和从其中删除;kset_register()函数完成 kset的注册而kset_unregister()函数则完成kset的注销。
3.3 subsystem相关函数
subsystem有一组完成类似的函数,分别是:
void subsystem_init(struct subsystem *subsys);
int subsystem_register(struct subsystem *subsys);
void subsystem_unregister(struct subsystem *subsys);
struct subsystem *subsys_get(struct subsystem *subsys)
void subsys_put(struct subsystem *subsys);
4. 设备模型组件
在上述内核对象机制的基础上,Linux的设备模型建立在几个关键组件的基础上,下面我们详细阐述这些组件。
4.1 devices
系统中的任一设备在设备模型中都由一个device对象描述,其对应的数据结构struct device定义为:
struct device {
struct list_head g_list;
struct list_head node;
    struct list_head bus_list;
    struct list_head driver_list;
    struct list_head children;
    struct device *parent;
    struct kobject kobj;
    char bus_id[BUS_ID_SIZE];
    struct bus_type *bus;
    struct device_driver *driver;
    void *driver_data;
    /* Several fields omitted */
};
g_list 将该device对象挂接到全局设备链表中,所有的device对象都包含在devices_subsys中,并组织成层次结构。Node域将该对象挂接 到其兄弟对象的链表中,而bus_list则用于将连接到相同总线上的设备组织成链表,driver_list则将同一驱动程序管理的所有设备组织为链 表。此外,children域指向该device对象子对象链表头,parent域则指向父对象。Device对象还内嵌一个kobject对象,用于引 用计数管理并通过它实现设备层次结构。Driver域指向管理该设备的驱动程序对象,而driver_data则是提供给驱动程序的数据。Bus域描述设 备所连接的总线类型。
内核提供了相应的函数用于操作device对象。其中Device_register()函数将一个新的device对象 插 入设备模型,并自动在/sys/devices下创建一个对应的目录。Device_unregister()完成相反的操作,注销设备对象。 Get_device()和put_device()分别增加与减少设备对象的引用计数。通常device结构不单独使用,而是包含在更大的结构中作为一 个子结构使用,比如描述PCI设备的struct pci_dev,其中的dev域就是一个device对象。
4.2 drivers
系统中的每个驱动程序由一个device_driver对象描述,对应的数据结构定义为:
struct device_driver {
    char *name;   设备驱动程序的名称
    struct bus_type *bus; 该驱动所管理的设备挂接的总线类型
    struct kobject kobj;    内嵌kobject对象
    struct list_head devices;   该驱动所管理的设备链表头
    int (*probe)(struct device *dev); 指向设备探测函数,用于探测设备是否可以被该驱动程序管理
int (*remove)(struct device *dev); 用于删除设备的函数
/* some fields omitted*/
};
与device 结构类似,device_driver对象依靠内嵌的kobject对象实现引用计数管理和层次结构组织。内核提供类似的函数用于操作 device_driver对象,如get_driver()增加引用计数,driver_register()用于向设备模型插入新的driver对 象,同时在sysfs文件系统中创建对应的目录。Device_driver()结构还包括几个函数,用于处理热拔插、即插即用和电源管理事件。
4.3   buses
系统中总线由struct bus_type描述,定义为:
struct bus_type {
char   * name; 总线类型的名称
struct subsystem subsys; 与该总线相关的subsystem
struct kset drivers; 所有与该总线相关的驱动程序集合
struct kset devices; 所有挂接在该总线上的设备集合
struct bus_attribute * bus_attrs; 总线属性
struct device_attribute * dev_attrs; 设备属性
struct driver_attribute * drv_attrs;   驱动程序属性
int (*match)(struct device * dev, struct device_driver * drv);
int (*hotplug) (struct device *dev, char **envp, int num_envp, char *buffer, int buffer_size);
int (*suspend)(struct device * dev, u32 state);
int (*resume)(struct device * dev);
};
每 个bus_type对象都内嵌一个subsystem对象,bus_subsys对象管理系统中所有总线类型的subsystem对象。每个 bus_type对象都对应/sys/bus目录下的一个子目录,如PCI总线类型对应于/sys/bus/pci。在每个这样的目录下都存在两个子目 录:devices和drivers(分别对应于bus_type结构中的devices和drivers域)。其中devices子目录描述连接在该总 线上的所有设备,而drivers目录则描述与该总线关联的所有驱动程序。与device_driver对象类似,bus_type结构还包含几个函数 (match()、hotplug()等)处理相应的热插拔、即插即拔和电源管理事件。
4.4 classes
系统中的设备类由 struct class描述,表示某一类设备。所有的class对象都属于class_subsys子系统,对应于sysfs文件系统中的/sys/class目录。 每个class对象包括一个class_device链表,每个class_device对象表示一个逻辑设备,并通过struct class_device中的dev域(一个指向struct device的指针)关联一个物理设备。这样,一个逻辑设备总是对应于一个物理设备,但是一个物理设备却可能对应于多个逻辑设备。此外,class结构中 还包括用于处理热插拔、即插即拔和电源管理事件的函数,这与device对象和driver对象相似。

你可能感兴趣的:(Linux设备驱动程序学习笔记)