Linux --基础IO

如何理解 "文件"?

狭义理解

  • 文件通常指存储在磁盘上的数据集合。

  • 磁盘是一种永久性存储介质,所以文件在断电后依然存在。

  • 磁盘属于外设 (既是输入设备也是输出设备),因此对文件的所有操作,本质上都是对设备的输入/输出,简称为 I/O(Input/Output)。

广义理解

  • 在 Linux 下,一个核心设计哲学是:一切皆文件

  • 这不仅仅是说普通文件,还包括键盘、显示器、网卡、管道、进程等,它们都被统一抽象为"文件"。

  • 这种抽象使得开发者可以用一套统一的接口(open、read、write、close)来操作各种不同的硬件和逻辑资源。

文件 = 属性 + 内容

  • 一个空文件(0 KB)也会占用磁盘空间,因为它需要存储元数据(如文件名、大小、权限、时间戳等)。

  • 文件操作分为两类:

    • 内容操作:读写数据。

    • 属性操作:修改权限、查看大小等。

系统角度

  • 对文件的操作,本质上是进程对文件的操作。没有进程,就没有文件操作。

  • 磁盘的管理者是操作系统 ,因此任何进程想要访问文件,都必须通过操作系统提供的系统调用接口,而不能直接操作磁盘硬件。

C 语言文件 I/O 接口

打开文件:fopen

复制代码
FILE *fp = fopen("myfile", "w");

打开的文件 myfile 会在哪个路径下?

当前进程的工作目录 。每个进程都有一个 cwd(Current Working Directory),可以通过 /proc/<pid>/cwd 查看。

写文件:fwrite

复制代码
const char *msg = "hello bit!\n";
fwrite(msg, strlen(msg), 1, fp);

读文件:fread

复制代码
ssize_t s = fread(buf, 1, strlen(msg), fp);

输出到显示器的多种方式

复制代码
fwrite("hello fwrite\n", 1, strlen("hello fwrite\n"), stdout);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
  • stdinstdoutstderr 是 C 默认打开的三个标准流,类型都是 FILE*

  • 它们对应的文件描述符分别是 0、1、2。

系统级文件 I/O 接口

C 库函数虽然方便,但它们本质上是对系统调用的封装。底层真正干活的是系统调用。

标志位传递技巧

Flag 就是一个开关选项,用二进制的一位来表示某个状态是否开启。

优点:一个整数可以表示多个开关状态

系统调用(如 open)常常使用位掩码(bitmask)来传递多个选项:

复制代码
#define ONE   0001  // 0001
#define TWO   0002  // 0010
#define THREE 0004  // 0100

void func(int flags) {
    if (flags & ONE)   printf("ONE ");
    if (flags & TWO)   printf("TWO ");
    if (flags & THREE) printf("THREE ");
}
func(ONE | TWO);  // 同时传递 ONE 和 TWO
标志 含义
O_RDONLY 只读打开(Read ONLY)
O_WRONLY 只写打开(Write ONLY)
O_RDWR 读写打开(Read + Write)

写文件:open + write

复制代码
umask(0);  // 设置文件创建掩码
int fd = open("myfile", O_WRONLY | O_CREAT, 0644);
//成功:返回文件描述符(一个非负整数,如 3)
//失败:返回 -1,并设置 errno

write(fd, msg, len);
//    │    │    │
//    │    │    └─ 要写入的字节数
//    │    └────── 数据缓冲区指针
//    └─────────── 文件描述符(从open获得)
//返回值
//成功:返回实际写入的字节数
//失败:返回 -1

close(fd);

读文件:open + read

复制代码
open("myfile", O_RDONLY);
//     │         └───── 打开标志:只读
//     └─────────────── 文件名
ssize_t read(int fd, void *buf, size_t count);
//       │       │       │          │
//       │       │       │          └─ 要读取的最大字节数
//       │       │       └──────────── 存储数据的缓冲区
//       │       └──────────────────── 文件描述符
//       └──────────────────────────── 返回值类型(有符号)

主要接口说明

系统调用 功能 头文件
open 打开/创建文件 <fcntl.h>
read 从文件读取数据 <unistd.h>
write 向文件写入数据 <unistd.h>
close 关闭文件描述符 <unistd.h>
lseek 移动文件读写指针 <unistd.h>

重要参数

  • O_RDONLY / O_WRONLY / O_RDWR:必须且只能选一个。

  • O_CREAT:文件不存在则创建,需要指定 mode(权限)。

  • O_APPEND:追加写入。

  • O_TRUNC:打开时清空文件内容。

文件描述符(fd)与重定向

什么是文件描述符?

