【Linux】进程间通信---管道

进程间通信

概念

进程间通信是干什么的?

    我们先来思考一个问题, 当两个人面对面交谈的时候, 为什么一个人能够听得见另一个人说话呢, 是因为声音有空气作为介

质, 通过空气这个介质, 双方才得以听到对方的讲话, 进程间通信就和人与人说话一样, 通过一个公共的媒介, 进行进程间的数

据传输, 资源共享, 进程之间的协同控制, 事件通知等等.

目的

操作系统为什么要给用户提供进程间通信方式?

    因为每个进程之间都是独立的, 进程间通信需要一个公共的媒介, 进程间通信方式就是这个公共的媒介, 但是有不同的需求

不同的应用场景, 因此操作系统提供了多种不同的进程间通信方式, 继续往下看. 

方式

管道

    1. 命名管道

    2. 匿名管道

System V 标准

    消息队列    共享内存    信号量

POSIX 标准

    消息队列    共享内存    信号量    互斥锁    条件变量    读写锁

注:

  在这篇博客中我们着重说的是管道, POSIX中的信号量互斥锁以及后面的两个我们在后面的多线程中会详细讲解, 消息队列

已经很少用了, 后面博客中我们能会说一说原理

管道

  管道是用于连接一个读进程和一个写进程以实现他们之间通信的一个共享文件, 也叫做 pipe 文件, 管道提供字节流服务.

    我们把从一个进程连接到另一个进程的一个数据流称为一个管道, 管道在内核中创建, 因为在用户态各个进程之间是独立的

管道的本质:

    操作系统在内核中提供的一块缓冲区 (只要进程能够访问到这块缓冲区就可以实现通信)

管道的特点:

    1. 管道是一个半双工通信, 提供双向选择, 但是只能单向传输 

    2. 匿名管道只能用于具有亲缘关系的进程间通信,命名管道可以用于任意的进程间通信

    3. 管道的读写特性:

        若没有数据, 则read会阻塞, 直到读取到数据

        若管道中数据满了, 则write会阻塞, 直到有空闲空间(有数据被读走)

        若所有读端被关闭, 则write触发异常

        若写端关闭, 则read读完数据返回0

    5. 管道创建成功后, 提供IO操作, 返回文件描述符作为句柄

        文件描述符有两个: 一个用于读取数据, 一个用于写入数据

    6. 管道提供字节流服务(传输灵活 / 数据粘连)

    7. 管道自带同步于互斥功能

        读写操作数据大小不超过PIPE_BUF大小, 读写操作受保护

        同步: 保证临界资源访问的时序可控性(一个进程操作完了另一个才能操作)

        互斥: 保证临界资源同一时间唯一访问性(一个进程操作的时候别的进程都不能操作)

       管道的同步于互斥的体现就是:

            管道没有放数据则读阻塞, 放了数据之后唤醒对方, 对方读数据--------------同步

            数据往管道中没放完别人不能读也不能写------------互斥

  在这里要给大家说的事, 进程在操作管道的时候, 如果没有用到哪一端, 则把这一端关闭掉.

匿名管道: 

 int pipe(int fd[2]);

  参数:

    fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端

  返回值:

    成功返回0,失败返回错误代码

  我们在代码中体会匿名管道的使用:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

int main()
{
    //创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if(ret < 0)
    {
        perror("pipe error");
        return -1;
    }
    //使用匿名管道实现子进程与父进程之间的通信
    int pid = fork();
    if(pid < 0)
    {
        return -1;
    }
    else if (pid == 0)
    {
        //child
        const char* ptr = "Acesses";
        write(pipefd[1], ptr, strlen(ptr));
    }
    else
    {
        //parent
        char buf[1024] = {0};
        ret = read(pipefd[0], buf, 1023);
        if(ret < 0)
        {
            perror("read error");
        }
        else
        {
            printf("buf:[%s]\n", buf);
        }
    }
    return 0;
}

  编译运行:

  可以看到, 父进程成功读取到了子进程写入的数据.

然后我们来体会管道的特性:

(1)若没有数据, 则read会阻塞, 直到读取到数据

  我们让子进程先睡3秒看看.

 else if (pid == 0)
    {
        //child
        sleep(3);
        const char* ptr = "Acesses";
        write(pipefd[1], ptr, strlen(ptr));
    }

  运行:

  

  可以看到, 父进程等待了三秒之后才读到了子进程写入的数据.

(2)若管道中数据满了, 则write会阻塞, 直到有空闲空间(有数据被读走)

  我们对代码再稍作修改, 在子进程中一直写入数据, 直到缓冲区写满.

