一、共享内存的IPC原理
共享存储允许两个或多个进程共享一给定的存储区。因为数据不需要在客户机和服务器之间复制,所以这是最快的一种IPC。使用共享存储的唯一窍门是多个进程之间对一给定存储区的同步存取。若服务器将数据放入共享存储区,则在服务器做完这一操作之前,客户机不应当去取这些数据。
共享内存的数据结构:
两个进程在使用此共享内存空间之前,需要在进程与共享内存之间建立联系,即将共享内存挂载到进程中,在进程中使用一个指针指向该共享内存空间。此时,被挂载的内存空间就可以被该进程访问了。
在使用共享内存的进行数据存取时,一般都配合二元信号量使用。一般情况下,对一段内存空间的写操作不允许几个进程同时进行,因为允许多个进程同时写操作的不确定性。因此,有必要使用二元信号量来同步两个进程以实现对共享内存的写操作。
由于共享内存需要占用大量的内存空间,系统对共享内存做了以下限制:
二、Linux共享内存管理
1、创建共享内存
在使用段共享内存之前,需要先创建它。创建共享内存的系统调用shmget函数声明如下:
#include <sys/shm.h>
int shmget(key_t key, int size, int flag) ;
//返回:若成功则为共享内存I D,若出错则为- 1
第一个参数key_t类型的key值,一般由ftok函数产生。
第二个参数size为欲创建的共享内纯段大小(单位为字节)。如果正在创建一个新段(一般在服务器中),则必须指定其size。如果正在存访一个现存的段(一个客户机),则将size指定为0。
第三个参数shmflg用来标识共享内存段的创建标识,包括:
2、共享内存控制
Linux系统使用shmctl函数来实现共享内存空间的控制,包括读取状态、设置状态和删除操作。
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf) ;
//返回:若成功则为0,若出错则为-1
第一个参数为要操作的共享内存标识符,该值一般由shmget函数返回。
第二个参数为要执行的操作:
• IPC_STAT :对此段取shmidds结构,并存放在由buf指向的结构中。
• IPC_SET :按buf指向的结构中的值设置与此段相关结构中的下列三个字段:shmperm.uid、shmperm.gid以及shmperm.mode。此命令只能由下列两种进程执行:一种是其有效用户I D等于shmperm.cuid或shmperm.uid的进程;另一种是具有超级用户特权的进程。
• IPC_RMID :从系统中删除该共享存储段。因为每个共享存储段有一个连接计数(shmnattch在shmidds结构中) ,所以除非使用该段的最后一个进程终止或与该段脱接,否则不会实际上删除该存储段。不管此段是否仍在使用,该段标识符立即被删除,所以不能再用shmat与该段连接。此命令只能由下列两种进程执行:一种是其有效用户ID等于shmperm.cuid或shmperm.uid的进程;另一种是具有超级用户特权的进程。
• SHM_LOCK :锁住共享存储段。此命令只能由超级用户执行。
• SHM_UNLOCK :解锁共享存储段。此命令只能由超级用户执行。
第三个参数为struct shmid_ds结构的临时共享内存变量信息,此内容根据第二个参数的不同而改变。
3、映射共享内存对象
在进程使用一段共享内存空间前,需要将该共享内存与当前进程建立联系,即将该共享内存映射(挂接)到当前进程。系统调用将shmat函数来实现讲一个共享内存段映射到调用进程的数据段中。
#include <sys/shm.h>
void *shmat(int shmid, void *addr, int flag) ;
//返回:若成功则为指向共享存储段的指针,若出错则为-1
第一个参数为要操作的共享内存标识符,该值一般由shmget函数返回。
第二个参数shmaddr指定共享内存的映射地址。如果该值为非零,则将用此值作为映射共享内存的地址,除非只计划在一种硬件上运行应用程序(这在当今是不大可能的),否则不用指定共享段所连接到的地址。所以一般应指定addr为0,以便由内核选择地址。
第三个参数用来指定共享内存段的访问权限和映射条件。
如果设置为0,则默认有读写权限,如果设置为SHM_RDONLY,则不能进行写操作。
4、分离共享内存对象
在使用完毕共享内存后,需要使用shmdt函数调用将其与当前进程分离。shmdt函数声明如下:
#include <sys/shm.h>
int shmdt(void *addr) ;
//返回:若成功则为0,若出错则为- 1
addr参数是以前调用shmat时的返回值。如果成功,shmdt将使相关shmid_ds结构中的shm_nattch计数器值减1。
在创建子进程时需要注意一下几点:
(1)使用fork()函数创建子进程后,该进程继承父亲进程挂载的共享内存。
(2)如果调用exec执行一个新的程序,则所有挂载的共享内存将被自动卸载
(3)如果在某个进程中调用的exit()函数,所有挂载的共享内存将与当前进程脱离关系。
三、共享内存处理应用示例
1、功能描述
此程序实现没有亲缘关系的两个进程间通过共享内存进行数据通信,同时,使用信号量来保证两个进程的读写同步:即发送方在发送数据时,接收方不能接收数据;接收方在接收数据时,发送方不能发送数据。
在代码使用中,使用共享内存来传递数据,使用信号量来同步读写端(此处仅使用二元信号量)。基本思路如下:
(1)首先设置信号量初始值为0,表示没有写入任何数据,不可以读。
(2)发送端在信号量的值为0时写入一段数据到内存中,并阻塞读进程,写入完成后,设置信号量的值为1,表示可以读数据。此时也不能再写数据了。
(3)接收端在信号量的值为1时读出数据并阻塞写入端,读出完成后,设置信号量的值为0,表示读出完成,可以再写数据。
2、源码分析
发送端首先创建信号量或者消息共享内存(或者是读取两者的ID值)并挂接共享内存,然后初始化信号量的值为0。接着进入死循环,首先读取信号量的值是否为0,如果为0,表示可以写入数据,从键盘读取数据写入到内存中,然后执行信号量自加1操作,表示允许读操作,系统显示此时不能再写数据。如果输入的数据是”end“则表示结束通信,将卸载共享内存,删除操作在接收端完成。
发送端:
#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <unistd.h>
#include <sys/sem.h>
int main(int argc, char *argv[])
{
int running = 1;
int shid;
int semid;
int value;
void *sharem = NULL;
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_flg = SEM_UNDO;
if((semid = semget((key_t)123456, 1, 0666|IPC_CREAT)) == -1) //创建信号量(或者读ID)
{
perror("semget");
exit(EXIT_FAILURE);
}
if(semctl(semid, 0, SETVAL, 0) == -1) //设置初始值为0
{
printf("sem init error");
if(semctl(semid, 0, IPC_RMID, 0) != 0) //如果设置初始失败删除它
{
perror("semctl");
exit(EXIT_FAILURE);
}
exit(EXIT_FAILURE);
}
shid = shmget((key_t)654321, (size_t)2048, 0600|IPC_CREAT); //创建共享内存(读取ID)
if(shid == -1)
{
perror("shmget");
exit(EXIT_FAILURE);
}
sharem = shmat(shid, 0, 0); //挂接共享内存到当前进程
if (sharem == NULL)
{
perror("shmat");
exit(EXIT_FAILURE);
}
while(running)
{
if((value = semctl(semid, 0, GETVAL)) == 0) //读取值如果为0时
{
printf("write data operate\n"); //表示此时可以写
printf("please input something: ");
scanf("%s", sharem); //从键盘输入信息
sem_b.sem_op = 1; //置信号量自加操作
if(semop(semid, &sem_b, 1) == -1) //执行信号量自加1操作,运行读
{
fprintf(stderr, "semaphore_p failed\n");
exit(EXIT_FAILURE);
}
}
if(strcmp(sharem, "end") == 0) //比较是否时结束符号
running--;
}
shmdt(sharem); //卸载
return 0;
}
接收端首先创建信号量或者消息共享内存(或者是读取两者的ID值)并挂接共享内存,接着进入死循环,首先读取信号量的值是否为1,如果为1,表示可以读入数据,将从共享内存中读出数据输出,然后执行信号量自减1操作,表示允许写操作,系统显示此时不能再读数据。如果输入的数据是”end“则表示结束通信,将卸载共享内存和信号量。
接收端:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/shm.h>
int main(int argc, char *argv[])
{
int running = 1;
char *shm_p;
int shmid;
int semid;
int value;
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_flg = SEM_UNDO;
if((semid = semget((key_t)123456, 1, 0666|IPC_CREAT)) == -1) //创建信号量(或者读ID)
{
perror("semget");
exit(EXIT_FAILURE);
}
if((shmid = shmget((key_t)654321, (size_t)2048, 0600|IPC_CREAT)) == -1) //创建共享内存(或者读ID)
{
perror("shmget");
exit(EXIT_FAILURE);
}
shm_p = shmat(shmid, NULL, 0); //挂接
if(shm_p == NULL)
{
perror("shmat");
exit(EXIT_FAILURE);
}
while(running)
{
if((value=semctl(semid, 0, GETVAL)) == 1) //读取值是否为1
{
printf("read data operate\n"); //为1表示允许读操作
sem_b.sem_op = -1; //设置自减操作
if(semop(semid, &sem_b, 1) == -1) //执行自减操作
{
fprintf(stderr, "semaphore_p failed\n");
exit(EXIT_FAILURE);
}
printf("%s\n", shm_p); //输出信息
}
if (strcmp(shm_p, "end") == 0) //是否结束
running--;
}
shmdt(shm_p); //卸载
if(shmctl(shmid, IPC_RMID, 0) != 0) //删除共享内存
{
perror("shmctl");
exit(EXIT_FAILURE);
}
if(semctl(semid, 0, IPC_RMID, 0) != 0) //删除信号量
{
perror("semctl");
exit(EXIT_FAILURE);
}
return 0;
}