
目录
1. 核心概念
在开始前我们先来了解一下什么是文件IO?
在 Linux 系统中,一个基本哲学是 “Linux下一切皆文件”。这意味着不仅普通的磁盘文件是文件,设备、管道、套接字等都可以被抽象成文件,通过统一的文件 I/O 接口进行操作。
狭义上讲,指普通的文本文件,或二进制文件,包括日常所见的txt文档、源代码、word文档、压缩包、图片、视频、音频文件等:
广义上讲,除了狭义上的文件外,几乎所有可操作的设备或结构都可视为文件。包括键盘、鼠标、硬盘、串口、显示屏、触摸屏等,也包括网络通讯端口(多机通信要用到的文件)、进程间通讯管道(单机通信)等抽象概念:
2. 系统IO和标准IO对比
进行文件 I/O 的核心就是一组系统调用,它们直接与 Linux 内核交互,实现对文件的操作。对文件的操作,基本上就是输入输出,因此也一般称为IO接口:
- 在操作系统层面上:这一组专门针对文件的IO接口就被称为系统IO --- 偏向于底层(设备文件)
- 在标准库的层面上:这一组专门针对文件的IO接口就被称为标准IO --- 偏向于上层(软件程序文件)
其中系统IO控制文件的方式,是众多系统调用中专用于文件操作的一部分接口:

标准IO控制文件的方式,是众多标准函数当中专用于文件操作的一部分接口:

总结一下:标准IO实际上是对系统IO的封装,系统IO是更接近底层的接口,如果把系统IO比喻为菜市场,提供各式肉蛋奶菜,那么标准IO就是对这些基本原理的进一步封装,是品类和功能更加丰富的酒庄饭店。
| 特性维度 | 系统 I/O | 标准 I/O |
|---|---|---|
| 提供方 | 操作系统(Linux 系统调用) | C 运行库(遵循 ANSI C 标准) |
| 接口层级 | 底层,直接进入内核 | 高层,建立在文件 I/O 之上 |
| 核心概念 | 文件描述符,一个整数(如 0, 1, 2) | 文件流,FILE* 结构体指针 |
| 缓冲机制 | 无缓冲或内核缓冲,用户无感知 | 用户态缓冲(全缓冲、行缓冲、无缓冲) |
| 主要函数 | open, read, write, lseek, close | fopen, fread, fwrite, fprintf, fscanf, fclose |
| 性能特点 | 每次调用都触发系统调用,上下文切换开销大 | 通过缓冲区减少系统调用次数,效率通常更高 |
| 控制粒度 | 精细,可控制每一次读写的确切位置和方式 | 较粗,操作单位是流,定位不如 lseek 精确 |
| 可移植性 | 依赖于操作系统,不同 Unix 系统可能略有差异 | 跨平台性好,遵循 C 标准,代码无需修改 |
| 适用场景 | 设备文件、管道、套接字、需要精确控制(如数据库) | 普通磁盘文件、文本处理、格式化 I/O |
这里补充两个概念:文件描述符和文件流、以及缓冲机制。
文件描述符vs文件流
文件描述符:是进程文件描述符表的索引,内核通过它来找到对应的文件。它是非负整数。
文件流:是 C 库定义的 FILE 结构体指针。这个结构体内部封装了一个文件描述符,以及一个 I/O 缓冲区和其他状态信息。
缓冲机制
系统 I/O:每次 read/write 都是一次系统调用,需要从用户态切换到内核态,开销较大。虽然内核也有自己的页面缓存,但系统调用的开销无法避免。
标准 I/O:在用户空间维护一个缓冲区。
- 全缓冲:当缓冲区满时,才进行一次实际的 I/O 操作(系统调用)。常用于磁盘文件。
- 行缓冲:遇到换行符 \n 或缓冲区满时,进行 I/O 操作。常用于标准输入/输出。
- 无缓冲:立即输出。常用于标准错误 stderr。
举个例子,假如要写入 1000 字节,每次 1 字节。
系统IO:
使用 write:会进行 1000 次 系统调用。
标准IO
使用 fputc(假设缓冲区 1024 字节):前 1024 次调用都只是在填充用户缓冲区,0 次系统调用。只有当缓冲区满或调用 fflush 时,才进行 1 次 write 系统调用。
补充解释,通过例子解释一下区别:
解释一下系统调用(系统IO)和库函数(标准IO)不同的叫法但是表示的东西是一样的。
假如你想要去银行取钱
你的应用程序 = 你本人
用户空间 = 银行大厅
内核空间 = 银行金库和核心业务系统
库函数 = 银行大堂经理或ATM机
系统调用 = 进入金库的授权指令
此时你想要去取100块钱
方法一:
- 你走到金库门口(用户态到内核态的边界)。
- 你大喊一声系统调用指令:sys_open("我的保险箱")(触发软中断,进入内核态)。
- 金库保安(内核)验证你的身份,打开金库门。
- 你进去找到你的保险箱,再喊:sys_read(保险箱, 余额信息)。
- 你查看余额,然后喊:sys_write(保险箱, 取出100, 余额更新)。
- 你拿着100块钱,走出金库(返回用户态)。
- 整个过程繁琐、高风险(你自己操作)、效率低(每次进出金库都要严格检查)。
方法二:
- 你走到ATM机(库函数,如printf或自定义函数)前。
- 你插入银行卡,输入密码,点击“取款100元”(调用库函数)。
- ATM机(库函数内部)帮你完成了一系列复杂操作:它可能先检查自己的钱箱够不够(用户空间逻辑)。然后,它代替你去与银行核心系统(内核)交互,执行了上述所有sys_open, sys_read, sys_write等系统调用。它还可能为你提供了交易凭条(格式化输出)。你直接从ATM出钞口拿到100块钱。
- 整个过程简单、安全、高效,因为你不需要关心底层细节,而且ATM机可能还有缓存,取小额钱甚至不需要每次都联系核心系统。
3. 标准IO
为了方便演示,我们创建一个file_io的文件进行后续操作,首先打开文件夹:

