在所有编程语言(至少我使用的)中,必须先打开一个文件,然后才能对其进行读写。
但是这个开放操作实际上是做什么的呢?
典型函数的手册页面实际上没有告诉你任何东西,除了它“打开一个文件进行读写”:
http://www.cplusplus.com/reference/cstdio/fopen/
https://docs.python.org/3/library/functions.html#open
显然,通过使用该函数,您可以知道它涉及到创建某种对象,以方便访问文件。
另一种说法是,如果我要实现一个开放函数,它在Linux上需要做什么?
记账,主要是。这包括各种检查,如“文件是否存在?”和“我是否有权限打开此文件进行写入?”。
但这些都是内核的东西——除非你正在实现自己的玩具操作系统,没有太多的东西需要深入研究(如果你是,玩得开心——这是一个很好的学习经历)。当然,您仍然应该了解在打开文件时可能收到的所有错误代码,以便正确地处理它们——但这些通常都是不错的小抽象。
代码级别上最重要的部分是,它为您提供了打开文件的句柄,您可以将其用于对文件进行的所有其他操作。难道不能使用文件名来代替这个任意的句柄吗?当然,但使用手柄也有一些好处:
The system can keep track of all the files that are currently open, and prevent them from being deleted (for example).
Modern OSs are built around handles - there's tons of useful things you can do with handles, and all the different kinds of handles behave almost identically. For example, when an asynchronous I/O operation completes on a Windows file handle, the handle is signalled - this allows you to block on the handle until it's signalled, or to complete the operation entirely asynchronously. Waiting on a file handle is exactly the same as waiting on a thread handle (signalled e.g. when the thread ends), a process handle (again, signalled when the process ends), or a socket (when some asynchronous operation completes). Just as importantly, handles are owned by their respective processes, so when a process is terminated unexpectedly (or the application is poorly written), the OS knows what handles it can release.
Most operations are positional - you read from the last position in your file. By using a handle to identify a particular "opening" of a file, you can have multiple concurrent handles to the same file, each reading from their own places. In a way, the handle acts as a moveable window into the file (and a way to issue asynchronous I/O requests, which are very handy).
Handles are much smaller than file names. A handle is usually the size of a pointer, typically 4 or 8 bytes. On the other hand, filenames can have hundreds of bytes.
Handles allow the OS to move the file, even though applications have it open - the handle is still valid, and it still points to the same file, even though the file name has changed.
您还可以使用其他一些技巧(例如,在进程之间共享句柄,从而在不使用物理文件的情况下拥有通信通道;在unix系统上,文件也用于设备和各种其他虚拟通道,所以这不是严格必要的),但它们并没有真正绑定到open操作本身,所以我不打算深入研究这一点。
当你打开一个文件时到底会发生什么,这取决于操作系统。下面我将描述在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.
在几乎所有高级语言中,打开文件的函数都是对应内核系统调用的包装器。它也可以做其他奇特的事情,但是在当代的操作系统中,打开一个文件必须总是通过内核。
这就是为什么fopen库函数或Python的open函数的实参非常类似于open(2)系统调用的实参。
除了打开文件,这些函数通常还会设置一个缓冲区,用于读/写操作。这个缓冲区的目的是确保每当您想读取N个字节时,相应的库调用将返回N个字节,而不管对底层系统调用的调用是否返回更少。
我对实现我自己的功能不感兴趣;只是为了理解到底发生了什么……“超越语言”,如果你喜欢的话。
在类unix操作系统中,成功调用open将返回一个“文件描述符”,它只是用户进程上下文中的一个整数。因此,该描述符将被传递给与打开的文件交互的任何调用,并且在对其调用close之后,该描述符将失效。
重要的是要注意,对open的调用就像一个进行各种检查的验证点。如果不是所有条件都满足,则调用失败,返回-1而不是描述符,错误类型在errno中指出。基本检查包括:
文件是否存在;
调用进程是否有权限以指定的方式打开该文件。这是通过将文件权限、所有者ID和组ID与调用进程的ID相匹配来确定的。
在内核上下文中,进程的文件描述符和物理打开的文件之间必须存在某种映射。映射到描述符的内部数据结构可能包含另一个处理基于块的设备的缓冲区,或者指向当前读/写位置的内部指针。