C文件/Linux内核级文件理解
Linux内核级文件
1. 回顾C文件接口
1.1 fopen
函数原型
FILE *fopen( const char *filename, const char *mode );
参数
-
filename: 文件的路径。
-
mode: 文件的打开方式。
返回值
- 成功返回
FILE*
结构体指针;失败返回NULL
。
文件打开方式 | 含义 | 如果文件不存在 |
---|---|---|
“r” | 读 | 文件不存在会报错 |
“w” | 写(清空写) | 建立一个新的文件 |
“a” | 写(追加写) | 建立一个新的文件 |
“r+” | 读写 | 文件不存在会报错 |
“w+” | 读写(清空写) | 建立一个新的文件 |
“a+” | 读写(追加写) | 建立一个新的文件 |
“rb” | 以二进制模式读 | 文件不存在会报错 |
“wb” | 以二进制模式写(清空写) | 建立一个新的文件 |
“ab” | 以二进制模式写(追加写) | 建立一个新的文件 |
“rb+” | 以二进制模式读写 | 文件不存在会报错 |
“wb+” | 以二进制模式读写(清空写) | 建立一个新的文件 |
“ab+” | 以二进制模式读写(追加写) | 建立一个新的文件 |
1.2 fclose
函数原型
int fclose( FILE *stream );
参数
- stream: 关闭的文件流。
返回值
- 成功返回 0 ;失败返回
EOF
。
1.3 fgetc
函数原型
int fgetc( FILE *stream );
参数
- stream: 读取的文件流。
返回值
- 读取成功返回该字符的
ASCII
值;读取失败或文件结束返回EOF
。
1.4 fputs
函数原型
int fputc( int c, FILE *stream );
参数
-
c: 写入的字符
-
stream: 写入的文件流。
返回值
- 写入成功返回该字符的
ASCII
值;写入失败返回EOF
。
1.5 fgets
函数原型
char *fgets( char *string, int n, FILE *stream );
参数
-
string: 存放从文件流中读取到的字符串。
-
n: 读取的字符个数,包含
'\0'
,实际读到n - 1
个字符。 -
stream: 读取的文件流。
返回值
- 读取成功返回字符串首元素地址;读取失败或文件结束返回
NULL
。
1.6 fputs
函数原型
int fputs( const char *string, FILE *stream );
参数
-
string: 写入的字符串。
-
stream: 写入的文件流。
返回值
- 写入成功返回写入的字符数;写入失败返回
EOF
。
1.7 fscanf
函数原型
int fscanf( FILE *stream, const char *format [, argument ]... );
参数
-
stream: 读取的文件流。
-
format: 格式化数据和
scanf
用法类似。
返回值
- 读取成功返回读入的参数个数;读取失败返回
EOF
。
1.8 fprintf
函数原型
int fprintf( FILE *stream, const char *format [, argument ]...);
参数
-
stream: 写入的文件流。
-
format: 格式化数据和
printf
用法类似。
返回值
- 写入成功返回实际写入的字符数;写入失败返回一个负数。
1.9 fread
函数原型
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
参数
-
buffer: 存放从文件流中读取到的字符串。
-
size: 每个数据项的大小。
-
count: 要读取的数据项数量。
-
stream: 读取的文件流。
返回值
- 读取成功返回实际读取的数据项数量(<=
count
);读取失败或到达文件结尾,返回比count
小的值。
1.10 fwrite
函数原型
size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
参数
-
buffer: 写入的数据。
-
size: 每个数据项的大小。
-
count: 写入的数据项数量。
-
stream: 写入的文件流。
返回值
- 写入成功返回实际写入的数据项数量(通常等于
count
);写入失败或到达文件结尾,返回比count
小的值。
2. 基础IO
2.1 理解文件
-
⽂件在磁盘⾥。
-
磁盘是永久性存储介质,因此⽂件在磁盘上的存储是永久性的。
-
磁盘是外设(即是输出设备也是输⼊设备)。
-
磁盘上的⽂件 本质是对⽂件的所有操作,都是对外设的输⼊和输出 简称 IO。
-
Linux 下⼀切皆⽂件(抽象化的过程)。
-
文件是文件属性和文件内容的集合,所有文件的操作本质是文件内容操作和文件属性操作。
-
对文件的操作本质是进程对文件的操作。
2.2 操作文件的系统调用
2.2.1 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
: 追加写 -
O_TRUNC
: 若文件存在,则清空文件
-
-
mode: 仅当使用
O_CREAT
标志时,用于指定新创建文件的权限。通常为0666
- 实际权限会受到进程的
umask
影响,最终权限为mode & ~umask
- 实际权限会受到进程的
返回值
- 打开文件成功返回新打开的文件描述符,打开失败返回-1。
2.2.2 close
函数原型
#include <unistd.h>
int close(int fd);
参数
- fd: 要关闭的文件描述符。
2.2.3 write
函数原型
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数
-
fd: 往该文件描述符中写。
-
buf: 数据缓冲区指针,指向要写入的数据内存地址。
-
count: 写入的字节数,表示从 buf 中写多少字节到文件。
返回值
- 写入成功返回实际写入的字节数(
ssize_t
类型,可能小于count
);失败时返回-1。
#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, 0664);if(fd < 0){perror("open");return 1;} int count = 5;const char *msg = "hello world\n";while(count--) {ssize_t s = write(fd, msg, strlen(msg));} close(fd);return 0;
}
2.2.4 read
函数原型
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
参数**
-
fd: 从该文件描述符中读。
-
buf: 数据缓冲区指针,用于存储读取到的数据。
-
count: 要读取的最大字节数(不包含
'\0'
)。
返回值
-
读取成功返回实际读取的字节数。
-
> 0
:成功读取的字节数(可能小于count) -
= 0
:表示到达文件末尾
-
-
失败时返回-1。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>int main()
{int fd = open("myfile", O_RDONLY);if(fd < 0){perror("open");return 1;} const char *msg = "hello world\n";char buf[1024];while(1) {ssize_t s = read(fd, buf, strlen(msg));if(s > 0){buf[s] = 0;printf("%s", buf);} else {break;}} close(fd);return 0;
}
2.3 文件描述符
-
内核级抽象:文件描述符是 进程与内核之间的桥梁,进程通过它操作文件,而无需关心底层细节。
-
数组索引:在 Linux 内核中,每个进程有一个 文件描述符表(File Descriptor Table),文件描述符表本质是结构体指针数组,
fd
是该表的索引,指向一个 文件对象(struct file)。 -
非负整数:从0开始分配,依次递增。
-
Linux进程默认情况下会打开三个文件描述符。
-
0
:标准输入(通常是键盘) -
1
:标准输出(通常是显示器) -
2
:标准错误(通常是显示器)
-
-
文件描述符的分配规则:在 files_struct 数组当中,找到当前没有被使⽤的最⼩的⼀个下标,作为新的⽂件描述符。
task_struct
:Linux 中代表进程的核心结构体(进程控制块),包含进程所有关键信息(状态、PID、文件描述符表指针等 )。图中用struct files_struct *files
指向该进程的文件描述表结构体。
files_struct
:管理进程打开文件的结构体,内部通过fd_array[]
(文件描述符数组)存储struct file*
指针,数组下标就是 文件描述符(如 0、1、2…)。
-
struct file
:位于 内核的全局文件表(所有进程共享)。存储文件的 打开状态(如读写位置、访问模式等)。-
关键字段:
-
f_flags
:打开文件的标志(如O_RDONLY
、O_WRONLY
)。 -
f_inode
:指向文件的 inode(实际文件数据)。 -
ref_count
:引用计数,节省空间和时间(多个进程可能共享同一个struct file
,引用计数确保资源不被提前释放)。
-
2.4 重定向
重定向 是Linux 系统中一种 改变输入/输出默认路径 的机制,允许程序从非常规位置读取和输入。
在 Linux 中,重定向通过 文件描述符(File Descriptor) 和系统调用实现
2.4.1 dup2
dup2
是 Linux 系统中用于 复制文件描述符(File Descriptor) 的核心系统调用,主要作用是 重定向输入/输出。
dup2
系统调用的核心操作是将文件描述符数组 oldfd
下标中的内容(struct file
)拷贝覆盖至fd数组 newfd
下标中。
函数原型
#include <unistd.h>
int dup2(int oldfd, int newfd);
参数
-
oldfd: 需要复制的 原文件描述符。
-
newfd: 指定的新文件描述符。
返回值
-
成功:返回新的文件描述符(值与newfd相同)。
-
失败:返回-1。
#include <unistd.h>
#include <fcntl.h>int main() {int fd = open("test.txt", O_WRONLY | O_CREAT, 0664);dup2(fd, 1); // 将 stdout(1) 重定向到文件close(fd);printf("This goes to output.txt!\n"); // 写入文件而非终端return 0;
}
2.5 标准输出和标准错误的理解
标准输出(stdout) 和 标准错误(stderr),可以通过重定向能力,把常规消息和错误消息进行分离。
-
将正常输出保存到文件,同时让错误显示在终端:
command 1> output.log 2> error.log
-
合并两者到同一文件(
2>&1
或&>
):command 1>log.txt 2>&1 command &> log.txt # Bash 简写方式(stdout + stderr 都写入 log.txt)
2.6 Linux一切皆文件
Linux 一切皆文件(Everything is a file)是 Linux 系统的一个核心设计哲学,它并不是说所有东西都真的是磁盘上的文件,而是指 系统通过统一的文件接口来访问和管理各种资源。Linux 一切皆文件本质上是通过软件层(主要是内核)在硬件和用户之间抽象出的统一接口,这种抽象使得用户和程序可以用相同的操作(如 read
、write
、open
、close
)来处理不同类型的对象,极大简化了系统交互的复杂性。
-
隐藏硬件差异:用文件接口统一访问异构资源。
-
简化交互:用户只需操作文件,无需理解硬件细节。

