bash中IO重定向功能和管道功能的模拟

80 篇文章 6 订阅
33 篇文章 1 订阅

“程序默认使用标准输入输出”,这是Unix哲学中的其中一条。

1 bash中的重定向模拟

用户登陆系统后,系统已经打开了终端,并在描述符表中使用三个描述符0,1,2来进行索引。由于Unix系统中描述符表是被子进程继承的,所以以后生成的任何进程都自动拥有了这三个描述符。其中的0用于索引标准输入设备,1用于索引标准输出设备,2则用于索引标准错误输出设备。

像C库中的printf()函数就是向描述符1索引的设备进行写入,scanf()则从描述符0指向的设备进行读取。如果新进程没有主动改变这三个描述符的索引内容,那么这些函数就是从终端进行读取并把输出写入终端。也可以这么说,输入输出函数只认识描述符,而不管描述符究竟指向什么设备。这样带来的好处就是,程序随时可以更改0,1,2这三个描述符实际指向的设备,从而动态的改变程序输入的来源,输出的目的地,这就是大名鼎鼎的“IO重定向”。

Unix哲学要求程序的输入、输出和错误输出要使用0,1,2这三个描述符,而不要指定描述符实际指向的设备。至于0,1,2究竟指向什么设备是由程序的调用者来指定的。这个调用者往往就是shell。在shell中可以通过简单的<来对0描述符进行指定设备,通过>对1描述符指定设备。下面是一个bash中简单的输入输出重定向例子。


smstongtekiMac-mini:~ smstong$ ls -l / > 1.txt

smstongtekiMac-mini:~ smstong$ cat 1.txt 

total 16445

drwxrwxr-x+ 78 root  admin     2652 Jan  2 09:32 Applications

drwxr-xr-x+ 63 root  wheel     2142 Nov 15 08:58 Library

drwxr-xr-x@  2 root  wheel       68 Aug 25 08:15 Network

drwxr-xr-x+  4 root  wheel      136 Oct 26 20:55 System

drwxr-xr-x   5 root  admin      170 Oct 26 21:01 Users

drwxrwxrwt@  3 root  admin      102 Jan 10 13:15 Volumes

drwxr-xr-x@ 39 root  wheel     1326 Oct 26 20:57 bin

drwxrwxr-t@  2 root  admin       68 Aug 25 08:15 cores

dr-xr-xr-x   3 root  wheel     4228 Jan  5 08:19 dev

lrwxr-xr-x@  1 root  wheel       11 Oct 26 20:46 etc -> private/etc

dr-xr-xr-x   2 root  wheel        1 Jan  5 08:19 home

-rwxr-xr-x@  1 root  wheel  8393256 Sep 20 13:22 mach_kernel

dr-xr-xr-x   2 root  wheel        1 Jan  5 08:19 net

drwxr-xr-x@  6 root  wheel      204 Oct 26 21:01 private

drwxr-xr-x@ 62 root  wheel     2108 Dec 23 09:02 sbin

lrwxr-xr-x@  1 root  wheel       11 Oct 26 20:47 tmp -> private/tmp

drwxr-xr-x@ 12 root  wheel      408 Nov 20 09:20 usr

lrwxr-xr-x@  1 root  wheel       11 Oct 26 20:48 var -> private/var

lrwxr-xr-x   1 root  wheel       49 Oct 26 17:08 用户信息 -> /Library/Documentation/User Information.localized


由于ls这个工具程序本身使用的是标准输入输出,而标准输入输出默认指向的都是终端。例子中,使用>把cat的输出重定向到了1.txt这个文件。这样1描述符就指向了1.txt这个文件,ls的输出就写入了1.txt文件中了。


cat进程的描述符表
描述符指向设备
0终端
1终端     1.txt
2终端
  
  

那么bash是如何设置cat进程的描述符表的呢?这就涉及到了bash的工作原理。bash在执行一个程序前,先fork出一个子进程,然后在这个子进程中执行指定的程序。由于子进程会继承父进程的描述符表,所以fork出来的子进程拥有和bash进程完全一样的描述符表,子进程在执行指定程序前,修改自身的描述符表,把1指向的设备修改为1.txt文件。这样随后在执行ls的时候,输出自然就写入1.txt文件了。


下面我们用C语言来实现对这种重定向的模拟。源码如下:

