目录
一、进程理解和进程控制块
进程理解
Linux中的进程
查看进程
1.ps ajx 查看所有的进程信息
2. /proc/目录查看
系统调用接口
getpid() 获取进程的pid
编辑
getppid() 获取进程的父进程的pid
fork创建进程
fork用法:
fork原理理解:
二、进程状态
进程状态理解
操作系统进程状态分类
运行状态
阻塞状态
挂起状态
Linux进程状态与演示
R状态(运行状态)
S/S+状态(浅度睡眠状态, 属于阻塞状态)
D状态(深度睡眠状态, 属于阻塞状态)
T状态(stopped状态,属于阻塞状态)
编辑
t状态(tracing stop状态,属于阻塞状态)
X状态(死亡状态)
Z状态(僵尸状态)
孤儿进程
一、进程理解和进程控制块
进程理解
课本观点: 内存中的程序/运行起来的程序
由冯诺依曼体系结构知,程序在运行之前先要加载到内存中
而在所有程序运行之前,OS是最先运行起来的,所以当其他程序加载到内存后,OS就要对进程做管理了,如何管理?先描述,再组织!描述就是形成结构体对象,里面包含被管理进程的各种属性,比如id,数据或代码地址等等), 组织就是用数据结构将结构体对象链接起来,此时对进程的管理就转化成了对链表的增删查改,所以当有1个新的程序加载到内存时,只需要在数据结构(比如链表)添加节点即可
上述描述进程的结构体对象叫做进程控制块(pcb)
进程定义: 进程 = 可执行程序 + 内核数据结构(pcb等)
进程 vs 程序
1.程序在磁盘中,进程在内存中
2.进程比程序多了内核级别的数据结构
3.进程是动态的,程序是静态的
动态的意思是一个进程可能不会一次运行完,所以可能运行了一部分,cpu又去调度其他进程了,此时该进程就处于等待状态,一会运行完其他进程,cpu又继续调度该进程,该进程又运行起来了,这就是动态的含义!
Linux中的进程
Linux中进程的pcb具体是struct task_struct{ }, pcb是所有操作系统的进程控制块的统称,而struct task_struct 是具体的操作系统---Linux的进程控制块
Linux中是用双链表把task_struct组织起来的
不要认为一个进程的pcb只能在一张链表里, 可能同时在多个数据结构中
查看进程
1.ps ajx 查看所有的进程信息
2. /proc/目录查看
/proc/是一个动态的目录,存放了所有存在的进程,目录的名称就是用这个进程的id命名的!
/proc/里面的数字就是进程对应的pid
当一个程序运行起来之后变成进程, /proc/目录下就会添加对应该进程的目录
当终止掉进程之后,发现/proc/下对应的进程的目录也就没有了
下图是查看/proc/目录下一个进程对应的目录所包含的文件,这些文件包含的是内存中的进程的属性信息
删除进程对应的可执行程序之后,进程是知道的!!
cwd表示当前工作目录,简称当前目录,所以之所以不带路径时创建的文件默认位置就是cwd,默认情况下,进程启动所在的目录就是当前目录,也就是cwd的内容
所以如果改变了cwd的内容,那么"当前目录"也就变了!
系统调用接口
getpid() 获取进程的pid
哪个进程调getpid()接口,就返回哪个进程的pid
获取进程的pid有何用??比如可以通过进程pid杀死进程
getppid() 获取进程的父进程的pid
每次重新运行,进程的pid都在变, 而ppid始终不变,ppid的意思是parent process id, 也就是父进程id的意思,而这个父进程是bash,也就是命令行解释器
fork创建进程
之前启动进程是用./启动,我们还可以通过代码来创建进程,fork()是一个系统调用,用来创建进程
fork用法:
1.fork()之前只存在1个父进程,fork()之前的代码只会被父进程执行一次,而fork()之后的代码会被父子进程都执行一次
2.fork返回值不唯一,给子进程返回0,给父进程返回子进程的pid
3.创建子进程肯定是为了让父进程和子进程做不一样的事情,且由于fork给父子进程的返回值不同,所以我们可以通过if/else分支语句分流来让父子进程执行不同的代码
fork原理理解:
1. fork()创建的子进程有自己的代码和数据吗?? fork()之后的代码父子进程都会执行,那么fork()之前的代码子进程会执行吗?? 不会执行的话子进程可以看到fork()之前的代码吗?
①fork()创建子进程,并没有自己的代码和数据,OS只会为子进程创建pcb, 以父进程为模板,把父进程的大部分属性数据拷贝给子进程pcb, 少部分会修改(比如pid, ppid), 所以fork()之后的代码父子进程都会执行
②fork之前的代码和数据子进程也可以看到,但是程序在执行的有程序计数器(pc指针)会保存当前执行指令的下一条指令地址,当fork语句之前完毕后,程序计数器也来到了fork()下一行,所以子进程只会执行fork()之后的代码
2.fork给子进程返回0, 父进程返回子进程的id, 如何理解?
父进程:子进程 = 1 :n, 所以一个进程的父进程是唯一的,给子进程返回0就表示进程正常结束了,而1个父进程的子进程可能有多个,为了便于父进程为子进程的管理,所以要给父进程返回子进程的pid来唯一标识子进程
3.fork()之后,父子进程谁先运行??
父子进程谁先运行用户是不确定的,由各自PCB中的调度信息(时间片, 优先级等) + 调度器算法共同决定!再简单说,是由操作系统决定的!
4.如何理解fork()有两个返回值?一个函数为啥有两个返回值?
有两个返回值说明fork()函数里的return语句被执行了两次, 而一个函数要完成的核心工作都在return语句之前, 比如fork()函数要做的事情:找到父进程pcb, 为子进程pcb申请空间,以父进程pcb为模板创建子进程pcb, 让子进程pcb指向父进程的代码和数据,将子进程放入调度队列和父进程一样去排队,所以在return语句之前,子进程已经被创建好了,而一旦创建好了,父子进程就要执行一样的代码,return语句也是代码,所以子进程也要执行return语句,所以fork()函数会有两个返回值
5.如何理解同一个变量有不同的值??
---父子进程是独立的
杀死子进程后,父进程正常运行
杀死父进程后,子进程正常运行
可以看到,进程之间是独立的(包括父子进程),由于进程 = 代码/数据 + 内核数据结构,内核数据结构是独立的很好理解,因为父子进程都有各自独立的pcb, 代码和数据却是共享的,这如何保证独立性??
首先代码是只读的,不会修改,所以当父进程死亡后,OS发现子进程还在读取代码,所以不会回收代码,只会回收父进程pcb, 而数据是会被修改的,比如父进程是要依据变量是否为0来决定是否退出,而子进程就是要修改该变量的,这就无法独立了
所以结论就是代码是共享的,而数据父子进程必须各自私有一份,但并不是直接把父进程要用到的所以变量拷贝一份,否则可能造成空间浪费或创建子进程效率低下,OS是不会做低效的事情的,所以采用写时拷贝的方法,正常是共享同一份数据(浅拷贝),当子进程需要对数据做修改时,再拷贝需要修改的变量即可(深拷贝)
返回的本质是写入,id是父进程定义的变量,里面保存的是数据,所以返回的时候,发生了写时拷贝,所以一个变量有两个值,更深入的理解到进程地址空间再展开
同一个变量,同样的地址,但是有不同的内容,说明这里打印出的地址不是物理地址!
二、进程状态
进程状态理解
进程状态本质就是pcb中的一个字段/变量:int status, 所以我们可以通过宏定义将状态设置成不同的值来代表不同的状态, 例如:
#define NEW 1
#define RUNNING 2
#define BLOCK 3
所谓的进程状态变化就是修改整形变量的值(将pcb中的status设置成新值),例如:
pcb->status = NEW
然后就可以通过判断进程状态,将进程放入不同的队列当中, 例如:
if(pcb->status == NEW) pcb放入...队列
else if(pcb->status == RUNNING) pcb放入...队列
else pcb放入...队列
操作系统进程状态分类
运行状态
处于运行队列中的进程都是运行状态,不一定正在被cpu调度,而是"我已经准备好了,可以随时被cpu调度"
阻塞状态
当一个进程被调度器调度后放到cpu上去执行,在执行代码的时候或多或少代码中会有访问软硬件的操作,以访问硬件中的键盘为例,加入代码中有scanf/cin函数,等待用户去输入,但是用户就是不输入,此时键盘上没有数据,也就是键盘资源没有就绪,那么该进程就无法继续往下执行了!
而硬件资源没有就绪,操作系统一定是先知道的,因为操作系统要管理硬件资源,先描述再组织,用链表的形式将硬件设备对应的结构体变量链接起来,进行管理, 如下图所示,每种设备都有描述该设备属性的结构体对象,OS就将这些结构体变量链接起来,而每个结构体内部有1个成员变量是等待队列指针,用来链接因该设备资源没有准备就绪而导致处于阻塞状态的进程
进程状态改变的本质就是:
1.将进程pcb放入新的队列中(如运行队列或等待队列等)
2.改变pcb中的状态值(如将status修改成RUNNING或BLOCK)
把进程pcb从运行队列放入等待队列叫做该进程阻塞了,而把进程pcb从等待队列放入运行队列叫做该进程被唤醒了!
在用户层面,看到的进程阻塞的现象就是进程卡住了! 卡住的原因就是某种软硬件资源没有准备就绪,OS把进程pcb放入了等待队列并且把状态值修改了!所以我们看到程序一卡一卡的其实就是OS把进程pcb从等待队列到运行队列之间来回移动!
挂起状态
当一个进程被阻塞了,注定了硬件资源没有准备就绪,此时这个进程就无法被cpu调度,但是该进程的代码和数据都在内存中,但是cpu无法执行,如果此时内存资源已经严重不足,而OS是不允许任何低效或者浪费的行为发生的, 此时OS如何解决该问题?
因为该进程阻塞了,代码和数据无法执行或访问,所以OS会将当前的代码和数据交换到磁盘中,节省了一部分的内存资源,此时进程所处的状态就叫做挂起状态!
进程被挂起说明:
1.交换代码和数据是针对所有被阻塞的进程, OS会把所有设备的状态列表检测一遍,从而根据pcb中的status的状态找到被阻塞的进程代码和数据
2.将内存代码和数据交换到磁盘上,访问外设效率是很低,但是目前的主要矛盾就是OS内存资源不足,所以要先解决主要矛盾
3.置换代码和数据到磁盘的特定位置---swap分区(大小一般和内存大小一样或者是内存的2倍), 当底层资源就绪后,该进程又要被重新调度了,此时OS又会把swap分区的代码和数据重新加载到内存中
4.swap分区不宜过大,否则swap过大,导致swap很难被写满,OS一旦出现内存资源不足,就会很依赖swap分区,频繁的和swap分区进行代码和数据置换,导致整个系统的效率低下
5.我们此处说的挂起全称是阻塞挂起,也就是挂起的前提是进程阻塞了,所以OS在选择进程调度时优先选择阻塞的进程
6.还有一种挂起叫做就绪挂起,也就是进程没有正在运行就被挂起,这将会导致大量的进程被挂起,很多代码和数据都要被交换到内存中去,当OS要调度时,大量代码和数据又要加载到内存中,所以大量时间OS在和外设交互,这就要求OS在换入换出和效率之间取得平衡
Linux进程状态与演示
上述介绍的运行状态,阻塞状态和挂起状态不是具体的OS的状态,下面介绍Linux中的进程状态
R状态(运行状态)
R状态就是运行状态,表明该进程正在被调度
S/S+状态(浅度睡眠状态, 属于阻塞状态)
这个进程在运行,但是不是R状态,却是S+状态,对比处于R状态进程的代码,发现只多了一句printf语句,printf设计了访问硬件(显示器)的操作, 而cpu执行代码速度很快,OS快速的将进程pcb在运行队列和等待队列来回切换,显示器资源不一定准备就绪了,所以我们看到的进程状态大多是阻塞状态,偶尔会有运行状态
这个进程在运行期间,命令行bash无法运行,且ctrl+c可以终止该进程,这种进程叫做前台进程
下面是以后台方式运行进程,此时进程的状态就是S了,进程运行时命令行解释器也可以运行,因为是后台运行(就和你在windows上同时干多件事情,比如下载软件的同时看电影), ctrl+c也是终止不了的,此时可以 kill -9 进程id 来杀死进程
D状态(深度睡眠状态, 属于阻塞状态)
为了避免OS误杀系统内的进程,导致重要数据丢失,OS给进程设置了一种状态叫做深度睡眠状态,处于深度睡眠状态的进程OS也没有资格杀掉,所以深度睡眠就是为了防止进程向磁盘写入关键数据时而被杀掉导致数据丢失最后出问题;深度休眠状态用户一般见不到,要是用户见到了,说明几乎计算机快要挂掉了!
T状态(stopped状态,属于阻塞状态)
进程之所以会有暂停状态,是因为进程可能会做一些系统层面不允许的事情,比如说将来进程已经和显示器没有关系了,但是还强制向显示器写入,或者OS暂时不想让当前的进程访问某些资源,就会将进程设置成T状态!
进程原本处于S+状态,但是暂停之后处于T状态,而不是T+状态,因为本来就是前台进程,暂停之后需要在命令行上输入,所以系统将进程设置成了后台进程,否则无法继续输入
t状态(tracing stop状态,属于阻塞状态)
debug程序的时候,追踪进程,遇到断点,进程暂停了!
所以阻塞状态是某种资源没有就绪,可以是软件资源,可以是硬件资源,也可以是用户的指令,一个处于阻塞状态也可能是等待其他进程!
X状态(死亡状态)
死亡状态就是该进程运行完终止了或者被OS杀死了,该进程都进入死亡状态,而死亡状态是一个瞬时状态,一般很难查到!
Z状态(僵尸状态)
创建进程的目的就是为了让进程完成某项任务,所以当进程退出的时候,应该告诉父进程或者操作系统任务完成的如何了,如何告诉呢??
进程退出后应该把执行任务的相关情况写入到自己的pcb中(main函数的 return 语句就是在干这件事,0表示进程正常退出), 进程的代码和数据可以释放了,但是pcb还不能释放,因为父进程/OS还要去读取pcb的内容,要知道任务完成如何了以及进程异常退出的原因!
当进程的代码和数据已经释放但是pcb还没有释放,此时进程所处的状态就叫做僵尸状态!
当父进程/OS读取退出进程的pcb之后,pcb中的status状态就会被改成X状态,然后pcb被释放!
子进程已经退出,但是父进程没有释放,此时子进程的pcb就会一直占据内存,造成内存泄漏!所以解决办法就是回收子进程!
孤儿进程
如果父进程先退出了,父进程会处于僵尸状态吗?? 子进程又会如何呢??
可以看到,父进程退出后,并没有处于僵尸状态,因为父子关系是相对的,父进程也有它的父进程,就是命令行解释器,所以父进程退出后被命令行解释器进程给回收了,而子进程没有了父进程,PPID变成了1,表明被1号进程领养了,这种进程就叫做孤儿进程!
计算机启动后,最先跑起来的进程是0号进程,操作系统是1号进程,在操作系统加载到内存之后0号进程就释放了!
父进程先于子进程退出,子进程必须要被领养,否则系统内就会存在大量的孤儿进程,导致严重的内存泄漏!