一、冯诺依曼体系结构
我们常见的计算机如:笔记本,不常见的计算机如:服务器,大部分都遵循冯诺依曼体系结构
目前我们熟知的计算机都是由一个一个的硬件组件组成的
如下:
输入单元:包括键盘、鼠标、扫描仪、写板等等
中央处理器(CPU):含有运算器和控制器
输出单元:显示器打印机等
冯诺依曼体系结构的特点就是:
从输入到输出,数据都必须经过存储器(存储器指的是内存)
那么为啥要设计内存这个东西呢?
首先我们应该都有听过一个木桶效应,那么如果是我们的外设和CPU之间直接进行连接,那么整个计算的过程的速度就是由最慢的那个设备决定了,然后我们的外设可能是以ms为单位的时间,而我们的CPU是以us为单位的,这两个时间上就差了很大一个量级了。所以在CPU和我们的外设之间又引入了内存。
但是又有个疑问了:
就是引入了内存,但是其最慢的设备不还是外设嘛,那么按照木桶效应来说,那么速度不还是由外设决定嘛。
所以还有操作系统这个角色,其对于外设的数据,会局部性加载,就是比如我们要查看一个文件的10MB的内容,但是实际上我们操作系统在管理的时候,会加载100MB的内容到我们的内存中,这是因为局部性原理,我们查看完这10MB的内容后,大概率是会去查看另外的90MB的内容。
所以,后续对于计算机的运行速度,是由内存决定的。
总结如下:
不考虑缓存,这里的CPU能且只对内存进行读写,不能访问外设(输入或输出设备)
外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取
所以所有设备只能和内存打交道。
所以也就理解了前面提到的,所有的程序都要先加载到内存中,才能被CPU获取到。
还有一个问题就是,那么我们直接将寄存器进行布局,布局多一点,也可以达到很大的存储空间, 那么为啥不这样做呢?
这是因为我们的计算机是给人使用的,从商业角度上,价格不能太过于昂贵,这样才能被更多的人使用,才能构建出我们现在这么庞大的互联网,所以冯诺依曼为我们的计算机提高了性价比,而且对于计算机的运行速度也没有造成太大的影响。
首先我们先来固定一个说法,我们平时一直说的输入输出,到底是从谁的角度来看的,因为对于数据的输入来说,从键盘角度,我们又是输出,对于内存来说是输入,对于显示器来说获取到数据是输入,将数据打印在屏幕上是输出 。
在此我们统一口径:是站在内存的角度。或者是站在加载到内存的程序的角度。
所以我们在C语言和C++代码程序中,都会先包含stdio.h和iosteam头文件。
我们从外设将数据加载到内存,是一个拷贝的过程,从内存加载到CPU也是一个拷贝的过程,CPU将计算好的数据传递给内存也是一个拷贝的过程,从内存将数据输出给外设也是一个拷贝的过程。所以计算机的效率问题是由设备的拷贝效率决定的。
下面我们通过一个qq给某个人发送消息来理解整个数据流动的过程:
首先qq之间进行信息的传递,实际上就是两个主机之间进行通信,那么就是两个冯诺依曼体系的机器进行通信。
从发送方:其从键盘输入信息,然后拷贝到内存,然后CPU获取到内存中的数据进行加密处理,然后又拷贝到内存,然后就其输出到我们的输出设备网卡和显示器,然后通过网络传输到接收方的主机,然后接收方的输入设备网卡获取到数据,将其输入给内存,然后给CPU进行解密计算,然后又通过内存输出给显示器。
二、操作系统
1、概念
任何计算机都包含一个基本的程序集合,其称为操作系统。
操作系统包括:
内核(进程管理、内存管理、文件管理、驱动管理)
其他程序:函数库,shell程序等
简单来说操作系统是一个进行管理计算机软件硬件资源的,比如管理底层的驱动程序的硬件设备。
那么为啥要操作系统来进行管理呢?我们要操作那个硬件,其无法自己完成么?
这是因为大部分硬件我们用户是无法直接去进行操作的,所以通过操作系统来帮助我们对硬件进行使用。
操作系统可以给我们提供一个更加接近人类的操作逻辑,使我们使用计算机的时候,得到一个稳定
可靠、方便、安全的操作环境。
2、进一步理解操作系统
操作系统就好比我们学校的辅导员这样,我们的学校就是我们的硬件,那么学生就是用户,那么我们要使用学校的各个服务,那么就需要通过辅导员呀,宿管阿姨的帮助呀.
还有,好比如银行,其有前台、窗口、钱库等硬件。
然后还有大堂经理、业务员等
那么我们第一次去,存款,那么我们对于这个硬件的操作肯定不熟悉,那么就需要工作人员的帮忙。
然后我们取钱,不可能直接进入到钱库拿吧,也需要经过业务员的操作。
所以我们的操作系统其给了我们用户一个"系统调用接口",允许我们通过接口间接和计算机的软件硬件交互。
3、系统调用
操作系统对外会提供一些接口,给上层开发进行使用,这部分由操作系统提供的接口就称为系统调用
然后操作系统提供的系统调用接口,其对于用户的要求其实挺高的,所以后续有的开发者,对于部分系统调用的接口进行了封装,然后这些被封装的系统调用形成一个一个的库。
那么我们的用户要如何去使用系统调用呢?
我们日常使用电脑的时候,其实并没有感觉到这些,我们只需要在鼠标键盘上操作即可。
我们这个操作就是对操作系统发起操作,然后操作系统根据我们的操作,然后去进行系统调用的转换。
三、进程理解
课本的概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU实体,内存的实体)
下面我们来进行讲解:
我们先来了解一下操作系统是如何进行管理的:
还是前面下学校例子:
我们很多同学见过校长,可能就是新生开学典礼和毕业典礼上会见到,所以其对于学生的管理如下:
首先找到对应的部门,然后由这个部分作为执行者去找学生,然后完成对应的工作。
就比如校长需要开除这个学年绩点不及格的最后五个学生,那么不需要去当面找学生,只需要在学生的管理系统中找绩点最低的五个学生,然后和辅导员说,让辅导员去执行这个开除的操作即可。
所以其角色如下:
操作系统->校长
底层驱动->辅导员
底层硬件->学生
操作系统管理的不是每个硬件的具体数据,而是其属性。
就比如我们的校长,其管理的是我们在学校是各种属性,成绩呀,奖学金评选呀,保研等。
我们的操作系统是可以同时运行很多程序的,那么每一个程序都需要加载到内存中吧,那么我们的内存中就一定会存在很多的进程,那么这些进程也需要被操作系统进行管理。
就好比我们的学校,我们要对学生进行管理,那么我们可以将学生的一些数据使用一个学生管理系统进行记录保存管理,那么我们要先对学生这个进行描述,然后将学生的信息进行组织。
所以我们的操作系统对于进程,先描述一个进程
在每个执行任务被加载到内存中的时候,操作系统在内存中会申请一个空间,这个空间就是操作系统中用来描述该进程的结构体。
所以未来我们要是有一百个进程,那么在我们的内存中就会有一百个结构体对象描述我们的进程的。
所以对于磁盘中的文件,其不是进程,对于加载到内存中的程序,其也不是进程。
进程=操作系统内核结构体对象+进程的代码和数据
描述进程:PCB
PCB是在任何操作系统中都使用的叫法。
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合
在Linux操作系统下PCB叫:task_struct。
这个结构体中包含了进程的所有属性,进程的所有属性都可以通过这个结构体直接或者间接的获取到。
我们在Windows中双击一个应用程序,在Linux中输入指令,在手机上点击APP,其本质都是启动一个进程。
所以总的来说进程就是:
进程=内核数据结构PCB+自己的程序的代码和数据
所以操作系统对于进程的管理就变成了对于PCB的管理。
PCB中一般就存储以下的信息:
标识符pid:描述本进程的唯一标识符,用来区别其他进程。
就好比我们在学校中,每一个学生都会有学号,在社会上我们都有身份证号,这都是在不同场景下唯一标识我们身份的东西。
状态:任务状态、退出代码、退出信号等等
在我们的PCB中也会有标识其状态的标识符,这个我们后续会进行详细讲解
优先级:相对于其他进程的优先级
相当于我们生活中的排队,谁在前那么谁就先拥有这个资源,那么我们的进程就是谁的优先级高,那么谁就可以先得到CPU的资源。
程序计数器:程序中即将被执行的下一条指令的地址
在我们的CPU中会存在很多的寄存器,这些寄存器中会存放着进程执行过程中的各自临时数据,这是因为我们的代码不是一次就执行完的。所以在我们的CPU中,还有一个寄存器EIP,其就是告诉我们的CPU下一次要执行的指令。
内存指针:包括程序代码和进程相关数据的指针,还有其他进程共享的内存块指针
上下文数据:进程执行的时候处理器寄存器中的数据
一个进程并不是执行完毕,才会让出CPU资源的,当代计算机都会给每一个进程分配时间片,当时间片执行完毕,那么就会自动出让CPU,让别的进程使用。
所以就会有一个进程并没有执行完毕,然后CPU就被别的进程使用了,所以这样就会产生进程切换和进程调度的工作。
这是基于时间片的轮转调度,后面我们在对于信号的讲解中会进行详细讲解,这里我们就做一个了解即可。
我们的CPU中,寄存器硬件中有一套,但是其可以在不同的时间段保存不同进程的数据,所以实质上,上下文数据是属于进程的数据。是属于进程私有的!!!!
I/O状态信息:包括显示的I/O请求,分配给进程I/O设备和被进程使用的文件列表
记账信息:可能包括处理器时间总和,使用的时钟总数和、时间限制、记帐号等
四、查看进程
首先我们要注意的是,我们要查看一个进程,那么我们要先保证其在我们的内存中运行才行。
指令:
ps命令
ps查看进程是属于静态查看,其有如下选项:
a :显示所有用户的进程
u:显示用户信息
x:显示没有控制终端的进程
-e:显示所有进程
-f:显示完整的进程的信息
top命令
按q退出
P表示按照CPU的使用率进行排序
M表示按照内存的使用率排序
基本使用如下:
ps -f |head -1;ps aux| grew -w "具体的程序的名字"
ps -p PID
1、标识符
标识符就类似我们的身份证,学号等,可以表示我们的身份的东西,其表示了每一个进程,在task_struct结构体中存放了两个成员变量:PID和PPID
PID:进程的唯一标识
PPID: 进程的父进程的PID
那么我们要在这个进程中获取自己的pid、其实就是获取进程自己的task_tstruct结构体中的属性值,但是我们是无法自己获取到这个值的,所以操作系统必须给我们提供一个系统来给我们获取pid。
我们可以通过头文件<sys/types.h>中的两个库函数来查看父子进程的PID
getpid();
getppid();
如下:
如果我们将其打开,然后终止,在打开,然后再运行,我们会发现我们的pid的数字是一直增大的,其就是线性增大的,到一个阈值后,会回旋。
再Linux系统中,新的进程往往是通过父进程创建的。
可以看到其是线性增大的。
我们再看看其父进程:
我们可以发现,我们的父进程的pid是没有发生变化的,那么这就说明了我们的父进程是没有发生变化的。那么我们可以通过ps来查看父进程到底是谁
在我们的Linux系统中,从你开机开始,就会存在一个进程,bash,其就是我们的命令行解释器,所以我们在Linux中执行的每一个指令还有自己写的程序,都是通过其进行创建的子进程进行执行的。
2、查看进程的信息
进程的信息我们可以通过/proc来查看。
proc其就是我们Linux中的一个磁盘文件,其会保存着我们当前的进程的信息。
可以看到上面的数字,就是我们当前机器所有的进程。那么我们可以通过/proc/进程pid查看到进程的详细信息。
可以看到上面的话,有一个信息cwd,其就是我们的当前进程的工作路径。
所以这也是为啥我们之前使用fopen函数对一个文件进行打开创建,如果我们不去指定路径,那么其就会在当前路径下进行创建。
所以我们对于当前路径要有一个新的认识:当前路径就是当前进程的工作路径。
3、fork函数
前面我们知道了如何进行查看当前进程的pid和父进行的pid,那么我们是否可以通过代码的方式创建进程呢?
创建进程的本质就是OS中多了一个子进程,那么就多了一个task_struct结构体和进程的代码和数据。
所以我们的操作系统需要给我们提供系统调用来创建系统调用。
fork函数是一个系统调用,其作用是在当前的进程下创建一个子进程,其需要包含头文件<unistd.h>
这个函数的返回值为一个pid_t变量,pid_t实际上是一个整型变量,如果pid_t==0,那么就说明进程创建成功
下面我们来看看fork函数是如何进行进程的创建的。
其是由当前正在运行的进程调用,这个正在运行的进程就是后面创建的进程的父进程。
然后子进程PCB结构成员的参数基本都是按照父进程,然后生成唯一PID的标识符。
然后子进程和父进程共享代码,数据则是由子进程在访问的时候直接可以访问父进程,然后子进程要对数据进行修改的话,那么其会在内存中申请一块和父进程一样的空间,然后对其数据进行修改。
可以看到我们使用了fork函数后,确实是出现了两个pid。
但是我们疑惑的是,fork会有两个返回值,然后呢给子进程返回的是0,给父进程返回的是子进程的pid。然后一个函数怎么可以返回两次呢?一个变量这么会接收两次返回值呢?
下面我们一一进行解答:
1、为啥给子进程返回的是0,给父进程返回的是子进程的pid?
首先我们知道在日常使用中父进程和子进程的比例是1:n的,那么我们的父进程也是需要对其进行管理的,所以创建好一个子进程要将其的信息给我们的父进程一份。
2、fork函数,其为啥能够返回两次?
首先我们思考一个问题:我们的函数在要进行return的时候,其工作是否已经完成了?
答案是完成了,就和下班一样,完成了工作才能下班,那么也就是说在返回之前我们的子进程就已经创建好了,那么我们的子进程此时也会运行在fork函数,那么子进程也就也执行了这个return。
所以子进程执行返回一次,父进行执行一次。
3、为啥一个id变量可以接收两个返回值?
首先我们知道,在创建子进程,那么就要在内存为其创建一个task_struct结构体,然后子进程其不是在我们的磁盘中存在的,那么其代码和数据是靠父进程给的,所以其会先和父进程共享一个代码和数据,但是此时实际上子进程在内存中还是没有单独的内存空间的。其实在进程和物理空间之间,还会有一个虚拟地址空间,对于虚拟地址空间我们后续会进行详细讲解。所以我们的子进程创建出来后,其是先有一个虚拟地址空间,只有当父子进程中,有一个进程要对数据和代码进行修改,那么就会触发写时拷贝,然后其在物理内存上就是独立的了,所以父子进程再id上实际上是不同的两个内存空间。