冯诺依曼体系结构
计算机大多遵守冯诺依曼体系结构

各部件说明
输入单元:键盘,摄像头,麦克风,磁盘等
中央处理器(CPU):含有运算器和控制器等,运算器进行数据计算任务,运算又分为算术运算和逻辑运算,控制器对计算过程进行一定的控制
输出单元:显示器,打印机,播放器硬件,磁盘等
存储器: 又称为内存 ,输入设备和输出设备都是外设,CPU是内设
注:上述部件都是硬件 ,用系统总线和IO总线连起来
体系的运作原理
外设要输入或者输出数据,只能写入内存或者从内存中读取,输入到内存的数据经过cpu处理后返回存储器传回内存,再输出数据。因此,所有设备的数据流动都直接和内存发生,不会跳过内存和其他设备进行数据传输。
一般来说,存储容量越大,速度就越慢,下面是运算速度金字塔:

为什么要用存储器作为中介?
外设存储量大,因此速度慢,假如数据在输入设备中处理后以极慢的速度传到速度极快的cpu中处理,大部分时间cpu都在空等 ,运行效率将非常慢。因此引入运算速度和存储容量居于外设和内设的之间的内存作为中介,提高运算效率
操作系统
概念:
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS) 。操作系统包括:内核 (进程管理,内存管理,文件管理,驱动管理)和其他程序(例如函数库,shell程序等等)。操作系统是一款专门做管理的软件,其设计目的是为了管理好所有的软硬件资源,从而为用户程序(应用程序)提供一个良好的执行环境。操作系统在计算机体系中的位置如下图所示。

系统调用和库函数概念补充
在开发角度,操作系统对外表现为一个整体,但适度暴露自己的部分接口 ,供上层开发者使用,这部分由操作系统提供的接口,叫做系统调用 (system call)。 系统调功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,进而形成库,有了库,就有利于更上层用户或者开发者进行二次开发。例如touch命令只停留在第一层的"指令操作"上,"创建文件"这一操作本质是通过系统调用接口实现的。
"管理"是什么?
管理的核心就是先描被管理对象,再组织被管理对象
在一个大型的系统里面,如大学,由于底下学生众多,校长不可能对每个学生的实际情况都了如指掌,只能权衡学生的哪些属性比较重要 ,如绩点、违纪次数、志愿时长等。这些指标确立下来以后,由于学生数量众多,需要有合理的方式对这些指标统一管理 ,如使用登记簿,学生档案等。再以代码为例,相当于针对需要管理的数据创建了一个类,再选择合适的数据结构进行管理 。这就是**"先描述,再组织**"的过程。这个逻辑将会贯穿我们学习操作系统始终。
操作系统管理软硬件资源的方式:
知道了管理的本质后,就可以理解操作系统是如何进行管理的了:先对进程进行描述,再对进程组织进行组织。
什么是进程?
程序 是一个静态的指令集合,比如你电脑上的一个 .exe 文件或一段源代码。它只是在硬盘上的一堆数据。
进程的官方解释:当程序被操作系统加载到内存中并开始执行时,它就变成了一个"活"的东西------进程。它拥有自己的生命周期:创建、运行、暂停、终止。
计算机运行时会有多个进程 ,为了对进程更好地管理 ,按照前文讲到地管理逻辑,需要对这些进程先描述,再组织。
先描述:
操作系统会为每个进程创建结构体对象:进程控制块 (PCB),这些对象里面包含进程的各种重要属性,如:进程编号(pid)、进程状态、优先级、程序计数器(即将被执行的下一条指令的地址)以及指针(指向这个进程对应的代码和数据的)。创建完毕后,像校长一样,操作系统不再关心代码和数据,只需管理好PCB即可。
此时可以得到进程的通俗解释:进程=PCB数据结构对象+代码和数据,而操作系统只关心PCB,代码则靠程序员维护。
再组织:
操作系统会选择合适的数据结构将这些进程组织起来,进程的最基本组织方式是双链表 ,但需要注意的是,操作系统不是靠某种数据结构对象就能把所有进程管理起来 ,一个进程可能存在于多个数据结构对象之中。后续的Linux学习主要围绕着操作系统级别的数据结构展开。
实操操作:
获取进程的PID:
在代码中通过调用getpid()函数来获取并打印进程的pid,之后就可以在操作系统层面上通过pid查看进程的状态

