LV10-04-并发与竞争-02-并发控制机制

本文主要是并发控制机制的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
Linux开发板 华清远见 底板: FS4412_DEV_V5 核心板: FS4412 V2
u-boot 2013.01
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
文件下载链接
------

前边大概了解了并发控制机制大概有原子变量、自旋锁、互斥锁和信号量几种,下边就来详细学习一下吧。

一、原子变量

1. 原子变量简介

首先看一下原子变量,原子变量就是指存取不可被打断的特殊整型变量。假如现在要对无符号整形变量 a 赋值,值为 3,对于 C 语言来讲很简单,直接就是:

1
a = 3;

但是编译成汇编之后,就变成了下边的样子:

1
2
3
ldr r0, =0X30000000 /* 变量 a 地址         */
ldr r1, = 3 /* 要写入的值 */
str r1, [r0] /* 将 3 写入到 a 变量中 */

这样的话,一个简单的赋值被拆分为三步,一个线程正在将 a 赋值为 3,只剩下最后一步了,此时另一个线程需要将其赋值为 5 ,而打断了上一个线程赋值为 3 的操作,那么之前赋值为 3 的线程得到的 a 的值将会是 5 了,这是不被允许的。要解决这个问题就要保证三行汇编指令作为一个整体运行,也就是作为一个原子存在,这个时候键可以将 a 设置为原子变量。

【注意事项】

(1)在linux内核中,有区分32位原子变量和64位原子变量,他们的原理是一样的,只是操作函数名可能会有不同,需要注意一下。后边写的 API 函数笔记都是 32 位原子变量使用的。

【适用场合】共享资源为单个整型变量的互斥场合

2. 相关结构体

Linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量,这个结构体定义在linux内核源码的这个文件中:

1
2
include/linux/types.h          # 32位
include/asm-generic/atomic64.h # 64位

我们打开这个文件,会看到该结构体定义如下:

1
2
3
4
5
6
7
8
9
// 32位
typedef struct {
int counter;
} atomic_t;

// 64位
typedef struct {
long long counter;
} atomic64_t;

3. 原子整型操作API

以下是针对于32位原子变量进行操作的相关函数。

3.1 ATOMIC_INIT()

我们使用以下命令查询一下函数所在头文件:

1
grep ATOMIC_INIT -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
/* 需包含的头文件,定义在 include/asm-generic/atomic.h 中 */
#include <asm/atomic.h>

/* 函数声明 */
#define ATOMIC_INIT(i) { (i) }

【函数说明】该函数是一个宏,用于定义原子变量的时候对其初始化。

【函数参数】

  • i :int 类型,表示定义原子变量时要赋的初值。

【返回值】none

【使用格式】

1
atomic_t a = ATOMIC_INIT(0); //定义原子变量 a 并赋初值为 0

【注意事项】 none

3.2 atomic_set()

我们使用以下命令查询一下函数所在头文件:

1
grep atomic_set -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 需包含的头文件,定义在 include/asm-generic/atomic.h 中 */
#include <asm/atomic.h>

/* 函数声明 */
/**
* atomic_set - set atomic variable
* @v: pointer of type atomic_t
* @i: required value
*
* Atomically sets the value of @v to @i.
*/
#define atomic_set(v, i) (((v)->counter) = (i))

// 其实可以写成这样
void atomic_set(atomic_t *v, int i);

【函数说明】该函数是一个宏,用于设置原子变量的值,就是向 v 写入 i 值。

【函数参数】

  • v :atomic_t * 类型,表示要赋值的原子变量。
  • i :int 类型,表示要设置的值。

【返回值】none

【使用格式】none

【注意事项】 none

3.3 atomic_read()

我们使用以下命令查询一下函数所在头文件:

1
grep atomic_read -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 需包含的头文件,定义在 include/asm-generic/atomic.h 中 */
#include <asm/atomic.h>

/* 函数声明 */
/**
* atomic_read - read atomic variable
* @v: pointer of type atomic_t
*
* Atomically reads the value of @v.
*/
#ifndef atomic_read
#define atomic_read(v) (*(volatile int *)&(v)->counter)
#endif
// 其实可以写成这样
int atomic_read(atomic_t *v);

