Linux 入门七:从基础到进阶的文件操作

一、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 核心文件操作分类

  1. 基础操作:打开 / 关闭文件、读写数据(文件交互的入口与核心)
  2. 控制操作:权限管理、光标定位、文件状态查询(精细化管理文件属性)
  3. 高级操作:原子操作、文件锁定、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_WRONLYO_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); // 向文件写入数据  
参数详解
函数参数说明
readfd文件描述符(通过 open 函数返回)。
buf用于存储读取数据的缓冲区(指针)。
count期望读取的字节数。成功时返回实际读取的字节数(0 表示文件末尾),失败返回 -1
writefd文件描述符(需以写模式打开)。
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 系统为每个进程预定义了三个标准文件描述符,用于输入输出操作:

描述符宏定义默认设备功能典型用法
0STDIN_FILENO键盘(标准输入)接收用户输入数据scanf("%d", &num);(底层调用 read(0, ...)
1STDOUT_FILENO屏幕(标准输出)输出程序结果printf("Hello\n");(底层调用 write(1, ...)
2STDERR_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);  
}  

知识总结:系统级文件操作核心脉络

  1. 打开文件:通过 open 函数设置模式(如 O_RDWR)和权限(结合 umask 计算实际权限)。
  2. 读写数据:使用 read/write 直接操作文件描述符,适合高性能场景(如大文件复制)。
  3. 特殊描述符:掌握 0(输入)、1(输出)、2(错误)的用途,学会重定向标准流。
  4. 高级控制
    • 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);  
    

知识总结:系统级操作核心脉络

  1. 文件操作
    • open 函数通过 flags 组合实现多样打开模式,权限计算需考虑 umask 影响。
    • read/write 直接操作描述符,适合大文件高效读写;lseek 实现光标精准定位。
  2. 目录与文件管理
    • 目录操作需区分空目录删除(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 个文件)。
  • 底层机制:系统为每个进程维护 文件描述符表,记录打开文件的状态(如光标位置、权限、文件类型等)。
  • 特殊值
    • 0STDIN_FILENO):标准输入(默认键盘)。
    • 1STDOUT_FILENO):标准输出(默认屏幕)。
    • 2STDERR_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)。
操作流程(以写锁为例)
  1. 初始化锁结构体
    struct flock lock = {  
        .l_type = F_WRLCK,  // 写锁(独占锁,其他进程无法读写)  
        .l_whence = SEEK_SET,  
        .l_start = 0,       // 从文件开头锁定  
        .l_len = 0          // 锁定整个文件  
    };  
    
  2. 加锁
    if (fcntl(fd, F_SETLK, &lock) == -1) {  
        perror("lock failed"); // 失败原因:已有其他进程持有写锁  
    }  
    
  3. 解锁
    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 数据位,无校验)
  1. 包含必要头文件
    #include <termios.h>  // 串口配置相关结构体  
    
  2. 获取当前配置
    struct termios options;  
    if (tcgetattr(fd, &options) == -1) {  
        perror("tcgetattr failed");  
        return -1;  
    }  
    
  3. 设置波特率
    cfsetispeed(&options, B115200); // 设置输入波特率  
    cfsetospeed(&options, B115200); // 设置输出波特率  
    
  4. 配置数据位与校验位
    options.c_cflag &= ~CSIZE;    // 清除数据位掩码  
    options.c_cflag |= CS8;       // 设置 8 数据位  
    options.c_cflag &= ~PARENB;   // 禁用奇偶校验(无校验位)  
    
  5. 激活配置
    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);  
}  

知识总结:进阶操作核心要点

  1. 文件大小控制ftruncate 用于精准调整文件大小,注意写入模式打开文件的必要性。
  2. 原子操作pread/pwrite 避免多进程场景下的文件指针混乱,确保数据读写原子性。
  3. 描述符技巧dup/dup2 实现描述符复制与重定向,灵活控制标准输入输出。
  4. 并发控制fcntl 文件锁区分读锁 / 写锁,确保多进程访问文件的一致性。
  5. 设备交互ioctl 结合 termios 结构体配置串口等设备,掌握波特率、数据位等参数设置。
  6. 权限检测access 函数快速校验文件访问权限,提升程序健壮性。

