-
冯诺依曼系统
-
冯诺依曼体系结构是现代计算机的硬件体系结构
-
输入设备:键盘
-
输出设备:显示器
-
存储器:内存
-
运算器:CPU
-
控制器:程序计数器,指令寄存器
-
硬盘不被作为存储器:读写速度太慢 而CPU处理速度非常快 机械硬盘:200MB/s 固态:400/500M
-
内存属于易失性介质,具有掉电易失性,因而不作为长期存储设备
-
硬件体系决定软件行为:所有设备都是围绕存储器(内存)工作的 有些设备在某些场景是输入设备,有些场景下是输出设备
2.操作系统概念与定位
-
任何计算机系统都包含的一个基本的程序集合,成为操作系统(OS),可以理解为一款专用于管理计算机的软件,包括:
-
内核(进程管理,内存管理,文件管理,驱动管理)
-
其他程序(函数库,shell程序)
-
目的:与硬件进行交互,对下管理所有软硬件资源,对上为用户(应用程序)提供良好的执行环境
-
搞管理:描述 + 高效的组织
-
库函数和系统调用接口的关系:属于上下级关系,库函数是对系统调用接口的一层封装
-
描述:在C语言中使用结构体 ,组织:用链表或其他高效的数据机构
3.进程概念:
-
什么是进程: (从用户的角度简单理解)进行中的程序,是一种抽象化的概念,在有些操作系统下可能其实也叫任务。
-
从操作系统的角度:操作系统对进程管理时,对进程的具象化的描述信息
-
这个描述信息是操作系统对进程的具象化描述---这个描述就是进程,这个描述叫进程控制块 PCB(统称),这个结构体里记录了进程要运行什么数据进程,要执行哪些代码,Linux下这个描述结构体叫 struct task _struct, linux 下的进程实际上就是task_struct 这个结构体
-
凭什么操作系统可以通过PCB或task_struct 这个结构体来运行程序?那就要看操作系统到底如何描述的进程?或者说这个结构体中到底有什么东西
-
程序要运行起来时,肯定要进行数据的处理,并且CPU要运行程序的指令,程序属于一种文件,要被存储到磁盘上,由冯诺依曼体系结构来看,要将这个文件加载到内存中来,这个文件不仅包括他要运行的代码,还有要处理的数据。进程对器要进行描述。故而要有个内存指针。
-
内存指针:指向这个进程运行起来后,程序以及数据被加载到的,在内存中的位置。这时就可以认为这个PCB就是要运行的这个程序。
-
标识符:pid 某个进程独有的一个标志,用于识别这个进程。操作系统要寻找这个PCB,就要通过这个唯一的标识,就能准确的找到这个进程。进程名称不一定唯一,但pid这个标识是唯一的。操作系统对描述信息的组织,是通过双向链表,操作系统要找进程,就是在双向链表里找这个PCB,就是通过pid来找这个PCB。
-
文件状态信息,
-
记账信息:记录进程在CPU上运行的时间,如果记录少,说明被CPU调度的少,处于饥饿状态。sleep函数使进程进入休眠状态,制止CPU的调度
-
进程状态,
-
上下文数据:CPU处理数据要先加载到CPU上,再返回到内存中,若不同的进程在分时技术的情况下调度切换,需要对数据的处理情况进行记录,否则被新的进程覆盖之后,下一次再轮到这个进程,CPU认为已经处理过了,直接返回会返回一个错误的数据,数据要保存在上下文数据中,防止切换调度时,数据会丢失。
-
程序计数器:记录当前进程下一步要执行的指令,指令要被加载到CPU里,当他的时间片被耗尽,要进入下一个进程,必须要保存前一个进程下一步的指令,否则下一次轮询到时,无法对这个进程 再次执行
-
进程优先级:CPU资源的优先分配权。
-
CPU的分时技术:操作系统完成的一个功能 ,用于使多个进程同时运行
-
查看进程: /proc 目录保存当前运行的进程信息 数字命名的目录,数字是pid,一个目录就是一个进程,目录里保存的就是进程运行信息的保存,当进程退出时,目录会被删除。
-
ps 查看进程
-
-ef 查看操作系统当前运行的所有进程
-
aux 查看信息更加详细,如CPU占有率,虚拟内存大小等
-
在进程中进行操作:获取进程的pid的接口 getpid ();
-
man 手册 1号 命令信息 2号 系统调用接口 3号 函数库
-
进程状态,创建进程,僵尸进程,孤儿进程,形成原因和危害(主要是僵尸进程),如何避免
-
创建进程:创建一个PCB
-
fork接口:创建子进程,就是通过复制一个调用进程创建一个新的进程,创建一个新的PCB,并且它会复制父进程PCB中的数据。唯一不复制的是pid,pid是用来标识不同的进程的,并且父进程的pid也会更改。
-
因为复制了父进程的PCB,因此父子进程的代码是共享的,也就复制了内存指针,内存指针是一样的,但是子进程创建了自己的pid 而数据则是独有的,子进程并不从代码起始位置运行,而是从创建子进程的位置开始
-
创建子进程的目的:希望子进程单独去干与父进程不同的事务,首先通过fork接口的返回值区分父子进程
-
fork的返回值: 父进程返回子进程的pid,子进程返回的是0,错误(创建失败)返回-1
-
主要是根据返回值来进行父子进程的代码分流,根据判断就可以
-
对于父进程,返回创建的子进程的pid
-
对于子进程,返回的是0
-
对大量的数据进行处理,可用子进程提高效率
-
4. 进程状态:运行,就绪,阻塞
-
运行态(R):运行,就绪 现在能够运行或正在运行
-
可中断睡眠态(S):可以被打断的睡眠状态,可以以最直观常用的方法进行唤醒
-
不可中断睡眠状态(D):磁盘休眠,必须用特殊的方法来进行唤醒,这个状态必须一次性完成,必须等待状态结束才能进行唤醒。
-
停止态(T):不接受任何信息,完全停止活动,不再占用时间片
-
死亡状态(X):时间很短,进程死亡的一瞬间,很少遇见
-
追踪状态(t):几乎很少出现
-
僵死态(Z):处于僵死态的进程就是僵尸进程。
-
进程被标记为+,前台进程:占用了终端前台,最新的命令既可以成为前台进程,shell一直在进行命令的捕捉,前台进程运行起来,将shell寄到后台,前台进程可以将数据直接输入到当前的进程,而不是输入到shell,Ctrl+C关闭的就是前台进程,后台进程除了只能被kill掉,无法对齐进行其他操作。
-
kill 命令 杀死一个进程,将其变成了僵尸进程,虽然退出了,但是pid还在
-
-9 可以进行强杀 kill -9 也没法将僵尸进程杀掉
-
僵尸进程如何产生的,有什么危害,怎么避免
-
形成原理:子进程先于父进程退出,为了保存退出原因,因此子进程资源不会被完全释放,操作系统检测到子进程退出,这是会通知父进程,这时父进程可以去获取子进程退出原因,允许操作系统释放资源,这种情况子进程资源才会被完全释放。如果父进程没有接到这个通知,或没有理会这个通知,也就是意味着父进程没有允许操作系统子进程的资源(操作系统不会自己进行释放资源),那么这时候子进程将处于僵死状态成为僵尸进程。 进程退出而资源没有释放。
-
危害:僵尸进程依然占据着一定量的资源没有完全释放,会造成资源泄露,僵尸进程过多,会使进程运行失败,创建进程失败。
-
避免:关注子进程得退出(操作系统的通知) 进程等待,通过再次创建子进程,让改子进程变为孤儿进程,可以避免长时间等待
-
进程等待是防止僵尸进程的产生,而等待时会只进行等待,有时候不希望进行长时间的等待
-
会话:每次启动系统,会开启一个新的会话,相当于开启新的一天,会话退出,会对运行的程序有影响 常见:登录会话
-
杀死僵尸进程:父进程退出僵尸子进程也会退出(因为僵尸子进程的本质原因是要保存退出原因给父进程看的,然而父进程已经退出了,子进程保留退出原因的做法也就 失去意义了,那么僵尸进程也就没有存在的意义了,因此操作系统就可以直接释放资源了) 这种通过杀死父进程来解决僵尸进程,是最直接但不是最可取的办法。
-
孤儿进程:父进程先于子进程退出,那么子进程就会成为孤儿进程。(父进程被干掉,子进程变为后台进程,进入后台运行)仅仅在后台运行,不受前台任何影响。
-
孤儿进程不是没有父进程,而是父进程转变为init进程,意味着此时子进程退出后,将由init进程来处理他的身后事,而init进程可以直接允许操作系统释放资源,因此不会成为僵尸进程。 孤儿进程退出时通知的是init进程
-
孤儿进程变得“极其强大”就会变成守护进程
-
守护进程:特殊的孤儿进程,运行在后台,并且跟 终端以及会话 的关系完全脱离
-
进程优先级(pri):决定了进程获取CPU资源的优先级。实际上是一个值,值越小,优先级越高,该进程会先被CPU调度处理。
-
为什么要有优先级存在:为了让操作系统的运行更加合理。更加顺畅。有些进程需要更好的体验,需要高优先级 如 交互式进程需要更充分,更快的相应,这样用户体验会更好
-
优先级(priority)设置:优先级无法直接被设置,但是可以通过设置nice值进而设置优先级的值。 越小越高 PRI=PRI+NI NI的取值范围: -20~19
-
设置命令: nice 直接设置 nice值 不设置时默认为0 进程运行时无法再设置,只能用 renice 重新修改 renice -n num -p pid
-
进程竞争性:因为操作系统上的进程都一直争抢CPU资源,因此具有竞争性
-
进程的独立性:为了让一个进程的操作不对另一个进程造成影响,进程之间需要具有独立性。
-
进程的并行:CPU资源足够的情况下,多个进程可以同时运行 真正的多个进程同时往前走
-
进程的并发:CPU资源不够的情况下,多个进程切换调度运行 同一时间只能处理一个
环境变量:一个用来存储操作系统运行环境的所需的一些功能性参数的变量 如同win下的dir或linux的ls
-
如何查看:env set 查看所有环境变量
-
echo $PATH 打印单独的环境变量(需要知道环境变量的名字) ./的作用是告诉shell程序在在当前路径,如同将程序放在PATH配置的路径下,就可以在那里运行一样
-
环境变量在操作系统中具有全局性(环境变量对所有运行程序生效,父进程可以访问,则子进程也可以访问),普通变量不具有全局性。
-
代码中获取环境变量:
-
获取所有环境变量:
-
程序运行参数 main函数的第三个参数如果设置了就是用于定义存储变量(属于形参可以随意命名,如 char* env[]),这个字符串指针数组,存放的都是环境变量,最后以NULL结尾
-
通过全局变量 extern char** environ; 在C库中定义了,使用时需要特别声明
-
通过环境变量的名称获取单独的环境变量; getenv() 接口 内容通过返回值获取,返回的是地址,若没有返回的是空
-
如何设置/转换/添加一个条件变量:export 关键字 声明或定义成一个环境变量
-
删除环境变量:unset 变量名
程序地址空间:进程的虚拟地址空间----结构体 进程对内存的描述信息 mm _struct
-
为什么是虚拟地址空间:因为进程要求地址必须是连续的,所以如果直接使用物理地址,则内存使用率太低,以及缺乏内存访问控制,因此使用分页式内存管理:虚拟出一套连续的地址,经过页表来将使用的地址映射到物理内存,这样映射到物理地址就不需要连续的内存,内存的访问控制也就可以通过中记录的地址属性进行控制。
-
缺乏的内存访问控制,比如因为两个进程由于都直接运行在物理内存中,就可以直接通过物理地址进行访问,就容易发生一个程序访问另一个程序的地址,并有可能修改该程序的数据,造成该程序崩溃的问题
-
而物理内存对这种情况是没有能力进行干预的,因为如果物理内存,比如规定某块空间是只读的,那这一块空间就永远无法再写入数据了,所以物理内存是不应该有任何属性的。
-
分段式内存管理:
-
虚拟内存进行访问控制的办法:比如将程序的代码部分和数据部分分开,并规定两个部分访问内存的不同部分。这样就能解决内存的访问控制问题。内存使用率依然很低
-
页表式:
-
通过一个虚假的地址表,将整个物理内存地址进行映射,类似于对程序进行欺骗,加定所使用的空间是连续的,由于程序运行需要连续的内存地址,而使用连续的内存地址,就会降低内存的使用效率,为了提高内存的使用效率,就得将内存分散开来使用,而分散开来,内存地址就不连续了,程序就无法运行了,就需要使用虚假的地址空间,造成有连续空间的一个假象,使得这个连续空间与分散开的地址空间做个映射空间。
-
对进程来说,在虚拟地址空间层面来说,使用的地址是连续的,而在物理内存层面上,实际是用的分散开的。
-
程序地址空间是一个进程的虚拟地址空间,没有真的开辟一段空间来保存进程
-
物理地址 很难做到访问控制 但是虚拟地址就可以做到
-
为什么要用虚拟内存地址而不是直接使用物理内存地址:缺乏内存访问控制,内存利用率低
-
现在使用的是分页式内存管理(虚拟地址空间+页表)
页表的作用:
-
记录虚拟地址与物理地址映射关系
-
对内存的访问控制 (页表上记录虚拟地址对当前物理地址的访问权限)
实际上子进程复制父进程,其实就是给子进程创建PCB,然后将父进程PCB中的数据拷贝过来这时候父子进程因为虚拟地址空间页表完全一样,因此代码和数据看起来都一样,(代码段指向同一个位置)
但是由于进程应该具有独立性,并且任意进程修改数据不因该对其他进程造成影响,因此子进程需要开辟真正的物理内存来存储这些数据。
但是子进程有可能根本就不用这些数据,这时候要是直接开辟内存以及更新页表,拷贝数据,就在做无用功,因此操作系统一开始并不是直接开辟内存拷贝所有数据,而是等待有任意的一个进程修改数据的时候再重新的给子进程开辟物理内存空间,拷贝数据---写时拷贝技术
提高了创建进程的速度
进程调度:调度进程有时说的是CPU调度进程,实际是操作系统调度某个进程在CPU上运行,实际上调度的是PCB,根据优先级来创建了一个队列数组,并且使用位图标志队列
操作系统在CPU上切换调度进程(PCB)运行,因为进程有优先级 过期队列的指针跟调度指针的调换
CPU的大O(1)调度算法
如果进程非常多,那么进程是一个一个轮询切换调度的在cpu上运行,而进程是有优先级的,如果只靠一个双向链表来管理,需要先进行遍历,找优先级最高的那个先调度,效率太低,或者说有一个队列,把链表中的PCb里的地址添加进去,但是由于有优先级,也要对队列先进行过滤,依然很慢
以空间换时间的思路:假定按照140个优先级,建成140个队列,组成一个队列数组,按优先级将进程放入对应下标的队列中,调度时按照下标调度相应优先级队列中的进程,但依然要对队列进行判空,不空说明队列中有需要调度的进程
用位图来标志,按照140个比特位的位图,0表示队列中没有进程,1表示队列中有进程,以省去对队列进行判空的步骤,轮询到该队列时直接从队列中取出进程,只需要一次遍历。
由于进程被调度之后,需要再次调度,而从队列中取出时,就需要向相应队列中重新添加一边,这时,可以再建立140个队列,从过期队列中取出时,直接放进活动队列中,只需要两个指针的互换,就可以实现