1)Linux操作系统是一个多任务操作系统,2.6以上版本都支持任务抢占。
2)现在的CPU大部分都是多核心CPU,多核心CPU同时并发执行程序。
3)不管是多核心CPU还是单核心CPU在任务执行过程都可能产生中断。
多进程并发:
进程之间会存在多进程同时并发访问同一共享资源,就会产生竞争。
任务抢占:
当进程在访问某个共享资源的时候发生任务抢占,随后进入了高优先级的进程,如果该进程也访问了同一共享资源,那么也会造成进程与进程之间的竞争。
多核心CPU:
多处理器系统上的进程与进程之间是严格意义上的并发,每个处理器都可以独自调度运行一个进程,在同一时刻有多个进程在同时运行。那么就会发生并发运行产生竞争状态。
中断处理:
当进程在访问某个共享资源的时候发生了中断,随后进入中断处理程序,如果在中断处理程序中,也访问了该共享资源。虽然不是严格意义上的并发,但是也会造成了对该资源的竞争。
因此,采用同步机制的目的就是避免多个进程并发访问同一共享资源。
注:共享资源就是内存中的数据,保护共享资源就是使得内存中的数据同一时间只能被一个任务所访问。
1.原子操作
2.信号量
3.互斥锁
4.自旋锁
如果多个进程打开同一个设备文件,那么就会发生冲突。解决方法思路就是保证让同一时间只能由一个进程所访问一个设备文件!
以LED灯驱动为例,其解决方法:
static int open_flag = 0;
int ledDev_open(struct inode *inode, struct file *file)
{
//判断是否已经被打开过,如果已经被别的进程打开,则返回
if(open_flag){
return -EBUSY;
}
open_flag = 1;
printk("*****%s*****\n",__FUNCTION__);
led_con = ioremap(0x110002E0,4);
led_dat = ioremap(0x110002E4,4);
*led_con &= 0xffff0000;
*led_con |= 0xffff1111;
*led_dat |= 0xffffffff;
return 0;
}
int ledDev_close(struct inode *inode, struct file *file)
{
printk("*****%s*****\n",__FUNCTION__);
*led_dat |= 0xffffffff;
iounmap(led_con);
iounmap(led_dat);
open_flag = 0;//进程关闭文件时候把标志清0,否则下一次就永远打不开了
return 0;
}
以上是使用一个整形变量来实现保护共享资源。
但是,该解决方法不安全,问题根源是这部分代码不能一次性执行完成。
所以,解决安全问题就可以想办法让这部分代码一次性执行完成。
所谓原子操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断。原子操作需要硬件的支持,因此是架构相关的,ARM架构的API和原子类型的定义都定义在内核源码中的arch/arm/include/asm/atomic.h文件中。
Linux内核定义了叫做atomic_t的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量,此结构体定义在include/linux/types.h文件中。
typedef struct {
int counter;
}atomic_t;
1.ATOMIC_INIT(i)
宏功能:动态初始化一个原子变量,值为i
宏原型:#define ATOMIC_INIT(i) {(i)}
示例:atomic_t atom = ATOMIC_INIT(1);
2.atomic_read(v)
宏功能:读取原子变量v中的值
宏原型: #define atomic_read(v) (*(volatile int *)&(v)->cointer)
3.atomic_set(v, i)
宏功能:设置原子变量v的值为i
宏原型: #define atomic_set(v,i) (((v)->counter) = (i))
4.void atomic_add(int i, atomic_t *v)
功能:把原子变量v的值加上i
5.void atomic_sub(int i, atomic_t *v)
功能:把原子变量v的值减去i
6.atomic_sub_and_test(i,v)
宏功能:把原子变量v的值减去i,判断相减后的原子变量值是否为0,如果为0返回真,否则为假
宏原型: #define atomic_sub_and_test(i,v) (atomic_sub_return(i,v) == 0)
7.atomic_inc(v)
宏功能:把原子变量v加上1
宏原型: #define atomic_inc(v) atomic_add(1,v);
8.atomic_dec(v)
宏功能:把原子变量v减去1
宏原型: #define atomic_dev(v) atomic_sub(1,v)
9.atomic_dec_and_test(v)
宏功能:把原子变量v的值减去1,判断相减后的原子变量值是否为0,如果为0返回真
宏原型: #define atomic_dec_and_test(v) (atomic_sub_return(1,v) == 0)
注:参数为原子变量的地址
10.atomic_inc_and_test(v)
宏功能:把原子变量v的值加上1,判断相加后的原子变量值是否为0,如果为0返回真
宏原型: #define atomic_inc_and_test(v) (atomic_add_return(1,v) == 0)
11.atomic_add_negative(i, v)
宏功能:把原子变量v的值加上i,判断相加后的原子变量值是否为负数,如果为负数返回真
宏原型: #define atomic_add_negative(i,v) (tomic_add_retuen(1,v) < 0)
12.int atomic_add_return(int i, atomic_t *v)
作用:把原子变量v的值加i,返回相加后原子变量的结果
13.int atomic_sub_return(int i, atomic_t *v)
作用:把原子变量v的值减i,返回相减后原子变量的结果
14.atomic_inc_return(atomic_t *v)
宏功能:把原子变量v的值加1后,返回结果
宏原型: #define atomic_inc_return(v) (atomic_add_return(1,v))
15.atomic_dec_return(atomic_t *v)
宏功能:把原子变量v的值减1后,返回结果
宏原型: #define atomic_dec_return(v) (atomic_sub_return(1,v))
//定义原子变量,初始化值为1
atomic_t atomic_v = ATOMIC_INIT(1);
int ledDev_open(struct inode *inode, struct file *file)
{
//判断是否已经被打开过,如果已经被别的进程打开,则返回
if(!atomic_dec_and_test(&atomic_v )){
return -EBUSY;
}
printk("*****%s*****\n",__FUNCTION__);
led_con = ioremap(0x110002E0,4);
led_dat = ioremap(0x110002E4,4);
*led_con &= 0xffff0000;
*led_con |= 0xffff1111;
*led_dat |= 0xffffffff;
return 0;
}
int ledDev_close(struct inode *inode, struct file *file)
{
printk("*****%s*****\n",__FUNCTION__);
*led_dat |= 0xffffffff;
iounmap(led_con);
iounmap(led_dat);
atomic_set(&atomic_v,1);//不管原来值是什么,直接设置1
return 0;
}
以上就是使用原子操作实例保护共享资源。
如果有三个进程,通过串口设备在串口控制台终端输出
进程A:printf(“123456”);
进程B:printf(“abcdefg”);
进程C:printf(“hello”);
理论分析应该会出现混合输出数据的情况,但实际上我们也没有看到过混合输出的情况, 这所以没有出现,是因为驱动中对串口的写操作进行保护,让每个进程的每一次读写操作都是完整。解决方法核心思路:进程在对设备执行写操作时,发现设备被占用,应该去等待休眠!
解决方法:
static int write_flag = 1;//1.定义一个写标志
ssize_t ledDev_write(struct file *file, const char __user *buff, size_t count, loff_t *loff)
{
int i;
char led_buff[4] = {0};//存放接收来自应用层的数据
printk("*****%s*****\n",__FUNCTION__);
//2.判断写标志是否为1,若不为1则休眠
while(write_flag != 1){
msleep(1);
}
write_flag--;//3.把写标志-1,为了让下一个进程无法运行到这
if(copy_from_user(led_buff, buff, count)!=0){
printk("copy_from_user error\n");
return -1;
}
printk("led_buff = %s\n",led_buff);
for(i=0;i<4;i++){
if(led_buff[i] == '1'){
*led_dat &= ~(1<<i);
}else{
*led_dat |= 1<<i;
}
}
write_flag ++;//4.恢复成原来的1
return 0;
}
以上是使用一个整形变量实现保护共享资源。
同样的,以上保护代码存在安全隐患
我们之前已经接触过很多次信号量的概念了,信号量是同步的一种方式。Linux内核也提供了信号量机制,信号量常常用于控制对共享资源的访问。信号量可以使进程进入休眠状态。
进程要想访问共享资源,首先必须得到信号量,获取信号量的操作将把信号量的值减1,若当前信号量的值为0,表明无法获得信号量,该任务挂起直到信号量可用;若当前信号量的值为正数,表示可以获得信号量,因而可以立刻访问被该信号量保护的共享资源。当任务访问被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现。
信号量在创建时需要设置一个初始值,表示同时可以有几个任务可以访问该信号量保护的共享资源,即信号量可以运行多个进程同时访问共享资源。如果初始值为1就类似互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。
所在路径:include/linux/semaphore.h
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
lock:这个是内核实现信号量内部机制使用到的,驱动开发者不需要接触到。
count:信号值,值为正数表示可以获得资源,0表示不能获得资源。一般情况也不需要直接操作这个值,内核会提供相关的API函数来使用
wait_list:和lock一样,不需要用户接触到。它的作用是把所有等待使用相同的信号的进程链接在一起。
所在路径:include/linux/semaphore.h
1.静态初始化:DEFINE_SEMAPHORE(name)
宏功能:定义一个变量名为name的信号量,并且对它进行初始化,信号值为1。
宏原型:#define DEFINE_SEMAPHORE(name) \
struct semaphore name = __SEMAPHORE_INITIALIZER(name,1)
2.动态初始化:void sema_init(struct semaphore *sem, int val);
功能:动态初始化一个信号量结构,信号值为val
注意:使用该函数之前要先为信号量开辟空间
参数:
sem:要初始化的信号量结构变量地址
val:要设定的信号值,一般情况下都设置为1。
3.void down(struct semaphore *sem);
功能:
阻塞地申请一个信号量。如果没有获得信号量,会一直睡眠(不占用CPU),直到得到信号量为止。如果成功获得信号量,信号量值减1。
参数:
sem:要申请的信号量结构变量地址
4.int down_interruptible(struct semaphore *sem);
功能:
阻塞地申请一个信号量。如果没有获得信号量,会一直睡眠(不占用CPU),直到得到信号量为止或者被信号中止(比如Ctrl + C这个信号可以把它唤醒)。如果成功获得信号量,信号量值减1。
参数:
sem:要申请的信号量结构变量地址
返回值:
0:成功获得信号量
-EINTR:被信号中断唤醒的
5.int down_trylock(struct semaphore *sem);
功能:
尝试获得信号量,不管是否有信号量都直接返回,有就拿,没有也没有关系,都不等待。如果成功获得信号量,信号量值减1。
参数:
sem:要申请的信号量结构变量地址
返回值:
0:成功获得信号量
1:没有获得信号量
6.int down_timeout(struct semaphore *sem, long jiffies);
功能:和down相同,区别在于返回的条件多了一个,就是超时没有得到信号返回。
参数:
sem:要申请的信号量结构变量地址
jiffies:超时时间,使用时钟节拍为单位
返回值:
0:成功获得信号量返回
-ETIME:超时返回
7.void up(struct semaphore *sem);
功能:释放一个信号量,实际上把信号量值进行加1操作。每调用一次信号值+1
参数:要释放的信号量结构变量地址
使用步骤:
实现write函数中的数据缓冲区同一时刻只能被一个进程访问。
1.添加头文件:#include
2.定义信号量:struct semaphore sem_wr;
3.在加载函数中初始化信号量:
sema_init(&sem_wr,1);
4.在copy_from_user前申请信号量:
down(&sem_wr);//其他进程在执行这个函数时候得不到信号会在这里阻塞
5.在copy_from_user后释放信号量:
up(&sem_wr);
static struct semaphore write_sem;//1.定义一个信号量
ssize_t ledDev_write(struct file *file, const char __user *buff, size_t count, loff_t *loff)
{
int i;
char led_buff[4] = {0};//存放接收来自应用层的数据
printk("*****%s*****\n",__FUNCTION__);
down(&write_sem);//3.申请信号量
if(copy_from_user(led_buff, buff, count)!=0){
printk("copy_from_user error\n");
return -1;
}
printk("led_buff = %s\n",led_buff);
for(i=0;i<4;i++){
if(led_buff[i] == '1'){
*led_dat &= ~(1<<i);
}else{
*led_dat |= 1<<i;
}
}
up(&write_sem);//4.释放信号量
return 0;
}
以上是使用信号量实现保护共享资源。
也可以通过信号量来实现某一个设备同一时刻只能被一个进程打开。
1.静态定义一个信号量变量:
DEFINE_SEMAPHORE(sem_open);
2.在open接口函数中申请信号量:
down_trylock( (&sem_open);
3.在release接口函数中释放信号量
up(&sem_open);//释放信号量,否则下一次就不能打开了。
若使用信号量进行驱动保护,则read,wirte接口函数也就没必要进行保护了
互斥锁主要用于实现内核中的互斥访问功能。互斥访问表示一次只有一个进程(线程)可以访问共享资源,我们可以将信号量的值设置为1就可以使用信号量进行互斥访问了。虽然通过信号量可以实现互斥,但是Linux提供了一个比信号量更专业的机制来进行互斥,就是互斥锁。
在编写Linux驱动的时候遇到需要互斥访问的地方建议在使用中尽量使用互斥锁。方便简单!
所在路径:include/linux/mutex.h
struct mutex {
/* 1: unlocked, 0: locked, negative: locked, possible waiters */
atomic_t count;
spinlock_t wait_lock;
struct list_head wait_list;
#if defined(CONFIG_DEBUG_MUTEXES) || defined(CONFIG_SMP)
struct task_struct *owner;
#endif
#ifdef CONFIG_DEBUG_MUTEXES
const char *name;
void *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};
内核使用这个结构体来描述一个互斥锁,使用互斥锁步骤如下:
1)定义一个struct mutex结构变量
2)使用相应的API去初始化上一步定义的结构体变量
3)在访问共享资源的前面,使用相应的API函数申请互斥锁
4)当共享资源访问完毕,使用相应的API函数释放锁
1.DEFINE_MUTEX(mutexname)
宏功能:定义一个名字为mutexname的互斥锁变量,并且静态初始化该变量。
宏原型:#define DEFINE_MUTEX(mutexname) \
struct metux mutexname = __MUTEX_INITIALIZER(mutexname)
2.mutex_init(mutex)
宏功能:动态初始化一个互斥锁变量
宏原型:#define mutex_init(mutex) \
do { \
static struct lock_class_key __key; \
__mutex_init((mutex), #mutex, &__key); \
}while(0)
参数:mutex是互斥锁变量的地址
注意:使用该函数之前先开辟互斥锁空间
3.int mutex_is_locked(struct mutex *lock)
功能:判断一个互斥锁是否已经被锁定
参数:lock要申请的互斥锁变量地址。
返回值:1已经被锁定,0没有被锁定。
4.void mutex_lock(struct mutex *lock)
功能:
请求获得一个锁,如果得不到则睡眠。得到了就往下运行,和前面信号量down函数相似。睡眠期间不能被信号唤醒
参数:lock要申请的互斥锁变量地址。
5.int mutex_lock_interruptible(struct mutex *lock);
功能:
请求获得一个锁,如果得不到则睡眠。得到了就往下运行,和前面信号量down_interruptible函数相似。睡眠期间可以被信号唤醒。
参数:lock要申请的互斥锁变量地址。
6.int mutex_trylock(struct mutex *lock);
功能:请求获得一个锁,不管是否得到,都马上返回。功能和前面信号量的down_trylock函数相似。
参数:lock要申请的互斥锁变量地址。
返回值:1:成功得到锁,0:没有得到锁
7.void mutex_unlock(struct mutex *lock);
功能:解锁一个已经获得互斥锁
参数:lock要申请的互斥锁变量地址。
static struct mutex write_mutex;//定义互斥锁
ssize_t ledDev_write(struct file *file, const char __user *buff, size_t count, loff_t *loff)
{
int i;
char led_buff[4] = {0};//存放接收来自应用层的数据
printk("*****%s*****\n",__FUNCTION__);
mutex_lock(&write_mutex); //申请互斥锁
if(copy_from_user(led_buff, buff, count)!=0){
printk("copy_from_user error\n");
return -1;
}
printk("led_buff = %s\n",led_buff);
for(i=0;i<4;i++){
if(led_buff[i] == '1'){
*led_dat &= ~(1<<i);
}else{
*led_dat |= 1<<i;
}
}
mutex_unlock(&write_mutex); //释放互斥锁
return 0;
}
以上是使用互斥锁实现保护共享资源。
也可以通过互斥锁实现进程独占设备驱动。
自旋锁与互斥锁的区别:
相同点:
功能和信号量,互斥锁一样,可以用来对共享资源,临界代码的保护。
不同点:
互斥锁:在申请锁的时候,如锁已经被占用,会睡眠(放弃CPU,存在进程调度)。
自旋锁:在申请锁的时候,如锁已经被占用,不会睡眠,而是始终在循环检测锁是否已经被释放,如果释放了,则马上就获得锁,进入临界代码。
互斥锁适用场合:允许休眠的代码,或者临界代码执行时间比较长的情况。
自旋锁适用场合:不允许休眠的代码,或者临界代码执行时间很短。
注意:在中断服务程序中要进行共享资源,临界代码保护时只能选择自旋锁或非阻塞版本的互斥锁或非阻塞版本信号量。
自旋锁概念:
自旋锁最初就是为了SMP系统设计的,实现在多处理器情况下临界区保护。
自旋锁是为了防止多处理器并发而引入的一种锁,它可应用于中断处理等部分。对于单处理器来说,防止中断处理中的并发可简单采用关闭关闭中断的方式,不需要自旋锁。自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。
其工作原理为自旋锁只能被一个内核任务持有,如果一个内核任务试图请求一个已被其他内核任务持有的自旋锁,那么这个任务就会一直进行忙循环检测,等待锁重新可用。要是锁未被占用,请求它的内核任务便能立刻得到它并且继续执行。
所在路径:include/linux/spinlock_types.h
typedef struct spinlock {
union {
struct raw_spinlock rlock;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
struct {
u8 __padding[LOCK_PADSIZE];
struct lockdep_map dep_map;
};
#endif
};
} spinlock_t;
所在路径:include/linux/spinlock.h
1. spin_lock_init(_lock)
宏功能:用于初始化自旋锁_lock,使用时候传递的是指针,自旋锁在真正使用前必须先初始化。
宏原型:#define spin_lock_init(_lock) \
do { \
spinlock_check(_lock); \
raw_spin_lock_init(&(_lock)->rlock);\
}while(0)
参数:_lock:自旋锁变量的地址
2.要保护的共享资源不会在任何中断程序使用到,共享资源在普通的函数接口使用
void spin_lock(spinlock_t *lock);
void spin_unlock(spinlock_t *lock);
3.要保护的共享资源会在软中断程序使用到:如前面学习的tasklet,timer_list和绑定函数都属于软件中断形式,也就是通常所说的中断下半部最常用使用的实现方式。
void spin_lock_bh(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock);//立即获得锁返回真,不能立即获得锁返回假
void spin_unlock_bh(spinlock_t *lock)
4.要保护的共享资源会在硬件中断服务程序中使用到,并且不需要保存当前的CPU状态标志。
上锁的同时关闭本地中断
void spin_lock_irq(spinlock_t *lock);
int spin_trylock_irq(spinlock_t *lock); //立即获得锁返回真,不能立即获得锁返回假
void spin_unlock_irq(spinlock_t *lock);
5.要保护的共享资源会在硬件中断服务程序中使用到,并且需要保存当前的CPU状态标志。
上锁的同时关闭本地中断,同时把标志寄存器的值保存到变量flags中。
spin_lock_irqsave(lock,flags);
spin_trylock_irqsave(lock,flags); //立即获得锁返回真,不能立即获得锁返回假
void spin_unlock_irqrestore(spinlock_t *lock,unsigned long flags);
示例:
1.有一个全局变量flag,这个变量在驱动程序的xxx_write接口函数和中断服务函数都有对它访问,则必须使用spin_lock_irq或spin_lock_irqsave进行上锁,访问完毕后,使用spin_unlock_irq或spin_unlock_irqrestore解锁。
2.有一个全局变量flag,这个变量仅在普通代码中进行访问(如read,write接口),则访问这个变量时候需要使用spin_lock来实现上锁,访问完毕后使用spin_unlock来解锁。
自旋锁编程步骤
1.定义自旋锁变量
2.初始化自旋锁变量
3.根据自己要保护的共享资源出现位置,选择合适上锁方法
4.在访问共享资源后,使用和上锁配套的解锁函数进行解锁
可以利用自旋锁保护write接口函数中的数据缓冲区,只需要在写操作前后调用自旋锁即可。在这里我们介绍使用自旋锁实现设备独占的方法:
static spinlock_t open_lock;//定义一个自旋锁
static int open_flag = 0;//定义一个打开标志
int ledDev_open(struct inode *inode, struct file *file)
{
spin_lock(&open_lock);//上锁
if(open_flag){
spin_unlock(&open_lock);//解锁,注意这条忘记写会导致死锁
return -EBUSY;
}
open_flag++;
spin_unlock(&open_lock);//解锁
printk("*****%s*****\n",__FUNCTION__);
led_con = ioremap(0x110002E0,4);
led_dat = ioremap(0x110002E4,4);
*led_con &= 0xffff0000;
*led_con |= 0xffff1111;
*led_dat |= 0xffffffff;
return 0;
}
int ledDev_close(struct inode *inode, struct file *file)
{
printk("*****%s*****\n",__FUNCTION__);
*led_dat |= 0xffffffff;
iounmap(led_con);
iounmap(led_dat);
open_flag--;//由于使用自旋锁实现设备独占,所以该行代码同一时间只会有一个进程访问
return 0;
}
以上是使用自旋锁实现保护共享资源。