Linux - 基础IO【中】

一、系统文件I/O

在 Linux 编程体系中,C 标准库 I/O 是上层封装,而 系统文件 I/O 是底层基石 ------ 它是操作系统内核提供的原生接口 ,是所有上层文件操作(包括 C 库 fopen/fread)的最终执行入口。

1.1 一种传递标记位的方法

Linux 系统调用中,大量接口(如 open)需要通过一个整型变量传递多个配置标记 ,这种设计的核心是利用二进制位运算(按位或 | 组合标记、按位与 & 校验标记

核心原理

  • 每个标记对应一个独立的二进制位(如 0x010x020x04),互不冲突;
  • 按位或 | 组合多个标记,实现 "多标记共存"
  • 按位与 & 校验某个标记是否被启用,实现**"单标记检测"**。
  • 可以采用位图传递标记位
标记位 含义 适用场景
O_RDONLY 只读模式 纯读取文件,不可写
O_WRONLY 只写模式 纯写入文件,不可读
O_RDWR 读写模式 可读可写
O_CREAT 不存在则创建 需配合权限参数,如0644
O_TRUNC 截断文件(清空原有内容) 打开文件时清空内容
O_APPEND 追加模式(写时移到文件末尾) 不覆盖原有内容,仅追加
复制代码
#include <stdio.h>

#define ONE_FLAG   (1 << 0)
#define TWO_FLAG   (2 << 0)
#define THREE_FLAG (3 << 0)
#define FOUR_FLAG  (4 << 0)

void Print(int flags)
{
    if (flags & ONE_FLAG)
    {
        printf("One!\n");
    }
    if (flags & TWO_FLAG)
    {
        printf("Two!\n");
    }
    if (flags & THREE_FLAG)
    {
        printf("Three!\n");
    }
    if (flags & FOUR_FLAG)
    {
        printf("four!\n");
    }
}

int main()
{
    Print(ONE_FLAG);
    printf("\n");
    Print(ONE_FLAG | TWO_FLAG);
    printf("\n");
    Print(ONE_FLAG | TWO_FLAG | THREE_FLAG);
    printf("\n");
    Print(ONE_FLAG | TWO_FLAG | THREE_FLAG | FOUR_FLAG);
    printf("\n");
    Print(TWO_FLAG);
    printf("\n");
    return 0;
}

操作文件,除了上小节的C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问, 先来直接以系统代码的形式,实现和上面一模一样的代码:

1.2 hello.c写文件

把相关头文件加上~

1.2.1 umask掩码

编译链接完,生成文件,我们发现权限位不太对

所以我们再加上权限码 0666

受系统的权限影响,没有达到我们的预想

umask(文件创建掩码)是 Linux 系统的一个进程级属性 ,用来限制新创建文件 / 目录的默认权限

  • 系统有一个默认的 umask 值(通常是 00020022);
  • 当你调用 open(带 O_CREAT)或 mkdir 时,最终权限 = 你指定的 mode & ~umask

举个例子:

  • 你指定 mode = 0666(rw-rw-rw-)
  • 系统默认 umask = 0022(----w--w-)
  • 最终文件权限 = 0666 & ~0022 = 0644(rw-r--r--)
  • umask(0) 的目的是:让你在 open 里指定的 mode 权限 100% 生效,不被系统默认的 umask 扣减。

1.2.2 write

注意:strlen(msg) 不需要 +1

1.2.3 O_TRUNC清空

复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char* argv[])
{
    umask(0);
    int fd = open("log.txt", O_CREAT | O_WRONLY, 0666);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    printf("fd:%d\n", fd);

    const char* msg = "qqq!\n";
    int cnt = 1;
    while (cnt)
    {
        write(fd, msg, strlen(msg));
        cnt--;
    }

    close(fd);
    return 0;
}

1.2.4 O_APPEND追加

复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char* argv[])
{
    // 清空文件创建掩码,使 open 指定的 0666 权限完全生效
    umask(0);
    
    // 打开 log.txt,不存在则创建,存在则追加内容(O_APPEND)
    // 权限设置为 0666 (rw-rw-rw-)
    int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
    
    // 检查打开是否失败
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    
    // 打印获取到的文件描述符值,通常为 3
    printf("fd:%d\n", fd);

    // 定义待写入的字符串
    const char* msg = "abcd";
    int cnt = 1; // 循环次数,这里只为了演示一次写入
    
    // 循环向文件写入数据
    while (cnt)
    {
        // write 系统调用:向 fd 写入 msg 内容,长度为字符串长度
        write(fd, msg, strlen(msg));
        cnt--; // 循环递减
    }

    // 关键步骤:关闭文件描述符,释放内核资源
    close(fd);
    
    return 0;
}
  • O_APPEND 的作用

    • 这是你图中标注的重点。它表示追加模式
    • 每次调用 write 向文件写数据时,数据都会被写到文件的末尾 ,而不会覆盖文件原本的内容。
    • 如果不加 O_APPEND,再次打开文件写,默认会从文件开头开始写,导致内容被覆盖。
  • umask(0) 的必要性

    • 虽然你指定了权限 0666,但系统默认的 umask 通常是 0022
    • umask(0) 是为了屏蔽系统默认的权限限制,确保最终生成的 log.txt 确实是 0666 权限,而不是被系统截成 0644
  • write 函数

    • 第一个参数是 fd(文件句柄)。
    • 第三个参数 strlen(msg)不含 \0 结束符的字节数,这是系统级 IO 的标准写法。

