打开文件实际上做了什么?

时间:2015-11-03 09:16:52

标签: c linux

在所有编程语言(我至少使用过)中,必须先打开文件才能读取或写入文件。

但是这个开放操作实际上做了什么?

典型功能的手册页实际上并没有告诉你除了它以外的任何其他内容,而是打开一个文件来读/写':

http://www.cplusplus.com/reference/cstdio/fopen/

https://docs.python.org/3/library/functions.html#open

显然,通过使用该函数,您可以告诉它涉及创建某种有助于访问文件的对象。

另外一种方法是,如果我要实现open函数,那么在Linux上需要做什么?

8 个答案:

答案 0 :(得分:181)

在几乎所有高级语言中,打开文件的函数都是相应内核系统调用的包装器。它也可能做其他奇特的东西,但在现代操作系统中,打开文件必须始终通过内核。

这就是fopen库函数或Python open的参数与open(2)系统调用的参数非常相似的原因。

除了打开文件外,这些函数通常还会设置一个缓冲区,该缓冲区将与读/写操作一起使用。此缓冲区的目的是确保无论何时要读取N个字节,相应的库调用都将返回N个字节,无论对底层系统调用的调用是否返回较少。

  

我实际上并不想实现自己的功能;只是在了解到底是怎么回事......如果你愿意的话,“超越语言”。

在类Unix操作系统中,成功调用open会返回“文件描述符”,它只是用户进程上下文中的整数。因此,此描述符将传递给与打开的文件交互的任何调用,并在调用close之后,描述符将变为无效。

值得注意的是,对open的调用就像是进行各种检查的验证点。如果不满足所有条件,则通过返回-1而不是描述符来调用失败,并在errno中指示错误类型。必要的检查是:

  • 文件是否存在;
  • 调用进程是否有权以指定模式打开此文件。这是通过将文件权限,所有者ID和组ID与调用进程的相应ID进行匹配来确定的。

在内核的上下文中,进程的文件描述符和物理打开的文件之间必须存在某种映射。映射到描述符的内部数据结构可能包含另一个处理基于块的设备的缓冲区,或者指向当前读/写位置的内部指针。

答案 1 :(得分:80)

我建议你看看this guide through a simplified version of the open() system call。它使用以下代码片段,它代表打开文件时幕后发生的事情。

0  int sys_open(const char *filename, int flags, int mode) {
1      char *tmp = getname(filename);
2      int fd = get_unused_fd();
3      struct file *f = filp_open(tmp, flags, mode);
4      fd_install(fd, f);
5      putname(tmp);
6      return fd;
7  }

简而言之,这是代码的作用,一行一行:

  1. 分配一块内核控制的内存,并从用户控制的内存中将文件名复制到其中。
  2. 选择一个未使用的文件描述符,您可以将其视为当前打开文件的可增长列表中的整数索引。每个进程都有自己的列表,尽管它由内核维护;您的代码无法直接访问它。列表中的条目包含底层文件系统用于从磁盘中提取字节的任何信息,例如inode编号,进程权限,打开标志等。
  3. filp_open函数具有实现

    struct file *filp_open(const char *filename, int flags, int mode) {
            struct nameidata nd;
            open_namei(filename, flags, mode, &nd);
            return dentry_open(nd.dentry, nd.mnt, flags);
    }
    

    做了两件事:

    1. 使用文件系统查找与传入的文件名或路径对应的inode(或更一般地说,文件系统使用的任何内部标识符)。
    2. 使用有关inode的基本信息创建struct file并将其返回。这个结构成为我之前提到的打开文件列表中的条目。
  4. 将返回的结构存储(“安装”)到进程的打开文件列表中。

  5. 释放分配的内核控制内存块。
  6. 返回文件描述符,然后可以将其传递给read()write()close()等文件操作函数。其中每一个都会将控制权移交给内核,内核可以使用文件描述符在进程列表中查找相应的文件指针,并使用该文件指针中的信息实际执行读,写或关闭。
  7. 如果您有野心,可以将此简化示例与Linux内核中open()系统调用的实现进行比较,该函数称为do_sys_open()。找到相似之处你不应该有任何困难。

    当然,这只是调用open()时发生的事情的“顶层” - 或者更确切地说,它是在打开文件的过程中调用的最高级别的内核代码。高级编程语言可能会在此基础上添加其他层。在较低级别上有很多事情发生。 (感谢Ruslanpjc50进行解释。)粗略地,从上到下:

    • open_namei()dentry_open()调用文件系统代码,它也是内核的一部分,用于访问文件和目录的元数据和内容。 filesystem从磁盘读取原始字节,并将这些字节模式解释为文件和目录树。
    • 文件系统再次使用block device layer(内核的一部分)从驱动器获取这些原始字节。 (有趣的事实:Linux允许您使用/dev/sda等从块设备层访问原始数据。)
    • 块设备层调用存储设备驱动程序,该驱动程序也是内核代码,用于从机器代码中的“读取扇区X”等中级指令转换为单个input/output instructions。存储设备驱动程序有多种类型,包括IDE(S)ATASCSIFirewire等,与驱动器可以使用的不同通信标准相对应。 (请注意,命名是一团糟。)
    • I / O指令使用处理器芯片和主板控制器的内置功能在发送到物理驱动器的电线上发送和接收电信号。这是硬件,而不是软件。
    • 在电线的另一端,磁盘的固件(嵌入式控制代码)解释电子信号以旋转磁盘并移动磁头(HDD),或读取闪存ROM单元(SSD)或任何必要的内容访问该类存储设备上的数据。

    这也可能是somewhat incorrect due to caching。 :-P严重的是,我遗漏了许多细节 - 一个人(不是我)可以写多本书来描述整个过程是如何运作的。但这应该会给你一个想法。

