并发与竞态

多个执行单元同时、并行的被执行称为并发,而并发对共享资源(临界区)的访问很容易引起竞态,避免竞态的方法主要有两种,信号量和自旋锁。

1. 信号量
信号量,使用信号量需要包含结构体<linux/semaphore.h>,信号量结构定义如下:
struct semaphore {
	spinlock_t		lock;
	unsigned int		count;
	struct list_head	wait_list;
};
1.1 信号量的初始化
信号量的初始化使用函数sem_init,原型如下:
void sema_init(struct semaphore *sem, int val);
其中参数val用于初始化信号量结构中的count值,即信号量的初值。

1.2 互斥信号量
如果信号量只有1和0这两个值,即count最大值为1,那么这种信号量称为互斥(mutex)信号量,定义一个互斥信号量可以使用宏DECLARE_MUTEX,该宏用于静态定义一个互斥信号量并对信号量做初始化,其中信号量的count值初始化为1,当然也可以自己定义一个信号量,然后使用宏init_MUTEX对其做初始化,效果同前面是一样的,而宏init_MUTEX_LOCKED也是对互斥信号量做初始化,只是将信号量中的count值初始化为0,即一开始就是锁住的。

1.3 信号量中的P、V操作
在Linux内核中,P操作使用down函数,函数有:
void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
down操作用于对信号量中的count值做减1操作,其中down函数有可能会导致进程睡眠,即对临界资源访问时,首先是判断该临界资源是否能够被访问(判断信号量的count值),如果不能对其进行访问(count值为0),那么将导致进程睡眠。而down_interruptible函数同down函数类似,只是使用down_interruptible函数会将进程设置成TASK_INTERRUPTIBLE状态,意味着该进程可以被其它信号唤醒,如果该进程在睡眠过程中接收到了信号,那么该进程就会被唤醒,而down_interruptible函数返回一个非0值。而down_trylock函数不会引起进程的睡眠,如果临界资源不可被访问,down_trylock函数会立即返回,返回一个非0值,而如果可以被访问,则返回0值。

V操作使用up函数,函数原型为:
void up(struct semaphore *sem);
down操作是对信号量中的count值做减1操作,那么up操作就是对count值做加1操作,即释放对临界区的控制权,同时唤醒被睡眠的进程,从信号量结构中可以看到有个链表wait_list,那么被睡眠的进程就会被添加到这条链表上,那么在执行up操作时就从这条链表上去获取被睡眠的进程。

1.4 信号量实例(来自Linux Kernel Development)
/* define and declare a semaphore, named mr_sem, with a count of one */
static DECLARE_MUTEX(mr_sem);
/* attempt to acquire the semaphore ... */
if (down_interruptible(&mr_sem)) {
	/* signal received, semaphore not acquired ... */
}
/* critical region ... */
/* release the given semaphore */
up(&mr_sem);


2. 读写信号量
普通信号量对所有调用者执行互斥操作,而不管调用者是读还是写操作,而有的时候只需要对写执行互斥操作,而对读则没有限制,这样可以大大提高性能。
使用读写信号量需要包含头文件<linux/rwsem.h>,读写信号量结构定义如下:
struct rw_semaphore {
	signed long             count;
	spinlock_t              wait_lock;
	struct list_head        wait_list;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
	struct lockdep_map dep_map;
#endif
};

2.1 读写信号量初始化
初始化使用函数init_sem,原型如下:
void init_rwsem(struct rw_semaphore *sem);
当然也可以使用宏DECLARE_RWSEM静态定义一个读写信号量并对其做初始化。

2.2 读写信号量的P、V操作
read操作有:
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);

write操作有:
void down_write(struct rw_semaphore *sem);
down_write_trylock(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);
void downgrade_write(struct rw_semaphore *sem);

读写信号量只允许一个写入者或多个读取者拥有该信号量,但是写信号量拥有更高的优先级,即在执行写操作时,读操作是不允许的。当写操作完成之后需要调用downgrade_write,来允许读取者的访问。


3. 自旋锁
自旋锁只有两种状态,即“锁住”和“解锁”,那为什么叫自旋锁,同信号量有什么区别?自旋锁和信号量都用于对临界区的互斥访问,但是信号量在临界区不能够被访问时,会引起调用者的睡眠,而自旋锁则一直循环在这里等待被解锁,所以形象的被称为自旋锁,所以信号量的特点是睡眠,自旋锁的特点是忙等。
使用自旋锁需要包含头文件<linux/spinlock.h>

3.1 自旋锁定义与初始化
初始化使用spin_lock_init,原型如下:
void spin_lock_init(spinlock_t *lock);
或者使用宏SPIN_LOCK_UNLOCKED在编译时就进行初始化,例如:
spinlock_t my_lock = SPIN_LOCK_UNLOCKED;
或者使用宏DEFINE_SPINLOCK静态定义一个自旋锁并对其做初始化,注意初始化都是将其初始化成解锁状态。

