dup和dup2 与重定向

在linux纷繁复杂的内核代码中,sys_dup()的代码也许称得上是最简单的之一了,但是就是这么一个简单的系统调用,却成就了unix/linux系统最著名的一个特性:输入/输出重定向。
    sys_dup()的主要工作就是用来“复制”一个打开的文件号,使两个文件号都指向同一个文件。既然说简单,我们就首先来看一下它的代码(定义在fs/fcntl.c中):

1 asmlinkage long sys_dup(unsigned int fildes)
 2 {
 3     int ret = -EBADF;
 4     struct file * file = fget(fildes);
 5 
 6     if (file)
 7         ret = dupfd(file, 0);
 8     return ret;
 9 }
10 

而sys_dup()的主体是dupfd()(定义在同一个文件中):

 

1 static int dupfd(struct file *file, int start)
 2 {
 3     struct files_struct * files = current->files;
 4     int ret;
 5 
 6     ret = locate_fd(files, file, start);
 7     if (ret < 0)
 8         goto out_putf;
 9     allocate_fd(files, file, ret);
10     return ret;
11 
12 out_putf:
13     write_unlock(&files->file_lock);
14     fput(file);
15     return ret;
16 }

 

注:dup和dup2的原型如下:
#include <unistd.h>

int dup(int file_descriptor);
int dup2(int file_descriptor1, int file_descriptor2)
dup返回的文件描述符总是取最小的可用值
dup2返回的文件描述符或者与file_descriptor2相同,或者是第一个大于该参数的可用值。

    而这么一个简单的系统调用是如何完成重定向这个艰巨的任务的呢?我们不妨先看个例子。
    当我们在shell下输入如下命令:“echo hello!”,这条命令要求shell进程执行一个可执行文件echo,参数为“hello!”。当shell接收到命令之后,先找到 bin/echo,然后fork()出一个子进程让他执行bin/echo,并将参数传递给它,而这个进程从shell继承了三个标准文件,即标准输入(stdin),标准输出(stdout)和标准出错信息(stderr),他们三个的文件号分别为0、1、2。而至于echo进程的工作很简单,就是将参数“hello!”写到标准输出文件中去,通常都是我们的显示器上。但是如果我们将命令改成“echo hello! > foo”,则在执行时输出将会被重定向到磁盘文件foo中(注:重定向于文件描述符有关)。我们假定在此之前该shell进程只有三个标准文件打开,文件号分别为0、1、2,以上命令行将按如下序列执行:
    (1) 打开或创建磁盘文件foo,如果foo中原来有内容,则清除原来内容,其文件号为3。
    (2) 通过dup()复制文件stdout,即将文件号1出的file结构指针复制到文件号4处,目的是将stdout的file指针暂时保存一下
    (3) 关闭stdout,即1号文件,但是由于4号文件对stdout也同时有个引用,所以stdout文件并未真正关闭,只是腾出1号文件号位置。
    (4) 通过dup(),复制3号文件(即磁盘文件foo),由于1号文件关闭,其位置空缺,故3号文件被复制到1号,即进程中原来指向stdout的指针指向了foo。
    (5) 通过系统调用fork()和exec()创建子进程并执行echo,子进程在执行echo前夕关闭3号和4号文件,只留下0、1、2三个文件,请注意,这时的1号文件已经不是stdout而是磁盘文件foo了。当echo想向stdout文件写入“hello!”时自然就写入到了foo中。
    (6) 回到shell后,关闭指向foo的1号与3号文件文件,再用dup()和close()将2号恢复至stdout,这样shell就恢复了0、1、2三个标准输入/输出文件。

    由此可见,当echo程序(或其他)在运行的时候并不知道stdout(对于stdin和stderr同样)指向什么,进程与实际输出文件或设备的结合是在运行时由其父进程“包办”的。这样就简化了子进程的程序设计,因为在设计时只要跟三个逻辑上存在的文件打交道就可以了。可能有人会觉得这很像面向对象中的多态和重载,没有什么新奇之处,但是如果你活在30甚至40年前,可能你会改变你的看法。 

相信大部分在Unix/Linux下编程的程序员手头上都有《Unix环境高级编程》(APUE)这本超级经典巨著。作者在该书中讲解dup/dup2之前曾经讲过“文件共享”,这对理解dup/dup2还是很有帮助的。这里做简单摘录以备在后面的分析中使用:

    Stevens said:
    (1) 每个进程在进程表中都有一个记录项,每个记录项中有一张打开文件描述符表,可将视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
    (a) 文件描述符标志。
    (b) 指向一个文件表项的指针。
    (2) 内核为所有打开文件维持一张文件表。每个文件表项包含:
    (a) 文件状态标志(读、写、增写、同步、非阻塞等)。
    (b) 当前文件位移量。
    (c) 指向该文件v节点表项的指针。
    图示:
    文件描述符表
    ------------
    fd0 0 | p0 -------------> 文件表0 ---------> vnode0
    ------------
    fd1 1 | p1 -------------> 文件表1 ---------> vnode1
    ------------
    fd2 2 | p2
    ------------
    fd3 3 | p3
    ------------
    ... ...
    ... ...
    ------------
    一、单个进程内的dup和dup2
    假设进程A拥有一个已打开的文件描述符fd3,它的状态如下:
    进程A的文件描述符表(before dup2)
    ------------
    fd0 0 | p0
    ------------
    fd1 1 | p1 -------------> 文件表1 ---------> vnode1
    ------------
    fd2 2 | p2
    ------------
    fd3 3 | p3 -------------> 文件表2 ---------> vnode2
    ------------
    ... ...
    ... ...
    ------------
    经下面调用:
    n_fd = dup2(fd3, STDOUT_FILENO);后进程状态如下:
    进程A的文件描述符表(after dup2)
    ------------
    fd0 0 | p0
    ------------
    n_fd 1 | p1 ------------
    ------------
    fd2 2 | p2
    ------------ _|
    fd3 3 | p3 -------------> 文件表2 ---------> vnode2
    ------------
    ... ...
    ... ...
    ------------
    解释如下:
    n_fd = dup2(fd3, STDOUT_FILENO)表示n_fd与fd3共享一个文件表项(它们的文件表指针指向同一个文件表项),n_fd在文件描述符表中的位置为 STDOUT_FILENO的位置,而原先的STDOUT_FILENO所指向的文件表项被关闭,我觉得上图应该很清晰的反映出这点。按照上面的解释我们就可以解释CU中提出的一些问题:
    (1) "dup2的第一个参数是不是必须为已打开的合法filedes?" -- 答案:必须。
    (2) "dup2的第二个参数可以是任意合法范围的filedes值么?" -- 答案:可以,在Unix其取值区间为[0,255]。
    另外感觉理解dup2的一个好方法就是把fd看成一个结构体类型,就如上面图形中画的那样,我们不妨把之定义为:
    struct fd_t {
    int index;
    filelistitem *ptr;
    };
    然后dup2匹配index,修改ptr,完成dup2操作。
    在学习dup2时总是碰到“重定向”一词,上图完成的就是一个“从标准输出到文件的重定向”,经过dup2后进程A的任何目标为STDOUT_FILENO的I/O操作如printf等,其数据都将流入fd3所对应的文件中。下面是一个例子程序:
    #define TESTSTR "Hello dup2n"
    int main() {
    int fd3;
    fd3 = open("testdup2.dat", 0666);
    if (fd < 0) {
    printf("open errorn");
    exit(-1);
    }
    if (dup2(fd3, STDOUT_FILENO) < 0) {
    printf("err in dup2n");
    }
    printf(TESTSTR);
    return 0;
    }
    其结果就是你在testdup2.dat中看到"Hello dup2"。
