ChatGPT解决这个技术问题 Extra ChatGPT

打开文件实际上是做什么的?

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

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

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

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 的一个完全可选的部分,在过去的某些实现中甚至不存在。

B
Blagovest Buyukliev

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

这就是为什么 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”这样的伪文件系统,它们可能会做任意事情当您打开文件时)
请注意,对文件打开的检查——文件是否存在,你是否有权限——实际上是不够的。在您的脚下,该文件可能会消失,或者其权限可能会发生变化。一些文件系统试图阻止这种情况,但只要您的操作系统支持网络存储,就不可能阻止(如果本地文件系统行为不端并且是合理的,操作系统可能会“恐慌”:当网络共享不这样做时这样做的一个不是一个可行的操作系统)。这些检查也在文件打开时完成,但也必须(有效地)在所有其他文件访问时完成。
不要忘记评估和/或创建锁。这些可以是共享的,也可以是独占的,并且可以影响整个文件,也可以只影响其中的一部分。
D
David Z

我建议您看看 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  }

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

分配一块内核控制的内存并将文件名从用户控制的内存复制到其中。选择一个未使用的文件描述符,您可以将其视为当前打开文件的可增长列表的整数索引。尽管它由内核维护,但每个进程都有自己的此类列表;您的代码无法直接访问它。列表中的条目包含底层文件系统将用于从磁盘中提取字节的任何信息,例如 inode 编号、进程权限、打开标志等。 filp_open 函数具有实现 struct file *filp_open(const char *filename, int flags, int mode) { struct nameidata nd; open_namei(文件名,标志,模式,&nd);返回dentry_open(nd.dentry,nd.mnt,标志);它做了两件事:使用文件系统查找与传入的文件名或路径相对应的 inode(或更一般地说,文件系统使用的任何内部标识符)。创建一个包含有关 inode 基本信息的结构文件并返回它。这个结构成为我之前提到的打开文件列表中的条目。将返回的结构存储(“安装”)到进程的打开文件列表中。释放分配的内核控制内存块。返回文件描述符,然后可以将其传递给文件操作函数,如 read()、write() 和 close()。这些中的每一个都将控制权交给内核,内核可以使用文件描述符在进程列表中查找相应的文件指针,并使用该文件指针中的信息来实际执行读取、写入或关闭。

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

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

open_namei() 和 dentry_open() 调用文件系统代码,它也是内核的一部分,以访问文件和目录的元数据和内容。文件系统从磁盘读取原始字节并将这些字节模式解释为文件和目录树。

文件系统使用块设备层(也是内核的一部分)从驱动器获取这些原始字节。 (有趣的事实:Linux 允许您使用 /dev/sda 等从块设备层访问原始数据。)

块设备层调用存储设备驱动程序,这也是内核代码,以将中级指令(如“读取扇区 X”)转换为机器代码中的单个输入/输出指令。有几种类型的存储设备驱动程序,包括 IDE、(S)ATA、SCSI、Firewire 等,对应于驱动器可以使用的不同通信标准。 (请注意,命名是一团糟。)

I/O 指令使用处理器芯片和主板控制器的内置功能在连接到物理驱动器的线路上发送和接收电信号。这是硬件,不是软件。

在电线的另一端,磁盘的固件(嵌入式控制代码)解释电信号以旋转盘片并移动磁头(HDD),或读取闪存 ROM 单元(SSD),或访问数据所需的任何东西那种类型的存储设备。

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


g
gsamaras

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

在 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 速度具有明显的优势,您可以在一张纸上计算周期数,从而计算它们在现实世界中所花费的时间。


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

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

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


这与实际问题有什么关系?
它描述了在 Linux 中打开文件时在低级别发生的情况。我同意这个问题相当广泛,所以这可能不是 jramm 正在寻找的答案。
再说一遍,不检查权限?
L
Luaan

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

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

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

系统可以跟踪当前打开的所有文件,并防止它们被删除(例如)。

现代操作系统是围绕句柄构建的——你可以用句柄做很多有用的事情,所有不同类型的句柄的行为几乎相同。例如,当在 Windows 文件句柄上完成异步 I/O 操作时,会通知句柄 - 这允许您在句柄上阻塞直到它被通知,或者完全异步完成操作。等待文件句柄与等待线程句柄(例如,当线程结束时发出信号)、进程句柄(同样,在进程结束时发出信号)或套接字(当某些异步操作完成时)完全一样。同样重要的是,句柄归其各自的进程所有,因此当进程意外终止(或应用程序编写不佳)时,操作系统知道它可以释放哪些句柄。

大多数操作都是定位的 - 您从文件中的最后一个位置读取。通过使用句柄来识别文件的特定“打开”,您可以对同一个文件有多个并发句柄,每个句柄都从它们自己的位置读取。在某种程度上,句柄充当文件中的可移动窗口(以及发出异步 I/O 请求的一种方式,非常方便)。

句柄比文件名小得多。句柄通常是指针的大小,通常为 4 或 8 个字节。另一方面,文件名可以有数百个字节。

句柄允许操作系统移动文件,即使应用程序已打开它 - 句柄仍然有效,并且它仍然指向同一个文件,即使文件名已更改。

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


用初学者的语言很好地解释了..我来这里是为这个问题写一个简单的答案,但你的答案是完美的!
r
ratchet freak

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

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

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

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


O
OmarL

基本上,调用 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 可以将这些位置传递给磁盘驱动程序。在这个级别,具体情况有所不同。


佚名

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

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

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

希望有帮助。