在C语言中,printf背后的实现原理是什么?

13

可能是重复问题:
理解printf函数的底层实现

我不是在寻找printf函数的具体实现,而是想知道,在C语言中调用printf时发生了什么?所有的软件和硬件级别上发生的活动都有哪些。

我认为: PrintfCAll -> KernelModeOn -> SystemCallMade -> 数据放入某种输出缓冲区 -> 输出被转储到某个控制器的缓冲区中 -> 控制器将其转储到监视器上 -> 中断CPU以表示工作已完成。

我的看法正确吗? 谢谢。

编辑:可以使用Unix平台,例如Ubuntu。 还有人能告诉我数据从何处流出,监视器是否有控制器?上面的时间表有多少是正确的?


1
在哪个平台上?这完全是实现定义的。 - Billy ONeal
3
类似的之前的问题:[https://dev59.com/rHE95IYBdhLWcg3wGqAH] - Sunil Bojanapally
3个回答

24
以下是一般性描述和总结,基于编程概念而非任何特定实现。 对 printf 的调用始于普通的子程序调用,内核模式并未参与。在很大程度上,printf 是普通的代码,可以用 C 语言编写。printf 代码主要负责解释格式字符串,将参数转换为要写入的字符串,并将这些字符串写入输出文件。其中大部分工作都将通过 printf 调用的子程序完成,例如将数字(如 intfloat)转换为数字串(代表数字的字符串)的子程序等。 printf 还可能调用 malloc 或相关例程以获取存储字符串的缓冲区的内存。我将不在本答案中描述 malloc 调用。 解释格式字符串、转换参数并准备要写入的字符串的所有工作都可以用 C 语言完成,尽管高质量的库可能会使用各种特定于目标的优化,包括汇编语言,以提高速度或效率。 当 printf 有要打印的字符串时,它将调用一个例程将字符串写入 stdout 中。这个例程可能是 fwrite 或类似的例程。在讨论中,我假设它是 fwrite。 通常情况下,流是带缓冲区的。因此,当 printf 调用 fwrite 时,fwrite 会检查其缓冲区的占用情况。如果来自 printf 的新字符串可以适应缓冲区,则 fwrite 只需将该字符串添加到缓冲区并返回。如果缓冲区已满,则 fwrite 调用另一个例程实际上将缓冲区内容写入文件。 (通常,这涉及将部分传入字符串填充到缓冲区中,将缓冲区写入文件[并标记缓冲区为空],然后将剩余的传入字符串复制到新空缓冲区中)。根据情况,还可能触发其他事情来写入缓冲区,例如检测传入字符串中的换行符。 假设 fwrite 调用系统例程 write 来写入缓冲区。 write 的外表是一个库例程;fwrite 执行普通的子程序调用以调用 write。系统例程将有一部分是普通子程序,但是,当它们需要进行繁琐的工作时,会有某种系统调用指令(有时称为陷阱)。当您执行系统调用指令时,处理器会执行几个操作。它将处理器寄存器保存在指定的位置。这包括通用寄存器和描述用户进程状态的特殊寄存器。然后处理器切换到内核模式,通常需要设置位以指示新的执行状态具有特权(允许更改特殊处理器寄存器,执行特殊指令等),并从其他位置加载寄存器或将其设置为已知值。特别地,程序计数器(处理器读取要执行的指令的位置)被设置为指向一个特定位置,其中操作系统具有处理系统调用的代码。
现在,处理器正在内核模式下执行。通常,此时处理器的工作是尽快退出内核模式,以便可以在进程之间恢复时间共享并准备进行其他工作。此外,现代操作系统有许多层,因此很难精确地说在此时发生了什么。
一种情况是,系统调用处理程序(当系统调用发生时调用的软件)读取用户进程的保存寄存器和内存,以确定进程所请求的内容。在每个系统上,都规定了一种传递参数给系统调用的方法。例如,某个寄存器可能包含表示请求的数字(0表示写入,1表示读取,2表示获取当前时间,3表示更改内存映射等),而每个请求都将在其他寄存器或内存中传递一些特定的参数(一个寄存器可能包含内存中的地址,而另一个寄存器则包含要写入的长度)。
因此,系统调用处理程序确定正在进行哪个请求,并派遣代码来处理它。这可能涉及收集请求的参数并将它们组成描述要执行的工作的描述,然后将该工作放在队列上并离开系统调用处理程序。
当有工作要做时,操作系统可能不会返回到用户进程。如前所述,现代操作系统有许多层。有设备驱动程序、内核扩展、微内核、操作系统内部的软件库等。无论操作系统如何组织,在某个时间,它都决定执行系统调用请求的工作。
对于向标准输出写入,工作被发送到“设备驱动程序”,这是一个处理“设备”工作的软件名称。最初,设备是连接到系统的硬件设备。设备驱动程序会将要写入的数据复制到内存中的特殊位置,并使用特殊指令向设备发出命令,从内存中读取该数据并将其发送到设备发送到的任何地方(终端、磁盘驱动器等)。设备驱动程序的另一部分将是在完成工作时调用的例程。 (此调用类似于系统调用,但通常称为中断。)完成工作后,设备驱动程序将向操作系统的其他部分发送一个消息,并最终将关于系统调用结果的信息写入用户进程的内存或寄存器中,然后恢复用户进程的执行。

现今,许多“设备”都是实现虚拟设备的软件。用户进程的标准输出很可能是某种伪终端。由于该伪终端没有实际的硬件终端,因此它必须通过请求其他软件来处理写请求。

当伪终端作为图形显示中终端窗口的一部分时,有一些软件实现了终端窗口。该软件接受被写入标准输出的文本,决定应将其放置在窗口的哪个位置,并调用其他软件将字符转换为窗口像素的变化。也就是说,有一些软件正在读取字符,在一些表格和其他数据中查找字符的描述(如字体描述等),并将这些字符绘制在图像缓冲区中。

当图像缓冲区准备好后,将调用更多的软件将图像缓冲区写入显示器。同样,这涉及传递数据到另一个设备驱动程序。最终,它到达实际的硬件设备,该设备将数据并在显示器上显示出来。

总之,这里涉及了一个巨大的事件链。数据通过多个层次进行上下传输,可能涉及多个不同的用户进程和多个不同的设备驱动程序以及许多软件库。要全面了解整个过程是困难的。通常,人们不会尝试一次性理解整个过程,而是会分别学习每个步骤。例如,在我的职业生涯中,有时我必须处理系统调用指令的细节。但是,在考虑整个系统如何工作时,我会考虑相互通信的较大级别进程,而不考虑这些通信的详细信息。


2

C的printf调用会写入程序的标准输出缓冲区。如果连接了控制台/终端,则控制台/终端将读取该数据,并通过视频驱动程序显示它。


1
假设它是一个视频终端。 - Keith Thompson

0

printf函数并不是C语言的一部分,因为C语言本身没有定义输入或输出。printf函数只是标准库函数中的一个有用函数,可以被C程序访问。printf函数的行为在ANSI标准中定义。如果您使用的编译器符合此标准,则所有功能和属性都应该对您可用。


C语言是由同一ISO标准定义的,该标准定义了printf的行为。将它们区分开来有些毫无意义。 - Potatoswatter
当然,任何语言标准或规范与该语言使用的库之间存在差异。库的作用是支持或增强语言的边界。 - Pramod
我认为标准定义了语言和标准库,但这里所要求的是实现定义。OP想知道标准库如何执行标准规定的操作。这取决于实现和底层操作系统。 - Axel
我必须同意你们两个的观点:printf()是一个库函数,严格来说它并不是语言本身的一部分。然而,C标准定义了它的行为和可用性,因此它是C标准的一部分。 - user529758
1
@Potatoswatter:这个讨论实际上是关于“语言”这个词的含义。标准在两个意义上使用这个词:覆盖整个标准,或仅覆盖第6节(第7节涵盖库)。printf绝对是C语言的一部分。 - Keith Thompson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接