【发布时间】:2009-04-04 17:01:29
【问题描述】:
我正在为我的 C 程序编写一个小内核。
这不是(目前)操作系统内核,它只是我在不依赖外部源(即 stdio.h)的情况下跟踪程序中的输入和输出的一种方式。你可能会问我为什么要这样做;这只是为了让我知道它是如何工作的,以便我对程序流程有更多、更多(最终目标是完全)控制。
我想知道是否有人知道一些关于 C 输入和输出的教程(使用内联 asm?)而不依赖任何其他代码。
【问题讨论】:
我正在为我的 C 程序编写一个小内核。
这不是(目前)操作系统内核,它只是我在不依赖外部源(即 stdio.h)的情况下跟踪程序中的输入和输出的一种方式。你可能会问我为什么要这样做;这只是为了让我知道它是如何工作的,以便我对程序流程有更多、更多(最终目标是完全)控制。
我想知道是否有人知道一些关于 C 输入和输出的教程(使用内联 asm?)而不依赖任何其他代码。
【问题讨论】:
裸机和 stdio 之间有很大的空间。你说你不是在写操作系统内核,但不管你是否在操作系统下运行。
在没有操作系统的情况下直接在硬件上运行,您仍然希望将所有 I/O 操作封装在一个模块中,即使您没有为所有 I/O 模块正式定义设备驱动程序接口和框架跟随。这在很大程度上依赖于架构,并使您有责任了解与您可能使用的每个 I/O 设备交互的所有细节。对于某些设备,这可能很快成为一项巨大的开发工作。这对嵌入式系统来说不是问题,但以这种方式在商业硬件上运行既不容易也不推荐。
在操作系统中运行时,您可能无法(也不应该想要)访问实际的硬件寄存器和中断。如果您正在开发自定义 I/O 设备,最佳实践是使其符合现有标准,以便您需要尽可能少的低级自定义软件。这就是为什么您会看到许多通过 USB 连接并将自己标识为 HID(人机接口设备)的自定义用户界面小工具。作为 HID,现有的 USB 驱动程序负责物理层,操作系统提供的 HID 驱动程序负责逻辑接口,为应用程序提供非常简单的高级访问 API。
操作系统的关键角色之一是为所有设备提供一致的 I/O API。通常,它采用open()、close()、read()、write() 和ioctl() 函数的形式(名称各不相同,但至少前四个的某种形式将始终存在)。然而,操作系统层非常原始。通常,操作系统调用无需太多处理就被转发到设备驱动程序,然后设备驱动程序将数据转发到设备上。通常,操作系统低级调用会阻塞调用者,直到它们完成,并且它们通常对有意义的缓冲区大小有限制。例如,对磁盘设备的原始访问通常需要一次访问整数个磁盘块。
不要忘记文件系统和网络协议之类的东西……通过封装在操作系统中,所有这些都变得更加可靠和兼容。
即使为单个字符调用read() 和write() 是可以接受的,但这通常不是最好的性能。操作系统调用相对昂贵,如果您可以在一次调用中读取多个字符,那么您的性能会大大提高。
这就是 C 的 stdio 库以及其他环境中的各种其他缓冲库的起源。 stdio 库提供了一个缓冲层,将 C 代码与底层硬件的块大小隔离开来。即使在完全自主开发的操作系统上,您可以完全控制所有设备,像 C stdio 这样的东西仍然很有价值。
编写自己的 stdio 替换是一项非常有价值的练习,即使您没有在生产代码中使用它,也是我向任何想要了解printf() 和 scanf() 之间真正发生的事情的人推荐的练习以及终端或文件。
P.J. Plauger 的书The Standard C Library 是一个宝贵的资源。在其中,作者介绍了 ANSI 标准中指定的完整 C 运行时库的实现。他对他所做的具体实现选择的讨论很有价值,并且与这个问题的上下文相吻合,关于为什么指定一些标准库特性的讨论也很有趣。
【讨论】:
这类事情是非常特定于架构的。简而言之,您的 I/O 设备将向 CPU 提出硬件中断。 CPU 将调用与中断相关的代码,该代码将适当地处理它;对于输入设备,它将从设备获取可用数据,对于输出设备,中断通常意味着设备已准备好发送下一个数据。
旧的 8088/8086 CPU 架构是一个很好的简单起点,可以让您开始了解这一点。通常,BIOS 将是处理硬件中断的地方,但您始终可以编写自己的中断。 ;)
【讨论】:
你必须使用 BIOS 调用,Int 10h 是用于写入显存的调用。
http://hdebruijn.soo.dto.tudelft.nl/newpage/interupt/out-0100.htm
【讨论】: