QNX-9—QNX官网文档翻译—Programming Overview
注:翻译网址
QNX Software Development Platform --> Programming --> Programmer's Guide --> Programming Overview
https://www.qnx.com/developers/docs/7.1/index.html#com.qnx.doc.neutrino.prog/topic/overview.html
一、概述
QNX Neutrino RTOS 架构由微内核和一些协作进程组成。 这些进程通过各种形式的进程间通信机制 (IPC) 相互通信。 消息传递是 QNX Neutrino 中 IPC 的主要形式。
图1. QNX Neutrino 架构充当一种“软件总线”,可让您动态插入/拔出操作系统模块。
1. An application as a set of processes
使用一组协作进程的想法并不限于操作系统“系统进程”。 您的应用程序应该以完全相同的方式编写。 您可能有一些驱动程序进程从某些硬件收集数据,然后需要将该数据传递给其他进程,然后由
其他进程对该数据进行操作。
2. Some definitions
不同的操作系统对于“进程”、“线程”、“任务”、“程序”等术语通常有不同的含义。
3. Priorities and scheduling
尽管系统架构手册中对优先级和调度策略进行了很好的讨论,但在程序员指南的上下文中回顾该主题将有所帮助。
4. Scheduling policies
5. Why threads?
6. Talking to hardware
7. Summary
模块化架构在整个系统中显而易见:QNX Neutrino RTOS 本身由一组协作进程组成,应用程序也是如此。
相关概念
The QNX Neutrino Microkernel (System Architecture)
Processes and Threads (Getting Started with QNX Neutrino)
相关参考
procnto
pthread_setschedparam()
pthread_setschedprio()
sched_setparam()
sched_setscheduler()
sched_yield()
SchedSet(), SchedSet_r()
二、An application as a set of processes
使用一组协作进程的想法并不限于操作系统“系统进程”。 您的应用程序应该以完全相同的方式编写。 您可能有一些驱动程序进程从某些硬件收集数据,然后需要将该数据传递给其他进程,然后由其他进程对该数据进行操作。
我们以监控水库水位的应用程序为例。 如果水位升得太高,那么您需要提醒操作员并打开一些流量控制阀。
在硬件方面,您将在计算机的 I/O 板上连接一些水位传感器。 如果传感器检测到有水,就会导致 I/O 板产生中断。
该软件包含一个与 I/O 板对话的驱动程序进程,并包含一个处理板中断的中断处理程序。 您还将有一个 GUI 进程,当驱动程序告诉您这样做时,该进程将显示一个警报窗口,最后,另一个驱动程序进程将打开/关闭流量控制阀。
为什么要把这个应用程序分成多个进程? 为什么不在一个流程中完成所有事情呢? 有以下几个原因:
1. 每个进程都存在于自己受保护的内存空间中。 如果存在一个错误,即指针具有对进程无效的值,那么当下次使用该指针时,硬件将生成一个错误,由内核处理(内核将在进程上设置 SIGSEGV 信号) )。
这种方法有两个好处。 首先,杂散指针不会导致一个进程覆盖另一个进程的内存。 这意味着一个进程可能会变坏,而其他进程仍在运行。
第二个好处是,错误将恰好在使用指针时发生,而不是在覆盖其他进程的内存时发生。 如果允许指针覆盖另一个进程的内存,那么问题直到稍后才会显现出来,因此调试起来会困难得多。
2. 根据需要在应用程序中添加或删除进程非常容易。 这意味着应用程序可以是可扩展的——添加新功能只是添加进程的问题。
3. 进程可以即时启动和停止,这对于动态升级或只是停止有问题的进程非常有用。
4. 进程可以轻松地分布在网络环境中的多个处理器上。
5. 如果进程的代码集中于完成一项工作,则该进程的代码会简单得多。 例如,充当驱动程序、GUI 前端和数据记录器的单个进程的构建和维护将相当复杂。 这种复杂性会增加出现错误的可能性,并且任何此类错误都可能会影响流程正在执行的所有活动。
6.不同的程序员可以在不同的进程上工作,而不必担心覆盖彼此的工作。
三、Some definitions
不同的操作系统对于“进程”、“线程”、“任务”、“程序”等术语通常有不同的含义。
在 QNX Neutrino RTOS 中,应用程序通常意味着一组进程,尽管有时(尤其是对于 UI 部分)它可能仅意味着一个进程。 程序是编译和链接操作生成的文件; 当您在 QNX 目标上运行程序时,这会创建一个进程; 基本上,进程是正在运行的程序的特定实例。
线程是单个执行或控制流。 在最低级别,这相当于程序计数器或指令指针寄存器前进一些机器指令。 每个线程都有自己的该寄存器的当前值。
进程是由一个或多个线程共享的资源的集合。 这些资源至少包括以下内容:
(1) 进程中的所有代码和数据,包括所有非局部(非堆栈)变量。
(2) 信号处理程序(尽管通常有一个线程处理信号,并且在所有其他线程中阻止它们)
(QNX Neutrino 7.1 或更高版本)内核在进入和离开信号处理程序时保存和恢复 FPU 上下文,因此在其中使用浮点运算是安全的。
(3) 信号忽略状态.
(4) 文件描述符 (fds) 及其相关描述符,例如套接字、mqueue 描述符等。
(5) channels
(4) side-channel 连接
与这些资源的所有权一起进行的还有清理工作。 当进程终止时,所有进程拥有的资源都会被清理,包括终止所有线程、释放所有内存、关闭所有文件描述符等。######## 正常终止(例如,调用 exit() )和异常终止(例如, 由于访问无效内存而死亡)。
线程不共享诸如堆栈、各种寄存器的值、SMP 线程亲和性掩码以及其他一些东西之类的东西。
驻留在两个不同进程中的两个线程不会有太多共享。 它们唯一共享的是 CPU,如果它们在多核处理器上运行,甚至可能不共享 CPU。 不同进程中的线程可以共享内存,但这需要一些设置(请参阅 C 库参考中的 shm_open() 示例)。
当您运行程序(创建进程)时,您会自动运行一个线程。 该线程称为“主”线程,因为在 C 程序中运行的第一个程序员提供的函数是 main()。 如果需要的话,主线程可以创建额外的线程。
主线程有一些特别之处。 一是如果正常返回,它返回的代码会调用 exit()。 调用 exit() 会终止进程,这意味着进程中的所有线程都被终止。 所以当你从主线程正常返回时,进程就终止了。 当进程中的其他线程正常返回时,它们返回的代码将调用 pthread_exit(),该函数仅终止该线程。
关于主线程的另一个特殊之处是,如果它以进程仍然存在的方式终止(例如,它调用 pthread_exit() 并且进程中有其他线程),那么主线程堆栈的内存就不会被清理。 这是因为命令行参数位于该堆栈上,其他线程可能需要它们。 如果任何其他线程终止,则该线程的堆栈将被释放。
四、Priorities and scheduling
尽管系统架构手册中对优先级和调度策略进行了很好的讨论,但在程序员指南的上下文中回顾该主题将有所帮助。
QNX Neutrino RTOS 提供优先级驱动的抢占式架构。 优先级驱动意味着每个线程都可以被赋予一个优先级,并且能够根据该优先级访问 CPU。 如果低优先级线程和高优先级线程都想运行,则高优先级线程将运行。#########
抢占式是指如果当前正在运行一个低优先级线程,然后突然有一个高优先级线程要运行,那么高优先级线程就会接管CPU并运行,从而抢占低优先级线程。
在多核 (SMP) 系统上,QNX Neutrino 仍然在可用内核之一上运行最高优先级的可运行线程。 其他核心运行系统中的其他线程,但不一定是下一个最高优先级的线程。
4.1 Priority range
线程的调度优先级范围为 1 到 255(最高优先级),与调度策略无关。 线程默认继承其父线程的优先级。
4.2 BLOCKED and READY states
要完全理解调度的工作原理,您必须首先理解线程处于 BLOCKED 状态和线程处于 READY 状态时的含义。 您还必须了解内核中称为就绪队列的特定数据结构。
4.3 The ready queue
就绪队列是内核数据结构的简化版本,由每个优先级一个条目的队列组成。 每个条目又包含另一个处于特定优先级的 READY 线程队列。 任何未就绪的线程都不在任何队列中,但当它们变为就绪状态时,它们就会在任何队列中。
4.4 Rescheduling a running thread
每当由于内核调用、异常或硬件中断而进入时,微内核都会做出调度决策。
4.1 Priority range
线程的调度优先级范围为 1 到 255(最高优先级),与调度策略无关。 线程默认继承其父线程的优先级。
非特权线程的优先级范围为 1 到 63(默认情况下);启用了 PROCMGR_AID_PRIORITY 能力的线程(请参阅 procmgr_ability())允许将优先级设置为高于 63。您可以使用 procnto -P 选项更改非特权进程允许的优先级范围。
进程管理器有一组特殊的空闲线程(每个可用 CPU 核心一个),它们的优先级为 0,并且始终准备运行。当空闲线程被安排在该核心上运行时,CPU 核心被视为空闲。 在任何时刻,核心要么空闲,要么忙碌; 只有通过一段时间内的平均,才能说 CPU 处于一定百分比的繁忙状态(例如,75% 的 CPU 使用率)。
线程同时具有实际优先级(real priority)和有效优先级(effective priority),并根据其有效优先级进行调度。 线程本身可以同时改变其真实优先级和有效优先级,但有效优先级可能会因为优先级继承或调度策略而改变。 通常情况下,有效优先级与实际优先级相同。
中断处理程序的优先级高于任何线程,但它们的调度方式与线程不同。 如果发生中断,则:
(1) 无论运行什么线程,都会失去处理中断的核心上的 CPU。 在多核机器上,由于硬件配置和 BSP 设置的不同,这可能会有所不同。
(2) 内核运行 BSP 提供的一些特定于硬件的代码来识别中断。
(3) 内核调用适当的中断处理程序或处理程序。
(4) 内核运行 BSP 提供的一些特定于硬件的代码来执行中断结束处理。
图1. 线程优先级范围从 0(最低)到 255(最高)。
尽管中断处理程序的调度方式与线程不同,但它们被认为具有更高的优先级,因为中断处理程序将抢占任何正在运行的线程。
超出范围的优先级请求(QNX Neutrino 6.6 或更高版本)
作为 POSIX 的扩展,当您设置优先级时,可以将其包装在以下宏中以指定如何处理超出范围的优先级请求:
SCHED_PRIO_LIMIT_ERROR(priority) — 指示错误
SCHED_PRIO_LIMIT_SATURATE(priority) — 使用允许的最大优先级(达到“最大饱和点”)
如果您希望默认情况下超出范围的优先级请求在最大允许值处“饱和”而不是导致错误,您可以将 s 或 S 附加到 procnto 的 -P 选项。
4.2 BLOCKED and READY states
要完全理解调度的工作原理,您必须首先理解线程处于 BLOCKED 状态和线程处于 READY 状态时的含义。 您还必须了解内核中称为就绪队列的特定数据结构。
如果线程不需要 CPU,则该线程将被阻塞,发生这种情况的原因有多种,例如:
(1) 线程正在休眠。
(2) 该线程正在等待来自另一个线程的消息。
(3) 该线程正在等待其他线程拥有的互斥锁。
在设计应用程序时,您总是尝试对其进行安排,以便如果任何线程正在等待某些内容,请确保它不会 spinning 在循环中而耗尽 CPU。 一般来说,尽量避免轮询。 如果您确实必须进行轮询,那么您应该尝试在轮询之间休眠一段时间,从而在低优先级线程需要时为它们提供 CPU。
对于每种类型的阻塞都有一个阻塞状态。######### 我们将在这些状态出现时对其进行简要讨论。 一些阻塞状态的示例是 REPLY 阻塞、RECEIVE 阻塞、MUTEX 阻塞、INTERRUPT 阻塞和 NANOSLEEP 阻塞。 当线程被 SIGSTOP 信号挂起并等待 SIGCONT 时,还有一个 STOPPED 状态。
如果一个线程想要一个 CPU,但当前有其他东西拥有它,则该线程处于就绪状态。 如果线程当前拥有 CPU,则它处于 RUNNING 状态。 简而言之,READY 或 RUNNING 的线程不会被阻塞。
4.3 The ready queue
就绪队列是内核数据结构的简化版本,由每个优先级一个条目的队列组成。 每个条目又包含另一个处于优先级的 READY 线程队列。 任何未就绪的线程都不在任何队列中,但当它们变为就绪状态时,它们就会在任何队列中。
我们首先考虑单核系统上的就绪队列。
图1. 单核系统上五个线程的就绪队列。
在上图中,线程 B–F 已就绪。 线程A当前正在运行。 所有其他线程 (G–Z) 均被阻塞。 线程 A、B 和 C 具有最高优先级,因此它们将根据正在运行的线程的调度策略共享处理器。
活动线程是处于 RUNNING 状态的线程。
每个线程都被分配一个优先级。 调度程序通过查看分配给处于 READY 状态(即能够使用 CPU)的每个线程的优先级来选择下一个要运行的线程。 选择运行位于其优先级队列头部的具有最高优先级的线程。 在上图中,线程 A 以前位于优先级 10 队列的头部,因此线程 A 被移至 RUNNING 状态。
在多核系统上,这变得更加复杂,存在诸如核心亲和性优化和各种线程的 CPU 掩码等问题,######### 使得调度决策更加复杂。 但就绪队列概念也成为多核系统调度决策的主要驱动力。
4.4 Rescheduling a running thread
每当由于内核调用、异常或硬件中断而进入时,微内核都会做出调度决策。
每当任何线程的执行状态发生变化时,就会做出调度决策——线程可能驻留在哪个进程中并不重要。 线程在所有进程中进行全局调度。############
通常,正在运行的线程会继续运行,但只要正在运行的线程出现以下情况,调度程序就会执行从一个线程到另一个线程的上下文切换:
(1) 被阻塞
当正在运行的线程必须等待某些事件发生(响应 IPC 请求、等待互斥锁等)时,它会被阻塞。 被阻塞的线程从正在运行的数组中删除,并选择另一个线程来运行:
a. 在单核(简单)情况下,位于其优先级队列头部的最高优先级就绪线程被选择并允许运行。
b. 在多核系统中,调度程序在决定如何准确调度其他线程方面具有一定的灵活性,着眼于优化缓存使用并最大限度地减少线程迁移。 这可能意味着某些处理器将运行较低优先级的线程,而较高优先级的线程正在等待在其上次运行的处理器上运行。 当下一次运行较低优先级线程的处理器做出调度决策时,它将选择较高优先级的线程。
无论如何,单处理器系统上的实时调度规则保证在多核系统上得到维护。
当被阻塞的线程随后被解除阻塞时,它通常被放置在其优先级的就绪队列的末尾。
(2) 被抢占
当较高优先级的线程被放入就绪队列时(由于其阻塞条件得到解决,该线程变为就绪状态),正在运行的线程将被抢占。 被抢占的线程将移动到该优先级的就绪队列的开头,并且更高优先级的线程将运行。 当处于该优先级的线程再次运行时,该线程将恢复执行 - 被抢占的线程不会失去其在优先级队列中的位置。########
(3) Yields
正在运行的线程自愿让出处理器(例如,通过 sched_yield)并被放置在该优先级的就绪队列的末尾。 然后,最高优先级的线程将运行(它可能仍然是刚刚让出的线程)。
五、Scheduling policies
为了满足各种应用的需求,QNX Neutrino 提供了以下调度策略:
FIFO scheduling — SCHED_FIFO Round-robin scheduling — SCHED_RR Sporadic scheduling — SCHED_SPORADIC
另一种调度策略(称为“other”—SCHED_OTHER)的行为方式与 round-robin 相同。 我们不建议使用“other”调度策略,因为它的行为将来可能会发生变化。
系统中的每个线程可以使用任何调度策略方法运行。 调度方法在每个线程的基础上有效,而不是在节点上的所有线程和进程的全局基础上有效。
请记住,这些调度策略仅在共享相同优先级的两个或多个线程就绪时适用(即,线程直接相互竞争)。 如果较高优先级线程变为就绪状态,它会立即抢占所有较低优先级线程。
在下图中,三个具有相同优先级的线程处于就绪状态。 如果线程 A 阻塞,线程 B 将运行。
线程可以调用 pthread_attr_setschedparam() 或 pthread_attr_setschedpolicy() 来设置用于它创建的任何线程的调度参数和策略。
尽管线程从其父线程获取其初始优先级和调度策略(通常通过继承),但线程可以调用 pthread_setschedparam() 来请求更改内核应用的算法和优先级,或调用 pthread_setschedprio() 来仅更改优先级。 线程可以通过调用 pthread_getschedparam() 来获取有关其当前算法和策略的信息。 这两个函数都将线程 ID 作为第一个参数; 您可以调用 pthread_self() 来获取调用线程的 ID。 例如:
struct sched_param param; int policy, retcode; /* Get the scheduling parameters. */ retcode = pthread_getschedparam(pthread_self(), &policy, ¶m); if (retcode != EOK) { printf ("pthread_getschedparam: %s.\n", strerror (retcode)); return EXIT_FAILURE; } printf ("The assigned priority is %d, and the current priority is %d.\n", param.sched_priority, param.sched_curpriority); /* Increase the priority. */ param.sched_priority++; /* Set the scheduling algorithm to FIFO */ policy = SCHED_FIFO; retcode = pthread_setschedparam(pthread_self(), policy, ¶m); if (retcode != EOK) { printf ("pthread_setschedparam: %s.\n", strerror (retcode)); return EXIT_FAILURE; }
当您获取调度参数时,sched_param 结构的 sched_priority 成员将设置为分配的优先级,sched_curpriority 成员将设置为线程当前运行的优先级(由于优先级继承,该优先级可能会有所不同)。
我们的库提供了多种获取和设置调度参数的方法:
pthread_getschedparam()、pthread_setschedparam()、pthread_setschedprio()
这些是您便携性的最佳选择。
SchedGet(), SchedSet()
您可以使用它们来获取和设置调度优先级和策略,但它们不可移植,因为它们是内核调用。
sched_getparam()、sched_setparam()、sched_getscheduler() 和 sched_setscheduler()
这些函数旨在用于单线程进程。
我们对这些函数的实现并不完全符合 POSIX。在多线程应用程序中,它们获取或设置进程 pid 中线程 1 的参数,或者如果 pid 为 0,则获取或设置调用线程的参数。如果您依赖此行为,您的代码将无法移植。 POSIX 1003.1 规定这些函数应在多线程应用程序中返回 -1 并将 errno 设置为 EPERM。
getpriority(), setpriority()
已弃用; 不要使用这些函数。
5.1 FIFO scheduling
5.2 Round-robin scheduling
5.3 Sporadic scheduling
偶发(SCHED_SPORADIC)调度策略通常用于为给定时间段内线程的执行时间提供上限,即限制最大运行时间。
5.1 FIFO scheduling
在 FIFO (SCHED_FIFO) 调度中,选择运行的线程会继续执行,直到:
(1) 自愿放弃控制(例如,它阻止)
(2) 被更高优先级的线程抢占
5.2 Round-robin scheduling
在循环 (SCHED_RR) 调度中,选择运行的线程将继续执行,直到:
(1) 自愿放弃控制权
(2) 被更高优先级的线程抢占
(3) 消耗尽其时间片
图1. 循环调度。 线程 A 一直运行直到耗尽其时间片; 下一个 READY 线程(线程 B)现在运行。
时间片是分配给每个进程的时间单位。(进程还是线程?) 一旦它消耗了它的时间片,线程就会被放置在就绪队列中的队列末尾,并且相同优先级的下一个就绪线程将获得控制权。
时间片计算:4 × ticksize
如果您的处理器速度大于 40 MHz,则ticksize 大小默认为 1 毫秒;######### 否则,默认为 10 毫秒。 因此,默认时间片为 4 毫秒(大多数 CPU 的默认值)或 40 毫秒(较慢硬件的默认值)。
除了时间分片之外,循环调度方法与 FIFO 调度相同。
5.3 Sporadic scheduling
偶发或零星(SCHED_SPORADIC)调度策略通常用于为给定时间段内线程的执行时间提供上限。
当在服务周期性和非周期性事件的系统上执行速率单调分析 (RMA) 时,此行为至关重要。 本质上,该算法允许线程为非周期性事件提供服务,而不会危及系统中其他线程或进程的硬期限。
在零星调度下,线程的优先级可以在前台或正常优先级与后台或低优先级之间动态振荡。 有关详细信息,请参阅系统架构指南的 QNX Neutrino 微内核一章中的“Sporadic scheduling”。
六、Why threads?
现在我们对优先级有了更多的了解,我们可以讨论为什么您可能想要使用线程。 我们看到了将事物分解为单独进程的许多充分理由,但是多线程进程的目的是什么?
拥有多个线程的基本目的是让您可以在完成旧的事情(即已经在处理的事情)之前开始新的事情。
我们以驱动为例。 驱动程序通常有两项义务:一是与硬件对话,二是与其他进程对话。 一般来说,与硬件对话比与其他进程对话对时间要求更高。 当来自硬件的中断进入时,需要在相对较小的时间窗口内得到服务——驱动程序此时不应该忙于与另一个进程通信。
解决此问题的一种方法是选择一种与其他进程对话的方式,这种情况根本不会出现(例如,不要将消息发送到另一个进程,这样您就必须等待确认,不要在任何时候都这样做) -代表其他进程消耗处理等)。
另一种方法是使用两个线程:一个处理硬件的较高优先级线程和一个与其他进程通信的较低优先级线程。 较低优先级的线程可以与其他进程交谈,而完全不会影响时间关键的作业,因为当中断发生时,较高优先级的线程将抢占较低优先级的线程,然后处理中断。
尽管这种方法确实增加了控制对两个线程之间的任何公共数据结构的访问的复杂性,但 QNX Neutrino 提供了同步工具,例如互斥锁(互斥锁),可以确保对线程之间共享的任何数据的独占访问。
七、Talking to hardware
正如本章前面提到的,编写设备驱动程序就像编写任何其他程序一样。 只有核心操作系统服务驻留在内核地址空间中; 其他所有内容(包括设备驱动程序)都驻留在进程或用户地址空间中。 这意味着设备驱动程序具有常规应用程序可用的所有服务。
QNX Neutrino 下的驱动程序开发人员可以使用许多模型。 通常,您正在编写的驱动程序类型将决定您将遵循的驱动程序模型。 例如,图形驱动程序遵循一种特定模型,这允许它们插入屏幕图形子系统,网络驱动程序遵循不同的模型,等等。
另一方面,根据您的目标设备类型,遵循任何现有的驱动程序模型可能根本没有意义。
本节概述了访问和控制设备级硬件的一般情况。
某些硬件操作需要 I/O 权限; 否则你会得到一个保护错误。 要获取线程的 I/O 权限,请确保您的进程启用了 PROCMGR_AID_IO 功能(请参阅 procmgr_ability()),然后调用 ThreadCtl():
ret = ThreadCtl(_NTO_TCTL_IO, 0); if (ret == -1) { // An error occurred. }
7.1 探测硬件
如果您的目标是具有一组固定硬件的封闭式嵌入式系统,则您的驱动程序可能会假设它将要控制的硬件存在于系统中并以某种方式进行配置。 但如果您的目标是更通用的系统,您需要首先确定该设备是否存在。 然后,您需要弄清楚设备是如何配置的(例如,哪些内存范围和中断级别属于设备)。
对于某些设备,有一个用于确定配置的标准机制。 与 PCI 总线接口的设备具有这样的机制; 每个 PCI 设备都有一个分配给它的唯一供应商和设备 ID。 有关详细信息,请参阅 PCI 服务器用户指南。
不同的总线具有不同的机制来确定哪些资源已分配给设备。 在某些总线上,例如 ISA 总线,没有这样的机制。 如何确定系统中是否存在 ISA 设备以及它是如何配置的? 答案取决于卡(PnP ISA 设备除外)。
7.2 访问硬件
一旦确定了为设备分配了哪些资源,您就可以开始与硬件进行通信了。 如何执行此操作取决于资源。
(1) I/O resources
为了执行端口 I/O,您需要使用 mmap_device_io() 将 I/O 基地址映射到进程的地址空间。 例如:
uintptr_t iobase;
iobase = mmap_device_io(base_address_size, cpu_base_address);
要进行端口 I/O,请使用 in8()、in32()、out8() 等函数,将寄存器索引添加到 iobase 以寻址特定寄存器:
out32(iobase + SHUTDOWN_REGISTER, 0xdeadbeef);
请注意,在 x86 系统上不需要调用 mmap_device_io(),但为了可移植性,包含它仍然是一个好主意。 对于某些旧版 x86 硬件,调用它可能没有意义; 例如,VGA 兼容设备在众所周知的固定位置(例如 0x3c0、0x3d4、0x3d5)具有 I/O 端口,而没有 I/O 基础的概念。 例如,您可以访问 VGA 控制器,如下所示:
out8(0x3d4, 0x11); out8(0x3d5, in8(0x3d5) & ~0x80);
(2) Memory-mapped resources
对于某些设备,寄存器是通过常规内存操作访问的。 要访问设备的寄存器,您需要通过调用 mmap() 将它们映射到驱动程序虚拟地址空间中的指针。 例如:
volatile uint32_t *regbase; /* device has 32-bit registers */ regbase = mmap( NULL, base_address_size, PROT_READ | PROT_WRITE | PROT_NOCACHE, MAP_PHYS | MAP_SHARED, NOFD, cpu_base_address);
请注意以下事项:
a. 我们使用 volatile 关键字声明 regbase,以防止编译器优化对设备寄存器的访问。
b. 我们指定 PROT_NOCACHE 标志以确保 CPU 不会推迟或省略对设备寄存器的读/写周期。
在 32 位和 64 位 ARM 目标上,PROT_NOCACHE 导致 RAM 被映射为普通非缓存,但非 RAM 被映射为强有序设备内存。 要获得更精细的控制,请参见 shm_ctl_special()。
现在您可以使用 regbase 指针访问设备的内存。 例如:
regbase[SHUTDOWN_REGISTER] = 0xdeadbeef;
(3) IRQs
您可以通过调用 InterruptAttach() 或 InterruptAttachEvent() 将中断处理程序附加到设备。 例如:
InterruptAttach(_NTO_INTR_CLASS_EXTERNAL | irq, handler, NULL, 0, _NTO_INTR_FLAGS_END);
驱动程序必须成功调用 ThreadCtl(_NTO_TCTL_IO, 0) 才能附加中断。
InterruptAttach() 和 InterruptAttachEvent() 之间的本质区别在于通知驱动程序设备已触发中断的方式:
a. 使用 InterruptAttach(),驱动程序的处理函数由内核直接调用。 由于它在内核空间中运行,因此处理程序的功能受到严格限制:
(a1) 在此处理程序中,调用大多数 C 库函数是不安全的。
(a2) 如果您在处理程序中花费太多时间,则较低或相同优先级的其他进程和中断处理程序将无法运行。
(a3) 在中断处理程序中执行过多的工作会对系统的实时响应能力产生负面影响。
我们建议您在处理程序中执行最少的操作(例如,在硬件级别确认中断),然后将事件传递给驱动程序。 然后,驱动程序按照驱动程序的正常优先级完成进程级别的其余工作。
b. 使用 InterruptAttachEvent(),您可以指定当设备触发中断时要传递给驱动程序的事件。 然后驱动程序在进程级别处理中断。
有关详细信息,请参阅本指南中的“Writing an Interrupt Handler”一章。
八、Summary
模块化架构在整个系统中显而易见:QNX Neutrino RTOS 本身由一组协作进程组成,应用程序也是如此。
每个单独的进程可以包含多个协作线程。 “将一切保持在一起”的是 QNX Neutrino 中基于优先级的抢占式调度,它确保时间关键的任务在正确的时间由正确的线程或进程处理。
posted on 2023-06-23 18:58 Hello-World3 阅读(342) 评论(0) 编辑 收藏 举报