当前位置: 首页 > news >正文

【Linux系统】基础IO

文章目录

  • 一、理解"文件"
  • 二、回顾C文件接口
    • 1.fopen函数(打开文件)和 fclose函数(关闭文件)
    • 2.fwrite函数(写文件)和 fread函数(读文件)
    • 3.格式化输入输出函数(补充内容)
    • 4.stdin、stdout 和 stderr(介绍FILE类型)
  • 三、文件相关系统调用接口
    • 1.open系统调用 和 close系统调用
    • 2. write系统调用 和 read系统调用
  • 四、文件描述符fd(引入struct file以及管理它的方式)
  • 五、重定向
    • 1.重定向介绍
    • 2.dup2 系统调用(无需close(fd),就能实现重定向的方法)
    • 3.将重定向功能 与 进程程序替换结合
    • 4.在命令行执行重定向命令
  • 六、缓冲区
    • 1.缓冲区的介绍(以输出视角介绍缓冲区)
    • 2.以输入(读取数据)视角介绍缓冲区
    • 3.语言级缓冲区 和 内核级缓冲区存在的意义
    • 4.一道综合性习题(结合重定向、缓冲区 和 写实拷贝)
    • 5.模拟实现 fopen、fwrite、fflush、fclose等函数(函数的实现中有具体的缓冲区刷新过程)


一、理解"文件"

  • 文件的狭义理解:

• 文件是存储在磁盘里的
• 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的
• 磁盘是外设(即是输出设备也是输入设备)
• 对磁盘上的文件的所有操作,本质是对外设进行输入和输出,简称 IO(input,output)操作

  • 文件的广义理解:

• Linux 下⼀切皆文件(键盘、显示器、网卡、磁盘…… 这些都是抽象化的过程)(后面会讲如何去 理解)


文件操作的归类认知:

• 文件是文件属性(元数据)和文件内容的集合(文件 = 属性(元数据)+ 内容)
• 所有的文件操作本质是 对文件内容操作 或 对文件属性操作

- 访问一个文件,都必须先把文件打开,这是为什么?
打开文件,本质是把文件从磁盘加载到内存中(冯诺依曼体系结构规定 CPU只能对内存进行读写),只有把文件加载到内存,才能对文件进行读写操作。

- 文件是谁打开的?
用户通过bash进程(命令行解释器),创建进程,在进程中执行fopen函数(底层封装了open) 或 open系统调用打开文件的。
梳理一下就是:用户有打开文件的想法,通过启动进程来实现。所以在本质上:文件是被进程打开的。

文件的读写本质不是通过 C 语言 / C++的库函数来操作的(这些库函数只是为用户提供方便,fopen,fwrite,fread等C语言库函数底层都封装了系统调用),而是通过文件相关的系统调用接口来实现的。

一个进程可以同时打开很多文件,操作系统中会同时存在很多进程,所以操作系统一定同时存在大量打开的文件。
操作系统要对大量打开的文件进行管理(管理的本质:先描述,再组织),所以一定存在一种数据结构体,描述被打开的文件(就类似于 进程pcb),然后使用数据结构(如链表)组织起这些结构体。

进程有pcb(task_struct),未来进程也会有打开的文件,进程要对自己打开的文件进行管理,所以研究打开的文件,本质是在研究:进程与文件的关系!

二、回顾C文件接口

1.fopen函数(打开文件)和 fclose函数(关闭文件)

在这里插入图片描述在这里插入图片描述

FILE * fopen(const char *path, const char *mode);
fopen函数 有两个参数: path(文件路径+文件名),mode(打开方式)
// 需注意的是第一个参数可以省略文件路径,省略时采用默认文件路径(也就是进程的当前工作路径cwd)

那系统怎么知道进程的当前工作路径在哪里呢?
可以使用 ls /proc/[进程id] -l 命令查看当前正在运行进程的信息:

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ps ajx | grep myprocess
23097 23330 23330 23097 pts/1    23330 R+    1000   0:32 ./myprocess
23294 23339 23338 23294 pts/2    23338 R+    1000   0:00 grep --color=auto myprocess
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ls /proc/23097 -l
total 0
...
lrwxrwxrwx 1 zh zh 0 Jul  4 14:23 cwd -> /home/zh/test
-r-------- 1 zh zh 0 Jul  4 14:23 environ
lrwxrwxrwx 1 zh zh 0 Jul  4 14:19 exe -> /usr/bin/bash
dr-x------ 2 zh zh 0 Jul  4 14:18 fd
dr-x------ 2 zh zh 0 Jul  4 14:23 fdinfo
...

其中:
• cwd:指向进程当前工作路径。
• exe:指向启动当前进程的可执行文件的完整路径

打开文件,本质是进程打开,进程是知道自己在哪里(进程pcb中有cwd变量,cwd中保存了当前工作路径),当文件不带路径,进程会默认给文件加上进程当前工作路径。


打开方式(mode):

在这里插入图片描述

  • FILE *fopen(“myfile”, “r”);
    // r方式:从文件的开头开始读取文件内容。不存在指定文件则设置错误码并返回NULL

  • FILE *fopen(“myfile”, “w”);
    // w方式:若文件存在,打开文件并清空文件中内容,再向文件写入内容;若文件不存在,创建新文件,向新文件写入内容

  • FILE *fopen(“myfile”, “a”);
    // a方式:若文件存在,打开文件,从文件已有内容后接着写入内容;若文件不存在,创建新文件,向新文件写入内容

补充:fclose函数在这里插入图片描述在这里插入图片描述
如果打开文件后没有使用fclose函数关闭文件就结束程序,那么向文件中写入的数据可能会丢失。因为,在向文件写数据时,是先将数据输出到FILE中的缓冲区(语言级缓冲区),待缓冲区充满后才正式输出给文件内核缓冲区(普通文件的语言级缓冲区刷新规则是 全缓冲,也就是缓冲区满了才刷新)。如果当数据未充满语言级缓冲区时程序就结束运行,就有可能使缓冲区中的数据丢失。但如果我们在使用完文件之后,及时使用fclose函数将文件关闭就可以避免写入文件数据丢失的情况,因为fclose函数在关闭文件的同时,还会把语言级缓冲区中的数据刷新到磁盘文件,然后才撤销文件信息区。

2.fwrite函数(写文件)和 fread函数(读文件)

在这里插入图片描述在这里插入图片描述

  1. size_t fwrite(const void *ptr,size_t size,size_t nmemb,FILE *stream);
    功能:将 count 个元素(每个元素的大小为 size 字节)从 ptr 指向的内存块写入流中
    返回值:返回成功写入的元素个数