1.2.5 写入数据类型

  • 不管你写的是:
    • 字符串 "123456"
    • 整数 123456
    • 图片、音频、可执行文件
  • 在 OS 眼里,全都是一串二进制字节,它只负责把这些字节从进程缓冲区搬到文件 / 设备缓冲区,完全不关心这些字节代表什么 "类型"。
  • 没有 "文本模式" 和 "二进制模式" 的区别,这是上层语言(比如 C 标准库)给你封装的概念。
复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char* argv[])
{
    umask(0);
    int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    printf("fd:%d\n", fd);

    int a = 12345;
    //const char* msg = "abcd";
    int cnt = 1;
    while (cnt)
    {
        // 当成字符来写 ---
        char buffer[16];
        snprintf(buffer, sizeof(buffer), "%d", a);
        write(fd, buffer, strlen(buffer));
        cnt--;
    }

    close(fd);
    return 0;
}
  • 直接 write(&a, sizeof(a)) 会把整数的二进制内存布局写入文件,人类无法阅读
  • 通过 snprintf 先将整数转成字符串,再写入文件,就能得到人类可读的文本内容
  • 这正是 printf 等格式化输出函数的底层实现思路:先把数值类型转成字符串,再调用系统 write 输出

1.3 hello.c读文件

读文件的核心是用 read 系统调用,从文件中读取数据到缓冲区,同样需要处理文件描述符和读写位置。

  • read 读取成功返回实际读取的字节数 ,到达文件末尾返回 0
  • 缓冲区 buf 需预留 \0 位置,避免打印时出现乱码;
  • 读取完成后必须 close,否则会造成文件描述符泄漏。
