深入理解Linux管道实现

 曾经的一个误解


        日常工作中,要将数据从一个库迁移到另一个,可以使用“mysqldump xxx | mysql xxx”这个命令,这个命令先从源导出数据,然后经管道通过mysql命令导入目标数据库,正是通过这个命令的一些担心,加深了我对管道的认识。当时的担心是这样的:该命令不停的从源数据库取数据,然后写到目标数据库,那么在写入的时候,会不会创建多个MySQL链接,导致性能问题呢?查看后发现目标MySQL只有一个数据库连接,这是怎么回事呢?

误解的澄清


        先放上结论:其实bash在执行管道的时候,是同时创建左右两个进程,而且只会创建一次,并不是左边进程有输出再去创建右边进程处理。我们日常使用管道,多是左边进程很快结束的那种,对于左边进程是持续输出的情况(比如tail -f,tcpdump),以为是左边每输出一次内容就会去新建一个右边的进程来处理,其实管道右边的进程只会创建一次,他会持续的从管道读取数据并进行处理,所以能接在管道右边的程序,都是有一定要求的,它必须要能持续的从标准输入取数据处理。我的误解有点类似于cgi的工作方式,即每请求一次,就会创建一个进程,而管道的实际工作方式,更像是fastcgi的方式,处理进程只创建一次,在进程内部去不停的取数。

管道是怎么实现的?

        bash脚本其实都是由/bin/bash这一用户进程执行的,所以管道的实现代码在bash中,它的实现原理如下:

#include 
#include 
#include 
#include 
#include 
#include 
    
int main(int argc, char** argv) {
    int fd[2];
    if (pipe(fd) != 0) {
        exit(errno);
    }
        
    int pid = fork();
    if (pid == -1) {
        exit(errno);
    } else if (pid == 0) {
        close(0);
        dup(fd[0]);
        close(fd[1]);
        close(fd[0]);
        execlp("wc", "wc", "-c", NULL);
    } else {
        close(1);
        dup(fd[1]);
        close(fd[0]);
        close(fd[1]);
        execlp("cat", "cat" , "/etc/hostname" , NULL);
    }
}

        上面的命令模拟了“cat /etc/hostname | wc -c”的执行过程,也就是打印出/etc/hostname这个文件中的字符数。程序先fork了一个子进程,来执行管道操作右边的命令wc,然后在父进程中执行左边的命令cat(实现上应该也是启动一个子进程),由此可以看出,左右两边的命令是同时执行的。

        在子进程中(pid==0分支),先close(0)来关闭标准输入(注意这里的标准输入其实是父进程的标准输入,也就是当前控制台,因为子进程会继承父进程的文件描述符,而且还共享偏移),随后利用dup(fd[0])来从管道的读端口复制一个文件描述符,而复制文件描述符也会产生一个新的文件描述符,而新描述符产生的规则是:当前没有使用的最小的文件描述符,所以正好是刚才关闭的0,随后使用execlp加载wc执行,这样一番操作,也就将管道的读端设置为了wc命令的标准输入,而wc命令内部是从标准输入读取的,也就会从管道读取了。

        而在主进程中类似,先close再dup,将标准输出设置为管道的的写端,然后启动cat,这样cat的输出最终就到达了wc命令中。

为什么标准输入、输出、错误输出要硬编码为0、1、2?

        unix的哲学是:一个程序只做一件事,但这些独立的程序,始终是要连接起来完成更复杂的操作的,所以必须得要有一个连接点,而且得要独立于编程语言,也就是不管用C、Java、Python写的程序,都要能支持连接,所以只能选择依赖于操作系统的一个标识,也就是文件描述符。我们开始学习编程的时候就知道:每个程序开始运行就默认有三个文件描述符0、1、2,分别代表标准输入、标准输出、错误输出。正是因为这样0、1、2是硬编码的,所以才能实现跨语言通用。

为什么新描述符要使用最小的未被使用的?

        还有一个让人费解的设计是:新分配的描述符总是当前进程的最小的未被使用的描述符,其实这是为了能精确控制新生成的文件描述符的具体数值,看下面的命令:

if (fork() == 0) { 
    close(0); 
    open("input.txt", O_RDONLY); 
    exec("cat", argv); 
}

        上面的代码将0号描述符关闭,然后马上打开一个文件,那么这个文件的描述符就是0,而0被约定为程序的标准输入,那么接下来创建的cat命令进程,当从标准输入取数据的时候,会从input.txt这个文件中取,这也就是shell中输入重定向的实现原理。

为什么现在编码较少使用文件描述符了?

        现代的程序多是跨主机的通信,而不简单的是同一主机内部的通信了,进程间的连接点变成了ip和端口了,也就是通过网络协议,而不再通过父子进程间共享的文件描述符了。

你可能感兴趣的:(操作系统,linux,服务器)