进程A在读出valid时发现它是1,减1后为0,这时if不成立;但是修改后的值尚未写回内存;假设这时被程序B抢占,程序B读出valid仍为1,减1后为0,这时if不成立,最后成功返回;轮到A继续执行,它把0值写到valid变量,最后也成功返回。这样程序A、B都成功打开了驱动程序。
所谓“原子操作”就是1.2的操作不会被打断。
原子变量类型如下,实际上就是一个结构体(内核文件include/linux/types.h):
typedef struct{
int counter;
}atomic_t;
atomic_read,atomic_set这些操作都只需要一条汇编指令,所以它们本身就是不可打断的。问题在于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)
以ATOMIC_OP为例在UP系统中的实现
对于ARMv6以下的CPU系统,不支持SMP(单核CPU)。原子变量的操作简单粗暴:关中断。代码如下(arch\arm\include\asm\atomic.h):
对于ARMv6及以上的CPU(多核),有一些特殊的汇编指令来实现原子操作,不再需要关中断,代码如下(arch\arm\include\asm\atomic.h):
在ARMv6及以上的架构中,有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表示失败。
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;
}
在ARMv6以下的架构里,不支持SMP系统,关中断。
在ARMv6及以上的架构中,不需要关中断,有ldrex、strex等指令。
Linux内核提供了很多类型的锁,它们可以分为两类:
① 自旋锁(spinning lock);
② 睡眠锁(sleeping lock)。
spinlock函数在内核文件include\linux\spinlock.h中声明,如下表:
自旋锁的加锁、解锁函数是:spin_lock、spin_unlock,还可以加上各种后缀,这表示在加锁或解锁的同时,还会做额外的事情:
semaphore函数在内核文件include\linux\semaphore.h中声明,如下表:
mutex函数在内核文件include\linux\mutex.h中声明,如下表:
举例简单介绍一下,上表中第一行“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去尝试获得自旋锁,这就会导致死锁:所以需要关中断。
假设只有程序A、程序B会抢占资源,这2个程序都是可以休眠的,所以可以使用信号量,代码如下:
static DEFINE_SPINLOCK(clock_lock); // 或 struct semaphore sem; sema_init(&sem, 1);
if (down_interruptible(&sem)) // if (down_trylock(&sem))
{
/* 获得了信号量 */
}
/* 释放信号量 */
up(&sem);
对于down_interruptible函数,如果信号量暂时无法获得,此函数会令程序进入休眠;别的程序调用up()函数释放信号量时会唤醒它。
在down_interruptible函数休眠过程中,如果进程收到了信号,则会从down_interruptible中返回;对应的有另一个函数down,在它休眠过程中会忽略任何信号。
也可以使用mutex,代码如下:
static DEFINE_MUTEX(mutex); //或 static struct mutex mutex; mutex_init(&mutex);
mutex_lock(&mutex);
/* 临界区 */
mutex_unlock(&mutex);
注意:一般来说在同一个函数里调用mutex_lock或mutex_unlock,不会长期持有它。这只是惯例,如果你使用mutex来实现驱动程序只能由一个进程打开,在drv_open中调用mutex_lock,在drv_close中调用mutex_unlock,这也完全没问题。
static DEFINE_SPINLOCK(lock); // static spinlock_t lock; spin_lock_init(&lock);
spin_lock_bh(&lock); //禁止软中断
/* 临界区 */
spin_unlock_bh(&lock);
在Softirq之间(含timer、tasklet、相同的Softirq、不同的Softirq),都可以使用spin_lock()、spin_unlock()来访问临界区。
示例代码如下:
static DEFINE_SPINLOCK(lock); // static spinlock_t lock; spin_lock_init(&lock);
spin_lock(&lock);
/* 临界区 */
spin_unlock(&lock);
示例代码如下:
unsigned long flags;
static DEFINE_SPINLOCK(lock); // static spinlock_t lock; spin_lock_init(&lock);
spin_lock_irqsave(&lock, flags);
/* 临界区 */
spin_unlock_irqrestore(&lock, flags);