目录
冯·诺依曼体系结构
目录
冯·诺依曼体系结构,也称为普林斯顿体系结构,是计算机体系结构的一种。是由冯·诺依曼在1945年提出的,是一种基于存储程序的计算机体系结构。该体系结构包含了四个主要组件:
中央处理器(CPU):负责执行指令、控制计算机的操作;
存储器(Memory):存储器用于存储指令和数据;
输入设备:包括键盘, 鼠标,扫描仪, 写板等用于向计算机输入数据。
输出设备:显示器,打印机等用于从计算机输出结果。
冯·诺依曼体系结构的核心思想是将程序和数据存储在同一种类型的存储器中,并且用同样的方式进行访问,这种存储器被称为随机存取存储器(RAM)。因此,冯·诺依曼体系结构被认为是现代计算机体系结构的基础。
关于冯·诺依曼,必须知道的几点:
这里的存储器指的是内存。
不考虑缓存情况,CPU只能对内存进行读写,不能访问外设(输入或输出设备) 外设(输入或输出设备)。
要输入或者输出数据,也只能写入内存或者从内存中读取。
就是所有设备都只能直接和内存打交道而cpu只能和内存打交道。
那么为什么cpu不能直接从硬盘读取数据,而要先经过内存呢?主要有以下几点:
1、CPU靠指令集工作,随着CPU的主频越来越高,处理速度越来越快,CPU的处理能力和信息吞吐能力远大于硬盘。如果cpu直接和硬盘直接进行数据交换性能和数据丢失。
2、硬盘只是一个存储器,计算结果和运行速度最重要,只要在硬盘中读取足够的信息就开始计算了,这样的机器硬盘不如内存重要。
3、内存比硬盘数据吞吐量大,速度快。在加载系统后,主要使用的数据都已经加载进了内存中。这样可以加快系统的速度,CPU是火箭的话,缓存就像飞机,内存是火车,硬盘像轮船。
4、CPU对数据会有一个预判(局部性原理),这个预判是和程序有关的,系统会将可能要使用的数据提前缓存到内存中从而提高性能。
当然对冯·诺依曼的理解,不能只停留在概念上,要深入到对软件数据流理解上,例如:
我们在使用聊天软件时对方是如何到我们要表达的意思?也就是两冯诺依曼体系的计算机如何进行数据交换的?
我们通过键盘输入信息,这个信息会被加载到内存中,内存又将信息传给cpu处理,在处理完后交给输出设备网卡,经过网络层到达对方的网卡(既是输入设备也是输出设备)加载到内存交给cpu处理后显示在对方的显示器上,完成数据传递。
操作系统:
操作系统是一种系统软件,是计算机系统中控制和协调计算机硬件资源和应用程序运行的核心程序。操作系统提供了一个桥梁,使得计算机硬件与应用程序之间可以进行有效的交互和通信,同时也对计算机硬件进行管理和优化,包括内存管理、进程管理、文件管理、设备管理等。常见的操作系统包括Windows、MacOS、Linux等。
那么操作系统干什么用的呢?
我们常说苹果手机,安卓手机,苹果笔记本和其他笔记本,这些电子产品都是由电子元器件组成的,为什么我们在购买时还要进行对比?其实我们在购买电子产品时我们购买的不仅是硬件,还有操作系统,试着想一想,如果没有操作系统我们购买的产品不能运行,不能操作,那么它和一块砖头有什么区别?而有了操作系统我们可以通过键盘、鼠标进行输入操作,可以通过显示器显示输出结果,这是操作系统通过驱动对电脑的硬件进行管理,我们才可以使用硬件。
操作系统是如何进行管理的:
可以用一个图书馆管理员来比喻操作系统进行管理的过程。如下:
1. 内存管理:图书馆管理员管理图书馆的书籍,保证书籍摆放整齐,存放在合适的位置上,方便读者借阅。类似地,操作系统管理计算机内存,确保所有程序都能够高效地运行,避免内存的浪费和泄漏。
2. 进程管理:图书馆管理员管理借书还书的流程,确保读者不会同时借阅过多的书籍,避免混乱和冲突。类似地,操作系统管理计算机上正在运行的各个进程,确保这些进程不会出现冲突和崩溃,同时保证每个进程能够获得足够的计算机资源。
3. 文件管理:图书馆管理员管理图书馆的书籍,确保每本书都被合理地存放,并且方便读者查找借阅。类似地,操作系统管理计算机中存储的文件,确保这些文件被正确地组织和存放,方便用户管理和使用。
4. 设备管理:图书馆管理员管理图书馆的设备,如借书机、还书机、安保设备等,确保这些设备运行正常。类似地,操作系统管理计算机的各种设备,确保这些设备正常运行,并且满足用户的需求。
5. 用户管理:图书馆管理员管理图书馆的用户,确保读者有资格借阅书籍,并且能够按时归还。类似地,操作系统管理计算机上的用户账户和权限,确保用户能够合法地访问计算机资源,并且不会对计算机系统造成损害。
系统调用(syste call)
系统调用是操作系统提供给应用程序调用的一组接口,可以让应用程序请求操作系统提供的服务,例如文件操作、网络通信、进程管理等。系统调用通常由应用程序通过编程语言中提供的系统调用接口(API)来调用。当应用程序调用系统调用时,会将控制权转移给操作系统的内核,在内核中执相关操作,然后再将控制权返回给应用程序。系统调用是操作系统和应用程序之间通信的重要手段。
假设你正在编写一段程序,需要从键盘上获取用户输入并将其存入文件中。在一般的编程语言中,你可能会使用类似于 "scanf()" 或 "gets()" 等函数从键盘上获取输入,然后再使用 "fopen()","fputc()"等函数将输入文本写入文件中。而在操作系统中,这些函数需要通过系统调用来完成。而系统调用是操作系统提供给应用程序的一组接口,用于访问操作系统内核提供的服务和资源。在上述的场景中,应用程序通过调用系统调用来请求操作系统打开文件、读写文件、以及读取键盘输入等等操作。操作系统在接收到系统调用后,会执行相应的内核代码来完成这些操作,然后将结果返回给应用程序。这个过程中涉及到用户模式和内核模式的切换,以及堆栈切换等操作。因此,系统调用是操作系统提供的一种重要的接口,它使得应用程序可以访问操作系统提供的功能和资源。
系统调用和库函数
1.系统调用是由操作系统提供的接口函数,是最底层的调用,用于访问底层硬件资源 ,面向硬件的,例如:文件,网络进程等。系统调用在使用上,功能比较基础,对用户要求比较高,有的开发者对部分系统调用进行封装,从而形成有库,有了库利于更上层用户进行二次开发。
2.库函数是过程调用,调用开销小;系统调用需要在用户空间和内核空间之间切换、调用开销大。
3.各个操作系统的系统调用是不同的,因此系统调用一般没有跨操作系统的可移植性,而库函数的可移植性好(C语言函数库在Windows和Linux环境下都能操作);
进程:
课本概念:程序的一个执行实例,正在执行的程序等。
内核观点:担当分配系统资源(CPU时间,内存)的实体。、
引入
我们编写的程序在经过编译链接接处理后会生二进制机器可执行程序(以.exe结尾的文件)。当程序要运行时,可执行程序从硬盘(外存)加载到到内存上,这个过程就是将二进制可执行文件复制到内存中,此时操作系统会创建一个结构体对象task_struct,用来描述内存中可执行程序的各种信息。task_struct是Linu内核的一种数据结构,它会被载到RAM中,并包含着进程的各种信息当可执行程序加载到内存中,建立了对应的结构体对象,并且放进进程PCB进程控制块中后,我们就说这是一个进程。
所以在到这里我们对进程有了一个更新的认知,即进程=内核数据结构+对应的可执行程序代码。当有多份可执程序从磁盘加到成内存,操作系统会根据可执行程序的个数创建对应的结构体对象个数,然后操作系统会根据结构体对象中某个Key值将,结构体对象选出来让CPU执行,CPU会根据结构体对象中的内容,找到相应的可执行程序进行执行。
所以我们可以知道:操作系统用一个结构体对象去表示这个可执行程序,通过管理这个结构体对象,在组织这个可执行文件, 而管理的本质就是先描述再组织。
进程控制块
进程 PCB 是进程控制块(Process Control Block)的缩写,是一种操作系统的数据结构,用于存储和管理进程的各种信息,是内核中用于表示进程和线程的结构体,定义在 <linux/sched.h>头文件中。 task_struct 中的重要信息:
- 进程描述符(pid):每个进程/线程在内核中都有一个独特的编号。这个编号就是 pid。
- 进程状态(state):进程/线程可以有不同的状态,比如运行、睡眠、停止等等。
- 进程优先级 (prio):是内核中用于调度进程的指标。
- 进程调度策略 (policy):内核中有多个进程调度策略可供选择,如 CFS、RT 等等。
- 进程上下文(context):表示当前进程的 CPU 寄存器、栈、指令计数器以及其他进程指令执行的状态。
- 进程资源使用情况(resources):用于跟踪进程使用的各种系统资源,例如内存、文件描述符等等。
- 进程间通信 (IPC):用于描述进程之间通信的相关信息。
- 内核态栈 (kernel stack):用于保存进程上下文的内核态栈的位置和大小。
- 进程权限 (capabilities):表示进程对系统资源的访问权限。
通过 PCB,操作系统可以对进程进行管理和调度,并且在进程切换时保存和恢复进程的执行现场。每个进程在系统中都有一个唯一的进程 PCB,PCB 中存储了进程的状态以及运行时的数据,这些数据包括进程的优先级、寄存器状态、缓冲区大小、进程状态等等。操作系统在进行进程管理和调度时,可以根据 PCB 中的信息来判断进程的状态和优先级,以及分配资源等等操作。因此,PCB 是操作系统中非常重要的数据结构之一。
一个进程从创建的到结束的整个过程可以总结为:
1. 首先,操作系统将程序加载到内存中。操作系统会从外存中读取程序的指令,并将其复制到内存中。
2. 然后,操作系统会分析程序的指令,并将其转换为机器指令,也就是CPU能识别的指令。
3. 接着,操作系统会设置CPU的程序计数器(PC)指向程序的第一条指令,然后CPU就可以开始执行程序了。
4. 最后,CPU会依次执行程序中的指令,每条指令执行完毕后,PC就会指向下一条指令,直到程序执行完毕。
查看进程
首先我们编写以下程序,使用getpid()调出进程的进程描述符也就是pid;
编译运行后输出进程的pid为2270。
当我们重新执行程序时输入命令:ps ajx | head -1 && ps axj | grep "process" 查看程序的pid变为了2287,这说明同一个程序,在执行时pid可能不同。
如何结束一个进程?使用kill -9 pid,就可以结束掉一个进程,例如以下进程,当我们输入kill -9 2287,此进程被杀死了。
现在我们执行以下代码输出进程PID和它的父进程
getpid函数是一个系统调用,用于获取当前进程的进程ID。取当前进程的进程ID,并且进程ID在进程的整个生命周期中都是不变的。
其中,pid_t是一个整数类型,通常是int或者long类型。调用该函数会返回当前进程的进程ID。
输出结果如下:此进程的pid为3340,它的父进程pid为3316,那进程3316又是什么?
输入指令 ps axj | head -1 && ps ajx | grep 3316 可以看到它是bash,而bash是shell的一种,shell是命令解释器,本质上也是一个进程,因为他有独立的pid。命令行启动的所以程序最终都会变成进程,而该进程的父进程都是bash。
那为什么要有bash呢?因为操作系统在执行开发者的程序时结果时未知的如果bash直接管理这个程序,在程序出现问题时bash进程也将结束,bash进程结束,我们将无法和操作系统进行交互,就无法让操作系统为我们提供服务。所以bash要创建一个子进程去管理程序,即使程序出现问题,也不影响bash。
fork()函数
那如何从代码层面创建子进程呢?我们可以调用系统调用fronk()函数创建子进程。先来看以下代码:
按照我们对代码的理解,上述代码应该循环输出AAAAAAAAAAA和BBBBBBBBBBBBBB,并且输出AAAAAAAAAAA和输出BBBBBBBBBBBBBB的PID和PPID应该时一样的,但输出BBBBBBBBBBBBBB的进程PID有时和输出AAAAAAAAAAA的不一样,并且输出BBBBBBBBBBBBBB的PPID是调用fork()函数的进程PID,也就是说在调用fork()函数后创建了子进程3362。
fork()函数原型:返回值pid_t为整型数据int类型,它是没有参数的,也就是那一个进程调用了fork()函数该进程就创建了子进程。
fork()函数对父进程返回子进程的PID,对子进程返回0。
我们再来看以下的代码:
是不是感觉和以往的结果有一点不一样?不一样的地方不仅是创建了一个子进程而是同一个地址空间竟然输出了不一样的值。
那我们如何去理解fork()函数?
要理解fork()函数,我们就要理解以下三点:
1.fork()函数做了什么?
2.如何理解代码和数据?
3.fork()函数为什么会有两个返回值?
1.fork()函数做了什么?
前面说过进程=内核数据结构+进程得代码和数据。在调用fork()函数后操作系统会以父进程为模板去创建一个独立的PCB,子进程的PCB是父进程PCB的副本,包括代码、数据、堆栈和其他进程的大部分属性,注意是大部分并不全部,例如子进程有直接的PID,它的PPID和父进程也不一样,
但它和父进程指向同一份代码和数据。
2.如何理解代码和数据?
在生活中我们使用微信和QQ,钉钉关闭微信不会影响QQ和钉钉,关闭钉钉也不影响其他程序的运行,说明它们的运行是具有独立性的,而微信,QQ钉钉也是一个程序是进程,进程的运行也是具有独立性的,进程的独立性在以后会深入讲解。
那么父子进程的运行是不也具有独立性呢?答案是是的。
我们来执行以下代码: 并将此程序的进程杀死,但是它创建的子进程并没有停止,说明父子进程是相互独立的,只是此时子进程的父进程PPID变为了855,也就是bash。
到这里有读者会有疑惑说不对啊!父进程和子进程读的是同一份代码为什么能说是独立的呢?他们只是有自己的PCB,独立性又该如何保证呢?这里要引入一个新的概念,有调用fork()函数后父子进程可以共同去执行之后的代码,也可以if else进行分流执行,但不管如何执行子进程执行的只是进程的部分代码,在学C语言时我们知道代码是只读的。
当我们执行以下代码时:父进程将x的地址空间该为10086,而子进程仍然输出的是100,子进程和父进程之间具有完全独立的内存空间,它们之间的变量和数据不会相互影响,即父进程对数据修改不会影响子进程,这是因为当有一个执行流尝试修改数据时,操作系统会自动触发写时拷。
3.fork()函数为什么会有两个返回值?
当程序准备执行return语句时,函数的主体功能已经完成,而fork()函数的功能是创建PCB,并将PCB插入内核数据结构中。
在为执行return语句时fork()函数已经创建了PCB,而且PCB可能已经被调度了,而return语句被执行了两次,让我们好像看到了两个返回值。在上面的程序中,我们用ret去接收返回值时,就是返回值写入ret变量空间的过程,,那为什么ret变量会有两个值?这里还是写值拷贝,ret变量在子进程中和父进程中空间虽然地址编号是一样的,但不是物理空间,而是虚拟地址空间。