打开文件实际上是做啥的?

Posted

技术标签:

【中文标题】打开文件实际上是做啥的?【英文标题】:What does opening a file actually do?打开文件实际上是做什么的? 【发布时间】:2016-02-03 09:05:51 【问题描述】:

在所有编程语言(至少我使用的)中,您必须先打开一个文件,然后才能对其进行读取或写入。

但是这个打开操作实际上是做什么的呢?

典型功能的手册页实际上并没有告诉您除了“打开文件进行读/写”之外的任何内容:

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

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

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

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

【问题讨论】:

编辑此问题以关注C 和Linux;因为 Linux 和 Windows 所做的不同。否则,它有点太宽泛了。此外,任何更高级别的语言最终都会调用系统的 C API 或编译为 C 来执行,所以留在“C”级别是把它放在最小公分母上。 更不用说不是所有编程语言都有这个功能,或者它是一个高度依赖环境的功能。当然,这些天来确实很少见,但是直到今天,文件处理还是 ANSI Forth 的一个完全可选的部分,在过去的某些实现中甚至不存在。 【参考方案1】:

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

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

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

希望对您有所帮助。

【讨论】:

【参考方案2】:

你想谈论的任何文件系统或操作系统我都可以。很好!


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

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

通常情况下,加载器首先会读取一个简短的、固定格式的标题,它至少指示预期的字节数,以及可能的附加信息,例如文件名、文件类型和加载地址。读取这个短标题后,程序可以决定是继续加载主要数据块,还是退出加载例程并为用户显示适当的消息。

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


这个答案的一点背景

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

之所以提到“紧密循环”是因为 (1) CPU,一个 Z80-A(如果没有记错的话)真的很慢:3.5 MHz,以及 (2) Spectrum 没有内部时钟!这意味着它必须准确地计算每次的T-states(指令时间)。单身的。操作说明。在该循环内,只是为了保持准确的蜂鸣时间。 幸运的是,这种低 CPU 速度具有明显的优势,您可以在一张纸上计算周期数,从而计算它们在现实世界中所花费的时间。

【讨论】:

@BillWoodger:是的。但这是一个公平的问题(我的意思是你的)。我以“过于宽泛”投票结束,我的回答旨在说明这个问题实际上是多么广泛。 我认为你的答案有点过于宽泛了。 ZX Spectrum 有一个 OPEN 命令,这与 LOAD 完全不同。而且更难理解。 @Jongware:嗯,我记得很清楚我的纸质手册,因为大约 25 年前,我想知道与 OP 相同的问题(当时没有这样 sigh)。我在 Internet 上也找不到它,但请查看 this Wikipedia entry,尤其是 OPEN#CLOSE# 命令。 虽然我编辑了我的问题以限制到 linux/windows 操作系统以保持它打开,但这个答案是完全有效和有用的。正如我的问题所述,我不打算实施某些东西或让其他人做我的工作,我正在学习。要学习,您必须提出“大”问题。如果我们不断关闭关于 SO 的问题,因为它“过于宽泛”,它可能会成为一个让人们为你编写代码而不解释什么、在哪里或为什么的地方。我宁愿把它当作我可以来学习的地方。 这个答案似乎证明了你的对问题的解释过于宽泛,而不是问题本身过于宽泛。【参考方案3】:

我建议你看看this guide through a simplified version of the open() system call。它使用下面的代码sn-p,它代表了当你打开一个文件时在幕后发生的事情。

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  

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

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

    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);
    
    

    它做了两件事:

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

    将返回的结构存储(“安装”)到进程的打开文件列表中。

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

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


当然,这只是调用open() 时发生的事情的“顶层”——或者更准确地说,它是在打开文件的过程中调用的***别的内核代码。高级编程语言可能会在此基础上添加额外的层。在较低的层次上发生了很多事情。 (感谢Ruslan和pjc50的解释。)大致从上到下:

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

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

【讨论】:

【参考方案4】:

这取决于操作系统在您打开文件时究竟会发生什么。下面我将描述 Linux 中发生的事情,因为它让您了解打开文件时会发生什么,如果您对更详细的信息感兴趣,可以查看源代码。我没有涉及权限,因为它会使这个答案太长。

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

【讨论】:

这与实际问题有什么关系? 它描述了在 Linux 中打开文件时在低级别发生的情况。我同意这个问题相当广泛,所以这可能不是 jramm 正在寻找的答案。 再说一遍,不检查权限?【参考方案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,BIOS 可能会将这些位置传递给磁盘驱动程序。在这个级别,具体情况有所不同。

【讨论】:

【参考方案6】:

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

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

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

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

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

【讨论】:

用初学者的语言很好地解释了..我来这里是为这个问题写一个简单的答案,但你的答案是完美的!【参考方案7】:

它的核心是打开阅读时没有什么花哨的东西实际上需要发生。它所需要做的就是检查文件是否存在以及应用程序是否有足够的权限来读取它并创建一个句柄,您可以在该句柄上向该文件发出读取命令。

实际读取将在这些命令上发送。

操作系统通常会通过启动读取操作来填充与句柄关联的缓冲区,从而抢占先机。然后,当您实际进行读取时,它可以立即返回缓冲区的内容,而无需等待磁盘 IO。

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

【讨论】:

【参考方案8】:

在几乎每一种高级语言中,打开文件的函数都是对应内核系统调用的封装。它也可以做其他花哨的事情,但在现代操作系统中,打开文件必须始终通过内核。

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

除了打开文件之外,这些函数通常会设置一个缓冲区,随后将用于读/写操作。这个缓冲区的目的是保证每当你想读取 N 个字节时,对应的库调用都会返回 N 个字节,而不管对底层系统调用的调用是否返回更少。

我实际上对实现自己的功能并不感兴趣;只是为了了解到底发生了什么……“超越语言”,如果你愿意的话。

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

需要注意的是,对open 的调用就像一个验证点,在此进行各种检查。如果不是所有条件都满足,则调用失败并返回-1 而不是描述符,并且错误类型在errno 中指示。基本检查是:

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

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

【讨论】:

值得注意的是,在类Unix操作系统中,内核结构文件描述符映射到的,称为“打开文件描述”。所以进程FD映射到内核OFD。这对于理解文档很重要。例如,查看man dup2 并检查打开文件描述符(即恰好打开的FD)和打开文件描述(OFD)之间的细微差别. 是的,权限在打开时检查。您可以阅读内核“开放”实现的源代码:lxr.free-electrons.com/source/fs/open.c,尽管它将大部分工作委托给特定的文件系统驱动程序。 (在 ext2 系统上,这将涉及读取目录条目以确定哪个 inode 中包含元数据,然后将该 inode 加载到 inode 缓存中。请注意,可能存在像“/proc”和“ /sys",当你打开一个文件时,它可能会做任何事情) 请注意,对文件打开的检查——文件是否存在,你是否有权限——实际上是不够的。在您的脚下,该文件可能会消失,或者其权限可能会发生变化。一些文件系统试图阻止这种情况,但只要您的操作系统支持网络存储,就不可能阻止(如果本地文件系统行为不端并且是合理的,操作系统可能会“恐慌”:当网络共享不这样做时这样做的一个不是一个可行的操作系统)。这些检查在文件打开时完成,但也必须(有效地)在所有其他文件访问时完成。 不要忘记评估和/或创建锁。这些可以是共享的,也可以是独占的,并且可以影响整个文件,也可以只影响其中的一部分。

以上是关于打开文件实际上是做啥的?的主要内容,如果未能解决你的问题,请参考以下文章

“OperationContext.Current.GetCallbackChannel”实际上是做啥的?

AFNetworking 中的 registerHTTPOperationClass 实际上是做啥的? [关闭]

NSLog 实际上是做啥的?

ImageView android:cropToPadding,它实际上是做啥的?

VC++“排除目录”项目设置实际上是做啥的?

VST/VLD 实际上是做啥的?