答案 2 :(得分:68)

我想谈谈的任何文件系统或操作系统都没关系。很好!

在ZX Spectrum上,初始化LOAD命令会使系统进入紧密循环,读取音频输入行。

数据开始由恒定音调指示,然后是一系列长/短脉冲,其中短脉冲用于二进制0,而较长的脉冲用于二进制1https://en.wikipedia.org/wiki/ZX_Spectrum_software)。紧密加载循环收集位直到它填充一个字节(8位),将其存储到内存中,增加内存指针,然后循环返回扫描更多位。

通常,加载程序首先读取的是一个简短的固定格式标头,至少表示预期的字节数,以及可能的附加信息,如文件名,文件类型和加载地址。在阅读这个短标题后,程序可以决定是继续加载大部分数据,还是退出加载例程并为用户显示相应的消息。

可以通过接收与预期一样多的字节来识别文件结束状态(固定数量的字节,软件中的硬连线或诸如标题中指示的可变数字)。如果加载循环在预期的频率范围内没有接收到一段时间的脉冲,则会引发错误。

关于这个答案的一点背景

所描述的程序从常规录音带加载数据 - 因此需要扫描音频输入(它与标准插头连接到录音机)。 LOAD命令在技术上与open文件相同 - 但它实际上与实际上加载文件相关联。这是因为录音机不受计算机控制,您无法(成功)打开文件但不能加载它。

提到“紧密循环”是因为(1)CPU,Z80-A(如果存储器服务),真的很慢:3.5 MHz,以及(2)Spectrum没有内部时钟!这意味着它必须准确地为每个人保持 T状态(指令时间)的计数。单。指令。在该循环内,只是为了保持准确的蜂鸣声时间 幸运的是,低CPU速度具有明显的优势,您可以计算一张纸上的循环次数,从而计算它们将采用的实际时间。

答案 3 :(得分:16)

这取决于操作系统打开文件时到底发生了什么。下面我将介绍Linux中发生的情况,因为它可以让您了解打开文件时会发生什么,如果您对更多细节感兴趣,可以查看源代码。我没有覆盖权限,因为它会使这个答案太长。

在Linux中,每个文件都被称为inode的结构识别。每个结构都有一个唯一的编号,每个文件只有一个inode编号。此结构存储文件的元数据,例如文件大小,文件权限,时间戳和指向磁盘块的指针,但不是实际的文件名本身。每个文件(和目录)都包含一个文件名条目和用于查找的inode编号。当您打开文件时,假设您具有相关权限,则使用与文件名关联的唯一inode编号创建文件描述符。由于许多进程/应用程序可以指向同一个文件,因此inode有一个链接字段,用于维护指向该文件的链接总数。如果目录中存在文件,则其链接计数为1,如果它具有硬链接,则其链接计数为2,如果进程打开文件,则链接计数将增加1.

答案 4 :(得分:11)

主要是簿记。这包括各种检查,如“文件是否存在?”和“我是否有权打开此文件进行书写?”。

但这是所有内核的东西 - 除非你实现自己的玩具操作系统,否则没有太多需要深入研究(如果你有,那就玩得开心 - 这是一次很棒的学习经历)。当然,您仍然应该学习打开文件时可以收到的所有可能的错误代码,以便您可以正确处理它们 - 但这些通常都是很好的小抽象。

代码级别最重要的部分是它为打开的文件提供了句柄,您可以将其用于对文件执行的所有其他操作。难道你不能使用文件名而不是这个任意句柄?嗯,当然 - 但使用手柄会带来一些好处:

  • 系统可以跟踪当前打开的所有文件,并防止它们被删除(例如)。
  • 现代操作系统是围绕句柄构建的 - 你可以使用句柄做很多有用的事情,而且所有不同类型的句柄的行为几乎相同。例如,当在Windows文件句柄上完成异步I / O操作时,会发出句柄信号 - 这允许您阻止句柄直到它发出信号,或者完全异步完成操作。等待文件句柄与等待线程句柄(例如,当线程结束时发出信号),进程句柄(再次,在进程结束时发出信号)或套接字(当某些异步操作完成时)完全相同。同样重要的是,句柄由它们各自的进程拥有,因此当进程意外终止(或者应用程序编写得很糟糕)时,操作系统会知道它可以释放什么句柄。
  • 大多数操作都是位置的 - 您从文件的最后一个位置read开始。通过使用句柄来标识文件的特定“开头”,您可以将多个并发句柄放在同一个文件中,每个句柄都从它们自己的位置读取。在某种程度上,句柄充当文件的可移动窗口(以及发出异步I / O请求的方式,非常方便)。
  • 句柄 小于文件名。句柄通常是指针的大小,通常为4或8个字节。另一方面,文件名可以有数百个字节。
  • 句柄允许操作系统移动文件,即使应用程序已将其打开 - 句柄仍然有效,即使文件名已更改,它仍指向同一文件。 / LI>

您还可以执行其他一些操作(例如,在进程之间共享句柄以使用物理文件而没有的通信通道;在unix系统上,文件也用于设备和各种其他虚拟通道,所以这不是绝对必要的),但它们并没有真正与open操作本身联系在一起,因此我不打算深入研究它。

答案 5 :(得分:7)

当它开放阅读时,其核心实际上需要才能实现。它需要做的只是检查文件是否存在,并且应用程序具有足够的权限来读取它并创建一个句柄,您可以在该句柄上向文件发出读命令。

这些命令会实际读取。

操作系统通常会通过启动读取操作来填充与句柄相关联的缓冲区来开始阅读。然后,当您实际执行读取时,它可以立即返回缓冲区的内容,而不是需要在磁盘IO上等待。

为了打开要写入的新文件,操作系统需要在目录中为新(当前为空)文件添加一个条目。然后再创建一个句柄,您可以在其上发出写命令。

答案 6 :(得分:5)

基本上,对open的调用需要找到文件,然后记录它需要的内容,以便以后的I / O操作可以再次找到它。这很模糊,但在我能立即想到的所有操作系统上都是如此。具体细节因平台而异。这里已经有很多答案谈论现代桌面操作系统。我已经对CP / M进行了一些编程,所以我将提供有关它如何在CP / M上工作的知识(MS-DOS可能以相同的方式工作,但出于安全原因,它通常不会像今天这样完成)。

在CP / M上你有一个叫做FCB的东西(正如你提到的C,你可以把它称为结构;它实际上是RAM中包含各种字段的35字节连续区域)。 FCB具有用于写入文件名的字段和用于标识磁盘驱动器的(4位)整数。然后,当您调用内核的Open File时,通过将其放在CPU的一个寄存器中来传递指向此结构的指针。一段时间后,操作系统返回结构略有变化。无论您对此文件执行何种I / O操作,都会将指向此结构的指针传递给系统调用。

CP / M对这个FCB做了什么?它会保留某些字段供自己使用,并使用它们来跟踪文件,因此最好不要在程序中触摸它们。打开文件操作在磁盘开头的表中搜索与FCB中的名称相同的文件('?'通配符与任何字符匹配)。如果找到文件,它会将一些信息复制到FCB中,包括磁盘上文件的物理位置,以便后续的I / O调用最终调用BIOS,这可能会将这些位置传递给磁盘驱动程序。在这个级别,细节会有所不同。

答案 7 :(得分:-7)

简单来说,当您打开文件时,实际上是在请求操作系统将所需文件(将文件内容复制)从辅助存储器加载到ram进行处理。这背后的原因(加载文件)是因为你不能直接从硬盘处理文件,因为它与Ram相比速度极慢。

open命令将生成一个系统调用,然后将文件内容从辅助存储(硬盘)复制到主存储(Ram)。

我们'关闭'一个文件,因为文件的修改内容必须反映到硬盘中的原始文件中。 :)

希望有所帮助。