一、进程间通信
1.1 进程间通信的概念
进程间通信(IPC,Interprocess communication)是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。
1.2 进程间通信的本质
通俗的来讲,进程间通信其实就是为了让不同的进程看到同一份资源。
各个运行的进程之间都具有独立性,这个独立性主要体现在数据层面,而逻辑代码层面可以实现共有(例如子进程和父进程),因此实现各个进程之间的通信非常困难。若要想实现进程间通信,必须借助第三方资源。这些进程通过向第三方资源的写入或者读取数据,进而实现通信,第三方资源其实就是操作系统提供的一段内存区域。
1.3 进程间通信的目的
- 数据传输:一个进程将它的数据传输给另外的进程。
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知发生了某种事件,比如子进程终止时需要通知父进程。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
1.4 进程间通信的分类
- 管道
- 匿名管道
- 命名管道
- System V IPC
- System V 共享内存
- System V 消息队列
- System V 信号量
- POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
二、管道
2.1 管道的概念
管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个 “管道”。管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。
例如,统计我们当前使用云服务器上的登录用户个数。
who | wc -l
其中,who命令和wc命令都是两个程序,当它们运行起来后就变成了两个进程,who进程通过标准输出将数据传输到“管道”当中,wc进程再通过标准输入从“管道”当中读取数据,至此便完成了数据的传输,进而完成数据的进一步加工处理。
2.2 匿名管道
2.2.1 匿名管道的原理
匿名管道仅限于本地父子进程之间的通信
匿名管道实现父子进程间通信的原理就是,让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信。
注意:
- 这里父子进程看到的同一份文件资源是由操作系统来维护的,所以当父子进程对该文件进行写入操作时,该文件缓冲区当中的数据并不会进行写时拷贝。
- 管道虽然是一种文件,但是程序对管道写入的数据不会刷新进磁盘中,因为将数据刷新进磁盘会降低进程间通信的效率。
2.2.2 pipe函数
pipe系统函数的功能就是创建一个匿名管道。
函数原型:
#include <unistd.h>
int pipe(int fildes[2]);
参数:
fildes: pipe函数的参数是一个输出型参数,数组fildes用于返回两个指向管道读端和写端的文件描述符,其中fidles[0]表示读端,fildes[1]表示写端。
返回值:
创建成功返回0,失败返回错误码。
2.2.3 匿名管道的创建与使用
1、首先父进程调用pipe系统调用创造匿名管道。
2、父进程调用fork函数创建子进程。
3、 父进程关闭写端,子进程关闭读端。
此时就可以实现父子间的进程通信了,即子进程向管道中写入数据,父进程从管道中读取数据。
【注意事项】
- 管道只能够进行单向通信,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端。
- 从管道写端写入的数据会被内核缓冲,直到从管道的读端被读取。即只有当子进程写完了数据,父进程才能读取。
站在文件描述符角度来深度理解管道
站在内核角度看待管道
所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了 “Linux一切皆文件思想” 。
以下是父子进程间通信的简单代码:
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <ctime>
#include <string>
#include <sys/wait.h>
#include <cassert>
using namespace std;
int main()
{
//创建匿名管道
int pipefd[2] = {0};
if(pipe(pipefd) != 0)
{
cerr << "pipe error" << endl;
return 1;
}
//创建子进程
pid_t id = fork();
if(id < 0)
{
cerr << "fork error" << endl;
return 2;
}
else if(id == 0)
{
//子进程进行读操作,应该关闭写
close(pipefd[1]);
#define NUM 1024
char buffer[NUM];
while(true)
{
cout << "时间戳:" <<(uint64_t)time(nullptr) << endl;
memset(buffer, 0, sizeof(buffer));
ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
if(s > 0)
{
buffer[s] = '\0';
cout << "子进程收到消息,内容是:" << buffer << endl;
}
else if(s == 0)
{
cout << "父进程写完了,子进程退出" << endl;
break;
}
else
{
//do nothing
}
}
close(pipefd[0]);
exit(0);
}
else
{
//父进程进行写操作,应该关闭读
close(pipefd[0]);
const char* msg = "你好啊,子进程!我是父进程。这次发送信息的编号:";
int cnt = 0;
while(cnt < 5)
{
char sendBuffer[1024];
sprintf(sendBuffer, "%s : %d", msg, cnt);
sleep(1);
write(pipefd[1], sendBuffer, strlen(msg));
++cnt;
cout << "cnt:" << cnt <<endl;
}
close(pipefd[1]);
cout << "父进程写完了" <<endl;
}
}
2.2.4 匿名管道读写规则
pipe2函数与pipe函数类似,也是用于创建匿名管道,其函数原型如下:
int pipe2(int pipefd[2], int flags);
pipe2函数的第二个参数用于设置选项。
- 当没有数据可读时:
- O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
- O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
- 当管道满时:
- O_NONBLOCK disable: write调用阻塞,直到有进程读走数据。
- O_NONBLOCK enable:调用返回-1,errno值为EAGAIN。
- 如果所有管道写端对应的文件描述符被关闭,则read返回0。
- 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。
- 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
- 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
2.2.5 匿名管道的特点
1、只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信
通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
2、内核会对管道操作进行同步与互斥
在一段时间内只允许一个进程访问的资源,又称独占资源。管道在同一时刻只允许一个进程对其进行写入或者读取操作,因此管道也属于临界资源。临界资源需要被保护,否则可能会出现同一个时刻有多个进程对同一个管道进行写入或读取的操作,导致同时读写,交叉读写等情况以至于最后读取的数据没能达到预期的结果。
为了避免这些问题,操作系统对管道操作进行同步与互斥:
- 同步:是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。
- 互斥:是指散步在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。
同步是一种更为复杂的互斥,而互斥是一种特殊的同步。也就是说互斥是两个任务之间不可以同时运行,他们会相互排斥,必须等待一个线程运行完毕,另一个才能运行,而同步也是不能同时运行,但他是必须要按照某种次序来运行相应的线程(也是一种互斥)!因此互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,即任务是无序的,而同步的任务之间则有顺序关系。
3、管道的生命周期随进程
管道本质上就是文件,它依赖于文件系统,即当所有打开管道的进程都退出后,该文件也会被释放掉,所以管道的生命周期随进程。
4、管道是半双工的,数据只能向一个方向流动
在数据通信中,数据在线路上的传送方式可以分为以下三种:
- 单工通信(Simplex Communication):单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。
- 半双工通信(Half Duplex):半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。
- 全双工通信(Full Duplex):全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。
管道是半双工的,数据只能向一个方向传输,若需要双向通信,则需要创建两个管道。
5、管道提供流式服务
一个进程向管道中写入数据,另一个进程每次从管道读取的数据的多少是任意的,这种被称为流式服务,与之相对应的是数据报服务。
- 流式服务: 数据没有明确的分割,不分一定的报文段。
- 数据报服务: 数据有明确的分割,读数据按报文段读取。
2.3. 命名管道
2.3.1 命名管道的概述
匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了命名管道(FIFO),也叫有名管道、FIFO 文件。
命名管道(FIFO)不同于无名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中。这样,即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信。因此,通过 FIFO 不相关的进程也能交换数据。
2.3.2 命名管道与匿名管道的区别
命名管道与匿名管道的特点部分相同,不同之处在于:
- 匿名管道由
pipe
函数创建并打开,而命名管道有mkfifo
函数创建,使用open
函数打开。 - 命名管道在文件系统中作为一个特殊的文件而存在,但命名管道中的内容却存放在内存中,向命名管道中写入的数据不会刷新到磁盘中。
- 当使用命名管道的进程退出后,命名管道文件将继续保存在文件系统中以便以后使用。
2.3.3 命名管道打开的规则
1、 如果当前打开操作是为读而打开FIFO时
- O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
- O_NONBLOCK enable:立刻返回成功
2、如果当前打开操作是为写而打开FIFO时
- O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
- O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
2.3.4 命名管道的创建
使用mkfifo
命令创建匿名管道
mkfifo fifo
使用这个命名管道文件,就能实现两个进程之间的通信了。我们在一个进程中用shell脚本每秒向命名管道写入一个字符串,在另一个进程中用cat命令从命名管道当中进行读取。现象就是当第一个进程启动后,另一个进程会每秒从命名管道中读取一个字符串打印到显示器上。这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,即通信。
当管道的读端进程退出后,写端进程再向管道写入数据就没有意义了,此时写端进程会被操作系统杀掉,在这里就可以很好的得到验证:当我们终止掉读端进程后,因为写端执行的循环脚本是由命令行解释器bash执行的,所以此时bash就会被操作系统杀掉,我们的云服务器也就退出了。
使用mkfifo
函数创建管道
在程序中使用mkfifo
函数创建命名管道,函数原型如下:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数:
- pathname: 表示要创建的命名管道文件。
若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。 - mode: 表示创建命名管道文件的默认权限。
返回值:
- 命名管道创建成功,返回0。
- 命名管道创建失败,返回-1。
例如,使用下面的代码创建命名管道:
#include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#define FILE_NAME "myfifo"
int main()
{
umask(0); //将权限掩码设置为0
if(mkfifo(FILE_NAME, 0666) < 0)
{
perror("mkfile");
return 1;
}
//创建命名管道成功...
return 0;
}
编译运行产生结果如下:
2.3.5 命名管道实现Server与Client进程间通信
实现服务端和客户端的通信,需要先启动服务端并让其创建一个命名管道文件,任何再以读的方式打开该命名管道,之后就能读取到来自客户端写入的消息。
服务端代码:
#include "comm.h"
using namespace std;
#define NUM 1024
int main()
{
//创建命名管道文件
umask(0);
if (mkfifo(IPC_PATH, 0600) != 0)
{
cerr << "mkfifo error" << endl;
return 1;
}
//打开命名管道文件
int pipeFd = open(IPC_PATH, O_RDONLY);
if (pipeFd < 0)
{
cerr << "open error" << endl;
return 2;
}
//向命名管道中读取数据
char buffer[NUM];
while (true)
{
ssize_t s = read(pipeFd, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = '\0';
cout << "客户端 -> 服务端# " << buffer << endl;
}
else if (s == 0)
{
cout << "客户端退出了,服务端也退出!" << endl;
break;
}
else
{
//读取数据错误
cout << "read: " << strerror(errno) << endl;
break;
}
}
close(pipeFd);
cout << "服务端退出" << endl;
unlink(IPC_PATH);
return 0;
}
对于客户端来说,因为命名管道已经由服务端创建好了,所以不需要再次创建,客户端只需要以写的方式打开服务端创建的命名管道并写入数据即可,从而实现客户端与服务端的进程间通信。
客户端代码:
#include "comm.h"
using namespace std;
#define NUM 1024
int main()
{
//打开命名管道
int pipeFd = open(IPC_PATH, O_WRONLY);
if(pipeFd < 0)
{
cerr << "open error" << endl;
return 2;
}
//向命名管道中写入数据
char line[NUM];
while(true)
{
printf("请输入你的消息# ");
fflush(stdout);
memset(line, 0, sizeof(line));
//fgets得到C风格字符串,会在末尾自动添加 '\0'
if(fgets(line, sizeof(line), stdin) != nullptr)
{
line[strlen(line) - 1] = '\0'; //处理回车 例如输入 abcd/n/0
write(pipeFd, line, strlen(line));
}
else
{
break;
}
}
close(pipeFd);
cout << "客户端退出" << endl;
return 0;
}
为了让客户端和服务端使用同一个命名管道文件,这里让客户端和服务端都共同包含一个头文件,该头文件当中提供这个共用的命名管道文件的文件名,这样客户端和服务端就可以通过这个文件名,打开同一个命名管道文件,进而进行通信了。
共同头文件代码:
#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
函数的作用是将一个已经存在的路径名pathname
和一个proj_id
转换成一个key
值,称为IPC键值,在使用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中。需要注意的是,pathname所指定的文件必须存在且可存取。
【注意】
- 使用ftok函数生成key值可能会产生冲突,此时可以对传入ftok函数的参数进行修改。
- 需要进行通信的各个进程,在使用ftok函数获取key值时,都需要采用同样的路径名和和整数标识符,进而生成同一种key值,然后才能找到同一个共享资源。
传入shmget函数的第三个参数shmflg有两种常用组合方式:
组合方式 | 说明 |
---|---|
IPC_CREAT | 如果系统中不存在与key值相等的共享内存,则新创建一个共享内存,否则之间返回已经存在的共享内存的key值 |
IPC_EXCL 按位或 IPC_CREAT | 如果系统中不存在与key值相等的共享内存,则新创建一个共享内存,否则出错返回 |
也就是说使用组合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
同时运行两个程序,我们发现它们关联的是同一个共享内存。
此时我们就可以让服务端和客户端进行通信了,这里以简单的发送字符串为例。
以上就是利用共享内存实现进程间通信。但是我们发现共享内存的使用方式和普通内存一样,实际上共享内存是被映射到了进程的地址空间中了(堆栈之间)。对于每一个进程而言,就是把共享内存挂接到自己的用户空间,之后共享内存就类似于堆或者栈空间,可以被用户直接使用。
然而,使用共享内存进行通信,没有任何的访问控制,它是直接被通信进程双方共享的,因此可以直接通信,所以共享内存是速度最快的进程间通信方式,但是由于没有访问控制,所以也是不安全的。