进程基础知识
1、进程的概念
我们编译的可执行文件只是储存在硬盘的静态文件,运行时被加载到内存,CPU执行内存中指令,这个运行的程序被称为进程
进程是对运行时程序的封装,是操作系统进行资源调度和分配的基本单位
2、进程的实现
为了实现进程模型,操作系统维护着一张表格(一个结构数组),即进程表
每个进程占有一个进程表项(有些著作称这些为进程控制块)
该表项包含了一个进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号的调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时必须保存的信息,从而保证该进程随后能再次启动,就像从未中断过一样
3、并发与并行
- 单个核心在很短时间内分别执行多个进程,称为并发
- 多个核心同时执行多个进程称为并行
- 对于并发来说,CPU需要从一个进程切换到另一个进程,这个过程需要保存进程的状态信息
4、进程的状态
进程的状态分类:
- 就绪状态:等待被调度
- 运行状态:该时刻进程占用CPU
- 阻塞状态:该进程正在等待某一事件发生而暂时停止运行
对于阻塞状态,比如read系统调用阻塞,进程会占用内存空间,这是一种浪费行为,于是操作系统会有跟内存管理中物理页置换到磁盘一样的行为,把阻塞的进程置换到磁盘中,此时进程未占用物理内存,我们称之为挂起;挂起不仅仅可能是物理内存不足,比如sleep
系统调用或者用户执行Ctrl+Z
也可能导致挂起
阻塞态的进程占用着物理内存,在虚拟内存管理的操作系统中,通常会把阻塞态的进程的物理内存空间换出到硬盘,等需要再次运行的时候,再从硬盘换入到物理内存。
- 挂起态:描述进程没有占用实际的物理内存空间的情况
- 阻塞挂起状态: 进程在外存(硬盘)并等待某个事件的出现
- 就绪挂起状态: 进程在外存(硬盘),但只要进入内存,马上运行
5、进程状态的切换
就绪态->运行态:处于就绪状态的进程被操作系统的进程调度器选中后,就被分配CPU正式运行
运行态->就绪态:正在运行中的进程,由于分配的时间片用完,操作系统会把该进程变为就绪态,转而去执行其他的进程
运行态->阻塞态:当进程需要等待某个事件发生时,操作系统会将其变为阻塞态
阻塞态->就绪态:当进程等待的事件发生时,就从阻塞态变为就绪态,等待再次被调度
转换关系:
- 只有就绪态和运行态可以互相转换,其他都是单向转换
- 就绪态的进程通过调度算法从而获得CPU时间,转为运行状态;而运行状态的进程,在分配给它的CPU时间片完之后就会转为就绪状态,等待下一次调度
- 进程因为等待资源而阻塞,但是该资源不包括CPU时间,缺少CPU时间会从运行态转换为就绪态
- 当进程等待的外部事件发生时,则由阻塞态转换为就绪态,如果此时没有其他进程运行,就转换为运行态,否则该进程将处于就绪态,等待CPU空闲轮到它运行
6、进程控制块(PCB)
操作系统对进程的感知,是通过进程控制块PCB数据结构来描述的。它是进程存在的唯一标识,其包括以下信息:
1、进程描述信息- 进程标识符(PID):标识各个进程,每个进程都有一个唯一的标识符
- 用户标识符(UID):进程归属的用户,主要为共享和保护服务
- 进程当前状态:描述进程的状态信息,作为处理机调度的依据
- 进程优先级:进程抢占处理机的优先级
- 虚拟内存地址空间信息,打开文件列表和所使用的的输入/输出设备信息
- 主要是指处理机中个寄存器的值,当进程切换时,CPU寄存器的值都被保存在相应PCB中,以便重新执行该进程时能从断点处继续执行
PCB通过链表形式组织起来,比如有就绪队列、阻塞队列等,方便增删,方便进程管理
7、进程切换为何比线程慢
涉及到虚拟内存的问题,进程切换涉及虚拟地址空间的切换而线程不会
因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,所以同一个进程中的线程进行切换时不涉及虚拟地址空间的转换。
把虚拟地址转换为物理地址需要查找页表,页表查找是一个很慢的过程(至少访问2次内存),因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个Cache就是TLB(快表)
由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么当进程切换后页表也要进行切换,页表切换后TLB就失效了,cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换则不会导致TLB失效,因为线程线程无需切换地址空间,这就是进程切换要比同进程下线程切换慢的原因
进程调度算法
01 先来先服务(First Come First Served, FCFS)
非抢占式的调度算法,按照请求的顺序进行调度
有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长
02 最短作业优先(Shortest Job First, SJF)
非抢占式的调度算法,按估计运行时间最短的顺序进行调度
长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度
03 高响应比优先(Hightest Response Ratio Next, HRRN)
权衡短作业和长作业,每次进行进程调度时,先计算响应比优先级,然后把响应比优先级最高的进程投入运行
04 时间片轮转调度(Round Robin, RR)
每个进程被分配一个时间段,允许该进程在该时间段内运行
- 如果时间片用完,进程还没运行完,会释放CPU,执行其他进程
- 如果进程在时间片结束前阻塞或结束,则CPU立即切换
05 最高优先级调度(Highest Priority First, HPF)
为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级
进程的优先级可分为静态优先级和动态优先级
- 静态优先级:创建进程的时候就确定了,运行过程中不再变化
- 动态优先级:根据进行的动态变化调整优先级,例如进程运行时间增加,则降低其优先级;如果进程等待时间增加,则升高其优先级
06 多级反馈队列
- 多级:表示有多个队列,每个队列的优先级从高到低,同时优先级越高时间片越短
- 反馈:如果有新的进程进入优先级高的队列,则停止执行当前进程,转而执行高优先级的进程
可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合
进程间通信
包括管道(匿名管道和有名管道)、消息队列、共享内存、信号和套接字。其中前四个属于同一台机器下进程间的通信,套接字则是用于网络通信。
管道
- 匿名管道
- 特点
- 匿名管道是一种特殊的文件,只存在于内存中
- 匿名管道只能用于具有亲缘关系的进程间的通信,如父子进程,兄弟进程
- 半双工通信,只能一端写数据,另一端读数据,如果要双方同时收发数据要2个管道
- 相关接口
int pipe(int fd[2])
- 读端
fd[0]
,写端fd[1]
,写数据的一方要关闭fd[0]
,读数据的一方关闭fd[1]
- 特点
- 有名管道
- 特点
- 有名管道是FIFO文件,存在于文件系统中,可以通过文件路径名指出
- 可以在不具有亲缘关系的进程间通信
- 相关接口
int mkfifo(const char* pathname, mode_t mode)
- pathname:即将创建的FIFO文件的路径名,如果文件存在要先删除
- mode:设置文件访问权限,八进制数,和open中的参数相同
- 特点
不管是匿名管道还是有名管道,进程写入的数据都是缓存在内核中,读进程也要从内核中读取
消息队列
- 特点
- 在内核中创建,生命周期随内核,独立于读写进程存在,如果没有释放消息队列或者关闭操作系统,消息队列会一直存在
- 全双工通信,每个进程都可以往队列中插入节点和获取节点,节点是带有类型的数据块
- 读进程可以根据消息类型有选择地接收数据
缺点:
- 消息队列不适合大数据的传输,每个消息体(数据块)都有最大长度限制,队列所包含的消息体的总长度也有限制
- 消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一个进程读取内核中的数据时,会发生从内核态拷贝数据到用户态的过程
共享内存
共享内存允许两个或者多个进程共享物理内存的同一块区域(段)。由于一个共享内存段会成为一个进程用户空间的一部分,因此这种IPC机制无须内核介入。一个进程将数据复制进共享内存中,这些数据就能被共享同一个段的进程使用
- 创建共享内存
int shmget(key_t key, int size, int flag)
- key:为共享内存段命名,共享同一块内存的进程使用同一个key
- size:共享内存大小
- flag:访问权限,和open的mode一样
- 成功返回一个和key相关的内存共享标识符,失败返回-1
- 连接到共享内存地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg)
- shmid:shmget返回的标识符
- shmaddr:共享内存地址,给NULL
- shmflag:访问模式,0表示读写权限
- 从共享内存分离
int shmdt(const void *shmaddr)
- shmaddr:shmat返回的地址指针
因为多个进程都可以同时修改共享内存,容易引起冲突,所以需要信号量来实现进程的同步与互斥
信号
信号是软件中断,是一种异步通信方式,信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而去处理某个突发事件
特点:
- 简单
- 不能携带大量信息
- 满足某个特定条件才发送
一个完整的信号周期
- 信号的产生
- 信号在进程中注册,以及注销
- 执行信号处理函数
Socket
用于不同主机之间的通信
用户态与内核态
内核态和用户态的 CPU 指令集权限不同,在Linux下,内核态是Ring 0,用户态是Ring 3,用户进程如果进行系统调用,就要切换到内核态,
在32位机器下,进程的虚拟地址空间为4G($2^{32}$),其中前3G是用户进程都可以使用的,3-4G是内核空间,所有进程都共用这一段空间
什么情况会导致用户态到内核态切换
- 系统调用:用户态进程主动切换到内核态的方式,用户态进程通过系统调用向操作系统申请资源完成工作
- 异常:当 CPU 在执行用户态的进程时,发生了一些没有预知的异常,这时当前进程会切换到处理此异常的内核相关进程中,也就是切换到了内核态,如缺页异常
- 中断:当 CPU 在执行用户态的进程时,外围设备完成用户请求的操作后,会向 CPU 发出相应的中断信号,这时 CPU 会暂停执行下一条即将要执行的指令,转到与中断信号对应的处理程序去执行,也就是切换到了内核态。如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后边的操作等