【函数说明】该函数是一个宏,用于读取原子变量 v 的值。

【函数参数】

  • v :atomic_t * 类型,表示要操作的原子变量。

【返回值】none

【使用格式】none

【注意事项】 none

3.4 atomic_add()

我们使用以下命令查询一下函数所在头文件:

1
grep atomic_add -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
/* 需包含的头文件,定义在 include/asm-generic/atomic.h 中 */
#include <asm/atomic.h>

/* 函数定义 */
static inline void atomic_add(int i, atomic_t *v)
{
atomic_add_return(i, v);
}

【函数说明】该函数用于给原子变量 v 加上 i 值。

【函数参数】

  • i :int 类型,表示要加的值。

  • v :atomic_t * 类型,表示要操作的原子变量。

【返回值】none

【使用格式】none

【注意事项】 none

3.5 atomic_sub()

我们使用以下命令查询一下函数所在头文件:

1
grep atomic_sub -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
9
/* 需包含的头文件,定义在 include/asm-generic/atomic.h 中 */
#include <asm/atomic.h>

/* 函数定义 */
static inline void atomic_sub(int i, atomic_t *v)
{
atomic_sub_return(i, v);
}

【函数说明】该函数用于给原子变量 v 减去 i 值。

【函数参数】

  • i :int 类型,表示要减的值。

  • v :atomic_t * 类型,表示要操作的原子变量。

【返回值】none

【使用格式】none

【注意事项】 none

3.6 atomic_inc()

我们使用以下命令查询一下函数所在头文件:

1
grep atomic_inc -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
9
/* 需包含的头文件,定义在 include/asm-generic/atomic.h 中 */
#include <asm/atomic.h>

/* 函数定义 */
static inline void atomic_inc(atomic_t *v)
{
atomic_add_return(1, v);
}

【函数说明】该函数用于给原子变量 v 加 1,也就是自增。

【函数参数】

  • v :atomic_t * 类型,表示要操作的原子变量。

【返回值】none

【使用格式】none

【注意事项】 none

3.7 atomic_dec()

我们使用以下命令查询一下函数所在头文件:

1
grep atomic_dec -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
9
/* 需包含的头文件,定义在 include/asm-generic/atomic.h 中 */
#include <asm/atomic.h>

/* 函数定义 */
static inline void atomic_dec(atomic_t *v)
{
atomic_sub_return(1, v);
}

【函数说明】该函数用于将原子变量 v 减 1,就是自减。

【函数参数】

  • v :atomic_t * 类型,表示要操作的原子变量。

【返回值】none

【使用格式】none

【注意事项】 none

3.8 atomic_inc_and_test()

我们使用以下命令查询一下函数所在头文件:

1
grep atomic_inc_and_test -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
/* 需包含的头文件,定义在 include/asm-generic/atomic.h 中 */
#include <asm/atomic.h>

/* 函数声明 */
#define atomic_inc_and_test(v) (atomic_inc_return(v) == 0)
// 其实可以写成
int atomic_inc_and_test(atomic_t *v);

【函数说明】该函数用于给 v 加 1,如果结果为 0 就返回真,否则返回假。

【函数参数】

  • v :atomic_t * 类型,表示要操作的原子变量。

【返回值】int类型,返回真表示原子变量值为0,返回假表示原子变量值非0。

【使用格式】none

【注意事项】 none

3.9 atomic_dec_and_test()

我们使用以下命令查询一下函数所在头文件:

1
grep atomic_dec_and_test -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
/* 需包含的头文件,定义在 include/asm-generic/atomic.h 中 */
#include <asm/atomic.h>

/* 函数声明 */
#define atomic_dec_and_test(v) (atomic_dec_return(v) == 0)
// 其实可以写成
int atomic_dec_and_test(atomic_t *v);

【函数说明】该函数用于给 v 减 1,如果结果为 0 就返回真,否则返回假。

【函数参数】

  • v :atomic_t * 类型,表示要操作的原子变量。

【返回值】int类型,返回真表示原子变量值为0,返回假表示原子变量值非0。

