以下是一般性的描述和总结,基于一般的编程概念,而不是任何具体的实现。
对printf 的调用以普通的子程序调用开始;不涉及内核模式。在很大程度上,printf 是普通代码,可以用 C 编写。printf 代码本身的大部分涉及解释格式字符串,将参数转换为要写入的字符串,并将这些字符串写入输出文件。大部分工作将通过printf 调用的子例程完成,例如将数字(int 或float 等对象)转换为数字(表示数字的字符串)的子例程。
printf 也可能调用malloc 或相关例程来为准备字符串的缓冲区获取内存。我将避免在此答案中描述 malloc 调用。
所有解释格式字符串、转换参数和准备要编写的字符串的工作都可以在 C 中完成,尽管高质量的库可能会使用各种特定于目标的优化,包括汇编语言,以提高速度或效率。
在某些时候,当printf 有一个字符串要打印时,它会调用一个例程将字符串写入stdout。这可能是fwrite 或一些类似的子程序。为了讨论,我假设它是fwrite。
通常,流是缓冲的。所以,当printf 调用fwrite 时,fwrite 检查它的缓冲区有多满。如果来自printf 的新字符串适合缓冲区,fwrite 只是将字符串添加到缓冲区并返回。如果缓冲区已满,则fwrite 调用另一个例程将缓冲区内容实际写入文件。 (通常,这涉及使用传入字符串的一部分填充缓冲区,将缓冲区写入文件 [并将缓冲区标记为空],然后将传入字符串的其余部分复制到新的空缓冲区中。)某些其他事情也可能触发写入缓冲区,例如检测传入字符串中的换行符,视情况而定。
假设要写入缓冲区,fwrite 调用系统例程 write。 write 的脸是库例程; fwrite 执行一个普通的子程序调用来调用write。系统例程会有一部分是普通的子例程,但是,当它们需要做具体的工作时,会有某种系统调用指令(有时称为陷阱)。
当您执行系统调用指令时,处理器会做几件事。它将处理器寄存器保存在指定位置。这包括描述用户进程状态的通用寄存器和特殊寄存器。然后处理器切换到内核模式,这通常涉及设置位以指示新的执行状态是特权(允许更改特殊处理器寄存器,执行特殊指令等)并从其他位置加载寄存器,或将它们设置为已知价值观。特别是,程序计数器(处理器读取要执行的指令的位置)被设置为指向一个特定的位置,操作系统有代码来处理系统调用。
现在处理器正在内核模式下执行。通常,此时处理器的工作是尽快退出内核模式,以便它可以恢复进程之间的分时并为其他工作做好准备。此外,现代操作系统有很多层,因此很难准确地说出此时会发生什么。
一种情况是系统调用处理程序(发生系统调用时调用的软件)读取用户进程保存的寄存器和内存以确定进程请求的内容。在每个系统上,都指定了一些将参数传递给系统调用的方法。例如,某个寄存器可能包含一个指示请求是什么的数字(0 表示写入,1 表示读取,2 表示获取当前时间,3 表示更改内存映射等),并且每个请求都会传入某些参数其他寄存器或内存中(一个寄存器可能包含内存中的地址,而另一个包含要写入的长度)。
因此,系统调用处理程序会找出正在发出的请求并分派给代码来处理它。这可能涉及收集请求的参数并将它们形成对要完成的工作的描述,然后将该工作放入队列并离开系统调用处理程序。
虽然有工作要做,但操作系统可能不会返回到用户进程。正如我之前提到的,现代操作系统中有很多层。操作系统中有设备驱动程序、内核扩展、微内核、软件库等等。然而,操作系统是有组织的,有时它会决定执行系统调用所要求的工作。
在写入标准输出的情况下,工作被发送到“设备驱动程序”,这是处理“设备”工作的软件的名称。最初,设备是连接到系统的硬件。设备驱动程序会将要写入的数据复制到内存中的一个特殊位置,并向设备发出命令(使用特殊指令)从内存中读取该数据并将其发送到设备发送它的任何地方(终端、磁盘驱动器) , 任何)。设备驱动程序的另一部分是工作完成时调用的例程。 (此调用类似于系统调用,但通常称为中断。)工作完成后,设备驱动程序会将消息传递回操作系统的其他部分,最终有关系统调用结果的信息将被写入用户进程的内存或寄存器,用户进程的执行将被重新启动。
今天,许多“设备”是实现虚拟设备的软件。用户进程的标准输出可能是某种伪终端。由于该伪终端没有实际的硬件终端,它必须通过请求其他软件的帮助来处理写入请求。
当伪终端是图形显示器上终端窗口的一部分时,有一些软件可以实现终端窗口。该软件接受写入标准输出的文本,决定将其放置在窗口中的哪个位置,并调用其他软件将字符转换为窗口中像素的变化。也就是说,某些软件正在读取字符,在某些表格和其他数据中查找它们的描述(字体描述等),然后将这些字符绘制到图像缓冲区中。
当图像缓冲区准备就绪时,会调用更多软件将图像缓冲区写入显示器。同样,这涉及将数据传递给另一个设备驱动程序。最终,它到达一个实际的硬件设备,该设备获取数据并将其显示在显示器上。
总之,有一个巨大的事件链。数据通过多个层向上和向下传递,可能涉及几个不同的用户进程和几个不同的设备驱动程序以及许多软件库。很难全面了解整个过程。通常,人们不想尝试一下子了解整个过程,而是会分别了解每个步骤。例如,在我的职业生涯中,有时我不得不处理系统调用指令的微小细节。但是,当考虑我的整个系统如何工作时,我会考虑更大级别的进程之间的通信,而不考虑这些通信如何工作的细节。