else if (pid == 0)
    {
        //child
        int i = 0;
        while(1)
        {
            const char* ptr = "Acesses";
            i += write(pipefd[1], ptr, strlen(ptr));
            printf("ret:%d\n", i);
        }
    }

  运行:

可以看到, 当我们写入了接进64k的数据后, 缓冲区满了, 这个时候不能写入了, write阻塞.

(3)若所有读端被关闭, 则write触发异常

这次我们在父进程中写入, 因为如果子进程先退出的话, 子进程并没有退出, 体现的不是很明显.

 else if (pid == 0)
    {
        //child
        close(pipefd[0]);
        sleep(1000);  //不读取数据
        char buf[1024] = {0};
        ret = read(pipefd[0], buf, 1023);
        if(ret < 0)
        {
            perror("read error");
        }
        else
        {
            printf("buf:[%s]\n", buf);
        }
    }
    else
    {
        //parent
        close(pipefd[0]);
        sleep(1);  //让子进程先把读端关闭父进程再开始写
        int i = 0;
        while(1)
        {
            const char* ptr = "Acesses";
            i += write(pipefd[1], ptr, strlen(ptr));
            printf("ret:%d\n", i);
        }
    }

  运行:

我们可以看到, 关闭所有读端之后, 父进程的写入触发异常, 程序直接退出

(4)若写端关闭, 则read读完数据返回0

  我们还是稍作修改, 关掉两个写, 然后让子进程读数据

else if (pid == 0)
    {
        //child
        close(pipefd[1]);
        char buf[1024] = {0};
        ret = read(pipefd[0], buf, 1023);
        if(ret < 0)
        {
            perror("read error");
        }
        else
        {
            printf("buf:[%s]\n", buf);
        }
    }
    else
    {
        //parent
        close(pipefd[1]);
        sleep(1000);
        int i = 0;
        while(1)
        {
            const char* ptr = "Acesses";
            i += write(pipefd[1], ptr, strlen(ptr));
            printf("ret:%d\n", i);
        }
    }

运行: 

可以看到, 当所有写端关闭, 什么也没有读到, 读返回0

管道符的实现

  我们在执行一个命令比如  ps -ef | grep pipe 的时候, 这个 | 就是一个管道符, 它的作用是连接两个命令, 将前面的输出结果

作为后面的输入, 这个管道就是使用匿名管道来实现的, 父进程是shell, ps 和 grep是两个子进程, 具有亲缘关系, shell创建

两个子进程之前先创建一个管道, 两个子进程就可以通过这个管道进行数据的传输, ps将打印结果全部重定向到grep中, 然后

grep循环从标准输入读取数据, 所以对标准输入也进行重定向, grep从管道读取端读取数据.

  创建两个子进程进行程序替换, 然后关闭用不到的一端.

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/wait.h>

int main()
{
    int fd[2];
    int ret = pipe(fd);
    if(ret < 0)
    {
        perror("pipe error");
        return -1;
    }
    int pid1 = fork();
    if(pid1 == 0)    
    {
        //child1
        //不用读端, 关闭
        close(fd[0]);
        dup2(fd[1], 1);
        execlp("ps","ps","-ef",NULL);
        exit(0);
    }
    int pid2 = fork();
    if(pid2 == 0)
    {
        //child2
        //不用写端, 关闭
        close(fd[1]);
        dup2(fd[0], 0);
        execlp("grep","grep","pipe",NULL);
        exit(0);
    }
    //父进程也打开管道了, 要关闭, 不然的话子进程二会一直循环读取, 不会退出
    close(fd[0]);
    close(fd[1]);
    waitpid(pid1, NULL, 0);
    printf("child1 exit---\n");
    waitpid(pid2, NULL, 0);
    printf("child2 exit---\n");
    return 0;
}

  运行:

 

 命名管道:

    为管道创建了一个管道文件, 这个管道文件就是管道的名字

创建:

1. 直接在命令行创建

mkfifo filename

2. 在程序里面创建

int mkfifo(const char *pathname, mode_t mode);
    pathname : 管道文件的路径
    mode     : 文件的打开权限

命名管道的打开特性:

    若管道文件没有被写的方式打开, 则以只读打开会阻塞

    若管道文件没有被读的方式打开, 则以只写打开会阻塞

读写特性和匿名管道基本一样

命名管道的基本使用 (实现进程间的通信):

fifo_read.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/fcntl.h>
int main()
{
    const char* file = "./tmp.fifo";
    umask(0);
    int ret = mkfifo(file, 0664);
    if(ret < 0)
    {
        //如果文件不是因为已经存在而报错, 则退出
        if(errno != EEXIST)
        {
            perror("mkfifo error");
            return -1;
        }
    }
    printf("create fifo success!!\n");
    int fd = open(file, O_RDONLY);
    if(fd < 0)
    {
        perror("open error");
        return -1;
    }
    printf("open fifo success!!\n");
    while(1)
    {
        char buf[1024] = {0};
        int ret = read(fd, buf, 1023);
        if(ret < 0)
        {
            perror("read error");
            return -1;
        }
        else if(ret == 0)
        {
            printf("write close\n");
            return -1;
        }
        printf("buf:[%s]\n", buf);
    }
    close(fd);
    return 0;
}
fifo_write.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/fcntl.h>

