回顾:
1.硬件定时器的特点
能够通过编程指定它的工作输出频率,周期性给CPU产生一个时钟中断信号;
linux内核也有对应的时钟中断的处理函数,这个函数被内核周期性的调用;
时钟中断的处理函数:
1.更新jiffies/jiffies_64
2.更新实际时间
3.检查当前进程的时间片
4.检查是否有到期的软件定时器(如果有,执行软件定时器处理函数,然后删除软件定时器)
...
2.HZ,tick,jiffies
HZ:给硬件定时器使用,ARM,HZ=100表明一秒钟产生100次时钟中断
jiffies:内核全局变量,记录自开机以来,发生了多少次时钟中断,每发生一次时钟中断,jiffies+1;
linux内核一般使用jiffies来表示时间
3.linux内核定时器
struct timer_list:用于描述定时器
定时器的实现是基于软中断,所以定时器的处理函数不能做休眠动作!
4.linux内核的延时函数
纳秒级延时:voidndelay(unsigned long nsecs);
微秒级延时:voidudelay(unsigned long usecs);
毫秒级延时:voidmdelay(unsigned long msecs);
ndelay/udelay/mdelay
这三种延时都是忙等待!
以下为睡眠延时:
void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsignedint millisecs);
void ssleep(unsigned int seconds);
/msleep/ssleep
jiffies/内核定时器/schedule_timeout
5.linux内核并发和竞态
5.1概念:
并发
竞态
互斥访问
临界区
共享资源
5.2竞态产生的情形:
1.多核
2.进程和进程的抢占
3.中断和进程
4.中断和中断
明确:进程的调度,抢占都是基于软中断来实现!
5.3linux内核解决竞态的方法:
1.中断屏蔽
2.原子操作
3.自旋锁
4.信号量
总结:让一个执行单元在访问临界区的执行路径具有原子性(不可打断)!
************************************************************************************
Day09
中断屏蔽:
能够解决如下竞态问题:
1.进程和进程之间的抢占
2.中断和进程
3.中断和中断
使用方法:
1.访问临界区之前屏蔽中断
unsigned long flags;
local_irq_disable();
或者
local_irq_save(flags);
2.访问临界区
3.访问完临界区以后,一定要使能中断
local_irq_enable();
或者
local_irq_restore(flags);
4.注意:屏蔽中断和使能中断必须成对使用!
关键注意点:在驱动编程时,如果考虑到竞态问题,并且采用中断屏蔽来实现互斥访问,一定要明确屏蔽中断之后,在执行临界区的代码时,速度一定要快,更不能做休眠动作!
因为中断对于linux系统的运行至关重要,硬件中断,进程的调度,抢占,定时器等都是依赖于中断来实现!
**********************************************************
原子操作:
笔试题:请实现将一个数的某个bit置1或者清0;
第一个同学:
int data = 0x12345;
data |= (1 << 5);
data &= ~(1 << 5);
第二个同学:
void set_bit(int nbit, int *data)
{
...
}
第三位同学:
void set_bit(int nbit, void *data)
{
if(n<8)
*(char*)data |=(1<<n);
elseif(n<16)
*(short*)data|=(1<<n);
elseif(n<32)
*(int*)data |=(1<<n);
}
第四位同学:
#define SET_BIT(nr, data) ...
#defineset_bit(n,data) (data |=(1<<n))
还是利用宏函数的方法好!
基于linux系统的参考答案(GNU C):
inline void set_bit(int nr, void *data)
{
...
}
linux内核原子操作:
原子操作能够解决所有竞态的问题;
原子操作分为:
位原子操作:
如果以后驱动中对共享资源进行位操作,并且为了避免竞态问题,一定要使用内核提供的位原子操作的方法,保证位操作的过程是原子的,不能自己去实现位操作,例如:
static int data; //全局变量,共享资源
//临界区
data |= (1 << 5); //这个代码不是原子的,有可能被别的任务打断!
如果不考虑多核引起的竞态,还有一种通过中断屏蔽来解决以上代码的竞态问题:
unsigned long flags;
local_irq_save(flags);//禁止中断
data |= (1 << 5); //访问临界区
local_irq_restore(flags);//使能中断
注意以上代码无法解决多核引起的竞态!
内核提供的位原子操作的方法:
set_bit/clear_bit/change_bit/test_bit/组合函数
对于data进行位操作的正确做法:
static int data;
set_bit(5, &data); //这个代码是原子的,不能被别的任务打断
注意:以上函数在多核情况下,会使用两条ARM的原子指令:
ldrex,strex,这个两条保证在CPU那一级别能够避免竞态,以上函数都是采用C的内嵌汇编来实现,如果用C语言来实现,编译器肯定是用ldr,str,但这个两条指令不能避免竞态!
案例:利用位原子操作将0x5555->0xaaaa,不允许使用change_bit函数!
整型原子操作:
如果以后驱动程序中,涉及的共享资源是整型数,就是原型要定义为char,short,int,long型的数据,并且它们是共享资源,为了避免竞态,可以考虑使用内核提供的整型原子操作机制来避免竞态问题。
说白了就是将原先的char,short,int,long类型换成atomic_t数据类型即可,然后配合内核提供的整型原子操作的函数进行对整型变量进行数学运算!
整型原子变量的数据类型:
atomic_t
如何使用:
分配整型原子变量
atomic_t v;
进行对整型变量的操作:
atomic_set/atomic_read/atomic_add/atomic_sub/atmoioc_inc/atomic_dec/atomic_inc_and_test...
对整型变量的操作一定要使用以上的函数进行,保证具有原子性。不能使用如下代码:
static int data; //全局变量,共享资源
//临界区
data++; //不是原子的!有可能被打断
解决的方法:
如果不考虑多核:
unsigned long flags;
local_irq_save(flags);
data++;
local_irq_restore(flags);
如果考虑多核:
atomic_t data;
atomic_inc(&data);
注意:以上整型原子操作的函数,如果在多核情况下,它们的实现都是C的内嵌汇编来实现的,都调用了ldrex,strex来避免竞态
案例:实现LED灯驱动,要求这个设备只能被一个应用程序打开
分析:
明确:app会调用open打开设备,close关闭设备
驱动:一定要提供对应的底层open,close的实现,注意不能省略底层这两个函数,因为需要在底层的open,close函数中做一些用户需求的代码(设备只能被一个应用程序打开)
方案:
static int open_cnt; //可以采用中断屏蔽
方案:
只有open_cnt为1时,才能正确打开设备!打开一个设备之后open_cnt变为0,不能再打开设备了!
static atomic_t open_cnt=1; //利用整型原子操作
static int led_open(struct inode *inode,
struct file *file)
{
if (!atomic_dec_and_test(&open_cnt)) {//atomic_dec_and_test(atomic_t*v)
//执行减操作,如果结果为0,返回true,否则返回false
printk("设备已被打开!\n");
atomic_inc(&open_cnt);
return -EBUSY;//给用户返回打开设备失败,设备忙
}
printk("进程打开设备成功!\n");
return 0;
}
static int led_close(struct inode *inode,
struct file *file)
{
atomic_inc(&open_cnt);
printk("进程关闭设备!\n");
return 0;
}
实验步骤:
1.insmod led_drv.ko
2.cat /proc/devices //查看申请的主设备号
3.cat /sys/class/myleds/myleds/uevent //查看创建设备文件的原材料
4.ls /dev/myled //查看设备文件
5. ./led_test & //启动A进程,让其后台运行,A进程进入休眠
6.ps //查看A进程的PID
7.top //查看A进程的状态和CPU的利用率,内存使用率
8../led_test //启动B进程
9.kill A进程的PID //杀死A进程
问题:
~ # rmmod led_drv
rmmod: can't unload 'led_drv': Resourcetemporarily unavailable
原因是驱动程序正在被一个进程占有!
******************************************************************************************
自旋锁:等于“自旋” + ”锁“
自旋锁特点:
1.自旋锁一般要附加在共享资源上;类似光有自行车锁,没有自行车是没有意义!
2.自旋锁的“自旋”的意思是想获取自旋锁的执行单元,在没有获取自旋锁的情况下,原地打转,忙等待着获取自旋锁;
3.一旦一个执行单元获取了自旋锁,在执行临界区时,不要进行休眠操作。“不够意思”。
4.自旋锁也是让临界区的访问具有原子性!
linux内核如何描述一个自旋锁:
数据类型:spinlock_t
如何使用自旋锁来对临界区进行互斥访问:
static int open_cnt; //全局变量,共享资源
1.分配自旋锁
spinlock_t lock
2.初始化自旋锁
spin_lock_init(&lock);
3.访问临界区之前获取自旋锁,进行锁定
spin_lock(&lock); //如果执行单元获取自旋锁,函数立即返回,如果执行单元没有获取锁,执行单元不会返回,而是原地打转!处于忙等待,直到持有自旋锁的执行单元释放自旋锁。
或者:
spin_trylock(&lock);//如果执行单元获取自旋锁,函数返回true,如果没有获取自旋锁,返回false,不会原地打转!
4.执行临界区的代码
if(--opencnt != 0) {
....
}
这个过程其他CPU或者本CPU的抢占进程无法来执行临界区,但是还会被中断所打断!如果考虑中断的因素,要使用衍生自旋锁
5.释放自旋锁
spin_unlock(&lock);//获取锁的执行单元释放锁,然后等待获取锁的执行单元停止原地打转而是获取自旋锁,然后开始对临界区的访问。
注意:以上自旋锁的操作只能解决多CPU和本CPU的进程抢占引起的竞态,但是无法处理中断引起的竞态,如果考虑到中断,必须采用衍生自旋锁!
衍生自旋锁本质上其实就是在普通的自旋锁的基础上进行屏蔽中断和使能中断的动作。
衍生自旋锁的使用:
static int open_cnt; //全局变量,共享资源
1.分配自旋锁
spinlock_t lock
2.初始化自旋锁
spin_lock_init(&lock);
3.访问临界区之前获取自旋锁,进行锁定
spin_lock_irq(&lock);//屏蔽中断,获取自旋锁
或者
spin_lock_irqsave(&lock,flags);//屏蔽中断,保存中断状态,获取自旋锁
4.访问临界区
if(--opencnt != 0) {
....
}
5.释放自旋锁
spin_unlock_irq(&lock);//释放自旋锁,使能中断
或者
spin_unlock_irqrestore(&lock,flags);//释放自旋锁,使能中断,保存中断状态
注意:衍生自旋锁能够解决所有的竞态问题!
自旋锁使用的注意事项:
1.一旦获取自旋锁,临界区的执行速度要快,更不能做休眠动作
案例:利用自旋锁,来实现一个设备只能被一个应用程序打开
*********************************************************
linux系统进程的状态:三个状态
1.进程的运行状态,linux系统描述运行中的进程通过TASK_RUNNING来表示!
2.进程的准备就绪状态,linux系统描述进程准备就绪用
TASK_READY来表示
3.进程的休眠状态,进程的休眠状态又分
3.1不可中断的休眠状态,linux系统描述用TASK_UNINTERRUPTIBLE来表示,如果进程的休眠状态为不可中断的休眠状态,在休眠期间,如果接收到了信号,不会立即处理信号,但是被唤醒以后,会判断之前是否接收到信号,如果有,那么处理信号!
3.2可中断的休眠状态,linux系统描述用TASK_INTERRUPTIBLE,在休眠期间,如果接收到了信号,会被信号唤醒,并且立即处理信号!
信号量:
由于自旋锁在访问临界区的时候,要求临界区不能做休眠动作,但是在某些场合,可能需要在临界区做休眠动作,又要考虑竞态问题,此时可以使用信号量来保护临界区。
信号量的特点:
1.又叫“睡眠锁”;
2.如果一个执行单元想要获取信号量,如果信号量已经被别的执行单元给持有,那么这个执行单元将进入休眠状态;直到持有信号量的执行单元释放信号量为止。
3.已经获取信号量的执行单元在执行临界区的代码时,也可以进行休眠操作!
4.明确信号量能让进程休眠!
linux内核描述信号量的数据类型:
struct semaphore
如何使用信号量:
1.分配信号量
struct semaphore sema
2.初始化信号量为互斥信号量
sema_init(&sema, 1);
3.在访问临界区之前获取信号量,对临界区进行锁定
down(&sema); //获取信号量,如果信号量已经被别的任务给持有,那么进程将进入不可中断的休眠状态;
或者:
down_interruptible(&sema);//获取信号量,如果信号量已经被别的任务给持有,那么进程将进入可中断的休眠状态;一般在使用的时候一定要对这个函数的返回值进行判断,如果函数返回0,表明进程正常获取信号量,然后访问临界区;如果函数返回非0,表明进程是由于接收到了信号引起的唤醒;
if(down_interruptible(&sema)) {
printk("进程被唤醒的原因是接收到了信号");
return -EINTR;
}else {
printk("正常获取信号量引起的唤醒");
printk("进程可以访问临界区");
}
注意:以上两个获取信号量的方法不能用于中断上下文中
或者:
down_trylock(&sema);//获取信号量,如果没有获取信号量,返回false,如果获取信号量,返回true.所以对返回值也要做判断
if(down_trylock(&sema)) {
printk("无法获取信号量");
return-EBUSY;
}else {
printk("获取信号量");
printk("访问临界区");
}
4.访问临界区
5.释放信号量
up(&sema); //一方面会释放信号量,另一方面还要唤醒之前休眠的进程
案例:采用信号量,实现一个设备只能被一个应用程序所打开;
对于这个案例,无需给定一个共享资源,如果A进程打开设备获取信号量,B进程也尝试打开设备,驱动直接让B进程进入休眠状态,直到A进程关闭设备释放信号量为止!
实验步骤:
down_interrruptible:
insmod led_drv.ko
./led_test & //启动A进程
./led_test & //启动B进程
ps //查看A,B进程的PID
top //查看A,B进程的状态
kill A进程
kill B进程
./led_test & //启动A进程
./led_test & //启动B进程
ps //查看A,B进程的PID
kill B进程
kill A进程
down:
./led_test & //启动A进程
./led_test & //启动B进程
ps //查看A,B进程的PID
top //查看A,B进程的状态
kill A进程
kill B进程
./led_test & //启动A进程
./led_test & //启动B进程
ps //查看A,B进程的PID
kill B进程
kill B进程
ps
top
kill A进程