进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。由于CPU不会直接和硬盘打交道,但程序是存储在硬盘上的,所以需要将程序先加载到内存中,再由CPU去调度,加载到内存中的程序就可以称为进程。在将程序加载到内存时,需要对程序进行管理,则就有了进程控制块-PCB的概念
进程控制块PCB:操作系统中用于描述进程的工具,其中包含的是进程属性的集合;Linux操作系统下的PCB是 task_struct的结构体。也就是说,CPU在调度时并不是直接去调度加载到内存中的程序,而是操作系统会把程序用一块新的空间利用PCB的结构将程序的属性等封装起来,CPU直接访问的是该程序对应的PCB结构体。
内存会将所有进程组织起来管理,也就是会将所有运行起来的进程都已task_struct链表的形式存起来,CPU就可以找到并且遵守队列的先进先出的性质
当程序运行起来我们就可以去查看对应的进行了
ls /proc
ls /proc/(对应的进程id)
可以通过上面两条指令去查看,第一条是查看当前所有的进程
这些数字就是进程对应的id,同样的我们可以通过指定的id去查看对应的进程
除了这个,当我们自己写好了程序运行起来时,我们也可以去获取我们自定义的程序的属性
ps axj | grep 进程名
现在我编写了一个死循环的程序,让其一直运行,然后我再查看这个进程,就可以查看到其属性了
fork可以用来创建一个新的进程,**其会有两个返回值,如果其返回值为0则创建的进程为子进程,大于0则为父进程。**则可以通过if分流查看其创建情况
这时又可以用到两个函数,一个为getpid用来获取当前进程的id值,另一个getppid用来获取当前进程的父进程的id值。
可以看到运行之后,创建出来的子进程对应的父进程就是创建出来的父进程。
像现在我们自定义的这些普通的进程都是可以直接[CTRL C]去结束进程的,当然我们也可以使用kill命令去杀死进程。
kill -9 进程id
可以看到kill命令还有很多种操作,不止是-9一种,需要用到的时候变化一下即可
不同的操作系统可能状态的名称不一样或者也会有不一样的状态,当从普适的操作系统进程状态来讲会有以下几种状态:运行,阻塞,挂起,新建,就绪,等待,挂机,死亡。
其中 运行,阻塞,挂起比较难理解
操作系统为了更好更合理的去分配CPU和各种硬件资源,为了更好的调度,所以会为CPU创建一个进程队列。当某一个进程进入了CPU的运行队列时,就可以说其已经是运行状态了,而并非等其真正运行起来才算是运行状态。
由于每一个硬件设备一次也只能执行一个进程,所以当需要访问硬件的进程和不需要访问硬件都全部在CPU的运行队列中时,可以理解为忙不过来,那这个时候操作系统就会将需要访问硬件的进程的PCB剥离出来,将该PCB放在硬件的等待队列中去等待。
那么这种等待的状态就是阻塞,其本质就是将该进程从CPU的运行队列中剥离出来,等待该进程获取到硬件资源时再将该进程调回CPU的运行队列
当一个进程在阻塞状态时,其代码和数据仍然会在内存中,那这个时候如果阻塞的进程过多时内存的空间就太浪费了,所以此时操作系统就会将阻塞的进程的代码和数据拷贝一份到硬盘中,然后释放掉内存中的那部分空间,从而减轻内存的空间压力。这种状态下的进程就叫做挂起状态。注意挂起进程并不会释放进程,进程对应的代码和数据仍然有备份在硬盘中
先来看看Linux内核中对状态的定义
/*
* 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 */
};
当一个进程进入了运行队列时,其就是运行状态了,我们可以查看进程的属性
ps axj | head -1 && ps axj | grep 进程名
可以看到此时进程的状态显示R+也就是运行状态
这个睡眠状态和阻塞状态是一样的,也就是需要等待某种硬件资源时,在硬件的等待队列中等待的时候
可以看到此时的进程是属于S+状态的,那么为什么会这样呢。因为printf函数需要等待显示器硬件的资源,CPU的运行速度比硬件的速度快太多了,所以99%的时间进程都是在等待的状态,因此我们大部分都只会看到进程是在睡眠等待的状态
如果当前的进程太多了,挂起也解决不了内存空间饱满的时候,操作系统就会自动的杀掉某一些进程,但是这里就会出现一些问题。因为操作系统杀死进程是不会根据用户意愿的,所以难免就会杀死一些比较重要的进程,当进程被杀死后就不可以恢复了。
那为了应对这种情况,Linux就设定了深度睡眠状态,处于深度睡眠状态的进程,是不可以被用户和操作系统杀掉的,只能通过自己醒来或者断电的操作才可以杀掉
深度睡眠一般只会出现在高IO的情况下
根据kill命令的选项 我们可以通过**-19来使得进程变为T状态,通过-18**让进程恢复运行状态
当我们让该进程暂停后恢复,可以发现进程此时的状态是R而不是**R+**了,这里要注意:当进程状态有+号时代表着此时进程时在前台运行的,我们可以利用CTRL C去杀死进程也可以通过kill;但是当进程状态没有+号时代表着后台运行,此时只能通过kill杀死进程
追踪暂停状态是一种特殊的暂停状态,进程处于此状态表示该进程正在被追踪,比如调试进程时
死亡状态代表着一个进程结束运行
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不取,那子进程就一直处于僵尸状态了。现在我们来实验一下,将父进程的子进程杀掉
像这种情况,父进程没有回收子进程的数据,就会出现僵尸状态。
注意僵尸状态会导致内存泄漏,所以我们要避免僵尸进程的出现
孤儿进程是指父进程提前退出后,子进程被操作系统领养的一种情况,被操作系统领养的进程就被称为孤儿进程
以上的情况就是孤儿进程,将子进程的父进程杀掉后,子进程就会直接被id为1的进程领养,也就是操作系统。
像我们的现实生活一样,我们去排队打饭的情况也是一个具有优先级的情况,先来的人优先级就比后来的人优先级高,所以先来的人先打饭。进程也是一样,优先级高的进程先执行。
Linux 中优先级通过两个变量 PRI和 NI来表示,每个进程默认的 PRI 都是 80,NI 都是 0,如果我们先修改某个进程的优先级,我们修改的是NI值,也就是说进程的优先级等于PRI + NI。但是需要注意,不管NI多大,进程的优先级始终在**[99,60]**这个区间内
如果想要修改优先级,首先top指令(可能需要sudo提高权限)------输入r-------输入需要修改的进程id--------输入想要修改的NI值
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
一般如果一个计算机只有一个CUP时,不可能会是等待一个进程执行完再执行下一个进程的,那如果是这样我们平常使用电脑就毫无体验感了。所以进程是需要切换的,每一个进程在CPU执行时都会有一个属于自己的时间片,过了这个时间片CPU就会切换进程执行。
CPU本身会有一套的寄存器硬件,每当CPU切换进程时,需要对进程的数据进行上下文保护和上下文恢复,所以当进程被切换时其数据就会被寄存器保护起来,等下次再执行时寄存器又会把数据恢复。要注意寄存器是被所有进程共享的,所以寄存器中的数据只属于该进程,上下文保护时保护的是寄存器里面的数据而不是寄存器