Linux并发控制技术

Linux并发控制技术,它的目标是让多个进程访问同一个资源克服其竟态。由现代操作系统都是并发的,例如对同一个资源的读写是并发进行的。为了解决读写的不同步问题,LINUX操作系统引入并发控制技术。这个技术包括如下实现方式:

A:原子操作

B:自旋锁

C:RCU

D:信号量

E:互斥体

F:完成量

 

A:原子操作

原子故名思意是不可分割的最小单位(现代物理学发是可以分的,我们估计当作不可分来理解问题),对于软件来说不可分就意味着在执行过程中不能被中断。LINUX操作系统分3种类型的原车类型:

/*多字节原子操作,就是对一个基本变量类型的改变是不可被打断的,它的实现原理是采用定义好的函数来操作变量,那说明这些函数对这些变量的操作就是原子的,以写atomic64_add为例来分析:

static inline void atomic64_add(u64 i, atomic64_t *v)对变量v指针的值加i

{

u64 result;

unsigned long tmp;

__asm__ __volatile__("@ atomic64_add\n"

"1: ldrexd %0, %H0, [%3]\n"

" adds %0, %0, %4\n"

" adc %H0, %H0, %H4\n"

" strexd %1, %0, %H0, [%3]\n"

" teq %1, #0\n"

" bne 1b"

: "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)

: "r" (&v->counter), "r" (i)

: "cc");

}

 

Ldrexd  strexd 由编译器确定当ARM核在执行这两个指令间的程序时是原子的,不可被中断的,首先读取v中的数据然后写入数据,不可中断,因此保证了这个变量v指向的内容变化是原子的。既这个变量的变化总是实时的,没有中间态。由编译器原理可知一条C代码会编译成多条汇编指令。原子性就是一条C指令的执行保证一次性完成。

*/

typedef struct {

int counter;//4BYTE

} atomic_t;

typedef struct {

long long counter;//8BYTE

} atomic64_t;

/*位原子操作,对传入的地址P中的某位的操作是原子,就是改变某一位是不可以被打断的*/

#define set_bit(nr,p) ATOMIC_BITOP(set_bit,nr,p)

#define clear_bit(nr,p) ATOMIC_BITOP(clear_bit,nr,p)

#define change_bit(nr,p) ATOMIC_BITOP(change_bit,nr,p)

#define test_and_set_bit(nr,p) ATOMIC_BITOP(test_and_set_bit,nr,p)

#define test_and_clear_bit(nr,p) ATOMIC_BITOP(test_and_clear_bit,nr,p)

#define test_and_change_bit(nr,p) ATOMIC_BITOP(test_and_change_bit,nr,p)

set_bit的实现和上面介绍的字节型原子操作原理是一样的。都是Ldrexd  strexd这两条指令完成读写,这其间的代码是不可被打断。保证变量p指向地址的位是原子变化 的。举例:

#define set_bit(nr,p) ATOMIC_BITOP(set_bit,nr,p)

#define ATOMIC_BITOP(name,nr,p) _##name(nr,p)

_set_bit

ENTRY(_set_bit)

bitop orr

ENDPROC(_set_bit)

.macro bitop, instr

ands ip, r1, #3

strneb r1, [ip] @ assert word-aligned

mov r2, #1

and r3, r0, #31 @ Get bit offset

mov r0, r0, lsr #5

add r1, r1, r0, lsl #2 @ Get word offset

mov r3, r2, lsl r3

1: ldrex r2, [r1]

\instr r2, r2, r3

strex r0, r2, [r1]

cmp r0, #0

bne 1b

bx lr

.endm

 

B:自旋锁

自旋锁从本质上讲就是保证代码片段(也称为临界区)的操作是原子的。配合上节原子操作的原理理解为,这个片段在执行时不允许其它进程再次仿问这个片段。仿问之前要先获取自旋锁,然后再执行,最后释放锁。如果获取不到锁,既已有其它进程占用了锁则这些正在申请自旋锁的进程会在一个小循环(FOR循环实现)里不断的扫描自旋锁。直到获得锁。因此对于小片段用自旋锁是可以的,如果代码量过长不建议用这个,这个由于不断FOR循环,进程调度器还会一直调度它,但是又不执行有用的过程,就浪费了时间。

一般使用流程:

Spinlock_t lock;

Spin_lock_jinit(&lock)

Spin_lock(&lock)

Spin_unlock(&lock)

下面来分析Spin_lock(&lock)原理。

static inline void spin_lock(spinlock_t *lock)

{

raw_spin_lock(&lock->rlock);

}

#define raw_spin_lock(lock) _raw_spin_lock(lock)

#define _raw_spin_lock(lock) __LOCK(lock)

#define __LOCK(lock) \

  do { preempt_disable(); __acquire(lock); (void)(lock); } while (0)

 

#define preempt_disable() \