open 的返回值是一个小整数,这就是文件描述符(file descriptor)

  • 默认情况下,每个 Linux 进程会打开三个文件描述符:

    • 0:标准输入(stdin)

    • 1:标准输出(stdout)

    • 2:标准错误(stderr)

      read(0, buf, sizeof(buf)); // 从键盘读
      write(1, buf, strlen(buf)); // 输出到屏幕
      write(2, buf, strlen(buf)); // 输出到错误输出

内核中的数据结构

每个进程在内核中有一个 task_struct,其中包含一个指向 files_struct 的指针。

  • files_struct 中有一个指针数组 fd_array[],每个元素指向一个已打开文件的 file 结构体。

  • 文件描述符就是这个数组的下标

    进程 task_struct
    └── files_struct
    └── fd_array[0] ──> file (stdin)
    └── fd_array[1] ──> file (stdout)
    └── fd_array[2] ──> file (stderr)
    └── fd_array[3] ──> file (我们打开的文件)

分配规则

总是分配当前未被使用的最小下标

复制代码
close(0);       // 释放 0
int fd = open("myfile", O_RDONLY);
printf("fd: %d\n", fd); // 输出 fd: 0

重定向的本质

当我们执行 close(1) 再打开一个新文件时,新文件会分配到 fd = 1。

复制代码
close(1);
int fd = open("myfile", O_WRONLY | O_CREAT, 0644);
printf("hello\n");  // 本来应该输出到屏幕,现在输出到 myfile

这就是输出重定向的本质 :改变 fd 数组下标指向的 file 对象。

更优雅的重定向:dup2

dup2(oldfd, newfd)newfd 指向 oldfd 所指向的文件,并关闭 newfd 原来的文件。

复制代码
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
dup2(fd, 1);  // 将标准输出重定向到 log.txt
printf("这条消息会写入文件,而不是屏幕\n");

理解 "一切皆文件" 的内核实现

struct filefile_operations

  • 每个打开的文件在内核中都对应一个 struct file

  • 关键在于 struct file 中的 f_op 成员,它指向一个 struct file_operations 结构体。

  • file_operations 中包含了大量的函数指针,如 readwriteopenreleasepoll 等。

    struct file_operations {
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    int (*open) (struct inode *, struct file *);
    int (*release) (struct inode *, struct file *);
    // ...
    };

多态的魅力

  • 对于磁盘文件read 指向磁盘驱动函数。

  • 对于键盘read 指向键盘驱动函数。

  • 对于显示器write 指向显示驱动函数。

对于上层应用来说,我们永远调用 read(fd, ...)write(fd, ...),由内核根据 fd 找到对应的 file,再调用相应的 file_operations 中的函数。

结论 :通过 struct file 和函数指针,Linux 实现了对所有设备的统一抽象,这就是 "一切皆文件" 的真谛。

缓冲区机制详解

什么是缓冲区?

缓冲区是内存中的一块区域,用于暂存数据,以减少系统调用的次数

为什么需要缓冲区?

  • 系统调用(如 read / write)会触发 CPU 从用户态切换到内核态,开销较大。

  • 磁盘 I/O 速度远慢于内存。

  • 通过缓冲区,我们可以积累一定量的数据后一次性进行系统调用,大幅提升效率。

三种缓冲类型(C 标准库提供)

类型 刷新时机 适用场景
全缓冲 缓冲区满或主动 flush 普通磁盘文件
行缓冲 遇到换行符 \n 或缓冲区满 终端(屏幕)
无缓冲 立即输出 stderr(错误信息)

一个经典案例:fork() + 重定向

复制代码
printf("hello printf\n");
fwrite("hello fwrite\n", 1, strlen("hello fwrite\n"), stdout);
write(1, "hello write\n", strlen("hello write\n"));
fork();
  • 直接运行到终端(行缓冲):看到三条输出各一次。

  • 重定向到文件(全缓冲):

    • write 是系统调用,没有用户级缓冲区,直接写入文件,输出一次

    • printffwrite 是库函数,自带缓冲区。由于是全缓冲,数据暂存在缓冲区中,fork 后,子进程复制了父进程的缓冲区。父进程退出刷新缓冲区,子进程退出也刷新缓冲区,导致输出两次

核心结论

  • printf / fwrite 的缓冲区是 C 库提供的用户级缓冲区 ,与 write 系统调用无关。

  • 系统调用本身没有用户级缓冲区,但内核有自己的页缓存(内核级缓冲区)。

FILE 结构体内部

因为IO相关函数与系统调⽤接⼝对应,并且库函数封装系统调用,所以本质上,访问文件都是通
过fd访问的。
所以FILE不仅仅是一个指针,它内部一定封装了文件描述符和缓冲区相关字段:

复制代码
struct _IO_FILE {
    int _fileno;        // 封装的 fd
    char* _IO_read_ptr; // 读缓冲区指针
    char* _IO_write_ptr;// 写缓冲区指针
    // ...
};

