以下是一般性描述和总结,基于编程概念而非任何特定实现。
对
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表示更改内存映射等),而每个请求都将在其他寄存器或内存中传递一些特定的参数(一个寄存器可能包含内存中的地址,而另一个寄存器则包含要写入的长度)。
因此,系统调用处理程序确定正在进行哪个请求,并派遣代码来处理它。这可能涉及收集请求的参数并将它们组成描述要执行的工作的描述,然后将该工作放在队列上并离开系统调用处理程序。
当有工作要做时,操作系统可能不会返回到用户进程。如前所述,现代操作系统有许多层。有设备驱动程序、内核扩展、微内核、操作系统内部的软件库等。无论操作系统如何组织,在某个时间,它都决定执行系统调用请求的工作。
对于向标准输出写入,工作被发送到“设备驱动程序”,这是一个处理“设备”工作的软件名称。最初,设备是连接到系统的硬件设备。设备驱动程序会将要写入的数据复制到内存中的特殊位置,并使用特殊指令向设备发出命令,从内存中读取该数据并将其发送到设备发送到的任何地方(终端、磁盘驱动器等)。设备驱动程序的另一部分将是在完成工作时调用的例程。 (此调用类似于系统调用,但通常称为中断。)完成工作后,设备驱动程序将向操作系统的其他部分发送一个消息,并最终将关于系统调用结果的信息写入用户进程的内存或寄存器中,然后恢复用户进程的执行。
现今,许多“设备”都是实现虚拟设备的软件。用户进程的标准输出很可能是某种伪终端。由于该伪终端没有实际的硬件终端,因此它必须通过请求其他软件来处理写请求。
当伪终端作为图形显示中终端窗口的一部分时,有一些软件实现了终端窗口。该软件接受被写入标准输出的文本,决定应将其放置在窗口的哪个位置,并调用其他软件将字符转换为窗口像素的变化。也就是说,有一些软件正在读取字符,在一些表格和其他数据中查找字符的描述(如字体描述等),并将这些字符绘制在图像缓冲区中。
当图像缓冲区准备好后,将调用更多的软件将图像缓冲区写入显示器。同样,这涉及传递数据到另一个设备驱动程序。最终,它到达实际的硬件设备,该设备将数据并在显示器上显示出来。
总之,这里涉及了一个巨大的事件链。数据通过多个层次进行上下传输,可能涉及多个不同的用户进程和多个不同的设备驱动程序以及许多软件库。要全面了解整个过程是困难的。通常,人们不会尝试一次性理解整个过程,而是会分别学习每个步骤。例如,在我的职业生涯中,有时我必须处理系统调用指令的细节。但是,在考虑整个系统如何工作时,我会考虑相互通信的较大级别进程,而不考虑这些通信的详细信息。