-梅头像
关注
linux(5) 进程封面图

linux(5) 进程

前言:

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

正文:

冯诺依曼体系:

我在这里画了一张图:

这张图演示的是数据在输入后,如何被计算机进行处理,这途中会经过这些硬件,那么就引出了

计算机的组成:

输⼊单元:包括键盘, ⿏标,扫描仪, 写板等
中央处理器(CPU):含有运算器和控制器等
输出单元:显⽰器,打印机等

存储器(内存):

是什么:

这里的存储器,就是指内存!!!!

也是我们接下来重点要说的

我们上面的图中,有输入设备,有输出设备,输入和输出,是相对于谁而言?

内存!!!

为何存在:

这时我们发现一个问题,我们发明计算机是为了处理数据,那么为什么还要有内存这个东西?

我们大可以像下面这样设计:

这样直接把输入交给CPU,再进行输出,不是更加简洁,更加快吗?

可惜答案并非如此

小demo:

这里在网上找了一张图,我们用这张图来看:

这张图自上而下,效率越来越低,而价格同时也是越来越低

所以一般磁盘这些存储空间就很大,效率不快;(毫秒)

相反,寄存器这些存储空间小,效率快(约1纳秒)

而内存,效率还行(约100纳秒)

所以可以发现,寄存器和磁盘的效率差距极大

内存缩小差距:

所以我们可以发现,如果输入输出(假设是磁盘)直接和CPU打交道,CPU速度极快,所以整个流程的速度由磁盘主导,那么就会很慢很慢(因为寄存器与磁盘速度差距过大)

那么,如何解决?

答案是引入内存。

在这张图中,有一个速度还行的内存在中间,那就带来了两个好处:

①. 内存本身速度远超磁盘,即使不做别的,只是把数据放在内存给cpu读写,就已经快成千上万倍

②. 预加载机制:程序访问的数据往往是连续,重复使用的,后续再次使用时,直接命中内存,会更加快

所以:我们就引入了内存,之后就出现一个结论

总结:

所有设备都只能直接和内存打交道!

操作系统(OS)

概念:

狭义上的理解:操作系统就是操作系统内核

广义上的理解:操作系统是操作系统内核加上程序

设计目的:

对下,进行管理:进程管理,文件管理,驱动管理,内存管理

对上,服务人,为用户提供一个良好的操作环境

图理解:

细谈管理:

假如,武斌当上了校长,他现在要管理学校里面的几千个学生,有一天,学校里要选一个打篮球很厉害的学生去代表学校参数,武斌就要从这几千个人里面找一个打篮球最厉害的,武斌很头疼.......总不能一个一个来谈话吧?所以武斌找到了他的下属(班主任),他让这些班主任去搜集学生的信息,然后最后把信息都录入到电脑上;然后呢?一个一个看嘛,好像武斌眼睛看瞎了也看不完呢,所以,武斌就想到,可以定义一个结构体,来把学生的信息录入进去,来描述这些学生,嗯.....录完后,条理多了,不过,还是很难找啊;所以武斌就想到一个办法,把描述好的学生结构体,用链表组织起来,然后,通过一个简单的比较大小接口,遍历了一下链表,武斌就找到了那个打篮球最好的学生~

所以,通过这个小故事,我们得出以下结论:

①:上级管理下级,不需要直接见面,只需管理它的数据即可

②:在管理的时候,我们通常要先描述事物的属性,再选择合适的数据结构把他们组织起来,这种先描述再组织的思维,非常重要

细说这张图片:

接下来,我们来看看这张图:

那这里,操作系统就相当于校长,驱动程序就相当于班主任,而硬件就相当于学生:

操作系统不可能直接和硬件打交道,就像校长不可能挨个找几千个学生,而驱动程序收集好硬件信息,进行描述,生成结构体,交由操作系统管理;

这时,我们把操作系统,驱动程序,硬件看成一个整体(学校),而我总有需求,来让操作系统给我做点事,那我直接动底层?我直接掀开硬盘,把东西写进去?太危险了!!!!所以呢,既然操作系统是硬件的管理者,那么在操作时,就先要问它的意见,所以它为了解决我们的诉求,就给自己开放了一些接口(系统调用接口),我们通过这个来对底层进行操作,但是,直接用系统调用接口,太难了吧,我是普通人我不会,所以就有人继续对系统调用接口进行封装,从而诞生了更易操作的用户操作窗口,来方便我们的使用,举个例子就是:

校园开放了一些校园办事窗口(系统调用接口),来给我们解决问题,但是啊,我是一个六十岁老大爷,我不太会,这时,就有服务人员(用户操作窗口)来帮助我,在它的帮助下,我(用户)就完成了我的诉求(命令)

小问题:

那么,我的操作系统,如何对进程进行管理?

先描述,再组织!

进程:

概念:

进程=内核数据结构(PCB) + ⾃⼰的程序代码和数据

PCB:

我们说,先描述,再组织;那么PCB是什么呢?

就是我们对程序代码和数据的描述!

官方一些的语言是:进程信息被放在⼀个叫做进程控制块的数据结构中,可以理解为进程属性的集合

在linux中,PCB叫做task_struct

显然,task_struct是操作系统管理进程的主要,所以我们来讲讲它:

task_struct:

内容:
标⽰符 : 描述本进程的唯⼀标⽰符,⽤来区别其他进程。(pid)
状态 : 任务状态,退出代码,退出信号等
优先级: 相对于其他进程的优先级。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下⽂数据 : 进程执⾏时处理器的寄存器中的数据
记账信息: 可能包括处理器时间总和,使⽤的时钟数总和,时间限制,记账号等
其他信息
声明:

我们主要讲解上面被标红的三项

标示符:
本质:

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):
并不意味着进程⼀定在运⾏中,它表明进程要么是在运⾏中要么在运⾏
队列⾥。
只要一个进程的task_struct在runqueue中,那么这个进程它就是运行状态
 #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)
有时候也叫不可中断睡眠状态,在这个
状态的进程通常会等待IO的结束。
T停⽌状态(stopped):
可以通过发送 SIGSTOP 信号给进程来停⽌(T)进程。这个被暂停的
进程可以通过发送 SIGCONT 信号让进程继续运⾏。
那么我们说过,后台进程不能从键盘读数据
有个办法可以让进程在后台运行
[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 ~]$ 

打印结果如下:

我们可以看到,杀了子进程以后,子进程变成z状态,其实呢,就是等待退出数据被回收,被回收以后,那么就从z状态变为x状态,释放PCB
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代表这个进程可被执⾏的优先级,其值越⼩越早被执⾏

NI :代表这个进程的nice值
PRI(new)=PRI(old)+nice
                  80
其中,nice的取值是从-20到19,左右皆闭,一共四十个值
而PRI的取值则是从60到99
当nice值为负值的时候,那么该程序将会优先级值将变⼩,即其优先级会变⾼,则其越快
被执⾏
所以,调整进程优先级,在Linux下,就是调整进程nice值
例:
[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

补充概念:
①竞争性: 系统进程数⽬众多,⽽CPU资源只有少量,甚⾄1个,所以进程之间是具有竞争属性的,为了⾼效完成任务,更合理竞争相关资源,便具有了优先级
②独⽴性: 多进程运⾏,需要独享各种资源,多进程运⾏期间互不⼲扰
③并⾏: 多个进程在多个CPU下分别,同时进⾏运⾏,这称之为并⾏
④并发: 多个进程在⼀个CPU下采⽤进程切换的⽅式,在⼀段时间之内,让多个进程都得以推进,称之为并发(我们电脑一般是这个)
进程切换:

CPU上下⽂切换:其实际含义是任务切换, 或者CPU寄存器切换。当多任务内核决定运⾏另外的任
务时, 它保存正在运⾏任务的当前状态, 也就是CPU寄存器中的全部内容。这些内容被保存在任务
⾃⼰的堆栈中, ⼊栈⼯作完成后就把下⼀个将要运⾏的任务的当前状况从该任务的栈中重新装⼊
CPU寄存器, 并开始下⼀个任务的运⾏, 这⼀过程就是context switch。
这里就体现上下文数据的重要性了:
这里只区分一点:
寄存器是CPU的,只有一套:
而寄存器内数据是进程私有的,每一个进程都有其自己的寄存器数据。
调度:

讲了这么多,终于来到这里了,调度是我们今天要讲的终极问题,需要结合我们以上讲的所有概念来说,不过在此之前,我们还有引入几个小demo:

①demo1:

定义一个结构体,如果只知道结构体中一个成员的地址,还知道这个结构体的类型,那么是可以知道这个结构体内所有地址的

②.demo2:

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

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

正式开始:

我给大家简单画了一下:

我们先看array[0]:

nr_active: 总共有多少个运⾏状态的进程
(用来提速}
queue[140]: ⼀个元素就是⼀个进程队列,相同优先级的进程按照FIFO规则进⾏排队调度,所以,
数组下标就是优先级!
这里有一个哈希映射的关系,0-99对应的是实时优先级,我们今天并不讨论
而100-139对应的是普通优先级(PRI+40)
bitmap[5]:⼀共140个优先级,⼀共140个进程队列,为了提⾼查找⾮空队列的效率,就可以⽤
5*32个⽐特位表⽰队列是否为空,这样,便可以⼤ 提⾼查找效率!
(也是提速)
效率:

O(1)

过程:

①.先看nr_active是否为0,防止没有进程让我去查找

②. 之后遍历位图,(实时优先级实时优先级跳着走,效率高,可以跳过一大片无进程的位置),直到找到位图为1的位置n

③.通过对应关系再到queue数组中的第n个位置,把第一个进程取出来,进行调度

过期队列:

此时我们发现,有一个问题:

如果我现在正在调度,假如在queue[120],而我一直在[119]新增进程,那么剩下的是不是就永远不会被调度了?

所以为了解决这个问题,我们有了过期队列

① 过期队列和活动队列结构⼀模⼀样
② 过期队列上放置的进程,都是时间⽚耗尽的进程
③  当有新进程时,插入到过期队列中
④. active指针永远指向活动队列
     expired指针永远指向过期队列
⑤  在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了⼀批
新的活动进程

尾:

终于写完了,从两点半开始写到八点多,思路的整理是个有趣的过程呢,不过我现在真的要休息了,明天还要五点半起床去备战奥拉星~~

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2601_96394870/article/details/163336482

文章来源crawl

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--