一、Linux 文件系统基础:一切皆文件的哲学
在 Linux 的世界里,“一切皆文件” 是核心设计哲学。无论是普通文件、目录、设备(如硬盘、串口),还是网络套接字,都被抽象为文件模型,通过统一的接口进行操作。这种设计让开发者能以一致的方式处理不同类型的资源。
1.1 文件类型详解
文件类型 | 核心特性 | 典型示例 / 路径 |
---|---|---|
普通文件 | 存储数据,分为文本文件(可读字符)和二进制文件(字节流数据) | hello.txt (文本)、image.bin (二进制) |
目录文件 | 存储文件和子目录列表,通过 ls 命令查看内容 | /etc 、/home/user |
设备文件 | - 字符设备:按字节读写(如键盘、串口) - 块设备:按数据块读写(如硬盘) | /dev/ttyS0 (串口)、/dev/sda (硬盘) |
管道文件 | 进程间通信通道,通过 mkfifo 创建,用于单向数据传输 | pipe_file |
套接字文件 | 网络通信抽象,常见于 /var/run 目录,用于进程间网络通信 | db.sock (数据库套接字) |
1.2 核心文件操作分类
- 基础操作:打开 / 关闭文件、读写数据(文件交互的入口与核心)
- 控制操作:权限管理、光标定位、文件状态查询(精细化管理文件属性)
- 高级操作:原子操作、文件锁定、IO 模型配置(应对复杂场景的关键技术)
二、标准 C 库文件操作:缓冲 IO 的便捷之道
标准 C 库提供带缓冲机制的文件操作函数,适合处理文本和格式化数据,减少系统调用次数,提升 I/O 效率。
2.1 打开文件:fopen 家族
FILE *fopen(const char *path, const char *mode); // 常规打开文件
FILE *freopen(const char *path, const char *mode, FILE *stream); // 重定向标准流(如 stdin/stdout)
FILE *fdopen(int fildes, const char *mode); // 文件描述符转文件指针
核心模式参数(快速查表)
模式 | 读写权限 | 创建 / 覆盖行为 | 定位起点 | 典型应用场景 |
---|---|---|---|---|
r | 只读 | 文件必须存在 | 文件开头 | 读取配置文件 |
w | 只写 | 清空已存在文件 / 创建新文件 | 文件开头 | 初始化日志文件 |
a | 追加写 | 文件不存在时创建 | 文件末尾 | 追加日志记录 |
r+ | 读写 | 文件必须存在 | 文件开头 | 读写配置文件 |
a+ | 读写追加 | 文件不存在时创建 | 文件末尾 | 读写日志文件 |
示例:安全打开文件并处理错误
#include <stdio.h>
int main() {
FILE *fp = fopen("data.txt", "a+");
if (fp == NULL) {
perror("Failed to open file"); // 打印系统级错误信息(如权限不足)
return 1;
}
fclose(fp); // 及时关闭文件,释放资源
return 0;
}
2.2 读写操作:从基础到进阶
① 格式化读写(文本处理首选)
// 写入:将结构化数据按格式写入文件
fprintf(fp, "Name: %s, Age: %d\n", "Alice", 30);
// 读取:按预设格式解析文件内容
char name[20];
int age;
fscanf(fp, "Name: %s, Age: %d", name, &age); // 自动跳过空格和换行
② 二进制读写(高效存储二进制数据)
struct Student {
char name[20];
int age;
};
struct Student stu = {"Bob", 25};
// 写入:直接存储结构体到文件
fwrite(&stu, sizeof(struct Student), 1, fp);
// 读取:恢复二进制数据到结构体
struct Student readStu;
fread(&readStu, sizeof(struct Student), 1, fp);
③ 单字符 / 字符串操作(细粒度数据处理)
int ch = fgetc(fp); // 读取单个字符(返回 ASCII 值,失败时返回 EOF)
fputc('A', fp); // 写入单个字符
char buf[100];
fgets(buf, 100, fp); // 读取一行(含换行符,最多读取 size-1 个字符)
fputs("Hello\n", fp); // 写入字符串(不自动添加换行符,需手动添加)
2.3 关键辅助函数(提升操作精度)
- 文件结束检测:
feof(fp)
返回非零值时表示到达文件尾(需在读操作后调用,避免误判) - 光标定位:
fseek(fp, 0, SEEK_END); // 定位到文件末尾 long size = ftell(fp); // 获取当前光标位置(即文件大小,单位:字节)
- 缓冲区控制:
fflush(fp)
强制刷新缓冲区(重要!避免程序崩溃导致数据未写入磁盘)
三、Linux 系统级文件操作:底层控制的力量
Linux 系统级文件操作通过内核提供的系统调用实现,直接操作文件描述符(File Descriptor),具有无缓冲、高性能的特点,适用于设备控制、底层资源管理等场景。
3.1 打开文件:open
函数深度解析
#include <fcntl.h>
int open(const char *pathname, int flags, mode_t mode);
函数参数详解
参数 | 说明 | |
---|---|---|
pathname | 要打开的文件路径(绝对路径或相对路径),如 "/etc/hosts" 或 "data.txt" 。 | |
flags | 打开文件的模式和标志(必背核心参数,可通过按位或 ` | ` 组合多个标志)。 |
mode | 仅在创建新文件(使用 O_CREAT 标志)时有效,指定文件权限(八进制,如 0666 )。 |
核心 flags
参数分类(按功能分组)
一、基础打开模式(必选其一)
标志位 | 功能 | 示例 | |
---|---|---|---|
O_RDONLY | 以只读模式打开文件,文件必须存在。 | open("config.txt", O_RDONLY); | |
O_WRONLY | 以只写模式打开文件。 | `open("log.txt", O_WRONLY | O_CREAT, 0644);` |
O_RDWR | 以读写模式打开文件。 | open("temp.txt", O_RDWR); |
二、创建与控制标志(可选组合)
标志位 | 功能 |
---|---|
O_CREAT | 若文件不存在则创建新文件。需配合 mode 参数设置权限(如 0666 )。 |
O_EXCL | 与 O_CREAT 同时使用时,若文件已存在则打开失败(避免重复创建)。 |
O_TRUNC | 若文件已存在且以写模式打开(O_WRONLY /O_RDWR ),则清空文件内容。 |
O_APPEND | 以追加模式打开文件,每次写入时自动定位到文件末尾(不覆盖原有内容)。 |
三、特殊模式(设备文件 / 管道专用)
标志位 | 功能 |
---|---|
O_NONBLOCK | 非阻塞模式,用于设备文件(如串口、管道)。无数据时立即返回,不阻塞进程。 |
权限计算:mode & ~umask
mode
:用户指定的文件权限(八进制,如0666
表示所有者 / 组 / 其他用户均可读可写)。umask
:系统默认权限掩码(通过umask
命令查看,默认值通常为0022
),用于屏蔽某些权限。- 实际权限 =
mode
按位与umask
的取反值。// 示例:创建文件并设置权限 int fd = open("user.txt", O_CREAT | O_WRONLY, 0666); // 若 umask=0002,实际权限为 0666 & ~0002 = 0664(所有者/组可读可写,其他用户可读)
3.2 读写操作:直接操作文件描述符
基础读写函数
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count); // 从文件读取数据
ssize_t write(int fd, const void *buf, size_t count); // 向文件写入数据
参数详解
函数 | 参数 | 说明 |
---|---|---|
read | fd | 文件描述符(通过 open 函数返回)。 |
buf | 用于存储读取数据的缓冲区(指针)。 | |
count | 期望读取的字节数。成功时返回实际读取的字节数(0 表示文件末尾),失败返回 -1 。 | |
write | fd | 文件描述符(需以写模式打开)。 |
buf | 包含要写入数据的缓冲区(指针)。 | |
count | 期望写入的字节数。成功时返回实际写入的字节数,失败返回 -1 。 |
示例:文件复制(系统调用版)
#include <fcntl.h>
int main() {
int src_fd = open("source.txt", O_RDONLY); // 只读打开源文件(fd=3)
int dest_fd = open("dest.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); // 创建目标文件
char buf[4096];
ssize_t n;
while ((n = read(src_fd, buf, sizeof(buf))) > 0) { // 循环读取,直到返回 0(文件末尾)
write(dest_fd, buf, n); // 将读取的数据写入目标文件
}
close(src_fd); // 关闭文件描述符,释放系统资源
close(dest_fd);
return 0;
}
3.3 文件描述符:0、1、2 的特殊意义
Linux 系统为每个进程预定义了三个标准文件描述符,用于输入输出操作:
描述符 | 宏定义 | 默认设备 | 功能 | 典型用法 |
---|---|---|---|---|
0 | STDIN_FILENO | 键盘(标准输入) | 接收用户输入数据 | scanf("%d", &num); (底层调用 read(0, ...) ) |
1 | STDOUT_FILENO | 屏幕(标准输出) | 输出程序结果 | printf("Hello\n"); (底层调用 write(1, ...) ) |
2 | STDERR_FILENO | 屏幕(标准错误) | 输出错误或警告信息 | perror("Error"); (底层调用 write(2, ...) ) |
重定向示例:将错误输出到文件
#include <stdio.h>
int main() {
freopen("error.log", "w", stderr); // 将标准错误流(2)重定向到文件
fprintf(stderr, "ERROR: Invalid file path at line %d\n", __LINE__);
// 错误信息不再显示在屏幕,而是写入 error.log
return 0;
}
3.4 高级操作:光标定位与状态查询
3.4.1 光标控制:lseek
函数
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
参数详解
参数 | 说明 |
---|---|
fd | 文件描述符(需以读 / 写模式打开)。 |
offset | 偏移量(字节数),可正(向后)可负(向前)。 |
whence | 偏移起点(必背常量):SEEK_SET (文件开头)、SEEK_CUR (当前位置)、SEEK_END (文件末尾)。 |
典型用法示例
场景 | 代码 | 效果 |
---|---|---|
定位到文件开头 | lseek(fd, 0, SEEK_SET); | 光标移动到第 1 字节(偏移量 0) |
从当前位置后移 10 字节 | lseek(fd, 10, SEEK_CUR); | 光标在当前位置基础上增加 10 字节 |
获取文件大小 | off_t size = lseek(fd, 0, SEEK_END); | size 存储文件总字节数 |
回退 5 字节 | lseek(fd, -5, SEEK_CUR); | 光标从当前位置向前移动 5 字节 |
3.4.2 文件状态获取:stat
家族函数
#include <sys/stat.h>
int stat(const char *path, struct stat *buf); // 通过路径获取文件状态
int fstat(int fd, struct stat *buf); // 通过文件描述符获取文件状态
int lstat(const char *path, struct stat *buf); // 获取符号链接本身的状态(而非指向的文件)
struct stat
关键成员
成员 | 说明 |
---|---|
st_mode | 文件类型 + 权限(通过宏判断,如 S_ISREG() 判断普通文件,S_IRUSR 表示所有者读权限)。 |
st_size | 文件大小(字节数,仅对普通文件有效,设备文件返回 0)。 |
st_mtime | 最后修改时间(time_t 类型时间戳,可通过 localtime() 转换为可读格式)。 |
示例:打印文件权限与大小
struct stat file_stat;
if (stat("data.txt", &file_stat) == 0) {
printf("文件权限(八进制):%o\n", file_stat.st_mode & 0777); // 提取权限(去掉文件类型标志)
printf("文件大小:%ld 字节\n", file_stat.st_size);
}
知识总结:系统级文件操作核心脉络
- 打开文件:通过
open
函数设置模式(如O_RDWR
)和权限(结合umask
计算实际权限)。 - 读写数据:使用
read
/write
直接操作文件描述符,适合高性能场景(如大文件复制)。 - 特殊描述符:掌握
0
(输入)、1
(输出)、2
(错误)的用途,学会重定向标准流。 - 高级控制:
lseek
实现光标精准定位(开头、当前位置、末尾偏移)。stat
家族函数获取文件详细信息(类型、权限、大小、时间等)。
通过系统调用,开发者可直接操控文件底层行为,满足高性能、高可靠性场景需求。后续可结合标准 IO 库(如 fopen
/fread
),在不同场景下灵活组合使用,提升开发效率。
四、目录与文件管理:系统运维必备技能
4.1 目录操作:创建、删除、遍历
核心函数与参数
#include <sys/stat.h>
#include <dirent.h>
1. 创建目录:mkdir
int mkdir(const char *pathname, mode_t mode);
- 参数:
pathname
:目录路径(如"./new_dir"
)。mode
:目录权限(八进制,如0755
表示所有者可读可写可执行,组 / 其他用户可读可执行)。
- 示例:
if (mkdir("project", 0755) == -1) { perror("mkdir failed"); // 失败原因可能是路径已存在或权限不足 }
2. 删除空目录:rmdir
int rmdir(const char *pathname);
- 限制:只能删除空目录,非空目录需先删除内部文件(可通过
rm -r
命令实现,但编程需递归删除)。 - 示例:
if (rmdir("empty_dir") == -1) { perror("rmdir failed"); // 错误码 `ENOTEMPTY` 表示目录非空 }
3. 遍历目录:opendir
+ readdir
DIR *opendir(const char *pathname); // 打开目录,返回目录流指针
struct dirent *readdir(DIR *dirp); // 读取目录项,返回 `struct dirent`
void closedir(DIR *dirp); // 关闭目录流
struct dirent
成员:d_name
:文件名(包含.
表示当前目录,..
表示上级目录)。
- 示例:
DIR *dir = opendir("."); struct dirent *entry; while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; // 跳过当前和上级目录 } printf("文件/目录:%s\n", entry->d_name); } closedir(dir); // 必须关闭目录流,避免资源泄漏
4.2 文件管理:重命名、删除、权限修改
1. 重命名文件 / 目录:rename
int rename(const char *oldpath, const char *newpath);
- 参数:
oldpath
:原路径(如"old.txt"
)。newpath
:新路径(如"new.txt"
或"./subdir/new.txt"
)。
- 示例:
if (rename("draft.txt", "final.txt") == -1) { perror("rename failed"); // 失败原因可能是路径不存在或权限不足 }
2. 删除文件:unlink
int unlink(const char *pathname);
- 注意:仅能删除文件,删除目录需用
rmdir
(且目录为空)。 - 示例:
if (unlink("temp.log") == -1) { perror("unlink failed"); // 错误码 `ENOENT` 表示文件不存在 }
3. 修改权限:chmod
int chmod(const char *pathname, mode_t mode);
- 参数:
pathname
:文件 / 目录路径。mode
:权限值(可通过宏组合,如S_IRUSR | S_IWUSR
表示所有者读写)。
- 权限宏定义:
S_IRUSR
(0400,所有者读)、S_IWUSR
(0200,所有者写)、S_IXUSR
(0100,所有者执行)。S_IRGRP
(0040,组读)、S_IWGRP
(0020,组写)、S_IXGRP
(0010,组执行)。S_IROTH
(0004,其他用户读)、S_IWOTH
(0002,其他用户写)、S_IXOTH
(0001,其他用户执行)。
- 示例:
// 设置文件为所有者读写,组和其他用户只读(0644) chmod("data.txt", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
知识总结:系统级操作核心脉络
- 文件操作:
open
函数通过flags
组合实现多样打开模式,权限计算需考虑umask
影响。read/write
直接操作描述符,适合大文件高效读写;lseek
实现光标精准定位。
- 目录与文件管理:
- 目录操作需区分空目录删除(
rmdir
)和非空处理,遍历目录时注意过滤.
和..
。 - 权限修改通过
chmod
结合宏定义实现细粒度控制,rename/unlink
用于文件重命名和删除。
- 目录操作需区分空目录删除(
通过掌握系统级文件操作,开发者可深入底层控制文件行为,为嵌入式开发、系统编程等场景打下坚实基础。实际开发中,建议结合标准 IO 库(如 fopen
)和系统调用,平衡效率与便捷性。
五、进阶操作:应对复杂场景的关键技术
5.1 改变文件大小:ftruncate
函数
#include <unistd.h>
int ftruncate(int fd, off_t length);
函数参数
参数 | 说明 |
---|---|
fd | 已打开的文件描述符(必须以 写入模式 打开,如 O_WRONLY 或 O_RDWR )。 |
length | 目标文件大小(字节数),用于扩展或截断文件。 |
核心功能
- 文件截断:若原文件大小 >
length
,超出部分被删除(数据永久丢失,谨慎使用)。 - 文件扩展:若原文件大小 <
length
,文件被扩展至length
字节,中间区域填充0
(形成文件空洞)。
示例:创建 1MB 空文件
int fd = open("large_file.dat", O_WRONLY | O_CREAT, 0644); // 创建文件并以只写模式打开
if (fd == -1) {
perror("open failed");
return -1;
}
ftruncate(fd, 1024 * 1024); // 设置文件大小为 1MB(1048576 字节)
close(fd); // 关闭文件描述符
5.2 原子操作:pread
/pwrite
函数
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset); // 原子读
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset); // 原子写
函数参数
参数 | 说明 |
---|---|
fd | 文件描述符(需以读 / 写模式打开)。 |
buf | 读写缓冲区(读时存储数据,写时提供数据)。 |
count | 读写的字节数。 |
offset | 相对于文件开头的偏移量(字节数),读写操作从该位置开始,不改变文件指针位置。 |
核心优势
- 无需手动定位:直接指定偏移量,无需先调用
lseek
,避免多线程 / 进程场景下的指针竞争。 - 原子性保证:操作期间文件指针保持不变,确保数据一致性(尤其适合多进程并发读写同一文件不同区域)。
示例:精准写入文件中间位置
int fd = open("data.bin", O_RDWR); // 以读写模式打开二进制文件
if (fd == -1) {
perror("open failed");
return -1;
}
pwrite(fd, "update", 6, 100); // 在第 100 字节处写入 "update",不影响当前文件指针
close(fd);
5.3 文件描述符深度解析
① 本质与特性
- 定义:文件描述符是进程打开文件的 非负整数句柄(范围
0~1023
,默认每个进程最多打开 1024 个文件)。 - 底层机制:系统为每个进程维护 文件描述符表,记录打开文件的状态(如光标位置、权限、文件类型等)。
- 特殊值:
0
(STDIN_FILENO
):标准输入(默认键盘)。1
(STDOUT_FILENO
):标准输出(默认屏幕)。2
(STDERR_FILENO
):标准错误(默认屏幕)。
② 描述符复制:dup
/dup2
int dup(int oldfd); // 自动分配最小可用描述符,与 `oldfd` 指向同一文件
int dup2(int oldfd, int newfd); // 指定新描述符(若 `newfd` 已打开则先关闭)
函数 | 区别 | 典型场景 |
---|---|---|
dup | 系统自动分配未使用的最小描述符(如 oldfd=3 ,返回 4 )。 | 透明复制描述符,无需指定具体数值。 |
dup2 | 手动指定 newfd ,若 newfd 已占用则先关闭,再指向 oldfd 。 | 重定向标准流(如将 stdout 指向文件)。 |
典型应用:重定向标准输出到文件
int log_fd = open("output.log", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (log_fd == -1) {
perror("open log failed");
return -1;
}
dup2(log_fd, 1); // 将标准输出(1)重定向到 log_fd,后续 `printf` 输出到文件
printf("This message is written to output.log\n");
close(log_fd);
5.4 文件锁定:fcntl
函数实现进程间同步
#include <fcntl.h>
struct flock {
short l_type; // 锁类型(`F_RDLCK` 读锁、`F_WRLCK` 写锁、`F_UNLCK` 解锁)
short l_whence; // 偏移起点(`SEEK_SET`/`SEEK_CUR`/`SEEK_END`)
off_t l_start; // 锁定区域起始位置(0 表示文件开头)
off_t l_len; // 锁定区域长度(0 表示整个文件)
pid_t l_pid; // 持有锁的进程 ID(仅 `F_GETLK` 时返回)
};
int fcntl(int fd, int cmd, struct flock *lock);
核心命令(cmd
参数)
命令 | 功能 |
---|---|
F_SETLK | 设置锁状态(阻塞模式:无冲突则加锁,有冲突则返回 -1 )。 |
F_GETLK | 检测锁冲突(返回第一个阻止当前锁的信息,无冲突时 l_type=F_UNLCK )。 |
操作流程(以写锁为例)
- 初始化锁结构体:
struct flock lock = { .l_type = F_WRLCK, // 写锁(独占锁,其他进程无法读写) .l_whence = SEEK_SET, .l_start = 0, // 从文件开头锁定 .l_len = 0 // 锁定整个文件 };
- 加锁:
if (fcntl(fd, F_SETLK, &lock) == -1) { perror("lock failed"); // 失败原因:已有其他进程持有写锁 }
- 解锁:
lock.l_type = F_UNLCK; fcntl(fd, F_SETLK, &lock); // 释放锁
注意事项
- 锁类型:读锁(
F_RDLCK
)可共享,允许多进程同时读;写锁(F_WRLCK
)互斥,仅允许单进程写。 - 建议性锁:需所有进程配合检查锁状态,否则可能导致数据不一致(不阻止强制读写)。
5.5 设备控制:ioctl
函数(串口 / 硬件操作)
#include <sys/ioctl.h>
int ioctl(int fd, int request, ...); // 第三个参数为可变参数,依赖设备类型
典型场景:串口配置(设置 115200 波特率,8 数据位,无校验)
- 包含必要头文件:
#include <termios.h> // 串口配置相关结构体
- 获取当前配置:
struct termios options; if (tcgetattr(fd, &options) == -1) { perror("tcgetattr failed"); return -1; }
- 设置波特率:
cfsetispeed(&options, B115200); // 设置输入波特率 cfsetospeed(&options, B115200); // 设置输出波特率
- 配置数据位与校验位:
options.c_cflag &= ~CSIZE; // 清除数据位掩码 options.c_cflag |= CS8; // 设置 8 数据位 options.c_cflag &= ~PARENB; // 禁用奇偶校验(无校验位)
- 激活配置:
if (tcsetattr(fd, TCSANOW, &options) == -1) { perror("tcsetattr failed"); return -1; }
5.6 权限检测:access
函数
#include <unistd.h>
int access(const char *pathname, int mode);
mode
参数(可通过 |
组合多个标志)
宏定义 | 检测类型 | 返回值 | 示例 |
---|---|---|---|
F_OK | 文件是否存在 | 0(存在),-1(不存在) | if (access("config.ini", F_OK)) { /* 文件不存在 */ } |
R_OK | 是否可读 | 0(可读),-1(不可读) | if (access("log.txt", R_OK)) { /* 无读权限 */ } |
W_OK | 是否可写 | 0(可写),-1(不可写) | if (access("data.txt", W_OK)) { /* 无写权限 */ } |
X_OK | 是否可执行 | 0(可执行),-1(不可执行) | if (access("/bin/bash", X_OK)) { /* 不可执行 */ } |
示例:安全检查配置文件权限
if (access("config.ini", R_OK | W_OK) != 0) {
fprintf(stderr, "Error: Missing read/write permission for config.ini\n");
exit(1);
}
知识总结:进阶操作核心要点
- 文件大小控制:
ftruncate
用于精准调整文件大小,注意写入模式打开文件的必要性。 - 原子操作:
pread/pwrite
避免多进程场景下的文件指针混乱,确保数据读写原子性。 - 描述符技巧:
dup/dup2
实现描述符复制与重定向,灵活控制标准输入输出。 - 并发控制:
fcntl
文件锁区分读锁 / 写锁,确保多进程访问文件的一致性。 - 设备交互:
ioctl
结合termios
结构体配置串口等设备,掌握波特率、数据位等参数设置。 - 权限检测:
access
函数快速校验文件访问权限,提升程序健壮性。
通过进阶操作的学习,开发者可应对复杂场景下的文件操作需求,从基础读写进阶到系统级控制,为高性能、高可靠性的程序开发奠定基础。
六、高级主题:IO 模型与时间处理
6.1 标准输入输出文件描述符
① 底层实现与缓冲区特性
Linux 进程启动时自动打开三个标准流,本质是文件描述符的高层封装,对应关系如下:
stdin
(标准输入) → 文件描述符0
(STDIN_FILENO
)stdout
(标准输出) → 文件描述符1
(STDOUT_FILENO
)stderr
(标准错误) → 文件描述符2
(STDERR_FILENO
)
核心特性:
- 缓冲区机制:
stdout
:默认行缓冲,数据会先存入缓冲区,遇到换行符或缓冲区满时才输出到屏幕(提升效率)。stderr
:无缓冲,错误信息立即输出,确保关键信息不丢失(如perror
直接调用write(2, ...)
)。
底层等价关系:
printf("hello") = fwrite("hello", 1, 5, stdout) = write(1, "hello", 5);
scanf("%d", &num) = fscanf(stdin, "%d", &num) = read(0, buf, sizeof(buf)) + 解析逻辑;
② 重定向实战:改变输入输出目标
通过 freopen
函数可将标准流重定向到文件,实现 “输入从文件读取” 或 “输出到文件存储”。
#include <stdio.h>
int main() {
// ① 输入重定向:从文件读取数据(替代键盘输入)
freopen("input.dat", "r", stdin); // 将标准输入重定向到 input.dat
int a, b;
scanf("%d %d", &a, &b); // 实际从文件读取数据
// ② 错误重定向:将错误信息写入日志
freopen("error.log", "a", stderr); // 追加模式打开日志文件
fprintf(stderr, "Error: Division by zero at line %d\n", __LINE__); // 错误信息写入 error.log
return 0;
}
6.2 时间处理:从时间戳到可读格式
核心函数与结构体
#include <time.h>
time_t time(NULL); // 获取 Unix 时间戳(1970-01-01 至今的秒数)
struct tm *gmtime(const time_t *timep); // 转换为 UTC 时间(格林威治时间,无时区偏移)
struct tm *localtime(const time_t *timep); // 转换为本地时间(自动调整时区,如中国为 UTC+8)
struct tm
成员解析(以 localtime
为例):
成员 | 含义 | 示例(2023-10-01 15:30:45) |
---|---|---|
tm_year | 年份(从 1900 开始,需 +1900) | 123 → 2023 年 |
tm_mon | 月份(0-11,需 +1) | 9 → 10 月 |
tm_mday | 日(1-31) | 1 |
tm_hour | 小时(0-23) | 15 |
tm_min | 分钟(0-59) | 30 |
tm_sec | 秒(0-59) | 45 |
示例:打印当前时间
int main() {
time_t now = time(NULL); // 获取时间戳
struct tm *local = localtime(&now); // 转换为本地时间
// 格式化为 "YYYY-MM-DD HH:MM:SS"
printf("Local Time: %d-%02d-%02d %02d:%02d:%02d\n",
local->tm_year + 1900, // 年份修正
local->tm_mon + 1, // 月份修正
local->tm_mday,
local->tm_hour,
local->tm_min,
local->tm_sec);
return 0;
}
6.3 IO 模型对比与 select
函数实战
四大 IO 模型核心特性
模型 | 阻塞特性 | 典型函数 | 适用场景 | 关键优势 |
---|---|---|---|---|
阻塞 IO | 操作未完成时进程挂起 | read /write | 简单文件操作(如日志写入) | 代码逻辑简单,无需处理异步事件 |
非阻塞 IO | 立即返回,无数据时 errno=EAGAIN | open(O_NONBLOCK) | 设备轮询(如串口实时数据读取) | 避免进程阻塞,适合实时性要求不高的轮询 |
多路转接 | 同时监控多个描述符 | select /poll | 高并发服务器(处理多文件 / 套接字) | 单进程管理大量描述符,降低资源消耗 |
异步 IO | 内核完成后通知 | aio_read /aio_write | 大数据量非阻塞操作(如大文件传输) | 无需轮询,内核主动通知操作完成 |
select
函数详解
#include <sys/select.h>
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptionset, struct timeval *timeout);
-
参数:
maxfd
:监控的最大描述符 + 1(如监控fd1=3, fd2=5
,则maxfd=6
)。readset/writeset/exceptionset
:分别用于监控可读、可写、异常的描述符集合。timeout
:超时时间,NULL
表示永久阻塞,{0, 0}
表示立即返回。
-
操作步骤:
- 初始化集合:
fd_set read_fds; FD_ZERO(&read_fds); // 清空集合 FD_SET(fd1, &read_fds); // 添加描述符 fd1 到读集合 FD_SET(fd2, &read_fds); // 添加描述符 fd2 到读集合
- 启动监控:
struct timeval timeout = {2, 0}; // 超时 2 秒 int ready = select(6, &read_fds, NULL, NULL, &timeout);
- 处理就绪事件:
if (ready > 0) { // 有描述符就绪 if (FD_ISSET(fd1, &read_fds)) handle_read(fd1); // fd1 可读时处理 if (FD_ISSET(fd2, &read_fds)) handle_read(fd2); // fd2 可读时处理 } else if (ready == 0) { printf("Select timeout, no data ready\n"); // 超时无事件 } else { perror("Select error"); // 错误处理 }
- 初始化集合:
6.4 串口编程基础(嵌入式开发核心)
① 串口设备文件路径
类型 | 设备路径 | 说明 |
---|---|---|
传统串口 | /dev/ttyS0 (COM1)、/dev/ttyS1 (COM2) | 主板集成串口,常见于嵌入式设备 |
虚拟串口(终端) | /dev/pts/0 、/dev/pts/1 | 终端模拟器(如 xshell 生成的串口) |
USB 转串口 | /dev/ttyUSB0 、/dev/ttyUSB1 | 插入 USB 转串口设备后动态生成 |
② 配置步骤(9600 波特率,8 数据位,无校验)
-
打开串口(非阻塞模式):
int fd = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY | O_NDELAY); if (fd == -1) { perror("Failed to open serial port"); exit(1); }
O_NOCTTY
:禁止串口成为控制终端(避免输入影响进程)。O_NDELAY
:非阻塞模式,无数据时立即返回。
-
获取当前配置:
struct termios options; if (tcgetattr(fd, &options) == -1) { perror("Failed to get serial options"); exit(1); }
-
设置核心参数:
// ① 波特率 cfsetispeed(&options, B9600); // 输入波特率 cfsetospeed(&options, B9600); // 输出波特率 // ② 数据位(8 位) options.c_cflag &= ~CSIZE; // 清除数据位掩码 options.c_cflag |= CS8; // 设置 8 数据位 // ③ 停止位(1 位,默认值无需额外设置,清除 CSTOPB) options.c_cflag &= ~CSTOPB; // ④ 无校验位 options.c_cflag &= ~PARENB; // 禁用奇偶校验
-
激活配置(立即生效):
if (tcsetattr(fd, TCSANOW, &options) == -1) { perror("Failed to set serial options"); exit(1); }
-
读写操作:
// 读取数据(非阻塞模式,无数据时返回 -1,errno=EAGAIN) char buf[1024]; ssize_t n = read(fd, buf, sizeof(buf)); if (n > 0) { printf("Received: %s\n", buf); } // 发送数据(如 AT 命令控制串口设备) const char *cmd = "AT\r\n"; write(fd, cmd, strlen(cmd));
知识总结:高级主题核心要点
- 标准流重定向:利用
freopen
灵活改变输入输出目标,适用于日志记录、批量数据处理。 - 时间处理:掌握
time
/gmtime
/localtime
的配合使用,实现时间戳与可读格式的转换。 - IO 模型选择:根据场景选择阻塞 / 非阻塞模式,
select
函数是多描述符监控的核心工具。 - 串口编程:熟悉设备路径、配置流程(波特率 / 数据位 / 停止位),是嵌入式开发与硬件交互的基础。
通过高级主题的学习,开发者可深入理解 Linux 文件操作的底层机制,应对复杂 IO 场景(如高并发、设备控制),为系统级编程和嵌入式开发提供支撑。
七、知识体系总结:从基础到实战的路线图
核心函数对比表
功能分类 | 标准 IO 函数 | 系统 IO 函数 | 特殊用途函数 |
---|---|---|---|
打开文件 | fopen /freopen | open /creat | fdopen (描述符转文件指针) |
读写文件 | fread /fwrite | read /write | pread /pwrite (原子操作) |
文件定位 | fseek /ftell | lseek | - |
文件控制 | fflush /fclose | close /ftruncate | fcntl (文件锁 / 状态控制) |
目录操作 | - | mkdir /rmdir /opendir | stat /lstat (文件状态查询) |
设备控制 | - | ioctl | termios (串口参数配置) |
新手成长路线
- 基础篇:掌握
fopen
/fread
/fwrite
,理解文件打开模式与缓冲区机制 - 系统篇:深入
open
/read
/write
/lseek
,掌握文件描述符与权限计算 - 进阶篇:学习文件锁(
fcntl
)、IO 模型(select
)、串口配置(termios
) - 实战篇:开发简易文件管理器、串口调试工具、多线程日志系统
关键提醒
- 错误处理:所有文件操作函数需检查返回值(如
fopen
返回NULL
、open
返回-1
),并用perror
打印具体错误 - 资源释放:及时关闭文件(
fclose
/close
)、释放目录流(closedir
)、解锁(fcntl
解锁操作) - 场景选择:文本处理用标准 IO,设备控制 / 高性能场景用系统 IO,复杂并发场景结合 IO 模型优化
通过系统学习文件操作的全生命周期(创建、读写、控制、删除),结合底层系统调用与高层库函数的对比,开发者能全面掌握 Linux 文件操作的核心逻辑。记住:多写代码、多调试,逐步积累实战经验,是掌握这一核心技能的关键。