/*
 *  I/O Redirection
 */

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

int main(int argc, char** argv)
{
    if(strcmp(argv[1], "redirection")==0){
        char* newDev = argv[2];  //重定向的目的文件名
        if(fork()==0){
            // 输出重定向为文件
            int fd = open(newDev, O_WRONLY|O_CREAT);
            dup2(fd,1);
            close(fd);

            // 执行ls程序
            char* av[] = {"ls", "-l", "/",  NULL};
            execv("/bin/ls", av); 
            printf("%s", strerror(errno));
        }else{
            exit(0);
        }   
    }   
    return 0;
}
为了避开和bash元字符的冲突,我们使用redireciton这个单词来代替bash中的>作为重定向关键字。

执行 ./a.out redirection 1.txt 会得到与上面完全一样的效果。

2 bash中管道的模拟

有了重定向,就可以轻易地改变一个程序的输入源,输出目的地。如下所示:

ls > 1.txt; wc < 1.txt

我们首先把ls的输出重定向为1.txt文件,然后又把wc的输入重定向为1.txt文件,这样通过1.txt这个中间媒介把ls的输出作为了wc的输入。这与bash中的管道作用相同。但是,从执行效率角度来看,靠中间文件连接两个程序的IO完全是低下的。为此,Unix在内核中专门为此设计了“管道”这种设备。首先,管道的存取数据完全是在内存中完成的,所以效率要比硬盘上的文件快得多;其次,管道具有很多文件不具备的特性,专门用于两个进程的通信。于是,上面的命令被下面的命令替代。

ls  | wc 

POSIX API中专门提供了pipe()函数用于生成一个管道。下面我们就使用这个函数来模拟bash中的管道操作符。同样为了避免与bash的管道操作符冲突,我们使用pipe关键字表示管道。

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

int fd[2]; //管道描述符, fd[0]用于从管道中读取数据, fd[1]用于向管道中写入数据

void sub_ls(char* path)
{
    dup2(fd[1], 1); // change the stdout(1) to reference fd[1]
    close(fd[0]);
    close(fd[1]);
    char* av[] = {"ls","-l", NULL};
    execv("/bin/ls", av);

}
void sub_wc(char* path)
{
    dup2(fd[0], 0); // change the stdin(0) to reference fd[0]
    close(fd[0]);
    close(fd[1]);
    char* av[] = {"wc", NULL};
    execv("/usr/bin/wc", av);
}

int main(int argc, char** argv)
{
    if(argc != 4 || strcmp(argv[2], "pipe")!=0){
        printf("Usage: ./a.out ls pipe wc\n");
        return -1; 
    }   
    pipe(fd);
    if(fork()==0){
        sub_ls(argv[1]);
    }else{
        if(fork()==0){
            sub_wc(argv[3]);
        }   
    }   
    return 0;
}

这样我们执行 ./a.out ls pipe wc就会得到与 ls | wc相同的效果了。

3 重定向带来的启示

(1)对于工具程序开发者,一定要遵守“默认使用标准输入、输出、错误输出”的哲学,不要直接操作文件或其他设备。
下面是一个不符合这条规范的例子:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main()
{
    int fd = open("/dev/tty", O_RDWR);
    dup2(fd,1);
    printf("I'm a bad tool!\n");
    return 0;
}
这个程序把自己的输出强制指定为/dev/tty这个终端设备。对于这样的程序,即使在bash中使用>对其进行了重定向也不起效果,因为程序自身对描述符1的修改在bash对描述符1的修改之后,所以相当于bash的重定向被程序抛弃。


另外,必要的时候要在输出前检查当前1描述符指向的设备类型,针对不同的类型产生不同的输出格式。ls就是一个很好的例子,它在输出前会检查输出设备类型,如果是终端,则会同时输出内容数据和颜色控制数据,便于终端用户查看;如果是管道或者文件,则只输出内容数据,便于后续处理。

(2)对于脚本开发者,熟悉常见工具程序特性,尽量使用管道让不同的工具协同工作。可以说,shell脚本是最重度的重定向和管道用户。

(3)对于一般程序开发者,对于有父子关系的进程们,可以使用管道完成进程间通信,使用前分析各种进程间通信机制(socket,管道,共享内存,消息队列,信号量,FIFO等等)的优缺点,择优选用。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值