【linux学习】深入理解linux文件I/O,从C标准库到内核态

大家好,我是程序员小青蛙,今天介绍文件系统的浅显理解。

前言

很多 C 语言初学者接触文件操作时,最先学会的是fopen/fread/fwrite这套标准库接口。但当我们深入 Linux 系统编程时,会发现还有另一套open/read/write系统调用接口。为什么会有两套接口?它们之间是什么关系?文件描述符到底是什么?重定向的底层原理是什么?"Linux 一切皆文件" 这句耳熟能详的话背后,又隐藏着怎样的设计哲学?

本文将从 C 标准库 I/O 出发,一步步深入 Linux 内核,揭开文件 I/O 的神秘面纱,带你理解这些问题的本质。

一、C 标准库 I/O 与 Linux 系统调用 I/O:封装与本质

1.1 两套接口的代码对比

我们先来看两段功能完全相同的代码,分别用 C 标准库和 Linux 系统调用实现文件写入:

C 标准库版本

cpp 复制代码
#include <stdio.h>
#include <string.h>

int main()
{
    FILE *fp = fopen("myfile", "w");
    if(!fp){
        printf("fopen error!\n");
    }

    const char *msg = "hello bit!\n";
    int count = 5;
    while(count--){
        fwrite(msg, strlen(msg), 1, fp);
    }

    fclose(fp);
    return 0;
}

Linux 系统调用版本

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

int main()
{
    umask(0);
    int fd = open("myfile", O_WRONLY|O_CREAT, 0644);
    if(fd < 0){
        perror("open");
        return 1;
    }

    const char *msg = "hello bit!\n";
    int count = 5;
    int len = strlen(msg);
    while(count--){
        write(fd, msg, len);
    }

    close(fd);
    return 0;
}

可以看到,两套接口非常相似:都有打开、写入、关闭操作,参数也有对应关系。这并非巧合 ------C 标准库的文件 I/O 函数,本质上就是对 Linux 系统调用的封装

cpp 复制代码
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg = "hello fwrite\n";
fwrite(msg, strlen(msg), 1, stdout);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
return 0;
}

直接输入到显示屏上


stdin & stdout & stderr

C默认会打开三个输入输出流,分别是stdin, stdout, stderr

仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针
打开文件的方式有r,r+,w,w+,a,a+,

还有fseekftellrewind的函数

1.2 库函数与系统调用的本质区别

要理解两者的关系,我们需要先明确两个概念:

  • 库函数 :运行在用户态 ,是编程语言提供的对系统调用的封装,方便开发者使用。例如fopen/fclose/fread/fwrite都属于 C 标准库 (libc) 函数。
  • 系统调用 :运行在内核态 ,是操作系统内核提供给用户程序的接口,是用户态访问内核资源的唯一方式。例如open/close/read/write都属于 Linux 系统调用。

当我们调用fwrite时,它并不会直接访问磁盘,而是先将数据写入 C 标准库提供的用户级缓冲区 ,当缓冲区满足一定条件(如缓冲区满、遇到换行符、调用fflush或进程退出)时,再调用系统调用write将数据一次性写入内核缓冲区,最终由内核将数据写入磁盘。

这种分层设计的好处是:

  1. 提高 I/O 效率:减少系统调用的次数(系统调用需要从用户态切换到内核态,开销较大)
  2. 提高代码可移植性:C 标准库屏蔽了不同操作系统系统调用的差异,使得同样的代码可以在 Windows、Linux、macOS 等不同系统上编译运行

1.3 接口函数open

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

int open(const char *pathname, int flags);

int open(const char *pathname, int flags, mode_t mode);

pathname:要打开或创建的目标文件

flags:打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行"或"运算,构成flags。

参数:

O_RDONLY:只读打开

O_WRONLY:只写打开

O_RDWR:读,写打开

这三个常量,必须指定一个且只能指定一个

O_CREAT :若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限

O_APPEND:追加写

返回值:

成功:新打开的文件描述符

