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

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

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

典型功能的手册页实际上不会告诉你任何东西,除非它'打开一个文件来读/写':

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

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

显然,通过使用这个函数,你可以告诉它涉及到创建某种对象,这有助于访问一个文件。

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


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

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

除了打开文件外,这些功能通常会设置一个缓冲区,以便随后进行读/写操作。 此缓冲区的目的是确保无论何时您想要读取N个字节,相应的库调用都将返回N个字节,而不管对底层系统调用的调用是否返回较少。

我实际上并不想执行我自己的功能; 只是在了解到底是怎么回事......'超越语言',如果你喜欢。

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

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

  • 文件是否存在;
  • 调用进程是否有权在指定模式下打开此文件。 这是通过将文件权限,所有者ID和组ID与调用进程的相应ID进行匹配来确定的。
  • 在内核环境中,进程的文件描述符和物理打开的文件之间必须有某种映射。 映射到描述符的内部数据结构可能包含处理基于块的设备的另一个缓冲区,或者指向当前读取/写入位置的内部指针。


    我建议你通过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  }
    

    简而言之,下面是代码的功能:

  • 分配一个由内核控制的内存块,并从用户控制的内存中复制文件名。
  • 选择一个未使用的文件描述符,您可以将其视为一个整数索引,并将其纳入可扩展的当前打开文件列表中。 每个进程都有自己的这样的列表,虽然它由内核维护; 你的代码不能直接访问它。 列表中的条目包含底层文件系统将用于从磁盘中提取字节的任何信息,例如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()调用文件系统代码,它也是内核的一部分,用于访问文件和目录的元数据和内容。 文件系统从磁盘读取原始字节,并将这些字节模式解释为文件和目录树。
  • 文件系统使用块设备层(也是内核的一部分)从驱动器获取这些原始字节。 (有趣的事实:Linux允许你使用/dev/sda等来访问块设备层的原始数据。)
  • 块设备层调用存储设备驱动程序(也是内核代码),以从诸如“读取扇区X”的中等级别指令转换为机器代码中的单独输入/输出指令。 有几种类型的存储设备驱动程序,包括IDE,(S)ATA,SCSI,Firewire等等,对应于驱动器可以使用的不同通信标准。 (请注意,命名是一团糟。)
  • I / O指令使用处理器芯片和主板控制器的内置功能在发送到物理驱动器的电线上发送和接收电信号。 这是硬件,而不是软件。
  • 在电线的另一端,磁盘的固件(嵌入式控制代码)解释电子信号以旋转盘片并移动磁头(HDD),或读取闪存ROM单元(SSD)或访问数据所需的任何内容该类型的存储设备。
  • 由于缓存,这可能也有些不正确。 :-P认真地说,我遗漏了很多细节 - 一个人(不是我)可以写出多本书来描述整个过程如何工作。 但是这应该给你一个想法。


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


    在ZX频谱上,初始化LOAD命令将使系统进入紧密循环,读取Audio In线路。

    数据开始由恒定的音调表示,之后接着是一个长/短脉冲序列,其中短脉冲是二进制0而较长脉冲是二进制1 (https://en.wikipedia)。组织/维基/ ZX_Spectrum_software)。 紧密加载循环会收集比特,直到它填充一个字节(8比特),将其存储到内存中,增加内存指针,然后循环返回以扫描更多比特。

    通常情况下,加载器会读取的第一件事是一个简短的固定格式头文件,至少指示期望的字节数,并且可能还有其他信息,例如文件名,文件类型和加载地址。 读完这个简短的标题后,程序可以决定是继续加载大部分数据,还是退出加载例程并向用户显示适当的消息。

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


    关于这个答案的一点背景

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

    因为(1)CPU,一个Z80-A(如果内存服务),真的很慢:3.5 MHz,(2)Spectrum没有内部时钟! 这意味着它必须精确地保持每个T状态的计数(指令时间)。 单。 指令。 在该循环内部,只是为了保持准确的蜂鸣声时间。
    幸运的是,低CPU速度具有明显的优势,您可以计算一张纸上的周期数,从而计算出它们所需的真实世界时间。

    链接地址: http://www.djcxy.com/p/54569.html

    上一篇: What does opening a file actually do?

    下一篇: fscanf csv in C. Value not assigned