Linux文件IO(六)-多次打开同一个文件

news2024/9/21 14:28:59

大家看到这个小节标题可能会有疑问,同一个文件还能被多次打开?事实确实如此,同一个文件可以被多次打开,譬如在一个进程中多次打开同一个文件、在多个不同的进程中打开同一个文件,那么这些操作都是被允许的。本小节就来探讨下多次打开同一个文件会有一些什么现象以及相应的细节问题?

验证一些现象

一个进程内多次 open 打开同一个文件,那么会得到多个不同的文件描述符 fd,同理在关闭文件的

时候也需要调用 close 依次关闭各个文件描述符。

针对这个问题,我们编写测试代码进行测试,如下所示:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
		 int fd1, fd2, fd3;
		 int ret;

		 /* 第一次打开文件 */
		 fd1 = open("./test_file", O_RDWR);
		 if (-1 == fd1) {
				 perror("open error");
				 exit(-1);
		 }

		 /* 第二次打开文件 */
		 fd2 = open("./test_file", O_RDWR);
		 if (-1 == fd2) {
				 perror("open error");
				 ret = -1;
				 goto err1;
		}

		 /* 第三次打开文件 */
		 fd3 = open("./test_file", O_RDWR);
		 if (-1 == fd3) {
				 perror("open error");
				 ret = -1;
				 goto err2;
		 }

		 /* 打印出 3 个文件描述符 */
		 printf("%d %d %d\n", fd1, fd2, fd3);
		 close(fd3);
		 ret = 0;
		err2:
		 close(fd2);

err1:
	 /* 关闭文件 */
	 close(fd1);
	 exit(ret);
}

上述示例代码中,通过 3 次调用 open 函数对 test_file 文件打开了 3 次,每一个调用传参一样,最后将3 次得到的文件描述符打印出来,在当前目录下存在 test_file 文件,接下来编译测试,看看结果如何:

从打印结果可知,三次调用 open 函数得到的文件描述符分别为 6、7、8,通过任何一个文件描述符对文件进行 IO 操作都是可以的,但是需要注意是,调用 open 函数打开文件使用的是什么权限,则返回的文件描述符就拥有什么权限,文件 IO 操作完成之后,在结束进程之前需要使用 close 关闭各个文件描述符。

在图 中,细心的读者可能会发现,调用 open 函数得到的最小文件描述符是 6,在上一章节内容中给大家提到过,程序中分配得到的最小文件描述符一般是 3,但这里竟然是 6!这是为何?其实这个问题跟vscode 有关,说明 3、4、5 这 3 个文件描述符已经被 vscode 软件对应的进程所占用了,而当前这里执行testApp 文件是在 vscode 软件提供的终端下进行的,所以 vscode 可以认为是 testApp 进程的父进程,相反,testApp 进程便是 vscode 进程的子进程,子进程会继承父进程的文件描述符。关于子进程和父进程这些都是后面的内容,这里暂时不给大家进行介绍,这是只是给大家简单地解释一下,免得大家误会!其实可以直接在 Ubuntu 系统的 Terminal 终端执行 testApp,这时你会发现打印出来的文件描述符分别是 3、4、5,这里就不给大家演示了。

一个进程内多次 open 打开同一个文件,在内存中并不会存在多份动态文件。

当调用 open 函数的时候,会将文件数据(文件内容)从磁盘等块设备读取到内存中,将文件数据在内存中进行维护,内存中的这份文件数据我们就把它称为动态文件!这是前面给大家介绍的内容,这里再简单地提一下。这里出现了一个问题:如果同一个文件被多次打开,那么该文件所对应的动态文件是否在内存中也存在多份?也就是说,多次打开同一个文件是否会将其文件数据多次拷贝到内存中进行维护?