在这里插入图片描述

  • 以 w方式打开指定文件,文件不存在,则创建一个新文件再写入:
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ll
total 28
-rw-rw-r-- 1 zh zh   139 Jun 15 12:58 makefile
-rwxrwxr-x 1 zh zh 10944 Jul  4 16:34 myprocess
-rw-rw-r-- 1 zh zh   316 Jul  4 16:34 process.c
-rw-rw-r-- 1 zh zh  6064 Jul  4 16:34 process.o
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess piece.txt w
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ll
total 32
-rw-rw-r-- 1 zh zh   139 Jun 15 12:58 makefile
-rwxrwxr-x 1 zh zh 10944 Jul  4 16:34 myprocess
-rw-rw-r-- 1 zh zh    10 Jul  4 16:35 piece.txt
-rw-rw-r-- 1 zh zh   316 Jul  4 16:34 process.c
-rw-rw-r-- 1 zh zh  6064 Jul  4 16:34 process.o
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat piece.txt
one piece
  • 以 a方式打开指定文件,若文件存在,从文件已有内容后接着写入内容:
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess piece.txt a
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat piece.txt
one piece
one piece
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess piece.txt a
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat piece.txt
one piece
one piece
one piece
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess piece.txt a
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat piece.txt
one piece
one piece
one piece
one piece
  • 以 w方式打开指定文件,若文件存在,清空文件中内容,再向文件写入内容:
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess piece.txt w
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat piece.txt
one piece
  1. size_t fread ( void *ptr,size_t size,size_t count,FILE *stream );
    功能:从流中读取 count 个元素(每个元素的大小为 size 字节),并将它们存储在 ptr 指定的内存块中。
    返回值:返回成功读取的元素个数。

在这里插入图片描述

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat piece.txt
i love one piece!
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess piece.txt r
i love on

3.格式化输入输出函数(补充内容)

printf 和 scanf 同系列的函数 被称为格式化输入输出函数:

在这里插入图片描述
在这里插入图片描述
(1)格式化输出演示:

int a = 12; // 整型在内存中占4字节
double e = 3.14; // 双精度浮点数在内存中占8字节
printf("%d %lf \n",a,e); // printf函数会根据数据的类型把它们转换成字符串中的字符:"12 3.14 \n",再输出

格式化过程:整型12(4字节)被转换成 字符 ‘1’、‘2’(共2字节);double浮点数3.14(8字节)被转换成 字符 ‘3’、‘.’、‘1’、‘4’(共4字节)

(2)格式化输入演示:

int a = 0;
double e = 0.0;
scanf("%d %lf",&a,&e); // 键盘输入"14 3.4",再按回车

键盘是以字符格式输入内容的,scanf函数要把键盘输入的字符内容转换成指定格式的内容。
格式化过程:字符 ‘1’、‘4’ (共2字节)被转换成 整型14(4字节);‘3’、‘.’、‘4’(共3字节)被转换成 double类型浮点数 3.4(8字节)

显示器(屏幕)和 键盘 都是字符设备。
打印在显示器上的内容全是字符,使用键盘输入的内容全是字符。

4.stdin、stdout 和 stderr(介绍FILE类型)

在这里插入图片描述

C程序在启动的时候,默认打开了3个标准输入输出流:

• stdin - 标准输入流 - > 键盘文件
• stdout - 标准输出流 - > 显示器文件
• stderr - 标准错误流 - > 显示器文件
显示器文件 stdin、stdout、stderr 三个流的类型是: FILE * ,通常称为文件指针类型。

FILE 是 struct _IO_FILE 的 typedef 别名,struct _IO_FILE类型 是在C语言标准库定义的结构体类型。
简化版struct _IO_FILE类型 如下:

struct _IO_FILE {int _flags;                 // 状态标志(缓冲模式标志是_flags字段的一部分)char* _IO_read_base;        // 读缓冲区起始char* _IO_read_end;         // 读缓冲区结束char* _IO_read_ptr;         // 读指针当前位置char* _IO_write_base;       // 写缓冲区起始char* _IO_write_end;        // 写缓冲区结束char* _IO_write_ptr;        // 写指针当前位置int _IO_buf_size;           // 缓冲区总大小int _fileno;                // 文件描述符off_t _offset;              // 文件偏移量...
};typedef struct _IO_FILE FILE;

FILE类型结构体 中维护着 两个缓冲区(输入缓冲区 和 输出缓冲区),这两个缓冲区都是语言级缓冲区;缓冲模式标志是_flags字段的一部分,缓冲模式标注其实就是指缓冲区的刷新策略,决定了什么时候将语言级缓冲区中维护的内容 刷新到 文件内核缓冲区(通过文件描述符 可以找到 文件内核缓冲区)。

三、文件相关系统调用接口

1.open系统调用 和 close系统调用

在这里插入图片描述在这里插入图片描述
int open(const char *pathname, int flags, mode_t mode);

  • open函数一共有 三个参数 :
    (1) 第一个参数 pathname:pathname(文件路径+文件名),可以省略文件路径,省略时采用默认文件路径(也就是进程的当前工作路径cwd)
    (2) 第三个参数 mode :文件权限,创建文件时需要指定文件权限,一般设为 0666
    (3) 第二个参数 flags : 读写方式(由 宏|宏|宏|… 的方式构成,这里的宏是被定义成只有一个比特位为1的整数)

常用宏:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
// 前三个宏(只有一个比特位为1的整数),必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 写入追加到文件末尾
O_TRUNC:若文件存在,打开时清空文件(需有写权限)

  • open函数返回值 :
    成功:新打开的文件描述符fd(后续详细介绍)
    失败:失败时返回-1并设置errno

int open(“myfile”, O_WRONLY|O_CREAT|O_TRUNC, 0666);
读写方式:O_WRONLY|O_CREAT|O_TRUNC(其实就相当于fopen函数 以w方式打开文件)
实际上,当 fopen函数以w方式打开文件时,底层就是这样调用 open系统调用

int open(“myfile”, O_WRONLY|O_CREAT|O_APPEND, 0666);
读写方式:O_WRONLY|O_CREAT|O_APPEND(其实就相当于fopen函数 以a方式打开文件)
实际上,当 fopen函数以a方式打开文件时,底层就是这样调用 open系统调用

int open(“myfile”, O_RDONLY, 0666);
读写方式:O_RDONLY(其实就相当于fopen函数 以r方式打开文件)
实际上,当 fopen函数以r方式打开文件时,底层就是这样调用 open系统调用

close系统调用功能:
关闭指定文件描述符,使它不再引用任何文件,并且可以重新用于其它用途。
如果fd是指向底层打开文件描述(struct file)的最后一个文件描述符,则释放与打开文件描述相关的资源;

在这里插入图片描述

2. write系统调用 和 read系统调用

  1. ssize_t write(int fd,const void *buf,size_t count);
    功能:将buf指向的缓冲区中的count个字节写入文件描述符fd所指向的文件
    返回值:成功时返回写入的字节数(可能小于count),失败时返回-1并设置errno

