进程基本概念
课本概念:在编程或软件工程的上下文中,进程通常被视为正在执行的程序的实例。当你启动一个应用程序时,操作系统会为这个程序创建一个进程。每个进程都有自己的独立内存空间,可以运行自己的指令序列,并可能与其他进程通信。进程是操作系统管理程序执行的基本单位。
程序的一个执行实例,正在执行的程序等。
内核观点:从操作系统的内核角度来看,进程是一个重要的抽象,用于封装执行环境和状态。内核必须跟踪每个进程的状态,包括它的内存映射、打开的文件、资源使用情况(如CPU时间、内存),以及进程的优先级和调度信息。内核通过进程调度算法决定哪个进程在特定时刻可以使用CPU,以及如何分配有限的系统资源给多个竞争的进程。
担当分配系统资源(CPU时间,内存)的实体。
每个程序员都清楚,当源代码经过编译和链接阶段,会转换成一个可直接运行的二进制文件,即我们通常所说的可执行程序。这个文件实质上就是存储在硬盘上的指令集合。然而,当我们通过双击这个可执行文件来启动它时,真正的动作是在操作系统层面将这些指令加载到计算机的随机存取存储器(RAM)中。这是因为中央处理器(CPU)只能直接访问内存中的指令,从而逐条执行它们。因此,一旦这个程序驻留在内存里,它的身份就发生了转变——从一个静态的“程序”变成了一个动态的“进程”。在操作系统术语中,这种状态下的程序被称为进程,它包含了执行环境以及与之相关的所有资源。
描述进程-PCB
系统当中可以同时存在大量进程,使用命令ps aux便可以显示系统当中存在的进程。
当计算机启动时,最先唤醒的是操作系统,它如同一位总指挥,负责调度和管理计算机的各种资源与活动。在众多任务中,进程管理尤为关键,因为计算机内部同时运行着成百上千个进程,每个进程都是执行中的程序实例。
那么操作系统是如何对进程进行管理的呢?
- 操作系统采用了一种高效且有序的方式来管理这些进程,这个过程可以精炼为“先描述 再组织”。首先,每当一个新的进程诞生,操作系统立即创建其“身份证明”,也就是进程控制块(PCB)。PCB 是一个数据结构,它记录了进程的所有重要信息,如进程状态、优先级、内存使用情况、打开的文件等,相当于进程的个人档案。
- 接下来,操作系统的进程管理功能通过这些PCB来跟踪和控制进程的状态变化。无论是分配CPU时间、调整进程优先级、还是处理进程间的通信,都基于对PCB信息的读取和修改。这样,操作系统无需直接与每个进程交互,而是通过对PCB的高效管理,实现了对所有进程的精准调控,确保了系统资源的合理分配和利用,以及程序的顺利执行。
操作系统为了有效地管理和追踪每个进程,采用了一种精妙的策略,即将每个进程的信息封装在一个称为进程控制块(PCB)的结构中。这些PCB并非孤立存在,它们被精心地组织成一个双链表结构,使得操作系统可以通过维护这个列表的头部指针,轻松访问和遍历整个进程集合。
这种设计赋予了操作系统强大的管理能力。例如,当需要创建一个新进程时,操作系统首先将该进程的代码和数据载入内存,随后为这个进程生成一个详细的PCB,并将其无缝地插入到双链表中,这一操作实质上标志着新进程的正式加入。
相反,当一个进程完成使命或需要终止时,操作系统会首先定位并移除对应的PCB,从而将其从双链表中剔除。接着,操作系统会清理内存中属于该进程的资源,释放它们供其他进程使用,或者将这些区域标记为可用状态。
因此,操作系统对进程的整个生命周期管理,实质上转化为对双链表的高效操作,包括但不限于添加、删除、查找和更新PCB。这种抽象层次的提升,不仅简化了操作系统的实现,也保证了系统资源的有效管理和进程间的顺畅协调。
task_struct-PCB的一种
进程控制块(PCB)是操作系统用于描述和管理进程的关键数据结构。在C++中,类似的概念可通过类来实现,但在C语言环境下,如Linux操作系统,PCB则是通过结构体(struct)来具体实现的。这意味着Linux内核中的PCB是一个包含多种信息字段的结构体,用于存储关于进程状态的所有必要数据。
- PCB实际上是对进程控制块的统称,在Linux中描述进程的结构体叫做task_struct。
- task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含进程的信息。
task_struct内容分类
在Linux操作系统中,task_struct
充当着进程控制块的角色,它是一个综合性的数据结构,用于封装与进程管理相关的所有关键信息。
以下是
task_struct
中主要包含的几类信息:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器(pc): 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
- 上下文数据: 进程执行时处理器的寄存器中的数据。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟总和,时间限制,记账号等。
- 其他信息。
查看进程
通过系统目录查看
在根目录下有一个名为proc的系统文件夹。
文件夹当中包含大量进程信息,其中有些子目录的目录名为数字。
这些数字其实是某一进程的PID,对应文件夹当中记录着对应进程的各种信息。我们若想查看PID为1的进程的进程信息,则查看名字为1的文件夹即可。
通过ps命令查看
单独使用ps命令,会显示所有进程信息。
russleo@VM-0-2-ubuntu:/$ ps aux
ps命令与grep命令搭配使用,即可只显示某一进程的信息。
russleo@VM-0-2-ubuntu:/$ ps aux | head -1 && ps aux | grep bash | grep -v grep
通过系统调用获取进程的PID和PPID
通过使用系统调用函数,getpid和getppid即可分别获取进程的PID和PPID。
当运行该代码生成的可执行程序后,便可循环打印该进程的PID和PPID。
我们可以通过ps命令查看该进程的信息,即可发现通过ps命令得到的进程的PID和PPID与使用系统调用函数getpid和getppid所获取的值相同。
通过系统调用创建进程- fork初始
fork函数创建子进程
fork是一个系统调用级别的函数,其功能就是创建一个子进程。
若是代码当中没有fork函数,我们都知道代码的运行结果就是循环打印该进程的PID和PPID。而加入了fork函数后,代码运行结果如下:
运行结果是循环打印两行数据,第一行数据是该进程的PID和PPID,第二行数据是代码中fork函数创建的子进程的PID和PPID。我们可以发现fork函数创建的进程的PPID就是proc进程的PID,也就是说proc进程与fork函数创建的进程之间是父子关系。
每出现一个进程,操作系统就会为其创建PCB,fork函数创建的进程也不例外。
我们知道加载到内存当中的代码和数据是属于父进程的,那么fork函数创建的子进程的代码和数据又从何而来呢?
实际上,使用fork函数创建子进程,在fork函数被调用之前的代码被父进程执行,而fork函数之后的代码,则默认情况下父子进程都可以执行。需要注意的是,父子进程虽然代码共享,但是父子进程的数据各自开辟空间(采用写时拷贝)。
注意: 使用fork函数创建子进程后就有了两个进程,这两个进程被操作系统调度的顺序是不确定的,这取决于操作系统调度算法的具体实现。
使用if进行分流
上面说到,fork函数创建出来的子进程与其父进程共同使用一份代码,但我们如果真的让父子进程做相同的事情,那么是不是创建子进程就没有什么意义了。
实际上,在fork之后我们通常使用if语句进行分流,即让父进程和子进程做不同的事。
fork函数的返回值:
1、如果子进程创建成功,在父进程中返回子进程的PID,而在子进程中返回0。
2、如果子进程创建失败,则在父进程中返回 -1。
既然父进程和子进程获取到fork函数的返回值不同,那么我们就可以据此来让父子进程执行不同的代码,从而做不同的事。
fork创建出子进程后,子进程会进入到 if 语句的循环打印当中,而父进程会进入到 else if 语句的循环打印当中。
Linux进程状态
一个进程从创建而产生至撤销而消亡的整个生命期间,有时占有处理器执行,有时虽可运行但分不到处理器,有时虽有空闲处理器但因等待某个时间的发生而无法执行,这一切都说明进程和程序不相同,进程是活动的且有状态变化的,于是就有了进程状态这一概念。
这里我们具体谈一下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*/
};
注意: 进程的当前状态是保存到自己的进程控制块(PCB)当中的,在Linux操作系统当中也就是保存在task_struct当中的。
在Linux操作系统当中我们可以通过 ps aux 或 ps axj 命令查看进程的状态。
STAT所在列表示运行状态。
russleo@VM-0-2-ubuntu:~$ ps aux
russleo@VM-0-2-ubuntu:~$ ps axj
运行状态-R
进程处于运行状态意味着它正准备运行或正在运行。这包括进程正在等待CPU时间片,或者正在使用CPU执行代码。在多任务操作系统中,进程可能会频繁地在运行状态和其他状态之间切换,这是由操作系统的调度策略决定的。
浅度睡眠状态-S
当进程在等待某些条件满足时,它会进入浅度睡眠状态。这通常发生在进程等待外部事件(如I/O操作完成、网络数据到达、信号量变为可用等)。进程在浅度睡眠状态下可以被信号唤醒,一旦等待的条件满足,进程就会回到运行队列中等待CPU调度。
深度睡眠状态-D
进程处于深度睡眠状态时,它正在等待某种硬件资源的完成,最常见的是I/O操作。与浅度睡眠不同,处于深度睡眠状态的进程无法被信号唤醒,必须等到硬件操作完成才能重新调度。这种状态通常是因为进程调用了read()
, write()
, select()
, 或其他系统调用而触发的。
暂停状态-T
暂停状态表示进程已被暂停,通常是通过接收到SIGSTOP、SIGTSTP等信号导致的。在暂停状态下,进程不会接收任何输入输出,也不会执行任何指令。它需要接收到SIGCONT信号才能解除暂停状态,重新开始执行。
僵尸状态-Z
当一个子进程结束但父进程没有通过wait()
或waitpid()
调用来回收子进程资源时,子进程会变成僵尸状态。僵尸进程只占用非常小的资源,主要是内核中的少量数据结构。尽管如此,大量的僵尸进程可能会导致内存泄漏和资源浪费,因为它们占据着进程表中的条目。
死亡状态-X
虽然在技术上Linux内核没有正式的“X”状态,但术语“死亡状态”有时用来描述一个进程在终止后但尚未被父进程清理的状态。一旦父进程通过wait()
或waitpid()
调用回收了子进程的资源,这个子进程的信息就会被内核释放,从而真正从系统中消失。
僵尸进程
前面说到,一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态。而处于僵尸状态的进程,我们就称之为僵尸进程。
例如,对于以下代码,fork函数创建的子进程在打印5次信息后会退出,而父进程会一直打印信息。也就是说,子进程退出了,父进程还在运行,但父进程没有读取子进程的退出信息,那么此时子进程就进入了僵尸状态。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("I am running...\n");
pid_t id = fork();
if(id == 0)
{ //child
int count = 5;
while(count)
{
printf("I am child...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
sleep(1);
count--;
}
printf("child quit...\n");
exit(1);
}
else if(id > 0)
{ //father
while(1)
{
printf("I am father...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
}
else
{ //fork error
}
return 0;
}
运行该代码后,我们可以通过以下监控脚本,每隔一秒对该进程的信息进行检测。
russleo@VM-0-2-ubuntu:~/7_23/proc$ while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;echo "######################";sleep 1;done
检测后即可发现,当子进程退出后,子进程的状态就变成了僵尸状态。
僵尸进程的危害
- 僵尸进程的退出状态必须一直维持下去,因为它要告诉其父进程相应的退出信息。可是父进程一直不读取,那么子进程也就一直处于僵尸状态。
- 僵尸进程的退出信息被保存在task_struct(PCB)中,僵尸状态一直不退出,那么PCB就一直需要进行维护。
- 若是一个父进程创建了很多子进程,但都不进行回收,那么就会造成资源浪费,因为数据结构对象本身就要占用内存。
- 僵尸进程申请的资源无法进行回收,那么僵尸进程越多,实际可用的资源就越少,也就是说,僵尸进程会导致内存泄漏。
孤儿进程
在Linux当中的进程关系大多数是父子关系,若子进程先退出而父进程没有对子进程的退出信息进行读取,那么我们称该进程为僵尸进程。但若是父进程先退出,那么将来子进程进入僵尸状态时就没有父进程对其进行处理,此时该子进程就称之为孤儿进程。
若是一直不处理孤儿进程的退出信息,那么孤儿进程就会一直占用资源,此时就会造成内存泄漏。因此,当出现孤儿进程的时候,孤儿进程会被1号init进程领养,此后当孤儿进程进入僵尸状态时就由int进程进行处理回收。
例如,对于以下代码,fork函数创建的子进程会一直打印信息,而父进程在打印5次信息后会退出,此时该子进程就变成了孤儿进程。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("I am running...\n");
pid_t id = fork();
if(id == 0)
{ //child
int count = 5;
while(1)
{
printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid(), count);
sleep(1);
}
}
else if(id > 0)
{ //father
int count = 5;
while(count)
{
printf("I am father...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
sleep(1);
count--;
}
printf("father quit...\n");
exit(0);
}
else
{ //fork error
}
return 0;
}
观察代码运行结果,在父进程未退出时,子进程的PPID就是父进程的PID,而当父进程退出后,子进程的PPID就变成了1,即子进程被1号进程领养了。
进程优先级
基本概念
什么是优先级?
优先级实际上就是获取某种资源的先后顺序,而进程优先级实际上就是进程获取CPU资源分配的先后顺序,就是指进程的优先权(priority),优先权高的进程有优先执行的权力。
优先级存在的原因?
优先级存在的主要原因就是资源是有限的,而存在进程优先级的主要原因就是CPU资源是有限的,一个CPU一次只能跑一个进程,而进程是可以有多个的,所以需要存在进程优先级,来确定进程获取CPU资源的先后顺序。
查看系统进程
在Linux或者Unix操作系统中,用ps -l命令会类似输出以下几个内容:
列出的信息当中有几个重要的信息,如下:
- UID:代表执行者的身份。
- PID:代表这个进程的代号。
- PPID:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号。
- PRI:代表这个进程可被执行的优先级,其值越小越早被执行。
- NI:代表这个进程的nice值。
PRI与NI
- PRI代表进程的优先级(priority),通俗点说就是进程被CPU执行的先后顺序,该值越小进程的优先级别越高。
- NI代表的是nice值,其表示进程可被执行的优先级的修正数值。
- PRI值越小越快被执行,当加入nice值后,将会使得PRI变为:PRI(new) = PRI(old) + NI。
- 若NI值为负值,那么该进程的PRI将变小,即其优先级会变高。
- 调整进程优先级,在Linux下,就是调整进程的nice值。
- NI的取值范围是-20至19,一共40个级别。
注意: 在Linux操作系统当中,PRI(old)默认为80,即PRI = 80 + NI。
查看进程优先级信息
当我们创建一个进程后,我们可以使用ps -al命令查看该进程优先级的信息。
注意: 在Linux操作系统中,初始进程一般优先级PRI默认为80,NI默认为0。
通过top命令更改进程的nice值
top
是一个实时的进程监测工具,它提供了交互式界面来查看和控制运行中的进程。你可以使用top
命令来改变一个或多个进程的nice值。
步骤:
启动top命令:在终端中输入
top
并按回车键。选择进程:在
top
界面中,你会看到一个动态更新的进程列表。使用键盘上的上下箭头键选择你想要更改nice值的进程。更改nice值:当你选中一个进程后,按
r
键。这会提示你输入一个新的nice值。输入一个整数(负数表示更高的优先级,正数表示更低的优先级),然后按回车键。确认更改:更改会立即生效,你可以在
top
的进程列表中看到nice值的变化。
示例:
假设你想降低PID为1234的进程的优先级,使其调度得更少:
- 启动
top
。- 使用上下箭头找到PID为1234的进程。
- 按
r
键。- 输入一个较高的数字,如
19
,然后按回车键。
通过renice命令更改进程的nice值
renice
是一个非交互式的命令,用于更改一个或多个进程的nice值,适用于脚本和自动化场景。
命令语法:
renice [选项] 新的nice值 [进程ID列表]
其中,
新的nice值
是-20至19之间的整数,进程ID列表
是一个或多个进程ID,可以是单个PID或PID范围。
示例:
1、如果你想提高PID为1234的进程的优先级,使它调度得更多,可以使用以下命令:
renice -n -5 1234
这会将PID为1234的进程的nice值减少5,即提高其优先级。
2、如果你想同时更改多个进程的nice值,可以使用逗号分隔的PID列表或PID范围:
renice -n 5 1234,5678,9012-9020
这会将PID为1234、5678以及9012到9020的所有进程的nice值增加5,即降低它们的优先级。
注意:请确保在使用
renice
时拥有足够的权限,因为通常需要root权限才能更改其他用户的进程的nice值。
四个重要概念
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便有了优先级。
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。
- 并行: 多个进程在多个CPU下分别同时进行运行,这称之为并行。
- 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。