Linux——进程基本概念中篇
文章目录
- Linux——进程基本概念中篇
- 一、通过系统调用创建进程——fork
- 1.1 fork的理解
- 1.2 fork的返回值
- 二、进程状态
- 2.1 运行状态
- 2.2 睡眠状态和休眠状态
- 2.3 停止状态和死亡状态
- 2.4 僵尸进程
- 2.5 孤儿进程
- 2.6 前台和后台进程
- 三、进程优先级
- 3.1 查看优先级
- 3.2 调整优先级
- 3.3 并行与并发
- 四、 Linux进程调度之大O(1)调度算法
- 4.1活动队列
- 4.2 过期队列
- 4.3 active指针和expired指针
一、通过系统调用创建进程——fork
通过上篇文章我们了解到,进程 = 可执行程序 + 内核数据结构(PCB) 而操作系统OS就是通过PCB中的内容来管理进程,同时我们也了解到每个PCB中含有着自己的标识符——pid,我们需要额外了解的是对于每一个新创建的进程,其实都是由他的父进程创建而来,所以除了pid以外还有一个ppid来标志这个进程的父进程
1.1 fork的理解
创建进程有三种方式:
- 运行程序
- 执行命令
- 调用fork
由于创建进程的本质就是进程 = 可执行程序(代码和数据) + 内核数据结构(PCB) ,三者在创建本质上是没有区别的
运行程序是./文件名
执行我们通过gcc或者g++编译好的可执行文件,而执行命令其实是Linux将一些常用功能写好打包通过环境变量使得系统能自动找到文件地址,直接输入文件名就能够直接执行
fork的本质是通过函数调用创建一个进程,而这个进程就需要可执行程序(代码和数据) + 内核数据结构(PCB),但fork是一个函数,是写在一段代码中的,只有当这段代码被编译执行成为一个进程(这里我们称为源进程),才会真正调用fork,但这个通过fork创建的新进程的代码从哪里来呢?
当源进程通过fork函数调用创建了一个新进程的时候,操作系统肯定也需要给新进程创建PCB,而代码和数据则是拷贝源进程的,所以我们一般称源进程为父进程,新进程为子进程
默认情况下会继承父进程的代码和数据,而且PCB也会以父进程为模板,初始化子进程的PCB(上下文信息也会继承,所以子进程fork前的代码不会运行),要注意代码是不可修改的(只读)
默认情况下会继承父进程的代码和数据,但也有特殊情况:对于父子都有的变量一个变量,父子任意一方将其修改,使得父进程与子进程中这个变量的值不同的时候,这时操作系统会针对变量发生写实拷贝,父子分别是两个值,因为进程具有独立性
这里补充写实拷贝的概念:
由于进程具有独立性,当一个进程的数据发生修改,为了不让另一个进程也被改变,所以会拷贝一份数据再修改
好处:提高效率(不修改就共享)
1.2 fork的返回值
当调用fork函数时,系统就帮我们创建了一个新的进程,而我们创建一个子进程的目的肯定是需要执行与父进程不同的任务,这时就势必需要通过选择结构来区分父子进程
通过fork的返回值:
失败:返回 -1
成功:
1️⃣ 给父进程返回子进程的pid
2️⃣ 给子进程返回0
如何理解一个函数会有两个返回值?
其实当fork执行到return语句的时候,fork的主要工作已经做完了,也就是说这时已经创建出了子进程,而我们上文中说到,子进程会和父进程有一样的代码和数据,其中就包括上篇中讲到的PC指针,它用于指向下一条需要执行的代码,所以这里看似是两个返回值,实际是父子进程各返回了一次,并且返回的值不同,而用于接收的变量n,由于父子之间的值不同,发生了写实拷贝
值得注意的是,fork后父子进程之间谁先被执行是不确定的,需要看CPU如何调度,先调度谁,谁先执行
二、进程状态
首先想想为什么要有进程状态?
进程状态可以方便OS快速判断进程的状态,方便使其完成各种功能,本质是一种分类
linux中的进程一般有以下状态:
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里
S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep)
D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束
T停止状态(stopped):可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT信号让进程继续运行
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
2.1 运行状态
在上篇中提到需要被运行的进程的PCB都被链接到一个运行队列中,在一个时间片中提供给CPU执行,到了下一个时间片就切换下一个进程,所以队列中的进程在排队被CPU调度
只要在运行队列中,所有的状态都叫做R状态,随时被CPU调用
2.2 睡眠状态和休眠状态
这两个状态实际上都是因为执行程序所需要的资源没有准备好,比如键盘的输入等等,这时由于资源不就绪,程序无法继续执行,进程就会被睡眠,等待资源就绪了才会被唤醒
这些处于睡眠或者休眠状态的进程会根据等待资源的不同,被链入不同的等待队列,比如等待键盘输入的会被链入键盘等待队列,等待磁盘读数的被链入磁盘等待队列
总结:
等CPU资源的队列叫运行队列
等外设资源的队列叫等待队列
当PCB1一旦获取磁盘资源,就会把PCB1的状态改成R(被唤醒),放入运行队列中,等到他获取CPU资源的时候,再读取磁盘
从运行队列到等待队列叫做挂起等待也叫阻塞,从等待队列到运行队列叫唤醒进程
一个进程可能因为运行的需要,在不同的队列里,每个队列代表不同的运行状态
睡眠状态(S)和休眠状态(D)的区别?
S是浅度睡眠,是可以中断的
D是深度睡眠,不可中断
为什么要有两种“睡眠”状态?
当一个进程想要往磁盘写入数据,写入磁盘需要时间,此时该进程就在内存等待结果(处于休眠状态),如果此时可用资源比较少,该进程就会被OS杀死,那么磁盘写入结果的返回就无法送达,可能会引发问题,但是如果是D状态,OS也无法杀死
为什么数据刷新这么快,还是S状态?
数据写入显示器需要时间,而CPU写入的速度太快了(比IO快得多),所以我们以为进程一直在执行,其实进程大部分时间在休眠
2.3 停止状态和死亡状态
停止状态T跟睡眠状态S很像,都可以挂起进程,那么他们有什么区别呢?
睡眠状态S虽然什么事都没干,但会更新核心数据,比方说我们写了个
sleep(10);
到了10秒就会唤醒该进程
而停止状态T就是彻底暂停,不会更新数据,直到人为改变
停止状态就像我们平时调试的时候打的断点,程序运行起来的时候会自动运行到断点的时候停下来,这时就是停止状态
实现原理:信号
kill -19 可以暂停进程
kill -18 可以继续进程
死亡状态(X):
我们在创建进程的时候需要代码数据和PCB,当进程死亡的时候,也要把这些资源回收回去
kill -9 可以杀死进程
2.4 僵尸进程
当一个进程在退出的时候,退出信息会由OS写入到当前退出进程的PCB中,可以允许进程的代码和数据空间被释放,但是不能允许进程的PCB被立即释放,因为进程在退出的时候,要有些退出信息,表明自己把任务完成的怎么样
但当进程退出的时候,进程的所有资源不会立即回收,而是进入僵尸状态(Z),把数据暂时保存(要写入退出信息),目的是为了判断是否将任务成功完成
资源是由父进程进行回收
如果一个进程Z状态了,但是父进程就是不回收它,PCB就要一直存在?
是的,如果父进程一直不回收,PCB就会一直存在
所以僵尸进程我们需要尽量避免,因为会导致过度占用空间和内存泄漏
验证:我们可以让父进程休息,然后杀死子进程就可以看到僵尸状态
2.5 孤儿进程
字面意思,父子进程同时运行,父进程被杀掉,子进程就变成了孤儿进程
孤儿进程将会被1号进程(OS本身)领养
如果是前台进程创建的子进程,如果孤儿了,会变成后台进程
2.6 前台和后台进程
有的状态有的后面带+号有的不带,这表示什么呢?
后面带+号表示前台进程
后面不带+号表示后台进程
区别:
前台进程运行时我们无法输入任何指令,但可以
Ctrl + c
干掉进程
而后台进程运行的时候可以执行命令,但是Ctrl+ c
不能干掉进程,想要杀死就用kill -9
三、进程优先级
首先要知道为什么有优先级,本质是因为资源不足
优先级也是PCB中的一个数据
它决定了程序获得CPU资源的顺序
Linux进程的优先级数值范围: 60 ~99
Linux中默认进程的优先级都是:80
3.1 查看优先级
ps指令:
作用:ps指令主要用来显示linux进程信息
选项:-a 显示所有进程
PRI与NI:
PRI :代表这个进程可被执行的优先级,其值越小越早被执行,默认值为80
NI :优先级修正数据,取值范围(-20 ~ 19)
Linux进程pcb中存在一个nice值:进程优先级的修正数据
pri(新)= pri(old) + nice
注意:
old pri都是从80开始的
nice调整最小是:-20,超过部分统一当成-20
nice调整最大是:19,超过部分统一当成19
总结:
PRI的最终值也取决于NI值(PRI=80+NI)NI范围:[-20,19]
3.2 调整优先级
调整优先级其实就是调整NI值
输入top后按r->输入进程pid->输入nice
我们知道NI的范围只能是-20 ~ 19,那么为什么不让范围更大呢?
OS调度的时候,要较为均衡的让每一个进程都要得到调度
优先级不管怎么调也只是相对的优先,不能出现绝对的优先,不然会让一些进程一直得不到资源,形成进程饥饿
3.3 并行与并发
并行:
多个进程在多个CPU下分别,同时进行运行,就是在任意时刻都有两个以上的进程在运行,这称之为并行
并发:
多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发(也就是之前说的等待队列中的时间片调度)
四、 Linux进程调度之大O(1)调度算法
前面我们了解到进程之间有优先级,但优先级也分实时优先级和普通优先级,在上文中提到的优先级就是普通优先级,这里我们暂不考虑实时优先级
4.1活动队列
那么在Linux系统中,OS到底是如何根据进程的优先级来调度进程的呢?
OS会在系统内维护一个运行队列,我们着重看上图队列中蓝色和红色的部分,队列中会维护两个队列,分别是活跃队列和过期队列,操作系统会根据活跃队列中优先级的顺序依次调用活跃队列中的进程,直到队列中的进程全部被执行
过程:
1. 从0下表开始遍历queue[140]
2. 找到第一个非空队列,该队列必定为优先级最高的队列
3. 拿到选中队列的第一个进程,开始运行,调度完成
4. 遍历queue[140]时间复杂度是常数,但还是太低效了
那么如何优化这个过程呢?
运行队列中维护了一个
bitmap[5]
32 bit * 5 = 160bit 可以记录160个位置的存在信息,一共有140个优先级,所以有140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样便可以大大提高查找效率,此时效率近似于O(1)
4.2 过期队列
如果在操作系统调用活跃队列的途中,来了许多新进程呢,并且新进程比现在执行的进程优先级还高怎么办呢?
新来的进程会按照优先级依次链入过期队列中,等待活跃队列执行完,当活跃队列中没有进程需要执行的时候,OS就会将指向活跃队列的指针和指向过期队列的指针进行交换,这样活跃队列中又有进程了,并且还能保证一定的调度公平性
过期队列和活动队列结构一模一样
过期队列上放置的进程,都是时间片耗尽的进程
当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
4.3 active指针和expired指针
active指针永远指向活动队列
expired指针永远指向过期队列
可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在
在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程