Linux I/O 数据结构关系详解

用最通俗的语言,把整个 I/O 系统的数据结构关系说清楚。

为了方便理解,我们先打个比方:

  • 进程就像一个"公司"。

  • task_struct 就是这个公司的"营业执照",上面记录了公司的一切信息。

  • files_struct 是公司的"固定资产登记表",记录了公司所有在用的设备。

  • 文件描述符(fd) 就是给每个设备编的"资产编号"(0号是键盘,1号是屏幕,2号是错误输出)。

  • struct file 是每个设备的"使用登记卡",记录了当前这个设备被谁在用、用到什么位置了。

  • file_operations 是设备的"操作说明书",告诉你怎么用这个设备(怎么读、怎么写)。

  • inode 是设备的"身份证",记录了它本身的属性(大小、权限、创建时间等)。

  • FILE* 是 C 库给每个设备配的"秘书",秘书手里有个小本本(缓冲区),先记下来再统一汇报给系统,减少打扰领导的次数。

核心数据结构关系

task_structfiles_struct

关系:包含关系

  • 每个进程在内核中都有一个 task_struct 结构体,它记录了进程的所有信息。

  • task_struct 里面有一个成员叫 files,它是一个指针,指向一个 files_struct 结构体。

  • 简单说 :进程通过 task_struct 找到了自己的 files_struct

    task_struct(进程控制块)

    └── files 指针 ──────→ files_struct(文件描述符表)

files_struct 与文件描述符(fd)

关系:数组存储关系

  • files_struct 里面最重要的东西是一个指针数组,叫 fd_array[]

  • 这个数组的下标就是文件描述符(fd),是一个从 0 开始的整数。

  • 数组的每个元素 都是一个指针,指向一个 struct file 结构体。

  • 简单说:fd 就是数组下标,通过 fd 就能找到对应的文件对象。

    files_struct

    └── fd_array[](指针数组)

    ├── [0] ──→ struct file(标准输入,键盘)
    ├── [1] ──→ struct file(标准输出,屏幕)
    ├── [2] ──→ struct file(标准错误,屏幕)
    ├── [3] ──→ struct file(我们自己打开的文件)
    └── [4] ──→ NULL(未使用)

struct filefile_operations

关系:指向关系

  • struct file 里面有一个成员叫 f_op,它是一个指针,指向一个 file_operations 结构体。

  • file_operations 里面全是函数指针,比如 .read.write.open.release 等。

  • 简单说struct file 通过 f_op 知道了"怎么操作这个文件"。

    struct file(打开的文件)

    ├── f_inode ──→ inode(文件属性)
    ├── f_pos(当前读写位置)
    ├── f_count(引用计数,几个 fd 在用)
    ├── f_flags(打开方式:只读、只写等)

    └── f_op ──→ file_operations(函数表)

    ├── .read = 某驱动_read函数
    ├── .write = 某驱动_write函数
    ├── .open = 某驱动_open函数
    └── .release = 某驱动_close函数

struct fileinode

关系:多对一关系

  • inode 代表一个"文件实体",记录了文件的静态属性(大小、权限、创建时间、数据在磁盘的位置等)。

  • 一个 inode 可以对应多个 struct file

  • 场景1 :同一个进程打开同一个文件两次,得到两个 struct file,但都指向同一个 inode

  • 场景2 :两个进程打开同一个文件,得到两个 struct file,但都指向同一个 inode

  • 简单说inode 是"文件本身",struct file 是"这次打开的状态"。

    磁盘上的文件 "data.txt"

    └── inode(唯一,存储文件属性)

    ├── 被进程 A 打开 → struct file A(f_pos=0,引用计数1)
    ├── 被进程 A 再次打开 → struct file A2(f_pos=0,引用计数1)
    └── 被进程 B 打开 → struct file B(f_pos=0,引用计数1)

    三个 struct file 的 f_inode 都指向同一个 inode

区别总结

概念 代表什么 生命周期 存在位置
inode 文件本身 文件创建到删除 磁盘 + 内存缓存
struct file 一次打开的状态 open()close() 内核内存
文件描述符 fd struct file 的索引 open()close() 进程的 files_struct

文件描述符(fd)与 FILE*

关系:封装关系

  • FILE* 是 C 标准库提供的结构体指针,定义在 stdio.h 中。

  • FILE 结构体内部有一个成员叫 _fileno,它就是对应的文件描述符(fd)。

  • 简单说FILE*fd 的"包装盒",里面除了 fd,还加了缓冲区等功能。

    FILE 结构体(C 库)

    ├── _fileno(封装的 fd,比如 3)
    ├── _IO_read_ptr(读缓冲区指针)
    ├── _IO_write_ptr(写缓冲区指针)
    ├── _IO_buf_base(缓冲区起始地址)
    └── _IO_buf_end(缓冲区结束地址)

    这个 FILE 结构体的地址就是 fopen() 返回的 FILE*

关键理解

  • fopen() 内部先调用 open() 获得 fd,再创建 FILE 结构体包装它。

  • fwrite() 先把数据写到 FILE 的缓冲区,条件满足时再通过 write() 系统调用把数据刷到内核。

  • printf()fprintf(stdout, ...) 的简化版,stdout 就是一个全局的 FILE*,它的 _fileno = 1。

多个 fd 指向同一个 struct filedup()fork()

关系:共享关系

  • dup(fd) :创建一个新的 fd,新 fd 和旧 fd 指向同一个 struct file

  • fork() :子进程复制父进程的 files_struct,子进程的 fd 也指向和父进程相同的 struct file

  • 此时,struct file 中的 f_count 引用计数会增加。

  • 后果 :这些 fd 共享 f_pos(读写位置),互相影响。

    情况1:dup()
    fd=3 ──→ struct file A(f_count=2)
    fd=4 ──→ 同一个 struct file A(dup 返回的新 fd)

    复制代码
      此时 read(fd=3) 和 read(fd=4) 共享读写位置

    情况2:fork()
    父进程 fd=3 ──→ struct file A(f_count=2)
    子进程 fd=3 ──→ 同一个 struct file A(复制了 fd 表)

    复制代码
      父子进程共享读写位置

file_operations 与设备驱动的关系

关系:回调关系

  • 设备驱动在初始化时,会注册自己的 file_operations 结构体。

  • 当用户调用 read() 时,内核通过 fd 找到 struct file,再找到 file_operations,然后调用其中的 .read 函数指针。

  • 简单说file_operations 是驱动提供的"服务清单",内核根据这个清单来调用驱动的功能。

    一个磁盘驱动注册的 file_operations:
    .read = ext4_read_file
    .write = ext4_write_file
    .open = ext4_open_file
    .release= ext4_release_file

    一个键盘驱动注册的 file_operations:
    .read = keyboard_read_key
    .write = NULL(键盘不需要写)
    .open = keyboard_open
    .release= keyboard_release

    一个显示驱动注册的 file_operations:
    .read = NULL(显示器不需要读)
    .write = display_write_char
    .open = display_open
    .release= display_release

统一调用

复制代码
// 用户调用 read(fd, buf, size)
// 内核内部实际执行的是:
ret = file->f_op->read(file, buf, size, &file->f_pos);
// 不管 fd 对应的是磁盘、键盘还是其他设备,调用方式完全一样!

总结

  1. 进程通过 task_struct 找到 files_struct,再通过 fd 找到 struct file,再通过 file_operations 调用驱动,最终操作硬件。 这是一条完整的调用链。

  2. fd 就是个数组下标,分配规则是最小未使用。重定向就是换掉数组中某个下标指向的对象。

  3. "一切皆文件"是靠 file_operations 函数表实现的,不同设备提供不同的函数表,但上层调用接口统一。

  4. FILE* 是对 fd 的封装,加上缓冲区是为了减少系统调用次数,提升性能。

  5. 缓冲区问题 是 C 库的"用户级缓冲",与系统调用的"内核级缓冲"是两码事。fork() 会复制用户级缓冲区,导致某些奇怪的现象。

相关推荐
资深电气设计20 分钟前
雕铣机主轴驱动升级趋势分析:变频器技术价值与选型逻辑探讨
运维
CZIDC22 分钟前
华为服务器TS200-2280 V2 iBMC导入SSL证书后问题案例分析
服务器·https·ssl
M78佐菲33 分钟前
Linux多线程:创建、回收与互斥同步
linux·笔记·学习·算法
名字还没想好☜37 分钟前
Prometheus 告警实战:写 alerting rules、用 Alertmanager 做路由分组与抑制
运维·前端·javascript·docker·kubernetes·prometheus
sunoo-2291 小时前
【Linux 系统编程】学习第七天:线程属性 | 互斥锁 | 死锁 | 信号量 核心知识点 + 踩坑实战
linux·c语言·笔记·vscode·学习
Zkaisen1 小时前
【Gitee】SSH 公钥、GPG 公钥、私人令牌的区别
运维·gitee·ssh
小雪崩1 小时前
嵌入式学习 day33:线程进阶
linux·c语言·学习
HXSYS1 小时前
无人机库房管理标准化,《无人机装调维修师》规范存储运维
运维·无人机
牢姐与蒯1 小时前
Linux进程(四).进程优先级以及进程切换,进程调度
linux·运维·服务器·ubuntu