Linux设备驱动中的并发控制(三)

信号量

信号量(Semaphore)是操作系统中最典型的用于同步和互斥的手段,信号量的值可以是0、1或者n。

信号量与操作系统中的经典概念PV操作对应。

+ P(S):①将信号量S的值减1,即S=S-1;②如果S≥0,则该进程继续执行;否则该进程置为等待状态,排入等待队列。

+ V(S):①将信号量S的值加1,即S=S+1;②如果S>0,唤醒队列中等待信号量的进程。Linux中与信号量相关的操作主要有下面几种。
  • 1.定义信号量
    下列代码定义名称为sem的信号量:
struct semaphore sem;
  • 2.初始化信号量
void sema_init(struct semaphore *sem, int val);

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

  • 3.获得信号量
void down(struct semaphore * sem);

该函数用于获得信号量sem,它会导致睡眠,因此不能在中断上下文中使用。

int down_interruptible(struct semaphore * sem);

该函数功能与down类似,不同之处为,因为down()进入睡眠状态的进程不能被信号打断,但因为down_interruptible()进入睡眠状态的进程能被信号打断,信号也会导致该函数返回,这时候函数的返回值非0。

int down_trylock(struct semaphore * sem);

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

在使用down_interruptible()获取信号量时,对返回值一般会进行检查,如果非0,通常立即返回-ERESTARTSYS,如:

if (down_interruptible(&sem))
    return  -ERESTARTSYS;
  • 4.释放信号量
void up(struct semaphore * sem);

该函数释放信号量sem,唤醒等待者。作为一种可能的互斥手段,**信号量可以保护临界区,它的使用方式和自旋锁类似。**与自旋锁相同,只有得到信号量的进程才能执行临界区代码。但是,与自旋锁不同的是,当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。用作互斥时,信号量一般这样被使用:
在这里插入图片描述
由于新的Linux内核倾向于直接使用mutex作为互斥手段,信号量用作互斥不再被推荐使用。

信号量也可以用于同步,一个进程A执行down()等待信号量,另外一个进程B执行up()释放信号量,这样进程A就同步地等待了进程B。其过程类似:

在这里插入图片描述
此外,对于关心具体数值的生产者/消费者问题,使用信号量则较为合适。因为生产者/消费者问题也是一种同步问题。

互斥体

尽管信号量已经可以实现互斥的功能,但是“正宗”的mutex在Linux内核中还是真实地存在着。

下面代码定义了名为my_mutex的互斥体并初始化它:

struct mutex my_mutex;
mutex_init(&my_mutex);

下面的两个函数用于获取互斥体:

void mutex_lock(struct mutex *lock);
int mutex_lock_interruptible(struct mutex *lock);
int mutex_trylock(struct mutex *lock);

mutex_lock()与mutex_lock_interruptible()的区别和down()与down_trylock()的区别完全一致,前者引起的睡眠不能被信号打断,而后者可以。

mutex_trylock()用于尝试获得mutex,获取不到mutex时不会引起进程睡眠。

下列函数用于释放互斥体:

void mutex_unlock(struct mutex *lock);

mutex的使用方法和信号量用于互斥的场合完全一样:

struct mutex my_mutex;      /* 定义mutex */
mutex_init(&my_mutex);      /* 初始化mutex */
mutex_lock(&my_mutex);      /* 获取mutex */
...                         /* 临界资源*/
mutex_unlock(&my_mutex);    /* 释放mutex */

自旋锁和互斥体都是解决互斥问题的基本手段,面对特定的情况,应该如何取舍这两种手段呢?选择的依据是临界区的性质和系统的特点。

从严格意义上说,互斥体和自旋锁属于不同层次的互斥手段,前者的实现依赖于后者。

在互斥体本身的实现上,为了保证互斥体结构存取的原子性,需要自旋锁来互斥。所以自旋锁属于更底层的手段。

互斥体是进程级的,用于多个进程之间对资源的互斥,虽然也是在内核中,但是该内核执行路径是以进程的身份,代表进程来争夺资源的。如果竞争失败,会发生进程上下文切换,当前进程进入睡眠状态,CPU将运行其他进程。鉴于进程上下文切换的开销也很大,因此,只有当进程占用资源时间较长时,用互斥体才是较好的选择。

当所要保护的临界区访问时间比较短时,用自旋锁是非常方便的,因为它可节省上下文切换的时间。但是CPU得不到自旋锁会在那里空转直到其他执行单元解锁为止,所以要求锁不能在临界区里长时间停留,否则会降低系统的效率。

由此,可以总结出自旋锁和互斥体选用的3项原则。

  • 1)当锁不能被获取到时,使用互斥体的开销是进程上下文切换时间,使用自旋锁的开销是等待获取自旋锁(由临界区执行时间决定)。若临界区比较小,宜使用自旋锁,若临界区很大,应使用互斥体
  • 2)互斥体所保护的临界区可包含可能引起阻塞的代码,而自旋锁则绝对要避免用来保护包含这样代码的临界区。因为阻塞意味着要进行进程的切换,如果进程被切换出去后,另一个进程企图获取本自旋锁,死锁就会发生。
  • 3)互斥体存在于进程上下文,因此,如果被保护的共享资源需要在中断或软中断情况下使用,则在互斥体和自旋锁之间只能选择自旋锁。当然,如果一定要使用互斥体,则只能通过mutex_trylock()方式进行,不能获取就立即返回以避免阻塞。(防止中断信号打不断睡眠造成了死锁)

完成量

Linux提供了完成量(Completion,关于这个名词,至今没有好的翻译,笔者将其译为“完成量”),它用于一个执行单元等待另一个执行单元执行完某事。

Linux中与完成量相关的操作主要有以下4种。

  • 1.定义完成量
    下列代码定义名为my_completion的完成量:
struct completion my_completion;
  • 2.初始化完成量
    下列代码初始化或者重新初始化my_completion这个完成量的值为0(即没有完成的状态):
init_completion(&my_completion);
reinit_completion(&my_completion)
  • 3.等待完成量
    下列函数用于等待一个完成量被唤醒:
void wait_for_completion(struct completion *c);
  • 4.唤醒完成量
    下面两个函数用于唤醒完成量:
void complete(struct completion *c);
void complete_all(struct completion *c);

前者只唤醒一个等待的执行单元,后者释放所有等待同一完成量的执行单元。

完成量用于同步的流程一般如下:

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TrustZone_Hcoco

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值