Linux——互斥与同步

目录

一种典型的竞态

内核中的并发

中断屏蔽

原子变量

自旋锁

读写锁

顺序锁



一种典型的竞态


        假设整型变量i是驱动代码中的一个个全局变量,在驱动的某个例程中执行了i++操作,而在中断服务程序中也执行了i++操作,在这种情形下我们来分析一下可能造成的数据紊乱情况。首先来看看i++操作用 ARM 汇编展开是怎样的。

ldr r1, [r0]

add r1, r1, #1

str r1,   [r0]

        假设变量i是存放在r0所指向的内存中的,也就是说,r0 寄存器中保存了变量的地址,并且变量i的初值为5。汇编代码第一行首先通过寄存器间接寻址将变量的值装载到了r1 寄存器中,然后使用 add 指令将r1 的值自加 1,最后使用 str 指令加1后的值存回变量i所在的内存中。由上可见,一条简单的i++操作翻译成汇编后变成了3 条指令,这就会引入一些问题。假设一条内核的执行路径刚好将第一条指令执行完成,外部产生了一个硬件中断,这时,这条内核执行路径被打断,而去执行中断处理程序。碰巧的是,中断处理程序也要对这个全局变量进行自加操作,中断处理程序在没有被打断的情况下成功将变量i的值自加到 6,然后中断处理程序返回,继续刚才被打断的内核执行路径。此时寄存器的值被恢复后,r1 的值还是之前从内存中取到的 5,然后自加1后变成6,最后又存入变量i所在的内存。很显然,这个过程共执行了两次i++操作,变量i的值应该为 7,但结果却为 6,这就造成了数据紊乱,也就是内核的不同路径同时《并非是严格意义上的同时,就如同被中断抢占了的情况一样,在一个路径还没有对共享资源访问完成时,被内核的另一条执行路径所抢占,也认为是同时) 对共享资源的访问选成了竞态。

内核中的并发

        总的来说,当内核有多条执行路径同时访问同一个共享资源时,就会造成竞态。常见的共享资源有全局变量、静态变量、硬件的寄存器和共同使用的动态分配的同一段内存等。造成竞态的根本原因就是内核中的代码对共享资源产生了并发(同时)的访问。那么内核中有哪些并发的情况呢?下面罗列如下。


(1)硬件中断一一当处理器允许中断的时候,一个内核执行路径可能在任何一个时间都会被一个外部中断打断。
(2)软中断和 tasklet一通过前面的知识我们知道,内核可以在任意硬中断快要返回之前执行软中断及 tasklet,也有可能唤醒软中断线程,并执行 tasklet。

(3)抢占内核的多进程环境——如果一个进程在执行时发生系统调用,进入到内核,由内核代替该进程完成相应的操作,此时如有一个更高优先级的进程准备就绪,内核判断在可抢占的条件成立的情况下可以抢占当前进程,然后去执行更高优先级的进程。
(4)普通的多进程环境一一当一个进程因为等待的资源暂时不可用时,就会主动放弃CPU,内核会调度另外一个进程来执行。
(5)多处理器或多核 CPU。在同一时刻,可以在多个处理器上并发执行多个程序这是真正意义上的并发
        并发对共享资源访问就会引起竟态,解决竟态的一个方法就是互斥,也就是对共享资源的串行化访问,即在一条内核执行路径上访问共享资源时,不允许其他内核执行路径来访问共享资源。共享资源有时候又叫作临界资源,而访问共享资源的这段代码又叫作临界代码段或临界区。内核提供了多种互斥的手段,下面逐一进行介绍。

中断屏蔽


        在说明竞态所举的例子中,造成竞态的原因是一条内核执行路径被中断打断了。如果在访问共享资源之前先将中断屏蔽(禁止),然后再访问共享资源,等共享资源访问完成后再重新使能中断就能避免这种竞态的产生。关于中断的屏蔽和使能的函数我们在前面已经讲过了,这里就不再赘述。需要另外说明的是,如果明确知道是哪一个中断会带来竞态,我们通常应该只屏蔽相应的中断,而不是屏蔽本地 CPU 的全局中断,这样可以使其他中断照常执行。如果非要屏蔽本地 CPU 的中断,那么应该尽量使用local_irq_save 和 local_irq_restore 这对宏,因为如果使用 local_irq_disable 和 local_irq_enable 这对宏,如果中断在屏蔽之前本身就是屏蔽的,那么 local_irq_enable 会将本来就屏蔽的中断错误地使能,从而造成中断使能状态的前后不一致。而且,中断屏蔽到中断重新使能之间的这段代码不宜过长,否则中断屏蔽的时间过长,将会影响系统的性能。在i++的例子中,可以在 i++之前屏蔽中断,之后重新使能中断,代码的形式如下

