struct semaphore sem;
2 初始化信号量该函数初始化信号量,并设置信号量 sem 的值为 val。尽管信号可以被初始化为大于 1 的值从而成为一个计数信号量,但是它通常不被这样使用。
void init_MUTEX(struct semaphore *sem);sema_init (struct semaphore *sem, 1)。
void init_MUTEX_LOCKED (struct semaphore *sem);sema_init (struct semaphore *sem, 0)。
此外,下面两个宏是定义并初始化信号量的“快捷方式 。前者定义一个名为 name 的信号量并初始化为 1,后者定义一个名为 name 的信号量并初始化为 0。
3 获得信号量该函数用于获得信号量 sem,它会导致睡眠,因此不能在中断上下文使用。
int down_interruptible(struct semaphore * sem);该函数功能与 down()类似, 不同之处为, 因为 down()而进入睡眠状态的进程不能被信号打断,而因为 down_interruptible()而进入睡眠状态的进程能被信号打断,信号也会导致该函数返回,这时候函数的返回值非 0。
int down_trylock(struct semaphore * sem);}
4 释放信号量
void up(struct semaphore * sem);
该函数释放信号量 sem,唤醒等待者。
信号量一般这样被使用,如下所示:
//定义号量 DECLARE_MUTEX(mount_sem); down(&mount_sem);//获取号量,保护临界区 ... critical section //临界区 ... up(&mount_sem);//释放号量
代码清单 7.3 给出了使用信号量实现设备只能被一个进程打开的例子,等同于代码清单 7.1 和代码清单 7.2。
代码清单 7.3 使用信号量实现设备只能被一个进程打开
static DECLARE_MUTEX(xxx_lock);//定义互斥锁 static int xxx_open(struct inode *inode,struct file *filp) { ... if (down_trylock(&xxx_lock)) //获得打开锁 return - EBUSY; //设备忙 ... return 0; /* 成功 */ } static int xxx_release(struct inode *inode, struct file *filp) { up(&xxx_lock); //释放打开锁 return 0; }
图 7.4 信号量用于同步
7.5.3 完成量用于同步
Linux 系统提供了一种比信号更好的同步机制, 即完成(completion, 关于这个名词, 至今没有好的翻译, 笔者将其译为 “完成量”) , 它用于一个执行单元等待另一个执行单元执行完某事。
Linux 系统中与 completion 相关的操作主要有以下 4 种。
1 定义完成量
下列代码定义名为 my_completion 的完成量。
struct completion my_completion;
2 初始化 completion
下列代码初始化 my_completion 这个完成量。
init_completion(&my_completion);
对 my_completion 的定义和初始化可以通过如下快捷方式实现。
DECLARE_COMPLETION(my_completion);
3 等待完成量
下列函数用于等待一个 completion 被唤醒。
void wait_for_completion(struct completion *c);
4 唤醒完成量
下面两个函数用于唤醒完成量。
void complete(struct completion *c);
void complete_all(struct completion *c);
前者只唤醒一个等待的执行单元,后者释放所有等待同一完成量的执行单元。
图 7.5 描述了使用完成量实现的同步功能
图 7.5 完成量用于同步
7.5.4 自旋锁 vs 信号量
自旋锁和信号量都是解决互斥问题的基本手段,面对特定的情况,应该如何进行选择呢?选择的依据是临界区的性质和系统的特点。
从严格意义上说,信号量和自旋锁属于不同层次的互斥手段,前者的实现依赖于后者。在信号量本身的实现上,为了保证信号量结构存取的原子性,在多CPU 中需要自旋锁来互斥。
信号量是进程级的,用于多个进程之间对资源的互斥,虽然也是在内核中,但是该内核执行路径是以进程的身份,代表进程来争夺资源的。如果竞争失败,会发生进程上下文切换, 当前进程进入睡眠状态, CPU 将运行其他进程。 鉴于进程上下文切换的开销也很大,因此,只有当进程占用资源时间较长时,用信号量才是较好的选择。
当所要保护的临界区访问时间比较短时,用自旋锁是非常方便的,因为它节省上下文切换的时间。但是 CPU 得不到自旋锁会在那里空转直到其他执行单元解锁为止,所以要求锁不能在临界区里长时间停留,否则会降低系统的效率。
由此,可以总结出自旋锁和信号量选用的 3 项原则。
l 当锁不能被获取时,使用信号量的开销是进程上下文切换时间 Tsw,使用自旋锁的开销是等待获取自旋锁 (由临界区执行时间决定) Tcs, 若 Tcs 比较小,应使用自旋锁,若 Tcs 很大,应使用信号量。
l 信号所保护的临界区可包含可能引起阻塞的代码, 而自旋锁则绝对要避免用来保护包含这样代码的临界区。因为阻塞意味着要进行进程的切换,如果进程被切换出去后,另一个进程企图获取本自旋锁,死锁就会发生。
l 信号存在于进程上下文,因此,如果被保护的共享资源需要在中断或软中断情况下使用,则在信号量和自旋锁之间只能选择自旋锁。当然,如果一定要使用信号量,则只能通过 down_trylock()方式进行,不能获取就立即返回以避免阻塞。
7.5.5 读写信号量
读写信号与信号量的关系与读写自旋锁和自旋锁的关系类似,读写信号可能引起进程阻塞, 但它可允许 N 个读执行单元同时访问共享资源, 而最多只能有一个写执行单元。因此,读写信号量是一种相对放宽条件的粒度稍大于信号量的互斥机制。
读写自旋锁涉及的操作包括如下 5 种。
1 定义和初始化读写信号量
struct rw_semaphore my_rws; /*定义读写号量*/
void init_rwsem(struct rw_semaphore *sem); /*初始化读写号量*/
2 读信号量获取
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
3 读信号量释放
void up_read(struct rw_semaphore *sem);
4 写信号量获取
void down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
5 写信号量释放
void up_write(struct rw_semaphore *sem);
读写信号量一般这样被使用,如下所示:
rw_semaphore rw_sem; //定义读写号量 init_rwsem(&rw_sem); //初始化读写号量 //读时获取号量 down_read(&rw_sem); ... //临界资源 up_read(&rw_sem); //写时获取号量 down_write(&rw_sem); ... //临界资源 up_write(&rw_sem);