Posix线程编程指南(1)

Posix线程编程指南(1)

线程创建与取消

杨沙洲 (pubb@163.net),工程师, 自由撰稿人

杨沙洲,男,现攻读国防科大计算机学院计算机软件方向博士学位。您可以通过电子邮件 pubb@163.net跟他联系。

简介: 这是一个关于Posix线程编程的专栏。作者在阐明概念的基础上,将向您详细讲述Posix线程库API。本文是第一篇将向您讲述线程的创建与取消。

标记本文!

发布日期: 2001 年10 月 01 日
级别: 初级
访问情况 6648 次浏览
建议: 0 (添加评论)

平均分 (共 15 个评分 )

线程创建

1.1 线程与进程

相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。在串行程序基础上引入线程和进程是为了提高程序的并发度,从而提高程序运行效率和响应时间。

线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移。

1.2 创建线程

POSIX通过pthread_create()函数创建线程,API定义如下:

int  pthread_create(pthread_t  *  thread, pthread_attr_t * attr,

void * (*start_routine)(void *), void * arg)

 

与fork()调用创建一个进程的方法不同,pthread_create()创建的线程并不具备与主线程(即调用pthread_create()的线程)同样的执行序列,而是使其运行start_routine(arg)函数。thread返回创建的线程ID,而attr是创建线程时设置的线程属性 (见下)。pthread_create()的返回值表示线程创建是否成功。尽管arg是void *类型的变量,但它同样可以作为任意类型的参数传给start_routine()函数;同时,start_routine()可以返回一个void *类型的返回值,而这个返回值也可以是其他类型,并由pthread_join()获取。

1.3 线程创建属性

pthread_create()中的attr参数是一个结构指针,结构中的元素分别对应着新线程的运行属性,主要包括以下几项:

__detachstate, 表示新线程是否与进程中其他线程脱离同步,如果置位则新线程不能用pthread_join()来同步,且在退出时自行释放所占用的资源。缺省为PTHREAD_CREATE_JOINABLE状态。这个属性也可以在线程创建并运行以后用pthread_detach()来设置,而一旦设置为 PTHREAD_CREATE_DETACH状态(不论是创建时设置还是运行时设置)则不能再恢复到PTHREAD_CREATE_JOINABLE状 态。

__schedpolicy,表示新线程的调度策略,主要包括SCHED_OTHER(正常、非实时)、 SCHED_RR(实时、轮转法)和SCHED_FIFO(实时、先入先出)三种,缺省为SCHED_OTHER,后两种调度策略仅对超级用户有效。运行 时可以用过pthread_setschedparam()来改变。

__schedparam,一个structsched_param结构,目前仅有一个sched_priority整型变量表示线程的运行优先级。这个参数仅当调度策略为实时(即 SCHED_RR或SCHED_FIFO)时才有效,并可以在运行时通过pthread_setschedparam()函数来改变,缺省为0。

__inheritsched, 有两种值可供选择:PTHREAD_EXPLICIT_SCHED和PTHREAD_INHERIT_SCHED,前者表示新线程使用显式指定调度策略和 调度参数(即attr中的值),而后者表示继承调用者线程的值。缺省为PTHREAD_EXPLICIT_SCHED。

__scope, 表示线程间竞争CPU的范围,也就是说线程优先级的有效范围。POSIX的标准中定义了两个值:PTHREAD_SCOPE_SYSTEM和 PTHREAD_SCOPE_PROCESS,前者表示与系统中所有线程一起竞争CPU时间,后者表示仅与同进程中的线程竞争CPU。目前 LinuxThreads仅实现了PTHREAD_SCOPE_SYSTEM一值。

pthread_attr_t结构中还有一些值,但不使用pthread_create()来设置。

为了设置这些属性,POSIX定义了一系列属性设置函数,包括pthread_attr_init()、pthread_attr_destroy()和与各个属性相关的pthread_attr_get---/pthread_attr_set---函数。

1.4 线程创建的Linux实现

我 们知道,Linux的线程实现是在核外进行的,核内提供的是创建进程的接口do_fork()。内核提供了两个系统调用__clone()和 fork(),最终都用不同的参数调用do_fork()核内API。当然,要想实现线程,没有核心对多进程(其实是轻量级进程)共享数据段的支持是不行的,因此,do_fork()提供了很多参数,包括CLONE_VM(共享内存空间)、CLONE_FS(共享文件系统信息)、 CLONE_FILES(共享文件描述符表)、CLONE_SIGHAND(共享信号句柄表)和CLONE_PID(共享进程ID,仅对核内进程,即0号 进程有效)。当使用fork系统调用时,内核调用do_fork()不使用任何共享属性,进程拥有独立的运行环境,而使用 pthread_create()来创建线程时,则最终设置了所有这些属性来调用__clone(),而这些参数又全部传给核内的do_fork(),从 而创建的"进程"拥有共享的运行环境,只有栈是独立的,由__clone()传入。

Linux线程在核内是以轻量级进程的形式 存在的,拥有独立的进程表项,而所有的创建、同步、删除等操作都在核外pthread库中进行。pthread库使用一个管理线程 (__pthread_manager(),每个进程独立且唯一)来管理线程的创建和终止,为线程分配线程ID,发送线程相关的信号(比如 Cancel),而主线程(pthread_create())的调用者则通过管道将请求信息传给管理线程。


回页首

线程取消

2.1 线程取消的定义

一般情况下,线程在其主体函数退出的时候会自动终止,但同时也可以因为接收到另一个线程发来的终止(取消)请求而强制终止。

2.2 线程取消的语义

线程取消的方法是向目标线程发Cancel信号,但如何处理Cancel信号则由目标线程自己决定,或者忽略、或者立即终止、或者继续运行至Cancelation-point(取消点),由不同的Cancelation状态决定。

线程接收到CANCEL信号的缺省处理(即pthread_create()创建线程的缺省状态)是继续运行至取消点,也就是说设置一个CANCELED状态,线程继续运行,只有运行至Cancelation-point的时候才会退出。

2.3 取消点

根 据POSIX标准,pthread_join()、pthread_testcancel()、pthread_cond_wait()、 pthread_cond_timedwait()、sem_wait()、sigwait()等函数以及read()、write()等会引起阻塞的系 统调用都是Cancelation-point,而其他pthread函数都不会引起Cancelation动作。但是pthread_cancel的手 册页声称,由于LinuxThread库与C库结合得不好,因而目前C库函数都不是Cancelation-point;但CANCEL信号会使线程从阻 塞的系统调用中退出,并置EINTR错误码,因此可以在需要作为Cancelation-point的系统调用前后调用pthread_testcancel(),从而达到POSIX标准所要求的目标,即如下代码段:

pthread_testcancel();

    retcode = read(fd, buffer, length);

    pthread_testcancel();

 

2.4 程序设计方面的考虑

如果线程处于无限循环中,且循环体内没有执行至取消点的必然路径,则线程无法由外部其他线程的取消请求而终止。因此在这样的循环体的必经路径上应该加入pthread_testcancel()调用。

2.5与线程取消相关的pthread函数

int pthread_cancel(pthread_t thread)
发送终止信号给thread线程,如果成功则返回0,否则为非0值。发送成功并不意味着thread会终止。

int pthread_setcancelstate(int state, int *oldstate)
设 置本线程对Cancel信号的反应,state有两种值:PTHREAD_CANCEL_ENABLE(缺省)和 PTHREAD_CANCEL_DISABLE,分别表示收到信号后设为CANCLED状态和忽略CANCEL信号继续运行;old_state如果不为 NULL则存入原来的Cancel状态以便恢复。

int pthread_setcanceltype(int type, int *oldtype)
设 置本线程取消动作的执行时机,type由两种取值:PTHREAD_CANCEL_DEFFERED和PTHREAD_CANCEL_ASYCHRONOUS,仅当Cancel状态为Enable时有效,分别表示收到信号后继续运行至下一个取消点再退出和 立即执行取消动作(退出);oldtype如果不为NULL则存入运来的取消动作类型值。

void pthread_testcancel(void)
检查本线程是否处于Canceld状态,如果是,则进行取消动作,否则直接返回。

Posix线程编程指南(2)

线程私有数据

杨沙洲 (pubb@163.net),工程师, 自由撰稿人

杨沙洲,男,现攻读国防科大计算机学院计算机软件方向博士学位。您可以通过电子邮件 pubb@163.net跟他联系。

简介: 这是一个关于Posix线程编程的专栏。作者在阐明概念的基础上,将向您详细讲述Posix线程库API。本文是第二篇将向您讲述线程的私有数据。

本文的标签:  代码库

标记本文!

发布日期: 2001 年10 月 01 日
级别: 初级
访问情况 3864 次浏览
建议: 0 (添加评论)

平均分 (共 6 个评分 )

概念及作用

在单线程程序中,我们经常要用到"全局变量"以实现多个函数间共享数据。在多线程环境下,由于数据空间是共享的,因此全局变量也为所有线程所共有。但有时应用程序设计中有必要提供线程私有的全局变量,仅在某个线程中有效,但却可以跨多个函数访问,比如程序可能需要每个线程维护一个链表,而使用相同的函数操 作,最简单的办法就是使用同名而不同变量地址的线程相关数据结构。这样的数据结构可以由Posix线程库维护,称为线程私有数据(Thread- specific Data,或TSD)。


回页首

创建和注销

Posix定义了两个API分别用来创建和注销TSD:

int pthread_key_create(pthread_key_t *key, void (*destr_function) (void *))

 

该函数从TSD池中分配一项,将其值赋给key供以后访问使用。如果destr_function不为空,在线程退出(pthread_exit())时将以key所关联的数据为参数调用destr_function(),以释放分配的缓冲区。

不论哪个线程调用pthread_key_create(),所创建的key都是所有线程可访问的,但各个线程可根据自己的需要往key中填入不同的值,这就相当于提供了一个同名而不同值的全局变量。在LinuxThreads的实现中,TSD池用一个结构数组表示:

static struct pthread_key_struct pthread_keys[PTHREAD_KEYS_MAX] = { { 0, NULL } };

 

创建一个TSD就相当于将结构数组中的某一项设置为"in_use",并将其索引返回给*key,然后设置destructor函数为destr_function。

注销一个TSD采用如下API:

int pthread_key_delete(pthread_key_t key)

 

这个函数并不检查当前是否有线程正使用该TSD,也不会调用清理函数(destr_function),而只是将TSD释放以供下一次调用 pthread_key_create()使用。在LinuxThreads中,它还会将与之相关的线程数据项设为NULL(见"访问")。


回页首

访问

TSD的读写都通过专门的Posix Thread函数进行,其API定义如下:

int  pthread_setspecific(pthread_key_t  key,  const   void  *pointer)

void * pthread_getspecific(pthread_key_t key)

 

写入(pthread_setspecific())时,将pointer的值(不是所指的内容)与key相关联,而相应的读出函数则将与key相关联的数据读出来。数据类型都设为void *,因此可以指向任何类型的数据。

在LinuxThreads中,使用了一个位于线程描述结构(_pthread_descr_struct)中的二维void *指针数组来存放与key关联的数据,数组大小由以下几个宏来说明:

#define PTHREAD_KEY_2NDLEVEL_SIZE       32

#define PTHREAD_KEY_1STLEVEL_SIZE   \

((PTHREAD_KEYS_MAX + PTHREAD_KEY_2NDLEVEL_SIZE - 1)

/ PTHREAD_KEY_2NDLEVEL_SIZE)

    其中在/usr/include/bits/local_lim.h中定义了PTHREAD_KEYS_MAX为1024,

    因此一维数组大小为32。而具体存放的位置由key值经过以下计算得到:

idx1st = key / PTHREAD_KEY_2NDLEVEL_SIZE

idx2nd = key % PTHREAD_KEY_2NDLEVEL_SIZE

 

也就是说,数据存放与一个32×32的稀疏矩阵中。同样,访问的时候也由key值经过类似计算得到数据所在位置索引,再取出其中内容返回。


回页首

使用范例

以下这个例子没有什么实际意义,只是说明如何使用,以及能够使用这一机制达到存储线程私有数据的目的。

#include <stdio.h>

#include <pthread.h>

pthread_key_t   key;

void echomsg(int t)

{

        printf("destructor excuted in thread %d,param=%d\n",pthread_self(),t);

}

void * child1(void *arg)

{

        int tid=pthread_self();

        printf("thread %d enter\n",tid);

        pthread_setspecific(key,(void *)tid);

        sleep(2);

        printf("thread %d returns %d\n",tid,pthread_getspecific(key));

        sleep(5);

}

void * child2(void *arg)

{

        int tid=pthread_self();

        printf("thread %d enter\n",tid);

        pthread_setspecific(key,(void *)tid);

        sleep(1);

        printf("thread %d returns %d\n",tid,pthread_getspecific(key));

        sleep(5);

}

int main(void)

{

        int tid1,tid2;

        printf("hello\n");

        pthread_key_create(&key,echomsg);

        pthread_create(&tid1,NULL,child1,NULL);

        pthread_create(&tid2,NULL,child2,NULL);

        sleep(10);

        pthread_key_delete(key);

        printf("main thread exit\n");

        return 0;

}

 

给例程创建两个线程分别设置同一个线程私有数据为自己的线程ID,为了检验其私有性,程序错开了两个线程私有数据的写入和读出的时间,从程序运行结果可以看出,两个线程对TSD的修改互不干扰。同时,当线程退出时,清理函数会自动执行,参数为tid。

Posix线程编程指南(3)

线程同步

杨沙洲 (pubb@163.net),工程师, 自由撰稿人

杨沙洲,男,现攻读国防科大计算机学院计算机软件方向博士学位。您可以通过电子邮件 pubb@163.net跟他联系。

简介: 这是一个关于Posix线程编程的专栏。作者在阐明概念的基础上,将向您详细讲述Posix线程库API。本文是第三篇将向您讲述线程同步。

本文的标签:  代码库

标记本文!

发布日期: 2001 年10 月 01 日
级别: 初级
访问情况 6826 次浏览
建议: 0 (添加评论)

平均分 (共 12 个评分 )

互斥锁

尽管在Posix Thread中同样可以使用IPC的信号量机制来实现互斥锁mutex功能,但显然semphore的功能过于强大了,在Posix Thread中定义了另外一套专门用于线程同步的mutex函数。

1. 创建和销毁

有两种方法创建互斥锁,静态方式和动态方式。POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁,方法如下: pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; 在LinuxThreads实现中,pthread_mutex_t是一个结构,而PTHREAD_MUTEX_INITIALIZER则是一个结构常量。

动态方式是采用pthread_mutex_init()函数来初始化互斥锁,API定义如下: int pthread_mutex_init(pthread_mutex_t *mutex, constpthread_mutexattr_t *mutexattr) 其中mutexattr用于指定互斥锁属性(见下),如果为NULL则使用缺省属性。

pthread_mutex_destroy()用于注销一个互斥锁,API定义如下: int pthread_mutex_destroy(pthread_mutex_t *mutex) 销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在Linux中,互斥锁并不占用任何资源,因此LinuxThreads中的pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。

2. 互斥锁属性

互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。当前(glibc2.2.3,linuxthreads0.9)有四个值可供选择:

  • PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
  • PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
  • PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
  • PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。

3. 锁操作

锁操作主要包括加锁pthread_mutex_lock()、解锁pthread_mutex_unlock()和测试加锁 pthread_mutex_trylock()三个,不论哪种类型的锁,都不可能被两个不同的线程同时得到,而必须等待解锁。对于普通锁和适应锁类型,解锁者可以是同进程内任何线程;而检错锁则必须由加锁者解锁才有效,否则返回EPERM;对于嵌套锁,文档和实现要求必须由加锁者解锁,但实验结果表明并没有这种限制,这个不同目前还没有得到解释。在同一进程中的线程,如果加锁后没有解锁,则任何其他线程都无法再获得锁。

int pthread_mutex_lock(pthread_mutex_t *mutex)

int pthread_mutex_unlock(pthread_mutex_t *mutex)

int pthread_mutex_trylock(pthread_mutex_t *mutex)

 

pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待。

4. 其他

POSIX线程锁机制的Linux实现都不是取消点,因此,延迟取消类型的线程不会因收到取消信号而离开加锁等待。值得注意的是,如果线程在加锁后解锁前被取消,锁将永远保持锁定状态,因此如果在关键区段内有取消点存在,或者设置了异步取消类型,则必须在退出回调函数中解锁。

这个锁机制同时也不是异步信号安全的,也就是说,不应该在信号处理过程中使用互斥锁,否则容易造成死锁。


回页首

条件变量

条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

1. 创建和注销

条件变量和互斥锁一样,都有静态动态两种创建方式,静态方式使用PTHREAD_COND_INITIALIZER常量,如下:
pthread_cond_t cond=PTHREAD_COND_INITIALIZER

动态方式调用pthread_cond_init()函数,API定义如下:
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr)

尽管POSIX标准中为条件变量定义了属性,但在LinuxThreads中没有实现,因此cond_attr值通常为NULL,且被忽略。

注销一个条件变量需要调用pthread_cond_destroy(),只有在没有线程在该条件变量上等待的时候才能注销这个条件变量,否则返回EBUSY。因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。API定义如下:
int pthread_cond_destroy(pthread_cond_t *cond)

2. 等待和激发

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t

*mutex, const struct timespec *abstime)

 

等待条件有两种方式:无条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait(),其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待,其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林 尼治时间1970年1月1日0时0分0秒。

无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait(),下同)的竞争条件(Race Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开 pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。

激发条件有两种形式,pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程。

3. 其他

pthread_cond_wait()和pthread_cond_timedwait()都被实现为取消点,因此,在该处等待的线程将立即重新运行,在重新锁定mutex后离开pthread_cond_wait(),然后执行取消动作。也就是说如果 pthread_cond_wait()被取消,mutex是保持锁定状态的,因而需要定义退出回调函数来为其解锁。

以下示例集中演示了互斥锁和条件变量的结合使用,以及取消对于条件等待动作的影响。在例子中,有两个线程被启动,并等待同一个条件变量,如果不使用退出回调函数(见范例中的注释部分),则tid2将在pthread_mutex_lock()处永久等待。如果使用回调函数,则tid2的条件等待及主线程的 条件激发都能正常工作。

#include <stdio.h>

#include <pthread.h>

#include <unistd.h>

pthread_mutex_t mutex;

pthread_cond_t  cond;

void * child1(void *arg)

{

        pthread_cleanup_push(pthread_mutex_unlock,&mutex);  /* comment 1 */

        while(1){

                printf("thread 1 get running \n");

        printf("thread 1 pthread_mutex_lock returns %d\n",

pthread_mutex_lock(&mutex));

        pthread_cond_wait(&cond,&mutex);

                    printf("thread 1 condition applied\n");

        pthread_mutex_unlock(&mutex);

                    sleep(5);

    }

        pthread_cleanup_pop(0);     /* comment 2 */

}

void *child2(void *arg)

{

        while(1){

                sleep(3);               /* comment 3 */

                printf("thread 2 get running.\n");

        printf("thread 2 pthread_mutex_lock returns %d\n",

pthread_mutex_lock(&mutex));

        pthread_cond_wait(&cond,&mutex);

        printf("thread 2 condition applied\n");

        pthread_mutex_unlock(&mutex);

        sleep(1);

        }

}

int main(void)

{

        int tid1,tid2;

        printf("hello, condition variable test\n");

        pthread_mutex_init(&mutex,NULL);

        pthread_cond_init(&cond,NULL);

        pthread_create(&tid1,NULL,child1,NULL);

        pthread_create(&tid2,NULL,child2,NULL);

        do{

        sleep(2);                   /* comment 4 */

                pthread_cancel(tid1);       /* comment 5 */

                sleep(2);                   /* comment 6 */

        pthread_cond_signal(&cond);

    }while(1); 

        sleep(100);

        pthread_exit(0);

}

 

如果不做注释5的pthread_cancel()动作,即使没有那些sleep()延时操作,child1和child2都能正常工作。注释3和注释4 的延迟使得child1有时间完成取消动作,从而使child2能在child1退出之后进入请求锁操作。如果没有注释1和注释2的回调函数定义,系统将 挂起在child2请求锁的地方;而如果同时也不做注释3和注释4的延时,child2能在child1完成取消动作以前得到控制,从而顺利执行申请锁的 操作,但却可能挂起在pthread_cond_wait()中,因为其中也有申请mutex的操作。child1函数给出的是标准的条件变量的使用方 式:回调函数保护,等待条件前锁定,pthread_cond_wait()返回后解锁。

条件变量机制不是异步信号安全的,也就是说,在信号处理函数中调用pthread_cond_signal()或者pthread_cond_broadcast()很可能引起死锁。


回页首

信号灯

信号灯与互斥锁和条件变量的主要不同在于"灯"的概念,灯亮则意味着资源可用,灯灭则意味着不可用。如果说后两中同步方式侧重于"等待"操作,即资源不可 用的话,信号灯机制则侧重于点灯,即告知资源可用;没有等待线程的解锁或激发条件都是没有意义的,而没有等待灯亮的线程的点灯操作则有效,且能保持灯亮状态。当然,这样的操作原语也意味着更多的开销。

信号灯的应用除了灯亮/灯灭这种二元灯以外,也可以采用大于1的灯数,以表示资源数大于1,这时可以称之为多元灯。

1. 创建和注销

POSIX信号灯标准定义了有名信号灯和无名信号灯两种,但LinuxThreads的实现仅有无名灯,同时有名灯除了总是可用于多进程之间以外,在使用上与无名灯并没有很大的区别,因此下面仅就无名灯进行讨论。

int sem_init(sem_t *sem, int pshared, unsigned int value)
这是创建信号灯的API,其中value为信号灯的初值,pshared表示是否为多进程共享而不仅仅是用于一个进程。LinuxThreads没有实现多进程共享信号灯,因此所有非0值的pshared输入都将使sem_init()返回-1,且置errno为ENOSYS。初始化好的信号灯由sem变 量表征,用于以下点灯、灭灯操作。

int sem_destroy(sem_t * sem)
被注销的信号灯sem要求已没有线程在等待该信号灯,否则返回-1,且置errno为EBUSY。除此之外,LinuxThreads的信号灯注销函数不做其他动作。

2. 点灯和灭灯

int sem_post(sem_t * sem)

 

点灯操作将信号灯值原子地加1,表示增加一个可访问的资源。

int sem_wait(sem_t * sem)

int sem_trywait(sem_t * sem)

 

sem_wait()为等待灯亮操作,等待灯亮(信号灯值大于0),然后将信号灯原子地减1,并返回。sem_trywait()为sem_wait()的非阻塞版,如果信号灯计数大于0,则原子地减1并返回0,否则立即返回-1,errno置为EAGAIN。

3. 获取灯值

int sem_getvalue(sem_t * sem, int * sval)

 

读取sem中的灯计数,存于*sval中,并返回0。

4. 其他

sem_wait()被实现为取消点,而且在支持原子"比较且交换"指令的体系结构上,sem_post()是唯一能用于异步信号处理函数的POSIX异步信号安全的API。


回页首

异步信号

由于LinuxThreads是在核外使用核内轻量级进程实现的线程,所以基于内核的异步信号操作对于线程也是有效的。但同时,由于异步信号总是实际发往某个进程,所以无法实现POSIX标准所要求的"信号到达某个进程,然后再由该进程将信号分发到所有没有阻塞该信号的线程中"原语,而是只能影响到其中一 个线程。

POSIX异步信号同时也是一个标准C库提供的功能,主要包括信号集管理(sigemptyset()、sigfillset()、 sigaddset()、sigdelset()、sigismember()等)、信号处理函数安装(sigaction())、信号阻塞控制(sigprocmask())、被阻塞信号查询(sigpending())、信号等待(sigsuspend())等,它们与发送信号的kill() 等函数配合就能实现进程间异步信号功能。LinuxThreads围绕线程封装了sigaction()何raise(),本节集中讨论 LinuxThreads中扩展的异步信号函数,包括pthread_sigmask()、pthread_kill()和sigwait()三个函数。 毫无疑问,所有POSIX异步信号函数对于线程都是可用的。

int pthread_sigmask(int how, const sigset_t *newmask, sigset_t *oldmask)
设置线程的信号屏蔽码,语义与sigprocmask()相同,但对不允许屏蔽的Cancel信号和不允许响应的Restart信号进行了保护。被屏蔽的信号保存在信号队列中,可由sigpending()函数取出。

int pthread_kill(pthread_t thread, int signo)
向thread号线程发送signo信号。实现中在通过thread线程号定位到对应进程号以后使用kill()系统调用完成发送。

int sigwait(const sigset_t *set, int *sig)
挂起线程,等待set中指定的信号之一到达,并将到达的信号存入*sig中。POSIX标准建议在调用sigwait()等待信号以前,进程中所有线程都应屏蔽该信号,以保证仅有sigwait()的调用者获得该信号,因此,对于需要等待同步的异步信号,总是应该在创建任何线程以前调用 pthread_sigmask()屏蔽该信号的处理。而且,调用sigwait()期间,原来附接在该信号上的信号处理函数不会被调用。

如果在等待期间接收到Cancel信号,则立即退出等待,也就是说sigwait()被实现为取消点。


回页首

其他同步方式

除了上述讨论的同步方式以外,其他很多进程间通信手段对于LinuxThreads也是可用的,比如基于文件系统的IPC(管道、Unix域Socket等)、消息队列(Sys.V或者Posix的)、System V的信号灯等。只有一点需要注意,LinuxThreads在核内是作为共享存储区、共享文件系统属性、共享信号处理、共享文件描述符的独立进程看待的。

Posix线程编程指南(4)

线程终止

杨沙洲 (pubb@163.net),工程师, 自由撰稿人

杨沙洲,男,现攻读国防科大计算机学院计算机软件方向博士学位。您可以通过电子邮件 pubb@163.net跟他联系。

简介: 这是一个关于Posix线程编程的专栏。作者在阐明概念的基础上,将向您详细讲述Posix线程库API。本文是第四篇将向您讲述线程中止。

标记本文!

发布日期: 2001 年11 月 01 日
级别: 初级
访问情况 4726 次浏览
建议: 1 (查看或添加评论)

平均分 (共 9 个评分 )

线程终止方式

一般来说,Posix的线程终止有两种情况:正常终止和非正常终止。线程主动调用pthread_exit()或者从线程函数中return都将使线程正 常退出,这是可预见的退出方式;非正常终止是线程在其他线程的干预下,或者由于自身运行出错(比如访问非法地址)而退出,这种退出方式是不可预见的。


回页首

线程终止时的清理

不论是可预见的线程终止还是异常终止,都会存在资源释放的问题,在不考虑因运行出错而退出的前提下,如何保证线程终止时能顺利的释放掉自己所占用的资源,特别是锁资源,就是一个必须考虑解决的问题。

最经常出现的情形是资源独占锁的使用:线程为了访问临界资源而为其加上锁,但在访问过程中被外界取消,如果线程处于响应取消状态,且采用异步方式响应,或者在打开独占锁以前的运行路径上存在取消点,则该临界资源将永远处于锁定状态得不到释放。外界取消操作是不可预见的,因此的确需要一个机制来简化用于资源 释放的编程。

在POSIX线程API中提供了一个pthread_cleanup_push()/pthread_cleanup_pop()函数对用于自动释放资源 --从pthread_cleanup_push()的调用点到pthread_cleanup_pop()之间的程序段中的终止动作(包括调用pthread_exit()和取消点终止)都将执行pthread_cleanup_push()所指定的清理函数。API定义如下:

void pthread_cleanup_push(void (*routine) (void  *),  void *arg)

void pthread_cleanup_pop(int execute)

 

pthread_cleanup_push()/pthread_cleanup_pop()采用先入后出的栈结构管理,void routine(void *arg)函数在调用pthread_cleanup_push()时压入清理函数栈,多次对pthread_cleanup_push()的调用将在清 理函数栈中形成一个函数链,在执行该函数链时按照压栈的相反顺序弹出。execute参数表示执行到pthread_cleanup_pop()时是否在弹出清理函数的同时执行该函数,为0表示不执行,非0为执行;这个参数并不影响异常终止时清理函数的执行。

pthread_cleanup_push()/pthread_cleanup_pop()是以宏方式实现的,这是pthread.h中的宏定义:

#define pthread_cleanup_push(routine,arg)                                     \

  { struct _pthread_cleanup_buffer _buffer;                                   \

    _pthread_cleanup_push (&_buffer, (routine), (arg));

#define pthread_cleanup_pop(execute)                                          \

    _pthread_cleanup_pop (&_buffer, (execute)); }

 

可见,pthread_cleanup_push()带有一个"{",而pthread_cleanup_pop()带有一个"}",因此这两个函数必须成对出现,且必须位于程序的同一级别的代码段中才能通过编译。在下面的例子里,当线程在"do some work"中终止时,将主动调用pthread_mutex_unlock(mut),以完成解锁动作。

pthread_cleanup_push(pthread_mutex_unlock, (void *) &mut);

pthread_mutex_lock(&mut);

/* do some work */

pthread_mutex_unlock(&mut);

pthread_cleanup_pop(0);

 

必须要注意的是,如果线程处于PTHREAD_CANCEL_ASYNCHRONOUS状态,上述代码段就有可能出错,因为CANCEL事件有可能在 pthread_cleanup_push()和pthread_mutex_lock()之间发生,或者在 pthread_mutex_unlock()和pthread_cleanup_pop()之间发生,从而导致清理函数unlock一个并没有加锁的 mutex变量,造成错误。因此,在使用清理函数的时候,都应该暂时设置成PTHREAD_CANCEL_DEFERRED模式。为此,POSIX的 Linux实现中还提供了一对不保证可移植的pthread_cleanup_push_defer_np()/pthread_cleanup_pop_defer_np()扩展函数,功能与以下代码段相当:

{ int oldtype;

 pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, &oldtype);

 pthread_cleanup_push(routine, arg);

 ...

 pthread_cleanup_pop(execute);

 pthread_setcanceltype(oldtype, NULL);

 }

 


回页首

线程终止的同步及其返回值

一般情况下,进程中各个线程的运行都是相互独立的,线程的终止并不会通知,也不会影响其他线程,终止的线程所占用的资源也并不会随着线程的终止而得到释放。正如进程之间可以用wait()系统调用来同步终止并释放资源一样,线程之间也有类似机制,那就是pthread_join()函数。

void pthread_exit(void *retval)

int pthread_join(pthread_t th, void **thread_return)

int pthread_detach(pthread_t th)

 

pthread_join()的调用者将挂起并等待th线程终止,retval是pthread_exit()调用者线程(线程ID为th)的返回值,如 果thread_return不为NULL,则*thread_return=retval。需要注意的是一个线程仅允许唯一的一个线程使用 pthread_join()等待它的终止,并且被等待的线程应该处于可join状态,即非DETACHED状态。

如果进程中的某个线程执行了pthread_detach(th),则th线程将处于DETACHED状态,这使得th线程在结束运行时自行释放所占用的 内存资源,同时也无法由pthread_join()同步,pthread_detach()执行之后,对th请求pthread_join()将返回错 误。

一个可join的线程所占用的内存仅当有线程对其执行了pthread_join()后才会释放,因此为了避免内存泄漏,所有线程的终止,要么已设为DETACHED,要么就需要使用pthread_join()来回收。


回页首

关于pthread_exit()和return

理论上说,pthread_exit()和线程宿体函数退出的功能是相同的,函数结束时会在内部自动调用pthread_exit()来清理线程相关的资源。但实际上二者由于编译器的处理有很大的不同。

在进程主函数(main())中调用pthread_exit(),只会使主函数所在的线程(可以说是进程的主线程)退出;而如果是return,编译器将使其调用进程退出的代码(如_exit()),从而导致进程及其所有线程结束运行。

其次,在线程宿主函数中主动调用return,如果return语句包含在pthread_cleanup_push()/pthread_cleanup_pop()对中,则不会引起清理函数的执行,反而会导致segment fault。

Posix线程编程指南(5)

杂项

杨沙洲 (pubb@163.net),工程师, 自由撰稿人

杨沙洲,男,现攻读国防科大计算机学院计算机软件方向博士学位。您可以通过电子邮件 pubb@163.net跟他联系。

简介: 这是一个关于Posix线程编程的专栏。作者在阐明概念的基础上,将向您详细讲述Posix线程库API。本文是第五篇将向您讲述pthread_self()、pthread_equal()和pthread_once()等杂项函数。

标记本文!

发布日期: 2001 年11 月 01 日
级别: 初级
访问情况 2909 次浏览
建议: 0 (添加评论)

平均分 (共 3 个评分 )

在Posix线程规范中还有几个辅助函数难以归类,暂且称其为杂项函数,主要包括pthread_self()、pthread_equal()和 pthread_once()三个,另外还有一个LinuxThreads非可移植性扩展函数 pthread_kill_other_threads_np()。本文就介绍这几个函数的定义和使用。

获得本线程ID

pthread_t pthread_self(void)

本函数返回本线程的标识符。

在LinuxThreads中,每个线程都用一个pthread_descr结构来描述,其中包含了线程状态、线程ID等所有需要的数据结构,此函数的实现就是在线程栈帧中找到本线程的pthread_descr结构,然后返回其中的p_tid项。

pthread_t类型在LinuxThreads中定义为无符号长整型。


回页首

判断两个线程是否为同一线程

int pthread_equal(pthread_t thread1, pthread_t thread2)

判断两个线程描述符是否指向同一线程。在LinuxThreads中,线程ID相同的线程必然是同一个线程,因此,这个函数的实现仅仅判断thread1和thread2是否相等。


回页首

仅执行一次的操作

int pthread_once(pthread_once_t *once_control, void (*init_routine)(void))

本函数使用初值为PTHREAD_ONCE_INIT的once_control变量保证init_routine()函数在本进程执行序列中仅执行一次。

#include <stdio.h>

#include <pthread.h>

pthread_once_t  once=PTHREAD_ONCE_INIT;

void    once_run(void)

{

        printf("once_run in thread %d\n",pthread_self());

}

void * child1(void *arg)

{

        int tid=pthread_self();

        printf("thread %d enter\n",tid);

        pthread_once(&once,once_run);

        printf("thread %d returns\n",tid);

}

void * child2(void *arg)

{

        int tid=pthread_self();

        printf("thread %d enter\n",tid);

        pthread_once(&once,once_run);

        printf("thread %d returns\n",tid);

}

int main(void)

{

        int tid1,tid2;

        printf("hello\n");

        pthread_create(&tid1,NULL,child1,NULL);

        pthread_create(&tid2,NULL,child2,NULL);

        sleep(10);

        printf("main thread exit\n");

        return 0;

}

 

once_run()函数仅执行一次,且究竟在哪个线程中执行是不定的,尽管pthread_once(&once,once_run)出现在两个线程中。

LinuxThreads使用互斥锁和条件变量保证由pthread_once()指定的函数执行且仅执行一次,而once_control则表征是否执 行过。如果once_control的初值不是PTHREAD_ONCE_INIT(LinuxThreads定义为0),pthread_once() 的行为就会不正常。在LinuxThreads中,实际"一次性函数"的执行状态有三种:NEVER(0)、IN_PROGRESS(1)、 DONE(2),如果once初值设为1,则由于所有pthread_once()都必须等待其中一个激发"已执行一次"信号,因此所有 pthread_once()都会陷入永久的等待中;如果设为2,则表示该函数已执行过一次,从而所有pthread_once()都会立即返回0。


回页首

pthread_kill_other_threads_np()

void pthread_kill_other_threads_np(void)

这个函数是LinuxThreads针对本身无法实现的POSIX约定而做的扩展。POSIX要求当进程的某一个线程执行exec*系统调用在进程空间中 加载另一个程序时,当前进程的所有线程都应终止。由于LinuxThreads的局限性,该机制无法在exec中实现,因此要求线程执行exec前手工终 止其他所有线程。pthread_kill_other_threads_np()的作用就是这个。

需要注意的是,pthread_kill_other_threads_np()并没有通过pthread_cancel()来终止线程,而是直接向管理线程发"进程退出"信号,使所有其他线程都结束运行,而不经过Cancel动作,当然也不会执行退出回调函数。尽管LinuxThreads的实验结果与文档说明相同,但代码实现中却是用的__pthread_sig_cancel信号来kill线程,应该效果与执行pthread_cancel()是一 样的,其中原因目前还不清楚。

Linux 的多线程编程的高效开发经验

杨 奕 (yangyish@cn.ibm.com),软件工程师, IBM

杨奕是 IBM 中国系统与技术实验室的软件工程师。他在上海交通大学电子工程系获得了硕士学位。他的工作主要是在各种操作系统平台上开发基与虚拟化技术的管理软件。

贺 皓 (haohe@cn.ibm.com),软件工程师, IBM

贺 皓是 IBM 中国系统与科技开发中心的软件工程师。他在复旦大学获得了计算机科学专业的学士与硕士学位。曾参加 IBM DS(DS 系列存储设备 ) Agent 的开发工作,他目前在 IBM SVC Agent 开发小组从事研发工作。你可以通过以下地址联系他:haohe@cn.ibm.com。

张 俊伟 (zhjunwei@cn.ibm.com),软件工程师, IBM

张 俊伟是 IBM 中国系统与技术部的软件工程师,他于 2005 年 3 月加入了 IBM,曾参加 IBM Storage Configuration Manager 项目的开发工作,他目前在 IBM DS (DS 系列存储设备 ) Agent 项目小组从事研发工作。

简介: 本文中我们针对 Linux上多线程编程的主要特性总结出 5 条经验,用以改善 Linux多线程编程的习惯和避免其中的开发陷阱。在本文中,我们穿插一些 Windows 的编程用例用以对比 Linux 特性,以加深读者印象。

本文的标签:  linux,linux多线程开发, thread,多线程, 的多线程编程的高效开发经验

标记本文!

发布日期: 2009 年4 月 23 日
级别: 中级
访问情况 10220 次浏览
建议: 0 (添加评论)

平均分 (共 20 个评分 )

背景

Linux 平台上的多线程程序开发相对应其他平台(比如 Windows)的多线程 API 有一些细微和隐晦的差别。不注意这些 Linux 上的一些开发陷阱,常常会导致程序问题不穷,死锁不断。本文中我们从 5 个方面总结出 Linux 多线程编程上的问题,并分别引出相关改善的开发经验,用以避免这些的陷阱。我们希望这些经验可以帮助读者们能更好更快的熟悉 Linux 平台的多线程编程。

我们假设读者都已经很熟悉 Linux 平台上基本的线程编程的 Pthread 库 API 。其他的第三方用以线程编程的库,如 boost,将不会在本文中提及。本文中主要涉及的题材包括线程开发中的线程管理,互斥变量,条件变量等。进程概念将不会在本文中涉及。


回页首

Linux 上线程开发 API 的概要介绍

多线程开发在 Linux 平台上已经有成熟的 Pthread 库支持。其涉及的多线程开发的最基本概念主要包含三点:线程,互斥锁,条件。其中,线程操作又分线程的创建,退出,等待 3 种。互斥锁则包括 4 种操作,分别是创建,销毁,加锁和解锁。条件操作有5 种操作:创建,销毁,触发,广播和等待。其他的一些线程扩展概念,如信号灯等,都可以通过上面的三个基本元素的基本操作封装出来。

线程,互斥锁,条件在 Linux 平台上对应的 API 可以用表 1 归纳。为了方便熟悉 Windows 线程编程的读者熟悉 Linux 多线程开发的 API,我们在表中同时也列出 Windows SDK 库中所对应的 API 名称。


表 1. 线程函数列表

对象

操作

Linux Pthread API

Windows SDK 库对应 API

线程

创建

pthread_create

CreateThread

退出

pthread_exit

ThreadExit

等待

pthread_join

WaitForSingleObject

互斥锁

创建

pthread_mutex_init

CreateMutex

销毁

pthread_mutex_destroy

CloseHandle

加锁

pthread_mutex_lock

WaitForSingleObject

解锁

pthread_mutex_unlock

ReleaseMutex

条件

创建

pthread_cond_init

CreateEvent

销毁

pthread_cond_destroy

CloseHandle

触发

pthread_cond_signal

SetEvent

广播

pthread_cond_broadcast

SetEvent / ResetEvent

等待

pthread_cond_wait / pthread_cond_timedwait

SingleObjectAndWait

 

多线程开发在 Linux 平台上已经有成熟的 Pthread 库支持。其涉及的多线程开发的最基本概念主要包含三点:线程,互斥锁,条件。其中,线程操作又分线程的创建,退出,等待 3 种。互斥锁则包括 4 种操作,分别是创建,销毁,加锁和解锁。条件操作有 5 种操作:创建,销毁,触发,广播和等待。其他的一些线程扩展概念,如信号灯等,都可以通过上面的三个基本元素的基本操作封装出来。


回页首

Linux 线程编程中的 5 条经验

尽量设置 recursive 属性以初始化 Linux 的互斥变量

互斥锁是多线程编程中基本的概念,在开发中被广泛使用。其调用次序层次清晰简单:建锁,加锁,解锁,销毁锁。但是需要注意的是,与诸如 Windows 平台的互斥变量不同,在默认情况下,Linux 下的同一线程无法对同一互斥锁进行递归加速,否则将发生死锁。

所谓递归加锁,就是在同一线程中试图对互斥锁进行两次或两次以上的行为。其场景在 Linux 平台上的代码可由清单 1 所示。


清单 1. Linux 重复对互斥锁加锁实例

// 通过默认条件建锁

    pthread_mutex_t *theMutex = new pthread_mutex_t;

    pthread_mutexattr_t attr;

    pthread_mutexattr_init(&attr);

    pthread_mutex_init(theMutex,&attr);

    pthread_mutexattr_destroy(&attr);

 

    // 递归加锁

    pthread_mutex_lock (theMutex);

    pthread_mutex_lock (theMutex);

    pthread_mutex_unlock (theMutex);

    pthread_mutex_unlock (theMutex);

 

在以上代码场景中,问题将出现在第二次加锁操作。由于在默认情况下,Linux 不允许同一线程递归加锁,因此在第二次加锁操作时线程将出现死锁。

Linux 互斥变量这种奇怪的行为或许对于特定的某些场景会所有用处,但是对于大多数情况下看起来更像是程序的一个 bug 。毕竟,在同一线程中对同一互斥锁进行递归加锁在尤其是二次开发中经常会需要。

这个问题与互斥锁的中的默认 recursive 属性有关。解决问题的方法就是显式地在互斥变量初始化时将设置起recursive 属性。基于此,以上代码其实稍作修改就可以很好的运行,只需要在初始化锁的时候加设置一个属性。请看清单 2 。


清单 2. 设置互斥锁 recursive 属性实例

pthread_mutexattr_init(&attr);

    // 设置 recursive 属性

    pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_RECURSIVE_NP);

    pthread_mutex_init(theMutex,&attr);

 

因此,建议尽量设置 recursive 属性以初始化 Linux 的互斥锁,这样既可以解决同一线程递归加锁的问题,又可以避免很多情况下死锁的发生。这样做还有一个额外的好处,就是可以让 Windows 和 Linux 下让锁的表现统一。

注意 Linux 平台上触发条件变量的自动复位问题

条件变量的置位和复位有两种常用模型:第一种模型是当条件变量置位(signaled)以后,如果当前没有线程在等待,其状态会保持为置位 (signaled),直到有等待的线程进入被触发,其状态才会变为复位(unsignaled),这种模型的采用以 Windows 平台上的 Auto-set Event 为代表。其状态变化如图 1 所示:


图 1. Windows 的条件变量状态变化流程

第二种模型则是 Linux 平台的 Pthread 所采用的模型,当条件变量置位(signaled)以后,即使当前没有任何线程在等待,其状态也会恢复为复位(unsignaled)状态。其状态变化如图 2 所示:


图 2. Linux 的条件变量状态变化流程

具体来说,Linux 平台上 Pthread 下的条件变量状态变化模型是这样工作的:调用 pthread_cond_signal() 释放被条件阻塞的线程时,无论存不存在被阻塞的线程,条件都将被重新复位,下一个被条件阻塞的线程将不受影响。而对于 Windows,当调用 SetEvent 触发 Auto-reset 的 Event 条件时,如果没有被条件阻塞的线程,那么条件将维持在触发状态,直到有新的线程被条件阻塞并被释放为止。

这种差异性对于那些熟悉 Windows 平台上的条件变量状态模型而要开发 Linux 平台上多线程的程序员来说可能会造成意想不到的尴尬结果。试想要实现一个旅客坐出租车的程序:旅客在路边等出租车,调用条件等待。出租车来了,将触发条件,旅客停止等待并上车。一个出租车只能搭载一波乘客,于是我们使用单一触发的条件变量。这个实现逻辑在第一个模型下即使出租车先到,也不会有什么问题, 其过程如图 3 所示:


图 3. 采用 Windows 条件变量模型的出租车实例流程

然而如果按照这个思路来在 Linux 上来实现,代码看起来可能是清单 3 这样。


清单 3. Linux 出租车案例代码实例

……

 // 提示出租车到达的条件变量

 pthread_cond_t taxiCond;

 

 // 同步锁

 pthread_mutex_t taxiMutex;

 

 // 旅客到达等待出租车

 void * traveler_arrive(void * name) {

    cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl;

    pthread_mutex_lock(&taxiMutex);

    pthread_cond_wait (&taxiCond, &taxtMutex);

    pthread_mutex_unlock (&taxtMutex);

    cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl;

    pthread_exit( (void *)0 );

 }

 

 // 出租车到达

 void * taxi_arrive(void *name) {

    cout<< ” Taxi ” <<(char *)name<< ” arrives. ” <<endl;

    pthread_cond_signal(&taxtCond);

    pthread_exit( (void *)0 );

 }

 

 void main() { 

    // 初始化

    taxtCond= PTHREAD_COND_INITIALIZER;

    taxtMutex= PTHREAD_MUTEX_INITIALIZER;

    pthread_t thread;

    pthread_attr_t threadAttr;

    pthread_attr_init(&threadAttr);

 

    pthread_create(&thread, & threadAttr, taxt_arrive, (void *)( ” Jack ” ));

    sleep(1);

    pthread_create(&thread, &threadAttr, traveler_arrive, (void *)( ” Susan ” ));

    sleep(1);

    pthread_create(&thread, &threadAttr, taxi_arrive, (void *)( ” Mike ” ));

    sleep(1);

 

    return 0;

 }

 

好的,运行一下,看看结果如清单 4 。


清单 4. 程序结果输出

Taxi Jack arrives.

    Traveler Susan needs a taxi now!

    Taxi Mike arrives.

    Traveler Susan now got a taxi.

 

其过程如图 4 所示:


图 4. 采用 Linux 条件变量模型的出租车实例流程

通过对比结果,你会发现同样的逻辑,在 Linux 平台上运行的结果却完全是两样。对于在 Windows 平台上的模型一, Jack 开着出租车到了站台,触发条件变量。如果没顾客,条件变量将维持触发状态,也就是说 Jack 停下车在那里等着。直到 Susan 小姐来了站台,执行等待条件来找出租车。 Susan 搭上 Jack 的出租车离开,同时条件变量被自动复位。

但是到了 Linux 平台,问题就来了,Jack 到了站台一看没人,触发的条件变量被直接复位,于是 Jack 排在等待队列里面。来迟一秒的 Susan 小姐到了站台却看不到在那里等待的 Jack,只能等待,直到 Mike 开车赶到,重新触发条件变量,Susan 才上了 Mike 的车。这对于在排队系统前面的 Jack 是不公平的,而问题症结是在于 Linux 平台上条件变量触发的自动复位引起的一个 Bug 。

条件变量在 Linux 平台上的这种模型很难说好坏。但是在实际开发中,我们可以对代码稍加改进就可以避免这种差异的发生。由于这种差异只发生在触发没有被线程等待在条件变量的时刻,因此我们只需要掌握好触发的时机即可。最简单的做法是增加一个计数器记录等待线程的个数,在决定触发条件变量前检查下该变量即可。改进后 Linux 函数如清单 5 所示。


清单 5. Linux 出租车案例代码实例

……

 // 提示出租车到达的条件变量

 pthread_cond_t taxiCond;

 

 // 同步锁

 pthread_mutex_t taxiMutex;

 

 // 旅客人数,初始为 0

 int travelerCount=0;

 

 // 旅客到达等待出租车

 void * traveler_arrive(void * name) {

    cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl;

    pthread_mutex_lock(&taxiMutex);

 

    // 提示旅客人数增加

    travelerCount++;

    pthread_cond_wait (&taxiCond, &taxiMutex);

    pthread_mutex_unlock (&taxiMutex);

    cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl;

    pthread_exit( (void *)0 );

 }

 

 // 出租车到达

 void * taxi_arrive(void *name)

 {

    cout<< ” Taxi ” <<(char *)name<< ” arrives. ” <<endl;

 

 while(true)

 {

        pthread_mutex_lock(&taxiMutex);

 

        // 当发现已经有旅客在等待时,才触发条件变量

        if(travelerCount>0)

        {

            pthread_cond_signal(&taxtCond);

            pthread_mutex_unlock (&taxiMutex);

            break;

        }

        pthread_mutex_unlock (&taxiMutex);

    }

 

    pthread_exit( (void *)0 );

 }

 

因此我们建议在 Linux 平台上要出发条件变量之前要检查是否有等待的线程,只有当有线程在等待时才对条件变量进行触发。

注意条件返回时互斥锁的解锁问题

在Linux 调用 pthread_cond_wait 进行条件变量等待操作时,我们增加一个互斥变量参数是必要的,这是为了避免线程间的竞争和饥饿情况。但是当条件等待返回时候,需要注意的是一定不要遗漏对互斥变量进行解锁。

Linux 平台上的 pthread_cond_wait(pthread_cond_t*cond, pthread_mutex_t *mutex) 函数返回时,互斥锁 mutex 将处于锁定状态。因此之后如果需要对临界区数据进行重新访问,则没有必要对 mutex 就行重新加锁。但是,随之而来的问题是,每次条件等待以后需要加入一步手动的解锁操作。正如前文中乘客等待出租车的 Linux 代码如清单 6 所示:


清单 6. 条件变量返回后的解锁实例

void * traveler_arrive(void * name) {

    cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl;

    pthread_mutex_lock(&taxiMutex);

    pthread_cond_wait (&taxiCond, &taxtMutex);

    pthread_mutex_unlock (&taxtMutex);

    cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl;

    pthread_exit( (void *)0 );

 }

 

这一点对于熟悉 Windows 平台多线程开发的开发者来说尤为重要。 Windows 上的 SignalObjectAndWait() 函数是常与 Linux 平台上的 pthread_cond_wait() 函数被看作是跨平台编程时的一对等价函数。但是需要注意的是,两个函数退出时的状态是不一样的。在 Windows 平台上,SignalObjectAndWait(HANDLE a,HANDLE b, …… ) 方法在调用结束返回时的状态是 a 和 b 都是置位(signaled)状态,在普遍的使用方法中,a 经常是一个 Mutex 变量,在这种情况下,当返回时,Mutex a 处于解锁状态(signaled),Event b 处于置位状态(signaled), 因此,对于 Mutex a 而言,我们不需要考虑解锁的问题。而且,在 SignalObjectAndWait() 之后,如果需要对临界区数据进行重新访问,都需要调用 WaitForSingleObject() 重新加锁。这一点刚好与 Linux 下的 pthread_cond_wait() 完全相反。

Linux 对于 Windows 的这一点额外解锁的操作区别很重要,一定得牢记。否则从 Windows 移植到 Linux 上的条件等待操作一旦忘了结束后的解锁操作,程序将肯定会发生死锁。

等待的绝对时间问题

超时是多线程编程中一个常见的概念。例如,当你在 Linux 平台下使用 pthread_cond_timedwait() 时就需要指定超时这个参数,以便这个 API 的调用者最多只被阻塞指定的时间间隔。但是如果你是第一次使用这个 API 时,首先你需要了解的就是这个 API 当中超时参数的特殊性(就如本节标题所提示的那样)。我们首先来看一下这个 API的定义。 pthread_cond_timedwait() 定义请看清单 7 。


清单 7.pthread_cond_timedwait() 函数定义

int pthread_cond_timedwait(pthread_cond_t *restrict cond,

              pthread_mutex_t *restrict mutex,

              const struct timespec *restrict abstime);

 

参数 abstime 在这里用来表示和超时时间相关的一个参数,但是需要注意的是它所表示的是一个绝对时间,而不是一个时间间隔数值,只有当系统的当前时间达到或者超过 abstime 所表示的时间时,才会触发超时事件。这对于拥有 Windows 平台线程开发经验的人来说可能尤为困惑。因为 Windows 平台下所有的 API 等待参数(如 SignalObjectAndWait,等)都是相对时间,

假设我们指定相对的超时时间参数如 dwMilliseconds (单位毫秒)来调用和超时相关的函数,这样就需要将dwMilliseconds 转化为 Linux 下的绝对时间参数 abstime 使用。常用的转换方法如清单 8 所示:


清单 8. 相对时间到绝对时间转换实例

/* get the current time */

    struct timeval now;

    gettimeofday(&now, NULL);

       

    /* add the offset to get timeout value */

    abstime ->tv_nsec = now.tv_usec * 1000 + (dwMilliseconds % 1000) * 1000000;

    abstime ->tv_sec = now.tv_sec + dwMilliseconds / 1000;

 

Linux 的绝对时间看似简单明了,却是开发中一个非常隐晦的陷阱。而且一旦你忘了时间转换,可以想象,等待你的错误将是多么的令人头疼:如果忘了把相对时间转换成绝对时间,相当于你告诉系统你所等待的超时时间是过去式的 1970 年 1 月 1 号某个时间段,于是操作系统毫不犹豫马上送给你一个 timeout 的返回值,然后你会举着拳头抱怨为什么另外一个同步线程耗时居然如此之久,并一头扎进寻找耗时原因的深渊里。

正确处理 Linux 平台下的线程结束问题

在Linux 平台下,当处理线程结束时需要注意的一个问题就是如何让一个线程善始善终,让其所占资源得到正确释放。在Linux 平台默认情况下,虽然各个线程之间是相互独立的,一个线程的终止不会去通知或影响其他的线程。但是已经终止的线程的资源并不会随着线程的终止而得到释放,我们需要调用 pthread_join() 来获得另一个线程的终止状态并且释放该线程所占的资源。 Pthread_join() 函数的定义如清单 9 。


清单 9.pthread_join 函数定义

int pthread_join(pthread_t th, void **thread_return);

 

调用该函数的线程将挂起,等待 th 所表示的线程的结束。 thread_return 是指向线程 th 返回值的指针。需要注意的是 th 所表示的线程必须是 joinable 的,即处于非 detached(游离)状态;并且只可以有唯一的一个线程对 th 调用 pthread_join() 。如果 th 处于 detached 状态,那么对 th 的 pthread_join() 调用将返回错误。

如果你压根儿不关心一个线程的结束状态,那么也可以将一个线程设置为 detached 状态,从而来让操作系统在该线程结束时来回收它所占的资源。将一个线程设置为 detached 状态可以通过两种方式来实现。一种是调用pthread_detach() 函数,可以将线程 th 设置为 detached 状态。其申明如清单 10 。


清单 10.pthread_detach 函数定义

int pthread_detach(pthread_t th);

 

另一种方法是在创建线程时就将它设置为 detached 状态,首先初始化一个线程属性变量,然后将其设置为 detached 状态,最后将它作为参数传入线程创建函数 pthread_create(),这样所创建出来的线程就直接处于 detached 状态。方法如清单 11 。


清单 11. 创建 detach 线程代码实例

………………………………… ..

    pthread_t       tid;

    pthread_attr_t  attr;

    pthread_attr_init(&attr);

    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    pthread_create(&tid, &attr, THREAD_FUNCTION, arg);

 

总之为了在使用 Pthread 时避免线程的资源在线程结束时不能得到正确释放,从而避免产生潜在的内存泄漏问题,在对待线程结束时,要确保该线程处于 detached 状态,否着就需要调用 pthread_join() 函数来对其进行资源回收。


回页首

总结与补充

本文以上部分详细介绍了 Linux 的多线程编程的 5 条高效开发经验。另外你也可以考虑尝试其他一些开源类库来进行线程开发。

1. Boost 库

Boost 库来自于由 C++ 标准委员会类库工作组成员发起,致力于为 C++ 开发新的类库的 Boost 组织。虽然该库本身并不是针对多线程而产生,但是发展至今,其已提供了比较全面的多线程编程的 API 支持。 Boost 库对于多线程支持的 API 风格上更类似于 Linux 的Pthread 库,差别在于其将线程,互斥锁,条件等线程开发概念都封装成了 C++ 类,以方便开发调用。 Boost 库目前对跨平台支持的很不错,不仅支持 Windows 和 Linux ,还支持各种商用的 Unix 版本。如果开发者想使用高稳定性的统一线程编程接口减轻跨平台开发的难度, Boost 库将是首选。

2. ACE

ACE 全称是 ADAPTIVE CommunicationEnvironment,它是一个免费的,开源的,面向对象的工具框架,用以开发并发访问的软件。由于 ACE 最初是面向网络服务端的编程开发,因此对于线程开发的工具库它也能提供很全面的支持。其支持的平台也很全面,包括 Windows,Linux 和各种版本Unix 。 ACE 的唯一问题是如果仅仅是用于线程编程,其似乎显得有些过于重量级。而且其较复杂的配置也让其部署对初学者而言并非易事。

 

参考资料

Linux 多线程应用中如何编写安全的信号处理函数

周 婷 (zhouting@cn.ibm.com),软件工程师, IBM 中国软件开发技术实验室

周婷,软件工程师,在上海交通大学获得自动控制专业学士和硕士学位。主要从事 Linux OS 下的开发工作,工作领域包括视频解码,IPV6, 网络安全, Web 开发。

刘 坚 (liujsh@cn.ibm.com),软件工程师, EMC

刘坚,Linux 爱好者,软件工程师,在上海交通大学获得学士和硕士学位。主要从事 Linux OS 下的开发工作,熟悉网络, Linux 内核。

唐 桂峰 (tangguif@cn.ibm.com),资深软件工程师, IBM

唐桂峰,资深软件工程师,在南京大学获得计算机专业学士和硕士学位。研究方向包括人工智能,网络。

简介: 关于代码的可重入性,设计开发人员一般只考虑到线程安全,异步信号处理函数的安全却往往被忽略。本文首先介绍如何编写安全的异步信号处理函数;然后举例说明在多线程应用中如何构建模型让异步信号在指定的线程中以同步的方式处理。

标记本文!

发布日期: 2009 年6 月 18 日
级别: 中级
访问情况 4737 次浏览
建议: 0 (添加评论)

平均分 (共 5 个评分 )

Linux 多线程应用中编写安全的信号处理函数

在开发多线程应用时,开发人员一般都会考虑线程安全,会使用 pthread_mutex 去保护全局变量。如果应用中使用了信号,而且信号的产生不是因为程序运行出错,而是程序逻辑需要,譬如 SIGUSR1、SIGRTMIN 等,信号在被处理后应用程序还将正常运行。在编写这类信号处理函数时,应用层面的开发人员却往往忽略了信号处理函数执行的上下文背景,没有考虑编写安全的信号处理函数的一些规则。本文首先介绍编写信号处理函数时需要考虑的一些规则;然后举例说明在多线程应用中如何构建模型让因为程序逻辑需要而产生的异步信 号在指定的线程中以同步的方式处理。


回页首

线程和信号

Linux 多线程应用中,每个线程可以通过调用 pthread_sigmask() 设置本线程的信号掩码。一般情况下,被阻塞的信号将不能中断此线程的执行,除非此信号的产生是因为程序运行出错如 SIGSEGV;另外不能被忽略处理的信号 SIGKILL 和 SIGSTOP 也无法被阻塞。

当一个线程调用 pthread_create() 创建新的线程时,此线程的信号掩码会被新创建的线程继承。

POSIX.1 标准定义了一系列线程函数的接口,即 POSIXthreads(Pthreads)。Linux C 库提供了两种关于线程的实现:LinuxThreads 和 NPTL(Native POSIX ThreadsLibrary)。LinuxThreads 已经过时,一些函数的实现不遵循POSIX.1 规范。NPTL 依赖Linux 2.6 内核,更加遵循 POSIX..1 规范,但也不是完全遵循。

基于 NPTL 的线程库,多线程应用中的每个线程有自己独特的线程 ID,并共享同一个进程ID。应用程序可以通过调用 kill(getpid(),signo) 将信号发送到进程,如果进程中当前正在执行的线程没有阻碍此信号,则会被中断,线号处理函数会在此线程的上下文背景中执行。应用程序也可以通过调用 pthread_kill(pthread_t thread, int sig) 将信号发送给指定的线程,则线号处理函数会在此指定线程的上下文背景中执行。

基于 LinuxThreads 的线程库,多线程应用中的每个线程拥有自己独特的进程 ID,getpid() 在不同的线程中调用会返回不同的值,所以无法通过调用kill(getpid(),signo) 将信号发送到整个进程。

下文介绍的在指定的线程中以同步的方式处理异步信号是基于使用了 NPTL 的 Linux C 库。请参考“Linux 线程模型的比较:LinuxThreads 和 NPTL”和“pthreads(7) - Linux man page”进一步了解 Linux 的线程模型,以及不同版本的 Linux C 库对 NPTL 的支持。


回页首

编写安全的异步信号处理函数

信号的产生可以是:

  • 用户从控制终端终止程序运行,如 Ctrk + C 产生 SIGINT;
  • 程序运行出错时由硬件产生信号,如访问非法地址产生 SIGSEGV;
  • 程序运行逻辑需要,如调用 kill、raise 产生信号。

因为信号是异步事件,即信号处理函数执行的上下文背景是不确定的,譬如一个线程在调用某个库函数时可能会被信号中断,库函数提前出错返回,转而去执行信号处理函数。对于上述第三种信号的产生,信号在产生、处理后,应用程序不会终止,还是会继续正常运行,在编写此类信号处理函数时尤其需要小心,以免破 坏应用程序的正常运行。关于编写安全的信号处理函数主要有以下一些规则:

  • 信号处理函数尽量只执行简单的操作,譬如只是设置一个外部变量,其它复杂的操作留在信号处理函数之外执行;
  • errno 是线程安全,即每个线程有自己的 errno,但不是异步信号安全。如果信号处理函数比较复杂,且调用了可能会改变 errno 值的库函数,必须考虑在信号处理函数开始时保存、结束的时候恢复被中断线程的 errno 值;
  • 信号处理函数只能调用可以重入的 C 库函数;譬如不能调用 malloc(),free()以及标准 I/O 库函数等;
  • 信号处理函数如果需要访问全局变量,在定义此全局变量时须将其声明为 volatile,以避免编译器不恰当的优化。

从整个 Linux 应用的角度出发,因为应用中使用了异步信号,程序中一些库函数在调用时可能被异步信号中断,此时必须根据errno 的值考虑这些库函数调用被信号中断后的出错恢复处理,譬如socket 编程中的读操作:

     rlen = recv(sock_fd, buf, len, MSG_WAITALL);

     if ((rlen == -1) && (errno == EINTR)){

         // this kind of error is recoverable, we can set the offset change

         //‘rlen’ as 0 and continue to recv

     }

 


回页首

在指定的线程中以同步的方式处理异步信号

如上文所述,不仅编写安全的异步信号处理函数本身有很多的规则束缚;应用中其它地方在调用可被信号中断的库函数时还需考虑被中断后的出错恢复处理。这让程序的编写变得复杂,幸运的是,POSIX.1 规范定义了sigwait()、 sigwaitinfo() 和 pthread_sigmask() 等接口,可以实现:

  • 以同步的方式处理异步信号;
  • 在指定的线程中处理信号。

这种在指定的线程中以同步方式处理信号的模型可以避免因为处理异步信号而给程序运行带来的不确定性和潜在危险。

sigwait

sigwait() 提供了一种等待信号的到来,以串行的方式从信号队列中取出信号进行处理的机制。sigwait()只等待函数参数中指定的信号集,即如果新产生的信号不在指定的信号集内,则 sigwait()继续等待。对于一个稳定可靠的程序,我们一般会有一些疑问:

  • 多个相同的信号可不可以在信号队列中排队?
  • 如果信号队列中有多个信号在等待,在信号处理时有没有优先级规则?
  • 实时信号和非实时信号在处理时有没有什么区别?

笔者写了一小段测试程序来测试 sigwait 在信号处理时的一些规则。


清单 1.sigwait_test.c

#include <signal.h>

#include <errno.h>

#include <pthread.h>

#include <unistd.h>

#include <sys/types.h>

 

void sig_handler(int signum)

{

    printf("Receive signal. %d\n", signum);

}

 

void* sigmgr_thread()

{

    sigset_t   waitset, oset;

    int        sig;

    int        rc;

    pthread_t  ppid = pthread_self();

 

    pthread_detach(ppid);

 

    sigemptyset(&waitset);

    sigaddset(&waitset, SIGRTMIN);

    sigaddset(&waitset, SIGRTMIN+2);

    sigaddset(&waitset, SIGRTMAX);

    sigaddset(&waitset, SIGUSR1);

    sigaddset(&waitset, SIGUSR2);

 

    while (1)  {

        rc = sigwait(&waitset, &sig);

        if (rc != -1) {

            sig_handler(sig);

        } else {

            printf("sigwaitinfo() returned err: %d; %s\n", errno, strerror(errno));

        }

    }

}

 

 

int main()

{

    sigset_t bset, oset;

    int             i;

    pid_t           pid = getpid();

    pthread_t       ppid;

 

    sigemptyset(&bset);

    sigaddset(&bset, SIGRTMIN);

    sigaddset(&bset, SIGRTMIN+2);

    sigaddset(&bset, SIGRTMAX);

    sigaddset(&bset, SIGUSR1);

    sigaddset(&bset, SIGUSR2);

 

    if (pthread_sigmask(SIG_BLOCK, &bset, &oset) != 0)

        printf("!! Set pthread mask failed\n");

 

    kill(pid, SIGRTMAX);

    kill(pid, SIGRTMAX);

    kill(pid, SIGRTMIN+2);

    kill(pid, SIGRTMIN);

    kill(pid, SIGRTMIN+2);

    kill(pid, SIGRTMIN);

    kill(pid, SIGUSR2);

    kill(pid, SIGUSR2);

    kill(pid, SIGUSR1);

kill(pid, SIGUSR1);

 

    // Create the dedicated thread sigmgr_thread() which will handle signals synchronously

    pthread_create(&ppid, NULL, sigmgr_thread, NULL);

 

    sleep(10);

 

    exit (0);

}

 

程序编译运行在 RHEL4 的结果如下:


图 1. sigwait 测试程序执行结果

从以上测试程序发现以下规则:

  • 对于非实时信号,相同信号不能在信号队列中排队;对于实时信号,相同信号可以在信号队列中排队。
  • 如果信号队列中有多个实时以及非实时信号排队,实时信号并不会先于非实时信号被取出,信号数字小的会先被取出:如 SIGUSR1(10)会先于 SIGUSR2 (12),SIGRTMIN(34)会先于 SIGRTMAX (64), 非实时信号因为其信号数字小而先于实时信号被取出。

sigwaitinfo() 以及 sigtimedwait() 也提供了与 sigwait() 函数相似的功能。

Linux 多线程应用中的信号处理模型

在基于 Linux 的多线程应用中,对于因为程序逻辑需要而产生的信号,可考虑调用sigwait()使用同步模型进行处理。其程序流程如下:

  1. 主线程设置信号掩码,阻碍希望同步处理的信号;主线程的信号掩码会被其创建的线程继承;
  2. 主线程创建信号处理线程;信号处理线程将希望同步处理的信号集设为 sigwait()的第一个参数。
  3. 主线程创建工作线程。


图 2. 在指定的线程中以同步方式处理异步信号的模型

代码示例

以下为一个完整的在指定的线程中以同步的方式处理异步信号的程序。

主线程设置信号掩码阻碍 SIGUSR1 和 SIGRTMIN 两个信号,然后创建信号处理线程sigmgr_thread()和五个工作线程 worker_thread()。主线程每隔10秒调用 kill() 对本进程发送SIGUSR1 和 SIGTRMIN 信号。信号处理线程sigmgr_thread()在接收到信号时会调用信号处理函数 sig_handler()。

程序编译:gcc -o signal_sync signal_sync.c -lpthread

程序执行:./signal_sync

从程序执行输出结果可以看到主线程发出的所有信号都被指定的信号处理线程接收到,并以同步的方式处理。


清单 2.signal_sync.c

#include <signal.h>

#include <errno.h>

#include <pthread.h>

#include <unistd.h>

#include <sys/types.h>

 

void sig_handler(int signum)

{

    static int j = 0;

    static int k = 0;

    pthread_t  sig_ppid = pthread_self();

    // used to show which thread the signal is handled in.

  

    if (signum == SIGUSR1) {

        printf("thread %d, receive SIGUSR1 No. %d\n", sig_ppid, j);

        j++;

    //SIGRTMIN should not be considered constants from userland,

    //there is compile error when use switch case

    } else if (signum == SIGRTMIN) {

        printf("thread %d, receive SIGRTMIN No. %d\n", sig_ppid, k);

        k++;

    }

}

 

void* worker_thread()

{

    pthread_t  ppid = pthread_self();

    pthread_detach(ppid);

    while (1) {

        printf("I'm thread %d, I'm alive\n", ppid);

        sleep(10);

    }

}

 

void* sigmgr_thread()

{

    sigset_t   waitset, oset;

    siginfo_t  info;

    int        rc;

    pthread_t  ppid = pthread_self();

 

    pthread_detach(ppid);

 

    sigemptyset(&waitset);

    sigaddset(&waitset, SIGRTMIN);

    sigaddset(&waitset, SIGUSR1);

 

    while (1)  {

        rc = sigwaitinfo(&waitset, &info);

        if (rc != -1) {

            printf("sigwaitinfo() fetch the signal - %d\n", rc);

            sig_handler(info.si_signo);

        } else {

            printf("sigwaitinfo() returned err: %d; %s\n", errno, strerror(errno));

        }

    }

}

 

 

int main()

{

    sigset_t bset, oset;

    int             i;

    pid_t           pid = getpid();

    pthread_t       ppid;

   

 

    // Block SIGRTMIN and SIGUSR1 which will be handled in

    //dedicated thread sigmgr_thread()

    // Newly created threads will inherit the pthread mask from its creator

    sigemptyset(&bset);

    sigaddset(&bset, SIGRTMIN);

    sigaddset(&bset, SIGUSR1);

    if (pthread_sigmask(SIG_BLOCK, &bset, &oset) != 0)

        printf("!! Set pthread mask failed\n");

   

    // Create the dedicated thread sigmgr_thread() which will handle

    // SIGUSR1 and SIGRTMIN synchronously

    pthread_create(&ppid, NULL, sigmgr_thread, NULL);

 

    // Create 5 worker threads, which will inherit the thread mask of

    // the creator main thread

    for (i = 0; i < 5; i++) {

        pthread_create(&ppid, NULL, worker_thread, NULL);

    }

 

    // send out 50 SIGUSR1 and SIGRTMIN signals

    for (i = 0; i < 50; i++) {

        kill(pid, SIGUSR1);

        printf("main thread, send SIGUSR1 No. %d\n", i);

        kill(pid, SIGRTMIN);

        printf("main thread, send SIGRTMIN No. %d\n", i);

        sleep(10);

    }

    exit (0);

}

 

注意事项

在基于 Linux 的多线程应用中,对于因为程序逻辑需要而产生的信号,可考虑使用同步模型进行处理;而对会导致程序运行终止的信号如 SIGSEGV 等,必须按照传统的异步方式使用 signal()、 sigaction()注册信号处理函数进行处理。这两种信号处理模型可根据所处理的信号的不同同时存在一个 Linux 应用中:

  • 不要在线程的信号掩码中阻塞不能被忽略处理的两个信号 SIGSTOP 和 SIGKILL。
  • 不要在线程的信号掩码中阻塞 SIGFPE、SIGILL、SIGSEGV、SIGBUS。
  • 确保 sigwait() 等待的信号集已经被进程中所有的线程阻塞。
  • 在主线程或其它工作线程产生信号时,必须调用 kill() 将信号发给整个进程,而不能使用 pthread_kill() 发送某个特定的工作线程,否则信号处理线程无法接收到此信号。
  • 因为 sigwait()使用了串行的方式处理信号的到来,为避免信号的处理存在滞后,或是非实时信号被丢失的情况,处理每个信号的代码应尽量简洁、快速,避免调用会产生阻塞的库函数。

回页首

小结

在开发 Linux 多线程应用中, 如果因为程序逻辑需要引入信号, 在信号处理后程序仍将继续正常运行。在这种背景下,如果以异步方式处理信号,在编写信号处理函数一定要考虑异步信号处理函数的安全;同时, 程序中一些库函数可能会被信号中断,错误返回,这时需要考虑对 EINTR 的处理。另一方面,也可考虑使用上文介绍的同步模型处理信号,简化信号处理函数的编写,避免因为信号处理函数执行上下文的不确定性而带来的风险。

免责声明:

  1. 本文所提出的方式方法仅代表作者个人观点。
  2. 本文属于原创作品,资料来源不超出参考文献所列范畴,其中任何部分都不会侵犯任何第三方的知识产权。

 

参考资料

Linux 线程实现机制分析

杨沙洲 (pubb@163.net)国防科技大学计算机学院

杨沙洲,目前在国防科技大学计算机学院攻读软件方向博士学位。

简介: 自从多线程编程的概念出现在Linux 中以来,Linux 多线应用的发展总是与两个问题脱不开干系:兼容性、效率。本文从线程模型入手,通过分析目前 Linux 平台上最流行的 LinuxThreads 线程库的实现及其不足,描述了 Linux 社区是如何看待和解决兼容性和效率这两个问题的。

本文的标签:  kernel,linux,thread,代码库, 内核, 多线程, 操作系统, 线程实现机制分析

标记本文!

发布日期: 2003 年5 月 19 日
级别: 初级
访问情况 6841 次浏览
建议: 0 (添加评论)

平均分 (共 23 个评分 )

一.基础知识:线程和进程

按照教科书上的定义,进程是资源管理的最小单位,线程是程序执行的最小单位。在操作系统设计上,从进程演化出线程,最主要的目的就是更好的支持SMP以及减小(进程/线程)上下文切换开销。

无论按照怎样的分法,一个进程至少需要一个线程作为它的指令执行体,进程管理着资源(比如cpu、内存、文件等等),而将线程分配到某 个cpu上执行。一个进程当然可以拥有多个线程,此时,如果进程运行在SMP机器上,它就可以同时使用多个cpu来执行各个线程,达到最大程度的并行,以提高效率;同时,即使是在单cpu的机器上,采用多线程模型来设计程序,正如当年采用多进程模型代替单进程模型一样,使设计更简洁、功能更完备,程序的执行效率也更高,例如采用多个线程响应多个输入,而此时多线程模型所实现的功能实际上也可以用多进程模型来实现,而与后者相比,线程的上下文切换开销就比进 程要小多了,从语义上来说,同时响应多个输入这样的功能,实际上就是共享了除cpu以外的所有资源的。

针对线程模型的两大意义,分别开发出了核心级线程和用户级线程两种线程模型,分类的标准主要是线程的调度者在核内还是在核外。前者更利于并发使用多处理器的资源,而后者则更多考虑的是上下文切换开销。在目前的商用系统中,通常都将两者结合起来使用,既提供核心线程以满足smp系统的需 要,也支持用线程库的方式在用户态实现另一套线程机制,此时一个核心线程同时成为多个用户态线程的调度者。正如很多技术一样,"混合"通常都能带来更高的 效率,但同时也带来更大的实现难度,出于"简单"的设计思路,Linux从一开始就没有实现混合模型的计划,但它在实现上采用了另一种思路的"混合"。

在线程机制的具体实现上,可以在操作系统内核上实现线程,也可以在核外实现,后者显然要求核内至少实现了进程,而前者则一般要求在核内同时也支持进程。核心级线程模型显然要求前者的支持,而用户级线程模型则不一定基于后者实现。这种差异,正如前所述,是两种分类方式的标准不同带来的。

当核内既支持进程也支持线程时,就可以实现线程-进程的"多对多"模型,即一个进程的某个线程由核内调度,而同时它也可以作为用户级线程池的调度者,选择合适的用户级线程在其空间中运行。这就是前面提到的"混合"线程模型,既可满足多处理机系统的需要,也可以最大限度的减小调度开销。绝 大多数商业操作系统(如Digital Unix、Solaris、Irix)都采用的这种能够完全实现POSIX1003.1c标准的线程模型。在核外实现的线程又可以分为"一对一"、"多对一"两种模型,前者用一个核心进程(也许是轻量进程)对应一个线程,将线程调度等同于进程调度,交给核心完成,而后者则完全在核外实现多线程,调度也在用户态完成。后者就是前面提到的单纯的用户级线程模型的实现方式,显然,这种核外的线程调度器实际上只需要完成线程运行栈的切换,调度开销非常小,但同时因 为核心信号(无论是同步的还是异步的)都是以进程为单位的,因而无法定位到线程,所以这种实现方式不能用于多处理器系统,而这个需求正变得越来越大,因此,在现实中,纯用户级线程的实现,除算法研究目的以外,几乎已经消失了。

Linux内核只提供了轻量进程的支持,限制了更高效的线程模型的实现,但Linux着重优化了进程的调度开销,一定程度上也弥补了这一缺陷。目前最流行的线程机制LinuxThreads所采用的就是线程-进程"一对一"模型,调度交给核心,而在用户级实现一个包括信号处理在内的线程管理机制。Linux-LinuxThreads的运行机制正是本文的描述重点。


回页首

二.Linux 2.4内核中的轻量进程实现

最初的进程定义都包含程序、资源及其执行三部分,其中程序通常指代码,资源在操作系统层面上通常包括内存资源、IO资源、信号处理等部 分,而程序的执行通常理解为执行上下文,包括对cpu的占用,后来发展为线程。在线程概念出现以前,为了减小进程切换的开销,操作系统设计者逐渐修正进程的概念,逐渐允许将进程所占有的资源从其主体剥离出来,允许某些进程共享一部分资源,例如文件、信号,数据内存,甚至代码,这就发展出轻量进程的概念。 Linux内核在2.0.x版本就已经实现了轻量进程,应用程序可以通过一个统一的clone()系统调用接口,用不同的参数指定创建轻量进程还是普通进 程。在内核中,clone()调用经过参数传递和解释后会调用do_fork(),这个核内函数同时也是fork()、vfork()系统调用的最终实 现:

<linux-2.4.20/kernel/fork.c>

int do_fork(unsigned long clone_flags, unsigned long stack_start,

struct pt_regs *regs, unsigned long stack_size)

 

其中的clone_flags取自以下宏的"或"值:

<linux-2.4.20/include/linux/sched.h>

#define CSIGNAL      0x000000ff 

/* signal mask to be sent at exit */

#define CLONE_VM    0x00000100

/* set if VM shared between processes */

#define CLONE_FS        0x00000200 

/* set if fs info shared between processes */

#define CLONE_FILES     0x00000400  

/* set if open files shared between processes */

#define CLONE_SIGHAND  0x00000800

/* set if signal handlers and blocked signals shared */

#define CLONE_PID    0x00001000 

/* set if pid shared */

#define CLONE_PTRACE  0x00002000 

/* set if we want to let tracing continue on the child too */

#define CLONE_VFORK  0x00004000 

/* set if the parent wants the child to wake it up on mm_release */

#define CLONE_PARENT  0x00008000 

/* set if we want to have the same parent as the cloner */

#define CLONE_THREAD  0x00010000 

/* Same thread group? */

#define CLONE_NEWNS  0x00020000  /* New namespace group? */

#define CLONE_SIGNAL   (CLONE_SIGHAND | CLONE_THREAD)

 

在do_fork()中,不同的clone_flags将导致不同的行为,对于LinuxThreads,它使用(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND)参数来调用clone()创建"线程",表示共享内存、共享文件系统访问计数、共享文件描述符表,以及共享信号处理方式。本节就针对这几个参数,看看Linux内核是如何实现这些资源的共享的。

1.CLONE_VM

do_fork()需要调用copy_mm()来设置task_struct中的mm和active_mm项,这两个 mm_struct数据与进程所关联的内存空间相对应。如果do_fork()时指定了CLONE_VM开关,copy_mm()将把新的 task_struct中的mm和active_mm设置成与current的相同,同时提高该mm_struct的使用者数目 (mm_struct::mm_users)。也就是说,轻量级进程与父进程共享内存地址空间,由下图示意可以看出mm_struct在进程中的地位:


2.CLONE_FS

task_struct中利用fs(structfs_struct *)记录了进程所在文件系统的根目录和当前目录信息,do_fork()时调用copy_fs()复制了这个结构;而对于轻量级进程则仅增加 fs->count计数,与父进程共享相同的fs_struct。也就是说,轻量级进程没有独立的文件系统相关的信息,进程中任何一个线程改变当前 目录、根目录等信息都将直接影响到其他线程。

3.CLONE_FILES

一个进程可能打开了一些文件,在进程结构task_struct中利用files(structfiles_struct *)来保存进程打开的文件结构(struct file)信息,do_fork()中调用了copy_files()来处理这个进程属性;轻量级进程与父进程是共享该结构的,copy_files() 时仅增加files->count计数。这一共享使得任何线程都能访问进程所维护的打开文件,对它们的操作会直接反映到进程中的其他线程。

4.CLONE_SIGHAND

每一个Linux进程都可以自行定义对信号的处理方式,在task_struct中的sig(struct signal_struct)中使用一个struct k_sigaction结构的数组来保存这个配置信息,do_fork()中的copy_sighand()负责复制该信息;轻量级进程不进行复制,而仅 仅增加signal_struct::count计数,与父进程共享该结构。也就是说,子进程与父进程的信号处理方式完全相同,而且可以相互更改。

do_fork()中所做的工作很多,在此不详细描述。对于SMP系统,所有的进程fork出来后,都被分配到与父进程相同的cpu上,一直到该进程被调度时才会进行cpu选择。

尽管Linux支持轻量级进程,但并不能说它就支持核心级线程,因为Linux的"线程"和"进程"实际上处于一个调度层次,共享一个 进程标识符空间,这种限制使得不可能在Linux上实现完全意义上的POSIX线程机制,因此众多的Linux线程库实现尝试都只能尽可能实现POSIX 的绝大部分语义,并在功能上尽可能逼近。


回页首

三.LinuxThread的线程机制

LinuxThreads是目前Linux平台上使用最为广泛的线程库,由Xavier Leroy (Xavier.Leroy@inria.fr)负责开发完成,并已绑定在GLIBC中发行。它所实现的就是基于核心轻量级进程的"一对一"线程模型,一 个线程实体对应一个核心轻量级进程,而线程之间的管理在核外函数库中实现。

1.线程描述数据结构及实现限制

LinuxThreads定义了一个struct_pthread_descr_struct数据结构来描述线程,并使用全局数组变量__pthread_handles来描述和引用进程所辖线程。在 __pthread_handles中的前两项,LinuxThreads定义了两个全局的系统线程:__pthread_initial_thread 和__pthread_manager_thread,并用__pthread_main_thread表征__pthread_manager_thread的父线程(初始为__pthread_initial_thread)。

struct _pthread_descr_struct是一个双环链表结构,__pthread_manager_thread所在的链表仅包括它一个元素,实际 上,__pthread_manager_thread是一个特殊线程,LinuxThreads仅使用了其中的errno、p_pid、 p_priority等三个域。而__pthread_main_thread所在的链则将进程中所有用户线程串在了一起。经过一系列 pthread_create()之后形成的__pthread_handles数组将如下图所示:


新创建的线程将首先在__pthread_handles数组中占据一项,然后通过数据结构中的链指针连入以__pthread_main_thread为首指针的链表中。这个链表的使用在介绍线程的创建和释放的时候将提到。

LinuxThreads遵循POSIX1003.1c标准,其中对线程库的实现进行了一些范围限制,比如进程最大线程数,线程私有数据区大小等等。在LinuxThreads的实现中,基本遵循这些限制,但也进行了一定的改动,改动的趋势是放松或者说扩大这些限制,使编程更加方便。这些限定宏主要集中在sysdeps/unix/sysv/linux/bits/local_lim.h(不同平台使用的文件位置不同)中,包括如下几个:

每进程的私有数据key数,POSIX定义_POSIX_THREAD_KEYS_MAX为128,LinuxThreads使用PTHREAD_KEYS_MAX,1024;私有数据释放时允许执行的操作数,LinuxThreads与POSIX一致,定义 PTHREAD_DESTRUCTOR_ITERATIONS为4;每进程的线程数,POSIX定义为64,LinuxThreads增大到 1024(PTHREAD_THREADS_MAX);线程运行栈最小空间大小,POSIX未指定,LinuxThreads使用 PTHREAD_STACK_MIN,16384(字节)。

2.管理线程

"一对一"模型的好处之一是线程的调度由核心完成了,而其他诸如线程取消、线程间的同步等工作,都是在核外线程库中完成的。在 LinuxThreads中,专门为每一个进程构造了一个管理线程,负责处理线程相关的管理工作。当进程第一次调用pthread_create()创建 一个线程的时候就会创建(__clone())并启动管理线程。

在一个进程空间内,管理线程与其他线程之间通过一对"管理管道(manager_pipe[2])"来通讯,该管道在创建管理线程之前 创建,在成功启动了管理线程之后,管理管道的读端和写端分别赋给两个全局变量__pthread_manager_reader和__pthread_manager_request,之后,每个用户线程都通过__pthread_manager_request向管理线程发请求,但管理线程本身并没有直接使用__pthread_manager_reader,管道的读端(manager_pipe[0])是作为 __clone()的参数之一传给管理线程的,管理线程的工作主要就是监听管道读端,并对从中取出的请求作出反应。

创建管理线程的流程如下所示:
(全局变量pthread_manager_request初值为-1)


初始化结束后,在__pthread_manager_thread中记录了轻量级进程号以及核外分配和管理的线程 id,2*PTHREAD_THREADS_MAX+1这个数值不会与任何常规用户线程id冲突。管理线程作为pthread_create()的调用者 线程的子线程运行,而pthread_create()所创建的那个用户线程则是由管理线程来调用clone()创建,因此实际上是管理线程的子线程。(此处子线程的概念应该当作子进程来理解。)

__pthread_manager()就是管理线程的主循环所在,在进行一系列初始化工作后,进入while(1)循环。在循环中, 线程以2秒为timeout查询(__poll())管理管道的读端。在处理请求前,检查其父线程(也就是创建manager的主线程)是否已退出,如果 已退出就退出整个进程。如果有退出的子线程需要清理,则调用pthread_reap_children()清理。

然后才是读取管道中的请求,根据请求类型执行相应操作(switch-case)。具体的请求处理,源码中比较清楚,这里就不赘述了。

3.线程栈

在LinuxThreads中,管理线程的栈和用户线程的栈是分离的,管理线程在进程堆中通过malloc()分配一个THREAD_MANAGER_STACK_SIZE字节的区域作为自己的运行栈。

用户线程的栈分配办法随着体系结构的不同而不同,主要根据两个宏定义来区分,一个是 NEED_SEPARATE_REGISTER_STACK,这个属性仅在IA64平台上使用;另一个是FLOATING_STACK宏,在i386等少 数平台上使用,此时用户线程栈由系统决定具体位置并提供保护。与此同时,用户还可以通过线程属性结构来指定使用用户自定义的栈。因篇幅所限,这里只能分析 i386平台所使用的两种栈组织方式:FLOATING_STACK方式和用户自定义方式。

在FLOATING_STACK方式下,LinuxThreads利用mmap()从内核空间中分配8MB空间(i386系统缺省的最 大栈空间大小,如果有运行限制(rlimit),则按照运行限制设置),使用mprotect()设置其中第一页为非访问区。该8M空间的功能分配如下 图:


低地址被保护的页面用来监测栈溢出。

对于用户指定的栈,在按照指针对界后,设置线程栈顶,并计算出栈底,不做保护,正确性由用户自己保证。

不论哪种组织方式,线程描述结构总是位于栈顶紧邻堆栈的位置。

4.线程id和进程id

每个LinuxThreads线程都同时具有线程id和进程id,其中进程id就是内核所维护的进程号,而线程id则由LinuxThreads分配和维护。

__pthread_initial_thread的线程id为 PTHREAD_THREADS_MAX,__pthread_manager_thread的是2*PTHREAD_THREADS_MAX+1,第一 个用户线程的线程id为PTHREAD_THREADS_MAX+2,此后第n个用户线程的线程id遵循以下公式:

        tid=n*PTHREAD_THREADS_MAX+n+1

       

 

这种分配方式保证了进程中所有的线程(包括已经退出)都不会有相同的线程id,而线程id的类型pthread_t定义为无符号长整型(unsigned long int),也保证了有理由的运行时间内线程id不会重复。

从线程id查找线程数据结构是在pthread_handle()函数中完成的,实际上只是将线程号按PTHREAD_THREADS_MAX取模,得到的就是该线程在__pthread_handles中的索引。

5.线程的创建

在pthread_create()向管理线程发送REQ_CREATE请求之后,管理线程即调用pthread_handle_create()创建新线程。分配栈、设置thread属性后,以pthread_start_thread()为函数入 口调用__clone()创建并启动新线程。pthread_start_thread()读取自身的进程id号存入线程描述结构中,并根据其中记录的调度方法配置调度。一切准备就绪后,再调用真正的线程执行函数,并在此函数返回后调用pthread_exit()清理现场。

6.LinuxThreads的不足

由于Linux内核的限制以及实现难度等等原因,LinuxThreads并不是完全POSIX兼容的,在它的发行README中有说明。

1)进程id问题

这个不足是最关键的不足,引起的原因牵涉到LinuxThreads的"一对一"模型。

Linux内核并不支持真正意义上的线程,LinuxThreads是用与普通进程具有同样内核调度视图的轻量级进程来实现线程支持的。这些轻量级进程拥有独立的进程id,在进程调度、信号处理、IO等方面享有与普通进程一样的能力。在源码阅读者看来,就是Linux内核的 clone()没有实现对CLONE_PID参数的支持。

在内核do_fork()中对CLONE_PID的处理是这样的:

          if (clone_flags & CLONE_PID) {

                if (current->pid)

                        goto fork_out;

        }

       

 

这段代码表明,目前的Linux内核仅在pid为0的时候认可CLONE_PID参数,实际上,仅在SMP初始化,手工创建进程的时候才会使用CLONE_PID参数。

按照POSIX定义,同一进程的所有线程应该共享一个进程id和父进程id,这在目前的"一对一"模型下是无法实现的。

2)信号处理问题

由于异步信号是内核以进程为单位分发的,而LinuxThreads的每个线程对内核来说都是一个进程,且没有实现"线程组",因此,某些语义不符合POSIX标准,比如没有实现向进程中所有线程发送信号,README对此作了说明。

如果核心不提供实时信号,LinuxThreads将使用SIGUSR1和SIGUSR2作为内部使用的restart和cancel 信号,这样应用程序就不能使用这两个原本为用户保留的信号了。在Linuxkernel 2.1.60以后的版本都支持扩展的实时信号(从_SIGRTMIN到_SIGRTMAX),因此不存在这个问题。

某些信号的缺省动作难以在现行体系上实现,比如SIGSTOP和SIGCONT,LinuxThreads只能将一个线程挂起,而无法挂起整个进程。

3)线程总数问题

LinuxThreads将每个进程的线程最大数目定义为1024,但实际上这个数值还受到整个系统的总进程数限制,这又是由于线程其实是核心进程。

在kernel2.4.x中,采用一套全新的总进程数计算方法,使得总进程数基本上仅受限于物理内存的大小,计算公式在kernel/fork.c的fork_init()函数中:

        max_threads = mempages / (THREAD_SIZE/PAGE_SIZE) / 8

       

 

在i386上,THREAD_SIZE=2*PAGE_SIZE,PAGE_SIZE=2^12(4KB),mempages=物理内 存大小/PAGE_SIZE,对于256M的内存的机器,mempages=256*2^20/2^12=256*2^8,此时最大线程数为4096。

但为了保证每个用户(除了root)的进程总数不至于占用一半以上物理内存,fork_init()中继续指定:

    init_task.rlim[RLIMIT_NPROC].rlim_cur = max_threads/2;

    init_task.rlim[RLIMIT_NPROC].rlim_max = max_threads/2;

   

 

这些进程数目的检查都在do_fork()中进行,因此,对于LinuxThreads来说,线程总数同时受这三个因素的限制。

4)管理线程问题

管理线程容易成为瓶颈,这是这种结构的通病;同时,管理线程又负责用户线程的清理工作,因此,尽管管理线程已经屏蔽了大部分的信号,但一旦管理线程死亡,用户线程就不得不手工清理了,而且用户线程并不知道管理线程的状态,之后的线程创建等请求将无人处理。

5)同步问题

LinuxThreads中的线程同步很大程度上是建立在信号基础上的,这种通过内核复杂的信号处理机制的同步方式,效率一直是个问题。

6)其他POSIX兼容性问题

Linux中很多系统调用,按照语义都是与进程相关的,比如nice、setuid、setrlimit等,在目前的LinuxThreads中,这些调用都仅仅影响调用者线程。

7)实时性问题

线程的引入有一定的实时性考虑,但LinuxThreads暂时不支持,比如调度选项,目前还没有实现。不仅LinuxThreads如此,标准的Linux在实时性上考虑都很少。


回页首

四.其他的线程实现机制

LinuxThreads的问题,特别是兼容性上的问题,严重阻碍了Linux上的跨平台应用(如Apache)采用多线程设计,从而 使得Linux上的线程应用一直保持在比较低的水平。在Linux社区中,已经有很多人在为改进线程性能而努力,其中既包括用户级线程库,也包括核心级和用户级配合改进的线程库。目前最为人看好的有两个项目,一个是RedHat公司牵头研发的NPTL(Native Posix Thread Library),另一个则是IBM投资开发的NGPT(NextGeneration Posix Threading),二者都是围绕完全兼容POSIX 1003.1c,同时在核内和核外做工作以而实现多对多线程模型。这两种模型都在一定程度上弥补了LinuxThreads的缺点,且都是重起炉灶全新设 计的。

1.NPTL

NPTL的设计目标归纳可归纳为以下几点:

  • POSIX兼容性
  • SMP结构的利用
  • 低启动开销
  • 低链接开销(即不使用线程的程序不应当受线程库的影响)
  • 与LinuxThreads应用的二进制兼容性
  • 软硬件的可扩展能力
  • 多体系结构支持
  • NUMA支持
  • 与C++集成

在技术实现上,NPTL仍然采用1:1的线程模型,并配合glibc和最新的Linux Kernel2.5.x开发版在信号处理、线程同步、存储管理等多方面进行了优化。和LinuxThreads不同,NPTL没有使用管理线程,核心线程 的管理直接放在核内进行,这也带了性能的优化。

主要是因为核心的问题,NPTL仍然不是100%POSIX兼容的,但就性能而言相对LinuxThreads已经有很大程度上的改进了。

2.NGPT

IBM的开放源码项目NGPT在2003年1月10日推出了稳定的2.2.0版,但相关的文档工作还差很多。就目前所知,NGPT是基于GNU Pth(GNU Portable Threads)项目而实现的M:N模型,而GNU Pth是一个经典的用户级线程库实现。

按照2003年3月NGPT官方网站上的通知,NGPT考虑到NPTL日益广泛地为人所接受,为避免不同的线程库版本引起的混乱,今后将不再进行进一步开发,而今进行支持性的维护工作。也就是说,NGPT已经放弃与NPTL竞争下一代LinuxPOSIX线程库标准。

3.其他高效线程机制

此处不能不提到Scheduler Activations。这个1991年在ACM上发表的多线程内核结构影响了很多多线程内核的设计,其中包括Mach3.0、NetBSD和商业版本 Digital Unix(现在叫Compaq True64 Unix)。它的实质是在使用用户级线程调度的同时,尽可能地减少用户级对核心的系统调用请求,而后者往往是运行开销的重要来源。采用这种结构的线程机制,实际上是结合了用户级线程的灵活高效和核心级线程的实用性,因此,包括Linux、FreeBSD在内的多个开放源码操作系统设计社区都在进行相关研 究,力图在本系统中实现SchedulerActivations。

 

参考资料

  • [Linus Torvalds,2002] Linux内核源码v2.4.20
  • [GNU,2002] Glibc源码v2.2.2(内含LinuxThreads v0.9)
  • [Thomas E. Terrill,1997] An Introduction to Threads Using The LinuxThreads Interface
  • [Ulrich Drepper,Ingo Molnar,2003] The Native POSIX Thread Library for Linux
  • http://www.ibm.com/developerworks/oss/pthreads/,NGPT官方网站
  • [Ralf S. Engelschall,2000] Portable Multithreading
  • [Thomas E. Anderson, Brian N. Bershad, Edward D. Lazowska, Henry M. Levy,1992] Scheduler Activations: Effective Kernel Support for the User-Level Management of Parallelism
  • [pcjockey@21cn.com] Linux线程初探

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值