在这里插入图片描述在这里插入图片描述
(1)open(“myfile”, O_WRONLY|O_CREAT|O_TRUNC, 0666);
读写方式:O_WRONLY|O_CREAT|O_TRUNC(其实就相当于fopen函数 以w方式打开文件)

在这里插入图片描述
文件不存在时,新创建文件并写入:

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ll
total 24
-rw-rw-r-- 1 zh zh  139 Jun 15 12:58 makefile
-rwxrwxr-x 1 zh zh 9792 Jul  5 10:48 myprocess
-rw-rw-r-- 1 zh zh  407 Jul  5 10:48 process.c
-rw-rw-r-- 1 zh zh 3928 Jul  5 10:48 process.o
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess log.txt
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ll
total 28
-rw-rw-r-- 1 zh zh   10 Jul  5 10:49 log.txt
-rw-rw-r-- 1 zh zh  139 Jun 15 12:58 makefile
-rwxrwxr-x 1 zh zh 9792 Jul  5 10:48 myprocess
-rw-rw-r-- 1 zh zh  407 Jul  5 10:48 process.c
-rw-rw-r-- 1 zh zh 3928 Jul  5 10:48 process.o
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat log.txt
one piece

文件存在,打开时清空文件再写入:

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess log.txt
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat log.txt
one piece
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess log.txt
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat log.txt
one piece

(2)open(“myfile”, O_WRONLY|O_CREAT|O_APPEND, 0666);
读写方式:O_WRONLY|O_CREAT|O_APPEND(其实就相当于fopen函数 以a方式打开文件)

在这里插入图片描述
文件不存在时,新创建文件并写入:

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ll
total 24
-rw-rw-r-- 1 zh zh  139 Jun 15 12:58 makefile
-rwxrwxr-x 1 zh zh 9792 Jul  5 10:53 myprocess
-rw-rw-r-- 1 zh zh  408 Jul  5 10:53 process.c
-rw-rw-r-- 1 zh zh 3928 Jul  5 10:53 process.o
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess ppt.txt
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ll
total 28
-rw-rw-r-- 1 zh zh  139 Jun 15 12:58 makefile
-rwxrwxr-x 1 zh zh 9792 Jul  5 10:53 myprocess
-rw-rw-r-- 1 zh zh   10 Jul  5 10:54 ppt.txt
-rw-rw-r-- 1 zh zh  408 Jul  5 10:53 process.c
-rw-rw-r-- 1 zh zh 3928 Jul  5 10:53 process.o
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat ppt.txt
one piece

文件存在,打开文件,写入追加到文件末尾:

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess ppt.txt
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat ppt.txt
one piece
one piece
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess ppt.txt
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat ppt.txt
one piece
one piece
one piece
  1. ssize_t read(int fd,void *buf,size_t count);
    功能:从文件描述符fd所指向的文件中读取最多count个字节的数据,并存储到buf指向的缓冲区中。
    返回值:成功时返回读取的字节数,失败时返回-1并设置errno。
    在这里插入图片描述在这里插入图片描述
    在这里插入图片描述
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat ppt.txt
i love one piece!
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess ppt.txt
i love on

四、文件描述符fd(引入struct file以及管理它的方式)

在当前学习阶段,我们可以暂时把文件分为两类:普通文件(磁盘上保存的文件)和 设备文件(表示硬件设备信息的文件,如键盘文件、显示器文件等)

一个进程可以同时打开很多文件,操作系统中会同时存在很多进程,所以操作系统一定同时存在大量打开的文件。
操作系统要对大量打开的文件进行管理(管理的本质:先描述,再组织),所以一定存在一种数据结构体,描述被打开的文件(就类似于 进程pcb),然后使用数据结构(如链表)组织起这些结构体。
在内核中描述被打开文件的结构体是 struct file(暂不需要了解结构体内部详细构造),该结构体中直接或间接包含了被打开文件的属性和内容。进程每打开一个文件,就会在内存中为它创建一个 struct file 结构体,操作系统使用一个全局双链表结构把所有 struct file 结构体管理起来:
在这里插入图片描述

进程有pcb(task_struct),未来进程也会有打开的文件,进程要对自己打开的文件进行管理。那么进程是如何管理自己打开的文件的呢?
进程的struct task_struct 中有一个结构体指针 struct files_struct *files,它指向结构体 struct files_struct,
struct files_struct 中有一个 struct file *fd_array[NR_OPEN_DEFAULT] 数组,这个数组中保存了进程打开的所有文件的地址。

在这里插入图片描述
在这里插入图片描述
fd_array数组 的数组下标就是 文件描述符!

补充知识:struct file中有一个引用计数,记录了指向它的文件描述符数量,当这个引用计数变为0的时候,证明最后一个指向它的文件描述符也被关闭了,此时释放这个struct file以及相关的资源


在操作系统角度,识别打开的文件,只认:int fd 文件描述符。
上图所展示的所有内容都是 操作系统层面,系统调用 open的返回值就是 fd文件描述符,根据文件描述符就能访问对应打开的文件;但回到语言层面,fopen函数的返回值是 FILE* 指针,指向FILE结构体,通过FILE* 指针也是能够对底层文件进行读写的,这就证明了 FILE结构体内部肯定封装了 fd文件描述符(只有文件描述符能访问打开的文件)

struct FILE {...int _fileno;                // 文件描述符...
};

前面我们讲过,C程序在启动的时候,默认打开了3个标准输入输出流:
• stdin - 标准输入流 - > 键盘文件
• stdout - 标准输出流 - > 显示器文件
• stderr - 标准错误流 - > 显示器文件
stdin、stdout、stderr 三个流的类型是: FILE * ,通常称为文件指针类型。

stdin、stdout 和 stderr指针指向的FILE结构体中封装的 文件描述符内容分别是 0、1、2

Linux下进程启动时,一般会默认打开3个文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
0、1、2对应的文件⼀般是:键盘文件,显示器文件,显示器文件

(1)下述代码能够证明进程启动时,一般会默认打开0、1、2 这3个文件描述符,0、1、2对应的文件⼀般是:键盘文件,显示器文件,显示器文件
在这里插入图片描述
代码运行结果,第一行都是从键盘输入的内容:

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess
abcdefghi
abcdefghi
abcdefghi
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess
one piece
one piece
one piece

文件描述符fd 的分配规则:给进程新打开的文件分配fd,会从 fd_array数组寻找下标最小的未被使用的元素,把这个元素的下标作为新打开文件的fd

(1)Linux下进程启动时,一般会默认打开0、1、2 这3个文件描述符。这也就表示一启动进程,该进程的 0、1、2 文件描述符( fd_array数组 0、1、2下标)就已经被使用了,所以后续再打开文件,会从 3 这个文件描述符开始分配:

在这里插入图片描述

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess
fd1: 3
fd2: 4
fd3: 5

(2)Linux下进程启动时,一般会默认打开0、1、2 这3个文件描述符。我们可以一开始就关闭 文件描述符0,使它不再引用任何文件,这样文件描述符 0就是最小且没有被使用的文件描述符。我们再打开文件时,会为它分配文件描述符0:

在这里插入图片描述

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess
fd: 0

五、重定向

1.重定向介绍

前面我们讲过,C程序在启动的时候,默认打开了3个标准输入输出流:
• stdin - 标准输入流 - > 键盘文件
• stdout - 标准输出流 - > 显示器文件
• stderr - 标准错误流 - > 显示器文件
stdin、stdout、stderr 三个流的类型是: FILE * ,通常称为文件指针类型。

stdin、stdout 和 stderr指针指向的FILE结构体中封装的 文件描述符内容分别是 0、1、2

Linux下进程启动时,一般会默认打开3个文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
0、1、2对应的文件⼀般是:键盘文件,显示器文件,显示器文件

  1. 演示输出重定向的效果:

在这里插入图片描述
这个程序中有printf函数,执行后却没在屏幕上打印出任何内容:

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ll
total 28
-rw-rw-r-- 1 zh zh   139 Jun 15 12:58 makefile
-rwxrwxr-x 1 zh zh 10896 Jul  5 21:06 myprocess
-rw-rw-r-- 1 zh zh   366 Jul  5 21:05 process.c
-rw-rw-r-- 1 zh zh  6000 Jul  5 21:06 process.o
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess

但却在myfile文件中发现了printf函数的打印内容:

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ll
total 32
-rw-rw-r-- 1 zh zh   139 Jun 15 12:58 makefile
-rw-rw-r-- 1 zh zh     6 Jul  5 21:07 myfile
-rwxrwxr-x 1 zh zh 10896 Jul  5 21:06 myprocess
-rw-rw-r-- 1 zh zh   366 Jul  5 21:05 process.c
-rw-rw-r-- 1 zh zh  6000 Jul  5 21:06 process.o
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat myfile
fd: 1

以上就是输出重定向的效果:可以改变输出的方向(原本printf函数应该输出内容到屏幕,输出重定向后,printf函数输出内容到指定文件)。

接下来解释输出重定向的原理(基于上述代码):

  • 进程刚启动时,默认打开3个文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
    0、1、2对应的文件⼀般是:键盘文件,显示器文件,显示器文件
    在这里插入图片描述
  • 进程执行了 close(1);这行代码后,文件描述符1被关闭,它不再指向任何文件,文件描述符1变成最小且没有被使用的文件描述符
    在这里插入图片描述
  • 当执行 int fd = open(“myfile”, O_WRONLY|O_CREAT, 0666); 这行代码,要给进程新打开的文件myfile分配最小且没有被使用的文件描述符fd,也就是1。
    到这一步输出重定向已经完成了,后面展示输出重定向的效果。
    在这里插入图片描述
  • 当执行到 printf(“fd: %d\n”, fd);这行代码时,printf函数是默认向标准输出流stdout输出内容的,而stdout指针指向的FILE结构体中封装的 文件描述符内容是1,此时文件描述符1指向文件myfile,所以printf函数输出内容到文件myfile
  1. 演示输入重定向的效果:

在这里插入图片描述

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat log.txt
piece
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess
piece

原理与输出重定向类似:
close系统调用关闭文件描述符0,文件描述符0变成最小且没有被使用的文件描述符;
open系统调用打开文件log.txt时,要给新打开的文件log.txt分配最小且没有被使用的文件描述符fd,也就是0。到这一步输入重定向已经完成了,后面是输入重定向的效果:

scanf函数默认读取标准输入流stdin中输入的内容,而stdin指针指向的FILE结构体中封装的文件描述符内容是0,此时文件描述符0指向文件log.txt,所以scanf函数会读取文件log.txt中的内容

2.dup2 系统调用(无需close(fd),就能实现重定向的方法)

在这里插入图片描述在这里插入图片描述
int dup2(int oldfd, int newfd);
功能:文件描述符newfd中的内容被 文件描述符oldfd中的内容覆盖
返回值:成功时返回文件描述符newfd,失败时返回-1并设置errno。

(1)示例演示 dup2系统调用 的用法:

在这里插入图片描述
上述代码中,dup2系统调用实现了输出重定向的效果:改变了输出的方向(原本printf函数应该输出内容到屏幕,输出重定向后,printf函数输出内容到指定文件)。

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ll
total 28
-rw-rw-r-- 1 zh zh   139 Jun 15 12:58 makefile
-rwxrwxr-x 1 zh zh 10888 Jul  5 23:24 myprocess
-rw-rw-r-- 1 zh zh   375 Jul  5 23:24 process.c
-rw-rw-r-- 1 zh zh  5936 Jul  5 23:24 process.o
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ll
total 32
-rw-rw-r-- 1 zh zh    10 Jul  5 23:24 log.txt
-rw-rw-r-- 1 zh zh   139 Jun 15 12:58 makefile
-rwxrwxr-x 1 zh zh 10888 Jul  5 23:24 myprocess
-rw-rw-r-- 1 zh zh   375 Jul  5 23:24 process.c
-rw-rw-r-- 1 zh zh  5936 Jul  5 23:24 process.o
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat log.txt
one piece

接下来解释dup2系统调用的原理(基于上述代码):

  • 进程刚启动时,默认打开3个文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
    0、1、2对应的文件⼀般是:键盘文件,显示器文件,显示器文件
    在这里插入图片描述
  • 当执行 int fd = open(“log.txt”, O_WRONLY|O_CREAT|O_TRUNC, 0666); 这行代码,要给进程新打开的文件log.txt分配最小且没有被使用的文件描述符fd,也就是3。

在这里插入图片描述

  • 当执行 dup2(fd, 1); 这行代码,文件描述符1中的内容被 文件描述符fd(也就是3)中的内容覆盖,其实就是fd_array数组中 下标为1的元素内容 被下标为3的元素内容覆盖,所以文件描述符1 也会指向文件log.txt。这就是 dup2系统调用实现重定向的原理。
    在这里插入图片描述

3.将重定向功能 与 进程程序替换结合

为什么Linux下进程刚启动时,它的0、1、2这3个文件描述符就处于被使用状态(一般情况下),这是如何实现的?
Linux操作系统启动时,会自动为我们创建bash进程(命令行解释器)。bash进程被创建后,它会先把键盘文件 和 显示器文件载入内存,为键盘文件分配文件描述符0,为显示器文件分配文件描述符1和2,然后才循环进行命令行解析工作。
在命令行启动的所有进程都是 bash进程创建的子进程,刚创建出的子进程中的fd_array数组中的内容完全是 父进程fd_array数组内容的拷贝。
bash进程 打开了3个文件描述符0、1、2,且0、1、2对应的文件是:键盘文件,显示器文件,显示器文件;命令行中创建的所有进程(bash的子进程),它们的fd_array数组中的内容完全是 bash进程的拷贝,所以它们的文件描述符0、1、2都是默认打开的(且指向键盘文件,显示器文件,显示器文件)

