进程管理
进程管理
进程与线程
进程的概念
- 进程的概念
- 指一个应用程序运行了起来, 进程是操作系统分配资源的一个最小单位
- 进程的结构
控制块(PCB)存放着进程的唯一ID,如果运行了多个微信进程,他们的ID也都是不一样的数据块存放着进程的原始数据和中间数据程序块多个进程共享 存放在文本区域 这个的多个进程是指如果开启了多个微信程序 则多个微信程序共享程序块
线程的概念
- 线程的概念
- 操作系统最小调度单位, 不能独立运行 被包含于进程, 一个进程可以有一个或多个进程
- 也被叫做轻量级进程 但是轻量级进程多用于内核级进程 而用户级线程成为线程
- 内核级线程
- 线程的控制块在操作系统的内核空间里 被称为内核级线程 但是线程的数据块和程序块还依然在用户控件
- 实现了真正意义上的线程并行
- 不需要运行时系统的参与
- 正因为数据块和程序块在用户空间,所以频繁的切换会导致内核开销大
- 用户级线程
- 线程的控制快在操作系统的用户控件里 被称为用户级线程 不依赖于操作系统 调度依赖于用户程序 在操作系统的视角里其实感觉不到用户级线程的存在
- 线程位于用户控件(不需要模式切换)
- 独立于操作系统(线程可以在不支持他们的操作系统上运行)
- 运行时系统可以切换用户控件中的本地阻塞线程(eg:等待另一个线程完成)
- 系统调用中,对一个线程的阻塞将会导致整个进程的阻塞
- 非真正意义的线程并行(一个进程安排在单个cpu上)
- 不存在时钟中断
- 更多的用户级线程的知识请看《如何理解协程?》
进程与线程的区别
- 进程是操作系统分配资源的最小单位 / 线程是操作系统最小调度单位
- 一个线程只属于一个进程 / 一个进程可以包括多个线程但最少有一个主线程
- 进程在执行过程中拥有独立的内存单元 / 而多个线程共享进程的内存
- 进程编程调试简单可靠性高,但是创建销毁开销大 / 线程正相反,开销小,切换速度快,但是编程调试相对复杂
- 进程间不会相互影响 / 线程一个线程挂掉将导致整个进程挂掉
- 进程适应于多核、多机分布 / 线程适用于多核
- 由于在创建或撤消进程时,系统都要为之分配或回收资源,如内存空间、IO设备等。因此,操作系统所付出的开销将显著地大于在创建或撤消线程时的开销 / 而线程切换只须保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作 / 所以进程切换的开销也远大于线程切换的开销
- 由于同一进程中的多个线程具有相同的地址空间,致使它们之间的同步和通信的实现,也变得比较容易。进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。在有的系统中,线程的切换、同步和通信都无须操作系统内核的干预
线程资源
- 详细文章请看《线程到底共享了哪些进程资源?》
| 线程共享的资源 | 线程私有的资源 |
|---|---|
| 堆区 | 寄存器 |
| 打开的文件 | 栈区 |
| 数据区(全局变量和static) | 程序计数器 |
| 代码区(每一个函数) | 栈指针 |
| 动态链接库 |
说一下 fork,wait,exec 函数
-
父进程产生子进程使用
fork拷贝出一个父进程的副本,此时只拷贝了父进程的页表,两个进程都读一块内存的 -
当有进程写的时候使用写时拷贝机制分配内存,
exec函数可以加载一个elf文件去替换父进程,从此父进程和子进程就可以运行不同的程序了 -
fork 从父进程返回子进程的 pid,从子进程返回 0,调用了
wait的父进程将会发生阻塞,直到 有子进程状态改变,执行成功返回 0,错误返回 -1 -
exec执行成功则子进程从新的程序开始运行,无返回值,执行失败返回 -1 -
fork()函数-
1 2 3#Fork:创建一个和当前进程映像一样的进程可以通过 fork() 系统调用: #include <sys/types.h> #include <unistd.h> pid_t fork(void); -
成功调用 fork() 会创建一个新的进程,它几乎与调用
fork()的进程一模一样,这两个进程都会 继续运行 -
在子进程中,成功的
fork()调用会返回0。在父进程中 fork() 返回子进程的 pid,如果出现错误,fork()返回一个负值 -
最常⻅的 fork() 用法是创建一个新的进程,然后使用
exec()载入二进制映像,替换当前进程的 映像。这种情况下,派生(fork)了新的进程,而这个子进程会执行一个新的二进制可执行文 件的映像。这种“派生加执行”的方式是很常⻅的 -
在早期的 Unix 系统中,创建进程比较原始。当调用 fork 时,内核会把所有的内部数据结构复 制一份,复制进程的⻚表项,然后把父进程的地址空间中的内容逐⻚的复制到子进程的地址空 间中。但从内核⻆度来说,逐⻚的复制方式是十分耗时的。现代的 Unix 系统采取了更多的优 化,例如 Linux,采用了写时复制的方法,而不是对父进程空间进程整体复制
-
孤儿进程 V.S 僵尸进程
-
什么是孤儿进程
-
一般情况下,子进程是由父进程创建,而子进程和父进程的推出顺序是无序的,两者之间都不知道谁先退出。
正常情况下,父进程会先结束调用
wait()或者waitpid()函数等到子进程完成后再退出,而一旦父进程不等待直接退出,则剩下的子进程会被init(pid=1)进程接收,成了孤儿进程(进程树中除了init都会有父进程)
-
-
什么是僵尸进程
- 如果子进程先退出了,父进程还未结束并且没有调用 wait 或者 waitpid 函数获取子进程的 状态信息,则子进程残留的状态信息( task_struct 结构和少资源信息)会变成僵尸进程
-
怎么处理僵尸进程
- 子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中 调用wait进行处理僵尸进程
- 原理是将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程
守护进程
- 什么是守护进程
- 守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
- 守护进程是一种很有用的进程
- 守护进程特点
- 守护进程最重要的特性是后台运行
- 守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进 程(特别是shell)中继承下来的
- 守护进程的启动方式有其特殊之处。它可以在Linux系统启动时从启动脚本/etc/rc.d中启 动,可以由作业规划进程crond启动,还可以由用户终端(shell)执行
- 实现一个守护进程
- 在父进程中执行
fork并exit推出 - 在子进程中调用
setsid函数创建新的会话 - 在子进程中调用
chdir函数,让根目录 ”/” 成为子进程的工作目录 - 在子进程中调用
umask函数,设置进程的umask为0 - 在子进程中关闭任何不需要的文件描述符
- 在父进程中执行
进程的创建过程,需要哪些函数,需要哪些数据结构
fork函数创造的子进程是父进程的完整副本,复制了父进程的资源,包括内存的内容task_struct内容vfork创建的子进程与父进程共享数据段,而且由vfork创建的子进程将先于父进程运行- linux 上创建线程一般使用的就是
pthead库,实际上linux也给我们提供了创建线程的系统调用,也就是clone
进程创建子进程,fork 详解
-
函数原型
pid_t fork(void) -
除了0号进程(系统创建的)之外,linux系统中都是由其他进程创建的。创建新进程的进 程,即调用fork函数的进程为父进程,新建的进程为子进程
-
fork函数不需要任何参数,对于返回值有三种情况
-
对于父进程,fork函数返回新建子进程的pid;
-
对于子进程,fork函数返回 0;
-
如果出错, fork 函数返回 -1
1 2 3 4 5 6 7 8 9 10 11 12int pid=fork(); if(pid < 0){ //失败,一般是该用户的进程数达到限制或者内存被用光了 ........ } else if(pid == 0){ //子进程执行的代码 ...... } else{ //父进程执行的代码 ......... }
-
进程运行
五种基本状态
- 创建: 创建好一个进程
- 就绪:进程做好了执行的准备 就差分配处理机
- 运行:该进程正在执行
- 阻塞:等待某事件发生才能执行,如等待I/O完成
- 终止:进程被关闭
进程控制
- 什么是进程控制
- 即os对进程实现有效的管理 比如进程的创建、进程切换等动作
- os 通过 原语 操作来实现进程控制
- 什么是原语
- 由若干条指令组成 具有原子性
- 原语的特点
- 原子操作 要么全部执行 要么全部失败 执行过程不会被终止
- 在管态/系统态/内核态下执行,常驻内存
- 是内核三大支撑功能之一(中断机制,时钟管理,原语操作)
进程是怎么运行的
-
创建原语:create
-
阻塞原语:block
-
唤醒原语:wakeup
-
撤销原语:destroy
-
挂起原语:suspend
-
激活原语:active

处理机
- 什么是处理机
- 根据一定的算法和原则将处理机资源进行重新分配的过程
- 前提:作业/进程数目一定远远大于处理机数目
- 目的:提高资源利用率 减少处理机空闲时间
- 处理机调度:调度的层次
- 高级调度/作业调度
- 把后备作业调入内存
- 只调入一次 调出一次
- 中级调度/内存调度
- 将进程调至外存 条件合适再调入内存
- 在内存 外存对换区进行进程对换
- 低级调度/进程调度
- 从就绪队列选取进程分配给处理机
- 最基本的调度 频率非常高(相当于一个时间片完成)
- 高级调度/作业调度
调度算法
先来先服务
- 算法内容:调度作业/就绪队列中最先入队者,等待操作完成和阻塞
- 算法原则:按作业/进程到达顺序服务
- 调度方式:非抢占调度
- 适用场景:作业/进程调度
- 优缺点
- 有利于CPU繁忙型作业 充分利用CPU资源
- 不利于I/O繁忙型作业 操作耗时 其他饥饿

短作业优先
- 算法内容:所需服务时间最短的作业/进程优先服务
- 算法原则:追求最小的平均周转时间
- 调度方式:非抢占式服务(除了服务时间最短 还有另一种方式-最短剩余时间优先)
- 适用场景:作业/进程调度
- 优缺点
- 平均等待/调度时间最少
- 长作业会增加或者饥饿
- 估计时间不准确 不能保证紧急任务及时处理

高响应比优先调度
- 算法内容:结合先来先服务&短作业优先,综合考虑等待时间和服务时间计算响应比 高的响应比优先调度
- 算法原则:综合考虑作业/进程的等待时间和服务时间
- 调度方式:非抢占调度
- 适用场景:作业/进程调度
- 响应比计算
- (等待时间 + 服务时间) / 服务时间
- 当前进程完成或者阻塞时重新计算所有的进程响应比

优先级调度
- 算法内容:又叫优先权调度,按作业/进程的优先级(紧迫程度)进行调度
- 算法原则:优先级最高的作业/进程先调度
- 调度方式:抢占(高优先级立即执行)/非抢占(高优先级等待当前进程让出后执行)
- 适用场景:作业/进程调度
- 优先级设置原则
- 静态/动态优先级
- 系统 > 用户 / 交互型 > 非交互型 / I/O型 > 计算型
- 低优先级进程可能会饥饿

时间片轮转调度
- 算法内容:按进程到达就绪队列的顺序,轮流分配一个时间片去执行,时间用完则剥夺
- 算法原则:公平、轮流为每个进程服务,进程在一定时间内都能得到响应
- 调度方式:抢占式 由时钟中断确定时间
- 适用场景:进程调度
- 优缺点
- 公平,响应快,适用于分时系统
- 时间片决定因素:系统响应时间、就绪队列进程数量、系统处理能力
- 时间片太大,相当于 先来先服务 / 太小 处理机切换频繁 开销增大
多级反馈队列调度
- 算法内容
- 设置多个按优先级排序的就绪队列
- 优先级从高到低 时间片从小到大
- 新进程采用队列降级法 (1. 进入第一级队列 按先来后到分时间片 2. 没有执行完,移动到第二级)
- 前面队列不为空 不执行后续队列进程
- 算法原则:集中前几种算法的优点
- 调度方式:抢占式
- 适用场景:进程调度
- 优缺点
- 对各类型相对公平 可以快速响应
- 也实现了短作业优先
- 周转时间短
- 在前几个队列部分执行

进程通信
共享内存
- 共享内存,指两个或多个进程共享一个给定的存储区
- 共享内存是最快的一种进程通信方式,因为进程是直接对内存进行存取
- 因为多个进程可以同时操作 所以需要进行同步
- 信号量 + 共享内存通常结合在一起使用
管道通信
-
无名管道
-
半双工 具有固定的写端以及读端
-
管道就是一个 队列 是先进先出
-
无名管道只能用于具有亲属关系的进程之间的通信
-
管道其实可以看成一个特殊的文件, 对于他的读写也可以使用 read()/write() 但是管道不属于文件系统,他是用于内存中
-
当一个管道建立时,会创建两个文件文件描述符,要关闭管道只需将这两 个文件描述符关闭即可
1Int pipe(int fd[2]);
-
-
有名管道(fifo)
-
有名管道可以用于无关的进程之间通信
-
有名管道有路径名与之相关联,她以一种特殊设备文件形式存在于文件系统中
1Int mkfifo(const char* pathname, mode_t mode);
-
消息队列
- 消息队列,是消息的连接表,存放在内核中。一个消息队列有一个标识符来标识
- 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级
- 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容不会被删除
- 消息队列可以实现消息的随机查询
信号量
- 什么是信号量
- 信号的数量
- 举例: 好比一个停车场,里面很多的停车位,停车位是任何车都可以停的,所以可以看成共享资源,那停车位满了,外面肯定就要排队
- P/V 操作
- P操作: wait原语,进程等待
- V操作: signal原语,唤醒等待进程
- 信号量的工作机制
- 由一个 剩余资源数量 & 进程等待队列 实现的结构体
- 如果有一个进程在执行 则剩余资源数量-1
- 如果剩余资源数量为负数,则让当前申请资源的进程进入等待队列并处于阻塞状态
- 如果一个进程执行完毕,则剩余资源数量+1,并且从等待队列唤醒一个进程
进程同步
进程同步
- 什么是进程同步
- 协调进程间的 相互制约关系, 使他们按照预期的方式执行的过程
- 两种相互制约形式
- 互斥: 进程排他性访问共享资源
- 同步: 进程间的合作,比如管道通信
- 互斥的实现
- 临界区: 比如打印机、音频设备等 通过对多个进程进程串行化来访问公共资源或一段代码 在任意时刻只允许一个进程访问 一个进程成功访问后就会加锁 直到释放锁 也唤醒其他的阻塞进程
- 互斥对象: 互斥对象和临界区很像,采用互斥对象机制,只有拥有互斥对象的进程才有访问公共资源的权限。因为互斥对象只有一个,所以能保证公共资源不会同时被多个进程同时访 问。当前拥有互斥对象的进程处理完任务后必须将进程交出,以便其他进程访问该资源
- 同步的实现
- 信号量: 上面已经介绍,在此不多赘述
- 事件对象: 通过通知操作的方式来保持进程的同步,还可以方便实现对多个进程的优先级 比较的操作
死锁
死锁
- 什么是死锁: 多个进程由于竞争资源而造成的阻塞现象,若无外力干预,这些进程都无法前进
- 死锁产生的原因
- 系统资源的竞争
- 进程推近顺序非法
- 死锁产生的必要条件
- 互斥条件: 共享资源的排他性访问
- 不剥夺条件: 访问时该共享资源不会被剥夺
- 请求并保持: 保持当前资源请求另一个资源
- 循环等待条件: 存在共享资源循环等待链
- 处理死锁的四大方法
- 预防死锁: 破坏死锁的四大必要条件
- 避免死锁: 在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免死锁的发生
- 检测死锁: 允许系统在运行过程中发生死锁,但可设置检测机构及时检测死锁的发生,并采取适当措施加以清除
- 解除死锁: 当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来
- 预防死锁 与 避免死锁 的区别
- 预防死锁是破坏死锁的四个必要条件,严格的防止死锁的出现
- 避免死锁是不破坏死锁的四个必要条件,因为就算4个必要条件也可能不会出现死锁,他的角度是从系统运行中注意避免死锁的产生
- 死锁的预防
- 破坏互斥条件
- 将只能互斥访问的资源改为同时共享访问(可以在中间加一个缓冲队列)
- 将独占锁改为共享锁
- 不是所有资源都能改成可共享的
- 破坏不剥夺/不可抢占条件
- 请求新资源无法满足时必须释放已有资源
- 由OS协助强制剥夺某进程持有的资源
- 实现复杂,代价高
- 此操作过多导致原进程任务无法推进
- 破坏请求并保持条件
- 进程开始时一次性申请所需资源
- 阶段性请求和释放资源
- 破坏循环等待条件
- 对所有资源进行排序
- 对资源的编号应相对稳定,限制了新设备增加
- 进程使用资源的顺序可能与系统编号顺序不同
- 限制了用户编程
- 破坏互斥条件
- 死锁的避免
- 银行家算法
- 系统预判进程请求是否导致不安全状态
- 是则拒绝请求,否则答应请求
- 银行家算法
- 死锁的检测
- 需要一种数据结构,保存有关资源的请求和分配信息
- 提供一种算法,利用这些信息监测是否形成了死锁
- 死锁的解除
- 资源剥夺: 挂起某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但应防止被挂起的进程长时间得不到资源,而处于资源匮乏的状态
- 撤销进程: 强制撤销部分、甚至全部死锁进程并剥夺这些进程的资源。撤销的原则可以按进程优先级和撤销进程代价的高低进行
- 进程回退: 让一(多)个进程回退到足以回避死锁的地步,进程回退时自愿释放资源而不是被剥夺。要求系统保持进程的历史信息,设置还原点。
