QNX 进程管理器

7 篇文章 9 订阅
7 篇文章 0 订阅

QNX 进程管理器

  进程管理器能够创建多个POSIX进程(每个进程可以包含多个POSIX线程)。在QNX中微子RTOS中,微核与进程管理器(procnto)是成对的,所有运行时系统都需要此模块。其主要职责包括:

  1. 进程管理——管理进程的创建、销毁和进程属性,如用户ID (uid)和组ID (gid)。
  2. 内存管理——管理一系列内存保护功能、共享库和进程间POSIX共享内存原语。
  3. 路径名管理——管理资源管理器可能附加到的路径名空间。

  用户进程可以向procnto发送消息,通过内核调用和进程管理器函数直接访问微内核函数。注意,用户进程通过调用MsgSend*()内核调用来发送消息。
  需要注意的是,在procnto中的线程调用微内核的方式与其他进程中的线程完全相同。进程管理器代码和微内核共享相同的进程地址空间这一事实表明不存在“特殊”或“私有”接口。系统中的所有线程共享相同的一致内核接口,并在调用微内核时执行特权切换。

进程管理

  procnto的第一个职责是动态创建新进程。这些进程将依赖于procnto的内存管理和路径名管理的其他职责。进程管理包括进程创建和销毁,以及进程属性(如进程id、进程组、用户id等)的管理。

  • Process primitives 进程原语
    posix_spawn() POSIX
    spawn() QNX
    fork() POSIX
    vfork() UNIX BSD extension
    exec*() POSIX
  1. posix_spawn() :
      posix_spawn()函数通过直接指定要加载的可执行文件来创建子进程。对于那些熟悉UNIX系统的人来说,posix_spawn()调用被模式化为fork()后面跟着exec*()。然而,QNX中它的操作效率要高得多,因为不需要复制fork()中的地址空间,而只需在调用exec*()时销毁并替换它。
      在UNIX系统中,使用fork()-then-exec*()方法创建子进程的主要优点之一是可以灵活地更改新子进程继承的默认环境。这是在fork child中完成的,仅仅在exec*()之前。例如,以下简单的shell命令将在exec*()ing之前关闭并重新打开标准输出:ls >file
      您可以使用posix_spawn()执行相同的操作;它使您可以控制环境继承的以下类,这些类在创建新的子进程时经常进行调整:
  • 文件描述符
  • 进程用户和组id
  • signal mask
  • ignored signals
  • 自适应分区(调度器)属性
      还有一个配套的函数posix_spawnp(),它不需要程序的绝对路径来spawn,而是使用调用者的PATH来搜索可执行文件。使用posix_spawn()函数是创建新子进程的首选方法。
  1. spawn()
      QNX中微子spawn()函数类似于posix_spawn()。spawn()函数使您可以控制以下内容:
  • 文件描述符
  • 进程组id
  • signal mask
  • ignored signals
  • the node to create the process on
  • 调度策略
  • 调度参数(优先级)
  • maximum stack size
  • runmask (for SMP systems)

