【Linux】基础 IO
8. 基础 I/O
8.1 文件理解
首先文件 = 内容 + 属性。就算文件大小为零,文件依然要在磁盘上占据空间。我们的文件是存储在磁盘上的,磁盘是一个外设,是永久性存储介质。既然是外设,我们就需要来与磁盘进行交互才进行操作。因此磁盘上的文件本质就是对文件的所有操作,都是对外设的输入输出,我们简称io。而对文件操作本质也是对文件的属性或者内容进行操作。
访问文件操作文件首先要打开文件,但是打开文件本质是谁打开的呢?是进程。因此文件操作本质也是进程在操作文件。(理解清楚,文件操作本质还是os帮你干的,只是我们当前程序的进程叫os去干的。)
8.2 C语言文件操作常见接口
1. FILE *fopen(const char *path, const char *mode);
用来打开文件的,第一个参数要写对应的文件名(要注意只会在当前工作目录下去找,不会去别的目录找)第二个参数则是怎么个方式打开这个文件,说白了就是打开后这个文件希望以什么权限操作。
| 模式 | 文件不存在 | 文件已存在 | 读写权限 | 偏移量 |
|---|---|---|---|---|
"r" | ❌ 报错 | ✅ 保留内容 | 只读 | 开头 |
"w" | ✅ 创建 | ❌ 清空内容 | 只写 | 开头 |
"a" | ✅ 创建 | ✅ 保留内容 | 只写(追加) | 末尾 |
所以 w 和 a 区别还是很大的,w 就相当于Linux的 > ,把你原有内容也要清空了。但是 a 就类似Linux的 >> ,只是追加内容 。
成功 → 返回 FILE* 指针(包含 fd、缓冲区、状态标志)
失败 → 返回 NULL,errno 记录具体错误(如权限不足、文件不存在)
典型用法:
FILE *fp = fopen("log.txt", "r");
if (fp == NULL)
{
perror("fopen");
return 1;
}
-
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
| 参数 | 含义 | 逻辑顺序 |
|---|---|---|
ptr | 数据存放的缓冲区地址 | 读到哪里去 |
size | 每个数据单元的大小(字节) | 每个单元多大 |
nmemb | 要读多少个单元 | 读多少个 |
stream | 文件流指针 | 从哪个文件读 |
这个第一个参数意思本质就是把我们要打开的文件内容写入到我们的自定义的缓冲区内,二三个参数就是一次写入的大小有多大,要读多少次。第四个就是文件名,就是前面fopen的文件指针。
如果返回值 == nmemb → 读取成功
如果返回值 < nmemb → 遇到文件末尾(EOF)或出错(需用 ferror 区分)
典型用法:
char buffer[1024];
size_t n = fread(buffer, 1, sizeof(buffer)-1, fp);
if (n > 0)
{
buffer[n] = '\0'; // 手动加结束符(如果当字符串用)
}
实际上我们一般不会知道实际文件大小,缓冲区大小怎么定义?如果文件很大,又应该怎么fread?
-
char buffer[ 4096 ]
因为 Linux 文件系统的默认块大小(Block Size)就是 4096 字节。也就是说,操作系统内核每次从硬盘读取数据,最少也是以 4KB 为单位搬运的。你用 4096 字节的缓冲区,正好和内核的“搬运效率”对齐,读写性能最高。
2. 我们可以不去管文件到底多大,都用 **4KB 的小碗**,在循环里一勺一勺舀,直到舀完。
char buf[4096];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), fp)) > 0)
// 相当于只要一直能fread出来东西就一直进循环打印
{
// 处理这 n 个字节(可能小于 4096,比如最后一段)
fwrite(buf, 1, n, stdout); // 比如直接输出到屏幕
}
这段代码可以处理任意大小的文件(几个字节到几十 GB),而且内存只占 4KB。
-
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
| 参数 | 含义 |
|---|---|
ptr | 数据来源的缓冲区地址(要写什么) |
size | 每个单元大小(字节) |
nmemb | 要写多少个单元 |
stream | 目标文件流 |
返回值:实际写入的单元个数。如果小于 nmemb,说明磁盘满了或出错。
典型用法:
char *msg = "hello world\n"; fwrite(msg, strlen(msg), 1, fp); // 写 1 个字符串
-
int feof(FILE *stream);
判断文件流的“文件结束标志”是否被触发。
返回值:
-
非 0(真) → 已经到达文件末尾
-
0(假) → 还没到末尾
要注意的是:feof 不会主动预测文件结束,它只会在 fread 或 fgetc 已经尝试读取但失败了之后,才返回真。所以,不能在进入循环前用 feof 判断,否则会多读一次或读不到数据。
错误写法:
while (!feof(fp))
{
fread(buffer, 1, 100, fp); // 最后一次循环会读失败,但 buffer 里是旧数据!
}
正确写法:
size_t n;
while ((n = fread(buffer, 1, sizeof(buffer)-1, fp)) > 0)
{
// 只处理成功读到的数据
}
// 循环结束后,如果需要判断是否出错,再结合 ferror
feof 在一个场景里特别必要:
feof 只在一个场景下有必要:当 fread 返回 0 时,你想知道是读完了,还是出错了。
size_t n = fread(buf, 1, sizeof(buf), fp);
if (n == 0)
{
if (feof(fp))
{
printf("正常读完\n");
}
else
{
perror("读取出错");
}
}
-
int fprintf(FILE *stream, const char *format, ...);
目标是任何 FILE* 流(文件、stdout、stderr),然后把格式化后的字符串写入指定的流。所以第一个参数可以是指定的输出流,也可以是指定的文件。
fprintf(stdout, "Hello %s\n", "world"); // 打印到屏幕 fprintf(fp, "写入文件: %d\n", 42); // 写入文件
要注意的是fprintf和fwrite都可以往文件写入内容,前者只写入文本内容,后者直接是二进制内容。不过只看在编辑器中的文本呈现效果两个没有区别,因为编辑器会给你转化一下,但是底层不一样。后者毕竟是二进制写入底层,一步到位。所以fwrite给文件存的也是二进制。所以fwrite只适合写数据,内存那些啊,但是想要写文本还是要用fprintf。
8.3 文件的系统接口
-
int open(const char *pathname, int flags, mode_t mode);
用来打开文件的。
| 参数 | 类型 | 含义 | 详细解释 |
|---|---|---|---|
pathname | const char * | 文件路径 | 你要打开的文件的路径字符串,比如 "test.txt" 或 "/home/user/data.bin"。 |
flags | int | 打开标志 | 位掩码。用 | 组合,告诉内核“我要怎么打开这个文件”。 |
mode | mode_t | 权限模式 | 只在创建新文件时有效(即 flags 里包含 O_CREAT 时)。指定文件的初始权限,比如 0644。 |
flags 的常用选项:
-
O_RDONLY:只读打开 -
O_WRONLY:只写打开 -
O_RDWR:读写打开 -
O_CREAT:如果文件不存在,就创建它(此时必须提供mode参数) -
O_TRUNC:如果文件已存在且以写方式打开,把文件长度截断为 0(清空) -
O_APPEND:每次写都追加到文件末尾
返回值:成功返回一个非负整数(文件描述符 fd),失败返回 -1
但是返回的一般都是从3开始的,0,1,2分别是标准输入,标准输出和标准错误。
-
ssize_t write(int fd, const void *buf, size_t count);
把内存数据写入内存,二进制形式。
| 参数 | 类型 | 含义 | 详细解释 |
|---|---|---|---|
fd | int | 文件描述符 | open 返回的那个整数,告诉内核“我要写到哪个文件”。 |
buf | const void * | 数据缓冲区 | 你要写入的数据在内存里的起始地址。注意是 void*,说明它不关心数据类型(二进制、字符串都行)。 |
count | size_t | 字节数 | 你要写入多少个字节。必须精确,写多了会读到垃圾数据,写少了会丢数据。 |
返回值:实际写入的字节数。可能小于 count(比如磁盘满了、被信号打断),也可能返回 -1(出错)。
-
ssize_t read(int fd, void *buf, size_t count);
读取文件的数据
| 参数 | 类型 | 含义 | 详细解释 |
|---|---|---|---|
fd | int | 文件描述符 | 从哪个文件读。 |
buf | void * | 数据缓冲区 | 你准备用来装数据的内存地址。注意这里没有 const,因为 read 会修改这块内存。 |
count | size_t | 最多读多少字节 | 你告诉内核“我最多能接收这么多字节”。如果文件剩余内容不足 count,就只读实际有的。 |
返回值:实际读到的字节数。
-
返回
0:表示读到了文件末尾(EOF)。 -
返回
-1:出错。 -
返回正数:实际读取的字节数(可能小于
count)。
关键点:
-
buf必须是你提前分配好的内存(比如char buffer[1024])。 -
同样无缓冲,每次调用都直接和硬盘交互。
-
int close(int fd);
就是关闭文件。
| 参数 | 类型 | 含义 |
|---|---|---|
fd | int | 要关闭的文件描述符 |
返回值:成功返回 0,失败返回 -1。
为什么必须调用是因为释放内核里的文件表项,回收 fd 编号。不关会导致 fd 泄漏,最终程序无法再打开新文件。
-
int fsync(int fd); int fdatasync(int fd);
两个函数都只有一个参数:fd,要同步的文件描述符。
返回值:
-
成功返回
0。 -
失败返回
-1,并设置errno。
fsync(int fd)
-
作用:把
fd对应的文件,所有脏数据 + 全部元数据刷到磁盘,并等磁盘确认。 -
元数据包括:文件大小、修改时间、inode 属性等。
-
返回时保证:数据和元数据都已持久化。
-
代价:慢,元数据分散,写入次数多。
fdatasync(int fd)
-
作用:把
fd对应的文件,脏数据 + 读取所必需的元数据刷到磁盘,并等磁盘确认。 -
必需的元数据主要是文件大小(否则读不到新写的数据)。
-
不刷修改时间、访问时间等"不影响数据读取"的元数据。
-
代价:通常比
fsync快。
参数说明
| 参数 | 含义 |
|---|---|
fd | 已打开文件的描述符,必须可写(O_WRONLY 或 O_RDWR) |
我们C语言那些常见的文件操作接口本质都是去封装了这些系统的文件操作的接口。
8.4 输入输出流
输入输出流千万要记住他们本质都是用来描述程序(进程)的。比如我们说的写入,输出本质都是同一件事情都是输出流。我们说的输出就是程序把数据打印到屏幕上,本质是数据从程序向外部流出(程序 → 屏幕);写入也一样,是程序把数据写给文件,本质也是数据从程序向外部流出(程序 → 文件)。
我们有stdin,stdout,stderr三个,C语言和C++都是一样的。分别是标准输入,标准输出,stderr也是输出,用来输出错误的。
然后输入流顾名思义, 就是数据从外部流向程序,程序从键盘读取到我们输入的数据,这个过程就是输入。
8.5 文件描述符
文件描述符(fd)是一个非负整数,是操作系统内核分配给进程的,用来标识一个已打开文件的索引或身份证号。
8.5.1 文件描述表
我们的操作系统当中会给每一个进程生成一个taskstruct,同样的一个进程可以打开很多个文件,每个文件说白了就是一个struct file。在每一个进程的taskstruct里头会维护一个指针指向一个files_struct的结构体,是进程为了可以更加有效的管理他们,知道打开的是哪个文件,因此维护了一个文件描述表。而文件描述表就是files_struct。在文件描述表里头就会有一个数组叫做struct file* fd-array[ ],也就是结构体指针数组 ,里面放着的就是我们的一个个file。
而这个数组的第0,1,2个位置就是我们的标准输入,标准输出,标准错误。
文件描述符的分配原则是把最小的还没被分配的fd分过去。但是你要是把文件描述符1号位置关了( close (1) ),那么新打开一个文件就会把这个最小的还没被分配的fd也就是1号分配过去。
8.5.2 重定向
重定向就是偷梁换柱:改变文件描述符(fd)默认指向的目标。
本来 fd 1(标准输出)默认指向屏幕。通过重定向,你让 fd 1 指向一个文件。结果就是:所有原本应该打印到屏幕的内容,都改道写进了文件。
Linux 里最常用的三种重定向(Shell 里的写法):
| Shell 写法 | 含义 | 底层操作 |
|---|---|---|
> | 输出重定向(覆盖) | close(1) + open(file, O_WRONLY|O_CREAT|O_TRUNC) |
>> | 输出重定向(追加) | close(1) + open(file, O_WRONLY|O_CREAT|O_APPEND) |
< | 输入重定向 | close(0) + open(file, O_RDONLY) |
2> | 错误重定向 | close(2) + open(file, O_WRONLY|O_CREAT|O_TRUNC) |
我们重定向操作本质就是通过close关闭某个文件描述符,再加上open打开某个文件以后分配fd来到这个位置上。
这里讲讲为什么非要close(1)做输出的重定向。因为 printf 这个函数的底层代码写死了:它永远只往 1 号文件描述符写。
程序启动时,1 号默认指向屏幕。你想让 printf 写到文件,唯一的办法就是让 1 号指向文件。那怎么让 1 号指向文件?这个时候必须先用 open 打开文件。但 open 的规则是永远分配当前最小的空闲编号。如果 1 号没被关掉,它就不是空闲的。open 就会分配 3 号给你。你拿到 3 号,但 printf 只认 1 号,所以 printf 还是往屏幕写,重定向失败。
所以,必须先把 1 号关掉,让它变成空闲状态。
然后你再 open,open 看到 1 号是空闲的,就会把新文件分配到 1 号。此时1号指向文件。printf 写 1 号,就写进了文件。
close ( 0 ) 原理也是一样的,scanf,cin等等都是默认从0号文件读的,0号文件连着我们的键盘等等输入设备。
所以重定向本质就是把某个描述符号(比如 1)原来指向的打开文件对象断开,改成指向另一个已打开的描述符(比如 3)所指向的同一个打开文件对象。
标准错误其实也是和1一样指向显示器文件。
所以我们重定向的完整写法应该是:xxx(文件名或者程序名) 1> 对应文件名
dup2:
#include <unistd.h> int dup2(int oldfd, int newfd);
oldfd —— 源文件描述符
-
含义:一个已经打开且有效的文件描述符。
-
它代表“我想要指向的那个文件/管道/socket”。
-
必须是有效的,否则
dup2直接失败返回-1,errno = EBADF。
newfd —— 目标文件描述符
-
含义:你想让它变成
oldfd的副本的那个描述符号。 -
它可以是:
-
已经打开的(比如 1、2)→
dup2会先自动关掉它,再让它指向oldfd的文件。 -
未使用的(比如 5、10)→ 直接让它指向
oldfd的文件。
-
-
它只是一个“号码”,由你指定,不像
dup那样由系统分配最小可用号。
比如:dup2(3,1)
特殊情况:oldfd == newfd
-
如果两个参数相同,
dup2什么都不做,直接返回newfd。 -
不会关闭、不会出错。这点经常被忽略。
返回值:
| 情况 | 返回 |
|---|---|
| 成功 | newfd |
| 失败 | -1,并设置 errno |
所以shell底层执行重定向大概就是这样的:
shell 执行一条带重定向的命令,比如:
ls > out.txt
底层大致是:
-
fork()出一个子进程 -
子进程里
open("out.txt", ...)拿到一个 fd -
用
dup2(fd, 1)把 stdout 重定向到该文件 -
close(fd)关掉多余的 -
exec()执行ls
8.6 文件缓冲区
为什么写了 fprintf ( fp , "hello" ) ,但如果不 fclose 或者 fflush,文件里就是空的?然后又为什么 printf 打印的东西有时候会卡住不显示?这些都是因为文件缓冲区。
文件缓冲区是内存里的一块区域。当你调用标准库函数(如 fwrite、fprintf)写数据时,数据并不会立刻进入硬盘,而是先被存进这块内存区域,等攒够了或者满足特定条件时,再一次性交给操作系统写入硬盘。它的本质是在用户态内存和内核态硬盘之间,加了一层蓄水池。
write 是系统调用,每次调用都要从用户态切换到内核态,这个切换成本很高。
假设你要写 1000 个字符,如果没有缓冲区,调用 1000 次 write,切换 1000 次内核态,CPU 累死,硬盘也累死。 但是如果有缓冲区:C 标准库在内存里搞一个 4KB 的蓄水池。你先往里倒 1000 个字符,等蓄水池满了,或者你主动要求,它才调用一次 write,把 4KB 一次性交给内核。1000 次系统调用变成了 1 次,性能提升非常大。
8.6.1 缓冲类型
C 标准库的缓冲区分为三种模式,分别适用于不同场景:
-
全缓冲(Fully Buffered)
-
适用:普通文件(硬盘文件)。
-
规则:缓冲区满了(通常是 4KB 或 8KB)才调用
write写入内核。FILE *fp = fopen("test.txt", "w"); fprintf(fp, "hello"); // 数据进了缓冲区,文件里还是空的! // 此时如果用另一个程序看 test.txt,里面什么都没有 fclose(fp); // 关闭时,缓冲区刷新,数据才真正写入硬盘
2. 行缓冲(Line Buffered)
-
适用:终端(屏幕)。
-
规则:遇到换行符
\n就刷新缓冲区,或者缓冲区满了也刷新。printf("hello"); // 没有换行,数据在缓冲区里,屏幕上不显示 printf("world\n"); // 遇到 \n,缓冲区刷新,屏幕显示 "helloworld"这就是为什么有时候 printf 没加 \n,程序还没结束,屏幕上就是不出字的原因。
3. 无缓冲(Unbuffered)
-
适用:标准错误(strerr)。
-
规则:不缓冲,直接 write 写入。
-
为什么:因为错误信息必须立刻显示,不能等缓冲区满。如果程序崩溃了,缓冲区里的错误信息丢失,你就永远不知道错在哪了。
因此缓冲区刷新条件:
printf 输出到终端时,缓冲区会在以下情况刷新:
-
遇到
\n(换行符) -
缓冲区满了
-
程序正常结束(
return或exit) -
手动调用
fflush(stdout)
也正是因为有了文件缓冲区,极大地减少了调用write的次数,每次要写入文件的时候都可以先写给缓冲区,然后通过write把缓冲区的内容写入文件。
8.6.2 缓冲区工作流程
所以我们的printf以后,先会把内容写入对应的缓冲区里,然后再通过write的底层调用再把内容写入os的内核里头。但是要注意的是,如果close把标准输出给关了,那么缓冲区的内容是写不进我们的内核里的。
对于fopen的返回值FILE*,FILE本质就是结构体,里头会包含一个缓冲区,文件一般都是在磁盘中存储,当我们打开以后,会先通过malloc或者new申请一片空间给缓冲区使用,申请成功以后通过FILE返回给我们的文件指针。缓冲区也是FILE的成员之一。
8.6.3 C/C++的缓冲区机制
printf("sss\n") vs cout << "sss" << endl
表面看:
-
C:
\n触发行缓冲 flush -
C++:
endl触发 flush
都刷一次,看起来一样。 但差别是:
1. \n 只在终端行缓冲下刷
C 的 printf:
-
stdout 指向终端 → 行缓冲 →
\n刷 -
stdout 重定向到文件 → 全缓冲 →
\n不刷
C++ 的 endl:
-
不管指向哪,都刷
-
终端也刷,文件也刷
所以 endl 是无条件 flush,\n 是有条件 flush。
2. endl 多了一次函数调用
endl 是函数(函数模板),cout << endl 相当于:
cout << '\n'; cout.flush(); // 显式调用
| 写法 | 重定向到文件时 |
|---|---|
printf("sss\n") | stdout 全缓冲,\n 不刷,攒着批量写,快 |
cout << "sss" << endl | endl 强制刷,每次都写,慢 |
所以问题不在行缓冲,在于写入文件的时候,C语言打印给显示器的时候行缓冲,写入文件哪怕有\n也是全缓冲。C++只要有endl那全是强制刷新缓冲区,不管是打印给显示器也好还是写入文件的时候也好,都是强制刷新缓冲区。在终端下也就是打印给显示器的时候是没区别的。
#include <cstdio>
#include <iostream>
using namespace std;
int main()
{
int a, b;
scanf("%d", &a); // 输入 "1 2",scanf 可能把 "1 2\n" 全读进 libc 缓冲区
cin >> b; // b 想读 2,但 2 可能已在 libc 缓冲区里,cin 去 fd 找不到 → 卡住
printf("a=%d b=%d\n", a, b);
return 0;
}
这种情况就是两者混用的情况,C++的 cin 会做一个 stdio同步,就是允许C++的 cin 或者 cout 直接读取 libc 也就是C语言的缓冲区,然后就不再需要维护自己的缓冲区了。这个过程是先 fd0 从通过特定函数指针访问到对应函数方法读取键盘输入的1,2和\n,然后把它们丢给 libc 缓冲区。然后scanf要读取了,因为 scanf 函数只读取1个数字,所以就近原则把1读了。然后到了 cin,这个时候 cin 会因为 stdio 同步,诶,就直接去读取到 libc 的缓冲区,然后把2,\n的内容读给 cin,然后解析出 2 ,并赋值或者初始化给对应变量。
但是把 cin 的 stdio同步 关了,那你整个代码肯定只能读到了1, 那2 \n都在libc缓冲区,你 cin 只能读自己的缓冲区读不了 libc 缓冲区。那你 cin 读不到东西,你自己还得多输入一次数字,进入到C++的自己的缓冲区。比如3,打印的就是a=1,b=3。中间的2其实就丢失了。
对于 cout 而言就是,cout 的时候就是先 cin 把要输出的内容写入缓冲区,然后缓冲区一步步给 os内核 然后 o s搞好给 fd1 ,fd1最后通过下层驱动等等最后给到输出设备。
对于 C++ 而言,每一次 cin 也会自动刷新一次 cout写入的缓冲区的内容。这个机制在 C++ 中被称为:流绑定(Stream Tying)。
cout << "请输入您的年龄: "; cin >> age;
这个代码没有 endl 也没有手动的 fflush 或者别的刷新机制,cout 的内容根本打印不给我们的显示器文件,我们相当于啥也看不到了。然后程序这个时候读到 cin ,又会一直等待我们的输入,从而造成一个逻辑上的 “ 假死 ” 状态。程序本身没卡住,运行正常,只是阻塞了等我们的键盘输入,但是在终端上啥也没有,用户也看不到内容。
C++的流绑定就是解决这个问题,会强制给你刷新一次 cout 缓冲区,让你看到 cout 的内容。
回到C语言,它写给终端是行缓冲,写文件是全缓冲,我们看看如下代码:
#include <cstdio>
#include <cstring>
#include <unistd.h>
int main()
{
// --- 库函数(拥有用户态缓冲区) ---
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
const char *s = "hello fwrite\n";
fwrite(s, strlen(s), 1, stdout);
// --- 系统调用(不经过用户态缓冲区) ---
const char *ss = "hello write\n";
write(1, ss, strlen(ss));
// --- 核心操作 ---
fork();
return 0;
}
然后输入指令:
# 1. 编译代码 g++ main.cpp -o a.out # 2. 运行并将标准输出重定向到 log.txt 文件 ./a.out > log.txt # 3. 查看 log.txt 文件的内容 cat log.txt
预期运行结果:
执行完上述命令后,cat log.txt 的输出应该与你图片中完全一致:
hello write hello printf hello fprintf hello fwrite hello printf hello fprintf hello fwrite
会发现不应该是只输出四个语句吗,怎么变成七个了。首先搞明白write是系统调用,不走缓冲区,所以最后会直接打印给显示器文件。然后由于 ./a.out 是行缓冲,有 \n,会一句一句的打印给显示器。但是后面我们做了一次重定向的操作,这个时候的结果就完全不一样了,因为C语言写入的时候是全缓冲。这个时候呢 ,缓冲区不会被刷新出去,所以除了write因为直接把数据写给内核,不走缓冲区,其他的三个语句都还在缓冲区内部。子进程创建后,把父进程的代码和数据也拿过来了,缓冲区内容自然也有。等到return 0的时候,把缓冲区的内容一起刷新出去。
关于 fork 拷贝父进程的事情还要再说明一下就是:操作系统只负责给子进程克隆用户态的内存,你像 write 这种系统层面的调用,os 不会给你克隆。
8.7 ”一切皆文件“ 理解
一切皆文件的本质,是 Unix/Linux 用一层统一的抽象接口,把种类完全不同的对象(普通文件、目录、设备、管道、socket、终端等)包装成同一种可被访问的形式。用户态通过 open 拿到 fd,之后一律用 read、write、ioctl、close 等系统调用操作,不需要关心背后到底是什么。
在内核态,VFS 层用 struct file 表示一个“已打开的文件”,里面持有一个 file_operations 函数指针表,表里是 read、write、open、release 等方法。VFS 接到请求后,通过这张表把调用分发给具体的实现;而每个驱动或文件系统各自提供自己的方法和私有数据结构(比如 inode、net_device、block_device),各自维护自己的属性和行为。
我们就可以通过那些函数指针调用到这些对应的读写方法等等。说白了就是内核用各种不同的结构体把硬件等对象像 task_struct 描述进程一样描述起来,里面存它们的属性,并维护函数指针表指向对应的读写方法等等,从而让进程能像操作文件一样使用它们。说白了只要让进程认为他们是文件,那用户自然也会觉得是文件了。
所以,这种统一靠的是“结构体 + 函数指针表”这种接口抽象,而不是把硬件真的塞进同一个结构体里。它让进程能像操作文件一样操作各种对象,也让内核能用同一套分发路径去处理它们。但要注意:统一的只是访问接口和分发方式,各对象在内核里的管理结构、生命周期、调度和资源核算仍然各自独立;进程管理走的是 task_struct 和调度器,和这套文件抽象是两回事。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)