一、原子操作:即不可再细分的操作,最小的执行单位,在操作完之前都不会被任何事件中断。
整型原子操作:对int类型的操作变成原子操作。
int i = 0;
i = i + 2; <--- 转换为汇编时,不止一条语句,所以可能会被中断。
数据类型:atomic_t 在 linux/types.h 中定义。
typedef struct
{
int counter;
}atomic_t;
atomic_t.counter的变量值变化就是原子的,当然我们不能直接去读写这个变量值,要使用一些函数才能对它进行操作,
这些函数都是围绕着 atomic_t.counter 变量的修改、获取而设计的。
示例:
/* 一般定义为全局变量 */
atomic_t n = ATOMIC_INIT(3); // 将变量 n.counter 的初始值设为 3 --> n.counter = 3
n.counter = 10; // 这种写法没有意义,它并不是原子操作。
atomic_set(&n, 2); // 将变量 n.counter 的初始值设为 2 --> n.counter = 2
atomic_add(5, &n); // 将变量 n.counter 的值加上 5 --> n.counter += 5
atomic_dec(&n); // 将变量 n.counter 的值减 1 --> n.counter -= 1
printk("n = %d", atomic_read(&n)); // 读取变量 n.counter 的值 此时 n.counter == 6
接口:
32位整型原子操作的其他的函数(列出,方便查询):
ATOMIC_INIT(int i) 宏 用 i 初始化 atomic_t 类型的变量
int atomic_read(atomic_t *v) 宏 读 v 的值
void atomic_set(atomic_t *v, int i); 宏 设 v 的值为 i
void atomic_add(int i, atomic_t *v); 宏 将 v 的值加 i
void atomic_sub(int i, atomic_t *v); 宏 将 v 的值减 i
void atomic_inc(atomic_t *v); 宏 将 v 的值加 1
void atomic_dec(atomic_t *v); 宏 将 v 的值减 1
int atomic_sub_and_test(int i, atomic_t *v); 宏 将 v 的值减 i,(0==v) ? 非0值 : 0;
int atomic_inc_and_test(atomic_t *v); 宏 将 v 的值加 1,(0==v) ? 非0值 : 0;
int atomic_dec_and_test(atomic_t *v); 宏 将 v 的值减 1,(0==v) ? 非0值 : 0;
int atomic_add_negative(int i, atomic_t *v); 宏 将 v 的值加 1,(v<0) ? 非0值 : 0;
int atomic_add_return(int i, atomic_t *v); 函 将 v 的值加 i,并返回 +i 后的结果
int atomic_sub_return(int i, atomic_t *v); 函 将 v 的值减 i,并返回 -i 后的结果
int atomic_inc_return(atomic_t *v); 宏 将 v 的值加 1,并返回 +1 后的结果
int atomic_dec_return(atomic_t *v); 宏 将 v 的值减 1,并返回 -1 后的结果
int atomic_add_unless(atomic_t *v, int a, int u); 涵 ( v!=u ) ? v+a,返回非0值 : 0;
int atomic_inc_not_zero(atomic_t *v); 宏 ( v!=0 ) ? v+1,返回非0值 : 0;
64位整型原子操作:和32位整型原子操作一致,所操作的接口只是名称不同,功能一致。
ATOMIC64_INIT(int i) 宏 用 i 初始化 atomic_t 类型的变量
int atomic64_read(atomic_t *v) 宏 读 v 的值
void atomic64_set(atomic_t *v, int i); 宏 设 v 的值为 i
void atomic64_add(int i, atomic_t *v); 宏 将 v 的值加 i
void atomic64_sub(int i, atomic_t *v); 宏 将 v 的值减 i
void atomic64_inc(atomic_t *v); 宏 将 v 的值加 1
void atomic64_dec(atomic_t *v); 宏 将 v 的值减 1
...
...
注意:
32位整型原子操作在64位下执行不会有问题,但是64位整型原子操作在32位系统下执行会造成难以预料的后果。
为了让自己的驱动程序通用,若非必要则尽量使用32位整型原子操作。
位原子操作:
这种操作的数据类型是 unsigned long, 32位系统下为32bit,64位系统下为64bit。
位原子操作函数主要功能是将 unsigned long 变量中的指定位设为0或设为1。
示例:
unsigned long value = 0;
// 设置 value 的第0位为1, value = 0000000000000000 0000000000000001
set_bit(0, &value);
// 设置 value 的第2位为1, value = 0000000000000000 0000000000000101
set_bit(2, &value);
// 设置 value 的第0位为0, value = 0000000000000000 0000000000000100
clear_bit(0, &value);
// 将 value 的第0位取反,第0位为1则设为0,为0则设为1
change_bit(0, &value);
接口:都是宏
void set_bit(int nr, void *addr); 将addr的第nr位设为 1
void clear_bit(int nr, void *addr); 将addr的第nr位设为 0
void change_bit(int nr, void *addr); 将addr的第nr位取反
int test_bit(int nr, void *addr); 如果addr的第nr位为1则返回非0值,否则返回0
int test_and_set_bit(int nr, void *addr); 将addr的第nr位设为 1,设置之前该位为1则返回非0值,否则返回0
int test_and_clear_bit(int nr, void *addr); 将addr的第nr位设为 0,设置之前该位为1则返回非0值,否则返回0
int test_and_change_bit(int nr, void *addr);将addr的第nr位设取反,设置之前该位为1则返回非0值,否则返回0
整型原子操作和位原子操作都是围绕一个变量的操作做为原子操作。
可以用来限定设备能被几个进程操作,和作为计数器使用。
实例:(例如操作打印机)
#define DevNumber 1
atomic_t v = ATOMIC_INIT(DevNumber); // 限定1个
// 打开打印机设备
int OpenPrinterDevice(unsigned char *buf, unsigned int size)
{
// 将v减1后,判断v是否为0
if(atomic_dec_and_test(v))
{
// v 为 0,表示成功得到操作权限
return 0;
}
else
{
// 表示 设备已经被占用。
return -EBUSY;
}
}
// 释放打印机设备
void ClosePrinterDevice(void)
{
atomic_set(&v, DevNumber);
}
二、自旋锁
原子操作可以让指定变量的操作是原子的。很多时候我们在处理一些数据执行某些动作的时候要保证执行过程中
不能被中断,要求是原子的,而整型、位原子操作要实现这种需求就会比较复杂一些。而使用自旋锁则简单很多。
示例:
/* 一般定义为全局变量 */
spinlock_t lock; // 定义一把自旋锁
spin_lock_init(&lock); // 初始化这把自旋锁
或者使用宏来定义并初始化 DEFINE_SPINLOCK(lock)
void MyLock()
{
/* 使用场合:中断下半部与中断服务程序不会进入临界区 */
spin_lock(&lock); // 获取并上锁
// ... <--- 关闭了内核的抢占,但仍受硬中断和中断下半部的影响
// 临界区代码
// ...
spin_unlock(&lock); // 释放解锁,恢复内核的抢占
}
void MyIRQ(void) // 产生中断
{
spin_lock(&lock); // 由于该锁未被释放,所以中断服务参数就会一直自旋(双重请求)
// ... // 而中断服务未退出 就无法退回MyLock(),就无法释放锁造成死锁
// 临界区代码
// ...
spin_unlock(&lock); // 释放解锁,恢复内核的抢占
}
这种情况下需要采用以下的方式上锁:
void MyLockIrq()
{
/*使用场合:
1、中断服务函数与中断下半部都需要进入该临界区
2、中断服务函数需要进入该临界区
*/
spin_lock_irq(&lock); // 获取并上锁 临界区的内容可能会被
// ... <--- 关闭了内核的抢占以及硬件中断响应,软中断依赖硬件中断,自然也不生效。
// 临界区代码
// ...
spin_unloc_irq(&lock); // 释放解锁,恢复内核的抢占以及硬件中断,中断下半部也有效
}
如果访问临界区的资源的代码不是放在中断服务函数中,而是放在中断下半部也会出现相似的情况,
即在MyLock()上锁之后,产生一个硬件中断,当执行完中断服务函数之后就可能会继续执行中断下
半部的代码,因为它可以抢占进程上下文,而低半部要获取的锁已经被MyLock()上锁,形成死锁。
这种情况也可以用void MyLockIrq()这种方式,但是最好用一下方式,更快:
void MyLockBh()
{
/* 使用场合:中断下半部与进程上下文都需要进入该临界代码 */
spin_lock_bh(&lock); // 获取并上锁
// ... <--- 关闭了内核的抢占以及中断下半部,但受硬件中断影响
// 临界区代码
// ...
spin_unloc_bh(&lock); // 释放解锁,恢复内核的抢占以及中断下半部
}
对于一个CPU的机器来说:当有A、B进程都要执行临界区的代码时,假设A先获得锁之后,B进程不会被调度,
系统呈现假死状态, 只有当A释放锁之后,B进程才会被调度再去获取锁,此时A已经释放锁,所以B也就顺利得到锁。
对于两个CPU的机器来说:当有A、B进程都要执行临界区的代码时,假设A先获得锁之后,B进程也会去获取锁,
但是锁已经被A得到,那么B进程则会一直不停的循环检测锁是否被释放,此时系统会呈现假死状态。
(这些现象可以在VM虚拟机上验证,VM虚拟机可以调整CPU个数)
A进程:
spin_lock(&lock); // 获取并上锁
// ...
// 临界区代码 <--- A在执行临界区代码 ---cpu0
// ...
spin_unlock(&lock); // 释放解锁
B进程:
spin_lock(&lock); // <--- 阻塞这里,一直在spin_lock内部不停的循环等待 ---cpu1
// ...
// 临界区代码
// ...
spin_unlock(&lock); // 释放解锁
也就说,只有一个进程能进入临界区,其他进程要想进入临界区只能自己在原地循环旋转等待。
使用注意事项:
1、自旋锁实际上是忙等待,因为在等待锁的时候是在不停的循环等待,长时间占用锁会极大降低系统性能。
2、要避免在临界区中调用可能会产生睡眠的函数,因为此时抢占、中断已经关闭,无法被唤醒导致无法解锁。
3、若数据被软中断共享,也需要加锁,因为在不同处理器上存在软中断同时执行问题。
4、注意避免死锁,例如上述例子,A进程获得了锁之后,又继续获取该锁,因为该锁已经被A获取,
所以该锁无法再次被A获取,A就会一直循环打转等待,A没有机会释放该锁,该CPU被锁死,
对于多颗CPU来说,其他进程又无法释放该锁,形成死循环,导致死机。
A进程:
spin_lock(&lock); // 获取并上锁 关闭了内核的抢占
spin_lock(&lock); // <--- 阻塞这里,一直在spin_lock内部不停的循环 cpu被锁死
// ...
// 临界区代码 <--- 无法得到执行
// ...
spin_unlock(&lock); // 没有机会释放
<wiz_tmp_tag id="wiz-table-range-border" contenteditable="false" style="display: none;">