预计阅读本页时间:-
11.4.2 作业、进程、线程和纤程管理API调用
新的进程是由Win32 API函数CreatProcess创建的。这个函数有许多参数和大量的选项,包括被执行文件的名称,命令行字符串(未解析)和一个指向环境字符串的指针。其中也包括了控制诸多细节的令牌和数值,这些细节包括了如何配置进程和第一个线程的安全性,调试配置和调度优先级等。其中一个令牌指定创建者打开的句柄是否被传递到新的进程中。该函数还接受当前新进程的工作目录和可选的带有关于此进程使用GUI窗口的相关信息的数据结构。Win32对新进程和其原始线程都返回ID和句柄,而非只为新进程返回一个ID号。
大量的参数揭示了Windows和UNIX在进程创建的开发设计上的诸多的不同之处。
1)寻找执行程序的实际搜索路径隐藏在Win32的库代码里,但UNIX中则显式地管理该信息。
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
2)当前工作目录在UNIX操作系统里是一个内核态的概念,但是在Windows里是用户态字符串。Windows为每个进程都打开当前目录的一个句柄,这导致了和UNIX一样的麻烦:除了碰巧工作目录是跨网络的情况下可以删除它,其他工作目录都是不能删除的。
3)UNIX解析命令行,并传递参数数组;而Win32需要每个程序自己解析参数。其结果是,不同的程序可能采用不一致的方式处理通配符(如*.txt)和其他特殊字符。
4)在UNIX中,文件描述符是否可以被继承是句柄的一个属性。不过在Windows中,其同时是句柄和进程创建参数的属性。
5)Win32是面向图形用户界面的,因此新进程能直接获得其窗口信息,而在UNIX中,这些信息是通过参数传递给图形用户界面程序的。
6)Windows中的可执行代码没有SETUID位属性,不过一个进程也可以为另一个用户创建进程,只要其能获得该用户的信用标识。
7)Windows返回的进程、线程句柄可以用在很多独立的方法中修改新进程/线程,例如复制句柄、在新进程中设置环境变量等。UNIX则只在fork和exec调用的时候修改新进程。
这些不同有些是来自历史原因和哲学原因。UNIX的设计是面向命令行的,而不是像Windows那样面向图形用户界面的。UNIX的用户相比来说更高级,同时也懂得像PATH环境变量的概念。Windows Vista继承了很多MS-DOS中的东西。
这种比较也有点偏颇,因为Win32是一个用户态下的对NT本地进程执行的包装器,就像UNIX下的系统库函数fork/exec的封装。实际的NT中创建进程和线程的系统调用NtCreateProcess和NtCreateThread比Win32版本简单得多。NT进程创建的主要参数包括代表所要运行的程序文件句柄、一个指定新进程是否默认继承创建者句柄的标志,以及有关安全模型的相关参数。由于用户态下的代码能够使用新建进程的句柄能对新进程的虚拟地址空间进行直接的操作,所有关于建立环境变量、创建初始线程的细节就留给用户态代码来解决。
为了支持POSIX子系统,本地进程创建有一个选项可以指定,通过拷贝另一个进程的虚拟地址空间来创建一个新进程,而不是通过映射一个新程序的段对象来新建进程。这种方式只用在实现POSIX的fork,而不是Win32的。
线程创建时传给新线程的参数包括:CPU的上下文信息(包括栈指针和起始指令地址)、TEB模板、一个表示线程创建后马上运行或以挂起状态创建(等待有人对线程句柄调用NtResumeThread函数)的标志。用户态下的栈的创建以及argv/argc参数的压入需要由用户态下的代码来解决,必须对进程句柄调用本地NT的内存管理API。
在Windows Vista的发行版中,包含了一个新的关于进程操作方面的本地API,这个接口将原来许多用户态下的步骤转移到了内核态下执行,同时将进程创建与起始线程创建绑定在一起进行。作这种改变的原因是支持通过进程划分信任边界。一般来说,所有用户创建的进程被同等信任,由用户决定信任边界在哪里。在Windows Vista中的这个改变,允许进程也可以提供信任边界,但是这意味着对于新进程句柄来说,创建者进程没有足够的权利在用户态下实现进程创建的细节。
1.进程间通信
线程间可以通过多种方式进行通信,包括管道、命名管道、邮件槽、套接字、远程过程调用(RPC)、共享文件等。管道有两种模式:字节管道和消息管道,可以在创建的时候选择。字节模式的管道的工作方式与UNIX下的工作方式一样。消息模式的管道与字节模式的管道大致相同,但会维护消息边界。所以写入四次的128字节,读出来也是四个128字节的消息,而不会像字节模式的管道一样读出的是一个512字节的消息。命名管道在Vista中也是有的,跟普通的管道一样都有两种模式,但命名管道可以在网络中使用,而普通管道只能在单机中使用。
邮件槽是OS/2操作系统的特性,在Windows中实现只是为了兼容性。它们在某种方式上跟管道类似,但不完全相同。首先,它们是单向的,而管道则是双向的。而且,它们能够在网络中使用但不提供有保证的传输。最后,它们允许发送进程将消息广播给多个接收者而不仅仅是一个接收者。邮件槽和命名管道在Windows中都是以文件系统的形式实现,而非可执行的功能函数。这样做就可以通过现有的远程文件系统协议在网络上来访问到它们。
套接字也与管道类似,只不过它们通常连接的是不同机器上的两个进程。例如,一个进程往一个套接字里面写入内容,远程机器上的另外一个进程从这个套接字中读出来。套接字同样也可以被用在同一台机器上的进程通信,但是因为它们比管道带来了更大的开销,所以一般来说它们只被用于网络环境下的通信。套接字原来是为伯克利UNIX而设计的,它的实现代码很多都是可用的,正如Windows发布日志里面所写的,Windows代码中使用了一些伯克利的代码及数据结构。
远程过程调用(RPC)是一种进程A命令进程B调用进程B地址空间中的一个函数,然后将执行结果返回给进程A的方式。在这个过程中对参数的限制很多。例如,如果传递的是个指针,那么对于进程B来说这个指针毫无意义,因此必须把数据结构打包起来然后以进程无关的方式传输。实现RPC的时候,通常是把它作为传输层之上的抽象层来实现。例如对于Windows来说,可以通过TCP/IP套接字、命名管道、ALPC来进行传输。ALPC的全称是高级本地过程调用(Advanced Local Procedure Call),它是内核态下的一种消息传递机制,为同一台机器中的进程间通信作了优化,但不支持网络间通信。基本的设计思想是可以发送有回复的消息,以此来实现一个轻量级的RPC版本,提供比ALPC更丰富的特性。ALPC的实现是通过拷贝参数以及基于消息大小的临时共享内存分配。
最后,进程间可以共享对象,如段对象。段对象可以同时被映射到多个进程的虚拟地址空间中,一个进程执行了写操作之后,其他进程可以也可以看见这个写操作。通过这个机制,在生产者消费者问题中用到的共享缓冲区就可以轻松地实现。
2.同步
进程间也可以使用多种形式的同步对象。就像Windows Vista中提供了多种形式的进程间通信机制一样,Vista也提供了多种形式的同步机制,包括信号量、互斥量、临界区和事件。所有的这些机制只在线程上工作,而非进程。所以当一个线程由于一个信号量而阻塞时,同一个进程的其他线程(如果有的话)会继续运行而并不会被影响。
使用Win32的API函数CreateSemaphore可以创建一个信号量,可以将它初始化为一个给定的值,同时也可以指定最大值。信号量是一个内核态对象,因此拥有安全描述符和句柄。信号量的句柄可以通过使用DuplicateHandler来进行复制,然后传递给其他进程使得多个进程可以通过相同的信号量来进行同步。在Win32的名字空间中一个信号量也可以被命名,可以拥有一个ACL集合来保护它。有些时候通过名字来共享信号量比通过拷贝句柄更合适。
对up和down的调用也是有的,只不过它们的函数名看起来比较奇怪:ReleaseSemaphore(up)和WaitForSingleObject(down)。可以给WaitForSingleObject一个超时时间,使得尽管此时信号量仍然是0,调用它的线程仍然可以被释放(尽管定时器重新引入了竞态)。WaitForSingleObject和WaitForMultipleObject是将在11.3节中讨论的分发者对象的常见接口。尽管有可能将单个对象的API封装成看起来更加像信号量的名字,但是许多线程使用多个对象的版本,这些对象可能是各种各样的同步对象,也可能是其他类似进程或线程结束、I/O结束、消息到达套接字和端口等事件。
互斥量也是用于同步的内核态对象,但是比信号量简单,因为互斥量不需要计数器。它们其实是锁,上锁的函数是WaitForSingleObject,解锁的函数是ReleaseMutex。就像信号量句柄一样,互斥量的句柄也可以复制,并且在进程间传递,从而不同进程间的线程可以访问同一个互斥量。
第三种同步机制是临界区,实现的是临界区的概念。临界区在Windows中与互斥量类似,但是临界区相对于主创建线程的地址空间来说是本地的。因为临界区不是内核态的对象,所以它们没有显式的句柄或安全描述符,而且也不能在进程间传递。上锁和解锁的函数分别是EnterCriticalSection和LeaveCriticalSection。因为这些API函数在开始的时候只是在用户空间中,只有当需要阻塞的时候才调用内核函数,它们比互斥量快得多。在需要的时候,可以通过合并自旋锁(在多处理器上)和内核同步机制来优化临界区。在许多应用中,大多数的临界区几乎不会被竞争或者只被锁住很短的时间,以至于没必要分配一个内核同步对象,这样会极大地节省内核内存。
我们讨论的最后一种同步机制叫事件,它使用内核态对象。就像我们前面描述的,有两类的事件——通知事件和同步事件。一个事件的状态有两种:收到信号和没收到信号。一个线程通过调用WaitForSingleObject来等待一个事件被信号通知。如果另一个线程通过SetEvent给事件发信号,会发生什么取决于这个事件的类型。对于通知事件来说,所有等待线程都会被释放,并且事件保持在set状态,直到手工调用ResetEvent进行清除;对于同步事件来说,如果有一个或多个线程在等待,那么有且仅有一个线程会被唤醒并且事件被清除。另一个替换的操作是PulseEvent,像SetEvent一样,除了在没有人等待的时候脉冲会丢失,而事件也被清除。相反,如果调用SetEvent时没有等待的线程,那么这个设置动作依然会起作用,被设置的事件处于被信号通知的状态,所以当后面的那个线程调用等待事件的API时,这个线程将不会等待而直接返回。
Win32的API中关于进程、线程、纤程的个数将近100个,其中大量的是各种形式的处理IPC的函数。对上面讨论的总结和另一些比较重要的内容可以参见图11-26。

可以注意到不是所有的这些都是系统调用。其中有一些是包装器,有一些包含了重要的库代码,这些库代码将Win32的接口映射到本地NT接口。另外一些,例如纤程的API,全部都是用户态下的函数,因为就像我们之前提到的,Windows Vista的内核态中根本没有纤程的概念,纤程完全都是由用户态下的库来实现的。