有两个单独的问题:
printf()
在内核内部运行时会做什么?很可能它会崩溃或什么都不做,因为您用于开发内核的 C 编译器的 RTL 可能会假设一些带有控制台、操作系统等的运行时环境。即使您使用的是 C/C++ 的独立实现,运行时可能会接管串行端口或其他什么来执行输出。您可能不希望这样,因为您的内核驱动程序将控制 I/O。因此,您需要从 RTL 重新实现底层文件 I/O。
printf()
在运行在内核之上的用户进程中运行时会发生什么?如果内核保护对硬件资源的访问,它就无能为力。来自 RTL 的底层文件 I/O 代码必须知道如何与内核通信以打开标准输入/输出“文件”的任何通道并执行数据交换。
您需要了解您使用的是 C/C++ 编译器 + RTL 的独立实现还是托管实现,以及所有影响。对于内核开发,您将使用独立的实现。对于用户空间开发,您需要一个托管实现,也许是一个交叉编译器,但运行时库必须像托管实现一样编写。请注意,在这两种情况下,您都可以使用相同的编译器,只需将其指向适当的头文件和库。例如,在 Linux 上,内核和用户空间的开发可以使用相同的 gcc 编译器完成,具有不同的头文件和库。
处理器不知道控制台是什么,或者内核是什么。一些代码必须实际访问硬件。当您printf()
从托管的 C/C++ 实现中获取时,该实现在其内部深处的某个地方将为它要运行的特定平台调用系统调用。该系统调用旨在写入一些包装“控制台”的抽象。在此系统调用的另一端是内核代码,它将将此数据推送到某些硬件。它甚至可能不是直接的硬件,它很可能是另一个进程的用户空间。
例如,每当您在 Unix 机器(KDE 的 Konsole、X11 xterm、OS X 终端等)上的基于 GUI 的终端中运行东西时,用户态进程调用printf()
在任何东西遇到硬件之前还有很长的路要走。即(即使这是简化的!):
printf()
将数据写入缓冲区
- 缓冲区被刷新到(写入)文件句柄。调用库
write()
函数。
- 库
write()
函数调用将控制权转移到内核的系统调用。
- 内核代码将数据从用户空间页面复制到内核端的非分页缓冲区,因为这些页面随时可能消失。
- 内核代码为给定的文件句柄调用写处理程序——在许多内核中,文件句柄被实现为具有虚拟方法的类。
- 文件句柄恰好是伪终端 (pty) slave。write 方法将数据传递给 pty master。
- pty master 填满给定伪终端的读取缓冲区,并唤醒等待相关文件句柄的进程。
- 实现 GUI 终端的进程唤醒并
read()
获取文件句柄。这通过库到系统调用。
- 内核调用 pty 主文件句柄的读取处理程序。
- 读取处理程序将其缓冲数据复制到用户空间。
- 系统调用返回。
- 终端进程获取数据,将其解析为控制代码,并更新其表示模拟屏幕的内部数据结构。它还在事件队列中对更新事件进行排队。控制返回到 GUI 库/框架的事件循环。这是通过事件完成的,因为这些事件通常是合并的。如果有大量可用数据,将在重新绘制任何内容之前对其进行处理以更新屏幕数据结构。
- 事件分派器将更新/重绘事件分派到“屏幕”小部件/窗口。
- 小部件/窗口中的事件处理程序代码使用内部数据结构在某处“绘制”。通常它会在位图后备存储上。
- GUI 库/框架代码向操作系统的图形驱动程序发出新数据在后备存储上可用的信号。
- 同样,通过系统调用,控制权被传递给内核。在内核中运行的图形驱动程序将在图形硬件上执行必要的魔法,将支持位图传递到屏幕。它可能是显式内存副本,或者是纹理副本与图形硬件的简单排队。