【Linux】进程间通信介绍及匿名管道使用

导读:本篇文章讲解 【Linux】进程间通信介绍及匿名管道使用,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com


进程间通信介绍

一, 进程间通讯相关概念

进程是一个独立的资源分配单元,不同进程之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。

进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信IPCInter Process Communication)。

进程间通信的目的:

  • 数据传输:一个进程需要将它的数据发送给另一个进程;
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它发生了某种事件,例如进程终止时需要通知父进程;
  • 资源共享:多个进程之间共享同样的资源,为了做到这一点,需要内核提供互斥和同步机制;
  • 进程控制:有些进程希望完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变;

二, 进程间通信的方式

进程间通信的方式分为一台主机上 的进程间的通信以及不同主机的进程间的通信。

不同主机进程间通信,类似常用的QQ,通过网络实现,常用的实现技术是Socket

同一主机进程间的通信分为:

  • Unix进程间通信方式,实现技术包括:匿名管道有名管道信号等;
  • System V进程间通信方式和POSIX进程间通信方式,实现技术有:消息队列共享内存信号量等;

在这里插入图片描述

接下来是关于匿名管道实现进程间通信的内容。

匿名管道实现进程间通信

三, 匿名管道

1. 初识匿名管道

管道也叫匿名管道也叫无名管道,是UNIX系统IPC的最古老的形式,所有的UNIX系统都支持这种通信机制。

通常说的管道是匿名管道。

例如:统计一个目录中文件的数目命令: ls | wc -l,为了执行该命令,shell创建了两个进程分别执行lswc|也称为管道符;

首先执行ls产生一个进程,会得到当前目录下的所有文件及文件夹,默认是输出到终端;
使用管道符|把要输出到终端的内容通过管道传输给另一个进程wc;默认wc是从当前终端获取输入,通过管道符|后变成从管道中获取输入;

如下图所示:
在这里插入图片描述

2. 匿名管道的特点

管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同;

管道拥有文件的特质:读操作、写操作,但管道没有文件实体。匿名管道一般用在有关系的进程之间,比如父子进程、兄弟进程之间等。

一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入管道的数据块的大小是多少。

通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的。管道中的数据结构是环形队列。

在管道中数据传递的方向是单向的,一端用于写入,一端用于读取,管道是半双工的。
需要注意的是,匿名管道只能用于具有亲缘关系的进程间的通信;管道默认是阻塞的,如果管道中没有数据,read阻塞;如果管道满了,write阻塞

单工:如遥控器发送数据给电视机,只能是单向的;
双工:如打电话之间联系是双向的;双车道可以双向行驶
半双工:同一间,数据只能单向传递;如对讲机,同一时间只能单向联系;

从管道读取数据是一次性操作,数据一旦被读走,就从管道中被抛弃,释放空间以便写更多的数据,管道中无法使用lseek()等函数随机访问数据。

在这里插入图片描述

3. 管道IPC操作

前面了解到fork()创建的子进程,父进程和子进程共享文件描述符表。所以使用匿名管道实现进程间通信时,首先要创建管道,然后再创建子进程,这样父进程和子进程就可以共享管道描述符。

■ 创建匿名管道

#include <unistd.h>
int pipe(int pipefd[2]);

使用示例:创建一个子进程,子进程和父进程之间相互发送和接收数据。


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

