理解进程和线程基础知识
关于进程和线程,可以用一句话来概括“进程是操作系统分配资源的最小单元,线程是操作系统运算调度的最小单元”。这句话虽然言简意赅的表示了进程和线程的区别,但是大多少人并不能很好的理解分配资源和调度的差异。
接下来,让我们逐步深入的学习,理解进程、线程的基础知识。
什么是进程
我们平时使用安装了Mac OS或者Windows操作系统的计算机,可以一边通过浏览器上网、一边通过微信聊天、还可以听着音乐。所有这些事情背后都是一个个正在运行中的软件程序。
这些软件程序要想运行起来,首先得将安装在磁盘上的程序代码加载到内存中,然后操作系统调度CPU去执行这些代码,比如从磁盘中读写文件、调用键盘、声卡等外设。在这个过程中,操作系统会记录应用程序需要多少内存,打开了什么文件、当有些资源不可用的时候要不要睡眠,当前进程运行到哪里了。操作系统把这些信息综合统计,存放在内存中,抽象为进程。
所以我们用一句话来概括进程:
操作系统把每个运行中的程序封装成独立的实体,分配各自所需资源,再根据调度算法调度CPU去执行,这个应用程序运行时的实例就是进程。
回到前面说的,我们使用微信、浏览器等在用户看来这些都是并行的在执行,但是对于计算机来说实则是操作系统通过时间片的方式让各个程序交替着执行。由于计算机CPU执行速度是非常快的,所以我们根本感觉不到,实际上CPU是并发方式执行应用程序。
并行和并发区别
CPU分为单核和双核
- 单核:处理器核并发执行进程
- 双核:每个处理器核分别并发执行进程,多个处理器之间并发执行进程互相不影响。
进程的结构
操作系统会提供一种机制,给被运行起来的进程分配所需的资源,这些资源也称虚拟地址空间。不同的虚拟地址空间与不同内存的物理地址映射起来。这样所有进程都不能直接访问物理内存,只需要访问自己所需要的虚拟地址空间。
那么虚拟地址空间又是什么结构?我们还是以一个运行起来的应用程序为例:
-
代码段:首选需要加载存放在磁盘上的二进制文件中的机器码到内存中(用于存放这些机器码的虚拟内存空间叫做代码段)
-
数据段:在程序代码中通常会定义大量的全局变量和静态变量。如果变量已经指定来初始值,在加载到虚拟地址空间中会放入数据段
-
BSS段:没有指定初始值的全局变量和静态变量存储在BSS段。这些未初始化的变量被加载到内存后会被初始化0值
-
堆:在程序运行期间往往需要动态申请内存,所以在虚拟地址空间也需要一块区域来存放这些动态申请的内存,这块区域就是堆。通过
malloc
分配 -
共享库的内存映射区:程序在运行过程中还需要依赖动态链接库,这些动态链接库以.so文件的形式存放在磁盘中。
- 比如C程序中的glibc库,里边对系统调用进行了封装。glibc 库里提供的用于动态申请堆内存的 malloc 函数就是对系统调用 sbrk 和 mmap 的封装。这些动态链接库也有自己的对应的代码段,数据段,BSS 段,也需要一起被加载进内存中。
- 还有用于内存文件映射的系统调用 mmap,会将文件与内存进行映射,那么映射的这块内存(虚拟内存)也需要在虚拟地址空间中有一块区域存储。
- 这些动态链接库中的代码段,数据段,BSS 段,以及通过 mmap 系统调用映射的共享内存区,在虚拟内存空间的存储区域叫做共享库的内存映射区。
-
栈:最后程序在运行时总会调用各种函数,在调用函数过程中使用到的局部变量和函数参数等也需要一块内存区域来保存。这一块区域在虚拟地址空间中叫做栈。
以上这些都属于用户态空间,是每个进程独有的。实际上进程的运行还需要内核提供资源,内核需要保存进程的机器上下文和它运行时的栈空间,便于内核随时中断或者恢复执行进程。
进程的状态
CPU是以时间片的方式调度进程,所以在一个进程运行期间至少具备三种基本状态:
上图中各个状态的意义:
- 运行状态(Runing):该时刻进程占用 CPU;
- 就绪状态(Ready):可运行,但因为其他进程正在运行而暂停停止;
- 阻塞状态(Blocked):该进程正在等待某一事件发生(如等待输入/输出操作的完成)而暂时停止运行,这时,即使给它 CPU 控制权,它也无法运行;
当然,进程另外两个基本状态:
- 创建状态(new):进程正在被创建时的状态;
- 结束状态(Exit):进程正在从系统中消失时的状态;
于是,一个完整的进程状态的变迁如下图:
-
创建状态:一个新进程被创建时的第一个状态;
-
创建状态 -> 就绪状态:当进程被创建完成并初始化后,一切就绪准备运行时,变为就绪状态,这个过程是很快的;
-
就绪态 -> 运行状态:处于就绪状态的进程被操作系统的进程调度器选中后,就分配给 CPU 正式运行该进程;
-
运行状态 -> 结束状态:当进程已经运行完成或出错时,会被操作系统作结束状态处理;
-
运行状态 -> 就绪状态:处于运行状态的进程在运行过程中,由于分配给它的运行时间片用完,操作系统会把该进程变为就绪态,接着从就绪态选中另外一个进程运行;
-
运行状态 -> 阻塞状态:当进程请求某个事件且必须等待时,例如请求 I/O 事件;
-
阻塞状态 -> 就绪状态:当进程要等待的事件完成时,它从阻塞状态变到就绪状态;
多进程
先上结论,多进程的目的是提高CPU的使用率。那么我们就要问了,为什么多进程能提升CPU的使用率?
- CPU同一时刻只能运行一个进程,而CPU个数总是比进程个数少,这就需要让多个进程共用一个CPU,CPU根据时间片或者抢占式的方式轮询调度进程
- 进程在运行过程中,拿不到所需要的资源,CPU会出现空闲的情况。这个时候就需要进程让出CPU,CPU调度其他进程。
比如下图,有五个进程,其中浏览器进程和微信进程依赖于网络和键盘的数据资源,如果不能满足它们,就应该通过进程调度让出 CPU。
而两个科学计算进程,则更多的依赖于 CPU,但是如果它们中的一个用完了自己的 CPU 时间,也得借助进程调度让出 CPU,不然它就会长期霸占 CPU,导致其它进程无法运行。需要注意的是,每个进程都会依赖一种资源,那就是 CPU 时间,你可以把 CPU 时间理解为它就是 CPU,一个进程必须要有 CPU 才能运行。
什么是线程
早期操作系统中并没有线程的概念,任务的调度采用的是时间片或者抢占式的方式来调度进程。由于每个进程的虚拟地址空间都是独立的,使得CPU在做调度时,会进行进程的上下文切换。
进程的上下文切换开销是较大的。切换前,需要保存上一个进程的CPU寄存器和程序计数器数据到系统内核中,然后再加载新任务的上下文到CPU寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新的任务。整个系统调用过程,其实发生了两次的CPU上下文切换(用户态–内核态–用户态)。
于是,为了应对越来越复杂的程序要求,发明了线程。
一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(共享代码段、数据段、BSS段、共享映射区等),同时每个线程都有独立的一套寄存器和栈。
进程和线程区别
线程与进程的比较如下:
- 进程是资源(包括内存、打开的文件等)分配的单位,线程是 CPU 调度的单位;
- 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈;
- 线程同样具有就绪、阻塞、执行三种基本状态,同样具有状态之间的转换关系;
- 线程能减少并发执行的时间和空间开销;
对于,线程相比进程能减少开销,体现在:
- 线程的创建时间比进程快,因为进程在创建的过程中,还需要资源管理信息,比如内存管理信息、文件管理信息,而线程在创建的过程中,不会涉及这些资源管理信息,而是共享它们;
- 线程的终止时间比进程快,因为线程释放的资源相比进程少很多;
- 同一个进程内的线程切换比进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同一个进程的线程都具有同一个页表,那么在切换的时候不需要切换页表。而对于进程之间的切换,切换的时候要把页表给切换掉,而页表的切换过程开销是比较大的;
- 由于同一进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更高了;
所以,线程比进程不管是时间效率,还是空间效率都要高。
小结
通常,我们启用一个应用程序就是进程。比如,我们在计算机上开启了微信和网易云音乐分别就是2个不同的进程,它们之间通过CPU时间片轮询的方式交替执行,这种方式也称为进程的上下文切换。
由于CPU上下文切换是有代价的。还是以微信为例。我们现在需要同时和2个好友聊天,如果每开启一个聊天窗口就fork
一个进程。fork
进程需要复制父进程的虚拟地址空间,从内存和CPU上都存在大量消耗。这个时候,我们就想到线程。
多个线程共享了进程的虚拟地址空间,只需要创建一份独立的寄存器和栈信息。所有从时间和空间上效率都比进程要高。