【使用格式】none

【注意事项】 none

3.10 atomic_sub_and_test()

我们使用以下命令查询一下函数所在头文件:

1
grep atomic_sub_and_test -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
/* 需包含的头文件,定义在 include/asm-generic/atomic.h 中 */
#include <asm/atomic.h>

/* 函数声明 */
#define atomic_sub_and_test(i, v) (atomic_sub_return((i), (v)) == 0)
// 其实可以写成
int atomic_sub_and_test(int i,atomic_t *v);

【函数说明】该函数用于从 v 减 i,如果结果为 0 就返回真,否则返回假。

【函数参数】

  • i :int 类型,表示要减的值。

  • v :atomic_t * 类型,表示要操作的原子变量。

【返回值】int类型,返回真表示原子变量值为0,返回假表示原子变量值非0。

【使用格式】none

【注意事项】 none

4. 原子位操作API

位操作也是很常用的操作, Linux 内核也提供了一系列的原子位操作 API 函数,只不过原子位操作不像原子整形变量那样有个 atomic_t 的数据结构,原子位操作是直接对内存进行操作 ,这里就不详细写了。

1
2
3
4
void set_bit(nr, void *addr);		// 设置addr的第nr位为1
void clear_bit(nr , void *addr); // 清除addr的第nr位为0
void change_bit(nr , void *addr); // 改变addr的第nr位为1
void test_bit(nr , void *addr); // 测试addr的第nr位是否为1

5. 使用步骤

以保证一个设备文件只能被打开一次为例:

  • (1)定义一个原子变量;
  • (2)设置原子变量的初始值为1;
  • (3)在驱动的open()函数中将原子变量减1并判断结果否为0,若为0,则说明可以进行操作,若为负数,说明已经有其他应用程序打开了这个设备文件,此时恢复减1之前的原子变量值,就是再加1,然后提示错误并返回。
  • (4)在关闭设备的时候,就是在驱动的close()函数中要对原子变量进行加1的操作,这样才能保证其他的进程或者线程可以正常打开这个设备文件。

二、自旋锁

1. 自旋锁简介

原子操作只能对整形变量或者位进行保护,但是,在实际的使用环境中怎么可能只有整形变量或位这么简单的临界区。举个最简单的例子,设备结构体变量就不是整型变量,我们对于结构体中成员变量的操作也要保证原子性,在线程 A 对结构体变量使用期间,应该禁止其他的线程来访问此结构体变量,这些工作原子操作都不能胜任。

当一个线程要访问某个共享资源的时候首先要先获取相应的锁, 锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。

对于自旋锁而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理,而是会一直傻傻的在那里“转圈圈”的等待锁可用。

比如现在有个公用电话亭,一次肯定只能进去一个人打电话,现在电话亭里面有人正在打电话,相当于获得了自旋锁。此时我们到了电话亭门口,因为里面有人,所以我们不能进去打电话,相当于没有获取自旋锁,这个时候我们肯定是站在原地等待,我们可能因为无聊的等待而转圈圈消遣时光,反正就是哪里也不能去,要一直等到里面的人打完电话出来。终于,里面的人打完电话出来了,相当于释放自旋锁,这个时候我们就可以使用电话亭打电话了,相当于获取到了自旋锁。

自旋锁的“自旋”也就是“原地打转”的意思,“原地打转”的目的是为了等待自旋锁可以用,可以访问共享资源。可以看到自旋锁的一个缺点:那就等待自旋锁的线程会一直处于自旋状态,这样会浪费处理器时间降低系统性能,所以自旋锁的持有时间不能太长。所以自旋锁适用于短时期的轻量级加锁,如果遇到需要长时间持有锁的场景那就需要换其他的方法了。

【注意事项】

(1)因为在等待自旋锁的时候处于“自旋”状态,因此锁的持有时间不能太长,一定要短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处理方式。

(2)自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则的话可能导致死锁。

(3)不能递归申请自旋锁,因为一旦通过递归的方式申请一个正在持有的锁,那么就必须“自旋”,等待锁被释放,然而正处于“自旋”状态,根本没法释放锁。结果就是自己把自己锁死了。