失败:-1

返回值:

在认识返回值之前,先来认识一下两个概念:系统调用和库函数

上面的fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。

而,open close read write seek都属于系统提供的接口,称之为系统调用接口

回忆一下我们讲操作系统概念时,画的一张图

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

二、文件描述符 (fd):内核管理文件的核心

2.1 文件描述符的本质

open系统调用成功时会返回一个非负整数,这个整数就是文件描述符 (File Descriptor, fd)。很多人只知道它是一个用来标识文件的整数,但它的本质是什么呢?

答案是:文件描述符本质上是进程文件描述符表的数组下标

让我们深入内核来看这个过程:

  1. 当进程调用open打开一个文件时,内核会在内存中创建一个struct file结构体,用来描述这个打开的文件(包含文件的属性、操作方法、当前读写位置等信息)
  2. 每个进程在内核中都有一个struct files_struct结构体,称为文件描述符表
  3. 这个结构体内部包含一个指针数组struct file *fd_array[],数组的每个元素都指向一个struct file结构体
  4. open函数会在这个数组中找到一个最小的未被使用的下标,将新创建的struct file结构体的地址存入该下标对应的位置,然后将这个下标作为返回值返回给用户

这就是为什么文件描述符总是从 0 开始的小整数 ------ 它只是数组的下标而已。

2.2 三个默认的文件描述符

Linux 进程在创建时,会默认打开三个文件描述符:

  • 0:标准输入 (stdin),默认对应键盘设备
  • 1:标准输出 (stdout),默认对应显示器设备
  • 2:标准错误 (stderr),默认对应显示器设备

这就是为什么我们可以直接使用printf输出到显示器,使用scanf从键盘输入 ------ 它们本质上是向文件描述符 1 写入数据,从文件描述符 0 读取数据。

我们可以用代码验证这一点:

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

int main()
{
    int fd = open("myfile", O_RDONLY);
    if(fd < 0){
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd); // 输出:fd: 3
    close(fd);
    return 0;
}

输出结果是 3,因为 0、1、2 已经被默认打开的三个标准流占用了,所以第一个新打开的文件的文件描述符是 3。

输入输出也可以利用这样的规则:

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
char buf[1024];
ssize_t s = read(0, buf, sizeof(buf));
if(s > 0){
buf[s] = 0;
write(1, buf, strlen(buf));
write(2, buf, strlen(buf));
}
return 0;
}

而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files,指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件

2.3 文件描述符的分配规则

文件描述符的分配遵循最小可用原则:内核会在文件描述符表中,从小到大寻找第一个未被使用的下标,分配给新打开的文件。

我们可以用下面的代码验证这个规则:

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

int main()
{
    close(1); // 关闭标准输出,文件描述符1被释放
    int fd = open("myfile", O_WRONLY|O_CREAT, 0644);
    if(fd < 0){
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd); // 输出:fd: 1
    fflush(stdout);
    close(fd);
    return 0;
}
cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0);
//close(2);
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}

在这个例子中,我们先关闭了文件描述符 1,然后打开一个新文件,内核会将最小的可用下标 1 分配给这个新文件,所以输出结果是 1。

三、重定向的底层原理:修改内核的指针

3.1 什么是重定向

在 Linux 命令行中,我们经常使用重定向符号:

  • >:输出重定向,将命令的输出写入文件而不是显示器
  • >>:追加重定向,将命令的输出追加到文件末尾
  • <:输入重定向,从文件读取输入而不是键盘

例如,执行ls > file.txt会将ls命令的输出写入file.txt文件中,而不是显示在屏幕上。

那么重定向的本质是什么呢?我们来看刚才的代码:

cpp 复制代码
close(1);
int fd = open("myfile", O_WRONLY|O_CREAT, 0644);
printf("fd: %d\n", fd);

这段代码执行后,我们会发现屏幕上没有任何输出,而myfile文件中却出现了fd: 1这行内容。