二、重定向后恢复
    CU上有这样一个帖子,就是如何在重定向后再恢复原来的状态?首先大家都能想到要保存重定向前的文件描述符。那么如何来保存呢,象下面这样行么?
    int s_fd = STDOUT_FILENO;
    int n_fd = dup2(fd3, STDOUT_FILENO);
    还是这样可以呢?
    int s_fd = dup(STDOUT_FILENO);
    int n_fd = dup2(fd3, STDOUT_FILENO);
    这两种方法的区别到底在哪呢?答案是第二种方案才是正确的,分析如下:按照第一种方法,我们仅仅在"表面上"保存了相当于fd_t(按照我前面说的理解方法)中的index,而在调用dup2之后,ptr所指向的文件表项由于计数值已为零而被关闭了,我们如果再调用dup2(s_fd, fd3)就会出错(出错原因上面有解释)。而第二种方法我们首先做一下复制,复制后的状态如下图所示:
    进程A的文件描述符表(after dup)
    ------------
    fd0 0 | p0
    ------------
    fd1 1 | p1 -------------> 文件表1 ---------> vnode1
    ------------ /|
    fd2 2 | p2 /
    ------------ /
    fd3 3 | p3 -------------> 文件表2 ---------> vnode2
    ------------ /
    s_fd 4 | p4 ------/
    ------------
    ... ...
    ... ...
    ------------
    调用dup2后状态为:
    进程A的文件描述符表(after dup2)
    ------------
    fd0 0 | p0
    ------------
    n_fd 1 | p1 ------------
    ------------
    fd2 2 | p2
    ------------ _|
    fd3 3 | p3 -------------> 文件表2 ---------> vnode2
    ------------
    s_fd 4 | p4 ------------->文件表1 ---------> vnode1
    ------------
    ... ...
    ... ...
    ------------
    dup(fd)的语意是返回的新的文件描述符与fd共享一个文件表项。就如after dup图中的s_fd和fd1共享文件表1一样。
    确定第二个方案后重定向后的恢复就很容易了,只需调用dup2(s_fd, n_fd);即可。下面是一个完整的例子程序:
    #define TESTSTR "Hello dup2n"
    #define SIZEOFTESTSTR 11
    int main() {
    int fd3;
    int s_fd;
    int n_fd;
    fd3 = open("testdup2.dat", 0666);
    if (fd3 < 0) {
    printf("open errorn");
    exit(-1);
    }
    /* 复制标准输出描述符 */
    s_fd = dup(STDOUT_FILENO);
    if (s_fd < 0) {
    printf("err in dupn");
    }
    /* 重定向标准输出到文件 */
    n_fd = dup2(fd3, STDOUT_FILENO);
    if (n_fd < 0) {
    printf("err in dup2n");
    }
    write(STDOUT_FILENO, TESTSTR, SIZEOFTESTSTR); /* 写入testdup2.dat中 */
    /* 重定向恢复标准输出 */
    if (dup2(s_fd, n_fd) < 0) {
    printf("err in dup2n");
    }
    write(STDOUT_FILENO, TESTSTR, SIZEOFTESTSTR); /* 输出到屏幕上 */
    return 0;
    }

    注意这里我在输出数据的时候我是用了不带缓冲的write库函数,如果使用带缓冲区的printf,则最终结果为屏幕上输出两行"Hello dup2",而文件testdup2.dat中为空,原因就是缓冲区作怪,由于最终的目标是屏幕,所以程序最后将缓冲区的内容都输出到屏幕。

 三、父子进程间的dup/dup2

    由fork调用得到的子进程和父进程的相同文件描述符共享同一文件表项,如下图所示:
    父进程A的文件描述符表
    ------------
    fd0 0 | p0
    ------------
    fd1 1 | p1 -------------> 文件表1 ---------> vnode1
    ------------ /|
    fd2 2 | p2 |
    ------------ |
    |
    子进程B的文件描述符表 |
    ------------ |
    fd0 0 | p0 |
    ------------ |
    fd1 1 | p1 ---------------------|
    ------------
    fd2 2 | p2
    ------------
    所以恰当的利用dup2和dup可以在父子进程之间建立一条“沟通的桥梁”。这里不详述。
    四、小结
    灵活的利用dup/dup2可以给你带来很多强大的功能,花了一些时间总结出上面那么多,不知道自己理解的是否透彻,只能在以后的实践中慢慢探索了。



 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值