(4)在编写驱动程序的时候我们必须考虑到驱动的可移植性,因此不管用的是单核的还是多核的 SOC,都将其当做多核 SOC 来编写驱动程序。

【适用场合】自旋锁适用于以下场合:

(1)异常上下文之间或异常上下文与任务上下文之间共享资源时。

(2)任务上下文之间且临界区执行时间很短时。

(3)互斥问题。

2. 相关结构体

Linux 内核使用结构体 spinlock_t 表示自旋锁 ,该结构体定义在linux内核源码的这个文件中:

1
include/linux/spinlock_types.h

我们打开这个文件,可以看到自旋锁结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct spinlock {
union {
struct raw_spinlock rlock;

#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
struct {
u8 __padding[LOCK_PADSIZE];
struct lockdep_map dep_map;
};
#endif
};
} spinlock_t;

3.自旋锁操作API

下边的这些自旋锁API函数适用于SMP或支持抢占的单CPU下线程之间的并发访问,也就是用于线程与线程之间,被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API 函数,否则的话会可能会导致死锁现象的发生。

自旋锁会自动禁止抢占,也就说当线程 A 得到锁以后会暂时禁止内核抢占。如果线程 A 在持有锁期间进入了休眠状态,那么线程 A 会自动放弃 CPU 使用权。线程 B 开始运行,线程 B 也想要获取锁,但是此时锁被 A 线程持有,而且内核抢占还被禁止了,线程 B 无法被调度出去,那么线程 A 就无法运行,锁也就无法释放,此时,死锁发生了!

3.1 spin_lock_init()

我们使用以下命令查询一下函数所在头文件:

1
grep spin_lock_init -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
/* 需包含的头文件*/
#include <linux/spinlock.h>

/* 函数定义 */
#define spin_lock_init(_lock) \
do { \
spinlock_check(_lock); \
raw_spin_lock_init(&(_lock)->rlock); \
} while (0)

// 追踪一下会发现其实这个函数可以写成这样
int spin_lock_init(spinlock_t *lock);

【函数说明】该函数初始化自旋锁。

【函数参数】

  • lock :spinlock_t * 类型,表示要初始化的自旋锁。

【返回值】int类型,一般不怎么关心这个返回值。

【使用格式】none

【注意事项】 none

3.2 spin_lock()

我们使用以下命令查询一下函数所在头文件:

1
grep spin_lock -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
/* 需包含的头文件*/
#include <linux/spinlock.h>

/* 函数定义 */
static inline void spin_lock(spinlock_t *lock)
{
raw_spin_lock(&lock->rlock);
}

【函数说明】该函数用于获取自旋锁,也叫作加锁。

【函数参数】

  • lock :spinlock_t * 类型,表示要获取的自旋锁。

【返回值】none

【使用格式】none

【注意事项】 none

3.3 spin_trylock()

我们使用以下命令查询一下函数所在头文件:

1
grep spin_trylock -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
/* 需包含的头文件 */
#include <linux/spinlock.h>

/* 函数定义 */
static inline int spin_trylock(spinlock_t *lock)
{
return raw_spin_trylock(&lock->rlock);
}

【函数说明】该函数用于尝试获取指定的自旋锁,如果没有获取到就返回 0。

【函数参数】

  • lock :spinlock_t * 类型,表示要获取的自旋锁。

【返回值】int 类型,没有获取到锁,返回0;获取到锁,返回非0值。

【使用格式】none

【注意事项】 none

3.4 spin_unlock()

我们使用以下命令查询一下函数所在头文件:

1
grep spin_unlock -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
9
/* 需包含的头文件 */
#include <linux/spinlock.h>

/* 函数定义 */
static inline void spin_unlock(spinlock_t *lock)
{
raw_spin_unlock(&lock->rlock);
}

【函数说明】该函数用于。释放自旋锁,也叫解锁

【函数参数】

  • lock :spinlock_t * 类型,表示要释放的自旋锁。

【返回值】none

【使用格式】none

【注意事项】 none

4. 用于中断的API