这就是最原始的输出重定向!printf函数本质上是向标准输出 (stdout) 写入数据,而 stdout 对应的文件描述符是 1。当我们关闭文件描述符 1,然后打开一个新文件时,新文件的文件描述符被分配为 1。此时,所有向文件描述符 1 写入的数据,都会被写入到这个新文件中,而不是显示器。

3.2 重定向的本质

从上面的例子可以看出,重定向的本质是:在内核层面,修改文件描述符表中对应下标指向的struct file结构体的地址

上层的printf/fwrite等函数并不知道底层发生了变化,它们仍然按照原来的方式向文件描述符 1 写入数据,但此时文件描述符 1 已经不再指向显示器设备,而是指向了我们打开的文件。

3.3 使用 dup2 系统调用实现重定向

上面的方法虽然可以实现重定向,但不够优雅,而且在多线程环境下可能会出现问题。Linux 提供了专门的系统调用dup2来实现文件描述符的复制,从而实现重定向。

dup2函数的原型如下:

cpp 复制代码
#include <unistd.h>
int dup2(int oldfd, int newfd);

dup2函数的作用是:将newfd指向oldfd指向的文件。如果newfd已经打开,则先关闭newfd

使用dup2实现输出重定向的代码如下:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    int fd = open("./log", O_CREAT | O_RDWR, 0644);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    dup2(fd, 1); // 将标准输出重定向到log文件
    printf("hello world\n"); // 这句话会输出到log文件中
    fflush(stdout);
    close(fd);
    return 0;
}

3.4 在 minishell 中实现重定向

理解了重定向的原理后,我们就可以在自己实现的简易 shell 中添加重定向功能了。核心思路是:

  1. 解析用户输入的命令,识别出重定向符号和目标文件名
  2. 在子进程中执行命令前,使用dup2将标准输入 / 输出重定向到目标文件
  3. 执行命令,命令的输入 / 输出就会自动重定向到目标文件
cpp 复制代码
# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <string.h>
# include <fcntl.h>
# define MAX_CMD 1024
char command[MAX_CMD];
int do_face()
{
memset(command, 0x00, MAX_CMD);
printf("minishell$ ");
fflush(stdout);
if (scanf("%[^\n]%*c", command) == 0) {
getchar();
return -1;
}
return 0;
}
char **do_parse(char *buff)
{
int argc = 0;
static char *argv[32];
char *ptr = buff;
while(*ptr != '\0') {
if (!isspace(*ptr)) {
argv[argc++] = ptr;
while((!isspace(*ptr)) && (*ptr) != '\0') {
ptr++;
}
}else {
while(isspace(*ptr)) {
*ptr = '\0';
ptr++;
}
}
}
argv[argc] = NULL;
return argv;
}
int do_redirect(char *buff)
{
char *ptr = buff, *file = NULL;
int type = 0, fd, redirect_type = -1;
while(*ptr != '\0') {
if (*ptr == '>') {
*ptr++ = '\0';
redirect_type++;
if (*ptr == '>') {
*ptr++ = '\0';
redirect_type++;
}
while(isspace(*ptr)) {
ptr++;
}
file = ptr;
while((!isspace(*ptr)) && *ptr != '\0') {
ptr++;
}
*ptr = '\0';
if (redirect_type == 0) {
fd = open(file, O_CREAT|O_TRUNC|O_WRONLY, 0664);
}else {
fd = open(file, O_CREAT|O_APPEND|O_WRONLY, 0664);
}
dup2(fd, 1);
}
ptr++;
}
return 0;
}
int do_exec(char *buff)
{
char **argv = {NULL};
int pid = fork();
if (pid == 0) {
do_redirect(buff);
argv = do_parse(buff);
if (argv[0] == NULL) {
exit(-1);
}
execvp(argv[0], argv);
}else {
waitpid(pid, NULL, 0);
}
return 0;
}
int main(int argc, char *argv[])
{
while(1) {
if (do_face() < 0)
continue;
do_exec(command);
}
return 0;
}

