进程间通信
目录
[5.3.命名管道 VS 匿名管道](#5.3.命名管道 VS 匿名管道)
[六、System V共享内存](#六、System V共享内存)
[七、System V消息队列](#七、System V消息队列)
[八、System V信号量](#八、System V信号量)
一、进程间通信理解
1.1.进程间通信的概念
**进程间通信(Inter Process Communication):**多个进程间进行数据交换和通信的技术
**进程间通信的本质:**让不同的进程先看到同一份资源(内存,不能由进程申请而是由OS提供)
开发者会设计一套统一的通信接口
1.2.进程间通信的目的
多个进程之间需要数据传输
多个进程之间需要共享资源
多个进程之间需要通知事件
多个进程之间需要进程控制
1.3.进程间通信的方式
- 基于文件的管道通信
- System V进程间通信
- POSIX进程间通信
二、管道
基于已有的技术通过文件直接进行通信
将文件缓冲区的数据刷新给另一个进程
**管道:**从一个进程连接到另一个进程的一个数据流(Unix早期进程间通信的形式)


三、匿名管道
3.1.匿名管道的概念
**匿名管道:**内存级文件,不需要文件路径,也没有文件名,常用作父子通信
子进程通过继承父进程的文件描述符表,来确认打开的是同一个管道文件
3.2.pipe函数
功能:创建匿名管道
cpp
int pipe(int fd[2]);
参数:
fd:⽂件描述符数组
fd[0]:表示读端
fd[1]:表示写端
返回值:成功返回0,失败返回错误代码

3.3.匿名管道的理解


3.4.实例代码
从键盘输入数据,写入管道,读取管道,输出到屏幕
实验代码
cpp
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdio>
#include <string.h>
//子进程写
void ChildWrite(int wfd)
{
char buffer[1024];
int cnt = 0;
while(true)
{
//将字符串内容写入数组
snprintf(buffer,sizeof(buffer),"I am child,pid: %d,cnt: %d",getpid(),cnt++);
//向文件写入时不用写\0
write(wfd,buffer,strlen(buffer));
sleep(1);
}
}
//父进程读
void FatherRead(int rfd)
{
char buffer[1024];
while(true)
{
buffer[0] = 0;
//将文件内容读取到数组
ssize_t n = read(rfd,buffer,sizeof(buffer) - 1);
if(n > 0)
{
//读取文件内容时要加\0
buffer[n] = 0;
std::cout << "child say: " << buffer << std::endl;
}
}
}
int main()
{
//1. 创建管道
int fds[2] = {0};//fds[0]:读 fds[1]:写
int n = pipe(fds);
if(n < 0)
{
std::cerr << "pipe error" << std::endl;
return 1;
}
std::cout << "fds[0]: " << fds[0] << std::endl;
std::cout << "fds[1]: " << fds[1] << std::endl;
//2. 创建子进程
pid_t id = fork();
if(id == 0)
{
//关闭子进程读
close(fds[0]);
//子进程写入
ChildWrite(fds[1]);
//关闭子进程写
close(fds[1]);
exit(0);
}
//3. 形成通信管道(父进程读,子进程写)
//关闭父进程写
close(fds[1]);
//父进程读取
FatherRead(fds[0]);
//回收子进程
waitpid(id,nullptr,0);
//关闭父进程读
close(fds[0]);
return 0;
}
实验现象


3.5.匿名管道的特性
特性一:只能是具有血缘关系的进程间通信
特性二:管道文件进行IO操作具有同步机制
特性三:管道是面向字节流的,怎么读与怎么写没有必然关系
- 子进程写的快,父进程读的慢:

- 子进程写的慢,父进程读的快:

特性四:管道是单向通信的,子进程写数据,父进程读数据,属于半双工
- 半双工:任何一个时刻,一方发送,一方接收
- 全双工:任何一个时刻,双方可以同时收发
特性五:(管道)文件的生命周期由进程决定
3.6.四种通信情况
- 写慢读快
读端进程阻塞等待写端进程写入
- 写快读慢
写满后,写端进程阻塞等待读端
- 写关读开
read的返回值0表示文件结束

- 写开读关
写端写入无意义,浪费内存空间,OS发送异常信号杀掉写端进程

实验代码
cpp
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdio>
#include <string.h>
//子进程写
void ChildWrite(int wfd)
{
char buffer[1024];
int cnt = 0;
while(true)
{
//将字符串内容写入数组
snprintf(buffer,sizeof(buffer),"I am child,pid: %d,cnt: %d",getpid(),cnt++);
//向文件写入时不用写\0
write(wfd,buffer,strlen(buffer));
printf("child: %d\n",cnt);
sleep(2);
}
}
//父进程读
void FatherRead(int rfd)
{
char buffer[1024];
while(true)
{
buffer[0] = 0;
ssize_t n = read(rfd,buffer,sizeof(buffer) - 1);
if(n > 0)
{
//读取文件内容时要加\0
buffer[n] = 0;
std::cout << "child say: " << buffer << std::endl;
}
else if(n == 0)
{
std::cout << "n : " << n << std::endl;
std::cout << "child 退出 father也退出" << std::endl;
break;
}
else
{
break;
}
break;
}
}
int main()
{
//1. 创建管道
int fds[2] = {0};//fds[0]:读 fds[1]:写
int n = pipe(fds);
if(n < 0)
{
std::cerr << "pipe error" << std::endl;
return 1;
}
std::cout << "fds[0]: " << fds[0] << std::endl;
std::cout << "fds[1]: " << fds[1] << std::endl;
//2. 创建子进程
pid_t id = fork();
if(id == 0)
{
//关闭子进程读
close(fds[0]);
//子进程写入
ChildWrite(fds[1]);
//关闭子进程写
close(fds[1]);
exit(0);
}
//3. 形成通信管道(父进程读,子进程写)
//关闭父进程写
close(fds[1]);
//父进程读取
FatherRead(fds[0]);
//关闭父进程读
close(fds[0]);
sleep(5);
//回收子进程
int status = 0;
int ret = waitpid(id,&status,0);
if(ret > 0)
{
printf("exit code : %d,exit signal : %d\n",(status >> 8) & 0xff,status & 0x7f);
sleep(5);
}
return 0;
}
实验现象


四、进程池
4.1.管道的容量
64KB(65536 / 1024)


4.2.管道写入的原子性
单次写入数据长度≤PIPE_BUF(4KB),OS保证原子写入
单次写入数据长度>PIPE_BUF(4KB),为非原子写入
原子写入:多进程同时向同一个管道写数据时,单次写入的数据不会出现穿插和拆分
示例:进程A写"aaa",进程B写"bbb",读端只会读到"aaabbb"或"bbbaaa",不会"aababba"

4.3.基于管道的进程池
**进程池:**父进程通过管道暂停和唤醒子进程,发送任务码让子进程执行相应任务

- ProcessPool.hpp
cpp
//.hpp文件:头文件与源文件没有分离
//.cxx文件:与.cc和.cpp一样,都是C++源文件的后缀
//法一:#pragma once
//法二:条件编译
#ifndef __PROCESS_POOL_HPP__
#define __PROCESS_POOL_HPP__
//头文件
#include <iostream>
#include <vector>
#include <unistd.h>
#include <cstdlib>
#include <sys/wait.h>
#include "Task.hpp"
//管道类(先描述)
class Channel
{
public:
//构造
Channel(int fd, pid_t id)
:_wfd(fd)
,_subid(id)
{
_name = "channel-" + std::to_string(_wfd) + "-" + std::to_string(_subid);//整型转字符串
}
//发送任务码
void Send(int code)
{
int n = write(_wfd, &code, sizeof(code));
(void)n;//避免编译报警(对于未使用变量的检查)
}
//关闭写文件描述符
void Close()
{
close(_wfd);
}
//回收子进程
void Wait()
{
pid_t rid = waitpid(_subid,nullptr,0);
(void)rid;
}
//析构
~Channel(){}
//Get方法
int Fd()
{
return _wfd;
}
pid_t SubId()
{
return _subid;
}
std::string Name()
{
return _name;
}
private:
int _wfd;//写文件描述符
int _subid;//子进程pid
std::string _name;//管道名字
};
//管道管理类(再组织)
class ChannelManager
{
public:
//构造
ChannelManager()
:_next(0)
{}
//构建管道
void Insert(int wfd, pid_t subid)
{
//Channel c(wfd,subid);
//_channels.push_back(std::move(c));
_channels.emplace_back(wfd,subid);
}
//选择管道
Channel &Select()
{
auto &c = _channels[_next];
_next++;
_next %= _channels.size();
return c;
}
//打印管道
void PrintChannel()
{
for(auto &channel : _channels)
{
std::cout << channel.Name() << std::endl;
}
}
//结束子进程
void StopSubProcess()
{
for(auto &channel : _channels)
{
channel.Close();
std::cout << "关闭: " << channel.Name() << std::endl;
}
}
//回收子进程
void WaitSubProcess()
{
for(auto &channel : _channels)
{
channel.Wait();
std::cout << "回收: " << channel.Name() << std::endl;
}
}
//析构
~ChannelManager(){}
private:
std::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 Work(int rfd)
{
while(true)
{
int code = 0;
ssize_t n = read(rfd, &code, sizeof(code));
if(n > 0)
{
//如果读取任务码错误
if(n != sizeof(code))
{
//继续读取
continue;
}
std::cout << "子进程[" << getpid() << "]收到一个任务码: " << code << std::endl;
//子进程执行任务
_tm.Execute(code);
}
else if(n == 0)
{
std::cout << "子进程退出" << std::endl;
break;
}
else
{
std::cout << "读取错误" << std::endl;
break;
}
}
}
//启动进程池
bool Start()
{
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]);
//子进程工作
Work(pipefd[0]);
//关闭子进程读
close(pipefd[0]);
//子进程退出
exit(0);
}
else
{
//关闭父进程读
close(pipefd[0]);
//构建管道
_cm.Insert(pipefd[1],subid);
}
}
return true;
}
//调试
void Debug()
{
_cm.PrintChannel();
}
//运行进程池
void Run()
{
//1. 选择一个任务
int task_code = _tm.Code();
//2. 选择一个管道/子进程(负载均衡地选择子进程完成任务)
//方法一:轮询(✔)
//方法二:随机
//方法三;channel添加负载指标
auto &c = _cm.Select();
std::cout << "选择了一个子进程: " << c.Name() << std::endl;
//3. 发送任务
c.Send(task_code);
std::cout << "发送了一个任务码: " << task_code << std::endl;
}
//结束进程池
void Stop()
{
//关闭父进程所有的写文件描述符
_cm.StopSubProcess();
//回收所有的子进程
_cm.WaitSubProcess();
}
//析构
~ProcessPool(){}
private:
ChannelManager _cm;//管道管理变量
TaskManager _tm;//任务管理变量
int _process_num;//进程个数
};
#endif
- Task.hpp
cpp
#pragma once
#include <iostream>
#include <vector>
#include <time.h>
typedef void (*task_t)();
/////////////////////debug//////////////////////////
void Printlog()
{
std::cout << "我是一个打印日志的任务" << std::endl;
}
void Download()
{
std::cout << "我是一个下载的任务" << std::endl;
}
void Upload()
{
std::cout << "我是一个上传的任务" << std::endl;
}
/////////////////////debug//////////////////////////
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;
};
cpp
#include "ProcessPool.hpp"
int main()
{
//1. 创建进程池类型对象
ProcessPool pp(gdefaultnum);
//2. 启动进程池
pp.Start();
//3. 自动派发任务
int cnt = 10;
while(cnt--)
{
//4. 运行进程池
pp.Run();
sleep(1);
}
//5. 回收进程池
pp.Stop();
sleep(1000);
return 0;
}
- Makefile
cpp
process_pool:Main.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f process_pool
实验现象

