最近抽时间在同事推荐的一本某培训机构出的一本书,虽然我对这种机构不感冒,不过坦率的讲,书中所总结的大都是比较重要的。
C标准库中许多函数构建于系统调用之上。比如库函数fopen()就是利用系统调用open()来执行打开文件的实际操作。printf()函数就是利用系统调用write()来执行。让我们来验证上述观点。

验证上述观点前需要熟悉strace命令,strace常用来跟踪进程执行时的系统调用和所接收的信号。来自man手册的介绍:

In the simplest case strace runs the specified command until it exits.  It intercepts and records the system calls which are called  by  a  process and the signals which are received by a process.  The name of each system call, its arguments and its return value are printed on standard error or to the file specified with the -o option.

fun.c

#include <stdio.h>

int main()
{
    printf("Hello,world.\n");
    return 0;
}

gcc -g -o fun fun.c
strace ./fun
可以看到如下输出:

execve("./fun", ["./fun"], [/* 24 vars */]) = 0
brk(0)                                  = 0x1524000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa7d03a7000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=80584, ...}) = 0
mmap(NULL, 80584, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa7d0393000
close(3)                                = 0
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0 \34\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2112384, ...}) = 0
mmap(NULL, 3936832, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fa7cfdc5000
mprotect(0x7fa7cff7c000, 2097152, PROT_NONE) = 0
mmap(0x7fa7d017c000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1b7000) = 0x7fa7d017c000
mmap(0x7fa7d0182000, 16960, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fa7d0182000
close(3)                                = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa7d0392000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa7d0390000
arch_prctl(ARCH_SET_FS, 0x7fa7d0390740) = 0
mprotect(0x7fa7d017c000, 16384, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ)     = 0
mprotect(0x7fa7d03a8000, 4096, PROT_READ) = 0
munmap(0x7fa7d0393000, 80584)           = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa7d03a6000
write(1, "Hello,world.\n", 13Hello,world.
)          = 13
exit_group(0)                           = ?
+++ exited with 0 +++

其中最后部分我们可以清晰的看到write(1, "Hello,world.\n", 13Hello,world.
) = 13,因此可以证明printf函数实现是通过系统调用write,那么既然有了系统调用write,为什么还要printf函数呢?我们用如下程序同样能够实现将Hello,world打印在标准输出中。

#include <stdio.h>
#include <unistd.h>
int main()
{
    write(1, "Hello,world.\n", 13);
    return 0;
}

我个人理解是提供更方便的接口,实现更复杂的功能,比如格式化,printf("%d %d %s %p\n",i,i,str,p);等等复杂的功能。
我们来看一次实际的系统调用步骤:

1.应用程序通过调用execve()函数,发起系统调用
2.execve()函数执行一条中断指令,引发处理器从用户态切换到内核态。
3.然后中断处理程序去找到相应的系统调用,执行完成后返回相应的执行结果到用户态。

除了上面说到实现更复杂的功能外,标准I/O可以减少系统调用的次数,提高系统效率,正如同上面所说的,在执行系统调用时会从用户态切换到内核态,然后再从内核态切换到用户态,切换会增加系统的开销,为了避免这种情况,标准I/O使用时在用户控件创建缓冲区, 读写时先写入缓冲区,在缓冲区满或者用户强制刷新缓冲区时执行实际写入。

该图概括了stdio函数库和内核所采用的缓冲,以及对各种缓冲类型的控制机制。从图中自上而下,首先是通过stdio库将用户数据传递到stdio缓冲区,该缓冲区位于用户态内存区。当缓冲区填满时,stdio库会调用write()系统调用,将数据传递到内核高速缓冲区(位于内核态内存区)。最终内核发起磁盘操作,将数据传递到磁盘。
左侧所示为可于任何时刻显示强制刷新各类缓冲区的调用。图右侧所示为促使刷新自动化的调用:一是通过禁用stdio库的缓冲,二是在文件输出类的系统调用中启用同步,从而使每个write()调用立刻刷新到磁盘。