查看系统为进程创建的文件夹:

得到pid之后,可以使用**" ps aux "** 指令查看当前系统运行的所有进程,在其中可以看找到正在运行的进程的Pid


当然也可以通过**" ps aux | grep 进程pid "**精确查看指定的进程

此外,当进程被创建的时候,系统自动为进程创建了文件夹,在**/proc路径**下,可以看到所有的正在运行的进程的文件夹,每个文件夹包含了很多进程的信息

进入文件夹以后可以看到有很多东西:

此外还可以通过getppid() 获取当前进程的父进程pid ,发现多个进程运行时父进程都是一个pid,这就是bash进程,这里展开讲进程的父子关系:
根:系统启动后,内核启动 PID1。它负责初始化系统,并启动其他必要的服务 。
树干:PID1 会启动一个图形登录界面或终端登录程序,等待输入用户名和密码。
树枝 :登录成功后,登录管理器会创建一个Shell进程 ,比如 bash。这个 bash 的父进程(PPID)就是登录管理器。
树叶: 在 bash 里输入 ./process运行代码。这时,bash会调用 fork()系统调用(见下文),复制自己,然后通过 exec() 把你的程序加载进去。所以我们的程序的父进程(PPID) 就是bash 。
关于fork()
要在操作系统中创建进程,除了通过运行程序让系统为我们自动创建以外,还可以使用fork函数主动创建。
fork()是一个库函数,调用fork之后系统会为当前进程创建子进程,这两个进程都会运行后续的代码。子进程的fork会返回0,而父进程的fork()返回一个大于0的数。
下面代码示例通过fork()手动创建子进程并运行后续代码:


可以看到,明明是一个if else分支,每次运行却输出了两句话,由此引出下面有关问题
为什么fork要给子进程返回0,给父进程返回子进程?
fork之后代码父子共享,fork返回不同的返回值是为了区分不同的执行流,执行不同的代码块。
fork函数怎么做到返回两次的?
首先fork本质是一个函数,这个函数的任务是创建一个子进程,所以肯定在返回之前已经创建了子进程,而父子进程都会执行接下来的代码,所以返回这句话总共会被两个fork各执行一次。
为什么变量会有不同的内容
书接上回,如果在fork函数内部,子进程已经被创建出来了,那么返回到调用fork语句的时候,已经存在了两个ret。父进程和子进程不共享数据,但是共享代码。
fork干了什么事情?
fork 创建一个新的进程(子进程),它几乎是父进程的完整副本。
子进程获得独立的 PCB(进程控制块),但其中的大部分属性(如环境变量、打开的文件描述符、信号处理方式等)从父进程复制而来。子进程的代码段、数据段、堆、栈等内存区域,在逻辑上是父进程的副本。
但如果fork后立即复制父进程的全部内存,导致耗时且浪费内存,因此内核采用写时拷贝技术:
共享物理内存 :fork 之后,父子进程的虚拟地址空间映射到同一块物理内存页,并将这些页标记为"只读"
延迟复制:只要父子进程都不修改这些页,它们就始终共享,节省内存。
触发复制:当其中一方试图写入某个页时,CPU 触发缺页异常,内核分配新的物理页,将原页内容复制过去,然后更新该进程的页表,使其指向新的物理页。此后,父子进程对该页的操作互不影响。
为什么要创建子进程?
为了让父子执行不同的事情,所以也需要让两个进程执行不同的代码块
fork创建出子进程以后,父子进程是谁先运行?
这是由调度器决定的、不确定的,调度器决定了从内存中挑选哪一个进程运行,它有自己的一套查找算法,调度原则一般保证公平,也就是所有进程的调用次数是接近的。
进程状态
task_struct有一个成员变量用于表示进程的状态,
如何查看进程状态
之前提到的查看进程的命令中,有一列就是代表着进程的状态:


