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

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

典型函数的手册页面实际上没有告诉你任何东西,除了它“打开一个文件进行读写”:

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

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

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

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


当前回答

在几乎所有高级语言中,打开文件的函数都是对应内核系统调用的包装器。它也可以做其他奇特的事情,但是在当代的操作系统中,打开一个文件必须总是通过内核。

这就是为什么fopen库函数或Python的open函数的实参非常类似于open(2)系统调用的实参。

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

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

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

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

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

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

其他回答

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

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

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

希望这能有所帮助。

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

正是在这些命令上,实际的读取将被分派。

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

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

在几乎所有高级语言中,打开文件的函数都是对应内核系统调用的包装器。它也可以做其他奇特的事情,但是在当代的操作系统中,打开一个文件必须总是通过内核。

这就是为什么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  }

简单地说,下面是代码逐行执行的操作:

Allocate a block of kernel-controlled memory and copy the filename into it from user-controlled memory. Pick an unused file descriptor, which you can think of as an integer index into a growable list of currently open files. Each process has its own such list, though it's maintained by the kernel; your code can't access it directly. An entry in the list contains whatever information the underlying filesystem will use to pull bytes off the disk, such as inode number, process permissions, open flags, and so on. The filp_open function has the implementation 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); } which does two things: Use the filesystem to look up the inode (or more generally, whatever sort of internal identifier the filesystem uses) corresponding to the filename or path that was passed in. Create a struct file with the essential information about the inode and return it. This struct becomes the entry in that list of open files that I mentioned earlier. Store ("install") the returned struct into the process's list of open files. Free the allocated block of kernel-controlled memory. Return the file descriptor, which can then be passed to file operation functions like read(), write(), and close(). Each of these will hand off control to the kernel, which can use the file descriptor to look up the corresponding file pointer in the process's list, and use the information in that file pointer to actually perform the reading, writing, or closing.

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


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

open_namei() and dentry_open() invoke filesystem code, which is also part of the kernel, to access metadata and content for files and directories. The filesystem reads raw bytes from the disk and interprets those byte patterns as a tree of files and directories. The filesystem uses the block device layer, again part of the kernel, to obtain those raw bytes from the drive. (Fun fact: Linux lets you access raw data from the block device layer using /dev/sda and the like.) The block device layer invokes a storage device driver, which is also kernel code, to translate from a medium-level instruction like "read sector X" to individual input/output instructions in machine code. There are several types of storage device drivers, including IDE, (S)ATA, SCSI, Firewire, and so on, corresponding to the different communication standards that a drive could use. (Note that the naming is a mess.) The I/O instructions use the built-in capabilities of the processor chip and the motherboard controller to send and receive electrical signals on the wire going to the physical drive. This is hardware, not software. On the other end of the wire, the disk's firmware (embedded control code) interprets the electrical signals to spin the platters and move the heads (HDD), or read a flash ROM cell (SSD), or whatever is necessary to access data on that type of storage device.

由于缓存,这也可能有些不正确。严肃地说,我漏掉了很多细节——一个人(不是我)可以写好几本书来描述整个过程是如何工作的。但这应该能给你一个概念。

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

In Linux every file is recognised by a structure called inode. Each structure has an unique number and every file only gets one inode number. This structure stores meta data for a file, for example file-size, file-permissions, time stamps and pointer to disk blocks, however, not the actual file name itself. Each file (and directory) contains a file name entry and the inode number for lookup. When you open a file, assuming you have the relevant permissions, a file descriptor is created using the unique inode number associated with file name. As many processes/applications can point to the same file, inode has a link field that maintains the total count of links to the file. If a file is present in a directory, its link count is one, if it has a hard link its link count will be two and if a file is opened by a process, the link count will be incremented by 1.