文件描述符复制和重定向

在Linux中只要调用open()函数就可以给被操作的文件分配一个文件描述符,除了使用这种方式Linux系统还提供了一些其他的 API 用于文件描述符的分配,相关函数有三个:dup, dup2, fcntl

1. dup

1.1 函数详解

dup函数的作用是复制文件描述符,这样就有多个文件描述符可以指向同一个文件了。函数原型如下:

1
2
#include <unistd.h>
int dup(int oldfd);
  • 参数: oldfd 是要被复制的文件描述符

  • 返回值:函数调用成功返回被复制出的文件描述符,调用失败返回 -1

下图展示了 dup()函数具体行为, 这样不过使用 fd1还是使用fd2都可以对磁盘文件A进行操作了。

被复制出的新文件描述符是独立于旧的文件描述符的,二者没有连带关系。也就是说当旧的文件描述符被关闭了,复制出的新文件描述符还是可以继续使用的。

1.2 示例代码

下面的代码中演示了通过dup()函数进行文件描述符复制, 并验证了复制之后两个新、旧文件描述符是独立的,二者没有连带关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main()
{
// 1. 创建一个新的磁盘文件
int fd = open("./mytest.txt", O_RDWR|O_CREAT, 0664);
if(fd == -1)
{
perror("open");
exit(0);
}
printf("fd: %d\n", fd);

// 写数据
const char* pt = "你好, 世界......";
// 写成功之后, 文件指针在文件尾部
write(fd, pt, strlen(pt));


// 复制这个文件描述符 fd
int newfd = dup(fd);
printf("newfd: %d\n", newfd);

// 关闭旧的文件描述符
close(fd);

// 使用新的文件描述符继续写文件
const char* ppt = "((((((((((((((((((((((骚年,你要相信光!!!))))))))))))))))))))))";
write(newfd, ppt, strlen(ppt));
close(newfd);

return 0;
}

2. dup2

2.1 函数详解

dup2() 函数是 dup() 函数的加强版,基于dup2() 既可以进行文件描述符的复制, 也可以进行文件描述符的重定向。文件描述符重定向就是改变已经分配的文件描述符关联的磁盘文件。

dup2() 函数原型如下:

1
2
3
4
5
6
7
8
9
#include <unistd.h>
// 1. 文件描述符的复制, 和dup是一样的
// 2. 能够重定向文件描述符
// - 重定向: 改变文件描述符和文件的关联关系, 和新的文件建立关联关系, 和原来的文件断开关联关系
// 1. 首先通过open()打开文件 a.txt , 得到文件描述符 fd
// 2. 然后通过open()打开文件 b.txt , 得到文件描述符 fd1
// 3. 将fd1从定向 到fd上:
// fd1和b.txt这磁盘文件断开关联, 关联到a.txt上, 以后fd和fd1都对用同一个磁盘文件 a.txt
int dup2(int oldfd, int newfd);
  • 参数: oldfd和``newfd` 都是文件描述符
  • 返回值: 函数调用成功返回新的文件描述符, 调用失败返回 -1

关于这个函数的两个参数虽然都是文件描述符,但是在使用过程中又对应了不同的场景,具体如下:

  • 场景1:
    假设参数 oldfd 对应磁盘文件 a.txt, newfd对应磁盘文件b.txt。在这种情况下调用dup2函数, 是给newfd做了重定向,newfd 和文件 b.txt 断开关联, 相当于关闭了这个文件, 同时 newfd 指向了磁盘上的a.txt文件,最终 oldfd 和 newfd 都指向了磁盘文件 a.txt。

  • 场景2:

    假设参数 oldfd 对应磁盘文件 a.txt, newfd不对应任何的磁盘文件(newfd 必须是一个大于等于0的整数)。在这种情况下调用dup2函数, 在这种情况下会进行文件描述符的复制,newfd 指向了磁盘上的a.txt文件,最终 oldfd 和 newfd 都指向了磁盘文件 a.txt。

  • 场景3:

    假设参数 oldfdnewfd两个文件描述符对应的是同一个磁盘文件 a.txt, 在这种情况下调用dup2函数, 相当于啥也没发生, 不会有任何改变。

2.2 示例代码

给dup2() 的第二个参数指定一个空闲的没被占用的文件描述符就可以进行文件描述符的复制了, 示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 使用dup2 复制文件描述符
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main()
{
// 1. 创建一个新的磁盘文件
int fd = open("./111.txt", O_RDWR|O_CREAT, 0664);
if(fd == -1)
{
perror("open");
exit(0);
}
printf("fd: %d\n", fd);

// 写数据
const char* pt = "你好, 世界......";
// 写成功之后, 文件指针在文件尾部
write(fd, pt, strlen(pt));


// 2. fd1没有对应任何的磁盘文件, fd1 必须要 >=0
int fd1 = 1023;

// fd -> 111.txt
// 文件描述符复制, fd1指向fd对应的文件 111.txt
dup2(fd, fd1);

// 关闭旧的文件描述符
close(fd);

// 使用fd1写文件
const char* ppt = "((((((((((((((((((((((骚年,你要相信光!!!))))))))))))))))))))))";
write(fd1, ppt, strlen(ppt));
close(fd1);

return 0;
}

将两个有效的文件描述符分别传递给 dup2() 函数,就可以实现文件描述符的重定向了。将第二个参数的文件描述符重定向到参数1文件描述符指向的文件上。 示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 使用dup2 文件描述符重定向
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main()
{
// 1. 创建一个新的磁盘文件
int fd = open("./111.txt", O_RDWR|O_CREAT, 0664);
if(fd == -1)
{
perror("open");
exit(0);
}
printf("fd: %d\n", fd);

// 写数据
const char* pt = "你好, 世界......";
// 写成功之后, 文件指针在文件尾部
write(fd, pt, strlen(pt));


// 2. 创建第二个磁盘文件 222.txt
int fd1 = open("./222.txt", O_RDWR|O_CREAT, 0664);
if(fd1 == -1)
{
perror("open1");
exit(0);
}

// fd -> 111.txt, fd1->222.txt
// 从定向, 将fd1指向fd对应的文件 111.txt
dup2(fd, fd1);

// 关闭旧的文件描述符
close(fd);

// 使用fd1写文件
const char* ppt = "((((((((((((((((((((((骚年,你要相信光!!!))))))))))))))))))))))";
write(fd1, ppt, strlen(ppt));
close(fd1);

return 0;
}

3. fcntl

3.1 函数详解

fcntl() 是一个变参函数, 并且是多功能函数,在这里只介绍如何通过这个函数实现文件描述符的复制获取/设置已打开的文件属性。该函数的函数原型如下:

1
2
3
4
#include <unistd.h>
#include <fcntl.h> // 主要的头文件

int fcntl(int fd, int cmd, ... /* arg */ );
  • 参数:
    • fd: 要操作的文件描述符
    • cmd: 通过该参数控制函数要实现什么功能
  • 返回值:函数调用失败返回 -1,调用成功,返回正确的值:
    • 参数 cmd = F_DUPFD:返回新的被分配的文件描述符
    • 参数 cmd = F_GETFL:返回文件的flag属性信息

fcntl() 函数的 cmd 可使用的参数列表:

参数 cmd 的取值 功能描述
F_DUPFD 复制一个已经存在的文件描述符
F_GETFL 获取文件的状态标志
F_SETFL 设置文件的状态标志

文件的状态标志指的是在使用 open() 函数打开文件的时候指定的 flags 属性, 也就是第二个参数

1
int open(const char *pathname, int flags);

下表中列出了一些常用的文件状态标志:

文件状态标志 说明
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读、写打开
O_APPEND 追加写
O_NONBLOCK 非阻塞模式
O_SYNC 等待写完成(数据和属性)
O_ASYNC 异步I/O
O_RSYNC 同步读和写

3.2 复制文件描述符

使用 fcntl() 函数进行文件描述符复制, 第二个参数 cmd 需要指定为 F_DUPFD(这是个变参函数其他参数不需要指定)。

1
int newfd = fcntl(fd, F_DUPFD);

使用 fcntl() 复制文件描述符, 函数返回值为新分配的文件描述符,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main()
{
// 1. 创建一个新的磁盘文件
int fd = open("./mytest.txt", O_RDWR|O_CREAT, 0664);
if(fd == -1)
{
perror("open");
exit(0);
}
printf("fd: %d\n", fd);

// 写数据
const char* pt = "你好, 世界......";
// 写成功之后, 文件指针在文件尾部
write(fd, pt, strlen(pt));


// 复制这个文件描述符 fd
int newfd = fcntl(fd, F_DUPFD);
printf("newfd: %d\n", newfd);

// 关闭旧的文件描述符
close(fd);

// 使用新的文件描述符继续写文件
const char* ppt = "((((((((((((((((((((((骚年,你要相信光!!!))))))))))))))))))))))";
write(newfd, ppt, strlen(ppt));
close(newfd);

return 0;
}

3.3 设置文件状态标志

通过 open()函数打开文件之后, 文件的flag属性就已经被确定下来了,如果想要在打开状态下修改这些属性,可以使用 fcntl()函数实现, 但是有一点需要注意, 不是所有的flag 属性都能被动态修改, 只能修改如下状态标志: O_APPEND, O_NONBLOCK, O_SYNC, O_ASYNC, O_RSYNC等。

得到已打开的文件的状态标志,需要将 cmd 设置为 F_GETFL,得到的信息在函数的返回值中

1
int flag = fcntl(fd, F_GETFL);

设置已打开的文件的状态标志,需要将 cmd 设置为 F_SETFL,新的flag需要通过第三个参数传递给 fcntl() 函数

1
2
3
4
5
6
// 得到文件的flag属性
int flag = fcntl(fd, F_GETFL);
// 添加新的flag 标志
flag = flag | O_APPEND;
// 将更新后的falg设置给文件
fcntl(fd, F_SETFL, flag);

举例: 通过fcntl()函数获取/设置已打开的文件属性,先来描述一下场景:

如果要往当前文件中写数据, 打开一个新文件, 文件的写指针在文件头部,数据默认也是写到文件开头,如果不想将数据写到文件头部, 可以给文件追加一个O_APPEND属性。实例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 写实例程序, 给文件描述符追加 O_APPEND
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main()
{
// 1. 打开一个已经存在的磁盘文件
int fd = open("./111.txt", O_RDWR);
if(fd == -1)
{
perror("open");
exit(0);
}
printf("fd: %d\n", fd);

// 如果不想将数据写到文件头部, 可以给文件描述符追加一个O_APPEND属性
// 通过fcntl获取文件描述符的 flag属性
int flag = fcntl(fd, F_GETFL);
// 给得到的flag追加 O_APPEND属性
flag = flag | O_APPEND; // flag |= O_APPEND;
// 重新将flag属性设置给文件描述符
fcntl(fd, F_SETFL, flag);

// 使用fd写文件, 添加的数据应该写到文件尾部
const char* ppp = "((((((((((((((((((((((骚年,你要相信光!!!))))))))))))))))))))))";
write(fd, ppp, strlen(ppp));
close(fd);

return 0;
}