一、说说概念
1、进程(process)
狭义定义:进程就是一段程序的执行过程。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
简单的来讲进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,我们称其为进程。
进程状态:进程有三个状态,就绪、运行和阻塞。就绪状态其实就是获取了出cpu外的所有资源,只要处理器分配资源就可以马上执行。就绪状态有排队序列什么的,排队原则不再赘述。运行态就是获得了处理器分配的资源,程序开始执行。阻塞态,当程序条件不够时候,需要等待条件满足时候才能执行,如等待i/o操作时候,此刻的状态就叫阻塞态。
2、程序
说起进程,就不得不说下程序。先看定义:程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。而进程则是在处理机上的一次执行过程,它是一个动态的概念。这个不难理解,其实进程是包含程序的,进程的执行离不开程序,进程中的文本区域就是代码区,也就是程序。
3、线程
通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
4、多线程
在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。线程是在同一时间需要完成多项任务的时候实现的。
最简单的比喻多线程就像火车的每一节车厢,而进程则是火车。车厢离开火车是无法跑动的,同理火车也不可能只有一节车厢。多线程的出现就是为了提高效率。
二、说说区别
1、进程与线程的区别:
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
-
简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
-
线程的划分尺度小于进程,使得多线程程序的并发性高。
-
另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
-
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
-
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
三、说说优缺点
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。同时,线程适合于在SMP(多核处理机)机器上运行,而进程则可以跨机器迁移。
什么是进程
进程是对计算机的一种抽象,
-
进程表示一个逻辑控制流,就是一种计算过程,它造成一个假象,好像这个进程一直在独占CPU资源
-
进程拥有一个独立的虚拟内存地址空间,它造成一个假象,好像这个进程一致在独占存储器资源
进程的虚拟内存地址空间分为用户空间和内核空间。用户空间从低端地址往高端地址发展,内核空间从高端地址往低端地址发展。用户空间存放着这个进程的代码段和数据段,以及运行时的堆和用户栈。堆是从低端地址往高端地址发展,栈是从高端地址往低端地址发展。
内核空间存放着内核的代码和数据,以及内核为这个进程创建的相关数据结构,比如页表数据结构,task数据结构,area区域数据结构等等。
从文件IO的角度来说,Linux把一切IO都抽象成了文件,比如普通文件IO,网络IO,统统都是文件,利用open系统调用返回一个整数作为文件描述符file descriptor,进程可以利用file descriptor作为参数在任何系统调用中表示那个打开的文件。内核为进程维护了一个文件描述符表来保持进程所有获得的file descriptor。
每调用一次open系统调用内核会创建一个打开文件open file的数据结构来表示这个打开的文件,记录了该文件目前读取的位置等信息。打开文件又唯一了一个指针指向文件系统中该文件的inode结构。inode记录了该文件的文件名,路径,访问权限等元数据。
操作操作系统用了3个数据结构来为每个进程管理它打开的文件资源
fork系统调用
操作系统利用fork系统调用来创建一个子进程。fork所创建的子进程会复制父进程的虚拟地址空间。
要理解“复制”和“共享”的区别,复制的意思是会真正在物理内存复制一份内容,会真正消耗新的物理内存。共享的意思是使用指针指向同一个地址,不会真正的消耗物理内存。理解这两个概念的区别很重要,这是进程和线程的根本区别之一。
那么有人问了如果我父进程占了1G的物理内存,那么fork会再使用1G的物理内存来复制吗,相当于一下用了2G的物理内存?
答案是早期的操作系统的确是这么干的,但是这样性能也太差了,所以现代操作系统使用了 写时复制Copy on write的方式来优化fork的性能,fork刚创建的子进程采用了共享的方式,只用指针指向了父进程的物理资源。当子进程真正要对某些物理资源写操作时,才会真正的复制一块物理资源来供子进程使用。这样就极大的优化了fork的性能,并且从逻辑来说子进程的确是拥有了独立的虚拟内存空间。
fork不只是复制了页表结构,还复制了父进程的文件描述符表,信号控制表,进程信息,寄存器资源等等。它是一个较为深入的复制。
从逻辑控制流的角度来说,fork创建的子进程开始执行的位置是fork函数返回的位置。这点和线程是不一样的,我们知道Java中的Thread需要写run方法,线程开始后会从run方法开始执行。
既然我们知道了内核为进程维护了这么多资源,那么当内存进行进程调度时进行的进程上下文切换就容易理解了,一个进程运行要依赖这么些资源,那么进程上下文切换就要把这些资源都保存起来写回到内存中,等下次这个进程被调度时再把这些资源再加载到寄存器和高速缓存硬件。
进程上下文切换保存的内容有:
页表 – 对应虚拟内存资源
文件描述符表/打开文件表 – 对应打开的文件资源
寄存器 – 对应运行时数据
信号控制信息/进程运行信息
进程间通信
虚拟内存机制为进程管理存储资源带来了种种好处,但是它也给进程带来了一些小麻烦,我们知道每个进程拥有独立的虚拟内存地址空间,看到一样的虚拟内地址空间视图,所以对不同的进程来说,一个相同的虚拟地址意味着不同的物理地址。伤感的句子我们知道CPU执行指令时采用了虚拟地址,对应一个特定的变量来说,它对应着一个特定的虚拟地址。这样带来的问题就是两个进程不能通过简单的共享变量的方式来进行进程间通信,也就是说进程不能通过直接共享内存的方式来进行进程间通信,只能采用信号,管道等方式来进行进程间通信。这样的效率肯定比直接共享内存的方式差
什么是线程
上面说了一堆内核为进程分配了哪些资源,我们知道进程管理了一堆资源,并且每个进程还拥有独立的虚拟内存地址空间,会真正地拥有独立与父进程之外的物理内存。并且由于进程拥有独立的内存地址空间,导致了进程之间无法利用直接的内存映射进行进程间通信。
并发的本质是在时间上重叠的多个逻辑流,也就是说同时运行的多个逻辑流。并发编程要解决的一个很重要的问题就是对资源的并发访问的问题,也就是共享资源的问题。而两个进程恰恰很难在逻辑上表示共享资源。
线程解决的最大问题就是它可以很简单地表示共享资源的问题,这里说的资源指的是存储器资源,资源最后都会加载到物理内存,一个进程的所有线程都是共享这个进程的同一个虚拟地址空间的,也就是说从线程的角度来说,它们看到的物理资源都是一样的,这样就可以通过共享变量的方式来表示共享资源,也就是直接共享内存的方式解决了线程通信的问题。而线程也表示一个独立的逻辑流,这样就完美解决了进程的一个大难题。
从存储资源的角度理解了线程之后,就不难理解计算资源的分配了。从计算资源的角度来说,对内核而言,进程和线程没有什么区别,所以内核用内核调度实体(KSE)来表示一个调度的单元。
clone系统调用
在Linux系统中,线程是使用clone系统调用,clone是一个轻量级的fork,淘宝开店教程它提供了一系列的参数来表示线程可以共享父类的哪些资源,比如页表,打开文件表等等。我们上面说过了共享和复制的区别,共享只是简单地用指针指向同一个物理地址,不会在父进程之外开辟新的物理内存。
clone系统调用可以指定创建的线程开始执行代码位置,也就是Java中的Thread类的run方法。
Linux内核只提供了clone这个系统调用来创建类似线程的轻量级进程的概念。C语言利用了Pthreads库来真正创建了线程这个数据结构。Linux采用了1:1的模型,即C语言的Pthreads库创建的线程实体1:1对应着内核创建的一个KSE。Pthreads运行在用户空间,KSE运行在内核空间。
既然线程共享了进程的资源,那么线程的上下文切换就好理解了。对操作系统来说,它看到要被调度进来的线程和刚运行的线程是同一个进程的,那么线程的上下文切换只需要保存线程的一些运行时的数据,比如
线程的id
寄存器中的值
栈数据
而不需要像进程上下文切换那样要保存页表,文件描述符表,信号控制数据和进程信息等数据。页表是一个很重的资源,我们之前说过,如果采用一级页表的结构,那么32位机器的页表要达到4MB的物理空间。 有一种沉默是感怀所以线程上下文切换是很轻量级的。
进程采用父子结构,init进程是最顶端的父进程,其他进程都是从init进程派生出来的。这样就很容易理解进程是如何共享内核的代码和数据的了。
而线程采用对等结构,即线程没有父子的概念,所有线程都属于同一个线程组,线程组的组号等于第一个线程的线程号。
我们来看看Java的线程到底是如何实现的。Java语言层面提供了java.lang.Thread这个类来表示Java语言层面的线程,并提供了run方法表示线程运行的逻辑控制流。
我们知道JVM是C++/C写的,JVM本身利用了Pthreads库来创建操作系统的线程。JVM还要支持Java语言创建的线程的概念。
聊聊JVM(五)从JVM角度理解线程 这篇已经说了从JVM的角度如何理解线程。 JVM提供了JavaThread类来对应Java语言的Thread,即Java语言中创建一个java.lang.Thread对象,JVM会相应的在JVM中创建一个JavaThread对象。同时JVM还创建了一个OSThread类来对应用Pthreads创建的底层操作系统的线程对象。
构建并发程序可以基于进程也可以线程,
比如Nginx就是基于进程构建并发程序的。而Java天生只支持基于线程的方式来构建并发程序。
最后再总结一下 进程VS 线程
- 进程采用fork创建,线程采用clone创建
- 进程fork创建的子进程的逻辑流位置在fork返回的位置,线程clone创建的KSE的逻辑流位置在clone调用传入的方法位置,比如Java的Thread的run方法位置
- 进程拥有独立的虚拟内存地址空间和内核数据结构(页表,打开文件表等),当子进程修改了虚拟页之后,会通过写时拷贝创建真正的物理页。线程共享进程的虚拟地址空间和内核数据结构,共享同样的物理页
- 多个进程通信只能采用进程间通信的方式,比如信号,管道,而不能直接采用简单的共享内存方式,原因是每个进程维护独立的虚拟内存空间,所以每个进程的变量采用的虚拟地址是不同的。多个线程通信就很简单,直接采用共享内存的方式,因为不同线程共享一个虚拟内存地址空间,变量寻址采用同一个虚拟内存
- 进程上下文切换需要切换页表等重量级资源,线程上下文切换只需要切换寄存器等轻量级数据
- 进程的用户栈独享栈空间,线程的用户栈共享虚拟内存中的栈空间,没有进程高效
- 一个应用程序可以有多个进程,执行多个程序代码,多个线程只能执行一个程序代码,共享进程的代码段
- 进程采用父子结构,线程采用对等结构