一、信号量

线程的信号量与进程间通信中使用的信号量的概念是一样,它是一种特殊的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作。如果一个程序中有多个线程试图改变一个信号量的值,系统将保证所有的操作都将依次进行。

而只有0和1两种取值的信号量叫做二进制信号量,在这里将重点介绍。而信号量一般常用于保护一段代码,使其每次只被一个执行线程运行。我们可以使用二进制信号量来完成这个工作。

C语言中的信号量分为两种,POSIX信号量和Syetem -V信号量

二、POSIX信号量

信号量的函数都以sem_开头,线程中使用的基本信号量函数有4个,它们都声明在头文件semaphore.h中。

1、sem_init函数

该函数用于创建信号量,其原型如下

int sem_init(sem_t *sem,int pshared,unsigned int value);

该函数初始化由sem指向的信号对象,设置它的共享选项,并给它一个初始的整数值。
pshared控制信号量的类型,如果其值为0,就表示这个信号量是当前进程的局部信号量,否则信号量就可以在多个进程之间共享,value为sem的初始值。调用成功时返回0,失败返回-1.

2、sem_wait函数

该函数用于以原子操作的方式将信号量的值减1。原子操作就是,如果两个线程企图同时给一个信号量加1或减1,它们之间不会互相干扰。它的原型如下:

int sem_wait(sem_t *sem);

等待信号量,如果信号量的值大于0,将信号量的值减1,立即返回。如果信号量的值为0,则线程阻塞。相当于P操作。成功返回0,失败返回-1。sem指向的对象是由sem_init调用初始化的信号量。

3、 sem_post函数

该函数用于以原子操作的方式将信号量的值加1。它的原型如下:

int sem_post(sem_t *sem);

释放信号量,让信号量的值加1。相当于V操作。与sem_wait一样,sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1.

4、sem_destroy函数

该函数用于对用完的信号量的清理。它的原型如下:

int sem_destroy(sem_t *sem);

成功时返回0,失败时返回-1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>

sem_t sem;

void func1(void* arg)
{
sem_wait(&sem);
int *running=arg;
printf("thread running1\n");
printf("%d\n",*running);
}

void func2(void* arg)
{
printf("pthread2 running\n");
sem_post(&sem);
}

int main()
{
sem_init(&sem,0,0);
pthread_t thread[2];
int a=5;
pthread_create(&(thread[0]),NULL,(void*)func1,(void*)&a);
printf("main thread running\n");
sleep(10);
pthread_create(&(thread[1]),NULL,(void*)func2,(void*)&a);
printf("main thread running2\n");
pthread_join(thread[0],NULL);
pthread_join(thread[1],NULL);
sem_destroy(&sem);

return 0;
}

首先创建信号量,第二个参数为0,表示这个信号量是当前进程的局部信号量,初始值为0。

然后使用pthread_create()函数创建两个线程,传入参数a。

线程1创建完成后,由于信号量初始化value=0,调用sem_wait会阻塞这个线程,信号量的值将减少1(此时<0),这个线程函数就会等待。

主线程sleep(10)后,线程2创建并执行调用,sem_post用于使信号量的值增加1,此时线程1可以执行。

使用pthread_join等待所创建的两个线程的结束。

以上代码输出为:

1
2
3
4
5
main thread running
main thread running2
pthread2 running
thread running1
5

可以用来做线程同步使用,比如升级流程代码中,可以在升级的初始化代码中使用sem_wait,最终被阻塞在那里,而用户执行升级命令的流程中才会执行到sem_post函数,从而触发到sem_wait后面的流程继续执行。

三、System -V信号量

1、创建SEM

功能:获取信号量ID

int semget(key_t key,int nsems,int semflg);

返回值:
成功:信号量ID
失败:-1
参数:
key:信号量的键值
nsems:信号量元素的个数
semflg:创建选项
IPC_CREAT:如果key对应的信号量不存在,则创建
IPC_EXCL:如果key对应的信号量存在,则报错
mode:信号量的访问权限
创建信号量时,还受到以下系统信息的影响

SEMMNI:系统中信号量的总数最大值
SEMMSL:每个信号量中信号量元素的个数最大值
SEMMNS:系统中素有信号量中的信号量元素的总数最大值
实例代码:

1
2
3
4
5
int main(void) 
{
key_t key = ftok(".",1);
int semid = semget(key,2,IPC_CREATE|0666);
}

2、PV操作

功能:对信号量进行PV操作,或者等零操作

int semop(int semid,struct sembuf sops[],unsigned nsops);

返回值:
成功:0
失败:-1

参数:
semid:信号量ID
sops:信号量操作结构体数组
nsops结果体数组元素个数
信号量操作结构体

1
2
3
4
5
struct sembuf { 
unsigned short sem_num;//信号量元素序号
short sem_op;//操作参数
short sem_flg;//操作选项
}

根据sem_op的值,信号量操作分为3中情况:
sem_op>0:V操作
sem_op=0:等零操作
sem_op
示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
int main() { 
key_t key = ftok(".", 1); // 创建(若已有则打开)一个包含2个元素的信号量组
int id = semget(key, 2, IPC_CREAT|0666); // 定义包含两个P/V操作的结构体数组
struct sembuf op[2];
op[0].sem_num = 0; // 信号量元素序号
op[0].sem_op = -2; // P操作
op[0].sem_num = 0; // 选项默认0
op[1].sem_num = 1; // 信号量元素序号
op[1].sem_op = +3; // V操作
op[1].sem_num = 0; // 选项默认0
// 同时对第0、1号信号量元素分别进行P、V操作
semop(id, op, 2);
}

3、获取或设置信号量的相关属性

