Linux Linux 线程和信号

davidgao · 2017年07月28日 · 6986 次阅读

什么是线程

线程,有时被称为轻量级进程 (Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程 ID,当前指令指针 (PC),寄存器集合和堆栈组成,每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。

同时线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。

一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。因此线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。

什么是信号

信号是一种 IPC 通信的形式,一般在 Unix,类 Unix 或 POSIX 兼容的系统中使用。信号是一种异步通知进程或同进程中某个指定线程的方式。 当信号被发送到进程的时候,操作系统会中断进程的控制流程,并且在执行非原子性的 CPU 指令时可以中断进程。

信号使用的风险(新手坑)

信号处理在存在竞态的,因为信号本身是异步的,在处理一个信号的过程中,令一个信号(甚至肯能是同类型的信号)会被直接发送到进程中请求进程处理。 信号是可以打断系统调用的,不谨慎处理会引起程序自身的混乱,所以进程的信号处理过程,尽量做到没有副作用,也不要使用不可重入的函数。

Linux 的线程

LinuxThreads

在 Linux 的上古时代,Linux 的线程技术和 POSIX 的标准是不同的,它使用自己的 LinuxThreads 库。这会为我们带来什么影响呢?

让我们来回顾一下 LinuxThreads 设计细节的一些基本理念:

  1. 系统必须能够响应终止信号并杀死整个进程。
  2. 以堆栈形式使用的内存回收必须在线程完成之后进行。因此,线程无法自行完成这个过程。
  3. 终止线程必须进行等待,这样它们才不会进入僵尸状态。
  4. 线程本地数据的回收需要对所有线程进行遍历;这必须由管理线程来进行。
  5. 如果主线程需要调用 pthread_exit(),那么这个线程就无法结束。主线程要进入睡眠状态,而管理线程的工作就是在所有线程都被杀死之后来唤醒这个主线程。

为了维护线程本地数据和内存,LinuxThreads 使用了进程地址空间的高位内存(就在堆栈地址之下)。 同步元语是使用信号来实现的。例如,线程会一直阻塞,直到被信号唤醒为止。并且,LinuxThreads 将每个线程都是作为一个具有惟一进程 ID 的进程实现的。LinuxThreads 接收到终止信号之后,管理线程就会使用相同的信号杀死所有其他线程(进程)。 由于异步信号是内核以进程为单位分发的,而 LinuxThreads 的每个线程对内核来说都是一个进程,且没有实现"线程组",因此,某些语义不符合 POSIX 标准,比如没有实现向进程中所有线程发送信号。如果核心不提供实时信号,LinuxThreads 将使用 SIGUSR1 和 SIGUSR2 作为内部使用的 restart 和 cancel 信号,这样应用程序就不能使用这两个原本为用户保留的信号了。在 Linux kernel 2.1.60 以后的版本都支持扩展的实时信号(从_SIGRTMIN 到_SIGRTMAX),因此不存在这个问题。根据 LinuxThreads 的设计,如果一个异步信号被发送了,那么管理线程就会将这个信号发送给一个线程,如果这个线程现在阻塞了这个信号,那么这个信号也就会被挂起,因此某些信号的缺省动作难以在现行体系上实现,比如 SIGSTOP 和 SIGCONT,LinuxThreads 只能将一个线程挂起,而无法挂起整个进程。

LinuxThreads 带来了什么问题

首先我们说下 POSIX 是如何定义多线程的:POSIX 下一个多线程的进程只有一个 PID。 根据上面我们对 LinuxThreads 的描述,我们可以总结出 LinuxThreads 有下面这些问题:

  1. 它使用管理线程来创建线程,并对每个进程所拥有的所有线程进行协调。这增加了创建和销毁线程所需要的开销。
  2. 由于它是围绕一个管理线程来设计的,因此会导致很多的上下文切换的开销,这可能会妨碍系统的可伸缩性和性能。
  3. 由于管理线程只能在一个 CPU 上运行,因此所执行的同步操作在 SMP 或 NUMA 系统上可能会产生可伸缩性的问题。
  4. 由于线程的管理方式,以及每个线程都使用了一个不同的进程 ID,因此 LinuxThreads 与其他与 POSIX 相关的线程库并不兼容。
  5. 信号用来实现同步原语,这会影响操作的响应时间。另外,将信号发送到主进程的概念也并不存在。因此,这并不遵守 POSIX 中处理信号的方法。

我们在这里不关注性能如何只关注 POSIX 兼容和信号处理问题。

NPTL

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

  1. POSIX 兼容性
  2. SMP 结构的利用
  3. 低启动开销
  4. 低链接开销(即不使用线程的程序不应当受线程库的影响)
  5. 与 LinuxThreads 应用的二进制兼容性
  6. 软硬件的可扩展能力
  7. 多体系结构支持
  8. NUMA 支持

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

Linux 线程总结

比较新的 Linux 都已经开始使用 NPTL 了,所以我们可以忽略 LinuxThreads 的存在了,介绍它主要是为了让诸位读者更深入的了解线程和信号的恩恩怨怨(不要丢鸡蛋)。

Linux 的信号

Linux 是如何处理信号的

随着 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

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请 注册新账号