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之间没有父子关系,只有兄弟关系。
如图,shell进程创建子进程1,和上面父子进程通讯的例子几乎一样,shell进程是读,子进程是写。
shell进程继续fork出了子进程2,因为shell进程保留了fd[0],也会被复制到子进程2。接下来不用多说,看图都明白,只要shell进程关闭fd[0],子进程1和子进程2就可以关联同一个管道。
子进程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;
}