前言:
今天我们来聊聊进程^^ ,我们会先从冯诺依曼体系中,认识到内存这个东西;然后,我们会聊一聊操作系统的管理。接着,我们将会开始有侧重的聊一聊进程中PCB的一些内容;最后我们将综合上文所有内容,来聊一聊进程被CPU调度,让它动起来
正文:
冯诺依曼体系:
我在这里画了一张图:

这张图演示的是数据在输入后,如何被计算机进行处理,这途中会经过这些硬件,那么就引出了
计算机的组成:
存储器(内存):
是什么:
这里的存储器,就是指内存!!!!
也是我们接下来重点要说的
我们上面的图中,有输入设备,有输出设备,输入和输出,是相对于谁而言?
内存!!!
为何存在:
这时我们发现一个问题,我们发明计算机是为了处理数据,那么为什么还要有内存这个东西?
我们大可以像下面这样设计:

这样直接把输入交给CPU,再进行输出,不是更加简洁,更加快吗?
可惜答案并非如此
小demo:

这里在网上找了一张图,我们用这张图来看:
这张图自上而下,效率越来越低,而价格同时也是越来越低
所以一般磁盘这些存储空间就很大,效率不快;(毫秒)
相反,寄存器这些存储空间小,效率快(约1纳秒)
而内存,效率还行(约100纳秒)
所以可以发现,寄存器和磁盘的效率差距极大
内存缩小差距:
所以我们可以发现,如果输入输出(假设是磁盘)直接和CPU打交道,CPU速度极快,所以整个流程的速度由磁盘主导,那么就会很慢很慢(因为寄存器与磁盘速度差距过大)
那么,如何解决?
答案是引入内存。

在这张图中,有一个速度还行的内存在中间,那就带来了两个好处:
①. 内存本身速度远超磁盘,即使不做别的,只是把数据放在内存给cpu读写,就已经快成千上万倍
②. 预加载机制:程序访问的数据往往是连续,重复使用的,后续再次使用时,直接命中内存,会更加快
所以:我们就引入了内存,之后就出现一个结论
总结:
操作系统(OS)
概念:

狭义上的理解:操作系统就是操作系统内核
广义上的理解:操作系统是操作系统内核加上程序
设计目的:
对下,进行管理:进程管理,文件管理,驱动管理,内存管理
对上,服务人,为用户提供一个良好的操作环境
图理解:
细谈管理:
假如,武斌当上了校长,他现在要管理学校里面的几千个学生,有一天,学校里要选一个打篮球很厉害的学生去代表学校参数,武斌就要从这几千个人里面找一个打篮球最厉害的,武斌很头疼.......总不能一个一个来谈话吧?所以武斌找到了他的下属(班主任),他让这些班主任去搜集学生的信息,然后最后把信息都录入到电脑上;然后呢?一个一个看嘛,好像武斌眼睛看瞎了也看不完呢,所以,武斌就想到,可以定义一个结构体,来把学生的信息录入进去,来描述这些学生,嗯.....录完后,条理多了,不过,还是很难找啊;所以武斌就想到一个办法,把描述好的学生结构体,用链表组织起来,然后,通过一个简单的比较大小接口,遍历了一下链表,武斌就找到了那个打篮球最好的学生~
所以,通过这个小故事,我们得出以下结论:
①:上级管理下级,不需要直接见面,只需管理它的数据即可
②:在管理的时候,我们通常要先描述事物的属性,再选择合适的数据结构把他们组织起来,这种先描述再组织的思维,非常重要
细说这张图片:
接下来,我们来看看这张图:
那这里,操作系统就相当于校长,驱动程序就相当于班主任,而硬件就相当于学生:
操作系统不可能直接和硬件打交道,就像校长不可能挨个找几千个学生,而驱动程序收集好硬件信息,进行描述,生成结构体,交由操作系统管理;