R运行状态(running):
通常一个cpu维护一个运行队列,位于该队列的进程都是运行态(虽然是按顺序运行,但因为cpu的速度很快,所以这些进程可在极短的时间内都被运行)
为了防止运行时间过久,每个进程都有一个时间片 ,假如时间片是10毫秒,10毫秒内进程没有运行完就会回到运行队列尾部重新排队。从而避免了计算机对某个进程进行死磕的情况。
S睡眠状态(sleeping):
意味着这个进程在等待事件完成。
D磁盘休眠状态(Disk sleep):
有时候也叫不可中断睡眠状态,D状态是内核给正在和硬件打交道的进程穿的"防弹衣",防止它在关键时刻被误杀,保护数据完整性。如果硬件永远不回应,这个进程就永远卡死,谁也奈何不了它。比如磁盘正在写入
T停止状态(stopped):
可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行
X死亡状态(dead):
这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
僵死状态(Zombies):
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面会学)没有读取到子进程退出的返回代码时,就会产生僵死(尸)进程。僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出而父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
僵尸进程危害
维护退出状态本身要用数据维护,这也属于进程基本信息,所以保存在PCB中,所以Z状态一直不退出,PCB一直都要维护
如果父进程创建了很多子进程而不回收,就会造成内存资源的浪费。因为数据结构对象本身就要占用内存,想想看,定义一个结构体变量,就是要在内存的某个位置开辟空间,这就会造成内存泄漏,至于如何避免后面会进行学习。
孤儿进程
如果父进程提前退出,子进程后退出,进入Z之后,那该如何处理呢?
父进程先退出,子进程就称之为**"孤儿进程"**,因为进程的回收一定要指定一个父亲进程,所以1号进程(即前面提到的进程的根)成为这个孤儿进程的父亲,负责回收该进程。
进程优先级
基本概念
进程的运行需要占用资源 。cpu资源分配的先后顺序,就是进程的优先权(priority)
如果某进程一直得不到资源,会造成"饥饿问题";在用户层面的表现上,就是用户卡死,长时间无响应,比如抢课无响应。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。还可以把进程运行到指定的CPU上 ,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
查看系统进程的各个属性
在linux或者unix系统中,用ps-l命令则会类似输出以下几个内容:

UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI (priori):代表这个进程可被执行的优先级,其值越小越早被执行
NI:代表这个进程的nice值
PRI and NI
PRI即进程的优先级,通俗点说就是程序被CPU执行的先后顺序 ,此值越小进程的优先级别越高
NI表示进程可被执行的优先级的修正数值
通过修改nice值使得PRI(new)=PRI(old)+nice
当nice值为负值的时候,该程序优先级值将变小,即其优先级会变高,则其越快被执行,所以,调整进程优先级,在Linux下,就是调整进程nice值。nice的取值范围是-20至19,一共40个级别。
但值得注意的是,系统调度器已经非常智能,所以不需要我们过多干涉优先级,因此PRI(old)指的是最开始调度器为进程分配的PRI值,而不是上一次更改后的PRI值(也就是说无论我们怎么改nice值,改几次,都不会使得该进程的PRI脱离初始PRI+-20)
如何查看进程的优先级:
用top 命令更改已存在进程的nice,进入top后按"r"-->输入进程PID-->输入nice值从而对进程的nice值进行修改:


操作系统如何根据优先级开展调度?
首先在运行队列里面用进程指针做哈希桶 ,数组下标代表着优先级。
同时维护两个哈希桶,一个负责按照优先级运行进程(运行数组 ),另一个用于存储运行时新增的运行请求(等待数组)
假如运行数组执行完毕,直接交换这两数组指针,继续按顺序运行新运行数组中指针指向的进程
等待数组里的进程如何判断是否运行完毕?
位图:40个下标是否存有进程指针,直接用40个0或者1表示,假如这40个都是0,说明这个数组里面已经没有进程指针了,运行完了,直接交换指针。每次运行完一个进程--即可,这时只需O1时间复杂度即可判断运行数组是否为空。
优先级其他有关概念
竞争性 : 系统进程数目众多,而CPU资源只有少量,所以进程之间是具有竞争性的。为了高
效完成任务,更合理竞争相关资源,便具有了优先级。
独立性 : 多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行 : 多个进程在多个CPU下分别,同时进行运行,这称之为并行
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发