关于这个问题,各位读者可以简单地思考一下,这里我们直接编写代码进行测试,测试代码如下所示:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
		 char buffer[4];
		 int fd1, fd2;
		 int ret;

		 /* 创建新文件 test_file 并打开 */
		 fd1 = open("./test_file", O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
		 if (-1 == fd1) {
				 perror("open error");
				 exit(-1);
		 }

		 /* 再次打开 test_file 文件 */
		 fd2 = open("./test_file", O_RDWR);
		 if (-1 == fd2) {
				 perror("open error");
				 ret = -1;
				 goto err1;
		 }

		 /* 通过 fd1 文件描述符写入 4 个字节数据 */
		 buffer[0] = 0x11;
		 buffer[1] = 0x22;
		 buffer[2] = 0x33;
		 buffer[3] = 0x44;
		 ret = write(fd1, buffer, 4);
		 if (-1 == ret) {
				 perror("write error");
				 goto err2;
		 }

		 /* 将读写位置偏移量移动到文件头 */
		 ret = lseek(fd2, 0, SEEK_SET);
		 if (-1 == ret) {
				 perror("lseek error");
				 goto err2;
		 }


		 /* 读取数据 */
		 memset(buffer, 0x00, sizeof(buffer));
		 ret = read(fd2, buffer, 4);
		 if (-1 == ret) {
				 perror("read error");
				 goto err2;
		 }
		 printf("0x%x 0x%x 0x%x 0x%x\n", buffer[0], buffer[1],
		 buffer[2], buffer[3]);
		 ret = 0;

	err2:
		 close(fd2);

	err1:
		 /* 关闭文件 */
		 close(fd1);
		 exit(ret);
}

当前目录下不存在 test_file 文件,上述代码中,第一次调用 open 函数新建并打开 test_file 文件,第二次调用 open 函数再次打开它,新建文件时,文件大小为 0;首先通过文件描述符 fd1 写入 4 个字节数据(0x11/0x22/0x33/0x44),从文件头开始写;然后再通过文件描述符 fd2 读取 4 个字节数据,也是从文件头开始读取。假如,内存中只有一份动态文件,那么读取得到的数据应该就是 0x11、0x22、0x33、0x44,如果存在多份动态文件,那么通过 fd2 读取的是与它对应的动态文件中的数据,那就不是 0x11、0x22、0x33、0x44,而是读取出 0 个字节数据,因为它的文件大小是 0。

接下来进行编译测试:

 

上图中打印显示读取出来的数据是 0x11/0x22/0x33/0x44,所以由此可知,即使多次打开同一个文件,内存中也只有一份动态文件。

一个进程内多次 open 打开同一个文件,不同文件描述符所对应的读写位置偏移量是相互独立的。同一个文件被多次打开,会得到多个不同的文件描述符,也就意味着会有多个不同的文件表,而文件读写偏移量信息就记录在文件表数据结构中,所以从这里可以推测不同的文件描述符所对应的读写偏移量是相互独立的,并没有关联在一起,并且文件表中 i-node 指针指向的都是同一个 inode,如下图所示:

 测试的方法很简单,只需在示例代码中简单地修改即可,将 lseek 函数调用去掉,然后在编译测试,如果读出的数据依然是 0x11/0x22/0x33/0x44,则表示第三点结论成立,这里不再给大家演示。

Tips:多个不同的进程中调用 open()打开磁盘中的同一个文件,同样在内存中也只是维护了一份动态文件,多个进程间共享,它们有各自独立的文件读写位置偏移量。

动态文件何时被关闭呢?当文件的引用计数为 0 时,系统会自动将其关闭,同一个文件被打开多次,文件表中会记录该文件的引用计数,如图  所示,引用计数记录了当前文件被多少个文件描述符 fd 关联。

多次打开同一文件进行读操作与 O_APPEND 标志

