目录
4.3 同一个进程中通过 dup(dup2)函数对文件描述符进行复制
1 空洞文件
1.1 空洞文件概念
空洞文件(Sparse File)是一种节省磁盘空间的文件类型,它允许文件包含未分配的空间,这些空间在文件系统中被视为“空洞”。这意味着文件的逻辑大小可能很大,但实际占用的磁盘空间却很小,因为只有实际写入数据的部分才会占用空间。这种特性使得空洞文件非常适合用于大型日志文件、数据库文件或备份文件等场景,其中文件的大部分可能从未被写入。
空洞文件的工作原理是,文件系统会在文件中创建一个虚拟的空洞区域,当尝试读取这些空洞区域时,操作系统会返回默认值(通常是零),而不是实际从磁盘读取数据。这可以减少磁盘I/O操作,提高访问速度,并且因为未分配的空洞不占用实际的磁盘空间,所以可以节省大量的存储资源。在实际使用中,当应用程序写入数据到空洞文件的空洞区域时,文件系统会分配实际的磁盘块来存储这些数据,空洞随即被填充。因此,空洞文件的大小可能会随着数据的写入而动态变化。
1.2 空洞文件示例
下面代码演示了如何在Linux系统中创建一个空洞文件,并向其中写入数据。代码如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(void)
{
int fd;
int ret;
char buffer[1024];
int i;
/* 打开文件 */
fd = open("./hole_file", O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 将文件读写位置移动到偏移文件头 4096 个字节(4K)处 */
ret = lseek(fd, 4096, SEEK_SET);
if (-1 == ret) {
perror("lseek error");
goto err;
}
/* 初始化 buffer 为 0xFF */
memset(buffer, 0xFF, sizeof(buffer));
/* 循环写入 4 次,每次写入 1K */
for (i = 0; i < 4; i++) {
ret = write(fd, buffer, sizeof(buffer));
if (-1 == ret) {
perror("write error");
goto err;
}
}
ret = 0;
err:
/* 关闭文件 */
close(fd);
exit(ret);
}
通过这个程序,最终会在文件./hole_file
中创建一个4KB大小的空洞,然后向其中写入4KB的数据。由于使用了lseek
跳过了文件开始的4KB,所以在文件的开始处会有一个4KB的空洞区域,后面跟着连续的4KB数据。运行并查看文件大小:
使用 ls 命令查看到空洞文件的大小是 8K,使用 ls 命令查看到的大小包括了空洞部分大小和真实数据部分大小;当使用 du 命令查看空洞文件时,其大小显示为 4K, du 命令查看到的大小是文件实际占用存储块的大小。
2 多次打开同一个文件
在Linux系统中,可以多次打开同一个文件,每次打开文件都会获得一个新的文件描述符(file descriptor)。文件描述符是一个非负整数,用于标识特定的文件或I/O通道。以下是一些关于多次打开同一个文件的要点:
-
独立的文件描述符:每次调用
open
函数时,如果成功,都会返回一个新的文件描述符,即使打开的是同一个文件。同理在关闭文件的时候也需要调用 close 依次关闭各个文件描述符。 -
独立的文件指针:每个文件描述符都有自己的文件指针,用于记录当前的读写位置。这意味着对同一个文件的每次打开都可以独立地进行读写操作。
-
文件状态标志:文件状态标志,如
O_APPEND
或O_NONBLOCK
,对每个文件描述符是独立的。不同的打开方式可能会产生不同的行为。 -
写入操作:如果多个进程或线程打开同一个文件进行写入,它们的写入操作可能会相互干扰,除非采取了适当的同步措施。
下面的示例代码演示了如何多次打开同一个文件,并在每次打开时打印出分配的文件描述符
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
const char* filename = "testfile.txt";
// 确保文件存在,如果不存在则创建
int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("Error creating file");
return 1;
}
close(fd); // 创建后关闭文件描述符
// 第一次打开文件,以只读方式
int fd1 = open(filename, O_RDONLY);
if (fd1 == -1) {
perror("Error opening file for reading");
return 1;
}
printf("File descriptor for read-only access: %d\n", fd1);
// 第二次打开文件,以只写方式
int fd2 = open(filename, O_WRONLY);
if (fd2 == -1) {
perror("Error opening file for writing");
close(fd1); // 记得关闭之前打开的文件描述符
return 1;
}
printf("File descriptor for write-only access: %d\n", fd2);
// 第三次打开文件,以追加方式
int fd3 = open(filename, O_WRONLY | O_APPEND);
if (fd3 == -1) {
perror("Error opening file for appending");
close(fd1);
close(fd2); // 记得关闭之前打开的文件描述符
return 1;
}
printf("File descriptor for append access: %d\n", fd3);
// 关闭所有打开的文件描述符
close(fd1);
close(fd2);
close(fd3);
return 0;
}
在这个程序中:
-
首先检查文件
testfile.txt
是否存在,如果不存在,则以只写方式(O_WRONLY
)、创建文件(O_CREAT
)和截断文件(O_TRUNC
)标志创建它,并设置文件权限为0644
。 -
使用不同的标志多次打开同一个文件,并打印每次操作得到的文件描述符。
-
在每次成功打开文件后,都应关闭文件描述符以释放资源。
代码运行结果如下:
3 复制文件描述符
在 Linux 系统中, open 返回得到的文件描述符 fd 可以进行复制, 复制成功之后可以得到一个新的文件描述符,使用新的文件描述符和旧的文件描述符都可以对文件进行 IO 操作,复制得到的文件描述符和旧的文件描述符拥有相同的权限。
复制文件描述符之意图
在Linux系统中,可以使用dup
或dup2
函数来复制文件描述符。
3.1 dup函数
首先使用此函数需要包含头文件<unistd.h>,dup
函数用于复制一个已经打开的文件描述符,它会返回一个新的文件描述符,该文件描述符与原始文件描述符具有相同的属性:
int dup(int oldfd);
- oldfd: 需要被复制的文件描述符。
- 返回值: 成功时将返回一个新的文件描述符,由操作系统分配,分配置原则遵循文件描述符分配原则;如果复制失败将返回-1,并且会设置 errno 值。
3.2 dup2函数
dup2
函数则允许你指定新的文件描述符的值,如果指定的文件描述符已经打开,它会被关闭并重新打开:
int dup2(int oldfd, int newfd);
函数参数和返回值含义如下:
- oldfd: 需要被复制的文件描述符。
- newfd: 指定一个文件描述符(需要指定一个当前进程没有使用到的文件描述符)。
- 返回值: 成功时将返回一个新的文件描述符,也就是手动指定的文件描述符 newfd;如果复制失败将返回-1,并且会设置 errno 值。
3.3 dup
和dup2
函数示例
下面是一个简单的示例,演示了如何使用dup
和dup2
函数复制文件描述符:
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
// 文件名
const char* filename = "testfile.txt";
// 打开文件,如果不存在则创建
int fd = open(filename, O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("Error opening/creating file");
exit(EXIT_FAILURE);
}
// 使用 dup 创建文件描述符的副本
int dup_fd = dup(fd);
if (dup_fd == -1) {
perror("Error duplicating file descriptor");
close(fd);
exit(EXIT_FAILURE);
}
printf("Original file descriptor: %d\n", fd);
printf("Duplicated file descriptor with dup: %d\n", dup_fd);
// 使用 dup2 创建文件描述符的新副本,并覆盖现有的文件描述符 5 (假设 5 尚未使用)
int new_fd = dup2(fd, 5);
if (new_fd == -1) {
perror("Error duplicating file descriptor with dup2");
close(fd);
close(dup_fd);
exit(EXIT_FAILURE);
}
printf("Duplicated file descriptor with dup2 to fd 5: %d\n", 5);
// 关闭文件描述符
close(fd);
close(dup_fd);
// 由于 dup2 将 fd 指向了文件描述符 5,我们可以直接关闭 5
close(5);
return 0;
}
在这个示例中:
- 我们使用
open
函数以读写模式 (O_RDWR
) 打开testfile.txt
文件,以O_CREAT
和O_TRUNC
标志创建并截断文件。 - 使用
dup
函数复制原始文件描述符fd
到dup_fd
。使用dup2
函数将fd
复制到文件描述符 5,如果文件描述符 5 已经打开,则会被fd
所引用的文件覆盖。 - 打印出原始和复制的文件描述符。
- 最后,关闭所有复制的文件描述符,包括通过
dup2
操作指向文件描述符 5 的那个。
运行代码输出如下结果:
4 文件共享
文件共享是操作系统中的一种机制,它允许多个进程访问同一个文件或文件系统。文件共享的核心是如何通过多个不同的文件描述符来指向同一个文件,例如多次调用 open 函数重复打开同一个文件得到多个不同的文件描述符、使用 dup()或 dup2()函数对文件描述符进行复制以得到多个不同的文件描述符。在Linux和其他类UNIX操作系统中,文件共享的概念可以通过几种不同的方式实现。
4.1 同一个进程中多次调用 open 函数打开同一个文件
这种情况非常简单,多次调用 open 函数打开同一个文件会得到多个不同的文件描述符,并且多个文件描述符对应多个不同的文件表,所有的文件表都索引到了同一个 inode 节点,也就是磁盘上的同一个文件。各数据结构之间的关系如下图所示:
同一进程多次 open 打开同一文件各数据结构关系图
4.2 不同进程中分别使用 open 函数打开同一个文件
两个不同进程 1 和2 分别是运行在 Linux 系统上两个独立的进程,在他们各自的程序中分别调用 open 函数打开同一个文件,进程 1 对应的文件描述符为 fd1,进程 2 对应的文件描述符为fd2, fd1 指向了进程 1 的文件表 1, fd2 指向了进程 2 的文件表 2;各自的文件表都索引到了同一个 inode 节点,从而实现共享文件。其数据结构关系图如下所示:
不同进程 open 打开同一文件数据结构关系图
4.3 同一个进程中通过 dup(dup2)函数对文件描述符进行复制
通过使用dup
或dup2
函数复制文件描述符实现文件共享,其数据结构关系如下图所示:
dup 复制文件描述符实现文件共享