spawn()函数的一些基本形式是:

  • spawn():使用显式指定的路径spawn。
  • spawnp():搜索当前路径调用spawn(第一个匹配的)

  当一个进程是spawn()'ed,子进程继承了它的父进程的以下属性:

  • 进程组id(除非在inherit.flags中设置了SPAWN_SETGROUP)
  • session membership
  • 实际用户ID和实际组ID
  • 附加组ID
  • 优先级和调度策略
  • 当前工作目录和根目录
  • 文件创建掩模
  • signal mask(除非在inherit.flags中设置了SPAWN_SETSIGMASK)
  • signal action指定为SIG_DFL
  • signal actions specified as SIG_IGN(除了那些被inherit修改过的。当在inherit.flags中设置SPAWN_SETSIGDEF时使用sigdefault)

  子进程与父进程有几个不同之处:

  • 将父进程捕获的信号设置为默认操作(SIG_DFL)。
  • 子进程的tms_utime、tms_stime、tms_cutime和tms_cstime 跟父进程分开被traced。
  • 对于子进程,在生成SIGALRM信号之前剩余的秒数被设置为0。
  • 子进程的挂起信号集为空
  • 父类设置的文件锁不会被继承。
  • 父进程创建的计时器不会被继承。
  • 由父进程设置的内存锁和映射不会被继承。
       如果在远程节点上派生出子进程,则不设置进程组ID和会话成员关系;子进程被放入一个新会话和一个新进程组。子进程可以通过使用环境全局变量(在< unisd .h>中找到)访问父进程的环境。
  1. fork()
      fork()函数通过与调用进程共享相同的代码并复制调用进程的数据来创建一个新的子进程,从而为子进程提供一个完全相同的副本。大多数流程资源都是继承的。以下资源是显式不继承的:
  • 进程ID
  • 父进程ID
  • 文件锁
  • 待处理信号和警报
  • 计时器
    fork()函数通常用于以下两个原因之一:
  • 创建当前执行环境的新实例
  • 创建一个运行不同程序的新进程
      创建新线程时,将公共数据放在显式创建的共享内存区域中。在POSIX线程标准之前,这是实现此目的的惟一方法。对于POSIX线程,fork()的这种用法可以通过使用pthread_create()在单个进程中创建线程来更好地实现。
      当创建一个运行不同程序的新进程时,对fork()的调用很快就会接着调用exec*()函数。这种操作也可以通过对posix_spawn()函数或QNX Neutrino spawn()函数的单个调用来更好地完成,这两个操作结合在一起的效率要高得多。
      由于QNX Neutrino提供了比使用fork()更好的POSIX解决方案,所以它的使用可能最适合于移植现有代码,以及编写必须在不支持POSIX pthread_create()或posix_spawn()的UNIX系统上运行的可移植代码。
  1. vfork()
      当fork()的目的是为对exec*()函数之一的调用创建一个新的系统上下文时,vfork()函数(应该只从单线程进程中调用)非常有用。vfork()函数与fork()函数的不同之处在于,子进程不会获得调用进程数据的副本。相反,它借用调用进程的内存和控制线程,直到对exec*()函数之一进行调用。当子进程使用它的资源时,调用进程被挂起。
      vfork()子进程不能从调用vfork()的过程中返回,因为最终从父进程vfork()返回的将是一个不再存在的堆栈框架。

  2. exec*()
      exec*()函数家族用一个从可执行文件加载的新进程替换当前进程。
      exe函数家族有:execl() execle() execlp() execlpe() execv() execve() execvp() execvpe()。exec*()函数通常位于fork()或vfork()之后,以便加载新的子进程。这可以通过使用posix_spawn()调用来更好地实现。

进程加载

  使用exec*()、posix_spawn()或spawn()调用从文件系统加载的进程是ELF(可执行和链接格式)。如果文件系统位于面向块的设备上,则将代码和数据加载到主内存中。默认情况下,包含二进制文件的内存页是按需加载的,但是您可以使用procnto -m选项来更改这一点;有关更多信息,请参见后面"Locking memory"。
  如果文件系统是内存映射的(例如,ROM/flash映像),则不必将代码加载到RAM中,但可以在适当的地方执行。这种方法使所有的RAM都可用于数据和堆栈,而将代码留在ROM或flash中。在所有情况下,如果同一个进程被加载超过一次,那么它的代码将被共享。

