🔥个人主页:guoguoqiang. 🔥专栏:Linux的学习
文章目录
- 匿名管道
- pipe
匿名管道
匿名管道(Anonymous Pipes)是Unix和类Unix操作系统中的一种通信机制,用于在两个进程之间传递数据。匿名管道通常用于命令行工具之间的数据传递;
匿名管道的工作原理是创建一个临时文件,该文件被称为管道文件,它仅存在于内存中,不持久化到磁盘。当一个进程创建一个匿名管道时,它会打开一个写入端和一个读取端。写入端通常由|运算符创建,而读取端则通过<运算符
匿名管道的一个关键特性是它是单向的,即只能从写入端到读取端传递数据。此外,一旦管道中的数据被读取,管道就会被关闭,不能再次使用。
1.概念与原理
匿名管道是进程间通信(IPC)的一种方式,主要用于具有父子关系的进程之间传递数据。它是在内核中开辟的一块缓冲区,作为进程间单向数据传输的通道。当一个进程(通常是父进程)调用pipe()系统调用时,内核会创建这个匿名管道,并返回两个文件描述符,一个用于写入数据(管道的写端),一个用于读取数据(管道的读端)。
例如,在 C 语言的 Linux 系统编程中,代码可能如下:
在上述代码中,首先通过pipe()函数创建匿名管道,得到pipefd数组,其中pipefd[0]是读端文件描述符,pipefd[1]是写端文件描述符。然后通过fork()函数创建子进程。子进程关闭写端,从读端读取数据;父进程关闭读端,向写端写入数据。
2.特点
半双工通信:数据只能在一个方向上流动。这意味着在某一时刻,要么是数据从写端流向读端,要么是通过创建另外的管道来实现相反方向的通信。
基于父子进程关系:匿名管道依赖于父子进程之间的继承关系。子进程通过继承父进程的文件描述符来访问管道,所以它主要用于有亲缘关系的进程之间的通信。
临时性:匿名管道是没有名字的,并且在最后一个使用它的进程结束后自动消失。它没有在文件系统中留下持久的标记,这与命名管道不同。
3.性能和效率方面
匿名管道的效率相对较高,因为它是在内核缓冲区中直接进行数据传递。不过,由于它是半双工的,在需要双向通信的复杂场景下,可能需要创建多个管道,这可能会增加系统开销。
它的读写操作通常是阻塞式的。当管道为空时,读操作会阻塞,直到有数据写入管道;当管道满时,写操作会阻塞,直到有空间可写。这种阻塞特性有助于简单地协调父子进程之间的数据传输,但在某些对实时性要求较高的场景下可能需要特殊处理,如使用非阻塞 I/O 或多路复用技术。
4.应用场景
在 Unix/Linux 系统的命令行操作中,匿名管道应用广泛。例如,ls -l | grep "txt"命令组合。ls -l命令会列出文件的详细信息,它的输出通过匿名管道传递给grep "txt"命令,grep命令则在这些输出中筛选出包含"txt"的行。这种命令组合方式可以方便地对一个命令的输出进行过滤、排序等操作,是匿名管道在实际系统操作中的典型应用。
pipe
在C语言中,pipe函数是用于创建一个匿名管道(也称为管道)的标准库函数。它允许进程之间通过管道进行通信。pipe函数的声明如下:
这个函数的作用是在调用进程和其子进程之间创建一个匿名管道。pipefd是一个整数数组,包含两个整数元素,分别用于读取和写入管道。
pipefd[0]:这是管道的读取端,可以通过它从管道中读取数据。
pipefd[1]:这是管道的写入端,可以通过它将数据写入管道。
pipe函数的返回值:
如果成功,pipe函数返回0。
如果失败,pipe函数返回-1,并设置errno以指示错误。
成功调用pipe函数后,返回的两个文件描述符pipefd[0]和pipefd[1]可以用于后续的读取和写入操作。
这个函数的作用是在调用进程和其子进程之间创建一个匿名管道。pipefd是一个整数数组,包含两个整数元素,分别用于读取和写入管道。
pipefd[0]:这是管道的读取端,可以通过它从管道中读取数据。
pipefd[1]:这是管道的写入端,可以通过它将数据写入管道。
pipe函数的返回值:
如果成功,pipe函数返回0。
如果失败,pipe函数返回-1,并设置errno以指示错误。
成功调用pipe函数后,返回的两个文件描述符pipefd[0]和pipefd[1]可以用于后续的读取和写入操作。
pipe
系统调用的基本概念- 在Unix和类Unix系统(如Linux)中,
pipe
是一个重要的系统调用,用于创建匿名管道。它的主要功能是在内核中开辟一块缓冲区,这个缓冲区用于在进程之间单向传输数据。 - 当调用
pipe
系统调用时,它会返回一个包含两个整数的数组(在C语言等编程语言中),这两个整数代表两个文件描述符。例如,在C语言代码中:
- 在Unix和类Unix系统(如Linux)中,
#include <unistd.h>
int pipefd[2];
int result = pipe(pipefd);
- 这里
pipefd[0]
是管道的读端文件描述符,用于从管道中读取数据;pipefd[1]
是管道的写端文件描述符,用于向管道中写入数据。
pipe
函数的参数和返回值- 参数:
pipe
函数通常只接受一个参数,即一个用于存储两个文件描述符的数组的地址。在C语言中,这个数组的类型是int
类型的数组,大小为2。例如上面代码中的pipefd
数组。 - 返回值:如果
pipe
调用成功,它会返回0,并将两个有效的文件描述符存储在传入的数组中;如果调用失败,它会返回 - 1,并且会设置errno
来指示错误原因。常见的错误原因可能包括系统资源不足(如内存不足)或文件描述符数量达到系统限制等。
- 参数:
pipe
在进程通信中的应用示例- 以下是一个简单的示例,展示了父子进程之间如何通过
pipe
创建的匿名管道进行通信:
- 以下是一个简单的示例,展示了父子进程之间如何通过
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
int pipefd[2];
pid_t pid;
char buffer[100];
// 创建管道
if (pipe(pipefd) == -1) {
perror("pipe");
return 1;
}
// 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
return 1;
} else if (pid == 0) {
// 子进程
close(pipefd[1]);// 关闭写端
int n = read(pipefd[0], buffer, sizeof(buffer));
if (n > 0) {
buffer[n] = '\0';
printf("子进程读取到: %s", buffer);
}
close(pipefd[0]);
} else {
// 父进程
close(pipefd[0]);// 关闭读端
char message[] = "这是父进程发送的数据";
write(pipefd[1], message, sizeof(message));
close(pipefd[1]);
}
return 0;
}
- 在这个示例中,首先通过
pipe
创建了匿名管道,然后使用fork
创建子进程。子进程关闭写端,从读端读取数据;父进程关闭读端,向写端写入数据,从而实现父子进程之间的单向通信。
pipe
与其他进程通信机制的比较- 与命名管道(FIFO)相比,
pipe
创建的匿名管道没有名字,只能用于有亲缘关系(父子进程)之间的通信,而命名管道可以用于无亲缘关系的进程之间的通信。 - 与消息队列相比,
pipe
是基于文件描述符的简单数据传输机制,主要用于单向的字节流传输;消息队列则是更灵活的消息传递机制,它可以按消息类型接收和发送,并且可以在多个进程之间实现全双工通信(通过合适的设计)。 - 与共享内存相比,
pipe
通过缓冲区进行数据传输,进程间的数据交换相对间接;共享内存则是让多个进程直接共享一块内存区域,数据交换更加直接,但需要更好的同步机制来避免数据冲突。
当需要进行通信时,需要通过pipefd[1]文件描述符,将数据拷贝到管道文件中;再通过pipefd[0]文件描述符,将管道文件中的数据拷贝到用户空间中。因而,管道通信时,需要产生两次拷贝。
- 与命名管道(FIFO)相比,
我们简单测试一下返回的文件描述符
#include <iostream>
#include <string>
#include <cerrno> // errno.h
#include <cstring> // string.h
#include <unistd.h>
int main(){
//1.创建管道
int pipefd[2];
int n = pipe(pipefd);//输出型参数,rfd,wfd
if(n != 0){
std::cerr << "errno: " << errno << ": "<< "errstring : " << strerror(errno) << std::endl;
return 1;
}
std::cout << "pipefd[0]: " << pipefd[0] << ", pipefd[1]: " << pipefd[1] << std::endl;
return 0;
}
接下来让子进程写入数据,父进程读数据,在此期间会关闭不需要的fd
#include <iostream>
#include <string>
#include <cerrno> // errno.h
#include <cstring> // string.h
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
const int size = 1024;
std::string getOtherMessage()
{
static int cnt = 0;
std::string messageid = std::to_string(cnt); // stoi -> string -> int
cnt++;
pid_t self_id = getpid();
std::string stringpid = std::to_string(self_id);
std::string message = "messageid: ";
message += messageid;
message += " my pid is : ";
message += stringpid;
return message;
}
// 子进程进行写入
void SubProcessWrite(int wfd)
{
int pipesize = 0;
std::string message = "father, I am your son prcess!";
char c = 'A';
while (true)
{
std::string info = message + getOtherMessage(); // 这条消息,就是我们子进程发给父进程的消息
write(wfd, info.c_str(), info.size()); // 写入管道的时候,没有写入\0, 有没有必要?没有必要
std::cerr << info << std::endl;
sleep(2); // 子进程写慢一点
// write(wfd, &c, 1);
// std::cout << "pipesize: " << ++pipesize << " write charator is : "<< c++ << std::endl;
// // if(c == 'G') break;
// sleep(1);
}
std::cout << "child quit ..." << std::endl;
}
// 父进程进行读取
void FatherProcessRead(int rfd)
{
char inbuffer[size]; // c99 , gnu g99
while (true)
{
//sleep(2);
std::cout << "-------------------------------------------" << std::endl;
// sleep(500);
ssize_t n = read(rfd, inbuffer, sizeof(inbuffer) - 1); // sizeof(inbuffer)->strlen(inbuffer);
if (n > 0)
{
inbuffer[n] = 0; // == '\0'
std::cout << inbuffer << std::endl;
}
else if (n == 0)
{
// 如果read的返回值是0,表示写端直接关闭了,我们读到了文件的结尾
std::cout << "client quit, father get return val: " << n << " father quit too!" << std::endl;
break;
}
else if(n < 0)
{
std::cerr << "read error" << std::endl;
break;
}
// sleep(1);
break;
}
}
int main(){
//1.创建管道
int pipefd[2];
int n = pipe(pipefd);//输出型参数,rfd,wfd
if(n != 0){
std::cerr << "errno: " << errno << ": "<< "errstring : " << strerror(errno) << std::endl;
return 1;
}
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;
sleep(1);
// 子进程 --- write
// 3. 关闭不需要的fd
close(pipefd[0]);
SubProcessWrite(pipefd[1]);
close(pipefd[1]);
exit(0);
}
// 3. 父进程读入数据
// 关闭不需要的fd
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);
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;
}
管道的四种情况 和5种特征
特征一 匿名管道:只能用来进行具有血缘关系的进程之间,进行通信,常用于父子进程之间通信
情况一 如果管道内部是空的 并且 write fd没有关闭,读取条件不具备,读进程会被阻塞,解决方案,等待读取条件具备,也就是写入数据
特征二 管道内部,自带进程之间同步的机制–>多执行流执行代码的时候,具有明显的顺序性
我们让子进程写慢一点,父进程持续去读
void SubProcessWrite(int wfd){
int pipesize=0;
std::string message="father , I am your son process!";
char a='A';
while(true){
std::string info =message+getOtherMessage();//子进程发给父进程的信息
write(wfd,info.c_str(),info.size());//写入管道的时候,没有写入\0,有没有必要?没有必要
std::cerr<<info<<std::endl;
sleep(5);
}
std::cout<<"child quit..."<<std::endl;
}
//父进程进行读取
void FatherProcessRead(int rfd){
char inbuffer[size];//c99
while(true){
//sleep(2)
std::cout<<"-----------------------------"<<std::endl;
ssize_t n=read(rfd,inbuffer,sizeof(inbuffer)-1);//sizeof(inbuffer)->==strlen(inbuffer)
if(n>0){
inbuffer[n]=0;//=='\0'
std::cout<<inbuffer<<std::endl;
}
else if(n==0){
//如果read的返回值为0,表示写端关闭了,我们读到了文件的结尾
std::cout<<"client quit,father get return val:"<<n<<"father quit too!"<<std::endl;
break;
}
else if(n<0){
std::cerr<<"read error"<<std::endl;
break;
}
//break;
}
}
现象就是,每隔5秒打印一次,也就是父进程读取要和子进程一致,子进程慢了,父进程就要读慢点,子进程写一条,父进程读一条;在5s期间,父进程就在等待子进程写数据,也就是读进程被阻塞(这里的这个情况就好像之前我们的scanf)
情况二 管道被写满,并且父进程不读且不关闭;管道被写满会被阻塞,如果要恢复正常就需要父进程读取数据(让写条件具备)
我们接下来让子进程疯狂写,父进程不读
void SubProcessWrite(int wfd){
int pipesize=0;
std::string message="father , I am your son process!";
char a='A';
while(true){
write(wfd,&a,1);
std::cout<<"pipesize:"<<++pipesize<<"write charator is:"<<a++<<std::endl;
}
std::cout<<"child quit..."<<std::endl;
}
现象就是子进程写到第65536就一直卡在哪里,我们父进程一直在,但是不读,这里我们也就可以计算出管道文件的大小65536/1024 = 64kb,也就是ubuntu下22.04管道大小是64kb
情况三 管道一直在读但是写端关闭了,读端read返回值会读到0,表示读到了文件结尾
我们让子进程写一条语句就关闭,父进程一直读
void SubProcessWrite(int wfd){
int pipesize=0;
std::string message="father , I am your son process!";
char a='A';
while(true){
write(wfd,&a,1);
std::cout<<"pipesize:"<<++pipesize<<"write charator is:"<<a++<<std::endl;
break;
}
std::cout<<"child quit..."<<std::endl;
}
void FatherProcessRead(int rfd){
char inbuffer[size];//c99
//sleep(500);
while(true){
//sleep(2)
std::cout<<"-----------------------------"<<std::endl;
ssize_t n=read(rfd,inbuffer,sizeof(inbuffer)-1);//sizeof(inbuffer)->==strlen(inbuffer)
if(n>0){
inbuffer[n]=0;//=='\0'
std::cout<<inbuffer<<std::endl;
}
else if(n==0){
//如果read的返回值为0,表示写端关闭了,我们读到了文件的结尾
std::cout<<"client quit,father get return val:"<<n<<"father quit too!"<<std::endl;
//break;
}
else if(n<0){
std::cerr<<"read error"<<std::endl;
break;
}
//break;
}
}
情况四 读端直接关闭 写端一直写–>写端进程会被操作系统直接使用13号信号关掉,相当于进程出现了异常
void SubProcessWrite(int wfd){
int pipesize=0;
std::string message="father , I am your son process!";
while(true){
char a='A';
write(wfd,&a,1);
std::cout<<"pipesize:"<<++pipesize<<"write charator is:"<<a++<<std::endl;
//break;
// std::string info =message+getOtherMessage();//子进程发给父进程的信息
// write(wfd,info.c_str(),info.size());//写入管道的时候,没有写入\0,有没有必要?没有必要
// std::cerr<<info<<std::endl;
//sleep(5);
}
std::cout<<"child quit..."<<std::endl;
}
//父进程进行读取
void FatherProcessRead(int rfd){
char inbuffer[size];//c99
//sleep(500);
while(true){
//sleep(2)
std::cout<<"-----------------------------"<<std::endl;
ssize_t n=read(rfd,inbuffer,sizeof(inbuffer)-1);//sizeof(inbuffer)->==strlen(inbuffer)
if(n>0){
inbuffer[n]=0;//=='\0'
std::cout<<inbuffer<<std::endl;
}
else if(n==0){
//如果read的返回值为0,表示写端关闭了,我们读到了文件的结尾
std::cout<<"client quit,father get return val:"<<n<<"father quit too!"<<std::endl;
break;
}
else if(n<0){
std::cerr<<"read error"<<std::endl;
break;
}
break;
}
}
特征三 管道文件的生命周期是随进程的
特征四 管道文件在通信的时候,是面向字节流的,写入的次数和读取的次数不是一一匹配的
- 面向字节流的含义
- 当管道作为进程间通信的方式时,它是面向字节流的。这意味着数据在管道中是以字节序列的形式存在和传输的,没有固定的消息格式或记录边界。就像文件读写操作中的字节流一样,数据是连续的字节序列。
- 例如,一个进程可以将任意长度的字节数据写入管道,无论是一个字符、一个字符串,还是一个包含复杂数据结构的二进制数据块。接收进程看到的只是一串连续的字节,它需要根据预先约定的协议或数据格式来解析这些字节,以理解发送方传递的信息。
- 写入次数和读取次数不匹配的原因
- 数据量不一致:
- 写入进程可能一次写入大量的数据,而读取进程可以根据自己的处理能力和需求,分多次读取这些数据。例如,写入进程将一个1000字节的文件内容一次性写入管道,读取进程的缓冲区大小只有100字节,那么它就需要读取10次才能将管道中的数据全部读完。
- 相反,写入进程也可以分多次写入少量数据,而读取进程可以一次读取这些累积的数据。比如写入进程每次写入10字节,共写入10次,读取进程可以在管道中有足够数据后一次性读取100字节。
- 读写速度差异:
- 写入进程和读取进程的执行速度不同。如果写入进程写入数据的速度很快,而读取进程处理数据的速度较慢,那么在管道中就会积累数据。写入进程可能已经完成了多次写入操作,而读取进程还在处理之前写入的数据。
- 例如,在一个生产者 - 消费者模型中,生产者进程(写入进程)快速生产数据并写入管道,消费者进程(读取进程)可能因为某些复杂的处理(如数据解密、格式转换等)而缓慢地从管道中读取数据,导致写入和读取次数不匹配。
- 数据处理的灵活性需求:
- 读取进程可能根据实际情况灵活地决定读取的时机和数据量。比如,读取进程可能需要等待特定的条件满足后才开始读取数据,或者只读取管道中的部分数据用于当前的处理步骤,剩余的数据留在管道中等待后续读取。这种灵活性使得读取次数不一定与写入次数相对应。
- 数据量不一致:
- 示例说明
- 假设有一个管道用于在进程A和进程B之间通信。进程A向管道写入数据:
char data1[] = "Hello";
char data2[] = " World";
write(pipe_write_fd, data1, sizeof(data1));
write(pipe_write_fd, data2, sizeof(data2));
- 进程B从管道读取数据:
char buffer[20];
int n1 = read(pipe_read_fd, buffer, 5);
buffer[n1] = '\0';
printf("第一次读取: %s\n", buffer);
int n2 = read(pipe_read_fd, buffer, sizeof(buffer));
buffer[n2] = '\0';
printf("第二次读取: %s\n", buffer);
- 在这个例子中,进程A分两次写入数据,而进程B分两次读取数据,但读取的字节数和写入的字节数以及次数并不完全一致。进程B根据自己的需求和缓冲区大小来决定每次读取的数据量。这体现了管道在通信时面向字节流以及写入和读取次数不匹配的特点。
面向字节流:管道文件在通信时,数据是以字节为单位进行传输的。这意味着写入端可以一次写入多个字节,而读取端可以一次读取多个字节,或者可以分多次读取。
写入次数和读取次数不是一一匹配的:由于管道是半双工的,写入端和读取端的数据传输不是同步进行的。这意味着,写入端可能已经写入了多个字节,而读取端还没有开始读取,或者读取端已经读取了部分数据,而写入端还在继续写入。
数据传输是异步的:管道通信是异步的,这意味着写入端和读取端之间的数据传输不一定是连续的。写入端可以写入数据,然后继续执行其他操作,而读取端可以等待数据准备好后再读取。
数据缓冲:管道内部通常有一个缓冲区,用于存储写入端写入的数据。当读取端开始读取时,它会从缓冲区中读取数据。如果缓冲区满了,写入端可能会阻塞,直到有空间可用。如果缓冲区空了,读取端可能会阻塞,直到有数据可读。
管道通信的完整性:尽管写入次数和读取次数不是一一匹配的,但管道通信的完整性得到了保证。写入端写入的数据最终会被读取端读取,反之亦然。
特征五 管道的通信模式,是一种特殊的半双工模式
- 半双工模式(Half - Duplex)
- 定义:半双工通信模式是指数据可以在两个方向上进行传输,但不能同时进行。在某一时刻,通信通道只能用于发送或者接收数据。就好像是一条单车道的道路,车辆(数据)可以往返行驶,但同一时间只能朝着一个方向。
- 工作原理:
- 以使用半双工模式通信的设备为例,比如对讲机。当一方按下通话按钮(开始发送数据)时,设备进入发送状态,此时接收功能暂时关闭。发送方的声音(数据)通过对讲机的通信频道发送出去,而接收方只能等待发送方结束发送后,才能按下自己的通话按钮进行回复(发送数据)。
- 在网络通信或计算机进程间通信的半双工管道中,数据的流向也是如此。例如,匿名管道(用于父子进程通信)通常是半双工的。当父进程向管道写入数据时,管道处于写入状态,此时子进程不能同时从同一管道写入数据,必须等待父进程完成写入并且管道状态切换到可读状态后,子进程才能从管道读取数据。
- 应用场景:
- 在一些简单的通信系统中比较常见,如早期的以太网(采用同轴电缆共享介质)就使用半双工通信。多个设备连接到同一条电缆上,设备在发送数据时会占用整个通信介质,其他设备需要等待它发送完成后才能发送自己的数据。
- 在一些资源受限或者对通信实时性要求不是特别高的进程间通信场景中也有应用。比如简单的传感器与控制器之间的通信,传感器采集数据后发送给控制器,控制器处理完数据后再发送指令给传感器,两者交替进行通信,半双工模式就可以满足需求。
- 全双工模式(Full - Duplex)
- 定义:全双工通信模式允许数据同时在两个方向上传输。可以将其想象成一条双向多车道的高速公路,车辆(数据)可以同时在两个方向上行驶,互不干扰。
- 工作原理:
- 在电话通信中就是典型的全双工模式。通话双方可以同时说话和聆听,声音(数据)在两个方向上同时传输。这是因为通信线路被设计成可以同时处理两个方向的信号。
- 在计算机网络的全双工以太网连接中,设备有独立的发送和接收线路,或者通过复杂的信号处理技术,可以在同一时间进行数据的发送和接收。在进程间通信中,一些通信机制也支持全双工,如消息队列(通过合理设计可以实现双向通信)和套接字(在TCP协议下支持全双工通信)。
- 应用场景:
- 对于需要同时进行双向、大量数据传输的场景非常关键。例如,在网络视频会议中,参会者既要发送自己的视频和音频数据,又要同时接收其他参会者的视频和音频数据,就需要全双工通信来保证会议的正常进行。
- 在分布式系统中,服务器和多个客户端之间的复杂交互也经常需要全双工通信。比如,数据库服务器既要接收客户端的查询请求和数据更新请求,又要同时向客户端发送查询结果和状态信息,全双工通信模式能够高效地支持这种复杂的交互。
总结来说,半双工通信需要轮流发送和接收数据,而全双工通信可以同时进行双向通信。全双工通信通常更高效,因为它允许多个设备或通道同时工作,而不需要等待。