一、什么是信号量?
信号量的本质就是计数器,记录临界资源的数目,用来协助进程同步互斥的访问临界资源。为什么不在进程中定义一个全局变量作为计数器呢?两个进程间的地址空间是各自独立的,各自有各自的虚拟内存空间。多进程之间不能看到各自进程中的全局变量。(进程间的虚拟内存https://blog.csdn.net/jane_yao/article/details/81635979)既然多个进程都能对信号量进行操作,那信号量本身也是临界资源。
因为信号量本身也是临界资源,所以对信号量的增加减少操作是原子的PV操作(不可中断的)。
二、信号量的操作
systemV版本的信号量申请时是以信号量集为单位申请的 ,P操作为申请资源(资源数目减一)V操作释放资源(资源数目加一)申请不到资源就挂起等待。
1.创建: int semget(key_t key, int nsems, int semflg)
参数1key用ftok获取key_t ftok(const char *pathname, int proj_id)参数2所申请信号量集中的元素的个数,最少申请一个参数3为创建IPC_CREAT | IPC_EXCL。返回值是创建的信号集标号。
2.初始化/删除:初始化和删除都是用的是 int semctl(int semid, int semnum, int cmd, ...);
参数1对标号semid的信号集操作,参数2信号集中下标为semnum的信号量进行设置,参数三创建时使用SETVAL删除时就直接使用IPC_RMID,初始化时有可变参数初始化就自己定义联合体
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO (Linux-specific) */
};SETVAL)
其中int val表示初始化时设置为几。
3.PV操作:int semop(int semid, struct sembuf *sops, unsigned nsops);
参数1对标号semid的信号量集进行操作,参数2信号量集中可能有多个信号量所以使用了系统自带的结构体sembuf
系统自带的sembuf的内容为
unsigned short sem_num; /* semaphore number */ //对哪个信号量进行操作 ,信号量在数组中的下标
short sem_op; /* semaphore operation */ //-1为P操作1为V操作
short sem_flg; /* operation flags */ //默认设置为0
参数3信号操作结构的数量,就是1
三、使用函数创建二元信号量实现互斥锁
创建两个进程,子进程打印C父进程打印P
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4 #include <sys/ipc.h>
5 #include <sys/sem.h>
6 #define PATH_NAME "/tmp"
7 #define PROJ_ID 0x5555
8
9 union semun {
10 int val; /* Value for SETVAL */
11 struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
12 unsigned short *array; /* Array for GETALL, SETALL */
13 struct seminfo *__buf; /* Buffer for IPC_INFO
14 (Linux-specific) */
15 };
16 //初始化的flg为SETVAL,后为自己定义的联合体,联合体的值为要设置的值
17 void initSem(int semid, int num,int val)
18 {
19 union semun arg;
20 arg.val = val;
21 semctl(semid,num,SETVAL,arg);
22 }
23
24 static void PV(int semid,int num,int op)
25 {
26 struct sembuf _sf;
27 _sf.sem_num = num;
28 _sf.sem_op = op;
29 _sf.sem_flg = 0;
30 semop(semid,&_sf,1);
31 }
32 void P(int semid,int num)
33 {
34 PV(semid,num, -1);
35 }
36
37 void V(int semid,int num)
38 {
39 PV(semid,num, 1);
40 }
41
42 int main()
43 {
44 key_t k = ftok(PATH_NAME,PROJ_ID);
45 if(k<0)
46 {
47 printf("ftok error\n");
48 return 1;
49 }
50 int semid = semget(k,1,IPC_CREAT|IPC_EXCL|0666);
51 if(semid<0)
52 {
53 return 2;
54 }
55 sleep(5);
56 //初始化和删除都用到semctl,参数不同
57 //初始化时可变参数列表传结构体
58 initSem(semid,0,1);//对信号集中下标为0的信号进行初始化,二元信号量所以最后一个参数为1
59 //PV操作
60 pid_t id = fork();
61 if(id == 0)
62 {
63 //child
64 while(1)
65 {
66 P(semid,0);
67 //打A时不能被干扰,实现互斥操作
68 printf("C");
69 usleep(12345);
70 fflush(stdout);
71 printf("C ");
72 usleep(32456);
73 fflush(stdout);
74 V(semid,0);
75 }
76 }
77 else
78 {
79 while(1)
80 {
81 P(semid,0);
82 printf("P");
83 usleep(10323);
84 fflush(stdout);
85 printf("P ");
86 usleep(34252);
87 fflush(stdout);
88 V(semid,0);
89 }
90 }
91 //删除
92 semctl(semid,0,IPC_RMID);
93 }
如果不加互斥锁则打印出的结果为:
可以看到打印P的进程可能会被打断,去打印C
加入互斥锁之后结果为:
就是想要的结果啦~
四、一些注意事项
和消息队列、共享内存一样生命周期随内核,进程结束不能自动释放必须使用ipcrm或者在程序末尾调用semctl.
POSIX信号量和systemV信号量作用相同,都是用于同步操作,但是POSIX可以用于线程间同步
为了解决进程间通信问题引入了临界资源,多个线程对临界资源的访问又会产生问题,为了保护临界资源引入了同步与互斥机制,为了实现同步与互斥引入信号量(进程中)、互斥锁(mutex_lock线程中)。