Linux 并发与竞争(二)

Linux 并发与竞争(二)

原子操作,自旋锁,读写锁,顺序锁相关内容在上节内容中,看这里

Linux并发与竞争(一)

1. 前言

协调好 Linux 并发导致的竞争问题,除了可以使用原子操作,自旋锁(含包含读写锁,顺序锁)之外还可以使用信号量,互斥体。有这么多的机制可用,它们各有特点并不是相互取代的关系,这些里面因该没有一种机制是通用的,所以这些机制都要了解(如同学习编程语言,语法都要会,根据逻辑要求使用不同的语法,这就没有学习哪些语法就够了的说法),不用考虑学习哪种机制更好,根据场景结合这些机制的特点去使用这些机制。

2. 信号量

信号量(Semaphore),可以用来保证两个或多个关键代码段,资源不被并发调用,只要是用来协调资源竞争的机制都是围绕这个出发点的,所以主要是理解不同机制实现资源协调的原理。

工作原理:

信号量的工作机制可以直接理解成计数器,信号量会有一个 大于 0 的初值,任何进程申请使用信号量,信号量个数会减一,任何进程离开临界区释放信号量,信号量个数会加一,当计数器减到 0 的时候就说明没有资源了。信号量为 0,其他进程要想访问就必须等待,等待别的进程可以进入睡眠(直到信号量值大于 0 时进程被唤醒,访问该资源)。

思考:

工作原理部分说明了什么信息呢?说明了一个进程只能申请一个信号量,信号量为 0 说明没有资源了,信号量为正说明还有多少资源,那信号量为负值时表示什么?可以表示还有几个进程正在等待资源(还差几个资源才满足访问)。所以假设计数值为 n,可以得到以下 3 种结论:

(1) n > 0,当前有可用资源,可用资源数量为 n。
(2) n = 0,资源都被占用,可用资源数量为 0,有 n 个进程正在占用资源。
(3) n < 0,资源都被占用,并且还有 n 个进程正在排队。

队列:

信号量为 0 时,其他进程要想访问就必须等待,等待也讲究先来后到,所以信号量除了一个用于统计信号量个数的变量还需要一个队列(阻塞队列),让等待资源的进程排好队并睡眠,资源空闲时按顺序唤醒进程去访问资源。

特性:

(1) 中断不能休眠,信号量不能用于中断中。

(2) 信号量会使等待线程进入休眠状态,因此适用于那些占用资源比较久的场合。

(3) 如果共享资源的持有时间比较短,就不适合使用信号量,因为频繁的休眠、切换线程会引起极大的开销。

如果信号量值的大于 1,那么这个信号量属于计数型信号量,如果要互斥的访问共享资源那么信号量的值就不能大于 1,此时的信号量属于二值信号量,这两种信号量原理相同。

生产者/消费者问题

系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用。(这里的 产品 理解为某种数据)生产者,消费者共享一个初始为空,大小为 n 的缓冲区。只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待。只有缓冲区不空时,消费者才能从中取出产品,否则必须等待。缓冲区是临界资源,在临界资源都被占用后,各进程必须互斥地访问。

2.1 方法浅析

Linux 内核定义了 semaphore 的结构体来表示信号量,使用之前先定义类型,类型定义在 “include\linux\semaphore.h” 文件中。

/* Please don't access any members of this structure directly */
struct semaphore {
    raw_spinlock_t        lock;
    unsigned int        count;
    struct list_head    wait_list;
};

可以看到结构体包含一个计数器 count(用于标记资源的数量),以及一个等待列表(也可以叫队列,用来存放等待资源的线程信息),是不是和上面表述的一样。

2.2 API 函数

下面是一些常用的信号量(对象)操作函数,注意 DEFINE_SEMAPHORE() 宏定义默认定义的是二值信号量,如果定义计数信号量,则自己直接定义结构体,再使用 sema_init() 函数去初始化结构体,并指定信号数量。

这些函数定义在 “kernel\locking\semaphore.c” 文件中。

