最近突然想到的一个问题。
这里以 Linux 平台为例。在使用 C 语言的标准 IO 操作文件时,一般会用 EOF 来判断是不是到达文件结尾了。比如下面的代码:
int c;
while ((c = fgetc(fp)) != EOF) {
putchar(c);
}
很多人都曾经误以为这个 EOF 是存在文件最后的内容,用来表示这个文件的结尾。我很久以前也这样以为过。但是后来用了二进制查看器查看这个文件,文件结尾就是正常的文本内容,没有 EOF 的存在。从此就更加疑惑了,这个 EOF 到底是怎么来的。
后来学习 Linux 内核后,突然明白了。实际是这样的,每一个在应用层打开的文件,在内核中都对应一个 file 的结构体,这个结构体的定义如下:
struct file {
...
struct path f_path;
const struct file_operations *f_op; /*操作函数集,比如 open, read, write */
atomic_long_t f_count;
unsigned int f_flags; /*打开标志,比如 NON_BLOCK 选项*/
fmode_t f_mode; /*打开方式,读还是写*/
loff_t f_pos; /*文件的当前偏移*/
...
};
在应用层调用 fgetc,实际会调用应用层的 read 函数,这个应用层的 read 函数最终会到内核中调用 f_op
中的 read 函数。
在 f_op
中的 read 函数中,会检查文件偏移 f_pos
是不是大于等于文件的大小,如果是,那么说明文件已经读到结尾了,没有内容可以读取了,就返回 0 值。否则就从文件偏移处读取指定字节数的文件内容,将读取的文件内容拷贝到缓冲区,并返回实际读取的个数。其内部实现逻辑大概是这样的 (真实代码比这稍复杂):
if (f_pos >= size)
return 0;
/* 将文件内容拷贝到用户态的缓冲区中,file_content 表示文件内容在内存中的起始地址 */
/* count 表示实际能读取的个数 */
if (copy_to_user(buf, file_content + f_pos, count)
return -EFAULT;
f_pos += count;
return count;
fgetc 内部调用 read 函数,若是 read 函数返回值返回值大于 0 (实际会等于 1),说明文件还未到尾且读取未出错,于是返回读取到的字符,如果 read 函数返回值为 0 或负值,说明已经到达文件末尾或出错,于是 fgetc 返回 EOF,这个 EOF 实际被定义成了 -1,以便和真正的字符 (unsigned char) 进行区分,这也是为什么 fgetc 的返回值是 int,而不是 unsigned char。内部实现大概如下:
int fgetc(FILE *fp) {
int fd = fileno(fp); /*将 FILE * 转成文件描述符*/
int n;
unsigned char c;
if ((n=read(fd, &c, 1)) > 0)
return c;
else
return EOF;
}