进程间通信:管道

54 阅读9分钟

1. 进程间通信介绍

1-1 进程间通信目的

• 数据传输:一个进程需要将它的数据发送给另一个进程

• 资源共享:多个进程之间共享同样的资源。

• 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

• 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

1-2 进程间通信发展

• 管道

• System V进程间通信

• POSIX进程间通信

2. 管道

什么是管道

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

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

进程间通信的前提是让不同的进程看到同一份资源

父子进程向同一标准输出个打印本质就是显示器文件被共享了

管道的定义:

管道是一个基于文件系统的一个内存级的单向通信文件,主要用来进程间通信(IPC)

3. 匿名管道

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

3-1代码实例

#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
    int fd[2] = {0};
    // 创建匿名管道
    int ret = pipe(fd);
    if (ret != 0)
        perror("pipe:");
    pid_t id = fork();
    if (id < 0)
        perror("fork:");
    else if (id == 0)
    {
        // child write
        // 子进程关闭管道读端
        close(fd[0]);
        int cnt = 0;
        while (cnt < 10)
        {
            sleep(1);
            char c = 'a' + cnt;
            cnt++;
            write(fd[1], &c, 1);
        }
    }
    else
    {
        // father read
        // 父进程关闭管道写端
        close(fd[1]);

        while (1)
        {
            char c;
            ssize_t n = read(fd[0], &c, 1);
            if (n < 0)
                perror("read error:");
            else if (n > 0)
            {
                cout << "父进程写:" << c << endl;
            }
            else if (n == 0)
            {
                // 文件读完
                cout << "文件读完" << endl;
                break;
            }
        }
    }
    // // 休眠5秒后,父进程关闭读端,看子进程会如何(此时子进程仍然在写)
    // sleep(5);
    // // 父进程关闭读端,此时管道只有子进程的写端
    // close(fd[0]);
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if (rid > 0)
    {
        int exitcode = (status >> 8) & 0xFF;
        int exitsig = (status & 0x7F); // 13
        cout << "wait success, rid: " << rid << ", exit code: " << exitcode << ", exitsig: " << exitsig << endl;
    }

    return 0;
}

3-2 用 fork 来共享管道原理

匿名管道是一个内存级的文件,没有名字,不在磁盘上,因此其文件缓冲区上的内容就不需要向磁盘刷新

父进程fork后子进程继承了父进程的匿名管道,此时父子进程可以看到同一份资源

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

父进程关闭fd[0],子进程关闭fd[1],此时就形成了一个单向通信的管道

父子进程关闭对应的不需要读写端是为了防止误触发

3-4读写规则

1. 单向通信

管道是半双工的,数据只能在一个方向上流动。一个进程写,另一个进程读。如果需要双向通信,必须创建两个管道。

2. 内核缓冲区

管道拥有一个由内核维护的缓冲区。写入者将数据写入这个缓冲区,读取者从同一个缓冲区读取数据。一旦数据被读取,它就会从管道中消失。缓冲区有大小限制。在 Linux 上,默认通常是64KB。

3. 原子性

当要写入的数据量小于等于 PIPE_BUE(Linux 上通常是 4096 字节)时,写入操作是原子的

这意味着多个进程同时向同一个管道写入小于PIPE_BUE 的数据块时,这些数据块不会相互穿插,保证了数据的完整性。

如果写入的数据量大于 PIPE_BUE,那么内核可能会将数据与其它进程写入的数据交织在一起,即非原子操作。

4. 生命周期

管道的生命周期与其关联的进程绑定。当所有指向管道读端和写端的文件描述符都被关闭后,管道会被内核销毁,其占用的资源也会被释放

3-5验证管道通信的4种情况

• 读正常,读端不读&&写满

情况描述

  • 写进程一直写使管道缓冲区已满
  • 写进程尝试继续写入
  • 读进程正常,不关闭读端但也不读取数据

结果

  • 写进程阻塞,直到读进程读取数据腾出缓冲区空间
  • 管道有最大容量限制(通常64KB),即写进程一次最多向文件缓冲区中写入64KB的数据
#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
    int fd[2] = {0};
    // 创建匿名管道
    int ret = pipe(fd);
    if (ret != 0)
        perror("pipe:");
    pid_t id = fork();
    if (id < 0)
        perror("fork:");
    else if (id == 0)
    {
        // child write
        // 子进程关闭管道读端
        close(fd[0]);
        int cnt = 0;
        while (true)
        {
            //sleep(1);
            int c = cnt;
            cnt++;
            write(fd[1], &c, 1);
            cout<<"子进程写:"<<c<<endl;
        }
    }
    else
    {
        // father read
        // 父进程关闭管道写端
        close(fd[1]);
        //父进程正常,但不关闭读端也不读取数据
    }
    return 0;
}

write写入的数据是字节流形式的,即一个一个字符

可以看到,子进程从0开始一直写入数据,直到写到65535时阻塞到那里 65536/1024=64

• 写正常&&读空

情况描述

  • 管道缓冲区为空
  • 读进程尝试读取
  • 写进程正常写入(但尚未写入)

结果

  • 读进程阻塞,直到写进程写入数据
#include <unistd.h>
#include <stdio.h>
#include<stdlib.h>
int main() {
    int fd[2];
    pipe(fd);
    
    if (fork() == 0) {
        // 子进程 - 读进程(立即读取)
        close(fd[1]);
        char buf[100];
        printf("Reading from empty pipe...\n");
        int n = read(fd[0], buf, sizeof(buf));
        printf("Read %d bytes\n", n);
        exit(0);
    } else {
        // 父进程 - 写进程(延迟写入)
        close(fd[0]);
        sleep(3);  // 延迟3秒写入
        write(fd[1], "Data", 5);
        printf("Data written\n");
    }
    return 0;
}

