文章目录
- 一、基本概念与基本操作
- 1.进程的概念(描述进程-PCB)
- 2.task_ struct 里的内容
- 3.查看进程标示符的方法(getpid函数,系统调用)
- 4.查看进程的方法
- 4.1 进程的信息可以通过 /proc 系统文件夹查看(不推荐,查看起来不方便)
- 4.2 top指令
- 4.3 ps 指令(推荐使用)
- 5.父进程 和 子进程(getppid函数)
- 6.创建子进程(fork函数,系统调用)
- 二、进程状态
- 1.操作系统中的进程状态(整体认识)
- 1.1 进程状态的作用和表示
- 1.2 进程之间是如何进行组织的(重点)
- 1.3 进程状态(新建状态、运行状态 和 阻塞状态)
- 1.4 阻塞挂起 和 就绪挂起
- 2.Linux操作系统中的进程状态
- 2.1 Linux下的进程状态表示
- 2.2 R运行状态、S睡眠状态、D磁盘休眠状态、T停止状态 和 t追踪停止状态
- 2.3 Z僵死状态(zombie)-僵尸进程
- 2.4 孤儿进程
- 三、进程优先级
- 1.进程优先级的概念
- 2.查看进程优先级的方法
- 3.更改进程优先级的方法
- 4.补充概念-竞争、独立、并行、并发
- 四、进程切换
- 五、Linux2.6内核进程O(1)调度队列
一、基本概念与基本操作
1.进程的概念(描述进程-PCB)
系统当中会同时存在大量的进程。操作系统要对大量的进程进行管理,管理的本质:先描述再组织
结构体struct pcb(process control block,进程控制块),pcb中存放着进程信息(进程属性的集合),一个pcb描述一个进程。
再使用数据结构链表把所有把所有pcb组织起来。这样,对进程的管理,就转换成了对链表的增删查改。
结论:进程 = pcb(内核数据结构) + 代码和数据(可执行程序)
pcb 是操作系统教材中的总称,Linux操作系统下的pcb是: task_struct
• 在Linux中描述进程的结构体叫做task_struct。
• task_struct是Linux内核的⼀种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
2.task_ struct 里的内容
task_ struct 里的内容主要如下:
- 标示符: 描述本进程的唯⼀标示符,用来区别其他进程。
- 状态: 进程状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据。
- I∕O状态信息: 包括显示的I/O请求,分配给进程的I∕O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
标示符、进程状态、优先级、上下文数据后续详细介绍
3.查看进程标示符的方法(getpid函数,系统调用)
进程标示符其实就是一个整型,是描述进程的唯⼀ ID 号(就像学生的学号)
如何获取一个进程的标示符,通过 getpid()函数,它的返回值类型是pid_t,其实就是typedef的整型
演示 getpid()函数的使用:
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ll
total 24
-rw-rw-r-- 1 zh zh 129 May 24 17:00 makefile
-rwxrwxr-x 1 zh zh 8464 May 24 18:41 process
-rw-rw-r-- 1 zh zh 191 May 24 18:40 process.c
-rw-rw-r-- 1 zh zh 1640 May 24 18:41 process.o
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main()
{while(1){printf("我是一个进程,pid:%d\n",getpid()); // 返回调用getpid函数的进程的标示符sleep(1);}return 0;
}
补充: pid_t 类型的定义过程解析
-
初始声明
在/usr/include/sys/types.h
文件中,pid_t
被定义为__pid_t
类型:#ifndef __pid_t_defined typedef __pid_t pid_t; #define __pid_t_defined #endif
-
中间层展开
进一步查看/usr/include/bits/types.h
文件,可以发现__pid_t
实际上是一个基于宏定义的扩展类型:__STD_TYPE __PID_T_TYPE __pid_t;
此处通过
__STD_TYPE
扩展了标准类型的定义方式,并最终依赖于__PID_T_TYPE
。 -
底层实现
接下来在/usr/include/bits/typesizes.h
中找到__PID_T_TYPE
的定义:#define __PID_T_TYPE __S32_TYPE
表明该类型实际上是
__S32_TYPE
类型的一个实例化。 -
基础类型确认
最终回到/usr/include/bits/types.h
文件,可以看到__S32_TYPE
被定义为普通的int
类型:#define __S32_TYPE int
至此明确了
pid_t
的本质——它是一种带符号的 32 位整数(即int
),但在不同平台下可能因编译器设置有所不同。
4.查看进程的方法
4.1 进程的信息可以通过 /proc 系统文件夹查看(不推荐,查看起来不方便)
/proc 系统文件夹下的数字就是进程的标示符,以标示符命名方便用户查找对应进程信息
如:要获取PID为1的进程信息,你只需要查看 /proc/1 这个⽂件夹。
示例:运行一个 process 程序(该进程调用了getpid函数得到了自己的标示符):
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ll
total 24
-rw-rw-r-- 1 zh zh 129 May 24 17:00 makefile
-rwxrwxr-x 1 zh zh 8464 May 24 18:41 process
-rw-rw-r-- 1 zh zh 191 May 24 18:40 process.c
-rw-rw-r-- 1 zh zh 1640 May 24 18:41 process.o
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main()
{while(1){printf("我是一个进程,pid:%d\n",getpid()); // 返回调用getpid函数的进程的标示符sleep(1);}return 0;
}
该进程运行之后,在 /proc 系统文件夹下查到了对应标示符的文件:
展示对应标示符的目录文件下的文件信息(想要查看该进程详细信息,还得查看该目录下文件中的内容):
我们就不查看该目录下文件中内容了。实际上进程的信息一般不会通过 /proc 系统文件夹查看,因为它里面保存的信息太杂太多,不方便查看。介绍这种查看进程信息的方式是为了介绍当前工作路径的知识,我们只需要关注两个信息:
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ll
total 24
-rw-rw-r-- 1 zh zh 129 May 24 17:00 makefile
-rwxrwxr-x 1 zh zh 8464 May 24 18:41 process
-rw-rw-r-- 1 zh zh 191 May 24 18:40 process.c
-rw-rw-r-- 1 zh zh 1640 May 24 18:41 process.o
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ pwd
/home/zh/test
/home/zh/test/process ,进程信息中会记录磁盘上可执行文件的保存位置,
cwd -> /home/zh/test ,进程信息中默认记录的当前工作路径就是可执行文件所处的路径
还记得C语言中的 fopen函数,当打开没有明确指定文件路径的文件时,fopen函数会直接在当前路径下寻找它,当前路径就是进程信息中记录的当前工作路径:
FILE * pFile = fopen(“myfile.txt”, “w”);
4.2 top指令
-
top 命令简介
top
是 Linux 系统下一种动态实时展示系统进程状态的工具。它不仅可以显示当前系统的运行状态,还可以帮助用户快速定位高资源消耗的进程。 -
启动方式
通过终端输入top
即可启动该命令。执行后会进入一个交互式的界面,在此界面上可以查看按 CPU 使用率排序的进程列表以及其他重要信息,比如内存使用情况、交换空间使用情况等。
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ top
-
主要功能特性
(1)实时更新:默认情况下每三秒钟刷新一次数据。
(2)进程排序:支持按照不同的指标(如 CPU 或者 Memory)重新排列进程顺序。
(3)终端操作友好:提供多种快捷键来改变视图模式或者筛选条件。 -
常用选项解析
(1)自定义延迟时间间隔
利用 -d
来指定刷新的时间差,默认单位为秒并且允许带小数部分表示十分之一秒精度:
top -d 5.0 # 设置刷新周期为五秒整
(2)仅关注某些特定 PIDs 的行为表现(推荐使用)
当只需要跟踪几个固定 ID 所属的任务变化趋势而不关心其他背景噪音干扰的时候非常有用:
top -p <pid1>,<pid2>...
例如针对单个 pid 号码的操作如下所示:
top -p 15124
4.3 ps 指令(推荐使用)
ps axj 查看当前所有进程的部分信息:
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ps axj
ps axj | head -1 展示头部第一行信息:
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ps axj | head -1 PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
ps axj | head -1 ; ps axj | grep “myprocess” 先打印头部第一行信息,再打印ps axj的内容中含"myprocess"的行:
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ps axj | head -1 ; ps axj | grep "myprocess" PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
14487 15253 15253 14487 pts/1 15253 S+ 1000 0:00 ./myprocess
14845 15257 15256 14845 pts/2 15256 S+ 1000 0:00 grep --color=auto myprocess
大部分命令,包括grep命令,都是一个可执行文件,它在工作的时候就变成了一个进程,因为grep查"myprocess"字符串时其实自己也就包含了"myprocess"字符串,所以查找时把自己也给查出来了,我们不需要去管,忽略它这一行。
5.父进程 和 子进程(getppid函数)
getppid()函数:获取当前进程的父进程的标示符
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main()
{while(1){printf("我是一个进程pid:%d,我的父进程ppid:%d\n",getpid(),getppid());sleep(1);}return 0;
}
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ll
total 24
-rw-rw-r-- 1 zh zh 135 May 24 20:26 makefile
-rwxrwxr-x 1 zh zh 8520 May 24 21:35 myprocess
-rw-rw-r-- 1 zh zh 223 May 24 21:34 process.c
-rw-rw-r-- 1 zh zh 1736 May 24 21:35 process.o
Linux系统中,是通过父进程创建子进程的方式,来使系统中的进程增多的!
我们在命令行中,执行命令/程序的时候,它们都会变成进程,这些进程的父进程都是-bash。换句话说,我们在命令行执行的命令/程序形成的进程都是由-bash进程创建的子进程。
6.创建子进程(fork函数,系统调用)
fork函数:创建一个子进程
返回值:创建子进程成功,会有两个返回值,返回子进程的pid(标示符)给父进程,返回 0 给子进程;创建子进程失败,返回 -1 给父进程。
示例演示fork函数使用:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main()
{printf("我是一个进程pid:%d\n",getpid());fork();printf("你能看到这条信息吗,pid:%d,ppid:%d\n",getpid(),getppid());sleep(1);return 0;
}
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ll
total 24
-rw-rw-r-- 1 zh zh 135 May 24 20:26 makefile
-rwxrwxr-x 1 zh zh 8520 May 24 22:38 myprocess
-rw-rw-r-- 1 zh zh 234 May 24 22:38 process.c
-rw-rw-r-- 1 zh zh 1872 May 24 22:38 process.o
现象:fork()之后的打印函数被执行了两次(父进程 和 子进程都执行了该打印函数)
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess
我是一个进程pid:16157
你能看到这条信息吗,pid:16157,ppid:15594
你能看到这条信息吗,pid:16158,ppid:16157
由上述现象可得 fork函数确实创建了一个新的子进程,那么 fork函数到底是如何创建子进程的呢?
Linux下的进程 = task_struct(描述进程信息的结构体) + 代码和数据(可执行程序)
所有,要创建子进程的话,就得创建它的 task_struct ,以及还得有代码和数据。
(1)在 Linux 中,创建一个新的子进程时,子进程的task_struct被创建出来,初始化时是以父进程的task_struct为模板的,只有些许内容需修改,如 pid 和 ppid 的值
(2)我们自己写的程序中,通过fork函数创建子进程时,子进程的代码和数据不是从磁盘文件中加载的,子进程刚创建出来时,默认共享父进程的代码和数据(当父进程 或 子进程写入数据时,数据会写实拷贝,进程在数据层面要保证独立性)
解释上述代码中,fork()之后的打印函数被执行了两次的现象:
fork()之后,子进程被创建,子进程共享了父进程的代码,子进程和父进程一样,都会执行fork函数之后的代码,所以打印函数会被执行两次(父进程一次,子进程一次)
一般用fork函数创建出子进程之后,会使用 if-else 语句,使父子进程分别执行不同的代码区域,从而实现让父子进程执行不同的任务:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main()
{printf("我是一个进程pid:%d\n",getpid());pid_t id = fork();if(id == 0){ // 子进程执行printf("我是子进程,pid:%d,ppid:%d\n",getpid(),getppid()); }else if(id > 0){// 父进程执行printf("我是父进程,pid:%d,ppid:%d\n",getpid(),getppid()); }else{perror("fork:"); // fork创建子进程失败}sleep(1);return 0;
}
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ll
total 24
-rw-rw-r-- 1 zh zh 135 May 24 23:53 makefile
-rwxrwxr-x 1 zh zh 8616 May 24 23:58 myprocess
-rw-rw-r-- 1 zh zh 460 May 24 23:58 process.c
-rw-rw-r-- 1 zh zh 2192 May 24 23:58 process.o
[zh@iZbp1dr1jtgcuih41mw88oZ test]$ ./myprocess
我是一个进程pid:17893
我是父进程,pid:17893,ppid:17621
我是子进程,pid:17894,ppid:17893
实现让父子进程分别执行不同的代码区域的原理就是:fork函数创建子进程成功后,会有两个返回值,返回子进程的pid(标示符)给父进程,返回 0 给子进程。
fork函数是如何做到分别给父子进程返回不同的返回值的呢?
首先输出一个结论,当一个函数执行到return语句的时候,表面这个函数的主体功能已经做完了!
fork函数的主体功能是创建子进程,在执行到fork函数的return语句之前,子进程就已经被创建出来了,子进程共享了父进程的代码,所以fork函数的return语句其实会被执行两次(父进程一次,子进程一次),这就是fork函数有两个返回值的原因。
能达到父子进程的 id 变量是不同的数值的原因: fork函数return返回数值本质是写入,当父进程 或 子进程写入数据时,数据会写实拷贝,进程在数据层面要保证独立性,所以父子进程的 id 变量可以保存不同的数据。(这也侧面反映了父子进程在执行fork函数的return语句时返回的数值是不同的)
二、进程状态
1.操作系统中的进程状态(整体认识)
1.1 进程状态的作用和表示
下图中各种进程状态之间的切换仅供参考:
每个进程都有对应的进程状态,进程状态的作用是什么?
处于何种进程状态,决定了进程接下来要进行的工作
进程状态是怎么表示的?
不同的进程状态被define成了不同的数字,所以进程状态就是一个数字
1.2 进程之间是如何进行组织的(重点)
先说结论:进程是使用数据结构双链表进行管理的
我们首先会想到下图设计方式来实现双链表管理进程(但实际上并不是):
实际上,是采用在task_struct中添加一个封装了前后指针的结构体来实现 双链表管理进程的形式:
这种封装方式有很多优点(1.3 进程状态 中详述),但也带来了一些问题,通过struct list_head node结构体封装的前后指针后,前后指针找到的只是该结构体节点在pcb中的位置,而我们想要访问pcb中的所有进程属性首先得知道pcb的首地址。 我们只知道一个pcb中struct list_head node的地址,如何通过它求得pcb的首地址呢?
1.3 进程状态(新建状态、运行状态 和 阻塞状态)
计算机中的资源是有限的,而进程数量众多,所以进程之间是存在竞争关系的,那么进程之间究竟在竞争哪些资源呢?
答:进程是处于内存之中的,内存可以与外设 和 CPU 之间直接进行交互。进程竞争的资源其实就分为两类:CPU资源 和 外设资源
- 进程是如何竞争CPU资源的:
结论1: 因为进程pcb中设置很多个struct list_head类型的节点,所以一个进程pcb可以同时连接在多个双链表结构中。
结论2: 进程只要被创建出来就会连入所有进程的全局双链表结构,直到该进程被删除,才会移除出该链表结构
结论3: 当进程刚被创建出来,进程pcb只连入了所有进程的全局双链表,还没有连入调度队列等链表结构的时候,进程处于 新建状态
结论4: 当进程pcb被连入CPU调度队列的时候,进程处于 运行状态,随时等待CPU调度执行(需注意上图调度队列中pcb的node仍连接在所有进程的全局双链表结构,一个进程pcb可以同时连接在多个双链表结构中的)
什么是就绪状态?
CPU调度队列中对存在大量进程pcb等待调度,但一个CPU同一时刻只能处理一个进程。调度队列中正在被CPU处理的进程,它的状态被称为 运行状态;其它大部分调度队列中等待CPU调度的进程,它们的状态被称为 就绪状态。
但其实没必要区分的这么细致,后续不再区分这两种状态,统一将所有连入CPU调度队列的进程pcb,对应的进程都认为是处于 运行状态。
- 进程是如何竞争外设资源的: 首先进程肯定是要先连接进CPU调度队列排队执行的,当CPU执行到某进程代码中需要调用外设资源的函数时(如:scanf函数需要调用键盘输入,printf函数需要调用屏幕输出),就需要把进程pcb从调度队列断链,连入对应外设的等待队列,等待外设资源(pcb连入外设等待队列,等待外设资源的时候处于 阻塞状态),当等待外设资源成功后,再把进程pcb 重新连入 CPU调度队列。
结论1:当进程pcb被连入外设等待队列的时候,进程处于 阻塞状态,等待外设资源。(上图只展示了一个进程申请外设资源的过程。当多个进程申请同一个外设资源时,它们的进程pcb都会连入该外设的等待队列,排队等待外设资源)
结论2:一个进程不能同时处于CPU调度队列 和 外设等待队列,进程在同一时刻只能在其中一种队列中等待资源。
1.4 阻塞挂起 和 就绪挂起
在磁盘中有一块区域叫swap 分区,它的容量大小一般设立为内存大小的1~2倍,当内存容量不足的时候,它可以为内存分担负担。那么它是如何进行分担的呢?
答:通过阻塞挂起 和 就绪挂起
Linux下的进程 = task_struct(描述进程信息的结构体) + 代码和数据(可执行程序)
一个进程的容量,一般都是代码和数据占大头
阻塞挂起: 当内存容量不足的,操作系统会把处于阻塞状态的进程的代码和数据换出内存保存到swap 分区中,只保留进程的pcb,反正处于阻塞状态的进程一时半会也用不着代码和数据,当该进程又进入CPU调度队列的时候,再把它的代码和数据从swap分区换入内存。
就绪挂起: 当内存严重不足的时候,操作系统会把处于就绪状态(处于CPU调度队列,但还在等待CPU调度)的进程的代码和数据换出内存保存到swap 分区中,只保留进程的pcb,当进程被CPU调度的时候,再把它的代码和数据从swap分区换入内存。
注:阻塞挂起 和 就绪挂起都是内存容量紧张的时候,操作系统自动执行的一些操作,我们只需要了解有这两个状态,不用去管具体操作。
2.Linux操作系统中的进程状态
2.1 Linux下的进程状态表示
/*
*The task state array is a strange "bitmap" of
*reasons to sleep. Thus "running" is zero, and
*you can test for combinations of others with
*simple bit tests.
*/
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 信号给进程来停止进程,此时停止的进程处于 T停止状态。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
• t追踪停止状态(tracing stop): 调试时代码遇到断点停止时的状态。
• X死亡状态(dead): 这个状态只是⼀个返回状态,你不会在任务列表里看到这个状态。
• Z僵死状态(Zombies): 是⼀个比较特殊的状态,后续详谈。
上述所讲的操作系统的进程状态是对于进程状态的整体认识,具体到某个操作系统下,进程状态的表现会有所不同。在Linux操作系统下,R状态对应运行状态,S 和 D状态对应阻塞状态,X 和 Z状态对应结束状态,有一定对应关系但会增加很多细节,毕竟上述所讲的操作系统的进程状态只是对进程状态的一个宏观认识,具体到某个操作系统会根据很多的实际情况做出一些改良。T 和 t状态是Linux下增加的状态。
2.2 R运行状态、S睡眠状态、D磁盘休眠状态、T停止状态 和 t追踪停止状态
- R运行状态
当进程代码运行且不执行 io 操作时,我们可以查到运行的进程处于R+(在讲解T状态时区分R+和R)运行状态。
同时我们可以看到grep进程此时也处于运行状态,而此云服务器设备只有一个CPU,一次只能执行一个进程,这侧面证明:在Linux下,只要处于CPU调度队列(运行队列)的进程都处于 运行状态。
- S睡眠状态
- 情况一:scanf函数等待键盘输入
当scanf等待键盘输入时,进程处于键盘设备的等待队列中,我们可以查到进程处于S+睡眠状态,其实就相当于阻塞状态
- 情况二:循环执行printf函数
该进程运行时的现象:飞快的执行printf函数在屏幕上打印
但我们却查到该进程处于S+睡眠状态,这是否有些奇怪?
其实这个飞快的打印速度只是对于人来说的,目前大多数家用计算机的CPU的执行速度是每秒50亿次,而我们执行这个代码打印的速度是远不及CPU的执行速度(可以说是差距非常非常非常大)。
哪是什么原因导致打印速度下降得这么夸张?
这是因为每次printf函数执行的时候,进程都会短暂进入屏幕设备等待队列等待屏幕资源,等待到屏幕资源后,会极其短暂的进入运行队列执行一次打印,后续不断循环此过程。
因为CPU的速度远远大于屏幕等外设的速度,所以循环执行printf函数打印时,其实绝大部分时间进程都在屏幕设备等待队列等待屏幕资源,也就是处于阻塞状态。
这就是为什么在该进程飞快的执行printf函数在屏幕上打印时,我们会查到该进程处于S+睡眠状态(其实小概率情况下能够查到该进程处于R运行状态)
- 情况三:执行sleep函数
执行sleep等休眠函数也能让进程处于S+睡眠状态。
- D磁盘休眠状态
D状态为什么会被称为磁盘休眠状态?
因为磁盘的空间是非常大的,所以内存一次可以与它进行容量非常大的数据传输
当内存中的某一进程向磁盘进行一次大容量数据传输时(比如:1GB数据),往往需要花费比较长的时间,在这一过程中可能会遇到一些不可控的情况,比如:内存空间突然变得极度紧张,操作系统为了保全自己,甚至会强制杀死一些进程,以此来释放一些内存空间。遇到这种情况,万一在进行大数据传输的进程被杀死了,那么可能会导致数据丢失。
操作系统考虑到了这种情况的存在,所以又新添加了一种状态叫 D磁盘休眠状态,也叫不可中断睡眠状态,在这个状态下的进程不会对OS的任何操作做出响应(OS杀不掉此进程),该进程只有等待IO数据传输的结束时才会解除D状态。
通常要进行大容量数据传输的进程都会处于一段时间的D状态,这是为了保护大容量数据的过程顺利进行,不被异常中断而导致数据丢失。
- T停止状态
想进入T状态,得通过信号指令,会用到以下几个信号:9 - 杀死进程;19 - 暂停进程;18 - 进程继续执行。
执行信号指令的格式:kill -n 进程pid(n指代数字,表示几号信号)
示例演示:
当进程代码运行且不执行 io 操作时,我们可以查到运行的进程处于R+运行状态 (R的后面带了+号,证明此进程是 前台进程,前台进程可以使用 ctrl + c 直接杀死)
对该进程执行19号信号使其暂停
查看被19信号暂停的进程,发现处于 T状态
使用18号信号让该进程继续执行,查看继续执行时进程的状态,发现处于 R状态 (R后不带加号,表示此进程已经是 后台进程)。这表明前台进程被信号暂停之后,继续执行会变成后台进程。
ctrl + c 不能杀死后台进程,后台进程只能被9号信号杀死。
- t追踪停止状态
使用gdb调试代码时,在代码中设置断点,然后 r 运行,gdb会创建子进程来运行代码,该进程中代码运行到断点位置停止,此时查看该进程的状态,发现处于 t状态。
2.3 Z僵死状态(zombie)-僵尸进程
在Linux系统中,是通过父进程创建子进程的方式,来使系统中的进程增多的。
父进程创建子进程的目的:让子进程去完成某项任务。
那么当子进程结束的时候,父进程得知道子进程的任务完成得怎么样?
进程结束的时候,不会立即释放该进程的所有资源,进程会把代码和数据释放掉,但是会把task_struct(pcb)保留,pcb中记录进程退出时候的退出信息(退出码),此时的进程处于 Z僵死状态。进程之所以存在这种状态,是为了让父进程读取到退出码,以此来得知子进程是否完成了任务。
父进程一旦读取了 Z僵死状态的进程pcb 中的退出码,该进程 立即变为X死亡状态,进程pcb也会被立即释放掉。
注: 如果父进程一直不读取 处于Z僵死状态的进程pcb 中的退出码,处于Z状态的子进程(僵尸进程)的pcb就会一直存在,会导致内存泄漏问题。
示例演示:
由上述代码中可以看出,子进程结束之后确实会进入Z僵死状态,如果父进程一直不读取 子进程pcb 中的退出码,它会一直处于 Z状态。
那么父进程如何读取子进程的退出码呢?
当子进程退出后,父进程需要使用wait()系统调用读取子进程的退出码。之后补充。
2.4 孤儿进程
如果子进程还在运行,但是父进程先退出了,会发生什么?
此时,子进程就会被 1 号进程(1号进程是操作系统的一部分)领养,该子进程被称为 孤儿进程
为什么孤儿进程会被领养呢?
因为孤儿进程也会退出,但它们原本的父进程已经退出了,没有父进程读取它的退出码,无法回收孤儿进程。所有需要被1号进程领养,以此来读取孤儿进程的退出码,从而回收孤儿进程。
示例演示:
子进程还在运行,但是父进程先退出时,该子进程会被 1 号进程领养。
三、进程优先级
1.进程优先级的概念
进程得到某种资源的先后顺序,就是指进程的优先级。优先级高的进程有优先获得某种资源的权利。
为什么要设置进程优先级?
本质就是:硬件资源有限,而进程众多。所有要设置优先级,让进程有序获得某种资源,可以改善系统性能。
进程优先级是怎么表示的?
就是用一个数表示,数越小表示优先级越高,表示进程优先级的数保存在进程pcb中(详见后续)
2.查看进程优先级的方法
⽤ps ‒al命令可以查看到进程的优先级:
- PRI : 代表这个进程可被执行的优先级,其值越小越早被执行
- NI : 代表这个进程的nice值
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
在Linux下,调整进程优先级不是直接去修改PRI的值,而是通过调整进程nice值来间接修改PRI值:PRI(new)=80+nice
(nice其取值范围是-20至19,⼀共40个级别。所有对应进程优先级PRI的范围就是60~99,同样也是40个级别)
3.更改进程优先级的方法
用top命令更改已存在进程的nice:
• top
• 进入top后按“r”‒>输入进程PID‒>输入nice值
// 注:修改进程优先级需要root权限
示例演示:
初始查看一下进程的PRI 和 NI 值,分别是80 和 0
进入top后按“r”‒>输入进程PID‒>输入nice值
通过上述方法将 nice修改为 -20
修改之后再查看进程的PRI 和 NI 值,发现变成60(PRI(new)=80+nice) 和 -20
4.补充概念-竞争、独立、并行、并发
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发: 多个进程在⼀个CPU下采用进程快速切换的方式,在⼀段时间之内,让多个进程都得以推进,称之为并发(任意一个时刻,在一个CPU上,只能有一个进程在运行)
四、进程切换
当代计算机都是分时操作系统,每个进程都有它合适的时间片(其实就是⼀个计数器)。时间片到达,进程就被操作系统从CPU中剥离下来。
其实一个进程基本都会被CPU调度多次才会执行完。 CPU调度进程的时候,CPU内的一套寄存器会记录过程中产生的临时数据(其中PC寄存器会记录下进程的代码执行到哪一行),当进程切换的时候,进程必须将CPU寄存器中记录的所有临时信息保存到自己进程的pcb中(下一次切换回该进程时,把pcb中保存的临时信息重新写入到CPU寄存器中,CPU就能通过PC寄存器得知上次执行到哪一行,然后继续向后执行),因为下⼀个将要运行的进程也会使用CPU的寄存器保存临时信息,会覆盖上一个进程的临时信息。
CPU上下文切换: 其实际含义是进程切换,或者CPU寄存器切换。当多进程内核决定运行另外的进程时,CPU寄存器中保存着正在运行进程的当前状态(也叫进程的上下文数据),正在运行的进程要把CPU寄存器中的所有内容保存到自己的pcb中(Linux内核0.11代码下,保存在进程task_struct中的tss_struct里), 保存工作完成后就把下⼀个将要运行的进程的当前状况信息(该进程的task_struct中tss_struct里保存的信息)重新装入CPU寄存器,并开始下⼀个进程的运行, 这⼀过程就是 进程切换。
参考⼀下Linux内核0.11代码:
五、Linux2.6内核进程O(1)调度队列
Linux2.6内核的CPU调度队列中的内容:
实际上,调度队列中的内容我们只需要关注下图所示的struct prio_array *active、struct prio_array *expired 和 struct prio_array arrays[2]即可:
先介绍一下struct prio_array结构体中的内容:
- struct list_head queue[140]: ⼀个元素就是⼀个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!
(前面我们介绍过进程优先级PRI的范围是60~99,只有40个级别,对应不上这里的140个级别。Linux系统既支持分时操作系统,也支持实时操作系统,当代计算机都是分时操作系统,这140个优先级只有后40个级别是给分时操作系统使用的;实时操作系统一般应用于工业领域,前100个级别是给它准备的,我们不用管。进程优先级PRI整体+40就能对应上queue队列中的优先级) - unsigned int nr_active: 记录queue[140]中总共有多少个运行状态的进程
- unsigned long bitmap[5]: ⼀共140个优先级,⼀共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!
struct prio_array arrays[2]中有两个结构体元素,active 和 expired指针分别指向其中一个元素,
active指针指向的元素称为活跃队列,expired指针指向的元素称为过期队列。
CPU只会调度活跃队列中的进程,如何进行调度的:
CPU调度活跃队列,是通过active指针,找到指向的活跃队列中的queue[140],从0下标开始遍历queue[140](分时操作系统中前100个进程队列为空,不使用),找到第⼀个非空队列,按照顺序调度该非空队列中排队的进程(相同优先级的进程按照FIFO规则进行排队调度),调度完该进程队列中所有排队的进程后,继续向下找非空对列,直到调度完queue[140]中所有进程队列。
当代计算机都是分时操作系统,每个进程都有它合适的时间片(其实就是⼀个计数器)。时间片到达,进程就被操作系统从CPU中剥离下来。一个进程基本都会被CPU调度多次才会执行完。
- 时间片耗尽的进程,被操作系统从CPU中剥离下来,被剥离下来的进程,只能重新入队列,入过期队列!
- 新增的进程一开始也是入过期队列!
- 被修改了nice值的进程,也就是被修改了进程优先级的进程,不会立即改变进程pcb所处的进程队列,当该进程在活跃队列被CPU调度后,重新入队列,入过期队列时,才会将进程pcb入修改后的进程优先级对应的进程队列。
过期队列和活动队列结构⼀模⼀样,过期队列上放置的进程,都是时间片耗尽的进程。
随着CPU不断调度活动队列中的进程,活动队列上的进程会越来越少,过期队列上的进程会越来越多。 当活动队列上的进程都被处理完毕(也就是活动队列的nr_active为0,已经没有运行状态的进程)之后,对过期队列的进程进行时间片重新计算,然后交换active指针和expired指针的内容(相当于原本的过期队列变成活动队列,原本的活动队列变过期队列),就相当于活动队列中又具有了⼀批新的活动进程,CPU又可以从头开始调度活动队列!