前言:以下是本章的重点
①:认识冯诺依曼系统
②:操作系统概念与定位,理解"管理"
③:深入理解进程概念,了解PCB
④:学习进程状态,学会创建进程,掌握僵尸进程和孤儿进程,及形成原因和危害
⑤:理解进程切换,以及Linux2.6 kernel,O(1)调度算法架构
⑥:理解环境变量,熟悉常见环境变量及相关指令,getenv/setenv函数
⑦:理解c内存空间分配规律,了解进程内存映像和应用程序区别,认识虚拟地址空间
一、冯诺依曼体系结构
我们常见的计算机例如笔记本,不常见的计算机如服务器,大部分都遵守冯诺依曼体系

我们当前所认识的计算机,都是由一个个硬件组成
输入单元:键盘、鼠标、扫描仪、写板等
中央处理器(CPU):含有运算器和控制器等
输出单元:显示器、打印机等
这里的存储器是指内存,CPU只能对内存进行读写,不能访问外设(输入输出设备)(数据层面)
外设(输入输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取
总结:所有设备都只能直接和内存打交道

二、操作系统(OS)
2.1 操作系统的概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS),笼统地说,操作系统包括:
①:内核(进程管理、内存管理、文件管理、驱动管理)
②:其他程序(例如数据库、shell程序)

2.2 设计OS的目的
对下,与硬件交互,管理所有的软硬件资源
对上,为用户程序(应用程序)提供一个良好的执行环境

2.3 系统调用和库函数
在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口就叫做系统调用
系统调用在使用上功能比较基础,对用户要求也就较高,所以有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就有利于上层用户或者开发者进行二次开发
三、进程
3.1 基本概念与基本操作
当前我们这样理解进程:进程 = 内核数据结构(task_struct) + 自己的程序代码和数据。
而在内核的观点:担当分配资源(CPU时间、内存)的实体是进程
3.1.1 描述进程 PCB
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合
这样的进程属性块叫做PCB,而在Linux操作系统下的PCB是:task_struct,换言之,task_struct是PCB的一种,是用来描述进程的一种结构体,它会被装载到RAM(内存)里,并包含着进程的信息
3.1.2 task_struct
内容分类
①标示符:描述本进程的唯一标示符,用来区别其他进程
②状态:任务状态,退出代码,退出信号等
③优先级:相对于进程的优先级
④程序计数器:程序中即将被执行的下一条指令的地址
⑤内存指针:包括程序代码和进程相关数据的指针,还有其他进程共享的内存块的地址
⑥上下文数据:进程执行时处理器的寄存器中的数据
⑦I/O状态数据:包括显示的IO请求、分配给进程的IO设备和被进程使用的
⑧记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等
⑨其他信息
组织进程
可以在内核源码中找到它,所有运行在系统里的进程都以task_struct双链表的形式在内核里

3.1.3 查看进程
- 进程的信息可以通过/proc系统文件夹查看 :ls /proc/
2.大多数进程信息同样可以私用top和ps这些用户级工具来获取:ps aux | grep test | grep -v grep
3.1.4 通过系统调用获取进程标识符
进程 id(PID)
父进程id(PPID)

3.1.5 通过系统调用创建进程(fork初识)
fork有两个返回值
父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)

fork后父子进程都会各自执行的后续的printf和Sleep,但是父进程的ret是子进程的 id,而子进程的ret会是0以此表示fork成功
fork之后通常要用if进行分流

3.2 进程状态

3.2.1 源码中的状态

R运行状态(running):并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列中(在Linux中就绪状态和运行状态是被合并到了R状态里,因为对内核来说,在运行队列中等待CPU调度和正在被cpu调度都是可被调度的,但是区别于阻塞状态)
S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也可叫做可中断睡眠(interrupible sleep))
D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束
T停止状态(stopped):可以通过发送SIGSTOP信号来停止(T)进程,这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态(几乎无法被捕捉到)
3.2.2 进程状态的查看

a: 显示一个终端所有的进程,包括其他用户的进程y
x: 显示没有控制终端的进程,例如后台运行的守护进程
j: 显示进程归属的进程ID、回话ID、父进程ID、以及与作业控制相关的信息
u: 以用户为中心的格式显进程信息,提供进程的详细信息