4.4.隐藏BUG

**方案一:**倒着关闭写端
最后一个进程只有父进程的一个写端,关闭后可以关闭该进程指向其他进程管道的写端
cpp
class ChannelManager
{
public:
//......
void CloseAndWait()
{
for(int i = _channels.size() - 1; i >= 0; i--)
{
_channels[i].Close();
std::cout << "关闭: " << _channels[i].Name() << std::endl;
channels[i].Wait();
std::cout << "回收: " << _channels[i].Name() << std::endl;
}
}
//......
}
**方案二:**让父进程单独指向每个管道的写端
通过管道管理数组,将子进程继承下来的堂兄进程写段全都关闭
cpp
class ChannelManager
{
public:
//......
void CloseAll()
{
for(auto &channel : _channels)
{
channel.Close();
}
}
void CloseAndWait()
{
for(auto &channel : channels)
{
_channels[i].Close();
std::cout << "关闭: " << _channels[i].Name() << std::endl;
channels[i].Wait();
std::cout << "回收: " << _channels[i].Name() << std::endl;
}
}
//......
}
class ProcessPool
{
public:
//......
bool Start()
{
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)
{
_cm.CloseAll();
//关闭子进程写
close(pipefd[1]);
//子进程工作
Work(pipefd[0]);
//关闭子进程读
close(pipefd[0]);
//子进程退出
exit(0);
}
else
{
//关闭父进程读
close(pipefd[0]);
//构建管道
_cm.Insert(pipefd[1],subid);
}
}
return true;
}
//......
}
五、命名管道
匿名管道:只能是具有血缘关系的进程进行进程间通信
命名管道:可以在不相关的进程直接交互数据
命名管道文件只会被打开,不会被刷新到磁盘
5.1.mkfifo命令
mkfifo命令:创建命名管道
unlink命令:删除文件

