1 概述
1.1 线程的定义
传统的UNIX进程概念在开发有分布式系统中的许多应用时已经显得力不从心(有时连简单的窗口响应问题都很难做好)。这些问题的最好解决之道就是线程,线程推 广了进程的概念使一个进程可以包含多个活动(或者说执行序列等等)。如今,由于线程概念的普及,在UNIX系统中已经普遍实现了线程机制,开发并发应用的程序员现在也可以广泛接触到线程的函数库了。
使用线程的优点在于:
A 改进程序的实时响应能力
B 更有效的使用多处理器
C 改进程序结构(多个线程共享同一地址空间,多爽;要是进程就只能通过那些乱七八糟的IPC来通讯)
D 减少对系统资源的使用(线程的切换要比进程快几个数量级,尤其是用户态线程)
[注意]:有的人将线程和轻量级进程(LWP)视为等同,但其实在不同的系统/实现中有不同的解释,LWP更恰当的解释可能为一个虚拟CPU或内核的线程。它可以在帮助用户态线程实现一些特殊的功能。但具体我也不清楚。
1.2 线程的实现
目前线程用两种方法实现:
(1)用户态线程:
由于内核并没有对多线程进程的支持,因此,内核中只有单线程进程的概念, 而多线程进程是通过一个和应用程序连接的函数库实现的。由于内核没有轻量 级进程(线程)的概念,因此它不能独立的对之进行调度,而是由一个线程运 行库来组织线程的调度,其主要工作在于在各个线程的栈之间调度。如果一个进程中的某一个线程调用了一个阻塞的系统调用,该进程就会被阻塞,当然该进程中的其他所有线程也同时被阻塞,因此UNIX使用了异步I/O机制。
这种机制主要的缺点在于在一个进程中的多个线程的调度中无法发挥多处理器的优势(如上述的阻塞情况)。
其优点包括:
A (相对于进程操作而言)某些线程操作的系统消耗大大减少。比如,对属于同一个进程的线程之间进行调度切换时不需要调用系统调用,因此将减少额 外的消耗,往往一个进程可以启动上千个线程也没有什么问题。
B 用户态线程的实现方式可以被定制或修改以适应特殊应用的要求。这对于多 媒体实时过程等尤其有用。另外,用户态线程可以比核心态线程实现方法的默认情况支持更多的线程。
(2)核心态线程
这种线程的实现方法允许不同进程中的线程按照同一相对优先调度方法进行调 度。这有利于发挥多处理器的并发优势。
目前线程主要的实现方法是用户态线程。有几个研究项目已经实现了一些核心态线程的形式。其中比较著名的是MACH分布式操作系统。通过允许用户代码对内核线程调度的参与,该系统将用户态和核心态两种线程实现方法的优点结合 了起来。通过提供这样一个两级调度机制,内核在保留了对处理器时间分配的 控制的同时,也使一个进程可以充分利用多处理器的优势。
1.3 线程库
POSIX线程库和Solaris线程库是目前使用最广泛的线程库。这两种实现方法都是可内操作的(INTER-OPERABLE),它们的功能相似,并可以用在同样的应用中。但是 只有采用POSIX标准的线程才能确保可以完全的移植到其他符合POSIX标准的环境中 。
两者相似处:
这两个库--libpthread和libthread--中的大部分函数都是相互对应的。 往往具有相似后缀名的的POSIX和SOLARIS函数就具有相似的功能、参数个数以及参数的作用。所有POSIX线程库的函数都以pthread为前缀,而所有SOLARIS线程库函数都以thr为前缀。
两者不同处:
POSIX
A 可移植性更好;
B 对每一个线程进行配置;
C 实现了线程取消;
D 强迫调度算法;
E 允许clean-up handlers for fork(2)调用;
Solaris
A 线程可以被挂起和继续;
B 优化的互斥和读写锁定;
C 可以提高并发性;
D 实现了守护线程,其进程不等待其死亡
1.4 线程标准
现在有三种不同的线程库的定义,其中每一种都想成为标准:WIN32,OS/2和POSIX。其中前两种都是专用的,只能用在它们各自的平台上(WIN32线程仅能运行于WINNT和WIN9X平台上,OS/2线程运行于OS/2平台上)。POSIX规范(IEEE1003.1caka Pthreads)则是适用于各种平台,而且已经或正在在所有主要的UNIX系统(包括LINUX)上实现,也包括VMS平台。
POSIX线程:
POSIX标准规定了所有线程库必须符合的标准,包括API和其相应的动作。它是POSIX扩展的一部分,因此它并不是XPG4标准所必需的,但它是X/OPEN UNIX98所要求的,而目前所有主要的UNIX生产商都遵从此标准。就在本文写作时,几乎所有UNIX生产商都发布了相应的线程库。
WIN32线程:
无论NT还是OS/2的实现都存在着和POSIX标准的一些基本性的不同之处--以至于无论从两者中任何一者移植到POSIX上都被证明存在相当难度。微软至今没有任何采用POSIX标准的计划。目前有一些自由共享的WIN32平台上的POSIX库,在OS/2平台上也有可供选择的POSIX库。
DCE线程:
在POSIX完成它的标准指定工作之前,它发布了一些仅供参考的草案。其中的草案4就被用作DCE线程库的基本内容。该草案和最后发布的标准基本相似,但缺少一些重要的不同。可能是因为现在已经没有人在为DCE写代码了。
SOLARIS线程:
也被称为UI线程,它是太阳微系统公司在POSIX委员会完成标准指定工作之前开发SOLARIS2所采用的线程库。尽管预计如今大部分程序员都将采用POSIX标准,但SOLARIS线程在一个相当长的时间内还将存在于SOLARIS2系统上。SOLARIS线程和POSIX线程两者的决大部分实质上是相同的。
1.5 LINUX线程的思想及特点
在INRIA(位于法国巴黎)的Xavier Leroy,以及Pavel Krauz,Richard Henderson和另外一些人,已经开发出了LINUX下的一种线程库,该线程库实现了一个叫“一对一 ”模型(One-to-One), 它可以利用多处理器。该库基于LINUX的一个新的系统调用--clone()(也就是说它只能用在LINUX上了)。该调用实现于LINUX2.0或以上版本,并可以运行于Intel,Alpha SPARC,m68k以及MIPS等处理器的机器上。其一个缺点在于它对信号的非标准处理上。
LinuxThreads采用称为1-1模型:每个线程实际上在核心是一个个单独的进程,核心的调度程序负责线程的调度,就象调度普通进程。线程是用系统调用clone()创建的,clone()系统调用是fork()的推广形式,它允许新进程共享父进程的存储空间、文件描述符和信号处理程序。
“一对一”模型的优点在于:
A 最小限度消耗的CPU级多处理技术(每个CPU一个线程);
B 最小限度消耗的I/O操作;
C 一种简单和强壮的实现(核心调度程序为我们做了大部分艰难的工作)。
该模型主要的缺点在于在互斥和条件操作时的环境切换的系统消耗更大,而该切换是通过内核来进行的。但是由于LINUX内核对环境切换的处理相当有效,因此在一定程度上弥补了这个缺点。
除了“一对一”模型之外还有两种基本模型。
“多对一”模型基于用户级的调度,线程切换完全由用户程序完成;从核心角度看,只有一个进程正在运行。这种模型不是我们所关心的,因为它无法利用多处理器的优点,而且要用不合理的方法处理I/O操作阻塞。目前LINUX上存在有数个用户态的线程库,但我发现它们在功能、性能以及强壮性上都存在缺陷。
“多对多”模型结合了核心态和用户态的调度:数个核心态线程并发的执行,每个都作为一个用户态线程的调度者对用户态线程进行调度。大多数商业版本的UNIX(SOLARIS,DIGITAL UNIX和IRIX)都采用此模型实现POSIX线程标准。该模型结合了“多对一”和“一对一”模型的优点,而且由于它能够避免另外两种模型缺陷,尤其是当内核在处理环境切换时效率较低时,比如DIGITAL UNIX,因而相当具有吸引力。但不幸的是,这种模型的实现相当复杂,而且需要LINUX内核所不能提供的内核功能。Linus Torvalds和其它Linux内核开发者一直以来都在全面简单化的原则下推
动“一对一”模型,并且他们大大的提高了Linux内核线程切换的效率。Linux线程遵从了他们一直以来的原则。
2 Linux核心对线程的支持
Linux核心对线程的支持主要是通过其系统调用,下文将进行系统的介绍。
2.1 系统调用clone()
以下是系统调用clone的代码:
asmlinkage int sys_clone(struct pt_regs regs)
{
unsigned long clone_flags;
unsigned long newsp;
clone_flags = regs.ebx;
newsp = regs.ecx;
if (!newsp)
newsp = regs.esp;
return do_fork(clone_flags, newsp, ®s);
}
与系统调用clone功能相似的系统调用有fork,但fork事实上只是clone的功能的一部分,clone与fork的主要区别在于传递了几个参数,而当中最重要的参数就是conle_flags,下表是系统定义的几个clone_flags标志:
标志 Value 含义
CLONE_VM 0x00000100 置起此标志在进程间共享地址空间
CLONE_FS 0x00000200 置起此标志在进程间共享文件系统信息
CLONE_FILES 0x00000400 置起此标志在进程间共享打开的文件
CLONE_SIGHAND 0x00000800 置起此标志在进程间共享信号处理程序
如果置起以上标志所做的处理分别是:
置起CLONE_VM标志:
mmget(current->mm);
/*
* Set up the LDT descriptor for the clone task.
*/
copy_segments(nr, tsk, NULL);
SET_PAGE_DIR(tsk, current->mm->pgd);
置起CLONE_ FS标志:
atomic_inc(¤t->fs->count);
置起CLONE_ FILES标志:
atomic_inc(&oldf->count);
置起CLONE_ SIGHAND标志:
atomic_inc(¤t->sig->count);
2.2 与线程调度相关的系统调用
以下是glibc-linuxthread用来进行调度的系统调度:
.long SYMBOL_NAME(sys_sched_setparam) /* 系统调用154 */
/*用来设置进程(或线程)的调度参数*/
.long SYMBOL_NAME(sys_sched_getparam)
/*用来获取进程(或线程)的调度参数*/
.long SYMBOL_NAME(sys_sched_setscheduler)
/*用来设置进程(或线程)的调度参数*/
.long SYMBOL_NAME(sys_sched_getscheduler)
/*用来获取进程(或线程)的调度参数*/
.long SYMBOL_NAME(sys_sched_yield)
/*用来强制核心重新调度进程(或线程)*/
.long SYMBOL_NAME(sys_sched_get_priority_max)
/*用来设置进程(或线程)的调度参数*/
.long SYMBOL_NAME(sys_sched_get_priority_min)
/*用来获取进程(或线程)的调度参数*/
.long SYMBOL_NAME(sys_sched_rr_get_interval) /* 系统调用161 */
/*用来获取进程(或线程)的调度时间间隔*/
3 Linux线程的实现
现在的0.8版LinuxThreads,是迄今为止在Linux下支持threads的最好的Runtime-library,而包含0.8版LinuxThreads的最好的Runtime-library 是glibc- 2.1,下文所要分析的正是glibc-linuxthreads-2.1。
首先介绍一下0.8版LinuxThreads,它实现了一种BiCapitalized面向
Linux的Posix 1003.1c"pthread"标准接口。LinuxThreads提供核心级线程即每个线程是一个独立的UNIX进程,通过调用新的系统调用与其它线程共享地址空间。线程由核心调度,就象UNIX进程调度一样。使用它的要求是:LINUX 版本2.0 或以上(要求有新的clone() 系统调用和新的实时调度程序)。对于Intel平台:要求有libc 5.2.18或后续版本,推荐使用
5.2.18 或 5.4.12 及其后续版本;5.3.12和5.4.7有问题,也支持glibc 2 ,实际上是支持它的一个特别合适的版本。到目前支持Intel, Alpha, Sparc , Motorola 68k, ARM and MIPS平台,还支持多处理器
4 问题及发展
4.1LinuxThread与POSIX标准
4.1.1 已知的漏洞和局限
A 没有共享的是它们的进程号和父进程号。根据标准应该相同,但这是我们没有实现的(直到clone()的CLONE_PID标志可用)。在最近的内核中(最近的2.1版本和即将发布的2.2版本),超过32个信号是作为实时信号提供的。当运行在这些内核上时,LinuxThreads使用两个保留的实时信号为内部操作用,因而留下两个空余信号SIGUSR1和SIGUSR2给用户代码用。线程共享进程号会使/proc出现问题并且信号可能会错传到父进程那里。现在,实现时使用了两个信号SIGUSR1和SIGUSR2,所以用户不能使用它们。理论上,应该为库 保留两个信号,但是大概2.1.60版本以上就没有这个问题了。
B 线程栈分配在高端存储空间,在初始进程的堆栈下2M远处。采用"按需增长" 策略,所以初始化时不会使用很多虚拟空间(现在是4K),如果需要可以增长到2M。为每个线程保留这么大的地址空间意味着,在32位体系结构下,每 个进程不能有超过大约1023个线程共存(假设每个用户进程有2GB地址空间)
,但是这是合理的,因为每个线程使用核心的进程表的一项,而它通常限制为512项。
C “按需增长”的潜在问题是无法防止用户映射某些数据到为线程栈保留的2M 地址,这可能导致以后的堆栈扩展失败。在使用这个库的时候,应该避免在固定的地址上映射。
D 信号处理不是与Posix标准完全一致,基于一个事实即线程是可以能够单独发送信号的特殊进程,所以没有发送一个信号到这个进程(所有线程的集合)
的概念。更精确的说,如下是标准的要求和实现如何满足它们的:(括号中是LINUX的具体处理方法)
1-同步信号(比如SIGFPE,是由线程的执行产生的)被传送到产生它们的线程那里(符合标准)。
2-一个致命的异步信号将终止进程中的所有线程。(符合标准。线程处理器一旦发现某一个线程因一个信号而终止,它将同时终止接收到此信号的其 它线程。)
3-一个异步信号将被送给程序的某一个没有阻塞该信号的线程(未指定具体是哪一个)。(不符合标准,信号只会根据进程ID发送给相应的线程,如果该线程正在阻塞该信号则该信号被阻塞。)
4-信号将被发送给至少一个线程。(符合标准。对于由终端产生或发送给进程组的信号,它们将被发送给所有符合条件的线程。)
E 目前对MIPS支持的实现是基于MIPS ISA II或更高级的处理器的。这些处理器通过ll/sc指令支持原子操作。而老式的R2000/R3000系列的处理器目前还不被支持,支持这些老式处理器需要更大的开销。
F 目前对ARM系列处理器的支持的实现中假设它们都有SWP指令(内存原子交换登记)。但实际是ARM1和ARM2处理器并不支持此指令。在StrongARM处理器中,SWP指令没有绕过CACHE,因此实现对多处理器的支持将会比较复杂。
4.1.2 进程共享的互斥、条件和信号量
为什么Linux线程没有实现进程共享的互斥、条件和信号量?
这是POSIX标准的一个可选部分。可移植的程序在使用此机制之前必需检查宏_POSIX_THREAD_PROCESS_SHARED是否存在。
该扩展标准的目的是使不同的进程(也就是说在不同的地址空间中)可以通过在共享内存(无论是SRV4的共享内存段还是用mmfile()产生的内存文件)中的互斥、条件和信号量来实现进程间的同步。
在Linux线程中没有实现此功能的原因是在Linux中互斥、条件和信号并不是独立的:它们的等待队列包含着指向线程描述符连接表的指针,而这些指针只在特定的地址空间才有效。
Matt Messier和Sean Walton花了相当长的时间来设计一个在进程间共享等待队列的适合的机制。我们得到了数个可以将下列三项特点中的两项结合起来的解决方案,但没有一个可以将三者结合起来:
* 允许不同UID的进程之间的共享;
* 支持取消操作;
* 支持pthread_cond_timedwait
由此我们知道进程间共享互斥、条件和信号量需要内核的某些支持(而目前并不支持)。这也许是Linus Torvalds的直觉“在内核中我们只需要clone()”的失败之处之一。
在内核提供对它们的支持之前,你最好使用传统的进程间通讯方式来同步进程:
SYSTEM V信号量和消息队列,或管道或SOCKETS。
4.2 Linux线程展望
未来的目标是全面的兼容POSIX标准和尽可能实现POSIX标准中可选的功能,并进一步提高LinuxThreads的效率。我相信这一天很快就会到来。
---pcjockey@21cn.com
综合了一些资料加上自己的一些解释
引用包括:
〈Write Multi-thread Code Solaris> -- 来自SUN的网站
〈Beyond Multiprocessing ...Multithreading the SunOS Kernel〉
-- J. R. Eykholt, S. R. Kleiman, S. Barton,
R. Faulkner, A. Shivalingiah, M. Smith,
D. Stein, J. Voll, M. Weeks, D. Williams ?
SunSoft, Inc.