3.4 Linux进程模型
Linux中的基本结构元素是进程,由可执行代码和如数据、文件描述符等组成的资源组合组成。这些资源完全是受保护的,因此一个进程不能直接访问另一个进程的资源。为了使两个进程相互通信,它们必须使用Linux定义的中间进程通信机制,如共享存储区域或管道。
由于它在系统中建立了一个高级别的保护,所以工作良好。错误的进程会被系统检测出来并在它对其他进程造成破坏前将其抛出(图3-7)。但是在创建进程时的过度开销和使用中间进程通信机制的代价是昂贵的。
一个线程只有代码。线程只存在于进程内部,一个进程中的所有线程都共享资源。因此所有的线程都可以同样地访问数据存储器和文件描述符。这个模型有时叫做轻量级多任务以与UNIX/Linux进程模型区分。
轻量级任务的优点是中间线程的通信更有效。它的缺点是任何线程都可以破坏其他线程的数据。多数的RTOS都曾经有类似轻量型的模型。当然最近几年,存储器保护的硬件花费明显下降,因此一些RTOS供应商开始提供他们系统的保护模式版本,这看起来像Linux进程模型。
3.4.1 fork()函数
Linux的生命从一个进程开始,这就是在启动时创建的init进程。系统中其他进程的创建都是通过调用fork()实现的。调用fork()的进程叫父进程,新创建的进程叫子进程。所以每个进程都有父和子,这取决于谁创建谁。
如果你是在一个多任务的操作系统中,这里的任务是函数通过调用任务创建服务生成的,那么fork进程看起来显然很奇怪。fork()函数创建一个父进程的副本——代码、数据、文件描述符和父进程目前拥有的其他任何资源。这可能会增加MB量级存储空间的复制。为避免复制许多可能被覆盖的东西,Linux引入了一个写时复制的策略。
fork()函数从复制进程数据结构和给子进程一个新的进程标识符(PID)开始。然后,复制一个页目录和页表。最开始,PTE均与父进程一样指向相同的物理页面。两个进程的所有页面都设置为只读。当其中的某一个进程试图写时,会引起一个页面错误。这会让Linux给该进程分配一个新页面,并复制现有页面的内容。
因为两个进程都执行同样的代码,所以它们都从fork()的返回值继续执行(这就是为什么很奇怪)。为了区分父进程和子进程,fork()给子进程返回一个函数值0,但是给父进程返回子进程的PID值。表3-1就是fork函数调用的一个小例子。
3.4.2 execve()函数
当然,99%的时间里都是子进程通过调用execve()激活一个新的程序来从磁盘导入一个可执行镜像文件。表3-2给出了一个简单的命令行注释器的架构形式。它从stdin读入一行文本,解析后调用fork()函数创建一个新进程。然后子进程调用execve()导入一个文件并执行之前输入的命令。execve()覆盖了调用进程的代码、数据和堆栈段。
如果这是普通的前景命令,则命令解释器必须等到命令执行完毕。这是通过waitpid()来完成的,该函数阻塞调用进程直到与pid参数匹配的进程结束。注意,大部分的多任务操作系统没有能力阻塞一个挂起了另一个进程的进程或任务。
如果execve()成功了,它没有返回值,而是将控制传递至新载入的程序。