5.2.mkfifo函数
mkfifo函数:创建命名管道

unlink函数:删除文件

5.3.命名管道 VS 匿名管道
匿名管道:由pipe函数创建并打开
命名管道:由mkfifo函数创建,由open函数打开
唯一的区别在于创建和打开方式不同
5.4.命名管道通信
- comm.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define PATH "."
#define FILENAME "fifo"
// 命名管道类
class NamedIpcFifo
{
public:
NamedIpcFifo(const std::string &path, const std::string &name)
: _path(path), _name(name)
{
_fifoname = _path + "/" + _name;
// 创建管道文件
umask(0); // 设置权限掩码
int n = mkfifo(_fifoname.c_str(), 0666);
if (n < 0)
{
std::cerr << "mkdir fifo error" << std::endl;
}
else
{
std::cout << "mkfifo success" << std::endl;
}
}
~NamedIpcFifo()
{
// 删除管道文件
int n = unlink(_fifoname.c_str());
if (n == 0)
{
std::cout << "remove fifo success" << std::endl;
}
else
{
std::cout << "remove fifo failed" << std::endl;
}
}
private:
std::string _path; // 管道所在目录路径
std::string _name; // 管道文件名称
std::string _fifoname; // 管道完整路径
};
// 文件操作类
class FileOper
{
public:
FileOper(const std::string &path, const std::string &name)
: _path(path), _name(name), _fd(-1)
{
_fifoname = _path + "/" + _name;
}
// 打开文件(读)
void OpenForRead()
{
// 写入方没有执行open的时候,读取方就要在open内部阻塞
// 直到有人把管道文件打开了,open才会返回
_fd = open(FILENAME, O_RDONLY);
if (_fd < 0)
{
std::cerr << "open fifo error" << std::endl;
return;
}
std::cout << "open fifo success" << std::endl;
}
// 打开文件(写)
void OpenForWrite()
{
_fd = open(FILENAME, O_WRONLY);
if (_fd < 0)
{
std::cerr << "open fifo error" << std::endl;
return;
}
std::cout << "open fifo success" << std::endl;
}
// 写文件
void Write()
{
std::string message;
int cnt = 1;
pid_t id = getpid();
while (true)
{
std::cout << "Please Enter# ";
std::getline(std::cin, message);
message += (", message number: " + std::to_string(cnt++) + "[" + std::to_string(id) + "]");
write(_fd, message.c_str(), message.size());
}
}
// 读文件
void Read()
{
while (true)
{
char buffer[1024];
int number = read(_fd, buffer, sizeof(buffer) - 1);
if (number > 0)
{
buffer[number] = 0;
std::cout << "Client Say# " << buffer << std::endl;
}
else if (number == 0)
{
std::cout << "client quit! me too!" << std::endl;
break;
}
else
{
std::cout << "read error" << std::endl;
break;
}
}
}
// 关闭文件
void Close()
{
if(_fd > 0)
{
close(_fd);
}
}
~FileOper()
{}
private:
std::string _path; // 管道所在目录路径
std::string _name; // 管道文件名称
std::string _fifoname; // 管道完整路径
int _fd; // 管道文件描述符
};
cpp
#include "comm.hpp"
int main()
{
// 在当前目录下创建管道文件
NamedIpcFifo fifo(PATH, FILENAME);
// 文件操作
FileOper readerfile(PATH, FILENAME);
readerfile.OpenForRead();
readerfile.Read();
readerfile.Close();
return 0;
}
cpp
#include "comm.hpp"
int main()
{
// 文件操作
FileOper writerfile(PATH, FILENAME);
writerfile.OpenForWrite();
writerfile.Write();
writerfile.Close();
return 0;
}
- Makefile
cpp
.PHONY:all
all:client server
client:client.cc
g++ -o $@ $^ -std=c++11
server:server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f client server
六、System V共享内存
System V:一种模块标准,是三种进程间通信机制的总称
IPC本质:让不同的进程看到同一份资源
6.1.共享内存的原理
多组进程使用不同的共享内存进行通信
多个共享内存同时存在就需要进行管理(先描述,再组织)
共享内存由描述自己的结构体对象与物理内存
进程与共享内存的关系就是内核数据结构关系

