信号量 semaphore 和 互斥体 mutex

信号量是一种睡眠机制,当一个进程试图获得一个不可用的(已经被占用)信号量时,信号量会将其推进一个等待队列,然后让他睡眠。
这时处理器能重获自有,去执行其他代码。
当持有的信号量可用(被释放)后,处于等待队列中的进程会被唤醒,并获得该信号量。


1. 内核的信号量定义如下,与用户空间的信号量实现有所不同:
    struct semaphore {
        atomic_t count; //指可以同时处于信号量保护的临界区中的进程的数目, 大多数情况 count = 1
        int sleepers;  //指定了等待(允许进入临界区的进程的)数目,不同于自旋锁,等待的进程会进入睡眠,直到信号量释放,才会被唤醒。这意味着CPU可以在等待期间执行其他任务。
        wait_queue_head_t wait; //实现一个队列,保存所有在该信号量上睡眠的进程的task_struct.
    };

信号量与自旋锁的比较:

a. 与自旋锁相比,信号量适合用于保护更长的临界区,以防止并行。

b. 在进程上下文切换处,只能用信号量,这时不能用自旋锁。而中断上下文中不能使用信号量,因为中断上下文中是不能进行调度的,这时只能用自旋锁。

c. 信号量不适合用于断保护短的代码范围,因为使用信号量时需要进入睡眠和再次唤醒,代价很高。

d. 信号量会导致进程睡眠,而使用自旋锁的进程,会在临界区外面一直自旋。

e. 信号量比自旋锁提供了更好的CPU利用率,因为没把时间花费在忙等待上,但是信号量比自旋锁的开销更大, 多了睡眠,维护等待队列以及唤醒等开销。

f. 在占用信号量的同时,不能再去占用自旋锁,因为在等待信号量时可能会睡眠,而持有自旋锁时,是不允许睡眠的。

g. 在与用户空间交换数据时,因为代码需要睡眠,所以此时信号量是唯一的选择。

h. 信号量不会禁止内核抢占,所有支持信号量的代码都可以被抢占,这点不同于自旋锁。

2. 互斥量,其实就是一个二值信号量。

DECLARE_MUTEX(mutex)

...

down(&mutex)

/* 临界区 */

up(&mutex)

进入临界区时,用down对使用计数器减1。在计数器=0时,其他进程不能进入临界区。

它也会遇到2种情况:

A. 该信号量已经被分配时,在用down去获取时,当前进程进入睡眠,并添加到与该信号量相关联的等待队列上。同时,该进程被置于TASK_UNINTERRUPTIBLE状态,在等待进入临界区的过程中, 无法接受信号。

B. 如果该信号量没有被分配,则该进程可以立即获得信号量并进入到临界区,而不会进入睡眠。

在退出临界区时,必须调用up。负责唤醒在信号量上睡眠的某个进程,然后该进程进入临界区,其他等待的进程继续睡眠。

 

3. 信号量&互斥体的初始化:

    // initialize mutex object
    #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 37)   //判断linux版本,
        init_MUTEX(&i2c.wr_sem);                        //linux版本低于2.6.37, 则使用init_MUTEX()初始化。
    #else
        sema_init(&i2c.wr_sem,1);                       //linux版本高于2.6.37,则使用sema_init()初始化信号量。
    #endif

4.信号量 ---semaphore
与自旋锁相同,只有得到信号量的进程才能执行临界区的代码。

与自旋锁不同的是,当获取不到信号量时,进程不会原地打转而是进入睡眠等待状态。

信号量允许任意数量的进程同时持有某一信号量, 这个数量由 count 指定,

当count > 1时, 它允许在一个时刻,至多有count个信号量持有者,它允许这些线程同时访问临界区。但是这种信号量对代码限制较多,内核中使用它的机会不多。

count = 1时,就是互斥体 Mutex.,使用信号量时基本都是使用互斥体。

4.1 定义信号量:
struct semaphore sem;  //定义信号量

void sema_init(struct semaphore *sem, int val); //初始化信号量,并设置信号量sem的值为val

void init_MUTEX(struct semaphore *sem);   //初始化一个用于互斥的信号量,它把信号量sem的值置为1。等同于 

sema_init(struct semaphore *sem, 1);

void init_MUTEX_LOCKED(struct semaphore *sem);  //也是初始化一个信号量,但它把信号两sem的值设为0,等同于 

sema_init(struct semaphore *sem, 0);
下面两个宏是定义并初始化互斥信号量的“快捷方式”
DECLARE_MUTEX(name)
DECLARE_MUTEX_LOCKED(name)
前者定义一个名为name的信号量并初始化为1,后者初始化为0

4.2 获得信号量:
void down(struct semaphore *sem); //获得信号两sem,但会导致睡眠,所以不能在中断上下文中使用。

