跳到主要内容位置

Linux驱动-同步互斥与原子变量

同步与互斥失败的例子#

三个失败的同步互斥例子:

1.在 C 语言里面通过一个全局变量标志位判断,但是可能有多个进程读取这个变量,如果在没有修改变量前就切换了,那么多个进程都会任务自己拿到了这个标志

2.使用flag--来改变标志位,这样虽然把标志位的读取和改变结合到了一个 C 语言语句,但是在 CPU 执行时仍是分为“读取、修改、写入”三个步骤,依旧可能会产生切换导致多个进程获得标志

3.关中断方式:在单 CPU 系统上可以实现同步与互斥

但是,注意需要考虑多 CPU 的情况,对于 SMP 系统,你只能关闭当前 CPU 核的中断,别的 CPU 核还可以运行程序,它们也可以来执行这个函数.

注意:SMP 就是 Symmetric Multi-Processors,对称多处理器;UP 即 Uni-Processor,系统只有一个单核 CPU。

原子操作的实现原理与使用方法#

原子变量的操作函数:kernel\arch\arm\include\asm\atomic.h

原子变量类型#

如下,实际上就是一个结构体(内核文件kernel/include/linux/types.h)

typedef struct {
int counter;
} atomic_t;

原子变量内核操作函数#

函数名作用
atomic_read(v)读出原子变量的值,即v->counter
atomic_set(v,i)设置原子变量的值,即v->counter = i
atomic_inc(v)v->counter++
atomic_dec(v)v->counter--
atomic_add(i,v)v->counter += i
atomic_sub(i,v)v->counter -= i
atomic_inc_and_test(v)先加1,再判断新值是否等于0;等于0的话,返回值为1
atomic_dec_and_test(v)先减1,再判断新值是否等于0;等于0的话,返回值为1

原子操作的定义#

atomic_read,atomic_set这些操作都只需要一条汇编指令,所以它们本身就是不可打断的。

例如:#define atomic_read(v) READ_ONCE((v)->counter)

问题在于atomic_inc这类操作,要读出、修改、写回。以atomic_inc为例,在atomic.h文件中,如下定义:

#define atomic_inc(v) atomic_add(1, v)

atomic_add又是怎样实现的呢?用下面这个宏:

ATOMIC_OPS(add, +=, add)

宏定义:

#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)
//在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); \
} \
//在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"); \
}

ATOMIC_OPS(add, +=, add)为例展开

ATOMIC_OPS(add, +=, add)
->替换 变成3个宏
ATOMIC_OP(add, +=, add) \
ATOMIC_OP_RETURN(add, +=, add) \
ATOMIC_FETCH_OP(add, +=, add)
->继续展开
ATOMIC_OP(add, +=, add)
->在ARMv6以下
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); \
}
->在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"); \
}

在ARMv6及以上的原子加法使用内联汇编调用一些特殊的指令独占修改与写入ldrex/strex

ldrex、strex指令,ex表示exclude,意为独占地。这2条指令要配合使用,如下:

  • 读出:ldrex r0, [r1]
    • 读取r1所指内存的数据,存入r0;并且标记r1所指内存为“独占访问”。
    • 如果有其他程序再次执行“ldrex r0, [r1]”,一样会成功,一样会标记r1所指内存为“独占访问”。
  • 修改r0的值
  • 写入:strex r2, r0, [r1]
    • 如果r1的“独占访问”标记还存在,则把r0的新值写入r1所指内存,并且清除“独占访问”的标记,把r2设为0表示成功。
    • 如果r1的“独占访问”标记不存在了(有别的进程先清除了),就不会更新内存,并且把r2设为1表示失败。

在ARMv6及以上的架构中,原子操作不再需要关闭中断,关中断的花销太大了。并且关中断并不适合SMP多CPU系统,你关了CPU0的中断,CPU1也可能会来执行些操作啊。

在ARMv6及以上的架构中,原子操作的执行过程是可以被打断的,但是它的效果符合“原子”的定义:一个完整的“读、修改、写入”原子的,不会被别的程序打断。它的思路很简单:如果被别的程序打断了,那就重来,最后总会成功的。

原子变量的使用示例#

static atomic_t valid = ATOMIC_INIT(1); // 初始化count为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;
}

atomic_dec_and_test,这是一个原子操作,在ARMv6以下的CPU架构中,这个函数是在关中断的情况下执行的,它确实是“原子的”,执行过程不被打断。

但是在ARMv6及以上的CPU架构中,这个函数其实是可以被打断的,但是它实现了原子操作的效果,如下图所示:

原子位#

原子位的操作函数在kernel\arch\arm\include\asm\bitops.h中,下表中p是一个unsigned long指针。

函数名作用
set_bit(nr,p)设置(*p)的bit nr为1
clear_bit(nr,p)清除(*p)的bit nr为0
change_bit(nr,p)改变(*p)的bit nr,从1变为0,或是从0变为1
test_and_set_bit(nr,p)设置(*p)的bit nr为1,返回该位的老值
test_and_clear_bit(nr,p)清除(*p)的bit nr为0,返回该位的老值
test_and_change_bit(nr,p)改变(*p)的bit nr,从1变为0,或是从0变为1;返回该位的老值

在ARMv6以下的架构里,不支持SMP系统,原子位的操作函数也是简单粗暴:关中断。例如

/*
* These functions are the basis of our bit ops.
*
* First, the atomic bitops. These use native endian.
*/
static inline void ____atomic_set_bit(unsigned int bit, volatile unsigned long *p)
{
unsigned long flags;
unsigned long mask = BIT_MASK(bit);
p += BIT_WORD(bit);
raw_local_irq_save(flags);
*p |= mask;
raw_local_irq_restore(flags);
}

在ARMv6及以上的架构中,不需要关中断,有ldrex、strex等指令,这些指令的作用在前面介绍过。还是以set_bit函数为例,代码如下: