Linux进程间通信(二)之管道1【匿名管道】

管道

什么是管道

管道是Unix中最古老的进程间通信的形式。

我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

image-20250408220824153

| 就是管道 wc 统计有几行

image-20250408221952170

匿名管道

#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

image-20250408220856534

用fork来共享管道原理

image-20250407152119049

image-20250408231241349

站在文件描述符角度-深度理解管道

父进程把文件打开两次(分别以r/w方式打开)

image-20250407152150066

站在内核角度-管道本质

image-20250407152203174

image-20250408233732261

所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件”思想。

如果两个进程没有关系就不能使用以上的原理进行通信!!!!

进程间必须是父子关系、兄弟关系、爷孙关系……!!

具有血缘关系的进程才能用上面原理进行通信。(常用于父子)

以上的工作是建立通信信道,还没有进行通信!

– 为什么建立信道的过程这么费劲?因为进程具有独立性,进程间通信是有成本的!

接口

open 打开的是磁盘级文件

pipe 打开的是内存级文件,以读/写方式把内存级文件打开。

image-20250408235123867

返回值

image-20250408235241954

参数

pipefd -> 是一个只有两个int型元素的数组
pipefd -> 还是一个输出型参数!
分别以读/写方式打开的文件的文件描述符数字带出来,让用户使用!
不考虑其他,默认情况下,是3和4

image-20250408235335781

pipefd[0] : 读下标
pipefd[1] : 写下标
实例代码

创建文件

image-20250409131205995

makefile

testpipe:testpipe.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -rf testpipe

验证 pipefd 的两个参数为3和4:

testpipe.cc

#include <iostream>
#include <unistd.h>
using namespace std;

#define N 2
int main()
{
    int pipefd[N]={0};
    int n=pipe(pipefd);
    if(n<0)return 1;

    cout<<"pipefd[0]="<<pipefd[0]<<" , pipefd[1]="<<pipefd[1]<<endl;

    return 0;
}

image-20250409131748871

c语言的接口:安全格式化的接口

把按照格式的内容(size大小的)写到字符串里。

image-20250409140225598

testpipe.cc

#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <string>
#include <cstdio>
#include <string.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
#define N 2
#define NUM 1024

//child
void Writer(int wfd)
{
    string s="hello,I am child";
    pid_t id=getpid();
    int number=0;

    char buffer[NUM];
    while(true)
    {
        buffer[0]=0;//字符串清空,只是为了提醒阅读代码的人,把这个数组当祖字符串了。
        snprintf(buffer,strlen(buffer),"%s-%d-%d",s.c_str(),id,number++);
        write(wfd,buffer,strlen(buffer));
        //不需要+1,管道是文件!c语言的规定与文件无关!只需要把内容写入!
        sleep(1);
    }
}

//father
void Reader(int rfd)
{
    char buffer[NUM];
    while(true)
    {
        buffer[0]=0;
        ssize_t n=(rfd,buffer,sizeof(buffer));//sizeof!=strlen
        //sizeof代表缓冲区的大小
        if(n>0)
        {
            buffer[n]=0;//把buffer当字符串使用。0=='\0'
            cout<<"father get a message["<<getpid()<<"]# "<<buffer<<endl;
        }
    }
}

int main()
{
    int pipefd[N]={0};
    int n=pipe(pipefd);
    if(n<0)return 1;

    // cout<<"pipefd[0]="<<pipefd[0]<<" , pipefd[1]="<<pipefd[1]<<endl;

    // child->w    father->r
    pid_t id=fork();
    if(id<0)return 2;
    if(id==0)
    {
        //child
        close(pipefd[0]);
        //IPC code
        Writer(pipefd[1]);
        close(pipefd[1]);
        exit(0);
    }
    //father
    close(pipefd[1]);
    //IPC code
    Reader(pipefd[0]);

    pid_t rid=waitpid(id,nullptr,0);
    if(rid<0)return 3;
    close(pipefd[0]);

    return 0;
}

image-20250409142335606

经历了几次拷贝?

写:buffer(用户缓冲区) -> 系统缓冲区
读:系统缓冲区 -> buffer(用户缓冲区)

更新完善后的代码:

#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <string>
#include <cstdio>
#include <string.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
#define N 2
#define NUM 1024

//child
void Writer(int wfd)
{
    string s="hello,I am child";
    pid_t id=getpid();
    int number=0;

    char buffer[NUM];
    while(true)
    {
        sleep(1);
        // buffer[0]=0;//字符串清空,只是为了提醒阅读代码的人,把这个数组当祖字符串了。
        // snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),id,number++);
        //资源是系统提供的,所以必须使用系统调用接口,system call
        // write(wfd,buffer,strlen(buffer));
        char c='a';
        write(wfd,&c,1);
        //不需要+1,管道是文件!c语言的规定与文件无关!只需要把内容写入!
        // sleep(1);
        number++;
        cout<<number<<endl;
        if(number>=5)
        {
            break;
        }
    }
}

//father
void Reader(int rfd)
{
    char buffer[NUM];
    while(true)
    {
        buffer[0]=0;
        ssize_t n=read(rfd,buffer,sizeof(buffer));//sizeof!=strlen
        //sizeof代表缓冲区的大小
        if(n>0)
        {
            buffer[n]=0;//把buffer当字符串使用。0=='\0'
            cout<<"father get a message["<<getpid()<<"]# "<<buffer<<endl;
        }
        else if(n==0)
        {
            cout<<"father read done!\n"<<endl;
            break;
        }
        else break;
        // cout<<"n : "<<n<<endl;
    }
}

int main()
{
    int pipefd[N]={0};
    int n=pipe(pipefd);
    if(n<0)return 1;

    // cout<<"pipefd[0]="<<pipefd[0]<<" , pipefd[1]="<<pipefd[1]<<endl;

    // child->w    father->r
    pid_t id=fork();
    if(id<0)return 2;
    if(id==0)
    {
        //child
        close(pipefd[0]);
        //IPC code
        Writer(pipefd[1]);
        close(pipefd[1]);
        exit(0);
    }
    //father
    close(pipefd[1]);
    //IPC code
    Reader(pipefd[0]);

    pid_t rid=waitpid(id,nullptr,0);
    if(rid<0)return 3;
    close(pipefd[0]);

    sleep(5);

    return 0;
}

image-20250409202245121

管道特点
  1. 具有亲缘关系的进程之间进行通信

    通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。

  2. 管道只能进行单向通信

    管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

  3. 管道通信需要让不同的进程看到同一份资源,多执行流是共享的,难免会出现访问冲突的问题。

临界资源竞争。父子进程是会进行协同的,内核会对管道操作进行同步与互斥。

– 保护管道文件的数据安全。

  1. 管道是面向字节流的。管道提供流式服务。

不管写了多少次,是以字节的方式一次就读完。

读端不管格式,读端看来就是一个一个的字节。(由上层区分内容)

  1. 管道是基于文件的,而文件的生命周期是随进程的!

如果进程退出了,文件会怎么办?

文件会被操作系统自动回收。

两个进程都退出了,管道就会被操作系统回收释放,所以管道的生命周期随进程。

管道的4种情况
  1. 读写端正常,写很慢,读很快,管道为空,读端就要阻塞。
  2. 读写端正常,写很快,读很慢,管道为满,写端就要阻塞。

​ 写完之后打印number

read读之前先sleep(5)

void Writer(int wfd)
{
    string s="hello,I am child";
    pid_t id=getpid();
    int number=0;

    char buffer[NUM];
    while(true)
    {
        buffer[0]=0;//字符串清空,只是为了提醒阅读代码的人,把这个数组当祖字符串了。
        snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),id,number++);
        //资源是系统提供的,所以必须使用系统调用接口,system call
        write(wfd,buffer,strlen(buffer));
        //不需要+1,管道是文件!c语言的规定与文件无关!只需要把内容写入!
        // sleep(1);
        cout<<number<<endl;
    }
}

//father
void Reader(int rfd)
{
    char buffer[NUM];
    while(true)
    {
        sleep(5);
        buffer[0]=0;
        ssize_t n=read(rfd,buffer,sizeof(buffer));//sizeof!=strlen
        //sizeof代表缓冲区的大小
        if(n>0)
        {
            buffer[n]=0;//把buffer当字符串使用。0=='\0'
            cout<<"father get a message["<<getpid()<<"]# "<<buffer<<endl;
        }
    }
}

image-20250409144718367

​ 很明显,管道是有大小的!

  1. 读端正常,写端关闭,读端就会读到0,表明读到了文件(pipe)结尾,不会被阻塞。

    //child
    void Writer(int wfd)
    {
        string s="hello,I am child";
        pid_t id=getpid();
        int number=0;
    
        char buffer[NUM];
        while(true)
        {
            char c='a';
            write(wfd,&c,1);
            number++;
            cout<<number<<endl;
            if(number>=5)
            {
                break;
            }
        }
    }
    
    //father
    void Reader(int rfd)
    {
        char buffer[NUM];
        while(true)
        {
            sleep(1);
            buffer[0]=0;
            ssize_t n=read(rfd,buffer,sizeof(buffer));//sizeof!=strlen
            //sizeof代表缓冲区的大小
            if(n>0)
            {
                buffer[n]=0;//把buffer当字符串使用。0=='\0'
                cout<<"father get a message["<<getpid()<<"]# "<<buffer<<endl;
            }
        }
    }
    

    image-20250409160459624

    image-20250409160521339

    image-20250409160833364

  2. 写端正常,读端关闭。操作系统就要杀掉正在写入的进程。通过13号信号杀掉。

操作系统是不会做低效、浪费等类似的工作的。如果做了就是系统bug

验证:

#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <string>
#include <cstdio>
#include <string.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
#define N 2
#define NUM 1024

