进程间通信---管道

进程间通信


概念

进程间通信就是在不同进程之间传播或交换信息数据, 简称IPC(Interprocess communication).

意义
  • 数据传输, 资源共享
  • 事件通知, 进程控制
本质

让不同的进程看到同一份资源

管道


匿名管道

原理:

匿名管道仅限于本地父子进程之间的通信, 本质就是让两个父子进程先看到同一份被打开的文件资源, 然后父子进程就可以对该文件进行写入或是读取操作, 进而实现进程间通信.

pipe函数:

  • int pipe(int pipefd[2]); // #include <unistd.h>
  • pipefd: 输出型参数, 数组pipefd用于返回两个指向管道读端和写端的文件描述符. pipefd[0]是管道读端的文件描述符, pipefd[1]是管道写端的文件描述符.
  • return int: pipe函数调用成功时返回0, 调用失败时返回-1.
  • 注意: 管道的信道是半双工的.

使用(父进程读, 子进程写为例):

  1. 父进程通过调用pipe函数创建管道
  2. 父进程创建子进程
  3. 父进程关闭写端,子进程关闭读端
// 父进程读子进程写---父进程关闭写端,子进程关闭读端 
static void test_pipe1()
{
    // 1.父进程先调用pipe函数,创建匿名管道获取读写端的文件描述符
    int pipefd[2];
    memset(pipefd,0,sizeof(pipefd));

    int ret=pipe(pipefd);
    if(ret!=0)
    {
        perror("pipe");
        exit(1);
    }

    // 2.父进程调用fork创建子进程, 在进程创建中子进程会继承父进程的struct file, 
    // 所以父子进程的pipefd中文件描述符会标定同一个匿名管道
    pid_t id=fork();
    if(id<0)
    {
        perror("fork");
        exit(2);
    }
    
    // 3.因为管道通信是半双工的, 所以需要根据需求关闭父子进程各一个fd

    if(0==id) 
    {
        // child code
        close(pipefd[0]);
        
        int n=10;
        while(n--)
        {
            char buffer[1024];
            sprintf(buffer,"hello father, I am child: %d",n);
            write(pipefd[1],buffer,strlen(buffer));
                
            sleep(1);
        }

        close(pipefd[1]);
        exit(0);
    }

    // father code
    close(pipefd[1]);
    
    while(true)
    {
        char buffer[1024];
        memset(buffer,0,sizeof(buffer));

        ssize_t n=read(pipefd[0],buffer,sizeof(buffer)-1);
        if(n>0)
        {
            buffer[n]='\0';

            printf("chlid# %s\n",buffer);
        }
        else if(0==n)
        {
            printf("child exit");
            break;
        }
        else 
        {
            perror("read");
            exit(3);
        }
    }

    close(pipefd[0]);
    exit(0);
}

int main(int argc,char *argv[],char *env[])
{
    test_pipe1();

    return 0;
}

运行结果:

image-20230420192048772

四种特殊情况:

  1. 写端进程不写, 读端进程一直读, 那么此时会因为管道里面没有数据可读, 对应的读端进程会被挂起, 直到管道里面有数据后, 读端进程才会被唤醒.

  2. 读端进程不读, 写端进程一直写, 那么当管道被写满后, 对应的写端进程会被挂起, 直到管道当中的数据被读端进程读取后, 写端进程才会被唤醒.

  3. 写端进程将数据写完后将写端关闭, 那么读端进程将管道当中的数据读完后, 就会继续执行该进程之后的代码逻辑, 而不会被挂起.

  4. 读端进程将读端关闭, 而写端进程还在一直向管道写入数据, 那么操作系统会通过发送SIGPIPE信号给写端进程从而将写端进程杀掉.

验证第4种情况, 当写端进程一直往匿名管道写时, 读端进程把匿名管道对应的读取文件描述符关闭, 写端进程会收到什么信号(父进程写, 子进程读为例):

void sigpipe_handler(int signum)
{
    printf("收到信号: %d\n",signum);
    exit(4);
}

// 父进程写子进程读---父进程关闭读端,子进程关闭写端
static void test_pipe2()
{
    // 1.pipe
    int pipefd[2];
    int ret=pipe(pipefd);
    if(ret<0)
    {
        perror("pipe");
        exit(1);
    }

    // 2.fork
    pid_t id=fork();
    if(id<0)
    {
        perror("fork");
        exit(2);
    }

    if(0==id)
    {
        // child code
        close(pipefd[1]); // 子进程关闭写端

        int n=3;
        while(n--)
        {
            char buffer[1024];

            ssize_t n=read(pipefd[0],buffer,sizeof(buffer)-1);
            if(n>0)
            {
                buffer[n]='\0';
                printf("father# %s\n",buffer);

                // sleep(1);
            }
            else if(0==n) 
            {
                printf("father close\n");
                exit(0);
            }
            else
            {
                perror("read");
                exit(3);
            }
        }

        close(pipefd[0]);
        exit(0);
    }

    // father code
    close(pipefd[0]); // 父进程关闭读端

    // 父进程对SIGPIPE信号进行捕获
    signal(SIGPIPE,sigpipe_handler);

    // 父进程一直往匿名管道中写入数据
    int count=0;
    while(true)
    {
        char buffer[1024];
        sprintf(buffer,"I am father, pid: %d, count %d\n",getpid(),count++);

        ssize_t n=write(pipefd[1],buffer,strlen(buffer));
        if(n>0)
        {
            // printf("write n>0: n: %d\n",n);
            usleep(5000); // 父进程写慢点
        }   
        else if(0==n)
        {
            printf("write 0==n: n: %d\n",n);
            break;
        }
        else
        {
            perror("write");
            exit(5);
        }
    }

    close(1);
    exit(0);
}

