From: http://blog.sina.com.cn/s/blog_602f87700100dqmk.html
Linux下thread编程(一)
Sam前些天在提供一个库给别的公司时,因为不喜欢使用pthread_jion等函数,被人骂为垃圾程序。呵呵,之前因为在写多thread程序时,习惯让每个thread都为detach属性,这样他们就可以自我管理。而不需要再由别人回收资源。呵呵,不说这么多了,把POSIXthread方面的东西记下来吧。
Linux下thread历史(Oldpthread与NPTL):
Linux创建之初,并不能真正支持thread. LinuxThreads项目使用clone()这个系统调用实现对thread的模拟。在_clone本来的意图是创建一个可定义各种配置的对当前进程的拷贝。LinuxThreads项目则利用了这一点,配置了一个与调用进程拥有相同地址空间的拷贝,把它作为一个thread.所以,常常有人说,linux下面没有进程线程之分,其实就是这个意思。但这个方法也有问题,尤其是在信号处理、调度和进程间同步原语方面都存在问题。另外,这个线程模型也不符合POSIX 的要求。
如果要改进LinuxThread.很明显,需要kernel层的支持。IBM和RedHat分别进行了研究,随着IBM的放弃,RedHat的Native POSIXThread Library(NPTL)就成唯一的解决方案了。这就是NPTL。
LinuxThreads最初的设计相信相关进程之间的上下文切换速度很快,因此每个内核线程足以处理很多相关的用户级线程。这就导致了一对一线程模型的革命。
LinuxThreads 设计细节的一些基本理念:
LinuxThreads 非常出名的一个特性就是管理线程(managerthread)。管理线程可以满足以下要求:
- 系统必须能够响应终止信号并杀死整个进程。
- 以堆栈形式使用的内存回收必须在线程完成之后进行。因此,线程无法自行完成这个过程。
- 终止线程必须进行等待,这样它们才不会进入僵尸状态。
- 线程本地数据的回收需要对所有线程进行遍历;这必须由管理线程来进行。
- 如果主线程需要调用
pthread_exit()
,那么这个线程就无法结束。主线程要进入睡眠状态,而管理线程的工作就是在所有线程都被杀死之后来唤醒这个主线程。
- 为了维护线程本地数据和内存,LinuxThreads 使用了进程地址空间的高位内存(就在堆栈地址之下)。
- 原语的同步是使用信号 来实现的。例如,线程会一直阻塞,直到被信号唤醒为止。
- 在克隆系统的最初设计之下,LinuxThreads 将每个线程都是作为一个具有惟一进程 ID 的进程实现的。
- 终止信号可以杀死所有的线程。LinuxThreads接收到终止信号之后,管理线程就会使用相同的信号杀死所有其他线程(进程)。
- 根据 LinuxThreads的设计,如果一个异步信号被发送了,那么管理线程就会将这个信号发送给一个线程。如果这个线程现在阻塞了这个信号,那么这个信号也就会被挂起。这是因为管理线程无法将这个信号发送给进程;相反,每个线程都是作为一个进程在执行。
- 线程之间的调度是由内核调度器来处理的。
LinuxThreads的设计通常都可以很好地工作;但是在压力很大的应用程序中,它的性能、可伸缩性和可用性都会存在问题。下面让我们来看一下LinuxThreads 设计的一些局限性:
- 它使用管理线程来创建线程,并对每个进程所拥有的所有线程进行协调。这增加了创建和销毁线程所需要的开销。
- 由于它是围绕一个管理线程来设计的,因此会导致很多的上下文切换的开销,这可能会妨碍系统的可伸缩性和性能。
- 由于管理线程只能在一个 CPU 上运行,因此所执行的同步操作在 SMP 或 NUMA系统上可能会产生可伸缩性的问题。
- 由于线程的管理方式,以及每个线程都使用了一个不同的进程 ID,因此 LinuxThreads 与其他与 POSIX相关的线程库并不兼容。
- 信号用来实现同步原语,这会影响操作的响应时间。另外,将信号发送到主进程的概念也并不存在。因此,这并不遵守 POSIX中处理信号的方法。
- LinuxThreads中对信号的处理是按照每线程的原则建立的,而不是按照每进程的原则建立的,这是因为每个线程都有一个独立的进程ID。由于信号被发送给了一个专用的线程,因此信号是串行化的 —— 也就是说,信号是透过这个线程再传递给其他线程的。这与POSIX 标准对线程进行并行处理的要求形成了鲜明的对比。例如,在 LinuxThreads 中,通过
kill()
所发送的信号被传递到一些单独的线程,而不是集中整体进行处理。这意味着如果有线程阻塞了这个信号,那么 LinuxThreads就只能对这个线程进行排队,并在线程开放这个信号时在执行处理,而不是像其他没有阻塞信号的线程中一样立即处理这个信号。
- 由于 LinuxThreads 中的每个线程都是一个进程,因此用户和组 ID的信息可能对单个进程中的所有线程来说都不是通用的。例如,一个多线程的
setuid()
/setgid()
进程对于不同的线程来说可能都是不同的。
- 有一些情况下,所创建的多线程核心转储中并没有包含所有的线程信息。同样,这种行为也是每个线程都是一个进程这个事实所导致的结果。如果任何线程发生了问题,我们在系统的核心文件中只能看到这个线程的信息。不过,这种行为主要适用于早期版本的LinuxThreads 实现。
- 由于每个线程都是一个单独的进程,因此 /proc 目录中会充满众多的进程项,而这实际上应该是线程。
- 由于每个线程都是一个进程,因此对每个应用程序只能创建有限数目的线程。例如,在 IA32 系统上,可用进程总数 ——也就是可以创建的线程总数 —— 是 4,090。
- 由于计算线程本地数据的方法是基于堆栈地址的位置的,因此对于这些数据的访问速度都很慢。另外一个缺点是用户无法可信地指定堆栈的大小,因为用户可能会意外地将堆栈地址映射到本来要为其他目的所使用的区域上了。按需增长(growon demand) 的概念(也称为浮动堆栈 的概念)是在 2.4.10 版本的 Linux内核中实现的。在此之前,LinuxThreads 使用的是固定堆栈。
NPTL,或称为 Native POSIX Thread Library,是 Linux 线程的一个新实现,它克服了LinuxThreads 的缺点,同时也符合 POSIX 的需求。与 LinuxThreads相比,它在性能和稳定性方面都提供了重大的改进。与 LinuxThreads 一样,NPTL 也实现了一对一的模型。
Ulrich Drepper 和 Ingo Molnar 是 Red Hat 参与 NPTL设计的两名员工。他们的总体设计目标如下:
- 这个新线程库应该兼容 POSIX 标准。
- 这个线程实现应该在具有很多处理器的系统上也能很好地工作。
- 为一小段任务创建新线程应该具有很低的启动成本。
- NPTL 线程库应该与 LinuxThreads 是二进制兼容的。注意,为此我们可以使用
LD_ASSUME_KERNEL
,这会在本文稍后进行讨论。
- 这个新线程库应该可以利用 NUMA 支持的优点。
与 LinuxThreads 相比,NPTL 具有很多优点:
- NPTL没有使用管理线程。管理线程的一些需求,例如向作为进程一部分的所有线程发送终止信号,是并不需要的;因为内核本身就可以实现这些功能。内核还会处理每个线程堆栈所使用的内存的回收工作。它甚至还通过在清除父线程之前进行等待,从而实现对所有线程结束的管理,这样可以避免僵尸进程的问题。
- 由于 NPTL 没有使用管理线程,因此其线程模型在 NUMA 和 SMP 系统上具有更好的可伸缩性和同步机制。
- 使用 NPTL 线程库与新内核实现,就可以避免使用信号来对线程进行同步了。为了这个目的,NPTL 引入了一种名为futex 的新机制。futex 在共享内存区域上进行工作,因此可以在进程之间进行共享,这样就可以提供进程间 POSIX同步机制。我们也可以在进程之间共享一个 futex。这种行为使得进程间同步成为可能。实际上,NPTL 包含了一个
PTHREAD_PROCESS_SHARED
宏,使得开发人员可以让用户级进程在不同进程的线程之间共享互斥锁。
- 由于 NPTL 是 POSIX 兼容的,因此它对信号的处理是按照每进程的原则进行的;
getpid()
会为所有的线程返回相同的进程 ID。例如,如果发送了SIGSTOP
信号,那么整个进程都会停止;使用LinuxThreads,只有接收到这个信号的线程才会停止。这样可以在基于 NPTL 的应用程序上更好地利用调试器,例如GDB。
- 由于在 NPTL 中所有线程都具有一个父进程,因此对父进程汇报的资源使用情况(例如 CPU和内存百分比)都是对整个进程进行统计的,而不是对一个线程进行统计的。
- NPTL 线程库所引入的一个实现特性是对 ABI(应用程序二进制接口)的支持。这帮助实现了与 LinuxThreads的向后兼容性。这个特性是通过使用
LD_ASSUME_KERNEL
实现的,下面就来介绍这个特性。
大部分现代 Linux 发行版都预装了 LinuxThreads 和NPTL,因此它们提供了一种机制来在二者之间进行切换。要查看您的系统上正在使用的是哪个线程库,请运行下面的命令:
$ getconf GNU_LIBPTHREAD_VERSION
这会产生类似于下面的输出结果:
NPTL 0.34
或者:
linuxthreads-0.10
Linux 发行版所使用的线程模型、glibc版本和内核版本
表 1 列出了一些流行的 Linux 发行版,以及它们所采用的线程实现的类型、glibc 库和内核版本。
线程实现 | C 库 | 发行版 | 内核 |
---|---|---|---|
LinuxThreads 0.7, 0.71 (for libc5) | libc 5.x | Red Hat 4.2 | |
LinuxThreads 0.7, 0.71 (for glibc 2) | glibc 2.0.x | Red Hat 5.x | |
LinuxThreads 0.8 | glibc 2.1.1 | Red Hat 6.0 | |
LinuxThreads 0.8 | glibc 2.1.2 | Red Hat 6.1 and 6.2 | |
LinuxThreads 0.9 | Red Hat 7.2 | 2.4.7 | |
LinuxThreads 0.9 | glibc 2.2.4 | Red Hat 2.1 AS | 2.4.9 |
LinuxThreads 0.10 | glibc 2.2.93 | Red Hat 8.0 | 2.4.18 |
NPTL 0.6 | glibc 2.3 | Red Hat 9.0 | 2.4.20 |
NPTL 0.61 | glibc 2.3.2 | Red Hat 3.0 EL | 2.4.21 |
NPTL 2.3.4 | glibc 2.3.4 | Red Hat 4.0 | 2.6.9 |
LinuxThreads 0.9 | glibc 2.2 | SUSE Linux Enterprise Server 7.1 | 2.4.18 |
LinuxThreads 0.9 | glibc 2.2.5 | SUSE Linux Enterprise Server 8 | 2.4.21 |
LinuxThreads 0.9 | glibc 2.2.5 | United Linux | 2.4.21 |
NPTL 2.3.5 | glibc 2.3.3 | SUSE Linux Enterprise Server 9 | 2.6.5 |
注意,从 2.6.x 版本的内核和 glibc 2.3.3 开始,NPTL所采用的版本号命名约定发生了变化:这个库现在是根据所使用的 glibc 的版本进行编号的。
Sam记得好像某篇文章讲,从2.6kernel开始,NPTL支持被加入。只需要glibc用NPTL就可以了。就算我们的系统里装上了NPTL库,也不会影响原来的程序,就算是那些老的程序,即使用了linuxthreads的头文件且在编译,连接的时候使用了linuxthreads的库的程序,我们也能够让它在执行的时候,动态连接到我们的NPTL库。从而发挥NPTL的作用。
另外,从GNU libc 2.4开始使用了NPTL方式,但Kernel版本需要 Linux2.6.0以上。
所以判断一个嵌入式平台是否支持NPTL,首先看kernel版本号,如果低于2.6.则肯定不支持NPTL.(因为如果kernel不支持NPTL.则libc再怎么做都没办法)。其次看libc版本号。即/lib/libc.so.xxxx。 另外:截至到目前:uclibc版本为0.9.29。 不支持NPTL。
所以:
Intel CE3100:
Kernel Version:2.6.23
LibC Version: libc-2.7.so
支持NPTL。
Intel CE2110:
Kernel Version:2.6.18
LibC Version: libc-2.3.6.so
不支持NPTL。
BCM7403:
Kernel Version:2.6.12-4.2
LibC Version: libuClibc-0.9.28.so
不支持NPTL。
Linux下thread编程(二)
thread属性:
pthread_create()中,第二个参数(pthread_attr_t)为将要创建的thread属性。通常情况下配置为NULL,使用缺省设置就可以了。
但了解这些属性,有利于更好的理解thread.
属性对象(pthread_attr_t)是不透明的,而且不能通过赋值直接进行修改。系统提供了一组函数,用于初始化、配置和销毁每种对象类型。
创建属性:
int pthread_attr_init(pthread_attr_t *attr);
创建的属性设定为缺省设置。
销毁属性:
int pthread_attr_destroy(pthread_attr_t *attr);
一:设置分离状态:
线程的分离状态有2种:PTHREAD_CREATE_JOINABLE(非分离状态),PTHREAD_CREATE_DETACHED(分离状态)
分离状态含义如下:
如果使用 PTHREAD_CREATE_JOINABLE创建非分离线程,则假设应用程序将等待线程完成。也就是说,程序将对线程执行pthread_join。 非分离线程在终止后,必须要有一个线程用 join来等待它。否则,不会释放该线程的资源以供新线程使用,而这通常会导致内存泄漏。因此,如果不希望线程被等待,请将该线程作为分离线程来创建。
如果使用 PTHREAD_CREATE_DETACHED创建分离thread,则表明此thread在退出时会自动回收资源和threadID.
Sam之前很喜欢使用分离thread.但现在慢慢使用中觉得这样是个不好的习惯。因为分离thread有个问题:主程序退出时,很难确认子thread已经退出。只好使用全局变量来标明子thread已经正常退出了。
另外:不管创建分离还是非分离的thread.在子thread全部退出之前退出主程序都是很有风险的。如果主thread选择return,或者调用exit()退出,则所有thread都会被kill掉。这样很容易出错。Sam上次出的问题其实就是这个。但如果主thread只是调用pthread_exit().则仅主线程本身终止。进程及进程内的其他线程将继续存在。所有线程都已终止时,进程也将终止。
int pthread_attr_getdetachstate(constpthread_attr_t *attr,int *detachstate);
int pthread_attr_setdetachstate(pthread_attr_t *attr, intdetachstate);
得到当前和分离状态和设置当前的分离状态。
二:设置栈溢出保护区大小:
栈溢出概念:
-
溢出保护可能会导致系统资源浪费。如果应用程序创建大量线程,并且已知这些线程永远不会溢出其栈,则可以关闭溢出保护区。通过关闭溢出保护区,可以节省系统资源。
-
线程在栈上分配大型数据结构时,可能需要较大的溢出保护区来检测栈溢出。
int pthread_attr_getguardsize(const pthread_attr_t *restrictattr,size_t *restrict guardsize);
int pthread_attr_setguardsize(pthread_attr_t *attr,size_tguardsize);
设置和得到栈溢出保护区。如果guardsize设为0。则表示不设置栈溢出保护区。guardsize的值向上舍入为 PAGESIZE 的倍数。
三:设置thread竞用范围:
竞用范围(PTHREAD_SCOPE_SYSTEM 或PTHREAD_SCOPE_PROCESS)指 使用PTHREAD_SCOPE_SYSTEM 时,此线程将与系统中的所有线程进行竞争。使用PTHREAD_SCOPE_PROCESS 时,此线程将与进程中的其他线程进行竞争。
int pthread_attr_getscope(const pthread_attr_t *restrictattr,int *restrict contentionscope);
int pthread_attr_setscope(pthread_attr_t *attr, intcontentionscope);
四:设置线程并行级别:
int pthread_getconcurrency(void);
int pthread_setconcurrency(int new_level);
Sam不理解这个意思。
五:设置调度策略:
POSIX 标准指定SCHED_FIFO(先入先出)、SCHED_RR(循环)或SCHED_OTHER(实现定义的方法)的调度策略属性。
-
SCHED_FIFO
如果调用进程具有有效的用户 ID 0,则争用范围为系统(PTHREAD_SCOPE_SYSTEM) 的先入先出线程属于实时 (RT)调度类。如果这些线程未被优先级更高的线程抢占,则会继续处理该线程,直到该线程放弃或阻塞为止。对于具有进程争用范围(PTHREAD_SCOPE_PROCESS)) 的线程或其调用进程没有有效用户 ID 0 的线程,请使用SCHED_FIFO。SCHED_FIFO 基于 TS 调度类。
-
SCHED_RR
如果调用进程具有有效的用户 ID 0,则争用范围为系统(PTHREAD_SCOPE_SYSTEM)) 的循环线程属于实时 (RT)调度类。如果这些线程未被优先级更高的线程抢占,并且这些线程没有放弃或阻塞,则在系统确定的时间段内将一直执行这些线程。对于具有进程争用范围(PTHREAD_SCOPE_PROCESS) 的线程,请使用 SCHED_RR(基于TS 调度类)。此外,这些线程的调用进程没有有效的用户 ID 0。
SCHED_FIFO是基于队列的调度程序,对于每个优先级都会使用不同的队列。SCHED_RR 与 FIFO相似,不同的是前者的每个线程都有一个执行时间配额。
int pthread_attr_getschedpolicy(const pthread_attr_t *restrictattr,int *restrict policy);
int pthread_attr_setschedpolicy(pthread_attr_t *attr, intpolicy);
六:设置优先级:
int pthread_attr_getschedparam(const pthread_attr_t *restrictattr,struct sched_param *restrict param);
int pthread_attr_setschedparam(pthread_attr_t *restrictattr,
比较复杂,Sam没去研究。
七:设置栈大小:
当创建一个thread时,会给它分配一个栈空间,线程栈是从页边界开始的。任何指定的大小都被向上舍入到下一个页边界。不具备访问权限的页将被附加到栈的溢出端(第二项设置中设置)。
指定栈时,还应使用 PTHREAD_CREATE_JOINABLE 创建线程。在该线程的pthread_join()调用返回之前,不会释放该栈。在该线程终止之前,不会释放该线程的栈。了解这类线程是否已终止的唯一可靠方式是使用pthread_join。
一般情况下,不需要为线程分配栈空间。系统会为每个线程的栈分配指定大小的虚拟内存。
#ulimit -a可以看到这个缺省大小
Linux下thread编程(三)
1.创建thread.int pthread_create(pthread_t *restrict thread, constpthread_attr_t *restrict attr,
参数1:pthread_t *restrict thread:创建thread的thread ID.
参数2:const pthread_attr_t *restrict attr:创建线程的属性。
参数3:void *(*start_routine)(void*):thread服务程序。
参数4:void *restrict arg:thread服务程序参数。
2. 等待目标线程终止:
pthread_join() 函数会一直阻塞调用线程,直到指定的线程终止。
指定的线程必须位于当前的进程中,而且不得是分离线程。所有创建时属性为PTHREAD_CREATE_JOINABLE的非分离thread.最终都需要调用pthread_join() or pthread_detach() 。这样thread所占资源和 Thread ID才被释放。
3. 分离thread:
int pthread_detach(pthread_t thread);
pthread_detach()指出当thread 结束时,thread所占资源和ThreadID会被释放和再利用。如果调用pthread_detach()时,thread没有结束,它并不会导致thread退出。它只对PTHREAD_CREATE_JOINABLE非分离thread有效。
4. 获取threadID:
pthread_t pthread_self(void);
返回调用thread的thread ID.
5. 比较thread ID:
int pthread_equal(pthread_t t1, pthread_t t2);如果 tid1和 tid2 相等,pthread_equal()将返回非零值,否则将返回零。
6. 向thread发信号:
int pthread_kill(pthread_t thread, int sig);
tid 所指定的线程必须与调用线程在同一个进程中。sig 参数必须来自signal(5) 提供的列表。
7.退出线程:
void pthread_exit(void *value_ptr);
pthread_exit()用来终止调用thread并置位value_ptr.这个值会交给pthread_join。
Thread的取消:
同一进程内,某个Thread可以向其它thread发送取消要求,要求目标thread退出运行。
取消请求的处理方式取决于目标线程的状态。状态由以下两个函数确定:pthread_setcancelstate() 和pthread_setcanceltype()。
pthread_setcanceltype()可以将取消类型设置为延迟或异步模式。创建线程时,缺省情况下会将取消类型设置为延迟模式。在延迟模式下,只能在取消点取消线程。在异步模式下,可以在执行过程中的任意一点取消线程。因此建议不使用异步模式。
执行取消操作存在一定的危险。大多数危险都与完全恢复不变量和释放共享资源有关。取消线程时一定要格外小心,否则可能会使互斥保留为锁定状态,从而导致死锁。或者,已取消的线程可能保留已分配的内存区域,但是系统无法识别这一部分内存,从而无法释放它。
如果创建thread时使用缺省设置,则thread可以被取消,并为异步方式,所以向某一thread发送pthread_cancel()后,并不保证什么时候目标thread会被取消。只有当目标thread运行至取消点时才会真正退出。
类似Read,write等阻塞函数可以被看作取消点,但Sam记得并不能保证。所以建议使用手动添加取消点。
pthread_testcancel();
当线程取消功能处于启用状态且取消类型设置为延迟模式时,pthread_testcancel()函数有效。如果在取消功能处于禁用状态下调用pthread_testcancel(),则该函数不起作用。
请务必仅在线程取消操作安全的序列中插入 pthread_testcancel()。
例如:Sam一直以为poll()函数这样的阻赛类函数为cancel点,但其实不是,需要手动添加cancel点。
这是将pthread_join与pthread_cancel搞混了。
thread分离可以在创建时设定,也可以用pthread_detach()在创建后设定。
被设定成分离线程后,表明它在退出thread时会自动回收资源。所以不需要pthread_join.但分离thread完全可以接收pthread_cancel()来退出。
误区2:已经退出的thread,再去对它pthread_cancel()会出错。
不会出错,如果某thread已经退出,再向它发送pthread_cancel().不会出错。但会返回ESRCH。此值为3。
这里显示出:一个thread,不管自身return或pthread_exit().此thread都算停掉了。只是不分离thread需要使用pthread_join来回收资源而已。
注意点1:不管是否分离,主thread先于其它thread退出,都是不可控的。也就是说会不可预知错误。
所以,主thread不要使用return,exit等退出。 而是使用pthread_exit().
主thread使用pthread_exit(). 则会阻赛之,直到所有子thread退出后才退出。
推荐的做法:
常常有这样的需求,一个子thread既需要在某些事件发生时自己退出,也可能被主thread要求退出。
则可以做如下设计:
子thread自己退出时,使用pthread_exit().
其它thread要求它退出时,是用pthread_cancel(). pthread_join().
则当其它thread先要求它退出时,走正常途径,pthread_cancel()导致其退出。pthread_join()确保其退出并回收资源。
当其自动使用pthread_exit()退出时,最终主thread也会调用pthread_cancel(),则返回错误。但pthread_join()则确保回收资源。
pthread系列函数错误码:
大多数系统函数执行正确返回0。否则返回-1。错误码在errno中。所以可以使用perror()来显示错误。
但pthread系列函数却通过返回值传递error code.并不向errno中写入错误码。所以不能使用perror()来查看错误原因。
可以使用strerror(pthread_rel) 来打印错误原因:
iRel_pthread = pthread_create(&mRtid, NULL,thread_Read_Data, this);
if(iRel_pthread != 0)
{