并发控制:进程通信之信号量

  信号量通常用于进程并发控制,此处并发有两个含义:进程共享资源的互斥,进程时序关系控制。这两种方式也是信号量最常见的应用。互斥量作为共享资源互斥最常用的方式,只能用于单一进程(要实现多进程,可以采用共享内存映射某个互斥量,但一般不这么做)。在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 }
View Code

以上代码在进程通信之共享内存中出现过,现在为了演示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具有原子性,该数组中的操作要么全部执行,要么都不执行。

 

并发控制:进程通信之信号量

上一篇:linux下.*.*.swp文件是什么?


下一篇:ubuntu18.04.4 配置 NFS 服务器