Linux进程间通信
1.理解层面


为什么?
匿名管道:
1.背景
子进程不需要把父进程的文件再加载一遍。
子进程共享父进程的文件,所以父进程和子进程的内容打在同一个显示器上面。
:文件共享,标准输入,标准输出,标准错误,文件。
把父子看到的同一份资源叫做管道

子进程拷贝父进程的文件描述符,这样子进程也可以通过文件描述符表来进一步访问文件,而不是父进程拷贝的文件再拷贝一份。
2.原理

创建管道:
父进程来两个一个进行读,一个进行写,
子进程拷贝父进程,
当父子进程进行通信时,
各自关闭一个相对的文件。
如:父进程关闭读,子进程关闭写,那么,父进程就可以写文件,子进程就可以读文件。
pipe,创建pipe,创建管道文件。
创建pipe不需要路径,是内存级的,没有文件名,所以是匿名管道。怎么保证两个进程打开的是同一个文件的?
子进程机场了父进程的文件描述符表。
struct file 是被拷贝了的,但是实际上的文件没有拷贝。

3.demo代码,测试接口

0,1,2内占用了,stdin,stdout,stderr所以就是3和4
模拟管道通信
cpp
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.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++);
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)
{
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)
{
// child
// code
// 3. 关闭不需要的读写端,形成通信信道
// f -> r, c -> w
close(fds[0]);
ChildWrite(fds[1]);
close(fds[1]);
exit(0);
}
// 3. 关闭不需要的读写端,形成通信信道
// f -> r, c -> w
close(fds[1]);
FatherRead(fds[0]);
waitpid(id, nullptr, 0);
close(fds[0]);
return 0;
}
因为snprintf把格式化的字符串 放到数组中的时候有末尾添加'\0' ,
read读取,不需要读取'\0',所以要-1
-1那个位置就是给\0流的空间
例子:
读取的最大字节数要-1,也就是如果buff[1024],如果rfd里的内容超过了1024,那就只能最大读入1023,剩下一个给\0,后面的再以此继续读入。

4.管道的特性

1.管道式面向字节流的。
怎么读和怎么写,没有关系。读的内容可能不完整。
2.正常情况下,一个子进程向显示器里写,父进程也往显示器上写入,子进程写的慢,会不会影响父进程呢?
答:互不影响。各跑各的。显示器统一缓冲区刷屏。
子进程每3秒写一条,那么父进程也只能每3秒读一条,没有就等。
写一条,读一条,有我就读,没有我就等,一个进程等一个进程->管道文件的同步机制。
子进程一直在写,父进程每3秒读一次。写的慢,读得慢

单次读的数据取决于缓冲区的大小
写得慢,读得快

3.管道式单向通信的。-->属于半双工的一种特殊情况。
任何一个是个,一个发,一个受 --- 半双工,!=单向通信
任何一个是库额,可以同时发收 --- 全双工
4.管道文件的生命周期是随进程的。管道美观,进程退了,纳闷操作系统自动刷新。管道消失。
5.种通信情况
1.写慢,读快 --- 读端就要阻塞(进程阻塞)
√
2.写快,读慢 --- 满了的时候,那么写就要阻塞。
√
3.写关,继续读 --- 写端写了一条就直接走了,父进程继续读,read就会督导返回值为0,表示读到文件结尾。

4.读关闭,写继续 --- 子进程写入,父进程读关闭。写端再写入没有任何意义,OS不会做没有意义的事情

cpp
// 3. 关闭不需要的读写端,形成通信信道
// f -> r, c -> w
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);
}