6.2.共享内存的接口
6.2.1.shmget函数
作用:在系统中创建共享内存
- 参数1:key值,不是由内核形成,而是在用户层构建并传入OS
- 参数2:共享内存大小,必须是4KB(4096)的整数倍,否则会向上取整
- 参数3:设置共享内存标志位,设置共享内存的权限
IPC_CREAT(获取):如果目标共享内存不存在,就创建,否则打开这个存在的共享内存并返回
(IPC_CREAT |) IPC_EXCL(创建):单独使用无意义,如果目标共享内存存在,就出错返回
key值类似命名管道约定同一个管道文件,保证共享内存存在,标识共享内存的唯一性
返回值:
- 创建成功:合法的共享内存标识符(类似文件描述符)
- 创建失败:-1

6.2.2.ftok函数
作用:将路径名与id整数的字符混合,通过算法构建一个key值,减少key值冲突

6.2.3.ipcs命令
**ipcs -m:**查看共享内存

6.2.4.ipcrm命令
**ipcrm -m shmid:**删除共享内存

用户层使用shmid管理共享内存,key只用来给内核区分唯一性
6.2.5.shmctl函数
作用:控制共享内存,删除共享内存
参数1:共享内存标识符
参数2:控制命令
IPC_RMID:删除共享内存
IPC_STAT:拷贝共享内存属性
参数3:共享内存属性表


