看完本文你会了解:
深入理解进程概念,了解PCB
学习进程状态,学会创建进程,掌握僵尸进程和孤儿进程,及其形成原因和危害
了解进程调度,Linux进程优先级,理解进程竞争性与独立性,理解并行与并发
首先学习进程前最好先了解一下操作系统的基本概念,不清楚的同学可以看一下上一篇博客,(35条消息) 冯诺依曼体系与操作系统_Sola一轩的博客-CSDN博客接下来正式开始进程概念的内容。
目录
进程
基本概念
什么是进程
Linux中查看进程
通过ps命令查看
通过系统目录查看
获取进程的标识符
创建一个子进程
进程状态
运行、阻塞、挂起
LInux中进程的状态
Z僵尸进程
孤儿进程
进程优先级
基本概念
查看系统进程
PRI and NI
其他概念
进程切换
什么是进程切换
课本概念:程序的一个执行实例,正在执行的程序等。
内核观点:担当分配系统资源(CPU时间,内存)的实体。
首先冯诺依曼体系结构规定了,我们运行程序需要加载到内存中,此时内存中的该程序就是进程。操作系统需要对内存中的这部分数据进行管理(先描述再组织)。
描述进程PCB
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
课本上称之为PCB(process control block)。
在Linux中描述进程的结构体叫做task_struct。(PCB的一种)
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
所有运行在系统里的进程都以task_struct链表的形式存在内核里。
task_ struct内容分类
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
先简单创建一个进程。编写一个程序运行即可。
#include
#include
using namespace std;
int main()
{
while(1)
{
cout<<"我是一个进程"<
编译运行它
g++ -o test test.cpp -std=c++11
每间隔一秒开始进行打印。
ps aux(ajx) | grep test(过滤出想查看的进程)
由此我们看到了该进程运行的情况。
日常中查看进程常用的命令
ps ajx | head -1 && ps ajx | grep 程序名字 | grep -v grep(把grep进程也给过滤)
这样我们方便看到进程的各种信息。
ls /proc
系统目录下,我们可以看到进程对应的文件目录。
我们进入我们创建的文件目录:
ls /proc/PID(进程对应的标识符)
我们加上al选项,查看详细信息
ls /proc/PID(进程对应的标识符) -al
由上可以看到exe后面一串绿色标识的就是当前进程的路径。
如果我们此时将当前进程对应的磁盘中的文件删除呢?有什么现象。
删除后exe后面的路径变成了红色,并提示你该程序已经被删除了,但由于程序是加载到内存中的,所以并不会影响我们程序的运行。
如何获取进程的标识符?通过系统调用。
进程的标识符就是它的PID。
getpid(); //获取当前进程的pid
getppid(); //获取父进程的pid
man手册中的介绍:
1 #include
2 #include
3 #include
4 #include
5
6 using namespace std;
7
8 int main()
9 {
10 while(1)
11 {
12 cout<<"我是一个进程,我的pid:"<
我们需要用到fork()这个系统调用
fork();
返回值:
在父进程返回子进程的pid,在子进程则会返回0;失败会在父进程返回-1,子进程不会被创建。
接下来我们实验一下:
1 #include
2 #include
3 #include
4 #include
5 #include
6 using namespace std;
7
8 int main()
9 {
10 pid_t id;
11 id = fork();
12 assert(id != -1);
13 if(id == 0) //子进程
14 {
15 while(true)
16 {
17 cout<<"我是子进程,我的pid:"<
这样我们就看到了两个进程正在运行。
进程有关的简单操作就暂时介绍到这里,详细的部分将在下一篇进程控制中详细的介绍。
相信大家实际上已经听到过相关的状态了,我们先从三个常听到的状态来进行介绍。
这三个最常听到的状态到底是怎么样的呢?
运行状态
CPU资源只有一份,进程很多,大家都需要用到CPU资源,因此CPU有一个运行队列,想要使用CPU资源的进程需要去运行队列中排队,当进程要么是在运行中要么在运行队列里时处于运行状态。
阻塞状态
外设的访问速度很慢,很明显,我们的进程在访问外设时也需要进行排队,等待外设资源准备就绪,每个外设也有自己的等待队列,当进程在等待外设资源时就处于阻塞状态。
挂起状态
当进程处于阻塞状态时,如果内存空间不够,可以先将内存中PCB对应的进程资源先放回磁盘,保留PCB进行排队,此时就是挂起的状态。
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在kernel源代码里定义:
/*
* 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
什么也不干,只是一直在运行。
#include
int main()
{
while(true)
{
; }
return 0;
}
此时该进程的运行状态就是R状态。
查看浅度睡眠状态S
我们让进程不断打印。
#include
int main()
{
while(true)
{
std::cout<<"i am a process"<
此时的状态是S了,这时有的人就会很疑惑,不对啊,进程不是一直在打印吗?怎么不是R状态。
因为CPU太快了,相比外设,在我们打印的时候大部分的时间实际上都处于等待显示器就绪的状态,即之前说的阻塞状态,Linux中显示的就是S浅度睡眠状态。
深度睡眠状态D(了解即可)
深度睡眠状态不是很好进行演示,在这里就直接和大家介绍了。
举个例子:
有一个进程需要向磁盘中写入200万条重要的用户数据,由于磁盘很慢,进程得等待磁盘返回的结果(写入成功或者写入失败),此时内存非常吃紧,OS看到这个进程什么事情也不干,在那里干等着,也许就会直接挂掉该进程。此时就可能造成重要的数据丢失的情况。为了防止这种情况,有一种状态叫做深度睡眠,这种状态的进程操作系统(OS)是无法挂掉它的。
查看暂停状态T
暂停状态需要我们用暂停信号来暂停进程
kill -l //查看有哪些信号
19号信号是暂停信号,18号信号是继续信号
接下来向一个运行中的程序发送暂停信号:
此时进程处于T状态
接下来发送18号信号,继续运行该程序
现在该进程就处于S状态了,但与最初不同的地方来了,S后面的+号没了。
此时的进程无法通过ctrl+c 来取消,原因就在于此时该进程成了后台进程。
有+号代表该进程是前台进程,无+号表示后台进程。
查看追踪暂停状态t
当我们用gdb对一个程序进行调试时,该进程就处于t状态。
死亡状态X
死亡状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,所以你不会在任务列表当中看到死亡状态。
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程就进入Z状态。
1 #include
2 #include
3 #include
4 #include
5 #include
6 using namespace std;
7
8 int main()
9 {
10 pid_t id;
11 id = fork();
12 assert(id != -1);
13 if(id == 0)
14 {
15 cout<<"我是子进程,我的pid:"<
此时子进程就处于僵尸状态。
僵尸进程的危害:
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!
那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
最终就会造成内存泄漏的问题。如何避免将在以后再介绍。
父进程先退出,子进程就称之为“孤儿进程”
孤儿进程会被1号init进程领养,由init进程回收
1 #include
2 #include
3 #include
4 #include
5 #include
6 using namespace std;
7
8 int main()
9 {
10 pid_t id;
11 id = fork();
12 assert(id != -1);
13 if(id == 0)
14 {
15 int count = 10;
16 while(count--)
17 {
18 cout<<"我是子进程,我的pid:"<
父进程3秒后退出,子进程10秒后退出。
此时子进程被1号进程领养,被领养后也变成了后台进程。
cpu资源分配的先后顺序,就是指进程的优先权(priority)。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:
我们很容易注意到其中的几个重要信息,有下:
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高。
那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice。
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行所以,调整进程优先级,在Linux下,就是调整进程nice值。
nice其取值范围是-20至19,一共40个级别。
需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
可以理解nice值是进程优先级的修正数据
用top命令更改已存在进程的nice:
top
进入top后按“r”–>输入进程PID–>输入nice值
将bash的nice值改为10.
此时修改nice值为5,PRI的值是多少呢?答案是85 ,其只会在原来的进程优先级上进行加减。
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
在CPU中采取时间片轮转的策略,每个进程执行相应的时间片后切换到下一个进程,以此来实现并发。
进程切换是个什么样的过程呢?通过画图来看一看。
此时如果当前进程还没执行完,但进程执行相应的时间片后需要切换到下一个进程,此时当前进程切换时需要带走寄存器中的上下文数据,让下一次执行该进程时能够从原来的位置继续向下执行。
在任何时刻,CPU里面的寄存器里面的数据,看起来是在大家都能看到的寄存器上。但是,寄存器内的数据,只属于当前运行的进程! 寄存器被所有进程共享,寄存器内的数据,是每个进程各自私有的上下文数据。