linux设备驱动程序开发看不懂,《linux设备驱动开发详解》读书笔记五

1、中断的分类

根据中断的来源可分为:外部中断和内部中断;

根据中断是否可屏蔽分为:可屏蔽中断与不可屏蔽中断(NMI);

根据中断入口跳转方式的不同分为:向量中断和非向量中断。

2、中断处理程序架构

设备的中断会打断内核中进程的正常调度和运行,系统对更高吞吐率的追求势必要求中断服务程序尽可能的短小精悍。为了解决这一矛盾,linux将中断处理程序分解为两个半部:顶半部和底半部。顶半部完成尽可能少的比较紧急的功能。下图描述了linux内核的中断处理机制。

b5a8e928cb0a5edd10898f3b14fd7203.png

3、linux中断编程之顶半部机制

与Linux设备驱动程序中断处理相关的函数有申请和释放IRQ(中断请求)函数,即request_irq和free_irq,这两个函数在头文件include/linux/interrupt.h文件中声明:

int request_irq(unsigned int irq, irqreturn_t (*handler)(int irq, void *dev_id, struct pt_regs *regs),unsigned long flags, const char *dev_name, void *dev_id);

该函数的作用是注册一个IRQ,其中参数irq是要申请的硬件中断号,参数handler是向系统登记的中断处理函数,是一个回调函数,中断发生时系统调用这个函数,参数dev_id是设备的ID,参数flags是中断处理的属性,若设置为SA_INTERRUPT,表明中断处理程序是FRQ(快速中断请求),FRQ程序被调用时屏蔽所有中断,而IRQ程序被调用时不屏蔽FRQ。若设为SA_SHIRQ,则多个设备共享中断,dev_id在中断共享时会用到。参数dev_name是定义传递给request_irq的字符串,用来在/proc/interrupts中显示中断的拥有者。

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

该函数的作用是释放一个IRQ,一般是在退出设备或关闭设备时调用。

void enable_irq(unsigned int irq);

该函数的作用是打开一个IRQ,允许该IRQ产生中断。

void disable_irq(unsigned int irq);

该函数的作用是关闭一个IRQ,禁止该IRQ产生中断。

4、底半部机制

Linux实现底半部中断的机制主要有tasklet,工作队列和软中断。

4.1 tasklet

定义tasklet及其处理函数,并将两者关联:

void my_tasklet_func(unsigned long);

DECLARE_TASKLET(my_tasklet,my_tasklet_func,data);

DECLARE_TASKLET定义名称为my_tasklet的tasklet并将其与my_tasklet_func函数绑定,传入这个函数的参数为data。在需要调度tasklet的时候引用tasklet_schedule函数就能使系统在适当时候运行:

tasklet_schedule(&my_tasklet);

使用tasklet作为下半部的处理中断的设备驱动程序模板如下:

点击(此处)折叠或打开

/*定义tasklet和下半部函数并关联*/

void my_do_tasklet(unsigned long);

DECLARE_TASKLET(my_tasklet, my_do_tasklet, 0);

/*中断处理下半部*/

void my_do_tasklet(unsigned long)

{

……/*编写自己的处理事件内容*/

}

/*中断处理上半部*/

irpreturn_t my_interrupt(unsigned int irq,void *dev_id)

{

……

/*调度my_tasklet函数,根据声明将去执行my_tasklet_func函数*/

tasklet_schedule(&my_tasklet)

……

}

/*设备驱动的加载函数*/

int __init xxx_init(void)

{

……

/*申请中断, 转去执行my_interrupt函数并传入参数*/

result=request_irq(my_irq,my_interrupt,IRQF_DISABLED,"xxx",NULL);

……

}

/*设备驱动模块的卸载函数*/

void __exit xxx_exit(void)

{

……

/*释放中断*/

free_irq(my_irq,my_interrupt);

……

}

4.2 工作队列介绍

工作队列(work queue)是另外一种将中断的部分工作推后的一种方式,它可以实现一些tasklet不能实现的工作,比如工作队列机制可以睡眠。这种差异的本质原因是,在工作队列机制中,将推后的工作交给一个称之为工作者线程(worker thread)的内核线程去完成(单核下一般会交给默认的线程events/0)。因此,在该机制中,当内核在执行中断的剩余工作时就处在进程上下文(process context)中。也就是说由工作队列所执行的中断代码会表现出进程的一些特性,最典型的就是可以重新调度甚至睡眠。