3.2.3 Z(zombie)僵尸进程
僵死状态是一个比较特殊的状态,当进程退出且父进程(使用wait()系统调用后)没有读取到子进程退出的返回代码时就会产生僵死进程
我们用自己的话说就是子进程退出后,内核会保留子进程的残留信息,等待父进程的检查并回收这些残留的残留信息,但是父进程检查回收的这个过程出现了意外(wait()系统调用出现了问题),子进程残留的这些信息没有得到清理,因此出现了僵尸状态。
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码
3.2.4 僵尸进程危害
①:进程的退出状态必须被维持下去,因为它期望告诉它的父进程的它的运行结果(即使没有结果,如果父进程一直不读取,那么子进程就会一直处于Z状态)
②:维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct中,换句话说,Z状态一直不退出,PCB就一直要维护
③:一个父进程创建了很多子进程都不收回,就会造成内存浪费
3.2.5 孤儿进程
父进程先退出,子进程就称之为"孤儿进程",孤儿进程会被1号init/systemd进程领养
3.3 进程优先级
3.3.1 基本概念
cpu资源分配的先后顺序,就是指进程的优先权(pirority)
优先权高的进程有优先执行权利,配置进程优先权对多环境的Linux很有作用,可以改善系统性能
还可以把进程运行到指定的cpu上,这样以来就把不重要的进程安排到某个cpu,大大改善性能
3.3.2 PRI和NI
PRI就是进程的优先级,或者通俗地说就是程序被CPU执行的先后的顺序,值越小的进程优先权越高
NI即nice值,其表示进程可被执行的优先级的修正数值
PRI加入NI(nice值)时新的PRI变为:PRI(new) = PRI(old)+ nice
所以调整进程的优先级,在Linux中就是调整nice值
nice值的取值范围是-20到19,
但是进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化,可以理解nice值是进程优先级的修正数据
3.3.3 补充概念:竞争、独立、并行、并发
竞争性:系统进程数目众多,而cpu资源只有少量,所以进程之间具有竞争属性,为了高效完成任务,更合理竞争相关资源,便具有了优先级
独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行:多个进程在多个CPU下分别,同时进行,这称之为并行
并发:多个进程在一个CPU下采用进程切换的方式,在一段时间内让多个进程得以推进,称之为并发
3.4 进程切换
CPU上下文切换:其实际含义是任务切换,或者CPU寄存器切换,当多任务内核决定运行另外的任务时,它保存正在运行任务的当前状态,也就是CPU寄存器中的全部内容,这些内容被保存在任务自己的堆栈中,入栈工作完成后就把下一个将要运行的任务的当前状况从该任务的堆栈中重新装入CPU寄存器,并开始下一个任务的运行,这一个过程就是context Switch

3.5 Linux2.6内核进程O(1)调度队列

