Linux内核同步

linux混混之牢骚:

 人生就像曾轶可,要是一开始就跑偏就再也跑不回来了

linux中的并发:

什么是并发?就是在多处理器(MP:multiple processor)的平台上,多个线程在不同的处理器上同时运行。或者是单处理器上,会有进程调度,从宏观上来看,也是一种并发。

并发坏处:当多个进程同时访问一个全局变量时,就会造成这个变量混乱。(当然要同时写,或同时有写有读。同时读应该不会造成混乱)。所以需要一种机制来控制不让他们同时访问一个变量。

 

机制:

1.原子操作

2.自旋锁

3.读--拷贝---更新  (RCU:read copy update)

4.信号量(互斥锁)

5.禁止本地中断

6.禁止软中断

 

1.原子操作

原子操作是将对一个变量的操作设置成原子操作,这样单单的一次操作中间不会被打断。

效果:  这样就防止了同时对一个数据操作造成混乱,如:同时读和写。读操作一次性就完成了,写操作也是一次性就完成了。所以 读操作 要不在写操作的前面(读到的是写之前的数据),要不在写操作的后面(读到的是写之后的数据)。但绝对不会在写操作的中间进行读,读出来的数据也是不之前,也不是之后。 

不足:  但是这样只能保证对单独一次操作的正确性。有个术语叫:临界区。即在整整一段程序区域内,这个变量都不能被其他操作修改。原子操作不可能将整整一代码都变成原子型的,一次操作完成。所以在临界区这个地方不适用原子操作。

原子操作的函数:

atomic_t  a; //定义一个原子变量
#define atomic_read(v)	((v)->counter)               //原子变量的读操作
#define atomic_set(v,i)	(((v)->counter) = (i))       //原子变量的赋值 
#define atomic_inc_not_zero(v) atomic_add_unless((v), 1, 0)

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

#define atomic_inc_and_test(v)	(atomic_add_return(1, v) == 0)
#define atomic_dec_and_test(v)	(atomic_sub_return(1, v) == 0)
#define atomic_inc_return(v)    (atomic_add_return(1, v))
#define atomic_dec_return(v)    (atomic_sub_return(1, v))
#define atomic_sub_and_test(i, v) (atomic_sub_return(i, v) == 0)

#define atomic_add_negative(i,v) (atomic_add_return(i, v) < 0)


以上是原子变量的各种操作。在arch/arm/include/asm/atomic.h中,还有其他的操作,可对应了解。

2.自旋锁

鉴于上面原子变量的缺点-无法保护临界区。 自旋锁可以保护临界区,在临界区的入口处,上锁。在出口处,解锁。 这样,当一个进程想要修改这个临界区的的变量的时候,也要进行上锁,在这个进程上锁的过程中,发现已经有进程访问了,这个进行就会不断的进行循环,check是否得到锁的进程已经解锁(注意是一直循环,这个应该是在SMP系统上的,在单CPU中,就不会循环check了)。

下面来看如何上锁:

static inline void spin_lock(spinlock_t *lock)
{
	raw_spin_lock(&lock->rlock);
}
#define raw_spin_lock(lock) _raw_spin_lock(lock)

在单CPU系统中:

#define _raw_spin_lock(lock)			__LOCK(lock)
#define __LOCK(lock) \
  do { preempt_disable(); __acquire(lock); (void)(lock); } while (0)
# define __acquires(x)	__attribute__((context(x,0,1)))

可以看出,在单CPU系统中,spin_lock仅仅调用了preempt_disable()。 所以在单cpu系统中,自旋锁只是在spin_lock中禁止了内核抢占,在spin_unlock中,允许了内核抢占。

其实,在单CPU中,禁止了内核抢占,也就相当于 没有其他进程可以运行了(当前进程在内核态运行),消除了其他进程修改临界区变量的可能性。

但是,只是禁止了内核抢占!!!CPU依然可以中断,依然可以在中断中对临界区变量进行修改。所以,如果临界区变量在中断会访问的话,那么 自旋锁是无效的。只能通过 禁止本地中断来解决。 (这是在单系统中的讨论)

 

多CPU系统中:

void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{
	__raw_spin_lock(lock);
}
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
	preempt_disable();
	spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
	LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

里面代码调用太多了,网上查了下资料:大概就是 在多CPU中,先禁止内核抢占,然后在进行lock的过程中,发现已经被lock住了,则会不断的循环等待。直到持有lock的那个进程,unlock之后。这个循环的进程才会进行lock然后继续运行。