新建一个文件,命名file_io(随便命名,没影响),完成后点击Create,然后点击上方open:

对于这里不明白可自行参考,下方链接VScode部分:
Linux应用开发·如何在Linux上安装VScode、gcc编译流程以及如何进行静态/动态链接的打包使用·详细步骤演示-优快云博客
3.1 打开文件(fopen)
开始前我们先来了解一下其功能:
| 功能 | 获取指定文件的文件指针 | |
| 头文件 | #include <stdio.h> | |
| 原型 | FILE *fopen (const char *__restrict __filename, const char *__restrict __modes) | |
| 参数 | __restrict __filename | 即将要打开的文件 |
| __restrict __modes | “r”:以只读方式打开文件,要求文件必须存在。 | |
| "r+":以读写方式打开文件,要求文件必须存在。 | ||
| "w" :以只写方式打开文件,文件如果不存在将会创建新文件,如果存在将会将其内容清空。 | ||
| "w+" :以读写方式打开文件,文件如果不存在将会创建新文件,如果存在将会将其内容清空。 | ||
| "a":以只写方式打开文件,文件如果不存在将会创建新文件,且文件位置偏移量被自动定位到文件末尾(即以追加方式写数据)。 | ||
| "a+" :以读写方式打开文件,文件如果不存在将会创建新文件,且文件位置偏移量被自动定位到文件末尾(即以追加方式写数据)。 | ||
| 返回值 | 成功 | 文件指针 |
| 失败 | NULL | |
| 备注 | 备注 | 总共6种打开模式,不能自创别的模式,比如"rw"是非法的 |

那么我们来演示一下,在刚刚打开的文件内新建文件:

创建一个.c文件:

编写代码,作用是读io.txt文件,如果为空则打开失败,如果不为空则打开成功:
#include<stdio.h>
int main(int argc, char const *argv[])
{
/**
* char *__restrict __filename:字符串表示要打开的文件名称
* char *__restrict __modes:访问模式
* (1)r: 只读模式,如果没有文件报错
* (2)w: 只写模式,如果文件存在清空文件,如果不存在创建新文件
* (3)a: 只追加模式,如果文件存在末尾追加写,没如果不存在创建新文件
* (4)r+: 读写模式,文件必须存在,写入是从头一个一个覆盖
* (5)w+: 读写模式,如果文件存在清空文件,如果文件不存在创建新文件
* (6)a+: 读追加写模式,如果文件存在末尾追加写,如果不存在创建新文件
* return: FILE * 结构体指针,表示一个文件
* 报错返回NULL
* extern FILE *fopen (const char *__restrict __filename,
const char *__restrict __modes)
*/
char *filename = "io.txt";
FILE * ioFile = fopen(filename,"r");
if(ioFile == NULL)
{
printf("failed 打开文件失败\n");
}
else
{
printf("success 打卡文件成功\n");
}
return 0;
}
测试为了方便演示还没有创建io.txt文件。
然后创建一个Makefile文件,文件内容:
CC:=gcc
fopen_test:fopen_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
对于这段代码我们分析一下:
定义编译器为 gcc,你可以将其理解为一个宏定义,方便后续更改:
CC:=gcc
例如,我们现在使用命令:
gcc -o test1 test1.c
gcc -o test2 test2.c
gcc -o test3 test3.c
gcc -o test4 test4.c
gcc -o test5 test5.c
gcc -o test6 test6.c
后面不想使用gcc编译器了,这样后续修改就会非常麻烦。
然后编译程序:
-$(CC) -o $@ $^
//等效于
gcc -o fopen_test fopen_test.c
- $@ = 目标文件名 (fopen_test)
- $^ = 所有依赖文件 (fopen_test.c)
运行编译好的程序,并且根据上述描述,可以拆分为:
-./$@
//等效于
./fopen_test
删除可执行文件:
-rm ./$@
//等效于
rm ./fopen_test
整理一下:
CC:=gcc
fopen_test:fopen_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
# 也就是说上述三条可以替换为
# gcc -o fopen_test fopen_test.c
# ./fopen_test
# rm ./fopen_test

