--------------------------------
本章主要涉及并发管理,我觉得这部分的理解需要建立在大量实践的基础之上,光是像本科操作系统课上那样讲道理是很难体会的。我记录了一些关键的函数和数据结构。
1.Race Conditions(RC)主要是因为对共享数据的并发访问没有作出合适的访问策略造成的。例如两个进程同时访问了一个共享数据。
所以制定好的访问策略,才能在Concurrency的时候避免RC。
所以作者建议尽量减少对资源的并发访问,但是又说明这几乎是不太可能的。所以唯有开发者加入并发控制的代码到module中去。目前主要采用的思想就是lock or mutual exclusion(锁和互斥)。
2.一般当某段代码需要进行并发访问管理的时候,就称之为critical section(CS)。一般为了对CS进行访问控制,采用lock的方法。当某个进程运行到某个CS的时候,它会去尝试获得lock,如果获取失败,就会进入block状态,从而sleep。根据不同的进程调度策略,该进程可能会等待若干时间后重新尝试获取lock。同时如果该进程获取了lock之后,进入CS,在CS执行的过程中也可能因为等待IO等进入sleep。
3.我个人认为semaphore也是lock思想的一种实现方式,P/V原语实际上可以代表进程对锁的获取和释放过程。当某个CS允许n个进程同时访问的时候,我们可以初始化semaphore=n,如果对某个CS的访问必须是互斥的(也就是同一时间只能有一个进程访问之),那么semaphore就要初始化为1.这时候的semaphore也称之为mutex。Linux kernel中几乎所有的semaphore都是mutex。
4.在linux中如果要使用semaphore,需要:
#include <asm/semaphore.h>
可见semaphore的实现是和平台相关的。具体的数据结构是:
struct semaphore {
atomic_t count;
int sleepers;
wait_queue_head_t wait;
};
如果想要建立一个普通的semaphore:
void sema_init(struct semaphore *sem, int val);
如果想要建立一个mutex,可以使用下面的两个宏:
DECLARE_MUTEX(name);
DECLARE_MUTEX_LOCKED(name);
二者的不同在于后者初始化之后mutex为0,任何进程想要进入CS,必须先解锁。
如果想要在运行期间创建mutex,可以使用:
void init_MUTEX(struct semaphore *sem);
void init_MUTEX_LOCKED(struct semaphore *sem);
针对P/V原语,P在linux 中称之为down,也就是减少1,V称之为up,也就是增加1,down有三种版本:
void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
其中第一个版本会尝试使用P原语,如果semaphore已经为0,该函数就会一直等待。第二个版本也是如此,但是可以被中断(interruptible)。第三个函数如果使用P原语失败,则立即返回。所以第二个和第三个函数返回后需要检验它们的返回值来确定是否真的获得了semaphore。
up函数为:
void up(struct semaphore *sem);
5.读写信号量
上面介绍的普通semaphore操作,无论是什么操作,都是一样的处理方法。但是针对写少读多的情况,我们可以让读操作在CS中并发,这样不会引起RC。这就需要一类新的semaphore,linux kernel也为我们提供了(书中提到这类semaphore现在已经很少使用了)
#include <linux/rwsem.h>
数据结构为
struct rw_semaphore;
这个数据结构必须在运行期间被初始化:
void init_rwsem(struct rw_semaphore *sem);
如果只是进行读操作,down/up函数有
void down_read(struct rw_semaphore *sem)
int down_read_trylock(struct rw_semaphore *sem)
void up_read(struct rw_semaphore *sem)
第二个函数如果请求读失败则立即返回,这里需要注意的是如果请求成功则返回非0,失败则0.这个返回值和linux中大多数函数成功返回0的习惯恰好相反。
对于写操作,down/up函数有:
void down_write(struct rw_semaphore *sem)
int down_write_trylock(struct rw_semaphore *sem)
void up_write(struct rw_semaphore *sem)
void downgrade_write(struct rw_semaphore *sem)
其中前三个函数和read版的行为类似,最后一个函数用于短期的写操作使用。
这种semaphore的形式可以让read的数量无限制,write进入CS需要互斥,同时保证write进入CS的时候没有read在CS中(否则读出的数据是dirty)。这样可能会导致reader starvation。所以这种方法适用于那些写操作不多而且持续时间不长的场所。
6.Completions
有一种情况就是在当前进程(A)启动一个处于进程之外的任务(B),可能在kernel space,可能在user space,然后等待B完成。在这种情况下,我们可以使用信号量来协调二者的先后顺序。用语句表示就是
struct semaphore sem;
init_MUTEX_LOCKED(&sem);
start_external_task(&sem);
down(&sem);
首先A创建一个mutex类型的semaphore,并立即将自己锁死在上面(参考前面的init_MUTEX_LOCKED宏定义,初始化sem为0),然后等待B完成后对sem进行up操作,此后A才能进行down操作,从而进入CS。
从上面的语句中可以看出,A必须等到B完成之后才能继续工作,这会极大的影响性能,所以semaphore是不适合用于这里的。为了不在这种情况下继续使用semaphore,从kernel 2.4.17开始加入了completion接口。
#include <linux/completion.h>
可以声明如下:
DECLARE_COMPLETION(my_completion);
或者动态声明如下:
struct completion my_completion;
init_completion(&my_completion);
如果A等待某个进程完成只需要执行如下函数
void wait_for_completion(struct completion *c);
需要注意的是,这个函数是不可中断的,所以如果没有进程completion,则A进程则会一直等待下去。
另一方面,B调用下列函数来通知completion:
void complete(struct completion *c);
void complete_all(struct completion *c);
第一个函数只能通知一个等待的进程,第二个函数可以通知全部进程。
源代码目录下的misc-modules/complete.c给出了使用completion的例子,是一个使用completion完成读写的字符设备。每个读操作都在等待一个写操作通知completion,此后至于谁读则不能控制。
7.Spinlocks
spinlocks是linux kernel提供的另一种互斥方法。spinlocks用于不能sleep的代码中,例如中断处理。较之semaphore可以提供更好的性能,当然也有更多的约束。
一个spinlock只有locked和unlocked两种状态,通常它是一个整型变量的一个bit,测试的时候如果是unlocked则设置为locked,然后进程会进入CS,否则就会执行一个循环,并不断查询bit,直到它可用为止,这个循环就是spin。
当然这个测试和设置locked的操作是atomic的,这样才能保证只有一个线程获得锁。另外如果spinlock运行在一个非抢占式的单cpu的系统中,那么一旦进入spin则会一直循环下去,因为不可能有别的线程获得cpu并释放lock。所以在这种情况下spinlocks会优化成为什么都不作。在SMP下则不会有这个问题,所以从这里也能看出来,spinlocks是平台相关的。
8.Spinlock的基本函数
#include <linux/spinlock.h>
编译期间的初始化:
spinlokc_t my_lock = SPIN_LOCK_UNLOCKED
运行期间初始化:
void spin_lock_init(spinlock_t *lock);
申请加锁的时候:
void spin_lock(spinlock_t *lock);
注意这个函数没有返回值,所以我们猜也可以猜到,它和前面介绍的那些可以interruptible的不同,一旦申请,就会一直spin直到得到锁为止。
解锁的时候:
void spin_unlock(spinlock_t *lock);
9.Spinlock和原子上下文
设想当一个进程获得spinlock之后进入了CS,随后调用了一个可能sleep的函数(例如cpoy_from_user,因为需要的page可能不在内存中,需要从硬盘copy,这回引起copy_from_user的sleep),这时就会引起别的正在spin的进程,在未来一段时间无法获得lock。
所以使用spinlock的时间要尽量短,而且在持有锁的时候,执行的代码要是原子的,也就是不能sleep的。
另一种情况就是在单cpu的机器上,一个进程获得了某个设备上的锁,这时出现中断,中断处理会首先要求获得该设备上的锁,就会不断的spin,占用cpu。而那个持有锁的进程确无法再获得cpu而释放锁,从而变成了deadlock。所以在单cpu上使用spinlock的时候要关中断。
10.Spinlock的其余函数
void spin_lock_irqsave(spinlock_t *lock, unsigned long flag);
void spin_lock_irq(spinlock_t *lock);
void spin_lock_bh(spinlock_t *lock);
第一个函数在获取spinlock之前关中断,并将其状态放入flag中。第二个函数需要你负责在释放 spinlock后来开中断。第三个函数在获取spinlock之前关掉软件中断。
对应的释放函数
void spin_unlock_irqstore(spinlock_t *lock, unsigned long flag);
void spin_unlock_irq(spinlock_t *lock);
void spin_unlock_bh(spinlock_t *lock);
另外还有非阻塞的获取函数
int spin_trylock(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock);
11.读/写 Spinlocks
spinlocks还有一组用于读写操作,允许多个读者同时进入CS,而写者进入CS必须是互斥的。
#include <linux/spinlock.h>
数据结构为rwlock_t
编译期间初始化为:
rwlock_t my_rwlock = RW_LOCK_UNLOCKED
运行期间初始化为:
rwlock_t my_rwlock;
rwlock_init(&my_rwlock);
它们也有和上面类似的操作函数,可以参考书P120-121.
12.原子变量
当CS中只含有一个简单的变量的时候,可以考虑使用原子变量。具体的可以参考书P124-126.