打开文件实际上有什么作用?


266

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

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

除了“打开文件以供读取/写入”外,典型功能的手册页实际上没有告诉您任何其他信息:

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

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

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

另一种表达方式是,如果我要实现一个open功能,那么在Linux上它将需要做什么?


13
编辑此问题以关注CLinux。因为Linux和Windows的功能不同。否则,范围太广。同样,任何高级语言最终都将为系统调用C API或编译为C以执行,因此保留为“ C”级别会将其置于最小公分母。
George Stocker

1
更不用说并不是所有的编程语言都具有此功能,或者它是高度依赖于环境的功能。当然,这些天当然很少见,但是到目前为止,文件处理是ANSI Forth的完全可选部分,并且在过去的某些实现中甚至没有出现。

Answers:


184

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

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

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

我实际上对实现自己的功能不感兴趣;只是想了解到底发生了什么...“超出语言范围”。

在类似Unix的操作系统中,成功调用将open返回“文件描述符”,该文件描述符在用户进程的上下文中仅仅是一个整数。因此,此描述符将传递到与打开的文件进行交互的任何调用,并且在对其进行调用之后close,该描述符将变为无效。

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

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

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


2
值得注意的是,在类Unix操作系统中,内核结构文件描述符被映射到“打开文件描述”。因此,过程FD被映射到内核OFD。这对于理解文档很重要。例如,查看man dup2并检查打开的文件描述符(也就是刚打开的FD)和打开的文件描述(OFD)之间的细微差别。
rodrigo 2015年

1
是的,权限在打开时检查。您可以阅读内核的“开放”实现的源代码:lxr.free-electrons.com/source/fs/open.c,尽管它会将大部分工作委托给特定的文件系统驱动程序。
pjc50

1
(在ext2系统上,这将涉及读取目录条目以识别哪个inode中包含元数据,然后将该inode加载到inode缓存中。请注意,可能存在诸如“ / proc”和“ / sys”之类的伪文件系统,它们可以执行任意操作当您打开文件时)
pjc50

1
请注意,实际上,对文件打开的检查(文件是否存在,您是否具有权限)是不够的。该文件可以在您的脚下消失,或者其权限可以更改。某些文件系统试图防止这种情况,但是只要您的操作系统支持网络存储,就无法避免(如果本地文件系统行为不正常且合理,则操作系统可能会“恐慌”:当网络共享不起作用时,操作系统可能会“恐慌”)可行的操作系统)。这些检查也在文件打开时进行,但也必须(有效)在所有其他文件访问时进行。
Yakk-亚当·内夫罗蒙特2015年

2
不要忘记评估和/或创建锁。这些可以是共享的,也可以是排他的,并且可以影响整个文件或仅部分文件。
Thinkeye,2015年

83

我建议您通过简化的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. 选择一个未使用的文件描述符,您可以将其视为当前打开文件的可增长列表中的整数索引。每个进程都有自己的此类列表,尽管它由内核维护。您的代码无法直接访问它。列表中的条目包含基础文件系统将用于从磁盘提取字节的信息,例如索引节点号,进程权限,打开标志等。
  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. 使用文件系统来查找与传入的文件名或路径相对应的索引节点(或更普遍地说,文件系统使用哪种内部标识符)。
    2. struct file使用有关索引节点的基本信息创建一个,并将其返回。该结构成为我前面提到的打开文件列表中的条目。
  4. 将返回的结构存储(“安装”)到进程的打开文件列表中。

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

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


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

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

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


67

您想要谈论的任何文件系统或操作系统对我来说都很好。真好!


在ZX Spectrum上,初始化LOAD命令将使系统陷入紧密的循环,读取Audio In行。

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

通常,加载程序首先要读取的是简短的固定格式的标头,它至少指示期望的字节数,并可能包含其他信息,例如文件名,文件类型和加载地址。读取此简短标题后,程序可以决定是继续加载大部分数据,还是退出加载例程并为用户显示适当的消息。

文件结束状态可以通过接收所需的任意数量的字节(固定数量的字节,在软件中进行硬连线,或可变数量(如标头中指示))来识别。如果加载环路在一定时间内未收到预期频率范围内的脉冲,则会引发错误。


关于这个答案的一点背景

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

之所以提到“紧密循环”,是因为(1)CPU,Z80-A(如果有内存),速度非常慢:3.5 MHz,并且(2)Spectrum没有内部时钟!这意味着它必须准确地记录每个状态T状态(指令时间)。单。指令。在该循环内,只是为了保持准确的蜂鸣时间。
幸运的是,较低的CPU速度具有明显的优势,您可以计算一张纸上的周期数,从而可以计算出实际的时间。


10
@BillWoodger:是的。但这是一个公平的问题(我是说你的)。我以“过于广泛”来投票结束,我的回答是要说明这个问题实际上有多广泛。
usr2564301 2015年

8
我认为您扩展的答案太多了。ZX Spectrum具有OPEN命令,这与LOAD完全不同。而且很难理解。
rodrigo 2015年

3
我也不同意结束问题,但是我非常喜欢你的回答。
恩佐·费伯

23
尽管我编辑了问题以将其限制为linux / windows OS,以使其保持打开状态,但是此答案是完全有效和有用的。正如我的问题所述,我不是在寻求实施某件事或让其他人来从事我的工作,而是在寻求学习。要学习,您必须提出“大问题”。如果我们不断地就SO的“范围太广”提出疑问,那么它就有可能成为让人们为您编写代码而又不提供任何解释,地点,原因的地方。我宁愿将其保留为可以学习的地方。
jramm

14
这个答案似乎证明对问题解释过于广泛,而不是问题本身过于广泛。
jwg 2015年

17

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

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


6
这与实际问题有什么关系?
比尔·伍德杰

1
它描述了在Linux中打开文件时在较低级别发生的情况。我同意这个问题是很广泛的,因此这可能不是jramm寻求的答案。
亚历克斯(Alex)

1
再次,没有检查权限吗?
比尔·伍德格

11

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

但这就是内核的全部内容-除非您要实现自己的玩具操作系统,否则就没有太多需要研究的内容(如果您有,请找乐子-这是一个很棒的学习经验)。当然,您仍然应该学习打开文件时可能收到的所有可能的错误代码,以便可以正确地处理它们-但这通常是一些不错的抽象概念。

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

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

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


7

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

在那些命令上,实际的阅读将被调度。

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

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


5

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

在CP / M上,您有一个称为FCB的东西(正如您提到的C,您可以将其称为结构;它实际上是RAM中包含各种字段的35字节连续区域)。FCB具有写文件名和标识磁盘驱动器的(4位)整数的字段。然后,当调用内核的“打开文件”时,通过将其放置在CPU的寄存器之一中来传递指向该结构的指针。一段时间后,操作系统返回,但结构有所更改。无论您对该文件执行什么I / O操作,都将指向此结构的指针传递给系统调用。

CP / M如何使用此FCB?它保留了某些字段供自己使用,并使用这些字段来跟踪文件,因此最好不要从程序内部接触它们。“打开文件”操作在磁盘开头的表中搜索与FCB中的名称相同的文件(“?”通配符与任何字符匹配)。如果找到文件,则会将某些信息复制到FCB中,包括文件在磁盘上的物理位置,以便随后的I / O调用最终会调用BIOS,BIOS会将这些位置传递给磁盘驱动器。在此级别,具体情况有所不同。


-7

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

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

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

希望能有所帮助。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.