上边的那些API 函数用于线程之间的并发访问,如果此时发生了中断,中断也想访问共享资源,那该怎么办呢?

首先,中断里面可以使用自旋锁,但是在中断里面使用自旋锁的时候,在获取锁之前一定要先禁止本地中断(也就是本 CPU 中断,对于多核 SOC来说会有多个 CPU 核),否则可能导致锁死现象的发生 。

image-20220919120725303

如上图,线程 A 先运行,并且获取到了 lock 这个锁,当线程 A 运行 funcA 函数的时候中断发生了,中断抢走了 CPU 使用权。右边的中断服务函数也要获取 lock 这个锁,但是这个锁被线程 A 占有着,中断就会一直自旋,等待锁有效。但是在中断服务函数执行完前,线程 A 是不可能执行的,所以,死锁又发生了。所以就需要在获取锁之前关闭本地中断

linux内核为我们也提供了一些API函数:

1
2
3
4
void spin_lock_irq(spinlock_t *lock); /* 禁止本地中断,并获取自旋锁 */
void spin_unlock_irq(spinlock_t *lock);/* 激活本地中断,并释放自旋锁 */
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);/* 保存中断状态,禁止本地中断,并获取自旋锁 */
void spin_unlock_irqrestore(spinlock_t*lock, unsigned long flags);/* 将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁 */

使用 spin_lock_irq / spin_unlock_irq 的时候需要用户能够确定加锁之前的中断状态,但实际上内核很庞大,运行也是“千变万化”,我们是很难确定某个时刻的中断状态,因此不推荐使用 spin_lock_irq/spin_unlock_irq。建议使用 spin_lock_irqsave / spin_unlock_irqrestore,因为这一组函数会保存中断状态,在释放锁的时候会恢复中断状态。

一般在线程中使用 spin_lock_irqsave / spin_unlock_irqrestore,在中断中使用 spin_lock/spin_unlock ,实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DEFINE_SPINLOCK(lock) /* 定义并初始化一个锁 */

/* 线程 A */
void functionA ()
{
unsigned long flags; /* 中断状态 */
spin_lock_irqsave(&lock, flags) /* 获取锁 */
/* 临界区 */
spin_unlock_irqrestore(&lock, flags) /* 释放锁 */
}

/* 中断服务函数 */
void irq()
{
spin_lock(&lock) /* 获取锁 */
/* 临界区 */
spin_unlock(&lock) /* 释放锁 */
}

5. 用于下半部的API

下半部(BH)也会竞争共享资源,如果要在下半部里面使用自旋锁 ,可以用这些函数:

1
2
void spin_lock_bh(spinlock_t *lock);   /* 关闭下半部,并获取自旋锁 */
void spin_unlock_bh(spinlock_t *lock); /* 打开下半部,并释放自旋锁 */

6. 使用步骤

  • (1)定义自旋锁;
  • (2)初始化自旋锁;
  • (3)需要加锁的地方获取自旋锁,加锁的代码执行完一定要解锁,加锁和解锁一定要成对出现。

三、互斥锁

1. 互斥锁简介

自旋锁在上锁的时候会一直自旋,占用CPU资源,这是很浪费资源和时间的,linux中还有一种互斥锁。互斥锁加锁的资源每次只能有一个线程访问,若是没有获取到互斥锁,线程或者进程便会休眠,让出CPU。

【注意事项】

(1)互斥锁可以导致休眠,因此不能在中断中使用互斥锁,中断中只能使用自旋锁。

(2)互斥锁保护的临界区可以调用引起阻塞的 API 函数。

(3)为一次只有一个线程可以持有互斥锁,因此,必须由互斥锁的持有者释放互斥锁。并且互斥锁不能递归上锁和解锁。

【适用场合】任务上下文之间且临界区执行时间较长时的互斥问题。

2. 相关结构体

linux 内核中使用 mutex 表示互斥锁,该结构体变量定义在linux内核源码的这个文件中

1
include/linux/mutex.h