重复打开同一个文件,进行写操作,譬如一个进程中两次调用 open 函数打开同一个文件,分别得到两个文件描述符 fd1 和 fd2,使用这两个文件描述符对文件进行写入操作,那么它们是分别写(各从各的位置偏移量开始写)还是接续写(一个写完,另一个接着后面写)?因为这两个文件描述符所对应的读写位置偏移量是相互独立的,所以是分别写,接下来我们还是编写代码进行测试,测试代码如下所示:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
		 unsigned char buffer1[4], buffer2[4];
		 int fd1, fd2;
		 int ret;
		 int i;

		 /* 创建新文件 test_file 并打开 */
		 fd1 = open("./test_file", O_RDWR | O_CREAT | O_EXCL,
		 S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
		 if (-1 == fd1) {
					 perror("open error");
					 exit(-1);
		 }

		 /* 再次打开 test_file 文件 */
		 fd2 = open("./test_file", O_RDWR);
		 if (-1 == fd2) {
				 perror("open error");
				 ret = -1;
				 goto err1;
		 }

		 /* buffer 数据初始化 */
		 buffer1[0] = 0x11;
		 buffer1[1] = 0x22;
		 buffer1[2] = 0x33;
		 buffer1[3] = 0x44;

		 buffer2[0] = 0xAA;
		 buffer2[1] = 0xBB;
		 buffer2[2] = 0xCC;
		 buffer2[3] = 0xDD;

		 /* 循环写入数据 */
		 for (i = 0; i < 4; i++) {
				 ret = write(fd1, buffer1, sizeof(buffer1));
				 if (-1 == ret) {
						 perror("write error");
						 goto err2;
				 }
		
				 ret = write(fd2, buffer2, sizeof(buffer2));
				 if (-1 == ret) {
						 perror("write error");
						 goto err2;
					 }
		 }

		 /* 将读写位置偏移量移动到文件头 */
		 ret = lseek(fd1, 0, SEEK_SET);
		 if (-1 == ret) {
				 perror("lseek error");
				 goto err2;
		 }

		 /* 读取数据 */
		 for (i = 0; i < 8; i++) {
				 ret = read(fd1, buffer1, sizeof(buffer1));
				 if (-1 == ret) {
						 perror("read error");
						 goto err2;
				 }

				 printf("%x%x%x%x", buffer1[0], buffer1[1],buffer1[2], buffer1[3]);
		 }
		 printf("\n");
		 ret = 0;
		
err2:
		 close(fd2);
		
err1:
		 /* 关闭文件 */
		 close(fd1);
		 exit(ret);
}

 重复两次打开 test_file 文件,分别得到两个文件描述符 fd1、fd2;首先通过 fd1 写入 4 个字节数据(0x11、0x22、0x33、0x44)到文件中,接着再通过 fd2 写入 4 个字节数据(0xaa、0xbb、0xcc、0xdd)到文件中,循环写入 4 此;最后再将写入的数据读取出来,将其打印到终端。如果它们是分别写,那么读取出来的数据就应该是 aabbccdd……,因为通过 fd1 写入的数据被 fd2 写入的数据给覆盖了;如果它们是接续写,那么读取出来的数据应该是 11223344aabbccdd……,接下里我们编译测试:

 

从打印结果可知,它们确实是分别写。如果想要实现接续写,也就是当通过 fd1 写入完成之后,通过 fd2写入的数据是接在 fd1 写入的数据之后,那么该怎么做呢?当然可以写入数据之前通过 lseek 函数将文件偏移量移动到文件末尾,如果是这样做,会存在一些问题,关于这个问题后面再给大家介绍;这里我们给大家介绍使用 O_APPEND 标志来解决这个问题,也就是将分别写更改为接续写。