int down_interruptible(struct semaphore *sem);

因down()而进入睡眠状态的进程不能被信号打断,而因down_interruptible()进入睡眠状态的进程能被信号打断。信号也会导致该函数返回,这时函数的返回值是0.

down_interruptible() 使用比较普遍。他会把进程置成 TASK_INTERRUPTIBLE 状态,然后睡眠。这种状态意味着进程可被信号唤醒。

int down_trylock(struct semaphore *sem);
尝试获得信号量sem,如果能获得,就获得该信号量返回0,否则返回非0值。

释放信号量

void up(struct semaphore *sem);

4.3 使用案例:

static DECLARE_MUTEX(mr_sem);    //定义并声明一个互斥信号量

if (down_interruptible(&mr_sem)) {
    //信号被接收,信号量还未获取
}

//获取信号量,进入临界区

up(&mr_sem);    //释放互斥信号量。

5 完成量用于同步 ---completion

类似:信号量用于同步,如果信号量被初始化为0,则可以用于同步。即一个执行单元的继续执行,需要等待另一执行单元完成某事。

完成量仅仅是提供了替代信号量的一个简单的解决办法。

用于一个执行单元等待另一执行单元完成某事。 例如: 当子进程执行或退出时,vfork()系统调用 使用完成量唤醒父进程。
struct completion my_completion;  //定义
init_completion(&my_completion);  //动态创建并初始化
或
DECLARE_COMPLETION(my_completion);

void wait_for_completion(struct completion *c); //用于等待一个completion被唤醒

void complete(struct completion *c);  //只唤醒一个等待的执行单元
void complete_all(struct completion *c);  //释放所有等待同一完成量的执行单元
完成量的用法:

a.  动态创建完成量,并初始化

b. 调用wait_for_completion() 进行等待。

c. 初始化完成后,调用 completion() 唤醒在等待的内核进行。


6.  读写信号量 ---rw_semaphore

允许多个读执行单元同时访问共享资源。而最多只能有一个写执行单元。

所有的读-写信号量都是互斥信号量, 也就是引用计数 count = 1.

static DECLARE_RWSEM(name);   //静态创建,定义并声明一个新信号量name
struct rw_semaphore my_rws;  //定义读写信号量
void init_rwsem(struct rw_semaphore *sem);  //初始化读写信号量
读写信号量 只对写者互斥,在没有写者锁的情况下,允许并发持有读锁的读者数不限。

同一时刻,在没有读者获得读锁的情况下,只允许有唯一的写者可以获得写锁。

所有的读-写信号量的睡眠都不会被信号打断,所以他只有一个down(), 没有down_interruptible().

获取/释放读信号量
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);   //如果成功获得信号量,则返回非0值;如果信号量被别进程使用,则返回0。注意:这里与普通信号量相反。
void up_read(struct rw_semaphore *sem);  
获取/释放写信号量
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)
downgrade_write()  //这个函数可以动态的将获取的写锁 转换为读锁。

读-写信号量与读-写自旋锁一样,使用时,代码中的读和写必须明白无误的分割开来,否则最好别用。

7. 互斥体 --- mutex,

mutex 其实就是一个二值信号量,也会使进程睡眠,所以不能在中断上下文中使用。

7.1 定义并初始化:

struct mutex my_mutex;   //定义

mutex_init(&my_mutex);   //初始化

或者

DEFINE_MUTEX(my_mutex);  //定义并初始化互斥体 my_mutex
7.2 获取/释放互斥体:
void fastcall mutex_lock(struct mutex *lock);             //睡眠不能被信号打断

int fastcall mutex_lock_interruptible(struct mutex *lock);  //睡眠能被信号打断

int fastcall mutex_trylock(struct mutex *lock);   //试图获得指定的mutex,如果成功返回1,否则失败返回0.

void fastcall mutex_unlock(struct mutex *lock);

mutex_is_locked(struct mutex *lock);   //如果锁已被征用,则返回1,否则返回0

7.3 mutex的使用场景:

a. 给mutex上锁的进程,必须负责给他解锁,(不能在一个进程上下文中锁定一个mutex,而在另一个进程上下文中给它解锁)。

所以mutex不适合内核与用户空间复杂的同步场景, 最常用的使用方式是:在同一进程上下文中上锁和解锁。

b. 递归的上锁和解锁是不允许的,也就是不能递归的持有同一个锁。

c. 当持有一个mutex时,进城不可以退出。

d. mutex 不能再中断或者下半部中使用,即时mutex_trylock() 也不行,因为会睡眠。

7.4 互斥体与自旋锁的使用场合:

低开销加锁 --- 自旋锁

短期锁定 --- 自旋锁

长期加锁 --- 互斥体

中断上下文加锁 --- 自旋锁

持有锁需要睡眠 --- 互斥体


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值