在这里插入图片描述
进程执行exe系列函数进行进程程序替换(不会创建新进程),不会影响进程历史打开的文件(被使用的文件描述符还是指向对应的文件,不受影响)!
在这里插入图片描述

(1)示例演示:

#include <stdio.h>
#include <stdlib.h>                                                           
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main() 
{    int pid = fork();    if(pid == 0)    {    int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);    if (fd < 0)    {    perror("open");    exit(1);    }    dup2(fd, 1);    execl("/usr/bin/ls", "ls", "-l", "-a", NULL);                                                                                                        printf("进程程序替换失败\n");    exit(1);    }    return 0;    
}   
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ll
total 24
-rw-rw-r-- 1 zh zh  139 Jun 15 12:58 makefile
-rwxrwxr-x 1 zh zh 9768 Jul  6 12:42 myprocess
-rw-rw-r-- 1 zh zh  518 Jul  6 12:41 process.c
-rw-rw-r-- 1 zh zh 4088 Jul  6 12:42 process.o
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess

子进程先进行了重定向,使文件描述符1指向文件log.txt,然后进行进程程序替换(替换成ls程序),程序替换不影响进程历史打开的文件(不影响文件描述符的指向,文件描述符1还是指向文件log.txt),ls程序默认向标准输出流stdout输出,而stdout指针指向的FILE结构体中封装的 文件描述符内容是1,此时文件描述符1指向文件log.txt,所以ls程序输出内容到文件log.txt:

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ll
total 28
-rw-rw-r-- 1 zh zh  320 Jul  6 12:42 log.txt
-rw-rw-r-- 1 zh zh  139 Jun 15 12:58 makefile
-rwxrwxr-x 1 zh zh 9768 Jul  6 12:42 myprocess
-rw-rw-r-- 1 zh zh  518 Jul  6 12:41 process.c
-rw-rw-r-- 1 zh zh 4088 Jul  6 12:42 process.o
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat log.txt
total 32
drwxrwxr-x 2 zh zh 4096 Jul  6 12:42 .
drwx------ 9 zh zh 4096 Jul  1 20:40 ..
-rw-rw-r-- 1 zh zh    0 Jul  6 12:42 log.txt
-rw-rw-r-- 1 zh zh  139 Jun 15 12:58 makefile
-rwxrwxr-x 1 zh zh 9768 Jul  6 12:42 myprocess
-rw-rw-r-- 1 zh zh  518 Jul  6 12:41 process.c
-rw-rw-r-- 1 zh zh 4088 Jul  6 12:42 process.o

进程 先进行重定向操作,再进行进程程序替换,因为进程程序替换不会影响进程历史打开的文件(被使用的文件描述符还是指向对应的文件,不受影响),所以替换后的程序也可以共享重定向的效果

4.在命令行执行重定向命令

  1. 输出重定向命令的使用:

在这里插入图片描述
(1)直接执行上述代码的效果:

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess
hello world
one piece: Success

(2)在命令行执行输出重定向命令

1>log.txt的效果是 以w方式打开文件log.txt,再对文件描述符1进行重定向,使文件描述符1指向文件log.txt。
printf函数默认向标准输出流stdout输出内容,而stdout指针指向的FILE结构体中封装的 文件描述符内容是1,此时文件描述符1指向文件log.txt,所以printf函数会向文件log.txt写入内容

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess 1>log.txt
one piece: Success
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat log.txt
hello world

// 注意:./myprocess >log.txt 这种写法的效果和 ./myprocess 1>log.txt 是一样的,因为输出重定向命令默认是改变文件描述符1的指向

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess 1>log.txt
one piece: Success
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess 1>log.txt
one piece: Success
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess 1>log.txt
one piece: Success
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat log.txt
hello world

(3)在命令行执行追加输出重定向命令

1>>log.txt的效果是 以a方式打开文件log.txt,再对文件描述符1进行重定向,使文件描述符1指向文件log.txt。

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess 1>>log.txt
one piece: Success
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat log.txt
hello world
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess 1>>log.txt
one piece: Success
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess 1>>log.txt
one piece: Success
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess 1>>log.txt
one piece: Success
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat log.txt
hello world
hello world
hello world
hello world

(4)标准输出流stdout 和 标准错误流stderr 指向的FILE结构体中封装的 文件描述符内容分别是1 和 2,在默认情况下,文件描述符1 和 2都指向显示器文件(此时标准输出 和 标准错误信息都打印在屏幕,不好区分),可以使用重定向功能分开标准输出流 和 标准错误流的输出信息

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess 1>ppt.txt 2>file.txt
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat ppt.txt
hello world
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat file.txt
one piece: Success

./myprocess 1>ppt.txt 2>file.txt 这类型命令的底层实现思路(重定向功能 和 进程程序替换结合):
bash进程 使用fork()系统调用创建子进程,在子进程中先创建出ppt.txt 和 file.txt这两个文件,然后进行重定向,最后进行进程程序替换(替换成 ./myprocess程序)


  1. 输入重定向命令的使用:

在这里插入图片描述
(1)直接执行上述代码的效果:

要等待键盘输入内容后,再读取键盘输入内容

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess
sosoksosjaojd
sosoksosj

(2)在命令行执行输入重定向命令:

0<log.txt的效果是 以r方式打开文件log.txt,再对文件描述符0进行重定向,使文件描述符0指向文件log.txt。
指定fread函数从标准输入流stdin中读取内容,stdin指针指向的FILE结构体中封装的 文件描述符内容是0,此时文件描述符0指向文件log.txt,所以fread函数会读取文件log.txt中的内容

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat log.txt
hello write
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess 0<log.txt
hello wri

// 注意:./myprocess <log.txt 这种写法的效果和 ./myprocess 0<log.txt 是一样的,因为输入重定向命令默认是改变文件描述符0的指向

六、缓冲区

1.缓冲区的介绍(以输出视角介绍缓冲区)

缓冲区是什么?
缓冲区是内存空间的⼀部分。也就是说,在内存空间中预留了⼀定的存储空间,这些存储空间⽤来缓
冲输⼊或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输⼊设备还是输出设
备,分为输⼊缓冲区和输出缓冲区

缓冲区的类型?
缓冲区主要分类两种类型:语言级缓冲区(用户缓冲区)和 内核级缓冲区
语言级缓冲区 封装在 FILE 结构体中;文件加载进内存的时候,内核不止为它创建了 file结构体,还开辟了两块内存空间,一块用作存储文件属性信息,一块作为文件内核缓冲区,file结构体中存储了指向这两块内存空间的指针。
在这里插入图片描述

