欢迎来到Cefler的博客😁
🕌博客主页:折纸花满衣
🏠个人专栏:题目解析
🌎推荐文章:【LeetCode】winter vacation training
目录
- 📋进程通信的目的
- 📋管道
- 匿名管道
- pipe函数创建匿名管道
- 管道的5种特性4种情况
- 站在文件描述符的角度看管道
- 命名管道
- 命名管道和匿名管道的区别
- 📋命名管道实现两个毫不相干进程间的读写联系
- Makefile
- common.h
- server.cc(读)
- client.cc(写)
📋进程通信的目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
📋管道
进程通信的管道底层原理是使用操作系统提供的文件描述符来实现。
在Linux中,管道是通过内核中的缓冲区来实现进程间数据传输的。管道可以被看作是一个字节流,它有两个文件描述符:**一个用于读取数据,一个用于写入数据。**这两个文件描述符分别被称为管道的读端和写端。当一个进程往管道的写端写入数据时,数据会被放入管道的缓冲区中,而另一个进程从管道的读端读取数据时,数据会从缓冲区中被取出。
具体来说,管道的底层原理如下:
-
创建管道:当调用pipe()函数时,操作系统会创建一个管道,并返回两个文件描述符,一个用于读取数据,一个用于写入数据。
-
数据传输:一个进程可以通过write()系统调用将数据写入管道的写端,数据会被放入管道的缓冲区中。另一个进程可以通过read()系统调用从管道的读端读取数据,数据会从缓冲区中被取出。读写操作是原子的,即每次写入或读取的数据大小是固定的。
-
阻塞与非阻塞:管道的写端和读端都可以设置为阻塞或非阻塞模式。在阻塞模式下,如果写端或读端没有准备好(缓冲区已满或为空),相应的写入或读取操作会被阻塞,直到准备好为止
匿名管道
当谈到 Linux 的匿名管道时,我们指的是一种特殊的进程间通信机制。它允许一个进程将输出直接发送给另一个进程,而无需使用临时文件或其他形式的中间存储。
匿名管道使用竖线符号(|)来表示,通过将一个进程的标准输出连接到另一个进程的标准输入来实现数据传输。这种连接使得第一个进程的输出变为第二个进程的输入,实现了进程间的数据流动。
在 Linux 中,匿名管道是通过 pipe
系统调用创建的。它返回两个文件描述符,一个用于读取数据,另一个用于写入数据。这两个文件描述符可以用于在相关进程之间传输数据。
以下是一个简单的示例来说明匿名管道的使用:
$ ls | grep "txt"
在这个示例中,ls
命令列出当前目录下的所有文件,并将输出通过匿名管道传递给 grep
命令。grep
命令会过滤出包含 “txt” 的文件。
匿名管道对于在 Linux 上进行进程间通信非常有用。然而,它也有一些限制,比如只能实现单向通信,只能用于有亲缘关系的进程(例如父子进程或兄弟进程),并且在数据量较大时可能会引起阻塞。
pipe函数创建匿名管道
pipe
函数是一个系统调用,用于在 Linux 系统中创建一个匿名管道。
它的函数原型如下:
#include <unistd.h>int pipe(int pipefd[2]);
pipe
函数接受一个整型数组 pipefd
作为参数,该数组包含两个文件描述符。pipefd[0]
是管道的读取端,pipefd[1]
是管道的写入端。
当成功调用 pipe
函数时,它会创建一个无名管道,并将对应的文件描述符存储在 pipefd
数组中。这样,我们就可以使用这两个文件描述符来实现进程间的通信。
以下是一个简单的示例,展示了如何使用 pipe
函数创建管道并进行进程间通信:
#include <unistd.h>
#include <stdio.h>
#include <string.h>int main() {int pipefd[2];char buffer[20];// 创建管道if (pipe(pipefd) == -1) {perror("pipe");return 1;}// 创建子进程pid_t pid = fork();if (pid < 0) {perror("fork");return 1;}if (pid == 0) {// 子进程从管道中读取数据close(pipefd[1]); // 关闭写入端read(pipefd[0], buffer, sizeof(buffer));printf("子进程收到消息:%s\n", buffer);close(pipefd[0]); // 关闭读取端} else {// 父进程向管道中写入数据close(pipefd[0]); // 关闭读取端const char* message = "Hello, child!";write(pipefd[1], message, strlen(message) + 1);close(pipefd[1]); // 关闭写入端}return 0;
}
在这个示例中,父进程创建了一个管道,并通过 write
函数向管道中写入消息。子进程通过 read
函数从管道中读取消息,并输出到控制台。
需要注意的是,为了正确使用管道,我们需要在适当的时候关闭文件描述符。父进程关闭了读取端,子进程关闭了写入端。
以上示例代码可以分为三个步骤:
1.建立管道
2.创建子进程
3.父子关闭不需要的fd,形成单向通信的管道
管道的5种特性4种情况
🌈 管道的4种情况
1.如果管道没有数据了,读端就只能等待
2.如果管道被写满了,写端必须等待,直到有空间为止
3.写端关闭,读端一直读取,读端读到read返回值为0,表示读到文件结尾
4.读端关闭,写端一直写入,OS会直接杀掉写端进程,通过向目标进程发送SIGPIPE(13)信号,终止目标进程
🌈 管道的5钟特性
1.只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创
建,然后该进程调用fork,此后父、子进程之间就可应用该管道
2.匿名管道,默认要给读写端提供同步机制
3.匿名管道是面向字节流的(假如写了n量的数据,不一定要全部读入,根据自己的设置需求,想怎么读就怎么读)
4.管道的生命周期是随进程的,进程结束,管道结束
5.管道是单向通信的,半双工通信
的一种特殊情况
站在文件描述符的角度看管道
父进程中两个文件描述符分别指向读和写,子进程继承父进程的文件描述符表。
父进程断开写的连接,子进程断开读的连接,最后形成子写,父读。
命名管道
命名管道(Named Pipe),也被称为 FIFO(First In First Out),是一种在 Linux 系统中用于进程间通信的机制。
与匿名管道不同的是,命名管道是通过文件系统中的一个特殊文件来实现的。它具有一个在文件系统中唯一标识的名称,并且可以由多个进程进行读写操作。
要创建一个命名管道,我们可以使用 mkfifo
命令或者在 C 语言中使用 mkfifo
函数。下面是一个示例,演示了如何使用命名管道进行进程间通信:
🍎命令行示例:
首先,在命令行中创建一个命名管道:
$ mkfifo mypipe
然后,在一个终端中执行以下命令,将消息写入命名管道:
$ echo "Hello, named pipe!" > mypipe
最后,在另一个终端中执行以下命令,从命名管道中读取消息:
$ cat mypipe
你将看到第二个终端输出了刚才写入的消息。
🍎C 语言示例:
以下是在 C 语言中使用命名管道进行进程间通信的示例:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>int main() {const char* fifo_file = "mypipe";const char* message = "Hello, named pipe!";char buffer[256];// 创建命名管道mkfifo(fifo_file, 0666);// 打开命名管道进行写操作int fd = open(fifo_file, O_WRONLY);write(fd, message, strlen(message) + 1);close(fd);// 打开命名管道进行读取操作fd = open(fifo_file, O_RDONLY);read(fd, buffer, sizeof(buffer));printf("Received message: %s\n", buffer);close(fd);// 删除命名管道unlink(fifo_file);return 0;
}
在这个示例中,我们首先使用 mkfifo
函数创建了一个命名管道,并指定了文件权限。然后,我们使用 open
函数打开命名管道进行写操作,并通过 write
函数向管道中写入消息。接着,我们再次使用 open
函数打开命名管道进行读取操作,并通过 read
函数读取管道中的消息。
需要注意的是,命名管道是阻塞的,如果没有进程同时打开读取端和写入端,那么写入端的进程将会被阻塞,直到有其他进程打开读取端。
命名管道和匿名管道的区别
命名管道(Named Pipe)和匿名管道(Anonymous Pipe)是两种不同的进程间通信机制,它们有以下几个区别:
-
创建方式:匿名管道通过
pipe
函数创建,而命名管道通过mkfifo
函数或者命令行的mkfifo
命令创建。 -
文件系统依赖性:匿名管道不依赖于文件系统,它只存在于内存中,没有对应的文件。命名管道则是在文件系统中创建了一个特殊文件,通过该文件进行读写操作。
-
进程间关系:匿名管道通常用于父子进程之间的通信,因为它们共享同一个进程空间。而命名管道可以用于任意进程之间的通信,只要它们可以访问到同一个命名管道文件。
-
生命周期:匿名管道在创建它的进程结束后自动销毁,无法被其他进程继续使用。而命名管道会一直存在于文件系统中,直到被显式地删除。
-
阻塞特性:匿名管道是阻塞的,如果没有进程同时打开读取端和写入端,写入端的进程将会被阻塞。命名管道也是阻塞的,但可以使用非阻塞方式打开以避免阻塞。
-
容量限制:匿名管道的容量是有限的,通常是几千字节。命名管道的容量取决于文件系统,一般比匿名管道大得多。
📋命名管道实现两个毫不相干进程间的读写联系
Makefile
.PHONY:all
all:server client #依赖关系,生成全部client:client.ccg++ -o $@ $^ -std=c++11
server:server.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f server client .fifo
common.h
#pragma once#define FILENAME ".fifo"
server.cc(读)
#include<iostream>
#include<cstring>
#include<cerrno>
#include <sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>#include "common.h"
using namespace std;int main()
{int n = mkfifo(FILENAME,0666);//创建命名管道if(n<0){cerr<<"errno:"<<errno<<",errstring:"<<strerror(errno)<<endl;return 1;}cout<<"mkfifo success...read"<<endl;int rfd = open(FILENAME,O_RDONLY);if(rfd<0){cerr<<"errno:"<<errno<<",errstring:"<<strerror(errno)<<endl;return 2;}char buffer[1024];while(true){ssize_t s = read(rfd,buffer,sizeof(buffer)-1);//读端if(s>0){buffer[s] = 0;//将最后一位置为反斜杠cout<<"Client say# "<<buffer<<endl;}else if(s==0){cout<<"client quit,server quit too"<<endl;break;}}close(rfd);cout<<"close fifo success..."<<endl;return 0;
}
client.cc(写)
#include<iostream>
#include<cstring>
#include<cerrno>
#include <sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>#include "common.h"
using namespace std;int main()
{int wfd = open(FILENAME,O_WRONLY);if(wfd<0){cerr<<"errno:"<<errno<<",errstring:"<<strerror(errno)<<endl;return 1;}cout<<"open fifo success...write"<<endl;string message;while(true){cout<<"Please Enter# ";getline(cin,message);ssize_t s = write(wfd,message.c_str(),message.size());//写端if(s<0){cerr<<"errno:"<<errno<<",errstring:"<<strerror(errno)<<endl;break;}}close(wfd);return 0;
}