我们打开这个文件,就可以看到互斥锁的结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct mutex {
/* 1: unlocked, 0: locked, negative: locked, possible waiters */
atomic_t count;
spinlock_t wait_lock;
struct list_head wait_list;
#if defined(CONFIG_DEBUG_MUTEXES) || defined(CONFIG_SMP)
struct task_struct *owner;
#endif
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
void *spin_mlock; /* Spinner MCS lock */
#endif
#ifdef CONFIG_DEBUG_MUTEXES
const char *name;
void *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};

3.互斥锁操作API

3.1 mutex_init()

我们使用以下命令查询一下函数所在头文件:

1
grep mutex_init -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 需包含的头文件*/
#include <linux/mutex.h>

/* 函数定义 */
# define mutex_init(mutex) \
do { \
static struct lock_class_key __key; \
\
__mutex_init((mutex), #mutex, &__key); \
} while (0)

// 追踪一下会发现其实这个函数可以写成这样
void mutex_init(mutex *lock)

【函数说明】该函数初始化互斥锁。

【函数参数】

  • lock :mutex * 类型,表示要初始化的互斥锁。

【返回值】none

【使用格式】none

【注意事项】 none

3.2 mutex_lock()

我们使用以下命令查询一下函数所在头文件:

1
grep mutex_lock -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
/* 需包含的头文件*/
#include <linux/mutex.h>

/* 函数声明 */
extern void mutex_lock(struct mutex *lock);

【函数说明】该函数用于获取互斥锁,也叫作加锁,如果获取不到就进行休眠。

【函数参数】

  • lock :struct mutex * 类型,表示要获取的互斥锁。

【返回值】none

【使用格式】none

【注意事项】 none

3.3 mutex_trylock()

我们使用以下命令查询一下函数所在头文件:

1
grep spin_trylock -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
/* 需包含的头文件 */
#include <linux/mutex.h>

/* 函数声明 */
extern int mutex_trylock(struct mutex *lock);

【函数说明】该函数用于尝试获取指定的互斥锁,如果没有获取到就返回 0。

【函数参数】

  • lock :struct mutex * 类型,表示要获取的互斥锁。

【返回值】int 类型,成功获取到锁,返回1;没有获取到锁,返回0。

【使用格式】none

【注意事项】 none

3.4 mutex_unlock()

我们使用以下命令查询一下函数所在头文件:

1
grep mutex_unlock -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
/* 需包含的头文件 */
#include <linux/mutex.h>

/* 函数声明 */
extern void mutex_unlock(struct mutex *lock);

【函数说明】该函数用于释放指定的互斥锁。

【函数参数】

  • lock :struct mutex * 类型,表示要释放的互斥锁。

【返回值】none

【使用格式】none

【注意事项】 none

3.5 mutex_is_locked()

我们使用以下命令查询一下函数所在头文件:

1
grep mutex_is_locked -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
/* 需包含的头文件 */
#include <linux/mutex.h>

/* 函数定义 */
static inline int mutex_is_locked(struct mutex *lock)
{
return atomic_read(&lock->count) != 1;
}

【函数说明】该函数用于判断 mutex 是否被获取。

【函数参数】

  • lock :struct mutex * 类型,表示要判断的互斥锁。

【返回值】int类型,已经被获取返回1,否则返回0。

【使用格式】none

【注意事项】 none

4. 使用步骤

  • (1)定义一个互斥锁;
  • (2)初始化互斥锁;
  • (3)需要加锁的地方获取互斥锁,加锁的代码执行完毕一定要释放互斥锁,互斥锁的获取和释放必须成对出现。

四、信号量

1. 信号量简介

Linux 内核提供了信号量机制,信号量常常用于控制对共享资源的访问

有这么一个例子,某个停车场有 100 个停车位,这 100 个停车位大家都可以用,对于大家来说这100 个停车位就是共享资源。假设现在这个停车场正常运行,我们要把车停到这个这个停车场肯定要先看一下现在停了多少车了?还有没有停车位?当前停车数量就是一个信号量,具体的停车数量就是这个信号量值,当这个值到 100 的时候说明停车场满了。停车场满的时我们可以等一会看看有没有其他的车开出停车场,当有车开出停车场的时候停车数量就会减一,也就是说信号量减一,此时我们就可以把车停进去了,我们把车停进去以后停车数量就会加一,也就是信号量加一。这就是一个典型的使用信号量进行共享资源管理的案例,在这个案例中使用的就是计数型信号量。

相比于自旋锁,信号量可以使线程进入休眠状态,使用信号量会提高处理器的使用效率,毕竟不用一直在那里“自旋”等待。但是,信号量的开销要比自旋锁大,因为信号量使线程进入休眠状态以后会切换线程,切换线程就会有开销。

信号量的特点:

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

(2)因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。

(3)如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。

信号量有一个信号量值,相当于一个房子有 10 把钥匙,这 10 把钥匙就相当于信号量值为10。因此,可以通过信号量来控制访问共享资源的访问数量,如果要想进房间,那就要先获取一把钥匙,信号量值减 1,直到 10 把钥匙都被拿走,信号量值为 0,这个时候就不允许任何人进入房间了,因为没钥匙了。如果有人从房间出来,那他要归还他所持有的那把钥匙,信号量值加 1,此时有 1 把钥匙了,那么可以允许进去一个人。相当于通过信号量控制访问资源的线程数,在初始化的时候将信号量值设置的大于 1,那么这个信号量就是计数型信号量,计数型信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源。如果要互斥的访问共享资源那么信号量的值就不能大于 1,此时的信号量就是一个二值信号量。

【适用场合】任务上下文之间且临界区执行时间较长时的互斥或同步问题。

2. 相关结构体

Linux 内核使用 semaphore 结构体表示信号量 ,该结构体定义在linux内核源码的这个文件中:

1
include/linux/semaphore.h

我们打开这个文件,会看到这个结构体定义如下:

1
2
3
4
5
6
7
/* Please don't access any members of this structure directly */
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};

