📢博客主页:https://blog.csdn.net/2301_779549673
📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
📢本文由 JohnKi 原创,首发于 CSDN🙉
📢未来很长,值得我们全力奔赴更美好的生活✨
文章目录
📢前言
🏳️🌈1. 进程间通信目的
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
🏳️🌈2. 管道
什么是管道
管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个管道
🏳️🌈3. 匿名管道
#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端返回值:
成功返回0,失败返回错误代码
3.1 示例代码
例⼦:从键盘读取数据,写⼊管道,读取管道,写到屏幕
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main(){
int fds[2];
char buf[100];
int len;
if(pipe(fds) == -1){
perror("make pipe");
exit(1);
}
// read from stdin
while(fgets(buf, 100, stdin)){
len = strlen(buf);
if(write(fds[1], buf, len) != len){
perror("write to pipe");
break;
}
memset(buf, 0x00, sizeof(buf));
// read from pipe
if((len = read(fds[0], buf, 100)) == -1){
perror("read from pipe");
break;
}
// write to stdout
if(write(1, buf, len) != len){
perror("write to stdout");
break;
}
}
}
3.2 用fork来共享管道原理
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
int fd[2];
pid_t pid;
// 创建管道
if (pipe(fd) == -1) {
perror("pipe");
return 1;
}
// 创建子进程
pid = fork();
if (pid < 0) {
perror("fork");
return 1;
}
else if (pid > 0) { // 父进程
// 父进程关闭管道读端
close(fd[0]);
// 父进程向管道写入数据
char* message = "Hello from parent!";
write(fd[1], message, sizeof(message));
// 父进程关闭管道写端
close(fd[1]);
}
else { // 子进程
// 子进程关闭管道写端
close(fd[1]);
// 子进程从管道读取数据
char buffer[100];
read(fd[0], buffer, sizeof(buffer));
printf("Child received: %s\n", buffer);
// 子进程关闭管道读端
close(fd[0]);
}
return 0;
}
- 创建管道
使用pipe(fd)函数创建一个管道,fd[0]是管道的读端,fd[1]是管道的写端。 - 创建子进程
使用fork()函数创建一个子进程。fork()函数返回值有三种情况:
如果返回值小于 0,表示创建子进程失败。
如果返回值等于 0,表示当前是子进程。
如果返回值大于 0,表示当前是父进程,返回值为子进程的 PID。 - 父进程操作
父进程首先关闭管道的读端fd[0]。
然后父进程向管道写入数据,这里写入字符串 “Hello from parent!”。
最后父进程关闭管道的写端fd[1]。 - 子进程操作
子进程首先关闭管道的写端fd[1]。
然后子进程从管道读取数据,将读取的数据存储在buffer中。
最后子进程打印出读取的数据,并关闭管道的读端fd[0]。
这段代码实现了在父进程和子进程之间通过管道进行通信,验证了图中所示的管道共享原理。
3.3 站在文件描述符角度-深度理解管道
总的来说可以归结为下面这张图,但是与上面代码有所不同的是,上述代码仅仅只能实现一次性的管道传输。
这部分代码实现了父子进程之间持续的双向通信,只要不手动终止程序,它们就可以一直交换消息。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
#define BUFFER_SIZE 100
int main() {
int parent_to_child[2];
int child_to_parent[2];
pid_t pid;
// 创建两个管道,一个用于父进程到子进程,一个用于子进程到父进程
if (pipe(parent_to_child) == -1 || pipe(child_to_parent) == -1) {
perror("pipe");
return 1;
}
pid = fork();
if (pid < 0) {
perror("fork");
return 1;
} else if (pid > 0) { // 父进程
close(parent_to_child[0]); // 关闭父进程到子进程管道的读端
close(child_to_parent[1]); // 关闭子进程到父进程管道的写端
char buffer[BUFFER_SIZE];
while (1) {
// 父进程向子进程发送消息
printf("Parent: Enter a message to send to child (or 'quit' to exit): ");
fgets(buffer, BUFFER_SIZE, stdin);
buffer[strcspn(buffer, "\n")] = 0; // 去除换行符
if (strcmp(buffer, "quit") == 0) {
break;
}
write(parent_to_child[1], buffer, strlen(buffer) + 1);
// 父进程从子进程接收消息
ssize_t n = read(child_to_parent[0], buffer, BUFFER_SIZE);
if (n > 0) {
printf("Parent received from child: %s\n", buffer);
}
}
close(parent_to_child[1]);
close(child_to_parent[0]);
} else { // 子进程
close(parent_to_child[1]); // 关闭父进程到子进程管道的写端
close(child_to_parent[0]); // 关闭子进程到父进程管道的读端
char buffer[BUFFER_SIZE];
while (1) {
// 子进程从父进程接收消息
ssize_t n = read(parent_to_child[0], buffer, BUFFER_SIZE);
if (n > 0) {
if (strcmp(buffer, "quit") == 0) {
break;
}
printf("Child received from parent: %s\n", buffer);
}
// 子进程向父进程发送消息
printf("Child: Enter a message to send to parent (or 'quit' to exit): ");
fgets(buffer, BUFFER_SIZE, stdin);
buffer[strcspn(buffer, "\n")] = 0; // 去除换行符
if (strcmp(buffer, "quit") == 0) {
break;
}
write(child_to_parent[1], buffer, strlen(buffer) + 1);
}
close(parent_to_child[0]);
close(child_to_parent[1]);
}
return 0;
}
3.4 站在内核角度-管道本质
从内核角度来看,管道本质上是一种特殊的文件系统机制,用于实现进程间通信(IPC)。以下是对管道本质的内核级理解:
- 文件结构与 inode
文件结构(file structure):每个进程在操作管道时,都会有一个对应的文件结构。在图中可以看到进程 1 和进程 2 都有各自的文件结构。这个文件结构包含了进程对管道操作所需要的信息,如文件模式(f_mode)、文件位置(f_pos)、文件标志(f_flags)、文件引用计数(f_count)等。
inode:文件结构通过f_inode指针指向一个 inode。inode 是内核中的一种数据结构,用于存储文件(这里是管道)的元数据信息,如文件的所有者(f_owner)、文件操作函数指针(f_op)等。对于管道来说,inode 中的操作函数指针会指向与管道操作相关的函数(如管道的读和写操作)。 - 管道的读写操作
管道写操作(write):当进程 1 执行管道写操作时,它通过自己的文件结构和 inode 找到对应的管道数据结构,并调用 inode 中的写操作函数。写操作函数会将数据写入管道的数据页(data page)。
管道读操作(read):当进程 2 执行管道读操作时,同样通过自己的文件结构和 inode 找到管道数据结构,并调用 inode 中的读操作函数。读操作函数会从管道的数据页中读取数据。 - 数据页
数据存储:管道的数据页是实际存储数据的地方。当一个进程向管道写入数据时,数据被存储在这个数据页中,而另一个进程从管道读取数据时,也是从这个数据页中获取数据。数据页在内核中被维护,并且通过 inode 和文件结构来访问。 - 管道的内核实现机制
同步与互斥:内核在实现管道时,需要处理多个进程对管道的并发访问问题。这通常涉及到同步和互斥机制,例如使用锁来确保在一个进程进行读或写操作时,其他进程不会同时进行冲突的操作。
缓冲区管理:管道的数据页实际上就是一个缓冲区,内核需要管理这个缓冲区的大小和数据的存储与读取顺序。当缓冲区满时,写操作需要阻塞;当缓冲区空时,读操作需要阻塞。
综上所述,从内核角度来看,管道是通过文件系统相关的数据结构(文件结构、inode 和数据页)以及相应的操作函数来实现进程间通信的一种机制,它涉及到对数据的缓冲、同步和互斥等内核级操作。
👥总结
本篇博文对 管道 做了一个较为详细的介绍,不知道对你有没有帮助呢
觉得博主写得还不错的三连支持下吧!会继续努力的~