前面给大家介绍了 open 函数的 O_APPEND 标志,当 open 函数使用 O_APPEND 标志,在使用 write 函数进行写入操作时,会自动将偏移量移动到文件末尾,也就是每次写入都是从文件末尾开始;这里结合本小节的内容,我们再来讨论 O_APPEND 标志,在多次打开同一个文件进行写操作时,使用 O_APPEND 标志会有什么样的效果,接下来进行测试:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
		 unsigned char buffer1[4], buffer2[4];
		 int fd1, fd2;
		 int ret;
		 int i;

		 /* 创建新文件 test_file 并打开 */
		 fd1 = open("./test_file", O_RDWR | O_CREAT | O_EXCL | O_APPEND, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
		 if (-1 == fd1) {
				 perror("open error");
				 exit(-1);
		 }

		 /* 再次打开 test_file 文件 */
		 fd2 = open("./test_file", O_RDWR | O_APPEND);
		 if (-1 == fd2) {
				 perror("open error");
				 ret = -1;
				 goto err1;
		 }

		 /* buffer 数据初始化 */
		 buffer1[0] = 0x11;
		 buffer1[1] = 0x22;
		 buffer1[2] = 0x33;
		 buffer1[3] = 0x44;

		 buffer2[0] = 0xAA;
		 buffer2[1] = 0xBB;
		 buffer2[2] = 0xCC;
		 buffer2[3] = 0xDD;

		 /* 循环写入数据 */
		 for (i = 0; i < 4; i++) {
				 ret = write(fd1, buffer1, sizeof(buffer1));
				 if (-1 == ret) {
						 perror("write error");
						 goto err2;
					}

				 ret = write(fd2, buffer2, sizeof(buffer2));
				 if (-1 == ret) {
						 perror("write error");
						 goto err2;
				 }
		 }

		 /* 将读写位置偏移量移动到文件头 */
		 ret = lseek(fd1, 0, SEEK_SET);
		 if (-1 == ret) {
				 perror("lseek error");
				 goto err2;
		 }

		 /* 读取数据 */
		 for (i = 0; i < 8; i++) {
				 ret = read(fd1, buffer1, sizeof(buffer1));
				 if (-1 == ret) {
						 perror("read error");
						 goto err2;
				 }

				 printf("%x%x%x%x", buffer1[0], buffer1[1],buffer1[2], buffer1[3]);
		 }
		 printf("\n");
		 ret = 0;
		
err2:
		 close(fd2);
		
err1:
		 /* 关闭文件 */
		 close(fd1);
		 exit(ret);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2152569.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PyRosetta打分函数介绍

在 PyRosetta 中,蛋白质结构的能量是通过打分函数(ScoreFunction)来评估的,这些打分函数基于 Rosetta 的能量方程。Rosetta 的能量函数是一种加权的分项能量表达式,包括不同的能量项来描述蛋白质的构象、相互作用和能量。核心能量函数的形式如下: 在 PyRosetta 中,打分函…

神经网络推理加速入门——一个例子看懂流水

之前的两篇文章介绍了流水这一技术&#xff0c;它用来进行程序的性能加速&#xff0c;本篇通过一个生活中的小例子&#xff0c;让大家更直观的了解什么是流水。 举个例子 早晨从起床到上班出门&#xff0c;我们一般会做以下几件事&#xff1a;刷牙、烧水、喝水、出门。 如果…

应届生必看 | 毕业第一份工作干销售好不好?

吉祥知识星球http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247485367&idx1&sn837891059c360ad60db7e9ac980a3321&chksmc0e47eebf793f7fdb8fcd7eed8ce29160cf79ba303b59858ba3a6660c6dac536774afb2a6330&scene21#wechat_redirect 《网安面试指南》…

都2024年了,看谁还不会用AI作图?这个全方位的系统教程真的别错过了!

大家好&#xff0c;我是画画的小强 如果给我们现在所处的时代一个标签&#xff0c;相信很多人都会选择人工智能。 其实&#xff0c;关于 AI 的讨论已经不局限在科学家和算法工程师之间&#xff0c;更多的是在各行各业的从业者之间&#xff0c;甚至也出现在了高考试卷中。 以…

多旋翼无人机维修、组装、调试技术详解

多旋翼无人机作为现代航拍、农业植保、物流运输等领域的重要工具&#xff0c;其性能的稳定性和操作的便捷性对于任务的完成至关重要。因此&#xff0c;掌握多旋翼无人机的维修、组装与调试技术&#xff0c;对于无人机操作员及维修人员来说至关重要。本文将详细介绍这三个方面的…

96 kHz、24bit 立体声音频ADC芯片GC5358描述

概述&#xff1a; GC5358 是一款高性能、宽采样率、立体声音频模数转换器。其采样率范围是8KHz~96KHz&#xff0c;非常适合从消费级到专业级的音频应用系统。单端模拟输入不需要外围器件。GC5358 音频有两种数据格式&#xff1a;MSB对齐和 I2S 格式&#xff0c;和各种如 DTV、D…

将Java程序打包成EXE程序

Java制作可执行jar 方式一&#xff1a;mainClass与lib分离 1&#xff09;将Java程序依赖的所有jar都拷贝在lib目录下&#xff0c;并添加到classpath中 2&#xff09;运行时指定MainClass pom.xml 这个pom.xml生成的jar可双击直接运行&#xff0c;但是因为没有将其依赖的jar…

焦化行业的变革力量:智能巡检机器人

根据相关数据&#xff0c;2024年1-2月份&#xff0c;焦炭产量为8039.5万吨&#xff0c;同比增长2.1%&#xff0c;这表明&#xff0c;我国焦化行业仍是全球最大的焦炭生产国和消费国&#xff0c;其市场规模占据了重要地位。焦化企业主要集中在山西省&#xff0c;其合计焦炭产能约…

基础漏洞——SSRF

目录 一.原理 二.引起ssrf的函数 三.这些函数具体作用 &#xff08;1&#xff09;File_get_content() &#xff08;2&#xff09;Fsockopen() &#xff08;3&#xff09;Curl_exec() 四.常见的业务场景&#xff08;可能出现的漏洞的地方&#xff0c;漏洞挖掘&#xff09…

FFmpeg开发笔记(五十六)使用Media3的Exoplayer播放网络视频

Android早期的MediaPlayer控件对于网络视频的兼容性很差&#xff0c;所以后来单独推出了Exoplayer库增强支持网络视频&#xff0c;在《Android Studio开发实战&#xff1a;从零基础到App上线(第3版)》一书第14章的“14.3.3 新型播放器ExoPlayer”就详细介绍了Exoplayer库的详细…

stack和queue(一)

接下来讲解一些stack栈和queue的简单使用 stack的概念 stack是一种容器适配器&#xff0c;专门用在具有后进先出操作的上下文环境中&#xff0c;其删除只能从容器的一端进行 元素的插入与提取操作。 特性是先进先出 后进后出 构造一个栈堆 int main() {deque<int>…

树莓派配置Qt+OpenCV

本次教程使用的树莓派镜像&#xff1a;树莓派镜像带图像界面下载 Qt的安装&#xff1a; 在命令行依次输入以下命令安装Qt&#xff1a; sudo apt-get updatesudo apt-get upgrade sudo apt-get install qtbase5-dev qtchooser sudo apt-get install qt5-qmake qtbase5-dev-t…

threejs加载高度图渲染点云,不支持tiff

问题点 使用的point来渲染高度图点云&#xff0c;大数据图片无效渲染点多&#xff08;可以通过八叉树过滤掉无效点增加效率&#xff0c;这个太复杂&#xff09;&#xff0c;但是胜在简单能用 效果图 code 代码可运行&#xff0c;无需npm <!DOCTYPE html> <html la…

MySQL聚合统计和内置函数

【数据库】MySQL聚合统计 王笃笃-CSDN博客https://blog.csdn.net/wangduduniubi?typeblog显示平均工资低于2000的部门和它的平均工资 mysql> select deptno,avg(sal) deptavg from emp group by deptno; --------------------- | deptno | deptavg | --------------…

0x08 MotionEye 视频监控组件 list 信息泄漏洞 CVE-2022-25568

参考&#xff1a; MotionEye 视频监控组件 list 信息泄漏洞 CVE-2022-25568 | PeiQi文库 (wgpsec.org) 一、漏洞描述&#xff1a; motionEye是用Python写的motion的Web前端&#xff0c;它可以监视视频信号并检测运动。它可以与多种类型的摄像机配合使用,也可以与电影文件一起…

PMP--二模--解题--41-50

文章目录 11.风险管理--风险代表对将来问题的预判&#xff0c;问题代表对过去问题事件的跟踪&#xff1b;两者联系&#xff1a;风险发生后会变成问题&#xff0c;而问题可能导致新的风险。41、 [单选] 在项目会议期间&#xff0c;一个团队发现三个月前关闭的问题仍然处于活跃状…

解决 Prettier ESLint 错误

解决 Prettier ESLint 错误 在 Vue.js 项目中使用 ESLint 和 Prettier 时&#xff0c;你可能会遇到类似以下的错误&#xff1a; frontend\src\views\dashboard\MobileConfigPanel.vue1:25 error Delete ␍ …

使用IDA Pro动态调试Android APP

版权归作者所有&#xff0c;如有转发&#xff0c;请注明文章出处&#xff1a;https://cyrus-studio.github.io/blog/ 关于 android_server android_server 是 IDA Pro 在 Android 设备上运行的一个调试服务器。 通过在 Android 设备上运行android_server&#xff0c;IDA Pro …

SpringBoot项目同时集成Mybatis和Mybatis-plus框架

1. 背景 Mybatis-plus可以生成CRUD&#xff0c;减少开发中SQL编写量&#xff0c;但是某些情况下我们需要多表关联查询&#xff0c;这时候Mybatis可以手写SQL的优势就体现出来了&#xff0c;在实际开发中&#xff0c;项目里面一般都是Mybatis和Mybatis-Plus公用&#xff0c;但是…

【Geoserver使用】Geoserver 3前瞻

文章目录 前言一、GeoServer 3 Call for Crowdfunding&#xff08;GeoServer 3 呼吁众筹&#xff09;二、Geoserver 3升级内容1.升级到3的几个原因2.Geoserver 3的四个升级方向 总结 前言 今天来看看最近Geoserver官方发布的关于Geoserver 3重大升级众筹这篇官方博客中提到的几…