进程和线程[翻译]
进程和线程是OS基本的概念,也是面试热门题目。以前看过这个文章,介绍进程与线程的区别与选择,讲的较浅显,又有一定覆盖面。前两天在某群中鸟哥又推荐了一遍,顺手翻译如下。原文请猛击这里。
===============华丽的分割线===============
什么是Fork?
Fork就是产生一个新的进程, 它和原进程看起来完全一样, 除了有一个新的进程ID, 拥有自己的内存地址空间. 新进程(子进程)和老进程(父进程)共享代码段, 各自独立运行.
Fork最常见的例子就是在shell下, 每当你运行一个命令, shell就会fork一个子进程来执行你的命令(严格来说, 是fork以后紧接着exec)
执行fork系统调用时,操作系统将拷贝父进程的所有page,并加载进独立的内存区域。但某些情况不需要这些page拷贝,比如exec系列的系统调用,因为execv将替换掉父进程的地址空间。
fork需要注意:
- 子进程有自己的独立进程id
- 子进程持有父进程的文件描述符
- 子进程不会继承父进程的文件锁
- 父进程中打开的信号量,在子进程中一样处于开启状态
- 子进程持有父进程的消息队列描述符
- 子进程有自己的地址与内存空间
相对而言fork被更普遍的使用,大致原因如下:
- 基于fork的开发实现更容易
- 更容易维护
- 因为进程在自己独立的虚拟地址空间中运行,所以fork更安全。如果一个进程crash或者缓冲区溢出,不会影响到其他的进程
- 基于线程的代码更难于debug
- fork的移植性更好
- fork在单核cpu上更快,因为没有锁和上下文切换的开销
使用fork的应用有: telnetd(freebsd), vsftpd, proftpd, Apache13, Apache2, thttpd, PostgreSQL.
fork的陷阱
- 每个新进程拥有独立内存地址空间,它带来了更长的启停时间
- 如果使用fork,需要考虑两个进程可能需要交互,而进程间通信的成本很高
- 如果父进程在子进程前退出,子进程将变成孤儿进程。而线程模型中可以简单的结束线程、挂起线程和恢复线程。如果程序退出,所有线程也会自动结束
- 存储空间不足可能导致fork失败
什么是线程?
线程是轻量级进程(LWPS)。一般认为,线程只是CPU状态(和其他一些minimal state),而进程包括其他的数据、堆栈、IO和信号等等。线程的overhead比fork要小,因为系统不需要初始化新的虚拟内存空间。在多核系统上,可以将执行流分配到其他的处理器上,通过并行和分布式处理得到速度提升;在单处理器的系统上,由于存在IO延迟和其他挂起执行的功能,使用线程一样可以获得收益。
同一进程内的线程将共享:
- 进程指令
- 绝大多数数据
- 打开的文件(描述符)
- 信号与信号处理函数
- 当前工作目录
- 用户和组id
每个线程有独立的:
- 线程id
- 寄存器、栈指针
- 栈(本地变量,返回地址)
- 信号mask
- 优先级
- errno返回值(errno是tls存储的)
线程需要注意:
- 在多处理器/多核系统中,线程效率最高
- 只占用一张进程表和一个schedule
- 进程中的所有线程共享相同的地址空间
- 线程不维护创建的线程列表,也不知道是哪个线程创建的自己
- 线程通过共享基本部件来减少overhead
- 线程在内存管理方面效率更高,因为它们使用相同的内存块而不是创建新的
线程的陷阱
- Race conditions:多个线程同时读写相同的数据,但却不知道其他线程存在,这可能导致数据混乱。这种情况我们称之为竞态条件(race conditions)。操作系统会调度线程,而不能确定其运行方式。线程可能不会按照创建的顺序运行;它们也可能以不同的速度运行。当线程执行时,可能会给出预期外的结果。在线程模型下,必须使用锁和join来获取可预测的执行顺序和产出。
- 线程安全代码:在多线程中运行的代码需要是线程安全的。这意味着使用静态变量和全局变量时,不能假设其他线程不会对它进行访问。如果代码使用了静态变量或全局变量,必须使用锁,或者重写函数,以避免使用这些变量。在C语言中,局部变量在栈上动态分析,因此,不使用静态数据和其他共享资源的函数都是线程安全的。程序中,非线程安全函数同一时间只能被一个线程调用,这点必须得到保证。很多不可重入函数返回了指向静态数据的指针,这可以通过返回动态分配的数据或由调用者提供空间的方式来避免。非线程安全函数的一个例子就是strtok,它也是不可重入的。它的可重入版本strtok_r是线程安全的。
线程的优势:
- 线程共享相同的内存空间,因此线程之间共享数据速度很快。
- 如果设计实现较好,使用线程将获得较大的速度提升,因为在多线程程序中没有进程级别的上下文切换。
- 线程启动和结束很快
使用线程的程序:MySQL, Firebird, Apache2, MySQL 323
FAQs
1. 我该使用哪个?
回答:取决于很多因素。fork比thread更重量级,有更高的启停成本。进程间通信(IPC)比线程间通信更困难,也更慢。实际上在通信方面,线程优势很大。但另一方面,一个线程crash后,将导致所有其他线程停止;并且只要一个线程出现缓冲区溢出,就会为所有的线程带来安全问题。
2. 哪个更好?
回答:这完全取决于需要。在当前的linux(2.6.x),进程和线程的切换成本没有太大区别(区别只有thread的MMU)。由于共享地址空间,线程还存在一个问题:一个线程的错误指针,可能导致其他线程的数据错误。
3. 什么时候要用线程或进程?
回答:
如果你想用多线程,那么正确的问题应该是:程序的哪些部分可以/不可以被线程化。以下是一些经验法则:
- 是否存在互不依赖的耗时操作(比如画窗体、打印文档、响应鼠标事件、计算表格的列、信号处理等等)?
- 数据锁不多(指共享数据的量小)?
- 准备好了考虑锁、死锁和竞态条件?
- 任务能否划分?比如是否可以一个线程处理信号,另一个处理GUI?
结论
- 用线程或者fork,取决于应用需求
- 线程更强大,但并不是万能的
- 基于线程比基于fork更难写(也更难维护),只适用于熟手
- 只在极其注重性能的程序中使用线程