作者前言
🎂 ✨✨✨✨✨✨🍧🍧🍧🍧🍧🍧🍧🎂
🎂 作者介绍: 🎂🎂
🎂 🎉🎉🎉🎉🎉🎉🎉 🎂
🎂作者id:老秦包你会, 🎂
简单介绍:🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂
喜欢学习C语言、C++和python等编程语言,是一位爱分享的博主,有兴趣的小可爱可以来互讨 🎂🎂🎂🎂🎂🎂🎂🎂
🎂个人主页::小小页面🎂
🎂gitee页面:秦大大🎂
🎂🎂🎂🎂🎂🎂🎂🎂
🎂 一个爱分享的小博主 欢迎小可爱们前来借鉴🎂
进程间通信(IPC)
- **作者前言**
- 进程通信的原因(目的)
- 啥叫进程间的通信
- 进程通信的条件
- 管道通信(匿名、命名)
- [system v](#system v)
-
- [system v 共享内存](#system v 共享内存)
- [system v 消息队列](#system v 消息队列)
- [system v 信号量](#system v 信号量)
- OS是如何组织管理IPC资源的
- mmap的文件映射(共享内存的另一种)
进程通信的原因(目的)
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
啥叫进程间的通信
是指在不同进程之间传递数据或信号的机制。由于进程是操作系统资源分配的基本单位,每个进程拥有独立的地址空间,一个进程无法直接访问另一个进程的数据,因此需要特定的方法来实现通信。
本质就是:让不同的进程,看到同一份资源 也就是"内存"。这个提供者不是进程,而是操作系统。
必然需要调用系统调用,进程调用系统调用,获取到相同资源
进程通信的条件
- 拥有交换数据的内存
- 提供的内容不能是双方之间的内存,保护进程之间的独立性
- 通信的本质就是,让不同的进程看到同一份资源(由操作系统提供)
由于是操作系统提供 的,往往会有很多种不一样的通讯方式。
管道通信(匿名、命名)
匿名管道(父子之间通信)

创建进程,首先会创建对应的PCB。其中文件描述符表(存在于PCB)也会创建,PCB中包含对于的文件 描述表指针(struct files_struct files),在文件描述符表(存储在进程的 PCB(如 Linux 的 task_struct->files)中),有对于的文件描述符列表里面包含有对应的标准输入、输出、错误以及打开文件的文件描述符。
当创建新的子进程,会创建出新的文件描述符表,然后把父进程文件描述符表里面的内容拷贝过来。
文件描述符表的认识

文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针 files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,其中0、1、2下标的位置是被标准输入、输出、错误占用,打开新的文件存储新的位置从3开始存储。每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
分配的规则
fd: 0 或者 fd 2 文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的
最小的一个下标,作为新的文件描述符。
磁盘存储的时候,我们往文件里面写入数据,首先会先写入到用户级缓冲区,然后fflush刷新到内核级缓冲区,然后操作系统再写入到磁盘。
而父进程打开的文件会创建对应的struct file对象,子进程不会进行再进行创建,子进程只会把父进程的文件描述符表里面的内容进行浅拷贝,在struct file fd_array\[\]数组里面的也会拷贝,但是里面的file对象不会进行拷贝,因为那是文件管理系统的事情。也不会创建一个新的file对象。如图:

这样在父进程进行打开的文件,在子进程也会拥有对应的文件描述符,也是可以看到文件缓冲区。这就满足了一个条件,让不同进程看到同一份资源(看到同一份缓冲区)。
而管道的出现,就是基于文件的观念创建出来的,让不同进程看到同一份资源
而在Linux中也有这个管道,
bash
ll | wc -l

管道本质上就是"文件"
在 Linux 内核中,管道(特别是命名管道 FIFO)确实是被当作一种特殊类型的文件来处理的。
当你使用 | 时,内核会在内存中创建一个缓冲区,并为其分配一个文件描述符(File Descriptor)。
对于前一个命令(如 ls),它只管往这个"文件"里写数据(标准输出);对于后一个命令(如 wc),它只管从这个"文件"里读数据(标准输入)。
两个进程完全不需要知道对方的存在,它们只是把管道当成了一个普通的文件进行读写。
简单的来说,
匿名管道 :基于文件。让不同进程看到同一份资源的通信方式叫做管道。
做法:通过创建父子进程,让子进程继承父进程的数据信息,让子进程看到对应的资源。
管道只能设置成单向通信的。
局限:
常用于父子进程和亲戚进程的。
原理如下:
我们就以父进程读出。子进程写入的方向实现一下管道,子进程通过对应的文件对象,把数据写入到缓冲区,然后父进程通过文件对象读取缓冲区的内容,这样父进程和子进程通过缓冲区来实现进程间的通信了。(管道是单向通信的)
这个管道并不会跟磁盘的文件进行挂钩,而是操作系统提供内存级文件,当父进程打开这个内存级文件的时候,会返回两个文件描述符,分别存储到父进程的文件描述符表里面去。
如图:

这个管道文件是操作系统设置的,不会刷新到磁盘里面,前面我们在Linux下打开文件,调用open这些系统调用,而使用这个管道文件,不能使用这个。
为此在Linux中提供有一个对于的系统调用,pipe 是 Linux(及类 Unix 系统)中的一个重要系统调用,用于创建匿名管道(Anonymous Pipe),实现进程间通信(IPC)。
#include <unistd.h>
int pipe(int pipefd[2]);
创建一个单向数据通道,返回两个文件描述符:
pipefd0:管道的读取端(read end)。
pipefd1:管道的写入端(write end)。
需要注意的是,这两个文件是内存级的,不会往磁盘中进行存储,也不会进行往磁盘读取。
调用open系统调用的时候,需要文件路径,而这个文件没有,这就是叫做匿名管道的原因。
代码如下:
makefile:
bash
test.o:test.cpp
g++ -E $< -o test.i -std=c++11
g++ -S test.i -o test.s
g++ -c test.s -o $@
libmyc.a:mystdio.cpp
g++ -c $< -std=c++11
ar -rc $@ mystdio.o
libmyc.so:mystdio.cpp
g++ -fPIC -c $^ -std=c++11
g++ -shared mystdio.o -o $@
test_a.exe:test.o libmyc.a
g++ $< -o $@ -I ./ -L ./ -l myc
test_so.exe:test.o libmyc.so
g++ $< -o $@ -I ./ -L ./ -l myc
.PHONY:clean a so
clean:
rm -rf test.i test.s *.o test_* *.so *a
a:
make
make libmyc.a
make test_a.exe
echo "可执行文件执行完毕,运行结果如下:"
./test_a.exe
so:
make
make libmyc.so
make test_so.exe
echo "可执行文件执行完毕,运行结果如下:"
./test_so.exe
mystdio,h
cpp
#pragma once
#include <sys/types.h>
#include<string.h>
#include<sys/wait.h>
#include<stdio.h>
#include<unistd.h>
#include<iostream>
class MyProcessShared
{
public:
MyProcessShared(int *fd);
void PipeCrete();
void ChildProcess();
void FileWrite(int f);
void FileRead(int f);
private:
int *_arr;
};
mystdio.cpp
cpp
#include"mystdio.h"
using namespace std;
MyProcessShared::MyProcessShared(int *fd)
{
_arr = fd;
}
void MyProcessShared::PipeCrete()
{
int n = pipe(_arr);
if(n < 0)
{
std::cerr << "pipe error" << endl;
return;
}
else
{
cout << "fd[0]: " << _arr[0] << endl;
cout << "fd[1]: " << _arr[1] << endl;
}
}
void MyProcessShared::FileWrite(int f)
{
char arr[100];
int cut = 100;
while(cut)
{
snprintf(arr, sizeof(arr), "pid:%d, num: %d", getpid(),cut--);//\0是c里面有的,不会写入到文件,使用相关函数,会自动添\0
if(write(f, arr, strlen(arr)) != -1)
cout << "写入成功,等待3秒\n" << endl;//注意
else
cout << "写入失败\n" << endl;
sleep(3);
}
}
void MyProcessShared::FileRead(int f)
{
int num = 0;
while(1)
{
char buf[100];
num = read(f, buf, sizeof(buf) - 1);
if(num <= 0)
break;
buf[sizeof(buf)] = '\0';
cout << buf << endl;//记得加\n,否则显示不出来,因为显示器的缓冲区是行缓冲,不然会打印不出来
}
}
void MyProcessShared::ChildProcess()
{
//创建子进程
pid_t id = fork();
//是否创建成功
if (id < 0)
return;
if(id == 0)//子进程
{
//子进程进行写入管道
close(_arr[0]);
FileWrite(_arr[1]);
close(_arr[1]);//关闭写入
exit(0);
}
else//父进程
{
//父进程进行读取管道
close(_arr[1]);
cout << "father process";
FileRead(_arr[0]);
close(_arr[0]);//关闭读取
//等待回收
int status = 0;
if(waitpid(id, &status, 0) == id)
{
cout << "子进程结束, 退出码:" << ((status >> 8) & 0Xff) << "退出信号:" << (status & 0x7f) << endl;
}
else
{
cout << "回收失败" << endl;
}
}
}
效果如下:

上面的代码是为了验证一下,pipe的写法,主要体现出管道的四个特征,
一个是管道是单向通信的(属于半双工通信),
一个是管道的同步机制(管道没有数据,读取等待,写入进行。有数据,读取进行。写入等待),
匿名管道只能在具备血缘进程才能使用,
管道是面向字节流(体现在写入简单,读取的时候需要按照对应的格式读取。),
下面开始正题
上面的代码中,子进程写入,父进程读取,一般会产生一些情况
情况1 写入快,读取慢,子进程不写入,也不关闭写的文件操作,父进程会阻塞,直到管道内有数据可以读取。
cpp
void MyProcessShared::FileWrite(int f)
{
char arr[100];
int cut = 100;
while(cut)
{
snprintf(arr, sizeof(arr), "pid:%d, num: %d", getpid(),cut--);//\0是c里面有的,不会写入到文件,使用相关函数,会自动添\0
if(write(f, arr, strlen(arr)) != -1)
cout << "写入成功,等待3秒\n" << endl;//注意
else
cout << "写入失败\n" << endl;
//sleep(3);
}
}
void MyProcessShared::FileRead(int f)
{
int num = 0;
while(1)
{
char buf[100];
num = read(f, buf, sizeof(buf) - 1);
if(num <= 0)
break;
buf[sizeof(buf)] = '\0';
cout << buf << endl;//记得加\n,否则显示不出来,因为显示器的缓冲区是行缓冲,不然会打印不出来
sleep(1);
}
}
当写入很快的时候,读取很慢,就会有如下情况:

可以看到每次读取的数据都不一样,这是因为管道是面向字节流的,并且写入数据是很多条的,当读取的时候,往往会多条一起读取.
情况2:写入很慢,读取很快子进程会一直写入,父进程不进行读取,直到子进程把管道写满,子进程会进行阻塞
cpp
void MyProcessShared::FileWrite(int f)
{
char arr[100];
int cut = 100;
while(cut)
{
snprintf(arr, sizeof(arr), "pid:%d, num: %d", getpid(),cut--);//\0是c里面有的,不会写入到文件,使用相关函数,会自动添\0
if(write(f, arr, strlen(arr)) != -1)
cout << "写入成功,等待3秒\n" << endl;//注意
else
cout << "写入失败\n" << endl;
sleep(1);
}
}
void MyProcessShared::FileRead(int f)
{
int num = 0;
while(1)
{
char buf[100];
num = read(f, buf, sizeof(buf) - 1);
if(num <= 0)
break;
buf[sizeof(buf)] = '\0';
cout << buf << endl;//记得加\n,否则显示不出来,因为显示器的缓冲区是行缓冲,不然会打印不出来
//sleep(1);
}
}
情况3:子进程写入一部分数据后不再写入,直接关闭写入文件操作,父进程会把管道内的数据读取完,当读取不到数据的时候会类似读取到文件结尾。
void MyProcessShared::FileWrite(int f)
{
char arr[100];
int cut = 100;
while(cut)
{
snprintf(arr, sizeof(arr), "pid: %d, num: %d", getpid(),cut--);//\0是c里面有的,不会写入到文件,使用相关函数,会自动添\0
if(write(f, arr, strlen(arr)) != -1)
cout << "写入成功,等待1秒" << endl;//注意
else
cout << "写入失败\n" << endl;
if(cut == 66)
close(_arr[1]);//关闭写入
sleep(2);
}
}
void MyProcessShared::FileRead(int f)
{
int num = 0;
while(1)
{
char buf[100];
num = read(f, buf, sizeof(buf) - 1);
buf[num] = '\0';
if(num < 0)
break;
else if(num == 0)
cout << "num:" << num << endl;
else
cout << buf << endl;//记得加\n,否则显示不出来,因为显示器的缓冲区是行缓冲,不然会打印不出来
//sleep(1);
}
}
如图:

情况4 : 父进程 读取一段时间,不再进行读取,并且关闭读操作, OS(操作系统)会直接终止子进程的写入,通过信号杀死子进程。
void MyProcessShared::FileWrite(int f)
{
char arr[100];
int cut = 100;
while(cut)
{
snprintf(arr, sizeof(arr), "pid: %d, num: %d", getpid(),cut--);//\0是c里面有的,不会写入到文件,使用相关函数,会自动添\0
if(write(f, arr, strlen(arr)) != -1)
cout << "写入成功,等待1秒" << endl;//注意
else
cout << "写入失败\n" << endl;
// if(cut == 66)
// close(_arr[1]);//关闭写入
sleep(1);
}
}
void MyProcessShared::FileRead(int f)
{
int num = 0;
while(1)
{
char buf[100];
num = read(f, buf, sizeof(buf) - 1);
buf[num] = '\0';
if(num <= 0)
break;
else
cout << buf << endl;//记得加\n,否则显示不出来,因为显示器的缓冲区是行缓冲,不然会打印不出来
break;
//sleep(1);
}
}

这个在Linux系统中,就会当读取关闭,操作系统就会认为这个管道已经失去了意义,就会使用kill -13命令杀死子进程,
总结匿名管道有5种特性:
- 自带同步机制
- 只能进行血缘关系的进程之间进行通信。
- pipe面向字节流
- 父子退出,管道自动释放,(管道文件的生命周期,随着进程的)
- 管道只能单向通信,
管道的容量和管道的原子性(要么执行要么不执行)
cpp
void MyProcessShared::FileRead(int f)
{
int num = 0;
while(1)
{
char buf[100];
num = read(f, buf, sizeof(buf) - 1);
buf[num] = '\0';
if(num <= 0)
break;
else
cout << buf << endl;//记得加\n,否则显示不出来,因为显示器的缓冲区是行缓冲,不然会打印不出来
// break;
sleep(100);
}
}
void MyProcessShared::FileWrite(int f)
{
char arr[100];
int num = 1;
while(num)
{
snprintf(arr, sizeof(arr), "pid: %d, num: %d", getpid(),num);//\0是c里面有的,不会写入到文件,使用相关函数,会自动添\0
if(write(f, arr, sizeof(char)))
{
cout << "写入成功, num:"<< num << endl;
}
else
{
cout << "写入失败" << endl;
break;
}
num++;
}
// char arr[100];
// int cut = 100;
// while(cut)
// {
// snprintf(arr, sizeof(arr), "pid: %d, num: %d", getpid(),cut--);//\0是c里面有的,不会写入到文件,使用相关函数,会自动添\0
// if(write(f, arr, strlen(arr)) != -1)
// cout << "写入成功,等待1秒" << endl;//注意
// else
// cout << "写入失败\n" << endl;
// // if(cut == 66)
// // close(_arr[1]);//关闭写入
// sleep(1);
// }
}
效果如下:

可以看到管道的大小是65537个字节(B),也就是64KB。
管道的原子性如下:
bash
man 7 pipe

意思就是:POSIX.1规定,写入小于PIPE_BUF字节的数据必须是原子操作,这个_BUF的大小就是4KB。也就是说写入的最少也就是4KB,这也就是说原子性要么子进程写入4KB的数据,然后父进程读取到4kB,要么就是子进程写够4kb的数据,父进程才能读取。
匿名管道和Linux中的'|'的区别
在Linux中执行下面的指令
sleep 1000 | sleep 2000 | sleep 3000
这里会创建3个进程和两个管道(这个管道就是匿名管道),把进程1的标准输出重定向到管道1的写端,进程2的标准输入重定向到管道1的读端,以此类推。
然后再再执行进程查看操作
ps ajx | grep sleep
如图:

可以看到如上的图像, 启动的这三个进程属于兄弟关系,这三个进程的父进程都是bash进程,也就是我们用户进程。这三个进程之间通过管道进行通信,
匿名管道的应用场景(进程池)
-
进程池(Process Pool)是一种并发编程技术 ,用于管理和复用多个子进程,以提高任务执行效率,避免频繁创建/销毁进程的开销。它通常用于多任务并行处理(如Web服务器、批量数据处理等)
主进程控制多个进程

创建多个子进程, 然后父进程往对应的通道发送任务码,对应的子进程执行对应的任务.
写法1:
.cpp#include
#include<unistd.h>
#include
#include
#include
#include
#include<sys/types.h>
#include "process.hpp"
using namespace std;
enum{
Yes = 0,
UsageError = 1,
PipeError
};class Chanel
{
public:
Chanel()
{} Chanel(int &fd, pid_t &pid, std::string &name) :_fd_W(fd) ,_processChildPid(pid) ,_name(name) { } void WriteDate(const char * date) { int signe = write(_fd_W, date, strlen(date)); if(signe < 0) std::cout << "write error" << std::endl; } void PrintfDebug() { std:: cout << "_fd_w:" << _fd_W << std::endl; std:: cout << "_processChildPid:" << _processChildPid << std::endl; std:: cout << "_name:" << _name << std::endl; } void ChanelName() { std::cout << _name << std::endl; } void SendTask(uint32_t code) { write(_fd_W, &code, sizeof(uint32_t)); } ~Chanel() { }private:
int _fd_W;
pid_t _processChildPid;
std::string _name;
};int CreateProcess(std::vector
&pipeList, const int &processNum, wor_k work1)
{
for(int i = 0; i < processNum; i++)
{
int buf[2] = {0};
if(pipe(buf) < 0)//管道
return PipeError;
std::string name ="childprocess"+std::to_string(i+1);
pid_t pid = fork();
if(pid == 0)
{
close(buf[1]);
dup2(buf[0], 0);//把标准输入重定向到管道的读端,也就是把原本从键盘读取内容。现在变成了从管道的读端读取内容
//任务
work1();
// sleep(100);
exit(0);
}
pipeList.push_back(Chanel(buf[1], pid, name));
close(buf[0]);
cout << name << "创建成功"<< std::endl;
}
sleep(10);
return Yes;
}class ProcessPool
{
public:
ProcessPool(int &argc, char* argv[], wor_k work1)
:_processNum(std::stoi(argv[1]))
{
_processCode = CreateProcess(_pipeList, _processNum, work1);
}int ProcessCode() { if(_processCode < 1) { return PipeError; } return Yes; } Chanel& NextChanel() { static int next = 0; Chanel &c = _pipeList[next++]; next %= _pipeList.size();//负载均衡选择进程 return c; } void Debug() { for(auto e: _pipeList) { e.PrintfDebug(); } } ~ProcessPool() { }private:
std::vector_pipeList;
int _processNum;
int _processCode;};
int main(int argc, char* argv[])
{
srand(time(NULL));
if(std::stoi(argv[1]) < 1)
return UsageError;
ProcessPool pool(argc, argv, work);/*父进程写入*/ while(1) { //选择一个进程和管道 Chanel & chanel = pool.NextChanel(); chanel.PrintfDebug(); //选择任务 uint32_t code = NextTask(); //发送任务 chanel.SendTask(code); sleep(2); } // pool.Debug(); return pool.ProcessCode();}
.hpp
#pragma once
#include<cstdio>
#include<unistd.h>
#include<iostream>
#include<stdlib.h>
#include<time.h>
using namespace std;
typedef void(* wor_k)();
void TaskOne()
{
std::cout << "this is task one" << std::endl;
}
void TaskTwo()
{
std::cout << "this is task two" << std::endl;
}
void TaskThree()
{
std::cout << "this is task three" << std::endl;
}
wor_k tasks[] = {TaskOne, TaskTwo, TaskThree};
uint32_t NextTask()
{
return rand() % 3;
}
void work()//这里不需要传入fd,因为前面把文件描述列表中的0位置变成了管道的读端。
{
while(1)
{
uint32_t task_Code = 0;
ssize_t num = read(0,&task_Code, sizeof(uint32_t));//从管道中读取对应的数据
if(task_Code >= 3)
continue;
else
{
tasks[task_Code]();
}
std::cout << "I am child Process, pid" << getpid() << std::endl;
sleep(1);
}
}
写法2 :
makefile
bash
test.exe:test.cc
g++ -E $^ -o test.i -std=c++11
g++ -S test.i -o test.s
g++ -c test.s -o test.o
g++ test.o -o $@
test_a.exe:test.cc processpool.cpp
g++ -c *.cc -std=c++11 -g
g++ -c *.cpp -std=c++11 -g
ar -rc libprocess_pool.a processpool.o task.o
g++ test.o -o $@ -I ./ -L ./ -l process_pool -g
test_so.exe:test.cc processpool.cpp
g++ -c *.cc -std=c++11
g++ -c -fPIC *.cpp -std=c++11
g++ -shared processpool.o task.o -o libprocess_pool.so
g++ test.o -o $@ -I ./ -L ./ -l process_pool
.PHONY:clean a so
clean:
rm -rf *.o *.exe *.a *.so *.i *.s
a:
make test_a.exe
./test_a.exe
so:
make test_so.exe
./test_so.exe
task.h
cpp
#pragma once
#include<functional>
#include<iostream>
#include<unistd.h>
#include<vector>
#include <sys/types.h>
#include <sys/wait.h>
#include<istream>
#include<ostream>
#include<cstdlib>
#include<ctime>
#define NUM 2 //多少个任务
#define N 1 //多少个子进程
using namespace std;
using task_t = function<void()>;
class MyTaskManager
{
public:
MyTaskManager();
~MyTaskManager();
int SeleteTask();
void Registration(task_t a);//注册任务
void Execute(int code);//执行任务
private:
vector<task_t> _tm;
};
task.cpp
cpp
#include "task.h"
void Upload()
{
cout << "上传一个任务" << endl;
}
void Download()
{
cout << "下载一个任务" << endl;
}
void PrintLog()
{
cout << "打印日志" << endl;
}
MyTaskManager::MyTaskManager()
:_tm()
{
_tm.push_back(Upload);
_tm.push_back(Download);
_tm.push_back(PrintLog);
}
MyTaskManager::~MyTaskManager()
{
}
int MyTaskManager::SeleteTask()
{
return rand() % _tm.size();
}
void MyTaskManager::Registration(task_t a)
{
_tm.push_back(a);
}
void MyTaskManager::Execute(int code)
{
if(code > _tm.size() || code < 0)
{
cout << "子进程:" << getpid() << "执行任务失败" << endl;
return ;
}
_tm[code]();
}
.process.pool.h
cpp
#pragma once
#include"task.h"
using namespace std;
namespace bit
{
class Chennal //描述管道
{
public:
Chennal(int rfd, int wfd, pid_t child_pid);
int operator [](size_t i);
int Wfd();
int Rfd();
~Chennal();
void Fwrok(int code);
friend std::ostream& operator<<(std::ostream& out, const Chennal& a);
private:
int _wfd;
int _rfd;
pid_t _subid;
};
class ChannelManager//管道管理
{
public:
ChannelManager();
~ChannelManager();
pid_t Create();
void Destroy();
vector<Chennal> Remember();
void PrintPids();
private:
std::vector<Chennal> _pipe_arr;
};
class ProcessPool//进程池包含信道
{
public:
ProcessPool(int num);
~ProcessPool();
void CreateChild();
void Cwrok(int code);
void Debug();
void Pushtask();//发送任务码
void Wait();//回收所有子进程
int * ChildFcode();
void Fclose();
int SeleteTask();//获取任务码
void Stop();//关闭所有的子进程
private:
ChannelManager *_menager;
MyTaskManager * _tm;
int _process_num;//子进程数量
int *_child_fcode;//父进程写入管道码
};
} // namespace bit
processpool.cpp
cpp
#include"processpool.h"
//管道
bit::Chennal::Chennal(int rfd, int wfd,pid_t child_pid)
{
_wfd = wfd;
_rfd = rfd;
_subid = child_pid;
}
int bit::Chennal::operator[](size_t i)
{
if(i == 0)
return _rfd;
else if(i == 1)
return _wfd;
else if(i == 2)
return _subid;
return -1;
}
int bit::Chennal::Wfd()
{
return _wfd;
}
int bit::Chennal::Rfd()
{
return _rfd;
}
bit::Chennal::~Chennal()
{
}
void bit::Chennal::Fwrok(int code)
{
size_t num = write(this->Wfd(), &code, sizeof(code));
if (num == sizeof(code))
cout << "父进程 :" << getpid() << endl;
else if (num < 0)
cout << "父进程进程写入失败" << endl;
else
cout << "父进程进程写入的数据小于int,实际为" << num << endl;
}
//管道管理
bit::ChannelManager::ChannelManager()
: _pipe_arr()
{
}
bit::ChannelManager::~ChannelManager()
{
}
pid_t bit::ChannelManager::Create()
{
//创建管道
int arr[2] = {0};
if(pipe(arr) < 0)
{
cout << "pipe error" << endl;
return -1;
}
//创建子进程
pid_t id = fork();
if(id < 0)
{
cout << "进程创建失败" << endl;
}
Chennal a(arr[0], arr[1], id);
_pipe_arr.push_back(a);
if(id > 0)//父进程
{
for(auto e : Remember())
{
cout << e << endl;
}
//cout << *_pipe_arr.rbegin() << _pipe_arr.size() << endl;
cout << " 子进程" << id << " " << arr[0] << " "<< arr[1] << endl;
}
return id;
}
void bit::ChannelManager::Destroy()
{
_pipe_arr.pop_back();
}
vector<bit::Chennal> bit::ChannelManager::Remember()
{
return _pipe_arr;
}
void bit::ChannelManager::PrintPids()
{
for(auto & e : _pipe_arr)
{
std::cout << e << endl;
}
}
//进程池
bit::ProcessPool::ProcessPool(int num)
: _menager(new ChannelManager())
,_tm(new MyTaskManager())
{
_process_num = num;
_child_fcode = new int[num];
}
bit::ProcessPool::~ProcessPool()
{
delete _menager;
delete _tm;
delete [] _child_fcode;
}
void bit::ProcessPool::Cwrok(int code)
{
cout << "子进程开始工作" << endl;
while(1)
{
sleep(1);
int a = -1;
ssize_t num = read(code, &a, sizeof(int));
if(num > 0)
{
cout << "子进程id:" << getpid() << ", 获取任务码为:" << a << endl;
_tm->Execute(a);//执行任务
}
else if(num == 0)
{
cout << getpid() << " 子进程文件末尾" << endl;
break;
}
else
{
cout << getpid() << " 退出循环";
break;
}
}
}
void bit::ProcessPool::Debug()
{
_menager->PrintPids();
}
void bit::ProcessPool::Pushtask()//传入文件描述符
{
int num = NUM;
while(num)
{
sleep(2);
//选择任务
int task = _tm->SeleteTask();
cout << "任务为:" << task << endl;
//选择子进程
bit::Chennal childpipe = _menager->Remember()[num % N];//防止野指针,使用引用就会野指针
cout << childpipe << endl;
//选择信道
childpipe.Fwrok(task);
num--;
}
}
void bit::ProcessPool::Wait()
{
for(auto e : _menager->Remember())
{
int status = 0;
if(e[2] == waitpid(e[2],&status, 0))
{
cout << "子进程:" << e << " ,exit code:" << ((status >> 8) & 0xff) << ",signal code:" << (status & 0x7f) << endl;
}
else
{
cout << e << "回收失败" << endl;
}
}
}
int *bit::ProcessPool::ChildFcode()
{
return _child_fcode;
}
void bit::ProcessPool::Fclose()
{
for(auto e: _menager->Remember())
{
close(e[1]);
}
}
int bit::ProcessPool::SeleteTask()
{
return rand();
}
void bit::ProcessPool::Stop()
{
// //关闭所有的父进程写端
// Fclose();
// //回收子进程
// Wait();
//关闭方式1,从后往上关闭,其他进程仍然有多个写端
for(int i = _menager->Remember().size() - 1; i >= 0; i--)
{
int status = 0;
Chennal menst = _menager->Remember()[i];
cout << "开始回收" << menst[1] << endl;
close(menst[1]);//关闭父进程的写端
pid_t a = waitpid(menst[2],&status, 0);
if(a == menst[2])
{
cout << "子进程:" << menst << " ,exit code:" << ((status >> 8) & 0xff) << ",signal code:" << (status & 0x7f) << endl;
}
else
{
cout << menst << "回收失败" << endl;
}
}
}
void bit::ProcessPool::CreateChild()
{
for(int i = 0; i < _process_num; i++)
{
pid_t id = _menager->Create();
int wfd = _menager->Remember()[i][1];
int rfd = _menager->Remember()[i][0];
if( id > 0)//父进程
{
_child_fcode[i] = wfd;
cout << id << " r " << rfd << " w " << wfd <<endl;
//关闭读取
close(rfd);
}
else if(id == 0)
{
//关闭方式2,一个进程只有一个读取
// for(auto e: _menager->Remember())
// {
// close(e[1]);
// }
//方法1的关闭
close(wfd);
//工作
Cwrok(rfd);
//关闭读取
close(rfd);
exit(0);
}
}
}
namespace bit
{
std::ostream& operator<<(std::ostream &out, const Chennal &a)
{
out << "r:" <<a._rfd << " w:"<<a._wfd <<" child process pid: " << a._subid;
return out;// TODO: 在此处插入 return 语句
}
}
test.cpp
cpp
#include"processpool.h"
using namespace bit;
int main(int argc, char* argv[], char* env[])
{
srand((unsigned int)(time(NULL)));//设置起始种子为时间戳
bit::ProcessPool pool(N);
cout << "开始" << endl;
pool.CreateChild();
// pool.Debug();
pool.Pushtask();
pool.Stop();
return 0;
}
效果图如下:

命名管道
匿名管道只能在亲子之间进行通信,而命名管道可以在两个不相关的进程之间进行通信。
上面父子进程通过打开同一个文件,操作系统不会把文件的属性和内容加载两次,而是共有用,因为inode的编号是一样的,所以看到的内核级缓冲区的内容也是一样的,同样的,两个不同的进程进行通信,也要打开同一个路径下的文件,匿名管道和命名管道不同的就是,命名管道打开的文件是要有名字和路径的,路径具有唯一性。
在Linux中就要支持一个管道文件(不是平时见到的文件),这种文件不会刷新到磁盘,只会在内存中创建和销毁。
创建命名管道文件命令如下:
bash
mkfifo myfile
如图:

可以看到管道的文件类型是p,当我们往这个管道文件进行操作,使用echo 和cat进行操作,如图

可以看到bash1 往管道文件写入, 然后bash2 直接输出出来,echo这些命令都是进程,执行命令就是创建进程,这也就完成了不同进程之间的通信,这就是文件级的操作,
删除操作如下:
bash
rm myfifo
# 或者
unlink myfifo
如果使用代码的话,需要调用系统调用mkfifo
bash
int mkfifo(const char *pathname, mode_t mode);
这个跟系统调用的open是类似的,
mode的参数仍然是:数字,例如:0666,如果需要的话,可以使用umask设置文件掩码
代码如下:
comm.hpp
cpp
#pragma once
#include <sys/types.h>
#include <sys/stat.h>
#include<iostream>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
#include<functional>
#include<stdio.h>
#define FIFO_FILE "myfifo"
using namespace std;
using us = std::string;
class Myfifo
{
public:
Myfifo(const string& path, const string& name)
:_path(path)
,_name(name)
,_fd(-1)
{
}
void CreateFifo()
{
umask(0);
int fd = mkfifo((_path + _name).c_str(), 0666);
if( fd != 0)
{
cerr << _name << " error" << endl;
}
else
cout << _name << " creat success" << endl;
}
void OpenFifo(int mode)
{
_fd = open((_path + _name).c_str(), mode);
if( _fd < 0)
{
cerr << "open FIFO_FILE error" << endl;
return ;
}
cout << _name << " open success" << endl;
}
void WriteFifo()
{
OpenFifo(O_WRONLY);
us str;
while(true)
{
cout << "开始输入:";
cin >> str;
write(_fd, str.c_str(), str.size());
}
}
void ReadFifo()
{
OpenFifo(O_RDONLY);
int cut = 1;
while(true)
{
char arr[1024];
int n = read(_fd, arr, sizeof(arr) -1);
if(n > 0)
{
cout << "数据为: "<< arr << ", message number ----"<< cut++ << "["<< getpid() <<"]"<<endl;
continue;
}
else if(n == 0)
cout << "文件末尾" << endl;
else
cerr << "read error" << endl;
break;
}
}
void Close()
{
close(_fd);
_fd = -1;
}
~Myfifo()
{
if(unlink((_path + _name).c_str()) == 0)
{
cout << "renove fifo success" << endl;
}
else
cout << "renove fifo error" << endl;
}
private:
string _path;
string _name;
int _fd;
};
clien.cpp
cpp
#include"comm.hpp"
int main(int argc, char* argv[], char *env[])
{
//打开文件,进行写入
Myfifo fi("./", FIFO_FILE);
fi.WriteFifo();
fi.Close();
return 0;
}
serve.cpp
cpp
#include"comm.hpp"
int main(int argc, char* argv[], char *env[])
{
Myfifo fi("./", FIFO_FILE);
//创建管道文件
fi.CreateFifo();
//打开文件,进行读取入
fi.ReadFifo();
fi.Close();
return 0;
}
makefile:
bash
clien.exe:clien.cpp
g++ -E $^ -o clien.i
g++ -S clien.i -o clien.s
g++ -c clien.s -o clien.o -std=c++11
g++ clien.o -o $@ -I ./
serve.exe:serve.cc
g++ -E $^ -o serve.i
g++ -S serve.i -o serve.s
g++ -c serve.s -o serve.o -std=c++11
g++ serve.o -o $@ -I ./
libmycomm.so:comm.hpp
g++ -c -fPIC $^ -o comm.o
g++ -shared comm.o -o $@
.PHONY:clean exe1 exe2
clean:
rm -rf *.i *.s *.o *.exe .hpp.* myfifo
exe1:
make clien.exe
./clien.exe
exe2:
make serve.exe
./serve.exe
效果如下:

从代码中,可以看出,
- 写端未打开的时候,读取端会进行阻塞,
- 当写端关闭,读取端也会读取到文件末尾
命名管道的打开规则
• 如果当前打开操作是为读⽽打开FIFO时
◦ O_NONBLOCK disable:阻塞直到有相应进程为写⽽打开该FIFO
◦ O_NONBLOCK enable:⽴刻返回成功
• 如果当前打开操作是为写⽽打开FIFO时
◦ O_NONBLOCK disable:阻塞直到有相应进程为读⽽打开该FIFO
◦ O_NONBLOCK enable:⽴刻返回失败,错误码为ENXIO
也就是除此之外,和匿名管道是类似的,
system v
前面学习的管道通信,主要是借助系统提供的调用接口进行实现的,
system v是一种标准, Linux内核支持了这个模块,专门为这个设置了一个IPC通信模块,这个模块的设置和接口设置是类似的。
IPC的本质:让不同进程看到同一份资源。
system v 共享内存
前面我们知道目标文件链接动态库的时候,实际发生链接的过程是在运行时的,当可执行文件会先创建对应的PCB、 页表 、以及进程地址空间,然后通过的文件的inode ,找到文件的属性+内容,加载到内存中,可执行文件对应的逻辑地址,也会实现到进程地址空间中。动态库的加载也是类似的,而动态库的代码和数据映射到进程地址空间里的共享区,进程通过代码区,根据GOT表跳转到共享区执行,然后再返回代码区。这个共享内存的大致也是跟动态库的加载是类似的
大致原理思路:

在物理内存开辟一块空间, 然后在进程A和B的进程地址空间里面的共享区(栈和堆之间)里面也开辟一块空间,进行物理和虚拟地址的映射,这个方式就叫做共享内存
分为三步,1.创建共享内存。 2. 和进程地址空间形成映射关系 。 3 清除映射关系。
为此,开辟一块共享内存,就是创建一个内核数据结构体,里面包含有该内存的属性,内存的开辟是操作系统进行的,我们要开辟,就需要调用系统调用shmget,
cpp
int shmget(key_t key, size_t size, int shmflg);
key:这个共享内存段名字,可以理解为标识共享内存唯一性的
size:共享内存⼤⼩
shmflg:由九个权限标志构成,它们的⽤法和创建⽂件时使⽤的mode模式标志是⼀样的
取值为IPC_CREAT:共享内存不存在,创建并返回;共享内存已存在,获取并返回。
取值为IPC_CREAT | IPC_EXCL:共享内存不存在,创建并返回;共享内存已存在,出错返回。保证共享内存是全新的,如果单独使用IPC_EXCL无意义。如果要设置权限的话,跟文件类似,直接拼接,例如:IPC_CREAT | 0666,不用设置掩码
返回值:成功返回⼀个指针,指向共享内存第⼀个节;失败返回-1
关于key的,是区分当前的共享内存是否是我们需要,双方进行约定好key值,这样就可以判断内存是否是符合。这个key需要我们自己进行构建传入,我们可以随便传入,但是为了方便使用,常常使用一个算法函数ftok来构建key,
cpp
key_t ftok(const char *pathname, int proj_id);
pathname:传入一个文件路径
proj_id:随便传入一个整数
如果要删除共享内存或者修改,也可以使用系统调用shmctl:
cpp
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向⼀个保存着共享内存的模式状态和访问权限的数据结构,如果是删除,传入nullptr
返回值:成功返回0;失败返回-1
cmd的参数如下:

shmid_ds就是共享内存的结构体,如下:

里面有内存大小、挂接的时候。取消删除挂接的时间、更改的时间、哪个进程创建的等。
可以看到这个结构体还有一个类型ipc_perm,如图:

里面就包含有key以及mode等权限
下一步就是和进程地址空间形成映射关系,可以使用系统调用shmat,
cpp
数
功能:将共享内存段连接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid: 共享内存标识
shmaddr:指定连接的地址,也就是要挂接到哪一个虚拟地址处,如果不需要,就传入nullptr
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY,一般情况下传入0,表示默认 可读写,参数意思就是表示这个虚拟地址的权限有哪些,共享内存的权限往往会决定映射后最高虚拟地址的权限
返回值:成功返回⼀个指针,指向共享内存第⼀个节;失败返回-1,返回的地址是起始虚拟地址
运行完毕后,也是需要进行解除映射关系,这个需要调用系统调用接口shmdt:
cpp
int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
下面我们来实现一个server和cline通过命名管道发送命令,实现共享内存读取字符。
代码如下:
makefile:
cpp
clien.exe:clien.cc
g++ -E $^ -o clien.i
g++ -S clien.i -o clien.s
g++ -c clien.s -o clien.o -std=c++11
g++ clien.o -o $@ -I ./
server.exe:server.cc
g++ -E $^ -o server.i
g++ -S server.i -o server.s
g++ -c server.s -o server.o -std=c++11
g++ server.o -o $@ -I ./
libmycomm.so:comm.hpp
g++ -c -fPIC $^ -o comm.o
g++ -shared comm.o -o $@
.PHONY:clean exe1 exe2
clean:
rm -rf *.i *.s *.o *.exe .hpp.* myfifo
exe1:
make clien.exe
./clien.exe
exe2:
make server.exe
./server.exe
cpp
#include"comm.hpp"
int main(int argc, char* argv[], char* env[])
{
Myfifo fifo("./",FIFO_FILE);
fifo.OpenFifo(O_RDONLY);
MemorySharing memory(pathname.c_str(), gid, size, shmid, USER);
//查看共享内存是否存在
memory.GetMemory(IPC_CREAT);
//挂接
memory.MemoryLink();
//获取数据
char* arr = reinterpret_cast<char*>(memory.StartAddress());
while(true)
{
if(fifo.ReadFifo())//判断是否读取到文件末尾
cout << arr << endl;
else
break;
}
fifo.Close();
return 0;
}
cpp
#include"comm.hpp"
int main(int argc, char* argv[], char* env[])
{
//创建管道
Myfifo fifo("./",FIFO_FILE);
fifo.CreateFifo();
fifo.OpenFifo(O_WRONLY);
//创建共享内存
MemorySharing memory(pathname.c_str(), gid,size, shmid, CREATER);
memory.CreateMemory(IPC_CREAT | IPC_EXCL | 0666);
//挂接
memory.MemoryLink();
//写入数据
char* arr = reinterpret_cast<char*>(memory.StartAddress());
for(int i = 0 ;i < memory.Size(); i++)
{
arr[i] = 'a' + i;
cout << arr[i] << endl;
sleep(1);
//写入为偶数,发送消息到管道
if(i % 2 == 0)
fifo.WriteFifo();
}
fifo.Close();
memory.GetMemoryStruct();
//删除共享内存
// memory.Destroy();
return 0;
}
comm.hpp
cpp
#pragma once
#include<iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include<string.h>
#include<stdio.h>
#include<unistd.h>
#include <sys/stat.h>
#include<fcntl.h>
#include<functional>
#define FIFO_FILE "myfifo"
using us = std::string;
#define CREATER "creater"
#define USER "user"
#define ERR_EXIT(m) do { cerr << m << endl; exit(EXIT_FAILURE);} while(0)
using namespace std;
const int gid = 10;
const int size = 10;
const int shmid = -1;
const string pathname = "./makefile";
class MemorySharing
{
private:
void Create(int flg)
{//创建共享内存
_shmid = shmget(_key, _size, flg);
if(_shmid < 0)
ERR_EXIT("shmget error");
cout << _shmid << endl;
}
public:
MemorySharing(const char* path, int gid,const size_t& size,int shmid, const string & user)
:_shmid(shmid)
,_size(size)
,_usertype(user)
{
_key = ftok(path, gid);
if(_key < 0)
ERR_EXIT("ftok error");
cout << "_key: " <<_key << endl;
}
~MemorySharing()
{
UnlinkMemory();
Destroy();
}
void CreateMemory(int flg)
{
Create(flg);
}
void Destroy()
{
if(_shmid == shmid)
ERR_EXIT("memory destroy error,_shmid is -1");
if(shmctl(_shmid,IPC_RMID,nullptr)!= 0)
ERR_EXIT("memory destroy error");
}
void GetMemoryStruct()
{
struct shmid_ds ds;
shmctl(_shmid,IPC_STAT, &ds);
cout << ds.shm_segsz <<endl;
cout << ds.shm_perm.__key << endl;
}
void MemoryLink()
{
_start_memory = shmat(_shmid, nullptr, 0);
if(reinterpret_cast<long long>(_start_memory) < 0 )
ERR_EXIT("MemoryLink error");
cout << _start_memory << endl;
}
void GetMemory(int flg)
{
Create(flg);
}
int Size()
{
return _size;
}
void *StartAddress()
{
return _start_memory;
}
void UnlinkMemory()
{
if(shmdt(_start_memory) != 0)
ERR_EXIT("UnlinkMemory error ");
cout << "UnlinkMemory success" << endl;
}
private:
int _shmid;//共享内存id
size_t _size;//共享内存的大小
void* _start_memory;//映射后的虚拟起始地址
key_t _key;//key值
string _usertype;//用户类型
};
class Myfifo
{
public:
Myfifo(const string& path, const string& name)
:_path(path)
,_name(name)
,_fd(-1)
{
}
void CreateFifo()
{
umask(0);
int fd = mkfifo((_path + _name).c_str(), 0666);
if( fd != 0)
{
cerr << _name << " error" << endl;
}
else
cout << _name << " creat success" << endl;
}
void OpenFifo(int mode)
{
_fd = open((_path + _name).c_str(), mode);
if( _fd < 0)
{
cerr << "open FIFO_FILE error" << endl;
return ;
}
cout << _name << " open success" << endl;
}
void WriteFifo()
{
int num = 1;
if(write(_fd, &num, sizeof(int)) < 0)
ERR_EXIT("WriteFifo error ");
}
bool ReadFifo()
{
int cut = 0;
int n = read(_fd, &cut, sizeof(int));
if(n > 0)
{
cout << "数据为: " << cut << ", message number ----" << "[" << getpid() << "]" << endl;
return true;
}
else if (n == 0)
cout << "文件末尾" << endl;
else
ERR_EXIT("ReadFifo error ");
return false;
}
void Close()
{
close(_fd);
_fd = -1;
}
~Myfifo()
{
if(unlink((_path + _name).c_str()) == 0)
{
cout << "remove fifo success" << endl;
}
else
cout << "remove fifo error" << endl;
}
private:
string _path;
string _name;
int _fd;
};
效果如下:

我们可以在总结出:
- 共享内存是进程间通信速度最快的,
- 映射后,数据可以立刻被看见 ,没有同步机制,没有保护机制
- 不需要进行系统调用,用户直接使用
查看创建的所有共享内存命令如下:
bash
ipcs -m
如图:

如果要删除对于的共享内存,命令如下:
bash
ipcrm -m 共享内存id
如图:

如果没有删除的话,会一直存在。也就是声明周期的随内核
注意:操作系统开辟的内存往往是4kb的整数倍,也就是4096字节的整数倍,能真正使用的就是我们写明的,多出来的就会被浪费掉
除此之外,共享内存是可以双向通信的,
system v 消息队列
原理:

操作系统里面会有一个队列,进程AB两个进行通信的时候,例如。当进程A发送一个信息。就会把这个消息设置为类型1,然后形成一个队列节点链接到这个队列中,当进程B发送一个信息。就会把这个消息设置为类型2,然后形成一个队列节点也链接到这个队列中,然后双方就可以根据这个类型判断是哪个进程发送的,从而进行操作,这个队列叫做消息队列 。
由于这些都是操作系统进行操作的,我们需要用的话,需要调用其中的系统调用。
创建消息队列msgget:
cpp
int msgget(key_t key, int msgflg);
key : 某个消息队列的名字
• msgflg :由九个权限标志构成,它们的⽤⽤法和创建⽂⽂件时使⽤⽤的mode模式标志是⼀⼀样
返回的值
取值为IPC_CREAT:消息队列不存在,创建并返回;消息队列已存在,获取并返回。
取值为IPC_CREAT | IPC_EXCL:消息队列不存在,创建并返回;消息队列已存在,出错返回。保证消息队列是全新的,如果单独使用IPC_EXCL无意义。如果要设置权限的话,跟文件类似,直接拼接,
• 成功返回⼀个⾮负整数,即该消息队列的标识码;失败返回-1
删除消息队列或者更改msgctl:
cpp
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msgid : 由 msgget 函数返回的消息队列标识码
• cmd :将要采取的动作(有三个可取值)
返回值
• 成功返回0;失败返回-1
cmd的参数是:

msqid_ds就是消息队列的结构,如图:

ipc_perm的结构如下:

在消息队列中,不再是映射,而是发送消息和获取消息,对应的系统调用接口如下:
发送消息接口msgsnd:
cpp
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msgid : 由 msgget 函数返回的消息队列标识码
• msgp:是⼀个指针,指针指向准备发送的消息
• msgsz:是msgp指向的消息⻓度,这个⻓度不含保存消息类型的那个long int⻓整型
msgflg:控制着当前消息队列满或到达系统上限时将要发⽣⽣的事情, 0即可
( msgflg=IPC_NOWAIT 表⽰⽰队列满不等待,返回 EAGAIN 错误 )。
返回值
• 成功返回0;失败返回-1
关于参数msgp,传入的是一个结构体地址,这个结构体,如下:
cpp
struct msgbuf
{
long mtype; /* message type, must be > 0 */
char mtext[n]; /* message data */
};
// 以⼀个long int⻓整数开始,接收者函数将利⽤⽤这个⻓整数确定消息的类型
这个消息结构体需要我们先定义,其中的n可以自己定义,
获取消息系统调用为msgrcv:
cpp
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
msgid : 由 msgget 函数返回的消息队列标识码
• msgp :是⼀个指针,指针指向准备接收的消息
• msgsz :是 msgp 指向的消息⻓度,这个⻓度不含保存消息类型的那个long int⻓整型
• msgtype :它可以实现接收消息的类型,也可以模拟优先级的简单形式进⾏接收
• msgflg :控制着队列中没有相应类型的消息可供接收时将要发⽣⽣的事
返回值
• 成功返回实际放到接收缓冲区⾥⾥去的字符个数,失败返回 -1
代码如下:
查看当前的消息队列:
bash
ipcs -q
删除消息队列的命令如下:
bash
ipcrm -q 队列id
注意:支持全双工通信
system v 信号量
在写之前,先引入一些知识,
-
多个执⾏流(进程), 能看到的同⼀份公共资源:共享资源
-
被保护起来的共享资源叫做临界资源 ,比如命名管道里面的内核文件缓冲区
如图:

-
临界区:指访问临界资源的代码
-
非临界区: 没有访问临界资源的代码
-
保护临界区的代码就是保护临界资源,
-
保护临界资源的方式有两种:互斥和同步
-
任何时刻,只允许⼀个执⾏流(进程)访问临界资源,叫做互斥
-
多个执⾏流,访问临界资源的时候,具有⼀定的顺序性 ,叫做同步,例如上面的共享内存的代码,就是通过cline进程写入两个字符后。server进程再进行获取和输出,这就是一个同步。
-
系统中某些资源⼀次只允许⼀个进程使⽤,称这样的资源为临界资源或互斥资源。

-
原子性就是:一个操作要么全部执行成功,要么完全不执行,不存在"执行到一半"的中间状态。
下面开始进入信号:
信号量 又叫信号灯,本质就是一个计数器,表示临界资源中,可用资源的数量
以电影院为例子:

当购买票之后,这个座位就是购买者的了,如果把电影院比作临界资源,座位就是临界资源中的一个个小资源部分,临界资源就是座位的总和。这个信号量就是空座位之数,访问临界资源就相当于买票,就要申请信号量 ,本质 就是资源的预定,这个信号量就要减少。当信号量小于0,访问者就会被阻塞
信号量本身就是一个共享资源 ,当信号量进行减少时,必须保证原子性 ,这个操作叫P操作 , 而信号量增加也要保证原子性,这个操作叫做V操作
信号量只有0或者1的叫做二元信号量,本质就是互斥
后面我们使用临界资源,要么是整体使用,要么是分成多块使用,当整体使用的话,我们可以定义这个信号量为二元,如果是分成多块使用,就是多元信号量了。
说了这么多,为啥信号量会作为通信的范畴呢?
首先, 进行通信的前提是要看到同一块资源,不同的进程申请信号量,就是要先看到同一个信号量,并不是只有传递数据才是通信,同步和互斥也是通信。传递信息也是一种通信,
信号量的相关接口如下:
创建信号量:
cpp
int semget(key_t key, int nsems, int semflg);
key: 信号量集的键值,同消息队列和共享内存
• nsems: 信号量集中信号量的个数,注意不是临界资源的资源数量,而是信号量的数量,也就是临界资源的数量
• semflg: 同消息队列和共享内存,IPC_CREAT | IPC_EXCL
返回值:错误返回-1
这个函数创建的都是信号量集。
删除和修改信号量:
cpp
int semctl(int semid, int semnum, int cmd, ...);
semid: 由 semget 返回的信号集标识码
• semnum: 信号集中信号量的序号
cmd :传入IPC_RMID ,删除 传入SETVAL,就是初始化信号量下标为semnum的信号量
当cmd设置为SETVAL时,需要我们传入一个union semum,这个需要我们定义:如下:
cpp
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
如果设置为 IPC_STAT 或者 IPC_SET ,跟前面我们共享内存和消息队列是一样的,需要,传入一个结构体,这个就是信号量的结构体, 如下:
cpp
struct semid_ds {
struct ipc_perm sem_perm; /* Ownership and permissions */
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Last change time */
unsigned long sem_nsems; /* No. of semaphores in set */
};
struct ipc_perm {
key_t __key; /* Key supplied to semget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions */
unsigned short __seq; /* Sequence number */
};
创建和初始化不是原子性的,进行P和V操作是原子性的
操作信号量:
cpp
int semop(int semid, struct sembuf *sops, size_t nsops);
semid: 由 semget 返回的信号集标识码
• sops: 一个要进行操作的信号量集
nsops:一个要进行操作的信号量集中的数量
其中nsops中的元素结构体如下:
cpp
struct sembuf
{
unsigned short sem_num; /* semaphore number */ /* Index into the semaphore set (i.e., the nsems argument of semget) */
short sem_op; /* semaphore operation: -1 for P operation, +1 for V operation */
short sem_flg; /* operation flags */ /* Set to 0 */
}
```V操作
*/
short sem_flg; /* operation flags */设置为0就行
}
这个不需要我们定义
OS是如何组织管理IPC资源的
通过观察,可以知道共享内存、消息队列以及信号量,都是通过key来区分唯一性,被当作成一种资源,这个资源就是system v,下面提供一个图:

在Linux中,会存在一个全局的IPC资源叫ipc_ids,里面有一个指针,指向一个ipc_id_ary的结构体,这个结构体采用如下的结构方式:
cpp
struct xxx
{
int size;
ipc_perm arr[];//柔性数组
}
图中的msg_queue、sem_array以及shmid_kerne就是消息队列、信号量以及共享内存的结构体,这些结构体的第一个成员就是ipc_per结构体。
在ipc_id_ary的柔性数组中,存储的就是ipc_per结构体对象,当我们使用系统调用创建shmget、semget、msgget对应返回的数字就是这个柔性数组的下标,当操作系统从这个数组获取到地址买就会把这个地址强转为对应资源类型的地址,这样这个地址就可以访问其他的成员,如下:

把ipc_perm类型强转成msg_queue、sem_array以及shmid_kerne等结构体指针**,这就是C语言版的多态**
下面我们看一看Linux2.6.18的源代码里面的ipc_ids的结构:
cpp
struct ipc_ids {
int in_use;
int max_id;
unsigned short seq;
unsigned short seq_max;
struct mutex mutex;
struct ipc_id_ary nullentry;
struct ipc_id_ary* entries;
};
ipc_id_ary的如下:
cpp
struct ipc_id_ary {
int size;
struct kern_ipc_perm *p[0];
};
kern_ipc_perm的结构如下:
cpp
struct kern_ipc_perm
{
spinlock_t lock;
int deleted;
key_t key;
uid_t uid;
gid_t gid;
uid_t cuid;
gid_t cgid;
mode_t mode;
unsigned long seq;
void *security;
};
msg_queue 消息队列结构如下:
cpp
struct msg_queue {
struct kern_ipc_perm q_perm;
int q_id;
time_t q_stime; /* last msgsnd time */
time_t q_rtime; /* last msgrcv time */
time_t q_ctime; /* last change time */
unsigned long q_cbytes; /* current number of bytes on queue */
unsigned long q_qnum; /* number of messages in queue */
unsigned long q_qbytes; /* max number of bytes on queue */
pid_t q_lspid; /* pid of last msgsnd */
pid_t q_lrpid; /* last receive pid */
struct list_head q_messages;
struct list_head q_receivers;
struct list_head q_senders;
};
sem_array 信号量集结构如下:
cpp
struct sem_array {
struct kern_ipc_perm sem_perm; /* permissions .. see ipc.h */
int sem_id;
time_t sem_otime; /* last semop time */
time_t sem_ctime; /* last change time */
struct sem *sem_base; /* ptr to first semaphore in array */
struct sem_queue *sem_pending; /* pending operations to be processed */
struct sem_queue **sem_pending_last; /* last pending operation */
struct sem_undo *undo; /* undo requests on this array */
unsigned long sem_nsems; /* no. of semaphores in array */
};
共享内存的结构:
cpp
struct shmid_kernel /* private to the kernel */
{
struct kern_ipc_perm shm_perm;
struct file * shm_file;
int id;
unsigned long shm_nattch;
unsigned long shm_segsz;
time_t shm_atim;
time_t shm_dtim;
time_t shm_ctim;
pid_t shm_cprid;
pid_t shm_lprid;
struct user_struct *mlock_user;
};
可以看到共享内存的结构里面有一个file 结构体,这是因为创建共享内存,会创建一个只存在于内存中的虚拟文件。当使用系统调用shmat,,对应进程的PCB中的进程地址空间mm_struct就会创建一个vm_area_struct对象,里面有该区的起始地址和结束地址,还有一个struct file * vm_file;,这个存储的就是这个文件file地址,
mmap的文件映射(共享内存的另一种)
基本原理就是,把打开的文件,映射到进程地址空间里,不再使用OS提供的系统调用接口read和write进行操作,直接在进程地址空间里面进行操作,
共享内存就是在内存开辟一块空间进行映射,而mmap就是打开一个文件,通过文件的内核缓冲区映射的。
如图:

下面就是相关接口:
映射文件mmap:
cpp
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
void *addr :⼀个提⽰地址,表⽰希望映射区域开始的地址。然⽽,这个地址可能会被内核忽
略,特别是当我们没有⾜够的权限来请求特定的地址时。如果 addr 是 NULL ,则系统会⾃动选
择⼀个合适的地址。
size_t length : 要映射到进程地址空间中的字节数。这个⻓度必须是系统⻚⾯⼤⼩的整数倍
(通常是 4KB ,但可能因系统⽽异)。如果指定的 length 不是⻚⾯⼤⼩的整数倍,系统可能会
向上舍⼊到最近的⻚⾯⼤⼩边界(系统内存⻚⼤⼩为4KB(即4096字节),⽽请求的内存⼤⼩为
3500字节,则按照向上舍⼊的原则,应分配4096字节的内存)
int prot : 指定了映射区域的内存保护属性。可以是以下值的组合(使⽤按位或运算符 | ):
◦ PROT_READ :映射区域可读。
◦ PROT_WRITE :映射区域可写。
◦ PROT_EXEC :映射区域可执⾏。
int flags : 指定了映射的类型和其他选项
◦ MAP_PRIVATE :创建⼀个私有映射。对映射区域的修改不会反映到底层⽂件中。
◦ MAP_SHARED :创建⼀个共享映射。对映射区域的修改会反映到底层⽂件中(前提是⽂件是
以写⽅式打开的,并且⽂件系统⽀持这种操作)。
◦ 其他选项(如 MAP_ANONYMOUS 、 MAP_ANONYMOUS_SHARED 等)可能也存在于某些系统
上,⽤于创建不与⽂件关联的匿名映射。
int fd : ⼀个有效的⽂件描述符,指向要映射的⽂件或设备。对于匿名映射,这个参数可以
是 -1 (在某些系统上,也可以使⽤ MAP_ANONYMOUS 或 MAP_ANON 标志来指定匿名映射,此
时 fd 参数会被忽略)
• off_t offset : ⽂件中的起始偏移量,即映射区域的开始位置。 offset 和 length ⼀起定
义了映射区域在⽂件中的位置和⼤⼩。,如果为0,则文件的起始位置开始映射
返回值:
成功返回映射区的起始地址,错误返回 -1
解除映射关系:
cpp
int munmap(void *addr, size_t length);
addr:mmap的返回值
length:映射的长度
返回值:成功返回0 .失败返回-1
写一个server读取,cline写入的mmap代码:
makefile:
bash
server.exe:server.cpp
g++ -E $^ -o server.i -std=c++11
g++ -S server.i -o server.s
g++ -c server.s -o server.o
g++ server.o -o server.exe
cline.exe:cline.cc
g++ -E $^ -o cline.i -std=c++11
g++ -S cline.i -o cline.s
g++ -c cline.s -o cline.o
g++ cline.o -o cline.exe
.PHONY:clean exe1 exe2
clean:
rm -rf *i *.o *.s *.exe
exe1:
make server.exe
./server.exe
exe2:
make cline.exe
./cline.exe
mmap.hpp:
cpp
#include<iostream>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<cstdio>
#include <unistd.h>
using namespace std;
#define PATHNAME "./text.txt"
#define ERR_ER(m) \
do \
{ \
perror(m); \
exit(1);\
} while(0)
#define FILESIZE 100
class MyMmap
{
public:
MyMmap(int fd)
: _fd(fd)
, _statr_adders(nullptr)
{
cout << fd << endl;
}
void LinkMmap(int prot)
{
//调整文件大小
if(ftruncate(_fd, FILESIZE) == -1)
ERR_ER("ftruncate error");
void *adders = mmap(nullptr,FILESIZE,prot, MAP_SHARED,_fd,0);
if( adders == MAP_FAILED)
ERR_ER("mmap error");
_statr_adders = adders;
cout << _statr_adders << endl;
}
void RemoveMmap()
{
if(munmap(_statr_adders, FILESIZE) != 0)
ERR_ER("munmap error");
}
void WriteFile()
{
char * arr = reinterpret_cast<char*>(_statr_adders);
for(int i = 0; i < FILESIZE; i++)
{
arr[i] = 'a' + (i % 26);
cout << arr[i] << endl;
}
}
void ReadFile()
{
cout << _statr_adders << endl;
char * arr = reinterpret_cast<char*>(_statr_adders);
cout << arr << endl;
for(int i = 0; i < FILESIZE; i++)
{
cout << arr[i];
}
}
~MyMmap()
{
RemoveMmap();
}
private:
int _fd;
void* _statr_adders;
};
:
cpp
#include"mmap.hpp"
int main(int argc, char* argv[], char *env[])
{
umask(0);
int fd = open(PATHNAME, O_RDWR | O_CREAT | O_TRUNC, 0666);//注意,使用mmap的时候,文件一定要读写打开,否则就会权限不足
if(fd < 0)
ERR_ER("open error");
cout << " open success" << endl;
MyMmap map(fd);
map.LinkMmap(PROT_WRITE);//写入
map.WriteFile();
close(fd);
return 0;
}
server.cpp
cpp
#include"mmap.hpp"
int main(int argc, char* argv[], char *env[])
{
umask(0);
int fd = open(PATHNAME, O_RDWR);
if(fd < 0)
ERR_ER("open error");
cout << " open success" << endl;
MyMmap map(fd);
map.LinkMmap(PROT_READ);//读取
map.ReadFile();
close(fd);
return 0;
}
效果如下:
