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

jramm 261 c linux

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

但这种开放式操作实际上做了什么?

典型功能的手册页实际上并没有告诉你除了"打开读/写文件"以外的任何内容:

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

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

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

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

Blagovest Bu.. 181

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

这就是为什么fopen库函数的参数或Python openopen(2)系统调用的参数非常相似的原因.

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

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

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

重要的是要注意,调用open行为类似于进行各种检查的验证点.如果不满足所有条件,则通过返回-1而不是描述符来调用失败,并指示错误类型errno.必要的检查是:

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

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

  • 值得注意的是,在类Unix操作系统中,内核结构文件描述符被映射到,称为"打开文件描述".因此,过程FD被映射到内核OFD.这对理解文档很重要.例如,请参阅`man dup2`并检查_open文件描述符_(即碰巧打开的FD)和_open文件描述_(OFD)之间的微妙之处. (2认同)
  • 不要忘记评估和/或创建锁.这些可以是共享的,也可以是独占的,可以影响整个文件,也可以只影响其中的一部分. (2认同)

David Z.. 82

我建议您通过简化版open()系统调用来查看本指南.它使用以下代码片段,它代表打开文件时幕后发生的情况.

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. 创建一个struct file有关inode的基本信息并返回它.这个结构成为我之前提到的打开文件列表中的条目.
  4. 将返回的结构存储("安装")到进程的打开文件列表中.

  5. 释放分配的内核控制内存块.
  6. 返回文件描述符,然后可以传递给文件操作功能,如read(),write()close().其中的每一个都将控制交给内核,内核可以使用文件描述符在进程列表中查找相应的文件指针,并使用该文件指针中的信息实际执行读,写或关闭.

如果您有野心,可以将这个简化示例open()与Linux内核中的系统调用实现进行比较,这是一个名为的函数do_sys_open().找到相似之处你不应该有任何困难.


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

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

由于缓存,这也可能有些不正确.:-P严重的是,我遗漏了许多细节 - 一个人(不是我)可以写多本书来描述整个过程是如何运作的.但这应该会给你一个想法.


usr2564301.. 68

您想要谈论的任何文件系统或操作系统都很好.太好了!


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

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

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

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


关于这个答案的一点背景

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

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

  • 虽然我编辑了我的问题以限制linux/windows操作系统以保持打开,但这个答案完全有效且有用.正如我的问题所述,我不打算实施某些事情或让其他人去做我的工作,我希望学习.要了解你,你必须提出"大问题".如果我们不断关注SO的问题是因为"过于宽泛",那么它就有可能成为让人们为您编写代码的地方,而不会对什么,地点或原因给出任何解释.我宁愿把它当作我可以学习的地方. (23认同)
  • 你投票结束*和*回答? (19认同)
  • 这个答案似乎证明*你对问题的解释过于宽泛,而不是问题本身过于宽泛. (14认同)
  • @BillWoodger:是的.但这是一个公平的问题(我的意思是你的).我选择以"过于宽泛"的方式结束,我的回答是为了说明这个问题实际上是多么广泛. (10认同)
  • 我觉得你的答案有点太过分了.ZX Spectrum有一个OPEN命令,这与LOAD完全不同.而且更难理解. (8认同)
  • 关于这个问题我也不同意,但我真的很喜欢你的回答. (3认同)

Alex.. 17

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

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

  • 这与实际问题有什么关系? (6认同)

Luaan.. 11

记账,主要是.这包括各种检查,如"文件是否存在?" 和"我是否有权打开此文件进行写入?".

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

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

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

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


ratchet frea.. 7

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

这些命令就是实际读数将被分派.

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

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


Wilson.. 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,这可能会将这些位置传递给磁盘驱动程序.在这个级别,细节会有所不同.