5 多线索的操作 |
基本的程序员函数接口,而不是那些相关于线索或者多线索的接口,是系统V接口定义,第三版本(SVID3)。通常,多数当前UNIX系统调用保持不变。主要的差别是系统调用中对于轻量进程和执行它们的线索阻碍做它的块。然而程序员必须明白线索和LWP共享几乎全部程序员所见的进程资源,例如地址空间和文件描述符表。这会导致几个潜在的麻烦:
当一个程序启动时,核心创建一个轻量进程,并且此进程开始执行编译为主程序的线索。通过调用库,来创建附加的线索,此库说明了执行新线索以及为它使用的栈的一个过程。
依赖于实现、库、或者程序员提供的参数,一个线索在它的生命期中,可以与同一或者不同的轻量进程相关联。在线索和轻量进程之间可以有一对一的关系,或者一个或多个轻量进程可以由一组线索中的线索库复用。通常,一个线索不能告诉轻量进程和线索之间的关系是什么,尽管对于性能上的原因,或者为了避免一些死锁,一个程序可以要求拥有更多或者更少的轻量进程。
当一个线索执行一个核心调用时,在核心调用期间,它保持绑定到同样的轻量进程。如果核心调用阻塞,那个线索和它的轻量进程保持阻塞。其它轻量进程可以在那个程序中执行其它的轻量进程,包括执行其它的核心调用。同样的原理适用于缺页错。
对于线索或者轻量进程,没有系统范围的名字空间。因此,例如,不可能从一个进程外面将一个信号直接给一个特殊的轻量进程,或者知道哪一个轻量进程发送一个特殊的消息。
下面的状态对每一线索唯一:
所有其它的进程状态由进程内部的线索共享。
线索有一些私有的存储器(除了堆栈以外),称为线索局部存储器。程序中多数变量共享于所有执行它的线索中,但每一线索有它自己的线索局部变量的拷贝。概念上,线索局部存储器是非共享、静态分配的数据。C库变量errno是一个应该放在线索局部存储器中的一个很好的变量的例子。这允许每一线索直接引用errno,而且允许线索插入执行,而不用担心破坏其它线索中的errno。线索局部存储器对于访问是潜在昂贵的,因此它应该限制于重要的,例如支持旧的、非重入的接口。
一个线索是否绝对地防止访问其它的线索堆栈或者线索局部变量,是依赖于实现的,但是正确的线索必须永不去做它。
通过一个新的由编译器和链接器支持的 #pragma ,来得到线索局部存储器。线索局部存储器初始为零;不允许静态初始化。在C中,对于errno的线索局部存储器将说明如下:
#pragma unshared errno
extern int errno;
在程序启动时间,通过累加链接库的线索局部存储器的需求,由运行时间链接器来计算线索局部存储器的大小。这防止了线索局部存储器的确切大小作为库接口的一部分。一旦计算了大小,它就不会改变(例如,在进程中将来的动态链接)。这个限制使得一旦线索开始,就防止改变线索局部存储器的大小。因此在线索的启动时,就知道线索局部存储器的需求。通过使用线索局部存储器,可以建立更加动态的机制(例如POSIX 说明线索的数据)。
线索通过使用设备来进行彼此同步,此设备是由代表一个标准的语法集的实现所提供的。支持下面的同步类型:
体系结构允许广泛地实现每一所支持的同步类型。例如,互斥锁可以实现为旋转锁、睡眠锁、或者自适应锁等。
这些设备在内存中使用同步变量。变量可以静态分配和/或者在固定地址(与变量的限制对齐)。程序员可以在初始化变量的同时,选择特殊的同步变量的实现变形。如果变量初始化为零,使用缺省的实现。同步变量也可以放在进程间所共享的内存中。程序员选择每一同步类型的实现变形,以允许变量同步在共享此变量的进程中的线索。同步原语作为基本的映射对象的一部分,应用到共享变量。换句话说,可以在进程之间共享同步变量,尽管它们被映射到不同的虚拟地址。
不在共享内存的同步变量,对于核心是完全不知道的。在共享内存或者文件中的同步变量,对于核心也是不知道的,除非一个线索阻塞在它们上面。在后面的情况,线索暂时绑定到由核心阻塞的LWP上,好象在一个系统调用中。
每一线索有它自己的信号屏蔽。这允许一个线索,在它使用由信号处理所修改的状态时,阻塞一些信号。所有在同一地址空间中的线索共享一组信号句柄,这些句柄通常由signal()和其变形设置。如果需要,可以为一个特殊的应用程序,通过使用每一进程信号灯,来实现每一线索的信号句柄。例如,信号句柄使用处理信号的线索ID,以作为每一线索句柄表的索引。如果线索库要实现每一线索信号句柄,它必须以正确的文法,来确定几个线索在何时拥有不同的信号句柄的混合,SIG-IGN和SIG-DFL。另外,所有线索应该承担句柄状态。由于这个原因,感觉到每一线索信号句柄的库支持过于复杂而且程序员很可能混淆。
如果一个信号句柄标记为SIG-DFL或者SIG-IGN,接受信号的行为(退出、内存映象转储、停止、继续或者忽略)影响接受进程中的所有线索。
信号分成两类:陷阱和中断。陷阱(例如,SIGILL、SIGFPE、SIGSEGV)是由线索操作同步地引起的信号,并且仅由引起它们的线索处理。同一地址空间的几个线索可想象地产生和同时处理同类型陷阱。中断(例如,SIGINT、SIGIO)是由进程外的事件异步引起的信号。一个中断可以有任何线索处理,此线索在它的信号屏蔽中使能此中断。
如果使能一个线索以接受中断,仅选择一个。因此,几个线索可以处在同时处理同类型信号的进程中。如果所有线索屏蔽一个信号,它会在进程上挂起,直到一个线索不屏蔽此信号。对于单线索进程,进程接受的信号数少于或者等于发送数。
例如,一个应用程序可以使能几个线索,来处理特殊的I/O中断。当每一新中断到来,选择其它线索处理信号,直到所有被使能的线索是活动的。然后新信号将挂起,以等待线索完成处理和重新使能信号处理。
线索可以通过一个新的接口将信号发送给进程内的其它进程;thread_kill()。这时信号行为象一个陷阱而且仅由锁说明的线索处理。程序员也可以通过sigsend()将一个信号发送给所有线索。一个线索不能发送一个信号到其它进程中的线索,因为其它进程中的线索是不可见的。
不绑定到LWP上的线索可以不使用替代的信号栈。将可替代的信号栈加到非绑定的线索状态上,被认为是太昂贵了,以至于不能实现,因为这会要求一个系统调用,为每一请求它的线索的上下文切换建立替代栈。绑定到LWP上的线索可以使用替代栈,就象状态与每一LWP相联系。
setjmp()和longjmp()紧工作在一个特殊的线索内。尤其是,一个线索longjmp()到另一线索是错误的。因此,仅当处理此信号的线索执行setjmp()是才可能从一个信号句柄longjmp()。
对于线索多数可获得的接口,是那些在单线索UNIX中的UNIX进程所获得的。由上面提到的,它们中的有些接口在多线索环境下有不同的实现,但是打算提供“UNIX语法”作为正常的编程模型。本章描述一些需要创建和管理进程的附加的接口。
接口语法如图4所示。
thread_create()创建一个新的线索。如果stack_addr不为NULL,起始于stack_addr的stack_size字节的内存用于线索栈。这时任何线索局部存储器也可以放在堆栈上,以至于不干涉堆栈的增长。这允许一个语言运行时间库,控制线索存储器,而不用干涉它的内存分配器。初始栈指针处在所说明的栈中是更高或者更低地址,是依赖于机器的。如果stack_addr为NULL,从堆中分配栈。如果stack_size不为零,栈将是所说明的大小。否则使用一个缺省的堆栈大小。为零的线索局部存储器也分配给线索。thread_create()返回新线索的ID。线索ID仅在进程内部有意义。初始线索优先级和信号屏蔽被设置成与它的产生器相同的值。当新线索启动时,它通过调用func(arg)的过程以开始执行。如果func返回,线索退出(调用thread_exit())。flag参数提供下面的选项:
THREAD_STOP
线索在它创建后被立即悬挂。直到另一线索执行thread_continue()来启动它时,此线索才运行。如果不说明THREAD_STOP,线索是立即可运行的。
THREAD_NEW_LWP
与线索一起创建一个新的LWP。此新的LWP被加到用于执行线索的LWP池中。
THREAD_BIND_LWP
创建一个新的LWP,而且新的线索永久地绑定到它上。
THREAD_WAIT
说明另一线索将最终等待这个线索退出。这也意味着与THREAD_WAIT一起创建的线索的线索ID,将会等待线索返回时才可以重用。如果不用THREAD_WAIT创建线索,线索ID在线索退出的任何时候都可以重用。
thread_setconcurrency()设置真实的并发度(例如,LWP数目),其中应用程序中的非绑定线索请求为n。永久地绑定线索的LWP数不包含在n中。如果n为零(缺省),库自动地创建许多LWP,以用于调度非绑定线索,这是为避免死锁而要求的。此数可以通过用THREAD_NEW_LWP创建一个线索来增加。如果n小于当前最大数,LWP从池中移走。
thread_setconcurrency()仅保证这个并发度对于应用程序线索是可获得的。在任何某一时刻,由库所要求的实际的LWP数可以变化。
由库(n=0)自动产生的LWP数足够避免死锁,但是它也许不足以避免低性能;库可以创建过少或者过多的LWP。程序员可以通过使用THREAD_NEW_LWP创建线索,或者使用由应用程序所要求的thread_setconcurrency()来调节LWP的数目。
thread_exit()终止当前的线索,并且释放由线索包所分配的线索资源。
thread_wait()阻塞,直到被说明的线索退出。等待一个不用THREAD_WAIT属性创建的线索、等待当前的线索、在同一线索上有多个thread_wait()是错误的。如果thread_id为NULL,那么任何标记为THREAD-WAIT的线索的退出,会引起thread_wait()返回。如果当创建线索时,程序员提供了一个堆栈,那么当thread_wait()成功返回时,它可以重新声明。thread_wait()成功返回后,返回的thread_id在任何随后的线索操作中是不可用的。
此函数的一个替代的接口是带有id_type的waitid(),其中id_type等于下面之一:
P_THREAD
waitid()等待由id说明的线索。
P_THREAD_ALL
waitid()等待任何标记为THREAD-WAIT的线索。
线索的退出状态总是零。
thread_get_id()返回调用者的线索ID。
thread_sigsetmask()或者sigprocmask()设置线索信号屏蔽。
thread_kill()引起将被说明的信号发送到被说明的线索上。此函数的一个替代的接口是带有id_type的waitid(),其中id_type等于下面之一:
P_THREAD
将sig送到进程内部由id说明的线索上。
P_THREAD_ALL
将sig送到进程内部的所有线索上。
thread_stop()防止运行被说明的线索。如果thread_id为NULL,那么立即停止当前线索。thread_continue()初始地启动一个线索,或者在thread_stop()之后重新启动一个线索。可以推迟thread_continue()的影响,但是thread_stop()直到被说明的线索停止时才返回。
thread_prority()设置被说明线索的优先级。如果thread_id为NULL,使用当前的线索。 优先级必须大于或者等于零。增加被说明的优先级会增加调度优先级。返回旧的优先级。如果被说明的线索不运行,那么它可以或者不可以立即执行,尽管它的新的优先级大于当前执行线索的优先级。
线索同步设备设计成在一个进程内部以及进程之间同步线索。当初始化一个同步变量时,程序员必须说明同步变量是否在进程间共享。程序员也可以经常地说明其它的变形,例如额外调试、旋转等待等。程序员可以按位或者THREAD_SYNC_SHARED到变形类型中,以说明此变量在进程之间共享。
任何静态或者动态分配为零的变量可以立即使用,而不用更多的初始化,以及在缺省初始状态提供缺省的实现变形。一个带有实现变形类型为零的动态初始化,也说明缺省的实现变形。
互斥锁提供简单的互斥。它们在空间和时间上都是低开销的,由此适于高频率地使用。互斥锁是严格括入的(bracketing),因为一个线索释放由另一线索保持的锁是错误的。互斥锁用于防止临界区代码中数据的不一致性。它们也可以用于防止单线索代码。
mutex_enter()请求锁,如果它已经保持,会潜在地阻塞。
mutex_exit()释放锁,潜在地释放一个等待者。
mutex_tryenter()请求锁,如果它已经不保持。
mutex_tryenter()可以用于在那些通常破坏锁层次的操作中死锁。
条件变量用于等待,直到某一特殊条件为真。条件变量必须与互斥锁连起来使用。这实现了一个典型的监控器。
cv_wait()阻塞,直到条件信号成立。它在阻塞前释放相连的互斥锁,并且在返回前重新得到它。由于重新得到互斥锁会被其它等待互斥锁的线索阻塞,引起等待的条件必须重新测试。因此,典型的使用是:
mutex_enter(&m);
...
while (some_condition){
cv_wait(&cv, &m);
}
...
mutex_exit(&m);
这允许条件是一个复杂的表达式,因为它由互斥锁保护。如果多于一个的线索阻塞在条件变量上,不保证得到锁的顺序。
cv_signal()唤醒阻塞在cv_wait()上的一个线索。
cv_broadcast()唤醒阻塞在cv_wait()上的所有线索。由于cv_broadcast()引起所有线索阻塞在重新竞争互斥锁的条件上,应该小心地使用。例如,适当的使用cv_broadcast(),来允许线索在资源被释放时竞争可变的资源。
信号灯同步设备提供了经典的计数信号灯。它们没有互斥锁有效,但是它们不需要括入,使得它们可以用于异步事件通知(例如,信号灯句柄)。它们也包含状态,由此它们可以异步地使用,而不用得到由条件变量所要求的互斥锁。
sema_p()递减信号灯,潜在地阻塞线索。
sema_v()递增信号灯,潜在地释放一个等待的线索。
sema_tryp()如果不要求阻塞,递减信号灯。
多读、单一写锁允许许多线索只读地同时访问由此锁保护的对象。它仅允许一个线索访问对象,以在任何时间写,而且排斥任何读者。对于多读、单一写锁的一个好的参加者,是更多地查找对象而不是改变。简要地,这种类型的锁也被认为是读者/写者锁。
rw_enter()试图得到一个读者或者写者锁。type可以是下面之一:
RW_READER
得到一个读者锁。
RW_WRITER
得到一个写者锁。
rw_exit()释放一个读者或者写者锁,如果不会阻塞,rw_tryenter()得到一个读者或者写者锁。rw_downgrade()原子地将一个写者锁转换为一个读者锁。任何等待写者,保持等待。如果没有等待写者,它唤醒任何挂起的读者。rw_tryupgrade()试图原子地将一个读者锁转换为一个写者锁。如果在此过程中有另一个rw_tryupgrade(),或者有任何写者等待,它返回错误指示。
一个轻量进程包含一个核心中的数据结构,用于处理器调度、缺页错处理、和核心调用执行。它也包含对于LWP为私有的状态和与一个进程(地址空间)的联系。下面的程序员可见的状态由核心维护,而且对一进程内的每一LWP是唯一的。
所有其它进程状态都由进程内的LWP共享。
注意即使CPU利用率、虚拟时间报警和可替换的信号堆栈对于每一LWP是可获得的,对于每一个在LWP上复用的线索,不保持这个状态。请求此状态的线索必须绑定到一个LWP上。LWP状态是否包括对于核心所知的一个分离的堆栈域,是独立于实现的。当然,轻量进程同一个堆栈一起运行。
当所有LWP等待一些无定义、外部事件(例如,poll())时,将一个新信号,SIGWAITING,发送给进程。缺省的对SIGWAITING的处理是忽略它。为了避免死锁,线索包用来接受SIGWAITING,以引起创建外部LWP。这在功能上相似于在[Adderson 1990]中所描述的体系结构。
在为“无定义”的等待发送SIGWAITING时,假定阻塞某些事件上的很短的项,例如缺页错或者文件系统I/O可以花费相对于CPU速度而言的很长的时间。需要定义一个在这些情况的可替换的信号。
每一进程仅有一个实时间隔定时器,因此当它达到所说明的时间间隔时,它传送一个信号给一个地址空间。当需要此功能时,通过使用每一地址空间定时器,库例程可以实现多个每一线索定时器。每一LWP有两个私有间隔定时器;一个在LWP用户时间上递减,而另一个在LWP用户时间和系统代表LWP运行的时间上递减。当这些间隔定时器到期时,或者SIGVTALRM或者SIGPROF被适当地发送到拥有此时间间隔的LWP上。
允许对每一LWP独立地剖析。每一LWP可以设置分离的剖析缓冲区,但是如果需要累加信息,它也可以共享一个。在LWP用户事件上的每一次时钟滴答更新剖析信息。剖析的状态从创建LWP中继成。
资源限制在整个进程的资源利用率上设置限制(例如,对进程中的所有LWP的资源利用率求和)。当超过软资源限制,超过限制的LWP被发送一个合适的信号。可以通过getrusage()来对进程中所有LWP的资源利用率求和(包括CPU利用率)。
fork()系统调用试图复制已存在的UNIX语法。它复制地址空间和创建与原始一样的同样状态中的同一LWP。这会在原始进程中复制线索。当有任何LWP(线索)作出调用而不是调用fork()的LWP时,调用 fork()会引起中断系统调用,以返回EINTR。
新的系统调用,fork1(), 引起当前的线索/LWP派生,但是已存在于原始进程的线索和LWP不会在新进程中复制。fork1()定义如下:
int fork1();
返回值与fork()相似。
exit()和exec()系统调用二者都可正常工作,除非它们破坏了地址空间上的所有LWP。这两个调用阻塞,直到所有LWP(由此所有活动的线索)被消除。当exec()重建进程时,它创建一个单一LWP。进程启动代码,然后建立初始线索。
fork1()看上去有两个通常的用法:复制整个进程(BSD dump程序使用此技术),或者创建新进程以建立exec()。对于后个目的,fork1()更有效,因为不需要复制所有的LWP。然而使用fork1()有危险。首先,由于线索由线索库以数据结构的形式维护,线索库必须关心在fork1()后,仅有发出的线索保留在新的地址空间上,它是旧线索的一个复制。其次,程序员必须小心地调用函数,此函数不请求锁,此锁由已不再新进程中存在的线索保持。由于库能产生隐藏线索,这是很难决定的。最后,在可共享内存中(例如,带有MAP-SHARED标志的mmap())分配的锁,可以在进程中由线索保持,除非仔细考虑避免它。后个问题也可以由fork()引起。
使fork()完全复制进程,在语法上相似于单线索的fork()。对于程序员,它允许通常的使用,并且几乎没有陷阱。使用fork1(),仅派生一个线索,允许优化的fork()和立即的exec()(例如,system())。
通过priocntl()系统调用,LWP(和绑定线索)可以改变它们的调度类和类级别。对于“群”调度的一个新的调度类,可以获得以实现细粒度并行。LWP也可以要求绑定到一个CPU上,它依赖于调度类。
已扩充/proc文件系统,以反映进程模型的改变,此模型是由在进程级上附加的多线索而要求。必须地,一个核心进程模型接口能够提供仅访问核心支持的线索控制,即LWP。调式器控制库线索,是由调试器与线索库合作来完成,这需要/proc文件系统的帮助,以控制核心支持的LWP。
/proc文件系统的细节和一些对多线索支持的增强,可以在[Faulkner 1990]中找到。
Copyright: NPACT |