百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

正点原子I.MX6U嵌入式Linux C应用编程 第三章 深入探究文件I/O (1)

liebian365 2024-10-27 13:22 15 浏览 0 评论

今日头条/西瓜视频/抖音短视频 同名:正点原子原子哥

感谢各位的关注和支持,你们的支持是原子哥无限前进的动力。

第三章 深入探究文件I/O

由于本章内容较多,所以第三章 深入探究文件I/O将会分为几个部分进行内容的发布,更多精彩原创文章请持续关注正点原子原子哥官方账号。

经过上一章内容的学习,相信各位读者对Linux系统应用编程中的基础文件I/O操作有了一定的认识和理解了,能够独立完成一些简单地文件I/O编程问题,如果你的工作中仅仅只是涉及到一些简单文件读写操作相关的问题,其实上一章的知识内容已经够你使用了。

当然作为大部分读者来说,我相信你不会止步于此、还想学习更多的知识内容,那本章笔者将会同各位读者一起,来深入探究文件I/O中涉及到的一些问题、原理以及所对应的解决方法,譬如Linux系统下文件是如何进行管理的、调用函数返回错误该如何处理、open函数的O_APPEND、O_TRUNC标志以及等相关问题。

好了,废话不多说,开始本章的学习吧,加油!

本章将会讨论如下主题内容。

l 对Linux下文件的管理方式进行简单介绍;

l 函数返回错误的处理;

l 退出程序exit()、_Exit()、_exit();

l 空洞文件的概念;

l open函数的O_APPEND和O_TRUNC标志;

l 多次打开同一文件;

l 复制文件描述符;

l 文件共享介绍;

l 原子操作与竞争冒险;

l 系统调用fcntl()和ioctl()介绍;

l 截断文件;


一.1 Linux系统如何管理文件

一.1.1 静态文件与inode

文件在没有被打开的情况下一般都是存放在磁盘中的,譬如电脑硬盘、移动硬盘、U盘等外部存储设备,文件存放在磁盘文件系统中,并且以一种固定的形式进行存放,我们把他们称为静态文件。

文件储存在硬盘上,硬盘的最小存储单位叫做“扇区”(Sector),每个扇区储存512字节(相当于0.5KB),操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个“块”(block)。这种由多个扇区组成的“块”,是文件存取的最小单位。“块”的大小,最常见的是4KB,即连续八个sector组成一个block。

所以由此可以知道,静态文件对应的数据都是存储在磁盘设备不同的“块”中,那么问题来了,我们在程序中调用open函数是如何找到对应文件的数据存储“块”的呢,难道仅仅通过指定的文件路径就可以实现?这里我们就来简单地聊一聊这内部实现的过程。

我们的磁盘在进行分区、格式化的时候会将其分为两个区域,一个是数据区,用于存储文件中的数据;另一个是inode区,用于存放inode table(inode表),inode table中存放的是一个一个的inode(也成为inode节点),不同的inode就可以表示不同的文件,每一个文件都必须对应一个inode,inode实质上是一个结构体,这个结构体中有很多的元素,不同的元素记录了文件了不同信息,譬如文件字节大小、文件所有者、文件对应的读/写/执行权限、文件时间戳(创建时间、更新时间等)、文件类型、文件数据存储的block(块)位置等等信息,如图 3.1.1中所示(这里需要注意的是,文件名并不是记录在inode中,这个问题后面章节内容再给大家讲)。

所以由此可知,inode table表本身也需要占用磁盘的存储空间。每一个文件都有唯一的一个inode,每一个inode都有一个与之相对应的数字编号,通过这个数字编号就可以找到inode table中所对应的inode。在Linux系统下,我们可以通过"ls -i"命令查看文件的inode编号,如下所示:


上图中ls打印出来的信息中,每一行前面的一个数字就表示了对应文件的inode编号。除此之外,还可以使用stat命令查看,用法如下:

由以上的介绍大家可以联系到实际操作中,譬如我们在Windows下进行U盘格式化的时候会有一个“快速格式化”选项,如下所示:

如果勾选了“快速格式化”选项,在进行格式化操作的时候非常的快,而如果不勾选此选项,直接使用普通格式化方式,将会比较慢,那说明这两种格式化方式是存在差异的,其实快速格式化只是删除了U盘中的inode table表,真正存储文件数据的区域并没有动,所以使用快速格式化的U盘,其中的数据是可以被找回来的。

通过以上介绍可知,打开一个文件,系统内部会将这个过程分为三步:

1) 系统找到这个文件名所对应的inode编号;

2) 通过inode编号从inode table中找到对应的inode结构体;

3) 根据inode结构体中记录的信息,确定文件数据所在的block,并读出数据。

一.1.2 文件打开时的状态

当我们调用open函数去打开文件的时候,内核会申请一段内存(一段缓冲区),并且将静态文件的数据内容从磁盘这些存储设备中读取到内存中进行管理、缓存(也把内存中的这份文件数据叫做动态文件、内核缓冲区)。打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件进行相关的操作,而并不是针对磁盘中存放的静态文件。

当我们对动态文件进行读写操作后,此时内存中的动态文件和磁盘设备中的静态文件就不同步了,数据的同步工作由内核完成,内核会在之后将内存这份动态文件更新(同步)到磁盘设备中。由此我们也可以联系到实际操作中,譬如说:

l 打开一个大文件的时候会比较慢;

l 文档写了一半,没记得保存,此时电脑因为突然停电直接掉电关机了,当重启电脑后,打开编写的文档,发现之前写的内容已经丢失。

想必各位读者在工作当中都遇到过这种问题吧,通过上面的介绍,就解释了为什么会出现这种问题。好,我们再来说一下,为什么要这样设计?

因为磁盘、硬盘、U盘等存储设备基本都是Flash块设备,因为块设备硬件本身有读写限制等特征,块设备是以一块一块为单位进行读写的(一个块包含多个扇区,而一个扇区包含多个字节),一个字节的改动也需要将该字节所在的block全部读取出来进行修改,修改完成之后再写入块设备中,所以导致对块设备的读写操作非常不灵活;而内存可以按字节为单位来操作,而且可以随机操作任意地址数据,非常地很灵活,所以对于操作系统来说,会先将磁盘中的静态文件读取到内存中进行缓存,读写操作都是针对这份动态文件,而不是直接去操作磁盘中的静态文件,不但操作不灵活,效率也会下降很多,因为内存的读写速率远比磁盘读写快得多。

在Linux系统中,内核会为每个进程(关于进程的概念,这是后面的内容,我们可以简单地理解为一个运行的程序就是一个进程,运行了多个程序那就是存在多个进程)设置一个专门的数据结构用于管理该进程,譬如用于记录进程的状态信息、运行特征等,我们把这个称为进程控制块(Process control block,缩写PCB)。

PCB数据结构体中有一个指针指向了文件描述符表(File descriptors),文件描述符表中的每一个元素索引到对应的文件表(File table),文件表也是一个数据结构体,其中记录了很多文件相关的信息,譬如文件状态标志、引用计数、当前文件的读写偏移量以及i-node指针(指向该文件对应的inode)等,进程打开的所有文件对应的文件描述符都记录在文件描述符表中,每一个文件描述符都会指向一个对应的文件表,其示意图如下所示:

前面给大家介绍了inode,inode数据结构体中的元素会记录该文件的数据存储的block(块),也就是说可以通过inode找到文件数据存在在磁盘设备中的那个位置,从而把文件数据读取出来。

以上就是本小节给大家介绍到所有内容了,上面给大家所介绍的内容后面的学习过程中还会用到,虽然这些理论知识对大家的编程并没有什么影响,但是会帮助大家理解文件IO背后隐藏的一些理论知识,其实这些理论知识还是非常浅薄的、只是一个大概的认识,其内部具体的实现是比较复杂的,当然这个不是我们学习Linux应用编程的重点,操作系统已经帮我们完成了这些具体的实现,我们要做的仅仅只是调用操作系统提供API函数来完成自己的工作。

