Linux内核开发之中断与时钟(一) “小王,醒醒,开始上课了,今天咱们开始讲中断,这可是高级东西,错过不补哈”我使劲推着睡梦中的小王。 “嗯?感情好啊,快点,快点”小王一听有新东西讲,像打了鸡血似的兴奋,连我都怀疑起她是不是性格中喜新厌旧。 不管那么多了,我讲我的,她厌她的… 啥叫中断?就是指cpu在执行过程中,出现了某些突发事件时CPU必须暂停执行当前的程序,转去处理突发事件,处理完毕后CPU有返回原程序被中断的位置并继续执行。 中断的分法不懂,分类就不同,向什么内外部中断,可/不可屏蔽中断…等等乱七八糟一大堆,我这里要说明的一点是按照中断入口跳转方法的不同,可分为向量中 断和非向量中断。采用向量中断的CPU通常为不同的中断分配不同的中断号,当检测到某中断号的中断到来后,就自动跳转到与该中断号对应的地址执行。不同的 中断号有不同的中断地址(即入口)。而非向量中断的多个中断共享一个入口地址。进入后根据软件判断中断标志来识别具体是哪个中断。也就是说,向量中断是由 硬件提供中断服务程序入口地址,非向量中断由软件提供中断服务程序入口地址。 我们在后边会说到一个时钟定时器,它也是通过中断来实现的。它的原理很简单,嵌入式微处理器它接入一个时钟输入,当时钟脉冲到来时,就将目前的计数器值加1并和预先设置的计数值比较,若相等,证明计数周期满,产生定时器中断并复位目前计数器值。
Linux中断处理架构 设 备的中断会打断内核中进程的正常调度和运行,会影响系统的性能。为了在中断执行时间尽可能短和中断处理需完成大量工作之间找到一个平衡点,Linux将中 断处理程序分解成两个半部:顶半部和底半部。其中顶半部尽可能完成尽可能少的比较紧急的功能。而底半部几乎做了中断处理程序所有的事情,而且可以被新的中 断打断。 在linux设备驱动中,提供了一系列函数来帮助设备实现中断的相关操作: 1)设备申请中断 int request_irq(unsigned int irq, //irq是要申请的中断号 void (*handler)(int irq, void *dev_id, struct pt_regs * *regs),//回调函数,中断发生时,系统会调用该函数, unsigned long irqflags, const char *devname, void *dev_id); 其中irqflags是中断处理的属性,若设置为SA_INTERRUPT,则表示中断处理程序是快速处理程序,它被调用时屏蔽所有中断。若设置为SA_SHIRQ,则表示多个设备共享中断,dev_id在中断共享时会用到,一般设置为这个设备的设备结构体或者NULL. 该函数返回0表示成功,返回-INVAL表示中断号无效或处理函数指针为NULL,返回EBUSY表示中断已经被占用且不能共享。 2)释放中断 free_irq(unsigned int irq, void *dev_id); 3)使能和屏蔽中断 void disable_irq(int irq); //这个会立即返回 void disable_irq_nosync(int irq);//等待目前的中断处理完成再返回。 void enable_irq(int irq); 上述三个函数作用于可编程中断处理器,因此对系统内所有的CPU都生效。 void local_irq_save(unsigned long flags);//会将目前的中断状态保留在flags中 void local_irq_disable(void);//直接中断 这两个将屏蔽本CPU内的所有中断。对应的上边两个中断的方法如下 void local_irq_restore(unsigned long flags); void local_irq_enable(void);
我们两边说了Linux系统中中断是分为顶半部和底半部的,那么在系统实现方面是具体怎样实现的呢,这主要有tasklet,工作队列,软中断: 1)tasklet:使用比较简单,如下: void my_tasklet_function(unsigned long); //定义一个处理函数 DECLARE_TASKLET(my_tasklet, my_tasklet_function, data); //定义了一个名叫my_tasklet的tasklet并将其与处理函数绑定,而传入参数为data 在需要调度tasklet的时候引用一个tasklet_schedule()函数就能使系统在适当的时候进行调度运行:tasklet_schedule(&my_tasklet); 2)工作队列:使用方法和tasklet相似,如下: struct work_struct my_wq; //定义一个工作队列 void my_wq_func(unsigned long); //定义一个处理函数 通过INIT_WORK()可以初始化这个工作队列并将工作队列与处理函数绑定,如下: INIT_WORK(&my_wq, (void (*)(void *))my_wq_func, NULL); //初始化工作队列并将其与处理函数绑定 同样,使用schedule_work(&my_irq);来在系统在适当的时候需要调度时使用运行。 3)软中断:使用软件方式模拟硬件中断的概念,实现宏观上的异步执行效果,tasklet也是基于软中断实现的。 在Linux内核中,用softirq_action结构体表征一个软中断,这个结构体中包含软中断处理函数指针和传递给函数的参数,使用open_softirq()可以注册软中断对应的处理函数,而raise_softirq()函数可以触发一个中断。 软中断和tasklet仍然运行与中断上下文,而工作队列则运行于进程上下文。因此,软中断和tasklet的处理函数不能休眠,但工作队列是可以的。 local_bh_disable()和local_bh_enable()是内核用于禁止和使能软中断和tasklet底半部机制的函数。
下边咱们再来说说有关中断共享的相关点:中断共享即是多个设备共享一根硬件中断线的情况。Linux2.6内核支持中断共享,使用方法如下: *共享中断的多个设备在申请中断时都应该使用SA_SHIRQ标志,而且一个设备以SA_SHIRQ申请某中断成功的前提是之前该中断的所有设备也都以SA_SHIRQ标志申请该终端 *尽管内核模块可访问的全局地址都可以作为request_irq(….,void *dev_id)的最后一个参数dev_id,但是设备结构体指针是可传入的最佳参数。 *在中断带来时,所有共享此中断的中断处理程序都会被执行,在中断处理程序顶半部中,应迅速根据硬件寄存器中的信息比照传入的dev_id参数判断是否是被设备的中断,如果不是,应迅速返回。
结语:在这次讲解中说了三种Linux系统中中断的顶/底半部机制和中断共享的先关内容,但碍于页面空间的原因,没有给出例子,我在下次博客中会专门来对每个点给出典型的模版. “小涛哥,快醒醒,快醒醒..”小王使劲推着睡梦中的我,“你不是说今天要讲昨天有关的典型模板实例吗…” “啊?小姐啊,现在才早上8点,还让人睡觉不,别吵”我一头钻进被子里说。 “不管,谁让你昨天不说完,还卖个小关子,害我昨天晚上都没睡好,想了一晚上…” 我揉揉蒙蒙的眼说:“行,权当看在你渴求的心情上,但只此一次,下不为例,我还想好好睡懒觉呢..” 昨天我们讲了有关中断方面的东西,鉴于小王你不太懂,我今天就专门拿出一章来说说前边中断的使用典型模版,你照抄也方便不是: 1)在中断分类中,我们说到了有关向量中断和非向量中断,向量中断就是入口地址不同,进不同的地址做不同的事。那非向量中断则是进同一地址,至于区分就放在了进去后用条件判断,请看下边的模板: irq_handler() { ... int int_src = read_int_status(); //读硬件的中断相关寄存器 switch(int_src) //判断中断源 { case DEV_A: dev_a_handler(); break; case DEV_B: dev_b_handler(); break; .... default: break; } } 2)在底半部机制中,我们讲了tasklet,工作队列和软中断先来看tasklet tasklet使用模版: void xxx_do_tasklet(unsigned long); DECLARE_TASKLET(XXX_tasklet, xxx_do_tasklet, 0); void xxx_do_tasklet(unsigned long) //中断处理底半部 { ..... } irqreturn_t xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs) //中断处理顶半部 { ... tasklet_schedule(&xxx_tasklet); } int __init xxx_init(void) //设备驱动模块加载函数 { .. result= request_irq(xxx_irq, xxx_interrupt, SA_INTERRUPT, "XXX",NULL); //申请中断 ... } void __exit xxx_exit(void) //设备驱动卸载模块 { .. free_irq(xxx_irq, xxx_interrupt); //释放中断 .. } struct work_struct xxx_wq; void xxx_do_work(unsigned long); void xxx_do_work(unsigned long) //中断处理底半部 { ..... } irqreturn_t xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs) //中断处理顶半部 { ... schedule_work(&xxx_wq); } int xxx_init(void) //设备驱动模块加载函数 { .. result= request_irq(xxx_irq, xxx_interrupt, SA_INTERRUPT, "XXX",NULL); //申请中断 ... INIT_WORK(&xxx_wq, (void (*)(void *))xxx_do_work, NULL); ... } void __exit xxx_exit(void) //设备驱动卸载模块 { .. free_irq(xxx_irq, xxx_interrupt); //释放中断 .. } 3)在上节最后我还给你讲了有关中断共享的东西吧,小王,也把模版给你: irqreturn_t xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs) //中断处理顶半部 { ... int status = read_int_status(); //获取终端源 if(!is_myint(dev_id, status)) //判断是否是本设备的中断 { return IRQ_NONE://立即返回 } .. return IRQ_HANDLED; } int __init xxx_init(void) //设备驱动模块加载函数 { .. result= request_irq(xxx_irq, xxx_interrupt, SA_SHIRQ, "XXX",xxx_dev); //申请共享中断 ... } void __exit xxx_exit(void) //设备驱动卸载模块 { .. free_irq(xxx_irq, xxx_interrupt); //释放中断 .. } 共享中断中,我们仔细看一下其实也没什么,不是。就是在和前边中断中要修改一下中断标志,在中断处理中判断一下是否是自己本地的中断,这个我都用红色的标识出来了。
“小王,看,小涛哥说话算数吧,上边给出了所有模版,结合前一篇,相信你可以看的很顺利的。好了,我要补补刚的觉了,中间不许叫我哈,想我也不行”我打打哈欠说。 晚上7点10分.. “小涛哥,这章不是叫Linux设备驱动程序之中断与时钟,前边你讲了中断,还给了我很多模版,我都看懂了,这次是不是要开始讲时钟了..” “真聪明,越来越喜欢你这聪明的样子了,说的不错,今天就要开始一个新的模块--内核时钟”我很少夸人,为啥今天夸她呢了,呵呵. 定时器,意思大家都明白,我就不说了,要是不明白,把它想成个闹钟总可以吧.. 定时器分为硬件和软件定时器,软件定时器最终还是要依靠硬件定时器来完成。内 核在时钟中断发生后检测各定时器是否到期,到期后的定时器处理函数将作为软中断在底半部执行。实质上,时钟中断处理程序执行 update_process_timers函数,该函数调用run_local_timers函数,这个函数处理TIMER_SOFTIRQ软中断,运 行当前处理上到期的所有定时器。 Linux内核中定义提供了一些用于操作定时器的数据结构和函数如下: 1)timer_list:说定时器,当然要来个定时器的结构体 struct timer_list{ struct list_head entry; //定时器列表 unsigned long expires; //定时器到期时间 void (*function)(unsigned long) ;//定时器处理函数 unsigned long data; //作为参数被传入定时器处理函数 struct timer_base_s *base; } 2)初始化定时器:void init_timer(struct timer_list *timer);经过这个初始化后,entry的next为NULL,并给base赋值 4)删除定时器:int del_timer(struct timer_list *timer); 说明:del_timer_sync是del_timer的同步版,主要在多处理器系统中使用,如果编译内核时不支持SMP,del_timer_sync和del_timer等价. 5)修改定时器:int mod_timer(struct timer_list *timer, unsigned long expires); 下边是一个使用定时器的模版: struct xxx_dev /*second设备结构体*/ { struct cdev cdev; /*cdev结构体*/ ... struct timer_list xxx_timer; /*设备要使用的定时器*/ }; int xxx_func1(...) //xxx驱动中某函数 { struct xxx_dev *dev = filp->private_data; ... /*初始化定时器*/ init_timer(&dev->xxx_timer); dev->xxx_timer.function = &xxx_do_handle; dev->xxx_timer.data = (unsigned long)dev; dev->xxx_timer.expires = jiffies + delay; add_timer(&dev->xxx_timer); /*添加(注册)定时器*/ ... return 0; } int xxx_func2(...) //驱动中某函数 { ... del_timer(&second_devp->s_timer); ... } static void xxx_do_timer(unsigned long arg) //定时器处理函数 { struct xxx_device *dev = (struct xxx_device *)(arg); ... //调度定时器再执行 dev->xxx_timer.expires = jiffies + delay; add_timer(&dev->xxx_timer); } 在定时器函数中往往会在做完具体工作后,延迟expires并将定时器再次添加到内核定时器链表中,以便定时器能被再次触发(这句话我也是从别处抄来的,别告诉小王哈)。 在内核定时器中,常常少不了要说下内核延迟的事,请接着往下看: 1)短延迟:在linux内核中提供了三个函数来分别实现纳秒,微秒,毫秒延迟,原理上是忙等待,它根据CPU频率进行一定次数的循环 void ndelay(unsigned long nsecs); void udelay(unsigned long usecs); void mdelay(unsigned long msecs); 毫秒延迟已经相当大了,当然更秒延迟当然要小一些,在内核中,为了性能,最好不要用mdelay,这会耗费大量cpu资源,那么咋办呢,凉拌.. void msleep(unsigned int millisecs); unsigned long msleep_interruptible(unsigned int millisecs); void ssleep(unsigned int seconds); 这三个是内核专门提供该我们用来处理毫秒以上的延迟。上述函数将使得调用它的进程睡眠参数指定的秒数,其中第二个是可以被打断的,其余的两个是不可以的。 2)长延迟:内核中进行延迟最常用的方法就是比较当前的jiffies和目标jiffies(当前的加上时间间隔的jiffies),直到未来的jiffies达到目标jiffies。比如: unsigned long delay = jiffies + 100; //延迟100个jiffies while(time_before(jiffies, delay)); 与time_before对应的还有一个time_after().其实就是#define time_before(a,b) time_after(b,a); 另外两个是time_after_eq(a,b)和time_before_eq(a,b) 3)睡着延迟:这显然是比忙等待好的方法,因为在未到来之前,进程会处于睡眠状态,把 CPU空出来,让CPU可以做别的事情,等时间到了,调用schedule_timeout()就可以唤醒它并重新调度执行。msleep和 msleep_interruptible本质上都是依靠包含了schedule_timeout的 schedule_timeout_uninterruptible()和schedule_ timeout_interruptible()实现。就像下边这样: void msleep(unsigned int msecs) { unsigned long timeout = msecs_to_jiffies(msecs) + 1; while(timeout) timeout = schedule_timeout_uninterruptible(timeout); } unsigned long msleep_interruptible(unsigned int msecs) { unsigned long timeout = msecs_to_jiffies(msecs) + 1; while(timeout && !signal_pending(current)) timeout = schedule_timeout_interruptible(timeout); return jiffies_to_msecs(timeout); } signed long __sched schedule_timeout_interruptible()signed long timeout) { __set_current_state(TASK_INTERRUPTIBLE); return schedule_timeout(timeout); } signed long __sched schedule_timeout_uninterruptible()signed long timeout) { __set_current_state(TASK_UNINTERRUPTIBLE); return schedule_timeout(timeout); } 另外还有如下:
time_on_timeout(wait_queue_head_t *q, unsigned long timeout);
interruptible_sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);
这两个将当前进程添加到等待队列,从而在等待队列上睡眠,当超时发生时,进程将被唤醒。
“小王,有关中断和系统时钟的咱们也讲完了,下次就给你来一个有关系统时钟的设备驱动例子,巩固一下吧,你可要抓紧哦..“ Linux内核开发之中断与时钟(四) “小王,小王,今天可是这一章节最后一节了,知识点咱们前边都讲过了,今天主要是给你用前边的东西讲一个实际例子---秒字符设备驱动程序” 这个驱动程序会在被打开的时候初始化一个定时器并将其添加到内核定时器链表中,每秒输出一次当前的jiffies,这意味着,定时器处理函数中每次都要修改新的expires。不多说了,看代码分析: #include …//必要的系统头文件 #define SECOND_MAJOR 252 /*预设的second的主设备号*/ static int second_major = SECOND_MAJOR; struct second_dev /*second设备结构体*/ { struct cdev cdev; /*cdev结构体*/ atomic_t counter;/* 一共经历了多少秒?*/ struct timer_list s_timer; /*设备要使用的定时器*/ }; struct second_dev *second_devp; /*设备结构体指针*/ static void second_timer_handle(unsigned long arg) /*定时器处理函数*/ { mod_timer(&second_devp->s_timer,jiffies + HZ); atomic_inc(&second_devp->counter); printk(KERN_NOTICE "current jiffies is %ld\n", jiffies); } int second_open(struct inode *inode, struct file *filp) /*文件打开函数*/ { /*初始化定时器*/ init_timer(&second_devp->s_timer); second_devp->s_timer.function = &second_timer_handle; second_devp->s_timer.expires = jiffies + HZ; add_timer(&second_devp->s_timer); /*添加(注册)定时器*/ atomic_set(&second_devp->counter,0); //计数清0 return 0; } int second_release(struct inode *inode, struct file *filp) /*文件释放函数*/ { del_timer(&second_devp->s_timer); return 0; } static ssize_t second_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) /*globalfifo读函数*/ { int counter; counter = atomic_read(&second_devp->counter); if(put_user(counter, (int*)buf)) return - EFAULT; else return sizeof(unsigned int); } static const struct file_operations second_fops = /*文件操作结构体*/ { .owner = THIS_MODULE, .open = second_open, .release = second_release, .read = second_read, }; static void second_setup_cdev(struct second_dev *dev, int index) /*初始化并注册cdev*/ { int err, devno = MKDEV(second_major, index); cdev_init(&dev->cdev, &second_fops); dev->cdev.owner = THIS_MODULE; dev->cdev.ops = &second_fops; err = cdev_add(&dev->cdev, devno, 1); if (err) printk(KERN_NOTICE "Error %d adding LED%d", err, index); } int second_init(void) /*设备驱动模块加载函数*/ { int ret; dev_t devno = MKDEV(second_major, 0); /* 申请设备号*/ if (second_major) ret = register_chrdev_region(devno, 1, "second"); else /* 动态申请设备号 */ { ret = alloc_chrdev_region(&devno, 0, 1, "second"); second_major = MAJOR(devno); } if (ret < 0) return ret; /* 动态申请设备结构体的内存*/ second_devp = kmalloc(sizeof(struct second_dev), GFP_KERNEL); if (!second_devp) /*申请失败*/ { ret = - ENOMEM; goto fail_malloc; } memset(second_devp, 0, sizeof(struct second_dev)); second_setup_cdev(second_devp, 0); return 0; fail_malloc: unregister_chrdev_region(devno, 1); } void second_exit(void) /*模块卸载函数*/ { cdev_del(&second_devp->cdev); /*注销cdev*/ kfree(second_devp); /*释放设备结构体内存*/ unregister_chrdev_region(MKDEV(second_major, 0), 1); /*释放设备号*/ } MODULE_AUTHOR("hanyan225"); MODULE_LICENSE("Dual BSD/GPL"); module_param(second_major, int, S_IRUGO); module_init(second_init); module_exit(second_exit); 下面是测试程序: #include ..//必要的头文件 main() { int fd; int counter = 0; int old_counter = 0; fd = open("/dev/second", O_RDONLY); /*打开/dev/second设备文件*/ if (fd != - 1) { while (1) { read(fd,&counter, sizeof(unsigned int));//读目前经历的秒数 if(counter!=old_counter) { printf("seconds after open /dev/second :%d\n",counter); old_counter = counter; } } } else { printf("Device open failure\n"); } } 当我们编译完驱动程序,并运行了测试程序后,会看到应用程序不断输出自打开/dev/second以来经历的秒数。如下:
#./test
seconds after open /dev/second 1
seconds after open /dev/second 2
..
..
再带一个中断,看看内核输出操作如下:
#tar –f /var/logs/message
current jiffies is 18569
current jiffies is 18669
current jiffies is 18769 ..
“小王,Linux设备驱动之中断与时钟也算说完了,告一段落了,也不知道你明白没,没明白,也没关系,不是有我吗,只是不要一早吵醒我就好,下次我们就要开始系统内存方面的东西了…”我说。 “好,小涛哥,我好好看看,不懂就问你,呵呵..”小王银铃般笑着还伴着怪脸,真是让人… |
|