标准I/O库提供了3种类型的缓冲区刷新策略(语言级缓冲区):

• 全缓冲: 默认用于普通文件。缓冲区满时刷新(缓冲区的大小一般为1024字节)。
• 行缓冲:默认用于显示器文件。遇到换行符 ‘\n’ 或 缓冲区满时刷新。
• 无缓冲:无缓冲是指语言级缓冲区不对字符进行缓存,直接调用系统调用。标准错误流stderr通常是采用无缓冲策略,这使得出错信息能够尽快地显示出来。

还有三种情形也能刷新语言级缓冲区:
• 进程退出时,主动刷新语言级缓冲区
• 调用 int fflush(FILE *stream); 函数,可以刷新指定FILE结构体维护的语言级缓冲区
• 调用 int fclose(FILE *fp); 函数关闭指定流时,会刷新对应FILE结构体维护的语言级缓冲区

  1. 显示器文件的行缓冲刷新策略示例:

在这里插入图片描述
运行结果,没有在屏幕上打印任何数据:

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess

解释: printf 默认向标准输出流stdout输出内容,而stdout指针指向的FILE结构体中封装的 文件描述符内容是1,未进行输出重定向,所以文件描述符1指向显示器文件,这也就意味着stdout指针指向的FILE结构体采用行刷新策略。
执行printf(“%s”,“one piece”);这行代码时,printf函数先把内容输出到stdout指针指向的FILE结构体维护的输出缓冲区(语言级缓冲区)中,然后该FILE结构体检查输出缓冲区的内容是否符合行缓冲刷新策略,显然不满足,没有换行符 ‘\n’ 且缓冲区没满,printf写入的内容暂存在FILE结构体维护的输出缓冲区。
执行close(1);这行代码时,文件描述符1被关闭,不再指向任何文件。
最后进程退出时,会主动刷新所有语言级缓冲区。stdout指针指向的FILE结构体中封装的 文件描述符内容是1,文件描述符1已经被关闭,所以FILE结构体维护的输出缓冲区中的内容不知道该刷新到哪一个文件的内核缓冲区,这部分内容就丢失了。

解决思路: 只要在执行close(1);这行代码之前,把FILE结构体维护的输出缓冲区中的内容刷新到 显示器文件的内核缓冲区,就能在屏幕上看到输出结果。有两种解决方案:
(1)使用fflush函数强制刷新该FILE结构体维护的语言级缓冲区
在这里插入图片描述

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess
one piece[zh@iZbp1dr1jtgcuih41mw88oZ test]$

(2)在输出的字符串末尾加上 ‘\n’

在这里插入图片描述
执行printf(“%s”,“one piece\n”);这行代码时,printf函数先把内容输出到stdout指针指向的FILE结构体维护的输出缓冲区(语言级缓冲区)中,然后该FILE结构体检查输出缓冲区的内容是满足行缓冲刷新策略(有换行符 ‘\n’ ),FILE结构体立刻将输出缓冲区中换行符(含换行符)前的内容刷新到文件描述符1指向文件的内核缓冲区。

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess
one piece
  1. 普通文件的全缓冲刷新策略示例:
#include <stdio.h>    
#include <stdlib.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <fcntl.h>    int main()    
{    close(1);    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);    if (fd < 0)    {    perror("open");    return 0;    }    printf("%s", "one piece\n");  close(fd);    return 0;    
}     

运行结果,没有在屏幕上打印任何数据,而且文件log.txt中没有任何内容:

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat log.txt

解释: printf 默认向标准输出流stdout输出内容,而stdout指针指向的FILE结构体中封装的 文件描述符内容是1,前两行代码进行了输出重定向,所以文件描述符1指向普通文件log.txt,这也就意味着stdout指针指向的FILE结构体采用全缓冲刷新策略。
执行printf(“%s”,“one piece\n”);这行代码时,printf函数先把内容输出到stdout指针指向的FILE结构体维护的输出缓冲区(语言级缓冲区)中,然后该FILE结构体检查输出缓冲区的内容是否符合全缓冲刷新策略,显然不满足,缓冲区没满,printf写入的内容暂存在FILE结构体维护的输出缓冲区。
执行close(1);这行代码时,文件描述符1被关闭,不再指向任何文件。
最后进程退出时,会主动刷新所有语言级缓冲区。stdout指针指向的FILE结构体中封装的 文件描述符内容是1,文件描述符1已经被关闭,所以FILE结构体维护的输出缓冲区中的内容不知道该刷新到哪一个文件的内核缓冲区,这部分内容就丢失了。

两种解决方案:
(1)使用fflush函数强制刷新该FILE结构体维护的语言级缓冲区

int main()                                                                                                                                  
{                                                                                                                                           close(1);                                                                                                                               int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);                                                                           if (fd < 0)                                                                                                                             {                                                                                                                                       perror("open");                                                                                                                     return 0;                                                                                                                           }                                                                                                                                       printf("%s", "one piece\n");                                                                                                            fflush(stdout);                                                                                                                                          close(fd);                                  return 0;                                   
}  

内容写入到文件log.txt中:

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat log.txt
one piece

(2)结尾不使用close(fd) 关闭文件描述符1

int main()                                                                                                                                  
{                                                                                                                                           close(1);                                                                                                                               int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);                                                                           if (fd < 0)                                                                                                                             {                                                                                                                                       perror("open");                                                                                                                     return 0;                                                                                                                           }                                                                                                                                       printf("%s", "one piece\n");                                                                                                                                        return 0;                                   
}  
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat log.txt
one piece
  1. 标准错误流stderr的无缓冲刷新策略示例:
    (标准错误流stderr指向的FILE结构体中封装的 文件描述符内容是2,它始终采取无缓冲刷新策略,无论 文件描述符2 指向普通文件 还是 显示器文件)
    (1)文件描述符2 指向显示器文件
int main()    
{    perror("one piece"); // perror函数默认向标准错误流stderr输出内容  close(2);    return 0;    
} 
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess
one piece: Success

(2)文件描述符2 指向普通文件

int main()    
{    close(2);    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);    if (fd < 0)    {    perror("open");    return 0;    }    perror("one piece");                                                                                                                                     close(fd);    return 0;    
}
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat log.txt
one piece: Success

补充知识:要调用系统调用才可以把 语言级缓冲区中的内容写入到 文件内核缓冲区,一旦数据写入到文件内核缓冲区,在用户层面上就认为写入工作完成了。 Linux内核(操作系统)会在合适的时机把文件内核缓冲区中的内容刷新到硬件设备中,这是操作系统的工作,在此我们暂不考虑这个方面。