/*
定义一个信号量,默认设置信号量的值为 1,
也就是说默认定义二值信号量,如果想使用计数信号量请用
sema_init 函数初始化时定义一个大于 1 的值
*/
DEFINE_SEMAPHORE(name);
/*初始化信号量 sem,设置信号量值为 val*/
void sema_init(struct semaphore *sem, int val);
/*获取信号量,因为会导致休眠,因此不能在中断中使用*/
void down(struct semaphore *sem);
/*
尝试获取信号量,如果能获取到信号量就获
取,并且返回 0。如果不能就返回非 0,并且
不会进入休眠
*/
int down_trylock(struct semaphore *sem);
/*
获取信号量,和 down 类似,只是使用 down 进
入休眠状态的线程不能被信号打断。而使用此
函数进入休眠以后是可以被信号打断的
*/
int down_interruptible(struct semaphore *sem);
/*释放信号量*/
void up(struct semaphore *sem);

本质上 DEFINE_SEMAPHORE() 宏定义也是定义结构体,看 Linux 源码这样定义。

#define DEFINE_SEMAPHORE(_name, _n) \
    struct semaphore _name = __SEMAPHORE_INITIALIZER(_name, _n)

下面再看一个信号量定义和使用实例。

struct semaphore sem; /* 定义信号量 */
sema_init(&sem, 1);   /* 初始化信号量 */
down(&sem);           /* 申请信号量 */

/*访问/操作资源临界区*/

up(&sem);             /* 释放信号量 */

3. 互斥体

互斥体用于保护要互斥访问的共享资源,互斥访问二值信号量(信号量值为 1)也可以实现,而互斥体加一个计数器可以模仿信号量的效果,它们各有特点并不是相互取代的关系。

区别与联系:

Mutex 相比信号量增加了所有权的概念,被锁住的 Mutex 只能由给它上锁的线程才能解开(解铃还需系铃人),Mutex 的功能也就因而限制在了构造临界区上。

二值信号量则可以由任一线程解开,是解决生产者-消费者问题的工具。比如某进程读取磁盘并进入睡眠,等待中断读取盘块结束之后来唤醒它。这就是可以用二值信号量的一个情景,这是 Mutex 解决不了的。

特性:

(1) 中断不能休眠,互斥体不能用于中断中。

(2) 只有 Mutex 的持有者才能释放 Mutex。

(3) Mutex 保护的临界区可以调用引起阻塞的 API 函数。

(4) 因为一次只有一个线程可以持有 Mutex,并且由于 (2) 的原因,因此 Mutex 不能递归上锁和解锁。

3.1 方法浅析

Linux 内核定义了 mutex 的结构体来表示互斥体,使用之前先定义类型,类型定义在 “include\linux\mutex.h” 文件中。

struct mutex {
    atomic_long_t       owner;
    raw_spinlock_t      wait_lock;
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
    struct optimistic_spin_queue osq; /* Spinner MCS lock */
#endif
    struct list_head    wait_list;
#ifdef CONFIG_DEBUG_MUTEXES
    void            *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map  dep_map;
#endif
};

从上面的结构体可以看到,互斥体包含 owner 变量,这个就是用来记录互斥体当前属于谁(线程),所以只有 mutex 的持有者才能释放 mutex。

3.2 API 函数

下面是一些常用的互斥体(对象)操作函数,这些函数定义在 “tools\perf\util\mutex.c” 文件中。

/*定义并初始化一个 mutex 变量*/
DEFINE_MUTEX(name);
/*初始化 mutex*/
void mutex_init(mutex *lock);
/*获取 mutex,也就是给 mutex 上锁,如果获取不到就进休眠*/
void mutex_lock(struct mutex *lock);
/*释放 mutex,也就给 mutex 解锁*/
void mutex_unlock(struct mutex *lock);
/*尝试获取 mutex,如果成功就返回 1,如果失败就返回 0*/
int mutex_trylock(struct mutex *lock);
/*判断 mutex 是否被获取,如果是的话就返回1,否则返回 0*/
int mutex_is_locked(struct mutex *lock);
/*使用此函数获取信号量失败进入休眠以后可以被信号打断*/
int mutex_lock_interruptible(struct mutex *lock);

下面看一个互斥体定义实例。

struct mutex lock; /* 定义互斥体 */
mutex_init(&lock); /* 初始化互斥体 */
mutex_lock(&lock); /* 申请互斥体 */

/*访问/操作资源临界区*/

mutex_unlock(&lock); /* 释放互斥体 */

自此在 Linux 中常用的资源竞争处理工具就这些了,原子操作,自旋锁,读写锁,顺序锁相关内容在上节内容中,在这里:

Linux并发与竞争(一)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值