打开终端make一下看看:

对于Makefile的使用,可以参考:
我们可以看到发生了报错,那是因为我们不能打开一个不存在的文件,我们可以使用一下别的命令,例如将r改为w,记得保存一下:
#include<stdio.h>
int main(int argc, char const *argv[])
{
/**
* char *__restrict __filename:字符串表示要打开的文件名称
* char *__restrict __modes:访问模式
* (1)r: 只读模式,如果没有文件报错
* (2)w: 只写模式,如果文件存在清空文件,如果不存在创建新文件
* (3)a: 只追加模式,如果文件存在末尾追加写,没如果不存在创建新文件
* (4)r+: 读写模式,文件必须存在,写入是从头一个一个覆盖
* (5)w+: 读写模式,如果文件存在清空文件,如果文件不存在创建新文件
* (6)a+: 读追加写模式,如果文件存在末尾追加写,如果不存在创建新文件
* return: FILE * 结构体指针,表示一个文件
* 报错返回NULL
* extern FILE *fopen (const char *__restrict __filename,
const char *__restrict __modes)
*/
char *filename = "io.txt";
FILE * ioFile = fopen(filename,"w");
if(ioFile == NULL)
{
printf("failed 打开文件失败\n");
}
else
{
printf("success 打卡文件成功\n");
}
return 0;
}

在make一下可以看到,显示打开成功,并且新创建了一个io.txt文件:

3.2 关闭文件(fclose)
了解一下功能:
| 功能 | 关闭指定的文件并释放其文件 | |
| 头文件 | #include <stdio.h> | |
| 原型 | int fclose (FILE *__stream); | |
| 参数 | FILE *__stream | 即将要关闭的文件 |
| 返回值 | 成功 | 0 |
| 失败 | EOF(负数) 通常关闭文件失败会直接报错 | |
| 备注 | fclose函数涉及内存释放,不可对同一个文件多次关闭 | |
然后创建一个文件,编写代码:
#include<stdio.h>
int main(int argc, char const *argv[])
{
char *filename = "io.txt";
FILE * ioFile = fopen(filename,"w");
if(ioFile == NULL)
{
printf("failed 打开文件失败\n");
}
else
{
printf("success 打卡文件成功\n");
}
int result = fclose(ioFile);
if(result == EOF)
{
printf("关闭文件失败\n");
}
else if(result == 0)
{
printf("关闭文件成功\n");
}
return 0;
}

对Makefile添加关闭相关的代码:
CC:=gcc
fopen_test:fopen_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
# 也就是说上述三条可以替换为
# gcc -o fopen_test fopen_test.c
# ./fopen_test
# rm ./fopen_test
fclose_test:fclose_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
运行一下看看:

当然这里你不想来回切换输命令,可以下载一个插件:

下载完这个插件后,Makefile的每条target上方都出现执行按钮,点击即可运行:

3.3 写入字节(fputc)
先来了解一下功能:
| 功能 | 将一个字符写入到一个指定的文件 | |
| 头文件 | #include <stdio.h> | |
| 原型 | int fputc (int __c, FILE *__stream); | |
| 参数 | int __c | 要写入的字符,ASCII码对应的char |
| FILE *__stream | 写入的文件指针,要打开的一个文件 | |
| 返回值 | 成功 | 写入到的字符,返回char |
| 失败 | EOF | |
| 备注 | 无 | |
创建文件编写代码:
#include<stdio.h>
int main(int argc, char const *argv[])
{
/**
* char *__restrict __filename:字符串表示要打开的文件名称
* char *__restrict __modes:访问模式
* (1)r: 只读模式,如果没有文件报错
* (2)w: 只写模式,如果文件存在清空文件,如果不存在创建新文件
* (3)a: 只追加模式,如果文件存在末尾追加写,没如果不存在创建新文件
* (4)r+: 读写模式,文件必须存在,写入是从头一个一个覆盖
* (5)w+: 读写模式,如果文件存在清空文件,如果文件不存在创建新文件
* (6)a+: 读追加写模式,如果文件存在末尾追加写,如果不存在创建新文件
* return: FILE * 结构体指针,表示一个文件
* 报错返回NULL
* extern FILE *fopen (const char *__restrict __filename,
const char *__restrict __modes)
*/
char *filename = "io.txt";
FILE * ioFile = fopen(filename,"w");
if(ioFile == NULL)
{
printf("failed 打开文件失败\n");
}
else
{
printf("success 打开文件成功\n");
}
/**
* int __c: ASCII码对应的char
* FILE *__stream: 打开的一个文件
* return: 成功返回char,失败返回EOF
* int fputc (int __c, FILE *__stream);
*/
int put_result = fputc(97,ioFile);
if(put_result == EOF)
{
printf("写入文件失败\n");
}
else
{
printf("写入文件成功\n");
}
/**
* FILE *__stream: 即将要关闭的文件
* return: 成功0, 失败EOF(负数),通常关闭文件失败会直接报错
* int fclose (FILE *__stream);
*/
int result = fclose(ioFile);
if(result == EOF)
{
printf("关闭文件失败\n");
}
else if(result == 0)
{
printf("关闭文件成功\n");
}
return 0;
}
然后对Makefile进行修改:
CC:=gcc
fopen_test:fopen_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
# 也就是说上述三条可以替换为
# gcc -o fopen_test fopen_test.c
# ./fopen_test
# rm ./fopen_test
fclose_test:fclose_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
fputc_test:fputc_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
运行一下看看:

