4.1.5 应用程序员的接口(1)
值得注意的是,在图4-2的情形1和情形2中,软件任务必须被映射到能够被操作系统管理和调度的实体。程序员不能够简单地将处理器指派给必须执行的每个任务。这只能够由操作系统来完成。程序员必须使用操作系统能够理解的执行单元来使得操作系统理解软件任务。操作系统是开发人员的软件与多个内核之间的软件层。操作系统提供一组接口(API)来允许应用程序开发人员使用硬件资源和操作系统服务。为了利用所有操作系统服务,开发人员必须使用API。问题在于使用哪个操作系统API?每个操作系统厂商都提供了自己特有的API,尽管这些API的功能基本相同,但是它们不能够在不同平台之间移植。也就是说,使用Mac OS X(Darwin)API开发的软件不能够直接在Solaris上编译和执行,Solaris API不能够直接在Windows环境中编译和执行,依此类推。因此,需要通过使用操作系统API来对多个内核进行充分利用的程序,如果使用的是特定系统的API,那么就不能够被移植。这意味着如果希望将应用程序在新的环境中使用,就必须重写该应用程序。在多数情况下,这是不能够被接受的。这就是本书中我们使用POSIX API的原因。
1. 什么是POSIX以及为何使用它
POSIX(Portable Operating System Interface,可移植操作系统接口)是定义了标准操作系统接口和环境的标准,包括命令解释器(shell)和常见的实用程序,用于在源代码级别支持应用程序的可移植性。应用程序开发人员和系统实现人员均可使用这个标准。为了使得本书尽可能适用于更多的系统开发人员和应用程序开发人员,我们选择使用POSIX标准来介绍操作系统API。主流操作系统环境均声称对POSIX标准的基本支持,包括ZOS、Solaris、AIX、Windows、Mac OS X、Linux、HP-UX和IRIX。尽管每种环境有着自己专有的API,但均同时支持POSIX标准。既然我们讨论的概念、实例和程序都是基于POSIX标准的,您可以在任何环境中对它们进行验证。POSIX标准扮演跨平台伪码的角色,使得我们可以用一种能够在所有主要环境中实现的语言介绍多核编程(www.cppentry.com)的主要概念。此外,POSIX实现了一种"共同特性"操作系统接口。这意味着在多数情况下,将概念、原理和函数调用在必要时转换成专用的操作系统API是非常直观的。
由于POSIX标准的目的是在源码级别上提供可移植性,所以我们可以在POSIX组件之上构建类库、模板库和应用程序框架,然后将它们在所有主流操作系统环境中编译并使用。显然,对于特定平台的操作系统API,是不能够做到这一点的。尤其是工作于图4-1中的级别3和级别4的开发人员可以从这类可移植性中受益。用于并行处理的应用程序框架(如STAPL)以及模板库和类库(如TBB),可以通过对底层实现时的进程和线程使用POSIX API来得到可移植性。此外,如果使用POSIX API,则在多个环境中混合和协调高级应用程序框架和构建块库就变得可行了。在级别1和级别2工作的开发人员使用POSIX API可以做到一次编写、到处编译。由于大规模计算机配置,如集群、企业级服务器(大型机)乃至超级计算机均有兼容POSIX的操作环境,当可扩展性是重要的问题时,开发人员有着规格齐全的硬件支持。尽管多核处理器刚刚在台式机、开发者工作站和小的服务器上出现,但它们已经广泛用于大规模计算机配置达十余年。因此,当您投入到POSIX API的学习中时,从小的商业应用服务器到最大的基于集群的配置,均能够应用它。
POSIX标准使得我们可以以跨平台的方式来谈论多核编程(www.cppentry.com)与表4-1中列出的核心操作系统服务之间的交互。本书中的所有实例和程序均在兼容POSIX的环境中书写和编译,而且本书的两个附录包含了POSIX关于进程管理和线程管理的参考资料。
2. 进程管理
进程生命周期是本书不断提到的进程管理的一个重要方面。本书将进程生命周期概括为:
进程创建
进程调度/执行
进程终止
标准C++(www.cppentry.com)库没有提供任何处理进程生命周期中主要活动的服务,因此您在需要对进程编程(www.cppentry.com)时,需要寄希望于操作系统API。即使在CMP中,也可能没有足够的处理器来同时运行所有的进程。操作系统必须对进程进行多任务(multitasking)。多任务使得可以同时执行多个进程,而多线程允许一个进程同时执行多个任务。当操作系统使用调度策略来允许两个或多个进程并发共享CPU时,这被称为多任务。每个进程执行,直到运行了预设的时间长度或直到发生一些事件。给出的进程在内核上执行的时间间隔被称作时间片(quantum)。然后操作系统会切换到另一个进程。这个切换时间极短,产生进程同时执行的假象,而实际上在一个内核上,一次只有一个进程是活动的。这种进程间切换持续发生,直到所有进程都结束。调度策略决定了何时应当切换进程。调度策略还将控制当发生以下情形时该如何做:
进程或线程是一个运行线程,然后它成为了阻塞线程。
进程或线程是一个运行线程,然后它成为了被抢占线程。
进程或线程是一个阻塞线程,然后它成为了可运行线程。
一个运行线程调用了能够改变进程或线程的优先级或调度策略的函数。
在本书中,我们假定您的环境支持4种POSIX标准支持的基本调度策略:
- SCHED_FIFO
- SCHED_RR
- SCHED_SPORADIC
- SCHED_OTHER
表4-3包含了可以使用的每种基本调度策略的描述。
表4-3
|
POSIX调度策略< xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" /> |
描 述 |
|
SCHED_FIFO |
当时间片用完,线程被放置到其 优先级队列的头部 |
|
SCHED_RR |
当时间片用完,线程被放置到其 优先级队列的尾部 |
|
SCHED_SPORADIC |
使用服务器的调度策略 |
|
SCHED_OTHER |
根据实现(implementation)定义, 这是用于一般用途的最有效的调度策略 |
每个进程都被关联的调度策略和优先级所控制。同每种策略关联的是优先级范围。每种策略的定义指定了该策略的最小优先级范围。每种策略的优先级范围可能会与其他策略的优先级范围重叠。
操作系统也用来在进程间传输信号。当Process A必须向Process B发送一个终止信号时,操作系统将传输该信号。进程生命周期的每个主要步骤都受到操作系统的管理,而且您必须使用POSIX API来访问这些服务。要记住存储在磁盘的软件或程序不是进程或线程。进程是执行中的程序,拥有进程控制块(process control block)和进程表,而且被操作系统调度。线程是进程的一部分。在创建任何进程之前,必须由操作系统加载软件和程序。在创建线程之前,必须已经创建了进程或轻量级进程。
3. 进程管理实例:游戏场景
为了说明上述内容,我们将要看一个经典的游戏。我想出一个6字符的编码,这个编码中包含的字符可以多次出现,但是只能包含数字0~9或字符a~z。您的任务是猜出我所想的编码。在游戏中,时间限定为5分钟,如果能够在5分钟之内猜到我所想的内容,您就赢了。您拿出纸笔,进行简单的计算之后发现有4 496 388种可能。然后,在接下来的2分钟,您匆匆处理整个SDLC并提出如下的策略。首先,碰巧您有着一个文件包含4 496 388种可能编码,因此,您编写一个C++(www.cppentry.com)程序来完成类似示例4-1中的工作。
示例4-1
- //...
- bool Found = false;
- ifstream Fin(Possibilities)
- while(!Fin.eof() && !Fin.fail() && !Found)
- {
- getline(Fin,Guess);
- if(Guess == MagicCode){
- Found = true;
- }
- }
- //...