0%

进程通信-信号量

阅读更多

1 信号量(semophore)

1.1 两个概念

  1. 临界资源:一次只允许一个进程(一个线程)使用的资源叫做临界资源
  2. 临界区:访问临界资源的代码称为临界区

1.2 什么是信号量

信号量是一个计数器,其本质是一种数据操作锁,它本身不具有数据交换的功能,而是通过控制其他的通信资源(文件,外部设备)来实现进程间通信,它本身只是一种外部资源的标识。信号量在此过程中负责数据操作的互斥、同步等功能。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段

1.3 为什么使用信号量

为了防止出现因多个程序同时访问一个共享资源而而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。其中共享内存的使用就要用到信号量。

2 信号量的使用

当请求一个使用信号量来表示的资源时,进程需要先读取信号量的值来判断资源是否可用:

  1. 大于0,资源可以请求,将信号量的值-1(P操作)
  2. 等于0,无资源可用,进程会进入睡眠状态直至资源可用

当进程不再使用一个信号量控制的共享资源时,信号量的值+1(V操作)

PV操作均为原子操作。这是由于信号量主要的作用是维护资源的互斥或多进程的同步访问。而在信号量的创建及初始化上,不能保证操作均为原子性(SystemV版本信号量的缺陷),因为这个版本的信号量的创建与初始化是分开的

2.1 信号量的创建

创建信号量的函数声明如下:

1
2
3
4
5
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);
  1. 第一个参数key:依旧是可以通过ftok函数来获得,与消息队列中的key一模一样
  2. 第二个参数nsems:代表信号量的数量,也就是说可以一次性创建多个信号量
  3. 第三个参数semflg:又是和消息队列的flg参数一模一样:
    • IPC_CREAT:存在则打开,否则创建
    • IPC_CREAT | IPC_EXCL存在则出错返回,否则创建,这样保证了打开的是一个全新的信号量集。还是要注意,这个IPC_EXCL单独使用没有任何意义

2.2 信号量的初始化

初始化信号量的函数声明如下:

1
2
3
4
5
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...);
  1. 第一个参数semid:为信号量集的id
  2. 第二个参数semnum:表示给信号量集中的哪一个信号量进行初始化(从0开始,分别表示第一个信号量,第二个信号量,…)
  3. 第三个参数cmd:就是初始化命令了:SETVAL
  4. 最后用可变参数的方式传递信号量的初始值,这个参数的类型是union
1
2
3
4
5
6
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo ***buf;
}

其中val就是信号量的初始值,表示初始时临界资源的个数。其它字段暂时不关心

2.3 信号量的销毁

首先说说如何在终端通过命令查看信号量:ipcs -s。删除是 ipcrm -s 对应sem_id。命令都是类似的。下来是函数声明:

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

对,依旧是这个函数,这个ctl能干的事情多着呢。删除就是一次性删除整个信号量集,所以第二个参数缺省0即可。第三个参数就是删除命令:IPC_RMID

2.4 P操作和V操作

通过semop函数来完成,该函数声明如下:

1
2
3
4
5
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, size_t nsops);

其中sembuf包含如下字段:

1
2
3
unsigned short sem_num;
short sem_op;
shrot sem_flg;
  1. sem_num:表示要对信号量集中的哪一个信号量操作,sem_op为1,表示V操作,表示加1,sem_op为-1,表示P操作,表示减1
  2. sem_flg:信号操作标志,可能的选择有两种
    • IPC_NOWAIT:对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息
    • SEM_UNDO:程序结束时(不论正常或不正常),保证信号值会被重设为semop()调用前的值。这样做的目的在于避免程序在异常情况下结束时未将锁定的资源解锁,造成该资源永远锁定

3 C源码

comm.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#pragma once  

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>

#define _PATH_NAME_ "/tmp"
#define _PROJ_ID_ 0x666

union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo ***buf;
};

int create_sem_set(int nums);
int get_sem_set(int nums);
int init_sem_set(int sem_id, int which, int val);
int P(int sem_id);
int V(int sem_id);
int destroy(int sem_id);

comm.cpp

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
#include "comm.h"  

static int comm_sem_set(int nums, int flag)
{
key_t key = ftok(_PATH_NAME_, _PROJ_ID_);
if (key < 0)
{
perror("ftok");
return -2;
}

return semget(key, nums, flag);
}

int create_sem_set(int nums)
{
int flag = IPC_CREAT | IPC_EXCL | 0644;
return comm_sem_set(nums, flag);
}

int get_sem_set(int nums)
{
int flag = IPC_CREAT;
return comm_sem_set(nums, flag);
}

int init_sem_set(int sem_id, int which, int val)
{
union semun un;
un.val = val;

return semctl(sem_id, which, SETVAL, un);
}

static int pv(int sem_id, int op)
{
struct sembuf buf;
buf.sem_num = 0;
buf.sem_op = op;
buf.sem_flg = 0;

return semop(sem_id, &buf, 1);
}

int P(int sem_id)
{
return pv(sem_id, -1);
}

int V(int sem_id)
{
return pv(sem_id, 1);
}

int destroy(int sem_id)
{
return semctl(sem_id, 0, IPC_RMID);
}

test_sem.cpp

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
#include <stdio.h>  
#include <unistd.h>
#include <stdlib.h>
#include "comm.h"

int main()
{
int sem_id = create_sem_set(1);
init_sem_set(sem_id, 0, 1);

pid_t id = fork();
if (id < 0)
{
perror("fork");
return 1;
}
else if (id == 0) //child
{
while (1)
{
P(sem_id);

printf("A");
fflush(stdout);
usleep(rand()%12345);
usleep(200000);
printf("A");
fflush(stdout);
usleep(rand()%12345);

V(sem_id);
}
}
else //father
{
while (1)
{
P(sem_id);

printf("B");
fflush(stdout);
usleep(rand()%12345);
usleep(200000);
printf("B");
fflush(stdout);
usleep(rand()%12345);

V(sem_id);
}
}

destroy(sem_id);
return 0;
}

4 参考