3. 信号量操作API

3.1 sema_init()

我们使用以下命令查询一下函数所在头文件:

1
grep sema_init -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
9
10
/* 需包含的头文件*/
#include <linux/semaphore.h>

/* 函数定义 */
static inline void sema_init(struct semaphore *sem, int val)
{
static struct lock_class_key __key;
*sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}

【函数说明】该函数初始化信号量。

【函数参数】

  • sem :struct semaphore * 类型,表示要初始化的信号量。
  • val :int 类型,表示信号量的初始值。

【返回值】none

【使用格式】none

【注意事项】 none

3.2 down()

我们使用以下命令查询一下函数所在头文件:

1
grep down -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
/* 需包含的头文件*/
#include <linux/semaphore.h>

/* 函数声明 */
extern void down(struct semaphore *sem);

【函数说明】该函数用于获取信号量,因为会导致休眠(浅度休眠),因此不能在中断中使用。使用 down 进入休眠状态的线程不能被信号打断。

【函数参数】

  • sem :struct semaphore * 类型,表示要获取的信号量。

【返回值】none

【使用格式】none

【注意事项】 none

3.3 down_interruptible()

我们使用以下命令查询一下函数所在头文件:

1
grep down_interruptible -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
/* 需包含的头文件 */
#include <linux/semaphore.h>

/* 函数声明 */
extern int __must_check down_interruptible(struct semaphore *sem);

【函数说明】该函数用于获取信号量,也会导致休眠(浅度休眠),使用此函数进入休眠以后是可以被信号打断的。

【函数参数】

  • sem :struct semaphore * 类型,表示要获取的信号量。

【返回值】int 类型,没怎么用这个函数,用到了再补充。

【使用格式】none

【注意事项】 none

3.4 up()

我们使用以下命令查询一下函数所在头文件:

1
grep up -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
/* 需包含的头文件 */
#include <linux/semaphore.h>

/* 函数声明 */
extern void up(struct semaphore *sem);

【函数说明】该函数用于释放信号量。

【函数参数】

  • sem :struct semaphore * 类型,表示要释放的信号量。

【返回值】none

【使用格式】none

【注意事项】 none

4. 使用步骤

  • (1)定义一个信号量;
  • (2)初始化信号量;
  • (3)需要保护的代码段申请信号量,当执行完毕后要释放信号量,需要注意申请信号量和释放信号量要成对出现。