共享内存
共享内存是进程间通信中最简单的方式之一。
共享内存是系统出于多个进程之间通讯的考虑,而预留的的一块内存区。
共享内存允许两个或更多进程访问同一块内存,就如同 malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。
关于共享内存
当一个程序加载进内存后,它就被分成叫作页的块。
通信将存在内存的两个页之间或者两个独立的进程之间。
总之,当一个程序想和另外一个程序通信的时候,那内存将会为这两个程序生成一块公共的内存区域。这块被两个进程分享的内存区域叫做共享内存
因为所有进程共享同一块内存,共享内存在各种进程间通信方式中具有最高的效率。访问共享内存区域和访问进程独有的内存区域一样快,并不需要通过系统调用或者其它需要切入内核的过程来完成。同时它也避免了对数据的各种不必要的复制。
如果没有共享内存的概念,那一个进程不能存取另外一个进程的内存部分,因而导致共享数据或者通信失效。因为系统内核没有对访问共享内存进行同步,您必须提供自己的同步措施。
解决这些问题的常用方法是通过使用信号量进行同步。不过,我们的程序中只有一个进程访问了共享内存,因此在集中展示了共享内存机制的同时,我们避免了让代码被同步逻辑搞得混乱不堪。
为了简化共享数据的完整性和避免同时存取数据,内核提供了一种专门存取共享内存资源的机制。这称为互斥体或者mutex对象
例如,在数据被写入之前不允许进程从共享内存中读取信息、不允许两个进程同时向同一个共享内存地址写入数据等。
当一个进程想和另外一个进程通信的时候,它将按以下顺序运行:
获取mutex对象,锁定共享区域。
将要通信的数据写入共享区域。
释放mutex对象。
当一个进程从从这个区域读数据时候,它将重复同样的步骤,只是将第二步变成读取。
内存模型
要使用一块共享内存
进程必须首先分配它
随后需要访问这个共享内存块的每一个进程都必须将这个共享内存绑定到自己的地址空间中
当完成通信之后,所有进程都将脱离共享内存,并且由一个进程释放该共享内存块
在/proc/sys/kernel/
目录下,记录着共享内存的一些限制,如一个共享内存区的最大字节数shmmax
,系统范围内最大共享内存区标识符数shmmni
等,可以手工对其调整,但不推荐这样做。
理解 Linux 系统内存模型可以有助于解释这个绑定的过程。
linux系统内存模型
在 Linux 系统中,每个进程的虚拟内存是被分为许多页面的。这些内存页面中包含了实际的数据。每个进程都会维护一个从内存地址到虚拟内存页面之间的映射关系。尽管每个进程都有自己的内存地址,不同的进程可以同时将同一个内存页面映射到自己的地址空间中,从而达到共享内存的目的。
分配一个新的共享内存块会创建新的内存页面。因为所有进程都希望共享对同一块内存的访问,只应由一个进程创建一块新的共享内存。再次分配一块已经存在的内存块不会创建新的页面,而只是会返回一个标识该内存块的标识符。
一个进程如需使用这个共享内存块,则首先需要将它绑定到自己的地址空间中。
这样会创建一个从进程本身虚拟地址到共享页面的映射关系。当对共享内存的使用结束之后,这个映射关系将被删除。
当再也没有进程需要使用这个共享内存块的时候,必须有一个(且只能是一个)进程负责释放这个被共享的内存页面。
所有共享内存块的大小都必须是系统页面大小的整数倍。系统页面大小指的是系统中单个内存页面包含的字节数。在 Linux 系统中,内存页面大小是4KB,不过您仍然应该通过调用 getpagesize 获取这个值。
共享内存的实现分为两个步骤:
创建共享内存,使用shmget函数。
映射共享内存,将这段创建的共享内存映射到具体的进程空间去,使用shmat函数。
用于共享内存的函数
共享内存的使用,主要有以下几个API:ftok()
、shmget()
、shmat()
、shmdt()
及shmctl()。
#include <sys/shm.h>
void *shmat(int shm_id, const void *shm_addr, int shmflg);
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
int shmdt(const void *shm_addr);
int shmget(key_t key, size_t size, int shmflg);
与信号量相类似,通常需要在包含shm.h文件之前包含sys/types.h与sys/ipc.h这两个头文件。
用ftok()函数获得一个ID号
应用说明,在IPC中,我们经常用用key_t的值来创建或者打开信号量,共享内存和消息队列。
key_t ftok(const char *pathname, int proj_id);
参数 | 描述 |
---|---|
pathname | 一定要在系统中存在并且进程能够访问的 |
proj_id | 一个1-255之间的一个整数值,典型的值是一个ASCII值。 |
当成功执行的时候,一个key_t值将会被返回,否则-1被返回。我们可以使用strerror(errno)来确定具体的错误信息。
考虑到应用系统可能在不同的主机上应用,可以直接定义一个key,而不用ftok获得:
#define IPCKEY 0x344378
创建共享内存
进程通过调用shmget(Shared Memory GET,获取共享内存)来分配一个共享内存块。
int shmget(key_t key ,int size,int shmflg)
参数 | 描述 |
---|---|
key | 一个用来标识共享内存块的键值 |
size | 指定了所申请的内存块的大小 |
shmflg | 操作共享内存的标识 |
返回值:如果成功,返回共享内存表示符,如果失败,返回-1。
- 该函数的第二个参数key是一个用来标识共享内存块的键值。
彼此无关的进程可以通过指定同一个键以获取对同一个共享内存块的访问。不幸的是,其它程序也可能挑选了同样的特定值作为自己分配共享内存的键值,从而产生冲突。
用特殊常量IPC_PRIVATE作为键值可以保证系统建立一个全新的共享内存块。|
key标识共享内存的键值:0/IPC_PRIVATE。当key的取值为IPC_PRIVATE,则函数shmget将创建一块新的共享内存;如果key的取值为0,而参数中又设置了IPC_PRIVATE这个标志,则同样会创建一块新的共享内存。
- 该函数的第二个参数size指定了所申请的内存块的大小。
因为这些内存块是以页面为单位进行分配的,实际分配的内存块大小将被扩大到页面大小的整数倍。
- 第三个参数shmflg是一组标志,通过特定常量的按位或操作来shmget。这些特定常量包括:
IPC_CREAT:这个标志表示应创建一个新的共享内存块。通过指定这个标志,我们可以创建一个具有指定键值的新共享内存块。
IPC_EXCL:这个标志只能与 IPC_CREAT 同时使用。当指定这个标志的时候,如果已有一个具有这个键值的共享内存块存在,则shmget会调用失败。也就是说,这个标志将使线程获得一个“独有”的共享内存块。如果没有指定这个标志而系统中存在一个具有相同键值的共享内存块,shmget会返回这个已经建立的共享内存块,而不是重新创建一个。
模式标志:这个值由9个位组成,分别表示属主、属组和其它用户对该内存块的访问权限。
其中表示执行权限的位将被忽略。指明访问权限的一个简单办法是利用
映射共享内存
shmat()是用来允许本进程访问一块共享内存的函数,将这个内存区映射到本进程的虚拟地址空间。
int shmat(int shmid,char *shmaddr,int flag)
参数 | 描述 |
---|---|
shmid | 那块共享内存的ID,是shmget函数返回的共享存储标识符 |
shmaddr | 是共享内存的起始地址,如果shmaddr为0,内核会把共享内存映像到调用进程的地址空间中选定位置;如果shmaddr不为0,内核会把共享内存映像到shmaddr指定的位置。所以一般把shmaddr设为0。 |
shmflag | 是本进程对该内存的操作模式。如果是SHM_RDONLY的话,就是只读模式。其它的是读写模式 |
成功时,这个函数返回共享内存的起始地址。失败时返回-1。
要让一个进程获取对一块共享内存的访问,这个进程必须先调用 shmat(SHared Memory Attach,绑定到共享内存)。
将 shmget 返回的共享内存标识符 SHMID 传递给这个函数作为第一个参数。
该函数的第二个参数是一个指针,指向您希望用于映射该共享内存块的进程内存地址;如果您指定NULL则Linux会自动选择一个合适的地址用于映射。第三个参数是一个标志位,包含了以下选项:
SHM_RND表示第二个参数指定的地址应被向下靠拢到内存页面大小的整数倍。如果您不指定这个标志,您将不得不在调用shmat的时候手工将共享内存块的大小按页面大小对齐。
SHM_RDONLY表示这个内存块将仅允许读取操作而禁止写入。 如果这个函数调用成功则会返回绑定的共享内存块对应的地址。通过 fork 函数创建的子进程同时继承这些共享内存块;
如果需要,它们可以主动脱离这些共享内存块。 当一个进程不再使用一个共享内存块的时候
共享内存解除映射
当一个进程不再需要共享内存时,需要把它从进程地址空间中多里。
int shmdt(char *shmaddr)
参数 | 描述 |
---|---|
shmaddr | 那块共享内存的起始地址 |
成功时返回0。失败时返回-1。
应通过调用 shmdt(Shared Memory Detach,脱离共享内存块)函数与该共享内存块脱离。将由 shmat 函数返回的地址传递给这个函数。如果当释放这个内存块的进程是最后一个使用该内存块的进程,则这个内存块将被删除。对 exit 或任何exec族函数的调用都会自动使进程脱离共享内存块。
控制释放
shmctl控制对这块共享内存的使用
函数原型
int shmctl( int shmid , int cmd , struct shmid_ds *buf );
参数 | 描述 |
---|---|
shmid | 是共享内存的ID。 |
cmd | 控制命令 |
buf | 一个结构体指针。IPC_STAT的时候,取得的状态放在这个结构体中。如果要改变共享内存的状态,用这个结构体指定。 |
其中cmd的取值如下
cmd | 描述 |
---|---|
IPC_STAT | 得到共享内存的状态 |
IPC_SET | 改变共享内存的状态 |
IPC_RMID | 删除共享内存 |
返回值: 成功:0 失败:-1
调用 shmctl(”Shared Memory Control”,控制共享内存)函数会返回一个共享内存块的相关信息。同时 shmctl 允许程序修改这些信息。
该函数的第一个参数是一个共享内存块标识。
要获取一个共享内存块的相关信息,则为该函数传递 IPC_STAT 作为第二个参数,同时传递一个指向一个 struct shmid_ds 对象的指针作为第三个参数。
要删除一个共享内存块,则应将 IPC_RMID 作为第二个参数,而将 NULL 作为第三个参数。当最后一个绑定该共享内存块的进程与其脱离时,该共享内存块将被删除。
您应当在结束使用每个共享内存块的时候都使用 shmctl
进行释放,以防止超过系统所允许的共享内存块的总数限制。调用 exit 和 exec 会使进程脱离共享内存块,但不会删除这个内存块。
要查看其它有关共享内存块的操作的描述,请参考shmctl函数的手册页。
示例
简单映射一块共享内存
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#define IPCKEY 0x366378
typedef struct st_setting
{
char agen[10];
unsigned char file_no;
}st_setting;
int main(int argc, char** argv)
{
int shm_id;
//key_t key;
st_setting *p_setting;
// 首先检查共享内存是否存在,存在则先删除
shm_id = shmget(IPCKEY , 1028, 0640);
if(shm_id != -1)
{
p_setting = (st_setting *)shmat(shm_id, NULL, 0);
if (p_setting != (void *)-1)
{
shmdt(p_setting);
shmctl(shm_id,IPC_RMID,0) ;
}
}
// 创建共享内存
shm_id = shmget(IPCKEY, 1028, 0640 | IPC_CREAT | IPC_EXCL);
if(shm_id == -1)
{
printf("shmget error\n");
return -1;
}
// 将这块共享内存区附加到自己的内存段
p_setting = (st_setting *)shmat(shm_id, NULL, 0);
strncpy(p_setting->agen, "gatieme", 10);
printf("agen : %s\n", p_setting->agen);
p_setting->file_no = 1;
printf("file_no : %d\n",p_setting->file_no);
system("ipcs -m");// 此时可看到有进程关联到共享内存的信息,nattch为1
// 将这块共享内存区从自己的内存段删除出去
if(shmdt(p_setting) == -1)
perror(" detach error ");
system("ipcs -m");// 此时可看到有进程关联到共享内存的信息,nattch为0
// 删除共享内存
if (shmctl( shm_id , IPC_RMID , NULL ) == -1)
{
perror(" delete error ");
}
system("ipcs -m");// 此时可看到有进程关联到共享内存的信息,nattch为0
return EXIT_SUCCESS;
}
ipcrm命令删除共享内存
在使用共享内存,结束程序退出后。如果你没在程序中用shmctl()删除共享内存的话,一定要在命令行下用ipcrm命令删除这块共享内存。你要是不管的话,它就一直在那儿放着了。
简单解释一下ipcs命令和ipcrm命令。
取得ipc信息:
usage : ipcs -asmq -tclup
ipcs [-s -m -q] -i id
ipcs -h for help.
m 输出有关共享内存(shared memory)的信息
-q 输出有关信息队列(message queue)的信息
-s 输出有关“遮断器”(semaphore)的信息
删除ipc
usage: ipcrm [ [-q msqid] [-m shmid] [-s semid]
[-Q msgkey] [-M shmkey] [-S semkey] ... ]
两端通信的程序
读者程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define N 64
typedef struct
{
pid_t pid;
char buf[N];
} SHM;
void handler(int signo)
{
//printf("get signal\n");
return;
}
int main()
{
key_t key;
int shmid;
SHM *p;
pid_t pid;
if ((key = ftok(".", 'm')) < 0)
{
perror("fail to ftok");
exit(-1);
}
signal(SIGUSR1, handler);//注册一个信号处理函数
if ((shmid = shmget(key, sizeof(SHM), 0666|IPC_CREAT|IPC_EXCL)) < 0)
{
if (EEXIST == errno)//存在则直接打开
{
shmid = shmget(key, sizeof(SHM), 0666);
p = (SHM *)shmat(shmid, NULL, 0);
pid = p->pid;
p->pid = getpid();//把自己的pid写到共享内存
kill(pid, SIGUSR1);
}
else//出错
{
perror("fail to shmget");
exit(-1);
}
}
else//成功
{
p = (SHM *)shmat(shmid, NULL, 0);
p->pid = getpid();
pause();
pid = p->pid;//得到写端进程的pid
}
while ( 1 )
{
pause();//阻塞,等待信号
if (strcmp(p->buf, "quit\n") == 0) exit(0);//输入"quit结束"
printf("read from shm : %s", p->buf);
kill(pid, SIGUSR1);//向写进程发SIGUSR1信号
}
return 0;
}
写者程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define N 64
typedef struct
{
pid_t pid;
char buf[N];
} SHM;
void handler(int signo)
{
//printf("get signal\n");
return;
}
int main()
{
key_t key;
int shmid;
SHM *p;
pid_t pid;
if ((key = ftok(".", 'm')) < 0)
{
perror("fail to ftok");
exit(-1);
}
signal(SIGUSR1, handler); // 注册一个信号处理函数
if ((shmid = shmget(key, sizeof(SHM), 0666 | IPC_CREAT | IPC_EXCL)) < 0)
{
if (EEXIST == errno) // 存在则直接打开
{
shmid = shmget(key, sizeof(SHM), 0666);
p = (SHM *)shmat(shmid, NULL, 0);
pid = p->pid;
p->pid = getpid();
kill(pid, SIGUSR1);
}
else//出错
{
perror("fail to shmget");
exit(-1);
}
}
else//成功
{
p = (SHM *)shmat(shmid, NULL, 0);
p->pid = getpid(); // 把自己的pid写到共享内存
pause();
pid = p->pid; // 得到读端进程的pid
}
while ( 1 )
{
printf("write to shm : ");
fgets(p->buf, N, stdin); // 接收输入
kill(pid, SIGUSR1); // 向读进程发SIGUSR1信号
if (strcmp(p->buf, "quit\n") == 0) break;
pause(); // 阻塞,等待信号
}
shmdt(p);
shmctl(shmid, IPC_RMID, NULL); // 删除共享内存
return 0;
}