回顾:
谈谈中断:
1.为什么有中断
轮询
中断
2.中断硬件触发过程
中断控制器
3.中断的处理流程
异常向量表
保存现场
执行服务程序
恢复现场
4.linux中断编程
内核已经实现的内容:
异常向量表
保存现场
恢复现场
驱动开发关注内容:
申请中断硬件资源:中断号
注册中断对应的服务程序:中断处理函数
reqeust_irq/free_irq
5.linux内核对中断处理函数的要求:
中断不属于进程,独立于进程,不参与进程的调度切换;中断不能直接和用户进行数据的交互,需要配合系统调用!
linux内核对于硬件中断无优先级;
linux内核硬件中断优先级高于软件中断,软件中断又高于进程
,所以如果某一个硬件中断的处理函数长时间的占有CPU资源,导致别的硬件中断,软中断,进程就无法获取CPU资源,影响linux系统的并发能力和响应能力。
linux内核要求中断的处理函数执行的速度要快!
linux内核要求中断的处理函数不能做阻塞动作!
6.在某些时候中断处理函数的执行时间可能会很长,为了提供系统的响应能力和并发能力,这时需要考虑使用linux内核提供的顶半部和底半部机制来优化中断的执行过程。
顶半部:其实就是中断处理函数,做一些耗时较少,紧急的事情,其他耗时的内容让底半部来做!这个过程是不可中断的!
当然还要做登记底半部的内容,就是告诉CPU,顶半部执行完毕以后,请你在将来适当的时候去处理底半部的内容。
底半部:做原先中断处理函数中较为耗时,不紧急的内容,这个过程可以被打断!
底半部的实现机制:
1.tasklet
实现基于软中断,并且能够指定优先级!运行中断上下文中,tasklet对应的延时函数不能进行休眠动作!
2.工作队列
运行进程上下文中,所以对应的延时函数能够进行休眠操作。
3.软中断
软中断对应的延时函数能够同时运行在多个CPU上,而tasklet不行;
软中断对应的延时函数必须设计为可重入!
软中断的实现不能以模块的形式动态加载和卸载!
****************************************************************************************
Day 08
硬件定时器:一般硬件定时器集成在CPU的内部,有的可以使用外置的硬件定时器芯片;
特点:
可以人为通过编程来设置硬件定时器的工作频率;
硬件定时器一旦设定好了工作频率,只要上电,那么硬件定时器就会周期性的给CPU输出一个中断信号,称这个中断信号为时钟中断;
linux内核已经实现好了时钟中断对应的服务程序,这个服务程序也称之为时钟中断服务函数;
既然硬件定时器周期性的给CPU产生时钟中断,那么对应的中断服务程序就会被内核周期性的调用;
时钟中断服务函数做如下内容:
1.更新系统的运行时间,更新jiffies_64(jiffies)
2.更新实际时间
3.检查当前进程的时间片是否用完,决定是否需要重新调度新进程
4.检查是否有超时的软件定时器,如果有处理这个超时的软件定时器
5...
概念:
HZ:常数,最终会将这个常数设置为硬件定时器的工作频率,对于ARM平台,HZ=100,表明一秒钟产生100次的时钟中断;
tick:1/HZ,表明产生一次时钟中断的时间间隔为10ms
jiffies:是linux内核的全局变量,在内核任何一个文件中都能访问这个变量(unsignedlong),它用来记录自开机以来发生了多少次时钟中断,没发生一次时钟中断,jiffies加1;一般linux内核用jiffies来表示时间!例如:
unsigned long timeout = jiffies + HZ/2;
jiffies:表示当前系统运行时间;
HZ/2:表示时间间隔为500ms
timeout:表示500ms以后的系统运行时间;
注意:
jiffies是一个32位的变量,如果HZ=100,497天以后就会发生溢出(回绕)问题,一般内核使用jiffies_64(64位的变量)用来记录系统运行时间,而jiffies用来描述时间间隔。
jiffies = jiffies_64 & 0xFFFFFFFF;
回绕问题:
unsigned long timeout = jiffies + HZ/2; //加上HZ/2的timeout有可能发生回绕
//执行一些任务
//检查是否花的时间过长
if (timeout > jiffies) {//此时的jiffies仍然很大,但由于发生回绕timeout很小
//没有超时,很好
} else {
//超时了,发生错误
}
linux内核解决jiffies回绕问题使用:
time_after/time_before
#define time_after(a,b)((long)(b) - (long)(a) < 0))
#define time_before(a,b)((long)(a) - (long)(b) < 0))
切记:硬件定时器处理的最小时间为10ms,那么jiffies它能处理的时间也就是10ms。
***********************************************************************************
linux内核软件定时器
1.linux内核描述定时器使用的数据结构
struct timer_list {
unsigned long expires; //定时器的超时时间,例如如果设置超时时间的间隔为5秒;expires = jiffies + 5*HZ
void (*function)(unsigned long);//定时器的处理函数,当超时时间到期,内核就会执行定时器的处理函数,定时器到期内核会将定时器删除,也就是说定时器的处理函数只执行一次;
unsigned long data; //给定时器处理函数传递的参数,一般传递指针
};
如何使用内核定时器?
1.分配定时器对象
struct timer_list mytimer;
2.初始化定时器对象
init_timer(struct timer *timer);
函数功能:初始化定时器
参数:分配的定时器对象指针
注意:这个函数不会初始化expires,function,data这个三个字段,这三个字段需要程序员自己去指定,例如:
init_timer(&mytimer);
mytimer.expires = jiffies + 5*HZ
mytimer.function = mytimer_funtion;
mytimer.data = (unsigned long)&mydata;
3.向内核添加注册定时器
add_timer(&mytimer); //一旦添加完毕,内核就开始对这个定时器进行倒计时!时钟中断处理函数每隔10ms检查一次定时器是否到期,如果到期,内核执行对应的定时器处理函数,并且将定时器进行删除。
4.删除定时器
del_timer(&mytimer); //定时器到期,内核会帮你删除定时器,如果定时器没到期,可以使用此方法进行删除。
5.修改定时器的超时时间
mod_timer(&mytimer, jiffies + 2*HZ); //设置定时器的超时时间为2秒以后
相当于= del_timer 先将原先定时器删除
+expires = jiffies + 2*HZ 设置新的超时时间
+add_timer 重新添加定时器
注意:千万不能用以上三步骤来实现mod_timer,以上三步骤的执行路径不是原子的,有可能被打断!
6.注意:定时器的实现基于软中断,所以定时器的处理函数同样不能进行休眠操作!
7.如果想让定时器的处理函数重复执行,循环执行,只需在定时器处理函数中重新添加定时器即可!
案例1:利用定时器,每隔2秒钟打印"hello ,tarena"
案例2:利用定时器,每个2秒开关灯;
案例3:利用模块参数的知识(不能采用字符设备驱动框架)来动态修改灯的闪烁频率;2000ms,1000ms,500ms,200ms.
注意:加载完驱动模块以后,无需卸载的情况下,动态修改闪烁的频率!提示:权限为0或者非0
实验步骤:
insmod mytimer_drv.ko speed = 2000 //2秒闪烁
echo 500 >/sys/module/mytimer_drv/parameters/speed //500ms
毫秒和jiffies转换:
unsigned long timeout = jiffies +msecs_to_jiffies(500);
unsigned long timeout = jiffies + HZ/2;
*********************************************************
linux内核延时函数:
"延时":忙等待,休眠等待
忙等待:CPU不干别的事,原地空转;
休眠等待:休眠只针对进程,休眠是让进程休眠,而非CPU休眠,CPU有可能去处理中断,有可能在处理别的进程;
忙等待涉及的函数:
void ndelay(unsigned long nsecs); //纳秒级延时
void udelay(unsigned long usecs);//微秒级延时
void mdelay(unsigned long msecs);//微秒级延时
参数都是延时的时间间隔!
这三个函数的实现并不依赖于硬件定时器,因为硬件定时器处理的时间最小是10ms。利用BogoMIPS,这个参数通过cat /proc/cpuinfo来查看,表明一秒钟执行多少个百万指令集;
注意:如果延时的时间大于10ms,一般不再使用mdelay.
休眠设计的延时函数:
msleep:进程休眠,进程不可打断(休眠期间不处理信号),一旦休眠结束会处理之前的信号
msleep_interruptible:进程休眠,进程休眠期间可被信号中断
ssleep:和msleep一样!
利用jiffies和定时器来实现延时;
schedule_timeout(5*HZ);进程休眠,指定超时时间为5秒
********************************************************************************
linux内核并发和竞态:
1.概念:
并发:多个执行单元同时发生;
注意:执行单元包括硬件中断,软件中断,多进程
竞态:并发的多个执行单元同时访问共享资源,引起的竞争状态
形成竞态三个条件:
一定要有并发情况
一定要有共享资源
硬件资源(小到寄存器的某个bit位)
软件上的全局变量,例如open_cnt
并发的多个执行单元要同时访问共享资源
解决办法:
互斥访问:当多个执行单元对共享资源进行访问时,只能允许一个执行单元对共享资源进行访问,其他执行单元被禁止访问!
临界区:访问共享资源的代码区域,所以互斥访问就是对临界区的互斥访问!例如:
由于linux内核支持进程之间的抢占,当A在执行以上临界区时,一定要进行互斥访问,不让B进程发生抢占情况,保证A的执行路径不被打断!
总结:互斥访问本质上其实就是让临界区的执行路径具有原子性(不能被打断!)
问题:如何做到一个执行单元在访问临界区时,其他执行单元不能打断正在访问临界区的执行单元的路径呢?
强调:执行单元(包括硬件中断和软中断和进程)
2.linux内核产生竞态的情形:
第一种情形是多核(多CPU),多个CPU它们共享系统总线,共享内存,外存,系统IO,导致竞态;
第二种情形是单CPU的进程之间的抢占(必须具备抢占,抢占的原因是进程能够指定优先级),由于linux内核支持进程的抢占,多个进程访问共享资源,并且有抢占,也会导致竞态;(多进程没有抢占,则不会有竞态)
第三种情形是中断(硬件中断,软中断)和进程也会形成竞态(中断的优先级高于进程)
第四种情形是中断和中断。硬件中断的优先级高于软中断,软中断又分(2个)优先级!
3.linux内核解决竞态的方法:
这些方法的本质目的就是让临界区的访问具有原子性!
1.中断屏蔽:屏蔽掉硬件中断和软中断
能够解决的竞态情形:
进程与进程的之间的抢占(由于linux进程的调度,抢占都是基于软中断来实现)
中断和进程
中断和中断
linux内核提供了相关的中断屏蔽的方法:
1.屏蔽中断
unsigned long flags;
local_irq_disable(); //屏蔽中断
local_irq_save(floags);//屏蔽中断并且保存中断状态到flags中
2.使能中断
local_irq_enable(); //使能中断
local_irq_restore(flags); //使能中断,并且从flags中恢复屏蔽中断前保存的中断状态
linux内核中断屏蔽使用方法:
1.临界区之前屏蔽中断
2.执行临界区,中断不能打断,进程的抢占也不能发生
3.使能中断
例如:
staticint open_cnt = 1;
intuart_open(struct inode *inode,
structfile *file)
{
unsigned long flags;
local_irq_save(flags);//屏蔽中断
if (--open_cnt != 0) {
printk(“已被打开!\n”);
open_cnt++;
local_irq_restore(flags);//使能中断
return–EBUSY;
}
local_irq_restore(flags);//使能中断
printk(“打开成功\n”);
return 0;
}
2.原子操作
3.自旋锁
4.信号量