导入
(冯诺依曼结构)
冯诺依曼结构是我们大部分计算机都遵守的结构体系,我们所认识的计算机都有一个个硬件组成:
. 输入单元:键盘,鼠标,扫描仪等
. 中央处理器(CPU):含有运算器和控制器
. 输出单元:显示器,打印机等
注:这里的存储器指的是内存
不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
一句话,所有设备都只能直接和内存打交道。
基于冯诺依曼这一基本结构,让我们详细了解计算机在处理数据和返回数据的具体过程吧!
1.操作系统(0S)
总:OS本质上是一种进行软硬件资源管理的计算机软件
宏观来说OS包括两大部分:
(1)内核:用于进程/线程管理,文件系统,内存管理,驱动管理;
(2)其他程序:shell,glibc,原生函数库等
下图是程序在用户与底层间运行的模块图,该图具象化了用户与底层间的联系:
目的:
从该图我们可以看出OS处于一个承上启下的位置,所以设计OS的目的是:
(1)OS对下进行资源管理,使其稳定,高效,安全的工作;
(2)OS对上给用户提供稳定,高效的运行环境
系统调用和库函数概念
在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分 由操作系统提供的接口,叫做系统调用。
系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统 调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
注:向上提供的接口只能是C语言的接口,所有软件的底层都必须和C直接或间接有关
下来我们针对OS的进程管理做展开:
计算机是如何进行进程管理的呢?
1. 描述起来,用struct结构体
2. 组织起来,用链表或其他高效的数据结构
让我们通过下面讲述再次理解“先描述在组织”这一管理的本质与方法
2.进程
总:进程 = 内核数据结构(结构体task_struct) + 程序代码数据
内核:(担当分配系统资源(CPU时间,内存)的实体)
概念:一般而言正在执行的程序我们称为进程
图示文件的运行:
解释:
描述进程:
(1)进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
(2)课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
task_struct-PCB的一种
(1)在Linux中描述进程的结构体叫做task_struct。
(2)task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
task_struct内容分类:
(a)标示符: 描述本进程的唯一标示符,用来区别其他进程。
(b)状态: 任务状态,退出代码,退出信号等。
(c)优先级: 相对于其他进程的优先级。
(d)程序计数器: 程序中即将被执行的下一条指令的地址。
(e)内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
(f)上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
(g)I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
(h)记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
(i)其他信息
(1)标识符
查看进程:
详细:
简略:
利用ps -ajx查看:
以test为例:
最上面一行的前两个即为进程的标识符:
在了解他们之前我们要知道每一个进程都是由一个父进程创建而来(后面有解释具体原因),可以方便管理
PID:进程本身编号
PPID:父进程编号
对于这两个标识符我们可以利用系统调用来查看:
geipid--PID:
getppid--PPID:
以test为例:
(2)状态
储备:
认识系统调用fork
子进程的程序代码是从父进程中继承的,一般而言,代码可以分享,但数据是各自私有的!
return:fork如果成功会给子进程返回0,父进程大于1
以test为例:
为什么ret有两个返回值:
程序在fork后return之前就已经产生了父子两个进程,他们会分别return一个ret!
并行与并发:
并行:任何时刻多进程同时执行;
并发:CPU执行进程代码,并不是执行完一个才开始执行下一个,而是给每一个进程分配一个时间片,基于时间片进行调度轮转(在一段时间内执行一个程序)(针对于单CPU)
时间片
使进程分时进行的一个结构体属性
等待
等待本质:连入目标外部设备后,CPU不调度
等待分为两种情况:
内存:
在内存上等待称为运行(只要进程在运行队列里)
操作系统:
在操作系统上的等待称为阻塞
图示:
挂起
本质:用时间换空间
在内存资源严重不足时,操作系统会让阻塞的数据换出到磁盘(swap分区),等外设输入后再换入内存并改变状态
介绍:(具体介绍以Linux为例)
下面的状态在kernel源代码里定义:
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
在Linux中进程状态具体有:
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列 里。
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))(进程可被kill -编号杀掉)
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的 进程通常会等待IO的结束。(进程不可被强制杀掉)
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可 以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
t状态 (tracing stop):当进程被追踪,遇到断点停止
Z状态(僵尸状态):僵死状态(Zombies)是一个比较特殊的状态。
当进程退出并且父进程(使用wait()系统调用,后面讲) 没有读取到子进程退出的返回代码时就会产生僵死(尸)进程 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
以test为例:
命令行监控脚本:
while :; do ps ajx | head -1 && ps ajx | grep test | grep -v grep;sleep 1; echo "###############"; done
僵尸进程的危害:
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎 么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话 说,Z状态一直不退出,PCB一直都要维护?是的!
那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构 对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空 间!
内存泄漏?是的!
最后补充一个状态:孤儿进程:(子进程被祖父进程接收)
父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
父进程先退出,子进程就称之为“孤儿进程”
孤儿进程被1号init进程领养,当然要有init进程回收喽。
(3)优先级
利用ps -l我们可以看到:
其中PRI代表进程可被执行的优先级,越小越早执行,NI代表进程nice值,两者合起来构成有优先级计算公式:Linux进程最终优先级 = PRI(缺省值80) + NI(修行数据)
其中NI取值为[-20,19),则优先级范围为[60,99)
补:可以利用top -r +编号修改优先级
(4)进程切换
储备:
a.进程执行时会有许多的临时数据,在CPU寄存器中
(如:pc指针(eip)储存当前执行命令的下一条命令地址;
ir:指令寄存器(保存正在执行的命令))
b.CPU内部的寄存器数据,是进程执行时的瞬时状态信息数据
c.寄存器 != 寄存器的上下文数据(数据保存在各自的PCB中)
挂起swap交换本质:在进程换出时将相关寄存器内容保存起来,换回时将历史保存数据恢复到寄存器
寄存器是CPU中在CPU与内存间的特殊高速处理器!
图示:
单个PBC:
进程切换图示:
源码:
(5)Linux真实调度算法O(1)
a.利用哈希桶体现优先级
根据哈希桶的140个数据从而确定了Linux共有四十个优先级,相同优先级会链在同一位置,相当于一个优先级会有一个进程链。
b.调度队列
注意:
(1)CPU调度只会从active队列中进行调度
调度情况有:
运行退出
不退出但时间片到了
有新进程产生
后两种情况均放入expired队列等待,最终造成active队列数据减少,expired队列数据增多,当active队列数据没了,两队列交换继续按优先级进行进程调度
(2)为减少检测哈希桶数据的误差,降低交换的风险,利用位图bitmap[5],32*5=160覆盖140个元素的哈希桶
调度进程时以32位一次进行检测
(3)Linux链式结构均为双链表,进程中存在一个属性struct_link结构体用于进程间的链接
(4)进程可以处在许多队列中,因为其PCB中含有许多结果体属性用于链接各队列
error:
(5)若已知一进程某一属性我们可以利用偏移量访问其他属性
图示:
总结:
程序在运行时:
(1)会将磁盘文件加载到内存中,在加载之前,会根据默认的结构体事先建立PCB(task_struct),然后根据加载的文件补充其他属性,并加上时间片等信息(先描述);
(2)将多个task_struct链在一起(再组织);(创建)
(3)链好后根据结构体中struct-link链到struct*head加载到运行队列中,并根据优先级加载到哈希桶中并以active队列包装,并与expried队列配合合作;(等待,挂起)
(4)CPU从active队列取数据,加载到寄存器中处理并返回处理结果,或者返回未完成的数据,并保留临时数据,方便下次调用(运行)
这便是程序运行的流程,理解后便可以在脑海里形成动态图!