在最底层(至少对于用户空间代码),您会使用系统调用。在类UNIX平台上,这些包括:
open
close
read
write
lseek
...等等。这些调用通过传递名为文件描述符的东西来工作。文件描述符只是不透明整数。在操作系统内部,每个进程都有一个文件描述符表,其中包含所有文件描述符和相关信息,例如它是哪个文件,它是什么类型的文件等。
此外,还有类似于UNIX系统调用的Windows API调用:
Windows使用类似于文件描述符的HANDLE
来传递参数,但我认为它们比较不灵活。例如,在UNIX上,文件描述符不仅可以表示文件,还可以表示套接字、管道和其他东西。
C标准库函数fopen
、fclose
、fread
、fwrite
和fseek
仅仅是这些系统调用的包装器。
当你打开一个文件时,通常文件内容并没有被读入内存。当你使用fread
或read
时,你告诉操作系统将特定数量的字节读入缓冲区。这个特定数量的字节可以是文件长度,但也可以不是。因此,如果需要,你可以只将文件的一部分读入内存。
对忍者编辑的回答:
你问这在机器代码级别上是如何工作的。我只能解释在Linux和Intel 32位架构上是如何工作的。当你使用系统调用时,一些参数被放入寄存器中。参数放入寄存器后,会触发中断0x80
。例如,要从stdin
(文件描述符0)读取1千字节到地址0xDEADBEEF
,你可能会使用以下汇编代码:
mov eax, 0x03 ; system call number (read = 0x03)
mov ebx, 0 ; file descriptor (stdin = 0)
mov ecx, 0xDEADBEEF ; buffer address
mov edx, 1024 ; number of bytes to read
int 0x80 ; Linux system call interrupt
int 0x80
会触发一个软件中断,通常操作系统已在中断向量表或中断描述符表中注册该中断。无论如何,处理器都会跳转到内存中的特定位置。一旦到达那里,通常操作系统将进入内核模式(如果必要),然后对eax
进行类似于C语言中的switch
的操作。从那里,它将跳转到read
的实现中。在read
中,它通常会从调用进程的文件描述符表中读取有关描述符的一些元数据。一旦获取了所需的所有数据,它将执行相应的操作,然后返回给用户代码。
假设它正在从磁盘读取,而不是从管道、stdin
或其他非物理位置读取。同时,假设它正在从主硬盘上读取。此外,假设操作系统仍然可以访问BIOS中断。
要访问文件,它需要执行一些文件系统操作。例如,遍历目录树以查找实际文件所在的位置。我不打算详细介绍这个过程,因为我相信你能猜到。
有趣的部分是从磁盘读取数据,无论是文件系统元数据、文件内容还是其他内容。首先,你会得到一个逻辑块地址(LBA)。LBA只是磁盘上一块数据的索引。每个块通常为512字节(尽管这个数字可能已经过时了)。如果我们可以访问BIOS并且操作系统使用它,那么它将把LBA转换为CHS表示法。CHS(柱面-磁头-扇区)表示法是另一种引用硬盘部分的方式。它曾经对应物理概念,但现在已经过时,但几乎所有BIOS都支持它。从那里,操作系统将数据塞入寄存器并触发中断0x13,即BIOS的磁盘读取中断。
这就是我能解释的最低层次了。我相信,在我假设操作系统使用BIOS之后的部分已经过时了。不过,在那之前的所有内容仍然是有效的,我相信,如果不是以简化的方式。