unsigned long flags;
local_irq_save(flags);
i++;
local_irq_restore(flags);


使用中断屏蔽来做互斥时的注意事项总结如下。
(1)对解决中断引起的并发而带来的竞态简单高效。
(2)应该尽量使用local_irq_save和 local_irq_restore 来屏蔽和使能中断
(3)中断屏蔽的时间不宜过长。
(4)只能屏蔽本地CPU的中断,对多CPU 系统,中断也可能会在其他CPU上产生


原子变量


        如果一个变量的操作是原子性的,即不能再被分割,类似于在汇编级代码上也只要一条汇编指令就能完成,那么对这样变量的访问就根本不需要考虑并发带来的影响。因此,内核专门提供了一种数据类型 atomic_t,用它来定义的变量为原子变量,其类型定义如下。
/*include/1inux/types.h */
 

   175	typedef struct {
   176		int counter;
   177	} atomic_t;


        由上可知,原子变量其实是一个整型变量。对于整型变量,有的处理器专门提供一些指令来实现原子操作(比如ARM 处理器中的swp 指令),内核就会使用这些指令来对原子变量进行访问,但是有些处理器不具备这样的指令,内核将会使用其他的手段来保证对它访问的原子性,比如中断屏蔽。但对于驱动开发者来说,他们并不关心这些,他们更关心内核提供了哪些接口来操作原子变量。现将主要的 API罗列如下。

atomic read(v)
atomic set(v, i)
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_add_negative(int i, atomic_t *v)
void atomic_add(int i, atomic_t *V);
void atomic_sub(int i, atomic_t *v);
void atomic_inc(atomic_t *v);
void atomic dec(atomic_t *v);
atomic_dec_return(v)
atomic_inc_return(V)
atomic_sub_and_test(i, v)
atomic_dec_and_test(v)
atomic_inc_and_test(v)
atomic_xchg(ptr,v)
atomlc_cmpxchg(v,old,new)
void atomic_clear_mask(unsigned long mask, atomic_t *v);
void atomic_set_mask(unsigned int mask, atomic_t *v);
void set_bit(int nr, volatile unsigned long *addr);
void clear_bit(int nr, volatile unsigned long *addr);
void change_bit(int nr, volatlle unsigned long *addr);
int test_and_set_bit(int nr, volatile unsigned long *addr);
int test_and_clear_bit(int nr, volatile unsigned long *addr);
int test_and_change_bit(int nr, volatile unsigned long *addr);


        atomic_read;读取原子变量v的值。
        atomic_set(v,i), 设置原子变量v的值为i。
        atomic_add、 atomic_sub: 将原子变量加上i成减去 i, 加“ _return”表示还要返回修改后的值,加“ _negative”表示当结果为负返回真。

        atomic_inc、atomic_dec:将原子变量自加1或自减 1, 加“_return”表示还要返回改后的值,加“ _test”表示结果为0返回真。
        atomic_xchg:交换v和ptr 指针指向的数据。

        atomic_cmpxchg: 如果v的值和 old 相等,则将 V 的值设为 new,并返回原来的值。        

        atomic_clear_mask:将v中mask为1的对应位清零。
        atomic_set_mask:将v中mask为1的对应位置一.
        set_bit、clear_bit、change_bit: 将nr位置一、清零或翻转,有 test 前缀的还要返回来的值。
        在i++的那个例子中,我们可以使用下面的代码来保证对它访问的原子性操作,第一行使用ATOMIC_INT对原子变量赋初值
 

atomic_t i = ATOMIC_INIT(5);

atomic_inc(&1);

        需要说明的是,原子变量虽然使用方便,但是其本质是一个整型变量,对于非整形变量(如整个结构)就不能使用这一套方法来操作,而需要使用另外的方法。但是在能够使用原子变量时就尽可能地使用原子变量,而不要使用复杂的锁机制,因为相比于锁机制,它的开销小。


自旋锁


        有这样一个不太优雅的例子:一个公共的卫生间,很多人排队去方便,但是要进去就要先打开卫生间的门,进去后将门反锁,出来后再开锁。很显然,在门被反锁的期间,其他人是进不去的,只有干着急,越等越急后,就急得团团转,于是就原地自旋了。

        这个例子很能说明在内核中的另一种互斥手段一一自旋锁的特性,在访问共享资源(卫生间)之前,首先要获得自旋锁《卫生间门上的锁),访问完共享资源后解锁。其他内核执行路径(其他人)如果没有竞争到锁,只能忙等待,所以自旋锁是一种忙等锁。

        内核中自旋锁的类型是spinlock_t,相关的API如下

 

