众所周知,每个进程使用的数据是是它独有的,即使之前没有,它也会在自己的内存区域拷贝一个(这里不考虑共享内存机制)。
如果进程间需要通信,有共享资源时,信号,管道,消息队列,信号量,共享内存,这几种技术都可以帮我们。这里先介绍信号量
信号量定义:
为了防止出现因多个程序同时访问一个共享资源而引发的一些问题,我们需要任一时刻只有一个执行线程访问代码的临界区域。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它。
信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。
这里二进制信号量是最常见的信号量,本文也重点讨论二进制信号量
工作原理:
信号量只能进行两种操作(等待和发送信号),即PV操作
P:如果信号量变量>=0,就将其减-,如果值为0,就挂起该进程的执行
V:如果有其他进程因等待信号量变量而被挂起,就将其恢复,如果没有,就将变量加-;
通俗一点的说就是:如果一个进程执行P操作进入了临界区,变量变为0,而另一个进程执行P的时候发现变量为0,将被挂起。而第一个进程执行完了后执行V操作时发现第二个进程在等待该变量,就将其恢复,这时变量没变值。
函数:
1:semget函数:创建一个新信号量或者取消一个已有信号量
int semget(key_t key, int num_sems, int sem_flags);
第一个参数key是整数值(唯一非零),不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,程序先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有semget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。
第二个参数num_sems指定需要的信号量数目,它的值几乎总是1。
第三个参数sem_flags是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。
semget函数成功返回一个相应信号标识符(非零),失败返回-1.
2、semop函数:改变信号量的值
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
sem_id是由semget返回的信号量标识符,sembuf结构的定义如下:
struct sembuf{ short sem_num;//除非使用一组信号量,否则它为0 short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作, //一个是+1,即V(发送信号)操作。 short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号, //并在进程没有释放该信号量而终止时,操作系统释放信号量 };
3、semctl函数: 直接控制信号量信息
int semctl(int sem_id, int sem_num, int command, ...);
如果有第四个参数,它通常是一个union semum结构,定义如下:
union semun{ int val; struct semid_ds *buf; unsigned short *arry; };
这里举个进程使用信号量来通信的例子: 例子是两个相同的程序同时向屏幕输出字符,这个例子要同一时间只有一个进程输出字符
#include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <sys/sem.h> union semun { int val; struct semid_ds *buf; unsigned short *arry; }; static int sem_id = 0; static int set_semvalue(); static void del_semvalue(); static int semaphore_p(); static int semaphore_v(); int main(int argc, char *argv[]) { char message = 'X'; int i = 0; //创建信号量 sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT); if(argc > 1) { //程序第一次被调用,初始化信号量 if(!set_semvalue()) { fprintf(stderr, "Failed to initialize semaphore\n"); exit(EXIT_FAILURE); } //设置要输出到屏幕中的信息,即其参数的第一个字符 message = argv[1][0]; sleep(2); } for(i = 0; i < 10; ++i) { //进入临界区 if(!semaphore_p()) exit(EXIT_FAILURE); //向屏幕中输出数据 printf("%c", message); //清理缓冲区,然后休眠随机时间 fflush(stdout); sleep(rand() % 3); //离开临界区前再一次向屏幕输出数据 printf("%c", message); fflush(stdout); //离开临界区,休眠随机时间后继续循环 if(!semaphore_v()) exit(EXIT_FAILURE); sleep(rand() % 2); } sleep(10); printf("\n%d - finished\n", getpid()); if(argc > 1) { //如果程序是第一次被调用,则在退出前删除信号量 sleep(3); del_semvalue(); } exit(EXIT_SUCCESS); } static int set_semvalue() { //用于初始化信号量,在使用信号量前必须这样做 union semun sem_union; sem_union.val = 1; if(semctl(sem_id, 0, SETVAL, sem_union) == -1) return 0; return 1; } static void del_semvalue() { //删除信号量 union semun sem_union; if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1) fprintf(stderr, "Failed to delete semaphore\n"); } static int semaphore_p() { //对信号量做减1操作,即等待P(sv) struct sembuf sem_b; sem_b.sem_num = 0; sem_b.sem_op = -1;//P() sem_b.sem_flg = SEM_UNDO; if(semop(sem_id, &sem_b, 1) == -1) { fprintf(stderr, "semaphore_p failed\n"); return 0; } return 1; } static int semaphore_v() { //这是一个释放操作,它使信号量变为可用,即发送信号V(sv) struct sembuf sem_b; sem_b.sem_num = 0; sem_b.sem_op = 1;//V() sem_b.sem_flg = SEM_UNDO; if(semop(sem_id, &sem_b, 1) == -1) { fprintf(stderr, "semaphore_v failed\n"); return 0; } return 1; }
结果如下:
可以发现X和 我输入的第一个参数0都是成对的打印的,这是因为p操作进入临界区后,另一个进程无法进入,只有当前进程再次打印一个字符并执行V操作后,另一个进程才能够进去。
为了验证信号量的作用,这里举个不用信号量的例子:
#include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { char message = 'X'; int i = 0; if(argc > 1) message = argv[1][0]; for(i = 0; i < 10; ++i) { printf("%c", message); fflush(stdout); sleep(rand() % 3); printf("%c", message); fflush(stdout); sleep(rand() % 2); } sleep(10); printf("\n%d - finished\n", getpid()); exit(EXIT_SUCCESS); }
结果:
结果很明显了,0X并不是成对打印的。