目录
同步与互斥失败的例子
1.失败示例一
对于这个代码
static int valid = 1;
static ssize_t gpio_key_drv_open(struct inode *node, struct file *file)
{
if(!valid)
{
return -EBUSY;
}
else
{
valid = 0;
}
return 0; //成功打开文件
}
假如一个线程要打开这个文件,当刚刚运行到else里面还没有将valid赋值为0时,被另一个程序所打断,这另一个程序也要打开这个文件,此时valid的值为1,它也可以打开文件,运行完之后相当于两个线程同时打开了同一个文件,这是就会出错
2.失败示例二
对于示例一的失败主要是因为在将valid的值赋值为0时中间太长了,于是做出了以下改进:
static int valid = 1;
static ssize_t gpio_key_drv_open(struct inode *node, struct file *file)
{
if(--valid)
{
valid++;
return -EBUSY;
}
return 0;
}
对于这个代码。将valid赋值为0的过程大大缩减(使用--valid),但是将 --valid 这个代码使用汇编语言来看的话,这条指令会分成三部分(读出,修改,写入)
如果在读出的时候执行另一个线程,此时valid的值还是为1,那么就会打开文件,全部执行完之后可还是会出现两个线程打开同一个文件的情况
3.失败示例三
我们都知道打断当前线程运行需要用到中断,于是就有了:
static int valid = 1;
static ssize_t gpio_key_drv_open(struct inode*node, struct file*file)
{
unsigned long flags;
raw_local_irq_save(flags); //关闭中断
if(--valid)
{
valid++;
raw_local_irq_restore(flags); //恢复之前的状态
return -EBUSY;
}
raw_local_irq_restore(flags); //恢复之前的状态
return 0;
}
对于单个CPU来说这个代码无疑是没有问题的,但是如果有多个CPU,如果同时运行到if(--valid)
此时两个CPU就会同时打开同一个文件,此时还是会出错
解决方法
1.原子变量
使用原子变量实现互斥
原子变量的操作函数在Linux内核文件中的arch\arm\include\asm\atomic.h中
原子变量类型如下,实际上就是一个结构体(内核文件include/linux/types.h)
typedef struct {
int counter;
}atomit_t;
它并不特殊,特殊的是它的操作函数(v都是 atomic_t 指针)
函数名 | 作用 |
atomic_read(v) | 读出原子变量的值,即 v->couter |
atomic_set(v,i) | 设置原子变量的值,即 v->couter = i |
atomic_inc(v) | v->couter++ |
atomic_dec(v) | v->couter-- |
atomic_add(i,v) | v->couter += i |
atomic_sub(i,v) | v->couter -= i |
atomic_inc_and_test(v) | 先加一,再判断新值是否等于0:等于0的话,返回值为1 |
atomic_dec_and_test()v | 先减一,再判断新值是否等于0:等于0的话,返回值为1 |
在内核里面原子变量的实现有两种方法
1.对于小于ARMV6指令集(不支持SMP(也就是多个CPU))
其实就是关中断,然后恢复中断
2.对于大于等于ARMV6指令集
使用内联汇编(特殊指令)
static inline void atomic_add(int i, atomic_t *v) { unsigned long tmp; //用于存储strex指令的返回值 int result; //用于存储从内存加载的值 prefetchw(&v->counter); __asm__ __volatile__("@ atomic_" add "\n" "1: ldrex %0, [%3]\n" " add %0, %0, %4\n" " strex %1, %0, [%3]\n" " teq %1, #0\n" " bne 1b" : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter) : "r" (&v->counter), "Ir" (i) : "cc"); } 这段代码使用ARM架构中的ldrex 和 strex 指令来确保在多线程环境中对一个整数的假发操作是安全的 prefetchw(&v->counter); //使用prefetchw指令预取v->counter的值,以提高后续访问的性能。这是一种 //预测性加载的优先 __asm__ __volatile__("@ atomic_add\n //进入汇编模块使用__volatile__修饰符表示编译器不应该优化 //这个代码块。@ atomic_add是一个注释,说明这段汇编代码的 //目的 "1: ldrex %0, [%3]\n" //标签1:定义了一个跳转位置。ldrex 指令从 v->counter 的地址(即 //&v->counter 这个指针的值)加载(读)当前值到 %0(即 //result)。 " add %0, %0, %4\n" //使用 add 指令将 result(当前的计数器值)与传入的值 i 相加,并将结果 存回 result。 " strex %1, %0, [%3]\n" //使用 strex 指令尝试将更新后的 result 值存储回 v->counter //地址。%1(即 tmp)将被设置为 0(成功)或 1(失败),取决于 //是否成功写入计数器。如果在这次写入事件中没有其他核心对这个地 //址进行了写操作,strex 会成功 " teq %1, #0\n" //使用 teq 指令将 tmp 与 0 进行比较,以检查 strex 是否成功。 " bne 1b" //如果 strex 失败(即 tmp 不等于 0,表示其他核心已修改了 v->counter),则 //跳转回标签 1 重新尝试读取和写入。这个循环保证了只有在没有其他线程修改 //counter 的情况下,才会更新它。 : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter) //这是输出约束,表示: //result 将被输入输出,且是可写的寄存器。 //tmp 将是一个新的寄存器。 //v->counter 将被视为输入和输出,且允许在 //内存中进行更改。 : "r" (&v->counter), "Ir" (i) //这是输入约束,表示: //&v->counter 是一个需要被读取的内存地址。 //i 是一个立即数,并通过寄存器传递给汇编。 : "cc"); //表示此汇编代码会对条件码(cc)产生影响,以便编译器了解可能的状态变化。
这里面的 ldrex 和 strex指令与ldr 和 str 指令的区别就是可以将寄存器标记为独占访问,如果在读出时别其他CPU访问,那么寄存器就被设置为独占,此时后续操作就会失败
操作实例:
static atomic_t valid = ATOMIC_INIT(1);
static ssize_t gpio_key_drv_open (struct inode *node, struct file *file)
{
if (atomic_dec_and_test(&valid))
{
return 0;
}
atomic_inc(&valid);
return -EBUSY;
}
static int gpio_key_drv_close (struct inode *node, struct file *file)
{
atomic_inc(&valid);
return 0;
}
Linux中锁的介绍和使用
1.锁定类型
Linux内核提供了很多类型的锁,它们可以分为两类:1. 自旋锁 2.睡眠锁
1.1自旋锁
简单来说就是无法获得锁时,不会休眠,会一直循环等待。有这些自旋锁
自旋锁 | 描述 |
raw_spinlock_t | 原始自旋锁 |
bit_spinlocks | 位自旋锁 |
自旋锁的加锁,解锁函数时:spin_lock, spin_unlock ,还可以加上各种后缀,这表示在加锁或解锁的同时,还会做额外的事情
后缀 | 描述 |
_bh() | 加锁时禁止下半部(软中断),解锁时使能下半部(软中断) |
_irq() | 加锁时禁止中断,解锁时使能中断 |
_irqsave/restore | 加锁时禁止中断并记录状态,解锁时恢复中断位所标记的状态 |
1.2睡眠锁
简单来说就是无法获得锁时,当前线程就睡休眠。有这些睡眠锁:
休眠锁 | 描述 |
mutex | mutual exclusion,彼此排斥,即互斥锁(后面讲解) |
rt_mutex | |
semaphore | 信号量、旗语(后面讲解) |
rw_semaphore | 读写信号量,读写互斥,但是可以多人同时读 |
ww_mutex | |
percpu_rw_semaphore | 对rw_semaphore的改进,性能更优 |
2.锁的内核函数
2.1自旋锁
spinlock函数在内核文件include\linux\spinlock.h中声明,如下表:
函数名 | 作用 |
spin_lock_init(_lock) | 初始化自旋锁为unlock状态 |
void spin_lock(spinlock_t *lock) | 获取自旋锁(加锁),返回后肯定获得了锁 |
int spin_trylock(spinlock_t *lock) | 尝试获得自旋锁,成功获得锁则返回1,否则返回0 |
void spin_unlock(spinlock_t *lock) | 释放自旋锁,或称解锁 |
int spin_is_locked(spinlock_t *lock) | 返回自旋锁的状态,已加锁返回1,否则返回0 |
自旋锁的加锁、解锁函数是:spin_lock、spin_unlock,还可以加上各种后缀,这表示在加锁或解锁的同时,还会做额外的事情:
后缀 | 描述 |
_bh() | 加锁时禁止下半部(软中断),解锁时使能下半部(软中断) |
_irq() | 加锁时禁止中断,解锁时使能中断 |
_irqsave/restore() | 加锁时禁止并中断并记录状态,解锁时恢复中断为所记录的状态 |
2.2信号量semaphore
semaphore函数在内核文件include\linux\semaphore.h中声明,如下表:
函数名 | 作用 |
DEFINE_SEMAPHORE(name) | 定义一个struct semaphore name结构体, count值设置为1 |
void sema_init(struct semaphore *sem, int val) | 初始化semaphore |
void down(struct semaphore *sem) | 获得信号量,如果暂时无法获得就会休眠 返回之后就表示肯定获得了信号量 在休眠过程中无法被唤醒, 即使有信号发给这个进程也不处理 |
int down_interruptible(struct semaphore *sem) | 获得信号量,如果暂时无法获得就会休眠, 休眠过程有可能收到信号而被唤醒, 要判断返回值: 0:获得了信号量 -EINTR:被信号打断 |
int down_killable(struct semaphore *sem) | 跟down_interruptible类似, down_interruptible可以被任意信号唤醒, 但down_killable只能被“fatal signal”唤醒, 返回值: 0:获得了信号量 -EINTR:被信号打断 |
int down_trylock(struct semaphore *sem) | 尝试获得信号量,不会休眠, 返回值: 0:获得了信号量 1:没能获得信号量 |
int down_timeout(struct semaphore *sem, long jiffies) | 获得信号量,如果不成功,休眠一段时间 返回值: 0:获得了信号量 -ETIME:这段时间内没能获取信号量,超时返回 down_timeout休眠过程中,它不会被信号唤醒 |
void up(struct semaphore *sem) | 释放信号量,唤醒其他等待信号量的进程 |
2.3互斥量mutex
mutex函数在内核文件include\linux\mutex.h中声明,如下表:
函数名 | 作用 |
mutex_init(mutex) | 初始化一个struct mutex指针 |
DEFINE_MUTEX(mutexname) | 初始化struct mutex mutexname |
int mutex_is_locked(struct mutex *lock) | 判断mutex的状态 1:被锁了(locked) 0:没有被锁 |
void mutex_lock(struct mutex *lock) | 获得mutex,如果暂时无法获得,休眠 返回之时必定是已经获得了mutex |
int mutex_lock_interruptible(struct mutex *lock) | 获得mutex,如果暂时无法获得,休眠; 休眠过程中可以被信号唤醒, 返回值: 0:成功获得了mutex -EINTR:被信号唤醒了 |
int mutex_lock_killable(struct mutex *lock) | 跟mutex_lock_interruptible类似, mutex_lock_interruptible可以被任意信号唤醒, 但mutex_lock_killable只能被“fatal signal”唤醒, 返回值: 0:获得了mutex -EINTR:被信号打断 |
int mutex_trylock(struct mutex *lock) | 尝试获取mutex,如果无法获得,不会休眠, 返回值: 1:获得了mutex, 0:没有获得 注意,这个返回值含义跟一般的mutex函数相反, |
void mutex_unlock(struct mutex *lock) | 释放mutex,会唤醒其他等待同一个mutex的线程 |
int atomic_dec_and_mutex_lock(atomic_t *cnt, struct mutex *lock) | 让原子变量的值减1, 如果减1后等于0,则获取mutex, 返回值: 1:原子变量等于0并且获得了mutex 0:原子变量减1后并不等于0,没有获得mutex |
2.4semaphore和mutex的区别
semaphore中可以指定count为任意值,比如有10个厕所,所以10个人都可以使用厕所。
而mutex的值只能设置为1或0,只有一个厕所。
是不是把semaphore的值设置为1后,它就跟mutex一样了呢?不是的。
看一下mutex的结构体定义,如下:
它里面有一项成员“struct task_struct *owner”,指向某个进程。一个mutex只能在进程上下文中使用:谁给mutex加锁,就只能由谁来解锁。
而semaphore并没有这些限制,它可以用来解决“读者-写者”问题:程序A在等待数据──想获得锁,程序B产生数据后释放锁,这会唤醒A来读取数据。semaphore的锁定与释放,并不限定为同一个进程。
主要区别列表如下:
semaphore | mutex | |
几把锁 | 任意,可设置 | 1 |
谁能解锁 | 别的程序、中断等都可以 | 谁加锁,就得由谁解锁 |
多次解锁 | 可以 | 不可以,因为只有1把锁 |
循环加锁 | 可以 | 不可以,因为只有1把锁 |
任务在持有锁的期间可否退出 | 可以 | 不建议,容易导致死锁 |
硬件中断、软件中断上下文中使用 | 可以 | 不可以 |
3.何时使用何种锁
举例简单介绍一下,上表中第一行“IRQ Handler A”和第一列“Softirq A”的交叉点是“spin_lock_irq()”,意思就是说如果“IRQ Handler A”和“Softirq A”要竞争临界资源,那么需要使用“spin_lock_irq()”函数。为什么不能用spin_lock而要用spin_lock_irq?也就是为什么要把中断给关掉?假设在Softirq A中获得了临界资源,这时发生了IRQ A中断,IRQ Handler A去尝试获得自旋锁,这就会导致死锁:所以需要关中断。
4.内核抢占等额外的概念
早期的的Linux内核是“不可抢占”的,假设有A、B两个程序在运行,当前是程序A在运行,什么时候轮到程序B运行呢?
① 程序A主动放弃CPU:
比如它调用某个系统调用、调用某个驱动,进入内核态后执行了schedule()主动启动一次调度。
② 程序A调用系统函数进入内核态,从内核态返回用户态的前夕:
这时内核会判断是否应该切换程序。
③ 程序A正在用户态运行,发生了中断:
内核处理完中断,继续执行程序A的用户态指令的前夕,它会判断是否应该切换程序。
从这个过程可知,对于“不可抢占”的内核,当程序A运行内核态代码时进程是无法切换的(除非程序A主动放弃),比如执行某个系统调用、执行某个驱动时,进程无法切换。
这会导致2个问题:
① 优先级反转:
一个低优先级的程序,因为它正在内核态执行某些很耗时的操作,在这一段时间内更高优先级的程序也无法运行。
② 在内核态发生的中断不会导致进程切换
为了让系统的实时性更佳,Linux内核引入了“抢占”(preempt)的功能:进程运行于内核态时,进程调度也是可以发生的。
回到上面的例子,程序A调用某个驱动执行耗时的操作,在这一段时间内系统是可以切换去执行更高优先级的程序。
对于可抢占的内核,编写驱动程序时要时刻注意:你的驱动程序随时可能被打断、随时是可以被另一个进程来重新执行。对于可抢占的内核,在驱动程序中要考虑对临界资源加锁。