Linux中输入和输出基本过程
1.文件内核级缓冲区
前面在如何理解Linux一切皆文件的特点中提到为了保证在Linux中所有进程访问文件时的方式趋近相 同,在f ile 结构体中存在一个 files_operations 结构体指针,对应的结构体保存所有文件操作的函 数指针(这个结构体也被称为操作表)
每一个f ile 结构体中除了有自己的操作表以外还有一个文件的内核级缓冲区,这个缓冲区不同于语言层 面的缓冲区,在调用底层系统接口的读或者写时,会有一方先将内容保存到该缓冲区,再将内容移动到 指定设备
例如,对于读函数 read 来说,以从磁盘文件读取文件内容为例,首先操作系统会从磁盘中读取文件内容 到缓冲区,再由 read 函数从缓冲区将内容读取到指定的设备;对于 将内存中需要写入的内容先写入缓冲区,再由 write 函数来说,首先操作系统会 write 函数从缓冲区写入到磁盘中。除了前面提到的两个 单一过程外,还要一种复合过程:对文件的内容进行修改。这一种过程既涉及到读取,也涉及到写入, 所以首先需要通过操作系统读入需要修改的内容到内核级缓冲区,由 read 函数将缓冲区中的数据读到内 存,在程序运行中对读取到的内容进行修改,再通过 write 函数将修改后的内容写入到缓冲区,最后由 操作系统从缓冲区写入到磁盘
如果抽象化一下 read 函数和 write 函数的过程可以发现这两个函数的基本行为就是读写缓冲区,所以 这两个函数也可以理解为拷贝函数, read 函数即为将缓冲区中的数据拷贝到内存中的指定位置, write 函数即为将内存中的数据拷贝到缓冲区
上面整个过程中, read 函数和 write 函数只完成对缓冲区中的数据进行处理,但是缓冲区中的数据何 时移动到指定的设备由操作系统自主决定
如果用户向自主决定何时将内核级缓冲区中的数据刷新到内核级缓冲区可以使用
fsync
函数,其原型如下:
int fsync(int fd);
读或者写过程示意图如下:
2.何为重定向
前面提到,文件描述符是Linux中每一个文件的唯一标识符,也就是说,通过文件描述符可以唯一确定一 个文件,而stdin、stdout和stderr是每一个C语言程序默认打开的三个文件,对应的文件描述符 为0、1和2,而之所以文件描述符是从0开始,本质是因为文件描述符是fd_array数组的下标,当用户 再打开一个文件时,对应的文件结构指针就会存储到fd_array的指定位置,而因为下标0、1和2已经被 占用,所以新开的文件对应的下标只能从3开始
现在关闭0号位置的文件,观察效果,例如下面的代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{close(0);int fd1 = open("test1.txt", O_WRONLY | O_TRUNC | O_CREAT);int fd2 = open("test2.txt", O_WRONLY | O_TRUNC | O_CREAT);int fd3 = open("test3.txt", O_WRONLY | O_TRUNC | O_CREAT);int fd4 = open("test4.txt", O_WRONLY | O_TRUNC | O_CREAT);printf("%d\n", fd1);printf("%d\n", fd2);printf("%d\n", fd3);printf("%d\n", fd4);close(fd1);close(fd2);close(fd3);close(fd4);return 0;
}
如果关闭的是2号文件,观察效果,例如下面的代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{close(2);int fd1 = open("test1.txt", O_WRONLY | O_TRUNC | O_CREAT);int fd2 = open("test2.txt", O_WRONLY | O_TRUNC | O_CREAT);int fd3 = open("test3.txt", O_WRONLY | O_TRUNC | O_CREAT);int fd4 = open("test4.txt", O_WRONLY | O_TRUNC | O_CREAT);printf("%d\n", fd1);printf("%d\n", fd2);printf("%d\n", fd3);printf("%d\n", fd4);close(fd1);close(fd2);close(fd3);close(fd4);return 0;
}
运行:
从上面的代码中可以看出,在Linux中,一个文件被打开时,文件描述符的分配原则是在 fd_array 中找 到最小的且没有被其他 file 结构指针占用的位置
以关闭0号位置为例,示意图如下:
在上面的演示中,先关闭了某一个文件描述符较前的文件,再打开另一个文件,打开的这个文件所处的 位置是就是被关闭的那个文件的位置,此时再使用输出语句输出内容,原本应该输出到文件描述符较前 的原始文件中,比如前面的 stdin ,后面却输出到了文件中 test1.txt ,这个过程就称为文件重定 向,主要原理就是文件描述符是固定不变的,而输出和输入只认文件描述符,不会在意这个文件描述符 对应的位置指向的是哪一个文件
在Linux中,存在一个系统调用接口使得可以在程序中实现重定向,这个函数为dup2
,其原型如下:
int dup2(int oldfd, int newfd);
在Linux操作手册中, dup2 函数的描述如下:
dup2() makes newfd be the copy of oldfd, closing newfd first if necessary
参数解释:
oldfd
:该参数表示待拷贝的文件的文件指针对应的文件描述符newfd
:该参数表示被覆盖的文件的文件指针对应的文件描述符
示意图如下:
所以,使用dup2就可以实现使用printf函数本应该输出到stdout中,而输出到test1.txt的效 果,代码如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>int main()
{int fd1 = open("test1.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);int fd2 = open("test2.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);int fd3 = open("test3.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);int fd4 = open("test4.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);dup2(fd1, 1);printf("%d\n", fd1);printf("%d\n", fd2);printf("%d\n", fd3);printf("%d\n", fd4);close(fd1);close(fd2);close(fd3);close(fd4);return 0;
}
需要注意,因为在使用 dup2 函数之前, stdout 已经被打开了,此时 open 函数打开 时就只能使用3号下标的位置,所以可以看到 test1.txt 文件 dup2 函数的确实现了将内容输出到 test1.txt 文件而不 是st dout 中,此时示意图如下:
这里需要注意一个细节:尽管 dup2 函数会关闭被覆盖的文件 test1.txt 占用了被覆盖的文件 stdout ,但是关闭后被拷贝的文件 stdout 的位置,所以再打开其他文件依旧会从7号位置开始,例如下 面的代码演示:
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>int main()
{int fd1 = open("test1.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);int fd2 = open("test2.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);int fd3 = open("test3.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);int fd4 = open("test4.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);dup2(fd1, 1);int fd5 = open("test4.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666); // 重定向后打开一个新文件printf("%d\n", fd1);printf("%d\n", fd2);printf("%d\n", fd3);printf("%d\n", fd4);printf("%d\n", fd5);close(fd1);close(fd2);close(fd3);close(fd4);close(fd5);return 0;
}
但是,前面的代码中,只演示了关闭0号位置和2号位置的文件,如果此时关闭1号位置的文件,再打开同 样的四个文件,输出每一个文件的文件描述符,从前面的规律可以推出,输出语句会因为1号位置的文件 是test1.txt而将内容输出到test1.txt中,例如下面的代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{close(1);int fd1 = open("test1.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);int fd2 = open("test2.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);int fd3 = open("test3.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);int fd4 = open("test4.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);printf("%d\n", fd1);printf("%d\n", fd2);printf("%d\n", fd3);printf("%d\n", fd4);close(fd1);close(fd2);close(fd3);close(fd4);return 0;
}
编译运行上面的代码后可以看到不论是test1.txt还是其他文件都没有显示需要的内容,控制台也没有 正常打印需要的内容
但是如果将代码修改为如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{// ...fflush(stdout);close(fd1);close(fd2);close(fd3);close(fd4);return 0;
}
编译运行上面的代码后可以看到结果如下:
现在就引入了一个问题,为什么加了fflush(stdout)就可以看到需要的效果
前面提到,在调用操作系统接口中的读写函数时会涉及到一个文件内核级缓冲区,这个缓冲区是为了减 少操作系统进行IO操作时产生的时间和空间的消耗,但是如果用户使用的是语言级别的接口,例如 fread或者fwrite等,那么此时就会出现用户的接口到操作系统的消耗,这个消耗主要来自于语言级 函数多次调用系统接口时创建函数栈帧,所以为了减少这种情况下的时间和空间消耗,就存在了语言级 别的缓冲区
但是,在实现进度条时提到了\n可以刷新语言级别的缓冲区,此处为什么用了\n也没有刷新,原因就 在于语言级别的缓冲区刷新情况有三种:
- 行刷新,主要是
stdout
文件时的刷新 - 满刷新,当语言级别的缓冲区写满时刷新
- 不缓冲
而此处的\n就属于行刷新,但是此时的printf认识的fd为1的文件并不是stdout,所以行刷新就 失效了,转换成默认的满刷新,所以需要调用fflush(stdout)将当前语言级别缓冲区中的数据调用写 函数刷新到操作系统的内核级缓冲区,再由操作系统自主刷新到输出设备中
如果将前面的代码修改为如下,观察效果:
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>int main()
{close(1);int fd1 = open("test1.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);int fd2 = open("test2.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);int fd3 = open("test3.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);int fd4 = open("test4.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);printf("%d\n", fd1);printf("%d\n", fd2);printf("%d\n", fd3);printf("%d\n", fd4);return 0;
}
编译运行上面的代码后可以看到结果如下:
可以看到此时尽管没有fflush(stdout)也可以正常将内容输出到test1.txt,主要原因是当程序即 将结束时,会进行缓冲区自动刷新,相当于自动进行了一次fflush(stdout),而前面调用 close(fd)时之所以没有刷新缓冲区是因为系统内部的文件已经被关闭,语言级的缓冲区无法将内容通 过系统调用刷新到系统内核级缓冲区
所以,由上面的结果可以推出,在C语言中,文件结构FILE肯定包含文件描述符和缓冲区,而C语言的 所有输入输出函数本质也是拷贝函数,只是源头或者目标是其结构体中的缓冲区,对应的源码如下:
struct _IO_FILE {int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags//缓冲区相关/* The following pointers correspond to the C++ streambuf protocol. *//* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */char* _IO_write_base; /* Start of put area. */char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. *//* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */struct _IO_marker *_markers;struct _IO_FILE *_chain;int _fileno; //封装的文件描述符#if 0int _blksize;
#elseint _flags2;
#endif_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary *//* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1];/* char* _save_gptr; char* _save_egptr; */_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
3.子进程与缓冲区
前面演示的都是只有一个进程打开关闭文件,如果此时在自动刷新语言级别的缓冲区之前创建了一个子 进程,观察下面代码的执行效果:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);dup2(fd, stdout->_fileno);// C库函数printf(" hello printf\n");fprintf(stdout, " hello fprintf\n");const char *message = " hello fwrite\n";fwrite(message, 1, strlen(message), stdout);// 系统调用const char *w = " hello write\n";write(1, w, strlen(w));// 创建子进程fork();return 0;
}
查看 log.txt 的内容
可以看到,除了系统调用接口 write 函数以外,其他的C语言库函数均出现两次打印的情况,本质就是 因为语言级缓冲区自动刷新到系统内核级缓冲区机制,而因为 fork 创建了子进程,所以子进程在不修改 的情况下会共享父进程中的所有内容,包括语言级缓冲区,所以在最后程序结束时,因为父进程没有刷 新语言级缓冲区,所以创建子进程之后,父子进程都要进行一次刷新操作,所以父子进程都通过系统调 用接口将数据写入到内核级缓冲区中,而之所以 write 函数没有写入两次,就是因为父进程已经调用完 成write函数了,系统内核级缓冲区已经有对应的内容,所以此时子进程就不需要再调用了,最后由操 作系统决定将所有内容刷新到文件中
相关文章:

Linux中输入和输出基本过程
1.文件内核级缓冲区 前面在如何理解Linux一切皆文件的特点中提到为了保证在Linux中所有进程访问文件时的方式趋近相 同,在f ile 结构体中存在一个 files_operations 结构体指针,对应的结构体保存所有文件操作的函 数指针(这个结构体也被称为…...
使用 acme.sh 签发和自动续期 ssl https 证书
acme.sh 是一个热度非常高的签发和自动续期 https 证书的工具,虽然官网上提供了充分的操作说明,但是不够简洁,本文以在 nginx 中签发和配置http 为例,列出必要的几个简单步骤。 安装 因为网络原因,github 大部分人是…...
spring重点面试题总结
bean的生命周期 在 Spring 中,BeanDefinition、Bean 实例化、依赖注入、Aware 接口的处理、以及 BeanPostProcessor 的前置和后置处理等,都是 Spring 容器管理 Bean 生命周期的关键部分。下面我将详细解释这些过程。 1. 通过 BeanDefinition 获取 Bean…...

新的一章:codegeex
三层结构的优点:可扩展性,可复用性...

游戏引擎学习第50天
仓库: https://gitee.com/mrxiao_com/2d_game Minkowski 这个算法有点懵逼 回顾 基本上,现在我们所处的阶段是,回顾最初的代码,我们正在讨论我们希望在引擎中实现的所有功能。我们正在做的版本是初步的、粗略的版本,涵盖我们认…...

快速理解类的加载过程
当程序主动使用某个类时,如果该类还未加载到内存中,则系统会通过如下三个步骤来对该类进行初始化: 1.加载:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个…...

医院跌倒检测识别 使用YOLO,COCO ,VOC格式对4806张原始图片进行标注,可识别病人跌倒,病人的危险行为,病床等场景,预测准确率可达96.7%
医院跌倒检测识别 使用YOLO,COCO ,VOC格式对4806张原始图片进行标注,可识别病人跌倒,病人的危险行为,病床等场景,预测准确率可达96.7% 数据集分割 4806总图像数 训练组70% 3364图片 有效集20&#…...
[Unity Shader] 【游戏开发】【图形渲染】Unity Shader的种类2-顶点/片元着色器与固定函数着色器的选择与应用
Unity 提供了不同种类的 Shader,每种 Shader 有其独特的优势和适用场景。在所有类型的 Shader 中,顶点/片元着色器(Vertex/Fragment Shader)与固定函数着色器(Fixed Function Shader)是两种重要的着色器类型。尽管它们具有不同的编写方式和用途,理解其差异与应用场景,对…...
浏览器端的 js 包括哪几个部分
一、核心语言部分 1. 变量与数据类型 变量用于存储数据,在 JavaScript 中有多种数据类型,如基本数据类型(字符串、数字、布尔值、undefined、null)和引用数据类型(对象、数组、函数)。 let name "…...

GoogLeNet网络:深度学习领域的创新之作
目录 编辑 引言 GoogLeNet的核心创新:Inception模块 Inception模块的工作原理 1x1卷积:降维与减少计算量 1x1卷积的优势 深度分离卷积:计算效率的提升 深度分离卷积的实现 全局平均池化:简化网络结构 全局平均池化的作…...

深入C语言文件操作:从库函数到系统调用
引言 文件操作是编程中不可或缺的一部分,尤其在C语言中,文件操作不仅是处理数据的基本手段,也是连接程序与外部世界的重要桥梁。C语言提供了丰富的库函数来处理文件,如 fopen、fclose、fread、fwrite 等。然而,这些库…...
Java序列化
Java序列化 简单来说: 序列化是将对象的状态信息转换为可以存储或传输的形式(如字节序列)的过程。在 Java 中,通过序列化可以把一个对象保存到文件、通过网络传输到其他地方或者存储到数据库等。最直接的原因就是某些场景下需要…...

基坑表面位移沉降倾斜自动化监测 非接触式一体化解决机器视觉
基于变焦视觉位移监测仪的基坑自动化监测新方案是一种集成了光学、机械、电子、边缘计算、AI识别以及云平台软件等技术的自动化系统。该方案利用变焦机器视觉原理,结合特殊波段成像识别技术和无源靶标,实现了非接触式大空间、多断面、多测点的高精度水平…...
提升效率:精通Windows命令行的艺术
文章目录 引言1. 基本目录操作命令dir:列出目录内容cd:更改目录mkdir 和 rmdir:创建和删除目录 2. 文件操作命令copy:复制文件或目录move:移动或重命名文件/目录del:删除文件 3. 文件查看命令typeÿ…...

ESP32-S3-devKitC-1 点亮板上的WS2812 RGB LED
ESP32-S3-devKitC-1 板上自带了一个RGB LED,型号为 WS2812。 RGB LED 在板上的位置如下图所示。 为了点亮这个WS2812,需要确定这颗RGB LED连接到哪个GPIO上了。 下面是确定GPIO管脚的过程: 1、根据原理图 2、根据PCB布局图: 程…...

python调用matlab函数(内置 + 自定义) —— 安装matlab.engine
文章目录 一、简介二、安装matlab.engine2.1、基于 CMD 安装2.2、基于 MATLAB 安装(不建议) 三、python调用matlab函数(内置 自定义) 一、简介 matlab.engine(MATLAB Engine API for Python):…...

CAD c# 生成略缩图预览
代码如下: using (Transaction tr currentdb.TransactionManager.StartTransaction()){//当前数据库开启事务using (Database tempdb new Database(false, true)) //创建临时数据库(两个参数:是否创建符号表,不与当前文档关联){try{Bitmap …...

端点鉴别、安全电子邮件、TLS
文章目录 端点鉴别鉴别协议ap 1.0——发送者直接发送一个报文表明身份鉴别协议ap 2.0——ap1.0 的基础上,接收者对报文的来源IP地址进行鉴别鉴别协议ap 3.0——使用秘密口令,口令为鉴别者和被鉴别者之间共享的秘密鉴别协议ap 3.1——对秘密口令进行加密&…...

汽车电子元件的可靠性保障:AEC-Q102认证
AEC-Q102标准的起源与价值 随着汽车电子系统的日益复杂,电子器件必须能够在极端的温度、湿度、振动和电磁干扰等恶劣条件下保持性能。AEC-Q102标准由汽车电子委员会(AEC)制定,专门针对LED、激光二极管和光电二极管等光电器件&…...

主成分分析法大全(包括stata+matlab)
数据简介:主成分分析(Principal Component Analysis,PCA), 是一种统计方法。通过正交变换将一组可能存在相关性的变量转换为一组线性不相关的变量,转换后的这组变量叫主成分。在实际课题中,为了…...

【Java_EE】Spring MVC
目录 Spring Web MVC 编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 编辑参数重命名 RequestParam 编辑编辑传递集合 RequestParam 传递JSON数据 编辑RequestBody …...

12.找到字符串中所有字母异位词
🧠 题目解析 题目描述: 给定两个字符串 s 和 p,找出 s 中所有 p 的字母异位词的起始索引。 返回的答案以数组形式表示。 字母异位词定义: 若两个字符串包含的字符种类和出现次数完全相同,顺序无所谓,则互为…...
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别 直接训练提示词嵌入向量的核心区别 您提到的代码: prompt_embedding = initial_embedding.clone().requires_grad_(True) optimizer = torch.optim.Adam([prompt_embedding...

(转)什么是DockerCompose?它有什么作用?
一、什么是DockerCompose? DockerCompose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器。 Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。 DockerCompose就是把DockerFile转换成指令去运行。 …...
安卓基础(aar)
重新设置java21的环境,临时设置 $env:JAVA_HOME "D:\Android Studio\jbr" 查看当前环境变量 JAVA_HOME 的值 echo $env:JAVA_HOME 构建ARR文件 ./gradlew :private-lib:assembleRelease 目录是这样的: MyApp/ ├── app/ …...

RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)
RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发,后来由Pivotal Software Inc.(现为VMware子公司)接管。RabbitMQ 是一个开源的消息代理和队列服务器,用 Erlang 语言编写。广泛应用于各种分布…...

论文阅读:Matting by Generation
今天介绍一篇关于 matting 抠图的文章,抠图也算是计算机视觉里面非常经典的一个任务了。从早期的经典算法到如今的深度学习算法,已经有很多的工作和这个任务相关。这两年 diffusion 模型很火,大家又开始用 diffusion 模型做各种 CV 任务了&am…...

jdbc查询mysql数据库时,出现id顺序错误的情况
我在repository中的查询语句如下所示,即传入一个List<intager>的数据,返回这些id的问题列表。但是由于数据库查询时ID列表的顺序与预期不一致,会导致返回的id是从小到大排列的,但我不希望这样。 Query("SELECT NEW com…...

路由基础-路由表
本篇将会向读者介绍路由的基本概念。 前言 在一个典型的数据通信网络中,往往存在多个不同的IP网段,数据在不同的IP网段之间交互是需要借助三层设备的,这些设备具备路由能力,能够实现数据的跨网段转发。 路由是数据通信网络中最基…...
2.2.2 ASPICE的需求分析
ASPICE的需求分析是汽车软件开发过程中至关重要的一环,它涉及到对需求进行详细分析、验证和确认,以确保软件产品能够满足客户和用户的需求。在ASPICE中,需求分析的关键步骤包括: 需求细化:将从需求收集阶段获得的高层需…...