复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char* argv[])
{
    umask(0);
    //int fd = open("log.txt", O_CREAT|O_WRONLY|O_TRUNC, 0666);
    int fd = open("log.txt", O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    printf("fd:%d\n", fd);

    while(1)
    {
        char buffer[64];
        int n = read(fd, buffer, sizeof(buffer)-1);
        if(n > 0)
        {
            buffer[n] = 0;
            printf("%s", buffer);
        }
        else if(n == 0)
        {
            break;
        }
    }

    close(fd);
    return 0;
}

1.4 接口介绍

当 open 带 O_CREAT 却不传 mode 时,内核会用栈上的随机垃圾值当文件权限,导致权限异常(比如不可读),后续读文件时要么权限不足打开失败,要么读到未初始化的缓冲区垃圾数据,最终表现为 "乱码"。

接口函数 功能 核心参数 返回值
open(const char *path, int flags, ...) 打开 / 创建文件 path:文件路径;flags:标记位组合;...:可选权限(仅创建时) 成功返回 fd,失败返回 - 1
write(int fd, const void *buf, size_t count) 向文件写数据 fd:文件描述符;buf:待写数据缓冲区;count:待写字节数 成功返回实际写字节数,失败返回 - 1
read(int fd, void *buf, size_t count) 从文件读数据 fd:文件描述符;buf:存储数据缓冲区;count:最大读字节数 成功返回实际读字节数,到达末尾返回 0,失败返回 - 1
lseek(int fd, off_t offset, int whence) 移动文件读写位置 fd:文件描述符;offset:偏移量;whence:基准位置(SEEK_SET/SET_CUR/SET_END) 成功返回新的读写位置,失败返回 - 1
close(int fd) 关闭文件描述符 fd:待关闭的文件描述符 成功返回 0,失败返回 - 1

1.5 open函数返回值

open 函数的返回值是文件描述符(fd),其核心规则如下:

  1. 成功返回 :非负整数,最小从 0 开始(后续会详细讲 fd 分配规则)
  2. 失败返回固定为 -1,并设置全局错误码 errno,可通过 perror 打印错误原因;
  3. fd 的本质:进程打开文件的唯一标识,后续所有读写操作都依赖 fd 定位文件。

系统调用 & 库函数

所以,可以认为 ,f#系列的函数,都是对系统调用的封装,方便二次开发

二、 文件描述符fd

2.1 0&1&2

Linux 系统中,每个进程启动时会默认打开 3 个文件描述符 ,对应 C 标准库的 stdin/stdout/stderr,这是操作系统为进程预设的基础 I/O 通道。

fd 值 名称 宏定义 对应设备 缓冲区类型 用途
0 标准输入 STDIN_FILENO 键盘 行缓冲 接收用户输入(如 scanf 底层)
1 标准输出 STDOUT_FILENO 显示器 行缓冲 输出正常结果(如 printf 底层)
2 标准错误 STDERR_FILENO 显示器 无缓冲 输出错误信息(如 perror 底层)

2.1.1 FILE里封装文件描述符

2.2 文件描述符的分配规则

Linux 为每个进程维护一个文件描述符表(fd_array,这是一个指针数组,数组下标就是 fd,数组元素指向内核的 file 结构体(代表打开的文件实例)。

2.2.1 分配核心规则

从当前未被使用的 最小下标分配新的 fd

  • 进程启动时,0/1/2 已被占用,因此首次打开普通文件时,fd 默认为 3;
  • 如果手动关闭 0/1/2 中的某个 fd,后续打开文件会优先复用该下标(这是重定向的核心原理)。

2.2.2 理解

  • 内核不会直接读写磁盘,而是先把文件内容加载到内核文件缓冲区
    • 读:先从缓冲区读,缓冲区没数据才从磁盘加载。
    • 写:先写到缓冲区,由内核定期刷回磁盘(提升性能)。
  • read 本质是:内核缓冲区 → 用户 buffer 的数据拷贝。

完整链路总结

  1. 用户调用 open → 内核创建 struct file → 分配最小可用 fd → 把 fd_array[fd] 指向这个 struct file
  2. 用户调用 read(fd, buffer, ...) → 内核通过 fd 找到 struct file从文件缓冲区拷贝数据到用户 buffer。
  3. 用户调用 close(fd) → 减少 struct file 引用计数 → 计数为 0 时释放内核资源
  4. 多个进程可以打开同一个文件 →各自有独立的 struct file(独立读写位置),共享磁盘文件数据。

Q:一个文件可以被打开多次吗?

✅ 可以!每次 open 都会创建一个新的 struct file,有独立的读写位置和状态,互不干扰。

Q:fd 是什么?为什么是整数?

fd 就是 fd_array[] 的下标,用整数是为了高效索引,让内核快速找到对应的 struct file

Q:为什么不直接读写磁盘?

磁盘太慢!内核缓冲区是为了性能:批量读写、减少磁盘 IO 次数,同时让上层操作更流畅。

fd 是数组下标,struct file 是内核里的「打开文件实例」,文件 IO 本质是内核缓冲区和用户 buffer 之间的拷贝。

2.3 重定向

重定向是 Linux 系统 I/O 的核心高级功能 ,本质是修改文件描述符表中某个下标对应的 file 指针指向,让原本输出到显示器 / 键盘的内容,转而输出到文件 / 其他设备。

类型 命令示例 本质
输出重定向 ./a.out > output.txt 将 fd=1(stdout)的指向改为 output.txt
追加重定向 ./a.out >> output.txt 将 fd=1 的指向改为 output.txt,且以追加模式打开
输入重定向 ./a.out < input.txt 将 fd=0(stdin)的指向改为 input.txt
错误重定向 ./a.out 2> error.txt 将 fd=2(stderr)的指向改为 error.txt

核心原理(以输出重定向为例)

  1. 关闭 fd=1(断开与显示器的关联);
  2. 打开目标文件(如 log1.txt),操作系统会分配最小可用 fd(此时为 1);
  3. 后续所有向 fd=1 写入的数据,都会写入 log1.txt,而非显示器。

我们在这里看一个现象:

2.4 使用dup2系统调用

手动关闭 fd 再打开文件的方式虽然能实现重定向,但不够灵活。Linux 提供 dup2 系统调用,可直接复制文件描述符的指向,实现更高效、更可控的重定向。

  • 功能 :将 oldfd 的文件指向复制newfd
  • 参数
    • oldfd:已打开的文件描述符(源指向);
    • newfd:目标文件描述符(被覆盖的指向);
  • 返回值 :成功返回 newfd,失败返回 -1
  • 核心逻辑 :如果 newfd 已被占用,dup2 会先自动关闭 newfd,再完成复制。

输出重定向

关闭stdout , 选项选择 只写 + 打开文件 + 清空文件

追加重定向

关闭stdout , 选项选择 只写 + 打开文件 + 追加

输入重定向

关闭stdin,选项选择 只读

相关推荐
顺风尿一寸2 小时前
从 Java File.length() 到 Linux 内核:一次系统调用追踪之旅
java·linux
Xzq2105092 小时前
网络编程套接字(UDP)
运维·服务器·网络
主角1 72 小时前
Linux系统安全
linux·运维·系统安全
翼龙云_cloud2 小时前
阿里云代理商:阿里云百炼视频混剪实战
服务器·阿里云·云计算
网硕互联的小客服2 小时前
CentOS 7 实现自动备份数据到百度网盘的具体步骤与方法
运维·服务器·网络·安全·自动化
1candobetter2 小时前
服务器公网访问策略与穿透方案汇总
运维·服务器
fetasty2 小时前
Android手机改造Linux服务器
linux·服务器
debug 小菜鸟2 小时前
深入理解强一致性与弱一致性:从CAP定理到电商实战选型
linux·负载均衡
未来之窗软件服务2 小时前
服务器运维(四十七)鸿蒙系统Mongoose服务器伪请求pseudo http —东方仙盟
java·运维·服务器·服务器运维·仙盟创梦ide·东方仙盟