✨✨ 欢迎大家来到贝蒂大讲堂✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:Linux学习
贝蒂的主页:Betty’s blog
1. 初识进程
1.1 进程的概念
在计算机世界中,进程是一个关键概念。它是程序的执行实例,一般而言,当可执行程序被加载到内存后就形成了进程。进程担当着分配系统资源(如CPU
时间和内存)的重要角色,是操作系统进行资源分配和调度的基本单位,使得程序能够在系统中实际运行并完成特定任务。
1.2 进程的理解
进程信息被放在一个叫做进程控制块(PCB
)的数据结构中,可以理解为进程属性的集合。PCB
是进程存在的唯一标识。在Linux
环境下,PCB
就是task_struct
,一个包含进程属性信息的结构体。
然后我们就可以将进程理解为:被进程控制块PCB
所管理的可执行程序。一旦可执行程序被执行加载到内存,操作系统就会创建对应的PCB
将其管理,最后就形成了进程。
最后操作系统通过双向链表的形式将各个进程控制块PCB
联系起来,方便管理。如果新创建一个进程就将其对应的PCB
链接入双向链表中,退出一个进程就是将对于的PCB
删除即可。
1.3 task_struct的内容
task_struct
是Linux
当中的进程控制块,主要包含以下信息:
- 标示符(
PID
): 描述本进程的唯一标示符,用来区别其他进程。- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器(
pc
指针): 程序中即将被执行的下一条指令的地址。- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
- 上下文数据: 进程执行时处理器的寄存器中的数据。
- 进程的代码是不可能在很短时间运行完的,规定每个进程的时间片(单次运行的最长时间),用户感受到的多个进程同时运行,本质上是
CPU
的快速切换。CPU
只有一套寄存器,为了保护上下文,进程的这些临时数据被写入在PCB
中,再来执行时,恢复上下文。
I/O
状态信息: 包括显示的I/O
请求,分配给进程的I/O
设备和被进程使用的文件列表。- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 进程创建出来,
CPU
要执行它对应的代码,然而CPU
很少,进程很多。因此OS
内有一个调度模块,负责较为均衡的调度每一个进程,较为公平的获得CPU
资源。让每个进程都能获得CPU
资源,让每个进程都能被执行。
- 其他信息。
1.4 查看进程
在根目录下存在一个proc
文件夹,里面包含了大量关于进程的信息。
其中有很多关于数字的子目录,这些数字其实是某一进程的PID
,记录着对应进程的各种信息。
我们可以通过指令ps aux
查看所有进程信息。
我们也可以将ps
指令与grep
指令搭配使用,显示某一进程的信息。
2. 创建进程
2.1 进程标识符
通过系统调用函数getpid
和getppid
可以分别获取进程的PID
(进程ID)和PPID
(父进程ID)。
下面我们可以通过一个测试程序观察:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
while(1)
{
printf("proc PID :%d,parent PID:%d\n",getpid(),getppid());
sleep(1);
}
return 0;
}
我们可以通过ctrl+c
或者kill -9 进程的PID
杀死相应的进程。
2.2 fork函数
我们可以使用fork
函数创建一个子进程。
如果上述代码加上fork
之后,又会打印什么呢?
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
fork
while(1)
{
printf("proc PID :%d,parent PID:%d\n",getpid(),getppid());
sleep(1);
}
return 0;
}
第一行数据是该进程的PID
和PPID
,第二行数据是代码中通过调用fork
函数创建的子进程的PID
和PPID
。其中该进程的PID
就是子进程的父进程PID
,所以我们可以说这两个进程是父子关系。而该进程的父进程就是bash
,一般而言,在命令行上运行的指令,父进程基本都是bash
。
并且值得注意的是:在子进程被创建之前的代码被父进程执行,而子进程被创建之后的代码,则默认情况下父子进程都可以执行。父子进程虽然代码共享,但是父子进程的数据各自开辟空间(采用写时拷贝)。
注意:使用fork
函数创建子进程后就有了两个进程,这两个进程被操作系统调度的顺序是不确定的,这取决于操作系统调度算法的具体实现。
2.3 fork的返回值
因为父子进程代码共享,而且fork
函数在执行return
语句时已经创建好了子进程,所以return
语句会被父子进程执行两次,所以fork
函数肯定有两个返回值。
- 如果子进程创建成功,在父进程中返回子进程的
PID
,而在子进程中返回0。- 如果子进程创建失败,则在父进程中返回 -1。
所以我们可以通过fork
的返回值,让父子进程分别去执行不同的过程。代码示例如下:
#include<stdio.h>
#include<unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
while(1)
{
printf("I am child PID:%d PPID:%d\n",getpid(),getppid());
sleep(1);
}
}
else if (id > 0)
{
//parent
while(1)
{
printf("I am parent PID:%d PPID:%d\n",getpid(),getppid());
sleep(1);
}
}
else
{
//fork error
}
return 0;
}
并且进程之间具有独立性,即使一个进程中途异常退出,也不会影响其他进程。
3. 进程状态
在Linux
中,存在大量进程,有些进程可能在运行,有些进程可能在休眠。为了方便操作系统管理,我们需要对进程的状态进行表示。
在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 *task_state_array[] = {
"R (running)", /* 0*/
"S (sleeping)", /* 1*/
"D (disk sleep)", /* 2*/
"T (stopped)", /* 4*/
"T (tracing stop)", /* 8*/
"Z (zombie)", /* 16*/
"X (dead)" /* 32*/
};
我们可以使用指令ps aux
或者 ps axj
查看对应的进程状态。
3.1 运行状态-R
**R运行状态(running) **: 运行状态不一定占用CPU
,并不意味着进程一定在运行中,一个进程处于R
状态,它只是表明进程要么是在运行中要么在运行队列里,随时可以被CPU
调度 也就是说,可以同时存在多个处于R
状态的进程。
比如如下所有处于运行队列的进程都处于运行状态。
3.2 浅度睡眠状态-S
S浅睡眠状态(sleeping): 当需要完成某种任务,而条件不具备时,需要进程进行某种等待,此时的状态就是浅睡眠状态。
比如一下程序:
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("I am running\n");
sleep(100);
return 0;
}
浅度睡眠状态可以用kill
指令杀死对应进程。
3.3 深度睡眠状态-D
D深度休眠状态(Disk sleep):有时候也叫不可中断睡眠(深度睡眠)状态,在这个状态的进程通常会等待IO
的结束。
例如,某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于深度睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。
3.4 停止状态-T
T停止状态(stopped):可以通过发送SIGSTOP
信号来停止进程,这个被暂停的进程可以通过发送SIGCONT
信号让进程继续运行。
3.5 僵尸状态-Z
Z僵尸状态(Zombies):当一个进程退出时,该进程曾经申请的资源并不是立即被释放,而是要暂时存储一段时间,以供操作系统或是其父进程进行读取,如果退出信息一直未被读取,则相关数据是不会被释放掉的。一旦进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态。
3.6 死亡状态-X
X死亡状态(dead):当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了。同样因为不存在了,我们也无法观察到死亡状态。
4. 僵尸进程与孤儿进程
4.1 僵尸进程
处于僵尸状态的进程,我们就称之为僵尸进程。
例如,以下这段代码,fork
函数创建的子进程在打印3次信息后会退出,而父进程会一直打印信息。也就是说,子进程退出了,父进程还在运行,此时父进程没有读取子进程的退出信息,那么此时子进程就进入了僵尸状态。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
pid_t id = fork();
if (id == 0) {
// child
int count = 3;
while (count > 0) {
printf("I am child,PID:%d PPID: %d\n", getpid(), getppid());
sleep(2);
count--;
}
printf("I am child, I quit\n");
} else if (id > 0) {
// parent
while (1) {
printf("I am parent,PID:%d PPID:%d\n", getpid(), getppid());
sleep(2);
}
} else {
// fork error
printf("fork error\n");
}
return 0;
}
运行开始我们可以通过该脚本指令检测:while :; do ps axj | head -1 && ps axj | grep pro.out | grep -v grep;echo "######################";sleep 1;done
如果僵尸进程一直不回收,子进程就会一直处于僵尸状态,而维护子进程系统会创建对应PCB
,进而造成系统资源的浪费。并且随着僵尸进程的增多,实际使用的资源就会越少,会造成严重的内存泄漏问题。
4.2 孤儿进程
若子进程先退出而父进程没有对子进程的退出信息进行读取,那么我们称该进程为僵尸进程。但若是父进程先退出,那么此时子进程没有父进程对其进行处理,此时该子进程就称之为孤儿进程。若是一直不处理孤儿进程,那么孤儿进程就会一直占用资源,造成内存泄漏。因此,当出现孤儿进程的时候,孤儿进程一般会被1号init
进程领养。
比如说下面这段代码,fork
函数创建的子进程会一直打印信息,而父进程在打印3次信息后会退出,此时该子进程就变成了孤儿进程。
5. 进程的优先级
5.1 优先级的概念
优先级实际上就是获取某种资源的先后顺序,而进程优先级实际上就是进程获取CPU
资源分配的先后顺序,就是指进程的优先权(priority),优先权高的进程有优先执行的权力。
优先级存在的主要原因就是资源是有限的,一个CPU
一次只能跑一个进程,而进程是可以有多个的,所以需要存在进程优先级,来确定进程获取CPU
资源的先后顺序。
5.2 查看优先级
我们可以通过指令ps -l
查看进程的优先级。
- PRI:代表这个进程可被执行的优先级,其值越小越早被执行。在
Linux
操作系统当中,PRI(old)
默认为80,即PRI = 80 + NI
。- NI:代表这个进程的
nice
值。NI的取值范围是-20至19,一共40个级别。
5.3 修改优先级
因为PRI = 80 + NI
,所以我们只需要修改NI
值就能达到修改优先级的目的。
修改NI
值一共有两种方法:
第一种就是输入top
指令,然后按下R
键输入要修改进程的PID
,最高输入要修改的NI
值。比如下列我们将进程a.out
的Ni
修改为10,其优先级PRI
变为90。
第二种就是使用renice
指令,语法为renice NI PID
。比如说我们下面将a.out
的NI
修改为15,其优先级PRI
变为95。
其中无论哪种方法,如果想将NI
值调为负值,也就是将进程的优先级调高,都需要使用sudo
提升权限。
6. 进程调度队列
我们以Linux 2.6
版本为例,详细谈一谈进程调度队列。
active
指针:永远指向活动队列。expired
指针:永远指向过期队列。nr_active
:代表总共有多少个运行状态的进程。queue[140]
:前面说到nice
值的取值范围是-20~19,共40个级别,依次对应queue
当中普通优先级的下标100~139,相同优先级的进程按照FIFO
规则进行排队调度。而下标0~99对应的实时进程,实时进程是指先将一个进程执行完毕再执行下一个进程,现在基本不存在这种机器了,所以对于queue
当中下标为0~99的元素我们不关心。bitmap[5]
:这是一个位图,queue
数组当中一共有140个元素,即140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5 × 32个比特位表示每个队列是否为空。
所以调度过程如下:
- 首先从0下标开始遍历活动队列
queue[140]
。- 选中队列的第一个非空进程即优先级最高的基础,开始运行,调度完成后放入过期队列。
- 继续选中队列的第二个非空进程进行调度,直到所有活动队列都被调度,即
bitmap[5]
等于0。- 如果活动队列全部被调度完毕,交换将
active
指针和expired
指针的内容,让过期队列变成活动队列,活动队列变成过期队列,这样就又有了一批新的活动进程,如此循环进行即可。
- 如果是新创建的进程则放入过期队列。