好了,废话不多说,我们接着看下一小节内容。

一.2 返回错误处理与errno

在上一章节中,笔者给大家编写了很多的示例代码,大家会发现这些示例代码会有一个共同的特点,那就是当判断函数执行失败后,会调用return退出程序,但是对于我们来说,我们并不知道为什么会出错,什么原因导致此函数执行失败,因为执行出错之后它们的返回值都是-1。

难道我们真的就不知道错误原因了吗?其实不然,在Linux系统下对常见的错误做了一个编号,每一个编号都代表着每一种不同的错误类型,当函数执行发生错误的时候,操作系统会将这个错误所对应的编号赋值给errno变量,每一个进程(程序)都维护了自己的errno变量,它是程序中的全局变量,该变量用于存储就近发生的函数执行错误编号,也就意味着下一次的错误码会覆盖上一次的错误码。所以由此可知道,当程序中调用函数发生错误的时候,操作系统内部会通过设置程序的errno变量来告知调用者究竟发生了什么错误!

errno本质上是一个int类型的变量,用于存储错误编号,但是需要注意的是,并不是执行所有的系统调用或C库函数出错时,操作系统都会设置errno,那我们如何确定一个函数出错时系统是否会设置errno呢?其实这个通过man手册便可以查到,譬如以open函数为例,执行"man 2 open"打开open函数的帮助信息,找到函数返回值描述段,如下所示:


从图中红框部分描述文字可知,当函数返回错误时会设置errno,当然这里是以open函数为例,其它的系统调用也可以这样查找,大家可以自己试试!

在我们的程序当中如何去获取系统所维护的这个errno变量呢?只需要在我们程序当中包含<errno.h>头文件即可,你可以直接认为此变量就是在<errno.h>头文件中的申明的,好,我们来测试下:

#include <stdio.h>
#include <errno.h>
int main(void)
{
printf("%d\n", errno);
return 0;
}

以上的这段代码是不会报错的,大家可以自己试试!

一.2.1 strerror函数

前面给大家说到了errno变量,但是errno仅仅只是一个错误编号,对于开发者来说,即使拿到了errno也不知道错误为何?还需要对比源码中对此编号的错误定义,可以说非常不友好,这里介绍一个C库函数strerror(),该函数可以将对应的errno转换成适合我们查看的字符串信息,其函数原型如下所示(可通过"man 3 strerror"命令查看,注意此函数是C库函数,并不是系统调用):

#include <string.h>
char *strerror(int errnum);

首先调用此函数需要包含头文件<string.h>。

函数参数和返回值如下:

errnum:错误编号errno。

返回值:对应错误编号的字符串描述信息。

测试

接下来我们测试下,测试代码如下:

示例代码 3.2.1 strerror测试代码

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

int main(void)
{
int fd;

/* 打开文件 */
fd = open("./test_file", O_RDONLY);
if (-1 == fd) {
printf("Error: %s\n", strerror(errno));
return -1;
}

close(fd);
return 0;
}

编译源代码,在Ubuntu系统下运行测试下,在当前目录下并不存在test_file文件,测试打印结果如下:

从打印信息可以知道,strerror返回的字符串是"No such file or directory",所以从打印信息可知,我们就可以很直观的知道open函数执行的错误原因是文件不存在!

一.2.2 perror函数

除了strerror函数之外,我们还可以使用perror函数来查看错误信息,一般用的最多的还是这个函数,调用此函数不需要传入errno,函数内部会自己去获取errno变量的值,调用此函数会直接将错误提示字符串打印出来,而不是返回字符串,除此之外还可以在输出的错误提示字符串之前加入自己的打印信息,函数原型如下所示(可通过"man 3 perror"命令查看):

#include <stdio.h>
void perror(const char *s);

需要包含<stdio.h>头文件。

函数参数和返回值含义如下:

s:在错误提示字符串信息之前,可加入自己的打印信息,也可不加,不加则传入空字符串即可。

