目录
一. 共享内存实现进程间通信的原理
二. 共享内存相关函数
2.1 共享内存的获取 shmget / ftok
2.2 共享内存与进程地址空间相关联 shmat
2.3 取消共享内存与进程地址空间的关联 shmdt
2.4 删除共享内存 shmctl
2.5 通信双方创建共享内存代码
三. 共享内存实现进程间通信
3.1 实现方法及特性
3.2 为共享内存添加访问控制
四. 总结
要实现进程间通信,就必须让相互之间进行通信的进程看到同一份资源(同一块内存空间),如通过管道实现进程间通信,本质就是让两个进程分别以读和写的方式打开同一份管道文件,一个进程向管道中写数据,另一个进程再从管道中将数据读出,这样两个进程就可以看到同一份内存空间,从而实现了进程间通信。
System V共享内存实现进程间通信的方式与管道相同,区别在于管道是基于文件的,而共享内存则是直接申请内存空间,不需用进行文件相关操作。通过System V共享内存实现通信的进程,都会使用物理内存中的同一块空间,这一块公共的物理内存空间经过通信双方进程的页表,映射到进程地址空间的共享区,通信双方进程在运行期间,拿到共享区虚拟地址,通过页表映射,就可以看到同一块物理内存,就可以实现进程间通信。
如果操作系统内有多组通过System V共享内存方式相互通信的进程处于运行状态,那么就会存在多组共享内存,操作系统需要对这些共享内存空间进行管理,管理方式为:先通过struct结构体进行描述,再利用特定的数据结构组织。
可以这样理解:共享内存 = 共享的物理内存 + 对应的内核级数据结构。
共享内存实现进程间通信的步骤可以总结为:创建共享内存 -> 共享内存与地址空间相关联 -> 通信 -> 共享内存与地址空间解绑 -> 销毁共享内存。
shmget函数:获取共享内存
头文件:#include
、#include 函数原型:int shmget(key_t key, size_t size, int shmflg)
函数参数:
key -- 特定共享内存的标识符
size -- 共享内存的大小
shmflg -- 共享内存获取的权限参数
返回值:创建成功返回共享内存的编号(称为shmid),失败返回-1
共享内存标识符key:OS中可能存在多个共享内存,需要保证通信双方看到同一块共享内存,因此,每个共享内存都需要一个特定的key值进行区分,这个key值是多少并不重要,只要保证它在OS中是唯一的即可。通信双方进程(Serve && Client)需要约定相同的算法,保证他们可以使用shmget获取到同一块共享内存。
ftok函数可以用于获取key值,只要调用ftok的实参相同,就会返回相同的key值。
ftok函数:获取共享内存标识符key
头文件: #include
、#include 函数原型:key_t ftok(const char* pathname, int proj_id);
函数参数:
pathname:项目(文件)路径
proj_id:项目(文件)的id编号
返回值:成功返回特定的key值,否则返回-1。
共享内存大小size:以字节为单位,建议取页(PAGE:4096bytes)大小的整数倍,因为如果获取共享内存空间的大小不是页大小的整数倍,OS就会向上取整申请到页大小整数倍的内存空间,但是多申请的空间却不能被用户所使用。如,申请4097bytes的共享内存,OS会实际申请2*4096bytes的空间,而能被使用的只有4097bytes,剩下的都浪费掉了。
权限参数shmflg:有IPC_CREAT、IPC_EXCL、共享内存起始权限码、0这几种选项,他们之间通过竖划线 | 隔开,每个选项都有其意义。
一般而言,通信双方分别以 IPC_CREAT | IPC_EXCL 和 0 的方式获取共享内存,确保一方创建全新的共享内存,另一方只能获取到该共享内存(传0阻断不存在创建新共享内存的可能)。
代码2.1以 IPC_CREAT | IPC_EXCL | 0666 的方式获取共享内存,运行代码,就可以成功获取共享内存,但是当第二次运行代码,却发现运行出错了(见图2.1),这是因为该共享内存再第一次程序运行后被创建,存在于操作系统中,IPC_CREAT | IPC_EXCL获取的共享内存一定是全新的,因此第二次运行程序会失败,删除该共享内存之后才可以再次成功运行。
结论:共享内存的生命周期是随OS内核的,而不是随进程的。
代码2.1:获取共享内存
// common.hpp -- 头文件
#pragma once
#include
#include
#include
#include
#define PATH_NAME "."
#define PROJ_ID 0x66
#define SIZE 4096
// shmServe.cc -- 客户端代码源文件(用于接收信息)
#include "common.hpp"
int main()
{
// 获取共享内存key值
key_t k = ftok(PATH_NAME, PROJ_ID);
if(k == -1)
{
perror("ftok");
exit(1);
}
// 创建共享内存
int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666);
if(shmid == -1)
{
perror("shmget");
exit(2);
}
printf("Serve# 共享内存获取成功,shmid:%d\n", shmid);
return 0;
}
这里介绍两条指令,分别用于查看共享内存信息和删除共享内存:
当然,也可以通过代码删除共享内存,本文后面会讲解。
shmat函数:将共享内存关联到进程地址空间
头文件:#include
、#include 函数原型:void* shmat(int shmid, const void* shmaddr, int shmflg)
函数参数:
shmid:进行挂接的共享内存的shmid
shmaddr:指定挂接的虚拟地址(传NULL表示让OS自动选择挂接地址)
shmflg:挂接权限相关参数
返回值:若成功返回挂接到的虚拟地址,失败返回nullptr
挂接地址shmaddr参数:由于我们并不可知虚拟地址的具体使用情况,所以这个参数基本都是传NULL/nullptr来让OS自动选择虚拟地址进行关联。
挂接权限shmflg:如果传SHM_RDONLY,这表示对应共享内存空间只有读权限,传其他都是读写权限,一般shmflg都传实参0。
当共享内存与虚拟地址关联期间,使用ipcs -m指令查看共享内存属性信息,nattch就会变为1,如果通信双方都与共享内存进行了关联,那么nattch就是2。
shmdt函数:让共享内存与当前进程脱离
头文件:#include
、#include 函数原型:int shmdt(const char* shmaddr)
返回值:成功返回0,失败返回-1
通过共享内存控制shmctl函数(共享内存控制函数),可以删除共享内存。
删除共享内存的操作只要通信双方有一方指向即可,否则会造成重复删除。一般而言,读取信息的进程创建新的共享内存,也负责删除共享内存,遵循谁创建、谁删除的原则。
shmctl函数:控制共享内存
头文件:#include
#include 函数原型:int shmctl(int shmid, int cmd, struct shmid_ds* buf)
函数参数:
shmid -- 共享内存的shmid
cmd -- 控制指令,选择操作
buf -- 指向描述共享内存属性信息的结构体指针
返回值:成功返回非负数,失败返回-1
形参cmd可以选择具体的控制策略:
代码2.2:头文件common.hpp -- 由通信双方共同包含
#pragma once
#include
#include
#include
#include
#include
#define PATH_NAME "."
#define PROJ_ID 0x66
#define SIZE 4096
代码2.3:服务端代码shmServe.cc -- 用于数据读取
#include "common.hpp"
int main()
{
// 获取共享内存key值
key_t k = ftok(PATH_NAME, PROJ_ID);
if(k == -1)
{
perror("Serve ftok");
exit(1);
}
printf("Serve# 成功获取key值,key:%d\n", k);
// 创建共享内存
int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666);
if(shmid == -1)
{
perror("Sreve shmget");
exit(2);
}
printf("Serve# 共享内存获取成功,shmid:%d\n", shmid);
// 将共享内存与进程相关联
char* shmaddr = (char*)shmat(shmid, NULL, 0);
if(shmaddr == nullptr)
{
perror("Serve shmat");
exit(3);
}
printf("Serve# 共享内存与进程成功关联,shmid:%d\n", shmid);
// 通信代码
// ... ...
// 让共享内存脱离当前进程
int n = shmdt(shmaddr);
if(n == -1)
{
perror("Serve shmdt");
exit(4);
}
printf("Serve# 共享内存成功脱离进程,shmid:%d\n", shmid);
// 删除共享内存
n = shmctl(shmid, IPC_RMID, NULL);
if(n == -1)
{
perror("Serve shmctl");
exit(5);
}
printf("Serve# 共享内存删除成功,shmid:%d\n", shmid);
return 0;
}
代码2.4:客户端代码shmClient.cc -- 用于数据发送
#include "common.hpp"
int main()
{
// 获取共享内存key值
key_t k = ftok(PATH_NAME, PROJ_ID);
if(k == -1)
{
perror("Client ftok");
exit(1);
}
printf("Client# 成功获取key值,key:%d\n", k);
// 创建共享内存
int shmid = shmget(k, SIZE, 0);
if(shmid == -1)
{
perror("Client shmget");
exit(2);
}
printf("Client# 共享内存获取成功,shmid:%d\n", shmid);
// 将共享内存与进程相关联
char* shmaddr = (char*)shmat(shmid, NULL, 0);
if(shmaddr == nullptr)
{
perror("Client shmat");
exit(3);
}
printf("Client# 共享内存与进程成功关联,shmid:%d\n", shmid);
// 通信代码
// ... ...
// 让共享内存脱离当前进程
int n = shmdt(shmaddr);
if(n == -1)
{
perror("Client shmdt");
exit(4);
}
printf("Client# 共享内存成功脱离进程,shmid:%d\n", shmid);
return 0;
}
在数据输入端(shmClient),我们可以将共享内存视为一块通过malloc得来的char*指向的一段动态内存空,可以使用printf系列函数向这块空间写数据,或者将共享内存空间视为数组,使用下标的形式给每个位置赋值,这样就实现了将数据写入共享内存。
在数据读取端(shmServe),可以将共享内存视为一个大字符串,通过特定的方式,从这个大字符串中获取数据即可。
代码3.1和代码3.2实现了共享内存进程间通信的简单逻辑,在shmClient端,通过下标访问的方式,每隔3s写一次数据,在shmServe端,每隔1s读取一次数据。先运行shmServe端代码,间隔几秒后运行shmClient端代码,根据图3.1展示的运行结果,shmServe端在shmClient端开始运行之前就开始读取共享内存中的内容,在shmClient运行起来后,由于读快写慢,shmClient写入的内容在shmServe端被多次读取,可见,共享内存,没有访问控制。
结论1:共享内存没有访问控制。
代码3.1:shmClient端发送数据
// 通信代码
char ch = 'a';
int count = 0;
for(; ch <= 'c'; ++ch)
{
shmaddr[count++] = ch;
printf("write succsee# %s\n", shmaddr);
sleep(3);
}
snprintf(shmaddr, SIZE, "quit");
代码3.2:shmServe端读取数据
// 通信代码
while(true)
{
printf("[Client say]# %s\n", shmaddr);
if(strcmp(shmaddr, "quit") == 0) break;
sleep(1);
}
通过观察上面的代码我们发现,用户可以直接向共享内存中写数据和从共享内存中读取数据,不需要经过用户级缓冲区,共享内存的读或写操作最少只需要一次拷贝即可完成。而通过管道进行读写,则需要将数据预先写入或读入缓冲区,才可以写入管道文件或读出。图3.2为使用管道和共享内存的方法进行进程间通信时,读和写操作涉及的数据拷贝情况,管道通信至少要进行两次数据拷贝,而共享内存可以只进行一次数据拷贝,因此共享内存是一种高效的进程间通信手段。
结论2:共享内存进行进程间通信,通信的一方向共享内存中写入数据,通信的另一方马上就能读取到数据,不需要向操作系统中拷贝数据,共享内存是所有进程间通信方法中效率最高的。
管道通信的特性总结:
通过使用命名管道加以辅助,就可以为共享内存添加访问控制,具体的实现方法和原理为:
代码3.3 ~ 3.5,为通过管道为共享内存添加访问控制的实现代码。
代码3.3:common.hpp头文件 -- 被通信双方源文件包含
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define PATH_NAME "."
#define PROJ_ID 0x66
#define SIZE 4096
#define FIFO_NAME "fifo.ipc"
#define MODE 0666
// 定义类,其构造和析构函数可以创建和销毁管道文件
class Init
{
public:
Init()
{
int n = mkfifo(FIFO_NAME, MODE);
if(n == -1) perror("mkfifo");
assert(n != -1);
(void)n;
}
~Init()
{
int n = unlink(FIFO_NAME);
assert(n != -1);
(void)n;
}
};
#define READ O_RDONLY
#define WRITE O_WRONLY
// 管道文件打开函数
int OpenFifo(const char* pathname, int flags)
{
int fd = open(pathname, flags);
assert(fd != -1);
return fd;
}
// 等待函数 -- 用于读端访问控制
// 管道内没有资源时就阻塞
void Wait(int fd)
{
uint32_t temp = 0;
ssize_t sz = read(fd, &temp, sizeof(uint32_t));
assert(sz == sizeof(uint32_t));
(void)sz;
}
// 唤醒函数 -- 用于写端进程控制
// 向管道内写数据,终止读端进程的阻塞等待
void WakeUp(int fd)
{
uint32_t temp = 1;
ssize_t sz = write(fd, &temp, sizeof(uint32_t));
assert(sz == sizeof(uint32_t));
(void)sz;
}
// 管道关闭函数
void CloseFifo(int fd)
{
close(fd);
}
代码3.4:读端源文件(shmServe.cc)代码
#include "common.hpp"
// 全局类对象
// 构造和析构函数分别负责管道文件的创建和销毁
Init init;
int main()
{
// 获取共享内存key值
key_t k = ftok(PATH_NAME, PROJ_ID);
if(k == -1)
{
perror("Serve ftok");
exit(1);
}
printf("Serve# 成功获取key值,key:%d\n", k);
// 创建共享内存
int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666);
if(shmid == -1)
{
perror("Sreve shmget");
exit(2);
}
printf("Serve# 共享内存获取成功,shmid:%d\n", shmid);
// 将共享内存与进程相关联
char* shmaddr = (char*)shmat(shmid, NULL, 0);
if(shmaddr == nullptr)
{
perror("Serve shmat");
exit(3);
}
printf("Serve# 共享内存与进程成功关联,shmid:%d\n", shmid);
// 通信代码
int fd = OpenFifo(FIFO_NAME, READ); // 只读方式打开管道文件
while(true)
{
Wait(fd); // 等待读取
printf("[Client say]# %s\n", shmaddr);
if(strcmp(shmaddr, "quit") == 0) break;
}
// while(true)
// {
// printf("[Client say]# %s\n", shmaddr);
// if(strcmp(shmaddr, "quit") == 0) break;
// sleep(1);
// }
// 让共享内存脱离当前进程
int n = shmdt(shmaddr);
if(n == -1)
{
perror("Serve shmdt");
exit(4);
}
printf("Serve# 共享内存成功脱离进程,shmid:%d\n", shmid);
// 删除共享内存
n = shmctl(shmid, IPC_RMID, NULL);
if(n == -1)
{
perror("Serve shmctl");
exit(5);
}
printf("Serve# 共享内存删除成功,shmid:%d\n", shmid);
CloseFifo(fd);
return 0;
}
代码3.5:写端源文件(shmClient.cc)代码
#include "common.hpp"
int main()
{
// 获取共享内存key值
key_t k = ftok(PATH_NAME, PROJ_ID);
if(k == -1)
{
perror("Client ftok");
exit(1);
}
printf("Client# 成功获取key值,key:%d\n", k);
// 创建共享内存
int shmid = shmget(k, SIZE, 0);
if(shmid == -1)
{
perror("Client shmget");
exit(2);
}
printf("Client# 共享内存获取成功,shmid:%d\n", shmid);
// 将共享内存与进程相关联
char* shmaddr = (char*)shmat(shmid, NULL, 0);
if(shmaddr == nullptr)
{
perror("Client shmat");
exit(3);
}
printf("Client# 共享内存与进程成功关联,shmid:%d\n", shmid);
// 通信代码
int fd = OpenFifo(FIFO_NAME, WRITE);
while(true)
{
ssize_t sz = read(0, shmaddr, SIZE); // 共享内存从键盘中读入数据(换行符也被写入)
assert(sz >= 0);
shmaddr[sz - 1] = '\0'; //末尾添加'\0'表示终止
WakeUp(fd); // 唤醒读端进程
if(strcmp(shmaddr, "quit") == 0) break;
}
// char ch = 'a';
// int count = 0;
// for(; ch <= 'c'; ++ch)
// {
// shmaddr[count++] = ch;
// printf("write succsee# %s\n", shmaddr);
// sleep(3);
// }
// snprintf(shmaddr, SIZE, "quit");
// 让共享内存脱离当前进程
int n = shmdt(shmaddr);
if(n == -1)
{
perror("Client shmdt");
exit(4);
}
printf("Client# 共享内存成功脱离进程,shmid:%d\n", shmid);
CloseFifo(fd);
return 0;
}