spin_lock_init( _lock)
void spin_lock(spinlock_t *lock);

void spin_lock_irq(spinlock_t *lock);
void spin_lock_irqsave(lock, flags);
void spin_lock_bh(spinlock_t *lock);

int spin_trylock(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock);
int spin_trylock_irq(spinlock_t *lock);
void spin_unlock(spinlonk_t *lock);
void spin_unlock_irq(spinlonk_t *lock);
void spin_unlock_irqrestore(spinlonk_t *lock, unsigned long flags);
void spin_unlock_bh(spinlock_t *lock);


spin_lock_init; 初始化自旋锁,在使用自旋锁之前必须要初始化
spin_lock:获取自旋锁,如果不能获得自旋锁,则进行忙等待。
spin_lock_bh:获取自旋锁并禁止下半部。
spin_lock_irq:获取自旋锁并竞争中断。
spin_lock_irqsave; 获取自锁并竞争中断,保存中断屏藏状态到fags中。
spin_trylock,尝试获取自锁,即使不能获取,立即返回,返回值为 0麦示成功获得自旋锁,否则表示没有获得自锁。其他的变体和 spin_lock 变体的意义相同。

spin_unlock;释放自锁,其他的 unlock 版本可以依据前面的解释判断其作用。

在i++的例子中,我们可以使用下而的代码来使自旋锁对了的操作进行互斥。

 

int i = 5;
/*定义自旋锁 */
spinlock_t lock;
/*用于保存中断屏蔽状态的变量*/
unsigned long flags;

/*使用自旋锁之前必须初始化自旋锁 */
spin_lock_init(&lock);
/*访问共享资源之前获得自旋锁,禁止中断,并将之前的中断屏蔽状态保存在flags变量中*/
spin_lock_irqsave(&lock, flags);
/*访网共享资源*/
i++;
/*共享资源访问完成后释放自旋锁,用 flags 的值恢复中断屏蔽的状态*/
spin_unlock_irqrestore(&lock,flags);


        从上面的例子可以看到,自旋锁的使用还是比较直观的,基本的步骤是定义锁、初始化锁、访问共享资源之前获得锁、访间完成之后释放锁。


        关于自旋锁的一些重要特性和使用注意事项总结加下。


(1)获得自旋锁的临界代码段执行时间不宜过长,因为是忙等锁,如果临界代码段执行时间过长,就意味着其他想要获得锁的内核执行路径会进行长时间的忙等待,这会影响系统的工作效率。
(2)在获得锁的期间,不能够调用可能会引起进程切换的函数,因为这会增加持锁的时间,导致其他要获取锁的代码进行更长时间的等待,更糟糕的情况是,如果新调度的进程也要获取同样的自旋锁,那么会导致死锁。
(3)自旋锁是不可递归的,即获得锁之后不能再获得自旋锁,否则会因为等待一个不能获得的锁而将自己锁死。
(4)自旋锁可以用于中断上下文中,因为它是忙等锁,所以并不会引起进程的切换.

(5)如果中断中也要访问共享资源,则在非中断处理代码中访问共享资源之前应该先禁止中断再获取自旋锁,即应该使用 spin_lock_irq 或 spin_lock_irqsave 来获得自旋锁,如果不这样的话,即使获得了锁中断也会发生,在中断中访问共享资源之前,中断也要获得一个已经被获得的自旋锁,那么中断将会被锁死,中断的下半部也有类似的情况。另外,推荐使用 spin_lock_irqsave 而不是 spin_lock_irq,原因同中断屏蔽中相关的描述.

(6)虽然一直都在说自旋锁是忙等锁,但是在单处理器的无抢占内核中,单纯的自旋锁(指不是禁止中断,禁止下半部的一些变体)获取操作其实是一个空操作,而在单处理器的可抢占内核中也仅仅是禁止抢占而己(但这会使高优先级的就绪进程的执行时间稍微推后一些)。真正的忙等待的特性只有在多处理器中才会体现出来,不过作为驱动开发者,我们不应该来假设驱动的运行环境,或者说都应该假设成运行在多处理器的可抢占系统上。


读写锁

        在并发的方式中有读一读并发、读一写并发和写一写并发三种,很显然,一般的资源的读操作并不会修改它的值(对某些读清零的硬件寄存器除外),因此读和读之间是完全允许并发的。但是使用自旋锁,读操作也会被加锁,从而阻止了另外一个读操作。为了提高并发的效率,必须要降低锁的粒度,以允许读和读之间的并发。为此,内核提供了一种允许读和读并发的锁,叫读写锁,其数据类型为rwlock_t,常用的API如下。

