十万个知识-001-ps|grep如何实现进程间通讯

3,511 阅读4分钟

Ubuntu 20.04

Linux Kernel 5.12.1

ps | grep 关键字

学习Linux都会知道上面这样一行命令,中间的竖线其实是匿名管道,将前一个命令的输出,作为后一个命令的输入。

实质这里执行了跨进程,本文分析一下。

涉及多少个进程?

先说结论,在shell下执行ps|grep,涉及shell、ps和grep三个进程

查看当前shell的进程id:

echo $$

输出当前shell的pid是3347

再查看当前的进程列表,打印出进程的父进程id:

ps o pid, ppid, comm | grep ""

PID    PPID COMMAND
989     968 gdm-x-session
991     989 Xorg
1091     989 gnome-session-b
3347    1947 bash
5018    3347 ps
5019    3347 grep

末尾三个进程就是涉及到的进程,其中ps进程和grep进程的父进程都是shell,因此是shell进程fork出了两个进程。

管道

ps进程数据传递到grep进程,使用了匿名管道。管道的创建,使用了下面的pipe函数:

#include <unisd.h>

int pipe(int fd[2]);

pipe函数通过参数填充返回两个文件描述符,fd[0]是读,fd[1]是写,在fd[1]写入将会在fd[0]读取。

管道

管道的原理本文先不讨论,简单说它是内核的一块内存。

到这个时候,创建的管道还只是一个进程里的事,没有起到进程间通讯的作用。

父子进程之间的通信

## 父子进程之间的通信

上图描述了父进程使用fork创建子进程并使用管道通讯的原理

进程使用task_struct结构描述,进程复制调用的是fork,进程持有的所有fd都会复制一份

父进程使用pipe函数创建一个管道,fd[0]和fd[1]暂时都在父进程下面。

父进程fork出了子进程,子进程的fd[0]和fd[1]复制来自父进程,这个时候,管道有着两个输入和两个输出,会带来混乱。

因此这个管道需要关闭多余的输入输出,父进程关闭fd[0],子进程关闭fd[1],父进程可以通过管道写入数据,子进程可以通过管道读取数据。

要记得管道是单向的,如果进程间需要双向通讯,需要创建两个管道。

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

int main()
{
  int fd[2];
  if (pipe(fd) == -1)
  {
    perror("pipe create fail");
    exit(EXIT_FAILURE);
  }

  int pipeRead = fd[0];
  int pipeWrite = fd[1];

  pid_t pid = fork();
  if (pid == -1)
  {
    fprintf(stderr, "fork failure");
    exit(EXIT_FAILURE);
  }

  if (pid == 0)
  {
    //子进程
    dup2(pipeRead, STDIN_FILENO);
    close(pipeWrite);
    execlp("grep", "grep", "", NULL);
  }
  else
  {
    //父进程
    dup2(pipeWrite, STDOUT_FILENO);
    close(pipeRead);
    execlp("ps", "ps", NULL);
  }

  return EXIT_SUCCESS;
}

兄弟进程之间的通信

回到上面的ps|grep,这里涉及到三个进程,仅仅用上面的父子进程间通讯是不够的。shell创建了ps和grep,ps和grep之间没有父子关系,只有兄弟关系。

兄弟进程之间的通信1

如图,shell进程创建子进程1,和上面父子进程通讯的例子几乎一样,shell进程是读,子进程是写。

兄弟进程之间的通信2

shell进程继续fork出了子进程2,因为shell进程保留了fd[0],也会被复制到子进程2。接下来不用多说,看图都明白,只要shell进程关闭fd[0],子进程1和子进程2就可以关联同一个管道。

兄弟进程之间的通信3

子进程1代入ps进程,子进程2代入grep进程,到目前为止,两个进程都有一个文件描述符关联管道。接下来要解决的是,如何将它们两者的输入输出通过管道传递,这需要用到dup2函数。

#include <unistd.h>

int dup2(int oldfd, int newfd);

dup2函数的作用是复制oldfd文件描述符成为newfd文件描述符,日常使用的标准输出、标准输入、标准出错,对应的文件描述符分别是:

  • STDOUT_FILENO
  • STDIN_FILENO
  • STDERR_FILENO

利用dup2函数,将程序创建的fd指向标准输入输出,ps进程的fd[1]指向标准输出,grep进程的fd[0]指向标准输入。

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

int main()
{
    int fd[2];
    if (pipe(fd) == -1)
    {
        perror("pipe create fail");
        exit(EXIT_FAILURE);
    }

    int pipeRead = fd[0];
    int pipeWrite = fd[1];

    //create child
    int count = 2;
    int childIndex = 0;
    for (childIndex = 0; childIndex < count; childIndex++)
    {
        pid_t pid = fork();
        if (pid == -1)
        {
            perror("pid error");
            exit(EXIT_FAILURE);
        }
        else if (pid == 0)
        {
            //子进程创建成功跳出
            break;
        }
    }

    if (childIndex == 0)
    {
        //子进程1将数据写入管道
        dup2(pipeWrite, STDOUT_FILENO);
        close(pipeRead);
        execlp("ps", "ps", NULL);
    }
    else if (childIndex == 1)
    {
        //子进程2从管道读取
        dup2(pipeRead, STDIN_FILENO);
        close(pipeWrite);
        execlp("grep", "grep", "", NULL);
    }
    else
    {
        //父进程不再关联管道
        close(pipeRead);
        close(pipeWrite);

        sleep(2);
    }

    return EXIT_SUCCESS;
}