这时,我们把操作系统,驱动程序,硬件看成一个整体(学校),而我总有需求,来让操作系统给我做点事,那我直接动底层?我直接掀开硬盘,把东西写进去?太危险了!!!!所以呢,既然操作系统是硬件的管理者,那么在操作时,就先要问它的意见,所以它为了解决我们的诉求,就给自己开放了一些接口(系统调用接口),我们通过这个来对底层进行操作,但是,直接用系统调用接口,太难了吧,我是普通人我不会,所以就有人继续对系统调用接口进行封装,从而诞生了更易操作的用户操作窗口,来方便我们的使用,举个例子就是:
校园开放了一些校园办事窗口(系统调用接口),来给我们解决问题,但是啊,我是一个六十岁老大爷,我不太会,这时,就有服务人员(用户操作窗口)来帮助我,在它的帮助下,我(用户)就完成了我的诉求(命令)
小问题:
那么,我的操作系统,如何对进程进行管理?
先描述,再组织!
进程:
概念:
进程=内核数据结构(PCB) + ⾃⼰的程序代码和数据
PCB:
我们说,先描述,再组织;那么PCB是什么呢?
就是我们对程序代码和数据的描述!
官方一些的语言是:进程信息被放在⼀个叫做进程控制块的数据结构中,可以理解为进程属性的集合
在linux中,PCB叫做task_struct
显然,task_struct是操作系统管理进程的主要,所以我们来讲讲它:
task_struct:
内容:
声明:
我们主要讲解上面被标红的三项
标示符:
本质:
linux中的每一个进程,操作系统都会分配一个唯一非负整数编号,即为pid
作用:
操作系统管理进程,通过pid定位进程
(就像校长管学生,通过学号)
演示:
我们写了一段代码
#include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 while(1)
6 {
7 printf("奥拉星牛逼,我的pid是:%d\n",getpid());
8 sleep(1);
9 }
10 }
我们把它编译之后,开始运行:

我们打开分屏:
[tsx@VM-0-10-centos ~]$ ps axj | head -1;ps axj | grep code
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13708 21756 21756 13708 pts/0 21756 S+ 1001 0:00 ./code
14287 21986 21985 14287 pts/1 21985 S+ 1001 0:00 grep --color=auto code
这几列分别是:
PPID: 父进程id,谁创建了当前进程,填谁
PID: 当前进程自身id
PGID: 进程组id
SID: 会话id
TTY: 控制终端
TPGID: 前台进程组id
STAT: 进程状态
(有+是前台进程,没有则是后台)(一会细讲)
UID: 用户id
TIME: 进程累计占用cpu时间
COMMAND: 启动进程的命令
上面那个是code,但同时,我们用grep命令,这也是个进程,所以它也显示出来了
内存指针:
一句话总结:用来指向进程对应的代码和数据

记账信息:
一句话总结:用来统计进程占用系统资源的各项数据,内核依靠它完成统计,调度,资源限制
小说明:
由于操作系统,它要满足理论在所有操作系统中都受用,那么就一定会抽象一些,而且还会和我们详细的操作系统有出入,所以:
状态这里我们分成两部分讲解:
①. 操作系统的定义
②. linux中的定义
操作系统状态:
操作系统中:
这里在网上找了一个图

那么这里,我们如何理解呢?
就绪状态:
资源全部准备完毕,只差cpu调度
(这里和linux中不一样,我们一会细分)
运行状态:
从就绪态被调度,正在CPU上执行命令
(这里和linux中不一样,我们一会细分)
阻塞状态
我们先创建一段代码
#include<stdio.h>
2 int main()
3 {
4 int a=1;
5 scanf("%d",&a);
6 printf("%d",a);
7 return 0;
8 }

我们可以看到,在我们运行之后,进程需要键盘上的数据,所以它就卡住了!
那么底层的原理就是:

就比如这张图,task_struct1对应的进程需要键盘输入,那么我就从当前的runqueue上断开,从而来到了硬件的等待队列中

所以,区分阻塞状态与运行状态(为什么是运行状态一会在linux状态中细说),就是看当前,我进程的task_struct在哪个队列之下排队!,在硬件等待队列中排队,那么就是阻塞状态
挂起状态:
挂起状态是什么呢?