当然:在SMP中,仍然有可能在中断中对lock的临界区进行修改,所以需要 禁止本地中断(在SMP中,禁止本地中断,只能禁止一个CPU的中断),然后再进行lock。具体为什么可以参考:《深入理解linux内核》的第五章,中文书:221页。

 

由以上分析

在(uniprocessor,UP)中,自旋锁只是进行了一下,禁止内核调度的工作。基本上不会出现,等待的现象(也不可能等待,如果要是等待到话,又禁止内核抢占,那么持有锁的进程也不可能释放锁,除非等到这个进程的时间片用完,主动进行调度,但这样系统会不会出问题呢?反正也是不存在的事,不多想。。。)。但是禁止内核调度会影响内核的实时性,lock住的 区域要尽可能的小。

在SMP中,自旋锁会禁止内核抢占,然后去获得锁,当无法获得时候,会不断的循环check是否已经释放了该锁。所以:在SMP中,也要让lock住的区域尽可能的小,减少其他进程等待lock的时间。但是另一方面,等待锁的过程中不断的循环,是比较浪费CPU资源的,但是如果另一个进程能够很快的释放锁的话,那么等待的时间也不会太多,并且还增加了效率(相比较信号量,不需要进程睡眠啊,调度啊,唤醒啊,等等的操作)。

 

自旋锁的操作:

spin_lock_init();  //初始化自旋锁,初始化后为 未上锁的 状态
spin_lock();       //上锁,循环等待直到变成 未上锁的状态,然后,进行上锁 
spin_unlock();     //解锁
spin_unlock_wait(); //等待,直到变成 未上锁的状态,才返回
spin_is_locked();   //测试是否上锁,上锁--返回1,未上锁--返回0
spin_trylock();     //试着去上锁,但不等待

 

自旋锁是一个统称,在linux系统中繁衍了很多各种各样的自旋锁和自旋锁函数,他们各有用途。但本意是不变的---循环等待,禁止内核抢占!

 下面介绍一下其他的自旋锁:

 读写自旋锁:犹如文章一开始讲到的,当几个进程同时去读一个变量的时候,对这个变量是没有影响的。读写锁基于这个理论。这个模型有趣的地方在于允许多个线程同时访问相同数据,但同一时刻只允许一个线程写入数据。如果执行写操作的线程持有此锁,则临界段不能由其他线程读取。如果一个执行读操作的线程持有此锁,那么多个读线程都可以进入临界段。

读写锁的操作函数:

rwlock_t my_rwlock;          //定义一个读写锁

rwlock_init( &my_rwlock );    //初始化

write_lock( &my_rwlock );     //上写锁,其他进程都无法进入

// critical section -- can read and write

write_unlock( &my_rwlock );    //解写锁


read_lock( &my_rwlock );        //上读锁,当有其他进程也 进行 读上锁时候,可以不需要等待

// critical section -- can read only

read_unlock( &my_rwlock );      
 


顺序锁:相比较 读写锁  顺序锁提供了让写锁操作也不需要 循环等到。即:当有进程获得了读锁,在读的时候,有进程要过的写的锁,也不需要等待可获得写锁,进行写操作。 但是在读的操作中,需要读两次,看两次是否相同,如果相同的话,才有效,不相同的话就要再读(当然在有写操作进入时候,才需要读两次)。具体可参照:《深入理解linux内核》第五章 中文209页。

具体操作:

typedef struct {
	unsigned sequence;
	spinlock_t lock;
} seqlock_t;         //顺序锁的结构体
static inline void write_seqlock(seqlock_t *sl)
static inline void write_sequnlock(seqlock_t *sl)
static inline int write_tryseqlock(seqlock_t *sl)
/* Start of read calculation -- fetch last complete writer token */
static __always_inline unsigned read_seqbegin(const seqlock_t *sl)
/*
 * Test if reader processed invalid data.
 *
 * If sequence value changed then writer changed data while in section.
 */
static __always_inline int read_seqretry(const seqlock_t *sl, unsigned start)

上面是顺序锁的操作,在include/linux/seqlock.h中还有很多顺序所的其他操作,write_seqcount_begin等,了解即可。

 

 但是在linux中,还有自旋锁的函数,他们不仅仅能lock住资源,还能其他的功能,如关闭中断等。。。