• 写关闭&&读正常

情况描述

  • 所有写端文件描述符已关闭
  • 读进程继续读取

结果

  • 读进程读取完缓冲区所有数据后,read返回0(文件结束)
  • 不会阻塞等待新数据
#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include<stdlib.h>
using namespace std;
int main()
{
    int fd[2] = {0};
    // 创建匿名管道
    int ret = pipe(fd);
    if (ret != 0)
        perror("pipe:");
    pid_t id = fork();
    if (id < 0)
        perror("fork:");
    else if (id == 0)
    {
        // child write
        // 子进程关闭管道读端
        close(fd[0]);
        int cnt = 0;
        while (cnt<10)
        {
            //子进程写
            char c = 'a'+cnt;
            cnt++;
            write(fd[1], &c, 1);
        }
        //子进程退出,写端关闭
        exit(0);
    }
    else
    {
        // father read
        // 父进程关闭管道写端
        close(fd[1]);

        while (1)
        {
            char c;
            ssize_t n = read(fd[0], &c, 1);
            if (n < 0)
                perror("read error:");
            else if (n > 0)
            {
                cout << "子进程写:" << c << endl;
            }
            else if (n == 0)
            {
                // 文件读完
                cout << "文件读完" <<"read返回值:"<<n<< endl;
                break;
            }
        }
    }
    
    return 0;
}

• 读关闭&&写正常

情况描述

  • 所有读端文件描述符已关闭
  • 写进程继续写入

结果

  • 写进程收到 SIGPIPE 信号(默认终止进程)
  • 如果忽略SIGPIPE,write返回-1,errno=EPIPE
#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
    int fd[2] = {0};
    // 创建匿名管道
    int ret = pipe(fd);
    if (ret != 0)
        perror("pipe:");
    pid_t id = fork();
    if (id < 0)
        perror("fork:");
    else if (id == 0)
    {
        // child write
        // 子进程关闭管道读端
        close(fd[0]);
        int cnt = 0;
        while (cnt < 10)
        {
            sleep(1);
            char c = 'a' + cnt;
            cnt++;
            write(fd[1], &c, 1);
        }
    }
    else
    {
        // father read
        // 父进程关闭管道写端
        close(fd[1]);
    }
    // 休眠5秒后,父进程关闭读端,看子进程会如何(此时子进程仍然在写)
    sleep(5);
    // 父进程关闭读端,此时管道只有子进程的写端
    close(fd[0]);
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if (rid > 0)
    {
        int exitcode = (status >> 8) & 0xFF;
        int exitsig = (status & 0x7F); // 13号信号SIGPIPE 信号
        cout << "wait success, rid: " << rid << ", exit code: " << exitcode << ", exitsig: " << exitsig << endl;
    }

    return 0;
}

注意:匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;

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

4. 命名管道

• 匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。

• 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。

命名管道:是一种特殊的文件类型,它在文件系统中有一个文件名,但不存储实际数据在磁盘上。数据仍然在内核的缓冲区中流动。

4-1 创建一个命名管道

• 命名管道可以从命令行上创建,命令行方法是使用下面这个命令:

mkfifo filename

命名管道也可以从程序里创建,相关函数有:

int mkfifo(const char *filename,mode_t mode);

4-2 匿名管道与命名管道的区别

• 匿名管道由pipe函数创建并打开。

• 命名管道由mkfifo函数创建,打开用open

• FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。

命名管道主要解决毫无关系的进程之间文件级通信的问题

4-3用命名管道实现server&client通信

//server.cpp
#include<iostream>
#include<sys/stat.h>
#include<sys/types.h>
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
#include<string>
#include<cstring>
using namespace std;
int main()
{
    int ret=mkfifo("np",0666);
    if(ret<0)
    {
        perror("mkfifo:");
        return 1;
    }
    int fd=open("np",O_WRONLY);
    if(fd<0)
    {
        perror("open:");
        return 1;
    }
    char a[100]={0};
    int n = 0;
    while((n = read(0,a,100)) > 0)
    {
        a[n]=0;
        //server写
        write(fd,a,n);
    }
    return 0;
}
//client.cpp
#include<iostream>
#include<sys/stat.h>
#include<sys/types.h>
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
#include<string>
#include<cstring>
using namespace std;
int main()
{
    
    int fd=open("np",O_RDONLY);
    if(fd<0)
    {
        perror("open:");
        return 1;
    }
    char a[100]={0};
    int n = 0;
    //client读
    while((n = read(fd,a,100)) > 0)
    {
        a[n]=0;
       cout<<a;
    }
    return 0;
}
//Makefile
.PHONY:all
all:server client

server:server.cpp
	g++ -o $@ $^ -std=c++11 -g

client:client.cpp
	g++ -o $@ $^ -std=c++11 -g

.PHONY:clean
clean:
	rm -f client
	rm -f server

到此,进程间通信:管道就讲完了,怎么样,是不是感觉大脑里面多了很多新知识。

如果觉得博主讲的还可以的话,就请大家多多支持博主,收藏加关注,追更不迷路

如果觉得博主哪里讲的不到位或是有疏漏,还请大家多多指出,博主一定会加以改正

博语小屋将持续为您推出文章