2.7 缓冲区
缓冲区本质上是内存中的临时存储区域,用于解决速度不匹配问题。缓冲区分为语言级缓冲区和系统内核缓冲区。在计算机系统中,不同组件的速度差异巨大:
-
CPU纳秒级
-
内存纳秒到微秒级
-
磁盘/网络毫秒级
缓冲区的核心价值:
-
减少系统调用次数(语言级缓冲区)
-
减少与I/O交互次数(内核级缓冲区)
缓冲策略:
-
全缓冲区:这种缓冲⽅式要求填满整个缓冲区后才进⾏I/O系统调⽤操作。对于磁盘⽂件的操作通常使⽤全缓冲的⽅式访问。
-
⾏缓冲区:在⾏缓冲情况下,当在输⼊和输出中遇到换⾏符时,标准I/O库函数将会执⾏系统调⽤操作。当所操作的流涉及⼀个终端(显示器)时(例如标准输⼊和标准输出),使⽤⾏缓冲⽅式。因为标准I/O库每⾏的缓冲区⻓度是固定的,所以只要填满了缓冲区,即使还没有遇到换⾏符,也会执⾏I/O系统调⽤操作,默认⾏缓冲区的⼤⼩为1024。
-
⽆缓冲区:⽆缓冲区是指标准I/O库不对字符进⾏缓存,直接调⽤系统调⽤。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显⽰出来。
除了上述列举的默认刷新⽅式,下列特殊情况也会引发缓冲区的刷新:
-
缓冲区满
-
执行flush语句
-
进程结束
-
关闭文件
当程序执行写操作时,数据通常的流动路径: 程序内存 → 语言级缓冲区 → 系统级缓冲区 → 存储设备,读操作则相反方向流动。流动本质是拷贝,将语言缓冲区的内容拷贝至系统级缓冲区,以此类推。