操作系统C++实践:深入理解标准IO与文件IO

操作系统C++实践:深入理解标准IO与文件IO
abinng😶🌫️IO操作(标准IO与文件IO)
本文语言不保证严谨,内容也不保证完全,欢迎指正错误、补充
一、前言
C语言提供了两套主要的IO系统:标准IO和文件IO。理解这两种IO能帮助我们理解到程序如何与操作系统交互。
二、标准IO与文件IO
标准IO:使用系统提供的C标准库函数进行IO操作,通过FILE指针作为文件的句柄(用于标识某个资源或对象的唯一标识符)。
文件IO:基于系统调用的IO操作,每进行一次系统调用,进程会从用户空间向内核空间进行一次切换,而进程会挂起,从而导致进程执行效率降低。
二者主要区别
标准IO相较于文件IO而言提供了缓冲机制,以及一些格式化操作(fprintf, fscanf等)和较好的跨平台性;文件IO较为底层,系统相关性较强,需要使用者处理更多细节。
相关接口:
- 标准IO:
printf/scanf, fopen/fclose, fread/fwrite, fprintf/fscanf, fgetc/fputc, fgets/fputs, fseek/ftell/rewind - 文件IO:
open/close, read/write, lseek
- 标准IO:
三、标准IO
3.1 FILE结构体
- 标准IO的一个重要部分,是系统提供的用于描述一个文件全部信息的结构体
- FILE结构体核心字段
1 | struct FILE |
实际的FILE结构体比较复杂,这里仅仅展示了核心字段的简化版本。
- 特殊的三个FILE指针:针对于终端文件而言的
- stderr:标准错误指针
- stdin:标准输入指针
- stdout:标准输出指针
3.2 常用接口
接下来是常用的一些接口(此处不再过多介绍,参考API手册或百度/bing/google即可)
1 | 打开关闭文件:fopen/fclose |
3.3 关于缓冲区
三种缓冲区(在大多数Linux系统中)
- 缓冲区大小可能因系统而异,这里给出的是常见值。
- 行缓存:与终端(标准输入输出)相关的缓冲区,大小为1024字节,用于stdin, stdout
- 全缓存:与文件相关的缓冲区,大小为4096字节,用于文件
- 无缓存:与标准错误相关的缓冲区,大小为0字节,用于stderr
缓冲机制
标准IO的缓冲机制是:当调用相关函数时,实际上是先将这些数据信息放入缓冲区中,等待缓冲区时机到了后,统一进行系统调用,将数据刷入内核空间,减少系统调用次数,提高效率
缓冲区的刷新时机
- 手动刷新:
fflush
int fflush(FILE *stream)可刷新给定文件指针对应的缓冲区自动刷新
1
2
3
4
5
6
7
8
9
10
11
12行缓存刷新时机:
1.程序结束时
2.遇到换行时
3.输入输出发生切换时
4.关闭行缓存对应的文件指针时
5.缓冲区满了后
全缓存刷新时机:
1.程序结束时
2.输入输出发生切换时
3.关闭全缓存对应的文件指针时
4.缓冲区满了后
不缓存刷新时机:放入立马刷新演示
行缓存演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
int main(int argc, const char *argv[])
{
/*
// 1. 程序结束前不会显示,因为没有换行符且程序被阻塞
printf("hello world"); // 终端不显示hello world
while(1); // 阻塞程序,防止结束
*/
/*
// 2. 程序结束时自动刷新缓冲区
printf("hello world"); // 程序自动结束后,终端上显示hello world
*/
/*
// 3. 遇到换行符时立即刷新
printf("hello world\n"); // 终端显示hello world
while(1); // 阻塞程序,防止结束
*/
/*
// 4. 输入输出切换时刷新缓冲区
int num = 0;
printf("请输入>>>");
scanf("%d", &num); // 输入输出发生切换
*/
/*
// 5. 关闭文件指针时刷新
printf("hello world");
fclose(stdout); // 关闭行缓存对应的文件指针
while(1); // 阻塞程序,防止结束
*/
/*
// 6. 缓冲区满时自动刷新
for(int i=0; i<1025; i++) // 超过1024字节了
{
printf("A");
}
while(1);
*/
/*
// 7. 手动刷新缓冲区
printf("hello world");
fflush(stdout);
while(1);
*/
return 0;
}全缓存演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
int main(int argc, const char *argv[])
{
// 打开一个文件进行读写操作
FILE *fp = NULL;
if((fp = fopen("./aa.txt", "r+")) == NULL) //读写的形式
{
perror("fopen error");
return -1;
}
/*
// 1. 普通写入不会立即刷新到文件
fputs("hello world", fp); // 不刷新
while(1);
*/
/*
// 2. 换行符对全缓存无效
fputs("hello world\n", fp); // 换行不刷新
while(1);
*/
/*
// 3. 程序结束时自动刷新
fputs("hello world\n", fp); // 程序结束刷新
*/
/*
// 4. 输入输出切换时刷新
fputs("hello world\n", fp); // 向文件中输出一个字符串
fgetc(fp); // 输入输出发生切换
while(1);
*/
/*
// 5. 关闭文件指针时刷新
fputs("hello world", fp);
fclose(fp); // 关闭缓冲区对应的文件指针
while(1);
*/
/*
// 6. 手动刷新缓冲区
fputs("hello world", fp);
fflush(fp); // 手动刷新
while(1);
*/
/*
// 7. 缓冲区满时自动刷新
for(int i=0; i<4097; i++)
{
fputc('A', fp); // 超过4096字节了
}
while(1);
*/
fclose(fp);
return 0;
}无缓存演示
1
2
3
4
5
6
7
8
9
int main(int argc, const char *argv[])
{
// 标准错误输出立即显示,无需等待程序结束
fputs("A", stderr);
while(1); // 阻塞程序,防止结束
}
- 手动刷新:
3.4 关于错误码
- 经常见到函数返回值介绍中“置位错误码”,这是什么意思?
当内核提供的函数出错后,内核空间会向用户空间反馈一个错误信息,由于错误信息比较多
也比较复杂,系统就给每种不同的错误信息起了一个编号,这就是系统提供的全局变量
errno ,是一个大于或等于0的整数
- 基础的常用的errno(不完全)
1 | define errno explain |
- 关于错误码的函数
1 |
|
- 演示
1 |
|
3.5 拷贝文件演示
1 |
|
四、文件IO
通过系统调用实现,使用一次文件IO接口,进程就会向内核空间进行一次切换。标准IO的相关接口中也调用了文件IO的操作,因为没有缓冲区,所以操作效率较低。
4.1 文件描述符
本质是一个大于等于0的整数,使用open函数打开文件时,会产生一个用于操作文件的句柄,这就是文件描述符
一个进程中,能够打开的文件描述符是有限制的,一般是1024个[0, 1023],可通过指令
ulimit -a进行查看,若要更改这个限制,可通过ulimit -n 数字进行更改文件描述符的使用原则一般是最小未分配原则
特殊的文件描述符:0、1、2,这三个文件描述符在一个进程启动时就默认被打开了,分别表示标准输入
stdin、标准输出stdout、标准错误stderr。通过以下程序验证1
2
3
4
5
6
7
8
9
10
int main(int argc, const char *argv[])
{
printf("stdin->_fileno = %d\n", stdin->_fileno); // 0
printf("stdout->_fileno = %d\n", stdout->_fileno); // 1
printf("stderr->_fileno = %d\n", stderr->_fileno); // 2
return 0;
}
4.2 常用接口
接下来是常用的一些接口(此处不再过多介绍,参考API手册或百度/bing/google即可)
1 | 打开关闭文件:open/close |
4.3 拷贝文件演示
1 |
|
五、选择建议
标准IO适用场景:
- 频繁的小数据读写(因为有缓冲区,减少系统调用)
- 格式化输入输出时(因为有fprintf, fscanf等函数)
- 文本文件处理(因为方便)
- 跨平台(因为方便,C标准库提供了兼容性)
文件IO适用场景:
- 大块数据读写(因为无缓冲区,每次都要系统调用)
- 需要精确控制IO行为(因为是基于系统调用)
- 系统级编程(因为是基于系统调用)
- 性能需求(因为偏底层,可自行避免标准IO中额外的操作)
到此就结束了,总结就是:
- 标准IO提供了便利的缓冲机制和格式化功能,适合大多数情况。
- 文件IO则提供了更底层的控制能力,适合系统编程和对性能有要求的情况。
希望大家喜欢,内容不多但都有用心 (´▽`ʃ♡ƪ)







