Linux-C/C++--深入探究文件 I/O (上)(文件的管理、函数返回错误、exit()、_ ...

打印 上一主题 下一主题

主题 887|帖子 887|积分 2661

        经过上一章内容的学习,相信各位读者对 Linux 系统应用编程中的基础文件 I/O 操纵有了一定的认识和理解了,可以或许独立完成一些简单地文件 I/O 编程问题,假如你的工作中仅仅只是涉及到一些简单文件读写操纵相关的问题,实在上一章的知识内容已经够你使用了。
        固然作为大部分读者来说,我相信你不会止步于此、还想学习更多的知识内容,那本章笔者将会同各位读者一起,来深入探究文件 I/O 中涉及到的一些问题、原理以及所对应的解决方法,譬如 Linux 系统下文件是如何进行管理的、调用函数返回错误该如那边理、open 函数的 O_APPEND、O_TRUNC 标志以及等相关问题。
   好了,废话不多说,开始本章的学习吧,加油!     本章将会讨论如下主题内容。            对    Linux    下文件的管理方式进行简单介绍;            函数返回错误的处置惩罚;            退出程序    exit()   、   _Exit()   、   _exit()   ;            空洞文件的概念;            open    函数的    O_APPEND    和    O_TRUNC    标志;            多次打开同一文件;            复制文件描述符;            文件共享介绍;            原子操纵与竞争冒险;            系统调用    fcntl()   和    ioctl()   介绍;            截断文件;    一、Linux 系统如何管理文件

1、静态文件与 inode

        文件在没有被打开的情况下一般都是存放在磁盘中的,譬如电脑硬盘、移动硬盘、U 盘等外部存储设备,文件存放在磁盘文件系统中,而且以一种固定的情势进行存放,我们把他们称为静态文件。
        文件储存在硬盘上,硬盘的最小存储单位叫做“扇区”(Sector),每个扇区储存 512 字节(相当于 0.5KB),操纵系统读取硬盘的时间,不会一个个扇区地读取,这样服从太低,而是一次性连续读取多个扇区,即一次性读取一个“块”(block)。这种由多个扇区组成的“块”,是文件存取的最小单位。“块”的巨细,最常见的是 4KB,即连续八个 sector 组成一个 block。
        以是由此可以知道,静态文件对应的数据都是存储在磁盘设备差别的“块”中,那么问题来了,我们在程序中调用 open 函数是如何找到对应文件的数据存储“块”的呢,难道仅仅通过指定的文件路径就可以实现?这里我们就来简单地聊一聊这内部实现的过程。
        我们的磁盘在进行分区、格式化的时间会将其分为两个地区,一个是数据区,用于存储文件中的数据;另一个是 inode 区,用于存放 inode table(inode 表),inode table 中存放的是一个一个的 inode(也成为 inode节点),差别的 inode 就可以表现差别的文件,每一个文件都必须对应一个 inode,inode 实质上是一个布局体,这个布局体中有许多的元素,差别的元素记录了文件了差别信息,譬如文件字节巨细、文件所有者、文件对应的读/写/执行权限、文件时间戳(创建时间、更新时间等)、文件类型、文件数据存储的 block(块)位置等等信息,如图1 中所示(这里必要留意的是,文件名并不是记录在 inode 中,这个问题后面章节内容再给大家讲)。

   

       图   1 inode table    与    inode               以是由此可知,inode table 表自己也必要占用磁盘的存储空间。每一个文件都有唯一的一个 inode,每一个 inode 都有一个与之相对应的数字编号,通过这个数字编号就可以找到 inode table 中所对应的 inode。在 Linux 系统下,我们可以通过"ls -i"命令查看文件的 inode 编号,如下所示:
   
     上图中 ls 打印出来的信息中,每一行前面的一个数字就表现了对应文件的 inode 编号。除此之外,还可 以使用 stat 命令查看,用法如下:
     
    由以上的介绍大家可以联系到现实操纵中,譬如我们在 Windows 下进行 U 盘格式化的时间会有一个 “快速格式化”选项
   
  通过以上介绍可知,打开一个文件,系统内部会将这个过程分为三步:
  1) 系统找到这个文件名所对应的 inode 编号;
  2) 通过 inode 编号从 inode table 中找到对应的 inode 布局体;
  3) 根据 inode 布局体中记录的信息,确定文件数据所在的 block,并读出数据。
  2、文件打开时的状态

           当我们调用 open 函数去打开文件的时间,内核会申请一段内存(一段缓冲区),而且将静态文件的数据内容从磁盘这些存储设备中读取到内存中进行管理、缓存(也把内存中的这份文件数据叫做动态文件、内核缓冲区)。打开文件后,以后对这个文件的读写操纵,都是针对内存中这一份动态文件进行相关的操纵,而并不是针对磁盘中存放的静态文件。
          当我们对动态文件进行读写操纵后,此时内存中的动态文件和磁盘设备中的静态文件就差别步了,数据的同步工作由内核完成,内核会在之后将内存这份动态文件更新(同步)到磁盘设备中。由此我们也可以联系到现实操纵中,譬如说:
   打开一个大文件的时间会比力慢;
   文档写了一半,没记得保存,此时电脑因为忽然停电直接掉电关机了,当重启电脑后,打开编写的文档,发现之前写的内容已经丢失。
          想必各位读者在工作当中都遇到过这种问题吧,通过上面的介绍,就解释了为什么会出现这种问题。
  二、返回错误处置惩罚与 errno

           在上一章节中,笔者给大家编写了许多的示例代码,大家会发现这些示例代码会有一个共同的特点,那就是当判定函数执行失败后,会调用 return 退出程序,但是对于我们来说,我们并不知道为什么会出错,什么缘故原由导致此函数执行失败,因为执行出错之后它们的返回值都是-1。
          难道我们真的就不知道错误缘故原由了吗?实在不然,在 Linux 系统下对常见的错误做了一个编号,每一个编号都代表着每一种差别的错误类型,当函数执行发生错误的时间,操纵系统会将这个错误所对应的编号赋值给 errno 变量,每一个进程(程序)都维护了自己的 errno 变量,它是程序中的全局变量,该变量用于存储就近发生的函数执行错误编号,也就意味着下一次的错误码会覆盖上一次的错误码。以是由此可知道,当程序中调用函数发生错误的时间,操纵系统内部会通过设置程序的 errno 变量来告知调用者究竟发生了什么错误!
          errno 本质上是一个 int 类型的变量,用于存储错误编号,但是必要留意的是,并不是执行所有的系统调用或 C 库函数出错时,操纵系统都会设置 errno,那我们如何确定一个函数出错时系统是否会设置 errno 呢?实在这个通过 man 手册便可以查到,譬如以 open 函数为例,执行"man 2 open"打开 open 函数的帮助信息,找到函数返回值描述段,如下所示:
   
          当函数返回错误时会设置 errno,固然这里是以 open 函数为例,别的的系统调用也可以这样查找你可以直接以为此变量就是在<errno.h>头文件中的申明的,好,我们来测试下:
  
  1. #include <stdio.h>
  2. #include <errno.h>
  3. int main(void)
  4. {
  5.     printf("%d\n", errno);
  6.     return 0;
  7. }