可以发现在io.txt当中写入了a:

3.4 写入字符串(fputs)
同样的了解一下功能:
| 功能 | 将数据写入到一个指定的文件 | |
| 头文件 | #include <stdio.h> | |
| 原型 | int fputs (const char *__restrict __s, FILE *__restrict __stream); | |
| 参数 | const char *__restrict __s | 要写入的字符串 |
| FILE *__restrict __stream | 需要写入的文件 | |
| 返回值 | 成功 | 返回非负整数(0,1) |
| 失败 | EOF | |
| 备注 | 无 | |
非常简单的代码,其他的继续复制粘贴,补充字符串的写入代码:
#include<stdio.h>
int main(int argc, char const *argv[])
{
/**
* char *__restrict __filename:字符串表示要打开的文件名称
* char *__restrict __modes:访问模式
* (1)r: 只读模式,如果没有文件报错
* (2)w: 只写模式,如果文件存在清空文件,如果不存在创建新文件
* (3)a: 只追加模式,如果文件存在末尾追加写,没如果不存在创建新文件
* (4)r+: 读写模式,文件必须存在,写入是从头一个一个覆盖
* (5)w+: 读写模式,如果文件存在清空文件,如果文件不存在创建新文件
* (6)a+: 读追加写模式,如果文件存在末尾追加写,如果不存在创建新文件
* return: FILE * 结构体指针,表示一个文件
* 报错返回NULL
* extern FILE *fopen (const char *__restrict __filename,
const char *__restrict __modes)
*/
char *filename = "io.txt";
FILE * ioFile = fopen(filename,"w");
if(ioFile == NULL)
{
printf("failed 打开文件失败\n");
}
else
{
printf("success 打开文件成功\n");
}
/**
* const char *__restrict __s: 要写入的字符串
* FILE *__restrict __stream: 需要写入的文件
* return: 成功返回非负整数(0,1),失败返回EOF
* int fputs (const char *__restrict __s, FILE *__restrict __stream);
*/
int putsR = fputs(" love letter\n",ioFile);
if(putsR == EOF)
{
printf("写入字符串失败\n");
}
else
{
printf("写入字符串%d成功\n",putsR);
}
/**
* FILE *__stream: 即将要关闭的文件
* return: 成功0, 失败EOF(负数),通常关闭文件失败会直接报错
* int fclose (FILE *__stream);
*/
int result = fclose(ioFile);
if(result == EOF)
{
printf("关闭文件失败\n");
}
else if(result == 0)
{
printf("关闭文件成功\n");
}
return 0;
}
来到Makefile,同样的方法修改:
CC:=gcc
fopen_test:fopen_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
# 也就是说上述三条可以替换为
# gcc -o fopen_test fopen_test.c
# ./fopen_test
# rm ./fopen_test
fclose_test:fclose_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
fputc_test:fputc_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
fputs_test:fputs_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
测试一下:

可以看到数据写入:

如果我们不想覆盖就可以将打开文件的权限有w换成a,进行追加:

3.5 格式化写入(fprintf)
都是非常简单的用法:
| 功能 | 将格式化数据写入到一个指定的文件或内存 | |
| 头文件 | #include <stdio.h> | |
| 原型 | int fprintf (FILE *__restrict __stream, const char *__restrict __fmt, ...) | |
| 参数 | FILE *__restrict __stream | 打开的文件 |
| const char *__restrict __fmt | 带格式化的长字符串 | |
| ... | 可变参数,填入格式化的长字符串 | |
| 返回值 | 成功 | 返回写入的字符串长度,也就是字符的个数,不包含换行符 |
| 失败 | EOF | |
| 备注 | 无 | |
编写代码,其他的复制fclose的代码,然后添加fprintf的代码:
#include<stdio.h>
int main(int argc, char const *argv[])
{
/**
* char *__restrict __filename:字符串表示要打开的文件名称
* char *__restrict __modes:访问模式
* (1)r: 只读模式,如果没有文件报错
* (2)w: 只写模式,如果文件存在清空文件,如果不存在创建新文件
* (3)a: 只追加模式,如果文件存在末尾追加写,没如果不存在创建新文件
* (4)r+: 读写模式,文件必须存在,写入是从头一个一个覆盖
* (5)w+: 读写模式,如果文件存在清空文件,如果文件不存在创建新文件
* (6)a+: 读追加写模式,如果文件存在末尾追加写,如果不存在创建新文件
* return: FILE * 结构体指针,表示一个文件
* 报错返回NULL
* extern FILE *fopen (const char *__restrict __filename,
const char *__restrict __modes)
*/
char *filename = "io.txt";
FILE * ioFile = fopen(filename,"w");
if(ioFile == NULL)
{
printf("failed 打开文件失败\n");
}
else
{
printf("success 打开文件成功\n");
}
/**
* FILE *__restrict __stream: 打开的文件
* const char *__restrict __fmt: 带格式化的长字符串
* ...可变参数: 填入格式化的长字符串
* return:成功,返回写入的字符串长度,也就是字符的个数,不包含换行符
* 失败,返回EOF
* int fprintf (FILE *__restrict __stream, const char *__restrict __fmt, ...)
*/
char *name = "翠花";
int printfR = fprintf(ioFile,"能和我一起去狗熊岭吗?\n\t\t %s",name);
if(printfR == EOF)
{
printf("字符串写入失败\n");
}
else
{
printf("字符串%d写入成功\n",printfR);
}
/**
* FILE *__stream: 即将要关闭的文件
* return: 成功0, 失败EOF(负数),通常关闭文件失败会直接报错
* int fclose (FILE *__stream);
*/
int result = fclose(ioFile);
if(result == EOF)
{
printf("关闭文件失败\n");
}
else if(result == 0)
{
printf("关闭文件成功\n");
}
return 0;
}
添加Makefile的代码:
CC:=gcc
fopen_test:fopen_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
# 也就是说上述三条可以替换为
# gcc -o fopen_test fopen_test.c
# ./fopen_test
# rm ./fopen_test
fclose_test:fclose_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
fputc_test:fputc_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
fputs_test:fputs_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
fprintf_test:fprintf_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
运行完如下:

3.6 读取字节(fgetc)
描述一下功能:
| 功能 | 读取文件内容 | |
| 头文件 | #include <stdio.h> | |
| 原型 | int fgetc (FILE *__stream); | |
| 参数 | FILE *__stream | 要打开的文件 |
| 返回值 | 成功 | 读取到的一个字节 |
| 失败或者到文件的末尾 | EOF | |
| 备注 | 注意文件需要有读权限 | |
运用非常简单,就一句,顺便打印一下:
char c = fgetc(ioFile);
printf("%c\n",c);
完整的函数,注释参考之前任意一个的,都一样:
#include<stdio.h>
int main(int argc, char const *argv[])
{
//打开文件
FILE * ioFile = fopen("io.txt","r");
if(ioFile == NULL)
{
printf("failed 打开文件失败\n");
}
else
{
printf("success 打开文件成功\n");
}
//读取文件的内容
char c = fgetc(ioFile);
printf("%c\n",c);
//关闭文件
int result = fclose(ioFile);
if(result == EOF)
{
printf("关闭文件失败\n");
}
else if(result == 0)
{
printf("关闭文件成功\n");
}
return 0;
}
来到Makefile去添加相关代码:
CC:=gcc
fopen_test:fopen_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
# 也就是说上述三条可以替换为
# gcc -o fopen_test fopen_test.c
# ./fopen_test
# rm ./fopen_test
fclose_test:fclose_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
fputc_test:fputc_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
fputs_test:fputs_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
fprintf_test:fprintf_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
fgetc_test:fgetc_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
注意我们知道在C语言当中,由于我使用的是UTF-8格式的文字输出,因此一个汉字会占3个字节(少数生僻字是4字节),而我们上面也提到了,读取的是一个字节,因此我们此时打印输出会显示三分之一的字节码,因此会显示乱码:

那么如何输出汉字呢?我们可以使用循环来实现,将fgetc文件进行修改,我们知道如果运行失败或者读到文件末尾都会打印EOF,我们进行判断如果不等于EOF那就移植打印,直到跳出循环:
char c = fgetc(ioFile);
while(c != EOF)
{
printf("%c",c);
c = fgetc(ioFile);
}
printf("\n");
此时我们在运行一下看看:

3.7 读取字符串(fgets)
总结一下函数功能:
| 功能 | 从指定文件读取数据 | |
| 头文件 | #include <stdio.h> | |
| 原型 | char * fgets (char *__restrict __s, int __n, FILE *__restrict __stream) | |
| 参数 | char *__restrict __s | 自定义缓冲区指针,用来接收读取到的字符串 |
| int __n | 自定义缓冲区大小,接收数据的长度 | |
| FILE *__restrict __stream | 打开要读的文件 | |
| 返回值 | 成功 | 自定义缓冲区指针 |
| 失败 | NULL | |
| 备注 | 当返回值为NULL时,文件可能已达到末尾,或者错误 | |
继续重新创建一个文件fgets_test.c,然后编写代码,由于函数读到文件末尾,或者失败就会返回NULL,那么我们可以使用while循环来,若是为NULL直接跳出循环:
char buffer[100];
while (fgets(buffer,sizeof(buffer),ioFile))
{
printf("%s",buffer);
}
printf("\n");
完整代码:
#include<stdio.h>
int main(int argc, char const *argv[])
{
//打开文件
FILE * ioFile = fopen("io.txt","r");
if(ioFile == NULL)
{
printf("failed 打开文件失败\n");
}
else
{
printf("success 打开文件成功\n");
}
char buffer[100];
while (fgets(buffer,sizeof(buffer),ioFile))
{
printf("%s",buffer);
}
printf("\n");
//关闭文件
int result = fclose(ioFile);
if(result == EOF)
{
printf("关闭文件失败\n");
}
else if(result == 0)
{
printf("关闭文件成功\n");
}
return 0;
}
将Makefile就行添加fgets_test.c相关:
CC:=gcc
fopen_test:fopen_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
# 也就是说上述三条可以替换为
# gcc -o fopen_test fopen_test.c
# ./fopen_test
# rm ./fopen_test
fclose_test:fclose_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
fputc_test:fputc_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
fputs_test:fputs_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
fprintf_test:fprintf_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
fgetc_test:fgetc_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
fgets_test:fgets_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
运行一下:

3.8 格式化读取(fscanf)
先了解一下功能:
| 功能 | 从指定文件读取格式化数据 | |
| 头文件 | #include <stdio.h> | |
| 原型 | int fscanf (FILE *__restrict __stream,const char *__restrict __format, ...) | |
| 参数 | FILE *__restrict __stream | 要读的文件 |
| const char *__restrict __format | 带有格式化的字符串(固定格式接收) | |
| ... | 可变参数,填写格式化的字符串(接收数据提前声明的变量) | |
| 返回值 | 成功 | 成功匹配到的参数个数 |
| 失败 | NULL | |
| 备注 | 报错或者文件结束EOF | |
开始前先创建一个user.txt文件:

然后创建一个fscanf_test.c文件用于编写代码,根据我们创建的txt文件,我们创建三个变量,将读取到的数据存储到这三个变量当中,进行打印:
char name[50];
int age;
char wife[50];
int scanfR = fscanf(ioFile,"%s %d %s",name,&age,wife);
if(scanfR != EOF)
{
printf("成功匹配到的参数%d\n",scanfR);
printf("%s在%d岁的时候爱上了%s\n",name,age,wife);
}
来到Makefile进行代码更改:
CC:=gcc
fopen_test:fopen_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
# 也就是说上述三条可以替换为
# gcc -o fopen_test fopen_test.c
# ./fopen_test
# rm ./fopen_test
fclose_test:fclose_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
fputc_test:fputc_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
fputs_test:fputs_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
fprintf_test:fprintf_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
fgetc_test:fgetc_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
fgets_test:fgets_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
fscanf_test:fscanf_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
运行一下看看,可以发现正常打印,不过只打印了一行:

我们通过while循环进行实现:
char name[50];
int age;
char wife[50];
int scanfR;
while((scanfR = fscanf(ioFile,"%s %d %s",name,&age,wife)) != EOF)
{
printf("成功匹配到的参数%d\n",scanfR);
printf("%s在%d岁的时候爱上了%s\n",name,age,wife);
}
可以发现能够正常打印:

我们现在来看打印的最后两句,首先是但对于第四行因为是空的,所以打印数据的时候会跳过,没有匹配上数据,而张三匹配上一个数据,后面年龄和配偶没有,并且这里的数据是覆盖写入的,没有就不会写入,而张三是有数据的,因此就会显示张三(将梁山伯覆盖掉)在16岁爱上祝英台(上一次的数据,又有张三这里没有数据,因此未被覆盖掉):