key值会被设置到共享内存的描述结构体中

6.2.6.shmat函数
作用:将共享内存挂接(attach)到进程的虚拟地址空间中
返回值:
挂接成功:虚拟空间的起始虚拟地址
挂接失败:-1
参数1:共享内存标识符
参数2:采用固定地址挂接,不需要传NULL
参数3:共享内存标志位,传0使用默认设置

6.2.7.shmdt函数
作用:将共享内存与虚拟内存去关联,减少共享内存结构体的引用计数

6.3.共享内存的实现
读写共享内存没有使用系统调用
共享区属于用户空间而非内核的
所以共享内存是进程通信最快的
- 映射后读写直接被对方看到
- 不需要系统调用或写入内容
但是通信双方没有保护机制,共享内存的写端只写了一部分就会被读端读取,导致数据不一致
有保护机制:
AA
AA BB
AA BB CC
无保护机制:
A
AA
AA B
AA BB
可以使用命名管道的同步机制对共享内存进行局部保护
进程与进程的共享内存映射:SystemV
进程与磁盘文件的共享内存映射:mmap
- shm.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <unistd.h>
#include "comm.hpp"
const int gdefaultid = -1; // 错误共享内存标识符
const int gsize = 4096; // 共享内存空间
const std::string pathname = "."; // 路径名称
const int projid = 0x66; // 生成key的任意id
const int gmode = 0666; // 共享内存权限
#define CREATER "creater"
#define USER "user"
// 共享内存类
class Shm
{
private:
void CreateHelper(int flg)
{
printf("key: 0x%x\n", _key);
_shmid = shmget(_key, gsize, flg);
if (_shmid < 0)
{
ERR_EXIT("shmget");
}
printf("shmid: %d\n", _shmid);
}
// 创建一个共享内存
void Create()
{
CreateHelper(IPC_CREAT | IPC_EXCL | gmode);
}
// 关联一个共享内存
void Attach()
{
_start_mem = shmat(_shmid, nullptr, 0);
if ((long long)_start_mem < 0)
{
ERR_EXIT("shmat");
}
printf("attach success\n");
}
// 去关联一个共享内存
void Detach()
{
int n = shmdt(_start_mem);
if (n == 0)
{
printf("Detach success\n");
}
}
// 获取一个共享内存
void Get()
{
CreateHelper(IPC_CREAT);
}
// 删除一个共享内存
void Destroy()
{
Detach();
if (_usertype == CREATER)
{
int n = shmctl(_shmid, IPC_RMID, nullptr);
if (n > 0)
{
printf("shmctl delete shm: %d success!\n", _shmid);
}
else
{
ERR_EXIT("shmctl\n");
}
}
}
public:
Shm(const std::string &pathname, int projid, const std::string &usertype)
: _shmid(gdefaultid), _size(gsize), _start_mem(nullptr), _usertype(usertype)
{
_key = ftok(pathname.c_str(), projid);
if (_key < 0)
{
ERR_EXIT("ftok");
}
if (_usertype == CREATER)
{
Create();
}
else if (_usertype == USER)
{
Get();
}
else
{
}
Attach();
}
// 返回起始虚拟地址
void *VirtualAddr()
{
printf("VirtualAddr: %p\n", _start_mem);
return _start_mem;
}
// 获取共享内存大小
int Size()
{
return _size;
}
// 获取共享内存的属性
void Attr()
{
struct shmid_ds ds;
int n = shmctl(_shmid, IPC_STAT, &ds);
printf("shm_segsz: %ld\n", ds.shm_segsz);
printf("key: 0x%x\n", ds.shm_perm.__key);
}
~Shm()
{
std::cout << _usertype << std::endl;
Destroy();
}
private:
int _shmid; // 共享空间标识符
key_t _key; // key值
int _size; // 共享空间大小
void *_start_mem; // 起始的虚拟地址
std::string _usertype; // 用户类型
};
- Fifo.hpp
cpp
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "comm.hpp"
#define PATH "."
#define FILENAME "fifo"
// 命名管道类
class NamedIpcFifo
{
public:
NamedIpcFifo(const std::string &path, const std::string &name)
: _path(path), _name(name)
{
_fifoname = _path + "/" + _name;
// 创建管道文件
umask(0); // 设置权限掩码
int n = mkfifo(_fifoname.c_str(), 0666);
if (n < 0)
{
ERR_EXIT("mkfifo");
}
else
{
std::cout << "mkfifo success" << std::endl;
}
}
~NamedIpcFifo()
{
// 删除管道文件
int n = unlink(_fifoname.c_str());
if (n == 0)
{
//bug:先析构fifo后,进程直接退出,导致shm析构没有被调用
//ERR_EXIT("unlink");
}
else
{
std::cout << "remove fifo failed" << std::endl;
}
}
private:
std::string _path; // 管道所在目录路径
std::string _name; // 管道文件名称
std::string _fifoname; // 管道完整路径
};
// 文件操作类
class FileOper
{
public:
FileOper(const std::string &path, const std::string &name)
: _path(path), _name(name), _fd(-1)
{
_fifoname = _path + "/" + _name;
}
// 打开文件(读)
void OpenForRead()
{
// 写入方没有执行open的时候,读取方就要在open内部阻塞
// 直到有人把管道文件打开了,open才会返回
_fd = open(FILENAME, O_RDONLY);
if (_fd < 0)
{
ERR_EXIT("openforread");
}
std::cout << "open fifo success" << std::endl;
}
// 打开文件(写)
void OpenForWrite()
{
_fd = open(FILENAME, O_WRONLY);
if (_fd < 0)
{
ERR_EXIT("openforwrite");
}
std::cout << "open fifo success" << std::endl;
}
// 唤醒读端
void Wakeup()
{
char c = 'c';
int n = write(_fd, &c, 1);
printf("尝试唤醒: %d\n", n);
}
// 等待写端
bool Wait()
{
char c;
int number = read(_fd, &c, 1);
if (number > 0)
{
printf("读端醒来: %d\n", number);
return true;
}
return false;
}
// 关闭文件
void Close()
{
if (_fd > 0)
{
close(_fd);
}
}
~FileOper()
{
}
private:
std::string _path; // 管道所在目录路径
std::string _name; // 管道文件名称
std::string _fifoname; // 管道完整路径
int _fd; // 管道文件描述符
};
cpp
#include "Shm.hpp"
#include "Fifo.hpp"
int main()
{
// 创建共享内存对象
Shm shm(pathname, projid, CREATER);
// 获取共享内存属性
sleep(5);
shm.Attr();
// 创建管道文件对象
NamedIpcFifo fifo(PATH, FILENAME);
// 文件操作
FileOper readerfile(PATH, FILENAME);
readerfile.OpenForRead();
// 指向共享内存的指针
char *mem = (char *)shm.VirtualAddr();
while (true)
{
// 管道未写入时读端默认阻塞,等待写端写入
if(readerfile.Wait())
{
printf("%s\n", mem);
}
else
{
break;
}
}
readerfile.Close();
return 0;
}
cpp
#include "Shm.hpp"
#include "Fifo.hpp"
int main()
{
// 文件操作
FileOper writerfile(PATH, FILENAME);
writerfile.OpenForWrite();
// 获取共享内存对象
Shm shm(pathname, projid, USER);
// 指向共享内存的指针
char *mem = (char *)shm.VirtualAddr();
int index = 0;
for (char c = 'A'; c <= 'Z'; c++, index += 2)
{
sleep(2);
// 向共享内存写
mem[index] = c;
mem[index + 1] = c;
mem[index + 2] = 0;
// 唤醒读端读取
writerfile.Wakeup();
}
writerfile.Close();
return 0;
}
- comm.hpp
cpp
#pragma once
#include <cstdio>
#include <cstdlib>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
- Makefile
cpp
.PHONY:all
all:client server
client:client.cc
g++ -o $@ $^ -std=c++11
server:server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f client server
七、System V消息队列
7.1.消息队列的原理
IPC本质:让不同的进程看到同一份资源
消息队列将该资源维护成一个队列
消息队列的生命周期随内核
消息队列提供了从一个进程向另外一个进程发送一块数据段方法
操作系统要对多个消息队列进行管理,先描述,再组织
两个进程通过约定key值保证看到同一个消息队列

