【转】通过共享内存和信号量实现进程间的通信
一:共享内存相关概念和使用函数:
共享内存是在多个进程之间共享和传递数据的一种方式。它允许两个不相关的进程访问同一个逻辑内存,共享内存是在两个正在运行的进程之间传递数据的一种非常有效的方式。
共享内存是由IPC为进程创建的一个特殊的地址范围,它将出现在该进程的地址空间中。其他进程可以将同一段共享内存连接到他们自己的地址空间中。所有进程都可以访问共享内存的地址。如果一个进程向共享内存写入了数据,所做的改动将立刻被可以访问同一段共享内存的任何其他进程看到。
共享内存的函数:
#include <sys/shm.h>
(1) shmget函数:
int shmget(key_t key, size_t size, int shmflg);
此函数的功能是创建共享内存。
第一个参数是程序需要提供一个参数key,它有效的为共享内存命名。
第二个参数size是以字节为单位指定需要共享的内存容量。
第三个参数是包含九个bit的权限标志,它们的作用与创建文件时使用的mode一样,由IPC_CREAT定义的一个特殊比特必须和权限标志按位或才能创建一个新的共享内存段。(权限标志对共享内存十分有用,因为它们允许一个进程创建的共享内存可以被共享内存的创立者所拥有的进程写入,同时其他用户创建的进程只能读取该共享内存)。
shmget函数返回一个共享内存的标示符,该标示符将用于后续的共享内存函数。如果创建失败,则返回-1。
(2)shmat函数:
void *shmat(int shm_id,const void *shm_addr,int shmflg);
此函数的作用是将共享内存连接到一个进程的地址空间中。
第一个参数是由shmget返回的共享内存标示符。
第二个参数是共享内存连接到当前进程中的地址位置,它通常是一个空指针,表示
让系统来选择共享内存出现的地址。
第三个参数是一组位标志,它有两种取值:SHM_RND(此标志与shm_addr联合使用,用来控制共享内存的地址),SHM_RDONLY(表示连接的内存只读)。
(3)shmdt函数
shmdt函数的作用是将共享内存从当前的进程中分离,它的参数是shmat返回的地址指针,成功后返回0,否则返回-1。(将共享内存分离并未删除它,只是使得该共享内存对当前的进程不在可用)
(4)shmctl函数
int shmctl(int shm_id ,int command, struct shmid_ds *buf)
第一个参数是shmget返回的共享内存的标示符。
第二个参数是command是要采取的动作,它有三种值:IPC_STAT(把shmid_ds结构中的数据设置为共享内存的当前关联值),IPC_SET(如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值),IPC_RMID(删除共享内存段)
第三个参数是一个指针,它指向包含共享内存模式和访问权限的结构。
函数执行成功后返回0,失败时返回-1。
二:示例程序
程序的目的是让两个进程都能访问到一块内存空间,当一个进程对该空间的数据修改后,另一个进程也能及时同步该空间的数据。代码如下所示:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define SHMKEY (key_t)0x1//共享内存的关键字
//两个进程共享的是该结构体大小的内存空间
struct user{
int userid;
char username;
};
int get_shm(size_t size)
{
int shmid;
shmid=shmget(SHMKEY,size,0666|IPC_CREAT);
if(shmid==-1){
printf("创建共享内存出错");
exit(EXIT_FAILURE);
}
return shmid;
}
int main(void)
{
int shmid;
struct user *segptr;
shmid=get_shm(sizeof(struct user));//获得共享内存的标示符
segptr=(struct user*)(shmat(shmid,0,0));//建立与共享内存的连接
segptr->userid=0;
segptr->username='a';
pid_t pid;
//创建子进程
pid=fork();
{
if(pid==0)
{
sleep(1);
printf("子进程:%d,%c\n",segptr->userid,segptr->username);
segptr->userid=1;
segptr->username='b';
printf("子进程:%d,%c\n",segptr->userid,segptr->username);
}
else
{
sleep(2);
//子进程修改完共享内存空间后,父进程访问共享内存空间
printf("父进程:%d,%c\n",segptr->userid,segptr->username);
}
}
shmdt(segptr);
return 0;
}
上述程序的运行的结果是:
子进程:0,a
子进程:1,b
父进程:1,b
运行结果表明,两个进程之间通过共享内存的方式进行通信成功。
但是,共享内存没有提供同步机制,也就是说在第一个进程结束对共享内存的写操作之前,并无自动的机制可以阻止第二个进程开始对它进行读取。我们将程序改动一下:
if(pid==0)
{
sleep(1);
printf("子进程:%d,%c\n",segptr->userid,segptr->username);
segptr->userid=1;
sleep(2);
segptr->username='b';
printf("子进程:%d,%c\n",segptr->userid,segptr->username);
}
else
{
sleep(2);
printf("父进程:%d,%c\n",segptr->userid,segptr->username);
}
子进程在第一秒时设置userid的值,父进程在第二秒的时候读取共享内存中的值,而子进程在第三秒的时候才完成对内存空间的修改,那么就说明在父进程访问共享内存的同时,子进程也在修改共享内存。这样就导致了数据的正确性出现问题。正确的做法应该是在子进程对该共享内存进程修改的时候进行加锁,使其他进程不能够对该共享空间进行修改。这里我们使用信号量机制来解决上述问题。
三:信号量相关概念及使用函数
信号量是用来控制多个进程对共享资源的计数器,它经常会被用作锁定的保护机制,当某个进程在对资源进行操作时阻止其他进程对资源的访问。
两个进程共享信号量sem,一旦其中一个进程执行了p(sem)操作,它将得到信号量,并且可以进入临界区域。而第二个进程将被阻止进入临界区域,因为当它试图执行p(sem)操作时,它会被挂起以等待第一个进程离开临界区域并且执行v(sem)操作释放信号量。
信号量相关的函数:
(1) semget函数
int semget(key_t key, int num_sems. Int sem_flags)
此函数的作用是:创建一个新信号量或取得一个已有的信号量的键
第一个参数是整数值,不相关的进程可以通过它访问同一个信号量,程序对所有的信号量的访问都是间接的,它先提供一个键,再由系统生成一个相应的信号量标示符。
第二个参数指定需要的信号量的数目,一般取值为1
第三个参数是一组标志位,取值一般为IPC_CREAT(返回一个新创建的或者已有的信号量)或者是(IPC_CREAT|IPC_EXCL)(此组合确保创建出的是一个新的,唯一的信号量,如果该信号量已经存在,它将返回一个错误。)
此函数在执行成功后返回一个信号量标示符,如果失败,则返回-1。
(2) semop函数
int semop(int sem_id,struct sembuf *sem_ops,size_t num_sem_ops)
此函数的作用是改变信号量对象中各个信号量的状态。
第一个参数是信号量的标示符
第二个参数是指向一个结构数组的指针,每个数组至少包含以下几个成员
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,…)
此函数的作用是允许我们直接控制信号量的信息
第一个参数是由semget返回的信号量标示符。
第二个参数是信号量变量,当需要用到成组的信号量时有用,它一般是0。
第三个参数是将要采取的动作。常用的两个值是SETVAL(用来把信号量初始化为一个已知的值),IPC_RMID(用于删除一个已经无需继续使用的信号量标示符)
如果还有第四个参数,它将会是union semun结构,
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
}
四:将信号量加入到示例程序中:
#include <sys/sem.h>//添加信号量头文件
#include <sem_union.h>//自定义的头文件,里面是semun联合体
#define SEMKEY (key_t)0x2//添加信号量的key
//封装函数,返回新创建的信号量的标示符
int get_sem()
{
int sem_id;
sem_id=semget(SEMKEY,1,0666|IPC_CREAT);
union semum sem_union;
sem_union.val=1;
if(semctl(sem_id,0,SETVAL,sem_union)==-1)
{
printf("初始化信号量出错");
exit(EXIT_FAILURE);
}
return sem_id;
}
//改变信号量的值,P操作
int sem_p(int sem_id)
{
struct sembuf sem_buf;
sem_buf.sem_op=-1;
sem_buf.sem_flg=SEM_UNDO;
sem_buf.sem_num=0;
if(semop(sem_id,&sem_buf,1)==-1)
{
printf("P操作错误\n");
exit(EXIT_FAILURE);
}
return 1;
}
//改变信号量的值,V操作
int sem_v(int sem_id)
{
struct sembuf sem_buf;
sem_buf.sem_flg=SEM_UNDO;
sem_buf.sem_num=0;
sem_buf.sem_op=1;
if(semop(sem_id,&sem_buf,1)==-1)
{
printf("V操作错误\n");
exit(EXIT_FAILURE);
}
return 1;
}
//重写main函数
int main(void)
{
int shmid;
int sem_id;
struct user *segptr;
shmid=get_shm(sizeof(struct user));
segptr=(struct user*)(shmat(shmid,0,0));
segptr->userid=0;
segptr->username='a';
pid_t pid;
sem_id=get_sem();
pid=fork();
{
if(pid==0)
{
sleep(1);
printf("子进程:%d,%c\n",segptr->userid,segptr->username);
sem_p(sem_id);
segptr->userid=1;
sleep(2);
segptr->username='b';
sem_v(sem_id);
printf("子进程:%d,%c\n",segptr->userid,segptr->username);
}
else
{
sleep(2);
sem_p(sem_id);
printf("父进程:%d,%c\n",segptr->userid,segptr->username);
sem_v(sem_id);
}
}
shmdt(segptr);
return 0;
}
将上述新定义的方法加入到之前的示例程序中,运行后的结果为:
子进程:0,a
子进程:1,b
父进程:1,b
如果是没有使用信号量的话,程序的输出应该是:
子进程:0,a
父进程:1,a
子进程:1,b
上述表明,子进程在修改共享内存空间时,父进程不能对该空间进行访问。从而保证了数据的正确性。