内存管理

  虽然一些实时内核或执行人员在开发环境中提供了对内存保护的支持,但是很少有人为运行时配置提供受保护的内存支持,原因是内存和性能方面的缺陷。但是,随着内存保护在许多嵌入式处理器上变得越来越普遍,内存保护的好处远远超过它对性能的影响。
  将内存保护添加到嵌入式应用程序(特别是对于任务关键型系统)所获得的关键优势是增强了鲁棒性。有了内存保护,如果在多任务环境中执行的进程试图访问没有显式声明或分配的内存,MMU硬件可以通知OS,然后OS可以中止线程(在失败/违规指令时)。
  这“保护”了进程地址空间,防止一个进程的线程中的编码错误“损坏”其他进程甚至操作系统中的线程使用的内存。这种保护对开发和已安装的运行时系统都很有用,因为它使事后分析成为可能。
  在开发过程中,常见的编码错误(例如,偏离的指针和索引超出数组界限)可能导致一个进程/线程意外地覆盖另一个进程的数据空间。如果覆盖触及了直到很久以后才再次引用的内存,您可能会花费数小时进行调试(通常使用在线仿真器和逻辑分析程序),以试图找到“罪魁祸首”。
  如果启用了MMU,操作系统可以在发生内存访问冲突时立即中止进程,向程序员提供即时反馈,而不是在一段时间后神秘地导致系统崩溃。然后,操作系统可以在失败的进程中提供错误指令的位置,或者直接在该指令上定位符号调试器。

内存管理单元(MMUs)

  典型的MMU通过将物理内存划分为若干个4 kb的页面来进行操作。处理器内的硬件然后使用一组存储在系统内存中的页表来定义虚拟地址的映射(即,应用程序中使用的内存地址)到CPU发出的访问物理内存的地址。
  当线程执行时,由操作系统管理的页表使用的内存地址如何“映射”到附加到处理器的物理内存。
Figure 32
  对于具有许多进程和线程的大型地址空间,描述这些映射所需的页表条目的数量可能非常大,远远大于可以存储在处理器中的数量。为了保持性能,处理器将经常使用的外部页表的部分缓存到TLB(翻译查找缓冲区)中。TLB缓存上的“未命中”服务是启用MMU所带来的开销的一部分。我们的操作系统使用各种聪明的页表安排来最小化这种开销。
  与这些页表相关联的是定义每个内存页属性的位。页面可以标记为只读、读写等。通常,执行进程的内存用只读页表示代码,读写页表示数据和堆栈。
  当操作系统执行上下文切换时(即,挂起一个线程的执行并继续执行另一个线程),它将操作MMU,为新恢复的线程使用一组可能不同的页表。如果操作系统在单个进程内的线程之间切换,则不需要进行MMU操作。
  当新线程继续执行时,在线程运行时生成的任何地址都将通过分配的页表映射到物理内存。如果线程试图使用一个没有映射到它的地址,或者它试图使用一个地址的方式违反了定义的属性(例如,写入只读页面),CPU将收到一个“错误”(类似于零除错误),通常作为一种特殊类型的中断实现。
  通过检查由中断推送到堆栈上的指令指针,操作系统可以确定导致线程/进程内内存访问错误的指令的地址,并据此采取行动。

运行时的内存保护

  内存保护不仅在开发过程中很有用,它也可以为现场安装的嵌入式系统提供更高的可靠性。许多嵌入式系统已经使用了硬件“看门狗定时器”来检测软件或硬件是否“失去了理智”,但是这种方法缺乏mmu辅助。
  硬件看门狗定时器通常被实现为一个可重触发的单稳态定时器,附加在处理器复位线路上。如果系统软件没有定期地对硬件计时器进行频闪,则计时器将过期并强制处理器复位。通常,系统软件的某些组件将检查系统完整性,并对计时器硬件进行频闪,以表明系统是“正常的”。
  尽管这种方法可以从与软件或硬件故障相关的锁定中恢复,但是它会导致完全的系统重新启动,并且在重新启动时可能会导致重大的“停机”。

