1. 文件描述符
在 Linux 中,文件 I/O 的核心概念是文件描述符 (File Descriptor)。每当进程打开一个文件时,操作系统都会返回一个整数,称为文件描述符,它用于标识文件,系统中的所有文件操作都依赖于该文件描述符
常见的文件描述符:
- 标准输入 (STDIN): 文件描述符 0
- 标准输出 (STDOUT): 文件描述符 1
- 标准错误 (STDERR): 文件描述符 2
2. 常用的文件操作函数
Linux 文件 I/O 提供了一系列的系统调用来进行文件的读写操作,这里是常用的几个函数:
-
open()
: 打开文件 -
read()
: 读取文件 -
write()
: 写入文件 -
lseek()
: 移动文件指针 -
close()
: 关闭文件
close()
函数
close()
函数用于关闭文件描述符,防止资源泄漏
int close(int fd);
open()
函数
open()
函数用于打开文件并返回文件描述符,它的原型如下
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
-
pathname
: 文件路径。 -
flags
: 打开模式,可以是以下几种或它们的组合:-
O_RDONLY
: 只读模式打开文件 -
O_WRONLY
: 只写模式打开文件 -
O_RDWR
: 读写模式打开文件 -
O_CREAT
: 文件不存在时创建文件 -
O_TRUNC
: 打开文件时清空文件内容 -
O_APPEND
: 以追加模式打开文件。如果文件是以写模式(O_WRONLY
或O_RDWR
)打开的,并且设置了O_APPEND
标志,则所有写入都会追加到文件的末尾,而不是覆盖文件的现有内容 -
O_EXCL
: 与O_CREAT
一起使用时,如果文件已经存在,则open
调用会失败。这通常用于创建文件时的原子性检查 -
O_SYNC
: 以同步方式打开文件。每次对文件进行的写操作都会等待物理写入完成后再继续。这会影响性能,但可以保证数据的完整性 -
O_DSYNC
: 类似于O_SYNC
,但只同步数据,不同步元数据(如修改时间和访问时间) -
O_NONBLOCK
: 对于设备文件或FIFO(命名管道),以非阻塞方式打开 -
O_CLOEXEC
: 在exec
调用之后关闭文件描述符。这有助于防止文件描述符在程序执行新程序时被意外继承
-
-
mode_t
是一个数据类型,通常用于表示文件的权限:-
S_IRUSR
:文件所有者具有读权限 -
S_IWUSR
:文件所有者具有写权限 -
S_IXUSR
:文件所有者具有执行权限 -
S_IRGRP
:文件所属组具有读权限 -
S_IWGRP
:文件所属组具有写权限 -
S_IXGRP
:文件所属组具有执行权限 -
S_IROTH
:其他用户具有读权限 -
S_IWOTH
:其他用户具有写权限 -
S_IXOTH
:其他用户具有执行权限
-
这里打开或创建了一个文件 example.txt
,并赋予该文件的拥有者读写权限,这里的S_IRUSR
:表示文件所有者具有读权限,S_IWUSR
:表示文件所有者具有写权限。
#include <fcntl.h> // 引入文件控制选项的头文件,例如O_RDWR, O_CREAT等
#include <unistd.h> // 引入POSIX操作系统API的头文件,例如close()函数
#include <stdio.h> // perror()函数定义在此
int main() {
// 尝试以读写模式打开(如果不存在则创建)名为"example.txt"的文件
// 文件权限设置为只有文件所有者可以读写(S_IRUSR | S_IWUSR)
int fd = open("example.txt", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
// 检查open()函数是否成功执行
// 如果返回-1,表示打开(或创建)文件失败
if (fd == -1) {
// 使用perror()函数打印错误信息
// perror()会根据全局变量errno的值,输出对应的错误信息字符串
// "open"是传递给perror()的字符串,用于标识是哪个函数调用失败
perror("open");
// 返回1,表示程序异常终止
// 在许多操作系统和脚本中,非零返回值通常表示错误或异常
return 1;
}
// 此处可以添加对文件的读写操作
// 例如:write(fd, "Hello, World!", 13);
// 但由于本示例仅关注文件的打开与关闭,因此省略了具体的文件操作
// 使用close()函数关闭文件描述符
// 文件描述符是一个整数,用于在后续的文件操作中标识文件
// 当文件不再需要时,应关闭它,以释放系统资源
close(fd);
// 程序正常结束,返回0
return 0;
}
read()
函数
read()
函数用于从文件中读取数据,其原型如下:
ssize_t read(int fd, void *buf, size_t count);
-
fd
: 文件描述符 -
buf
: 存放读取数据的缓冲区 -
count
: 要读取的字节数
这里从 example.txt
中读取数据并输出读取的字节数
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int fd = open("example.txt", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
char buffer[128];
ssize_t bytesRead = read(fd, buffer, sizeof(buffer));
if (bytesRead == -1) {
perror("read");
} else {
printf("Read %ld bytes: %s\n", bytesRead, buffer);
}
close(fd);
return 0;
}
write()
函数
write()
函数用于向文件中写入数据,其原型如下:
ssize_t write(int fd, const void *buf, size_t count);
-
fd
: 文件描述符 -
buf
: 要写入的数据 -
count
: 要写入的字节数
这里向 example.txt
写入字符串 "Hello, Linux I/O!"
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("open");
return 1;
}
const char *data = "Hello, Linux I/O!";
ssize_t bytesWritten = write(fd, data, sizeof(data));
if (bytesWritten == -1) {
perror("write");
} else {
printf("Wrote %ld bytes\n", bytesWritten);
}
close(fd);
return 0;
}
lseek()
函数
lseek()
用于移动文件指针,其原型如下:
off_t lseek(int fd, off_t offset, int whence);
-
fd
: 文件描述符 -
offset
: 偏移量 -
whence
: 指定如何使用偏移量,常见值为:-
SEEK_SET
: 从文件开头开始 -
SEEK_CUR
: 从当前文件指针位置开始 -
SEEK_END
: 从文件末尾开始
-
这里将文件指针移动到文件末尾
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int fd = open("example.txt", O_RDWR);
if (fd == -1) {
perror("open");
return 1;
}
off_t newPos = lseek(fd, 0, SEEK_END); // 移动到文件末尾
if (newPos == -1) {
perror("lseek");
} else {
printf("New file position: %ld\n", newPos);
}
close(fd);
return 0;
}
3. 文件 I/O 的错误处理
每个系统调用在失败时通常返回 -1
,并设置 errno
变量来指示具体的错误类型。可以通过 perror()
函数输出详细的错误信息
#include <errno.h>
#include <stdio.h>
if (fd == -1) {
perror("open failed");
}
4. 阻塞与非阻塞 I/O
Linux 提供了阻塞 (blocking) 和非阻塞 (non-blocking) I/O 模式:
- 阻塞 I/O: 系统调用会等待操作完成后才返回
- 非阻塞 I/O: 系统调用会立即返回,操作可能未完成
通过设置 open()
的标志为 O_NONBLOCK
,我们可以实现非阻塞 I/O
5. 文件锁定
在多进程环境下,文件锁定非常重要,Linux 提供了两种文件锁定机制:
-
flock()
: 提供简便的文件锁定接口 -
fcntl()
: 提供更细粒度的文件锁定控制
这里为文件 example.txt
添加独占锁,确保只有一个进程能进行写操作
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <stdio.h>
int main() {
int fd = open("example.txt", O_RDWR);
if (fd == -1) {
perror("open");
return 1;
}
if (flock(fd, LOCK_EX) == -1) {
perror("flock");
close(fd);
return 1;
}
// 文件锁定操作
flock(fd, LOCK_UN); // 解锁
close(fd);
return 0;
}
6. 异步 I/O 与内存映射
-
异步 I/O (AIO): 使用
libaio
或 POSIX 的aio_*
系列函数实现异步 I/O,允许进程在 I/O 操作未完成时继续执行其他任务 -
内存映射 (mmap): 使用
mmap()
函数将文件映射到进程的地址空间,可以直接像访问内存一样读写文件数据,提升 I/O 效率
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int fd = open("example.txt", O_RDWR);
if (fd == -1) {
perror("open");
return 1;
}
char *mapped = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mapped == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
// 使用映射的内存
printf("File contents: %s\n", mapped);
munmap(mapped, 4096); // 解除映射
close(fd);
return 0;
}
有点难,详细解释一下:
异步 I/O 的核心思想是让进程在执行 I/O 操作时不必等待操作完成,从而可以并发执行其他任务。通常的同步 I/O 操作是阻塞式的,也就是当一个 I/O 请求发出后,进程会被挂起,直到请求完成。而异步 I/O 允许程序在请求发出后立即返回,后台继续处理该请求,进程可以做其他事情
实现方式:
-
POSIX AIO: POSIX 的
aio_*
系列函数,如aio_read()
和aio_write()
提供异步 I/O 支持,常用于多任务系统和高并发应用。调用这些函数后,I/O 操作在后台执行,进程可继续执行其他代码。通过回调机制、信号、或轮询(polling)等方式,程序可以在 I/O 操作完成时得到通知 - libaio: libaio 是 Linux 系统中用于提供异步 I/O 的库,直接与内核进行交互。与 POSIX AIO 相比,libaio 提供了更高效的非阻塞 I/O 处理方式。它通过提交 I/O 请求给内核,并利用内核事件完成队列(completion queue)来通知用户态程序操作的完成
内存映射 (mmap)
内存映射 (mmap) 的主要功能是将文件或设备映射到进程的虚拟地址空间,从而能够像访问内存一样对文件进行读写操作。它减少了读写文件时内核与用户态之间的上下文切换,同时避免了传统的 read()
和 write()
系统调用的重复数据拷贝,极大地提高了文件 I/O 的效率
实现机制:
-
mmap() 函数: 使用
mmap()
函数,应用程序可以将一个文件或设备映射到其进程的虚拟地址空间。映射完成后,文件的内容就像内存中的数据一样可直接访问,无需通过额外的系统调用 - 虚拟内存页: 操作系统将文件的部分或全部内容映射到内存页,进程通过访问这些内存页来读取或写入文件。当访问的页面不在物理内存中时,操作系统会自动将对应的页面从磁盘加载到内存(称为“页面缺失”)
-
共享与私有映射: mmap 支持共享(
MAP_SHARED
)和私有(MAP_PRIVATE
)两种模式。共享模式下,多个进程可以映射同一个文件,任何进程的修改都会反映到文件中;私有模式下,修改仅对当前进程可见
所以:
- 异步 I/O (AIO) 适用于需要高并发、非阻塞 I/O 操作的场景,能够减少 I/O 操作的等待时间,充分利用 CPU 和 I/O 资源
- 内存映射 (mmap) 则提供了一种更高效的文件 I/O 方式,减少系统调用开销和数据拷贝,提高了文件读写的性能,特别适合处理大文件