四、缓冲区的 "坑" 与本质:为什么 fork 后输出次数不同?

4.1 一个令人困惑的现象

我们来看一段代码:

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
    const char *msg0="hello printf\n";
    const char *msg1="hello fwrite\n";
    const char *msg2="hello write\n";

    printf("%s", msg0);
    fwrite(msg1, strlen(msg1), 1, stdout);
    write(1, msg2, strlen(msg2));

    fork();
    return 0;
}

当我们直接运行这个程序时,输出结果是:

复制代码
hello printf
hello fwrite
hello write

但当我们将输出重定向到文件中时(./a.out > file.txt),查看文件内容会发现:

复制代码
hello write
hello printf
hello fwrite
hello printf
hello fwrite

同样的代码,只是输出目标不同,结果却大相径庭:printffwrite输出了两次,而write只输出了一次。这是为什么呢?

4.2 缓冲区的本质与分类

要解释这个现象,我们需要理解缓冲区的概念。

缓冲区本质上是一段内存空间,用来暂存 I/O 数据。缓冲区的存在是为了提高 I/O 效率 ------ 磁盘访问是毫秒级的,而内存访问是纳秒级的,差了 6 个数量级。如果每次写一个字节都直接访问磁盘,程序的效率会极其低下。

一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。

printffwrite库函数会自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。

而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后但是进程退出之后,会统一刷新,写入文件当中。

但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。

write没有变化,说明没有所谓的缓冲。

综上:printffwrite库函数会自带缓冲区,而write系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。

那这个缓冲区谁提供呢?printffwrite是库函数,write是系统调用,库函数在系统调用的"上层",是对系统调用的"封装",但是write没有缓冲区,而printffwrite有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。

在 Linux 中,缓冲区主要分为两类:

  1. 用户级缓冲区 :由 C 标准库提供,位于用户态内存中。FILE结构体内部就包含了用户级缓冲区的相关信息(缓冲区指针、大小、当前位置等)。
  2. 内核级缓冲区:由操作系统内核提供,位于内核态内存中。所有的磁盘 I/O 都会经过内核级缓冲区。

我们通常所说的缓冲区,指的是用户级缓冲区

4.3 缓冲区的刷新策略

C 标准库为不同的设备设置了不同的缓冲区刷新策略:

  • 行缓冲 :当缓冲区中遇到换行符\n时,刷新缓冲区。标准输出 (stdout) 默认采用行缓冲。
  • 全缓冲:当缓冲区被写满时,刷新缓冲区。普通文件默认采用全缓冲。
  • 无缓冲:不使用缓冲区,数据直接写入内核。标准错误 (stderr) 默认采用无缓冲。

缓冲区刷新的几种情况:

  1. 调用fflush函数强制刷新
  2. 进程正常退出时
  3. 缓冲区被写满时
  4. 行缓冲遇到换行符时

4.4 现象解释

现在我们可以解释刚才的现象了:

直接运行程序(输出到显示器)

  • 标准输出 (stdout) 采用行缓冲
  • printffwrite输出的内容都包含换行符\n,会立即刷新缓冲区,数据被写入内核
  • write是系统调用,没有用户级缓冲区,数据直接写入内核
  • 此时 fork () 执行时,用户级缓冲区已经是空的,所以子进程不会复制任何数据
  • 最终每个函数都只输出一次

输出重定向到文件

  • 标准输出 (stdout) 不再指向显示器,而是指向普通文件,刷新策略变为全缓冲
  • printffwrite输出的内容虽然包含换行符\n,但不会立即刷新缓冲区,数据会暂存在用户级缓冲区中
  • write是系统调用,没有用户级缓冲区,数据直接写入内核
  • 此时 fork () 执行时,会复制父进程的地址空间,包括用户级缓冲区中的内容
  • 当父进程和子进程退出时,都会刷新各自的用户级缓冲区,将数据写入文件
  • 所以printffwrite各输出了两次,而write只输出了一次

这个例子深刻地揭示了 C 标准库 I/O 和系统调用 I/O 的本质区别:C 标准库 I/O 有用户级缓冲区,而系统调用 I/O 没有

五、"Linux 一切皆文件":操作系统层面的多态

先看文件:

模式 硬链接数 文件所有者 组 大小 最后修改时间 文件名

stat命令可以看到更多

Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。政府管理各区的例子

超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck和inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了

GDT,Group Descriptor Table:块组描述符,描述块组属性信息,可以在了解一下块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用

inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。

i节点表:存放文件属性如文件大小,所有者,最近修改时间等

数据区:存放文件内容

将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过touch一个新文件来看看如何工作。

创建一个新文件主要有一下4个操作:

1.存储属性

内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。

2.存储数据

该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。

3.记录分配情况

文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。

4.添加文件名到目录

新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。

下面解释一下文件的三个时间:

Access最后访问时间

Modify文件内容最后修改时间

Change属性最后修改时间
理解硬链接

我们看到,真正找到磁盘上文件的并不是文件名,而是inode。其实在linux中可以让多个文件名对应于同一个inode。

abc和def的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode263466的硬连接数为2。

我们在删除文件时干了两件事情:1.在目录中将对应的记录删除,2.将硬连接数-1,如果为0,则将对应的磁盘释放。

软链接

硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件。

5.1 不仅仅是口号

"Linux 一切皆文件" 是 Linux 最著名的设计哲学之一。但很多人只是把它当作一个口号,并没有真正理解它的含义。

在 Linux 中,不仅仅是普通的磁盘文件被当作文件,几乎所有的系统资源都被抽象成了文件:

  • 硬件设备:键盘、鼠标、显示器、磁盘、网卡、打印机等
  • 进程间通信:管道、消息队列、共享内存等
  • 系统信息:/proc 目录下的各种文件
  • 网络连接:socket

所有这些完全不同的资源,都可以使用同一套文件操作接口(open/read/write/close)来访问。这是如何实现的呢?

5.2 VFS:虚拟文件系统层

Linux 内核中实现了一个虚拟文件系统 (Virtual File System, VFS) 层。VFS 定义了一套所有文件系统都必须实现的统一接口。

每个打开的文件在内核中都对应一个struct file结构体,这个结构体中有一个重要的成员:struct file_operations *f_opstruct file_operations是一个函数指针表,包含了对文件进行各种操作的函数指针,如readwriteopenrelease等。

不同的文件系统或设备驱动,会实现自己的struct file_operations函数表。例如:

  • 磁盘文件系统(如 ext4)会实现针对磁盘的readwrite函数
  • 键盘驱动会实现针对键盘的read函数
  • 显示器驱动会实现针对显示器的write函数
  • 网卡驱动会实现针对网络的readwrite函数

当用户调用read系统调用时,内核会根据文件描述符找到对应的struct file结构体,然后调用f_op指向的函数表中的read函数。

这就是 "一切皆文件" 的本质:通过 VFS 层和函数指针,实现了操作系统层面的多态。上层应用不需要关心底层是什么设备,只需要使用统一的文件操作接口即可,具体的操作细节由底层的驱动程序实现。

这种设计思想极其优雅,它让 Linux 系统的接口变得非常统一和简洁,极大地提高了系统的可扩展性和可维护性。

六、磁盘底层原理:文件系统的物理基础

6.1 磁盘的物理结构

磁盘是计算机中唯一的主要机械存储设备,它的物理结构决定了它的访问特性。

一个典型的机械硬盘由以下几个部分组成:

  • 盘片:硬盘通常有多个盘片,每个盘片的两面都可以存储数据
  • 磁头:每个盘面都有一个磁头,负责读写数据
  • 主轴:带动盘片高速旋转
  • 磁头臂:带动磁头在盘面上移动

盘片上的数据是按照同心圆来组织的,这些同心圆称为磁道 。每个磁道又被划分成多个扇区,每个扇区的大小通常是 512 字节。扇区是磁盘读写的基本单位。

