stdin, stdout, stderrFILE* 类型的,是流/文件指针,不是文件描述符

文件描述符:系统的概念,每一个进程内部都有一个打开文件表,文件描述符( int 类型)就是打开文件表的索引
流:是 C 语言的概念, C 语言通过流来对文件进行操作。流提供了用户缓冲区。

文件描述符和流比较

流/文件指针 文件描述符
函数 read(), write() fgets(), printf()
缓冲区 可以有 一般没有
类型 FILE* int
例子 stdin, stdout, stderr 0, 1, 2

stdout背后的标准输出文件

向一个文件描述符写入内容(通过系统提供的 write() 函数)不需要经过缓冲
向一个流写入内容(通过 C 语言提供的 fwrite() 函数)一般要经过缓冲

可以这样理解文件描述符和流的关系:

  • 流相当于在文件描述符外面包装了一层缓冲区buffer

image.png

流缓冲的存在

由于流缓冲的存在,向流中写入的内容,并不会马上写入到文件中

以下的例子可以很好地说明这一点:(虽然 fputs("first") 先调用,但是由于缓冲区的存在,实际写入的时刻在 fputs("\n")

  • write直接向标准输出文件写数据,直接到达;
  • fputs通过流stdout的缓冲区buffer向标准输出文件写数据,等到满足条件才到达。
  • 即:stdout并不等于标准输出文件。
  • 注:如果使用printf函数,内容会马上出现在屏幕上,大概是因为printf里面调用了fflush吧。

代码如下:

  1. int main(int argc, char **argv)
  2. {
  3. fputs("first ", stdout);
  4. char stdout_filedes[256] = "last ";
  5. write(fileno(stdout), stdout_filedes, sizeof(stdout_filedes));
  6. fputs("\n", stdout); // fputs 实际生效的时候
  7. return 0;
  8. }

得到输出为:

  1. >>> ./main.exe
  2. last first

流相关函数 & 描述符相关函数

操纵流的函数

注:流就是 FILE*
前缀为 f 的函数,这些函数都位于 stdio.h 中,是 C 标准库,有些符合 posix 标准
包括: fopen, flose, fprintf, fscanf, fputc, fputs, fgetc, fgets

操作文件描述符的函数

文件描述符就是 int 类型的
这些函数是系统提供的,分散在如 unistd.h 等文件中;不是 C 标准函数,有些符合 posix 标准
包括: read, write, close, open, fcntl, ioctl, lseek, pread, readdir, readlink, readv, select

流的模型示意

stdin, stdout, stderr为例:

FIFE* stdin是一个流

通过键盘向stdin写入数据,当输入数据充满了缓冲区,或者输入了换行符之后,缓冲区中的内容会被flush到内核中,此时,stdin就是可读的。

如果使用fgets函数来读取stdin。当stdin可读之后,fgets会读取一定长度的内容或者读一行内容,到指定的内存区域。

注:所谓的键盘缓冲,实际就是stdin的缓冲。

FIFE* stdout是一个流

如果使用fputs函数向stdout输出内容。内容首先被输入到内核,然后内核将内容放入stdout的缓冲区。如果缓冲区被充满,或者进入了一个换行符,缓冲区中的内容就会被输出到stdout

FIFE* stderr是一个流

stderr通常不设置缓冲区。

如图所示:
image.png

验证流缓冲

通过以下的例子,来说明流中的缓冲:
流缓冲的例子.PNG

很好地验证了stdout有缓冲,以及缓冲的作用机制。

输出输出重定向

不进行输入输出重定向时:
image.png

对标准输入进行重定向: ./main < temp
image.png

示意图:
未命名绘图.png

得出结论:输入输出重定向不会影响 stdin, stdout, stderr0, 1, 2 之间的对应关系
只会改变文件描述符到实际文件的对应关系。

打开文件表

进程的 PCB(process control block)中有一张打开文件表,文件描述符就是这张表的索引。每一个表项指向内核中的一个 file 结构体

file 结构体记录了文件的打开方式 f_flags ,当前位置 f_pos ,以及引用计数 f_count 等。其中:

  • 不同进程,就算打开同一个文件,对应的也是不同的 file 结构体;所以,每个进程针对同一个文件可以有不同的读写位置 f_pos
  • 引用计数 f_count 只有在 fork, dup 时才会增加

file 结构体还指向 file_operater 的结构体,这个结构体中全是函数指针,内核中各种文件操作函数。用户态的各种 read, write 函数,最终会调用这些函数。

如下图所示:
未命名绘图.png