Baurine's Blog

EOF 从何而来

June 28, 2013

最近突然想到的一个问题。

这里以 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;
}

Comments