目录
一、进程间通信介绍
什么是进程间通信
进程间通信目的
如何实现进程间通信
进程间通信分类
管道
System V IPC
POSIX IPC
二、管道
什么是管道
匿名管道
匿名管道的概念
匿名管道的接口
匿名管道的创建
匿名管道读写规则
匿名管道的应用场景
命名管道
创建一个命名管道
匿名管道与命名管道的区别
命名管道的打开规则
使用命名管道实现server&&client通信
System V共享内存
共享内存的原理
共享内存的数据结构
共享内存函数
ftok函数
创建共享内存—shmget函数
共享内存的释放—指令或shmctl函数
关联共享内存—shmat函数
共享内存的去关联—shmdt函数
使用共享内存实现server&&client通信
system V消息队列
system V信号量
进程间通信(IPC,InterProcess Communication)是两个或者多个进程实现数据层面的交互。日常生活中,一个大型的应用系统往往需要众多进程协作进行,进程通过与内核及其他进程之间的互相通信来协调它们的行为。但是进程间通信是有成本的,因为进程独立性的存在,导致进程通信的成本较高。
a.进程间通信的本质:必须让不同的进程看到同一份"资源"。
b."资源":特定形式的内存空间
这里的资源其实就是一段内存。它可能以文件方式提供,也可能以队列的方式也有可能提供的就是原始的内存块。这也就是进程通信方式有很多种的原因。
c. 这个"资源"谁提供? 一般是操作系统
为什么不是我们两个进程中的一个呢? 假设一个进程提供,这个资源属于谁? 这个进程独有,破坏进程独立性。第三方空间。
d. 我们进程访问这个空间,进行通信,本质就是访问操作系统!
进程代表的就是用户,“资源”从创建,使用(一般), 释放都要使用系统调用接口!!
举例:who | wc -l
说明:其中who和wc是我们的两个进程,who进程通过标准输出将数据放到管道中,wc通过标准输入从管道中读取数据,如此一来便实现了进程间通信。
注意:who命令用于查看当前云服务器的登录用户(一行显示一个用户),wc-l用于统计当前的行数。
我们常用pipe函数来创建匿名管道,我们用man来看一下pipe函数:
pipe函数的用法如下:
#include
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
匿名管道的创建原理如下图所示:
学习文件描述符的时候,我们知道一个进程的0,1,2文件描述符是会分配给标准输入、标准输出与标准错误的。
所以按照文件描述符的分配规则,操作系统为管道文件分配的两个文件描述符应该是3和4。
我们用一段代码来验证一下:
可以看到OS为管道文件分配的两个文件描述符确实是3和4。
了解了上面的匿名管道的创建原理之后,下面我们来看父子进程通过匿名管道通信的原理
在创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,用fork来共享管道原理。具体步骤如下(这里以父进程读,子进程写为例):
1. 父进程通过pipe函数创建管道
2.父进程通过调用fork函数创建子进程
3.fork之后父子进程各自关掉不用的文件描述符
如此一来父子进程便可以通过匿名管道建立了通信的通道。
下面我们再站在文件描述符角度-深度理解管道
父进程写,子进程读:
下面我们来用代码来实现一下父子进程通过匿名管道实现进程间通信:
#include
#include
#include
#include
#include //stdlib.h
#include
#include
#include
#define N 2
#define NUM 1024
using namespace std;
// child
void Writer(int wfd)
{
string s = "hello, I am child";
pid_t self = getpid();
int number = 0;
char buffer[NUM];
while (true)
{
// 构建发送字符串
buffer[0] = 0; // 字符串清空, 只是为了提醒阅读代码的人,我把这个数组当做字符串了
snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);
cout << buffer << endl;
// 发送/写入给父进程, system call
write(wfd, buffer, strlen(buffer)); // strlen(buffer) + 1???
sleep(1);
}
}
// father
void Reader(int rfd)
{
char buffer[NUM];
while(true)
{
buffer[0] = 0;
// system call
ssize_t n = read(rfd, buffer, sizeof(buffer)); //sizeof != strlen,sizeof(buffer)表示缓冲区大小
if(n > 0)
{
buffer[n] = 0; // 0 == '\0'
cout << "father get a message[" << getpid() << "]# " << buffer << endl;
}
}
}
int main()
{
int pipefd[N] = {0};
int n = pipe(pipefd);
if (n < 0)
return 1;
// child -> w, father->r
pid_t id = fork();
if (id < 0)
return 2;
if (id == 0)
{
// child
close(pipefd[0]);//子进程关闭读端
// IPC code
Writer(pipefd[1]);
close(pipefd[1]);
exit(0);
}
// father
close(pipefd[1]);//父进程关闭写端
// IPC code
Reader(pipefd[0]);
pid_t rid = waitpid(id, nullptr, 0);
if(rid < 0) return 3;
close(pipefd[0]);
return 0;
}
运行结果:
父子进程便通过匿名管道完成了进程间通信。
1.读写端正常,管道如果为空,读端就要阻塞
我们让子进程每5秒写一次,读端一直读,来观察一下现象
#include
#include
#include
#include
#include //stdlib.h
#include
#include
#include
#define N 2
#define NUM 1024
using namespace std;
// child
void Writer(int wfd)
{
string s = "hello, I am child";
pid_t self = getpid();
int number = 0;
char buffer[NUM];
while (true)
{
// 构建发送字符串
buffer[0] = 0; // 字符串清空, 只是为了提醒阅读代码的人,我把这个数组当做字符串了
snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);
cout << buffer << endl;
// 发送/写入给父进程, system call
write(wfd, buffer, strlen(buffer)); // strlen(buffer) + 1???
sleep(5);
}
}
// father
void Reader(int rfd)
{
char buffer[NUM];
while(true)
{
buffer[0] = 0;
// system call
ssize_t n = read(rfd, buffer, sizeof(buffer)); //sizeof != strlen,sizeof(buffer)表示缓冲区大小
if(n > 0)
{
buffer[n] = 0; // 0 == '\0'
cout << "father get a message[" << getpid() << "]# " << buffer << endl;
}
}
}
int main()
{
int pipefd[N] = {0};
int n = pipe(pipefd);
if (n < 0)
return 1;
// child -> w, father->r
pid_t id = fork();
if (id < 0)
return 2;
if (id == 0)
{
// child
close(pipefd[0]);//子进程关闭读端
// IPC code
Writer(pipefd[1]);
close(pipefd[1]);
exit(0);
}
// father
close(pipefd[1]);//父进程关闭写端
// IPC code
Reader(pipefd[0]);
pid_t rid = waitpid(id, nullptr, 0);
if(rid < 0) return 3;
close(pipefd[0]);
return 0;
}
运行结果:
我们看到当读取管道中的数据时,如果管道中没有数据或者写入速度小于读取速度,读取端可能会被长时间阻塞。
2.读写端正常,管道如果被写满,写端就要阻塞
我们让子进程一直写,父进程每5秒读一次来观察一下现象
#include
#include
#include
#include
#include //stdlib.h
#include
#include
#include
#define N 2
#define NUM 1024
using namespace std;
// child
void Writer(int wfd)
{
string s = "hello, I am child";
pid_t self = getpid();
int number = 0;
char buffer[NUM];
while (true)
{
// 构建发送字符串
buffer[0] = 0; // 字符串清空, 只是为了提醒阅读代码的人,我把这个数组当做字符串了
snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);
// cout << buffer << endl;
// 发送/写入给父进程, system call
write(wfd, buffer, strlen(buffer)); // strlen(buffer) + 1???
cout << number << endl;
}
}
// father
void Reader(int rfd)
{
char buffer[NUM];
while(true)
{
sleep(5);
buffer[0] = 0;
// system call
ssize_t n = read(rfd, buffer, sizeof(buffer)); //sizeof != strlen,sizeof(buffer)表示缓冲区大小
if(n > 0)
{
buffer[n] = 0; // 0 == '\0'
cout << "father get a message[" << getpid() << "]# " << buffer << endl;
}
}
}
int main()
{
int pipefd[N] = {0};
int n = pipe(pipefd);
if (n < 0)
return 1;
// child -> w, father->r
pid_t id = fork();
if (id < 0)
return 2;
if (id == 0)
{
// child
close(pipefd[0]);//子进程关闭读端
// IPC code
Writer(pipefd[1]);
close(pipefd[1]);
exit(0);
}
// father
close(pipefd[1]);//父进程关闭写端
// IPC code
Reader(pipefd[0]);
pid_t rid = waitpid(id, nullptr, 0);
if(rid < 0) return 3;
close(pipefd[0]);
return 0;
}
运行结果:
我们可以看到管道被写满了或者读端速度小于写端速度,导致我们这里的写端阻塞了。
这也说明管道是有固定大小的!!
3.读端正常读,写端关闭,读端就会读到0,表明读到了文件(pipe)结尾,不会被阻塞。
这里我们让子进程写,父进程每隔1秒读一次,5秒之后关闭写端文件描述符观察现象。
#include
#include
#include
#include
#include //stdlib.h
#include
#include
#include
#define N 2
#define NUM 1024
using namespace std;
// child
void Writer(int wfd)
{
string s = "hello, I am child";
pid_t self = getpid();
int number = 0;
char buffer[NUM];
while (true)
{
char c = 'c';
write(wfd, &c, 1); // strlen(buffer) + 1???
number++;
cout << number << endl;
if(number >= 5) break;
}
}
// father
void Reader(int rfd)
{
char buffer[NUM];
while(true)
{
sleep(1);
buffer[0] = 0;
// system call
ssize_t n = read(rfd, buffer, sizeof(buffer)); //sizeof != strlen,sizeof(buffer)表示缓冲区大小
if(n > 0)
{
buffer[n] = 0; // 0 == '\0'
cout << "father get a message[" << getpid() << "]# " << buffer << endl;
}
cout << "n: " << n << endl;
}
}
int main()
{
int pipefd[N] = {0};
int n = pipe(pipefd);
if (n < 0)
return 1;
// child -> w, father->r
pid_t id = fork();
if (id < 0)
return 2;
if (id == 0)
{
// child
close(pipefd[0]);//子进程关闭读端
// IPC code
Writer(pipefd[1]);
close(pipefd[1]);
exit(0);
}
// father
close(pipefd[1]);//父进程关闭写端
// IPC code
Reader(pipefd[0]);
pid_t rid = waitpid(id, nullptr, 0);
if(rid < 0) return 3;
close(pipefd[0]);
return 0;
}
运行结果:
我们可以看到当5秒过后,关闭写端文件描述符,read的返回值为0,读端读到的数据个数为0。
我们通过man查看read的返回值,可以看到返回0说明读到了文件的结尾。
于是我们就可以修改reader函数,当read返回为0时,直接break,不会造成阻塞:
void Reader(int rfd)
{
char buffer[NUM];
while(true)
{
sleep(1);
buffer[0] = 0;
// system call
ssize_t n = read(rfd, buffer, sizeof(buffer)); //sizeof != strlen,sizeof(buffer)表示缓冲区大小
if(n > 0)
{
buffer[n] = 0; // 0 == '\0'
cout << "father get a message[" << getpid() << "]# " << buffer << endl;
}
else if(n == 0)
{
printf("father read file done!\n");
break;
}
else break;
// cout << "n: " << n << endl;
}
}
再进行运行:
如果写端不写了并且关闭文件描述符,读端在读取完管道数据后会读到文件结尾。并且不会阻塞。
4.写端是正常写入,读端关闭了。
这里我们让子进程每隔1秒写一次,父进程读,5秒之后关闭读端文件描述符观察现象。
#include
#include
#include
#include
#include //stdlib.h
#include
#include
#include
#define N 2
#define NUM 1024
using namespace std;
// child
void Writer(int wfd)
{
string s = "hello, I am child";
pid_t self = getpid();
int number = 0;
char buffer[NUM];
while (true)
{
sleep(1);
// 构建发送字符串
buffer[0] = 0; // 字符串清空, 只是为了提醒阅读代码的人,我把这个数组当做字符串了
snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);
// 发送/写入给父进程, system call
write(wfd, buffer, strlen(buffer)); // strlen(buffer) + 1???
}
}
// father
void Reader(int rfd)
{
char buffer[NUM];
int cnt = 0;
while(true)
{
buffer[0] = 0;
// system call
ssize_t n = read(rfd, buffer, sizeof(buffer)); //sizeof != strlen,sizeof(buffer)表示缓冲区大小
if(n > 0)
{
buffer[n] = 0; // 0 == '\0'
cout << "father get a message[" << getpid() << "]# " << buffer << endl;
}
else if(n == 0)
{
printf("father read file done!\n");
break;
}
else break;
cnt++;
if(cnt>5) break;// 读取5s后关闭读端
// cout << "n: " << n << endl;
}
}
int main()
{
int pipefd[N] = {0};
int n = pipe(pipefd);
if (n < 0)
return 1;
// child -> w, father->r
pid_t id = fork();
if (id < 0)
return 2;
if (id == 0)
{
// child
close(pipefd[0]);//子进程关闭读端
// IPC code
Writer(pipefd[1]);
close(pipefd[1]);
exit(0);
}
// father
close(pipefd[1]);//父进程关闭写端
// IPC code
Reader(pipefd[0]);// 读取5s
close(pipefd[0]);//父进程关闭写端
cout << "father close read fd: " << pipefd[0] << endl;
sleep(5); //为了观察僵尸
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid < 0) return 3;
cout << "wait child success: " << rid << " exit code: " << ((status>>8)&0xFF) << " exit signal: " << (status&0x7F) << endl;
sleep(5);
cout << "father quit" << endl;
return 0;
}
运行结果:
我们通过监视窗口来看子进程的状态:
while :;do ps ajx | head -1 && ps ajx | grep testPipe | grep -v grep; sleep 1; echo "########################"; done
我们可以看到读端关闭以后,写端变成了僵尸进程,说明写端是被操作系统发信号给终止了。
并且通过status的次低八位我们看到写端是被操作系统13信号杀掉的。
为什么读端关闭,写端进程可能会被操作系统杀掉呢?
这是因为操作系统不会做浪费系统资源的事情,如果做了,这就是操作系统的bug。读端关闭了,证明就没人再去这个管道里面读数据了,既然没人读数据了,那我即使再去写那也没有意义了,所以操作系统会通过给写端发送信号从而将它终止掉。
管道的特征:
1.具有血缘关系的进程进行进程间通信,通常用于父子进程间通信,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
2.管道只能单向通信,数据只能从一个方向往另一个方向流动;需要双方通信时,需要建立起多个管道。
3.父子进程是会进程协同的,同步与互斥的保护管道文件的数据安全。同步:你快我快、你慢我慢,这也就是为什么读端和写端快的一方会被阻塞。互斥:在我读的时候你不能够写,在我写的时候你不能够读。
4.管道是面向字节流的。实际读取的时候,每次读取的字符个数是由该进程具体想读多少,和管道里由多少来决定的。这就好像我们开水龙头接水一样,你想多接点水就把水龙头的开关开到最大,如果不想接了关闭水龙头即可。
5.管道是基于文件的,而文件的生命周期是随进程的!
1、我们之前用过的 " | " 管道符号。
其实这个就是匿名管道。
cat test.txt | head -10 | tail -5
例如我们可以用 " | " 来进行获取文本的前n行和后n行。
Task.hpp
设计任务函数和初始化加载任务的函数,用vector存放自定义task_t类型的函数指针。
#pragma once
#include
#include
using namespace std;
typedef void(*task_t) ();
void task1()
{
cout << "lol 刷新日志" << endl;
}
void task2()
{
cout << "lol 更新野区,刷新出来野怪" << endl;
}
void task3()
{
cout << "lol 检测软件是否更新,如果需要,就提示用户" << endl;
}
void task4()
{
cout << "lol 用户释放技能,更新用的血量和蓝量" << endl;
}
void LoadTask(vector *tasks)
{
tasks->push_back(task1);
tasks->push_back(task2);
tasks->push_back(task3);
tasks->push_back(task4);
}
ProcessPool.cpp
#include "Task.hpp"
#include
#include
#include
#include
#include
#include
using namespace std;
const int processnum = 10; //创建十个子进程
vector tasks;
// 先描述
class channel
{
public:
channel(int cmdfd, int slaverid, const string &processname)
:_cmdfd(cmdfd), _slaverid(slaverid), _processname(processname)
{}
public:
int _cmdfd; // 发送任务的文件描述符
pid_t _slaverid; // 子进程的PID
string _processname; // 子进程的名字 -- 方便我们打印日志
};
void slaver()
{
while(true)
{
int cmdcode = 0;
int n = read(0,&cmdcode,sizeof(int));// 从键盘读取改为向管道(dup2)当中读取任务的号码。读取sizeof(cmdcode)个字节,如果父进程不给子进程发送数据呢??阻塞等待!
if(n == sizeof(int))
{
//执行cmdcode对应的任务列表
cout <<"slaver say@ get a command: "<< getpid() << " :cmdcode: " << cmdcode << endl;
if(cmdcode >= 0 && cmdcode < tasks.size()) tasks[cmdcode]();
}
if(n == 0) break;//读到错误码,直接break
}
}
// 输入:const &
// 输出:*
// 输入输出:&
void InitProcessPool(vector* channels)//vector存放进程池
{
//确保每个子进程只有一个写端
vector oldfds;
//创建进程池
for (int i = 0; i < processnum; i++)
{
int pipefd[2]; // 临时空间
int n = pipe(pipefd);
assert(!n); // 演示就可以
(void)n;
pid_t id = fork();//fork之后给父进程返回子进程的pid
if(id == 0)// child
{
// cout << "child: "<< getpid() << " close history fd: ";
// for(auto fd : oldfds)
// {
// cout << fd <<" ";
// close(fd);
// }
// cout << "\n";
close(pipefd[1]);
dup2(pipefd[0], 0);
close(pipefd[0]);//把0号描述符从键盘读取改为向管道当中读取
slaver();
cout << "process : " << getpid() << " quit" << endl;
exit(0);
}
//father
close(pipefd[0]);
// 添加channel字段
string name = "process-" + to_string(i);
channels->push_back(channel(pipefd[1],id,name));//添加每个进程的信息,包括写端id,进程id,和我们给进程起的名字
oldfds.push_back(pipefd[1]);
sleep(1);
}
}
void Debug(const vector &channels)
{
// test
for(const auto &c :channels)
{
cout << c._cmdfd << " " << c._slaverid << " " << c._processname << endl;
}
}
void Menu()
{
cout << "################################################" << endl;
cout << "# 1. 刷新日志 2. 刷新出来野怪 #" << endl;
cout << "# 3. 检测软件是否更新 4. 更新用的血量和蓝量 #" << endl;
cout << "# 0. 退出 #" << endl;
cout << "################################################" << endl;
}
void ctrlSlaver(const vector &channels)
{
int which = 0;
// int cnt = 5;
while (true)
{
int select = 0;
Menu();
cout << "Please Enter@ " << endl;
cin >> select;
if(select <= 0 || select > 4) break;
// select > 0&& select <=4
// 1. 选择任务
// int cmdcode = rand()%tasks.size();
int cmdcode = select - 1;
// 2. 选择进程
// int processpos = rand()%channels.size();
cout << "father say: " << " cmdcode: " <<
cmdcode << " already sendto " << channels[which]._slaverid << " process name: "
<< channels[which]._processname << endl;
// 3. 发送任务
write(channels[which]._cmdfd, &cmdcode, sizeof(cmdcode));
which++;
which %= channels.size();
sleep(1);
}
}
void QuitProcess(const vector &channels)
{
//vertion2
for(const auto &c : channels)
{
close(c._cmdfd);
waitpid(c._slaverid,nullptr,0);
}
//vertion1
//我们这里把他倒着关,因为每次重新创建一个新进程,子进程继承父进程的文件描述符表,会有对上一个进程的写权利,
//所以如果我们从前往后关,就不能完全结束进程,waitpid的时候就会阻塞。
// int last = channels.size() - 1;
// for (int i = last; i >= 0; i--)
// {
// close(channels[i]._cmdfd);
// waitpid(channels[i]._slaverid,nullptr,0);
// }
// for(const auto &c : channels) close(c._cmdfd);
// // sleep(5);
// for(const auto &c : channels) waitpid(c._slaverid,nullptr,0);
// // sleep(5);
}
int main()
{
LoadTask(&tasks);
// srand(time(nullptr)^getpid()^1023); // 种一个随机数种子
//再组织
vector channels;
// 1. 初始化
InitProcessPool(&channels);
// Debug(channels);
// 2. 开始控制子进程
ctrlSlaver(channels);
// 3. 清理收尾
QuitProcess(channels);
return 0;
}
运行演示:
我们常用mkfifo函数来创建命名管道,我们可以通过man来查看一下mkfifo函数
$ mkfifo filename
#include
#include
int mkfifo(const char *pathname,mode_t mode);
pathname:创建管道时所用文件的路径
mode:创建管道文件的权限
返回值:成功返回0,失败返回-1
注意:
下面我们来用一下mkfifo函数来创建一下命名管道:
#include
#include
#include
#include
#include
#include
#define FIFO_FILE "./myfifo"
#define MODE 0664
int main()
{
//创建管道
int n = mkfifo(FIFO_FILE, MODE);
if(n == -1)
{
perror("mkfifo");
return 1;
}
return 0;
}
如果当前打开操作是为读而打开FIFO时
如果当前打开操作是为写而打开FIFO时
要让服务器(server)和客户端(client)之间进行通信,首先需要确保他们可以共享访问一个资源,这个资源就是所谓的命名管道。在具体操作中,首先由服务器创建一个命名管道,然后客户端打开这个刚刚创建的管道。此后,客户端就可以向这个管道写入数据,而服务器则可以从这个管道读取数据。通过这种方式,我们就利用命名管道实现了服务器和客户端之间的进程间通信。
创建管道代码:
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define FIFO_FILE "./myfifo"
#define MODE 0664
enum
{
FIFO_CREATE_ERR = 1,
FIFO_DELETE_ERR,
FIFO_OPEN_ERR
};
class Init
{
public:
Init()
{
//创建管道
int n = mkfifo(FIFO_FILE, MODE);
if(n == -1)
{
perror("mkfifo");
exit(FIFO_CREATE_ERR);
}
}
~Init()
{
int m = unlink(FIFO_FILE);
if(m == -1)
{
perror("mkfifo");
exit(FIFO_DELETE_ERR);
}
}
};
服务端代码:
#include "comm.hpp"
using namespace std;
//管理管道文件
int main()
{
Init init;
//打开管道
int fd = open(FIFO_FILE,O_RDONLY);// 等待写入方打开之后,自己才会打开文件,向后执行, open 阻塞了!
if(fd < 0)
{
perror("open");
exit(FIFO_OPEN_ERR);
}
cout << "server open file done" << endl;
// 开始通信
while (true)
{
char buffer[1024] = {0};
int x = read(fd,buffer,sizeof(buffer));
if(x > 0)
{
buffer[x] = 0;
cout << "client say:" << buffer <
客户端代码:
#include "comm.hpp"
using namespace std;
int main()
{
int fd = open(FIFO_FILE,O_WRONLY);
if(fd < 0)
{
perror("open");
exit(FIFO_OPEN_ERR);
}
cout << "client open file done" << endl;
string line;
while (true)
{
cout << "Please Enter@ ";
getline(cin,line);
write(fd,line.c_str(),line.size());
}
close(fd);
return 0;
}
下面我们来运行一下代码,实现一下server与client之间的通信吧
1、先运行服务端,创建管道
可以看到我们运行服务端后,在目录下看到命名管道。
注意:此时服务端没有显示打开管道的信息。
2、运行客户端
可以看到,当我们运行客户端后,服务端才显示出打开文件。说明服务端等待写入方打开之后,自己才会打开文件,向后执行, open 阻塞了!
3、客户端向命名管道输入数据,服务端从管道当中读取
我们通过ps命令查看这两个进程的信息,他们的PID和PPID都不相同。这也就证明了,命名管道是可以让两个毫不相关进程之间实现进程间通信的。
下面我们来看一个问题,为什么在客户端和服务端通信的时候,命名管道文件的大小一直为0呢?
前面在学习匿名管道的时候,当写端不写了关闭文件描述符,读端在读取完管道数据后会读到文件结尾。当读端不读了关闭文件描述符,写端进程后续有可能会被操作系统直接杀掉。
那在我们命名管道这里如果客户端退出会发什么情况呢?
可以看到当我们使用Ctrl+c杀掉客户端进程后,服务端进程会读到文件结尾,此时read返回0,我们的服务端的进程也退出了。
我们经过前面的学习知道,进程间通信的本质:先让不同的进程看到同一份资源!
那共享内存是如何让两个进程看到同一份资源的呢?
操作系统在物理内存下先申请一块内存空间,然后将这块内存空间分别与两个进程的虚拟地址空间通过页表建立映射关系产生关联(挂接到进程地址空间)。至此这两个进程就看到了同一份资源,而这份资源就是我们的共享内存。
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
管道通信涉及两次数据拷贝,即写端将数据先拷贝到管道中,然后读端再从管道中拷贝数据到其进程中。但是,与管道不同,共享内存实现进程间通信时只需一次数据拷贝。写端将数据直接写入共享内存,而读端则能即时查看这些数据,无需再次进行拷贝操作。这使得共享内存成为进程间通信中最高效的方式。需要注意的是,这些用于进程间通信的共享资源都是由操作系统提供的,其中管道是由操作系统的文件管理系统部分提供支持,而共享内存则是由操作系统的进程管理系统部分提供支持。
在操作系统中,存在着大量的进程,这些进程之间需要进行通信,因此系统需要提供一种机制来支持这些进程之间的交互,而共享内存就是其中的一种方式。当一个进程需要使用共享内存时,操作系统会为它开辟一块内存,并且为了管理这块内存,操作系统会为它创建相应的数据结构。通过这些数据结构,操作系统可以追踪哪些进程正在使用共享内存,以及它们对共享内存的访问权限等信息。因此,共享内存的管理对于确保系统的安全性和稳定性至关重要。
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值创建一块共享内存,这样我们就可以让两个进程看到同一份内存了,这个key值是在系统层面来标识共享内存唯一性的。
但是我们在共享内存的数据结构中没看到你说的key值啊,这个key值在哪里呢?
我们看到共享内存数据结构的第一个成员是shm_perm,它是一个ipc_perm类型的结构体变量,其中ipc_perm结构体的定义如下,我们发现key值就存在shm_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;
};
我们来总结一下key:
进程通过共享内存实现进程间通信主要分为以下四个步骤:
下面我主要以上面四个步骤来为大家讲解几个接口进程通过共享内存实现进程间通信
首先要看到同一份资源,那么必须有标识操作系统上ipc资源的唯一性,所以创建共享内存之前,要用到ftok函数:
ftok函数的作用就是,将一个已存在的路径名pathname和一个整数标识符proj_id通过特定的算法转换成一个key值,称为IPC键值,在接下来使用shmget函数创建共享内存获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中。需要注意的是,pathname所指定的文件必须存在且可存取。
这里要注意:需要进行通信的多个进程之间,在使用ftok函数获取key值时,都必须采用同样的路径名和和整数标识符,进而生成同一种key值,然后才能看到同一个共享资源。
功能:用来创建共享内存
原型
int shmget(key_t key, size_t size, int shmflg);
参数
key:表示待创建共享内存在系统当中的唯一标识。
size:共享内存大小。
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
size:默认是4096字节的整数倍。即使给的不是4096的整数倍,分配的还是4096的整数倍,但是我们访问只能访问我们给的size大小。(如我们给4097,我们只能访问4097个字节,但是会分配给共享内存4076*2字节)
shmget函数的第三个参数shmflg,常用的组合方式有以下两种:
注:IPC EXCL: 不单独使用!
下面我们一起来创建一下共享内存:
#include
#include
#include
#include
#include
#include
#include
#include
#include "log.hpp"
using namespace std;
// 共享内存的大小一般建议是4096的整数倍
// 4097,实际上操作系统给你的是4096*2的大小
Log log;
const int size = 4096;
const string pathname="/home/GTY";
const int proj_id = 0x6666;
key_t GetKey()
{
key_t k = ftok(pathname.c_str(),proj_id);
if(k<0)
{
log(Fatal,"ftok error:%s",strerror(errno));
exit(1);
}
log(Info, "ftok success, key is : 0x%x", k);
return k;
}
int GetShareMemHelper(int flag)
{
key_t k = GetKey();
int shmid = shmget(k,size,flag);
if(shmid < 0)
{
log(Fatal, "create share memory error: %s", strerror(errno));
exit(2);
}
log(Info, "create share memory success, shmid: %d", shmid);
return shmid;
}
int CreateShm()
{
return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}
int GetShm()
{
return GetShareMemHelper(IPC_CREAT);
}
int main()
{
int shmid = CreateShm();
return 0;
}
运行结果:
释放共享内存的方式有两种:
我们可以使用ipcs指令来查看共享内存资源和查看消息队列以及信号量。
我们先来介绍一下使用指令释放共享内存资源,我们可以通过ipcs -m指令来查看共享内存资源:
我们发现之前的进程已经退出了,但是我们上次创建的共享内存资源它现在还在。所以我们这里还得出一个结论:可以得出共享内存的生命周期是随内核的。
ipcs命令输出的每列信息的含义如下:
我们可以通过ipcrm -m +shmid来释放共享内存:
可以看到我们通过ipcrm -m指令成功释放共享内存。
我们再来看一下使用shmctl函数释放共享内存:
我们先通过man指令来查看一下shmctl函数
shmctl这个函数的用法如下:
功能:用于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
注意:shmctl函数的第二个参数传入的常用的选项有以下三个:
下面我们使用shmct函数来释放一下共享内存资源:
#include
#include
#include
#include
#include
#include
#include
#include
#include "log.hpp"
using namespace std;
// 共享内存的大小一般建议是4096的整数倍
// 4097,实际上操作系统给你的是4096*2的大小
Log log;
const int size = 4096;
const string pathname="/home/GTY";
const int proj_id = 0x6666;
key_t GetKey()
{
key_t k = ftok(pathname.c_str(),proj_id);
if(k<0)
{
log(Fatal,"ftok error:%s",strerror(errno));
exit(1);
}
log(Info, "ftok success, key is : 0x%x", k);
return k;
}
int GetShareMemHelper(int flag)
{
key_t k = GetKey();
int shmid = shmget(k,size,flag);
if(shmid < 0)
{
log(Fatal, "create share memory error: %s", strerror(errno));
exit(2);
}
log(Info, "create share memory success, shmid: %d", shmid);
return shmid;
}
int CreateShm()
{
return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}
int GetShm()
{
return GetShareMemHelper(IPC_CREAT);
}
int main()
{
int shmid = CreateShm();
sleep(3);
shmctl(shmid,IPC_RMID,NULL);
sleep(3);
return 0;
}
我们在程序运行的时候,使用下面监控脚本来实时检测共享内存资源:
while :; do ipcs -m | head -3 && ipcs -m |grep GTY; sleep 1;done
可以看到3秒后,我们创建的共享内存资源被释放了。
函数用法如下:
功能:将共享内存段连接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
shmat函数第二个参数说明:
shmat函数的第三个参数传入的常用的选项:
#include
#include
#include
#include
#include
#include
#include
#include
#include "log.hpp"
using namespace std;
// 共享内存的大小一般建议是4096的整数倍
// 4097,实际上操作系统给你的是4096*2的大小
Log log;
const int size = 4096;
const string pathname="/home/GTY";
const int proj_id = 0x6666;
key_t GetKey()
{
key_t k = ftok(pathname.c_str(),proj_id);
if(k<0)
{
log(Fatal,"ftok error:%s",strerror(errno));
exit(1);
}
log(Info, "ftok success, key is : 0x%x", k);
return k;
}
int GetShareMemHelper(int flag)
{
key_t k = GetKey();
int shmid = shmget(k,size,flag);
if(shmid < 0)
{
log(Fatal, "create share memory error: %s", strerror(errno));
exit(2);
}
log(Info, "create share memory success, shmid: %d", shmid);
return shmid;
}
int CreateShm()
{
return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}
int GetShm()
{
return GetShareMemHelper(IPC_CREAT);
}
int main()
{
int shmid = CreateShm();
sleep(2);
char *shmaddr = (char*)shmat(shmid, nullptr, 0);
sleep(2);
return 0;
}
在我们运行代码时,使用下面监控脚本来实时检测管理共享内存的进程数:
while :; do ipcs -m | head -3 && ipcs -m |grep GTY; sleep 1;done
运行结果:
可以看到共享内存的进程关联数由0变成1,关联共享内存成功。
shmdt函数可以来让取消进程与关联共享之间的关联,我们可以通过man来看一下shmdt函数:
shmdt函数用法如下:
功能:将共享内存段与当前进程脱离
原型
int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
我们先让当前进程关联内存资源,2秒后我们再取消当前进程与共享内存的关联。
#include
#include
#include
#include
#include
#include
#include
#include
#include "log.hpp"
using namespace std;
// 共享内存的大小一般建议是4096的整数倍
// 4097,实际上操作系统给你的是4096*2的大小
Log log;
const int size = 4096;
const string pathname="/home/GTY";
const int proj_id = 0x6666;
key_t GetKey()
{
key_t k = ftok(pathname.c_str(),proj_id);
if(k<0)
{
log(Fatal,"ftok error:%s",strerror(errno));
exit(1);
}
log(Info, "ftok success, key is : 0x%x", k);
return k;
}
int GetShareMemHelper(int flag)
{
key_t k = GetKey();
int shmid = shmget(k,size,flag);
if(shmid < 0)
{
log(Fatal, "create share memory error: %s", strerror(errno));
exit(2);
}
log(Info, "create share memory success, shmid: %d", shmid);
return shmid;
}
int CreateShm()
{
return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}
int GetShm()
{
return GetShareMemHelper(IPC_CREAT);
}
int main()
{
int shmid = CreateShm();
sleep(2);
char *shmaddr = (char*)shmat(shmid, nullptr, 0);
sleep(2);
shmdt(shmaddr);
sleep(2);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
下面我们来看一下运行结果:
可以看到刚开始关联共享内存的进程数为0,过了2秒后关联共享内存的进程数为1,又过了2秒关联共享内存的进程数又变为0,共享内存的去关联成功。
服务端负责创建共享内存,创建好后将共享内存和服务端进行关联,之后服务端进入死循环等待客户端发送消息。最后通过客户端释放共享内存资源。
为了让服务端和客户端在使用ftok函数获取key值时,能够得到同一个key值,那么服务端和客户端传入ftok函数的路径名和和整数标识符必须相同,这样才能生成同一种key值,进而找到同一个共享资源进行关联。这里我们可以将这些需要共用的信息放入一个头文件当中,服务端和客户端共用这个头文件即可。
comm.h文件的代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include "log.hpp"
using namespace std;
// 共享内存的大小一般建议是4096的整数倍
// 4097,实际上操作系统给你的是4096*2的大小
Log log;
const int size = 4096;
const string pathname="/home/GTY";
const int proj_id = 0x6666;
key_t GetKey()
{
key_t k = ftok(pathname.c_str(),proj_id);
if(k<0)
{
log(Fatal,"ftok error:%s",strerror(errno));
exit(1);
}
log(Info, "ftok success, key is : 0x%x", k);
return k;
}
int GetShareMemHelper(int flag)
{
key_t k = GetKey();
int shmid = shmget(k,size,flag);
if(shmid < 0)
{
log(Fatal, "create share memory error: %s", strerror(errno));
exit(2);
}
log(Info, "create share memory success, shmid: %d", shmid);
return shmid;
}
int CreateShm()
{
return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}
int GetShm()
{
return GetShareMemHelper(IPC_CREAT);
}
服务端代码如下:
#include "comm.hpp"
extern Log log;
int main()
{
int shmid = CreateShm();
char *shmaddr = (char*)shmat(shmid, nullptr, 0);
while(true)
{
cout << "client say@ " << shmaddr << endl;//直接访问共享内存
sleep(1);
}
shmdt(shmaddr);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
客户端代码:
#include "comm.hpp"
int main()
{
int shmid = GetShm();
char* shmaddr = (char*)shmat(shmid,nullptr,0);
// 一旦有了共享内存,挂接到自己的地址空间中,你直接把他当成你的内存空间来用即可!
// 不需要调用系统调用
// ipc code
while(true)
{
cout << "Please Enter@ ";
fgets(shmaddr, 4096, stdin);
}
shmdt(shmaddr);
return 0;
}
运行结果:
我们看到的客户端与服务端关联到了同一块共享内存上,并完成了进程间通信。
我们可以看到服务端每隔一秒往共享内存里面读取数据,当客户端停止输入时,服务端读取的都是客户端之前写入的数据,它并没有阻塞。
通过这个现象我们可以得出一个结论:共享内存没有同步互斥之类的保护机制!!
小结共享内存的几个特性:
1.共享内存没有同步互斥之类的保护机制
2.共享内存是所有的进程间通信中,速度最快的!
3.共享内存内部的数据,由用户自己维护!
4.共享内存生命周期随内核,只要不删除,就一直存在于内核中,除非重启系统(当然这里指的是非手动操作,可以手动删除)
5.共享内存的删除操作并非直接删除,而是拒绝后续映射,只有在当前映射链接数为0时,表示没有进程访问了,才会真正被删除。
信号量主要用于同步和互斥的,下面先来看看什么是同步和互斥。
进程互斥