7.2.消息队列的接口
7.2.1.msgget函数
作用:创建一个消息队列

7.2.2.msgctl函数
作用:控制消息队列



7.2.3.ftok函数
作用:生成key值

7.2.4.msgsnd函数
作用:发送数据

节点数据的结构体
mtype:数据类型(区分进程)
mtext:数据内容

7.2.5.msgrcv函数
作用:接收数据

7.2.6.ipcs命令
ipcs -q:查看消息队列

7.2.6.ipcrm命令
ipcrm -q msqid:删除消息队列
八、System V信号量
8.1.关于并发编程的一些概念
共享内存使两个进程看到同一份资源,为通信提供前提
但是没有保护机制,数据不一致,信号量是一个具体的方案
**共享资源:**多个执行流(进程)能看到的同一份公共资源
**临界资源:**被保护起来的共享资源
**临界区:**在进程中涉及到互斥资源的程序段(访问临界资源对应的代码)
保护临界区代码就是在保护临界资源
**互斥:**任何时刻只允许一个执行流访问资源
类似于ATM机,每个人进入ATM机取钱之前要先开锁
进去后门会上锁,每次只能有一个人进入提款机取钱
**同步:**多个执行流访问临界资源时有一定的顺序性
类似用共享内存加命名管道,通过管道文件的读端阻塞
等规定数据写入共享内存后,向管道文件写入唤醒读端

