这里写目录标题
- [<font color="FF00FF">1. 进程间通信](#1. 进程间通信)
-
- [<font color="FF00FF">1.1 进程间通信目的](#1.1 进程间通信目的)
- [<font color="FF00FF">1.2 怎么通信](#1.2 怎么通信)
- [<font color="FF00FF">1.3 具体通信方式](#1.3 具体通信方式)
- [<font color="FF00FF">2. 匿名管道](#2. 匿名管道)
- [<font color="FF00FF">3. 进程池](#3. 进程池)
- [<font color="FF00FF">4. 命名管道](#4. 命名管道)
-
- [<font color="FF00FF">4.1 创建命名管道](#4.1 创建命名管道)
- [<font color="FF00FF">5. 共享内存](#5. 共享内存)
-
- [<font color="FF00FF">5.1 创建key值](#5.1 创建key值)
- [<font color="FF00FF">5.2 创建共享内存](#5.2 创建共享内存)
-
- [<font color="FF00FF">1. shmflg是标记位](#1. shmflg是标记位)
- [<font color="FF00FF">2. key](#2. key)
- [<font color="FF00FF">5.3 挂载共享内存到虚拟地址空间](#5.3 挂载共享内存到虚拟地址空间)
- [<font color="FF00FF">5.4 取消共享内存和虚拟地址空间的挂载](#5.4 取消共享内存和虚拟地址空间的挂载)
- [<font color="FF00FF">5.5 删除共享内存](#5.5 删除共享内存)
- [<font color="FF00FF">一些问题](#一些问题)
1. 进程间通信
1.1 进程间通信目的
- 数据传输:⼀个进程需要将它的数据发送给另⼀个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:⼀个进程需要向另⼀个或⼀组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另⼀个进程的执行(如Debug进程),此时控制进程希望能够拦截另⼀个进程的所有陷⼊和异常,并能够及时知道它的状态改变
1.2 怎么通信
进程间通信的本质是让不同的进程先看到同一份资源(内存),然后才能通信,这个资源必须由OS提供,所以就要有系统调用
1.3 具体通信方式
- 基于文件的管道通信
- System V本机通信
2. 匿名管道
通常用来做父子通信,基于文件

这个管道是内存级的,不会把缓冲区的内容刷到磁盘上,是OS单独设计的,配上单独的系统调用
int pipe( int pipe[2]);
输出型参数
-
为什么叫匿名管道?
不要文件路径,内存级的,没有文件名 -
我们怎么保证两个进程打开同一个管道文件呢?
子进程继承父进程的文件描述符表

c
#include<unistd.h>
#include<iostream>
using namespace std;
int main()
{
int fds[2]={0};
int n = pipe(fds);
if(n<0)
{
cerr<<"pipe error"<<endl;
return -1;
}
cout<<"fds[0]:"<<fds[0]<<endl;
cout<<"fds[1]:"<<fds[1]<<endl;
return 0;
}
结果是3,4证明管道是文件
c
#include <unistd.h>
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <cstdio>
using namespace std;
void ChildWritefds(int wfd)
{
char buff[1024];
int cnt = 0;
while (true)
{
snprintf(buff, sizeof(buff), "I am chile, pid: %d, cnt:%d", getpid(), cnt++);
write(wfd, buff, strlen(buff));
sleep(1);
}
}
void FatherReadfds(int rfd)
{
char buff[1024];
while (true)
{
buff[0] = 0;
ssize_t n = read(rfd, buff, sizeof(buff) - 1);
if (n > 0)
{
buff[n] = 0;
cout << "child say:" << buff << endl;
}
}
}
int main()
{
// 1. 创建管道
int fds[2] = {0}; // fds[0]:r fds{1}:w
int n = pipe(fds);
if (n < 0)
{
cerr << "pipe error" << endl;
return -1;
}
cout << "fds[0]:" << fds[0] << endl;
cout << "fds[1]:" << fds[1] << endl;
// 2. 创建子进程
pid_t id = fork();
if (id == 0)
{
// f:读 c:写
close(fds[0]);
ChildWritefds(fds[1]);
close(fds[1]);
exit(0);
}
close(fds[1]);
FatherReadfds(fds[0]);
waitpid(id, nullptr, 0);
close(fds[0]);
return 0;
}
打印结果可以看到cnt一直在变化,证明父子进程完成了通信
5种特性
- 匿名管道只能用来进行具有血缘关系的进程进行进程间通信(常用父子)
- 管道文件自带同步机制
- 管道是面向字节流的
- 管道是单向通信的 (属于半双工,要不父进程发,子进程读,要不就反过来)
- 管道文件的生命周期是随进程的
任何一个时刻,一个发,一个收 ---- 半双工
任何一个时刻,可以同时收发 ---- 全双工
四种通信情况
-
写慢,都快:读端阻塞(进程阻塞),等写
-
写块,读满:写满管道文件时,写段阻塞,等待读端读取到buffer里,才能继续写
-
写关,读继续: read返回值为0,表示读到文件末尾
-
读关闭,写继续:写端在写入,没有任何意义,OS不会做没意义的事,OS杀掉写端进程,发送异常信号,如何验证呢?父进程回收子进程时,拿到异常信号

-
在ubuntu环境下,管道的容量是64KB
-
如果单次向管道文件写入的字节<4096,就必须把字符串写完,父进程才能读,但是如果子进程写了3个字符串,在写第四个,那么父进程可以读三个半字符串,要读第四个字符串,只能等子进程写完,这是管道写入的原子性
3. 进程池

- ProcessPool.hpp
c
#ifndef _PROCESS_POOL_HPP_
#define _PROCESS_POOL_HPP_
#include <iostream>
using namespace std;
#include <vector>
#include <unistd.h>
#include <sys/wait.h>
#include "Task.hpp"
class channel // 信道
{
public:
channel(int wfd, pid_t id)
: _wfd(wfd), _subid(id)
{
_name = "channel-" + to_string(_wfd) + "-" + to_string(_subid);
}
~channel()
{
}
void Send(int code)
{
int n = write(_wfd, &code, sizeof(code));
(void)n;
}
int Fd()
{
return _wfd;
}
pid_t Subid()
{
return _subid;
}
string Name()
{
return _name;
}
void Close()
{
close(_wfd);
}
void wait()
{
pid_t rid = waitpid(_subid, nullptr, 0);
(void)rid;
}
private:
int _wfd;
pid_t _subid;
string _name;
};
class channelManager // 管理信道
{
public:
channelManager()
: _next(0)
{
}
~channelManager()
{
}
void Build(int wfd, pid_t subid)
{
_channels.emplace_back(wfd, subid);
}
void PrintChannel()
{
for (auto &channel : _channels)
{
cout << channel.Name() << endl;
}
}
channel &Select()
{
auto &c = _channels[_next++];
_next %= _channels.size();
return c;
}
void stopsubprocess()
{
for (auto &channel : _channels)
{
channel.Close();
cout << "关闭:" << channel.Name() << endl;
}
}
void waitsubprocess()
{
for (auto &channel : _channels)
{
channel.wait();
cout << "关闭:" << channel.Name() << endl;
}
}
private:
vector<channel> _channels;
int _next;
};
const int gdefaultnum = 5;
class ProcessPool // 进程池
{
public:
ProcessPool(int num)
: _process_num(num)
{
_tm.Register(printlog);
_tm.Register(download);
_tm.Register(upload);
}
void Debug()
{
_cm.PrintChannel();
}
~ProcessPool()
{
}
void Work(int rfd)
{
while (true)
{
int code = 0;
ssize_t n = read(rfd, &code, sizeof(code));
if (n > 0)
{
if (n != sizeof(code))
{
continue;
}
cout << "子进程[" << getpid() << "]收到一个任务码:" << code << endl;
_tm.execute(code);
}
else if (n == 0)
{
cout << "子进程退出" << endl;
break;
}
else
{
cout << "子进程读取失败" << endl;
break;
}
}
}
bool Create()
{
for (int i = 0; i < _process_num; i++)
{
int pipefd[2] = {0};
int n = pipe(pipefd);
if (n < 0)
{
return false;
}
pid_t subid = fork();
if (subid < 0)
{
return false;
}
else if (subid == 0)
{
close(pipefd[1]); // f:w c:r
Work(pipefd[0]);
close(pipefd[0]);
exit(0);
}
else
{
close(pipefd[0]);
_cm.Build(pipefd[1], subid);
}
}
return true;
}
void Run()
{
int taskcode = _tm.code();
auto &c = _cm.Select();
cout << "选择一个子进程:" << c.Name() << endl;
c.Send(taskcode);
cout << "发送了一个任务码:" << taskcode << endl;
}
void stop()
{
_cm.stopsubprocess();
_cm.waitsubprocess();
}
private:
channelManager _cm;
int _process_num;
TaskManager _tm;
};
#endif
c
#include"ProcessPool.hpp"
int main()
{
ProcessPool pp(gdefaultnum);
pp.Create();
int cnt=10;
while(cnt--)
{
pp.Run();
sleep(1);
}
pp.stop();
sleep(1000);
}
- Makefile
c
ProcessPool:Main.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f ProcessPool
- Task.hpp
c
#pragma once
#include <iostream>
using namespace std;
#include <vector>
#include <ctime>
typedef void (*task_t)();
void printlog()
{
cout << "我是一个打印日志的任务" << endl;
}
void download()
{
cout << "我是一个下载的任务" << endl;
}
void upload()
{
cout << "我是一个上传的任务" << endl;
}
class TaskManager
{
public:
TaskManager()
{
srand(time(nullptr));
}
void Register(task_t t)
{
_tasks.push_back(t);
}
int code()
{
return rand() % _tasks.size();
}
void execute(int code)
{
if (code >= 0 && code <= _tasks.size())
{
_tasks[code]();
}
}
~TaskManager()
{
}
private:
std::vector<task_t> _tasks;
};
4. 命名管道

这个管道文件也是内存级的,struct file会新建,因为可能有读写位置不同等原因,但是这个文件不会加载两次,struct file直指在内存里已有的文件
4.1 创建命名管道
- 命令行:mkfifo filename
- int mkfifo(const char *filename,mode_t mode);
- 命名管道和匿名管道是一致的,只有一点不同,命名管道可以用来进行不相关的进程进行进程间通信
- 读端创建管道成功后,阻塞等待写端打开管道文件后,读端才能成功打开管道文件
5. 共享内存

5.1 创建key值

5.2 创建共享内存

shmget是创建共享内存的系统调用,size表示创建共享内存的大小
1. shmflg是标记位
1.1 IPC_CREAT:如果共享内存不存在就创建,如果存在就打开已存在的共享内存,并返回 (一般用来获取)
1.2 IPC_CREAT | IPC_EXCL:如果创建的shm不存在,就创建它,如果已经存在,就会出错返回,只要shmget成功返回一定是一个全新的共享内存 !(一般用来创建)
2. key
不同进程在使用共享内存通信时,用key来标识共享内存唯一性
key不是内核形成的,而是用户层构建并传入OS的
一个进程创建共享内存时,把key写入共享内存的数据结构里,另一个进程拿到这个key,直接访问这个共享内存,这个key是约定好的,两个进程都可以看到,就像命名管道那样,用路径表示唯一性,而key可以由ftok构建出来,并且具有唯一性
我们怎么评估共享内存是否存在?
可以通过key来确定我们创建的共享内存是否已经存在
你这么保证两个进程拿到的是同一个共享内存呢?
可以通过key保证两个进程拿到的是同一个共享内存
进程结束了,如果没有进行删除共享内存,共享内存资源会一直存在,共享内存资源生命周期随内核,如果没有显示删除,即便进程退出了,IPC资源依旧被占用
ipcs -m :查看共享内存
ipcrm -m shmid:删除共享内存
这里为什么不用key删呢?
因为key未来只给内核来进行区分唯一性,需要用shmid来进行管理共享内存,而指令本质也是运行在用户空间的
5.3 挂载共享内存到虚拟地址空间

将共享内存挂接到进程的虚拟地址空间中,shmat:at:attach,关联
shmaddr:虚拟地址,固定地址进行挂接,但是一般设为nullptr,让OS自主分配
shmflg:权限,默认设置0666
返回值:这段虚拟地址空间的起始地址
跟malloc用法类似
5.4 取消共享内存和虚拟地址空间的挂载

shmaddr:填入shmat的返回值
5.5 删除共享内存

cmd:IPC_RMID,删除共享内存
struct shmid_ds ds;
cmd:IPC_STAT,获取共享内存的属性,buf输出型参数,传入&ds,输出型参数,可以拿到key

共享内存的内核数据结构
一些问题
- 读写共享内存时,并没有出现系统调用?
因为那段虚拟地址空间会被映射到共享区,共享区属于用户空间,可以让用户直接使用
共享内存是进程通信中,最快的方式?
- 映射之后读写直接被双方看到
- 不需要系统调用获取或写入内容,命名管道是因为管道文件只能提供系统调用来从内核缓冲区获取内容
- 通信双方没有同步机制
共享内存没有保护机制,这里指对数据的保护
在内核中,共享内存在创建的时候,它的大小必须是4KB的整数倍,如果设置大小4097,那么就开辟8KB,但是你查出的大小依旧是4097,相当于4095字节被浪费