doing5552

记录每日点滴,不枉人生一世

  C++博客 :: 首页 :: 联系 :: 聚合  :: 管理
  73 Posts :: 0 Stories :: 94 Comments :: 0 Trackbacks

公告

常用链接

留言簿(24)

我参与的团队

最新随笔

搜索

  •  

积分与排名

  • 积分 - 453964
  • 排名 - 47

最新随笔

最新评论

阅读排行榜

评论排行榜

by mutecat@byhh  2007-09.20

    最近一段时间在用c写cgi程序,接触了这两个系统调用dup/dup2,碰到了一些

问题, 也解决了一些问题, 写出来与大家分享,也方便以后参考:)

1. 文件描述符在内核中数据结构

    在具体说dup/dup2之前, 我认为有必要先了解一下文件描述符在内核中的形态。

一个进程在此存在期间,会有一些文件被打开,从而会返回一些文件描述符,从shell

中运行一个进程,默认会有3个文件描述符存在(0、1、2), 0与进程的标准输入相关联,

1与进程的标准输出相关联,2与进程的标准错误输出相关联,一个进程当前有哪些打开

的文件描述符可以通过/proc/进程ID/fd目录查看。 下图可以清楚的说明问题:

  进程表项
————————————————

   fd标志 文件指针
      _____________________
fd 0:|________|____________|————> 文件表
fd 1:|________|____________|
fd 2:|________|____________|
fd 3:|________|____________|
     |     …….         |
     |_____________________|

                图1
       
文件表中包含:文件状态标志、当前文件偏移量、v节点指针,这些不是本文讨论的

重点,我们只需要知道每个打开的文件描述符(fd标志)在进程表中都有自己的文件表

项,由文件指针指向。

2. dup/dup2函数

APUE和man文档都用一句话简明的说出了这两个函数的作用:复制一个现存的文件描述符。

#include <unistd.h>

int dup(int oldfd);

int dup2(int oldfd, int newfd);

从图1来分析这个过程,当调用dup函数时,内核在进程中创建一个新的文件描述符,此

描述符是当前可用文件描述符的最小数值,这个文件描述符指向oldfd所拥有的文件表项。

  进程表项
————————————————

   fd标志 文件指针
      _____________________
fd 0:|________|____________|                   ______
fd 1:|________|____________|—————-> |      |
fd 2:|________|____________|                  |文件表|
fd 3:|________|____________|—————-> |______|
     |     …….         |
     |_____________________|

                图2:调用dup后的示意图

如图2 所示,假如oldfd的值为1, 当前文件描述符的最小值为3, 那么新描述符3指向

描述符1所拥有的文件表项。

dup2和dup的区别就是可以用newfd参数指定新描述符的数值,如果newfd已经打开,则

先将其关闭。如果newfd等于oldfd,则dup2返回newfd, 而不关闭它。dup2函数返回的新

文件描述符同样与参数oldfd共享同一文件表项。

APUE用另外一个种方法说明了这个问题:

实际上,调用dup(oldfd);

等效与
        fcntl(oldfd, F_DUPFD, 0)

而调用dup2(oldfd, newfd);

等效与
        close(oldfd);
        fcntl(oldfd, F_DUPFD, newfd);

3. CGI中dup2

写过CGI程序的人都清楚,当浏览器使用post方法提交表单数据时,CGI读数据是从标准

输入stdin, 写数据是写到标准输出stdout(c语言利用printf函数)。按照我们正常的理

解,printf的输出应该在终端显示,原来CGI程序使用dup2函数将STDOUT_FINLENO(这个

宏在unitstd.h定义,为1)这个文件描述符重定向到了连接套接字。

dup2(connfd, STDOUT_FILENO); /*实际情况还涉及到了管道,不是本文的重点*/

如第一节所说, 一个进程默认的文件描述符1(STDOUT_FILENO)是和标准输出stdout相

关联的,对于内核而言,所有打开的文件都通过文件描述符引用,而内核并不知道流的

存在(比如stdin、stdout),所以printf函数输出到stdout的数据最后都写到了文件描述

符1里面。至于文件描述符0、1、2与标准输入、标准输出、标准错误输出相关联,这

只是shell以及很多应用程序的惯例,而与内核无关。

用下面的流图可以说明问题:(ps: 虽然不是流图关系,但是还是有助于理解)

printf -> stdout -> STDOUT_FILENO(1) -> 终端(tty)

printf最后的输出到了终端设备,文件描述符1指向当前的终端可以这么理解:

STDOUT_FILENO = open("/dev/tty", O_RDWR);

使用dup2之后STDOUT_FILENO不再指向终端设备, 而是指向connfd, 所以printf的

输出最后写到了connfd。是不是很优美?:)

4. 如何在CGI程序的fork子进程中还原STDOUT_FILENO

如果你能看到这里,感谢你的耐心, 我知道很多人可能感觉有点复杂, 其实

复杂的问题就是一个个小问题的集合。所以弄清楚每个小问题就OK了,第三节中

说道,STDOUT_FILENO被重定向到了connfd套接字, 有时候我们可能想在CGI程序

中调用后台脚本执行,而这些脚本中难免会有一些输入输出, 我们知道fork之后,

子进程继承了父进程的所有文件描述符,所以这些脚本的输入输出并不会如我们愿

输出到终端设备,而是和connfd想关联了,这个显然会扰乱网页的输出。那么如何

恢复STDOUT_FILENO和终端关联呢?

方法1:在dup2之前保存原有的文件描述符,然后恢复。

代码实现如下:

savefd = dup(STDOUT_FILENO); /*savefd此时指向终端*/

dup2(connfd, STDOUT_FILENO);   /*STDOUT_FILENO(1) 被重新指向connfd*/

…..  /*处理一些事情*/

dup2(savefd, STDOUT_FILENO);  /*STDOUT_FILENO(1) 恢复指向savefd*/

很遗憾CGI程序无法使用这种方法, 因为dup2这些不是在CGI程序中完成的,而是在

web server中实现的,修改web server并不是个好主意。

方法2: 追本溯源,打开当前终端恢复STDOUT_FILENO。

分析第三节的流图, STDOUT_FILENO是如何和终端关联的? 我们重头做一遍不就行

了, 代码实现如下:

ttyfd = open("/dev/tty", O_RDWR);

dup2(ttyfd, STDOUT_FILENO);

close(ttyfd);

/dev/tty是程序运行所在的终端, 这个应该通过一种方法获得。实践证明这种方法

是可行的,但是我总感觉有些不妥,不知道为什么,可能一些潜在的问题还没出现。

目前我就想到这两种方法, 不知道你有什么好的想法? 有的话希望告诉我:)

终于收尾了,一早上过来写,没想到写了两个小时才写完,好久没有写原创了,又重拾

了以前那美妙的感觉:)



-----------------------------------------------------------------------------------------------------------------------------





相信大部分在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可以给你带来很多强大的功能,花了一些时间总结出上面那么多,不知道自己理解的是否透彻,只能在以后的实践中慢慢探索了。

文章出处:飞诺网(www.firnow.com):http://dev.firnow.com/course/6_system/linux/Linuxjs/2008716/133222.html


posted on 2010-07-13 13:41 doing5552 阅读(499) 评论(0)  编辑 收藏 引用

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理