关于C#:打开一个文件实际上做了什么?

What does opening a file actually do?

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

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

典型功能的手动页面实际上不会告诉您除"打开文件进行读/写"以外的任何信息:

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

https://docs.python.org/3/library/functions.html打开

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

换句话说,如果我要实现一个open函数,它需要在Linux上做什么?


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

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

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

I am not actually interested in implementing my own function; just in understanding what the hell is going on...'beyond the language' if you like.

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

需要注意的是,对open的调用类似于进行各种检查的验证点。如果不满足所有条件,则调用失败,返回-1而不是描述符,错误类型在errno中指明。基本检查包括:

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

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


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

1
2
3
4
5
6
7
8
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功能实现

    1
    2
    3
    4
    5
    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()调用文件系统代码(也是内核的一部分),以访问文件和目录的元数据和内容。文件系统从磁盘读取原始字节,并将这些字节模式解释为文件和目录树。
    • 文件系统使用块设备层(也是内核的一部分)从驱动器获取这些原始字节。(有趣的事实:Linux允许您使用/dev/sda等工具从块设备层访问原始数据。)
    • 块设备层调用存储设备驱动程序(也是内核代码),将"读取扇区X"等中等级别的指令转换为机器代码中的单个输入/输出指令。有几种类型的存储设备驱动程序,包括IDE,(S)ATA、SCSI、FireWire等,与驱动器可以使用的不同通信标准相对应。(注意,命名混乱。)
    • I/O指令使用处理器芯片和主板控制器的内置功能来发送和接收连接到物理驱动器的电线上的电信号。这是硬件,不是软件。
    • 在线路的另一端,磁盘的固件(嵌入式控制代码)解释电信号以旋转盘片并移动磁头(HDD),或读取闪存ROM单元(SSD),或访问该类型存储设备上的数据所需的任何内容。

    由于缓存,这也可能有些不正确。:-p说真的,我遗漏了很多细节——一个人(不是我)可以写多本书来描述整个过程是如何工作的。但这应该给你一个主意。


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

    在ZX频谱上,初始化LOAD命令将使系统进入一个紧密的循环,读取音频。

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

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

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

    关于这个答案的一点背景知识

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

    之所以提到"紧环",是因为(1)CPU,Z80-A(如果内存可用),速度非常慢:3.5兆赫,(2)频谱没有内部时钟!这意味着它必须准确地计算每一个T状态(指令次数)。单一的。指令。在那个循环中,只是为了保持准确的哔哔声计时。幸运的是,这种低CPU速度有明显的优势,您可以计算一张纸上的周期数,从而计算出它们将占用的实际时间。


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

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


    bookkeeping,mostly。这包括各种支票样"文件不存在的?"和"我的permissions开这两个文件的写作呢?"。。。。。。。

    但这一切的核心的东西——除非你实现你自己的玩具,我们有太多的不到两个delve(如果你是有趣的,有它的一个伟大的学习经验。当然,学习不应该把所有的可能的错误代码,你可以在收到开一个文件,所以你可以恰当的行动,但它们通常是那些漂亮的小abstractions。

    最重要的一方在代码级别上,它给你一个演了两个开放的档案,这是使用所有的你做的其他的操作与一个文件。你不能使用它的文件名,而不是四起作用吗?好的,但用一个酸性的行为给予你一些优点:

    • 在系统可以保持跟踪所有的档案,是目前预防和开放,他们从被消去(例如)。
    • 现代操作系统都有一个内置在交易的吨(有用的事情可以做。所有的交易,和不同的脸部几乎identically交易行为。例如,当个异步I/O操作在一个Windows文件completes行动,行动signalled冰,这允许你在两块行动,直到它的signalled,或两个完整的操作asynchronously说。等你对一个文件做冰一样的AA的到底是一个等待的线程的行为(例如当signalled线程结束),流程(一次行动,signalled当流程结束),或一个插座(有些completes异步操作)。importantly只是作为交易的是国有的城市,他们各自的工艺流程,所以当A端unexpectedly冰(冰或应用难写的),我们知道它可以释放你的交易。
    • 操作位置必须是你read从负载的位置在你的文件。城市行为识别采用两种(一特有的"开放"的文件,你可以拥有多concurrent交易的两个相同的文件,每个阅读从他们自己的地方。在一个银河系的行为,作为一个moveable到窗口的文件(和一路异步I/O问题的要求,这是很方便的)。
    • 交易是小而多的文件名。通常,一个行动的冰的大小的一个指针,typically 4或8字节。在其他的手,文件名可以拥有大学hundreds字节。
    • "我们两个交易允许移动的应用程序文件,即使有信息公开的行动仍然有效,它的静止点的两个相同的文件,即使有了文件的名称。

    也有一些其他的技巧,你能做的(例如,两股之间的交易过程中有一个通信通道没有采用物理文件;按文件是UNIX系统,也用于器件和各种其他的虚拟通道,所以这不是严格必要的),但他们不真的open系运行它,所以我不持续两个delve成这样的。


    在它的核心,当打开阅读实际上不需要任何幻想发生。它所需要做的就是检查文件是否存在,并且应用程序有足够的特权来读取它,并创建一个句柄,在这个句柄上可以向文件发出读取命令。

    在这些命令上,实际的读取将被调度。

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

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


    基本上,调用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可能会将这些位置传递给磁盘驱动程序。在这一层面上,具体情况各不相同。


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

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

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

    希望有帮助。