一、冯诺依曼体系结构
1、概念
冯·诺依曼结构也称普林斯顿结构,是一种将程序指令存储器和数据存储器合并在一起的存储器结构。
最早的计算机器仅内含固定用途的程序。若想要改变此机器的程序,就必须更改线路、更改结构甚至重新设计此机器。当然最早的计算机并没有设计成那种可编程化。当时所谓的"重写程序"指的是纸笔设计程序步骤,接着制订工程细节,再施工将机器的电路配线或结构改变。
然后储存程序型电脑的概念改变了这一切,通俗的理解冯·诺伊曼结构与储存程序型电脑是互相通用的名词。
冯·诺依曼提出抛弃十进制,采用二进制作为数字计算机的数制基础。同时,预先编制计算程序,然后由计算机来按照人们事前制定的计算顺序来执行数值计算工作。并且大家把冯·诺依曼的这个理论称为冯·诺依曼体系结构。
冯诺依曼体系结构由这几个部分组成:
- 输入设备:键盘、鼠标、话筒、摄像头等等
- 输出设备:显示器、打印机、磁盘等等
- 存储器(通常指的是内存):内存是一种掉电易失的存储介质
- 中央处理器(CPU):由运算器和控制器组成
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。而目前为止我们所认识的计算机都是由一个个硬件设备构成的,这些硬件设备都是独立的。
在这个体系中,所有设备都是要连接的。拿什么链接?拿线连接。拿什么线连接?用总线连接,而所有的线都被集成在主板上,主板就是各个硬件设备之间互相集联的场所!
2、计算机存储结构
所有的设备都是要连接的,这句话不是目的而是手段。通过连接所有设备达到设备之间数据的流动的目的。
而设备之间数据的流动的本质是设备之间数据的来回拷贝,拷贝的整体速度是决定计算机效率的重要指标!
原本输入输出设备是直接与CPU连接的,但是输入输出设备距离CPU太远,导致设备的访问非常耗时间,与此同时,由于CPU处理的效率很快,所以这种组合的效率会以硬件设备为标准,计算机的效率会变得很慢!!
因此操作系统引入存储器也就是内存来充当CPU与硬件设备之间的媒介。
为什么要有内存?
- 引入内存把效率问题转化为软件问题
- 引入内存充当CPU与硬件设备之间的媒介,减少了CPU对外部硬件的访问次数,可以使计算机的效率得到提高
- 内存的造价比CPU的造价低,可以使计算机的成本降低
程序在运行的时候,必须先加载到内存中,这是为什么?
由于冯诺依曼体系的规定,外设的数据必须先加载到内存中,再由内存交给CPU去执行!
总结:
- 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)外设(输入或输出设备)
- 要输入或者输出数据,也只能写入内存或者从内存中读取。
- 一句话,所有设备都只能直接和内存打交道。
二、操作系统(Operator System)
1、概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
- 内核(进程管理,内存管理,文件管理,驱动管理)
- 其他程序(例如函数库,shell程序等等)
OS是一种软件,当我们要使用计算机时,操作系统就是第一个被加载的程序。
2、设计OS的目的
- 与硬件交互
- 管理所有的软硬件资源
- 为用户程序(应用程序)提供一个良好的执行环境
为什么要有操作系统(的管理)?
对下管理好软硬件资源 -- 手段
对上提供一个稳定、高效、安全的运行环境 -- 目的(为了能让用户用的舒服)
3、定位
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件。
3.1 如何理解"管理"
管理无非就是两种情况
- 管理者 --> 做决策的人叫做管理者
- 被管理者 --> 做执行的人叫做被管理者
而管理者和被管理者不需要见面,管理者可以通过管理被管理者的信息也就是数据进行管理。
就好比我们在一家大公司上班,可能到离职或者退休都见不到几次大老板。那我们上班的工资,绩效,考勤啥的数据就由老板来进行管理。
3.2 对于计算机而言
硬件部分:
计算机管理硬件:
本质就是一个先描述,再组织的过程
- 描述起来,用struct结构体
- 组织起来,用链表或者其他高效的数据结构
这样对每个硬件设备建立一个负责描述它们属性的结构体,就可以对数据的管理转向对某种数据结构的管理!
在数据量较大的情况下,管理者该如何管理?
对每个硬件建立的结构体并用链表把结构体给连接起来,然后再写一批对链表增删改查的代码。
这样管理者的所有决策的工作都变成了对链表的增删查改!
对被管理者的管理工作也变成对链表的增删查改!
软件部分(以用户使用为主):
我们为什么要设计操作系统这么一个软件?
目的就是为了对上也就是给用户一个安全、稳定、高效的运行环境。为了保证这些,操作系统就不允许任何人跳过操作系统提供的接口函数来访问操作系统、驱动以及底层硬件设备!
所以系统提供了一系列调用接口来给用户,使其方便访问操作系统。
这些接口就叫做系统调用接口!
而这些操作系统提供的接口函数都是由C语言设计的函数。
但由于系统提供的接口函数数量过多且操作起来对于用户来说有些复杂,于是为了用户的使用方便,操作系统将提供的这些接口函数封装成lib库。像lib库这样的方便用户使用的接口叫做用户操作接口。
这样用户就可以通过访问库中的库文件,也可以跳过lib库直接使用系统调用函数!
4、系统调用和库函数概念
系统调用
在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
库函数
系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
像我们使用的printf函数和scanf函数,它们运行都是需要硬件设备的。这证明它们既是C语言中的库函数,也是封装了操作系统提供的系统调用接口。这样才能才能通过操作系统来访问硬件设备!
在不同的系统中,当用户想要调用库中的函数时。lib库所封装的接口也会不一样,在Linux系统下,lib库就封装Linux系统提供的系统调用接口。在Windows系统下就封装Windows系统提供的系统调用系统。
这就体现了编程语言的跨平台性!
三、初识进程
1、基本概念
- 课本概念:程序的一个执行实例,正在执行的程序等
- 内核观点:担当分配系统资源(CPU时间,内存)的实体
- 广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元
1. 可以同时启动多个程序,那就得将多个程序加载到内存中
2. 操作系统就得管理多个加载到内存的程序
3. 操作系统如何管理加载到内存的程序呢?先描述,再组织
先描述:
- 存放在硬盘中的可执行的二进制文件给写入内存时,由于操作系统并不认识这些二进制文件。所以操作系统为了对这些二进制文件加以管理,特地给每个二进制文件都设立了一个描述该文件的结构体变量,接着把该文件对应的属性(标识符、进程ID、状态等等)给写进该结构体中
再组织:
- 后面操作系统为了方便管理这些结构体,所以就用链表的形式将这些结构体给链接起来。所以对进程的管理转化为对结构体的管理继而在转化为对链表的增删改查
所以到头来什么是进程?
进程 = 可执行程序 + 内核的数据结构
2、描述进程 -- PCB
可执行程序我们知道是什么,那这个内核数据结构又是什么呢?
内核数据结构是操作系统内核中用于组织和管理系统资源的数据形式或组织方式。这些数据结构通常用于描述和维护系统的状态、进程信息、内存分配、文件系统、设备驱动等方面的内容。
- 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
- 课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
而PCB也是内核数据结构中的一份子。
3、Linux中的PCB -- task_struct
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
3.1 task_struct内容分类
- 标示符(pid): 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针上(PCB通过内存指针找到进程的数据)
下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。其他信息
- CPU执行的本质:取指令 -> 分析指令 -> 执行指令。就这样一直循环,直到程序退出CPU。
- 中央处理器CPU中存在一种指针叫eip/pc指针,这种指针指向的是正在执行语句的下一条语句的地址。
- 程序中的函数跳转、判断、循环的本质都是修改CPU中的eip/pc指针。
- eip/pc指针指向哪个进程的代码就代表着哪个进程正在运行。
4、查看进程
4.1 ps指令 -- 用于查看当前运行的进程的运行状态
选项:
参数:
常用的ps指令:
ps ajx | head -1 && ps ajx | grep 进程名
在Linux中,进程的标示符用pid表示,标示符用来确定唯一的一个进程,区别别的进程。
而每个进程都会有自己的父进程,父进程的标示符用ppid表示。
从上图中我们可以看出,每当我们创立一个新的进程时,其对应的pid都会刷新!!
4.2 getpid() 和 getppid()
- getpid( ) 函数用来获取当前进程的标示符pid
- getppid( ) 函数用来获取当前进程的父进程的标示符ppid
- geipid 和 getppid 这两个函数都被包含在头文件<unistd.h>和<sys/types.h>中
4.3 进程的信息可以通过/proc 系统文件查看
tip:只有当进程存在时,才能根据进程的pid查看进程的信息
ls /proc/进程pid
当我们把正在执行的可执行程序给删掉后,此时进程对应的信息显示可执行程序已经被删除。
尽管程序已经被删除,但是进程却从未停止运行
因为在磁盘中的可执行程序被写入内存并转化为进程后,就和磁盘上的.exe问件没有任何关系了!
当我们在/proc 系统文件中能看到当前进程所对应的工作目录cwd
所以我们可以通过修改进程的工作目录来改变程序中文件创建的位置
首先我们先介绍用于修改进程工作目录的指令 -- chdir
可执行文件:
结果:
通过把进程的工作目录改到家目录,这样在程序中创建的文件就不会在原来的工作目录而是在修改后的目录下。
5、 父进程与子进程
进程只有在程序运行后被写入内存中,才会形成进程。
而每个普通进程都会有它的父进程,它的父进程就是-bash
为什么我们不手动创建一个进程呢?可以使用fork()函数来手动创建子进程
5.1 fork()函数:
fork有两个返回值
- 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
- 想让父子进程分别做不同的事
5.1.1 fork函数的疑问:
1. 为什么上图程序中的两个死循环都能跑?
- 创建进程时,系统中就会多一个进程。但是每个进程除了有系统创建的PCB外,还会有自己的代码和数据。而子进程刚创建出来的时候只有系统为其创建的PCB,没有属于自己的代码和数据,于是子进程就共享了父进程的代码和数据。
- 子进程被创建出来后,系统会为其创建PCB。但父进程会将本身PCB的大部分数据复制到子进程的PCB中,子进程也需要自己的标示符(pid)和父进程的标示符(ppid)。
- 对于CPU来说,创建一个进程无非就是多调度一个进程。所以CPU会同时调度父进程和子进程,从而执行父进程和子进程的代码。
2. 对于fork的返回值,为什么给父进程返回子进程的pid,给子进程返回0?
每一个子进程有且只有一个父进程,但是父进程会有很多个子进程(父 :子 = n :1)。所以子进程为了让父进程找到自己就必须把自己的标示符(pid)返回给父进程,而子进程就只需要在乎是否被创建成功,所以返回值就只要返回0即可。
3. 为什么fork函数会返回两次?
首先我们得先知道,当程序运行到return语句时,程序的核心代码已经跑完。
所以在fork函数中,当程序运行到return语句时,代表子进程已经被创建出来。所以父子两个执行流会分别return一次,就出现两个返回值。
4. 返回值是同一个变量,为什么既大于0,又小于0?
首先我们得先了解一个概念:(任意)进程之间具有独立性,互相并不影响!
tip:共享代码并不影响独立性,因为代码在加载到内存之后是不可能发生改变的!
- 由于父进程的代码和数据是对子进程共享的,但是代码是只读的,而数据就有可能会被父进程或子进程进行修改。
- 在子进程被创建后,系统就采用写时拷贝,使得父子进程代码共享,数据各自开辟一块空间。
写时拷贝:
当父/子进程想要修改数据时,系统为了防止共享的数据被随意修改,就为被修改数据开辟一块额外的空间并把想要修改的值写入。这样父/子进程就只需要对新开辟的空间的数据进行修改即可。
补充:
在Linux中,可以使用同一个变量名来表示不同的内存
在fork函数返回时,return的本质就是返回。当发生写时拷贝后,父进程和子进程虽然是同一个变量,但相同变量写入的值也会不同