spin_lock_irqsave(lock, flags)

  该宏获得自旋锁的同时把标志寄存器的值保存到变量flags中并失效本地中断。

spin_lock_irq(lock)

  该宏类似于spin_lock_irqsave,只是该宏不保存标志寄存器的值。

spin_lock_bh(lock)

  该宏在得到自旋锁的同时失效本地软中断。

spin_unlock(lock)

  该宏释放自旋锁lock,它与spin_trylock或spin_lock配对使用。如果spin_trylock返回假,表明没有获得自旋锁,因此不必使用spin_unlock释放。

spin_unlock_irqrestore(lock, flags)

  该宏释放自旋锁lock的同时,也恢复标志寄存器的值为变量flags保存的值。它与spin_lock_irqsave配对使用。

spin_unlock_irq(lock)

  该宏释放自旋锁lock的同时,也使能本地中断。它与spin_lock_irq配对应用。

spin_unlock_bh(lock)

  该宏释放自旋锁lock的同时,也使能本地的软中断。它与spin_lock_bh配对使用。

spin_trylock_irqsave(lock, flags)

该宏类似于spin_trylock_irqsave,只是该宏不保存标志寄存器。如果该宏获得自旋锁lock,需要使用spin_unlock_irq来释放。

spin_trylock_bh(lock)

  该宏如果获得了自旋锁,它也将失效本地软中断。如果得不到锁,它什么也不做。因此,如果得到了锁,它等同于spin_lock_bh,如果得不到锁,它等同于spin_trylock。如果该宏得到了自旋锁,需要使用spin_unlock_bh来释放。

spin_can_lock(lock)

  该宏用于判断自旋锁lock是否能够被锁,它实际是spin_is_locked取反。如果lock没有被锁,它返回真,否则,返回假。该宏在2.6.11中第一次被定义,在先前的内核中并没有该宏。

  获得自旋锁和释放自旋锁有好几个版本,因此让读者知道在什么样的情况下使用什么版本的获得和释放锁的宏是非常必要的。

  如果被保护的共享资源只在进程上下文访问和软中断上下文访问,那么当在进程上下文访问共享资源时,可能被软中断打断,从而可能进入软中断上下文来对被保护的共享资源访问,因此对于这种情况,对共享资源的访问必须使用spin_lock_bh和spin_unlock_bh来保护。

  当然使用spin_lock_irq和spin_unlock_irq以及spin_lock_irqsave和spin_unlock_irqrestore也可以,它们失效了本地硬中断,失效硬中断隐式地也失效了软中断。但是使用spin_lock_bh和spin_unlock_bh是最恰当的,它比其他两个快。

  如果被保护的共享资源只在进程上下文和tasklet或timer上下文访问,那么应该使用与上面情况相同的获得和释放锁的宏,因为tasklet和timer是用软中断实现的。

  如果被保护的共享资源只在一个tasklet或timer上下文访问,那么不需要任何自旋锁保护,因为同一个tasklet或timer只能在一个CPU上运行,即使是在SMP环境下也是如此。实际上tasklet在调用tasklet_schedule标记其需要被调度时已经把该tasklet绑定到当前CPU,因此同一个tasklet决不可能同时在其他CPU上运行。

  timer也是在其被使用add_timer添加到timer队列中时已经被帮定到当前CPU,所以同一个timer绝不可能运行在其他CPU上。当然同一个tasklet有两个实例同时运行在同一个CPU就更不可能了。

  如果被保护的共享资源只在两个或多个tasklet或timer上下文访问,那么对共享资源的访问仅需要用spin_lock和spin_unlock来保护,不必使用_bh版本,因为当tasklet或timer运行时,不可能有其他tasklet或timer在当前CPU上运行。

  如果被保护的共享资源只在一个软中断(tasklet和timer除外)上下文访问,那么这个共享资源需要用spin_lock和spin_unlock来保护,因为同样的软中断可以同时在不同的CPU上运行。(为什么不会呢?因为他们其实也是个线程,内核线程,是不锁是不允许他们切换的)

  如果被保护的共享资源在两个或多个软中断上下文访问,那么这个共享资源当然更需要用spin_lock和spin_unlock来保护,不同的软中断能够同时在不同的CPU上运行。

  如果被保护的共享资源在软中断(包括tasklet和timer)或进程上下文和硬中断上下文访问,那么在软中断或进程上下文访问期间,可能被硬中断打断,从而进入硬中断上下文对共享资源进行访问,因此,在进程或软中断上下文需要使用spin_lock_irq和spin_unlock_irq来保护对共享资源的访问。

  而在中断处理句柄中使用什么版本,需依情况而定,如果只有一个中断处理句柄访问该共享资源,那么在中断处理句柄中仅需要spin_lock和spin_unlock来保护对共享资源的访问就可以了。

  因为在执行中断处理句柄期间,不可能被同一CPU上的软中断或进程打断。但是如果有不同的中断处理句柄访问该共享资源,那么需要在中断处理句柄中使用spin_lock_irq和spin_unlock_irq来保护对共享资源的访问。

  在使用spin_lock_irq和spin_unlock_irq的情况下,完全可以用spin_lock_irqsave和spin_unlock_irqrestore取代,那具体应该使用哪一个也需要依情况而定,如果可以确信在对共享资源访问前中断是使能的,那么使用spin_lock_irq更好一些。

  因为它比spin_lock_irqsave要快一些,但是如果你不能确定是否中断使能,那么使用spin_lock_irqsave和spin_unlock_irqrestore更好,因为它将恢复访问共享资源前的中断标志而不是直接使能中断。

  当然,有些情况下需要在访问共享资源时必须中断失效,而访问完后必须中断使能,这样的情形使用spin_lock_irq和spin_unlock_irq最好。

  需要特别提醒读者,spin_lock用于阻止在不同CPU上的执行单元对共享资源的同时访问以及不同进程上下文互相抢占导致的对共享资源的非同步访问,而中断失效和软中断失效却是为了阻止在同一CPU上软中断或中断对共享资源的非同步访问。

 