cpp// 3. 关闭不需要的读写端,形成通信信道 // f -> r, c -> w 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); }
定义了一个整型变量
status,用于存储子进程的退出状态信息。调用
waitpid函数,等待进程ID为id的子进程结束。
&status是status的地址,用于存储子进程的退出状态。第三个参数
0表示阻塞等待子进程结束。
waitpid函数的返回值ret表示:
如果成功获取子进程的退出状态,则返回子进程的PID。
如果没有子进程,则返回
-1,并设置errno。
status>>8)&0xFF:获取子进程的退出代码(exit code)。如果子进程是正常退出的(通过exit或_exit函数),这个值就是子进程传递给exit函数的参数。
status&0x7F:获取导致子进程退出的信号编号(exit signal)。如果子进程是被信号终止的,这个值就是终止子进程的信号编号。


匿名管道-进程池的编写

父进程 管道 子进程
父进程通过向管道内写,子进程从管道读,通过这种形式实现对子进程的暂停和唤醒。
进程池:现有进程,后有任务,不需要有任务再创建子进程 --- 这种就叫做池化技术。

进程池代码编写
Makefile
cpp
testPipe:testPipe.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f testPipe
Main.cc
cpp
#include "ProcessPool.hpp"
int main()
{
// 这个代码,有一个藏得比较深的bug --- TODO
// 创建进程池对象
ProcessPool pp(gdefaultnum);
// 启动进程池
pp.Start();
// 自动派发任务
int cnt = 10;
while(cnt--)
{
pp.Run();
sleep(1);
}
// 回收,结束进程池
pp.Stop();
return 0;
}
Processpool.hpp
cpp
#ifndef __PROCESS_POOL_HPP__
#define __PROCESS_POOL_HPP__
#include <iostream>
#include <cstdlib> // stdlib.h stdio.h -> cstdlib cstdio
#include <vector>
#include <unistd.h>
#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);
}
~Channel()
{
}
//写入数据
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;
}
int Fd() { return _wfd; }
pid_t SubId() { return _subid; }
std::string Name() { return _name; }
private:
int _wfd;
pid_t _subid;
std::string _name;
// int _loadnum;
};
// 在组织,管道管理
class ChannelManager
{
public:
ChannelManager() : _next(0)
{
}
void Insert(int wfd, pid_t subid)
{
_channels.emplace_back(wfd, subid);
// Channel c(wfd, subid);
// _channels.push_back(std::move(c));
//_channels.push_back(c);
}
//按照轮询选择一个管道执行任务
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));
//如果文件中有足够的数据,read函数会将文件中的sizeof(code)个字节读取到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//-1错误
{
std::cout << "读取错误" << std::endl;
break;
}
}
}
bool Start()
{
for (int i = 0; i < _process_num; i++)
{
// 1. 创建管道
int pipefd[2] = {0};
int n = pipe(pipefd);
if (n < 0)
return false;
// 2. 创建子进程
pid_t subid = fork();
if (subid < 0)
return false;
else if (subid == 0)
{
// 子进程
// 3. 关闭不需要的文件描述符
close(pipefd[1]);
Work(pipefd[0]);
close(pipefd[0]);
exit(0);
}
else
{
// 父进程
// 3. 关闭不需要的文件描述符
close(pipefd[0]); // 写端:pipefd[1];
_cm.Insert(pipefd[1], subid); //ChannelManger插入一个Channel每新建一个进程
// wfd, subid
}
}
return true;
}
void Debug()
{
_cm.PrintChannel();
}
void Run()
{
// 1. 选择一个任务
int taskcode = _tm.Code();
// 2. 选择一个信道[子进程],负载均衡的选择一个子进程,完成任务
auto &c = _cm.Select();
std::cout << "选择了一个子进程: " << c.Name() << std::endl;
// 3. 发送任务
c.Send(taskcode);
std::cout << "发送了一个任务码: " << taskcode << std::endl;
}
void Stop()
{
// 关闭父进程所有的wfd即可
_cm.StopSubProcess();
// 回收所有子进程
_cm.WaitSubProcess();
}
~ProcessPool()
{
}
private:
ChannelManager _cm;
int _process_num;
TaskManager _tm;
};
#endif
Task.hpp
cpp
#pragma once
#include <iostream>
#include <vector>
#include <ctime>
typedef void (*task_t)();//函数指针
////////////////debug/////////////////////
void PrintLog()
{
std::cout << "我是一个打印日志的任务" << std::endl;
}
void Download()
{
std::cout << "我是一个下载的任务" << std::endl;
}
void Upload()
{
std::cout << "我是一个上传的任务" << std::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;
};
fork()补充知识
fork()用于创建子进程,有多个返回值,
== 0 的时候表示,正在处于子进程程序中
> 0 表示创建子进程成功,返回给父进程pid
也就是创建成功后,父进程也开始执行
bug
问题:
子进程拷贝了父进程的写端,当关掉父的写端时,但管道并没有结束,因为写端并没有被完全关掉,而且还有下面的子进程拷贝父进程的写端指向该管道,所以指向管道的写端的引用计数会逐渐增多
解决方案:
一、
倒着关闭。最后一个写端只有父进程,关闭最后一个,以上所有的子进程的写端也就陆续少一个。
以此类推,每次的最后一个管道都只有最后一个写端--父进程,使得只有一个进程指向该管道 --- 关闭成功。
二、让子进程拷贝父进程的文件描述符表后,关闭所有的写端。这样就可以避免其余的写端指向管道。真的让父进程一个人指向所有管道w端
子进程拿到父进程的管道写端,就给他关掉。
我们子进程让关闭自己继承下来的。他各个进程
在子进程之前,关掉父进程之前的写端
会不会把父进程自己的写端关掉?不会,因为有写时拷贝,子进程拿到的vector是之前的写端
子进程创建,要拷贝自父进程的文件描述符表,由于是写时拷贝,子进程下的写时拷贝,关掉拿到父进程的文件描述符表,对于父进程没有任何影响
关掉和回收
关掉当前子进程的哥哥的所有文件描述符表即可
命名管道
管道应⽤的⼀个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
• 如果我们想在不相关的进程之间交换数据,可以使⽤FIFO⽂件来做这项⼯作,它经常被称为命名 管道。
• 命名管道是⼀种特殊类型的⽂件
4-1 创建⼀个命名管道 2_24
命名管道可以从命令⾏上创建,命令⾏⽅法是使⽤下⾯这个命令:
cpp
mkfifo filename
命名管道也可以从程序⾥创建,相关函数有
cpp
int mkfifo(const char *filename,mode_t mode);
创建命名管道:
cpp
int main(int argc, char *argv[])
{
mkfifo("p2", 0644);
return 0;
}
4-2 匿名管道与命名管道的区别
• 匿名管道由pipe函数创建并打开。
• 命名管道由mkfifo函数创建,打开⽤open
• FIFO(命名管道)与pipe(匿名管道)之间唯⼀的区别在它们创建与打开的⽅式不同,⼀但这些 ⼯作完成之后,它们具有相同的语义。在
4-3命名管道的打开规则
• 如果当前打开操作是为读⽽打开FIFO时
◦ O_NONBLOCK disable:阻塞直到有相应进程为写⽽打开该FIFO
◦ O_NONBLOCK enable:⽴刻返回成功
• 如果当前打开操作是为写⽽打开FIFO时
◦ O_NONBLOCK disable:阻塞直到有相应进程为读⽽打开该FIFO
◦ O_NONBLOCK enable:⽴刻返回失败,错误码为ENXIO
进程A和B打开同一份文件 :