所有盘面上相同编号的磁道,共同组成了一个柱面

6.2 CHS 寻址方式

要在磁盘上定位一个扇区,需要三个参数:

  • C(Cylinder):柱面号,确定磁头要移动到哪个磁道
  • H(Head):磁头号,确定使用哪个盘面的磁头
  • S(Sector):扇区号,确定磁道上的哪个扇区

这种寻址方式称为CHS 寻址

磁盘访问一个扇区的时间主要由三部分组成:

  1. 寻道时间:磁头移动到目标磁道所需的时间,通常是几毫秒到十几毫秒
  2. 旋转延迟:盘片旋转到目标扇区经过磁头下方所需的时间,对于 7200 转 / 分钟的硬盘,平均旋转延迟约为 4.17 毫秒
  3. 传输时间:将数据从扇区读取到内存或将数据写入扇区所需的时间,通常非常短

可以看出,磁盘访问的主要开销是寻道时间和旋转延迟,这也是为什么磁盘访问比内存访问慢得多的原因。

6.3 文件系统的作用

磁盘的物理结构是圆形的,数据是按照 CHS 方式组织的。但对于用户来说,我们希望文件是线性的,可以按照文件名和偏移量来访问。

文件系统的作用就是将磁盘的圆形物理结构抽象成线性的逻辑结构,为用户提供一个简单、统一的文件访问接口。文件系统负责管理磁盘上的空闲空间,维护文件的元数据(文件名、大小、创建时间、存储位置等),并将用户的逻辑读写请求转换为物理磁盘的 CHS 读写请求。

七、总结

本文从 C 标准库 I/O 出发,一步步深入 Linux 内核,全面解析了文件 I/O 的本质:

  1. C 标准库 I/O 是对 Linux 系统调用 I/O 的封装,提供了用户级缓冲区,提高了 I/O 效率和代码可移植性。
  2. 文件描述符本质上是进程文件描述符表的数组下标,内核通过文件描述符表来管理进程打开的所有文件。
  3. 重定向的本质是在内核层面修改文件描述符表中对应下标指向的struct file结构体的地址
  4. 缓冲区是为了提高 I/O 效率而存在的,C 标准库提供的用户级缓冲区是导致 fork 后输出次数不同的根本原因。
  5. "Linux 一切皆文件" 是通过 VFS 虚拟文件系统层和函数指针实现的操作系统层面的多态,它让 Linux 系统的接口变得极其统一和简洁。
  6. 文件系统将磁盘的圆形物理结构抽象成线性的逻辑结构,为用户提供了简单、统一的文件访问接口。

学习系统级文件 I/O,不仅仅是学会使用几个函数,更重要的是理解操作系统如何管理文件和设备,理解用户态和内核态的交互方式。这些知识是学习网络编程、进程间通信、操作系统内核等高级主题的基础。

相关推荐
凉、介1 小时前
深入理解 ARMv8-A|处理器模式与寄存器
笔记·学习·嵌入式·arm
weixin_307779132 小时前
面向高性能保密计算的定制 Linux 系统构建与自动部署方案
linux·安全·网络安全·性能优化·系统安全
着迷不白2 小时前
五、文本处理工具+正则表达式
linux·运维·服务器
阿文的代码库2 小时前
康威尔生命游戏规则介绍与学习
学习
载数而行5202 小时前
Linux 4常用指令(文件/时间/搜索查找/压缩解压指令)
linux
我的xiaodoujiao2 小时前
API 接口自动化测试详细图文教程学习系列24--如何用Pytest去设计接口测试用例并执行
python·学习·测试工具·pytest
-To be number.wan2 小时前
计算机组成原理 | SRAM与DRAM
学习·计算机组成原理
不做无法实现的梦~3 小时前
MAVLink 协议教程
linux·stm32·嵌入式硬件·算法
实心儿儿3 小时前
Linux —— 线程控制(2)
linux·运维·服务器