c语言|C语言标准IO的理解( 二 )


关于系统调用的内容 , 这一讲的后面还会提到 。 并且 , 我也会在第 31 讲中再为你深入介绍 。 在这里你只需要知道 , 系统调用的过程涉及到进程在用户模式与内核模式之间的转换 , 其成本较高 。 为了提升 IO 操作的性能 , 同时保证开发者所指定的 IO 操作不会在程序运行时产生可观测的差异 , 标准 IO 接口在实现时通过添加缓冲区的方式 , 尽可能减少了低级 IO 接口的调用次数 。
让我们再把目光移回到之前的两段示例代码上 。 不知道你在运行对应的两段程序时 , 是否有观察到它们之间的差异呢?实际上 , 使用低级 IO 接口实现的程序 , 会在用户每次输入新内容到标准输入流中时 , 同时更新文件 “temp.txt” 中的内容 。 而使用标准 IO 接口实现的程序 , 仅会在用户输入的内容达到一定数量或程序退出前 , 再更新文件中的内容 。 而在此之前 , 这些内容将会被存放到缓冲区中 。
当然 , C 标准中并未规定标准 IO 接口所使用的缓冲区在默认情况下的大小 , 对于其选择 , 将由具体标准库实现自行决定 。
除此之外 , 标准 IO 还为我们提供了可以自由使用不同缓冲策略的能力 。 对于简单的场景 , 我们可以使用名为 fflush 的接口 , 来在任意时刻将临时存放在缓冲区中的数据立刻“冲刷”到对应的流中 。 而在相对复杂的场景中 , 我们甚至可以使用 setvbuf 等接口来精确地指定流的缓冲类型、所使用的缓冲区 , 以及可以使用的缓冲区大小 。
比如 , 我们可以在上述标准 IO 实例对应 C 代码的第 4 行后面 , 插入以下两行代码:
// ...
char buf[1024
;
setvbuf(fp buf _IOFBF 5);
// ...
此时 , 再次编译并运行程序 , 其执行细节与之前相比会有什么不同?欢迎在评论区告诉我你的发现 。
用于低级 IO 接口的操作系统调用接下来 , 让我们再来看一看低级 IO 的相关实现细节 。
在前面的内容中我曾提到过 , 低级 IO 接口在其内部会通过系统调用来完成相应的 IO 操作 。 那么 , 这个过程是怎样发生的呢?
实际上 , 你可以简单地将系统调用当作是由操作系统提供的一系列函数 。 只是相较于程序员在 C 源代码中自定义的“用户函数”来说 , 系统调用函数的使用方式有所不同 。 与调用用户函数所使用的 call 指令不同 , 在 x86-64 平台上 , 我们需要通过名为 syscall 的指令来执行一个系统调用函数 。
操作系统会为每一个系统调用函数分配一个唯一的整型 ID , 这个 ID 将会作为标识符 , 参与到系统调用函数的调用过程中 。 比如在 x86-64 平台上的 Linux 操作系统中 , open 系统调用对应的 ID 值为 2 , 你会在接下来的例子中看到它的实际用法 。
同用户函数类似的是 , 系统调用函数在被调用时 , 也需要通过相应的寄存器来实现参数传递的过程 。 而正如我在第 05 讲 中提到的那样 , SysV 调用约定中规定 , 系统调用将会使用寄存器 rdi、rsi、rdx、r10、r8、r9 来进行实参的传递 。 当然 , 除此之外 , rax 寄存器将专门用于存放系统调用对应的 ID , 并接收系统调用完成后的返回值 。
那么 , 让我们通过实际代码来看一看 , 如何在机器指令层面使用系统调用 。 在下面这段代码中 , 我们直接使用机器指令调用了 open 系统调用函数 。
#include <unistd.h>
#include <fcntl.h>
int main(void) {
const char str[
= \"Enter some characters:\\";
write(STDOUT_FILENO str sizeof(str));
const char* fileName = \"./temp.txt\";
// Call to `open` starts:
// const int fd = open(\"./temp.txt\" O_RDWR | O_CREAT);
volatile int fd;
asm(\"mov $2 %%rax\\\t\"
\"mov %0 %%rdi\\\t\"
\"mov $66 %%rsi\\\t\" // 2 | 64 -> 66;
\"syscall\\\t\"
\"mov %%rax %1\\\t\"
: \"=m\" (fileName)
: \"m\" (fd));
// Call ended.
if (fd > 0) {
char ch;
while (read(STDIN_FILENO &ch 1)) {
if (ch == 'z') break;
write(fd &ch sizeof(ch));

else {
const char errMsg[
= \"File open failed.\";
write(STDERR_FILENO errMsg sizeof(errMsg));

close(fd);
return 0;

可以看到 , 在上述代码的第 10 行 , 我们以内联汇编的形式 , 在程序的执行流中插入了 5 条机器指令 。 其中 , 第 1 条指令 , 我们将系统调用 open 对应的整型 ID 值 2 放入到了寄存器 rax 中;第 2 条指令 , 我们将存放有目标文件名称的字节数组 fileName 的首地址放到了寄存器 rdi 中 , 该参数也对应着低级 IO 接口 open 的第一个参数 。 接下来的一条指令 , 我们将配置参数对应表达式 O_RDWR | O_CREAT 的计算结果值 66 放入到了寄存器 rsi 中 。 最后 , 通过指令 syscall , 我们得以调用对应的系统调用函数 。