对于tasklet机制(中断处理程序也是如此),内核在执行时处于中断上下文(interrupt context)中。而中断上下文与进程毫无瓜葛,所以在中断上下文中就不能睡眠。因此,当推后的那部分中断程序需要睡眠时,工作队列毫无疑问是最佳选择;否则用tasklet。

(1)工作队列的实现

工作队列work_struct结构体,位于/include/linux/workqueue.h

typedef void (*work_func_t)(struct work_struct *work);

struct work_struct {

atomic_long_t data; /*传递给处理函数的参数*/

#define WORK_STRUCT_PENDING 0/*工作是否正在等待处理标志*/

#define WORK_STRUCT_FLAG_MASK (3UL)

#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)

struct list_head entry; /* 连接所有工作的链表*/

work_func_t func; /* 要执行的函数*/

#ifdef CONFIG_LOCKDEP

struct lockdep_map lockdep_map;

#endif

};

这些结构被连接成链表,当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的work_struct对象从链表上移去。当链表上不再有对象的时候,它就会继续休眠。可以通过DECLARE_WORK在编译时静态地创建该结构,以完成推后的工作。

(2)工作的创建(静态方法)

#define DECLARE_WORK(n, f) \

struct work_struct n = __WORK_INITIALIZER(n, f)

而后边这个宏为一下内容:

#define __WORK_INITIALIZER(n, f) { \

.data = WORK_DATA_INIT(), \

.entry = { &(n).entry, &(n).entry }, \

.func = (f), \

__WORK_INIT_LOCKDEP_MAP(#n, &(n)) \

}

其为参数data赋值的宏定义为:

#define WORK_DATA_INIT() ATOMIC_LONG_INIT(0)

这样就会静态地创建一个名为n,待执行函数为f,参数为data的work_struct结构。

(3)工作的创建(动态方法)

在运行时通过指针创建一个工作:

INIT_WORK(struct work_struct *work, void(*func) (void *));

这会动态地初始化一个由work指向的工作队列,并将其与处理函数绑定。宏原型为:

#define INIT_WORK(_work, _func) \

