进程和线程是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更难写(也更难维护),只适用于熟手
- 只在极其注重性能的程序中使用线程
OPROFILE是一个开源的profiling工具,类似于vtune和gprof。但不需要像gprof一样,必须优雅退出才可以剖分。今天在看HandlerSocket文章时,想重复一下作者的oprofile结果,因此,安装试用了一下,遇到一些问题,过程记录如下。
1. 连到服务器开发机上,发现已经安装了oprofile。尝试初始化: sudo opcontrol –init 出现错误:kernel doesn’t support oprofile. 看着很吓人,以为需要重新编译内核。但仔细看了看介绍,说2.6的内核已经以module的方式支持oprofile。因此,只需要sudo modprobe oprofile一下,如果正常就可以继续,否则就说明没有安装这个module,需要重编内核,在menuconfig时选择是否添加。。
如果modprobe正确,仍出现这个错误,那说明opcontrol没有对应的kernel driver. 解决方案是重新编译安装oprofile。
2. 安装
wget http://prdownloads.sourceforge.net/oprofile/oprofile-0.9.6.tar.gz
tar xvfz oprofile.*.gz
./configure --prefix=/home/work/software/output/ --with-kernel-support
出现
/usr/bin/ld: /usr/lib/gcc/x86_64-redhat-linux/3.4.4/../../../../lib64/libbfd.a(archures.o): relocation R_X86_64_32 against `a local symbol’ can not be used when making a shared object; recompile with –fPIC
改为:
./configure --prefix=/home/work/software/output/ --with-kernel-support --enable-shared=no
再make

服务器用的是64位的系统,而它默认依赖到32位的库.因此,导出LDFLAGS=-L/usr/lib64后,make clean后再make,就成功通过了.

最后还出个waring,提示要添加用户,如果不调试JIT,直接忽略,不用添加用户。
3. 这就可以开始使用oprofile了,不过需要注意的是,需要有root权限才可以运行,请向系统管理员索要sudo权限。
4. 对mysqld进行profile为例:
sudo opcontrol --reset
sudo opcontrol --separate=lib --no-vmlinux --start --image=/home/software/output/libexec/mysqld
在其他机器起压力,压力停止后再进行后续操作
sudo opcontrol --dump
sudo opcontrol --shutdown
opreport -l /home/software/output/libexec/mysqld
opannotate -s /home/software/output/libexec/mysqld
参考:http://oprofile.sourceforge.net/doc/install.html
Linux系统中通过ps命令查看进程状态,可以看到系统启动时间。但如果启动超过一天,它会变成问号。那如何获取这些进程的启动时间呢?
在Linux系统中,时间管理有两个基本概念:xtime和jiffies。
xtime主要给time系函数使用,结构比较简单(include\linux\time.h):
struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds, 纳秒,以前的版本是微秒*/
};
tv_sec就是大家平常所说的unix时间戳,在CMOS中维护,关机时由电池维持正常运行。
系统启动时,通过get_cmos_time()取cmos时间赋值。运行时,通过设置系统定时器,每隔一段时间触发一个节拍,他们管这个节拍叫tick。每触发一次tick,就会通过update_wall_time_one_tick()来更新xtime。
而jiffies是内核中的一个全局变量,它的功能看起来很简单:记录从系统启动以来的tick数。但它就是解开我们问题的关键。
在/proc/<pid>/stat中( 源码请参考proc_pid_stat(),文件是fs/proc/array.c ),维护了进程的很多状态信息,其中第22项是进程启动时的jiffies值,通过它可以计算出进程启动时,系统已经开机的时间。把这个时间加上系统启动时间(/proc/stat),就可以得到进程启动时间。
最后得到的脚本如下:
#!/bin/sh
function show_start_time( )
{
pid=$1
JIFFIES=`cat /proc/$pid/stat | cut -d" " -f22`
UPTIME=`grep btime /proc/stat | cut -d" " -f2`
START_SEC=$(( $UPTIME + $JIFFIES / 100 ))
date -d "1970-01-01 UTC $START_SEC seconds"
}
if [ $# > 1 ]
then
for pid in $@; do show_start_time ${#pid};done
fi
while read pid; do show_start_time ${#pid}; done
脚本中100是“用户可见”的tick频率(tick_rate),它的值等于我们熟悉的常量CLOCKS_PER_SEC。为什么要说“用户可见”呢?实际上,新版本的内核tick_rate,已经远高于100了(i386的是1000),但以前很多程序,都依赖于这个数。为了兼容,于是内核又做了一层封装。