软件看门狗

  当在受内存保护的系统中出现间歇性的软件错误时,操作系统可以捕获该事件并将控制权传递给用户编写的线程,而不是内存转储工具。这个线程可以对如何最好地从故障中恢复做出明智的决定,而不是像硬件看门狗定时器那样强制进行完全重置。软件看门狗可以:

  1. 中止由于内存访问冲突而失败的进程,并简单地重新启动该进程,而不关闭系统的其余部分。
  2. 终止失败的进程和任何相关进程,将硬件初始化为“安全”状态,然后以协同的方式重启相关进程。
  3. 如果故障非常严重,关闭整个系统,并发出声音警报。
      这里的重要区别是,我们保留了嵌入式系统的智能、程序化控制,即使控制软件中的各种进程和线程可能由于各种原因而失败。硬件看门狗定时器仍然用于从硬件“挂起”中恢复,但是对于软件故障,我们现在有了更好的控制。
      在执行这些恢复策略的某些变体时,系统还可以收集有关软件故障性质的信息。例如,如果嵌入式系统包含或能够访问某些大容量存储(闪存、硬盘驱动器、连接到另一台具有磁盘存储的计算机的网络),软件监控器可以生成按时间顺序归档的转储文件序列。然后可以使用这些转储文件进行崩溃诊断。
      嵌入式控制系统通常采用这些“部分重启”方法来避免操作人员经历任何系统“停机”,甚至意识不到这些快速恢复软件故障。由于转储文件是可用的,软件开发人员可以检测和纠正软件问题,而不必在不方便的时间处理关键系统发生故障时所导致的紧急事件。如果我们将其与硬件看门狗定时器方法和其导致的服务中断时间进行比较,我们的选择就很明显了!
      事后转储文件分析对于任务关键型嵌入式系统尤其重要。当某一关键系统在该领域出现故障时,应作出重大努力确定故障的原因,以便在其他系统出现类似故障之前设计并应用“修复”。转储文件为程序员提供了他们解决问题所需的信息,如果没有这些信息,程序员可能除了客户神秘的抱怨“系统崩溃”之外就没什么其他信息可获取了。

质量控制

  通过将嵌入式软件划分为一组协作的、受内存保护的进程(包含线程),我们可以很容易地将这些进程视为“组件”,以便在新项目中再次使用。由于显式定义(以及硬件强制的)接口,可以将这些进程集成到应用程序中,并且确信它们不会破坏系统的整体可靠性。
  此外,由于流程的精确二进制映像(而不仅仅是源代码)正在被重用,我们可以更好地控制由于重新编译源代码、重新链接、开发工具的新版本、头文件、库例程等而可能导致的更改和不稳定性。由于进程的二进制映像是重用的(其行为可能通过命令行选项进行修改),因此我们对该二进制模块更有信心(比更改进程的二进制映像),因为更容易传递到新的应用程序中。
  尽管我们努力为所部署的系统生成无错误的代码,但软件密集型嵌入式系统的现实情况是,编程错误最终会出现在发布的产品中。与其假装这些bug不存在(直到客户打电话报告它们),我们应该采用“关键任务”的心态。系统的设计应该能够容忍并能够从软件故障中恢复。在我们构建的嵌入式系统中利用集成MMUs提供的内存保护是朝着这个方向迈出的良好的一步。

充分保护模型

  我们的完全保护模型将映像中的所有代码重新定位到一个新的虚拟空间中,启用MMU硬件并设置初始页表映射。这允许procnto在正确的、支持mmu的环境中启动。然后,流程管理器将接管此环境,根据其启动的进程的需要更改映射表。

私人虚拟内存

  在全保护模型中,每个进程都有自己的私有虚拟内存,内存大小为2或3.5 g(取决于CPU)。这是通过使用CPU的MMU来实现的。由于在两个完全私有的地址空间之间获取可寻址性的复杂性增加,进程切换和消息传递的性能成本将会增加。
在这里插入图片描述
  对于每个进程的页表,每个进程的内存成本可能增加4 KB到8 KB。注意,这个内存模型支持POSIX fork()调用。

路径名称管理

todo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值