目录
一、冯诺依曼体系结构
二、进程
1、关于进程
关于PCB结构体
2、查看进程
①ps
②/proc
3、getpid
4、getppid
5、fork()
fork基本用法
6、进程状态
7、孤儿进程
8、进程优先级
修改nice值:top
9、进程的几个特性
冯诺依曼体系结构如下图所示:
而上面的输入设备,输出设备,存储器,运算器,控制器又是什么呢?
存储器:内存
输入设备:键盘,摄像头,话筒,磁盘,网卡...
输出设备:显示器,音响,磁盘,网卡...
CPU(中央处理器):包括运算器、控制器
运算器:算术运算,逻辑运算
控制器:CPU是可以响应外部事件的,协调外部就绪事件
首先说说运行速度:
CPU&&寄存器 > 内存 > 磁盘/SSD(固态硬盘) > 光盘 > 磁带
那既然CPU运行速度是最快的,为什么冯诺依曼体系结构中还要在输入输出设备中间用存储器呢,直接用CPU处理输入输出岂不是更好用,速度更快。
当然可以这样用,但是如果真的这样使用,就会造成昂贵的成本,原本几千快的电脑,如果全部使用CPU处理各种数据,可能会花费十几万甚至几十万的成本,这是十分不划算的,并且我们大众需要的是性价比高的,即价钱便宜性能好,所以这里引用了内存即存储器,速度比磁盘快,价钱也比CPU便宜,可以很好解决这方面问题。
存储器可以将数据缓存在存储器中,进而CPU在存储器中读取数据时,不用再访问外设,可以在一定程度上提高效率
CPU读取数据(数据+代码),都是从内存中读取的。站在数据角度上,CPU不和外设直接交互
CPU要处理数据,需要先将外设中的数据加载到内存。站在数据角度上,外设只和内存直接交互
有句话是:程序要运行,必须先被加载到内存中
现在我们可以用冯诺依曼体系结构知识解释,因为CPU读数据都是从内存中读取的,所以程序要运行,必须先被加载到内存中
下面举个例子,能更深入的理解冯诺依曼体系结构的知识:
小王和小张在两个不同的城市,他们用QQ联系,那么QQ发送一句话的过程大概是怎样的呢?
小王通过输入设备(键盘)将数据输入到存储器(内存)中,CPU通过内存分析数据后,再将数据写回给内存,之后内存再将数据给到输出设备(网卡),接着就发送到网络(后面会说到,这里只涉及冯诺依曼相关知识),然后到了小张那边用输入设备(网卡)接收消息,把数据从网卡读到内存里,继续刚刚的过程,将数据交给CPU计算分析,然后CPU再将数据写回内存,最后再将数据刷新到输出设备(显示器)上
具体步骤如下图所示,紫色箭头就是上述全部过程:
操作系统:先描述,再组织
Windows下,我们自己电脑上启动一个软件,本质就是启动了一个进程
Linux下,运行一条命令,在运行的时候,其实就是在系统层面创建了一个进程
Linux是可以同时加载多个程序的,是可以同时存在大量的进程在系统中的(OS里就是内存中)
下面对进程下定义:
进程 = 对应的代码和数据 + 进程对应的PCB结构体
当把数据加载到内存中,就变为进程了,这时会生成一个struct结构体PCB,包含了该进程的所有属性,这时对进程的管理,就变为了对进程结构体PCB链表的增删查改
PCB全称是:process control block,在不同的操作系统中,PCB的名字不同
在Linux中,PCB结构体是task_struct,它会被装载到内存中并且包含进程的属性信息
task_struct的内容:
①标识符:描述进程的唯一标识符,用来区分其他进程
②状态:任务状态,退出代码,退出信号等
③优先级:相对于其他进程的优先级
④程序计数器:程序中即将被执行的下一条指令的地址
⑤内存指针:包括程序代码和进程相关的数据的指针
⑥上下文数据:进程执行时处理器的寄存器中的数据
⑦IO状态信息⑧记账信息等等
ps是查看进程的命令
首先打开两个页面,都是我自己路径下
在test.c文件里写了死循环打印hello world的程序,然后编译运行
在左边的页面内输入ps,ps只能查看当前终端的进程:
ps axj是查看所有的进程:
而我们要查看的是自己刚刚的进程,所以输入:ps axj | grep 'test'
再把头部带上,即输入:ps axj | head -1 && ps axj | grep 'test'
这样查看,我们可以发现,./test是在运行的,所以有./test进程,又因为grep在查看进程,所以还有一个grep进程
头部有一个PID,代表进程ID,它代表当前进程的ID值
这时我们将右边的死循环Ctrl + c终止了,这时再执行ps axj | head -1 && ps axj | grep 'test'命令,会发现只有一个grep进程了
有一个系统文件夹是/proc,存放的是进程信息
这时我们运行./test,然后查看进程:
发现test的进程PID是21599,然后我们在proc这个系统文件夹中也可以看到当前的进程PID21599(-l是显示属性-d是只显示路径)
而当我们Ctrl + c终止死循环进程时,再查看就没有这个进程信息了
所以我们可以发现proc这个文件夹是动态的
getpid是获得我们的进程PID的,下面是用man查看getpid的介绍
需要包两个头文件,其中返回值pid_t其实就是无符号整型
接下来将test.c改造一下,使用getpid这个函数:
然后右边窗口运行test后,左边窗口查看进程:
可以发现getpid获得的PID和我们ps查看的PID相同
而我们如果想终止这个死循环,可以Ctrl + c终止,也可以kill -9 PID终止
Ctrl + c终止:
kill -9 PID终止:
getppid是获得父进程的PID
下面是通过man查看getppid
和getpid一样,同样是包含两个头文件,同样返回值是pid_t
接下来将test.c里改变一下,将ppid也打印出来:
运行出来的PID和PPID与我们grep查看的一样
我们终止后再重新运行,如下图:
却发现,PID变了,但是PPID却一直是21147,那我们用ps查看一下21147,如下图:
我们发现PID是21147的进程是bash,而前面我们说过,bash的shell外壳程序
所以在执行命令时,所有的命令最终都是以bash的子进程的方式去运行的
fork()是创建一个子进程
下面是man查看fork
fork在头文件unistd.h中
需要注意的是fork函数的返回值:
失败的时候:返回-1
成功的时候:①给父进程返回子进程的pid ②给子进程返回0
下面使用fork给大家示范一下返回值的场景:
在fork函数前面打印一下进程的pid,然后执行fork函数后,分别打印fork函数的返回值ret,与对应的pid和ppid
大家观察可知,fork函数执行前,是父进程在执行,pid是22942,执行fork函数后,成功的话会有两个返回值①给父进程返回子进程的pid ②给子进程返回0
所以ret是0的就是子进程,而子进程的pid是22943
ret是子进程pid(22943)的就是父进程,而父进程的pid和fork执行前的pid一样,都是22942
通过上面的示例,可以更清楚的理解fork的两个返回值
fork之后代码是父子进程共享的
下面例子可以清楚显示:
在fork函数后,有if和else if语句,分别判断fork的两个返回值,且都是死循环,下面看结果:
一个父进程一个子进程,交叉进行死循环,让父子进程在fork函数后面执行不同的代码,所以fork之后有两个不同的执行流,可以有两个while(1)同时执行,而ret这个返回值,在父进程里面是子进程的pid,在子进程里面是0
并且可以清楚观察到,子进程的ppid就是父进程的pid
一个父进程可能有多个子进程,而一个子进程只能有一个父进程
所以父进程:子进程 = 1 :n
所以给父进程返回子进程的pid用于区分子进程,而子进程只有一个父进程,并不需要区分,所以返回0
而在创建一个子进程的时候,操作系统需要新建一个task_struct(PCB结构体),而这个新建的task_struct内部属性大部分是以父进程为模板的
fork函数实现时,运行到return前面就说明核心代码已经执行结束了
这时子进程已经被创建出来了,可能会已经被放到运行队列尾部了
所以这时父子进程一起执行,父进程被调度时会return,然后当子进程被调度时,return也会执行,所以fork函数会有两个返回值
操作系统进程的状态:
新建:PCB资源刚分配好,还没有放到运行队列中就是新建状态
运行:task_struct 结构体在运行队列中排队,就叫做运行态
阻塞:等待非CPU资源就绪,这个状态就叫做阻塞状态
挂起:当内存不足的时候,OS通过适当的置换进程的代码和数据到磁盘,此时进程的状态就叫做挂起状态
退出:退出状态
Linux操作系统进程的状态:
R运行状态:表明进程要么在运行中,要么在运行队列里,对应上面的运行态
S睡眠状态:也可以称为可中断睡眠,表明进程在等待事件完成,对应上面的阻塞状态,
D磁盘睡眠状态:深度睡眠,不可以被中断,不可以被被动唤醒
t调试状态:调试时的状态
T暂停状态:单纯的暂停状态
X终止状态:瞬时性非常强
Z僵尸状态:一个进程已经退出,还不允许被OS释放,处于一个被检测状态;维持该状态,是为了让父进程或OS来进行回收
只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程就进入僵尸状态
僵尸进程会造成资源泄露,必须使用wait/waitpid接口进行等待处理
子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程称为“僵尸进程”
而如果父进程先退出,子进程还在运行,子进程就被称为“孤儿进程”
孤儿进程会被“领养”,被1号进程领养(init,即系统本身)
之所以要被领养,因为父进程退出后,未来子进程如果退出了,父进程早已不在,没有人来回收它,需要领养的进程进行回收
下面是test.c的代码,父进程执行三次就结束,而子进程一直死循环:
继续分为左右两个窗口,右边窗口运行,左边窗口观察进程:
可以观察到,右边开始运行以后,ps观察进程,蓝线划出来的就是子进程,这时父进程还没有退出,子进程的ppid还是父进程的pid,等父进程三次运行完退出后,子进程的ppid变为1,表示被1号进程领养
之所以要有优先级,是因为CPU是有限的,但是进程太多了,所以需要用这种方式竞争资源
而优先级就是确定谁先获得资源,谁后获得资源
优先级是选择将谁放在CPU上执行的重要调度指标
Linux中具体的优先级做法:
优先级 = 老的优先级 + nice值
下面具体演示:
创建两个窗口,右边窗口先写一个test.c,代码是死循环打印hello world,再加上打印pid的值,然后运行;在左边窗口观察进程信息,输入 ps -al | head -1 && ps -al | grep test:
在还没有运行程序时:
运行后:
可以看到进程信息中pid和打印的pid相同,所以就是正在运行的进程,并且蓝色圈圈出来的PRI就是优先级,NI就是nice值
PRI表示这个进程被执行的优先级,值越小越早被执行
NI是nice值,表示进程可被执行的优先级的修正数值
所以加入nice值后,新的优先级 = 老的优先级 + nice值,当nice值为负数时,该程序的优先级值会变小,优先级会变高,即越快被执行
首先重复刚刚的操作,可以看到当前是PRI是80,nice值是0
top相当于Windows中的任务管理器
再打开一个窗口,输入top
然后输入r
相当于告诉你renice就是修改nice值,而刚刚运行的pid是27103,所以再输入27103
现在就可以输入nice值,我们输入30,再用ps打印观察进程信息
可以发现优先级值变为了99,而nice值变为了19,那为什么不是30呢
因为Nice值的取值范围是-20 ~ 19,超过19自动当做19
所以Nice值为19,而优先级值也由刚刚的80变为了99
而我们如果想将优先级调高点,那就是nice值调低,将nice值输入-100,必须以sudo运行top:
这时nice值变为最小值-20,同时PRI也由80变为60,为什么不是刚刚的99-20变为79呢?其实每次设置优先级值都是由80开始设置,即从进程最开始的优先级开始设置
①竞争性:进程数量多,而CPU资源很少,所以进程直接是有竞争属性的,所以就有了优先级
②独立性:多进程运行,要独享各种资源,多进程运行期间互不干扰
③并行:多个进程在多个CPU下分别、同时进行运行,称之为并行
④并发:多个进程在一个CPU下采用进程切换的方式,在一段时间内,让多个进程都得以推进,称之为并发(里面有时间片、抢占与出让的概念)
时间片:每一个进程在CPU执行时都会有一段时间,比如10ms,运行完就该下一个进程进CPU
抢占与出让:有可能进程a在CPU中运行的时间是10ms,而5ms就进行完了,这时进程a就相当于在出让CPU资源;而抢占就是指优先级高的可以抢占优先级低的进程的CPU资源
切换:
CPU中有很多寄存器,如果此时进程a正在运行,那么CPU中的寄存器里面,保存的一定是进程a的临时数据,而寄存器中存储的进程a的临时数据,就叫做进程a的上下文数据
当进程a由于并发、时间片的约束,暂时被切下来时,需要进程a带走自己的上下文数据
带走进程a的上下文数据暂时保存的目的是:下次回CPU运行时,能够重新恢复上去,就能继续按照之前执行的逻辑继续向后执行,就如同之前没有中断过一样
CPU中的寄存器只有一份,但是上下文数据可以有多份,分别用于对应不同的进程