3.读-复制-更新(RCU)

 

读-复制-更新 操作和读写锁的机制是有些类似的。但是读写锁 在写操作时候,如果有读操作在锁的话,写操作要循环等待的。但是RCU即便有读操作是进行,写操作也不会循环等待的。他的具体机制是这样的:当读操作进入临界区的时,会调用rcu_read_lock 和 rcu_read_unlock来表示 进入和退出临界区(不论此刻有没有写操作在进行临界区,读操作都不会等待)。当然在这段时间内,是不允许进程切换的。当写操作进入时候,会先将临界区的变量资源拷贝一份,然后在拷贝的这一份上进行修改。 当读操作都运行完,退出临界区后,rcu_tasklet进程会将写操作的那份copy,更新到源地址处。  这是2.6版本新出来的锁机制,效率是比较高的。

RCU使用的API函数:

表 10  所有RCU API使用情况总汇
rcu_read_lock 和 rcu_read_unlock 是读操作时候,进入和出来的时候调用的函数,会调用preempt_disable 和 preempt_enable。

rcu_read_lock_bh 和 rcu_read_unlock_bh 也是毒操作时候,进入和出来时候调用的函数,不过会调用 local_bh_disable()和local_bh_enable()

call_rcu 和call_rcu_bh 是回调函数。会在写后,将数据update到元数据中。被 rcu_tasklet调用。

具体可参数:《深入理解linux内核》第五章,中文211页。和http://www.ibm.com/developerworks/cn/linux/l-rcu/index.html IBM资料中的RCU解释。

 

4.信号量(互斥锁)

信号量在创建时需要设置一个初始值,表示同时可以有几个任务可以访问该信号量保护的共享资源,初始值为1就变成互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。一个任务要想访问共享资源,首先必须得到信号量,获取信号量的操作将把信号量的值减1,若当前信号量的值为负数,表明无法获得信号量,该任务必须挂起在该信号量的等待队列等待该信号量可用;若当前信号量的值为非负数,表示可以获得信号量,因而可以立刻访问被该信号量保护的共享资源。当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果信号量的值为非正数,表明有任务等待当前信号量,因此它也唤醒所有等待该信号量的任务。

即:初始值为1的信号量就是互斥锁。信号量使用在单系统中,是支持内核抢占的,当无法获取信号量时,就会挂起进程,直到信号量释放,才唤醒。所以 相比较 自旋锁,信号量不会降低系统实时性,当然,会挂起进程,还需要等待唤醒,唤醒就需要在任务调度中,任务的唤醒会有延迟。 但自旋锁 会自循环,所以不存在唤醒。所以,当临界资源很少,占用时间很段的时候,用自旋锁是明智的。但是临界区很长,用自旋锁会明显影响实时性。

 

信号量的API有:


DECLARE_MUTEX(name)

该宏声明一个信号量name并初始化它的值为0,即声明一个互斥锁。


