大家好,接下来的一段时间我将开始学习野火的Linux系统课程并将学习到的干货逐步更新到我的CSDN博客中。没时间刷课的同学可以把我的博客喂给AI 突击一下。
目录
1. 内容概览
2. 文件描述符fd
2.1 基本概念
2.2 标准文件描述符
2.3 打开文件对象的共享特性
3. 打开文件
3.1 打开/创建文件函数:open ()
3.2 返回值与错误处理
5. 关闭文件
5.1 关闭文件函数close ():
5.2 返回值
5.3 核心原理
6. 读取文件
6.1 读取文件函数read ():
6.2 返回值
6.3 短读与中断
7. 写入数据
7.1 写入数据函数write ():
7.2 write_all 函数
8.文件偏移控制
8.1 文件偏移控制函数 lseek ():
8.2 返回值
8.3 偏移基准
8.4 文件空洞原理
9. 页缓存与数据落盘机制
9.1 IO 数据流转路径
9.2 三个同步函数对比
sync()
fsync() / fdatasync()
10. 完整文件复制工程代码(标准严谨版)
11. 高频易错点总结
1. 内容概览
Linux 中对文件的基本操作通常通过系统调用完成:
open → read / write → lseek(可选)→ sync / fsync(可选)→ close常用接口:
- open():打开或创建文件,返回文件描述符。
- read():从文件读取数据。
- write():向文件写入数据。
- lseek():修改当前文件偏移量。
- close():关闭文件描述符。
- sync():请求回写系统中所有文件系统的脏数据。
- fsync():同步指定文件的数据和必要元数据。
- fdatasync():主要同步指定文件的数据。
2. 文件描述符fd
task_struct:进程 PCB,描述进程所有信息;struct files_struct *files成员,指向该进程的打开文件管理结构。每个进程独立一份 files_struct。files_struct:本进程打开文件总管理对象。内部核心是fd_array[]数组(fd 表)。- fd 就是这个数组的索引下标(非负整数:0,1,2,3...)
- 数组里存的是
struct file*指针,不是文件本身
- fd 表(fd_array):下标 = fd;每个表项:指针 + fd 私有标志(如 FD_CLOEXEC)。fd 是进程私有的!不同进程相同 fd 数字,指向完全不同的 struct file
struct file:一次打开操作的上下文对象(内核对象)。同一个文件,可以 open 多次,生成多个独立 struct file。- 里面保存本次打开的状态:偏移、打开标志、访问模式、文件操作函数集等。
成员 含义 考点重点 f_pos 文件偏移量(off_t) 读写位置;存在 struct file,不是 fd!多个 fd 共享同一个 struct file 就共享同一个偏移;两次 open 生成两个 file,偏移互相独立。lseek 修改 f_pos。 f_flags 文件状态标志 open 传入的 flags(O_RDONLY/O_APPEND/O_NONBLOCK 等);属于打开文件对象属性,多个 fd 共享。可以用 fcntl 修改。 f_mode 访问模式 文件读写权限,标识只读 / 只写 / 读写,内核用来校验读写是否合法。 f_op struct file_operations *文件操作函数集内核回调函数表:read、write、llseek、release 等。不同设备(普通文件 / 管道 /socket)挂载不同的操作函数。read/write 系统调用最终调用 f_op->read/f_op->write。 f_path 保存路径相关信息,dentry+super_block 用来拿到 inode、文件系统信息;记录这个打开文件对应的目录项。
- inode:描述磁盘上文件本身(文件名、大小、权限、磁盘块)。多个 struct file 可以指向同一个 inode。
2.1 基本概念
文件描述符(file descriptor,fd)是用户程序用来引用已打开文件或其他 I/O 对象的非负整数。
从概念上看,fd 是当前进程文件描述符表中的索引。表项进一步指向内核中的打开文件对象struct file。
int fd; fd = open(pathname, flags, mode);2.2 标准文件描述符
进程启动后fd表(fd_array)默认的三个fd:
| fd | 宏 | 含义 |
|---|---|---|
| 0 | STDIN_FILENO | 标准输入 |
| 1 | STDOUT_FILENO | 标准输出 |
| 2 | STDERR_FILENO | 标准错误 |
这是约定而非永久绑定,支持关闭、重定向到文件、管道、套接字等。
2.3 打开文件对象的共享特性
- 两次 open:生成两个独立
struct file,偏移独立。 - 复制 fd:多个 fd 共享同一个
struct file,共享文件偏移。 - FD_CLOEXEC:属于fd 私有标志,不共享。
- 父子进程:fd 表独立,但指向同一个内核 file 对象。
3. 打开文件
3.1 打开/创建文件函数:open ()
头文件与函数原型: #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(const char *pathname, int flags, ... /* mode_t mode */); 常用写法: int fd = open("a.txt", O_RDONLY); int fd = open("b.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); !!只有带 O_CREAT 时,第三个权限参数才生效。open()的flags=唯一主模式+多个副模式按位或。
| 标志 | 含义 |
|---|---|
O_RDONLY | 只读打开 |
O_WRONLY | 只写打开 |
O_RDWR | 读写打开 |
| 标志 | 作用说明 |
|---|---|
O_CREAT | 文件不存在则创建,必须搭配 mode 参数 |
O_EXCL | 配合 O_CREAT,文件存在则 open 失败,防竞争创建 |
O_TRUNC | 可写打开已有文件,清空文件内容(截断为 0) |
O_APPEND | 原子追加,每次写自动跳到文件末尾(多进程安全) |
O_NONBLOCK | 非阻塞 IO,读写不等待 |
O_SYNC | 同步写,数据 + 元数据强制落盘 |
O_DSYNC | 仅同步数据 + 必要元数据,效率高于 O_SYNC |
O_DIRECT | 绕过页缓存读写,需要内存对齐 |
O_CLOEXEC | exec 执行时自动关闭 fd,防文件描述符泄露 |
注意:O_DIRECT并非完全无条件绕过缓存,也不保证数据永久落盘。
仅供了解:
mode 权限与 umask
创建文件最终权限公式:最终权限 = mode & ~umask
1、mode 是什么open 函数手动传入的预设权限,仅创建文件(O_CREAT)时生效。 示例:
0666预设:所有者、同组、其他用户 全部可读可写。2、umask 是什么系统全局权限掩码,作用:删掉不安全的权限。 系统默认一般
0002:禁止 “其他用户” 写权限。3、& ~ 是什么运算
~umask:把系统要禁止的权限翻转成允许的权限
mode & ~umask:在你预设权限里,保留系统允许的、剔除系统禁止的4、为什么必须写八进制、必须带前缀 0?Linux 权限是三组三位数权限,正好对应八进制 0~7。
0666= 八进制权限(合法)
666= 编译器识别为十进制,完全错乱,权限彻底错误
3.2 返回值与错误处理
- 成功:返回非负整数 fd
- 失败:返回-1,设置 errno
int fd = open("a.txt", O_RDONLY); if (fd == -1) { perror("open"); return 1; }注意:关闭 0/1/2 后,open 可能返回 0、1、2,不能用 fd>2 判断合法性。
5. 关闭文件
5.1 关闭文件函数close ():
头文件和函数原型 #include <unistd.h> int close(int fd);5.2 返回值
- 成功:0
- 失败:-1
5.3 核心原理
close 仅删除当前进程 fd 表项、引用计数 - 1;只有引用计数为 0,内核才真正释放 file 对象。
注意:close 不删除磁盘文件,删除文件需要unlink。
6. 读取文件
6.1 读取文件函数read ():
头文件和函数原型 #include <unistd.h> ssize_t read(int fd, void *buf, size_t count); #参数:fd 文件描述符、buf 用户缓冲区、count 期望读取字节数。6.2 返回值
| 返回值 | 含义 |
|---|---|
| >0 | count:成功读取全部字节 |
| =0 | 读到文件末尾 EOF,不是错误 |
| -1 | 读取失败 |
必须用ssize_t(有符号整型)接收返回值。
6.3 短读与中断
短读是正常现象:文件末尾、管道 / 套接字、信号中断、非阻塞 IO 都会导致读取不足 count。errno == EINTR属于可重试错误。
7. 写入数据
7.1 写入数据函数write ():
头文件和函数原型 #include <unistd.h> ssize_t write(int fd, const void *buf, size_t count);7.2 write_all 函数
短写:
write返回正数,但实际写入字节少于期望写入数量,属于正常现象(管道、socket 容易出现,普通文件较少)。
功能:解决write存在短写的问题,保证把指定全部字节完整写入;同时处理系统调用被信号中断(EINTR)的可重试情况。
函数源码
#include <errno.h> static int write_all(int fd, const void *data, size_t size) { const char *p = data; while (size > 0) { ssize_t n = write(fd, p, size); if (n > 0) { p += n; size -= (size_t)n; } else if (n == -1 && errno == EINTR) { continue; } else { return -1; } } return 0; }参数:
fd:目标文件描述符data:待写入数据的起始地址size:需要写入的总字节数
返回值:
0:全部数据写入成功-1:发生 IO 错误,写入失败
使用示例:
char str[] = "hello world"; int ret = write_all(fd, str, sizeof(str)); if(ret == -1){ perror("write_all"); }重点:write 成功仅代表数据进入内核页缓存,不代表落盘。
8.文件偏移控制
8.1 文件偏移控制函数 lseek ():
#include <unistd.h> off_t lseek(int fd, off_t offset, int whence); 若whence为SEEK_SET,基准点为文件开头 若whence为SEEK_CUR,基准点为当前位置 若whence为SEEK_END,基准点为文件末尾8.2 返回值
- 成功:返回当前文件偏移,
- 失败:返回 - 1。
8.3 偏移基准
| 宏 | 基准位置 | 最终偏移 |
|---|---|---|
| SEEK_SET | 文件开头 | offset |
| SEEK_CUR | 当前偏移 | 当前 + offset |
| SEEK_END | 文件末尾 | 文件长度 + offset |
8.4 文件空洞原理
write(fd, "123", 3); // 偏移=3 lseek(fd, 100, SEEK_CUR); // 偏移=103 write(fd, "abc", 3); // 103位置写入中间空白区域形成文件空洞:逻辑存在、磁盘不占空间,读取默认补 0。
注意:管道、套接字、FIFO不支持 lseek。
9. 页缓存与数据落盘机制
9.1 IO 数据流转路径
读:磁盘 → 内核页缓存 → 用户缓冲区
写:用户缓冲区 → 内核页缓存 → 磁盘(延迟回写)
write 返回成功只是写入缓存,断电丢失风险存在。
9.2 三个同步函数对比
sync()
void sync(void);全局刷新所有文件系统脏页,效率低,不精准。
fsync() / fdatasync()
int fsync(int fd); int fdatasync(int fd);- fsync:同步数据 + 全部元数据(权限、时间等)
- fdatasync:仅同步数据 + 必要元数据,性能更高
10. 完整文件复制工程代码(标准严谨版)
#include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> static int write_all(int fd, const void *data, size_t size) { const char *p = data; while (size > 0) { ssize_t n = write(fd, p, size); if (n > 0) { p += n; size -= (size_t)n; } else if (n == -1 && errno == EINTR) { continue; } else { return -1; } } return 0; } int main(int argc, char **argv) { char buf[4096]; int src = -1; int dst = -1; int result = EXIT_FAILURE; if (argc != 3) { fprintf(stderr, "用法:%s <源文件> <目标文件>\n", argv[0]); return EXIT_FAILURE; } src = open(argv[1], O_RDONLY); if (src == -1) { perror("open source"); goto cleanup; } dst = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0666); if (dst == -1) { perror("open destination"); goto cleanup; } for (;;) { ssize_t nread = read(src, buf, sizeof buf); if (nread > 0) { if (write_all(dst, buf, (size_t)nread) == -1) { perror("write"); goto cleanup; } } else if (nread == 0) { break; } else if (errno != EINTR) { perror("read"); goto cleanup; } } result = EXIT_SUCCESS; cleanup: if (dst != -1 && close(dst) == -1) perror("close destination"); if (src != -1 && close(src) == -1) perror("close source"); return result; }关键:目标文件必须加O_TRUNC,防止旧文件残留尾部数据。
11. 高频易错点总结
- read 返回 0 是 EOF,不是错误。
- read/write 返回值必须用ssize_t,不能用 int。
- write 存在短写,必须循环写完。
- O_CREAT 必须带八进制 mode,权限受 umask 影响。
- O_APPEND 是原子追加,比手动 lseek 安全(多进程)。
- write 成功 ≠ 数据落盘,持久化需要 fsync/fdatasync。
- 两次 open:两个独立 struct file,文件偏移 f_pos 互相独立;
- dup:多个 fd 共用同一个 struct file,共享同一份 f_pos
- lseek 不能用于管道、套接字等流式文件。