进程A打开文件:
创建一个文件描述符表,创建一个struct file,通过路径dentry找到inode属性,inode属性找到映射关系找到文件内容,磁盘上要把内容加载到缓冲区里面, 缓冲区刷新,拿到文件内容。
进程B打开文件:
不是指向它,而是自己再创建一个struct file 通过路径找inode发现已经被打开,
没必要再加载一份已经打开过的文件到内存里,
struct file指向inode,再指向同一个文件缓冲区。
让不同的进程看到同一份资源。->通过打开同一个路径下的同一个文件。->文件有路径,有名字->路径具有唯一性
|
命名管道。
管道文件,只会被打开,不需要刷新
mkfifo创建一个命名管道:
mkdifo 名字:
cilent.cc,打开
sever.cc,很多很多操作

命名管道通信代码:
server.cc:读取文件
cpp
#include "comm.hpp"
int main()
{
// 创建管道文件
NamedFifo fifo("/", FILENAME);
// 文件操作了
FileOper readerfile(PATH, FILENAME);
readerfile.OpenForRead();
readerfile.Read();
readerfile.Close();
return 0;
}
client.cc:写文件
cpp
#include "comm.hpp"
int main()
{
FileOper writerfile(PATH, FILENAME);
writerfile.OpenForWrite();
writerfile.Write();
writerfile.Close();
return 0;
}
comm.hpp:
cpp
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define PATH "."
#define FILENAME "fifo"
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
class NamedFifo
{
public:
NamedFifo(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;
}
}
~NamedFifo()
{
// 删除管道文件
int n = unlink(_fifoname.c_str());
if (n == 0)
{
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()
{
// 打开, write 方没有执行open的时候,read方,就要在open内部进行阻塞
// 直到有人把管道文件打开了,open才会返回!
_fd = open(_fifoname.c_str(), O_RDONLY);
if (_fd < 0)
{
ERR_EXIT("open");
}
std::cout << "open fifo success" << std::endl;
}
void OpenForWrite()
{
// write
_fd = open(_fifoname.c_str(), O_WRONLY);
if (_fd < 0)
{
ERR_EXIT("open");
}
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()
{
// 正常的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::cerr << "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;
};
宏,退出的时候,打印错误消息,并退出
\续行
代码块级别的替换
共享内存 - SystemV2_24
system V:是一种标准,IPC通信模块,通信接口设计,原理,接口,相似性
IPC本质:
让不同的进程先看到同一份资源
共享内存 - SystemV
1.共享内存的原理
进程在自己的堆栈内申请好空间(虚拟地址空间),然后把物理内存的物理地址空间和虚拟地址空间通过页表进行映射即可,左侧提供虚拟地址,把物理内存通过页表映射到自己的虚拟地址空间。
通过对虚拟地址的起始地址访问共享内存

1.申请空间...都是由操作系统自己完成的,操作系统系统系统调用,我们用系统调用完成上面的工作。
2.关闭:进程A把自己申请的虚拟地址空间free掉,再把页表清除掉
(取消关联关系,释放内存)
进程申请1024个字节,那么操作系统会不会立马给我们1024个字节,只是虚拟地址空间给上1024个字节
页表映射关系此时还没有建立。
3.可能同时存在有多组进程,都在使用不同的共享内存。
有的正使用,有的新建,有的关联两个....
4.要不要管理共享内存?怎么管理
先描述 再组织
语言:共享内存的内核结构体对象+物理内促
进程和共享内存的关系就是内核数据结构之间的关系。
共享内存是一定有储存共享内存信息的结构体,对内存的管理转化成对结构体的管理
引用计数:共享内存关联几个进程,为0时,直接关掉。
1.shmget:
系统调用,系统当中创建一个共享内存
size->大小,
shmflg->标记位,
**IPC_T:**创建共享内存,如果目标内存不存在,就创建,否则,打开这个已经存在的共享内存,并返回
IPC_EXCL|PC_CREA:(单独使用,无意义!):如果你呀创建的shm不存在,就常见它,如果已经存在shmget就会出错返回
只要shmaget成功返回,一定是一个全新的共享内存

shmget返回值:
成功 -> 整数:作为共享内存的标识符,->共享内存的唯一性,
两个唯一值,一个shmget,??用户来用,一个key,用户传来一个 key。
我们怎么评估共享内存存在于否???又怎么保证两个不同的进程,拿到的就是同一个共享内存呢?
2.key_t key:
不同的进程,使用shm来通信,必须标识共享内存的唯一性,
唯一性由key来区分,不是内核直接形成,而是在用户层,构建并传入给OS
id:共享内存
id由A交给B - >AB之间能通信 -> AB之间约定一个key, 把key设计到共享内存,
AB都可以通过key找到对应的共享内存。
~=命名管道,同一个路径的文件(唯一性)


key_t ftok():创建形成一个唯一的key

pthname和prejoid构建一个key值
key 用于标识共享内存段
shmget 是用来根据 key 获取或创建共享内存段的系统调用,返回一个标识符用于后续操作
代码:
comm.hpp
ipc -c:查看共享内存
ipcrm -m shmid:删除共享内存
创建共享内存,如果进程结束了,没有删除共享内存,共享内存资源会一直存在 --- 共享内存的资源,生命周期岁资源 ---没有显示的删除,即是进程退出了,IPC资源依旧被占用。
删除:
1.ipcs/ipcrm
2.代码级别的删除
共享内存不删那么共享内存就一直存在,知直到我们把操作系统重启。
问题:删除的时候,为什么只能使用shmid,而不是使用key?key未来只给内核来进行区分唯一性,需要用shmid进行管理共享内存。
//代码级别删除共享内存
![]()
共享内存id
cmd:命令IPC_RMID
共享内存的信息,存世不管,空nullptr
3.shmat:->将共享内存挂接到进程的地址空间中
并且返回起始虚拟地址,

shmid:映射哪一个共享内存
shmaddr:虚拟地址,固定地址进行挂接!
指定一个虚拟地址,在该虚拟地址处进行挂接。
实际上不现实,万一设置错了,那么就会出错现错误,操作系统清除,我们不清除,通常设置成nullptr即可,不能盲目设置。
shmflg:设置成0,默认设置,共享内存的权限问题
perms:权限问题。
失败 - 1
成功:起始虚拟地址
malloc申请连续虚拟地址空间。
shmat:申明虚拟地址空间 --- 申请共享内存实际上都是差不多的。
将共享内存挂接到进程的地址空间当中,并返回虚拟地址

返回起始虚拟地址:
问题1:我们计算机时64位的, 所以用longlong8个字节转成int变成4字节精度损失
问题2:共享内存权限问题共享内存没有权限,不让映射创建共享内存需要权限,当作文件来看
加上权限0666
Client.cc获取共享内存 - Get():

给client共享映射共享内存
缩写:没有key创建,有key就获取
思路:
server创建共享内存,设置key, 关联共享内存 -- 读
clinet获取共享内存,获取唯一标识key,关联共享内存 -- 写
关联 0 ~ 2 ~ 0 ~ 删除
管道属于文件级的内核文件缓冲区,必须用系统调用:

直接以指针地址的方式访问--->快

通信双方,没有所谓的同步机制。
可能读一半就跑了。
没有保护机制->所以也快
一个写一个读,两个毫不影响,随意读,读坏数据
命名管道->约束共享内存:
实现成双成对地出现AA BB CC。。。。。
![]()
共享内存的大小问题:
4096bytes
在内核中,共享内存在创建的时候,它的大小,必须是4kb(4096)的整数倍。
4097 - > 4096*2 ->向上4kb取整
查出来4097,:系统分配出来4096*2,你能用的只有4097.
接口:
shmdt:当前共享内存去关联
void Detach()
为什么?


子进程拷贝了父进程的写端,当关掉父的写端时,但管道并没有结束,因为写端并没有被完全关掉,而且还有下面的子进程拷贝父进程的写端指向该管道,所以指向管道的写端的引用计数会逐渐增多






共享内存不删那么共享内存就一直存在,知直到我们把操作系统重启。
















