并发控制:进程通信之信号量
信号量通常用于进程并发控制,此处并发有两个含义:进程共享资源的互斥,进程时序关系控制。这两种方式也是信号量最常见的应用。互斥量作为共享资源互斥最常用的方式,只能用于单一进程(要实现多进程,可以采用共享内存映射某个互斥量,但一般不这么做)。在Linux操作系统中,有两种类型的信号量:XSI信号量和POSIX信号量。本章分别讲解以上两种信号量。
1. POSIX信号量
POSIX信号量是我们最常用的信号量,其又分为命名信号量和无名信号量。无名信号量只能存在于内存中,这意味着它只能用于同一进程中;命名信号量则可以被任何知道它们名字的进程使用。
1.1 命名信号量
先来看看命名信号量的相关使用函数:
#include <semaphore.h> /* 1. 创建或打开信号量,只有两个参数时,为打开信号量;四个参数时,为创建信号量。使用方法,oflag和mode参数与open()函数一模一样; 2. 通常创建时的方式为: sem1=sem_open(name1,O_CREAT|O_EXCL,00777,初始值);如果成功,返回信号量;失败,返回SEM_FAILED */ sem_t *sem_open(const char *name, int oflag); sem_t *sem_open(const char *name, int oflag,mode_t mode, unsigned int value);
int sem_close(sem_t *); //关闭信号量,释放与此信号量相关的资源,如果进程退出时没有关闭信号量,那么将自动关闭,此时信号量的值不会改变,仍会保存下来
/*
信号量的销毁非常重要,稍后将给出一个错误的示例代码,演示没有正确销毁信号量时导致的错误
*/
int sem_unlink(const char *name); //销毁信号量,如果没有打开的信号量引用,直接销毁;如果有打开的引用,延迟到最后一个打开关闭时销毁
//信号量的获取与释放,从函数名就能看出来其使用方法
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
int sem_post(sem_t *sem);
int sem_getvalue(sem_t *restrict, int *restrict);
命名信号量的使用非常简单,但有一点必须注意:使用完信号量之后一定要调用sem_unlink()销毁信号量。在进程通信之共享内存中,用到了命名信号量,先复制其正确代码:
1 #include <sys/mman.h> 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <string.h> 5 #include <fcntl.h> 6 #include <unistd.h> 7 #include <semaphore.h> 8 #include <sys/wait.h> 9 10 #define SIZE sizeof(long) 11 #define loop 10000 12 #define NAME1 "mysem1" 13 #define NAME2 "mysem2" 14 15 static int update(long* ptr) 16 { 17 return ((*ptr)++); 18 } 19 20 21 int main() 22 { 23 int fd,i; 24 25 if((fd=open("/dev/zero",O_RDWR|O_TRUNC))<0) 26 { 27 printf("open file failed\n"); 28 exit(1); 29 } 30 31 long* data; 32 if((data=(long*)mmap(0,SIZE,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0))==MAP_FAILED) 33 { 34 printf("mmap error\n"); 35 exit(1); 36 } 37 close(fd); 38 39 40 pid_t pid; 41 sem_t *sem1,*sem2; 42 sem1=sem_open(NAME1,O_CREAT|O_EXCL,00777,0); 43 sem2=sem_open(NAME2,O_CREAT|O_EXCL,00777,1); 44 45 if(sem1==SEM_FAILED || sem2==SEM_FAILED) 46 { 47 printf("sem open failed\n"); 48 sem_unlink(NAME1); 49 sem_unlink(NAME2); 50 } 51 52 if((pid=fork())<0) 53 { 54 printf("fork error\n"); 55 exit(1); 56 } 57 if(pid>0) 58 { 59 for(i=0;i<loop;i+=2) 60 { 61 sem_wait(sem2); 62 if(update(data)!=i) 63 printf(" parent update error\n"); 64 printf("parent:%d\n",i); 65 sem_post(sem1); 66 } 67 68 //printf("result:%l\n",(*data)); 69 sem_close(sem1); 70 sem_close(sem2); 71 72 sem_unlink(NAME1); 73 sem_unlink(NAME2); 74 int status; 75 wait(&status); 76 } 77 else 78 { 79 for(i=1;i<=loop;i+=2) 80 { 81 sem_wait(sem1); 82 if(update(data)!=i) 83 printf("child update error\n"); 84 printf("child:%d\n",i); 85 sem_post(sem2); 86 } 87 exit(0); 88 } 89 90 exit(0); 91 }
以上代码在进程通信之共享内存中出现过,现在为了演示sem_unlink()的重要作用,对以上代码稍作修改:
1 /* 2 1. 上面代码的43-50行先注释掉,因为此时只创建了sem1,所以程序不能正常运行(此时sem1没有销毁); 3 2. 取消1的注释,再将45-50行改为: 4 */ 5 if(sem1==SEM_FAILED) 6 { 7 printf("sem open failed\n"); 8 sem_unlink(NAME1); 9 } 10 if(sem2==SEM_FAILED) 11 { 12 printf("sem open failed\n"); 13 sem_unlink(NAME2); 14 } 15 16 /* 17 1. 这么看,好像没有什么区别,如果一开始就这么写,当然不会有问题。但是由于1中的操作,之前内存中已经有了sem1; 18 2. 那么再这么修改,由于sem1已经有了,此时会销毁sem1,创建sem2,由于缺少sem1导致程序运行失败; 19 3. 再次运行程序,由于有了sem2,此时会销毁sem2,创建sem1,程序运行失败。 20 4. 不管怎么运行,sem1和sem2只有一个存在。 21 */
通过上面的例子,主要想说明两点:
(1) 如果不调用sem_unlink(),信号量将会一直存在;
(2) 建议多个信号量有一个创建失败,所有信号量都调用一次sem_unlink(),就是为了避免上面的错误示例(这种错误虽然很少发生,但是很难查出来)。
1.2 未命名信号量
未命名信号量只能在进程内部使用,只是在调用和关闭时不同。主要函数为
#include <semphore.h> int sem_init(sem_t* sem,int pshared,unsigned int value); //初始化信号量 int sem_destroy(sem_t* sem); //销毁信号量
除了以上两个函数代替了sem_open,sem_close,sem_unlink,其他函数均一样,此处不再进行程序示例。
2. XSI信号量
XSI信号量相对于POSIX信号量要复杂很多,因为其以下特性:
(1) 信号量并非单个非负值,而是一个或多个信号量的集合。创建信号量时,要指定其中信号数量;
(2) 创建信号量(semget)和初始化信号量(semctl)相互独立,不能原子操作;
(3) XSI信号量不会在进程退出后自动销毁(所有XSI IPC都有这个毛病,包括消息队列,共享存储,信号量)。
每个信号量集合含有一个semid_ds结构体
struct semid_ds { struct ipc_perm sem_perm; unsigned short sem_nsems; //信号数量 time_t sem_otime; time_t sem_ctime;
//可能还有其他成员 }
其中ipc_perm是每一个XSI IPC都有的结构体,见进程通信之共享内存3.1节。每个信号量本身含有一个无名结构体
struct { unsigned short semval; //semphore value pid_t sempid; //pid for last operation unsigned short semncnt; //process awaiting semval>curval unsigned short semzcnt; //process awaiting semval==0 }
使用XSI信号量时,主要用到的函数如下:
#include <sys/sem.h> /*
函数名:semget nsems表示信号集合的信号数量,如果创建新集合,指定nsems;如果引用现有集合,nsems=0 */ int semget(key_t key,int nsems,int flag); /*
函数名:semctl 设置选项,最后一个union参数可选,根据cmd的命令 cmd命令: IPC_STAT:取semid_ds结构体,存于arg.buf中 IPC_SET:按照arg.buf中的semid_ds结构体,设置sem_perm.uid,sem_perm.gid,sem_perm.mode IPC_RMID:删除信号量集合,跟共享存储的IPC_RMID不同,此处立即删除,其他还在引用的进程再次使用该信号量集合时报错EIDRM GETVAL:获取semnum的semval值 SETVAL:设置成员semnum的semval GETPID:获取成员semnum的sempid GETNCNT:获取semnum的semcnt GETALL:获取所有信号量的semval,保存在arg.array中 SETALL:将所有信号量的semval设置为对应的arg.array值 */ int semctl(int semid,int semnum,int cmd,.../*union semun arg*/); union semun { int val; //for SETVAL struct semid_ds* buf; //for IPC_STAT and IPC_SET unsigned short *array //for SETALL and GETALL }
/*
函数名:semop(自动执行信号量集合上的操作数组)
semoparray:sembuf的数组
nops:semoparray中元素个数
*/
int semop(int semid,struct sembuf semoparray[],size_t nops); //sem_buf定义如下:
struct sem_buf
{
usigned short sem_num; //member int set,即信号量集合中信号的序号
short sem_op; //operations
short sem_flag; //IPC_NOWAIT,SEM_UNDO
};
semop使用时主要有以下情况:
(1) sem_op为正值,相当于该信号量加上sem_op的值;如果sem_flag标志为SEM_UNDO,则相当于该信号量减去sem_op的值;
(2) sem_op为负,获取信号量的资源;如果指定了SEM_UNDO,则该信号量加上sem_op的绝对值;
(3) 如果信号量的值减去sem_op的值为负(资源不够),有以下情况:
指定了IPC_NOWAIT,返回EAGAIN错误
未指定IPC_NOWAIT,semzcnt加1,进程挂起直到以下情况发生:资源足够;系统中该信号量删除,返回EIDRM;进程捕捉到中断信号返回,semzcnt减1,函数调用出错并返回EINTR。
注:semop具有原子性,该数组中的操作要么全部执行,要么都不执行。