复制代码
    以上的这段代码是不会报错的,大家可以自己试试!    1、strerror 函数

   在 C 语言中,  strerror 函数是一个标准库函数,用于根据错误代码返回一个描述性字符串,该字符串详细说明了错误的缘故原由。该函数是  <string.h> 头文件的一部分   
  1. #include <string.h>
  2. char *strerror(int errnum);
复制代码


  • errnum:这是一个整数,表现错误代码,通常是由系统调用或库函数返回的错误代码。常见的错误代码是由 <errno.h> 头文件定义的常量,例如 EIO, ENOMEM, EINVAL 等。
   

  • 返回一个指向描述该错误的字符串的指针。假如错误代码有效,返回一个以 null 结尾的错误描述字符串。假如传入的错误代码无效或无法辨认,返回一个指向标准错误消息的字符串。
    示例:
  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <errno.h>
  4. int main() {
  5.     FILE *file = fopen("nonexistent_file.txt", "r");
  6.    
  7.     if (file == NULL) {
  8.         printf("Error opening file: %s\n", strerror(errno));
  9.     }
  10.     return 0;
  11. }
复制代码
在这个示例中,假如文件 nonexistent_file.txt 无法打开,fopen 会返回 NULL,而且 errno 会被设置为一个表现错误的代码。strerror(errno) 会返回一个与该错误代码相关的描述性字符串,比如 "No such file or directory"。
    常见的错误代码与对应的描述
  

  • EINVAL:无效的参数。
  • ENOMEM:内存不敷。
  • EIO:输入/输出错误。
  • EBADF:坏的文件描述符。
  • EACCES:权限被拒绝。
  • ENOSPC:没有充足的空间。
  2、perror 函数

        在 C 语言中,perror 函数是一个用于打印错误信息的标准库函数,它将标准错误流 stderr 中输出一条错误消息。该消息是由一个给定的字符串和当前 errno 错误代码的描述组成的。
  
  1. #include <stdio.h>
  2. void perror(const char *s);