如果我们在进程运行时,内存突然不够了,那么为了腾出内存,我们会把wait_queue中的进程的内存和数据放在swap分区之中,这种状态叫阻塞挂起状态,直到我要被CPU调度,我才会从swap分区中把我的代码和数据取出来。
那如果,把wait_queue中的内存和数据挂起以后,内存还是不够呢?那么我们就会把runqueue中的内存和数据挂起,这叫就绪挂起状态
那再极端些,都挂起后,内存还是不够呢 ?那么CPU就要杀进程了
小结:
那么,操作系统中的进程状态就讲完了,接下来,我们讲讲linux中的进程状态:
linux状态:
R运⾏状态(running):
#include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 while(1)
6 {
7 printf("奥拉星牛逼\n");
8 }
9 }
我运行一下,就成了这样:
奥拉星牛逼
奥拉星牛逼
奥拉星牛逼
奥拉星牛逼
奥拉星牛逼
奥拉星牛逼
奥拉星牛逼
奥拉星牛逼
奥拉星牛逼
奥拉星牛逼
奥拉星牛逼
奥拉星牛逼
奥拉星牛逼
然后我看一下状态:
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13708 27314 27314 13708 pts/0 27314 S+ 1001 0:00 ./code
14287 27354 27353 14287 pts/1 27353 S+ 1001 0:00 grep --color=auto code
[tsx@VM-0-10-centos ~]$
那么此时,我查看进程状态:
不对吧?居然是S?
这里是有原因的:
打印输出本质是向硬件执行io写操作,终端窗口接受字符速度跟不上死循环打印速度,此时进程放弃CPU资源,变成s状态,写出完成之后,排完队后变成R状态,等待它写入时候继续变成S状态
[tsx@VM-0-10-centos ~]$ cat code.c
#include<stdio.h>
#include<unistd.h>
int main()
{
while(1)
{
;
}
}
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13708 28373 28373 13708 pts/0 28373 R+ 1001 0:11 ./code
14287 28445 28444 14287 pts/1 28444 S+ 1001 0:00 grep --color=auto code
就是R状态了
前台进程与后台进程:
只需记住一点,前台进程可以读取键盘输入,后台进程不行
S睡眠状态(sleeping):
[tsx@VM-0-10-centos ~]$ cat haha.c
#include<stdio.h>
int main()
{
int a=1;
scanf("%d",&a);
printf("%d",a);
return 0;
}
[tsx@VM-0-10-centos ~]$ ./haha
我一查看:
[tsx@VM-0-10-centos ~]$ ps axj | head -1;ps axj | grep haha
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13708 29535 29535 13708 pts/0 29535 S+ 1001 0:00 ./haha
14287 29738 29737 14287 pts/1 29737 S+ 1001 0:00 grep --color=auto haha
是睡眠状态,那么我
[tsx@VM-0-10-centos ~]$ ps axj | head -1;ps axj | grep haha
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13708 29535 29535 13708 pts/0 29535 S+ 1001 0:00 ./haha
14287 29738 29737 14287 pts/1 29737 S+ 1001 0:00 grep --color=auto haha
[tsx@VM-0-10-centos ~]$ kill -9 29535
[tsx@VM-0-10-centos ~]$ ^C
我把这个进程杀了
[tsx@VM-0-10-centos ~]$ ./haha
Killed
它能被杀掉,所以叫做浅度睡眠
D磁盘休眠状态(Disk sleep)
T停⽌状态(stopped):
[tsx@VM-0-10-centos ~]$ ./haha &
[1] 31517
[tsx@VM-0-10-centos ~]$
就是在后面加&,此时我们再查看状态:
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13708 31517 31517 13708 pts/0 13708 T 1001 0:00 ./haha
14287 31650 31649 14287 pts/1 31649 S+ 1001 0:00 grep --color=auto haha
我们发现:T!
T这个状态呢,一般是出现问题的时候会出现的
做对的事情,叫休眠
做错的事情,叫暂停
同时:
[tsx@VM-0-10-centos ~]$ ps axj | head -1;ps axj | grep code
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13708 2598 2598 13708 pts/0 2598 S+ 1001 0:00 ./code
14287 2660 2659 14287 pts/1 2659 S+ 1001 0:00 grep --color=auto code
[tsx@VM-0-10-centos ~]$ kill -19 2598
[tsx@VM-0-10-centos ~]$ ps axj | head -1;ps axj | grep code
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13708 2598 2598 13708 pts/0 13708 T 1001 0:00 ./code
14287 2831 2830 14287 pts/1 2830 S+ 1001 0:00 grep --color=auto code
[tsx@VM-0-10-centos ~]$ kill -18 2598
[tsx@VM-0-10-centos ~]$ ps axj | head -1;ps axj | grep code
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13708 2598 2598 13708 pts/0 13708 S 1001 0:00 ./code
14287 2897 2896 14287 pts/1 2896 S+ 1001 0:00 grep --color=auto code
kill -19 pid
可以让进程停止
kill -18 pid
可以让进程继续,不过前台进程会变为后台进程,不能ctrl+c结束,只能用kill -9杀死
Z僵尸状态(zombie):
子进程被杀了或者结束以后,代码和数据被释放,PCB需要维持,此时变成僵尸进程。
代码如下
#include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 pid_t id=fork();
6 if(id==0)
7 {
8 while(1)
9 {
10 printf("我是子进程,我的pid是:%d,我的父亲pid是:%d\n",getpid(),getppid());
11 sleep(1);
12 }
13 }
14 else{
15 while(1)
16 {
17 printf("我是父进程,我的pid是:%d\n",getpid());
18 sleep(1);
19 }
20 }
21 return 0;
22 }
接下来我们进行一些操作:
[tsx@VM-0-10-centos ~]$ ps axj | head -1;ps axj | grep code
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13708 6312 6312 13708 pts/0 6312 S+ 1001 0:00 ./code
6312 6313 6312 13708 pts/0 6312 S+ 1001 0:00 ./code
14287 6392 6391 14287 pts/1 6391 S+ 1001 0:00 grep --color=auto code
[tsx@VM-0-10-centos ~]$ kill -9 6313
[tsx@VM-0-10-centos ~]$ ps axj | head -1;ps axj | grep code
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13708 6312 6312 13708 pts/0 6312 S+ 1001 0:00 ./code
6312 6313 6312 13708 pts/0 6312 Z+ 1001 0:00 [code] <defunct>
14287 6674 6673 14287 pts/1 6673 S+ 1001 0:00 grep --color=auto code
[tsx@VM-0-10-centos ~]$
打印结果如下:

X死亡状态(dead):
孤儿进程:
操作如下:
[tsx@VM-0-10-centos ~]$ ps axj | head -1;ps axj | grep code
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13708 8102 8102 13708 pts/0 8102 S+ 1001 0:00 ./code
8102 8103 8102 13708 pts/0 8102 S+ 1001 0:00 ./code
14287 8143 8142 14287 pts/1 8142 S+ 1001 0:00 grep --color=auto code
[tsx@VM-0-10-centos ~]$ kill -9 8102
[tsx@VM-0-10-centos ~]$ ps axj | head -1;ps axj | grep code
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1 8103 8102 13708 pts/0 13708 S 1001 0:00 ./code
14287 8373 8372 14287 pts/1 8372 S+ 1001 0:00 grep --color=auto code
[tsx@VM-0-10-centos ~]$
打印结果如下:
我是父进程,我的pid是:8102
我是子进程,我的pid是:8103,我的父亲pid是:8102
我是父进程,我的pid是:8102
我是子进程,我的pid是:8103,我的父亲pid是:8102
Killed
[tsx@VM-0-10-centos ~]$ 我是子进程,我的pid是:8103,我的父亲pid是:1
我是子进程,我的pid是:8103,我的父亲pid是:1
我是子进程,我的pid是:8103,我的父亲pid是:1
我是子进程,我的pid是:8103,我的父亲pid是:1
我是子进程,我的pid是:8103,我的父亲pid是:1
我是子进程,我的pid是:8103,我的父亲pid是:1
我们发现,他的父进程先结束的话,那么他就会认systemd为父亲,同时变成后台进程,它结束后由systemd给它“收尸”
从上面的孤儿进程和僵尸进程我们可以发现:
进程是有独立性的
优先级:
概念:
首先区分,权限和优先级:
权限:能不能?
优先级:能,但是什么时候?
PRI和NI
我们输入ps -l 后会出现这些
[tsx@VM-0-10-centos ~]$ ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 R 1001 9440 14287 0 80 0 - 38324 - pts/1 00:00:00 ps
0 S 1001 14287 14286 0 80 0 - 29216 do_wai pts/1 00:00:00 bash
其中,PRI代表这个进程可被执⾏的优先级,其值越⼩越早被执⾏
例:
[tsx@VM-0-10-centos ~]$ ps -al
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 T 1001 1483 13708 0 80 0 - 1054 do_sig pts/0 00:00:00 haha
0 S 1001 10941 13708 0 90 10 - 1054 hrtime pts/0 00:00:00 code
1 S 1001 10942 10941 0 80 0 - 1054 hrtime pts/0 00:00:00 code
0 R 1001 11295 14287 0 80 0 - 38332 - pts/1 00:00:00 ps
[tsx@VM-0-10-centos ~]$
我在改的时候,把pid为10941的进程nice改为10,那么PRI变为90
补充概念:
进程切换:
调度:
讲了这么多,终于来到这里了,调度是我们今天要讲的终极问题,需要结合我们以上讲的所有概念来说,不过在此之前,我们还有引入几个小demo:
①demo1:
定义一个结构体,如果只知道结构体中一个成员的地址,还知道这个结构体的类型,那么是可以知道这个结构体内所有地址的

②.demo2:
其实,在linux中,数据结构有些不同,比如说在我们的task_struct中:

可能是这样的结构,那么它就同时成为了两种链表的节点,二叉树的节点,还可以更多它是把数据结构内嵌到里面,这样做还有一个好处就是,只需要写一组代码,就可以实现该数据结构在所有自定义类型中的使用。
正式开始:

我给大家简单画了一下:
我们先看array[0]:
效率:
O(1)
过程:
①.先看nr_active是否为0,防止没有进程让我去查找
②. 之后遍历位图,(实时优先级实时优先级跳着走,效率高,可以跳过一大片无进程的位置),直到找到位图为1的位置n
③.通过对应关系再到queue数组中的第n个位置,把第一个进程取出来,进行调度
过期队列:
此时我们发现,有一个问题:
如果我现在正在调度,假如在queue[120],而我一直在[119]新增进程,那么剩下的是不是就永远不会被调度了?
所以为了解决这个问题,我们有了过期队列

尾:
终于写完了,从两点半开始写到八点多,思路的整理是个有趣的过程呢,不过我现在真的要休息了,明天还要五点半起床去备战奥拉星~~
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2601_96394870/article/details/163336482