2.以输入(读取数据)视角介绍缓冲区

  1. 语言级缓冲区
  • 位置:用户空间内存
  • 读取流程
    1. 当调用 fread(), fgets() 等 C语言库函数时
    2. 先检查 语言级缓冲区是否有足够数据
    3. 如果语言缓冲区有足够数据,直接从语言缓冲区复制到用户内存(语言缓冲区有足够数据,数据读取到这一步就结束了;语言缓冲区数据不足就接着执行后续步骤)
    4. 如果语言缓冲区数据不足,触发 read() 系统调用填充语言缓冲区(文件内核缓冲区有足够内容,直接读取文件内核缓冲区中内容填充语言缓冲区;文件内核缓冲区数据不够,触发磁盘 I/O,从磁盘读取数据内容到文件内核缓冲区,然后再读取文件内核缓冲区中的内容填充语言缓冲区)
    5. 语言缓冲区被填充之后,再从语言缓冲区复制到用户内存
// 读取示例
FILE *fp = fopen("data.txt", "r");
char buffer[1024];// 首次读取:语言级缓冲区为空,触发系统调用填充整个语言级缓冲区(大小为1024字节)
fread(buffer, 1, 512, fp); // 后续读取:语言级缓冲区有足够数据,直接从语言级缓冲区获取数据
fread(buffer + 512, 1, 512, fp);

第一次读取只需要512字节数据,为什么触发系统调用会直接把语言级缓冲区填充满呢?
这是因为系统调用是需要成本的,我们要尽可能减少系统调用的使用,这样能提高效率。
触发系统调用填充语言级缓冲区时,直接把语言级缓冲区填满无疑能最大化减少系统调用的使用。语言缓冲区的内容足够多,下一次进行读取时,语言级缓冲区就更可能拥有足够数据,直接从语言级缓冲区获取数据,就不会触发系统调用填充语言级缓冲区。

  1. 内核缓冲区
  • 位置:内核空间内存
  • 读取流程
    1. 当调用 read() 系统调用
    2. Linux内核检查文件内核缓冲区中是否有足够的数据
    3. 如果内核缓冲区中有足够的数据,直接复制到用户内存(内核缓冲区有足够数据,数据读取到这一步就结束了;内核缓冲区数据不足就接着执行后续步骤)
    4. 如果内核缓冲区中没有足够的数据,触发磁盘 I/O,从磁盘读取数据内容到文件内核缓冲区
    5. 内核缓冲区被填充之后,再从内核缓冲区复制到用户内存
      // 注:语言级缓冲区 也属于用户内存!

注意:当多次读取同一文件时,首次读取可能较慢(需磁盘 I/O),后续读取会更快(第一次读取,进行磁盘 I/O时会读取大量的数据到内核缓冲区,因为磁盘 I/O速度极慢,使用一次尽可能多读取数据存放到内核缓冲区,减少后续磁盘 I/O的使用。内核缓冲区中缓冲了大量数据,后续读取就很少需要使用磁盘 I/O,所有读取速度就更快了)。

3.语言级缓冲区 和 内核级缓冲区存在的意义

在 Linux 系统中,语言级缓冲区(如 C 标准库的 stdio 缓冲区)和内核级缓冲区(如内核的页缓存 Page Cache)的存在具有核心意义,主要体现在以下方面:

  1. 减少系统调用次数 和 磁盘 I/O次数,提升性能:

    • 语言级缓冲区: 应用程序的读写操作(如 printf, fwrite, fread)并非每次都直接调用 write/read 系统调用。数据先被累积在应用进程内存中的语言级缓冲区里。当缓冲区满、遇到换行符(行缓冲)、或显式调用 fflush(全缓冲)时,才将整块数据通过一次系统调用写入内核缓冲区或从内核缓冲区读取。这极大减少系统调用次数(执行一次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗⼀定的CPU时间),大大提升性能。
    • 内核级缓冲区: 当应用程序调用 write 时,数据通常只是复制到内核缓冲区就“返回成功”了,实际的磁盘写入由内核在后台异步完成。同样,read 会优先从内核缓冲区读取数据,如果数据已在内核缓冲区中,则无需访问慢速的物理磁盘,这减少了磁盘 I/O次数,大大提升性能。
  2. 解耦执行速度差异,平滑 I/O:

    • 写操作: 应用程序产生数据的速度可能远快于磁盘写入速度。语言级缓冲区允许应用程序快速“交付”数据后继续执行,而不必阻塞等待慢速的磁盘写入。内核级缓冲区进一步接收这些数据块,并在后台以更高效的方式(合并相邻写请求、按磁盘块大小对齐)写入磁盘。
    • 读操作: 应用程序需要数据时,内核级缓冲区可能已经预读或缓存了所需数据(或更多相邻数据),使得应用程序的 read 调用速度更快(直接读取内核缓冲区的数据),无需等待缓慢的磁盘I/O。语言级缓冲区则可能一次性从内核读入更多数据供后续使用。
  3. 提高资源利用率:

    • 批量处理数据(无论是应用层批量写入内核,还是内核批量写入磁盘。实现批量处理数据的原理:缓冲区能实现缓存多次数据,然后一次刷新)比逐字节操作高效得多。它减少了中断次数、优化了磁盘访问模式(顺序写代替随机写)、提高了 CPU 和磁盘的利用率。
  4. 保证数据安全(内核缓冲区):

    • 虽然内核缓冲区延迟了磁盘写入,但内核提供了机制确保关键数据的持久化:
      • 同步写入 (O_SYNC, fsync, fdatasync): 强制要求数据落盘后才返回,确保数据安全,但牺牲性能。
      • 事务和日志文件系统: 依赖内核缓冲机制,但通过日志记录等保证元数据一致性,在崩溃后能恢复。
    • 语言级缓冲区本身不保证持久化fflush 只保证数据从语言缓冲区传递到了内核缓冲区,数据是否落盘仍需依赖内核机制(如 fsync:它能强制将内核缓冲区的数据刷新到磁盘)。

总结关键意义:

  • 语言级缓冲区: 主要目标是减少用户态与内核态之间的上下文切换和系统调用次数,提升应用程序自身的执行效率。它是应用程序和内核 I/O 系统之间的第一道加速层。
  • 内核级缓冲区: 主要目标是减少物理磁盘 I/O 操作次数、优化磁盘访问模式、利用内存速度掩盖磁盘延迟,提升整个系统的 I/O 吞吐量和响应速度。它是内存与磁盘之间的关键缓存层。

两者协同工作,通过在不同层次进行缓冲,有效解决了应用程序执行速度、内存访问速度与磁盘 I/O 速度之间的巨大鸿沟,是 Linux 高性能 I/O 的基石。

4.一道综合性习题(结合重定向、缓冲区 和 写实拷贝)

#include <stdio.h>      
#include <stdlib.h>                                                           
#include <unistd.h>      
#include <string.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, strlen(msg1), 1, stdout);                  write(1, msg2, strlen(msg2));               fork(); // 这道题的要点                                    return 0;                                                                 
}    

