✨个人主页:熬夜学编程的小林
💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】
目录
1、进程为什么要通信
进程也是需要某种协同的,然而协同的前提条件是通信(信息的传递与交换过程),因此进程需要通信。
事实:进程具有独立性,进程 = 内核数据结构 + 数据和代码,独立即内核数据结构独立 和 数据和代码独立。
进程间通信目的
数据传输 :一个进程需要将它的数据发送给另一个进程
资源共享 :多个进程之间共享同样的资源。
通知事件 :一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
2、进程如何通信
进程间通信,成本可能会稍微高一些,因此进程的独立性。
进程间通信的前提:先让不同的进程看到同一份(操作系统)资源("一段内存")。
1、一定是某一个进程先需要通信,然后让OS创建一块共享资源
2、OS必须提供很多的系统调用。
- OS创建的共享资源不同,系统调用接口也不同 ---- 进程间通信的种类也会有不同的种类。
3、进程间常见的通信方式
管道(直接复用内核代码通信)
- 匿名管道pipe
- 命名管道
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
4、管道
4.1、什么是管道
管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个"管道"。
4.2、匿名管道
4.2.1、定义
匿名管道本质上是一个内存级的文件 ,用于在进程之间(特别是父子进程之间)传输数据。与命名管道(Named Pipe,也称为FIFO)不同,匿名管道没有文件名,且在文件系统中没有对应的节点。它通常仅存在于内存中,随进程的结束而消失。
4.2.2、特点
- 单向通信:匿名管道是单向的,数据只能从一个方向流动。如果需要双向通信,需要创建两个匿名管道。
- 半双工模式:在任何给定时间内,一个管道只能用于读或写操作,但不能同时进行。
- 自动创建与销毁:当调用pipe()系统调用创建管道时,系统会自动为其分配两个文件描述符,分别用于读和写。管道的生命周期与进程相关,随着进程的结束而销毁。
- 有限容量:匿名管道通常有一个有限的缓冲区容量,如果缓冲区满,写操作将会阻塞,直到有数据被读取并腾出空间。
- 无名性:与命名管道不同,匿名管道没有文件名,无法在文件系统中直接访问。
4.2.3、创建与使用
匿名管道通过调用pipe()系统调用来创建。该函数会返回一个包含两个文件描述符的数组,其中数组的第一个元素是读端文件描述符 ,第二个元素是写端文件描述符。然后,可以通过fork()系统调用创建子进程,并通过这两个文件描述符在父子进程间进行数据传输。
#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:
成功返回0,失败返回错误代码
测试创建管道
会用到的头文件和常量
#include <iostream>
#include <cerrno> // <==> errno.h
#include <cstring> // <==> string.h
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string>
const int size = 1024;
主函数
// 测试创建管道
int main()
{
// close(0);// 关闭文件描述符
// 1、创建管道
int pipefd[2];
int n = pipe(pipefd); // 输出型参数 rfd wfd
// 返回0表示成功,不等于0则失败且更新错误码
if(n != 0)
{
std::cerr << "errno: "<< errno << "errstring: " << strerror(errno) << std::endl;
return 1;
}
// pipefd[0] 读(0 -> 嘴巴 -> r) pipefd[1] 写(1 -> 笔 -> w)
// 打印文件描述符
std::cout << "pipefd[0]: " << pipefd[0] << ",pipefd[1]: " << pipefd[1] << std::endl;
return 0;
}
执行结果
正确打印出文件描述符,表示成功创建管道。
关闭0号文件描述符,继续测试是否满足文件描述符规则!
同样符合规则,先占用0号文件描述符,且成功创建管道。
进行通信测试
获取信息函数
std::string GetOtherMessage()
{
static int cnt = 0;// 静态变量,全局的,在函数能使用
std::string messageid = std::to_string(cnt); // stoi string -> int
cnt++;
pid_t id = getpid();
std::string stringid = std::to_string(id);
std::string message = "messageid: ";
message += messageid;
message += "stringid: ";
message += stringid;
return message;
}
子进程写入信息
// 子进程进行写入
void SubProcessWrite(int wfd)
{
int pipesize = 0;
std::string message = "father,I am your son process!";
char c = 'A';
while(true)
{
std::string info = message + GetOtherMessage(); // 这条消息,就是我们子进程发给父进程的消息
write(wfd,info.c_str(),info.size()); // 写入管道的时候,没有写入\0, 有没有必要?没有必要
sleep(1); // 子进程写慢一点
}
}
父进程读取信息
// 父进程进行读取
void FatherProcessRead(int rfd)
{
char buffer[size];
while(true)
{
ssize_t n = read(rfd,buffer,sizeof(buffer) - 1); // strlen()
// 返回值大于0表示成功读取
if(n > 0)
{
buffer[n - 1] = 0;// \0
std::cout << buffer << std::endl;
}
// 返回值是0,表示写端直接关闭了,我们读到了文件的结尾
else if(n == 0)
{
std::cout << "client quit, father get return val: " << n << " father quit too!" << std::endl;
break;
}
// 返回值小于0表示读取错误
else if(n < 0)
{
std::cerr << "read error" << std::endl;
break;
}
}
}
主函数
int main()
{
// close(0); // 关闭文件描述符
// 1、创建管道
int pipefd[2];
int n = pipe(pipefd); // 输出型参数 rfd wfd
// 返回0表示成功,不等于0则失败且更新错误码
if(n != 0)
{
std::cerr << "errno: "<< errno << "errstring: " << strerror(errno) << std::endl;
return 1;
}
// pipefd[0] 读(0 -> 嘴巴 -> r) pipefd[1] 写(1 -> 笔 -> w)
std::cout << "pipefd[0]: " << pipefd[0] << ",pipefd[1]: " << pipefd[1] << std::endl;
// 2、创建子进程
pid_t id = fork();
if(id == 0)
{
std::cout << "子进程关闭不需要的fd了,准备发消息了" << std::endl;
// 子进程 --write
// 3、关闭不需要的文件描述符 read
close(pipefd[0]);
SubProcessWrite(pipefd[1]);
close(pipefd[1]);
exit(0);
}
sleep(1);
// 父进程
// 3、关闭不需要的文件描述符 write
std::cout << "父进程关闭不需要的fd了,准备收消息了" << std::endl;
close(pipefd[1]);
FatherProcessRead(pipefd[0]);
std::cout << "5s,father close rfd" << std::endl;
sleep(5);
close(pipefd[0]);
// 回收子进程
int status = 0;
pid_t rid = waitpid(id,&status,0);
// 返回值大于0回收成功
if(rid > 0)
{
std::cout << "wait child process done, exit sig: " << (status & 0x7f) << std::endl;
std::cout << "wait child process done, exit code(ign): " << ((status >> 8) & 0xFF) << std::endl;
}
return 0;
}
用fork来共享管道原理
运行结果
原理
父进程竟然要关闭不需要的fd,为什么开始需要打开呢?可以不关闭?
必须打开,因为需要让子进程继承下去。可以不关闭,但是建议关闭,防止误写。
为什么父子进程会向同一个终端显示器打印数据呢?
由于父子进程共享了相同的输出环境(通常是终端或显示器)。
进程默认会打开0,1,2号文件描述符,怎么做到的呢?
该进程时bash的子进程,bash打开了,所有子进程也默认打开了,我们只需要做好约定即可。
close();为什么我们子进程主动close(0,1,2),不影响父进程继续使用显示器文件呢?
因为子进程close(0,1,2)不是直接就关闭文件描述符,而是存在一个引用计数,当有进程指向该文件描述符时就++,关闭文件描述符先对引用计数--,如果引用计数等于0则关闭文件描述符。
4.2.4、测试管道接口
代码验证:
管道的四种情况:
1、如果管道内部是空的 && write fd 没有关闭,读取条件不具备,读进程会被阻塞 -- 即 wait,等待读取条件具备,即写入数据条件具备。
2、管道被写满 && read fd 不读且没有关闭, 写进程会被阻塞,即写条件不具备,需要等待读取数据。
3、管道一直在读 && 写端关闭了wfd,读端返回值会读到0,表示读到文件结尾。
演示一
演示二
4、rfd直接关闭,写段wfd一直写入?写段操作会被操作系统直接使用13号信号关掉。相当于进程出现了异常。
管道的五种特征:
1、匿名管道:只用来进行具有血缘关系的进程之间进行通信,通常用于父子之间进行通信。因为子进程能看到父进程的数据。
2、管道内部,自带进程之间同步机制,多执行流执行代码的时候,具有明显的顺序性。
可能会出现管道被多个进程读取的情况,那么数据就可能出现不一致问题,因此管道内部自带同步机制。
3、管道文件的生命周期是随进程
4、管道文件在通信的时候,是面向字节流的。写的次数和读取的次数不是一一匹配。
代码
运行结果
5、管道的通信模式,是一种特殊的半双工模式(在任何给定时间内,一个管道只能用于读或写操作,但不能同时进行)。
全双工模式:在任何给定时间内,一个管道能用于读和写操作,能同时进行。
补充: