进程是一个非常重要的概念,了解它,你会更清晰的认知计算机中的程序执行。看待计算机的角度都会不一样。
冯诺依曼体系
首先,来介绍计算机的结构-冯诺依曼体系
这是现代计算机的逻辑结构。
输入设备:键盘、鼠标、手柄、网卡…
输出设备:屏幕、硬盘、音响、网卡…
存储器:内存
也就是说,在不考虑缓存的情况下,CPU只会与内存进行交互,输入输出设备也是只与内存(存储器)进行直接交互。
控制器是进行决策的,决策的对象是内存中的数据与代码。
运算器是根据控制器的指令进行运算。
比如:键盘输入时,键盘中的寄存器会进行按键识别,识别后会把寄存器的数据给内存中相应的寄存器,CPU中的控制器会控制其寄存器中的数据读到运算器中,在根据指令进行操作。
各种硬件单元,使用的是线:总线(IO总线,系统总线),进行连接。
通信在体系下的硬件操作
使用即时通信软件,例如QQ,微信。
如果两个人想要发信息,需要两个人的QQ都要打开,则意味着,QQ这个程序变成了进程(后面介绍),正在执行中。
A从键盘上输入发送的信息,信息被读取到内存中,再被CPU读取,根据QQ的加密或者数据打包,再还要符合网络协议的格式,被CPU处理,再放入内存中,被输出设备(显示屏,网卡)读取,信息原封不动的显示在显示器上,同时,被打包后的信息会被发送进入网络,服务器上,
再发送到B的网卡中,数据包再被读取到内存中,被CPU解码,发送回内存,再被显示器读取,显示。就完成了一整个通信的硬件逻辑操作的过程
外设与CPU的交互
像输入输出设备也是能与CPU交互的,但不是数据交互,而是信号级别的交互,比如:中断(外部中断)
当你在键盘上输入数据时,CPU是如何知道输入好了数据呢?就是靠外设和CPU之间的信号交互。当数据输入好了,给CPU(控制器)发送一个硬件级别的电脉冲,CPU就会把外设寄存器中的数据读入到内存中。
除此之外,在数据层面上,CPU是不会和外设进行交互的,因为效率太低了。
所有设备在数据层面上都只能和内存(存储器)进行交互。
操作系统OS
一个计算机中,仅仅只有硬件体系是不够的,还需要管理硬件,然硬件被组织起来。
所以开发了一款软件,来对硬件资源进行管理,就是操作系统。同时,操作系统还为其它应用程序提供了一个可执行的软件环境
逻辑图
其中的驱动,是进行硬件和OS之间处理信息的,防止不同的硬件导致OS出现问题,让OS与硬件进行解耦。
概括
所以说操作系统就是
内核(内存管理,文件管理,进程管理,驱动管理)
其它程序(函数库,shell程序)
设计OS的目的,是为了
- 与硬件交互,管理所有的软硬件资源
- 为用户程序(应用程序)提供一个良好的执行环境
纯粹是为了进行管理。
管理的概念
我目前还是个学生,对于这个方面还是举学校的例子。
学生入学后,就进入了学校的管理系统,就属于被管理这的身份。而我们学生是看不到那些系统里的信息的。
而每个学院每一级都有一个辅导员,他负责处理我们的入学生活之类的。他看上去是个管理者,
但站在整个学校的角度来看,校长才是真正的管理者,而我们学生是被管理者,辅导员之类的是执行者。
而校长靠什么来管理我们的呢?靠教务系统,系统里已经全部组织好了全校学生的信息。比如,要打学校之间的辩论赛了,校长在教务系统中挑10名绩点最高的学生去打比赛,校长选好后,告诉辅导员,让辅导员去组织一下赛前准备。这个过程就是一个被组织的过程。
所以可以得出一个概念,管理,其实就是,先描述,再组织的过程。
PCB-进程描述
操作系统对于进程也是要进行管理的,也是要遵循,先描述再组织的过程。
那一个OS要管理的进程肯定不止一个,那那么多的进程要么怎么一一描述?
使用C语言中的结构体的概念,因为linux内核就是那C语言写的。
关于进程的所有信息都被放置在一个进程控制块中的结构体中,结构体中右进程所有信息的分类。
task_struct
{
}
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
而一个PCB进程控制块来规划、组织一个进程,那OS要管理多个进程的时候,就是要管理多个PCB。那OS就会用一个数据结构来管理多个PCB。
使用了链表来组织的数据结构。
进程概念
当一个可执行文件(exe文件),没有执行时,是被保存在硬盘中,当被执行后,这个可执行程序的代码和数据就会被从硬盘中,加载到内存中。
且,当电脑开机时,第一个加载到内存中的进程就是操作系统。然后加载到内存中的进程就会被操作系统管理起来,管理进程的PCB。
创建进程
可执行文件的代码和数据会被加载到内存中,同时,OS会为这个进程创建一个PCB结构体来描述这个进程,同时,会把新创建的PCB连接到执行链表中,等待被CPU读取。
删除进程
当选择删除时,操作系统会先找到,要被删除进程的PCB,然后根据PCB中的信息,找到在内存中的代码和数据的位置,将这个内存销毁,再将·PCB从队列中剔除,并销毁PCB的所处空间。这就完成了一个进程的删除。
PCB的成员
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级-pid。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
时间片
时间片:由于在计算机中,一定是进程多,CPU少,但是一个CPU不能只执行一个进程,其它进程也要执行,所以有了时间片的概念,即一个进程在CPU上执行的时间,通常是比较短的,这个进程在CPU上执行一点时间,立马换下一个进程,就遮掩给,依次进行。在我们看起来1就像是多个进程在同时进行的。
在正常的进程调度的过程中,要基于时间片来进行调度,也就是基于时间片的轮转
我先声明一下,这些图片都是我在OneNote上做的笔记,但复制就变成了图片了。
标识符
状态
优先级
程序计数器
记账信息
进程切换与调度
上下文数据
结构体指针
CPU的操作
当程序变成进程时, CPU要对进程进行操作。
CPU只负责执行三个操作,循环进行。
读取指令->分析指令->执行指令
这个进程替换就是又操作系统来决定的。
CPU取指令时,要从指令寄存器eip中读取即将要执行的下一条指令的地址。也就是PCB中程序计数器的概念。
实际上,操作系统对进程的管理,就是对PCB链表的增删查改。
再概括一下,操作系统对进程的管理是,先描述,再组织。描述就是创建PCB,组织就是对链表进行操作。
系统调用创建进程-fork
如果是子进程,其返回的就是0
如果是父进程,其返回的就是子进程的pid
如果进程创建失败,就会返回-1。
这个是fork函数的返回值的信息。
从某种意义上说,这个函数有两个返回值。
因为它不仅会返回代表子进程的0,还有代表父进程的子进程的pid。
至于为什么说他又两个返回值,看这个。
这是输出结果。
看到么?这有两个返回值,正好对应了上面的介绍。
这就是可以通过,fork函数系统调用创建子进程,有两个进程,就可以让两个进程执行不同的操作,利用其返回值。
这是效果
一直执行不同的操作。
进程状态
进程再内存中也是有状态的。
对于操作系统而言,所有的进程都需要被操作系统识别,区分,而操作系统不能像我们人类能这么抽像的理解,所以,进程状态要让OS理解,就必须符合OS的规则,要将其数据化。
在OS中,进程的所有状态的都是可以数据化的,
而进程状态->数据化,所有的数据都被保存在PCB中,不然PCB怎么叫进程状态控制块。
像task_struct
中,就有一个指针数组专门存储进程的所有状态。
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 */
};
R状态(运行状态)
举例
注意:
S状态-休眠状态
举例:
比如说:你在课堂上太困了,然后你跟你同桌说,太困了,要睡一觉,等老师来了,就赶紧叫醒你。这时,触发你醒的条件就是老师来了。然而,这也可以因其他情况叫醒你,比如,老师叫你上去做题目,这又是另外一种条件。或者你自己睡醒了。你是随时能够被唤醒的。
休眠状态的进程也是这样子。
D状态-深度睡眠
可以防止进程被操作系统杀掉,而导致进程执行任务失败后造成资源丢失,而内存泄漏。
T状态-暂停状态
经过信号SIGSTOP可以让进程处于暂停的状态
这个和休眠状态很像,按照编程需要进行进程暂停或运行。
Z-僵尸状态
一个进程需要一个退出码,其OS需要根据退出码来判断这个进程为什么退出。是正常退出还是因为其他原因而导致进程退出。
这个退出码就是main函数中return返回的值。
可以用echo $?
来显示退出码。
且这个进程的退出码保存在task_struct
中,也就是PCB中,OS会在这里读取退出信息。
孤儿状态
这是父进程托管子进程的状态。
当我杀掉父进程时,子进程的pid未变动,而父进程已经变成了OS
子进程被1号进程接管了。
进程间的性质
- 独立性:多进程运行需要独享各种资源,多进程运行期间互不干扰
- 并行:多进程在多个CPU下分别、同时的进行(任意时刻)
- 并发:多个进程在一个CPU下采用切换的方式,在同一时间段内,多个进程都得以推进
- 竞争:系统进程数目众多,而CPU资源有限,所以进程之间存在竞争性,为了高效完成任务,进程获得相关资源的先后顺序便有了优先级
当进程数目较多时,对某一该进程而言,其切换到周期就变大了,就反映出一种”卡“的感觉
进程优先级
- cpu资源分配的先后顺序,就是指进程的优先权(priority)。
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
top任务管理器
PRI与NICE值
- PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
- 那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
- PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
- 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
- 所以,调整进程优先级,在Linux下,就是调整进程nice值
- nice其取值范围是-20至19,一共40个级别
需要强调一点的是:进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进 程的优先级变化。
可以理解nice值是进程优先级的修正数据
通过top指令也能修改一个进程的NICE值,来间接修改一个进程的优先级。
top->按 ‘r’ -> 输入进程的PID - >输入NICE值
通过ps -al
可以查看进程
我把子进程的优先级修改成了60,也就是说我将NICE值变成了-20,所以子进程的优先级是最高的。
提醒:普通权限是不被允许修改进程优先级的,要sudo提升权限。才会允许。
环境变量
- 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。 - 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
由系统提供的环境变量,那就有变量名与变量内容。
如,经常使用的
ls
命令,这其实某种程度上说是一个可执行程序。就跟我们让自己写的程序运行。
一般是./myproc
,.
是指在当前目录下查找,找到myproc
后,再执行。而ls/mkdir/rm/mv....
都是一样的,要先找到,在执行这个程序。这些查找其路径,就是靠环境变量来完成。
常见的环境变量
- PATH : 指定命令的搜索路径
- HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
- SHELL : 当前Shell,它的值通常是/bin/bash。
像ls
指令就是PATH去指定命令下搜索。
就会去这些路径下搜索命令。
ls指令是被打包后的。
从理论上说,只要我们把自己编写的可执行程序也放入PATH可以找到的路径下,那么也能像这些指令一样运行。
比如:sudo cp -f myproc /usr/bin
(不推荐,可能会对系统造成影响)
export PATH=$PATH:可执行文件的路径
这样就是添加了PATH查找的路径
如果你破坏了环境变量,也没什么,重新进一下系统,服务器会重新配置环境变量的。
HOME
可以显示自己所处的工作目录。也就是说这个环境变量保存了当前所处的工作目录
SHELL
显示命令行解释器的版本
env
可以显示所有的环境变量
三个main函数的参数
int main(int argc,char* argv[],char* envp[])
argc-命令行参数个数
argv-命令行参数列表
envp-环境变量参数列表
后面的就是命令行参数。
解释一下:ls -a/-al...
这些都是命令,而 ls 是命令,而后面的-a -al 都是选项
就有点像,函数传递的不同的参数,而执行不同的指令
最后一个元素是NULL代表结尾。
代码运行后
把环境变量全部给打印出来了。
如果把这个程序加入PATH中,就也能变成一个指令。
进程地址空间
先来回顾一下程序地址空间。
我们曾经认为的程序空间的地址就是这样,可以直接对应与硬盘的空间。系统给程序分配了2的32次方
(32位机下)的大小,总共4G,当时我还不理解是什么意思。
现在,在了解了进程后,这个其实是叫进程地址空间更合适一些。
提问:这个进程地址空间是不是内存呢?
回答:不是(后面解答)
进程地址空间对应的地址不是物理地址
这是一份程序,还没跑起来。有一个全局变量val
,在父进程时,让该进程休眠了1秒,也就是说让子进程先跑。
在子进程的分流中执行了修改全局变量的值。
看看输出结果。
子进程修改后的val的值变成20了,而父进程的值依旧是之前的100,而两个进程分流后的val的地址依旧是一样的,没有改变
也就是说,val的地址没变,但是子进程与父进程的val值取已经发生了改变。看似不可能,却是实实在在的发生了改变。
先前提过,父进程创建了子进程,子进程会共享父进程的代码和数据。
这也就证明了一点,这个进程地址空间,其对应的地址,根本就不是真实的物理地址,如果其对应的是真实的物理地址,那么两个地址一样的空间,是不可能读取到两个不同的值
虚拟地址
进程地址空间对应的地址,其实是虚拟地址。也就是说,我们在代码中操作的指针、地址,也不是真实地址,而也是虚拟地址。
而在Linux操作系统中,OS负责将物理地址转换为虚拟地址。
再来一个联想,打印输出的val的虚拟地址在父子进程中是一样的,而值是不一样的也就是说,这个val的真实物理地址是不一样的,只不过操作系统将不同的物理地址处理成了相同的虚拟地址。
逻辑关系
页表是OS处理虚拟与物理地址之间映射关系的方案,页表使用一种算法,将物理地址处理成虚拟地址,且,让物理地址不可见。同时对于子进程也是一样的。
当程序运行时,子进程的分流修改了val的值,由于进程之间是具有独立性的,子进程的数据被修改了,并不会影响父进程的数据。为了保证这种进程间的独立性,OS会位这个val在内存中申请过一个空间,修改val的值,同时,这个新申请的空间的地址就又会被页表处理,使虚拟地址不变。
对父进程也一样
这次,我让父进程先运行,先对val的值进行修改,子进程后运行。看看结果。
同样,为了满足进程之间的独立性,并不是只有子进程会让OS为它在其它地方申请一块空间来保存修改后的值,对于任何进程都一样,无论是父进程还是子进程。
进程地址空间的理解
其本质是内存中的一种数据结构 mm_struct
也就是意味着,结构体成员中存在不同的区域,而进程地址空间中存在不同的区域,静态区,代码区,数据区,堆区,栈区…
而这些被mm_struct
这个结构体进行维护,所以说,结构体中就存在stack_start、stack_end、heap_start、heap_end
差不多是这样的成员。
而理解上,进程地址空间是物理内存的一种度量。物理内存本身是不具备任何衡量标准的,只是代表一个个地址,而进程地址空间,就像是一把尺子上的刻度,将物理内存逻辑化、形象化、数据化了。
不仅仅是内存被划分区域了,对于磁盘中的程序,可执行文件(Linux中是ELF文件),也同样被划分了不同的区域,代码段,数据段…当这个文件被执行时,会根据规则,将这些分段全部链接在一起。
为什么会存在进程地址空间
- 有了进程地址空间提供的虚拟地址、虚拟空间,这样可以防止代码中有些操作直接访问物理地址,这样,哪怕是访问了未申请区域,也仅仅是虚拟空间中的越界,,不会存在系统界别的越界访问,在虚拟空间中,操作系统是可以掌握的,最多就是代码执行出错,系统杀进程或者进程崩了,不会影响到真实的物理内存。同时,页表可以对真实的物理地址进行保护,让真实的物理地址完全不可见,可以保护内存。
- 大一统的好处:让所有的进程都遵循同一个处理规则,所有的进程都有着相同的内存处理规则
- 每个进程都认为自己是独占内存的,更好的完成进程的独立性且合理的分配空间,方便操作系统合理的规划所有控件的调度。
因为一个进程不可能是所有的代码数据都同时加载到内存中,因为内存明显不够,且把CPU是一条指令一条指令的执行,一次性把所有代码和数据都加载到内存中,大部分的代码和数据并不会被执行,这样非常占用资源,所以,调度算法会处理,当需要执行的部分,才会进行加载,分配资源空间。这样操作系统可以很高效的分配资源。
将进程调度和内存管理进行解耦分离对于部分的进程调度,只有当执行到的时候才会为其分配资源,减少内存负荷。
创建进程
就是创建 tast_struct 、mm_struct、页表....