跳到主要内容
Linux 基础 I/O 原理与系统调用 | 极客日志
C
Linux 基础 I/O 原理与系统调用 介绍 Linux 基础 I/O 概念,涵盖文件狭义与广义理解(一切皆文件),对比 C 标准库接口与系统调用接口。详细讲解文件描述符 fd 的分配规则、默认流(0/1/2)、重定向机制及 dup2() 系统调用。最后分析缓冲区定义、作用及刷新机制,通过现象说明用户级与内核级缓冲区的差异。
云朵棉花糖 发布于 2026/3/29 更新于 2026/5/25 34 浏览1、理解'文件'
1.1 狭义理解
文件在磁盘里 。
磁盘是永久性存储介质,因此文件在磁盘上永久性存储 。
磁盘是外设(即是输出设备也是输入设备)。
对磁盘文件的所有操作(如读取、写入)本质上都是对外设的输入/输出 ,简称I/O (Input/Output)。
1.2 广义理解
Linux 中,一切皆文件 (键盘、显示器、网卡、磁盘……这些都是抽象化的过程)。
1.3 文件操作的归类认知
文件 = 属性(元数据)+ 内容 。
对于0KB 的空文件是占用磁盘空间的 ,有文件属性。
所有的文件操作本质 是文件内容操作 和文件属性操作 。
1.4 系统角度
对文件的操作本质 是进程对文件的操作 。
磁盘 的管理者 是操作系统 。
文件的读写本质 不是通过 C 语言/C++的库函数来操作的(这些库函数只是为用户提供方便),而是通过文件相关的系统调用接口来实现 的。
文件分为**'内存级 (被打开)'文件**,'磁盘级 (未打开)'文件 。
本节主讲'内存级 (被打开)'文件 。
2、回顾 C 文件接口
2.1 文件的打开与关闭
FILE *fopen (const char *path, const char *mode) ;
mode 含义 文件不存在时 文件存在时 写入方式 "r"只读 返回 NULL 正常打开 不可写入 "r+"读写 返回 NULL 正常打开 从当前位置覆盖 "w"只写(新建) 新建文件 清空原内容 从头写入 "w+"读写(新建) 新建文件
"a"追加(只写) 新建文件 保留内容,追加到末尾 只能末尾追加
"a+"追加(读写) 新建文件 保留内容,可读/追加写入 可读,但写入仅限末尾
ls /proc/[ 进程 id] -l 命令,查看当前正在运行进程的信息。
cwd :指向进程的当前工作目录 ,创建文件和打开文件的默认路径 。
exe :指向启动当前进程的可执行文件的路径 。
2.2 文件的读写函数 函数名 功能描述 适用流类型 参数说明 返回值 备注 fgetc 从流中读取单个字符 所有输入流 (如 stdin、文件)FILE *stream(文件指针)读取的字符(int)失败返回 EOF 通常用于逐字符处理 fputc 向流写入单个字符 所有输出流 (如 stdout、文件)int char(字符)FILE *stream写入的字符(int)失败返回 EOF fgets 从流中读取一行文本 所有输入流 char *str(缓冲区)int n(最大长度)FILE *stream成功返回 str 失败返回 NULL 保留换行符 ` ` fputs 向流写入一行文本 所有输出流 const char *str(字符串)FILE *stream成功返回非负值 失败返回 EOF 不自动添加换行符 fscanf 格式化输入(类似 scanf) 所有输入流 FILE *stream const char *format(格式字符串)...(变量地址)成功匹配的参数数量 失败返回 EOF 需注意缓冲区溢出风险 fprintf 格式化输出(类似 printf) 所有输出流 FILE *stream const char *format ...(变量值)成功返回写入字符数 失败返回负值 fread 二进制输入 (块读取)文件流 void *ptr(缓冲区)size_t size(每块大小)size_t nmemb(块数)FILE *stream实际读取的块数 用于结构体等二进制数据 fwrite 二进制输出 (块写入)文件流 const void *ptr(数据地址)size_t size size_t nmemb FILE *stream实际写入的块数
写字符串,不用写 \0,因为这是 C 语言的规定,不是文件的规定,写进去会乱码。
2.3 stdin & stdout & stderr C 程序启动 ,默认打开 三个输入输出流,分别是stdin ,stdout ,stderr 。
#include <stdio.h>
extern FILE *stdin ;
extern FILE *stdout ;
extern FILE *stderr ;
3、系统文件 I/O
3.1 一种传标志位的方式 #include <stdio.h>
#define ONE (1 << 0)
#define TWO (1 << 1)
#define THREE (1 << 2)
void func (int flags) {
if (flags & ONE) printf ("flags has ONE! " );
if (flags & TWO) printf ("flags has TWO! " );
if (flags & THREE) printf ("flags has THREE! " );
printf ("\n" );
}
int main () {
func(ONE);
func(THREE);
func(ONE | TWO);
func(ONE | TWO | THREE);
return 0 ;
}
3.2 文件的系统调用接口
3.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) ;
flags :打开文件时的选项标志,可以使用以下常量通过"或"运算 (|) 组合:
O_RDONLY :只读 打开。
O_WRONLY :只写 打开。
O_RDWR :读写 打开。
O_CREAT :若文件不存在则创建 它(需要 mode 参数 ,设置新文件的访问权限 )。
O_APPEND :追加写 模式。
O_TRUNC :如果文件已存在 且为普通文件 ,打开时会将其长度截断为 0 ,逻辑上的清空 (类似与 vector 的 size)
成功 :返回新打开的文件描述符 fd (非负整数 )
失败 :返回**-1**。
那么 C 语言的 fopen 的 flag 就是:
"w" = O_CREAT | O_WRONLY | O_TRUNC;
"a" = O_CREAT | O_WRONLY | O_APPEND;
"w+" = O_CREAT | O_RDWR | O_TRUNC;
"a+" = O_CREAT | O_RDWR | O_APPEND。
3.2.2 read() & write() & close() #include <unistd.h>
ssize_t read (int fd, void *buf, size_t count) ;
#include <unistd.h>
ssize_t write (int fd, const void *buf, size_t count) ;
#include <unistd.h>
int close (int fd) ;
read() 和 write() 的 buf 都是 void*,不关心数据格式,以二进制流输入输出。
首先,底层都是二进制流的输入输出。
字符按 ASCII(或 UTF-8 等) 输入 (读出),按 ASCII(或 UTF-8 等) 输出 (写入)。对于字符设备,字符通过 ASCII(或 UTF-8 等) 转化成二进制写到里面,然后通过 ASCII(或 UTF-8 等) 解释,以字符的形式显示。
字符流的输入输出,是因为,我们输入输出的是字符串 。
3.3 库函数和系统调用 类型 示例函数 所属层级 特点 库函数 fopen, fclose, fread, fwriteC 标准库 (libc) 1. 提供更高级的抽象 2. 带缓冲区 3. 可移植性更好 4. 最终会调用系统调用 系统调用 open, close, read, write, lseek操作系统接口 1. 直接与内核交互 2. 无缓冲区 3. 效率更高但更底层 4. 与具体操作系统相关
3.4 文件描述符 fd
3.4.1 0 & 1 & 2 Linux 进程默认 情况下会有 3 个缺省打开的文件描述符 ,分别是
0 ,1 ,2 对应的物理设备 一般是:键盘 ,显示器 ,显示器
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main () {
char buf[1024 ];
ssize_t s = read(0 , buf, sizeof (buf) - 1 );
if (s > 0 ) {
buf[s] = '\0' ;
write(1 , buf, s);
write(2 , buf, s);
}
return 0 ;
}
而现在知道,文件描述符就是从 0 开始的小整数 。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了 file 结构体,表示一个已经打开的文件对象。而进程执行 open 系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针 * files ,指向一张表 files_struct ,该表最重要的部分就是包含一个指针数组 ,每个元素都是一个指向打开文件的指针 !所以,本质上,文件描述符就是该数组的下标 。所以,只要拿着文件描述符,就可以找到对应的文件。
C 语言的 stdin(fd = 0),stdout(fd = 1),stderr(fd = 2),是一个 FILE 结构体的指针,FILE 结构体里面封装了文件描述符 fd ,其他语言也一样。
3.4.2 文件描述符的分配规则 #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);
close(fd);
return 0 ;
}
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main () {
close(0 );
int fd = open("myfile" , O_RDONLY);
if (fd < 0 ) {
perror("open" );
return 1 ;
}
printf ("fd: %d\n" , fd);
close(fd);
return 0 ;
}
在 Linux 系统中,文件描述符的分配原则 :最小的 ,没有被使用 的下标 ,作为fd ,给新打开的文件 。
3.4.3 重定向 #include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main () {
close(1 );
int fd = open("myfile" , O_CREAT | O_WRONLY | O_TRUNC, 0644 );
if (fd < 0 ) {
perror("open" );
return 1 ;
}
printf ("fd: %d\n" , fd);
fflush(stdout );
close(fd);
exit (0 );
}
因为语言层只认 stdout 中的 fd = 1,此时下标为 1 的指针指向 myfile,所以
本来应该输出到显示器上的内容,输出到了 myfile 文件中。
用法是**'命令 + 重定向'**。
cat log.txt > myfile ,实际上是 cat log.txt 1>myfile ,只重定向了标准输出 ,
cat log.txt 1>myfile 2>&1 ,重定向了标准输出和标准错误 。
3.4.4 重定向系统调用 dup2() #include <unistd.h>
int dup2 (int oldfd, int newfd) ;
如:dup2(fd,0),实现输入重定向,dup2(fd,1),实现输出重定向。
所以,重定向 = 文件打开方式 + dup2() 。
4、理解一切皆文件 首先,在 Windows 中是文件的东西,它们在 Linux 中也是文件;其次一些在 Windows 中不是文件的东西,比如进程、磁盘、显示器、键盘这样的硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们获得信息;甚至管道,也是文件;将来我们要学习网络编程中的 socket(套接字)这样的东西,使用的接口跟文件接口也是一致的。
这样做最明显的好处是,开发者仅需要使用一套 API ,即可调取 Linux 系统中绝大部分的资源 。举个简单的例子,Linux 中几乎所有读(读文件,读系统状态,读 PIPE)的操作都可以用 read 函数来进行;几乎所有更改(更改文件,更改系统参数,写 PIPE)的操作都可以用 write 函数来进行。
上图中的外设,每个设备都可以有自己的 read、write,但一定是对应着不同的操作方法!!但通过 struct file 下的 struct file_operations 中的各种函数回调,让我们开发者只用 file 便可调取 Linux 系统中绝大部分的资源!!这便是 "Linux 下一切皆文件" 的核心理解。
5、缓冲区
5.1 缓冲区的定义
5.2 缓冲区的作用
5.3 缓冲区的机制
用户级语言层缓冲区,避免频繁调用系统调用 (成本高),提高 C 语言接口的效率。
文件内核缓冲区,提高系统调用的效率。
可以通过 fsync(),将文件内核缓冲区的数据刷新到硬件。
一般认为数据交给 OS,就相当于交给硬件。
现象 1: #include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main () {
close(1 );
int fd = open("log.txt" , O_CREAT | O_WRONLY | O_TRUNC, 0664 );
if (fd < 0 ) {
perror("open" );
return 1 ;
}
printf ("hello world: %d\n" , fd);
close(fd);
return 0 ;
}
这个时候,对于普通文件 ,应该是满了刷新 ,可是没满,也没有强制刷新,然后关闭了 fd,在程序退出时,刷新,但 fd 已经关闭了,刷新不了,所以 log.txt 中不会有数据。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main () {
close(1 );
int fd = open("log.txt" , O_CREAT | O_WRONLY | O_TRUNC, 0664 );
if (fd < 0 ) {
perror("open" );
return 1 ;
}
printf ("hello world: %d\n" , fd);
fflush(stdout );
close(fd);
return 0 ;
}
现象 2: #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, 1 , strlen (msg1), stdout );
write(1 , msg2, strlen (msg2));
fork();
return 0 ;
}
hello printf hello fwrite hello write
但是重定向一下 ./hello > file,结果:
hello write hello printf hello fwrite hello printf hello fwrite
重定向,改变了刷新方式 ,普通文件,满了刷新,可是没满,也没有强制刷新,程序退出时,刷新,父子进程各刷新一份。
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online