用户名: 密码: 忘记密码? 注册

形象的了解dup和dup2函数:转

作者:  时间: 2010-09-28
    相信大部分在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 dup2\n"
    int main() {
    int fd3;
    fd3 = open("testdup2.dat", 0666);
    if (fd < 0) {
    printf("open error\n");
    exit(-1);
    }
    if (dup2(fd3, STDOUT_FILENO) < 0) {
    printf("err in dup2\n");
    }
    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 dup2\n"
    #define SIZEOFTESTSTR 11
    int main() {
    int fd3;
    int s_fd;
    int n_fd;
    fd3 = open("testdup2.dat", 0666);
    if (fd3 < 0) {
    printf("open error\n");
    exit(-1);
    }
    /* 复制标准 输出描绘符 */
    s_fd = dup(STDOUT_FILENO);
    if (s_fd < 0) {
    printf("err in dup\n");
    }
    /* 重定向标准 输出到文件 */
    n_fd = dup2(fd3, STDOUT_FILENO);
    if (n_fd < 0) {
    printf("err in dup2\n");
    }
    write(STDOUT_FILENO, TESTSTR, SIZEOFTESTSTR); /* 写入testdup2.dat中 */
    /* 重定向恢复标准 输出 */
    if (dup2(s_fd, n_fd) < 0) {
    printf("err in dup2\n");
    }
    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可以给你带来很多强大的功效,花了一些光阴总结出上面那么多,不知道自己了解的是否透彻 ,只能在以后的实践中慢慢摸索了。