原子操作,自旋锁,信号量,互斥量

目录

同步与互斥失败的例子

1.失败示例一

2.失败示例二

3.失败示例三

 解决方法

1.原子变量

 Linux中锁的介绍和使用

1.锁定类型

1.1自旋锁

1.2睡眠锁

 2.锁的内核函数

2.1自旋锁

2.2信号量semaphore

2.3互斥量mutex

2.4semaphore和mutex的区别

3.何时使用何种锁 

4.内核抢占等额外的概念 


同步与互斥失败的例子

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调用某个驱动执行耗时的操作,在这一段时间内系统是可以切换去执行更高优先级的程序。

对于可抢占的内核,编写驱动程序时要时刻注意:你的驱动程序随时可能被打断、随时是可以被另一个进程来重新执行。对于可抢占的内核,在驱动程序中要考虑对临界资源加锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值