4. 系统IO
系统调用是操作系统内核向用户空间应用程序提供的最小、最底层的接口。它是用户程序主动从用户态(User Space)切换到内核态(Kernel Space)的一种方式,目的是请求操作系统内核为其提供服务,例如操作硬件、管理进程、进行文件IO等。
4.1 打开文件(open)
了解一下功能:
| 功能 | 打开文件 | ||||
| 头文件 | #include<unistd.h> #include<fcntl.h> | ||||
| 原型 | int open (const char *__path, int __oflag, ...) | ||||
| 参数 | const char *__path | 打开文件的路径 | |||
| int __oflag | 打开文件的模式 | O_RDONLY | 只读模式 | 三者互斥 | |
| O_WRONLY | 只写模式 | ||||
| O_RDWR | 读写模式 | ||||
| O_CREAT | 如果文件不存在,则创建文件 | ||||
| O_EXCL | 如果使用O_CREAT选项且文件存在,则返回错误消息 | ||||
| O_NOCTTY | 当文件为中断时,阻止该终端成为进程的控制终端 | ||||
| O_TRUNC | 如果文件已经存在,则删除文件中原有数据 | ||||
| O_APPEND | 追加写模式 | ||||
| ... | 可变参数,在使用O_CREAT创建文件的权限的时候会用到(这里需要参考文件权限怎么计算rwx) 例如:0664 (八进制数据,0作为开头,rw-rw-r--) | ||||
| 返回值 | 成功 | 文件描述符 | |||
| 失败 | -1,同时设置全局变量error表示对应的错误 | ||||
文件权限如何计算,如果不熟悉可以参考:
Linux命令进阶·如何修改文件和文件夹权限(chomd命令的使用)、如何修改用户和用户组(chown命令的使用)_linux更改文件所属用户和权限-优快云博客
创建一个open_test.c文件,编写代码,读hello.txt文件,如果不存在则创建:
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
int main(int argc, char const *argv[])
{
int fd = open("hello.txt",O_RDONLY | O_CREAT,0664);
if(fd == -1)
{
printf("打开文件失败\n");
}
return 0;
}
来到Makefile文件添加代码:
open_test:open_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
运行一下看一下,可以看到成功创建:

看一下权限:

可以看到按照我们给的权限创建的,我们将hello.txt删除,更改一下权限:
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
int main(int argc, char const *argv[])
{
int fd = open("hello.txt",O_RDONLY | O_CREAT,0775);
if(fd == -1)
{
printf("打开文件失败\n");
}
return 0;
}

运行完我们发现权限发生更改了:

这里需要注意一点,Linux操作系统有文件权限保护,默认创建的文件会被删除掉其他用户的写权限。
拓展,了解一下:
对于上述解释我们就需要引入一个核心概念:umask(用户文件创建掩码)。
umask 是一个权限掩码,它像一个过滤器,用来“剥夺”或“屏蔽”掉新创建文件和目录的某些权限,以增强安全性。
实际权限的计算公式:
- 最终权限 = 默认完整权限 - umask权限
- 更准确的计算方式是进行位运算:最终权限 = 默认完整权限 & (~umask)
通常情况下,普通用户的默认 umask 通常是 0002,root 用户的默认 umask 通常是 0022。如果不知道可以通过umask命令查看:

例如,我们现在创建一个新文件 test.txt,文件默认权限: 666 (-rw-rw-rw-)
我们创建一个0666的权限的文件:

运行后可以发现权限被去掉写权限也就是 0666-0002=0664 的权限:

4.2 读取文件(read)
这里其实使用方面和上面标准IO大同小异,这里先解释一下功能,下面整体举一个例子:
| 功能 | 从指定文件读取数据 | |
| 头文件 | #include<unistd.h> #include<fcntl.h> | |
| 原型 | ssize_t read (int __fd, void *__buf, size_t __nbytes) | |
| 参数 | int __fd | 一个整数表示要从读取数据的文件描述符 |
| void *__buf | 一个指向缓冲区,读取的数据将被存放到这个缓冲区中 | |
| size_t __nbytes | 一个size_t类型的整数,表示要读取的最大字节数,系统调用将尝试读取最多这么多字节的数据,但是实际读取的字节数可能会少于请求的数量 | |
| 返回值 | 成功 | 返回实际读取的字节数,这个值可能小于nbytes,如果遇到文件结尾(EOF)或者因为网络读取等原因提前结束读取 |
| 失败 | -1 | |
| 备注 | 无 | |
4.3 写入文件(write)
解释一下功能:
| 功能 | 从指定文件写入数据 | |
| 头文件 | #include<unistd.h> #include<fcntl.h> | |
| 原型 | ssize_t write (int __fd, const void *__buf, size_t __n) | |
| 参数 | int __fd | 一个整数,表示要写入数据的文件描述符 |
| const void *__buf | 一个指向缓冲区,写入的数据需要先存放到这个缓冲区中 | |
| size_t __n | 一个size_t类型的整数,表示要写入的字节数,函数会尝试写入_n个字节的数据,但是实际写入的字节数可能会少于请求的数据量 | |
| 返回值 | 成功 | 返回实际写入的字节数,这个值可能小于_n,如果写入操作因故提前结束,如:磁盘满、网络阻塞等情况 |
| 失败 | -1 | |
| 备注 | 无 | |
4.4 关闭文件(close)
解释一下功能:
| 功能 | 关闭指定的文件 | |
| 头文件 | #include<unistd.h> #include<fcntl.h> | |
| 原型 | int close (int __fd); | |
| 参数 | int __fd | 即将要关闭的文件 |
| 返回值 | 成功 | 0 |
| 失败 | -1 | |
| 备注 | 无 | |
4.5 组合使用
首先我们先创建一个.c文件,如system_test.c文件,然后引入头文件:
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
然后创建主函数,先打开一个文件,这里我们打开之前创建的io.txt文件,通过if语句判断,我们知道如果成功打开文件返回文件描述符(这个下面会详细介绍),失败返回-1,文件打开失败我们通过exit退出:
int fd = open("io.txt", O_RDONLY);
if(fd == -1)
{
printf("打开文件失败\n");
exit(EXIT_FAILURE);
}
其中对于 exit() 函数和 _exit() 函数:
- exit():标准库函数,属于 ISO C 标准,调用通过 atexit() 注册的函数(按注册的相反顺序),刷新所有打开的 stdio 流缓冲区,关闭所有打开的 stdio 流,删除 tmpfile() 创建的临时文件。
- _exit():系统调用,属于 POSIX 标准,直接调用内核终止进程,立即终止进程,不执行任何清理操作,不刷新 stdio 缓冲区,不调用 atexit() 注册的函数。
/* We define these the same for all machines.
Changes from this to the outside world should be done in `_exit'. */
#define EXIT_FAILURE 1 /* Failing exit status. */
#define EXIT_SUCCESS 0 /* Successful exit status. */
然后创建一个数组缓冲区用于存放读取到的数据,然后创建一个变量,用于接收读取到的字节数,通过while预计判断,当读取到的自己数大于0的时候,循环写入:
char buffer[1024];
ssize_t bytes_read;
while ((bytes_read = read(fd, buffer, sizeof(buffer))) > 0)
{
// 使用实际读取的字节数,而不是缓冲区大小
write(STDOUT_FILENO, buffer, bytes_read);
}
检查时候成功并退出:
// 检查读取错误应该在循环结束后
if(bytes_read == -1)
{
printf("读取文件出错\n");
close(fd);
exit(EXIT_FAILURE);
}
close(fd);
完整函数:
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
int main(int argc, char const *argv[])
{
int fd = open("io.txt", O_RDONLY);
if(fd == -1)
{
printf("打开文件失败\n");
exit(EXIT_FAILURE);
}
char buffer[1024];
ssize_t bytes_read;
while ((bytes_read = read(fd, buffer, sizeof(buffer))) > 0)
{
// 使用实际读取的字节数,而不是缓冲区大小
write(STDOUT_FILENO, buffer, bytes_read);
}
// 检查读取错误应该在循环结束后
if(bytes_read == -1)
{
printf("读取文件出错\n");
close(fd);
exit(EXIT_FAILURE);
}
close(fd);
return 0;
}
然后我们来到 Makefile 添加编译:
system_test:system_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
运行一下:

5. 文件描述符
在Linux系统中,当我们打开或创建一个文件(或套接字)时,操作系统会提供一个文件描述符(File Descriptor,FD) ,其是Linux/Unix系统中用于访问输入/输出资源的抽象句柄,它是一个非负整数,在进程的文件描述符表中作为索引。
每个进程默认打开三个文件描述符:
| FD | 名称 | 标准IO流 | 用途 | 默认设备 |
|---|---|---|---|---|
| 0 | STDIN_FILENO | stdin | 标准输入 | 键盘 |
| 1 | STDOUT_FILENO | stdout | 标准输出 | 显示器 |
| 2 | STDERR_FILENO | stderr | 标准错误输出 | 显示器 |
进程级别的三层结构:
进程任务结构
↓
文件描述符表 (File Descriptor Table)
↓
文件表 (File Table) - 包含文件状态标志、当前偏移量等
↓
inode表 (Inode Table) - 包含文件元数据、数据块指针等
找了几张图片来了解一下,首先文件描述符表在底层通过数组来实现的,文件描述符实际上是这个数组的偏移量,这个表是每个程序自己的:

我们上面也提到了每个进程默认打开三个文件描述符,而当我们调用函数执行代码的时候,默认是从3开始的,描述符表会增加一项:
int fd = open("io.txt", O_RDONLY);

这个file地址会指向内核里,内核里会存放一些文件描述(注意存的不是文件):

那么文件存放在那呢?存放在path下,某个挂载点(某个硬盘)下的某个目录,这里指向的是文件,中间以内核搭建桥梁:

我们现在找到了文件那么如何对文件的数据进行读写呢?那就需要找到inode(这是文件的唯一编号),对相关修改数据进行:

当我们执行open(等系统调用时,内核会创建一个新的structfile,这个数据结构记录了文件的元数据(文件类型、权限等)、文件路径、支持的操作等,然后分配文件描述符,将struct file维护在文件描述符表中,最后将文件描述符返回给应用程序。我们可以通过后者对文件执行它所支持的各种函数操作,而这些函数的函数指针都维护在structfile_operations数据结构中。文件描述符实质上是底层数据结构struct file的一个引用或者句柄,它为用户提供了操作底层文件的入口。

其他Linux应用开发相关都存放在下方链接当中(持续更新中······):





被折叠的 条评论
为什么被折叠?