3.2 自旋锁解锁与加锁
加锁函数有:
void spin_lock(spinlock_t *lock);
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
void spin_lock_irq(spinlock_t *lock);
void spin_lock_bh(spinlock_t *lock);
int spin_trylock(spinlock_t *lock);
自旋锁可以用于中断处理函数中(信号量就不能,因为会导致睡眠),那么在中断处理函数中使用自旋锁时,首先需要禁止本地中断,然后再获取锁,spin_lock_irqsave将这两个步骤合并成一个,而中断状态则包存在flags中。而spin_lock_irq只会禁止本地中断,而不会存储中断状态,所以需要确保在加锁前中断是被激活的。而spin_lock_bh只会禁止软中断,硬中断会继续执行,即至禁止底半部中断。

解锁函数有:
void spin_unlock(spinlock_t *lock);
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);
void spin_unlock_irq(spinlock_t *lock);
void spin_unlock_bh(spinlock_t *lock);

注意Linux自旋锁是不递归的,即在锁定期间试图去获取一个自旋锁,而此时自旋锁是被锁定的,那么将会被自旋在这里,等待释放自旋锁,而因为自旋则永远没有机会去释放锁,将造成死锁。

自旋锁主要用于短时间的轻量级加锁,以确保只能被一个执行线程拥有。

自旋锁最初是为多核处理器设计的,如果在单处理器上,内核又是一个非抢占内核,那么是没有自旋锁的。

3.3 自旋锁实例(摘自Linux Kernel Development)
DEFINE_SPINLOCK(mr_lock);
spin_lock(&mr_lock);
/* critical region ... */
spin_unlock(&mr_lock);


4. 读写自旋锁
同读写信号量一样,自旋锁也有读写自旋锁,即确保写操作的唯一性,而并发的读是允许的。

4.1 读写自旋锁的初始化
初始化使用函数rwlock_init,原型如下:
void rwlock_init(rwlock_t *lock);
或者使用宏DEFINE_RWLOCK静态定义和初始化一个读写自旋锁,例如:
DEFINE_RWLOCK(mr_rwlock);

4.2 读写自旋锁相关函数
读操作:
void read_lock(rwlock_t *lock);
void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
void read_lock_irq(rwlock_t *lock);
void read_lock_bh(rwlock_t *lock);
void read_unlock(rwlock_t *lock);
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);

写操作:
void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);
void write_unlock(rwlock_t *lock);
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);



5. completion
completion主要用于线程的同步,例如一个任务需要等待另一个任务的完成,那么就需要用到这里的completion,需要包含<linux/completion.h>。

5.1 completion的定义和初始化
初始化使用函数init_completion,原型如下:
void init_completion(struct completion *x);
或者使用宏DECLARE_COMPLETION静态定义和初始化一个completion,例如:
DECLARE_COMPLETION(mr_comp);

5.2 completion的相关操作
void wait_for_completion(struct completion *);
void complete(struct completion *);
void complete_all(struct completion *);
注意wait_for_completion是一个非中断等待,complete和complete_all两个函数类似,都是用于唤醒线程,只是complete只会唤醒一个线程,而complete_all会唤醒所有的等待线程。


6. 原子操作
原子操作即保证指令以原子的方式执行(执行过程不会被中断),内核提供了两组原子操作,即整数原子操作和位原子操作。

6.1 整数原子操作
整数原子操作的类型是atomic_t,定义在<linux/types.h>中,定义如下:
typedef struct {
	volatile int counter;
} atomic_t;
可以看到实际上就是int类型,虽然int类型是32位,但是这里只使用了其中的24位。

原子操作的接口都是定义在<asm/atomic.h>中,也就是同具体的体系架构有关。

6.1.1 设置原子变量的值
设置原子变量的值使用函数atomic_set,原型如下:
void atomic_set(atomic_t *v, int i);
也可以使用宏ATOMIC_INIT在定义原子变量时就对其做初始化,例如:
atomic_t v = ATOMIC_INIT(0);

6.1.2 读取原子变量值
读取原子变量值使用函数atomic_read,原型如下:
int atomic_read(const atomic_t *v);

6.1.3 原子变量的加和减
加使用函数atomic_add,减使用函数atomic_sub,原型如下:
void atomic_add(int i, atomic_t *v); /* 原子变量加i */
void atomic_sub(int i, atomic_t *v); /* 原子变量减i */

6.1.4 原子变量自增自减
自增自减使用函数atomic_inc和atomic_dec,原型如下:
void atomic_inc(atomic_t *v);
void atomic_dec(atomic_t *v);

6.1.5 执行操作并测试结果
int atomic_sub_and_test(int i, atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_inc_and_test(atomic_t *v);
执行操作后并测试原子值,如果其值为0,则返回true,否则返回false。

int atomic_add_negative(int i, atomic_t *v);
atomic_add_negative函数在执行add操作后,如果原子值为负是返回true,否则返回false。

6.1.6 执行操作并返回结果
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);
执行操作后将原子值返回。

6.2 位原子操作
使用位原子操作需要包含头文件<asm/bitops.h>。

6.2.1 设置位
void set_bit(unsigned int nr, volatile unsigned long *addr);
参数nr即要设置的哪位。

6.2.2 清除位
void clear_bit(int nr, volatile unsigned long *addr);

6.2.3 改变位
void change_bit(int nr, volatile unsigned long *addr);
对指定的位取反。

6.2.4 测试位
int test_bit(int nr, const volatile unsigned long *addr);

6.2.5 测试并操作位
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);

你可能感兴趣的:(并发与竞态)