DECLARE_MUTEX_LOCKED(name)

该宏声明一个互斥锁name,但把它的初始值设置为0,即锁在创建时就处在已锁状态。因此对于这种锁,一般是先释放后获得。


void sema_init (struct semaphore *sem, int val);

该函用于数初始化设置信号量的初值,它设置信号量sem的值为val。


void init_MUTEX (struct semaphore *sem);

该函数用于初始化一个互斥锁,即它把信号量sem的值设置为1。


void init_MUTEX_LOCKED (struct semaphore *sem);

该函数也用于初始化一个互斥锁,但它把信号量sem的值设置为0,即一开始就处在已锁状态。


void down(struct semaphore * sem);

该函数用于获得信号量sem,它会导致睡眠,因此不能在中断上下文(包括IRQ上下文和softirq上下文)使用该函数。该函数将把sem的值减1,如果信号量sem的值非负,就直接返回,否则调用者将被挂起,直到别的任务释放该信号量才能继续运行。


int down_interruptible(struct semaphore * sem);

该函数功能与down相似,不同之处为,down不会被信号(signal)打断,但down_interruptible能被信号打断,因此该函数有返回值来区分是正常返回还是被信号中断,如果返回0,表示获得信号量正常返回,如果被信号打断,返回-EINTR。


int down_trylock(struct semaphore * sem);

该函数试着获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0,否则,表示不能获得信号量sem,返回值为非0值。因此,它不会导致调用者睡眠,可以在中断上下文使用。


void up(struct semaphore * sem);

该函数释放信号量sem,即把sem的值加1,如果sem的值为非正数,表明有任务等待该信号量,因此唤醒这些等待者。

 

 

读写信号量:

读写信号量 相比较 于信号量  就好像 自旋锁相比较与 读写自旋锁。  允许多次读同时进如临界区。

 

DECLARE_RWSEM(name)

该宏声明一个读写信号量name并对其进行初始化。


void init_rwsem(struct rw_semaphore *sem);

该函数对读写信号量sem进行初始化。


void down_read(struct rw_semaphore *sem);

读者调用该函数来得到读写信号量sem。该函数会导致调用者睡眠,因此只能在进程上下文使用。


int down_read_trylock(struct rw_semaphore *sem);

该函数类似于down_read,只是它不会导致调用者睡眠。它尽力得到读写信号量sem,如果能够立即得到,它就得到该读写信号量,并且返回1,否则表示不能立刻得到该信号量,返回0。因此,它也可以在中断上下文使用。


void down_write(struct rw_semaphore *sem);

写者使用该函数来得到读写信号量sem,它也会导致调用者睡眠,因此只能在进程上下文使用。


int down_write_trylock(struct rw_semaphore *sem);

该函数类似于down_write,只是它不会导致调用者睡眠。该函数尽力得到读写信号量,如果能够立刻获得,就获得该读写信号量并且返回1,否则表示无法立刻获得,返回0。它可以在中断上下文使用。


void up_read(struct rw_semaphore *sem);

读者使用该函数释放读写信号量sem。它与down_read或down_read_trylock配对使用。如果down_read_trylock返回0,不需要调用up_read来释放读写信号量,因为根本就没有获得信号量。


void up_write(struct rw_semaphore *sem);

写者调用该函数释放信号量sem。它与down_write或down_write_trylock配对使用。如果down_write_trylock返回0,不需要调用up_write,因为返回0表示没有获得该读写信号量。


void downgrade_write(struct rw_semaphore *sem);

该函数用于把写者降级为读者,这有时是必要的。因为写者是排他性的,因此在写者保持读写信号量期间,任何读者或写者都将无法访问该读写信号量保护的共享资源,对于那些当前条件下不需要写访问的写者,降级为读者将,使得等待访问的读者能够立刻访问,从而增加了并发性,提高了效率。

 

completion:

这是一个和信号量一样的机制。

使用 complete()来释放,使用wait_for_completion()来获取。 在获取中,也会进行挂起。

 

 

禁止中断:

禁止本地中断:local_irq_disable()  local_irq_enable();   local_irq_save(); local_irq_restore();

禁止软中断:local_bh_disable(); local_bh_enable();

 

 下面是IBM写的linux同步分析:

http://www.ibm.com/developerworks/cn/linux/l-synch/part1/index.html

http://www.ibm.com/developerworks/cn/linux/l-synch/part2/index.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值