rwlock_init (lock)

read_trylock(lock)
write_trylock(lock)

read_lock(lock)
write_lock(lock)

read_lock_irq(lock)
read_lock_irgsave(lock, flags)
read_lock_bh(lock)

write_lock_irq(lock)
write_lock_irqsave(lock, flags)
write_lock_bh(lock)

read_unlock(lock)
write_unlock(lock)

read_unlock_irq(lock)
read_unlock_irqrestore(lock, flags)
read_unlock_bh(lock)

write_unlock_irg(lock)
write_unlock_irqrestore(lock, flags)
write_unlock_bh(lock)



有了前面对自旋锁的了解,相信大家都能知道这些宏的含义,下面是一个应用例子
 

int i = 5;
unsigned long flags;
rwlock_t lock;

/*使用之前先初始化读写锁*/
rwlock_init(&lock);

/*要改变变量的值之前获取写锁*/
write_lock_irqsave(&lock, flags);
i++;
write_unlock_irqrestore(&lock, flags);

int V;
/*只是获取变量的值先获得读锁 */
read_lock_irqsave(&lock, flags);
v = i;
read_unlock_irqrestore(&lock, flags);


        读写锁的使用也需经历定义、初始化、加锁和解锁的过程,只是要改变变量的值需先获取写锁,值改变完成后再解除写锁,读操作则用读锁。这样,当一个内核执行路径在获取变量的值时,如果有另一条执行路径也要来获取变量的值,则读锁可以正常获得,从而另一条路径也能获取变量的值。但如果有一个写在进行,那不管是写锁还是读锁都不能获得,只有当写锁释放了之后才可以。很明显,使用读写锁降低了锁的粒度,即对锁的控制更加精细了,从而获得了更高的并发性,带来了更高的工作效率。


顺序锁


        自旋锁不允许读和读之间的并发,读写锁则更进了一步,允许读和读之间的并发,顺序锁又更进了一步,允许读和写之间的并发。为了实现这一需求,顺序锁在读时不上锁,也就意味着在读的期间允许写,但是在读之前需要先读取一个顺序值,读操作完成后,再次读取顺序值,如果两者相等,说明在读的过程中没有发生过写操作,否则要重新读取。显然,写操作要上锁,并且要更新顺序值。顺序锁特别适合读很多而写比较少的场合,否则由于反复的读操作,也不一定能够获取较高的效率。顺序锁的数据类型是seqlock_t,其类型定义如下。
 

typedef struct (
    struct seqcount seqcount;
    spinlock_t lock;
} seglock_t;


        很显然,顺序锁使用了自旋锁的机制,并且有一个顺序值 seqcount。顺序锁的主要API如下。


 

seqlock_init(x)
unsigned read_seqbegin(const seqlock_t *sl);
unsigned read_seqretry(const seqlock_t *sl, unsigned start);
void write_seqlock(seqlock_t *sl);
void write_sequnlock(seqlock_t *s1);
void write_seqlock_bh(seqlock_t *sl);
void write_sequnlock_bh(seqlock_t *s1);
void write_seqlock_irq(seqlock_t *sl);
void write_sequnlock_irq(seqlock_t *sl);
write_seqlock_irqsave(lock,flags)
void write_sequnlock_irqrestore(seqlock_t *sl, unsigned long flags);


        seqlock_init:初始化顺序锁。
        read_seqbegin:读之前获取顺序值,函数返回顺序值。
        read_seqretry:读之后验证顺序值是否发生了变化,返回1表示需要重读,返回0麦示读成功。
        write_seqlock:写之前加锁,其他的变体请参照自旋锁。
        write_sequnlock:写之后解锁,其他的变体请参照自旋锁。
        在i++的例子中,我们可以使用下面的代码来使顺序锁对i的操作进行互斥.
 

int i=5;
unsigned long flags;

/*定义顺序锁*/
seqlock_t lock;
/*使用之前必须初始化顺序锁 */
seqlock_init(&lock);

int v;
unsigned start;
do {
    /* 读之前要先获取顺序值 */
    start = read_seqbegin(&lock);
    v = i;
    /* 读完之后检查顺序值是否发生了变化,如果是,则要重读 */
} while (read_seqretry(&lock,start));

/* 写之前获取顺序锁 */
write_seqlock_irqsave(&lock,flags);
i++;
/*写完后释放顺序锁*/
write_sequnlock_irqrestore(&lock,flags);

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宇努力学习

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值