文章目录
- 📖 前言
- 1. 父进程与子进程
- 2. fork函数创建子进程
- 2.1 认识fork函数:
- 2.2 fork函数两个返回值的原因:
- 2.3 fork函数的返回值意义:
- 3. 进程状态
- 3.1 运行状态(R):
- 3.2 终止状态(X):
- 3.2 阻塞状态(S):
- 3.3 进程挂起:
- 3.4 深度睡眠状态:(D)
- 3.5 僵尸状态(Z):
- 3.5 - 1 模拟僵尸进程
- 3.5 - 2 长时间僵尸的危害
- 3.6 孤儿进程:
- 3.7 暂停状态:(T / t)
- 3.7 - 1 T(stopped)
- 3.7 - 1 t(tracing stop)
📖 前言
在我们有了进程的初步基本概念之后,我们就要来学习一下如何创建进程,进程的几种状态,进程的优先级的问题,搬好小板凳要开讲了🙋🙋🙋……
本文实验系统:CentOS 7.6
~
1. 父进程与子进程
父进程:指已创建一个或多个子进程的进程~
我们通过getpid
函数来获取当前进程的ID,也可以通过getppid
来获取父进程的ID:
代码演示:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while(1)
{
printf("I am a process! pid: %d ppid: %d\n",getpid(), getppid());
sleep(1);
}
return 0;
}
我们多次执行上述代码观察当前进程和其父进程的ID:
之前的只是我们知道了,当每次执行一个可执行程序之后,进程的ID都会改变,上图也验证了这一点,但是我们惊奇的发现,为啥父进程的ID始终都是一个值,一直都是不变的呢??
几乎我们在命令行上所执行的所有的指令(你的cmd),都是bash进程的子进程!
衍生问题:
- bash怎么创建的子进程?
- bash怎么让子进程执行我的程序?
- bash的父进程又是谁?
我们后面再说…
2. fork函数创建子进程
2.1 认识fork函数:
fork函数是用来创建子进程的,它有两个返回值。
fork函数:
fork函数的返回值:
- 成功的话:将子进程的pid返回给父进程,0被返回给子进程。
- 失败的话:-1直接返回给父进程,没有子进程没创建。
这就有点违背我们之前学习的认知了,因为我们之前接触的函数有返回值的也就只有一个返回值,从来没听过有两个返回值的说法。
代码演示:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
printf("Hello World! id = %d\n", id);
return 0;
}
运行的结果非常的诡异:一条打印语句竟然有两个打印结果,并且,我们并没有对id
值做任何修改,但是结果却是有两个不一样的id
值。
这在我们之前的编程语言学习中是不可能的,我们是不接受的。
我们再来看一段代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
//id == 0 : 子进程 , id > 0 : 父进程
if(id == 0)
{
while(1)
{
printf("我是子进程,我的pid:%d,我的父进程是:%d\n", getpid(), getppid());
sleep(1);
}
}
else
{
while(1)
{
printf("我是父进程,我的pid:%d,我的父进程是:%d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
- C语言上
if
和else if
可以同时执行吗?不能!! - C语言中,有没有可能两个以上的死循环同时运行?不能!!
但是上述代码执行结果却是和我们C语言中相违背了~
2.2 fork函数两个返回值的原因:
如何解释呢?fork如何做到会有不同的返回值?
既然是有两个返回值,那么一定是曾经被返回了两次,要不然怎么可能有两个返回值呢?
要想弄清楚这个问题,我们就要知道一个C语言阶段的知识:
调用一个函数,当这个函数准备return的时候,这个函数的核心功能完成了吗??
答案是肯定的!!
- fork之后,OS做了什么?是不是系统多了一个进程。
- 那么我们知道进程是:task_struct + 数据和代码
- 那么子进程也是:task_struct + 子进程的数据和代码
- 子进程的task_ stuct对象内部的数据从哪里来呢??基本是从父进程继承下来的。
- 子进程执行代码,计算数据的,子进程的代码从哪里来呢??没有地方来!!
- 所以子进程和父进程执行同样的代码,fork之后,父子进程代码共享!!
这也就实现了,通过不同的返回值,让不同的进程执行不同的代码。
父进程return一次,子进程return一次,不就是两次返回吗,不就有两个返回值吗?
- fork之后,父进程和子进程会共享代码,一般都会执行后续的代码 — printf为什么会打印两次的问题。
- fork之后,父进程和子进程返回值不同,可以通过不同的返回值,判断让父子执行不同的代码块!!
2.3 fork函数的返回值意义:
fork()为什么给父进程返回子进程的pid,给子进程返回0?
在之前学习过的二叉树中,我们知道一个父结点可以有多个孩子结点,但是一个孩子结点只能有唯一的父亲节点。
- 进程也是这样,一个父进程可以有一个或多个子进程,但是一个子进程只能有唯一的父进程。
- 父进程必须有标识子进程的方案,fork之后,给父进程返回子进程的pid!
- 子进程最重要的是要知道自己被创建成功了,因为子进程找父进程成本非常低!getppid()
- 一个子进程只有一个父进程,给子进程返回0,给父进程返回自己的pid。
-
- 一个是让自子进程得知自己被创建成功了。
-
- 一个是让父进程更好的去控制子进程。
-
- 子进程有方法获取父进程pid,就没必要返回了。
补充:
- 进程是由task_struct 和 对应的数据和代码组成
- 那么我们平时用的指令的执行后,它的进程对应的代码在哪呢?
- 我们以
ls
为例: -
ls
变成进程之后,该进程的代码就是从磁盘/usr/bin/ls
路径下读取数据代码。
3. 进程状态
操作系统就像是计算机里的哲学一样,因为操作系统这门学科讲的范围很宽泛,它的理论内容适用于各个操作系统,而我们要具体的学习某一款操作系统那就是
Linux
。
首先我们凡是说进程,就必须先想到进程的task_ struct
。
- 进程状态本质上是个整数,整数在进程的task struct中。
- task_ struct中会包含进程的相关的信息。
我们来看一下进程的几种状态:
我们先来看一下进程状态之间的关系:
上面一大堆乱七八糟的,看上去是真的乱,真让人头大,我们慢慢来讲~🙋🙋🙋……
3.1 运行状态(R):
- 运行状态:是进程在CPU上运行,就叫运行态吗??答案是否定的!!
- 也叫R状态,running~
概念:
一个进程被运行,操作系统当中每个
CPU
,系统都会个CPU创建一个runqueue
,所以一个进程想被调动,说白了就是将自己的进程放到运行队列当中就可以。
如何理解进程被运行:
进程只要在运行队列中叫做运行态,不代表正在运行,代表我已经准备好了,随时可以调度!!
进程排队实际上是让这个进程控制块去排队。
3.2 终止状态(X):
- 终止状态:这个进程已经被释放,就叫做终止态吗??答案是否定的!!
- 也叫X状态,dead~
是该进程还在,只不过永远不运行了,随时等待被释放!!
- 进程都终止了,为什么不立马释放对应的资源,而要维护一个终止态??
- 释放要花时间吗??有没有可能,当前你的操作系统很忙呢??
进程终止状态:进程已完成执行。
终止就是最终资源释放,但是PCB控制块至少还在,要让操作系统来释放。
3.2 阻塞状态(S):
- 一个进程,使用资源的时候,可不仅仅是在申请CPU资源!
- 进程可能申请更多的其他资源:磁盘,网卡,显卡,显示器资源,声卡 / 音响。
- 如果我们申请CPU资源,暂时无法得到满足,需要排队的 — 运行队列
- 那么如果我们申请其他慢设备的资源呢 — 也是需要排队的!(task_ struct在进程队列排队)
- 阻塞状态,也叫S状态,sleeping~
重点:
当进程访问某些资源(磁盘网卡),该资源如果暂时没有准备好,或者正在给其他进程提供服务,此时:
- 当前进程要从
runqueue
中移除。 - 将当前进程放入对应设备的描述结构体中的等待队列!
网卡,声卡,显卡每一个操作系统都要先管理起来,所以操作系统要先描述再组织~
- 当我们的进程此时在等待外部资源的时候,该进程的代码,不会被执!!
- 我的进程卡住了 — 进程阻塞!上层看来就是某些任务卡住了。
当设备在硬件层面准备好了,一定会通过某种方式让操作系统知道,再将该进程的PCB放在CPU的运行队列当中。
补充:
- CPU运行的速度非常快,但是运行队列的周转周期非常短,看起来所有进程都在运行。
- 单核CPU在任意时间点都只能运行一个进程。
我们来看一下S状态:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("I am a process: %d\n", getpid());
}
return 0;
}
有个疑问,为什么我们一直向显示器打印,但却是S状态呢?
- 我们不能用我们的认知速度来认知计算机
- 因为CPU足够快,外设又很慢,CPU速度的速度是远大于外设的
- 大部分时间进程是在等待外设的显示器准备好的
- 所以大部分时间都是S状态,都是睡眠阻塞状态
- 只有很小的机会查看进程才能看到R运行状态
总结:
虽然一直在刷屏,但是还是S状态,原因也是在printf上,不断地往显示器上打印,但是显示器是个外设速度非常慢,即便是闲着准备好被刷新也需要花费时间。这个进程90%的情况都是在等,在等显示器就绪。
只有光是一个死循环,不调用外设的时候才是一直处于R状态。
3.3 进程挂起:
- 如果内存不足了怎么办??操作系统就要帮我们进行 “辗转腾挪” ~~
- 短期内不会被调度(你等的资源,短期内不会就绪)进程,它的代码和数据依旧在内存中!!就是白白的浪费空间!
- OS就会把该进程的代码和数据置换到磁盘上!
概念:
一个进程对应的代码和数据,被操作系统因为内存资源不足而导致操作系统将该进程的代码和是数据临时的置换到磁盘当中,此时叫做进程挂起。
只把描述进程的PCB留在了内存中。
往往内存不足的时候,伴随着磁盘被高频率访问!
3.4 深度睡眠状态:(D)
首先D状态也是一种阻塞状态,而我们上述的S状态也是阻塞状态,只是D状态是一种深度睡眠~
- D状态可以理解为
deep - 深 / disk - 磁盘
的意思
假设场景:
当一个进程向磁盘写文件的时候,由于要写的文件很大,所以进程要在那里等,如果等的时间太长了的话,操作系统见到一个进程在那里很悠闲直接把它干掉了,那等磁盘将文件写完之后,回头一看,傻眼了,进程不见了,那写入的文件怎么处理呢??如果该文件写入失败了,结果返回的时候发现进程不见了,那么数据就丢了,后果很严重。
尽然进程要等,就是要等一个返回值,就是为了判断文件写成功了没!!
所以这个进程不能随便杀掉,所以操作系统就将该进程设置成了D状态!!
- 凡是D状态的进程,操作系统无权杀掉该进程,只能等该进程自己醒来。
- D状态的进程操作系统没权利将其杀掉,只能通过关机重启 or 拔掉电源的方式来强制杀掉该进程。
- 如果一个系统当中存在大量的D状态进程,关机都关不掉。
- 一般而言,linux中,如果我们等待的是磁盘资源,我们进程阻塞所处的状态就是D。
不过这种情况不多见,很难能见到~
补充:
- 有时候闪退的问题,是服务器压力过大,OS是会终止用户进程的!
3.5 僵尸状态(Z):
僵尸进程:Z - zombie
- X状态:我们上面说的死亡,状态可以随时将他的资源回收掉。
- Z状态:僵尸状态先不急进入X (死亡状态)
当一个Linux中的进程退出的时候,一般不会直接进入X状态(死亡,资源可以立与回收),而是进入Z状态。
我们不禁发出疑问,为什么??
- 首先我们要知道,进程为什么被创建出来呢??
-
- 一定是因为要有任务让这个进程执行。
- 当该进程退出的时候,我们怎么知道,这个进程把任务给我们完成的如何了呢??
-
- 一般需要将进程的执行结果告知给父进程OS。
既然如此,那么父进程派生的子进程,任务执行完了吗??执行的对不对??中途有无被操作系统杀掉??这些情况都需要告知父进程或者操作系统。
- 进程Z,就是为了维护退出信息,可以让父进程或者OS读取的!
- 操作系统 or 父进程知道之后可以做第二次决策。
- 僵尸进程存在的意义就是说明这个进程退出的时候是因为什么原因退出的。
衍生问题:
- 进程退出的情况有哪些?怎么退出进程?
- 操作系统可以读取子进程的退出信息,因为PCB就是它创建的。
- 父进程如何读取子进程的退出信息?
- 父进程如何让子进程的状态由Z变成X?
以后再说…
3.5 - 1 模拟僵尸进程
如果创建子进程,子进程退出了,父进程不退出,也不等待子进程,子进程退出之后所处的状态就是Z状态。(ps:所谓等待子进程就是回收)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("我是子进程,我剩下 %d S\n", cnt);
cnt--;
sleep(1);
}
printf("我是子进程,我已经僵尸了,等待被检测\n");
exit(0);
}
else
{
while(1)
{
sleep(1);
}
}
return 0;
}
子进程五秒后退出,父进程一直在跑,也不回收子进程。我们代码没有写回收,所以子进程就是没有回收。
写一个可以在屏幕上循环打印出进程状态的脚本:
前五秒的进程状态如下:
五秒之后的进程状态:
3.5 - 2 长时间僵尸的危害
- 如果没有人回收子进程的僵尸,该状态会一直维护!该进程的相关资源(task_struct) 不会被释放!一个很严重的问题那就是 — 内存泄漏!!
什么情况会一直僵尸?
- 父进程不回收它,会一直僵尸状态。
- 一个进程僵尸了,是不可被杀死的。都已经成僵尸了还怎么杀死~😂😂😂
一般必须要求父进程进行回收,后面说~
3.6 孤儿进程:
- 子进程不退出,父进程先退出了。
- 父进程的父进程是bash,会自动回收掉他的子进程,所以我们看不到该父进程的僵尸状态。
只需要将之前的代码稍微改一点:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
while(1)
{
printf("我是子进程\n");
sleep(1);
}
printf("我是子进程,我已经僵尸了,等待被检测\n");
}
else
{
int cnt = 3;
while(cnt)
{
printf("我是父进程,我:%d\n", cnt--);
sleep(1);
}
exit(0);
}
return 0;
}
子进程一直不退,父进程3秒后退出。
再用之前的方式来观察。
我们突然发现,父进程3秒后消失不见了,然后原来的S+
变成了S
。
- 状态后面有
+
的,代表这个进程是个前台进程能在键盘ctr/ + C
的是前台进程后台进程ctr/ + C
干不掉。 - 孤儿进程没有那个
+
,它是个后台进程。
只能通过命令来杀,这个后台进程被杀掉之后操作系统就将其回收了。
kill -9 进程的pid
我们还发现子进程的pid变成了1:
- 如果父进程提前退出,子进程还在运行,子进程会被1号进程领养!!
- 如果父进程先退出了,此时子进程再想退出的时候就没人回收没人管了。
- 所以此时给出的方案是,当一个子进程的父进程退出了,该子进程会被1号进程领养。(所以叫孤儿进程)
- 1号进程就是操作系统
3.7 暂停状态:(T / t)
- 暂停状态有两个:一个是
T
,一个是t
。
3.7 - 1 T(stopped)
我们先来将暂停状态T
:
我们先来写一段死循环打印:
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("Hello World!\n");
sleep(1);
}
return 0;
}
它肯定是S状态。
我们上述用kill -9 进程ID
的方式来杀掉进程,还有kill
其他的选项:
我们通kill -19 进程ID
就将这个进程暂停掉了:
我们可以用kill -18 进程ID
再将进程恢复启动起来。
补充:
3.7 - 1 t(tracing stop)
- tracing - 追踪~
- 进程被调试的时候,遇到断点所处的状态。
此时我们再来查看一下进程:
我们再次回头看那张进程状态关系图,此时理解就不难了,随他怎么变,咱都能认得!!
- 就绪: 就是外设准备好了,可以把对应的进程放到某个运行队列里了。等待某种资源的时候一旦就绪就能运行。
- 挂起阻塞: 在阻塞的时候操作系统可以将进程换入换出,这就叫做挂起阻塞,意思就是一个进程是阻塞状态,就将其挂起来,此时进程没有被执行,代码也没有在内存当中,进程只是在等,PCB在内存中。
- 挂起就绪: 阻塞状态操作系统嫌弃它太闲了,就讲它换出去了,外设就绪了,例如磁盘准备了,但是此时代码还在外头,这就叫做挂起就绪,此时再将代码再换入,现在这个进程整体准备好了,再放入运行队列。