1.进程间通信:
1.数据传输:一个进程需要将它的数据发送给另一个进程
2.资源共享:多个进程之间共享同样的资源
3.通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
4.进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
5.原因:为了实现两个或者多个进程实现数据层面的交互,因为进程独立性的存在,导致进程通信的成本比较高,通信可以用于:1. 基本数据 2. 发送命令 3. 某种协调 4. 通知 ...很多场景下需要多个进程协同工作来完成要求,比如管道
6.方法(三种)
1.管道(通过文件系统通信),分为匿名管道和命名管道
2.System V(聚焦本地通信),分为共享内存,消息队列,信号量
3.POSIX(通信可以跨主机),分为共享内存,消息队列,信号量,互斥量,条件变量,读写锁
7.介绍管道
1.进程间通信的本质,必须让不同进程看到同一份资源
2.资源是特定形式的内存空间
3.资源一般由操作系统提供(多进程的共享资源必须由第三方提供)
4.进程访问空间进行通信,本质就是访问操作系统,进程代表的就是用户,资源从创建,使用(一般),释放--系统调用接口
5.从底层设计,从接口设计,都要由操作系统独立设计,一般操作系统,会有一个独立的通信模块--隶属于文件系统--IPC 通信模块,定制标准 -- 进程间通信是有标准的 -- system V && posix
6. 基于文件级别的通信方式 --管道
8.匿名管道:父子间通信仍然正常进行,并且效率还非常的高,而且还没有影响进程的独立性。而这种不进行IO(不刷到磁盘),就是内存级文件,这种由文件系统提供公共资源的进程间通信,就叫做管道
1.原理:没有名字的文件(struct file),匿名管道用于父子间通信,或者由一个父创建的兄弟进程(必须有“血缘“)之间进行通信
2.现在我们知道了匿名管道就是没有名字的文件,通过管道进行通信时,只需要通信双方打开同一个文件就可以,我们通过系统调用open打开文件的时候,会指定打开方式,是读还是写
1.当父进程以写方式打开一个文件的时候,创建的子进程会继承父进程的一切
2.此时子进程以写的方式打开的这个文件
3.既然是通信,势必有一方在写,一方在读,而现在父子双方都是以写的方式打开,它们怎么进行通信呢?父进程以读和写的方式打开同一份文件两次。可以理解为管道就是文件,有缓冲区,由父子进程引入管道文件
4.父进程拷贝创建出子进程,PCB 是肯定要的,files_struct 指向同一文件
5.父子进程公用文件实现了,那么对于读写的控制是怎么实现的呢?
1.为了防止父进程对管道进行误读,以及子进程对管道进行误写,破坏通信规则
2.将父进程的读端关闭,将子进程的写端关闭,使用系统调用close(fd)
3.只想让父子进程进行单向通信!所以称为管道通信。把文件这套拿过来,用就是为了简单,我们想让子进程进行写入,父进程进行读取,如果要双向通信,就需要多个管道,如果进程没有任何血缘关系,就不能用如上原理进行通信,因为无法继承一套file_struct
6.至此,通信了吗??没有。建立通信信道--为什么这么费劲?--因为进程具有独立性,要操作系统忙活才能建立出信道,通信是有成本的!
9.pipe:头文件include<unistd.h>它的形参是pipefd[2](之后存储两个进程的文件描述符,注意从3开始)
1.该形参是是一个输出型参数,表示一个数组,该数组有两个元素,分别为0和1
2.下标为0的元素表示管道读端的文件描述符fd
3.下标为1的元素表示管道写端的文件描述符fd
4.通过系统调用pipe,可以直接获得这两个文件描述符,并将其添加到父进程的文件描述符表中,无需两次打开内存级别的文件
5.返回值为int类型的整数,用来反馈管道创建的情况,0表示成功,-1表示创建失败,会将错误码自动写入errno中
6.代码
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string>
#include <iostream>
#include <cstdlib>
#include <cstdio> // 包含snprintf和perror的头文件
const int NUM = 100; // 缓冲区大小
const int N = 2; // 管道数组的大小
using namespace std;
void Write(int wfd) {
string s = "hello, I am child";
int number = 0;
char buffer[NUM]; // 定义一个缓冲区
while (true) {
sleep(1);
snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), getpid(), number++);
// 构建字符串
write(wfd, buffer, sizeof(buffer)); // 实现传参写入
cout << number << endl; // 打印计数
if (number >= 5) break;
}
}
void Reader(int rfd) {
char buffer[NUM];
while (true) {
buffer[0] = 0;
ssize_t n = read(rfd, buffer, sizeof(buffer)); // 读取缓冲区
if (n > 0) {
buffer[n] = 0; // 终止字符串,打印
cout << "father get a message[" << getpid() << "]# " << buffer << endl;
} else if (n == 0) {
cout << "father read file done!" << endl;
break;
} else {
perror("Error reading from pipe"); // 打印错误信息
break;
}
}
}
int main() {
int pipefd[N] = {0}; // 管道数组描述符
int n = pipe(pipefd);
if (n < 0) {
perror("Error pipe");
return 1;
}
pid_t id = fork(); // 创建子进程
if (id < 0) {
perror("Error forking");
return 2; // 回忆点1
}
if (id == 0) {
// 子进程
close(pipefd[0]);
Write(pipefd[1]); // 传入写端
close(pipefd[1]);
exit(0); // 回忆点2
} else {
// 父进程
close(pipefd[1]);
Reader(pipefd[0]);
// 回忆点3
// 等待回收
pid_t rid = waitpid(id, NULL, 0);
if (rid < 0) {
perror("Error waiting for child process");
return 3;
}
close(pipefd[0]); // 关闭读端
}
cout << "Program finished." << endl;
return 0;
}
使用g++遍历
1.函数Writer
用于子进程向父进程写入数据。wfd
是写端文件描述符,s
是发送的字符串,self
是子进程ID,number
是一个计数器。每次循环中,构建一个包含字符串、子进程ID和计数器的消息,然后写入管道。每写一次,计数器加一,循环5次后退出
2.函数Reader
用于父进程从管道读取数据。rfd
是读端文件描述符。每次循环中,尝试从管道读取数据,如果成功读取到数据,将其打印出来。如果读取到文件结尾或发生错误,则退出循环
3.主函数:创建一个管道,pipefd
数组包含两个文件描述符,一个用于读,一个用于写,调用fork()
创建子进程,如果fork()
失败,打印错误信息并退出,如果是子进程,关闭读端,调用Writer
函数写数据,写完后关闭写端并退出,如果是父进程,关闭写端,调用Reader
函数读数据,等待子进程结束,关闭读端,最后打印程序结束的信息并返回
4.重点:
1.return 2
的设置 : 如果 fork()
失败,程序将返回 2
。这里的数字 2
是一个约定的返回值,用于表示 fork()
调用失败。通常情况下,不同的错误情况会使用不同的返回值来区分
2.子进程关闭管道的写端 (pipefd[1]
) 并随后调用 exit(0)
是为了:确保子进程只执行写操作,写完退出
3.pid_t rid = waitpid(id, nullptr, 0) :使用 waitpid
函数来等待子进程的结束,结束后父进程一定要实现资源回收
5.waitpid
是一个系统调用,用于等待指定子进程的终止状态
6.参数 id
是子进程的 PID(进程标识符
7.参数nullptr
表示不关心子进程的状态信息
8.参数0
表示默认的行为,即等待任何子进程的终止
9.buffer[0] = 0;
- 清空字符串,标志数组 buffer
作为字符串使用
10.buffer[n] = 0; --终止字符串,进行打
11.snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);
- 使用安全的格式化函数,防止缓冲区溢出
12.snprintf 参数设计简洁说明:
buffer:目标字符数组,用于存储格式化后的字符串。
sizeof(buffer):指定缓冲区大小,防止溢出。
"%s-%d-%d":定义字符串格式,包含字符串、两个整数和一个分隔符。
s.c_str():提供要插入的字符串。
self:插入的第一个整数。
number++:使用并递增的第二个整数
13.必须要调用 write read,系统调用,操作系统起到了一个媒介的作用
10.管道的五大特征:
1.具有血缘关系的进程,进行进程间通信
2.管道只能单向通信(子进程休眠父进程也会休眠)
3.管道是基于文件的,而文件的生命周期是随进程的
4.父子进程是会进程协同的,同步与互斥的保护管道文件的数据安全(管道是有固定大小的)
5.管道是面向字节流的
验证管道的固定大小: 使用 ulimit -a
命令可以查看当前用户进程的资源限制,其中包括管道的大小。管道大小在输出中通常标记为 pipe size
ulimit -a | grep "pipe size"
解释管道大小:管道的默认大小在不同内核版本中可能有所不同。在许多 Linux 系统上,默认的管道大小是 64 KB
查看linux发行版和版本:使用 cat /etc/redhat-release
命令可以查看 Red Hat 或 CentOS 系统的发行版和版本信息
cat /etc/redhat-release
测试管道大小: 要测试管道大小是否为 64 KB,可以使用 dd
命令尝试写入一个大于 64 KB 的文件到管道,并观察是否能够成功写入
dd if=/dev/zero of=/dev/null bs=65536 count=1 2>&1 | grep records
如果管道大小确实是 64 KB,那么上面的命令应该能成功写入 64 KB 数据。
查找 pipe_buf
大小: pipe_buf
的大小定义在内核头文件中,可以通过查看内核源码或使用 grep
命令在内核头文件中搜索。
grep -R "define PIPE_BUF" /usr/include/linux
或者,如果你有内核源码树,可以在源码中搜索
grep -R "define PIPE_BUF" /path/to/linux-source-code/include/linux
进行原子性读取测试: 为了确保在不间断的情况下完整读取 “hello world” 字符串,并保证原子性,可以创建一个管道,并在一个进程中写入字符串,在另一个进程中读取
管道的四种情况:
1.读写段正常,管道如果为空,读端就要阻塞
2.读写端正常,管道如果被写满,写端就要阻塞
3.读写正常读,写端关闭,读端就会读到 0,表面读到了文件(pipe)结尾,不会被阻塞
eg. 父进程等待子进程(Z)退出,测试 n=0
4.写端是正常写入,读端关闭
一些小思考:
1. 一个进程中可以开两个 pipe:
一个进程可以创建多个管道,每个管道都有独立的读写端。
2. read 读到文件尾,write 一写就会更新到文件头:
管道是循环缓冲区,写入数据会覆盖最早的数据,read 指针会自动跳到新的数据开始处。
3. 覆盖写,但为什么不会读取垃圾信息?read 指针也在移动:
管道确保了写入和读取的原子性,每次读取都是连续的数据块,不会读取到被覆盖的旧数据。
管道为什么无法实现写完马上就读
缓冲区限制:管道有一个固定大小的缓冲区。当这个缓冲区满了之后,进一步的写操作将会被阻塞,直到有空间可用。这意味着,如果写入的数据超过了缓冲区的大小,那么这些数据不会立即被读取,因为它们还在等待空间变得可用。
读写顺序:管道是按照顺序操作的。写入端(writer)必须先写入数据,然后读取端(reader)才能从管道中读取这些数据。如果读取端没有进行读取操作,写入端的数据可能会留在缓冲区中。
原子性:管道的读写操作具有原子性。这意味着每次读取操作要么读取一个完整的数据块,要么读取0个字节(如果缓冲区为空)。这防止了读取到不完整或“垃圾”数据。
阻塞与非阻塞:默认情况下,读取端如果尝试从一个空的管道中读取数据,将会被阻塞,直到有数据可读。
进程调度:操作系统的进程调度可能影响管道的读写操作。即使缓冲区中有数据,如果读取进程没有获得CPU时间,那么读取操作也不会发生
管道一般的使用方法(后给前)