1. 为什么要有中断?
CPU的处理速度比外部硬件块.2.采用轮询的方式比较耗费CPU资源.3.中断处理的效率比较高.. 内核处理优先级:硬件中断 >软件中断 > 普通进程
2. 中断的硬件触发流程.
硬件外设产生硬件的电信号变化,这个电信号首先发送给中断控制器(能够打开,关闭中断,能够指定中断的优先级,还能够判断中断是否发生),中断控制器判断是否使能,判断优先级,最终决定是否给cpu发送一个电信号,cpu一旦检测到这个电信号以后,cpu就会处理这个中断。
1. linux内核如何实现硬件中断编程.
3.1gpio对于内核是一种资源,系统调用号对于内核是一种资源,设备号对于内核也是一种资源,物理内存对于内核是一种资源,同样硬件中断对于内核也是一种宝贵的资源!所以在内核处理中断时,首先要向内核去申请中断资源。
3.2中断的处理流程包括4个部分:建立异常向量表,编写保存现场的代码,编写恢复现场的代码,编写硬件中断对应的服务程序。对于一般的arm裸板程序,需要完全自己实现4个部分。但是在内核中断编程时,前3个都是内核已经帮你写好!对于驱动开发只需实现第4部,编写硬件中断对应的服务程序。
2. 申请硬件中断资源和注册对应的硬件中断的服务程序.
4.1int request_irq(unsigned int irq,irq_handler_t handler,unsigned long irqflags,const char *name,void *dev_id);
函数功能:
1.向内核申请硬件中断资源2.注册这个硬件中断对应的服务程序,一旦这个中断被触发,内核就会调用这个中断对应的服务程序;
参数:irq:待申请的中断号,用来在内核空间中表示硬件资源,内核用irq_eint(0)来表示外部中断0,用irq_eint(1)来表示外部中断1,这个宏都是在内核平台头文件定义,由芯片公司实现。中断号0--31内核保留!
handler:待注册的中断处理函数
irqflsgs:中断标志 给中断控制器确定中断的出发方式
IRQF_TRGGER_RISING|TRQF_TRIGGER_FALLING,这个宏最终是给中断控制器配置的!如果硬件中断时内部中断(串口)(外设涉及中断触发方式无法人为去配置,一般这个参数写0.)
IRQF_SHARED:表示多个设备共享中断 IRQF_SAMPLE_RANDOM:用于随机数种子的随机采样.IRQF_TRIGGER_RISING:上升沿触发中断 IRQF_TRIGGER_FALLING:下降沿触发中断
IRQF_TRIGGGER_HIGH:高电平触发 IRQF_TRIGGER_LOW:低电平触发
IRQF_DISABLE:本中断处理时,其它中断进行屏蔽。
name:中断设备的名称,出现在cat /proc/interrupts 一般用于中断调试
dev_id:传递给中断处理函数的指针,通常用于共享中断时传递设备结构体,可以通过dev_id给中断处理函数传递参数 返回:成功返回0,失败返回负值,-EINVAL:表示申请的中断号无效或者中断处理函数
4.2硬件中断如果不在使用,一定要释放硬件中断资源和卸载对应处理函数:
free_irq(int irq,void *dev_id);函数功能:释放中断和卸载对应的处理函数
参数:irq:中断号dev_id:这个参数一定要和注册中断时传递的参数保持一致,否则内核出现崩溃!在注册的时候,中断号就已经进行了绑定!
3. 中断处理函数原型。
irqturn_t(*irq_handler_t)(int irq,void *dev_id);
irq:中断号dev_id:在注册中断处理函数时传递过来的参数,如果不传递参数指定为NULL,
返回:常见返回值如下:IRQ_NONE:中断未做处理IRQ_HANDLED:正常后处理应该返回该值
6.中断处理函数的要求。
1.明确之前所说的硬件中断优先级仅仅适用于中断控制器
2.linux内核对于硬件中断无优先级这个概念,明确linux内核硬件中断优先级
高于软件中断的优先级,软中断的优先级高于进程;
软中断分为优先级(2级),进程也有优先级。
3.就是因为linux内核对于硬件中断无优先级,所以要求中断处理函数的执行速度要快,让中断及时释放cpu资源给别的中断或者进程使用。如果中断处理函数长时间的占有cpu资源,别的硬件中断或者进程,软中断无法获取cpu资源,影响系统的并发能力和响应能力!
4.在中断处理函数中千万不能调用引起阻塞(忙等待或者休眠等待)的函数,例如
copy_to_user,copy_from_user,kmalloc
5.中断不属于任何进程,不参与进程的调度(实时linux内核将中断线程化)
6.中断处理函数中不能和用户进行数据的交互,如果要进行数据交互,一定要配合系统调用
(struct file_operations)!
7.linux内核指定的中断栈为1页,早期内核中断栈共享进程内核栈(8k).
7. 共享中断:多个硬件外设共享一个硬件中断资源。
注意:有多个硬件外设,意味着有多个设备驱动,有多个设备驱动意味着每一个驱动都会调用request_irq注册中断。
共享中断的编程要求:
1.必须指定IRQF_SHARED;2.dev_id必须不一样3.中断处理函数不能调用disable_irq(关闭中断),如果关闭中断,那么共享设备的资源不能处理中断。4.cpu区分外设产生中断前提是外设必须硬件设备上具备判断中断是否是其产生的条件!外设硬件不具备这个条件,这个外设不能使用共享中断!
4. 关于中断处理的相关操作.
6.1理论知识: 在linux内核中硬件中断无优先级,软件中断中有两个优先级 可以设置进行的优先级.中断处理为内核空间.由于中断处理函数不属于任何进程,因为没有用户进程占用的地址空间,则中断处理函数向用户空间发送数据和接收数据。中断中需要内核和用户进行数据交互需要通过系统调用接口(file_operations接口),阻塞分为忙等待和休眠等待.
6.2中断处理函数的特点.(解决此办法的底半部和顶半部)
1.中断随机出现,且中断处理函数不属于任何进程,处于中断上下文. 2.执行速度要快和简洁,且中断栈为一页. 3.不能向用户空间发送和接收数据. 4.不能调用可能引起阻塞的函数. Copy_to_user/kmalloc.
6.3如果中断处理函数需要执行的时间比较长,怎么解决?
明确:如果中断处理函数长时间的占用cpu资源,会导致别的任务无法获取cpu资源,影响系统的并发能力和响应能力。甚至如果在中断处理函数中进行休眠操作,最终导致linux系统处于僵尸状态!
结论:
linux内核为了提供系统的并发能力和响应能力,解决中断处理函数长时间的占有cpu的情况,linux内核将中断处理函数进行划分,划分为两部分:顶半部,底半部。
注意这种划分不是函数的划分和函数之间的调用!
顶半部:本质上还是之前的中断处理函数,其中完成的内容相对比较紧急,耗时较短,遵循linux内核要求中断处理函数实行的速度这个原则,一旦中断发生之后,内核首先执行顶半部内容,但是这个顶半部占用cpu的时间非常短,也就保证其他任务可以及时获取到cpu的资源。其他复杂的事情可以放在底半部执行。顶半部不可以被中断!顶半部还需要登记底半部,告诉cpu我的中断还需要一些比较耗时的内容在将来(空闲时)要你去完成!
底半部:完成之前中断处理函数中比较不耗时,不紧急的事情!可以被别的中断(硬件中断和软件中断,甚至是进程)打断!
5. 中断底半部的实现。(重点)
1.tasklet(小任务)
2.工作队列--与等待队列不同
3.软中断
7.1tasklet:又名“小任务”,软中断任务,优先级高于进程,但是低于硬件中断。运行在中断上下文!
linux内核描述tasklet使用的数据结构:
struct tasklet_struct{ <linux/interrupt.h>
void (*func)(unsigned long );//底半部处理函数
unsigned long data; //给底半部处理函数传递的参数,一般传递指针,要注意在处理函数中对数据类型的转换};
1.分配初始化tasklet对象
方法1:
DECLARE_TASKLET(tasklet变量名,tasklet处理函数,给处理函数传递的参数);
方法2:
struct tasklet_struct tasklet; //分配
tasklet_init(&tasklet,处理函数,给处理函数传递的参数);//初始化
2.在顶半部(中断处理函数)中调用tasklet_schedule函数进行登记底半部tasklet,是登记而不是执行!一旦登记成功,顶半部肯定会先执行完毕,赶紧释放cpu资源,tasklet的处理函数会cpu空闲时去执行。
3.注意事项:tasklet还是工作在中断上下文中,遵循中断的处理过程,千万不能做休眠阻塞的事情!
问题:底半部就需要休眠,怎么办?工作队列机制
案例:将按键中断采用tasklet来实现。
<linux/interrupt.h>tasklet定义:DECLARE_TASKLET(taskletname,tasklet_func,data);
--yaskletname:待定义的tasklet名字--tasklet_func:tasklet处理函数--data:待传入tasklet处理函数的参数
代码:
#include<linux/init.h>
#include<linux/module.h>
#include<linux/irq.h>
#include<linux/interrupt.h>
//定义硬件相关的数据结构
struct btn_resource{
int irq; //中断号
char *name;//中断名称};//分配初始化按键信息
static structbtn_resource btn_info[] ={
[0] = {
.irq = IRQ_EINT(0),
.name ="KEY_UP"
},
[1] ={
.irq = IRQ_EINT(1),
.name ="KEY_DOWM"
}
};
static int mydata =0x5555;
static void btn_tasklet_func(unsignedlong data){
int *p=(int *)data;//数据类型转换
printk("底半部%s:data=%#x",__func__,*p);
}
//1. 分配初始化tasklet对象//第一个参数是tasklet对象名
//第二个参数是tasklet延后处理函数//第三个参数给延后处理函数传递的参数
staticDECLARE_TASKLET(btn_tasklet,
btn_tasklet_func,
(unsignedlong)&mydata);
//中断处理函数(顶半部)
static irqreturn_t button_isr(intirq, void *dev_id){
//2.登记底半部tasklet
tasklet_schedule(&btn_tasklet);
printk("顶半部%s\n",__func__);
return IRQ_HANDLED;
}
static intbtn_init(void){
int i;
//申请硬件中断资源和注册中断处理函数
for(i=0;i<ARRAY_SIZE(btn_info);i++)
request_irq(btn_inifo[i].irq,button_isr,IRQF_TRIGGER_FALLING||TRQF_TRIGGER_RISING,i
btn_info[i].name,&btn_info[i]);
return 0;
}
static voidbtn_exit(void){
int i;
//释放硬件中断资源和卸载中断处理函数
for(i=0;i<ARRAY_SIZE(btn_info);i++)
free_irq(btn_info[i].irq,&btn_info[i]);
}
module_init(btn_init);
module_exit(btn_exit);
MODULE_LICENSE("GPL");
**********************************************
问题:底半部就需要休眠,怎么办?
答;tasklet的延后处理函数不允许休眠,但是在某些场合可能进行休眠操作,又要延后执行,这是可以考虑使用工作队列。工作队相关的延后处理函数允许休眠。明确:“休眠” 这个词仅仅适用于进程!
7.2工作队列(work queue)内核线程执行。
工作队列是linux kernel中将工作推后执行的一种机制;这种机制和硬件中断或tasklet不同之处在于工作队列是把推后的工作交由一个内核线程去执行,因此工作队列的优势就在于它允许重新调用甚至睡眠.工作队列是2.6内核开始引入的机制,在2.6.20之后,工作队列的数据结构发生了一些变化,被拆分成两个部分 在此,主要对2.6.20之后的版本作介绍
定义在<linux/workqueue.h>
工作队列实现过程:
1.工作队列延后执行设计数据结构
struct work_struct{
atomic_long data; //记录工作状态
work_func_t func; //工作处理函数,由用户实现,一旦cpu空闲,cpu就会执行这个函数
} typedef void(*work_fucn_t)(struct work_struct *work);//工作原型
struct delayed_work{
struct work_struct work; //用来包含工作延后处理函数
struct timer_list timer;//用来指定执行时间间隔
}; //处理延时
2.如何使用工作队列来进行延后处理?
分配工作或者延后工作对象
struct work_struct work; //分配一个普通的工作队列
struct delayed_work dwork;//分配一个延时工作
初始化工作或者延时工作
INIT_WORK(&work,work_function);//初始化普通的工作,并且指定工作的延后处理函数work_function
INIT_DELAYED_WORK(&dwork,dwork_fuction);//初始化延时的工作,并且指定延时工作的处理函数dwork_fuction
在顶半部(中断处理函数)中登记普通的工作或者延时工作:schedule_work(&work);//一旦登记,cpu在空闲时立即执行普通工作的处理函数或者schedule_delayed_work(&dwork,5*HZ);//将延时工作的登记放在5秒以后去登记,一旦登记完毕,cpu在空闲时立即执行延后工作的处理函数。
注意:工作队列工作在进程上下文,允许休眠,但是它的优先级要低于tasklet(工作在中断上文中)
案例:利用工作队列实现按键驱动案例:利用工作队列实现2s流水灯操作
#include <linux/init.h>
#include <linux/module.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
//分配延时工作对象
static struct delayed_work dwork;//延时工作
//延时工作处理函数
//work指针指向延时工作dwork的第一个成员,也就是指向延时工作对象
static void dwork_function(struct work_struct *work){
//开关灯,如果是开,你就关,如果是关,你就开,
//不允许使用if else来判断
//重新登记
schedule_delayed_work(&dwork,2*HZ);
}
static int btn_init(void){
//申请gpio资源,配置为输出口,输出为0
//初始化延时工作
INIT_DELAYED_WORK(&dwork,dwork_function);
schedule_delay_work(&dwork,2*HZ);
return 0;
}
static void btn_exit(void){
cancel_delayed_work(&dwork);//取消延时工作
}
module_init(btn_init);
module_exit(btn_exit);
MODULE_LICENSE("GPL");
4 0 0:00 [events/0]
4号线程(进程)来处理工作队列 0为单核cpu。
**************************************************************************
总结:调用schedule_work或者schedule_delayed_work这两个函数,都会将工作和延时工作交给内核默认的工作队列和内核线程,内核默认的线程叫【events/0】..[event/cpu编号],这种用法的优点是简单易用,程序员不需要关心如何创建工作队列和内核线程,但是缺点是导致内核的线程的负载过程,执行的效率太低!可以考虑创建自己的工作队列和内核线程去处理自己的工作或者延时工作。
linux内核描述工作队列的数据结构:
struct workqueue_strut; //里面存放的登记的工作或者延时工作。
如何创建自己的工作队列和内核线程?
工作者线程
1. 工作者线程本质上是一个普通的内核线程,在默认情况下,每个CPU均有一个类型为“events”的工作者线程,当调用schedule_work时,这个工作者线程会被唤醒去执行工作链表上的所有工作。
2. 以上介绍操作均是采用缺省工作者线程来实现工作队列,其优点是简单易用,缺点是如果缺省工作队列负载太重,执行效率会很低,返回需要我们创建自己的工作者线程和工作队列。
struct workqueue_struct *creat_workqueue(char *name);
函数功能:会给每一个cpu(如果是多核的cpu,会耗费cpu资源)创建自己的工作队列和内核线程,并且将自己的工作队列和内核线程进行绑定,以后自己的内核线程只处理自己工作队列上的工作或者延时工作。
返回值:创建的工作队列的指针
参数name:创建的内核线程的名字,通过ps命令查看creat_singlethread_workqueue:这个函数仅仅创建一个内核或者工作队列,他没有和具体的cpu进行绑定,在使用的时候可以指定到具体的cpu上去!
如何将自己的工作或者延时工作交给自己的内核线程去处理?
queue_work(struct workqueue_struct *queue, struct work_struc *work);
queue_delayed_work(struct workqueue_struct *queue, struct work_struc *work, unsigned long delay);
注意跟schedule_work和schedule_delayed_work区别
销毁自己的工作队列和内核线程:destroy_workqueue(自己的工作队列指针);
案例:优化按键驱动,创建自己的工作队列和内核线程处理按键中断。
*****************************************************************\
3.软中断:软中断对应的延后处理函数运行在中断上下文中,tasklet本身也是基于软中断实现的,它和tasklet之间的区别:1.软中断的延后处理函数可以同时在多个cpu上同时执行!但是tasklet不行,只能在一个cpu上运行。2.软中断的处理函数在设计的时候必须具备可重入性。
函数一:static int g_data;//全局变量
void swap(int *x,int *y){
g_data =*x;
*x = *y;
*y = g_data;
}
函数二:void swap(int *x,int *y){
int data;
data =*x;
*x =*y;
*y =data;
}
如何将一个函数设计为重入函数:1.尽量避免使用全局变量2.如果使用全局变量,一定要进行互斥访问,比如加锁,或关闭中断.3.软中断的处理函数不能以模块的形式实现,必须修改内核源码,静态编译内核。
案例:按键驱动程序的简单实现
下面是基于中断和消息的按键驱动程序,其工作原理是:当应用程序读取键值时,会调用按键驱动程序的read函数,而我们实现的read函数检测完读取长度后没有直接读取键值而是等待按键消息,如果没有按键,程序会进入休眠状态,这样可以节省大量的CPU,而当我们按键时硬件会产生中断,程序自动进入中断处理函数,在中断处理函数中,驱动程序读取键值存入全局变量并激活read函数中等待的消息,应用程序被迅速唤醒并通过read函数读取键值,如此,完成了获取键值的工作。下面是源码,比较简单,也就不多说了。
上面这种方式实现的按键驱动程序有个弊端,如果我们不按键,应用程序将会永远阻塞在这里,幸运的是,linux内核提供了poll/select机制,可以设置超时等待时间,如果在这个时间内读取到键值则正常返回,反之则超时退出。使内核支持poll非常简单,为file_operations的poll成员提供poll处理函数即可。
很多情况下,我们的程序在等待按键期间需要处理其它任务而不是在这里空等,这时,就需要采用异步模式了。所谓异步模式,实际上是采用消息机制(以本 文的按键程序为例),即当驱动程序检测到按键后发送消息给应用程序,应用程序接收到消息后再去读取键值。与前面的两种模式相比,最大的不同在于异步方式是 驱动告诉应用程序来读而不是应用程序主动去读。
到这里,这个驱动程序基本上就算可以了,当然,还有对阻塞和非阻塞的支持,同步与互斥的支持,而阻塞与非阻塞无非是加上个逻辑判断,同步与互斥根应用程序的同步控制也差不多,无非就是信号量或者原子操作。
6. Linux等待队列。使进程在内核空间进行休眠 与进程在用户空间休眠(sleep)不同
工作队列:是中断底半部机制,是实现延后执行的一个手段。(内核线程处理)
等待队列:是让进程在内核空间进行休眠的,但是他们针对处理的对象都是进程!
案例:分析应用程序串口工具操作串口硬件设备的过程。
1.外设的处理速度远远慢于cpu!
2.应用程序在应用空间没有权利访问硬件设备,只有通过系统调用跑到内核空间才有权限访问硬件设备!
3.一个应用程序读取串口硬件设备采用两种方法:轮询和中断。
当串口设备没有接收到数据,应用程序一旦发现,利用内核提供的睡眠机制,应用程序在内核空间进入到休眠状态;一旦串口设备给cpu产生中断信号,中断信号的到来也就代表这数据的到来,这时只需唤醒休眠的应用程序,让应用程序读取串口数据。
8.1内核等待队列。-在Linux驱动程序中,可以使用等待队列来实现进程阻塞。等待队列可以看作保存在进程的容器,在阻塞进程时,将进程放入等待队列中当进程被唤醒时,从等待队列中取出进程。 实际上,信号量等对进程的阻塞在内核中也依赖等待队列来实现。
8.2 进程的状态分类。
运行;TASK_RUNNING 准备就绪:TASK_READY 可中断休眠:TASK_INTERRUOTIBLE. 不可中断的休眠:TASK_UNINTERRUOTIBLE 。
进程要“休眠‘:要休眠的进程会将cpu资源全部从当前进程中撤出来,将cpu资源给被的任务去使用,比如另外一个进程;进程之间的切换:由内核调度器来实现,这个调度去就是用来管理进程的!
jiffies current(指向当前进程的指针) 内核全局变量
8.3linux内核等待队列的实现。
老鹰-》调度器:内核已经实现 鸡妈妈-》等待队列头 小鸡-》休眠的进程
内核描述休眠的进程,内核等待队列头涉及的数据类型:
#include<linux/wait.h>
Struct_wait_queue_head{
spinlock_t lock;
struct list_head task_list;
}; Typedef struct _wait_queue_head wait_queue_head_t;
下面为描述一个等待队列对象等待队列让进程休眠,而不是唤醒。
方法一:1.分配等待队列头 2.初始化等待队列头 3.定义并初始化等待的队列 4.将等待队列添加到等待队列头中 5.设置当前进程的状态 6.进程进行休眠 7.休眠被唤醒 8.设置进程的状态为运行状态,获取唤醒的原因 9.移除等待队列.
具体实现:
1.分配等待队列头wait_queue_head_t wq;
2.初始化等待队列头init_waitqueue_head(&wq);
1 2 为静态方法 DECLARE_WAIT_QUEUE_HEAD(name); 定义并初始化一个队列
3.如果一个进程要访问设备,发现设备不可用,进入休眠,此时分配这个进程的休眠容器
DECLARE_WAITQUEUE(wait,current); //小鸡 进程 wait:表示保存当前进程的容器
current:他是内核的全局变量,在linux内核中,内核用struct task_struct结构体来描述每一个进程,那么当前进程获取cpu资源是,current就指向当前进程(哪个进程获取cpu资源,current就指向这个进程对应的task_strtct结构体对象)
例如打印出当前进程的pid和name
printk("current process name is %d pid is %d\n",
current->comm,current->pid);
或者
wait_queue_t wait;//定义一个等待的进程
init_waitqueue_entry(&wait,current); //初始化一个容器,休眠的进程.
注意:如果有多个休眠的进程,必须为每一个进程分配一个容器,并且current也会分别执行不同的进程!
4.然后将当前进程添加到休眠的队列中去add_wait_queue(&wq,&wait);
注意:仅仅是将当前进程添加到这个队列中去,但进程还处于运行状态!
5.设置当前进程的休眠状态(可中断或者不可中断)
可中断的休眠状态:current->state =TASK_INTERRUPTIBLE;
不可中断的休眠状态:current->state = TASK_UNINTERRUPTIBLE;
6.让进程进入真正的休眠状态schedule(); //启动调度器,并且让cpu资源从当前进程撤下来,给别的进程去使用,至此当前进程运行到这个地方就停止不动!一旦被唤醒,这个函数返回,当前进程接着执行
指定超时时间的休眠:把schedule()换成schedule_timeout(5*HZ);
前者的休眠是永久休眠(没有被驱动主动唤醒或者接收到信号)
后者的休眠是指定了睡眠的时间,例如5秒,如果没有接收到信号,也没有接收到驱动主动唤醒,一旦5秒到期,此函数也会返回,返回0,否则返回非零(驱动主动唤醒或者接收到了信号)!
7.如果进程被设置为可中断的休眠状态,进程被唤醒的方法有两种:1硬件设备可用,产生中断,由中断来唤醒;2进程接收到了信号引起的唤醒;
所以要判断唤醒的原因:判断是否接受到信号引起的唤醒:
if(signal_pending(current)){
printk("当前进程是由于接收到了信号引起的唤醒");
printf("给用户返回-ERESTARTSYS");
}else{
printk("中断引起的唤醒,说明数据可用");
printk("后续继续获取数据");
}
8.不管是硬件中断引起的唤醒,从新设置当前进程的状态为运行状态
9.将当前进程从休眠进程中移除remove_wait_queue(&wq,&wait);
进程的休眠动作完成
参考代码:有一个进程读取按键数据:
wait_queue_head_t wq; //全局变量
驱动入口函数或者open函数:
init_waitqueue_head(&wq);
驱动read函数:
static ssize_t btn_read(..){
wait_queue_t wait; //分配一个当前进程的容器
init_waitqueue_entry(&wait,current); //把当前进程添加到这个容器中
add_wait_queue(&wq,&wait); //将当前进程添加到休眠队列中
current->state =TASK_INTERRUPTIBLE;//设置当前进程的休眠状态
schedule(); //进入真正的休眠,一旦被唤醒,进程接着执行
if(signal_pending(current)){ //判断唤醒的原因
printk("接收到了信号引起的唤醒");
ret =-ERESTARTSYS;
}
else {printk("按键有操作.chanshenyiqihonzgn");}
curent->state =TASK_RUNNING; //设置当前进程的状态为运行
remove_wait_queue(&wq,&wait); //从休眠队列中移出,上报按键数据
return ret;
}
唤醒的方法有两种:
1.接收到信号引起的唤醒 2.驱动主动唤醒休眠的进程,方法如下
wake_up(wait_queue_head_t *queue);
唤醒由queue指向的等待队列数据链中的所有睡眠类型的等待进程
wake_up_interruptible(wait_queue_head_t *queue);
唤醒由queue指向的等待队列数据链中的所有睡眠类型为TASK_INTERRUPTIBLE的等待进程
案列1:实现读进程唤醒写进程,写进程唤醒读进程。
根据以上案例,2在底层驱动中的read函数能够给用户上报一个按键的信息(键值和按键的状态),提示可以把底层驱动的write函数作为终端来使用。
案例3:利用等待队列实现按键驱动(将按键中断的处理函数换成write的处理函数),要求按键上报的信息为键值的状态!例如:KEY_UP:0X50 KEY_DOWN:0X51 按键状态:按下为1,松开为0
分析:read->fops->cdev->中断->休眠->等待队列
案例:实现按键驱动,指定超时,而不是永久休眠!
方法二:
1.分配等待队列头wait_queue_head_t wq;
2.初始化等待队列头init_waitqueue_head(&wq);
3.如果进程进入休眠状态:wait_event(wq,condition);
c为真,立即返回,不休眠 c为假,进程进入不可中断的休眠状态,当前进程被添加到wq所对应的等待队列头中或者wait_event_interruptible(wq,condition);
c为真,立即返回,不休眠 c为假,进程进入可中断的休眠状态,进程被添加到wq所对应的等待队列头中
或者 wait_event_timeout(wq,condition,5*HZ);
c为真,立即返回,不休眠 c为假,进程进入不可中断的休眠状态,进程被添加到wq所对应的等待队列头中,并且超时时间到了,立即返回
wait_event_timeout_interruptible(wq,condition,5*HZ);
c为真,立即返回,不休眠 c为假,进程进入可中断的休眠状态,进程被添加到wq所对应的等待队列头中,
并且超时时间到了,进程也被唤醒。
总结:以上宏涉及的condition其实就是代表是否是驱动主动唤醒,如果驱动主动唤醒,应该让condition设置为真,否则还是为假。
linux内核阻塞和非阻塞:
阻塞:进程的状态要不为忙,要不进行休眠忙等待:原地进程空转 休眠等待:利用等待队列机制
非阻塞:如果应用程序进入到内核,发现数据不可用,不会进行忙等待也不会休眠,而是返回用户空间!
linux系统访问文件默认采用阻塞方式:问:如果应用程序访问设备文件等采用非阻塞如何实现?
答:只需在打开open设备或者文件时,指定一个操作选项即可:O_NONBLOCK,例如:
int fd =open("/dev/myled",O_RDWR|O_NONBLOCK);
问:虽然应用程序在打开设备的时候,指定了O_NONBLOCK,但是操作设备只能在内核空间的驱动中完成,那么驱动如何知道应用程序时采用非阻塞呢?如果一旦驱动发现应用采用非阻塞方式来访问设备,进程发现设备不可用,那么进程立即返回到用户空间,如果发现设备可用,操作设备!
答:每当应用程序调用open打开设备文件,通过软中断,陷入内核空间,找到对应的内核函数sys_open,sys_open创建的struct file结构体对象来描述设备文件打开以后的状态信息,其中file结构体中有一个unsigned long f_flags来保存open打开时,指定的属性(O_RDWR|O_NONBLOCK),底层驱动的接口函数,例如:int led_open(struct innode *inode,struct file *file)
int led_close(struct innode *inode,struct file *file)ssize_t led_close(struct file *file) .....
所以底层驱动只需要通过file指针就能够获取f_flags操作属性,那么底层驱动只需要通过以下代码就可以判断应用程序采用阻塞还是非阻塞:
if(file->f_flags&O_NONBLOCK){
//采用非阻塞
if(如果设备不可用){
//立即返回用户空间
return -EAGIN;
}else{
//如果设备可用,操作设备
}else{
//采用阻塞
//采用等待队列}
总结:以后设备驱动只要阻塞的实现,必须添加非阻塞的实现过程.:给之前的按键驱动添加非阻塞的实现。
7. Linux内核定时器。使用硬件定时器(具有周期性)来实现时间.
9.1时钟中断时钟中断由系统定时硬件以周期性的间隔产生。
定时器硬件的特点:1.工作频率人为的设定。2.定时器硬件会周期性的给CPU产生中断信号。、
能够通过编程指定它的工作输出频率,周期性得出cpu产生一个时钟信号;
linux内核也有对应的时钟中断的处理函数,这个函数被内核周期性的调用;
时钟中断处理函数:1.更新jiffies/jiffies_64 2.更新实际时间3.检查进程的时间片
4.检查是否有到期的软件定时器...
HZ:给硬件定时器使用,ARM,HZ=100表明一秒钟产生100次时钟中断
jiffies:内核全局变量,记录发生了多少次时钟中断,每发生一次时钟中断,
jiffies +1; linux内核一般使用jifies来表示时间
Jiffies:unsigned long型的变量,由于每次当发生时钟中断的时候,内核内部的计数器的值就会增加1,这个值子系统引导的时候被初始化为0,因此使用jiffies用来记录操作系统引导以来的发生时钟中断的次数。
9.2定时器的使用方法:
#inclued <linux/timer.h>
1.分配定时器对象
Struct timer_list{/***/
Unsigned long expires; //期望定时器执行的jffies值
Void (*function)(unsigned long);
Unsigned long data;
}; Struct timer_list timer;
2.初始化定时器对象void init_timer(struct timer_list *timer);
3.指定定时器的超时时间处理函数 Timer.function = function;
4.指定定时器的超时时间为指定时间 Timer.expires = jiffies + msecs_to_jffies(speed) ;
speed为毫秒值 注意:2 3 4 可以用下面代替宏来代替
Struct timer_list TIMER_INITIALIZER(_function, _expires, _data);
5.向内核注册定时器对象并且启动定时器void add_timer(struct timer_list *timer);
6.删除定时器int del_timer(struct timer_list *timer);
7.修改一个已经调度的定时器结构的到期时间。
int mod_timer(struct tiimer_list *timer, unsigned long expires);
1.定时器的管理必须尽可能做到轻量级。 2.大部分的定时器在最多几秒或者几分钟内到期,很少存在长期延时的定时器。3.定时器应该注册在同一cpu上运行。
注意:1.定时器的实现基于软中断,所以定时器的处理函数同样不能进行休眠操作!
2.如果想让定时器的处理函数重复的执行,循环执行,只需在定时器处理函数中重新添加定时器即可!
案例1:利用定时器,每隔2s种打印“hello,tarena” 案例2:利用定时器,每个2s开关灯;
案例3:利用模块参数的标识(不能采用字符设备驱动框架)来动态修改灯的闪烁频率:2000ms,1000ms,
Static int speed =1000; Module_param(speed, int , 0664) ;
利用jiffies和定时器来实现延时;schedule_timeout(5*HZ);进程休眠,指定超时时间为5秒。
echo 500 > /sys/module/mytimer_drv/parameters/speed
注意:加载完驱动模块以后,无需卸载的情况下,动态修改闪烁的频率!提示:权限为0或者非零。
9.3linux内核的相关延时函数分为忙等待和休眠等待:短延时(忙)、毫秒延时、长延时、睡眠延时。
忙等待 :cpu不干别的事,原地空转;
休眠等待:休眠只针对于进程,休眠是让进程休眠,而非cpu休眠,cpu有可能去处理中断,有可能在处理别的进程;
短延时:本质为进行忙等待。
Void ndelay(unsignedlong nsecs);纳秒延时 Void udelay(unsignedlong usecs);微秒延时
void mdelay(unsignedlong msece);毫秒延时
内核中,最好不要使用mdelay函数,因为将会浪费CPU资源。
这三个函数的实现并不依赖与硬件定时器,因为硬件定时器处理的时间最小是10ms。利用BogoMIPS,这个参数,通过cat /proc/cpuinfo 来查看,表明一秒钟执行多少个百万指令集;
注意:如果延时的时间大于10ms,一般不再使用mdelay。
毫秒延时:这些函数会使得调用进程休眠,时间由参数指定。
毫秒级:void msleep(unsignedint millisecs);
Unsigned long msleep_interruptible(unsignedint millisecs)
秒级:void ssleep(unsignedint seconds);
长延时:对于精度不高的延时,可以使用jiffies达到要求。
Unsigned longtime_before(unsigned long spurce, unsigned long target);
Unsigned longtime_after(unsigned long target , unsigned long source);
睡眠延时:睡眠延时是比忙等待更好的方法,睡眠延时在等待的时间到来之前处于睡眠的状态,CPU资源可以被释放供其它进程使用。
signed longschedule_timeout(unsigned long timeout);
-timeout:需要睡眠的jiffies计数值,用HZ来计算。
-返回非0表示超时时间到达返回,0为进入可打断睡眠时返回。
毫秒和jiffies转换
unsigned long timeout =jiffies +msecs_to_jiffies(500);
unsigned long timeout =jiffies =HZ/2;