Linux/Unix系统IPC是各种进程间通信方式的统称,但是其中极少能在所有Linux/Unix系统实现中进行移植。随着POSIX和Open Group(X/Open)标准化的推进呵护影响的扩大,情况虽已得到改善,但差别仍然存在。一般来说,Linux/Unix常见的进程间通信方式有:管道、消息队列、信号、信号量、共享内存、套接字等。博主将在《进程间通信方式总结》系列博文中和大家一起探讨学习进程间通信的方式,并对其进行总结,让我们共同度过这段学习的美好时光。这里我们就以其中一种方式共享内存展开探讨,共享内存是IPC中最常用也是最快的一种进程间通信的方式。顾名思义,共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址,就好像它们是由用C语言函数malloc分配的内存一样。而如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。特此提醒,共享内存内有提供同步访问机制,这个需要使用着自己去实现。可能有的小伙伴不是很理解,共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无同步机制可以阻止第二个进程开始对它进行读取操作,这样读取的就是脏数据,如果此时再来一个进程对该共享内存进行写操作,你想想共享内存将会乱成一锅粥,通信将成为灾难。所以我们通常需要用其他的机制来同步对共享内存的访问,例如前面说到的信号量。有关信号量的更多内容,可以查阅我的另一篇文章:《进程间通信方式总结——信号量》。在那里,博主将共享内存和信号量结合在一起使用,通过信号量使共享内存拥有同步机制。当然,你可以对共享内存进行封装,实现一个拥有同步机制的共享内存API,这个就由你自己去实现咯。好了,废话不多说,下面就让我们一起进行共享内存的学习吧。
shmget根据给定的key值创建指定大小的内存空间
函数原型:int shmget(key_t key, size_t size, int shmflg);
key:与信号量的semget函数一样,程序需要提供一个参数key(非0整数),它有效地为共享内存段命名,shmget函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1。
size:共享内存的容量,以字节为单位
shmflg:shmflg是权限标志,它的作用与open函数的mode参数一样,如果要在key标识的共享内存不存在时,创建它的话,可以与IPC_CREAT做或操作。共享内存的权限标志与文件的读写权限一样,举例来说,0644,它表示允许一个进程创建的共享内存被内存创建者所拥有的进程对该共享内存有读取和写入数据的权利,同时其他用户创建的进程只能读取共享内存。shmflg为0,取共享内存标识符,若不存在则函数会报错;IPC_CREAT:当shmflg&IPC_CREAT为真时,不管是否已存在该块共享内存,则都返回该共享内存的ID,若不存在则创建共享内存;如果存在这样的共享内存,返回此共享内存的标识符;IPC_CREAT|IPC_EXCL:如果内核中不存在键值与key相等的共享内存,则新建一个消息队列;如果存在这样的共享内存则报错。
返回值:成功返回共享内存标识符,失败返回-1,错误原因存于error中。
错误码:
EINVAL:参数size小于SHMMIN或大于SHMMAX
EEXIST:预建立key所指的共享内存,但已经存在
EIDRM:参数key所指的共享内存已经删除
ENOSPC:超过了系统允许建立的共享内存的最大值(SHMALL)
ENOENT:参数key所指的共享内存不存在,而参数shmflg未设IPC_CREAT位
EACCES:没有权限
ENOMEM:核心内存不足
连接共享内存标识符为shmid的共享内存,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问。每当调用shmat函数成功返回,共享内存连接数便加1。
函数原型:void *shmat(int shm_id, const void *shm_addr, int shmflg);
shm_id:shmget函数返回的共享内存标识。
shm_addr:指定共享内存出现在进程内存地址的什么位置,直接指定为NULL让内核自己决定一个合适的地址位置
shmflg:一组标志位,通常为0。SHM_RDONLY只读, 0可读写;(SHM_COPY、SHM_MAP、SHM_RND不在此说明)
返回值:成功返回共享内存地址,失败返回-1,错误码存于error中。
错误码:
EACCES:无权限以指定方式连接共享内存
EINVAL:无效的参数shmid或shmaddr
ENOMEM:核心内存不足
shmdt函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用,删除共享内存需要shmctl来完成。Shmdt只是将共享内存连接数减1,并使进程中的共享内存连接地址无效,不会将共享内存状态置为删除状态。
函数原型:int shmdt(const void *shm_addr);
shm_addr:shmat函数返回的地址指针(连接的共享内存的起始地址),调用成功时返回0,失败时返回-1。
返回值:成功返回0,失败返回-1,错误码存于error中。
错误码:EINVAL:无效的参数shmaddr
函数原型:int shmctl(int shm_id, int command, struct shmid_ds *buf);
shm_id:shmget函数返回的共享内存标识符。
command:采取的操作,它可以取下面的三个值
IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中
IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内
IPC_RMID:删除共享内存段。
buf:共享内存状态结构体指针,它指向共享内存模式和访问权限的结构。
返回值:成功返回0,失败返回-1,错误码存于error中。
错误码:
EACCESS:参数cmd为IPC_STAT,确无权限读取该共享内存
EFAULT:参数buf指向无效的内存地址
EIDRM:标识符为shmid的共享内存已被删除
EINVAL:无效的参数cmd或shmid
EPERM:参数cmd为IPC_SET或IPC_RMID,却无足够的权限执行
struct shmid_ds {
struct ipc_perm shm_perm;
int shm_segsz;
time_t shm_atime;
time_t shm_dtime;
time_t shm_ctime;
unsigned short shm_cpid;
unsigned short shm_lpid;
short shm_nattch;
unsigned short shm_npages;
unsigned long *shm_pages;
structvm_area_struct *attaches;
};
#include
#include
#include
#include
#include
#include
//定义共享内存标识符
int shmid = -1;
//信号处理函数
void handler(int s)
{
fprintf(stdout, "exit\n");
//将共享内存置为删除状态,且引用计数减1
shmctl(shmid, IPC_RMID, 0);
exit(EXIT_SUCCESS);
}
int main(int argc, char **argv)
{
if (argc < 2) fprintf(stderr, "usage:%s key\n", argv[0]),exit(EXIT_FAILURE);
//安装信号
signal(SIGINT, handler);
char * addr = NULL;
//创建共享内存,如果失败打印并退出
if (-1 == (shmid = shmget(atoi(argv[1]), 256, IPC_CREAT | 0666)))
{
perror("shmget");
exit(EXIT_FAILURE);
}
//将共享内存映射到当前进程
//参数1:shmget函数的返回值,共享内存的标识符
//参数2:指定共享内存映射到进程中的地址(NULL:有内核分配)
//参数3:一组标志位,通常为0
if (-1 == (int)(addr = (char *)shmat(shmid, NULL, 0)))
{
fprintf(stderr, "addr == NULL");
exit(EXIT_FAILURE);
}
fprintf(stdout, "share:");
//从标准输入读入数据
fgets(addr, 256, stdin);
//fork创建子进程(读时共享,写时拷贝)
pid_t pid = fork();
//子进程
if (0 == pid)
{
//子进程继承父进程已连接的共享内存地址
//输出共享内存中内容
fprintf(stdout, "share: %s\n", addr);
exit(EXIT_SUCCESS);
}
//创建子进程(由于vfork创建的子进程完全共享父进程空间,相当于创建了线程)
pid = vfork();
if (0 == pid)
{
//子进程调用exec函数替换进程空间
//子进程与已连接的共享内存自动脱离
execlp("ls", "ls", "-l", NULL);
exit(EXIT_FAILURE);
}
//让进程挂起(此时不占用CPU资源)
for (; ;)
{
pause();
}
return0;
}
#include
#include
#include
#include
#include
//定义共享内存标识符
int shmid = -1;
//信号处理函数
void handler(int s)
{
fprintf(stdout, "exit\n");
//将共享内存置为删除状态,并使引用计数减1
shmctl(shmid, IPC_RMID, 0);
exit(EXIT_SUCCESS);
}
int main(int argc, char **argv)
{
if (argc < 2) fprintf(stderr, "usage:%s key\n", argv[0]),exit(EXIT_FAILURE);
//安装信号
signal(SIGINT, handler);
char * addr = NULL;
//打开共享内存
//由于写进程已经创建了共享内存空间,读进程只需连接即可(后面两个参数传0即可)
if ((shmid = shmget(atoi(argv[1]), 0, 0)) == -1)
{
strerror(errno);
exit(EXIT_FAILURE);
}
//将共享内存映射到当前进程
if (-1 == (int)(addr = (char *)shmat(shmid, NULL, 0)))
{
fprintf(stderr, "addr == NULL");
exit(EXIT_FAILURE);
}
//读取共享内存数据
fprintf(stdout, "share:%s", addr);
//将当前进程挂起
for (; ;)
{
pause();
}
return 0;
}
程序运行结果:
我们可以看到使用共享内存进行进程间的通信真的是非常方便,而且函数的接口也简单,数据的共享还使进程间的数据不用传送,而是直接访问内存,也加快了程序的效率。同时,它也不像匿名管道那样要求通信的进程有一定的父子关系。需要注意的是,共享内存没有提供同步的机制,这使得我们在使用共享内存进行进程间通信时,往往要借助其他的手段来进行进程间的同步工作,如信号量等。
此外还有几个需要注意的地方,fork和vfork后,子进程继承父进程已连接的共享内存地址,fork会使共享内存连接数nattch加1,而vfork则不会,这也体现了fork与vfork之间的区别。exec后该子进程与已连接的共享内存地址自动脱离(detach),即连接计数自动减1。进程结束后,已连接的共享内存地址会自动脱离(detach),即引用计数减1(如果你已经调用过shmctl(shmid,IPC_RMID,0)删除共享内存,或shmdt卸载共享内存,那么引用计数是不会重复减1的,这个你大可放心)。当然,仅仅是脱离,并没有删除共享内存。shmdt函数调用并不删除所指定的共享内存区,而只是将先前用shmat函数连接(attach)好的共享内存脱离(detach)当前的进程。也就是说,shmat将共享内存连接到进程地址空间,并将共享内存连接计数nattch加1,而shmdt是进程中连接的工作内存地址无效,并使共享内存连接计数nattch减1。shmctl(shmid,IPC_RMID,0)删除共享内存,将共享内存标记为删除,并使共享内存连接计数nattch减1。只有在引用计数为0且标记为删除状态时才真正删除该共享内存。程序退出后,引用计数nattch减1,如果没有将共享内存置为删除状态,即使共享内存连接计数nattch为0,也不会被删除。此时你必须使用ipcrm命令来删除该共享内存,释放共享内存空间。因此,在创建共享内存实现多个进程间通信,至少需要一个进程将共享内存置为删除状态。
关于共享内存的学习我们就到此结束了,相信大家都有所收获,希望小伙伴们都已经理解并掌握了共享内存的常用方法。如果你觉得对进程间通信的方式不胜了解,还有些许疑惑,请关注博主《进程间通信方式总结》系列博文,相信你在那里能找到答案。