下面我们讲解大O(1)调度算法:
①:首先是最外层的runqueue(进程运行队列),这是每个CPU独立维护的
lock:自旋锁,保护队列操做的线程安全
nr_running:当前队列中正在运行/就绪的进程总数
cpu_switces:CPU执行进程切换的次数
curr:指向当前CPU正在运行的进程
idle:指向当前CPU的空闲进程
*active/*expired:指向优先级数组队列(活跃/过期队列)
②:然后是核心prio_array_t (优先级数组队列)
runqueue内部通过两个prio_array_t结构体管理进程,这是O(1)调度的核心设计
array[0] = 活跃队列:存放时间片未耗尽的进程
array[1] = 过期队列:存放时间片已耗尽的进程
③:prio_array_t 的内部结构(优先级管理)
每个prio_array_t 都包含三个关键字段,用来高效管理140 个优先级的进程队列
nr_active: 当前队列中所有优先级的进程总数
bitmap[5]: 优先级位图(用5个32位长整型,共160位覆盖140个优先级),其中每一个bit对应一个优先级,bit=1表示该优先级队列有进程,调度时通过位图快速找到最高优先级的非空队列(只需遍历五次)
queue[140]: 140个优先级对应的双向链表,每一个链表对应一个优先级,数值越小优先级越高,相同优先级的进程FIFO顺序排队,调度时取队头进程,其中:0~99是实时进程优先级,100~139:普通进程优先级(对应nice值-20~19)
④:调度逻辑(双队列如何工作的)
CPU从active队列中,通过bitmap找到最高优先级的非空队列,取队头进程执行,当时间片耗尽,就会从active移到expired队列的对应优先级链表,当active队列的nr_active变为0,交换active和expired指针,进程重新获得时间片继续调度

总结:在系统中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增加,我们称之为进程调度O(1)算法
四、命令行参数和环境变量
4.1 基本概念:
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
4.2 常见的环境变量:
①PATH:指定命令的搜索路径
②HOME:指定用户的主工作目录(用户登录到Linux系统中时,默认的目录)
③SHELL:当前shell,它的值通常是/bin/bash
4.3 查看环境变量方法
echo $NAME (NAME是你的环境变量名称)
4.4 和环境变量相关的命令
echo:显示某个环境变量值
export:设置一个新的环境变量
env:显示所有环境变量
unset:清除环境变量
set:显示本地定义的shell变量和环境变量

4.3 通过代码获取环境变量
①:命令行的第三个参数

②:通过第三方变量environ获取

③:通过系统调用获取或设置环境变量

环境变量通常具有全局属性,可以被子进程继承下去

五、程序地址空间
5.1 虚拟地址
如果我们这样测试代码:

我们会达到下面的结果:

我们发现:父子进程输出地址是一致的,但是变量内容不一样,所以:
①:变量内容不一样,所以父子进程输出的变量绝不是同一个变量
②:地址一样,说明该地址不是物理地址
③:在Linux地址下,这种地址叫做虚拟地址
④:我们在c/c++语言看到的地址全部都是虚拟地址,物理地址用户是看不见的,有OS统一管理
OS必须负责将虚拟地址转化为物理地址
5.2 进程地址空间
"程序的地址空间"这样的描述是不准确的,应该说是"进程地址空间",
分页&虚拟地址空间:

同一个变量地址相同其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址
5.3 虚拟内存管理
描述Linux下进程的地址空间的所有信息的结构体是mm_struct(内存描述符),每个进程只有一个mm_struct,在每个进程的task_struct结构中,有一个指向该进程的mm_struct结构体指针

可以说mm_struct结构是对整个用户空间的描述,每一个进程都会有自己独立的mm_struct,这样每一个进程都会有自己独立的地址空间才能互不干扰
这是task_struct 到mm_struct,进程的地址空间的分布:

既然每一个进程都有自己独立的mm_struct,操作系统肯定要将这么多进程的mm_struct组织起来,虚拟空间的组织方式有两种:
1.当虚拟区较少时采取单链表,由mmap指针指向这个链表
2.当虚拟区间多时采取红黑树进行管理,由mm_rb指向这棵树
Linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域(VMA),由于每个不同质的虚拟内存区域功能和内部机制都不同,因此一个进程使用多个vm_area_struct结构来连接各个VMA,方便进程快速访问


5.4 为什么要有虚拟地址空间
也就是问程序如果直接操作物理内存会造成什么问题?
安全问题:
每一个进程都可以访问任意的内存空间,这也就意味着一个任意一个进程都能去读写系统相关内存区域,如果是一个木马病毒,就会造成安全隐患
地址不确定:
编译完成后的程序是存放在硬盘中,当运行时需要将程序搬到内存中去运行,如果直接使用物理内存我们就无法确定内存现在使用到哪里了,也就是说拷贝的实际内存地址每一次运行都是不确定的
效率低下:
如果出现物理内存不够用的情况,我们一般是将不常用的进程拷贝到磁盘的交换分区,腾出内存,但是如果是物理地址的话就需要将整个进程一起拷走,这样内存和磁盘之间拷贝时间太长,效率低下
那么虚拟地址空间和分页机制就能解决问题吗?
①因为有地址空间的存在和页表的映射的存在,我们的物理内存中可以对未来的数据进行任意位置 的加载!物理内存的分配 和 进程的管理就可以做到没有关系, 。 进程管理模块和内存管理模块就完 成了解耦合
②因为页表的映射的存在,程序在物理内存中理论上就可以任意位置加载。它可以将地址空间上的 虚拟地址和物理地址进行映射,在 进程视角所有的内存分布都可以是有序的