前言:
老生常谈,进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元
基本概念:
计算机中的资源:
1. 计算资源:CPU提供的计算能力
2. 存储资源:内存、硬盘等提供的存储能力
3. 经典冯诺依曼结构:CPU+存储器+IO
CPU可视为黑盒,输入的是待处理的指令和数据,输出的是处理后的结果;即CPU只提供计算能力,不提供计算资源的调度管理;因此,计算资源由操作系统负责进行分配,负责分配计算资源的部分是操作系统的调度模块,按照一定规则进行计算资源的分配,比如按时间片分配计算资源;
存储器是由虚拟内存机制来管理的,因为进程表示一个运行的程序,而程序的代码段、数据段这些都是存放在磁盘中的,在运行时加载到内存中,所以进程是面向磁盘的;
补充:虚拟页是对磁盘文件的分配,然后被缓存到物理内存的物理页中;总之,存储资源是由虚拟内存机制进行管理和分配的;因此,操作系统分配资源(存储资源)的最小单元是进程;
**注意:**Linux内核是没有线程这个概念的,只有内核调度实体(Kernal Scheduling Entry, KSE)这个概念,Linux的线程本质上是一种轻量级的进程,是通过clone系统调用来创建的,进程是一种KSE,线程也是一种KSE,因此,线程是操作系统调度的最小单元;
从文件IO的角度来说,Linux把一切IO都抽象成了文件,比如普通文件IO,网络IO等都是文件,利用系统调用open返回的一个整数作为文件描述符file descriptor,进程可以利用file descriptor作为参数在任何系统调用中表示那个打开的文件;内核为进程维护了一个文件描述符表来保持进程所有获得的file descriptor;
每调用一次open系统调用,内核会创建一个打开文件(open file)的数据结构来表示这个打开的文件,记录了该文件目前读取的位置等信息。打开文件又唯一保存了一个指针指向文件系统中该文件的inode结构,inode记录了该文件的文件名,路径,访问权限等元数据;
相关的系统调用:fork系统调用
作用:创建一个子进程,该子进程会复制父进程的虚拟地址空间;
复制VS共享–进程VS线程:复制会占用新的物理内存,而共享不会占用新的内存,共享可视为指向的是同一个物理内存中的内容,这也是线程和进程之间的一个根本区别;
现代操作系统为了优化fork系统调用导致的内存占用问题(父进程占用1G物理内存,刚创建的子进程也会占用1G物理内存),采用写时复制的方案进行优化;也就是说,在刚创建子进程时(子进程并没有对数据进行操作)采用共享的方式,指向父进程占用的物理内存,当子进程真正要进行操作时,才去占用物理内存进行复制
进程间通信:由于每一个进程拥有独立的虚拟地址空间,而不同进程的同一虚拟地址空间可能对应的物理地址空间不同,所以,无法通过共享内存的方式完成进程间的通信,只能采用信号,管道等方式来进行进程间通信;
线程解决的最大问题:可以很简单地表示共享资源,这里说的资源指的是存储器资源,资源最后都会加载到物理内存,一个进程的所有线程都是共享这个进程的同一个虚拟地址空间的,也就是说从线程的角度来说,它们看到的物理资源都是一样的,这样就可以通过共享变量的方式来表示共享资源,通过直接共享内存的方式解决了线程通信的问题。而线程也表示一个独立的逻辑流,这样就完美解决了进程的一个大难题;
相关的系统调用:clone系统调用
clone是一个轻量级的fork,它提供了一系列的参数来表示线程可以共享父类的哪些资源,比如页表,打开文件表等等(注意:这里涉及的是共享这一概念)
同一个进程的线程对应的上下文切换,只需要保存线程的一些运行时的数据,比如线程的id、寄存器中的值以及栈数据等,相比进程来说,是轻量级的;
进程VS线程
1. 进程采用fork创建,线程采用clone创建
2. 进程fork创建的子进程的逻辑流位置在fork返回的位置,线程clone创建的KSE的逻辑流位置在clone调用传入的方法位置,比如Java的Thread的run方法位置
3. 多个进程通信只能采用进程间通信的方式,比如信号,管道,而不能直接采用简单的共享内存方式,原因是每个进程维护独立的虚拟内存空间,所以每个进程的变量采用的虚拟地址是不同的;多个线程通信就很简单,直接采用共享内存的方式,因为不同线程共享一个虚拟内存地址空间,变量寻址采用同一个虚拟内存
4. 进程上下文切换需要切换页表等重量级资源,线程上下文切换只需要切换寄存器等轻量级数据
5. 进程的用户栈独享栈空间,线程的用户栈共享虚拟内存中的栈空间,没有进程高效
6. 一个应用程序可以有多个进程,执行多个程序代码,多个线程只能执行一个程序代码,共享进程的代码段
7. 进程采用父子结构,线程采用对等结构