在介绍进程之前,我们需要先了解关于进程的一些边缘的操作系统知识
以及硬件知识,因为操作系统不仅对进程有着管理,也同时管理着
各种软硬件,所以我们先从计算机组成来入手
我们都知道当今世界的计算机基本上都采用冯诺依曼结构体系,那么为什么要选择使用冯诺依曼结构体系而不使用其他体系,它的好处是什么呢?
我们可以看到冯诺依曼结构体系将一个计算机组成分成了大致五个部分
输入设备:诸如键盘、话筒、摄像头、网卡、磁盘等等。
存储器:就是我们所说的内存,他与磁盘不是一个东西
输出设备:显示器、喇叭、网卡、磁盘等等
cpu:cpu中的运算器和控制器两部分。里面有着寄存器以及各种级别的缓存。
这是一张存储分级的示意图,我们可以凭经验发现,越往金字塔顶端的硬件,他的内容的IO效率肯定是越高,而且也意味着造价很高。这其中的S2、S3可以理解为内存,那么同等大小的内存肯定是要比磁盘昂贵的。
所以我们就可以知道cpu的运算不会直接与外设直接进行交互,它是通过各个界别的存储介质来一级一级的运到cpu上,这样可以保证cpu的效率,防止在大量的IO读取上浪费时间。在数据层面上,cpu会优先与内存进行交互。
这时候可定有人会想,那我们何必要磁盘呢?把存储介质全稿成内存,那岂不是计算机的运行效率更高了,这样的现象当然会使计算机的运行效率更加的高,但是这样的计算机普通人是绝对用不起的。所以基于冯诺依曼体系结构的计算机它的本质是用比较少的钱造出一款效率不错的计算机,让普通人也能够使用到计算机。
我们目前知道程序=代码+数据,且程序在运行之前都要将程序加载到内存里面,但是程序是什么?我们在linux里可以看到可执行程序他就是一个二进制文件,在Windows下他也只是一个后缀为.exe的二进制文件,既然是文件,那他必然是被存储在磁盘(外设)中,所以当运行一个程序时,需要将程序加载到内存中,cpu不会直接与外设交互。这些现象都是由体系结构决定的。
现在我们来认识一下操作系统。
首先操作系统他也是一款管理计算机软硬件的软件,他是计算机开机时第一个启动的程序。
操作系统管理软硬件,目的是为了给用户提供良好的使用环境。
那么操作系统是如何管理这些软硬件的呢?
让我们先来举一个例子:
假如你是一个校长,你的学校里只有几个或者十几个学生,你会很好管理,你们每天抬头不见低头见,你能够明确知道每个学生的状态,你也了解每个学生的信息,有个什么活动很简单就能把学生管理起来。但是如果学生有几百个呢?这时候你就无法清楚每个学生的状态和信息了,并且有活动的时候,你管理学生就会很费劲,那如果学生是几万人呢?所以这时候就有一个身份:辅导员和班长等等。她负责执行校长的决定,来管理学生,或者辅导员再让班长来执行更进一步的决定,来管理班级。并且对于每个学生他们的信息都会有一个专门的excel表格来存储这些学生的信息,诸如:姓名,学号,紧急联系人电话等等。从此以后,校长就不需要见到你这个人从而对你进行进一步的管理,校长只需要对excel上的数据进行了解/修改,从而达到对某一位学生的管理即可。在这里面,校长充当了一个决策者的身份,而学生充当了一个被管理者,辅导员则是一个执行者。校长管理学生不需要见到这个学生,只需要管理这个学生的数据。所以管理学生的本质不是管理人,而是管理学生的数据。
那我们作为一个程序员,使用面向对象的思想,将学生用一个结构体将他描述起来,随后存到数据库中,使用学生管理系统就可以更管理好学生,不需要直接管理这个学生。
我们可能会在这个结构体里有学生的姓名、年龄、学号等等数据,这是一个对学生进行描述的过程,然后我们再用数据结构诸如链表,将每个学生链接起来,从而达到组织的目的,当有一个学生加入到我们学校,我们只需要把这个学生描述好,再将这个对象加入到我们的链表中。所以,从此往后我们对学生的管理,就转化成了对这个链表的增删查改。
那我们可以得出:对大量对象的管理我们可以像上述那样,先把这个对象描述好,形成一个结构体,再把这些结构体用数据结构组织起来。就可以实现对对象的管理。所以我们得到了一个观点:先描述,再组织。任何的管理工作就可以使用这样的方法进行计算机建模。
而操作系统的管理主要核心分为四种管理:进程管理、驱动管理、文件IO管理、内存管理。
而计算机中有着大量的软硬件,那么操作系统也是使用上面的先描述(结构体)再组织(数据结构),将进程、驱动等等管理起来。
上述我们把操作系统管理软硬件的方法讲述好,但是操作系统的目的是为了给用户良好的使用环境,那么它又是如何给用户良好的使用环境的呢?这里我们的用户暂且从开发者的角度来看他。
我们作为一个开发者,当我们想知道计算机中的内存使用多少了,文件打开了多少个,我们可以直接访问对内存文件管理的结构体中的成员来获取,这种方式在技术层面上是可行的,但是这种方式在安全层面来说是不可行的,因为不乏有些开发人员他是专门窃取/修改操作系统的被管理对象的数据,从而对操作系统产生不好的影响。操作系统又无法分辨用户是哪种人,所以操作系统选择不相信任何用户。那既然操作系统不相信任何用户,但是他还得给用户提供服务,那他又是怎么做到的呢?
这里我们再再举个例子:
在现实生活中,不相信人们,又要给人们提供服务的地方很多,比如银行。
在银行中有着各种办公设备,办公场地,休息的地方等等,而这些东西和地方又有专门的人来维护和各自的管理人员。在此基础上又会滋生出很多的服务,比如存取钱,贷款等等,而存取钱,贷款,是有人来存取钱,存取钱也不会直接对钱进行操作(进入金库),而是先在银行的的电脑的对业务系统中进行数据的操作,对数据进行管理,从而达到业务的办理。在这个业务的办理过程中,会让办理业务的人(存钱的人)直接在电脑上进行数据的修改吗?显然不是的,这个过程有工作人员来进行操作,银行不相信你,假如你要存钱,但是你只把存钱的数据修改了,没存进钱怎么办?所以你会发现,银行有着厚厚的一层防护的玻璃防止办理业务的人直接访问到金库,或者是电脑中的业务办理中的数据。但是他们又开着小孔依然给你提供这些服务。那么类比一下,银行中的各种办公设备、场地休息场所就是计算机中的硬件,对这些进行维护管理的人员就是这些硬件的驱动。而由银行业务的出现导致出现需要对各种办公用品、场地、设施以及业务办理时对各种数据的管理就是类似于操作系统的四种核心管理,那么厚厚的墙就是用户和操作系统之间出现的新的东西,叫做系统调用接口,也可以理解为系统调用函数。所以当我们使用操作系统时,是不会直接与操作系统的软硬件管理的结构体直接打交道,而是写操作系统人们会写一批系统调用接口来供人们使用,这样就防止了坏人肆意的修改结构体中的东西。而用户在使用计算机的时候,也不会是直接使用系统调用,因为系统调用的使用还是有些麻烦。所以在系统调用的上层还有着shell外壳(图形化界面),各种库,Linux中的各种指令等等,这些一起实现了用户的良好的使用环境。比如我们使用的printf函数,它会将内容输出到显示器上,那么它使用了硬件,那他也肯定在函数内部封装着系统调用,它属于C语言的标准库。shell外壳、图形化界面,也是对系统调用接口的封装方便普通人使用,指令比如Linux中的ls。所以很多功能,不需要开发者自己再去写了,只需要使用库函数就可以。提高了开发者的开发效率,降低开发成本。
在以前我们可能接触过进程这个名词,也大概了解进程好像是什么,有人说进程是运行起来的程序,有人说进程是加载到内存里的程序,也有人说进程是动态的,程序是静态的…所以现在我们来了解什么是进程。
当我们启动电脑后,内存中一定会被加载许多程序,而这些程序又会形成进程,那这些进程需不需要管理?答案一定是需要的,那么如何管理?答案就是先描述,再组织怎么描述?操作系统主要是C语言写的,那肯定是用C语言描述啊,所以对于将一个进程描述成一个结构体,这个结构里面包含了对于进程的描述。那么对于这么一个结构体,操作系统中有一个名词叫做PCB(process control block)。进程控制块。那么这个结构体里大概会有些什么呢?他会有:id,状态,优先级,代码地址&&数据地址…。最后将这些进程用数据结构组织起来。
struct PCB{
//id
//状态
//优先级
//代码地址
//代码数据地址
//......
};
所以进程严格来说进程是:进程 = 代码 + 进程结构体(pcb)。
而Linux操作系统具体的做法是:如果要运行一个可执行程序,就会先将他加载到内存中,同时操作系统会形成对应程序的pcb,然后将pcb链入到像链表一样的数据结构中,等轮到它运行的时候再将它调度到运行队列中,Linux中的进程结构体叫做task_struct。所以现在对进程的管理,就变成了对进程链表的增删查改。
而且在Linux系统中的进程他是存储在双链表中,并且他不只存储在一张链表中。对于一张普通链表节点:
struct listnode{
DateType val;
//各种属性...
listnode* prev;
listnode* next;
};
Linux中进程链表节点:
struct task_struct{
//进程的各种属性...
struct dlist list;(有多个)
//进程的各种属性....
};
//其中:
struct dilst{
listnode* prev;
listnode* next;
};
所以它可以同时在多个数据结构中,并且他的位置我们可以发现与普通链表有些不同。
它指向的是下一个节点的对应的list字段,那么我们就会有一个疑问,他该怎么访问结构体的数据呢?
其实我们这里就用到一个知识点,有一个函数offsetof(),他是一个计算一个结构体成员偏移量的函数。它的原理也很简单,应用到进程这里就大致是:
#define cur(list) (task_struct*)((int)(&list)- (int)(&((task_struct*)0)->list))
使用起来就是cur(list)->pid(成员)。
他就是将0强转成进程节点类型的指针然后访问list,取地址就可以得到相对于0地址的list的偏移量,再用一个进程结构体list的地址减去这个偏移量,那么就会得到指向这个结构体的地址了。
所以我们现在要有一个观点:我们运行的所有指令,软件,自己写的程序,最终都是进程。
今天我们认识了进程是什么,之后我们在谈论,进程中的成员。