do { \

static struct lock_class_key __key; \

\

(_work)->data = (atomic_long_t) WORK_DATA_INIT(); \

lockdep_init_map(&(_work)->lockdep_map, #_work, &__key, 0);\

INIT_LIST_HEAD(&(_work)->entry); \

PREPARE_WORK((_work), (_func)); \

} while (0)

在需要调度的时候引用类似tasklet_schedule()函数的相应调度工作队列执行的函数schedule_work(),如:

schedule_work(&work);/*调度工作队列执行*/

如果有时候并不希望工作马上就被执行,而是希望它经过一段延迟以后再执行。在这种情况下,可以调度指定的时间后执行函数:

schedule_delayed_work(&work,delay);

函数原型为:

int schedule_delayed_work(struct delayed_work *work, unsigned long delay);

其中是以delayed_work为结构体的指针,而这个结构体的定义是在work_struct结构体的基础上增加了一项timer_list结构体。

struct delayed_work {

struct work_struct work;

struct timer_list timer; /* 延迟的工作队列所用到的定时器,当不需要延迟时初始化为NULL*/

};

这样,便使预设的工作队列直到delay指定的时钟节拍用完以后才会执行。

(4)模板

使用工作队列处理中断下半部的设备驱动程序模板如下:

点击(此处)折叠或打开

/*定义工作队列和下半部函数并关联*/

struct work_struct my_wq;

void my_do_work(unsigned long);

/*中断处理下半部*/

void my_do_work(unsigned long)

{

……/*编写自己的处理事件内容*/

}

/*中断处理上半部*/

irpreturn_t my_interrupt(unsigned int irq,void *dev_id)

{

……

schedule_work(&my_wq)/*调度my_wq函数,根据工作队列初始化函数将去执行my_do_work函数*/

……

}

/*设备驱动的加载函数*/

int __init xxx_init(void)

{

……

/*申请中断,转去执行my_interrupt函数并传入参数*/

result=request_irq(my_irq,my_interrupt,IRQF_DISABLED,"xxx",NULL);

……

/*初始化工作队列函数,并与自定义处理函数关联*/

INIT_WORK(&my_irq,(void (*)(void *))my_do_work,NULL);

……

}

/*设备驱动模块的卸载函数*/

void __exit xxx_exit(void)

{

……

/*释放中断*/

free_irq(my_irq,my_interrupt);

……

}

一个实验全程如下:

点击(此处)折叠或打开

#include

#include

#include

#include

#include

#include

#include

#include

#include

#define MAJOR 251

static char drv_name[] ="interrupt_dev";

static char kernel_buf[1024];

char var;

#define key_irq1 IRQ_EINT7

struct work_struct my_wq;

void my_do_tasklet(unsigned long);

DECLARE_TASKLET(my_tasklet,my_do_tasklet,0);

void my_do_tasklet(unsigned long data)

{

printk("-----tasklet-----\n");

}

void my_do_work(unsigned long data)

{

printk("----work_queue-----\n");

}

static void led_init()

{

printk("led_init!\n");

__raw_writel(((__raw_readl(S3C2410_GPBCON) & (~(0xff<<10))) | (0x55<<10)), S3C2410_GPBCON);

__raw_writel(__raw_readl(S3C2410_GPBUP) | (0xf<<5), S3C2410_GPBUP);

__raw_writel(__raw_readl(S3C2410_GPBDAT) | (0xf<<5), S3C2410_GPBDAT);

}

static void Key_init()

{

printk("key_init\n");

// __raw_writel(((__raw_readl(S3C2410_GPFCON) & (~(0xff<<0))) | (0x02<<14)), S3C2410_GPFCON);

}

static irqreturn_t irq_handle_key(void)

{

printk("this is for a test!\n");

schedule_work(&my_wq);

tasklet_schedule(&my_tasklet);

return IRQ_RETVAL(IRQ_HANDLED);

}

static int char_dev_open(struct inode *inode,struct file *file)

{

led_init();

Key_init();

INIT_WORK(&my_wq,my_do_work);

if(request_irq(key_irq1,irq_handle_key,IRQF_TRIGGER_FALLING,drv_name,NULL) == 0)

{

printk("request_irq success!\n");

}else{

printk("request_irq fail!\n");

return -1;

}

return 0;

}

static int char_dev_release(struct inode *inode,struct file *file)

{

free_irq(key_irq1,NULL);

printk("\n\nchar_dev_release success!");

return 0;

}

static struct file_operations char_dev_fops = {

.owner = THIS_MODULE,

.open = char_dev_open,

.release = char_dev_release,

};

static int __init char_dev_init(void)

{

printk("module init\n");

if(register_chrdev(MAJOR,drv_name,&char_dev_fops)<0)

{

printk("fail to register!\n");

return -1;

}

else

printk("success to register!\n");

return 0;

}

static void __exit char_dev_exit(void)

{

unregister_chrdev(MAJOR,drv_name);

printk("module exit\n");

}

module_init(char_dev_init);

module_exit(char_dev_exit);

MODULE_LICENSE("GPL");

MODULE_AUTHOR(

4.3 软中断

由于此书说,“驱动的编写者不会也不宜直接使用softirq”,故这里也不作讲解。大家可以参考这篇文章来学习软中断:

5、内核定时器编程

Linux内核2.4版中去掉了老版本内核中的静态定时器机制,而只留下动态定时器。相应地在timer_bh()函数中也不再通过run_old_timers()函数来运行老式的静态定时器。动态定时器与静态定时器这二个概念是相对于Linux内核定时器机制的可扩展功能而言的,动态定时器是指内核的定时器队列是可以动态变化的,然而就定时器本身而言,二者并无本质的区别。考虑到静态定时器机制的能力有限,因此Linux内核2.4版中完全去掉了以前的静态定时器机制。

5.1 Linux内核对定时器的描述

Linux在include/linux/timer.h头文件中定义了数据结构timer_list来描述一个内核定时器:

struct timer_list {

struct list_head list;

unsigned long expires;

unsigned long data;

void (*function)(unsigned long);

};

各数据成员的含义如下:

(1)双向链表元素list:用来将多个定时器连接成一条双向循环队列。

(2)expires:指定定时器到期的时间,这个时间被表示成自系统启动以来的时钟滴答计数(也即时钟节拍数)。当一个定时器的expires值小于或等于jiffies变量时,我们就说这个定时器已经超时或到期了。在初始化一个定时器后,通常把它的expires域设置成当前expires变量的当前值加上某个时间间隔值(以时钟滴答次数计)。

(3)函数指针function:指向一个可执行函数。当定时器到期时,内核就执行function所指定的函数。而data域则被内核用作function函数的调用参数。

5.2 Linux内核对定时器初始化

内核函数init_timer()用来初始化一个定时器。实际上,这个初始化函数仅仅将结构中的list成员初始化为空。如下所示(include/linux/timer.h):

static inline void init_timer(struct timer_list * timer)

{

timer->list.next = timer->list.prev = NULL;

}

5.3 时间比较操作

在定时器应用中经常需要比较两个时间值,以确定timer是否超时,所以Linux内核在timer.h头文件中定义了4个时间关系比较操作宏。这里我们说时刻a在时刻b之后,就意味着时间值a≥b。Linux强烈推荐用户使用它所定义的下列4个时间比较操作宏(include/linux/timer.h):

#define time_after(a,b) ((long)(b) - (long)(a) < 0)

#define time_before(a,b) time_after(b,a)

#define time_after_eq(a,b) ((long)(a) - (long)(b) >= 0)

#define time_before_eq(a,b) time_after_eq(b,a)

5.4 动态内核定时器的实现

Linux是怎样为其内核定时器机制提供动态扩展能力的呢?其关键就在于“定时器向量”的概念。所谓“定时器向量”就是指这样一条双向循环定时器队列(对列中的每一个元素都是一个timer_list结构):对列中的所有定时器都在同一个时刻到期,也即对列中的每一个timer_list结构都具有相同的expires值。显然,可以用一个timer_list结构类型的指针来表示一个定时器向量。

显然,定时器expires成员的值与jiffies变量的差值决定了一个定时器将在多长时间后到期。在32位系统中,这个时间差值的最大值应该是0xffffffff。因此如果是基于“定时器向量”基本定义,内核将至少要维护0xffffffff个timer_list结构类型的指针,这显然是不现实的。

另一方面,从内核本身这个角度看,它所关心的定时器显然不是那些已经过期而被执行过的定时器(这些定时器完全可以被丢弃),也不是那些要经过很长时间才会到期的定时器,而是那些当前已经到期或者马上就要到期的定时器(注意!时间间隔是以滴答次数为计数单位的)。

基于上述考虑,并假定一个定时器要经过interval个时钟滴答后才到期(interval=expires-jiffies),则Linux采用了下列思想来实现其动态内核定时器机制:对于那些0≤interval≤255的定时器,Linux严格按照定时器向量的基本语义来组织这些定时器,也即Linux内核最关心那些在接下来的255个时钟节拍内就要到期的定时器,因此将它们按照各自不同的expires值组织成256个定时器向量。而对于那些256≤interval≤0xffffffff的定时器,由于他们离到期还有一段时间,因此内核并不关心他们,而是将它们以一种扩展的定时器向量语义(或称为“松散的定时器向量语义”)进行组织。所谓“松散的定时器向量语义”就是指:各定时器的expires值可以互不相同的一个定时器队列。

具体的组织方案可以分为两大部分:

(1)对于内核最关心的、interval值在[0,255]之间的前256个定时器向量,内核是这样组织它们的:这256个定时器向量被组织在一起组成一个定时器向量数组,并作为数据结构timer_vec_root的一部分,该数据结构定义在kernel/timer.c文件中,如下述代码段所示:

点击(此处)折叠或打开

/*

* Event timer code

*/

#define TVN_BITS 6

#define TVR_BITS 8

#define TVN_SIZE (1 << TVN_BITS)

#define TVR_SIZE (1 << TVR_BITS)

#define TVN_MASK (TVN_SIZE - 1)

#define TVR_MASK (TVR_SIZE - 1)

struct timer_vec {

int index;

struct list_head vec[TVN_SIZE];

};

struct timer_vec_root {

int index;

struct list_head vec[TVR_SIZE];

};

static struct timer_vec tv5;

static struct timer_vec tv4;

static struct timer_vec tv3;

static struct timer_vec tv2;

static struct timer_vec_root tv1;

static struct timer_vec * const tvecs[] = {

(struct timer_vec *)&tv1, &tv2, &tv3, &tv4, &tv5

};

#define NOOF_TVECS (sizeof(tvecs) / sizeof(tvecs[0]))

基于数据结构timer_vec_root,Linux定义了一个全局变量tv1,以表示内核所关心的前256个定时器向量。这样内核在处理是否有到期定时器时,它就只从定时器向量数组tv1.vec[256]中的某个定时器向量内进行扫描。而tv1的index字段则指定当前正在扫描定时器向量数组tv1.vec[256]中的哪一个定时器向量,也即该数组的索引,其初值为0,最大值为255(以256为模)。每个时钟节拍时index字段都会加1。显然,index字段所指定的定时器向量tv1.vec[index]中包含了当前时钟节拍内已经到期的所有动态定时器。而定时器向量tv1.vec[index+k]则包含了接下来第k个时钟节拍时刻将到期的所有动态定时器。当index值又重新变为0时,就意味着内核已经扫描了tv1变量中的所有256个定时器向量。在这种情况下就必须将那些以松散定时器向量语义来组织的定时器向量补充到tv1中来。

(2)而对于内核不关心的、interval值在[0xff,0xffffffff]之间的定时器,它们的到期紧迫程度也随其interval值的不同而不同。显然interval值越小,定时器紧迫程度也越高。因此在将它们以松散定时器向量进行组织时也应该区别对待。通常,定时器的interval值越小,它所处的定时器向量的松散度也就越低(也即向量中的各定时器的expires值相差越小);而interval值越大,它所处的定时器向量的松散度也就越大(也即向量中的各定时器的expires值相差越大)。

内核规定,对于那些满足条件:0x100≤interval≤0x3fff的定时器,只要表达式(interval>>8)具有相同值的定时器都将被组织在同一个松散定时器向量中。因此,为组织所有满足条件0x100≤interval≤0x3fff的定时器,就需要26=64个松散定时器向量。同样地,为方便起见,这64个松散定时器向量也放在一起形成数组,并作为数据结构timer_vec的一部分。基于数据结构timer_vec,Linux定义了全局变量tv2,来表示这64条松散定时器向量。如上述代码段所示。

对于那些满足条件0x4000≤interval≤0xfffff的定时器,只要表达式(interval>>8+6)的值相同的定时器都将被放在同一个松散定时器向量中。同样,要组织所有满足条件0x4000≤interval≤0xfffff的定时器,也需要26=64个松散定时器向量。类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了tv3全局变量来表示这64个松散定时器向量。

对于那些满足条件0x100000≤interval≤0x3ffffff的定时器,只要表达式(interval>>8+6+6)的值相同的定时器都将被放在同一个松散定时器向量中。同样,要组织所有满足条件0x100000≤interval≤0x3ffffff的定时器,也需要26=64个松散定时器向量。类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了tv4全局变量来表示这64个松散定时器向量。

对于那些满足条件0x4000000≤interval≤0xffffffff的定时器,只要表达式(interval>>8+6+6+6)的值相同的定时器都将被放在同一个松散定时器向量中。同样,要组织所有满足条件0x4000000≤interval≤0xffffffff的定时器,也需要26=64个松散定时器向量。类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了tv5全局变量来表示这64个松散定时器向量。

最后,为了引用方便,Linux定义了一个指针数组tvecs[],来分别指向tv1、tv2、…、tv5结构变量。如上述代码所示。

6、内核延时

6.1短延时

内核函数ndelay, udelay,以及mdelay对于短延时好用,分别延后执行指定的纳秒数,微秒数或者毫秒数.它们的原型是:

#include

void ndelay(unsigned long nsecs);

void udelay(unsigned long usecs);

void mdelay(unsigned long msecs);

这些函数的实际实现在,是体系特定的,并且有时建立在一个外部函数上.每个体系都实现udelay,但是其他的函数可能或者不可能定义;如果它们没有定义,提供一个缺省的基于udelay的版本.在所有的情况中,获得的延时至少是要求的值,但可能更多;实际上,当前没有平台获得了纳秒的精度,尽管有几个提供了次微秒的精度.延时多于要求的值通常不是问题,因为驱动中的短延时常常需要等待硬件,并且这个要求是等待至少一个给定的时间流失.

重要的是记住这3个延时函数是忙等待;因而在延迟过程中无法运行其他任务。因此,应当在没有其他实用方法的情况下才使用这些函数。

另一个方法获得毫秒(和更长)延时而不用涉及到忙等待.文件声明这些函数:

void msleep(unsigned int millisecs);

unsigned long msleep_interruptible(unsigned int millisecs);

void ssleep(unsigned int seconds)

前2个函数使调用进程进入睡眠给定的毫秒数.一个对msleep的调用是不可中断的;你能确保进程睡眠至少给定的毫秒数.如果你的驱动位于一个等待队列并且你想唤醒来打断睡眠,使用msleep_interruptible.从msleep_interruptible的返回值正常地是0;如果,但是,这个进程被提早唤醒,返回值是在初始请求睡眠周期中剩余的毫秒数.对ssleep的调用使进程进入一个不可中断的睡眠给定的秒数.

6.2长延时

长延时有三种方法:忙等待,让出处理器,超时(睡着延时)。前两种方法(在LDD3上有详细讲解)一般不建议使用,这里只讲第三种方法睡着延时。

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

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

#include linux/wait.h>

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 linux/sched.h>

signed long schedule_timeout(signed long timeout);

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

set_current_state(TASK_INTERRUPTIBLE);

schedule_timeout (delay);

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值