一、原子操作
在之前介绍信号量的实现原理中,我们已经了解到获取信号量的操作会导致进程的休眠,也就是存在进程的切换,这样会带来很大的系统开销。
针对单个变量的独占访问我们可以采用原子锁的方式来实现进程的同步。原子锁采用原子操作来实现。
1.1 什么是原子操作
原子操作,顾名思义,就是说像原子一样不可再细分不可被中途打断。一个操作是原子操作,意思就是说这个操作是以原子的方式被执行,要一口气执行完,执行过程不能够被OS的其他行为打断,是一个整体的过程,在其执行过程中,OS的其它行为是插不进来的。
原子操作可以保证对一个整型数据的修改是排他性的。Linux内核提供了一系列函数来实现内核中的原子操作,这些函数又分为两类,分别针对位和整型变量进行原子操作。
1.2 整型原子操作
设置原子变量的值:
void atomic_set(atomic_t *v, int i); /* 设置原子变量的值为i */ atomic_t v = ATOMIC_INIT(0); /* 定义原子变量v并初始化为0 */
获取原子变量的值:
atomic_read(atomic_t *v); /* 返回原子变量的值*/
原子变量加/减:
void atomic_add(int i, atomic_t *v); /* 原子变量增加i */ void atomic_sub(int i, atomic_t *v); /* 原子变量减少i */
原子变量自增/自减:
void atomic_inc(atomic_t *v); /* 原子变量增加1 */ void atomic_dec(atomic_t *v); /* 原子变量减少1 */
操作并测试:
int atomic_inc_and_test(atomic_t *v); int atomic_dec_and_test(atomic_t *v); int atomic_sub_and_test(int i, atomic_t *v);
上述操作对原子变量执行自增、自减和减操作后(注意没有加),测试其是否为0,为0返回true,否则返回false。
操作并返回:
int atomic_add_return(int i, atomic_t *v); int atomic_sub_return(int i, atomic_t *v); int atomic_inc_return(atomic_t *v); int atomic_dec_return(atomic_t *v);
上述操作对原子变量进行加/减和自增/自减操作,并返回新的值。
1.3 位原子操作
设置位:
void set_bit(nr, void *addr);
上述操作设置addr地址的第nr位, 所谓设置位即是将位写为1。
清除位:
void clear_bit(nr, void *addr);
上述操作清除addr地址的第nr位, 所谓清除位即是将位写为0。
改变位:
void change_bit(nr, void *addr);
上述操作对addr地址的第nr位进行反置。
测试位:
test_bit(nr, void *addr);
上述操作返回addr地址的第nr位。
测试并操作位:
int test_and_set_bit(nr, void *addr); int test_and_clear_bit(nr, void *addr); int test_and_change_bit(nr, void *addr);
上述test_and_xxx_bit(nr, void*addr)操作等同于执行test_bit(nr, void*addr)后再执行xxx_bit(nr, void*addr)。
二、整型原子操作实现源码
原子操作的实现是和CPU架构相关的,最底层是通过汇编代码实现的,不同的架构汇编代码是不一样的。这里我们只关注ARM架构的实现方式。
2.1 ATOMIC_INIT
ATOMIC_INIT在arch/arm/include/asm/atomic.h中定义:
#define ATOMIC_INIT(i) { (i) }
atomic_t在include/linux/types.h文件中定义:
typedef struct { int counter; } atomic_t;
2.2 atomic_add
整型原子操作相关的函数这里我们只介绍atomic_add,其它的实现自己查看源码,在arch/arm/include/asm/atomic.h中,我们定位到:
#define ATOMIC_OPS(op, c_op, asm_op) \ ATOMIC_OP(op, c_op, asm_op) \ ATOMIC_OP_RETURN(op, c_op, asm_op) \ ATOMIC_FETCH_OP(op, c_op, asm_op) ATOMIC_OPS(add, +=, add) ATOMIC_OPS(sub, -=, sub)
这里定义了一个宏ATOMIC_OPS(op, c_op, asm_op),值为ATOMIC_OP(op, c_op, asm_op) ATOMIC_OP_RETURN(op, c_op, asm_op) ATOMIC_FETCH_OP(op, c_op, asm_op)。
我们以ATOMIC_OPS(add, +=, add)为例,使用宏展开后为:
ATOMIC_OP(add, +, add) ATOMIC_OP_RETURN(add, +, add) ATOMIC_FETCH_OP(add, +, add)
我们定位到源码中宏ATOMIC_OP的定义,这里armv6以上指令集实现:
#define ATOMIC_OP(op, c_op, asm_op) \ static inline void atomic_##op(int i, atomic_t *v) \ { \ unsigned long tmp; \ int result; \ \ prefetchw(&v->counter); \ __asm__ __volatile__("@ atomic_" #op "\n" \ "1: ldrex %0, [%3]\n" \ " " #asm_op " %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"); \ }
armv6及以下指令集实现:
#define ATOMIC_OP(op, c_op, asm_op) \ static inline void atomic_##op(int i, atomic_t *v) \ { \ unsigned long flags; \ \ raw_local_irq_save(flags); \ v->counter c_op i; \ raw_local_irq_restore(flags); \ } \
我们把ATOMIC_OP(add, +, add),在armv6以上指令集中展开:
static inline void atomic_add(int i, atomic_t *v) { unsigned long tmp; 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"); @破坏描述部分 }
这里使用了嵌入式汇编,更多嵌入式汇编内容可以阅读我之前写的博客嵌入式Linux之常用ARM汇编。
我们使用R0替代R0,R1替代%1、R3替代%3、R4替代%4。
那么汇编的代码可以简化为:
初始条件: R3 = &v->counter @ 初始值所在地址 R4 = i @ 待加的值 1: ldrex R0, [R3] @ 独占访问指令 执行成功R0 = v->counter add R0, R0, R4 @ R0 = R0 + R4 strex R1, R0, [R3] @ 独占访问指令 执行成功[R3]=R0 teq R1, #0 @ 测试R1==0? bne 1b @ 如果R1=0,写入成功,否者跳转到1标号处 返回结果: result = R0 tmp = R1 v->counter = [R3]
ldrex R0,[R3]指令:
- ldrex在读取数据的时候,会将R3所在的内存空间设置一个独占标记,如果执行ldrex指令的时候发现已经被标记为独占访问了,并不会对指令的执行产生影响。
strex R1,R0,[R3]指令:
- strex在更新内存数值时,会检查该段内存是否已经被标记为独占访问,并以此来决定是否更新内存中的值;
- 如果执行这条指令的时候发现已经被标记为独占访问了,则将寄存器R0中的值更新到寄存器R3指向的内存,并将寄存器R1设置成0。指令执行成功后,会将独占访问标记位清除;
- 而如果执行这条指令的时候发现没有设置独占标记,则不会更新内存,且将寄存器R1的值设置成1;
为什么这里可以实现同步呢?我们举个简单的例子:
指令 | 进程1 | 进程2 | 影响 |
1 |
ldrex R0, [R3] |
|
设置独占标记 |
2 |
add R0, R0, R4 |
|
R0=R0+R4 |
3 |
|
ldrex R0, [R3] |
设置独占标记 |
4 |
|
add R0, R0, R4 |
R0=R0+R4 |
5 |
|
strex R1, R0, [R3] |
执行成功,R0写回[R3],清除独占标记 |
6 |
strex R1, R0, [R3] |
|
没有独占标记,执行失败 |
7 |
|
teq R1, #0 |
相等 R1=0 |
8 |
teq R1, #0 |
|
不相等 R1=1 |
9 |
b 1b |
|
跳转 |
从上面可以看到进程2执行strex更新v->count成功后,清空独占标志,会导致进程1更新失败,相当于进程1做了无效操作,进程1会重复执行步骤。
而我们的开发板S3C2440采用的armv4t架构,使用宏展开就是:
static inline void atomic_add(int i, atomic_t *v) { unsigned long flags; raw_local_irq_save(flags); v->counter + i; raw_local_irq_restore(flags); }
这个实现很简单,关中断,修改值,开中断。原理也很简单,关了中断后,操作系统就无法进行内核抢占了,因此就不存在进程切换了。
三、原子操作示例程序
修改信号量示例里面的驱动程序:
- 在驱动程序中首先定义并初始化一个原子变量:
- 增加驱动程序里的open函数和close函数里对原子变量的操作:
#include <linux/module.h> #include <linux/cdev.h> #include <linux/fs.h> #define OK (0) #define ERROR (-1) /* 原子变量 */ static atomic_t canopen = ATOMIC_INIT(1); int hello_open(struct inode *p, struct file *f) { /*自减1并判断是否位0 */ if(!atomic_dec_and_test(&canopen)){ /* 恢复原始值 */ atomic_inc(&canopen); printk("device busy,hello_open failed"); return ERROR; } printk("hello_open\n"); return 0; } ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l) { printk("hello_write\n"); return 0; } ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l) { printk("hello_read\n"); return 0; } int hello_close(struct inode *inode, struct file *file) { /* 恢复原始值 */ atomic_inc(&canopen); return 0; } struct file_operations hello_fops = { .owner = THIS_MODULE, .open = hello_open, .read = hello_read, .write = hello_write, .release = hello_close, }; dev_t devid; // 起始设备编号 struct cdev hello_cdev; // 保存操作结构体的字符设备 struct class *hello_cls; int hello_init(void) { /* 动态分配字符设备: (major,0) */ if(OK == alloc_chrdev_region(&devid, 0, 1,"hello")){ // ls /proc/devices看到的名字 printk("register_chrdev_region ok\n"); }else { printk("register_chrdev_region error\n"); return ERROR; } cdev_init(&hello_cdev, &hello_fops); cdev_add(&hello_cdev, devid, 1); /* 创建类,它会在sys目录下创建/sys/class/hello这个类 */ hello_cls = class_create(THIS_MODULE, "hello"); if(IS_ERR(hello_cls)){ printk("can't create class\n"); return ERROR; } /* 在/sys/class/hello下创建hellos设备,然后mdev通过这个自动创建/dev/hello这个设备节点 */ device_create(hello_cls, NULL, devid, NULL, "hello"); return 0; } void __exit hello_exit(void) { printk("hello driver exit\n"); /* 注销类、以及类设备 /sys/class/hello会被移除*/ device_destroy(hello_cls, devid); class_destroy(hello_cls); cdev_del(&hello_cdev); unregister_chrdev_region(devid, 1); return; } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL");
当第一个应用程序打开该驱动时,canopen为1,自减为0,atomic_dec_and_test返回真,取反为假,if条件不成立,打开驱动成功。
当第二个应用程序想要再次打开该驱动时,canopen为0,自减为-1,atomic_dec_and_test返回假,取反为真,if条件成立,再对canopen加1操作,返回错误,打开驱动失败,这样就实现了同一时刻驱动只能有一个使用者。
参考文章