进程与线程的区别,早已经成为了经典问题。自线程概念诞生起,关于这个问题的讨论就没有中止过。不管是初级程序员,仍是资深专家,都应该考虑过这个问题,只是层次角度不一样罢了。通常程序员而言,搞清楚两者的概念,在工做实际中去运用成为了焦点。而资深工程师则在考虑系统层面如何实现两种技术及其各自的性能和实现代价。以致于到今天,Linux内核还在持续更新完善(关于进程和线程的实现模块也是内核完善的任务之一)。linux
本文将以一个从事Linux平台系统开发的程序员角度描述这个经典问题。本文素材所有来源于工做实践经验与知识规整,如有疏漏或不正之处,敬请读者慷慨指出。程序员
0.首先,简要了解一下进程和线程。对于操做系统而言,进程是核心之核心,整个现代操做系统的根本,就是以进程为单位在执行任务。系统的管理架构也是基于进程层面的。在按下电源键以后,计算机就开始了复杂的启动过程,此处有一个经典问题:当按下电源键以后,计算机如何把本身由静止启动起来的?本文不讨论系统启动过程,请读者自行科普。操做系统启动的过程简直能够描述为上帝创造万物的过程,期初没有世界,可是有上帝,是上帝创造了世界,以后创造了万物,而后再创造了人,而后塑造了人的七情六欲,再而后人类社会开始遵循天然规律繁衍生息。。。操做系统启动进程的阶段就至关于上帝造人的阶段。本文讨论的所有内容都是“上帝造人”以后的事情。第一个被创造出来的进程是0号进程,这个进程在操做系统层面是不可见的,但它存在着。0号进程完成了操做系统的功能加载与初期设定,而后它创造了1号进程(init),这个1号进程就是操做系统的“耶稣”。1号进程是上帝派来管理整个操做系统的,因此在用pstree查看进程树可知,1号进程位于树根。再以后,系统的不少管理程序都以进程身份被1号进程创造出来,还创造了与人类沟通的桥梁——shell。从那以后,人类能够跟操做系统进行交流,能够编写程序,能够执行任务。。。面试
而这一切,都是基于进程的。每个任务(进程)被建立时,系统会为他分配存储空间等必要资源,而后在内核管理区为该进程建立管理节点,以便后来控制和调度该任务的执行。shell
进程真正进入执行阶段,还须要得到CPU的使用权,这一切都是操做系统掌管着,也就是所谓的调度,在各类条件知足(资源与CPU使用权均得到)的状况下,启动进程的执行过程。编程
除CPU而外,一个很重要的资源就是存储器了,系统会为每一个进程分配独有的存储空间,固然包括它特别须要的别的资源,好比写入时外部设备是可以使用状态等等。有了上面的引入,咱们能够对进程作一个简要的总结:设计模式
进程,是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操做系统结构的基础。它的执行须要系统分配资源建立实体以后,才能进行。数组
随着技术发展,在执行一些细小任务时,自己无需分配单独资源时(多个任务共享同一组资源便可,好比全部子进程共享父进程的资源),进程的实现机制依然会繁琐的将资源分割,这样形成浪费,并且还消耗时间。后来就有了专门的多任务技术被创造出来——线程。安全
线程的特色就是在不须要独立资源的状况下就能够运行。如此一来会极大节省资源开销,以及处理时间。多线程
1.好了,前面的一段文字是简要引入两个名词,即进程和线程。本文讨论目标是解释清楚进程和线程的区别,关于两者的技术实现,请读者查阅相关资料。架构
下面咱们开始重点讨论本文核心了。从下面几个方面阐述进程和线程的区别。
1).两者的相同点
2).实现方式的差别
3).多任务程序设计模式的区别
4).实体间(进程间,线程间,进线程间)通讯方式的不一样
5).控制方式的异同
6).资源管理方式的异同
7).个体间辈分关系的迥异
8).进程池与线程池的技术实现差异
接下来咱们就逐个进行解释。
1).两者的相同点
不管是进程仍是线程,对于程序员而言,都是用来实现多任务并发的技术手段。两者均可以独立调度,所以在多任务环境下,功能上并没有差别。而且两者都具备各自的实体,是系统独立管理的对象个体。因此在系统层面,均可以经过技术手段实现两者的控制。并且两者所具备的状态都很是类似。并且,在多任务程序中,子进程(子线程)的调度通常与父进程(父线程)平等竞争。
其实在Linux内核2.4版之前,线程的实现和管理方式就是彻底按照进程方式实现的。在2.6版内核之后才有了单独的线程实现。
2).实现方式的差别
进程是资源分配的基本单位,线程是调度的基本单位。
这句经典名言已流传数十年,各类操做系统教材均可见此描述。确实如此,这就是两者的显著区别。读者请注意“基本”二字。相信有读者看到前半句的时候就在内心思考,“进程岂不是不能调度?”,非也!进程和线程均可以被调度,不然多进程程序该如何运行呢!
只是,线程是更小的能够调度的单位,也就是说,只要达到线程的水平就能够被调度了,进程天然能够被调度。它强调的是分配资源时的对象必须是进程,不会给一个线程单独分配系统管理的资源。若要运行一个任务,想要得到资源,最起码得有进程,其余子任务能够以线程身份运行,资源共享就好了。
简而言之,进程的个体间是彻底独立的,而线程间是彼此依存的。多进程环境中,任何一个进程的终止,不会影响到其余进程。而多线程环境中,父线程终止,所有子线程被迫终止(没有了资源)。而任何一个子线程终止通常不会影响其余线程,除非子线程执行了exit()系统调用。任何一个子线程执行exit(),所有线程同时灭亡。
其实,也没有人写出只有线程而没有进程的程序。多线程程序中至少有一个主线程,而这个主线程其实就是有main函数的进程。它是整个程序的进程,全部线程都是它的子线程。咱们一般把具备多线程的主进程称之为主线程。
从系统实现角度讲,进程的实现是调用fork系统调用:
pid_t fork(void);
线程的实现是调用clone系统调用:
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */
);
其中,fork()是将父进程的所有资源复制给了子进程。而线程的clone只是复制了一小部分必要的资源。在调用clone时能够经过参数控制要复制的对象。能够说,fork实现的是clone的增强完整版。固然,后来操做系统还进一步优化fork实现——写时复制技术。在子进程须要复制资源(好比子进程执行写入动做更改父进程内存空间)时才复制,不然建立子进程时先不复制。
实际中,编写多进程程序时采用fork建立子进程实体。而建立线程时并不采用clone系统调用,而是采用线程库函数。经常使用线程库有Linux-Native线程库和POSIX线程库。其中应用最为普遍的是POSIX线程库。所以读者在多线程程序中看到的是pthread_create而非clone。
咱们知道,库是创建在操做系统层面上的功能集合,于是它的功能都是操做系统提供的。由此可知,线程库的内部极可能实现了clone的调用。无论是进程仍是线程的实体,都是操做系统上运行的实体。
最后,咱们说一下vfork() 。这也是一个系统调用,用来建立一个新的进程。它建立的进程并不复制父进程的资源空间,而是共享,也就说实际上vfork实现的是一个接近线程的实体,只是以进程方式来管理它。而且,vfork()的子进程与父进程的运行时间是肯定的:子进程“结束”后父进程才运行。请读者注意“结束”二字。并不是子进程完成退出之意,而是子进程返回时。通常采用vfork()的子进程,都会紧接着执行execv启动一个全新的进程,该进程的进程空间与父进程彻底独立不相干,因此不须要复制父进程资源空间。此时,execv返回时父进程就认为子进程“结束”了,本身开始运行。实际上子进程继续在一个彻底独立的空间运行着。举个例子,好比在一个聊天程序中,弹出了一个视频播放器。你说视频播放器要继承你的聊天程序的进程空间的资源干吗?莫非视频播放器想要窥探你的聊天隐私不成?懂了吧!
3).多任务程序设计模式的区别
因为进程间是独立的,因此在设计多进程程序时,须要作到资源独立管理时就有了自然优点,而线程就显得麻烦多了。好比多任务的TCP程序的服务端,父进程执行accept()一个客户端链接请求以后会返回一个新创建的链接的描述符DES,此时若是fork()一个子进程,将DES带入到子进程空间去处理该链接的请求,父进程继续accept等待别的客户端链接请求,这样设计很是简练,并且父进程能够用同一变量(val)保存accept()的返回值,由于子进程会复制val到本身空间,父进程再覆盖此前的值不影响子进程工做。可是若是换成多线程,父线程就不能复用一个变量val屡次执行accept()了。由于子线程没有复制val的存储空间,而是使用父线程的,若是子线程在读取val时父线程接受了另外一个客户端请求覆盖了该值,则子线程没法继续处理上一次的链接任务了。改进的办法是子线程立马复制val的值在本身的栈区,但父线程必须保证子线程复制动做完成以后再执行新的accept()。但这执行起来并不简单,由于子线程与父线程的调度是独立的,父线程没法知道子线程什么时候复制完毕。这又得发生线程间通讯,子线程复制完成后主动通知父线程。这样一来父线程的处理动做必然不能连贯,比起多进程环境,父线程显得效率有所降低。
PS:这里引述一个知名的面试问题:多进程的TCP服务端,可否互换fork()与accept()的位置?请读者自行思考。
关于资源不独立,看似是个缺点,但在有的状况下就成了优势。多进程环境间彻底独立,要实现通讯的话就得采用进程间的通讯方式,它们一般都是耗时间的。而线程则不用任何手段数据就是共享的。固然多个子线程在同时执行写入操做时须要实现互斥,不然数据就写“脏”了。
4).实体间(进程间,线程间,进线程间)通讯方式的不一样
进程间的通讯方式有这样几种:
A.共享内存 B.消息队列 C.信号量 D.有名管道 E.无名管道 F.信号
G.文件 H.socket
线程间的通讯方式上述进程间的方式均可沿用,且还有本身独特的几种:
A.互斥量 B.自旋锁 C.条件变量 D.读写锁 E.线程信号
G.全局变量
值得注意的是,线程间通讯用的信号不能采用进程间的信号,由于信号是基于进程为单位的,而线程是共属于同一进程空间的。故而要采用线程信号。
综上,进程间通讯手段有8种。线程间通讯手段有13种。
并且,进程间采用的通讯方式要么须要切换内核上下文,要么要与外设访问(有名管道,文件)。因此速度会比较慢。而线程采用本身特有的通讯方式的话,基本都在本身的进程空间内完成,不存在切换,因此通讯速度会较快。也就是说,进程间与线程间分别采用的通讯方式,除了种类的区别外,还有速度上的区别。
另外,进程与线程之间穿插通讯的方式,除信号之外其余进程间通讯方式均可采用。
线程有内核态线程与用户级线程,相关知识请参看个人另外一篇博文《Linux线程的实质》。
5).控制方式的异同
进程与线程的身份标示ID管理方式不同,进程的ID为pid_t类型,实际为一个int型的变量(也就是说是有限的):
/usr/include/unistd.h:260:typedef __pid_t pid_t;
/usr/include/bits/types.h:126:# define __STD_TYPE typedef
/usr/include/bits/types.h:142:__STD_TYPE __PID_T_TYPE __pid_t;
/usr/include/bits/typesizes.h:53:#define __PID_T_TYPE __S32_TYPE
/usr/include/bits/types.h:100:#define __S32_TYPE int
在全系统中,进程ID是惟一标识,对于进程的管理都是经过PID来实现的。每建立一个进程,内核去中就会建立一个结构体来存储该进程的所有信息:
注:下述代码来自 Linux内核3.18.1
include/linux/sched.h:1235:struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
...
pid_t pid;
pid_t tgid;
...
};
每个存储进程信息的节点也都保存着本身的PID。须要管理该进程时就经过这个ID来实现(好比发送信号)。当子进程结束要回收时(子进程调用exit()退出或代码执行完),须要经过wait()系统调用来进行,未回收的消亡进程会成为僵尸进程,其进程实体已经不复存在,但会虚占PID资源,所以回收是有必要的。
线程的ID是一个long型变量:
/usr/include/bits/pthreadtypes.h:60:typedef unsigned long int pthread_t;
它的范围大得多,管理方式也不同。线程ID通常在本进程空间内做用就能够了,固然系统在管理线程时也须要记录其信息。其方式是,在内核建立一个内核态线程与之对应,也就是说每个用户建立的线程都有一个内核态线程对应。但这种对应关系不是一对一,而是多对一的关系,也就是一个内核态线程能够对应着多个用户级线程。仍是请读者参看《Linux线程的实质》普及相关概念。此处贴出blog地址:
http://my.oschina.net/cnyinlinux/blog/367910
对于线程而言,若要主动终止须要调用pthread_exit() ,主线程须要调用pthread_join()来回收(前提是该线程没有被detached,相关概念请查阅线程的“分离属性”)。像线发送线程信号也是经过线程ID实现的。
6).资源管理方式的异同
进程自己是资源分配的基本单位,于是它的资源都是独立的,若是有多进程间的共享资源,就要用到进程间的通讯方式了,好比共享内存。共享数据就放在共享内存去,你们均可以访问,为保证数据写入的安全,加上信号量一同使用。通常而言,共享内存都是和信号量一块儿使用。消息队列则不一样,因为消息的收发是原子操做,于是自动实现了互斥,单独使用就是安全的。
线程间要使用共享资源不须要用共享内存,直接使用全局变量便可,或者malloc()动态申请内存。显得方便直接。并且互斥使用的是同一进程空间内的互斥量,因此效率上也有优点。
实际中,为了使程序内资源充分规整,也都采用共享内存来存储核心数据。无论进程仍是线程,都采用这种方式。缘由之一就是,共享内存是脱离进程的资源,若是进程发生意外终止的话,共享内存能够独立存在不会被回收(是否回收由用户编程实现)。进程的空间在进程崩溃的那一刻也被系统回收了。虽然有coredump机制,但也只能是有限的弥补。共享内存在进程down以后还完整保存,这样能够拿来分析程序的故障缘由。同时,运行的宝贵数据没有丢失,程序重启以后还能继续处理以前未完成的任务,这也是采用共享内存的又一大好处。
总结之,进程间的通讯方式都是脱离于进程自己存在的,是全系统均可见的。这样一来,进程的单点故障并不会损毁数据,固然这不必定全是优势。好比,进程崩溃前对信号量加锁,崩溃后重启,而后再次进入运行状态,此时直接进行加锁,可能形成死锁,程序再也没法继续运转。再好比,共享内存是全系统可见的,若是你的进程资源被他人误读误写,后果确定也是你不想要的。因此,各有利弊,关键在于程序设计时如何考量,技术上如何规避。这提及来又是编程技巧和经验的事情了。
7).个体间辈分关系的迥异
进程的备份关系森严,在父进程没有结束前,全部的子进程都尊从父子关系,也就是说A建立了B,则A与B是父子关系,B又建立了C,则B与C也是父子关系,A与C构成爷孙关系,也就是说C是A的孙子进程。在系统上使用pstree命令打印进程树,能够清晰看到备份关系。
多线程间的关系没有那么严格,无论是父线程仍是子线程建立了新的线程,都是共享父线程的资源,因此,均可以说是父线程的子线程,也就是只存在一个父线程,其他线程都是父线程的子线程。
8).进程池与线程池的技术实现差异
咱们都知道,进程和线程的建立时须要时间的,而且系统所能承受的进程和线程数也是有上限的,这样一来,若是业务在运行中须要动态建立子进程或线程时,系统没法承受不能当即建立的话,必然影响业务。综上,聪明的程序员发明了一种新方法——池。
在程序启动时,就预先建立一些子进程或线程,这样在须要用时直接使唤。这就是老人口中的“多生孩子多种树”。程序才开始运行,没有那么多的服务请求,必然大量的进程或线程空闲,这时候通常让他们“冬眠”,这样不耗资源,要否则一大堆孩子的口食也是个负担啊。对于进程和线程而言,方式是不同的。另外,当你有了任务,要分配给那些孩子的时候,手段也不同。下面就分别来解说。
进程池
首先建立了一批进程,就得管理,也就是你得分开保存进程ID,能够用数组,也可用链表。建议用数组,这样能够实现常数内找到某个线程,并且既然作了进程池,就预先估计好了生产多少进程合适,通常也不会再动态延展。就算要动态延展,也能预估范围,提早作一个足够大的数组。不为别的,就是为了快速响应。原本错进程池的目的也是为了效率。
接下来就要让闲置进程冬眠了,可让他们pause()挂起,也可用信号量挂起,还能够用IPC阻塞,方法不少,分析各自优缺点根据实际状况采用就是了。
而后是分配任务了,当你有任务的时候就要让他干活了。唤醒了进程,让它从哪儿开始干呢?确定得用到进程间通讯了,好比信号唤醒它,而后让它在预先指定的地方去读取任务,能够用函数指针来实现,要让它干什么,就在约定的地方设置代码段指针。这也只是告诉了它怎么干,还没说干什么(数据条件),再经过共享内存把要处理的数据设置好,这也子进程就知道怎么作了。干完以后再来一次进程间通讯而后本身继续冬眠,父进程就知道孩子干完了,收割成果。
最后结束时回收子进程,向各进程发送信号唤醒,改变激活状态让其主动结束,而后逐个wait()就能够了。
线程池
线程池的思想与上述相似,只是它更为轻量级,因此调度起来不用等待额外的资源。
要让线程阻塞,用条件变量就是了,须要干活的时候父线程改变条件,子线程就被激活。
线程间通讯方式就不用赘述了,不用繁琐的通讯就能达成,比起进程间效率要高一些。
线程干完以后本身再改变条件,这样父线程也就知道该收割成果了。
整个程序结束时,逐个改变条件并改变激活状态让子线程结束,最后逐个回收便可。
<<<本文完结>>>