复制代码


  • s:这是一个字符串参数,它会被输出在错误信息前面,用来描述错误的上下文。s 后面会附带一个冒号和空格(假如 s 为空,错误信息只包含系统默认的错误消息)。
    功能,perror 会使用全局变量 errno 来天生错误信息。errno 是由操纵系统或 C 库的系统调用或库函数设置的,它代表了近来一次的错误代码。perror 会将 errno 对应的错误描述与 s 字符串一起打印到标准错误流(stderr)。
  具体来说,它会输出如下格式的消息:
  1. s: <error_description>
复制代码
其中,<error_description> 是由 errno 值所指示的错误信息。例如,假如发生了 "文件未找到" 错误,perror 会打印相关的错误描述。
    返回值:perror 函数没有返回值(void 类型)
    示例:
  1. #include <stdio.h>
  2. #include <errno.h>
  3. int main() {
  4.     FILE *file = fopen("nonexistent_file.txt", "r");
  5.     if (file == NULL) {
  6.         perror("Error opening file");
  7.     }
  8.     return 0;
  9. }
复制代码
在这个例子中,程序试图打开一个不存在的文件。假如文件打开失败,perror 会输出如下信息:
  1. Error opening file: No such file or directory
复制代码
三、exit_exit_Exit

          当程序在执行某个函数出错的时间,假如此函数执行失败会导致后面的步调不能在进行下去时,应该在出错时停止程序运行,不应该让程序继承运行下去,那么如何退出程序、停止程序运行呢?有过编程经验的读者都知道使用 return,一般原则程序执行正常退出 return 0,而执行函数出错退出 return -1,前面我们所编写的示例代码也是如此。
          在 Linux 系统下,进程(程序)退出可以分为正常退出和非常退出,留意这里说的非常并不是执行函数出现了错误这种情况,非常每每更多的是一种不可预料的系统非常,可能是执行了某个函数时发生的、也有可能是收到了某种信号等,这里我们只讨论正常退出的情况。
          在 Linux 系统下,进程正常退出除了可以使用 return 之外,还可以使用 exit()、_exit()以及_Exit(),下面我们分别介绍。
  1、_exit()_Exit()函数

          main 函数中使用 return 后返回,return 执行后把控制权交给调用函数,结束该进程。调用_exit()函数会清除其使用的内存空间,并销毁其在内核中的各种数据布局,关闭进程的所有文件描述符,并结束进程、将控制权交给操纵系统。_exit()函数原型如下所示:
  1. #include <unistd.h>
  2. void _exit(int status);
复制代码
调用函数必要传入 status 状态标志,0 表现正常结束、若为别的值则表现程序执行过程中检测到有错误发生。使用示例如下:
  1. #include <sys/types.h>
  2. #include <sys/stat.h>
  3. #include <fcntl.h>
  4. #include <unistd.h>
  5. #include <stdio.h>
  6. int main(void)
  7. {
  8.     int fd;
  9.     /* 打开文件 */
  10.     fd = open("./test_file", O_RDONLY);
  11.     if (-1 == fd) {
  12.         perror("open error");
  13.         _exit(-1);
  14.     }
  15.     close(fd);
  16.     _exit(0);
  17. }
复制代码
用法很简单,大家可以自行测试!
  _Exit()函数原型如下所示:
  1. #include <stdlib.h>
  2. void _Exit(int status);
复制代码
_exit()和_Exit()两者等价,用法作用是一样的,这里就不再讲了,必要留意的是这 2 个函数都是系统调用。
  2、exit()函数

  exit()函数_exit()函数都是用来停止进程的,exit()是一个标准 C 库函数,而_exit()和_Exit()是系统调用。执行 exit()会执行一些清理工作,末了调用_exit()函数。exit()函数原型如下:
  1. #include <stdlib.h>
  2. void exit(int status);
复制代码
            该函数是一个标准 C    库函数,使用该函数必要包含头文件   <stdlib.h>   ,该函数的用法和   _exit()/_Exit()   是一样的,这里就不再多说了。     本小节就给大家介绍了 3 中停止进程的方法:
   main 函数中运行 return;
   调用 Linux 系统调用_exit()或_Exit();
   调用 C 标准库函数 exit()。
          不管你用哪一种都可以结束进程,但还是推荐大家使用 exit(),实在关于 return、exit、_exit/_Exit()之间的区别笔者在上面只是给大家简单地描述了一下,以致不太确定我的描述是否精确,因为笔者并不太多去关心其间的差别,对这些概念的描述会比力模糊、笼统,假如大家看不明确可以自己百度搜索相关的内容,固然对于初学者来说,不太建议大家去查找这些东西,至少对你现阶段来说,意义不是很大。好,本小节就介绍这么多,我们接着学习下一小节的内容。
  本小节内容到此结束。下一篇介绍:空洞文件的概念;open 函数的 O_APPEND 和 O_TRUNC 标志;多次打开同一文件;复制文件描述符;

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

干翻全岛蛙蛙

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表