do { \

inc_preempt_count(); \ //关闭内核抢占,当barrier之后的代码正在执行时,并且获得了锁,但并没有释放的情况,发生了中断,当中断返回时有一个进程调度点,如果这个时候有一个更高优化级的进程要调用当前锁内处码,由执行一样的流程进入来获取锁,但获取不了就一直自旋FOR循环,但前一个进程获取了自旋锁并且由于优化低被换出了CPU休眠态了。那此时前一个进程一直醒不了,当前进程又获取不了锁,这样就可能导致死锁了,由于切出的进程也有可能切入CPU的所以是可以导致死锁,要看调度器当前由多少进程可执行的优化级。死锁的情况有可能出现。所以要关闭抢占,锁取了锁的程序还是要能得到继续执行。

barrier(); \//内存屏障,(barrier只在编译阶段有效,不生成任代码,他的作用是:内存信息已经修改,在这条指令后面的寄存器的值必须从内存中重新获取,既不用寄存器的值由编译自动确定的寄存器。代码的先后顺序必须按照原有的产生汇编代码,Linux 内核提供函数 barrier() 用于让编译器保证其之前的内存访问先于其之后的完成),保证这条指令前面的代码按原指令生成汇编,并且不用寄存器和缓存中的值,直接操作内存

//*mb*保证这个之前的代码在这个后的代码之前执行。它是hardware barrier 和compiler barrier的组合,有上面barrier的功能,同时他也有只能有一个CPU占用当前总线,既发出LOCK信号,占用HOST总线,同时其它的CPU会监听总线。既保证*mb*后指令按照先后顺序执行,并且在一个CPU内按照编译生成的指令顺序执行,而不是在由CPU并行执行。防止指令乱序,乱序产生的原因,如果一个变量全景变量X在存在由两个进程调用的情况,如果A进程调它并赋之为0,则A进程后的代码对A的操作就应该是0,如果没有加内在BARRIER则存在这种可能,X也会由B进程调用,并且赋值了2.同于进程是并行执行的,则可能A输出是2了,这样就导致A进程得到的值不是自己赋的值,这就是乱序。防止这种情况发生就需要在A赋值语句和使用X语句中间加入*MB*,表示X前后的语句独占总线,此时其它进程不能操作运行指令。LINUX操作系统支持SMP处理器,每个CPU并行运行进程,由系统LINUX设计的调度策略。多核CPU在硬件上会通过一定的技术来保证CACHE中的数据的一致性。多枋都有自己独立的CACHE 和寄存器  看门狗 定时器等。但只要把数据写入内存时就会保证各个CAHCE是一致的,否则对内存的操作就乱套了。

http://www.cnblogs.com/liaokang/p/5614748.html//barrier *mb*介绍

} while (0)

# define __acquire(x) __context__(x,1)//里面是FOR循环判断变量是否还可以获取,既变量是否为0,如果不为0则一直FOR循环。调度器一个可以调度它,又没有有效的指令可以执行。就有点浪费资源了。

 

读写锁:

它是自锁的升级版,自旋锁不管读或写都只能有一个进程操作这个锁变量。但量如果定义的锁变量读写锁,则可以多个进程可以同时获得读锁,但只有一个进进程能获得写锁,同时读锁和写锁是不能同时获取的。它的实现变量就是根据获取时对锁变量中的读或写锁变量赋值,在获取时会去判断这些值。

rwlock_init //定义读写锁

#define write_lock(lock) _raw_write_lock(lock) //获取写锁

do_raw_write_lock//和读原理一样。

/*

static inline void arch_write_lock(arch_rwlock_t *rw)

{

unsigned long tmp;

__asm__ __volatile__(

"1: ldrex %0, [%1]\n"

" teq %0, #0\n"

WFE("ne")

" strexeq %0, %2, [%1]\n"//ldrex  strexeq之间的代码是原子的,相当对锁的操作只能原子的。

" teq %0, #0\n"

" bne 1b""//如果获取不到相关锁就一直循环

: "=&r" (tmp)

: "r" (&rw->lock), "r" (0x80000000)

: "cc");

 

smp_mb();

}

 

*/

#define read_lock(lock) _raw_read_lock(lock)//获取读锁

do_raw_read_lock

/*

static inline void arch_read_lock(arch_rwlock_t *rw)

{

unsigned long tmp, tmp2;

__asm__ __volatile__(

"1: ldrex %0, [%2]\n"

" adds %0, %0, #1\n"

" strexpl %1, %0, [%2]\n" //ldrex   strexpl之间的代码是原子的,相当对锁的操作只能原子的。

WFE("mi")

" rsbpls %0, %1, #0\n"

" bmi 1b"//如果获取不到相关锁就一直循环

: "=&r" (tmp), "=&r" (tmp2)

: "r" (&rw->lock)

: "cc");

 

smp_mb();

}

*/

和自旋锁一样如果获取不到相关锁就会一直FOR循环不退出。

 

//http://blog.sina.com.cn/s/blog_6d7fa49b01014q86.html

 

顺序锁:

顺序锁与读写锁类似,只是为写锁赋予了更高的权限。在读写自旋锁中,读锁和写锁的优先级是相同的,当读锁获取自旋锁时,写锁必须等待,直到临界区的代码执行完成,并释放自旋锁为止,反之变然。而顺序锁在获取读锁的时候,任然可以获取写锁,并继续执行临区中的代码。也就是说,写锁永远不会被阻塞(写锁任然可以被写锁阻塞)

write_seqlock

write_sequnlock

read_seqbegin

read_seqretry

 

兴趣交流群抠抠: 461283592

 

C:RCU

Read_Copy_Update它也可以同样看成rw_lock spin_lock的升级版,与seqrw_lock区别是前者有原子操作可能会导致流水线停滞,有指令重排序等问题,而后者在读上面完全没有延迟和正常代码一样,只是写锁时前者会去直接运行可以导致读锁一直等等,而后者在写时会复制数据出来操作它,而不影响读数据的进行,待读完成后才把数据指针指向新数写的数据。同时两者的写不可以同时写,当然RCU可以设置成有多个写同时进行。

 

兴趣交流群抠抠: 461283592

 

D:信号量

Semaphoer 信号量是用于保护临界区的一种常用方法,它的使用方式与自旋锁类似,与自旋相同,只是得到信号量的进程才能执行临界区代码。但与自旋锁不同的,在未获取信号量,进程不会像自旋锁一样原地打转,而是进入休眠等待状态。因此当信号量阻赛时消耗的系统资源(主要是CPU资源)并不多,也不会出死机现像。内核中的关于信号实现的原代码没看懂,以后有机会再去查找,但一定要抓住一个核心思想就是:关于自旋锁结构体的中变量count代表可以有几个进程可以仿问它的变化和判断一定是原子的,否则信号量在多核CPU或多进程调度上应该是很难实现的。原子的变化和判断才能确保临界区代码的保护是正常的。常用方法和API:

/* Please don't access any members of this structure directly */

struct semaphore {

spinlock_t lock;//应该是用于COUNT变量的变化原子性,实现代码没看懂。

unsigned int count;//它的变化和判断是原子的

struct list_head wait_list;//对于该信号量的访问的等待休眠进程表

};

sema_init

Down

down_interruptible

down_killable

down_trylock

down_timeout

Up

 

读写信号量:

读写信号量和信号量的关系与读写自旋锁和自旋锁的关系类似。读信号量和写信号量的互斥的,但允许N个读执行单独同时访问共享资源(同时获取读信号量),而最多只允许有一个写单元获取写信号量。因此,读写信号量相对于信号量更宽松。对于读多写少的情况会明显提高 程序的执行效率。(这个内核源码没看懂,有机会请教高人),下面列出常用函数。

struct rw_semaphore {

long count;

spinlock_t wait_lock;

struct list_head wait_list;

#ifdef CONFIG_DEBUG_LOCK_ALLOC

struct lockdep_map dep_map;

#endif

};

init_rwsem

down_read

down_write

down_write_trylock

down_read_trylock

up_read

up_write

 

兴趣交流群抠抠: 461283592

 

 

 

E:互斥体

尽管信号量已经可以实现互斥的功能,而且还包含了DECLARE_MUTEX,init_MUTEX等定义信号量的宏和函数。从名字上看也体现了互斥的概念,但LINUX内核仍然直接提供了直接的互斥体。真正实现的原理是确保某变量的变化是原子的,然后根据这个变量的原子性来实现更复杂的互斥功能。从变量的原子到木段代码的原子性是实现复杂竟态访问不出现问题的真实原理。关于这部分的LINUX真实代码没有看懂  暂时不去看。当不能获取互斥时访问内核这部分代码的进程就休眠和信号量以及读写信息号一样。不像旋转锁那样一直原地旋转可以节省CPU。

struct mutex {

/* 1: unlocked, 0: locked, negative: locked, possible waiters */

atomic_t count;

spinlock_t wait_lock;

struct list_head wait_list;

#if defined(CONFIG_DEBUG_MUTEXES) || defined(CONFIG_SMP)

struct task_struct *owner;

#endif

#ifdef CONFIG_DEBUG_MUTEXES

const char *name;

void *magic;

#endif

#ifdef CONFIG_DEBUG_LOCK_ALLOC

struct lockdep_map dep_map;

#endif

};

 

mutex_init

mutex_trylock

mutex_lock

mutex_lock_interruptible

mutex_lock_killable

mutex_unlock

 

兴趣交流群抠抠: 461283592

 

 

F:完成量

完成量用于一个执行单元等待另一个执行单元执行完成某项工作,也就是说,如果在执行某段代码之前必须要执行另一项代码,就要使用完成量。

struct completion {

unsigned int done;

wait_queue_head_t wait;

};

DECLARE_COMPLETION

init_completion //done=0

 

-wait_for_completion-do_wait_for_common

do_wait_for_common函数会判断done,如果它为0则进入休眠状态。schedule_timeout(timeout);同时把它加入等待队列wait_queue_head_t wait;。一直等待完成量来唤醒,当唤醒时x->done--;会自减1.

Complete唤醒函数。

x->done++;会首先把它加1

__wake_up_common(&x->wait, TASK_NORMAL, 1, 0, NULL);会去唤醒等待队列的中等待的任务。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值