返回值:void无返回值。

测试

接下来我们进行测试,测试代码如下所示:

示例代码 3.2.2 perror测试代码

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

int main(void)
{
int fd;

/* 打开文件 */
fd = open("./test_file", O_RDONLY);
if (-1 == fd) {
perror("open error");
return -1;
}

close(fd);
return 0;
}

编译源代码,在Ubuntu系统下运行测试下,在当前目录下并不存在test_file文件,测试打印结果如下:


从打印信息可以知道,perror函数打印出来的错误提示字符串是"No such file or directory",跟strerror函数返回的字符串信息一样,"open error"便是我们附加的打印信息,而且从打印信息可知,perror函数会在附加信息后面自动加入冒号和空格以区分。

以上给大家介绍了strerror、perror两个C库函数,都是用于查看函数执行错误时对应的提示信息,大家用哪个函数都可以,这里笔者推荐大家使用perror,在实际的编程中这个函数用的还是比较多的,当然除了这两个之外,其它其它一些类似功能的函数,这里就不再给大家介绍了,意义不大!

一.3 exit、_exit、_Exit

当程序在执行某个函数出错的时候,如果此函数执行失败会导致后面的步骤不能在进行下去时,应该在出错时终止程序运行,不应该让程序继续运行下去,那么如何退出程序、终止程序运行呢?有过编程经验的读者都知道使用return,一般原则程序执行正常退出return 0,而执行函数出错退出return -1,前面我们所编写的示例代码也是如此。

在Linux系统下,进程(程序)退出可以分为正常退出和异常退出,注意这里说的异常并不是执行函数出现了错误这种情况,异常往往更多的是一种不可预料的系统异常,可能是执行了某个函数时发生的、也有可能是收到了某种信号等,这里我们只讨论正常退出的情况。

在Linux系统下,进程正常退出除了可以使用return之外,还可以使用exit()、_exit()以及_Exit(),下面我们分别介绍。

一.3.1 _exit()和_Exit()函数

main函数中使用return后返回,return执行后把控制权交给调用函数,结束该进程。调用_exit()函数会清除其使用的内存空间,并销毁其在内核中的各种数据结构,关闭进程的所有文件描述符,并结束进程、将控制权交给操作系统。_exit()函数原型如下所示:

#include <unistd.h>
void _exit(int status);

调用函数需要传入status状态标志,0表示正常结束、若为其它值则表示程序执行过程中检测到有错误发生。使用示例如下:

示例代码 3.3.1 _exit()使用示例

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

int main(void)
{
int fd;

/* 打开文件 */
fd = open("./test_file", O_RDONLY);
if (-1 == fd) {
perror("open error");
_exit(-1);
}

close(fd);
_exit(0);
}

用法很简单,大家可以自行测试!

_Exit()函数原型如下所示:

#include <stdlib.h>
void _Exit(int status);

_exit()和_Exit()两者等价,用法作用是一样的,这里就不再讲了,需要注意的是这2个函数都是系统调用。

一.3.2 exit()函数

exit()函数_exit()函数都是用来终止进程的,exit()是一个标准C库函数,而_exit()和_Exit()是系统调用。执行exit()会执行一些清理工作,最后调用_exit()函数。exit()函数原型如下:

#include <stdlib.h>
void exit(int status);

该函数是一个标准C库函数,使用该函数需要包含头文件<stdlib.h>,该函数的用法和_exit()/_Exit()是一样的,这里就不再多说了。

本小节就给大家介绍了3中终止进程的方法:

l main函数中运行return;

l 调用Linux系统调用_exit()或_Exit();

l 调用C标准库函数exit()。

不管你用哪一种都可以结束进程,但还是推荐大家使用exit(),其实关于return、exit、_exit/_Exit()之间的区别笔者在上面只是给大家简单地描述了一下,甚至不太确定我的描述是否正确,因为笔者并不太多去关心其间的差异,对这些概念的描述会比较模糊、笼统,如果大家看不明白可以自己百度搜索相关的内容,当然对于初学者来说,不太建议大家去查找这些东西,至少对你现阶段来说,意义不是很大。好,本小节就介绍这么多,我们接着学习下一小节的内容。

