###1. 多进程 / 多线程
很多需要并发处理的任务,如数据库的服务器端、网络服务器等, 可以使用多进程来实现, 也可以使用多线程来实现
使用多进程, 存在如下的优缺点:
- fork一个子进程的消耗是很大的,即使使用现代的写时复制(copy-on-write)技术。
- 各个进程拥有自己独立的地址空间,进程间的协作需要复杂的IPC技术,如pipe,共享内存, unix socket等
- 多进程可以避免内存泄漏
- 多进程可以实现特权分离
使用多线程, 则存在如下的优缺点
- 建立新的线程的开销较小
- 线程共享进程的代码可全局数据, 线程间通信方便
- 线程共享进程的代码可全局数据, 可能存在竞争, 需要同步
###2. 用户空间线程
这里的用户线程指的是完全建立在用户空间的线程库,用户线程的建立,同步,销毁,调度完全在用户空间完成,不需要内核的帮助, 内核并没有直接对用户线程进程调度,内核的调度对象和传统进程一样,还是进程本身,内核并不知道用户线程的存在,用户线程之间的调度由在用户空间实现的线程库实现
因为用户空间线程完全在用户态实现线程,因此也就和具体的内核没有什么关系,可移植性和扩展性方面比较好
用户空间线程的缺点是一个用户线程如果阻塞在系统调用中,则整个进程都将会阻塞
###3. 内核空间线程
内核空间线程是指由内核来负责建立,同步, 销毁, 调度的线程,因为内核空间实现的线程由内核负责调度, 因此它能够利用多处理器的优势, 并且, 某一个线程阻塞不会导致其所属的进程阻塞
###4. 轻量级进程LWP
LWP本质仍然是进程,与普通进程相比:
- LWP与其它进程共享所有(或大部分)逻辑地址空间和系统资源
- 一个进程可以创建多个LWP,这样它们共享大部分资源
- LWP有它自己的进程标识符,并和其他进程有着父子关系;这是和类Unix操作系统的系统调用vfork()生成的进程一样的
- LWP由内核管理并像普通进程一样被调度
Linux内核是支持LWP的典型例子。Linux内核在 2.0.x版本就已经实现了轻量进程,应用程序可以通过一个统一的clone()系统调用接口,用不同的参数指定创建轻量进程还是普通进程,通过参数决定子进程和父进程共享的资源种类和数量,这样就有了轻重之分。在内核中, clone()调用经过参数传递和解释后会调用do_fork(),这个核内函数同时也是fork()、vfork()系统调用的最终实现。
在大多数系统中,LWP与普通进程的区别也在于它只有一个最小的执行上下文和调度程序所需的统计信息,而这也是它之所以被称为轻量级的原因
使用用轻量级进程去模拟线程。使得线程和进程的概念混淆了,有人说系统调度单位是进程,又有人说是线程,其实系统调度的单位一直就没有改变,只是后来部分线程和进程的界限模糊了
###5. linux 线程
linux内核并没有线程的概念. 每一个执行实体都是一个task_struct结构, 通常称之为进程. Linux内核在 2.0.x版本就已经实现了轻量进程, 后来为了引入多线程,Linux2.0~2.4实现的是俗称LinuxThreads的多线程方式,到了2.6,基本上都是NPTL的方式了
####5.1 LinuxThreads
linux 2.6以前, pthread线程库对应的实现是一个名叫linuxthreads的lib.这种实现本质上是一种LWP的实现方式,即通过轻量级进程来模拟线程,一个LWP对应一个线程。这个模型最大的好处是线程调度由内核完成了,而其他线程操作(同步、取消)等都是核外的线程库函数完成的, 内核并不知道有线程这个概念,在内核看来,都是进程, 而在用户看来, 每一个task_struct就对应一个线程
一组线程以及它们所共同引用的一组资源就是一个进程.但是, 一组线程并不仅仅是引用同一组资源就够了, 它们还必须被视为一个整体, POSIX标准提出了一系列的要求, 例如:
1, 查看进程列表的时候, 相关的一组task_struct应当被展现为列表中的一个节点; 2, 发送给这个”进程”的信号(对应kill系统调用), 将被对应的这一组task_struct所共享, 并且被其中的任意一个”线程”处理; 3, 发送给某个”线程”的信号(对应pthread_kill), 将只被对应的一个task_struct接收, 并且由它自己来处理; 4, 当”进程”被停止或继续时(对应SIGSTOP/SIGCONT信号), 对应的这一组task_struct状态将改变; 5, 当”进程”收到一个致命信号(比如由于段错误收到SIGSEGV信号), 对应的这一组task_struct将全部退出;
事实上, linuxthreads仅仅只是实现了第5点, 它是通过管理线程来实现的,即为每一个进程构造了一个管理线程,负责处理线程相关的管理工作。当进程第一次调用pthread_create()创建一个线程的时候就会创建并启动管理线程。然后管理线程再来创建用户请求的线程。也就是说,用户在调用pthread_create后,先是创建了管理线程,再由管理线程创建了用户的线程, 即管理线程是除了主线程之外的所有其它线程的父进程, 当管理线程检测到主线程或者其它线程非正常退出时, 就会杀死所有的线程, 然后自行退出
另外,这种通过LWP的方式来模拟线程的实现还存在一些比较严重的问题:
- 线程ID和进程ID的问题
- 按照POSIX的定义,同一进程的所有的线程应该共享同一个进程和父进程ID,而Linux的这种LWP方式显然不能满足这一点
- 信号处理问题
- 异步信号是以进程为单位分发的,而Linux的线程本质上每个都是一个进程,且没有进程组的概念,所以某些缺省信号难以做到对所有线程有效,例如SIGSTOP和SIGCONT,就无法将整个进程挂起,而只能将某个线程挂起
- 线程总数问题
- LinuxThreads将每个进程的线程最大数目定义为1024,但实际上这个数值还受到整个系统的总进程数限制,这又是由于线程其实是核心进程
- 管理线程问题
- 管理线程容易成为瓶颈,这是这种结构的通病;同时,管理线程又负责用户线程的清理工作,因此,尽管管理线程已经屏蔽了大部分的信号,但一旦管理线程死亡,用户线程就不得不手工清理了,而且用户线程并不知道管理线程的状态,之后的线程创建等请求将无人处理
- 同步问题
- LinuxThreads中的线程同步很大程度上是建立在信号基础上的,这种通过内核复杂的信号处理机制的同步方式,效率一直是个问题
- 其他POSIX兼容性问题
- Linux中很多系统调用,按照语义都是与进程相关的,比如nice、setuid、setrlimit等,在目前的LinuxThreads中,这些调用都仅仅影响调用者线程
- 实时性问题
- 线程的引入有一定的实时性考虑,但LinuxThreads暂时不支持,比如调度选项,目前还没有实现。不仅LinuxThreads如此,标准的Linux在实时性上考虑都很少
####5.2 NPTL
到了linux 2.6, glibc中有了一种新的pthread线程库–NPTL(Native POSIX Threading Library).本质上来说,NPTL还是一个LWP的实现机制,但相对原有LinuxThreads来说,做了很多的改进,NPTL实现了前面提到的POSIX的全部5点要求. 但是, 实际上, 与其说是NPTL实现了, 不如说是linux内核实现了这5点要求
- 在linux 2.6中, 内核有了线程组的概念, task_struct结构中增加了一个tgid(thread group id)字段, 如果这个task是一个”主线程”, 则它的tgid等于pid, 否则tgid等于进程的pid(即主线程的pid)
- 在clone系统调用中, 传递CLONE_THREAD参数就可以把新进程的tgid设置为父进程的tgid(否则新进程的tgid会设为其自身的pid
- 关联进程组和会话
- task->signal->pgid保存进程组的打头进程的pid
- task->signal->session保存会话 打头进程的pid
有了tgid, 内核或相关的shell程序就知道某个tast_struct是代表一个进程还是代表一个线程, 也就知道在什么时候该展现它们, 什么时候不该展现(比如在ps的时候, 线程就不要展现了).
getpid(获取进程ID)系统调用返回的也是tast_struct中的tgid(进程id), 而tast_struct中的pid(线程id)则由gettid系统调用来返回
为了应付”发送给进程的信号”和”发送给线程的信号”, task_struct里面维护了两套signal_pending, 一套是线程组共享的, 一套是线程独有的 :
- 通过kill发送的信号被放在线程组共享的signal_pending中, 可以由任意一个线程来处理
- 通过pthread_kill发送的信号(pthread_kill是pthread库的接口, 对应的系统调用中tkill)被放在线程独有的signal_pending中, 只能由本线程来处理
当线程停止/继续, 或者是收到一个致命信号时, 内核会将处理动作施加到整个线程组中