dup()和dup2()

声明:本文文字较多,比较枯燥,请耐下性子来看,收获会有的微笑

1、函数dup()和dup2()

(1)#include <unistd.h>


int dup(int oldfd);
int dup2(int oldfd, int newfd);

(2)功能: 复制文件句柄

(3)用法: int dup2(int oldfd,int newfd);

(4)如果调用成功,这两个函数都返回新分配或指定的文件描述符,如果出错则返回-1。dup返回的新文件描述符一定该进程未使用的最小文件描述符,这一点和open类似。dup2可以用newfd参数指定新描述符的数值。如果newfd当前已经打开,则先将其关闭再做dup2操作,如果oldfd等于newfd,则dup2直接返回newfd而不用先关闭newfd再复制。

(5)在unix高级编程中有介绍dup和dup2,但是没有实例说明, 笔者自己结合实例进行了测试了解。
在linux下,通过open打开以文件后,会返回一个文件描述符,文件描述符会指向一个文件表,文件表中的节点指针会指向节点表。看下图:


打开文件的内核数据结构


dup和dup2两个函数都可以用来复制打开的文件描述符,复制成功后和复制源共享同一个文件表。看下图:

执行dup后的内核数据结构


dup函数:dup(现存的文件描述符)
dup返回的新文件描述符一定是当前可以用描述符中的最小值。下面先打开一个文件来看下文件描述符,为保证测试成功,创建一个测试文件log.txt。

<span style="font-family:Microsoft YaHei;font-size:14px;">#include <fcntl.h>
#include <stdio.h>
 
int main(int argc, char *argv[])
{
    int fd;
    fd = open("./log.txt", O_RDWR);
 
    printf("%d\n", fd);
 
    return 0;
}</span>

上面的代码用读写打开了log.txt这个文件,编译上面的代码然后执行,执行成功的话,应当是输出3,因为0,1,2分别被标准输入,标准输出,标准错误输出占用了。使用dup复制这个文件描述符,并尝试移动fd偏移量:

<span style="font-family:Microsoft YaHei;font-size:14px;">#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
 
int main(int argc, char *argv[])
{
    int fd, copyfd;
 
    fd = open("./log.txt", O_RDWR);
    //复制fd
    copyfd = dup(fd);
 
    //输出copyfd,应当为4
    printf("%d\n", copyfd);
 
    //打印出fd和copyfd的偏移量,都为0
    printf("%d\n", (int)lseek(fd, 0, SEEK_CUR));
    printf("%d\n", (int)lseek(copyfd, 0, SEEK_CUR));
 
    //将fd的偏移量+3
    lseek(fd, 3, SEEK_SET);
 
    //打印出fd和copyfd的偏移量,都为3
    printf("%d\n", (int)lseek(fd, 0, SEEK_CUR));
    printf("%d\n", (int)lseek(copyfd, 0, SEEK_CUR));
 
    return 0;
}</span>

编译执行上例代码可以发现当移动fd的偏移量时,copyfd的偏移量也发生了变化。往文件里写入内容试试,先把log.txt内容清空。

<span style="font-family:Microsoft YaHei;font-size:14px;">#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
 
int main(int argc, char *argv[])
{
    int fd, copyfd;
 
    fd = open("./log.txt", O_RDWR);
    //复制fd
    copyfd = dup(fd);
 
    char buf1[] = "hello ";
    char buf2[] = "world!";
 
    //往fd文件写入内容
    if (write(fd, buf1, 6) != 6) {
        printf("write error!");
    }
 
    //打印出fd和copyfd的偏移量,经过上面的写操作,都变成6了
    printf("%d\n", (int)lseek(fd, 0, SEEK_CUR));
    printf("%d\n", (int)lseek(copyfd, 0, SEEK_CUR));
 
    //往copyfd写入内容
    if (write(copyfd, buf2, 6) != 6) {
        printf("write error!");
    }
 
    return 0;
}</span>


编译执行程序,log.txt的就有hello world!字符串了。


dup2函数:dup2(现存的文件描述符,可用的文件描述符)


dup2和dup函数一样,只是返回的文件描述符可以通过第二个参数”可用的文件描述符“指定。如果“可用的文件描述符“是打开状态,则会被关闭;如果”现存的文件描述符“和”可用的文件描述符“一样,则不会关闭,笔者认为这两个参数值一样的话,代码是没有任何意义的。

<span style="font-family:Microsoft YaHei;font-size:14px;">#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
 
int main(int argc, char *argv[])
{
    int fd, copyfd;
 
    fd = open("./log.txt", O_RDWR);
    //指定文件描述符号为1000
    copyfd = dup2(fd, 1000);
    //打印fd和copyfd,应当输出3 1000
    printf("%d %d\n", fd, copyfd);
 
    return 0;
}</span>

上面程序就是指定返回的文件描述为1000,再来看下指定的文件描述符是打开的情况,修改上例代码,将文件描述符指定为1:

<span style="font-family:Microsoft YaHei;font-size:14px;">copyfd = dup2(fd, 1);</span>

编译执行程序将看不到任何输出,因为1是终端标准输出的标识符,经过这样一复制后,标准输出就被关闭了,使用printf自然看不到输出信息了。

2、下面内容选自《UNⅨ环境高级编程》
Stevens said:
⑴ 每个进程在进程表中都有一个记录项,每个记录项中有一张打开文件描述符表,可将视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
(a) 文件描述符标志。
(b) 指向一个文件表项的指针。
⑵ 内核为所有打开文件维持一张文件表。每个文件表项包含:
(a) 文件状态标志(读、写、增写、同步、非阻塞等)。
(b) 当前文件位移量。
(c) 指向该文件v节点表项的指针。
图示:
文件描述符表
------------
fd0 0 | p0 -------------> 文件表0 ---------> vnode0
------------
fd1 1 | p1 -------------> 文件表1 ---------> vnode1
------------
fd2 2 | p2
------------
fd3 3 | p3
------------
... ...
... ...
------------
(1)单个进程内的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中提出的一些问题:
⑴ "dup2的第一个参数是不是必须为已打开的合法filedes?" -- 答案:必须。
⑵ "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 (fd3 < 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"。
(2)重定向后恢复
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中为空,原因就是缓冲区作怪,由于最终的目标是屏幕,所以程序最后将缓冲区的内容都输出到屏幕。
(3)父子进程间的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可以给你带来很多强大的功能,花了一些时间总结出上面那么多,不知道自己理解的是否透彻,只能在以后的实践中慢慢探索了。另外,对于dup和dup2总结起来就是“复制,共享

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值