(1)直接运行程序,结果很正常:

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess
hello printf
hello fwrite
hello write

(2)运行程序时进行重定向:

[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess >log.txt
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ cat log.txt
hello write
hello printf
hello fwrite
hello printf
hello fwrite

文件描述符1进行重定向后,指向文件log.txt
write是系统调用,它直接把数据写入到文件内核缓冲区
printf函数默认向标准输出流stdout输出,用户指定fwrite函数向标准输出流stdout输出。stdout指针指向的FILE结构体中封装的 文件描述符内容是1,文件描述符1指向普通文件log.txt,这也就意味着stdout指针指向的FILE结构体采用全缓冲刷新策略。
所以执行 printf(“%s”, msg0); 和 fwrite(msg1, strlen(msg1), 1, stdout); 这两行代码后,printf函数和fwrite函数输出的内容都暂时缓存在 stdout指针指向的FILE结构体 的输出缓冲区(语言级缓冲区)中。
执行 fork(); 这行代码创建子进程(共享父进程的代码和数据),子进程也能看到 stdout指针指向的FILE结构体 的输出缓冲区(语言级缓冲区)中的内容。
因为接下来就是 return 语句,所以父进程 和 子进程几乎同时结束(有先后顺序),进程结束时会刷新 所有语言级缓冲区中的数据,父子进程都退出,FILE结构体 的输出缓冲区(语言级缓冲区)中的内容被刷新了两次(进程间共享的数据,一个进程进行修改时会发生写实拷贝,数据会分离,进程间数据是具有独立性的,一个进程修改数据不会影响其它进程)

5.模拟实现 fopen、fwrite、fflush、fclose等函数(函数的实现中有具体的缓冲区刷新过程)

  • my_stdio.h
#define SIZE 1024
#define FLUSH_NONE 0 // 无缓冲刷新策略
#define FLUSH_LINE 1 // 行缓冲刷新策略
#define FLUSH_FULL 2 // 全缓冲刷新策略struct IO_FILE // 模拟struct FILE
{int flag; // 刷新⽅式int fileno; // ⽂件描述符char outbuffer[SIZE]; // 输出缓冲区int cap; // 容量int size; // 实际大小
};typedef struct IO_FILE mFILE;
mFILE* mfopen(const char* filename, const char* mode);
int mfwrite(const void* ptr, int num, mFILE* stream);
void mfflush(mFILE* stream);
void mfclose(mFILE* stream);
  • my_stdio.c
#include "my_stdio.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>mFILE* mfopen(const char* filename, const char* mode) // 模拟fopen函数
{int fd = -1;if (strcmp(mode, "r") == 0){fd = open(filename, O_RDONLY);}else if (strcmp(mode, "w") == 0){fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);}else if (strcmp(mode, "a") == 0){fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);}if (fd < 0) // 文件描述符小于0,证明open系统调用打开文件失败return NULL;mFILE* mf = (mFILE*)malloc(sizeof(mFILE));if (!mf){close(fd);return NULL;}mf->fileno = fd;mf->flag = FLUSH_LINE; // 简化一下实现,默认都采用行刷新策略(实际要根据文件描述符fd指向的文件类型来确定语言缓冲区刷新策略)mf->size = 0;mf->cap = SIZE;return mf;
}int mfwrite(const void* ptr, int num, mFILE* stream) // 模拟fwrite函数
{// 1. 先把内容写入到语言级缓冲区memcpy(stream->outbuffer + stream->size, ptr, num);stream->size += num;// 2. 检测是否要刷新语言级级缓冲区内容到内核级缓冲区// (不同的刷新策略有不同的刷新条件,我们实现的是简化版,只判断行刷新策略的刷新条件)if (stream->flag == FLUSH_LINE && stream->size > 0 && stream -> outbuffer[stream->size - 1] == '\n'){mfflush(stream);}return num;
}void mfflush(mFILE* stream) // 模拟fflush函数
{if (stream->size > 0){// 把语言级缓冲区保存的内容写到内核⽂件的⽂件缓冲区中!write(stream->fileno, stream->outbuffer, stream->size);stream->size = 0;}
}void mfclose(mFILE* stream) // 模拟fclose函数
{if (stream->size > 0){mfflush(stream);}close(stream->fileno);free(stream);
}
  • main.c
#include "my_stdio.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>int main()
{mFILE* fp = mfopen("./log.txt", "a");if (fp == NULL){return 1;}int cnt = 10;while (cnt){printf("write %d\n", cnt);char buffer[64];snprintf(buffer, sizeof(buffer), "hello message, number is : %d", cnt);cnt--;mfwrite(buffer, strlen(buffer), fp);mfflush(fp);sleep(1); // 方便观察现象}mfclose(fp);
}

http://www.dtcms.com/a/292387.html

相关文章:

  • Git使用git graph插件回滚版本
  • 【自定义一个简单的CNN模型】——深度学习.卷积神经网络
  • 大气能见度监测仪:洞察大气 “清晰度” 的科技之眼
  • 智慧教室:科技赋能,奏响个性化学习新乐章
  • MyBatis拦截器插件:实现敏感数据字段加解密
  • 中国科技信息杂志中国科技信息杂志社中国科技信息编辑部2025年第14期目录
  • 「芯生态」杰发科技AC7870携手IAR开发工具链,助推汽车电子全栈全域智能化落地
  • Vue中最简单的PDF引入方法及优缺点分析
  • docker build 和compose 学习笔记
  • CASB架构:了解正向代理、反向代理和API扫描
  • [转]Rust:过程宏
  • JMeter 实现 Protobuf 加密解密
  • AI 音频产品开发模板及流程(一)
  • 网络安全第三次作业搭建前端页面并解析
  • allegro 16.6配置CIS库报错 ORCIS-6129 ORCIS-6469
  • LeetCode 658.找到K个最接近的元素
  • .NET使用EPPlus导出EXCEL的接口中,文件流缺少文件名信息
  • Unity笔记——事件中心
  • 力扣-300.最长递增子序列
  • 以太坊网络发展分析:技术升级与市场动态的双重驱动
  • 快手开源 Kwaipilot-AutoThink 思考模型,有效解决过度思考问题
  • Cy3-COOH 花菁染料Cy3-羧基
  • linux-日志服务
  • Gitlab-CI实现组件自动推送
  • 常用 Flutter 命令大全:从开发到发布全流程总结
  • 检索增强型生成助力无人机精准数学推理!RAG-UAV:基于RAG的复杂算术推理方法
  • Lua语言
  • MybatisPlus-16.扩展功能-枚举处理器
  • ORACLE DATABASE 11.2.0.4 RAC Install
  • Vue-22-通过flask接口提供的数据使用plotly.js绘图(一)