运行结果:

image-20230420201150260

测试管道的大小:

// 测试管道的大小,父进程一直不读,子进程一直写
static void test_pipe_size()
{
    // 1.pipe
    int pipefd[2];
    int ret = pipe(pipefd);
    if (ret < 0)
    {
        perror("pipe");
        exit(1);
    }

    // 2.fork
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork");
        exit(2);
    }

    if (0 == id)
    {
        // child code
        close(pipefd[0]); // 子进程关闭写端
        // 子进程一直往匿名管道中写
        
        int count = 0; // 记录写入了多少个字节数
        while (true)
        {
            char one_byte = '$';
            ssize_t n = write(pipefd[1], &one_byte, 1);

            if (n > 0)
            {
                printf("count: %d bytes\n", ++count);
            }
        }

        close(pipefd[1]);
        exit(0);
    }

    // father code
    close(pipefd[1]); // 父进程关闭读端
    // 父进程一直不从匿名管道中读
    for(;;){}

    close(pipefd[0]);
    exit(0);
}

结论:我当前Linux版本(Linux VM-12-12-centos 3.10.0-1160.62.1.el7.x86_64 #1 SMP Tue Apr 5 16:57:59 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux)中管道的最大容量是65536字节. 当管道写满时, 写端进程继续写则会进入阻塞状态.

image-20230420203123618

命名管道

原理:

想要实现本地任意两个进程之间的通信, 可以通过命名管道来实现. 命名管道是一种特殊类型的文件(管道文件), 两个进程通过命名管道的文件路径打开同一个管道文件, 此时这两个进程也就看到了同一份资源,进而就可以进行进程间通信了。

mkfifo函数:

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

pathname: 根据传入pathname所指定的目录下创建命名管道文件, 默认在当前进程的工作目录下创建.

  1. /home/yx/code/ipc_fifo/fifofile: 表示在/home/yx/code/ipc_fifo目录下创建fifofile文件
  2. fifofile: 表示在默认目录下创建fifofile文件

mode: 表示创建命名管道文件的默认权限, 受usmask权限掩码影响, 实际权限=mode&(~umask);

return int: 创建成功返回0, 创建失败返回-1.

使用:

comm.h

#ifndef __COMM_H__
#define __COMM_H__

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

const char * FIFOFILEPATH="fifofile";
const int DEFAULTMODE=0666;
const int BUFFERSIZE=1024;

#endif

fifoServer.c

#include "comm.h"

static void server()
{
    // 1.调用mkfifo创建管道文件
    int ret=mkfifo(FIFOFILEPATH,DEFAULTMODE);
    if(ret<0) // 成功返回0,失败返回-1
    {
        perror("mkfifo");
        exit(1);
    }

    // 2.调用open以读的方式打开管道文件
    int fifo_fd=open(FIFOFILEPATH,O_RDONLY);
    if(fifo_fd<0)
    {
        perror("open");
        exit(2);
    }

    // 3.调用read从管道文件中读取数据
    char buffer[BUFFERSIZE];
    while(true)
    {
        ssize_t n=read(fifo_fd,buffer,sizeof(buffer)-1);
        if(n>0)
        {
            buffer[n]='\0';
            printf("%s\n",buffer);
        } 
        else if(n==0)
        {
            printf("client exit\n");
            break;
        }
        else
        {
            perror("read");
            close(fifo_fd); // 关闭fifo_fd文件描述符
            unlink(FIFOFILEPATH); // 删除管道文件
            exit(4);
        }
    }

    close(fifo_fd);
    unlink(FIFOFILEPATH);
    exit(0);
}

int main()
{
    server();

    return 0;
}

fifoClient.c

#include "comm.h"

static void client()
{
    // 1.调用open以写的方式打开管道文件
    int fifo_fd=open(FIFOFILEPATH,O_WRONLY);
    if(fifo_fd<0)
    {
        perror("open");
        exit(1);
    }

    // 2.调用write往管道文件中写入数据
    char buffer[BUFFERSIZE];
    while(true)
    {
        // 从0号文件描述符读取数据z
        printf("client# ");
        fflush(stdout);

        ssize_t n=read(0,buffer,sizeof(buffer)-1);
        buffer[n]='\0';

        // 将读取到的数据往管道文件中写入
        n=write(fifo_fd,buffer,strlen(buffer));
        if(n>0)
        {
            printf("write success n: %d\n",n);
        }
        else if(n==0)
        {
            // 读端关闭
            exit(2);
        }
        else
        {
            // 写入错误
            exit(3);
        }
    }

}

int main()
{
    client();
    return 0;
}

测试:

服务端先启动, 紧接着启动客户端. 客户端发送hello server, 可以看到服务端可以收到, 也即通过命名管道完成进程间通信. 客户端(写端)退出, 服务端(读端)也退出.
image-20230423180048142

  • 13
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值