int main()
{
    const char* file = "./tmp.fifo";
    umask(0);
    int ret = mkfifo(file, 0664);
    if(ret < 0)
    {
        //如果文件不是因为已经存在而报错, 则退出
        if(errno != EEXIST)
        {
            perror("mkfifo error");
            return -1;
        }
    }
    printf("create fifo success!!\n");
    int fd = open(file, O_WRONLY);
    if(fd < 0)
    {
        perror("open error");
        return -1;
    }
    printf("open fifo success!!\n");
    while(1)
    {
        char buf[1024] = {0};
        scanf("%s", buf);
        write(fd, buf, strlen(buf));
    }
    close(fd);
    return 0;
}

运行:

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. 实验目的 1) 加深对进程概念的理解,明确进程和程序的区别。 2) 进一步认识并发执行的实质。 3) 分析进程争用资源的现象,学习解决进程互斥的方法。 4) 学习解决进程同步的方法。 5) 了解Linux系统中进程通信的基本原理。   进程是操作系统中最重要的概念,贯穿始终,也是学习现代操作系统的关键。通过本次实验,要求理解进程的实质和进程管理的机制。在Linux系统下实现进程从创建到终止的全过程,从中体会进程的创建过程、父进程和子进程之间的关系、进程状态的变化、进程之间的互斥、同步机制、进程调度的原理和以管道为代表的进程间的通信方式的实现。 2. 内容及要求:   这是一个设计型实验,要求自行编制程序。   使用系统调用pipe()建立一条管道,两个子进程分别向管道写一句话:   Child process1 is sending a message!   Child process2 is sending a message!   父进程从管道读出来自两个子进程的信息,显示在屏幕上。   要求: 1) 父进程先接收子进程1发来的消息,然后再接收子进程2发来的消息。 2) 实现管道的互斥使用,当一个子进程正在对管道进行写操作时,另一子进程必须等待。使用系统调用lockf(fd[1],1,0)实现对管道的加锁操作,用lockf(fd[1],0,0)解除对管道的锁定。 3) 实现父子进程的同步,当子进程把数据写入管道后,便去睡眠等待;当父进程试图从一空管道中读取数据时,也应等待,直到子进程将数据写入管道后,才将其唤醒。 3.相关的系统调用 1) fork() 用于创一个子进程。 格式:int fork(); 返回值:在子进程中返回0;在父进程中返回所创建的子进程的ID值;当返回-1时,创建失败。 2) wait() 常用来控制父进程与子进程的同步。 在父进程中调用wait(),则父进程被阻塞,进入等待队列,等待子进程结束。当子进程结束时,父进程从wait()返回继续执行原来的程序。 返回值:大于0时,为子进程的ID值;等于-1时,调用失败。 3) exit() 是进程结束时最常调用的。 格式:void exit( int status); 其中,status为进程结束状态。 4) pipe() 用于创建一个管道 格式:pipe(int fd); 其中fd是一个由两个数组元素fd[0]和fd[1]组成的整型数组,fd[0]是管道的读端口,用于从管道读出数据,fd[1] 是管道的写端口,用于向管道写入数据。 返回值:0 调用成功;-1 调用失败。 5) sleep() 调用进程睡眠若干时间,之后唤醒。 格式:sleep(int t); 其中t为睡眠时间。 6) lockf() 用于对互斥资源加锁和解锁。在本实验中,该调用的格式为: lockf(fd[1],1,0);/* 表示对管道的写入端口加锁。 lockf(fd[1],0,0);/* 表示对管道的写入端口解锁。 7) write(fd[1],String,Length) 将字串String的内容写入管道的写入口。 8) read(fd[0],String,Length) 从管道的读入口读出信息放入字串String中。 4.程序流程 父进程: 1) 创建管道; 2) 创建子进程1; 3) 创建子进程2; 4) 等待从管道中读出子进程1写入的数据,并显示在屏幕上; 5) 等待从管道中读出子进程2写入的数据,并显示在屏幕上; 6) 退出。 子进程: 1) 将管道的写入口加锁; 2) 将信息“Child process n is sending message!”输入到变量OutPipe中,n=1,2; 3) 将OutPipe中信息写入管道; 4) 睡眠等待; 5) 将管道的写入口解锁; 6) 退出。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值