Linux 进程理解(续)
本篇若有难懂之处,可转至本人拙作 Linux 进程概念 ,可能会有相关内容
进程的创建与启动
进程启动
在 Linux 里有很多指令,它们的本质就是可执行程序,运行我们自己写出来的代码也是可执行程序,而这些可执行程序的二进制源文件都是在磁盘硬盘上进行保存,当需要运行时,相关代码和数据被加载进内存,而后 让系统创建进程并运行
所以在 Linux 系统中,大部分对可执行文件的执行操作,本质上都是 运行进程 ,可以使用如下指令查看后台进程:
ps axj
ps
:查看当前 Linux 系统中的进程a
:all 的意思xj
:显示进程的详细信息
例子:我们可以在一台机器打开两个终端,一个运行 ping www.baidu.com
指令,一个使用如下指令来查看后台进程:
ps axj | grep ping
指令解释:将 ps axj
查到的所有 进程 通过管道 |
传给 grep
,让 grep
查找所有关键字为 ping
的 进程
可以看到是有 两个 关键字为 ping
的 进程 ,有一个是我们想要的;还有一个是 grep
的进程,因为 grep
是系统指令,也是可执行程序,而且此指令格式原因会携带 ping
关键字,所以也会有一个关于 grep
查找 ping
的进程
虽然查出有后台进程 ping www.baidu.com
,但这一行的数据我们并不知道是什么意思,可以使用如下指令来查看每一列对应的名称:
ps axj | head -1
指令解释:将 ps axj
查到的所有 进程 通过管道 |
传给 head
,head
再提取出头一行信息,也就是这里的列名称
但还是不够直观,我们可以通过 &&
将两个指令级联起来,按顺序一起执行:
ps axj | head -1 && ps axj | grep ping
进程 pid (process id)
每一个 进程 都有自己的 唯一标识符,叫做 进程 pid (process id) ,在 task_struct
里是无符号整型
使用 ps
指令确实可以查看 pid
,那如果一个 进程 本身想要知道自己的 pid
怎么办?比如说现在写一个死循环的代码,无法使用 ps
,如何查看 pid
呢?
使用 系统调用 接口 getpid()
:
// process.c 源文件
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
// 获得本进程 pid ,是系统调用接口
pid_t id = getpid();
while (1)
{
printf("Process ongoing... id: %d\n", id);
sleep(1);
}
return 0;
}
# makefile 文件
bin=myprocess
src=process.c
$(bin):$(src)
gcc $^ -o $@
.PHONY:clean
clean:
rm -f $(bin)
make
后 ./myprocess
运行此程序即可:
可以看到一台机器两个终端的运行结果 pid
是一样的
终止方式
可以直接 ctrl + c
或者
kill -9 要终止的进程pid
代码方式创建进程(操作)
父进程解释
上面我们已经知道一个程序如何查看自己的进程 pid
,但还不够,一个 进程 都是由其 父进程 创建出来的,所以一个 进程 基本上都有其 父进程
如何查看其 父进程 呢?使用 系统调用 getppid()
:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid = getpid();
pid_t ppid = getppid();
while (1)
{
printf("Process ongoing... pid: %d, ppid: %d\n", pid, ppid);
sleep(1);
}
return 0;
}
如果反复运行终止 myprocess
,每次属于 myprocess
的进程编号 pid
都 不一样,但其父进程的 ppid
却是一样的
[root@localhost concept]$ ./myprocess
Process ongoing... pid: 18324, ppid: 17832
Process ongoing... pid: 18324, ppid: 17832
Process ongoing... pid: 18324, ppid: 17832
^C
[root@localhost concept]$ ./myprocess
Process ongoing... pid: 18325, ppid: 17832
Process ongoing... pid: 18325, ppid: 17832
Process ongoing... pid: 18325, ppid: 17832
^C
[root@localhost concept]$ ./myprocess
Process ongoing... pid: 18326, ppid: 17832
Process ongoing... pid: 18326, ppid: 17832
Process ongoing... pid: 18326, ppid: 17832
^C
[root@localhost concept]$
这里看到 pid
是连续的,这里是巧合,这里先不说原因,不过看到 父进程 的 pid
为 17832
,那这个 父进程 是谁呢?
ps axj | head -1 && ps axj | grep 17832
看到好像是叫 -bash
的进程,这就是传说中的 命令行解释器 啦
创建子进程
接下来就可以 创建子进程 了,使用 系统调用 fork()
即可创建:
// pocess.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("Only one process, pid: %d\n", getpid());
sleep(5);
fork();
printf("Two processes already exist, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(5);
return 0;
}
结果:
[root@localhost concept]$ ./myprocess
Only one process, pid: 19195
Two processes already exist, pid: 19195, ppid: 17832
Two processes already exist, pid: 19196, ppid: 19195
[root@localhost concept]$
可以看见,刚开始只有 pid
为 19195 的进程,当 fork()
之后,printf("Two processes already exist, pid: %d, ppid: %d\n", getpid(), getppid());
这句代码被打印了两遍,说明此刻已经存在两个进程
而 fork()
函数会 以当前进程为父进程,创建一个新的进程为子进程
在此程序开始时,假设 Only one process
后面的 pid
(19195)对应为进程 A
,后面创建的子进程为进程 B
,那么:
- 第一句
Two processes already exist
后面的pid
表明打印这条语句的是 A 进程,父进程ppid
是
17832
,就是 命令行解释器 的进程 - 第二句
Two processes already exist
后面的pid
表明打印这条语句的是 B 进程,父进程
ppid
是 A 进程!!!
如果没看明白,还可以在一台机器上打开两个终端,其中一个先运行如下指令:
while :; do ps axj | head -1 && ps axj | grep myprocess | grep -v grep; sleep 1; done
而后让另一个运行 myprocess
程序,结果:
这两个实验都表明:
程序刚开始的进程在执行 fork()
后,原来的进程会作为父进程创建一个新的子进程,且在这里,子进程和父进程共用后面剩余的代码和数据
使用子进程
为什么要创建子进程,因为我们想 要子进程和父进程执行不一样的代码,并发跑起来,提高运行效率,所以上面的实验就 只是演示创建进程,但 创建进程的目的 不是像上面这么用的
如何让子进程和父进程执行不一样的代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("Only one process, pid: %d\n", getpid());
sleep(5);
pid_t id = fork();
if (id == -1) return 1;
else if (id == 0)
{
// child_process
printf("id: %d, I am child process, pid: %d, ppid: %d\n", id, getpid(), getppid());
sleep(3);
}
else
{
// parent_process
printf("id: %d, I am parent process, pid: %d, ppid: %d\n", id, getpid(), getppid());
sleep(3);
}
return 0;
}
大家可以使用和之前一样的方法来运行此代码查看结果,会发现子进程和父进程跑着不一样的代码
这是因为 fork()
函数有 pid_t
的返回值时,将 有两个返回值 :
- 返回
== -1
,表示 创建子进程失败 - 返回
== 0
,表示 子进程 - 返回
>0
,表示 父进程
那么 根据返回值 就可以将 父子进程 区分开,之所以可以区分开,是因为 fork() 函数后面的代码就已经出现了两个进程,所以
if (id == -1) return 1;
else if (id == 0)
{
// child_process
printf("id: %d, I am child process, pid: %d, ppid: %d\n", id, getpid(), getppid());
sleep(3);
}
else
{
// parent_process
printf("id: %d, I am parent process, pid: %d, ppid: %d\n", id, getpid(), getppid());
sleep(3);
}
return 0;
其实上面这么多代码都是 父子进程共享的状态,之所以被区分开,是因为 父进程 和 子进程 的 fork()
返回值不一样,导致 if
选择语句执行不一样,呈现出不一样的状态
不过 fork()
函数如何做到可以返回两个值的呢?
很显然,在一个进程里并不可能,因为 C 语言语法并不支持,那如果是两个进程呢?
也就是说,在 fork()
执行到中间的某一部分时,子进程就已经被创建出来且可以被调度了,那么 fork()
函数的后续代码是被两个进程共享的,所以执行到 fork()
的 return
返回语句时,子进程返回子进程的,父进程返回父进程的,两个返回值根本不冲突!!!
一次创建多个进程
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
void RunChild()
{
while (1)
{
printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
int main()
{
const int process_cnt = 5; // 要创建的进程个数
for(int n = 0; n < process_cnt; ++n)
{
pid_t id = fork();
if (id == 0)
{
RunChild();
}
sleep(1);
}
while (1)
{
sleep(1);
printf("I am parent, pid: %d, ppid: %d\n", getpid(), getppid());
}
return 0;
}
测试一下咯 ^ ^
task_ struct 内容属性
在 Linux 中描述 进程 的结构体叫做 task_struct
,我们已然知道被 malloc
出来的一个 task_struct
节点就是一个 进程控制块 ,那 task_struct
是如何描述 进程 的呢
- 标示符 :描述本进程的 唯一标示符(
pid
/ppid
)(int
类型),用来区别其他进程 - 状态 :任务状态(运行、休眠、阻塞、挂起等等),退出代码,退出信号等
- 优先级 :相对于其他进程的优先级,理解为在 Linux 里将
task_struct
进行排队 - 程序计数器 :程序中即将被执行的下一条指令的地址
- 内存指针 :包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据 :进程执行时处理器的寄存器中的数据
I/O
状态信息 :包括显示的I/O
请求,分配给进程的I/O
设备和被进程使用的文件列表- 记账信息 :可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等
- 其他信息
如何实际的查看这些属性呢?
查看详细属性
进程的信息可以通过 /proc
系统文件夹查看,其原名就是 process
进程的意思;
当你开启一个进程后,会在 /proc
系统文件夹里出现一个以此进程 pid
命名的文件夹,里面就包含此进程的详细信息,我们还是以 myprocess
为例:
ls /proc/要查看的进程pid/
我们先讲两个目前来说可以理解的:
exe
运行 ll /proc/要查看的进程pid/
查看
很显然,这里的 exe
指向此进程对应的可执行文件,如果在 不打断此进程的情况下 删除 myprocess
文件呢?
如下:进程还在继续,但 exe
已经找不到源可执行文件的位置,显示 deleted
已被删除,为什么呢?
这是因为你删除的只是磁盘上的文件,但内存里还存在此进程的代码和数据,如果是上百 G 的游戏文件,而你的内存只有 16 G,可不敢这样,不然会真的找不到可执行文件
所以 PCB
会 记录自己是由哪个文件(可执行程序路径)加载进内存的
cwd
指 当前进程的工作路径(current work dir)
每个进程在启动的时候,会记录自己在哪个路径下启动,此路径就是当前路径
进程状态
Linux 的常见进程状态
一个进程被运行后一定会有相对应的状态,抽象出来就是在 task_struct
结构体内部有一个名为 status(int)
的属性,用整型为几个不同的状态进行宏定义,只要 status
赋为对应状态宏定义的值,即可表示或更改一个进程的状态,仅此而已
那 Linux 里有几个状态呢?
下面的状态在 kernel 源代码里定义:
static const char * const task_state_array[] = {
"R (running)", // 0
"S (sleeping)", // 1
"D (disk sleep)", // 2
"T (stopped)", // 4
"t (tracing stop)", // 8
"X (dead)", // 16
"Z (zombie)", // 32
};
R
运行状态(running):并不意味着进程一定在运行中,它表明 进程要么是在运行中要么在运行队列里S
睡眠(休眠)状态(sleeping):意味着进程 在等待事件或资源完成(这里的睡眠有时候也叫做 可中断睡眠,随时可以被外部信息打断(interruptible sleep))D
磁盘休眠状态(Disk sleep):有时候也叫 不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会 等待 IO 的结束,保证数据不被丢失T
停止状态(stopped):可以通过发送SIGSTOP
信号给目标进程,来 暂停目标进程;也可以通过发送SIGCONT
信号给这个被暂停的进程让它 继续运行X
死亡状态(dead):这个状态 只是一个返回状态,你不会在任务列表里看到这个状态
几个问题
设备速度差带来的迷惑的现象
首先来看这样一份代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while (1)
{
printf("This is a running process,pid: %d\n", getpid());
}
return 0;
}
在一台 Linux 机器(CentOS 7)上打开两个终端,一个运行此程序不中断,另一个运行如下指令:
while :; do ps axj | head -1 && ps axj | grep mystatus | grep -v grep; sleep 1; done
有如下现象:
可以看到圈出来的状态居然是 S
,难道此程序不是在跑着吗?那应该是 R
呀?为什么是 S
呢?
不急,继续操作,我们把上面 C 语言里的 printf("This is a running process,pid: %d\n", getpid());
语句注释掉,只留一个空的死循环,再重复如上实验,再来观察结果是:
这回状态结果为啥又是 R 了呢?
首先,现在理解一个进程在跑至少是 CPU 要执行此进程对应的代码吧?也就是 R 状态,空循环符合预期;但循环内若有 printf()
语句,对于 CPU 来说,把 printf()
里的内容刷新到屏幕上需要 CPU 搬运吗?不需要,是在内存里直接刷新到屏幕上的
那么 对于此进程而言, CPU 除了 要交代系统:请把缓冲区的内容刷新到屏幕上 这件事之外,就是在等待,直到内存把 printf()
里的内容刷新到屏幕上对吧?而 I/O 接口的速度远远没有 CPU 快,那也就是说 CPU 在执行这段死循环的时候,大部分的时间都是在等待,偶尔在执行 printf()
语句时出现的 R
状态又因为太快而捕捉不到,才导致所看到的一直都是 S
状态结果
如何使用 kill 将进程修改为 T 暂停状态
我们在此前已经利用 kill
命令的一个选项来终止一个进程,就是 -9
选项,那 kill
的选项都有什么呢?使用如下指令查看:
kill -l
使用 kill 就属于 使用一个进程去控制目标进程 ,而上图显示的就是 Linux 的 信号 ,所以开头均为 SIG
-9
就是 SIGKILL
,去了开头的 SIG
就是 kill
的意思
所以使用 kill
指令可以 向目标进程发信号 ,上面 T 状态提到的 SIGSTOP
信号和 SIGCONT
信号就分别对应 19 号和 18号
使用 kill
只需要选择对应的信号再加上进程 pid
即可:
kill -19 将要被暂停的进程pid
kill -18 让 被暂停的进程继续运行 的进程pid
这里就不演示了
D 状态的休眠如何就能保证数据不被丢失
我们先想象一个这样的场景:
一个银行机构的服务器端:在内存里,有一个进程 A 被加载后启动了,它的使命是将银行用户的转账流水存入外设磁盘里,这堆数据十分庞大
开始后,进程 A 发出请求,磁盘响应进行数据存储,于是进程 A 开始 等待外设磁盘转存完毕;如果此时进程 A 是 S 的 可中断睡眠 状态,且好巧不巧,此时内存资源严重不足,操作系统即将面临被挂掉的风险,为了保证系统的稳定服务,操作系统不得不开始寻找可以被释放的资源,此时发现进程 A 居然正在 S
的 可中断睡眠 状态,操作系统就 无差别的回收 进程 A 的资源,以缓解系统的空间资源问题
过了十几秒后,磁盘存储完毕,但不幸的是,由于某些硬件的原因,这堆十分庞大的数据并没有存储成功,但向进程 A 发出存储失败的信号后,并没有回应,因为进程 A 已经被操作系统强行回收了,但磁盘并不能因为这个而抉择怎么办,所以磁盘会继续接收其他进程的请求继续工作下去
那么结果就是:这一堆十分庞大的数据被丢失,给银行造成了严重的损失!!!
所以 D
的 不可中断睡眠状态 就有了大作用,如果进程 A 是 D
状态,深度睡眠状态,操作系统就无法回收其资源,也就不会导致数据丢失
D 状态进程如何苏醒
- 进程等待
I/O
设备完成,自己醒来 - 重启
- 断电
一般情况下,你机器上跑的进程并不会出现长时间的 D
状态;如果存在,就说明你的机器上 I/O
压力太大了或者是你的系统正在处于要挂的边缘
僵尸进程和孤儿进程
Z (zombie) - 僵尸进程
- 僵死状态(Zombies)是一个比较特殊的状态:当进程退出并且父进程(使用
wait()
系统调用)没有读取到子进程退出的返回代码时就会产生 僵死(尸)进程 - 僵死进程 会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码;所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程就会进入
Z
状态
我们做个实验,仔细阅读以下 C 语言代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
// child
int cnt = 5;
while (cnt)
{
printf("This is the child process, cnt: %d, pid: %d, ppid: %d\n", cnt, getpid(), getppid());
--cnt;
sleep(1);
}
}
else
{
// parent
while (1)
{
printf("This is the parent process, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
上述代码并不难看出,子进程只会被循环执行 5 秒,而父进程会一直被执行,而又不对子进程读取,那么 5 秒后的预期结果将会看到子进程从 S
状态(上述说过的迷惑现象)变为 Z
状态(僵尸状态)
验证:
同样,在一台 Linux (CentOS 7) 机器上打开两个终端窗口,一个运行上述代码不中断,另一个执行以下指令:
while :; do ps axj | head -1 && ps axj | grep mystatus | grep -v grep; sleep 1; done
得到程序打印结果:
得到进程信息结果:
这里的子进程已经运行完毕,但因为需要维护自己的退出信息,于是在自己的 task_struct
结构体内记录自己的退出信息,未来可以被父进程读取
如果父进程一直不读取,那么僵尸进程会一直存在,虽然僵尸进程的代码和数据已经被释放,但其数据结构节点还在,一直不释放,就会造成内存泄漏问题
一旦被父进程读取,那么僵尸进程就会变为一瞬间的 X
状态,最终查无此进程
为什么进程的退出状态必须被维持下去呢?
因为它要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于 Z
状态,且连 kill -9
都杀不掉
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在 task_struct(PCB) 中,换句话说, Z状态一直不退出, PCB 一直都要维护
那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费;因为数据结构对象本身就要占用内存
而我们在命令行下启动的进程一般都是 bash
的子进程,而 bash
进程会回收 Z
状态进程的
孤儿进程
父进程先退出,子进程就被称之为 “孤儿进程”,我们可以使用验证上面僵尸进程的方法来验证孤儿进程,下面是 C 语言代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
// child
while (1)
{
printf("This is the child process, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
else
{
// parent
int cnt = 5;
while (cnt)
{
printf("This is the parent process, cnt: %d, pid: %d, ppid: %d\n", cnt, getpid(), getppid());
sleep(1);
--cnt;
}
}
return 0;
}
那此时子进程没有父进程读取退出信息,不就是僵尸进程吗?子进程进入 Z
状态后,那该如何处理呢?
此时子进程会被 1号进程(暂且理解为 OS 本身)领养,当然要有 1号进程 回收喽,所以被领养后就不再是僵尸进程了,而是孤儿进程,是可以被 kill
掉的
进程的阻塞、挂起和运行
上面讲述的进程状态反映的是具体实现,我们通过 Linux 里表示进程状态的静态数组里的值一一展开讲述
那理论呢?接下来就是咯
运行状态(R)
每台设备里的 CPU 都要维护一个名为 运行队列(struct runqueue)的数据结构,你的进程想要被运行,就得被放入 CPU 所维护的 运行队列 里,往后 CPU 要调度一个进程,就只需要找到自己维护的 运行队列 即可
接下来就是 CPU 取出要运行的进程,将其在内存中对应的代码和数据信息加载到 CPU 的寄存器中,开始调度此进程
在 Linux 里,如果此进程正在被 CPU 执行,很显然它就是 R
状态(运行状态),但如果此进程正在 CPU 所维护的 运行队列 里,那它也是 R
状态
那这么多正在排队想要被调度的进程,都要等到 CPU 把前面的进程一个个的运行完吗?并不是,当代操作系统内核是基于 时间片 进行轮转调度的,让多个进程以切换的方式进行调度,每个进程只调度一个 时间片 的时间,不管有没有被执行结束,CPU 都会调度下一个进程;如果上一个进程的代码没有被执行完,将会被重新扔进 运行队列 里等待下一次的调度
在一个时间段内同时得以推进多个进程的代码,这就叫做 并发
上面这么简单,所以这肯定不是 Linux 的调度算法,只是一个增加理解的例子罢了
如果是多核多个 CPU ,那在机器的任何时刻,都同时有多个进程在同时运行,我们叫做 并行
阻塞态
先给出一个直观的理解:
学过 C 语言的应该都使用过 scanf()
函数,那进程 调度到此函数位置后,你却不做任何输入操作时,请问进程现处于什么状态呢?为什么是处于这个状态呢? scanf()
函数又在等待什么资源呢?
写一段带有 scanf()
函数的小代码使用上面提到的方法测试,卡到 scanf()
函数时,后台查看此进程就会发现是处于 S
状态,所以 阻塞态 实际上的反馈就是 D
状态或 S
状态
当前进程为什么要被 阻塞 呢?因为此进程要获取你键盘上的资源,所以要等待你键盘上的数据资源准备就绪后交给他
那我们刚才在 运行状态(R) 里提到,进程被放入 运行队列 里后,此时一旦就绪就是 运行状态
那我们凭什么判定此时的 scanf()
函数的 S
就是阻塞态呢?
这就是 OS 管理软硬件资源的强大之处,我们看到它可以管理 进程(软件),同样的方法它也可以 管理硬件,核心就是 先描述,再组织:
我们需要可以完全描述一个硬件的结构体,里面包含各种属性,例如:类型,状态等等;类型可以使用宏定义来判断这是个什么设备,状态可以描述这个设备的就绪状态
而 OS 要管理这个硬件,至少要有相应的数据结构来整合这些结构体,往后对硬件的管理就是对此数据结构的增删查改
仅仅是这些还是无法说明进程是阻塞态啊
对于描述每一个硬件的结构体,里面一定包含一个属性是指向等待自己的 等待队列(wait_queue) 的指针,当进程运行至 scanf()
函数后,此进程的 PCB
就会从 CPU 的 运行队列 里被剥离出来,再被链入键盘(KEY_BOARD)结构体所指向的 等待队列 中,而此时键盘结构体的状态属性也就是 非就绪状态,有几个进程要等待键盘资源咱就链入几个 PCB
,那么在 等待队列 中的进程就没有被调度,而是处于 S
状态,也就是 阻塞态
一旦读取到键盘上的回车,键盘结构体的状态属性也就是 就绪 ,此时的进程就从键盘(KEY_BOARD)结构体所指向的 等待队列 中剥离下来,再被链入到运行队列里重新被 CPU 调度,这个过程就被称为 唤醒
如此一来,阻塞和运行的状态变化,往往伴随着 PCB 被链入不同的队列里,所以入队列的不是进程的代码和数据
所以 状态变化一定是进程的 PCB 被链入到不同的队列当中
挂起态
大家知道在磁盘里存在 swap
分区吗?可能和内存大小一样,也可能是内存的 1.5 倍,甚至是 2 倍,那它是什么作用呢?咱先等一等
想象一个场景:
一台机器已经飞速运行了很长一段时间,内存空间资源即将被耗尽,而此时有一个处于阻塞态的进程 A 正在等待 I/O
或某一事件的就绪,很显然 A 进程并没有被 CPU 调度,为了可以保证系统的稳定性,OS 就会把目光放在进程 A 的身上
由于进程 A 正在处于 S
状态,没有被 CPU 调度,OS 就会把进程 A 在内存里对应的 代码和数据 给放进磁盘的 swap 分区中,这个过程被称之为 唤出,释放其原来的空间以缓解燃眉之急,但是进程 A 的 PCB
并不会被释放掉,所以此时的进程 A 就变成了 阻塞挂起态
当未来进程 A 等待的事件就绪或完成,需要被调度时,OS 再把进程 A 在 swap
分区里的数据调回内存,这个过程被称之为 唤入,但内存空间依然吃紧啊?没关系,OS 可以寻找下一个可以被挂起的进程,用这个新的被挂起的空间来调度进程 A
将内存资源交换到外设当中,看似拆东墙补西墙,但这却是使用内存资源更合理的方式
进程的 挂起态 会和其他状态进行组合,只要系统可以缓得过来,时机到了 OS 在给它唤醒就是,所以用户层是感知不到的,但频繁的唤入唤出会导致效率问题
进程切换
首先要理解:
CPU 内部存在非常多的寄存器,寄存器本身是硬件,具有数据存储能力,但 CPU 内部只有一套寄存器硬件
但 寄存器 != 寄存器的存储内容, CPU 内部所有寄存器存储的数据,可以有多套,有几个进程,就至少会有几套和该进程对应的 临时数据,而这套 临时数据 叫 进程的上下文
那所谓 进程切换 就是:
时间片到后,进程在 CPU 里被调度结束,会把 CPU 寄存器里的数据交给进程自己,在 Linux 里会存储在 tss_struct
的结构体内部(是 task_struct
的成员变量,被叫做 任务状态段,存储进程自己的上下文),这个过程叫做 保护上下文,接下来此进程会回到运行队列的结尾, CPU 再继续调度其他的进程
等到 CPU 下一次要调度此进程的数据时,会从 tss_struct
里将数据恢复至 CPU 内部的寄存器,从上一次运行结束的地方继续运行,这个过程叫做 恢复上下文
所以进程切换最重要的事情,就是 对进程上下文数据的保护和恢复
进程优先级
什么是优先级
- CPU 资源分配的先后顺序,就是指 进程的优先权(priority)
- 优先权高的进程有优先执行权利;配置进程优先权对多任务环境的 Linux 很有用,可以改善系统性能
- 还可以把进程运行到指定的 CPU 上,把不重要的进程安排到某个 CPU,可以大大改善系统整体性能
讲白了就是:指定一个进程获取某种资源的先后顺序
在 Linux 里,反映在 进程控制块 task_struct
上就是此结构体内部的一个字段或多个字段,也就是用一个整数或多个整数来评估优先级的高低
在 Linux 里,优先级数字越小,优先级越高
为什么要有优先级
请问你去食堂吃饭,或者是去图书馆自习时,为什么要有先来后到,为什么要排队呢?
计算机运行时会有很多进程,进程与进程之间的重要程度是不一样的,但 进程需要访问的资源却始终是有限的,所以必须确定谁先谁后
OS 关于调度和优先级的原则:
现在面向普通用户的 OS 基本上都是分时 OS ,是基于时间片调度轮转进程的操作系统,这样就可以保证在一段时间里同时推进多个进程,此乃并发,如果有进程长时间插队占用 CPU 资源,那后面需要被调度的进程就会导致 饥饿问题(长时间不被 CPU 调度),所以 调度要保持基本的公平
- 竞争性:系统进程数目众多,而 CPU 资源只有少量,甚至 1 个,所以进程之间是具有竞争属性的;为了高效完成任务,更合理竞争相关资源,便具有了 优先级
- 独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰
Linux 的优先级的特点以及查看方式
我们可以使用下面指令来查看:
ps -al
注意查看我圈出来的内容:
上图中:
PRI
(priority) 为当前进程的 实际优先级( nice
值为 0 时,为 默认优先级)
NI
为当前进程优先级的 修正数据,也就是 nice
值
UID
:代表执行者的身份
PID
:代表这个进程的代号
PPID
:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
所以:实际优先级 = 默认优先级 + nice 值,如此达到进程优先级的 动态修改 目的;调整进程优先级,在 Linux 下,就是调整进程 nice
值
(并不建议直接更改 默认优先级,因为可以会影响进程当前的调度;如果改 nice
值,会在下一次轮转的开始重新计算进程优先级,由于 CPU 速度极快,轮转也快,所以用户几乎感知不到修改延迟)
优先级调整
nice
值不允许用户随意大幅度调整,进程优先级必须可管可控,是有范围的:
nice
其取值范围是 -20
至 19
([-20, 19]
),一共 40 个级别,而上图的 mypriority
程序进程的优先级计算都是以 默认优先级 为 80 而计算的,所以不论是进行多少次更改,都有 实际优先级 = 80 + nice 值,查看 PRI
即可
操作:
首先输入 top
指令,再按 r
键进行修改,输入要修改的进程 pid
,再输入 nice
值即可
注意:
不可随便调整进程优先级,你会发现当你调整一次优先级后,再调整此进程就需要 root
权限,说明系统都不想你多次修改进程优先级