【Linux学习】进程间通信的方式(匿名管道、命名管道、共享内存)1:https://developer.aliyun.com/article/1383928
共同头文件代码:
#pragma once #include <iostream> #include <cstdio> #include <string> #include <cstring> #include <cerrno> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #define IPC_PATH "./.fifo"
最后编译完代码,先让服务端运行起来,再运行客户端,并向服务端发送消息。
当客户端退出后,服务端将管道当中的数据读完后就再也读不到数据了,那么此时服务端也就会去执行它的其他代码了(在当前代码中设定的是直接退出)。
三、System V共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
3.1 共享内存的原理
在Linux中,每个进程都有属于自己的进程控制块(PCB)和地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。
当两个进程通过页表将虚拟地址映射到物理地址时,在物理地址中有一块共同的内存区,即共享内存,这块内存可以被两个进程同时看到。这样当一个进程进行写操作,另一个进程读操作就可以实现进程间通信。但是,我们要确保一个进程在写的时候不能被读,因此我们使用信号量来实现同步与互斥。
对于一个共享内存,实现采用的是引用计数的原理,当进程脱离共享存储区后,计数器减一,挂架成功时,计数器加一,只有当计数器变为零时,才能被删除。当进程终止时,它所附加的共享存储区都会自动脱离。
3.2 共享内存的数据结构
操作系统中可能存在大量的进程在进行进程间通信,因此可能存在许多的共享内存。那么操作系统就必须对这些共享内存进行管理,所以操作系统出来为共享内存开辟空间外,还需要为共享内存创建维护管理相关的内核数据结构。
共享内存的数据结构:
struct shmid_ds { struct ipc_perm shm_perm; /* operation perms */ int shm_segsz; /* size of segment (bytes) */ __kernel_time_t shm_atime; /* last attach time */ __kernel_time_t shm_dtime; /* last detach time */ __kernel_time_t shm_ctime; /* last change time */ __kernel_ipc_pid_t shm_cpid; /* pid of creator */ __kernel_ipc_pid_t shm_lpid; /* pid of last operator */ unsigned short shm_nattch; /* no. of current attaches */ unsigned short shm_unused; /* compatibility */ void *shm_unused2; /* ditto - used by DIPC */ void *shm_unused3; /* unused */ };
当我们申请了一块共享内存后,为了让要实现通信的进程能够看到同一个共享内存,因此每一个共享内存被申请时都有一个key值,这个key值用于标识系统中共享内存的唯一性。可以看到上面共享内存数据结构的第一个成员是shm_perm,shm_perm是一个ipc_perm类型的结构体变量,每个共享内存的key值存储在shm_perm这个结构体变量当中,其中ipc_perm结构体的定义如下:
**加粗样式**struct ipc_perm{ __kernel_key_t key; __kernel_uid_t uid; __kernel_gid_t gid; __kernel_uid_t cuid; __kernel_gid_t cgid; __kernel_mode_t mode; unsigned short seq; };
3.3 共享内存函数
3.3.1 shmget函数
功能:
用来创建共享内存
函数原型:
#include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg);
参数:
key:表示带创建共享内存在系统当中的唯一标识符。
size:表示共享内存的大小。
shmflg:表示创建共享内存的方式。由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:
调用成功,返回一个有效的共享内存标识符(用户层标识符)。
调用失败,返回-1。
【注意】
传入shmget函数的第一个参数key,需要我们使用ftok函数进行获取。
ftok
函数原型
#include <sys/types.h> #include <sys/ipc.h> key_t ftok(const char *pathname, int proj_id);
【注意】
使用ftok函数生成key值可能会产生冲突,此时可以对传入ftok函数的参数进行修改。
需要进行通信的各个进程,在使用ftok函数获取key值时,都需要采用同样的路径名和和整数标识符,进而生成同一种key值,然后才能找到同一个共享资源。
传入shmget函数的第三个参数shmflg有两种常用组合方式:
也就是说使用组合IPC_CREAT
,一定会获得一个共享内存,但无法确认该共享内存是否是新建的共享内存。使用IPC_CREAT | IPC_EXCL
,只有shmget
函数调用成功时才会获得共享内存,并且该共享内存一定是新建的共享内存。
3.3.2 shmat函数
功能:
将共享内存段连接到进程地址空间。
函数原型:
#include <sys/types.h> #include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid:表示待关联共享内存的用户级标识符。
shmaddr:指定共享内存映射到进程地址空间的某一地址,通常设置为NULL,表示让内核自己决定一个合适的地址位置。
shmflg:表示关联共享内存时设置的某些属性。它的两个可能取值是SHM_RND和SHM_RDONLY。
返回值:
调用成功返回共享内存映射到进程地址空间中的起始地址。
失败则返回
(void*)-1
。
其中,作为shmat函数的第三个参数传入的常用的选项有以下三个:
选项 | 说明 |
SHM_RDONLY | 关联共享内存后只进行读取操作 |
SHM_RND | 若shmaddr不为NULL,则关联地址自动向下调整为SHMLBA的整数倍。公式:shmaddr-(shmaddr%SHMLBA) |
3.3.3 shmdt函数
功能:
将共享内存段与当前进程脱离。
函数原型:
#include <sys/types.h> #include <sys/shm.h> int shmdt(const void *shmaddr);
参数:
待去关联共享内存的起始地址,即调用shmat函数时得到的起始地址。
返回值:
调用成功,返回0;
调用失败,返回1。
【注意】
将共享内存段与当前进程脱离不等于删除共享内存段。
3.3.4 shmctl函数
功能:
用于控制共享内存。
函数原型:
#include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid:由shmget返回的共享内存标识码。
cmd:将要采取的动作(有三个可取值)。
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构。
cmd的三个值:
命令 | 说明 |
IPC_STAT | 把shmid_ds结构中的数据设置为共享内存的当前关联值 |
IPC_SET | 在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值 |
IPC_RMID | 删除共享内存段 |
3.4 共享内存的使用
3.4.1 共享内存的创建
使用ftok
函数和shmget
函数创建共享内存,代码如下:
#include <iostream> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <unistd.h> using namespace std; #define PATH_NAME "/home/lhf/linux_code/test_IPC" //路径名 #define PROJ_ID 0x14 //整数标识符 #define SIZE 4096 //共享内存的大小 int main() { key_t key = ftok(PATH_NAME, PROJ_ID); //获取key值 if (key < 0) { cerr << "ftok error" << endl; return 1; } int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL); //创建新的共享内存 if (shm < 0) { cerr << "shmget error" << endl; return 2; } cout << "key = " << key << endl; cout << "shm = " << shm << endl; return 0; }
编译运行,发现创建共享内存成功。
当我们再次运行时,发现创建失败。
原因是共享内存已经存在,而我们传入的shmflg参数是IPC_CREAT | IPC_EXCL
,即共享内存已经存在则会创建失败。
在Linux当中,我们可以使用ipcs
命令查看有关进程间通信设施的信息。
单独使用ipcs
命令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:
- -q:列出消息队列相关信息。
- -m:列出共享内存相关信息。
- -s:列出信号量相关信息。
例如,携带-m选项查看共享内存相关信息:
根据ipcs
命令的查看结果和我们的输出结果可以确认,共享内存已经创建成功了。
ipcs命令输出的每列信息的含义如下:
名称 | 说明 |
key | 系统区别各个共享内存的唯一标识 |
shmid | 共享内存的用户层id |
owner | 共享内存的拥有者 |
perms | 共享内存的权限 |
bytes | 共享内存的大小 |
nattch | 关联共享内存的进程数量 |
status | 共享内存的状态 |
【注意】
key是在内核层面上保证共享内存唯一性的方式,而shmid是在用户层面上保证共享内存的唯一性,key和shmid之间的关系类似于 FILE* 和 fd 之间的的关系。
3.4.2 共享内存的释放
通过上面创建共享内存的过程我们可以发现,当进程结束后,申请的共享内存依然存在,没有被操作系统释放。实际上,管道是生命周期是随进程的,而共享内存的生命周期是随内核的,也就是说进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放。如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC都是如此),同时也说明了IPC资源是由内核提供并维护的。
如果要将创建的共享内存释放,有两个方法,一就是使用命令释放共享内存,二就是在进程通信完毕后调用释放共享内存的函数进行释放。
使用命令释放共享内存:
使用ipcrm -m shmid
命令释放指定id的共享内存资源。
ipcrm -m 0
此时,共享内存已经释放成功。
使用shmctl函数释放内存:
#include <iostream> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <unistd.h> using namespace std; #define PATH_NAME "/home/lhf/linux_code/test_IPC" //路径名 #define PROJ_ID 0x14 //整数标识符 #define SIZE 4096 //共享内存的大小 int main() { key_t key = ftok(PATH_NAME, PROJ_ID); //获取key值 if (key < 0) { cerr << "ftok error" << endl; return 1; } int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL); //创建新的共享内存 if (shm < 0) { cerr << "shmget error" << endl; return 2; } cout << "key = " << key << endl; cout << "shm = " << shm << endl; cout<< "5s 后释放共享内存..." <<endl; sleep(2); shmctl(shm, IPC_RMID, nullptr); cout << "释放共享内存成功。" << endl; return 0; }
我们可以利用shell监控脚本查看共享内存创建和释放的过程:
while :; do ipcs -m ; echo "-----------------------";sleep 1;done
我们可以发现,共享内存最终成功被释放。
3.4.3 共享内存的关联
使用shmat
函数将共享内存连接到进程地址空间。代码如下:
#include <iostream> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <unistd.h> using namespace std; #define PATH_NAME "/home/lhf/linux_code/test_IPC" //路径名 #define PROJ_ID 0x14 //整数标识符 #define SIZE 4096 //共享内存的大小 int main() { key_t key = ftok(PATH_NAME, PROJ_ID); //获取key值 if (key < 0) { cerr << "ftok error" << endl; return 1; } int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建新的共享内存 if (shm < 0) { cerr << "shmget error" << endl; return 2; } cout << "key = " << key << endl; cout << "shm = " << shm << endl; cout << "开始关联共享内存..."<< endl; sleep(2); char* str = (char*)shmat(shm, nullptr, 0); if(str == (char*) -1) { cerr << "shmat error" << endl; return 1; } cout << "关联共享内存成功" << endl; cout << "开始释放共享内存..." << endl; sleep(2); shmctl(shm, IPC_RMID, nullptr); cout << "释放共享内存成功" << endl; return 0; }
此时我们发现关联失败,那是因为在使用shmget创建共享内存的时候我们没有给出权限,系统默认给的权限是0,因此该共享内存没有权限去关联进程。
修改权限后:
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建新的共享内存
此时关联成功。
3.4.4 共享内存的去关联
使用shmdt
函数取消共享内存与进程地址空间之间的关联。代码如下:
#include <iostream> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <unistd.h> using namespace std; #define PATH_NAME "/home/lhf/linux_code/test_IPC" //路径名 #define PROJ_ID 0x14 //整数标识符 #define SIZE 4096 //共享内存的大小 int main() { key_t key = ftok(PATH_NAME, PROJ_ID); //获取key值 if (key < 0) { cerr << "ftok error" << endl; return 1; } int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建新的共享内存 if (shm < 0) { cerr << "shmget error" << endl; return 2; } cout << "key = " << key << endl; cout << "shm = " << shm << endl; cout << "开始关联共享内存..."<< endl; sleep(2); char* str = (char*)shmat(shm, nullptr, 0); if(str == (char*) -1) { cerr << "shmat error" << endl; return 1; } cout << "关联共享内存成功" << endl; cout << "开始去关联..." << endl; sleep(2); shmdt(str); cout << "去关联成功" << endl; cout << "开始释放共享内存..." << endl; sleep(2); shmctl(shm, IPC_RMID, nullptr); cout << "释放共享内存成功" << endl; return 0; }
运行程序,通过监控即可发现该共享内存的关联数由1变为0的过程,即取消了共享内存与该进程之间的关联。
3.4.5 共享内存实现Server与Client进程间通信
知道了共享内存的创建、关联、去关联以及释放后,现在可以尝试让两个进程通过共享内存进行通信了。这里我们依然模拟客户端和服务端之间的进程间通信。
服务端负责创建共享内存,创建好后将共享内存和服务端进行关联,服务端代码如下:
#include "comm.h" using namespace std; int main() { //获取key key_t key = ftok(PATH_NAME, PROJ_ID); if (key < 0) { cerr << "ftok error" << endl; exit(1); } //创建共享内存 cout << "开始创建共享内存..." << endl; sleep(2); int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); if (shmid < 0) { cerr << "shmget error" << endl; exit(2); } cout << "创建共享内存成功" << endl; printf("key = %x\n", key); printf("shmid = %d\n", shmid); //关联共享内存 cout << "开始关联共享内存..." << endl; char *str = (char *)shmat(shmid, nullptr, 0); if (str == (char *)-1) { cerr << "shmat error" << endl; exit(3); } cout << "关联共享内存成功" << endl; //使用共享内存 cout << "开始使用共享内存..." << endl; sleep(2); while(true) { // 让读端进行等待 printf("%s\n", str); sleep(1); } cout << "使用共享内存结束" << endl; cout << "开始去关联共享内存..." << endl; sleep(2); shmdt(str); cout << "去关联共享内存成功" << endl; cout << "开始释放共享内存..." << endl; sleep(2); shmctl(shmid, IPC_RMID, nullptr); cout << "释放共享内存成功" << endl; return 0; }
客户端只需要直接和服务端创建的共享内存进行关联即可,代码如下:
#include "comm.h" using namespace std; int main() { //获取相同的key key_t key = ftok(PATH_NAME, PROJ_ID); if (key < 0) { cerr << "ftok error" << endl; exit(1); } // 获取共享内存 int shmid = shmget(key, SIZE, IPC_CREAT); if (shmid < 0) { cerr << "shmget error" << endl; exit(2); } //关联共享内存 char *str = (char *)shmat(shmid, nullptr, 0); if (str == (char *)-1) { cerr << "shmat error" << endl; exit(3); } //使用共享内存 int cnt = 0; while (cnt <= 26) { str[cnt] = 'A' + cnt; ++cnt; str[cnt] = '\0'; sleep(1); } // 去关联 shmdt(str); return 0; }
为了让服务端和客户端在使用ftok函数获取key值时,能够得到同一种key值,那么服务端和客户端传入ftok函数的路径名和和整数标识符必须相同,这样才能生成同一种key值,进而找到同一个共享资源进行挂接。这里我们可以将这些需要共用的信息放入一个头文件当中,服务端和客户端共用这个头文件即可。
共同头文件代码:
#include <iostream> #include <cstring> #include <cstdio> #include <cstdlib> #include <cerrno> #include <cassert> #include <unistd.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #define PATH_NAME "/home/lhf/linux_code/test_IPC" #define PROJ_ID 0x666 #define SIZE 4096
同时运行两个程序,我们发现它们关联的是同一个共享内存。
此时我们就可以让服务端和客户端进行通信了,这里以简单的发送字符串为例。
以上就是利用共享内存实现进程间通信。但是我们发现共享内存的使用方式和普通内存一样,实际上共享内存是被映射到了进程的地址空间中了(堆栈之间)。对于每一个进程而言,就是把共享内存挂接到自己的用户空间,之后共享内存就类似于堆或者栈空间,可以被用户直接使用。
然而,使用共享内存进行通信,没有任何的访问控制,它是直接被通信进程双方共享的,因此可以直接通信,所以共享内存是速度最快的进程间通信方式,但是由于没有访问控制,所以也是不安全的。