掌握 Linux 文件操作函数,让你的代码高效又稳健
1. 引言
1.1 Linux 文件操作相关函数简介
在 Linux
开发中,文件操作是最基础但又至关重要的能力。无论是读取配置文件、记录日志,还是处理数据存储,掌握 Linux
文件操作相关函数,能让你的代码更高效、更可靠。
1.2 你能获得什么
这篇文章将带你深入理解 Linux 文件操作的核心 API,包括 open
、read
、write
、close
、lseek
、stat
、mmap
等,帮助你:
- 正确打开、读取、写入和关闭文件,避免资源泄漏
- 提高文件操作的性能,减少不必要的系统调用
- 掌握高级文件操作,如
mmap
内存映射、文件锁等 - 避免常见错误,写出更健壮的代码 💪
2. 文件打开与关闭
2.1 open
打开文件
在 C 语言中,使用 open()
来打开文件。它是 openat()
的封装,直接调用时,相当于 openat(AT_FDCWD, ...)
,在当前工作目录下操作。
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int fd = open("test.txt", O_RDWR | O_CREAT, 0644);
if (fd == -1) {
perror("open failed");
return -1;
}
参数解析:
O_RDWR
:以读写模式打开文件O_CREAT
:如果文件不存在,则创建(必须搭配mode
参数,如0644
)0644
:文件权限,所有者可读写 (rw-
),组用户和其他用户可读 (r--
)
⚠️ 常见问题:
-
文件权限不生效?
O_CREAT
需要mode
参数,否则创建的文件权限不可控。- 受
umask
影响,0644
可能变成0600
(umask
默认0022
),可以用umask(0);
取消影响。
-
文件已存在但
O_CREAT
仍失败?- 可能缺少
O_WRONLY
或O_RDWR
,如果文件存在但无写权限,open()
仍然失败。
- 可能缺少
-
如何避免误覆盖?
-
不要用
O_TRUNC
,它会清空文件内容。 -
用
O_EXCL
,如果文件已存在就失败:int fd = open("test.txt", O_RDWR | O_CREAT | O_EXCL, 0644); if (fd == -1) { perror("File exists"); }
-
2.2 close
关闭文件
文件用完后,必须用 close(fd)
释放资源,否则可能导致文件描述符泄漏,影响系统稳定性。
if (close(fd) == -1) {
perror("close failed");
}
⚠️ 注意事项:
-
不能重复关闭:
close(fd); close(fd); // ❌ 错误:fd 已关闭,可能导致 `EBADF` 错误
-
必须检查
close()
返回值:- 如果
close()
失败(如NFS
断连),fsync()
可能也失败,数据可能未写入磁盘。 - 关闭失败可能意味着数据丢失,应立即
fsync(fd)
确保数据已落盘。
- 如果
-
如何避免泄漏?
- 尽早关闭:打开后立即
close(fd)
避免遗忘。 - 使用
valgrind
检查:valgrind --track-fds=yes ./my_program
- C++ 可用
RAII
机制,用std::unique_ptr<int, decltype(&close)> fd_guard(fd, close);
自动管理fd
。
- 尽早关闭:打开后立即
✅ 小结:
open()
需要正确传参,O_CREAT
需搭配mode
,O_EXCL
避免误覆盖。close()
不能重复调用,必须检查返回值,防止数据丢失。- 最佳实践:打开文件后尽快
close()
,并用fsync()
确保数据安全。🚀
3. 读写文件数据
3.1 read
读取文件
read()
负责从文件描述符 fd
中读取数据,并存入缓冲区 buffer
。
#include <unistd.h>
#include <stdio.h>
char buffer[100];
ssize_t bytesRead = read(fd, buffer, sizeof(buffer));
if (bytesRead == -1) {
perror("read failed");
}
🔍 参数解析:
fd
:文件描述符(必须是已打开的文件)buffer
:存储读取数据的缓冲区sizeof(buffer)
:读取的最大字节数
📌 返回值:
>0
:成功读取的字节数(可能小于请求的字节数)0
:文件已到达末尾 (EOF
)-1
:读取失败,需要检查errno
进行错误处理
⚠️ 错误处理:
-
EINTR
(被信号中断)read()
可能在读取过程中被信号打断,需要重新尝试:
ssize_t bytesRead; do { bytesRead = read(fd, buffer, sizeof(buffer)); } while (bytesRead == -1 && errno == EINTR);
-
EAGAIN
或EWOULDBLOCK
(非阻塞模式下无数据可读)- 说明当前
fd
没有可读数据,但不会阻塞,可以稍后重试。 - 适用于
O_NONBLOCK
方式打开的文件。
- 说明当前
-
EBADF
(无效文件描述符)- 确保
fd
已正确打开,并且具有读取权限 (O_RDONLY
或O_RDWR
)。
- 确保
3.2 write
写入文件
write()
负责将数据写入文件描述符 fd
,但不保证一次性写入所有数据,需要检查返回值。
#include <unistd.h>
#include <stdio.h>
ssize_t bytesWritten = write(fd, "Hello, Linux!", 13);
if (bytesWritten == -1) {
perror("write failed");
}
📌 返回值:
>0
:实际写入的字节数(可能小于请求的字节数)-1
:写入失败,需要检查errno
进行错误处理
⚠️ write()
可能的失败情况:
-
EINTR
(被信号中断)- 需要循环写入直到完成:
ssize_t totalWritten = 0; ssize_t bytesWritten; const char *data = "Hello, Linux!"; size_t dataLen = 13; while (totalWritten < dataLen) { bytesWritten = write(fd, data + totalWritten, dataLen - totalWritten); if (bytesWritten == -1) { if (errno == EINTR) { continue; // 继续尝试写入 } else { perror("write failed"); break; } } totalWritten += bytesWritten; }
-
EAGAIN
/EWOULDBLOCK
(非阻塞模式下写缓冲区已满)- 适用于
O_NONBLOCK
文件,需等待缓冲区可用。
- 适用于
-
ENOSPC
(磁盘空间不足)- 需要检查磁盘空间 (
df -h
) 并清理文件。
- 需要检查磁盘空间 (
-
EBADF
(无效文件描述符)- 确保
fd
已正确打开,并具有写入权限 (O_WRONLY
或O_RDWR
)。
- 确保
🎯 小结
read()
需要检查返回值,EINTR
需重新读取,0
表示EOF
。write()
不能假设一次写入完成,需要循环写入直到totalWritten == bytesRead
。
这样,你的文件操作就更健壮了 💪🚀!
4. 高级文件操作
在基础的文件 open
、read
、write
之外,Linux 还提供了一些高级文件操作函数,例如文件定位、文件信息查询、内存映射、文件控制等。本章将详细介绍它们的用法及最佳实践。
4.1 lseek
文件定位
lseek()
允许你随意跳转文件位置,适用于随机读取和文件扩展。
📝 使用方法:
#include <unistd.h>
#include <fcntl.h>
off_t offset = lseek(fd, 10, SEEK_SET); // 从文件开头跳过 10 个字节
if (offset == -1) {
perror("lseek failed");
}
📌 常见模式:
SEEK_SET
:从文件开头偏移offset
个字节SEEK_CUR
:从当前位置偏移offset
个字节SEEK_END
:从文件末尾偏移offset
个字节(offset
需为负值,向前移动)
⚠️ 注意事项:
lseek()
只是移动文件指针,不会修改文件内容。- 如果
offset
超过文件长度,文件不会立即增长,但后续write()
会填充空洞。 - 用于获取文件大小:
off_t fileSize = lseek(fd, 0, SEEK_END); printf("文件大小: %ld 字节\n", fileSize);
4.2 stat
获取文件信息
stat()
和 fstat()
用于获取文件的大小、权限、时间信息。
📝 使用方法:
#include <sys/stat.h>
#include <stdio.h>
struct stat fileStat;
if (stat("test.txt", &fileStat) == -1) {
perror("stat failed");
} else {
printf("文件大小: %ld 字节\n", fileStat.st_size);
printf("最后修改时间: %ld\n", fileStat.st_mtime);
}
📌 相关函数:
stat("file", &st)
:获取指定文件的信息fstat(fd, &st)
:获取已打开文件的信息lstat("file", &st)
:用于符号链接,不会解析真实路径
🔍 struct stat
关键字段:
st_size
:文件大小(字节)st_mode
:文件类型和权限(可用S_ISREG()
等宏判断)st_mtime
:最后修改时间
⚠️ 注意:
stat()
可能失败(如文件不存在、无权限),需检查errno
。st_mtime
是time_t
类型,可用ctime()
转换成人类可读格式。
4.3 mmap
内存映射文件
mmap()
将文件映射到内存,让你可以像访问数组一样操作文件,提高大文件读写效率。
📝 使用方法:
#include <sys/mman.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
int fd = open("test.txt", O_RDONLY);
struct stat st;
fstat(fd, &st); // 获取文件大小
char *mapped = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (mapped == MAP_FAILED) {
perror("mmap failed");
} else {
write(STDOUT_FILENO, mapped, st.st_size); // 直接输出映射的内容
munmap(mapped, st.st_size);
}
close(fd);
📌 参数解析:
NULL
:让内核选择映射地址st.st_size
:映射的文件大小PROT_READ
:只读模式(也可用PROT_WRITE
进行写入)MAP_PRIVATE
:私有映射,对内存的修改不会影响原文件
⚡ 适用场景:
- 大文件读取(避免
read()
的内核态拷贝开销) - 共享内存(
MAP_SHARED
可实现进程间通信)
⚠️ mmap()
注意事项:
-
mmap()
的内容会受lseek()
影响吗?- 不会,
mmap()
直接操作内存,与lseek()
无关。
- 不会,
-
如何正确释放
mmap()
资源?- 需要
munmap(mapped, size)
,否则可能会造成内存泄漏。
- 需要
4.4 fcntl
文件控制
fcntl()
用于修改文件状态,如非阻塞模式、文件锁等。
📝 设置非阻塞模式:
#include <fcntl.h>
#include <stdio.h>
int flags = fcntl(fd, F_GETFL); // 获取当前文件状态
fcntl(fd, F_SETFL, flags | O_NONBLOCK); // 添加 O_NONBLOCK(非阻塞模式)
📌 fcntl()
主要功能:
F_GETFL
/F_SETFL
:获取/设置文件状态,如O_NONBLOCK
F_GETFD
/F_SETFD
:获取/设置文件描述符标志,如FD_CLOEXEC
F_SETLK
/F_SETLKW
:文件锁(flock()
的替代方案)
🛠 fcntl()
进行文件加锁:
struct flock lock;
lock.l_type = F_WRLCK; // 写锁
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0; // 0 代表锁定整个文件
if (fcntl(fd, F_SETLK, &lock) == -1) {
perror("lock failed");
} else {
printf("文件已加锁\n");
}
⚠️ fcntl()
注意事项:
F_SETLK
是非阻塞锁,如果文件已被锁定,会立刻返回EBUSY
。F_SETLKW
是阻塞锁,会一直等待直到锁可用。- 文件锁是
advisory
,不是强制锁,其他进程如果不检查锁,仍然可以写入!
🎯 小结
lseek()
用于文件定位,适合随机读写和获取文件大小。stat()
能获取文件的大小、权限、修改时间,适合文件管理。mmap()
适用于大文件高效读写,但需要munmap()
释放内存。fcntl()
用于修改文件状态、加锁,常用于非阻塞 I/O 和进程间锁。
学会这些高级操作,你的 Linux 文件操作技能就更上一层楼了!🚀💪
5. 实战案例
理论结合实践,学得更快更牢!这一章我们通过实战案例,综合运用文件操作相关函数,并用 strace
观察系统调用的实际执行情况。
5.1 综合案例:文件复制
我们实现一个简单的文件复制程序,使用 open()
、read()
、write()
、close()
等函数,让你更熟悉 Linux 文件操作的流程。
📝 代码实现
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define BUF_SIZE 4096 // 缓冲区大小
void copy_file(const char *src, const char *dst) {
int src_fd = open(src, O_RDONLY);
if (src_fd == -1) {
perror("open source file failed");
return;
}
int dst_fd = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (dst_fd == -1) {
perror("open destination file failed");
close(src_fd);
return;
}
char buffer[BUF_SIZE];
ssize_t bytes_read, bytes_written;
while ((bytes_read = read(src_fd, buffer, BUF_SIZE)) > 0) {
char *ptr = buffer;
while (bytes_read > 0) {
bytes_written = write(dst_fd, ptr, bytes_read);
if (bytes_written == -1) {
perror("write failed");
close(src_fd);
close(dst_fd);
return;
}
bytes_read -= bytes_written;
ptr += bytes_written;
}
}
if (bytes_read == -1) {
perror("read failed");
}
close(src_fd);
close(dst_fd);
}
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <source> <destination>\n", argv[0]);
return 1;
}
copy_file(argv[1], argv[2]);
return 0;
}
📌 代码解析
-
open()
- 以
O_RDONLY
方式打开源文件。 - 以
O_WRONLY | O_CREAT | O_TRUNC
方式创建/清空目标文件。 - 目标文件权限
0644
,即所有者可读写,其他用户可读。
- 以
-
read()
&write()
- 循环读取:每次读取
BUF_SIZE
(4KB)数据到buffer
。 - 循环写入:确保所有数据写入目标文件(防止
write()
只写入部分数据)。
- 循环读取:每次读取
-
close()
- 释放文件描述符,防止资源泄漏。
⚠️ 注意事项
- 错误处理:每个
open()
、read()
、write()
、close()
调用后都要检查返回值。 write()
可能无法一次性写完所有数据,所以需要循环写入直到数据全部写入。- 大文件优化:使用
mmap()
可能会比read()
/write()
方式更高效。
5.2 用 strace
观察文件操作
想知道一个程序内部调用了哪些系统调用?strace
是一个非常强大的工具,它可以跟踪系统调用,帮助你分析程序的文件操作行为。
📝 基本用法
strace -e trace=open,read,write,close ./file_copy source.txt dest.txt
💡 -e trace=open,read,write,close
:只跟踪文件相关的系统调用。
📌 示例输出
open("source.txt", O_RDONLY) = 3
open("dest.txt", O_WRONLY|O_CREAT|O_TRUNC, 0644) = 4
read(3, "Hello, world!", 4096) = 13
write(4, "Hello, world!", 13) = 13
close(3) = 0
close(4) = 0
🔍 分析
open("source.txt", O_RDONLY) = 3
- 打开源文件
source.txt
,分配的文件描述符是3
。
- 打开源文件
open("dest.txt", O_WRONLY|O_CREAT|O_TRUNC, 0644) = 4
- 创建/清空
dest.txt
,分配的文件描述符是4
。
- 创建/清空
read(3, "Hello, world!", 4096) = 13
- 从文件
3
读取 13 字节内容(Hello, world!
)。
- 从文件
write(4, "Hello, world!", 13) = 13
- 将 13 字节写入目标文件
4
。
- 将 13 字节写入目标文件
close(3) = 0
和close(4) = 0
- 关闭文件,释放资源。
🎯 小结
- 文件复制是一个很好的实战练习,掌握
open()
、read()
、write()
、close()
的基本用法和错误处理。 strace
可以跟踪系统调用,帮助你分析程序的文件操作行为,是调试必备工具。
掌握这些内容后,你已经具备 Linux 文件操作的实战经验!💪🚀
6. 总结
-
文件操作 API 核心点:
open()
/close()
负责文件打开与关闭read()
/write()
进行数据读写lseek()
控制文件偏移stat()
获取文件信息mmap()
高效读写大文件
-
最佳实践:
- 错误处理要到位,检查
open
、read
、write
返回值 - 资源必须释放,用
close(fd)
关闭文件 - 考虑性能优化,大文件推荐
mmap()
- 错误处理要到位,检查
希望这篇文章能帮助你掌握 Linux 文件操作,让你的代码更高效、更稳健!🚀