//child
void Writer(int wfd)
{
    string s="hello,I am child";
    pid_t id=getpid();
    int number=0;

    char buffer[NUM];
    while(true)
    {
        sleep(1);
        buffer[0]=0;//字符串清空,只是为了提醒阅读代码的人,把这个数组当祖字符串了。
        snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),id,number++);
        //资源是系统提供的,所以必须使用系统调用接口,system call
        write(wfd,buffer,strlen(buffer));
        // char c='a';
        // write(wfd,&c,1);
        //不需要+1,管道是文件!c语言的规定与文件无关!只需要把内容写入!
        // sleep(1);
        // number++;
        // cout<<number<<endl;
        // if(number>=5)
        // {
        //     break;
        // }
    }
}

//father
void Reader(int rfd)
{
    char buffer[NUM];
    int cnt=0;
    while(true)
    {
        buffer[0]=0;
        ssize_t n=read(rfd,buffer,sizeof(buffer));//sizeof!=strlen
        //sizeof代表缓冲区的大小
        if(n>0)
        {
            buffer[n]=0;//把buffer当字符串使用。0=='\0'
            cout<<"father get a message["<<getpid()<<"]# "<<buffer<<endl;
        }
        else if(n==0)
        {
            cout<<"father read done!\n"<<endl;
            break;
        }
        else break;
        // cout<<"n : "<<n<<endl;

        cnt++;
        if(cnt>5)break;
    }
}

int main()
{
    int pipefd[N]={0};
    int n=pipe(pipefd);
    if(n<0)return 1;

    // cout<<"pipefd[0]="<<pipefd[0]<<" , pipefd[1]="<<pipefd[1]<<endl;

    // child->w    father->r
    pid_t id=fork();
    if(id<0)return 2;
    if(id==0)
    {
        //child
        close(pipefd[0]);
        //IPC code
        Writer(pipefd[1]);
        close(pipefd[1]);
        exit(0);
    }
    //father
    close(pipefd[1]);
    //IPC code
    Reader(pipefd[0]);
    close(pipefd[0]);
    cout<<"father close read fd:"<<pipefd[0]<<endl;
    sleep(5);

    int status=0;
    pid_t rid=waitpid(id,&status,0);
    if(rid<0)return 3;

    cout<<"wait child success:"<<rid<<" ,exit code: "<<((status>>8)&0xFF)<<" ,exit signal: "<<((status&0x7F))<<endl;


    sleep(5);

    cout<<"father quit"<<endl;

    return 0;
}

image-20250410170243599

image-20250410170452130

管道读写规则

管道大小

在不同内核里,管道大小有差别。

//child
void Writer(int wfd)
{
    string s="hello,I am child";
    pid_t id=getpid();
    int number=0;

    char buffer[NUM];
    while(true)
    {
        char c='a';
        write(wfd,&c,1);
        number++;
        cout<<number<<endl;
    }
}

//father
void Reader(int rfd)
{
    char buffer[NUM];
    while(true)
    {
        sleep(50);
        buffer[0]=0;
        ssize_t n=read(rfd,buffer,sizeof(buffer));//sizeof!=strlen
        //sizeof代表缓冲区的大小
        if(n>0)
        {
            buffer[n]=0;//把buffer当字符串使用。0=='\0'
            cout<<"father get a message["<<getpid()<<"]# "<<buffer<<endl;
        }
    }
}

image-20250409154709093

由此可知管道的大小是65536字节 也就是64KB

image-20250409155206792

PIPE_BUF 单次向管道写入的大小。

当没有数据可读时

O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。

O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。

当管道满的时候

O_NONBLOCK disable: write调用阻塞,直到有进程读走数据

O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

如果所有管道写端对应的文件描述符被关闭,则read返回0

如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出

当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。

原子性:要读就把规定的数据全部读走,要么就不读。

当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

image-20250409153705790

这里的pipe size 就是 PIPE_BUF (4KB)

应用场景

管道与我们之前学的知识哪些是有关系的呢?

1.cat test.txt | head -10 | tail -5

image-20250410171202620

这批 sleep 进程具有血缘关系!创建两个管道,三个进程,然后输入输出重定向。

命令行 | 底层就是pipe。

以上就是匿名管道。

  1. 自定义shell – 我们想让我们的shell支持 | 管道,代码该如何写。

    1.分析输入的命令字符串,获取有几个|,分割出多个子命令字符串
    2.malloc申请空间,pipe先申请多个管道
    3.循环创建多个子进程,分析每一个子进程的重定向情况。
    	a.首个:就是输出重定向,1->指定的管道写端
    	b.中间:输入输出重定向,0标准输入重定向到上一个管道的读端 1标准输出重定向到下一个管道的写端
    	c.末尾:输入重定向,0标准输入重定向到最后一个管道的读端。
    4.分别让不同的子进程执行不同的命令 -- exec* -- exec* 不会影响该进程曾经打开的文件,不会影响预先设置好的管道重定向。
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值