信号量和共享内存
关于进程间的通信,有很多的方法可以实现。管道、FIFO、消息队列、信号量以及共享内存都可以提供进程间通信功能。本文主要介绍的内容是信号量以及共享内存的使用。
一、 几个概念
理解信号量以及共享内存的概念以及学习对应的接口函数的使用,需要对标识符以及键等概念有所了解。下面我们逐一介绍以上概念。
标识符:
在对文件进行读取和写入操作的时候,我们需要一个对应的文件描述符来作为所有操作的入口。同样的,对信号量和共享内存的操作也使用一个标识符,这个标识符是后续介绍的这两个进程间通信机制的大部分函数接口的参数。
这个标识符的取值和文件标识符的取值不太一样。文件标识符是取最小的可用数字的,而信号量和共享内存的标识符在创建和删除之后,下一次的取值始终是向上递增1的,直到达到系统允许的最大限制值之后,回到0重新开始。
键:
要使多个进程之间可以实现数据的通信,我们必然要提供一种独立于信号量或者共享内存本身的媒介,来作为进程之间通信的桥梁,而这个桥梁即是我们现在将要介绍的键。
任何程序在使用进程间通信机制之前都要先建立对应的IPC(inter process communication)变量,而在建立这些变量的时候,都需要提供一个键。通常我们使用系统提供的接口来产生键值:
#include
key_t ftok(const char *path, in id)
参数:path是系统中存在的一个文件名, id是项目的编号
返回值:若成功返回键值,出错返回(key_t)-1
二、 信号量
当我们在执行进程间通信的程序时,难免会遇到一些临界性的代码。比如有两个程序在访问同一个文件。一个程序向文件写数据,而另一个文件要从文件里读取数据。这个时候问题就出现了:我们必须保证一个程序在没有向文件写完所有数据之前,另一个程序不能读取这个文件里的数据。我们知道文件记录锁可以满足我们的要求,实现数据的同步。而我们将要介绍的信号量也是一种提供数据同步的机制。
2.1 定义
信号量其实是一个计数器,用于计算当前的资源是否可用。为了获得共享资源,并且保持数据的同步,我们可以这样来使用信号量:
1) 测试控制该资源的信号量
2) 若此信号量的值为正,则进程可以使用该资源。对信号量做减1操作,表示他占用了这个资源
3) 若此信号量为的值0,则表示该资源已被占用。进程挂起等待,直到占用资源的进程释放该资源,使信号量的值为正。此时进程被唤醒,又返回至第1)步
通常信号量的值可以取任意正值,代表有多少个共享的资源。但一般情况下,我只使用0和1两个值,分别代表某一个资源的占用与空闲,这也叫做二进制信号量。
信号量集是包含一个或多个信号量的集合,但通常我们只使用含有一个信号量的信号量集。
2.2 数据结构和接口
每个信号量在内核中都有一个无名的结构表示,他至少包括以下成员:
struct
{
unsigned short semval; //信号量的值
pid_t sempid; //最后一次操作信号量的进程的ID
unsigned short semncnt; //挂起的等待至信号量的值变为正的进程数
unsigned short semzcnt; //挂起等待至信号量的值为0的进程数
….
}
Linux系统为我们提供了一下接口操作使用信号量:
1) 获取信号量标识符
#include
int semget(key_t key, int nsems, int flag)
参数:key代表键值,可以用ftok接口产生
nsems代表信号量的数目,若创建信号量则必须指定nsems,若引用已存在的信号量则将nsems指定为0
flag:类似open参数里的flag标志。他低端的9比特是信号量的权限位,同时我们可以用该权限标志和IPC_CREAT标志作或操作,这样就可以创建一个新的信号量。当然若是该信号量已存在,也不会产生错误,该标志将会被忽略。
返回值:成功返回信号量标识符,失败返回0
2) 操作信号量
#include
int semctl(int semid, int semnum, int cmd, /*union semun*/)
参数:semid 信号量标识符
semnum:要操作的信号量,若是在信号集中则可以取0值nsems-1之间的任意值,若只有一个信号量则取0即可。
cmd:操作信号量的指令,系统提供了10种不同的指令提供用户操作信号量。这里我们只介绍两个最常用的指令,其余的指令可以查阅资料获得。
a) SETVAL:用来把一个信号量的值初始化为我们设定的值,这个值通过第四个参数semun的val成员提供。该成员的结构如下:
union semnu
{
int val;
stuct semid_ds *buf;
unsigned short *array;
};
b) IPC_RMID:用于删除一个不再使用的信号量。该删除操作是立即发生的。任何在使用该信号量的进程,将在下次试图对信号量操作时产生错误。
返回值:根据cmd的不同,返回不同的值。对于SETVAL和IPC_RMID这两个指令,成功返回0,失败返回-1.
3) 对信号量进行增减操作,在该操作是一个原子操作
#include
int semop(int semid, struct sembuf semoparray[ ], size_t nops)
参数:semid: 信号量标识符
semoparray[ ]:指向信号量操作数组的指针。该指针有如下结构:
struct sembuf
{
unsigned short sem_num; //信号量的编号
short sem_op;
//操作中改变的数值,+1表示释放资源,-1表示占用资源
short sem_flag;
/*通常设为SEM_UNDO,当系统在没有释放信号量的情况下退出进程,系统将自动释放进程占用的信号量*/
}
nops : 操作的信号数量
返回值:成功返回0,失败返回-1
2.3 信号量使用
系统提供的接口相对复杂,使我们不太清楚信号量的使用方法。下面我们将利用系统提供的接口编写信号量操作的简单接口,当然这个接口是针对二进制信号量的(只取0和1)。
- 1)创建信号量
- int creat_sem(void)
- {
- int semid = 0;
- key_t key;
- key = ftok(FTOK_FILE, 11);
- if(key == -1)
- {
- printf("%s : key = -1!\n",__func__);
- return -1;
- }
- semid = semget(key, 1, IPC_CREAT|0666);
- if(semid == -1)
- {
- printf("%s : semid = -1!\n",__func__);
- return -1;
- }
- return semid;
-
- }
- 2)初始化信号量。在使用之前必须先初始化
- int set_semvalue(int semid)
- {
- union semun sem_arg;
- sem_arg.val = 1;
- if(semctl(semid, 0, SETVAL, sem_arg) == -1)
- {
- printf("%s : can't set value for sem!\n",__func__);
- return -1;
- }
- return 0;
- }
- 3) 占用资源,即执行p操作(p操作是用于描述获取信号量的术语)
- int sem_p(int semid)
- {
- struct sembuf sem_arg;
- sem_arg.sem_num = 0;
- sem_arg.sem_op = -1;
- sem_arg.sem_flg = SEM_UNDO;
- if(semop(semid, & sem_arg, 1) == -1)
- {
- printf("%s : can't do the sem_p!\n",__func__);
- return -1;
- }
- return 0;
- }
- 4) 释放资源,即v操作(v操作适用于描述释放信号量的术语)
- int sem_v(int semid)
- {
- struct sembuf sem_arg;
- sem_arg.sem_num = 0;
- sem_arg.sem_op = 1;
- sem_arg.sem_flg = SEM_UNDO;
- if(semop(semid, & sem_arg, 1) == -1)
- {
- printf("%s : can't do the sem_v!\n",__func__);
- return -1;
- }
- return 0;
- }
- 5) 删除信号量。
- int del_sem(int semid)
- {
- if(semctl(semid, 0, IPC_RMID) == -1)
- {
- printf("%s : can't rm the sem!\n",__func__);
- return -1;
- }
- return 0;
- }
三、 共享内存
共享内存允许几个不同的进程共享同一给定的存储区。当有一个进程往共享内存里写入数据,其他连接同一段共享内存的进程将立刻观察到该段内存里内容的改变。但是这也带来了一个问题:当一个进程往共享内存里写数据的时候,如果没有一个保护措施,那么另一个连接该共享内存的进程也可以对该共享的内容做读写操作,而这样的读写操作必然会带来数据的混乱。因此我们在编写使用共享内存的进程时,必须注意数据的同步,通常我们是利用之前介绍的信号量来实现的。
3.1 数据结构和接口
和之前介绍的信号量一样,系统为共享内存提供了一组接口和数据结构。下面我们将做详细的介绍:
1) 获取共享内存标识符。
#include
int shmget(key_t key, size_t size, int flag)
参数:key:键值,可以有ftok接口生成
size:将要创建的共享内存段的大小,若创建一个新的共享内存段则必须指定size,若只是引用一个已有的共享内存则将size指定为0
flag:和信号量一样。可以用IPC_CREAT和权限标志做按位或操作
返回值:成功返回共享内存的标识符,失败返回-1
2)控制共享内存
#include
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
参数:shmid:共享内存标识符
cmd:操作共享内存的指令。他只有3个可用指令。分别是:
a) IPC_STAT:获取当前共享内存属性值,保存在shmid_ds结构的buf中
b) IPC_SET:根据buf的内容修改共享内存的属性,但必须是共享内存的创建者或者超级用户才有这个权限
c) IPC_RMID:删除共享内存。但是在删除共享内存之前必须使最后一个连接此共享内存的进程与该内存段脱离。否则不会删除该存储段。
返回值:成功返回0,失败返回-1
3) 连接共享内存到进程
#include
void *shmat(int shmid, const void* addr, int flag)
参数:shmid:共享内存标识符
addr:共享内存连接到相关进程的地址,通常设为0,即由系统选择
flag:可以指定以何种方式连接共享内存。SHM_RDONLY则以只读方式连接共享内存,否则以读写方式连接此段。但是这个标志是与创建时的标志的交集。通常设为0。
返回值:若成功,返回指向共享内存的指针,否则返回(void*)-1
4) 脱离连接的共享内存
#include
int shmdt(void *addr)
参数:addr是之前调用shmdt时返回的连接地址。
返回值:成功返回0,失败返回-1
3.2 另一种形式的共享内存
另一种形式的共享内存是使用mmap函数将同一文件映射至多个进程的地址空间,同时使用MAP_SHARED标志。该标志指定了存储操作修改映射文件,也就是说存储操作相当于对文件调用了write函数。
四、 信号量和共享内存的实例
- #include <sys/ipc.h>
- #include <sys/sem.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include “sem.h”
- #define FTOK_FILE "/etc/profile"
- #define TEST_FILE "/home/hyt/AdvancedUnix/chapter15/sem_file.dat"
- #define STR_LEN 32
- #define SHM_SIZE 256
- union semun
- {
- int val;
- struct semid_ds *buf;
- unsigned short *array;
- };
- typedef struct _tag_shm
- {
- char buf[SHM_SIZE];
- unsigned short num;
- }shm_t;
- int main(void)
- {
- int semid,shmid;
- char buf[STR_LEN] = {0};
- int i = 0;
- void * pshm_addr = NULL;
- shm_t *pshm = NULL;
- /*获取信号量标识符*/
- semid = creat_sem( );
- if(semid == -1)
- { printf("%s : semid = %d!\n", __func__, semid);
- return -1;
- }
- /*创建信号量之后的,初始化操作*/
- if(set_semvalue(semid))
- {
- printf("%s : set_semvalue failed!\n",__func__);
- return -1;
- }
- /*获取共享内存标识符*/
- shmid = shmget(ftok(FTOK_FILE, 111), sizeof(shm_t), IPC_CREAT|0666);
- if(shmid == -1)
- {
- printf("%s : shmid = %d!\n",__func__, shmid);
- return -1;
- }
- /*当前进程连接该共享内存段*/
- pshm_addr = shmat(shmid, 0 , 0);
- if(pshm_addr == (void *)-1)
- {
- printf("%s : pshm_addr == (void*)0!\n",__func__);
- return -1;
- }
- pshm = pshm_addr;
- printf("read process semid is %d,shmid is %d!\n", semid,shmid);
- /*让写数据的进程先运行*/
- sleep(4);
- for(; ;)
- {
- /*占用信号量,p操作*/
- if(sem_p(semid))
- {
- printf("%s : sem_p failed !\n",__func__);
- return -1;
- }
-
- printf("enter read process!\n");
- printf("pshm->num is %d!\n",pshm->num);
- printf("pshm->buf is %s", pshm->buf);
- printf("leave read process!\n\n");
- /*释放信号量,v操作*/
- if(sem_v(semid))
- {
- printf("%s : sem_v failed!\n",__func__);
- return -1;
- }
- if(!strncmp(pshm->buf, "end", 3))
- break;
-
- sleep(2);
- }
- /*删除信号量*/
- if(del_sem(semid))
- {
- printf("%s : del_sem failed!\n", __func__);
- return -1;
- }
- /*进程和共享内存脱离*/
- if(shmdt(pshm_addr) == -1)
- {
- printf("%s : shmdt failed!\n",__func__);
- return -1;
- }
- /*删除共享内存*/
- if(shmctl(shmid, IPC_RMID, 0) == -1)
- {
- printf("%s : shmctl failed!\n",__func__);
- return -1;
- }
- printf("BYE!\n");
- return 0;
- }
- 客户端:(sem_write.c)
- #include <sys/ipc.h>
- #include <sys/sem.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #define FTOK_FILE "/etc/profile"
- #define TEST_FILE "/home/hyt/AdvancedUnix/chapter15/sem_file.dat"
- #define STR_LEN 32
- #define SHM_SIZE 256
- union semun
- {
- int val;
- struct semid_ds *buf;
- unsigned short *array;
- };
- typedef struct _tag_shm
- {
- char buf[SHM_SIZE];
- unsigned short num;
- }shm_t;
- int main(void)
- {
- int semid, shmid;
- char buf[STR_LEN] = {0};
- void *pshm_addr = NULL;
- shm_t * pshm = NULL;
- int i = 0;
- /*获取信号量标识符*/
- semid = creat_sem( );
- if(semid == -1)
- { printf("%s : semid = %d!\n", __func__, semid);
- return -1;
- }
- /*获取共享内存标识符*/
- shmid = shmget(ftok(FTOK_FILE,111), sizeof(shm_t), IPC_CREAT|0666);
- if(shmid == -1)
- {
- printf("%s: shmid = %d!\n", __func__, shmid);
- return -1;
- }
- /*当前进程连接该共享内存段*/
- pshm_addr = shmat(shmid, 0, 0);
- if(pshm_addr == (void *)-1)
- {
- printf("%s : pshm_addr = (void*)-1!\n",__func__);
- return -1;
- }
- pshm = pshm_addr;
- printf("read process : semid is %d, shmid is %d!\n",semid, shmid);
- for(; ;)
- {
- /*占用信号量,p操作*/
- if(sem_p(semid))
- {
- printf("%s : sem_p failed !\n",__func__);
- return -1;
- }
-
- printf("enter write process!\n");
- printf("enter something, end with end >\n");
- fgets(buf, STR_LEN, stdin);
- pshm->num = i++;
- strcpy(pshm->buf, buf);
- printf("leave write process!\n\n");
- /*释放信号量,v操作*/
- if(sem_v(semid))
- {
- printf("%s : sem_v failed!\n",__func__);
- return -1;
- }
- if(!strncmp(pshm->buf , "end", 3))
- break;
-
- sleep(2);
- }
- /*进程和共享内存脱离*/
- if(shmdt(pshm_addr) == -1)
- {
- printf("%s : shmdt is failed!\n",__func__);
- return -1;
- }
-
- printf(" Good Bye! \n");
- return 0;
- }
该程序分服务器进程和客户端进程,应用的信号操作函数在之前已经列出。服务器进程创建信号量和共享内存,并在等待4s之后,进入while循环。等待的过程中启动sem_write进程,该进程进入while循环获取信号量然后向共享内存输入数据,然后释放信号量(注意此过程的时间长度要大于四秒,否则不会进入唤醒sem_read进程)。此时服务器进程sem_read被唤醒,读取共享内存中的数据,然后释放信号量。然后sem_write进程又获取信号量,重复以上过程。
输出结果:
Sem_write进程分别向共享内存写了“hello linux”、“hello CinaUnix”以及“end”,对应的sem_read进程分别读取了以上内容并输出。
原文:http://blog.chinaunix.net/uid-26497562-id-3057208.html