相关推荐

4万多吨豪华游轮遇险 竟是因为这个原因……

(观察者网讯)4.7万吨豪华游轮搁浅,竟是因为油量太低?据观察者网此前报道,挪威游轮“维京天空”号上周六(23日)在挪威近海发生引擎故障搁浅。船上载有1300多人,其中28人受伤住院。经过数天的调...

“菜鸟黑客”必用兵器之“渗透测试篇二”

"菜鸟黑客"必用兵器之"渗透测试篇二"上篇文章主要针对伙伴们对"渗透测试"应该如何学习?"渗透测试"的基本流程?本篇文章继续上次的分享,接着介绍一下黑客们常用的渗透测试工具有哪些?以及用实验环境让大家...

科幻春晚丨《震动羽翼说“Hello”》两万年星间飞行,探测器对地球的最终告白

作者|藤井太洋译者|祝力新【编者按】2021年科幻春晚的最后一篇小说,来自大家喜爱的日本科幻作家藤井太洋。小说将视角放在一颗太空探测器上,延续了他一贯的浪漫风格。...

麦子陪你做作业(二):KEGG通路数据库的正确打开姿势

作者:麦子KEGG是通路数据库中最庞大的,涵盖基因组网络信息,主要注释基因的功能和调控关系。当我们选到了合适的候选分子,单变量研究也已做完,接着研究机制的时便可使用到它。你需要了解你的分子目前已有哪些...

知存科技王绍迪:突破存储墙瓶颈,详解存算一体架构优势

智东西(公众号:zhidxcom)编辑|韦世玮智东西6月5日消息,近日,在落幕不久的GTIC2021嵌入式AI创新峰会上,知存科技CEO王绍迪博士以《存算一体AI芯片:AIoT设备的算力新选择》...

每日新闻播报(September 14)_每日新闻播报英文

AnOscarstatuestandscoveredwithplasticduringpreparationsleadinguptothe87thAcademyAward...

香港新巴城巴开放实时到站数据 供科技界研发使用

中新网3月22日电据香港《明报》报道,香港特区政府致力推动智慧城市,鼓励公私营机构开放数据,以便科技界研发使用。香港运输署21日与新巴及城巴(两巴)公司签署谅解备忘录,两巴将于2019年第3季度,开...

5款不容错过的APP: Red Bull Alert,Flipagram,WifiMapper

本周有不少非常出色的app推出,鸵鸟电台做了一个小合集。亮相本周榜单的有WifiMapper's安卓版的app,其中包含了RedBull的一款新型闹钟,还有一款可爱的怪物主题益智游戏。一起来看看我...

Qt动画效果展示_qt显示图片

今天在这篇博文中,主要实践Qt动画,做一个实例来讲解Qt动画使用,其界面如下图所示(由于没有录制为gif动画图片,所以请各位下载查看效果):该程序使用应用程序单窗口,主窗口继承于QMainWindow...

如何从0到1设计实现一门自己的脚本语言

作者:dong...

三年级语文上册 仿写句子 需要的直接下载打印吧

描写秋天的好句好段1.秋天来了,山野变成了美丽的图画。苹果露出红红的脸庞,梨树挂起金黄的灯笼,高粱举起了燃烧的火把。大雁在天空一会儿写“人”字,一会儿写“一”字。2.花园里,菊花争奇斗艳,红的似火,粉...

C++|那些一看就很简洁、优雅、经典的小代码段

目录0等概率随机洗牌:1大小写转换2字符串复制...

二年级上册语文必考句子仿写,家长打印,孩子照着练

二年级上册语文必考句子仿写,家长打印,孩子照着练。具体如下:...

一年级语文上 句子专项练习(可打印)

...

亲自上阵!C++ 大佬深度“剧透”:C++26 将如何在代码生成上对抗 Rust?

...

取消回复欢迎 发表评论: