线程,有时被称为轻量级进程 (Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程 ID,当前指令指针 (PC),寄存器集合和堆栈组成,每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
同时线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。
一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。因此线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。
信号是一种 IPC 通信的形式,一般在 Unix,类 Unix 或 POSIX 兼容的系统中使用。信号是一种异步通知进程或同进程中某个指定线程的方式。 当信号被发送到进程的时候,操作系统会中断进程的控制流程,并且在执行非原子性的 CPU 指令时可以中断进程。
信号处理在存在竞态的,因为信号本身是异步的,在处理一个信号的过程中,令一个信号(甚至肯能是同类型的信号)会被直接发送到进程中请求进程处理。 信号是可以打断系统调用的,不谨慎处理会引起程序自身的混乱,所以进程的信号处理过程,尽量做到没有副作用,也不要使用不可重入的函数。
在 Linux 的上古时代,Linux 的线程技术和 POSIX 的标准是不同的,它使用自己的 LinuxThreads 库。这会为我们带来什么影响呢?
让我们来回顾一下 LinuxThreads 设计细节的一些基本理念:
为了维护线程本地数据和内存,LinuxThreads 使用了进程地址空间的高位内存(就在堆栈地址之下)。 同步元语是使用信号来实现的。例如,线程会一直阻塞,直到被信号唤醒为止。并且,LinuxThreads 将每个线程都是作为一个具有惟一进程 ID 的进程实现的。LinuxThreads 接收到终止信号之后,管理线程就会使用相同的信号杀死所有其他线程(进程)。 由于异步信号是内核以进程为单位分发的,而 LinuxThreads 的每个线程对内核来说都是一个进程,且没有实现"线程组",因此,某些语义不符合 POSIX 标准,比如没有实现向进程中所有线程发送信号。如果核心不提供实时信号,LinuxThreads 将使用 SIGUSR1 和 SIGUSR2 作为内部使用的 restart 和 cancel 信号,这样应用程序就不能使用这两个原本为用户保留的信号了。在 Linux kernel 2.1.60 以后的版本都支持扩展的实时信号(从_SIGRTMIN 到_SIGRTMAX),因此不存在这个问题。根据 LinuxThreads 的设计,如果一个异步信号被发送了,那么管理线程就会将这个信号发送给一个线程,如果这个线程现在阻塞了这个信号,那么这个信号也就会被挂起,因此某些信号的缺省动作难以在现行体系上实现,比如 SIGSTOP 和 SIGCONT,LinuxThreads 只能将一个线程挂起,而无法挂起整个进程。
首先我们说下 POSIX 是如何定义多线程的:POSIX 下一个多线程的进程只有一个 PID。 根据上面我们对 LinuxThreads 的描述,我们可以总结出 LinuxThreads 有下面这些问题:
我们在这里不关注性能如何只关注 POSIX 兼容和信号处理问题。
LinuxThreads 的问题,特别是兼容性上的问题,严重阻碍了 Linux 上的跨平台应用(如 Apache)采用多线程设计,从而使得 Linux 上的线程应用一直保持在比较低的水平。在 Linux 社区中,已经有很多人在为改进线程性能而努力,其中既包括用户级线程库,也包括核心级和用户级配合改进的线程库。目前最为人看好的有两个项目,一个是 RedHat 公司牵头研发的 NPTL(Native Posix Thread Library),另一个则是 IBM 投资开发的 NGPT(Next Generation Posix Threading),二者都是围绕完全兼容 POSIX 1003.1c,同时在核内和核外做工作以而实现多对多线程模型。这两种模型都在一定程度上弥补了 LinuxThreads 的缺点,且都是重起炉灶全新设计的。 NPTL 的设计目标归纳可归纳为以下几点:
在技术实现上,NPTL 仍然采用 1:1 的线程模型,并配合 glibc 和最新的 Linux Kernel2.5.x 开发版在信号处理、线程同步、存储管理等多方面进行了优化。和 LinuxThreads 不同,NPTL 没有使用管理线程,核心线程的管理直接放在核内进行,这也带了性能的优化。
比较新的 Linux 都已经开始使用 NPTL 了,所以我们可以忽略 LinuxThreads 的存在了,介绍它主要是为了让诸位读者更深入的了解线程和信号的恩恩怨怨(不要丢鸡蛋)。
随着 Linux 的内核版本不断提升,Linux 的信号现在已经可以按照线程级别的触发了,换句话说就是,每个线程可以关注自己的信号了,并且可以区别性对待了。那我们需要注意什么呢?
在多线程应用中,我们应当使用 sigaction 来代替 singal 函数,因为按 POSIX 的说法 singal 函数并没有明确定义自己在多线程应用中的行为。
可以使用 pthread_sigmask 来为每个线程设置独立的信号掩码。同时在多线程应用中应当避免使用 sigprocmask 这个函数,原因也是 POSIX 中该函数并没有明确定义自己在多线程应用中的行为。
这个时候,有人会产生疑问了,那么多线程下 kill 发出的进程级别的信号 A 怎么办?Linux 是这样解决的,它会把这个信号交付给任意一个没有屏蔽信号 A 的线程。如果这信号没有被任何线程设置 handler 进行处理,就会触发 POSIX 规定的默认动作。
接着有人就会问,我怎么向某个线程发消息呢,POSIX 为我们准备了 pthread_kill 函数,我们可以直接向特定的线程发送消息。那么如果一个线程收到信号 A,但是自己没有安装 handler 会发生什么?其实和进程级别的信号处理方法一样,直接触发默认动作,同样会结束整个进程。
在具有事件循环的应用中,在信号的的 handler 中,可以将信号直接放入程序的队列中,立刻返回。这样直到线程从程序的队列中取出这个信号为止,整个线程看起来就像没有“中断”。 如果不知道该怎么做,去看看著名的 libev 吧。
多读读 POSIX 标准和 Intel 的 CPU 体系结构,会让自己在开发变的容易些。
转自TechTalk