阅读前言
本文以QNX系统官方的文档英文原版资料为参考,翻译和逐句校对后,对QNX操作系统的相关概念进行了深度整理,旨在帮助想要了解QNX的读者及开发者可以快速阅读,而不必查看晦涩难懂的英文原文,这些文章将会作为一个或多个系列进行发布,从遵从原文的翻译,到针对某些重要概念的穿插引入,以及再到各个重要专题的梳理,大致分为这三个层次部分,分不同的文章进行发布,依据这样的原则进行组织,读者可以更好的查找和理解。
1. Character I/O(字符I/O)
对于任何实时操作系统来说,都有一个关键要求,那就是高性能的字符I/O。
与面向块的设备(例如,磁盘驱动器)相反,字符设备可以被描述为,由串行传输字节序列组成其I/O的设备。
与传统的POSIX和UNIX一样,这些字符设备位于/dev目录下的操作系统路径名空间中。例如,可以连接调制解调器或终端的串口设备可能在系统中显示为:/dev/ser1
。
PC硬件上的典型字符设备包括:
- 串口(serial ports)
- 并口(parallel ports)
- 文本模式控制台(text-mode consoles)
- 伪终端(pseudo terminals ,ptys)
程序可以使用标准的open(),close(),read()以及write()等API函数访问字符设备。额外的函数可用于操作字符设备的其他方面,如波特率,奇偶校验,流量控制等。
由于运行多字符设备很常见,因此它们被设计为一系列的驱动程序以及一个被称为io-char
的库,从而最大限度地提高代码重用能力。
io-char库由多个驱动程序使用
io-char
模块包含了设备上支持POSIX语义的所有代码。它还包含大量代码来实现POSIX之外的字符I/O特性(虽然这些特性是POSIX之外的,但是在实时系统中是必需的)。由于这些代码位于公共库中,因此所有的驱动程序都可以继承这些能力。
驱动程序是调用io-char
库的执行进程。在运行过程中,驱动程序首先启动并调用io-char
库。与其他 QNX Neutrino 进程一样,驱动程序可以根据被控制硬件的性质以及客户端请求的服务以不同的优先级进行运行。
一旦单个字符设备开始运行,则添加更多设备的内存成本就会降到最低,因为只有实现新驱动程序结构的代码才是新的。
1.1. 驱动程序或io-char
的通信
io-char库程序会管理应用程序和设备驱动程序之间的数据流。数据会通过一组与每个字符设备有关联的内存队列在库程序io-char及驱动程序之间进行流动。
每个设备使用三个队列。每个队列都使用先进先出(FIFO)机制进行实现。
QNX Neutrino RTOS中的设备I/O
接收到的数据由驱动程序将其放置到原始输入队列中,只有在应用程序要处理请求数据时才由io-char进行使用。(有关原始输入与编辑输入或规范输入的详细信息,请参阅本章后面的"输入模式"一节。)
驱动程序中的中断处理程序,通常会调用io-char中的可信库例程,来将数据添加到该队列中,这确保了一致的输入规则,并最大限度地减少了驱动程序的责任(并且减少了创建新驱动程序所需的工作量)。
io-char
模块会将输出数据放入输出队列中,以便在字符要物理传输到设备时由驱动程序使用。每次添加新数据时,模块都会在驱动程序中调用一个受信任的例程,以便它可以"踢一下"驱动程序使其进入操作状态(在它空闲的情况下)。由于使用了输出队列,io-char
实现了所有字符设备的后写。只有当输出缓冲区已满时,io-char
才会导致用户进程在写入时进行阻塞。
规范化队列(canon)完全由io-char管理,在以编辑模式处理输入数据时使用。该队列缓冲区的大小决定了特定设备可以处理的编辑输入行的最大大小。
这些队列的大小可以使用命令行选项进行配置。默认值通常足以处理大多数硬件配置,但是你可以"调优"这些值以减少总体系统内存需求,以及用于适应不寻常的硬件情况或处理独特的协议需求。
设备驱动程序只需将接收到的数据添加到原始输入队列中,或者从输出队列中消费和传输数据。io-char模块决定何时(以及是否)暂停输出传输,如何(以及如果)回显接收到的数据,等等。
1.2. 设备控制
低级别设备控制是使用devctl()调用实现的。
POSIX终端控制函数在devctl()
之上的分层如下:
获取终端属性。
设置终端属性。
获取终端的进程组领导者ID。
@>> 阅读扩展:
tcgetpgrp()
函数用于获取与特定终端相关联的进程组领导者的ID。在Unix和Linux系统中,终端(或更一般地说,任何打开的文件描述符,如串行端口)可以与一个或多个进程组相关联。进程组是一个或多个进程的集合,它们共享相同的终端或其他资源,并且可以一起接收信号。当你打开一个终端或串行端口时,它通常与调用进程的进程组相关联。然而,你可以使用
tcsetpgrp()
函数更改与终端相关联的进程组。
tcgetpgrp()
函数允许你查询当前与终端相关联的进程组领导者的PID(进程ID)。进程组领导者是一个进程组中的"代表"进程,通常用于接收与该终端相关的信号,如中断(INT)或退出(QUIT)信号。这个函数通常用于实现终端控制功能,如作业控制,它允许用户在前台和后台之间移动进程,并管理对终端的访问。
简而言之,
tcgetpgrp()
函数用于获取与特定终端相关联的进程组领导者的ID,这对于实现终端控制和作业管理功能至关重要。
设置终端进程组领导者ID。
发送一个中断条件。
暂停或重新启动数据传输/接收。
QNX Neutrino 扩展
QNX Neutrino 对终端控制API的扩展如下:
发起断开连接(Initiate a disconnect)。对于串行设备来说,这将会触发DTR线的脉冲信号;
@>> 阅读扩展:
tcdropline()
函数是用于初始化一个断开连接的操作。在串行通信的上下文中,这个函数会触发 DTR(Data Terminal Ready)线的脉冲信号。要理解这一点,我们需要知道 DTR 是串行通信中的一个控制信号,它由数据终端设备(DTE,如计算机)发送给数据通信设备(DCE,如调制解调器或串行端口)。当 DTE 准备好进行通信时,它会将 DTR 信号设置为高电平(逻辑 1)。
tcdropline()
函数通过脉冲 DTR 线来模拟一个断开连接的事件。这通常意味着将 DTR 信号从高电平降低到低电平,然后再返回到高电平,或者根据具体的硬件和驱动程序实现,可能只是简单地切换 DTR 信号的状态。在串行通信中,脉冲 DTR 线可以用来通知 DCE 设备断开连接,或者在某些情况下,作为重置或重新初始化通信链路的一种方式。例如,在某些调制解调器或串行设备中,降低 DTR 信号可能会触发设备挂断当前连接或重置其通信状态。
总的来说,
tcdropline()
函数通过操作 DTR 控制信号来模拟或触发一个断开连接的事件,这在串行通信编程和测试中可能是有用的。
将字符注入规范缓冲区(canonical buffer)。
@>> 阅读扩展:
在Unix和Linux系统中,终端输入和输出处理涉及几个概念,其中之一就是"canonical buffer"。当我们谈论
tcinject()
函数时,它是在讨论如何将字符注入到这个canonical buffer中。要理解这个概念,首先需要了解终端的输入模式。终端的输入可以配置为两种模式之一:canonical(规范)模式或非canonical(非规范)模式。
1. Canonical(规范)模式:【edited mode 或 cooked mode】
- 在这种模式下,输入被处理为一行。终端驱动程序会收集字符,直到收到换行符(通常是回车或换行符),然后将整行作为一个单位传递给应用程序。
- 这意味着,如果你在终端中输入文本,终端驱动程序会等待你按下回车键,然后将整行文本作为输入传递给程序。
- Canonical buffer 就是存储这些字符直到收到换行符的地方。
2. Non-canonical(非规范)模式:【raw mode】
- 在这种模式下,输入不需要等待换行符。字符可以立即传递给应用程序,或者可以配置终端驱动程序以特定的方式处理输入(例如,等待特定的字符数或超时)。
tcinject()
函数的作用是在 canonical buffer 中插入字符,就好像这些字符是从终端接收的一样。这对于模拟输入或测试依赖于终端输入的程序特别有用。简而言之,canonical buffer是终端驱动程序中用于存储输入字符直到收到换行符的缓冲区。
tcinject()
函数允许你向这个缓冲区插入字符,模拟用户输入。
1.3. 输入模式
每个设备都可以处于raw
输入模式或edited
输入模式。
1.3.1. raw
输入模式("原始"输入模式)
在原始模式(或者"生"模式)下,io-char
不会对接收到的字符进行编辑。这将对每个字符的处理减少到最低限度,并为读取数据提供最高性能的接口。
例如,全屏程序和串行通信程序就是在原始模式下使用字符设备的。
在原始模式下,中断处理程序将每个字符接收到原始输入缓冲区中。当应用程序从设备请求数据时,它可以指定在什么条件下满足输入请求。在满足条件之前,中断处理程序不会向驱动程序发出运行信号,并且驱动程序不会向应用程序返回任何数据。通常情况下,应用程序的简单读取会阻塞,直到至少有一个字符可用。
下图显示了全部可用条件:
满足输入请求的条件
在指定多个条件的情况下,当满足其中任何一个条件时,读取将会被满足。
- MIN
当应用程序知道它期望接收的字符数时,限定符MIN很有用。
任何知道数据帧字符数的协议都可以使用MIN来等待整个帧的到达。这大大减少了IPC和进程调度。MIN通常与TIME或TIMEOUT一起使用。MIN是POSIX标准的一部分。
- TIME
当应用程序正在接收流数据并希望在数据停止或暂停时得到通知时,限定符TIME非常有用。暂停时间的间隔大小以十分之一秒进行指定。TIME是POSIX标准的一部分。
- TIMEOUT
当应用程序知道在超时之前应该等待多长时间的数据时,限定符TIMEOUT很有用。超时时间指定为十分之一秒。
任何知道它所期望接收的数据帧的字符数的协议,都可以使用TIMEOUT。这可以与波特率相结合,在数据可用时做出合理的猜测。它可以作为一个木桩定时器(deadman timer)来检测"掉落"的字符。它也可以用于具有用户输入的交互式程序中,在给定时间内没有响应时,使读取超时。
TIMEOUT 是一个 QNX Neutrino 扩展,不是 POSIX 标准的一部分。
- FORWARD
当协议被特殊的帧字符分隔时,FORWARD 限定符很有用。例如,用于串行链路上的TCP/IP的PPP协议以帧字符开始和结束其数据包。当与 TIMEOUT 一起使用时,FORWARD 字符可以极大地提高协议实现的效率。协议进程将接收完整的帧,而不是逐个字符。在丢失帧字符的情况下,可以使用TIMEOUT或TIME来快速恢复。
这极大地减少了操作系统的 IPC 工作量,(并且对于给定的 TCP/IP 数据速率,大量的 IPC 会导致处理器利用率大大降低),而这可以极大的改善这种情况。有趣的是,在PPP协议中并不包含对其帧数据数量的计数值。如果没有数据 FORWARD 字符功能(data-forwarding character),对于PPP协议的实现,可能会被迫需要逐字符读取数据。
FORWARD是一个 QNX Neutrino 扩展,不是 POSIX 标准的一部分。
对应用程序通知的处理,"推送"到操作系统服务所提供组件中进行,这种能力或做法,降低了必须发生用户级处理的频率。最大限度地减少了系统中的 IPC 工作,并为应用程序处理释放了CPU周期。此外,如果实现协议的应用程序在与通信端口不同的网络节点上执行,则网络事务的数量也会最小化。
对于智能多端口串行卡,数据转发字符识别也可以在智能串行卡本身内进行实现,从而大大减少了智能串行卡必须中断主机处理器以进行中断服务的次数。
1.3.2. edited
输入模式("编辑"输入模式)
在编辑模式(edited mode)下,io-char对每个接收到的字符执行"line editing operations"(行编辑操作)。只有在"完整输入"一行时(通常是在接收回车[CR]时),应用程序进程才能使用这行数据。这种操作模式,通常会被称为"规范"模式(canonical mode),有时也称为"熟"模式("cooked" mode)。
大多数非全屏应用程序以编辑模式运行,因为这允许应用程序一次处理一行数据,而不必检查接收到的每个字符以及扫描行尾字符。
在编辑模式下,中断处理程序会将每个字符接收到原始输入缓冲区中。与原始模式不同,在原始模式中,驱动程序只在满足某些输入条件时才会调度运行,在编辑模式下,中断处理程序将会根据每个接收到的字符调度驱动程序。
这有两个原因。首先,编辑输入模式很少用于高性能通信协议。其次,编辑工作很重要,不适合在中断处理程序中完成。
当驱动程序运行时,io-char中的代码将会检查每次接收到的字符,并将该字符应用于的规范缓冲区,在规范缓冲区中构建行数据。当一行数据完成并且应用程序请求输入时,该行数据将会从规范缓冲区传输到应用程序(直接从规范缓冲区传输到应用程序缓冲区,不需要任何中间副本)。
用于编辑功能的代码可以正确地处理规范缓冲区中多个挂起的输入行,并允许应用程序读取部分的行。例如,如果应用程序只请求读取1个字符,而一个有10个字符的行可用时,就可能发生这种情况。在这种情况下,下一次读取将在上次读取结束的地方继续。
io-char
模块提供了一组丰富的编辑功能,包括完全支持使用光标键在输入行上进行移动,以及更改、插入或删除字符。下面是一些比较常见的功能:
- LEFT
将光标向左移动一个字符。
- RIGHT
将光标向右移动一个字符。
- HOME
将光标移动到行首。
- END
将光标移动到行尾。
- ERASE
擦除光标左侧的字符。
- DEL
擦除当前光标位置的字符。
- KILL
擦除整个输入行。
- UP
删除当前行并召回前一行。
- DOWN
擦除当前行并召回下一行。
- INS
在插入模式(insert)和改写模式(typeover)之间切换。(每个新行都是以插入模式开始)
行编辑字符因终端不同而有差异。控制台开始运行时总是会定义一套完整的编辑键。
如果终端通过串行通道连接,则需要定义适用于该特定终端的编辑字符。为此,你可以使用stty实用程序。例如,如果你有一个连接到串行端口的ANSI终端(比如称为/dev/ser1
),你可以使用以下命令从terminfo
数据库中提取适当的编辑键,并将其应用于/dev/ser1
节点:
bash
stty term=ansi </dev/ser1
1.4. 设备子系统性能
当设备处于原始模式时,设备子系统内的事件流被设计为最小化开销和最大化吞吐量。要做到这一点,需要使用以下设计规则:
- 中断处理程序需要将接收到的数据直接放入内存队列中。只有当读操作处于挂起状态,并且该读操作可以被满足时,中断处理程序才会安排驱动程序运行。在所有其他情况下,中断只会简单返回。此外,如果
io-char
已经在运行,则不会进行重复调度,因为在没有进一步通知的情况下就可以注意到新到达数据的可用性。 - 当一个读操作得到满足时,驱动程序直接从原始输入缓冲区向应用程序的接收缓冲区回复应用程序进程。最终结果是数据只会被复制一次。
这些规则(加上操作系统中固有的极小的中断和调度延迟)将会产生了一个非常精简的输入模型,该模型提供了POSIX标准一致性,以及提供了适合协议实现上实时需求的扩展性。
1.5. 控制台设备
系统控制台,(在文本模式下具有VGA兼容的图形芯片的系统控制台),由devc-con
或devc-con-hid
驱动程序进行管理。视频显卡/屏幕以及系统键盘统称为物理控制台(physical console)。
驱动程序devc-con or devc-con-hid允许通过虚拟控制台(virtual consoles),在物理控制台上并发地运行多个会话。devc-con
控制台驱动程序进程通常管理多组io-char
的 I/O 队列,这些队列将会作为一组字符设备提供给用户进程,其名称如/dev/con1
、/dev/con2
等。从应用程序的角度来看,系统"确实"有多个控制台可供使用。
当然,这里只有一个物理控制台(屏幕和键盘),所以每次只能显示其中一个虚拟控制台。键盘设备将会"连接"到当前可见的虚拟控制台中。
终端仿真
控制台驱动程序模拟ANSI终端。
1.6. 串行设备
串行通信通道由名为devc-ser*
的节点族的驱动程序进程进行管理。这些驱动程序可以管理多个物理通道,并提供名称为/dev/ser1
、/dev/ser2
等的字符设备。
当启动devc-ser*时,命令行参数可以指定安装哪些串行端口以及安装多少个串行端口。在pc兼容系统上,这通常是两个标准串行端口,通常称为com1
和com2
。dev -ser*
驱动程序可以直接支持大多数非智能多端口串行卡。
QNX Neutrino 包括各种串行驱动程序(例如,devc-ser8250
)。具体请参见《实用程序参考》中的"devc-ser*"项的内容。
dev -ser*
驱动程序支持硬件流控制(除了在编辑模式下),前提是硬件支持。(由POSIX定义,)调制解调器上的载波损耗信号(Loss of carrier)可以被编程为向应用程序进程提供 SIGHUP 信号。
1.7. 伪终端(ptys)
伪终端由dev -pty
驱动程序进行管理。
devc-pty的命令行参数可以指定要创建的伪终端的数量。
伪终端(pty)是一对字符设备:一个主设备和一个工作设备。工作设备提供了一个接口(与 POSIX 所定义的tty
设备所提供的接口相同)。然而,与其他tty
设备代表的是硬件设备不同,该工作设备反而由另一个进程通过伪终端的主设备来操作它。也就是说,在主设备上写入的任何内容都会作为输入被提供给工作设备;在工作设备上写入的任何内容都会作为输入呈现给主设备。因此,伪tty(pseudo-ttys)可用于连接那些需要与字符设备通信的进程。
Pseudo-ttys
pty
通常用于为telnet等程序创建伪终端接口,而telnet
使用 TCP/IP 向远程系统提供终端会话。