linux下面据我目前掌握的内容,一共有两类信号量,一类是POSIX信号量,一类是SystemV信号量.用法类似.名字也类似.不过范围不同.POSIX信号量分为两类,一类用于线程间,一类用于进程间,而SystemV只有一类,就是进程间通信.当然如果你喜欢也可以把用于进程间通信的信号量拿来放到线程间.
首先我们需要了解的一个概念应该是临界资源和临界区.所谓临界资源即多个进程同时访问的一个资源.此资源的特点是同一时间只能有一个进程对它进行访问.而临界区的概念,就是我们的程序中,负责访问临界资源的那段代码,一般也就是那么几行而已.
想要对一段代码进行独占式的访问,方法有很多.后续我将分享一篇自己写的自旋锁,也是独占式访问的一种方式.但是它的缺点也特别明显,由于这是一种忙等待的方式,如果等待时间过长,势必会影响到整个系统的效率和性能.并且这种方式并不支持进程间的互斥.还有很多其它的方式,由于本文分享的是SystemV信号量,所以其它的缺点自然就要罗列一下.比如通过open的原子调用结合O_EXCL来实现进程间互斥.的确可以达到效果.但是文件操作无疑要进行磁盘读写,这个效率相比内存就大打折扣了.
荷兰数学家迪杰斯特拉提出的信号量概念完美的解决了我们的问题.信号量是一个特殊的变量,只能取正值,且程序对其的访问是原子操作.涉及到了信号量,就不得不讲一下PV原语.
P : 如果信号量大于0,信号量-1,如果信号量为0, 则阻塞等待.
V : 如果没有进程等待,信号量+1, 如果此时存在阻塞,则解除一个等待.
头文件<sys/sem.h>
一般而言,这个头文件中已经包含了sys/types.h 和sys/ipc.h ,所以可以不用重复包含.
函数如下:
int semget(key_t key, int num, int flag);
int semctl(int semid, int sem_num, int command, ...);
int semop(int semid, struct sembuf, size_t num_sem_buf);
下面单独介绍:
semget 创建一个信号量独一无二的标记符.参数key_t为一个独一无二的键值.通过这个键值,我们可以实现进程间共享这个信号量.如果不想进程间使用,可以把这个键值指定为IPC_PRIVATE. 参数二指定信号量个数,注意是个数,不是值.这是SystemV信号量和POSIX的一个区别,严格意义上,SystemV的这组信号量API提供的是对一组信号量操作的方式.而不是一个.当然这个值大部分情况下都是1.flag参数指定了创建信号量的权限和操作,低九位表示权限.与IPC_CREAT 和 IPC_EXCL做或操作.如果指定了IPC_EXCL,表示如果信号量已经存在,则创建失败,如果没有这个选项,当信号量已经存在是,直接获取到这个信号量.
semget的返回值,如果成功,返回semid供semop和semctl来进行使用,如果失败返回-1
semop 这个就是我们所说的PV操作的执行者.首先得了解一下这个sembuf结构体.
struct sembuf{
int sem_num; 指定对信号量组的哪个进行操作,一般只有一个组,这个值取0
int op; +/-1 可以看到,如果是+1就是V操作,如果是-1就是P操作.
int flag; 指定一些特殊的操作.我所知道的只有一个标记SEM_UNDO.即当进程意外终止时,由系统回收这个信号量,不至于浪费系统资源.一般来说,建议加上这个标记.因为信号量不会自动消失.
}
semop返回值,如果成功返回0,如果失败返回-1
semctl 提供了一组操作信号量的方式.比如设定信号量初值.删除信号量.获取信号量状态等.
首先第一个参数就是semget返回的信号量标记.
第二个参数,指定信号量组,一般情况只要取0就可以了.
第三个参数指定对这组信号量执行的操作.常用的有IPC_RMID(删除一个信号量), SETVAL(设定信号量初值)
当这个参数是SETVAL的时候需要第四个参数,union semun
union semun{
int val; 设定信号量初值,这个值才是用来PV操作的值.
struct sem_ds *buf; 没用过
unsigned short *array; 没有用过.
}
这个联合体体,有的头文件中包含了,有的没有.如果在编译的时候报了没有这个联合体的错误.需要自己定义一个.
semctl的返回值,根据第三个参数的不同而不同,不过SETVAL和IPC_RMID是一样的,成功返回0, 失败返回-1
下面我们用信号量完成一个例子.我们首先创建一个文件,一个进程写,一个进程来读.刚刚启动时,读写进程执行P操作(这里有个细节,如果读进程获取到了资源怎么办,其实无所谓,就让他去读,反正读不到东西就执行V操作了),写进程写入一行以后执行V操作.循环执行,知道写进程输入end.ok,我们来试试吧.
好吧.被打脸了.看书的时候,书中的是单进程的例子.而我实验的是多进程的.从两点钟一直搞到四点.各种问题.
1. 进程卡在了semop 的P操作.原因是我把第三个参数搞错了.第三个参数是第二个参数的个数,不是第二个参数的大小.
2. semop产生invalid argument 错误.这个问题让我郁闷了半天.本以为一个进程已经给了信号量初值了.第二个应该就不要给了吧.可是没想到,还是得给.
3. semget 初次调用的时候,要指定信号量的组数.后面再次调用semget的时候就不要再指定了,所以这个参数置成0
4. 权限问题.如果一个权限高的用户创建了其中一个进程并且创建了信号量,那么当另外一个进程尝试执行P操作的时候就会失败,提示没有权限.
ok.上面就是我遇到的问题.下面贴出例子代码.当然有点小BUG .主线功能并没有影响.
write.c
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/sem.h> #include <errno.h> //通过man semctl就可以看到这个结构体,如果里面提到需要自己定义,拷出来就行了 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) */ }; #define FILENAME "alai" int main(int argc, char ** argv) { int ret; int runing = 1; int fd; char buffer[BUFSIZ] = {0}; int sem_id; struct sembuf sem_p; struct sembuf sem_v; union semun sem_un; sem_id = semget((key_t)123456789, 1, 0666|IPC_CREAT|IPC_EXCL); if (sem_id > 0) { sem_un.val = 1; ret = semctl(sem_id, 0, SETVAL, sem_un); if (ret < 0 ) { perror("\n"); exit(EXIT_FAILURE); } } else if (errno == EEXIST) { sem_id = semget((key_t)123456789, 0, 0666|IPC_CREAT); } else { perror("\n"); exit(EXIT_FAILURE); } ret = semctl(sem_id, 0, GETVAL, &sem_un); sem_p.sem_num = 0; sem_p.sem_op = -1; sem_p.sem_flg = SEM_UNDO; sem_v.sem_num = 0; sem_v.sem_op = +1; sem_v.sem_flg = SEM_UNDO; while (runing) { ret = semop(sem_id, &sem_p, 1); if (ret == -1 ) { perror("\n"); exit(EXIT_FAILURE); } fd = open(FILENAME, O_WRONLY | O_CREAT | O_TRUNC); printf("please input some text:\n"); ret = read(fileno(stdin), buffer, BUFSIZ); if (0 != ret) { write(fd, buffer, ret); } if (0 == strncmp(buffer, "end", 3)) { runing = 0; } bzero(buffer, BUFSIZ); close(fd); semop(sem_id, &sem_v, 1); } ret = semctl(sem_id, 0, IPC_RMID, sem_un); return 0; }
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/sem.h> #include <errno.h> //通过man semctl就可以看到这个结构体,如果里面提到需要自己定义,拷出来就行了 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) */ }; #define FILENAME "alai" int main(int argc, char ** argv) { int ret; int runing = 1; int fd; char buffer[BUFSIZ] = {0}; int sem_id; struct sembuf sem_p; struct sembuf sem_v; union semun sem_un; sem_id = semget((key_t)123456789, 1, 0666 | IPC_CREAT | IPC_EXCL); if (sem_id > 0) { sem_un.val = 1; ret = semctl(sem_id, 0, SETVAL, sem_un); if (ret == -1 ) { perror("\n"); exit(EXIT_FAILURE); } } else if (EEXIST == errno) { sem_id = semget((key_t)123456789, 0, 0666|IPC_CREAT); } else { perror("\n"); exit(EXIT_FAILURE); } sem_p.sem_num = 0; sem_p.sem_op = -1; sem_p.sem_flg = SEM_UNDO; sem_v.sem_num = 0; sem_v.sem_op = +1; sem_v.sem_flg = SEM_UNDO; while (runing) { ret = semop(sem_id, &sem_p, 1); if (ret == -1 ) { perror("\n"); exit(EXIT_FAILURE); } fd = open(FILENAME, O_RDONLY); if (-1 == fd) { perror("\n"); exit(EXIT_FAILURE); } read(fd, buffer, BUFSIZ); if (strncmp(buffer, "end", 3) == 0) { runing = 0; } printf("I have read %s\n", buffer); bzero(buffer, BUFSIZ); close(fd); semop(sem_id, &sem_v, 1); } ret = semctl(sem_id, 0, IPC_RMID, sem_un); return 0; }
运行结果如下:
关于信号量,就分享这么多.相信同学们看了这篇文章应该能够简单的入门.不过建议例子代码不要复制粘贴看结果,而是真正自己敲一遍,这样才能够理解的更透彻.