什么是线程

线程,有时被称为轻量级进程(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吧。

信号SIGSEGV

这个信号,也许是大家最不想见到,为什么呢?我们看这个信号的定义:

当当前程序对内存的引用无效时,就会产生当前信号,也就是我们常说的“段违例”。

以下几种情况会产生该信号:

1.进程引用的内存页面不存在(例如,该页面位于堆和栈之间的映射的区域) 2.进程试图更新只读内存页(例如,程序文本段或已经被标记为只读的内存映射区域) 3.进程试图在用户态去访问内核部分的内存

好了,我们都知道这个信号引发的结果就是进程退出。不过我们都忽视了一个问题,在现代的Linux上,按照POSIX的定义,这个信号是系统产生的线程级别的信号。换句话说,如果某个线程A出现了内存引用无效,那么产生的信号,会投递到线程A的信号队列中,而不是像进程级别的信号无法确定接受者是谁。

JVM的安全区域

如果我们想让所有Java线程停下来的时候,在JVM的JavaThread执行到大家所知道的test 特定页面的指令时,就会因为更新不可读页面而触发SIGSEGV信号。那么对于那些正在执行native代码的JavaThread该怎么办,JVM中的注释写的非常清楚,native返回JVM时会检查是否能返回的。

好了再多说一句,JVM是如果将特定内存保护起来的呢?这个需要看操作系统的API了,在Linux中是mprotect。

总结

多读读POSIX标准和Intel的CPU体系结构,会让自己在开发变的容易些。

Linux 的信号和线程的更多相关文章

  1. linux有关信号的FAQ

    1.为什么会出现系统调用被中断的情况? 进程在执行一个低速系统调用而阻塞期间捕捉到一个信号时,该系统调用就被中断不再继续执行.该系统调用返回出错,其errno被设置为EINTR.这样处理的理由是:因为 ...

  2. [转载]了解Linux的进程与线程

    本文转自Tim Yang的博客http://timyang.net/linux/linux-process/ .对于理解Linux的进程与线程非常有帮助.支持原创.尊重原创,分享知识! 上周碰到部署在 ...

  3. Linux多线程实践(1) --线程理论

    线程概念 在一个程序里的一个执行路线就叫做线程(thread).更准确的定义是:线程是"一个进程内部的控制序列/指令序列"; 一切进程至少有一个执行线程; 进程  VS. 线程  ...

  4. linux平台,对线程等待和唤醒操作的封装(pthread_cond_timedwait 用法详解)

    前言 linux平台下,线程等待和唤醒操作是很常见的,但是平台函数不易使用:笔者对此操作做了封装,使之更易于使用. 线程等待和唤醒函数比较 平台提供了线程等待相关函数,这些函数之间用法也有些差异: s ...

  5. linux 条件变量与线程池

    条件变量Condition Variables 概述 1. 条件变量提供了另外一种线程同步的方式.如果没有条件变量,程序需要使用线程连续轮询(可能在临界区critical section内)方式检查条 ...

  6. linux查看进程的线程数

    top -H -p $PID  #查看对应进程的那个线程占用CPU过高 1.top -H 手册中说:-H : Threads toggle 加上这个选项启动top,top一行显示一个线程.否则,它一行 ...

  7. 了解Linux的进程与线程

    了解Linux的进程与线程 http://timyang.net/linux/linux-process/ 上周碰到部署在真实服务器上某个应用CPU占用过高的问题,虽然经过tuning, 问题貌似已经 ...

  8. linux 进程信号集合 sigset_t

    sigset_t 号集及信号集操作函数:信号集被定义为一种数据类型: typedef struct { unsigned long sig[_NSIG_WORDS]: } sigset_t 信号集用来 ...

  9. linux下将不同线程绑定到不同core和cpu上——pthread_setaffinity_np

    =============================================================== linux下的单进程多线程的程序,要实现每个线程平均分配到多核cpu,主 ...

随机推荐

  1. Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'userController' method

    在使用SpringMVC的时候遇到了这个问题 问题原因:  在指定方法所对应的url地址的时候重复了, 也就是@RequestMapping("url")中, 两个方法使用了同一个 ...

  2. 前端vue 里的tab切换 减少dom操作

    <div class="vuedemo"> <div class="all"> <div class="tabone&q ...

  3. HDU - 4811 - Ball (思维)

    题意: 给出一定数量的三种颜色的球,计算如何摆放得到值最大(有一定顺序) 有三种摆放方法 1.如果放的是第一个(桌子上原来没有),数值不变 2.如果在末尾追加一个,那么增加前面不同颜色的个数的值 3. ...

  4. 【图论 搜索】bzoj1064: [Noi2008]假面舞会

    做到最后发现还是读题比赛:不过还是很好的图论题的 Description 一年一度的假面舞会又开始了,栋栋也兴致勃勃的参加了今年的舞会.今年的面具都是主办方特别定制的.每个参加舞会的人都可以在入场时选 ...

  5. Python变量、常量、数据类型

    1. 变量 变量是一种使用方便的占位符,用于引用计算机内存地址,该地址可以存储Script运行时可更改的程序信息.例如,可以创建一个名为ClickCount的变量来存储用户单击Web页面上某个对象的次 ...

  6. Vuex 实际使用中的一点心得 —— 一刷新就没了

    问题 在开发中,有一些全局数据,比如用户数据,系统数据等.这些数据很多组件中都会使用,我们当然可以每次使用的时候都去请求,但是出于程序员的"洁癖"."抠"等等优 ...

  7. if else elif 用法和区别

    1.If语句:“如果条件为真,执行子句中的代码."始终包含以下部分: if关键字: 条件(即求值为True或False的表达式): 冒号: 在下一行开始,缩进的代码块(称为if子句) 例如: ...

  8. Spring核心技术(十一)——基于Java的容器配置(一)

    基本概念: @Bean和@Configuration Spring中新的基于Java的配置的核心就是支持@Configuration注解的类以及@Bean注解的方法. @Bean注解用来表示一个方法会 ...

  9. 使用 RMAN 同步数据库

    使用 RMAN同步数据库 使用 RMAN 同步数据库 一.概述 二 操作步骤 (一).把生产库置为归档模式 (二).启动rman做数据库0级备份 (三).修改生产库数据库到未归档 (四).拷贝备份集到 ...

  10. 【10】css hack原理及常用hack

    [10]css hack原理及常用hack 原理:利用不同浏览器对CSS的支持和解析结果不一样编写针对特定浏览器样式.常见的hack有1)属性hack.2)选择器hack.3)IE条件注释 IE条件注 ...