int semctl(int semid,int semnum,int cmd....)

参数:
semid:信号量ID
semnum:信号量元素序号
cmd
IPC_STAT:获取属性信息
IPC_SET:设置属性信息
IPC_RMID:立即删除该信号量,参数semnum将被忽略
IPC_INFO:获得关于信号量的系统限制信息
SEM_INFO:获得系统为共享内存消耗的资源信息
SEM_STAT:获得系统中所有SEM的相关信息
GETALL:返回所有信号量元素的值,参数semnum将被忽略
GETNCNT:返回正阻塞在对该吸纳后量元素P操作的进程总数
GETPID:返回最后一个队该信号量元素操作的进程PID
GETVAL:返回该信号量元素的值
GETZCNT:返回正阻塞对该信号量元素等零操作的进程总数
SETALL:返回所有信号量元素的值,参数semnum将被忽略
SETVAL:设置该信号量元素的值
返回值
GETNCNT:semncnt
GETPID:sempid
GETVAL:semval
GETZCNT:semzcnt
IPC_INFO:内核中记录所有SEM信息的数组的下标最大值
SEM_INFO:同IPC_INFO
SEM_STAT:内核中看i路所有SEM信息的数组
失败:-1
这是一个变参函数,根据cmd的不同,可能需要第四个参数,第四个参数是一个如下所示的联合体,用户需要自己定义

1
2
3
4
5
6
union semun { 
int val;//当cmd为SERVAL时使用
struct semid_ds *buf;//当cmd为IPC_STAT或IPC_SET时使用
unsigned short *array;//当cmd为GETALL或SETALL时使用
struct seminfo *_buf;//当cmd为IPC_INFO时使用
}

当cmd为IPC_STAT或IPC_SET时,需要用到一下属性信息结构体

1
2
3
4
5
6
struct semid_ds { 
struct ipc_perm sem_perm;//权限相关信息
time_t sem_otime;//最后一次semop()的信息
tien_t sem_ctime;//最后一次状态改变时间
unsigned short sem_nsems;//信号量元素个数
}

使用例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
int my_mutex_p(key_t sem_tag, int *sem_id, unsigned int timeout)
{
int ret;
int semno = 0;
int val;
int semid =0;
struct sembuf smbf[2] = {{0}};

if (sem_id == NULL) {
return -EINVAL;
}

ret = semget(sem_tag, 1, IPC_CREAT | MSG_SEM_PERMISSION_RW_GROUP | IPC_EXCL);
if (ret < 0) {
if (errno == EEXIST) {
semid = semget(sem_tag, 1, 0);
if (semid < 0) {
return -EINVAL;
}
} else {
return -EINVAL;
}
} else {
semid = ret;
val = 1;
ret = semctl(semid, semno, SETVAL, val);
if (ret < 0) {
return -EINVAL;
}
}
*sem_id = semid;
smbf[0].sem_num = 0;
smbf[0].sem_op = -1;
smbf[0].sem_flg = (timeout == MUTEX_WAIT_FOR_EVER) ? SEM_UNDO : (SEM_UNDO | IPC_NOWAIT);

ret = semop(semid, smbf, 1);
if (ret == 0) {
return 0;
} else if (errno == EAGAIN) {
printf("have another process use\n");
} else {
printf("semop fail\n");
}

return -EINVAL;
}

int my_mutex_v(int sem_id)
{
int ret;
struct sembuf smbf;
smbf[0].sem_num = 0;
smbf[0].sem_op = 1;
smbf[0].sem_flg = SEM_UNDO | IPC_NOWAIT;

ret = semop(semid, &smbf, 1);
if (ret != 0) {
return -EINVAL;
}
return ret;
}

四、锁

互斥量从本质上说就是一把锁, 提供对共享资源的保护访问。

1、初始化

在Linux下, 线程的互斥量数据类型是pthread_mutex_t. 在使用前, 要对它进行初始化:

对于静态分配的互斥量, 可以把它设置为PTHREAD_MUTEX_INITIALIZER, 或者调用pthread_mutex_init.
对于动态分配的互斥量, 在申请内存(malloc)之后, 通过pthread_mutex_init进行初始化, 并且在释放内存(free)前需要调用pthread_mutex_destroy.

原型:

`int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restric attr);

int pthread_mutex_destroy(pthread_mutex_t *mutex);

头文件:#include<pthread.h>

返回值: 成功则返回0, 出错则返回错误编号.

说明: 如果使用默认的属性初始化互斥量, 只需把attr设为NULL. 其他值在以后讲解。

2、互斥操作:

对共享资源的访问, 要对互斥量进行加锁, 如果互斥量已经上了锁, 调用线程会阻塞, 直到互斥量被解锁. 在完成了对共享资源的访问后, 要对互斥量进行解锁。

首先说一下加锁函数:

头文件:#include<pthread.h>

原型:

1
2
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);

返回值: 成功则返回0, 出错则返回错误编号.

说明: 具体说一下trylock函数, 这个函数是非阻塞调用模式, 也就是说, 如果互斥量没被锁住, trylock函数将把互斥量加锁, 并获得对共享资源的访问权限; 如果互斥量被锁住了, trylock函数将不会阻塞等待而直接返回EBUSY, 表示共享资源处于忙状态。

再说一下解锁函数:
原型:

int pthread_mutex_unlock(pthread_mutex_t *mutex);

返回值: 成功则返回0, 出错则返回错误编号.


描述文字
© 2024 mengchao666 开心地使用 Stellar 主题,星际地球第
收集了 62 颗星星 ✧ 共 83.3k 粒星尘,好像写完一本 鲁迅 的《呐喊》了啊