冯诺依曼体系结构,进程概念,进程状态,进程优先级

冯诺依曼体系结构

我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。我们常识所知道的计算机硬件如CPU,磁盘,显示器,键盘等等,它们都隶属于冯诺依曼体系结构的一部分。在这里插入图片描述

  什么是冯诺依曼体系?我们又该如何去理解冯诺依曼体系结构呢?我将通过硬件的角度一一解读,使我们搭建一个宏观的概念理解。掌握计算机基本的IO过程。
  冯诺依曼体系的基本构成是由输入设备,存储器,输出设备以及运算器控制器构成的。
  冯诺依曼体系中的存储器指的是内存,而这里的内存是相对于磁盘或者是外存来说的。这里的外存指的是拥有永久性存储能力的外设。注意这里的外设,磁盘,键盘,显示器等都是外设。
那么什么是外设呢?这里的外设是相对于内存+CPU来说的!!外设分为输入设备和输出设备,例如键盘就是典型的输入设备,显示器就是典型的输出设备。而有些外设既具有输入能力,又具有输出能力,典型的例子就是计算机中的磁盘(既可以从磁盘中将数据读取到内存,又可以将数据写入到磁盘中)和网卡(即可以把数据发送到网络,又可以将数据从网络中拿回来)。所以我们的外设中,有单纯的输入和输出设备,也有既可以输入又可以输出的设备。
  什么是CPU?
  运算器+控制器+其他 = CPU(中央处理器)。对于中央处理器有一个最大的特征就是计算速度非常快。对于其他设备:存储器(内存)较快,外设较慢。CPU ,存储器,外设之间的速度差距是数量级级别的差距。CPU是用来计算的,而CPU要进行计算要不要数据呢?答案当然是需要数据的。在计算机中CPU其实很笨,它只能被动的接受别人的指令,别人的数据 来完成执行别人的指令,计算别人的数据的目的。而CPU想要执行别人的指令,首先需要CPU能够认识指令。CPU是如何认识的呢?答案是因为CPU内部有一个自己的指令集,我们在对代码进行编译的时候使代码变成二进制可执行程序,而其编译的本质就是将代码编译成CPU能够认识的指令。而另一个问题就是CPU想要计算数据,那么数据又是从哪里来的呢?
  在此CPU就有两种获取数据的方法:其一是通过临时存储介质也就是内存中获取,其二就是通过永久存储介质磁盘中获取的。在上文中已经提到了CPU(中央处理器),存储器,外设之间速度差异是非常大的(主流的机械硬盘速度大概在50-150MB/s之间,SSD大概是150-500MB/s,主流的CPU(带流水线)、内存的速度大概是硬盘速度的100~1000倍左右。)。磁盘也是一种外设,自然的磁盘中的数据向CPU传递数据的效率是非常慢的,要远远慢于CPU处理数据的速度。因此磁盘中的数据如果直接抵达给CPU,就造成CPU有大量的时间在等待数据从磁盘中传输给CPU,而不是在对数据进行计算,这样就极大的降低了CPU处理数据的效率。因此为了避免出现这种情况,计算机CPU在对磁盘中的数据进行计算处理的时候,并不是直接从磁盘中读取数据的,而是先将磁盘中的数据预先加载到内存,再让CPU从内存中读取相应的数据进行处理,这样计算机整体的计算效率就不会因为外设而被拖垮了。因此计算机就是通过这种方法来解决CPU与外设之间的速度不匹配的问题。
  其中我们将数据从外设搬到内存与从内存搬到外设的过程称之为IO。从以上的论述中我们得到了以下的结论:
  1.CPU不和外设打交道,和内存直接打交道
  2.所有的外设,有数据需要载入,只能载入到内存中,内存写出,也一定是写道外设中。
现在我们通过以上的知识来尝试解释这样的场景:我和我的朋友进行QQ聊天,“你好”数据流是如何在不同的电脑中流动的(不考虑网络)?
我和朋友的电脑都是一部冯诺依曼体系结构,通过键盘输入你好信息,消息需要经过CPU的加密写道内存当中的,定期刷新到外设中,该消息一份刷新到网卡,一份刷新到显示器中,数据经过网卡发送到网络,经过网络发送到朋友的网卡中,朋友的网卡收到后抵达给内存经过CPU解密刷新到朋友的显示器设备上。

操作系统

上文我们了解到了软件相关的知识,接下来就要了解一下软件----操作系统相关的知识。
操作系统是一个进行软硬件资源管理的软件。
操作系统要通过合理的管理软硬件资源管理手段,为用户提供良好的,稳定的,高效的,安全的执行环境。所以操作系统最重要的作用或者说任务就是去做管理

进程

我们经常说一个运行起来的(加载到内存)的程序就叫做进程。
我们知道我们写的代码是保存在磁盘中的,代码想要运行先要加载到内存中,当加载到内存中就成为了一个进程。对于计算机来说,同一时刻可能要处理很多不同的事情,运行很多不同的程序。因此此时可能会有很多不同的程序要被加载到内存之中,也就是说此时有很多个进程在内存中运行。那么问题就出现了,同一时间既然有这么多的进程需要加载到内存,那么这么多的进程操作系统是不是要给进程分配内存,是不是要判断什么时候进程结束,去释放进程的空间,是不是要知道进程此时处于什么状态等等。这一切都需要操作系统进行管理,如何进行管理?操作系统管理的核心方法就是:先描述,再组织。

描述进程–PCB

PCB就是一个进程控制块 struct task_struct。该task_struct结构体中保存的有进程的所有的属性,该进程的代码和属性地址等等。对于每一个要加载到内存的进程,都要分配一个PCB结构体对象,将结构体的一系列相关属性填入到结构体之中。然后如何将这么多的不同的进程的不同的结构体对象如何进行组织呢?操作系统是通过链表的结构对他进行组织的,不同的结构体对象保存进程的相关属性后会链入到链表之中与其他的PCB结构体对象连接起来。这样操作系统对进程的管理就转化成对链表的增删查改,对进程的管理就转变成了对链表的管理。
因此进程 = 内核数据结构(task_struct) + 进程对应的磁盘代码。
task_struct 内容的分类

标识符:描述进程的唯一标识符,用来区别其他进程
状态:任务状态,退出代码,退出信号等。
优先级:相对于其他进程的优先级
程序计数器:程序中将被执行的下一条指令的地址
内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块指针
上下文数据:进程执行时处理器的寄存器中的数据
I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表
记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记帐号等
其他信息

查看进程

在Windows系统中,我们打开任务管理器就可以看到目前正在运行的进程的状态

  1. 在这里插入图片描述

那么在LINUX系统中如何查看进程呢?
现在编写了myproc的如下代码

int main()
{
    while(1)
    {
        printf("我是一个进程!\n");
        sleep(1);
    }
    return 0;
}

make并运行后会一直在屏幕在打印这句话,而此时该程序就变成了一个进程。我们可以通过以下的方法进行查看:
输入指令:ps ajx | grep “myproc”
在这里插入图片描述
输入指令:ps ajx | head -1 && ps ajx | grep “myproc”,显示PID PPID 等信息
在这里插入图片描述
系统调用 函数getpid(),头文件<unistd.h>,获取进程的PID

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 
  4 int main()
  5 {
  6     while(1)
  7     {
  8         printf("我是一个进程!我的ID是:%d\n",getpid());
  9         sleep(1);
 10     }
 11     return 0;                                                                                              
 12 }

运行后,显示进程的ID是17921
在这里插入图片描述
在Linux中还有一个目录结构 /proc
ls /proc 查看当前时刻系统中正在运行的所有进程
在这里插入图片描述

创建子进程—fork

父进程

获取父进程PID,getppid()

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <sys/types.h>
  4 
  5 int main()
  6 {
  7     while(1)
  8     {
  9         printf("我是一个进程!我的ID是%d 父进程是%d\n",getpid(),getppid());
 10         sleep(1);                                                                  
 11     }
 12     return 0;
 13 }

显示出父进程的PID,经过多次CTRL + c ,发现父进程的PID并不会改变,为什么会出现这种情况呢?
在这里插入图片描述

我们通过进程查看命令查看父进程
在这里插入图片描述
可以发现的父进程是bash ,是命令行解释器,说明我们在命令行上运行的程序,是以bash的子进程运行的。这样我们在命令行启动的运行程序如果出现了BUG或者其他问题,父进程bash会通过命令行报告给用户,并不会影响bash。我们在每次登录的时候操作系统就给我们分配了一个bash 命令行解释器,因此不出意外的话,我们在命令行上启动的程序的父进程都是 bash 命令行解释器。

创建子进程

fork() 创建子进程

1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <sys/types.h>
  4 
  5 int main()
  6 {                                                                                                       
  7     fork();                     
  8     printf("我是一个进程,pid:%d,ppid:%d\n",getpid(),getppid())
  9     return 0;         
 10 }         

如果代码执行正确就应该在fork()之后就创建了一个子进程,创建子进程后此时在主函数内就应该有父进程和子进程两个执行流。那么如果这样的话,最后在屏幕上就应该打印出两条语句。运行之后屏幕上果然打印了两条语句。
在这里插入图片描述
也就是说父进程的PID是4186,子进程的PID是4187,而6525就是bash(父进程的父进程)。
fork()的返回值特性
子进程创建成功就会给父进程返回子进程的PID,而给子进程返回子0,也就是说一个函数有两个返回值!!

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    pid_t id = fork();//使用一个变量接收fork的返回值
    if(id==0)//运行子进程
    {
        while(1)
        {
            printf("我是一个子进程,pid:%d,ppid: %d\n",getpid(),getppid());
            sleep(1);
        }
    }

    if(id>0)//运行的父进程
    {
        while(1)
        {
            printf("我是一个父进程,pid:%d,ppid: %d\n",getpid(),getppid());
            sleep(2);
        }
    }
    return 0;
}

代码运行后,在屏幕上打印了我们预想的两段话。

进程状态

宏观理解进程状态

  要想理解操作系统进程状态,可以先从操作系统的角度搭建一个宏观的进程状态的概念。
  进程状态可分为运行状态,就绪状态,阻塞状态,创建状态,结束状态。我们接下来具体分析一下运行状态和阻塞状态以及一个阻塞状态
  我们都知道一个计算机有很多的外设,如键盘,显示器,网卡,显卡,磁盘。。。。对于每一个外设操作系统都要进行管理,如何管理呢?先描述,再组织,也就是每一个外设都要有相应的结构体描述组织,在结构体中包含了外设的所有属性,例如键盘描述结构体:struct div_keyboard,显示器描述结构体:struct dev_display…也就是说这些外设资源操作系统也是需要对其进行管理的,操作系统会在进程需要时为其进行调度
  而对于进程,操作系统对其的管理方式是PCB,也就是说task_struct结构体描述组织,包含进程的所有属性。当进程加载到内存后,进程所对应的数据和代码就要被CPU处理,但同时计算机可能有很多进程都要等着被处理。进程的数量一定是多与CPU的数量的,不能同时处理,因此操作系统为了管理这些需要被执行的进程,在内核中维护了一个进程运行队列的结构体 struct runqueue。所谓的让一个进程在CPU 上运行本质是让这个进程入执行队列,也就是将该进程的PCB结构体对象load到运行队列中。让进程去排队并不是让该进程的代码和数据去排队,而是将进程对应的结构体对象PCB去等待。而对于计算机而言,如果进程被入到运行队列中,就意味着该进程就已经处于running运行状态。也就是在运行队列里的进程就都是运行状态,而不是该进程正在cpu上执行。
  进程状态这里所谓的状态,其实是进程内部的一个属性,也在PCB task_struct 上保存着,可以将进程状态理解成一个整数int,当它的值是1的时候就代表运行,不同的值就代表不同的运行状态。
  当一个进程要运行的时候,不仅仅只需要CPU的处理,还可能伴随着对外设资源的调度,比如要在屏幕上打印几个数字就需要屏幕外设的资源,进程需要网络就需要网卡资源调度。所以不能单单觉得cpu的资源很有限,外设的资源也很少,而当外设资源不足时,进程也需要进行等待,而此时等待的却是外设资源。
  前面叙述了cpu资源不够时,进程会在一个进程运行队列中等待,而对于外设资源不够时,进程仍然需要等待。每一个外设描述的结构体中,都有一个等待队列。当一个进程在CPU中被处理的时候,该进程要访问外设资源时,而此时外设的资源不够的时候,CPU就会将该进程出运行队列,CPU此时去处理其他在运行队列的进程,这样计算机的运行效率就不会因为等待外设资源而变低。而对于出运行队列的进程此时因为要等待外设资源就会链入到外设等待队列的结构体中。而这样的在等待外设资源的进程处于的状态就叫做阻塞状态。
  所谓进程不同的状态,本质上就是在不同的队列中,等待某种资源!!
  当进程在等待外设资源的时候,也就是处于阻塞状态的时候,进程不会被调度,因为外设是很慢的,所以进程对应的数据和代码会有相当长的一段时间在内存中加载着,占用着内存的空间。假设在某一段时间内,有很多进程都处于阻塞状态,并且阻塞进程的数量还在增加,那么这么多的进程所对应的代码和数据同时都要占用内存的空间,一段时间后内存空间不足,无法支持这么多的阻塞进程加载到内存。而此时操作系统是如何解决问题的呢?因为这些阻塞进程的数据短期内不会被执行,er内存空间不足,此时操作系统就会将阻塞进程的代码和数据暂时换回到磁盘上,这样相应的内存空间就被节省出来了,操作系统可以将这些节省出来的空间去处理其他的事情。而把这种因为内存空间不够,阻塞的进程对应的代码和数据加载或者保存到磁盘上的进程状态称之为挂起状态!

Linux下的进程状态

上文中我们提到的是宏观的进程状态的概念,是适用于任何操作的理论基础,而对于不同的操作系统,进程状态的分类和实现可能各有不同。接下来我们来具体了解一下Linux操作系统的进程状态。
首先来了解一下Linux操作系统下一共有多少个进程状态。在Linux内核中,进程状态在kernel源代码中的定义如下:

/
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运行状态(running):并不意味着进程一定在运行中,他表明进程要么是在运行中要么是在运行队列中

S睡眠状态(sleeping):意味着进程在等待事件的完成(这里的睡眠时间也叫做可中断睡眠)

D磁盘休眠状态(Disk sleep)有时候也叫做不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束

T停止状态(stopped):可以通过发送SIGSTOP 信号给进程来停止进程,这个被暂停的进程可以通过发送SIGCONT 信号让进程继续运行。

X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

Z僵尸状态(zombie):僵尸进程

R运行状态

如何查看进程的状态呢?运行以下的代码:

#include <stdio.h>
#include <unistd.h>
int main()
{
 
    while(1)
    {
    }
    return 0;
}

运行以上代码后,该进程将持续的进行while循环,也就是说将一直处于运行的状态
通过指令:ps axj | grep myprocess 可查看进程的状态
在这里插入图片描述

也就是我们上述查看进程pid的指令,列表中 R+ 即代表进程的状态为运行状态,而其中的加号(+)代表的是该进程是一个前台进程(前台进程与后台进程将在后续进行详解)。

S 睡眠状态

如若我们将以上的代码加入打印操作:

#include <stdio.h>
#include <unistd.h>
int main()
{
    int a = 0;
    a = 1+1;
 
    while(1)
    {
        printf(“a的值是:%d\n”,a);
        sleep(2);
    }
    return 0;
}

运行并通过指令查看后会出现一种令人反直觉的现象:该代码应该是持续循环在屏幕上打印a的值,也就是说该进程应该持续的在运行中,而查看进程状态后却显示的是S+(sleep 睡眠)状态
在这里插入图片描述
printf打印本身是要在显示器上打印,显示器是外设,进程访问外设非常慢,大部分时间都在等待进程就绪,我们所见的是进程在屏幕上疯狂打印数据,但其实在底层进程大部分时间(99%)都在等待IO就绪,只有极少的1%的时间在执行printf代码,所以我们查看的时候也就有极少的概率能够查看到进程是处于运行状态的。

T 暂停状态

运行以下代码

#include <stdio.h>
#include <unistd.h>
int main()
{
 
    while(1)
    {
    }
    return 0;
}

查看该进程是一直处于运行状态
在这里插入图片描述
通过信号 kill -19 14378 可以使进程暂停,此时查看进程状态
在这里插入图片描述
此时查看到的状态就是T 暂停状态
通过信号kill -18 14378 可以使进程继续运行
在这里插入图片描述
我们若运行以下的代码

#include <stdio.h>
#include <unistd.h>
int main()
{
    int a = 0;
    a = 1+1;
 
    while(1)
    {
        printf(“a的值是:%d\n”,a);
        sleep(2);
    }
    return 0;
}

在这里插入图片描述

通过上文的讲解该程序一定大部分时间都处于 S+ 状态,当我们给该进程传入信号 kill -19 20636
在这里插入图片描述
不出所料显示的是T 状态 ,当传入信号 kill -18 20636
在这里插入图片描述
显示的是S状态 , 没有了 加号(+),该状态没有加号代表它是一个后台进程,

D状态(深度睡眠)

前文论述的S状态是浅度睡眠的状态,这种睡眠状态是可以终止的。而深度睡眠是不可以被终止的,在一般的情况下是不会被使用的,一般是在企业高IO,高并发的环境中会有相应的运用。下面举一个例子来理解深度睡眠的运用场景。假如磁盘有一批用户数据要写入内存,假设此时进程A正在进行数据的写入,如果此时正在运行的进程有很多,内存的无法承担这么多进程的压力,那么操作系统就会采用挂起进程的方法来减小内存的压力,但假如此时操作系统即使是挂起进程也无法减轻内存的压力,那么此时操作系统就会主动的杀死进程,如果此时进程A被操作系统杀死,就会造成数据的丢失,严重可能会引起严重的损失。因此为了避免操作系统杀死某些重要的进程,Linux就设置了一种深度睡眠的状态,处于该状态下的进程无法被操作系统杀掉,只能通过断电或者是自己醒来,来解决!

僵尸进程(状态)

僵尸进程是一种比较特殊的进程状态。当子进程退出而父进程还在运行,且没有回收子进程的退出信息的时候就会产生僵尸进程的状态。因为当子进程退出的时候,虽然它的代码和数据会被操作系统回收,但是子进程的PCB并没有被回收,仍然占用着资源,这样就会导致计算机的资源越来越少,也就造成了内存泄漏。我们通过以下的代码去查看僵尸进程

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        printf("我是子进程,pid:%d ,ppid:%d  \n",getpid(),getppid());
        sleep(5);
        exit(1);//五秒后子进程主动退出,父进程没有回收子进程,将会变成僵尸进程
    }
    if(id > 0)
    {
        while(1)
        {
            printf("我是父进程,pid:%d,ppid:%d \n",getpid(),getppid());
            sleep(1);
        }
     }
   return 0;
}

通过监控脚本查询进程的状态:
while :; do ps axj | head -1 && ps axj | grep mycmd | grep -v grep; sleep 1; done
查看情况如下:
在这里插入图片描述
我们发现了子进程在五秒后 由S+ 变成了 Z+ , 也就是说子进程已经变成了僵尸进程。

僵尸进程的危害:
1、进程的退出状态必须被维持下去,因为他要告诉关心它的进程也就是父进程,交给子进程的任务处理的怎么样了。如果父进程一种不读取,那子进程就会一直处于Z状态。

2、 维护退出状态本身就是要用数据去维护,也属于进程的基本信息,所以保存至task-struct 中,换句话说,Z状态一直不退出,PCB一直都要去维护。

3、如果一个父进程创建了很多子进程,而且不进行回收,因为数据结构本身就会占用内存,创建子进程是需要在内存的某个位置开辟空间的,因此子进程退出而父进程不进行回收就会造成内存泄露。

如何去避免僵尸进程,在后续的进程控制阶段就有方式去解决!

孤儿进程

父进程如果提前退出,子进程后退出,这样父进程会不会变成僵尸进程呢?答案是不会,因为父进程有自己的父进程也就是bash回收了父进程的退出信息。而此时的子进程就变成了孤儿进程,查看它的ppid是 1号进程 。当子进程变成孤儿进程时,操作系统就会领养孤儿进程,这样就可以防止子进程变成僵尸进程。
也就是说被操作系统领养的进程就是孤儿进程 。

进程优先级

我们曾经了解过权限的相关知识,而如今我们要聊的进程优先级它们两个是同一回事吗?当然不是,权限解决的是能不能的问题,优先级是已经获得了权限基础上操作系统会不会优先给某一进程分配资源。
进程优先级的机制是因为计算机能够分配的资源太少了,需要访问资源的进程太多了,这时候就需要有优先级来决定哪一个进程先被分配资源。

Linux下的优先级

LInux下的进程优先级本质就是pcb里面的一个整数数字(也可能是几个),通过整数来确定优先级。Linux系统用的是 PRI(priority) 和 NI(nice)来决定进程的优先级。
LInux最终的优先级 = 旧的优先级 + nice
Linux 是支持进程在运行的过程中,进行优先级的调整的,调整的策略就是更改 nice 值来调整进程的优先级。在不进行更改的情况下,nice值一般就是0.
运行以下的简单程序:

#include <stdio.h>
#include <unistd.h>
int main()
{
    while(1)
    {
        printf("pid: %d\n",getpid());
        sleep(1);
    }
    return 0;
}

查看进程的优先级可以通过以下的指令:
ps -la
在这里插入图片描述
进程一般默认的优先级是 PRI :80 , NI : 0 。
修改优先级
首先使用top命令 —> 进入top后按 “r” ----> 输入进程PID ----> 输入nice值。

其他概念

1、 竞争性: 系统进程数目众多,而CPU资源只有少量的,甚至是1个,所以进程之间是具有竞争属性的。为了高效的完成任务,更合理的竞争相关的资源,便具有了优先级

2、 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰

3、并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行。

4、并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间内,让多个进程都得以推进,称之为并发。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值