**原子性:**要么做,要么不做
学校学习:要么别学,要么考全校第一
锁本身也要被共享,所以在申请锁的时候,必须是原子的
要么申请好了,要么不申请
8.2.信号量的理解
**信号量(信号灯):**一个计数器,用来表明临界资源的数量多少
**作用:**保护临界区
**本质:**对资源的预定机制

信号量本身就是共享资源,申请时要减减,具有原子性(P操作)
进程用完资源退出后,信号量的值要加加,具有原子性(V操作)
通过PV操作完成进程对资源的预定机制
**多元信号量:**资源不是整体使用,通过信号量进行计数(同步)
**二元信号量:**资源整体使用,只有1或者0两态的信号量(互斥)


信号量与通信的关系
先访问信号量P,每个进程都得先看到同一个信号量
不是传递数据才是IPC通信,通知、互斥、同步也是
8.3.信号量的接口
8.3.1.semget函数
作用:创建信号量或信号量集
参数1:key值
参数2:信号量的个数
参数3:信号量集标志位

8.3.2.ftok函数
作用:生成key值

8.3.3.ipcs命令
ipcs -s:查看信号量或信号量集

8.3.4.ipcrm命令
ipcrm -s semid:删除信号量或信号量集
8.3.5.semctl函数
作用:控制信号量或信号量集
参数1:信号量集标识符
参数2:通过SET_VAL命令设置的信号量初值
参数3:信号量设置命令



8.3.6.semop函数
作用:对信号量集操作
参数1:信号量集标识符
参数2:信号量集数组
参数3:信号量集数组元素个数

sem_num:信号量集中信号量的下标
sem_op:-1进行P操作,+1进行V操作
sem_flag:默认为0

操作系统需要对大量的信号量集进行管理,需要先描述,再组织


九、内核IPC资源的组织管理
共享内存,消息队列,信号量用key来区分唯一性
在操作系统中共享内存,消息队列,信号量被当作了同一种资源,叫做System V IPC
IPC资源在内核中的组织形式