int main()
{
    // 在fork之前创建管道
    int pipefd[2]={0};
    int ret = pipe(pipefd);
    if(ret == -1)
    {
        perror("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    char buf[1024]={0};

    if(pid > 0)  // 父进程
    {
        printf("i am parent process , pid : %d\n",getpid());
        
        // 父进程接收数据
        while (1)
        {
            // 父进程从管道读数据
            int len = read(pipefd[0],buf,sizeof(buf));
            printf("parent receive : %s, pid : %d\n",buf,getpid());

            // 父进程向管道写数据
             char *str = "hello ,i am parent!";
            write(pipefd[1],str,strlen(str));
            sleep(2); 
        }       
    }
    else if(pid == 0)  // 子进程
    {
        printf("i am child process , pid : %d\n",getpid());
        while (1)
        {
            // 子进程 向管道中写数据
            char *str = "hello ,i am child!";
            write(pipefd[1],str,strlen(str));
            sleep(2);  

            // 子进程从管道读数据
            int len = read(pipefd[0],buf,sizeof(buf));
            printf("child receive : %s, pid : %d\n",buf,getpid());
        }
    }

    return 0;
}

显示结果,子进程和父进程交替发送数据,首先是父进程接收到子进程发送的数据,然后子进程接收到父进程发送的数据:因为代码中是子进程先向管道中写入数据,父进程先从管道中读出数据。 程序中不能子进程和父进程都先去读,如果这样会一直阻塞到这里;也不能都先去写,因为可能会导致父进程读取到父进程写的数据,子进程读取到子进程写的数据

在这里插入图片描述

■ 查看管道缓冲大小

可以使用命令 ulimit -a 查看管道缓冲大小;
在这里插入图片描述

还可以使用函数 fpathconf() 查看管道缓冲大小

#include <unistd.h>
long fpathconf(int fd, int name);

函数参数:

  • fd:文件描述符或者管道描述符;
  • name:查看管道缓冲区大小可以设置为_PC_PIPE_BUF,更多内容可以参考 man fpathconf

函数查看管道缓冲区大小示例:

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>

int main()
{
    int pipefd[2];
    int ret = pipe(pipefd);

    long size = fpathconf(pipefd[0],_PC_PIPE_BUF);  // 获取管道大小
    printf("pipe size : %ld\n",size);

    return 0;
}

显示结果:
在这里插入图片描述

4. 管道的读写特点

使用管道时需要注意几种特殊情况:(假设都是阻塞I/O操作下)

  1. 所有指向管道写端的文件描述符都关闭了,仍有进程从管道的读端读取数据,那么管道中剩余的数据被读取后,再次读取会返回0,类似于读到了文件末尾;
  2. 如果有指向管道写端的文件描述符没有关闭,且没有向管道中写数据,有进程从管道中读数据,那么管道中剩余的数据被读取后,再次读取会阻塞,直到管道中有数据了才能读取数据并返回;
  3. 如果所有指向管道读端的文件描述符都关闭了,仍有进程向管道中写数据,那么该写数据的进程会收到一个信号SIGPIPE,会导致进程异常终止;
  4. 如果有指向管道读端的文件描述符没有关闭,且没有从管道中读取数据,如果有进程向管道中写数据,那么在管道被写满的时候再次写会阻塞,直到管道中有空位置才能再次写入并返回;

总结就是:

  • 管道时:
    • 管道中有数据,read读取会返回实际读到的字节数;
    • 管道中没有数据:
      • 写端被全部关闭,read读取返回0;
      • 写端没有被完全关闭,read读取会阻塞等待,直到管道中有了数据;
  • 管道时:
    • 管道读端全部关闭,进程异常终止,向管道中写数据的进程收到SIGPIPE信号;
    • 管道读端没有全部关闭:
      • 管道已满,write写阻塞,直到管道中有空位置;
      • 管道没有满,write将把数据写入,并返回实际写入的字节数;

5. 匿名管道通信案例

/*
 * 实现 ps aux | grep root类似功能
 * 需要用到父子进程间通信
 *
 * 子进程执行 ps aux,子进程结束后将得到的数据发送给父进程
 * 父进程获取到数据,过滤
 *
 * 使用匿名管道 pipe()函数通信,使用exec函数族执行 ps aux命令
 * 获取到子进程的标准输出,子进程的标准输出重定向到父进程即管道的写端,dup2
 *
 */

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

int main()
{
    // 创建一个匿名管道
    int fd[2];
    int ret = pipe(fd);
    if (ret == -1)
    {
        perror("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();

    if (pid > 0)
    {
        // 关闭写端
        close(fd[1]);
        //父进程从管道中读取数据,过滤数据,并输出
        char buf[1024] = {0};
        int len = -1;
        // int len = read(fd[0],buf,sizeof(buf)-1);
        while ((len = read(fd[0], buf, sizeof(buf) - 1)) > 0)
        {
            printf("%s", buf);
            memset(buf, 0, sizeof(buf));
        }

        wait(0); // wait() 回收子进程资源
    }
    else if (pid == 0)
    {
        // 子进程中执行 ps aux
        // 文件描述符重定向,把标准输出重定向到管道的写端
        // 关闭读端
        close(fd[0]);
        dup2(fd[1], STDOUT_FILENO);

        execlp("ps", "ps", "aux", NULL);
        // perror("execlp");
        exit(0);
    }
    else
    {
        perror("fork");
        exit(0);
    }

    return 0;
}

6. 管道设置为非阻塞

可以使用fcntl()设置管道描述符为非阻塞状态。

int flag = fcntl(fd[0],F_GETFL);  // F_GETFL 获取文件描述符状态的标记
flag |= O_NOBLOCK;  // 修改flag值
fcntl(fd[0],F_SETFL,flag);  // 设置新的flag,设置为非阻塞

示例:

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

/**
 * 设置管道非阻塞
 * 使用函数fcntl()
 * int flag = fcntl(fd[0],F_GETFL);  // F_GETFL 获取文件描述符状态的标记
 * flag |= O_NOBLOCK;  // 修改flag值
 * fcntl(fd[0],F_SETFL,flag);  // 设置新的flag
 */

int main()
{
    // 在fork之前创建管道
    int pipefd[2] = {0};
    int ret = pipe(pipefd);
    if (ret == -1)
    {
        perror("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    char buf[1024] = {0};

    if (pid > 0) // 父进程
    {
        printf("i am parent process , pid : %d\n", getpid());

        // 关闭写端
        close(pipefd[1]);

        int flag = fcntl(pipefd[0], F_GETFL); // F_GETFL 获取文件描述符状态的标记

        flag |= O_NONBLOCK;              // 修改flag值
        fcntl(pipefd[0], F_SETFL, flag); // 设置新的flag,非阻塞模式

        // 父进程接收数据
        while (1)
        {
            // 父进程从管道读数据
            sleep(1);
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("len : %d\n", len);
            printf("parent receive : %s, pid : %d\n", buf, getpid());
            memset(buf, 0, sizeof(buf));
        }
    }
    else if (pid == 0) // 子进程
    {
        printf("i am child process , pid : %d\n", getpid());

        // 关闭读端
        close(pipefd[0]);

        while (1)
        {
            // 子进程 向管道中写数据
            char *str = "hello ,i am child!";
            write(pipefd[1], str, strlen(str));
            sleep(5);
        }
    }

    return 0;
}

显示结果:

i am parent process , pid : 49939
i am child process , pid : 49940
len : 18
parent receive : hello ,i am child!, pid : 49939
len : -1
parent receive : , pid : 49939
len : -1
parent receive : , pid : 49939
len : -1
parent receive : , pid : 49939
len : 18
parent receive : hello ,i am child!, pid : 49939
len : -1
parent receive : , pid : 49939

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/46071.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!