通过进阶操作的学习,开发者可应对复杂场景下的文件操作需求,从基础读写进阶到系统级控制,为高性能、高可靠性的程序开发奠定基础。

六、高级主题:IO 模型与时间处理

6.1 标准输入输出文件描述符

① 底层实现与缓冲区特性

Linux 进程启动时自动打开三个标准流,本质是文件描述符的高层封装,对应关系如下:

  • stdin(标准输入) → 文件描述符 0STDIN_FILENO
  • stdout(标准输出) → 文件描述符 1STDOUT_FILENO
  • stderr(标准错误) → 文件描述符 2STDERR_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=EAGAINopen(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} 表示立即返回。
  • 操作步骤

    1. 初始化集合
      fd_set read_fds;  
      FD_ZERO(&read_fds);  // 清空集合  
      FD_SET(fd1, &read_fds);  // 添加描述符 fd1 到读集合  
      FD_SET(fd2, &read_fds);  // 添加描述符 fd2 到读集合  
      
    2. 启动监控
      struct timeval timeout = {2, 0};  // 超时 2 秒  
      int ready = select(6, &read_fds, NULL, NULL, &timeout);  
      
    3. 处理就绪事件
      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 数据位,无校验)
  1. 打开串口(非阻塞模式)

    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:非阻塞模式,无数据时立即返回。
  2. 获取当前配置

    struct termios options;  
    if (tcgetattr(fd, &options) == -1) {  
        perror("Failed to get serial options");  
        exit(1);  
    }  
    
  3. 设置核心参数

    // ① 波特率  
    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;  // 禁用奇偶校验  
    
  4. 激活配置(立即生效)

    if (tcsetattr(fd, TCSANOW, &options) == -1) {  
        perror("Failed to set serial options");  
        exit(1);  
    }  
    
  5. 读写操作

    // 读取数据(非阻塞模式,无数据时返回 -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));  
    

知识总结:高级主题核心要点

  1. 标准流重定向:利用 freopen 灵活改变输入输出目标,适用于日志记录、批量数据处理。
  2. 时间处理:掌握 time/gmtime/localtime 的配合使用,实现时间戳与可读格式的转换。
  3. IO 模型选择:根据场景选择阻塞 / 非阻塞模式,select 函数是多描述符监控的核心工具。
  4. 串口编程:熟悉设备路径、配置流程(波特率 / 数据位 / 停止位),是嵌入式开发与硬件交互的基础。

通过高级主题的学习,开发者可深入理解 Linux 文件操作的底层机制,应对复杂 IO 场景(如高并发、设备控制),为系统级编程和嵌入式开发提供支撑。

七、知识体系总结:从基础到实战的路线图

核心函数对比表

功能分类标准 IO 函数系统 IO 函数特殊用途函数
打开文件fopen/freopenopen/creatfdopen(描述符转文件指针)
读写文件fread/fwriteread/writepread/pwrite(原子操作)
文件定位fseek/ftelllseek-
文件控制fflush/fcloseclose/ftruncatefcntl(文件锁 / 状态控制)
目录操作-mkdir/rmdir/opendirstat/lstat(文件状态查询)
设备控制-ioctltermios(串口参数配置)

新手成长路线

  1. 基础篇:掌握 fopen/fread/fwrite,理解文件打开模式与缓冲区机制
  2. 系统篇:深入 open/read/write/lseek,掌握文件描述符与权限计算
  3. 进阶篇:学习文件锁(fcntl)、IO 模型(select)、串口配置(termios
  4. 实战篇:开发简易文件管理器、串口调试工具、多线程日志系统

关键提醒

  • 错误处理:所有文件操作函数需检查返回值(如 fopen 返回 NULLopen 返回 -1),并用 perror 打印具体错误
  • 资源释放:及时关闭文件(fclose/close)、释放目录流(closedir)、解锁(fcntl 解锁操作)
  • 场景选择:文本处理用标准 IO,设备控制 / 高性能场景用系统 IO,复杂并发场景结合 IO 模型优化

通过系统学习文件操作的全生命周期(创建、读写、控制、删除),结合底层系统调用与高层库函数的对比,开发者能全面掌握 Linux 文件操作的核心逻辑。记住:多写代码、多调试,逐步积累实战经验,是掌握这一核心技能的关键。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值