多任务系统: 操作系统接管了所以的硬件资源,并且本身运行在一个受保护的的级别。所有的应用程序都以进程 的方式运行在比操作系统权限更低的级别,每个进程都有自己独立的地址空间,使得进程之间的地址空间相互隔离。CPU由操作系统统一进行分配,每个进程根据检测优先级的高低都有机会得到CPU,但是,如果允许时间超出一定的时间,操作系统会暂停该进程,将CPU资源分配给其他等待的进程。这种CPU的分配方式即所谓的抢占式 ,操作系统可以强制剥夺CPU资源并且分配给它认为目前最需要的进程。如果操作系统分配给每个进程的时间都很短,即CPU在多个进程间快速地切换,从而造成了很多进程都在同时运行的假象。目前几乎所有现代的操作系统都会采用这种方式,比如我们熟悉的 UNIX、Linux、Windows NT,以及Mac OS X等留下的操作系统。 ---- 《程序员的自我修养》
每个人在某一刻都有自己的状态,比如你现在在看这篇文章,那你的状态就是学习,别人在睡觉,那状态就是休息,而进程也有自己的状态,不同的状态展现了进程当前的情况。
我们先来了解一个常识,比方说,你现在正在使用抖音看直播,那对应的进程是否一直在CPU上运行呢?
答:并不是。
假如CPU上一直运行看直播这个进程,那其它的进程就没有办法使用CPU完成自己的程序,我们若是还想去下载一个软件、使用微信与朋友聊天或干其他的事情那只能等这个进程执行完后再去做,可事实并非如此,我们可以在刷抖音的同时下载软件,并在微信上去聊天,这是因为CPU可以在多个进程快速切换(CPU先运行一个进程之后在切换其他进程,如此运行进程,让每个进程都运行一点,在一个时间段内,代码都得以推进。CPU运行速度极快,我们的感官是感受不到的),造成多个进程同时运行的假象,这也就是我们文章开头提到的多任务系统
至此,我们了解到,进程在运行的时候是可以被操作系统管理调度的,那凭什么某个进程在某一刻可以被CPU运行,而其它进程不被运行呢?
这就取决于进程状态,进程不同的状态就体现了进程当前能做什么事情,不能做什么事情。所以进程处在什么样的位置取决于进程自己。
接下来,我们在来理解操作系统中状态的两个概念,目前具体的操作系统中进程的状态大都衍生于这两个概念,掌握之后再来看其他的状态可以变的轻松一点:
进程因为等待某种条件就绪,而导致的一种不推进的状态。
想要理解这个概念,我们需要知道,操作系统是如何看看待资源(磁盘、键盘、网卡、显卡等等)的。
首先,操作系统想要了解并调用某个计算机中的成员是通过 先描述,后组织 的方式,比如说进程,就是操作系统通过PCB(结构体,其中为程序的特性,Linux操作系统的PCB叫做task_struct )将运行到内存的程序变为一个个对象(一个进程还包括对应内存的代码和数据,我们这里不讨论这个),通过链表的数据结构,将对象存入,接着就是对链表的增删查改或其他操作,来管理进程。
其次,操作系统也是通过先描述、后组织 的方法来应对计算机资源的,将这些资源通过类似的结构体的形式创建出一个个对象,这些对象代表的就是一个个计算机资源,也将这些对象通过链表的数据结构,通过增删查改等方式管理对象从而管理计算机资源。
了解了这些基础,我们在通过两个例子,来看一下什么是进程的阻塞状态
当我们正在下载一款软件,但是突然断网了
当我们正在下载这款软件时,下载软件这个进程是在使用CPU运行的
当网线突然断掉,该进程就缺少了网络资源,CPU无法在进行运行该进程,于是操作系统便将其从CPU特定的某种队列 中将其拿出,放在了网络所对应的资源处(网卡)所维护的队列中,进行排队,等待网卡为该进程提供资源。
此时进程是不被CPU调度的,在用户看来就是卡住了,而这种等待资源的状态就是阻塞。
我们使用C语言编写一段程序,其中调用scanf函数需要通过键盘输入一个值
同理,在运行到scanf函数前,该程序所对应的进程通过CPU的运行来推进 代码的运行,但当运行到scamf函数需要从键盘输入值时,CPU就无法在将进程运行下去,此时它需要来自键盘的资源,操作系统也将其放到了所对应的资源所维护的队列中,进行排队。
此时CPU不在运行该进程,在用户看来这也就是程序卡了,而该进程的状态即为阻塞。
总结:
注意: CPU切换运行进程,这些进程都是可以被CPU调用的,阻塞是一个进程缺少一种或多种资源从而停止被调用,转去获取资源。
内存中只存储进程的PCB创建出的对象,而没有对应的代码和数据时,此时进程的状态就是挂起 。
当一个程序被加载到内存,操作系统将其以进程的形式运行后,在内存中,该进程包括PCB所创建的对象和对应的代码和数据,由CPU运行进程,推进代码的运行,当其缺少一种或多种资源后,将其从CPU特定的某种队列中提取,放入对应资源所在的队列,此时进程的状态为阻塞。
此时的进程处在阻塞的状态,需要等待资源,这就导致对应的代码和数据 ,占用着内存,却没有什么作用,此时操作系统会考虑将该代码和数据 转移到磁盘中,只保留进程的PCB,使内存可以存储更多需要处理的数据,此时内存中对应进程的状态就是挂起 (严格意义上讲,这种状态又叫阻塞挂起状态 )
当进程获取资源后再从磁盘内将对应的代码和数据转移到内存,重新调用CPU运行进程。
若当内存中有200个进程,其中只有30个使用CPU运行,推动代码运行,剩余的170个都处于阻塞状态,操作系统就会考虑将那170个进程中部分的代码和数据转移到磁盘,释放内存空间,提高内存利用率。
上面讲的是操作系统状态的理论部分,是宏观上操作系统状态最核心的概念,这些理论在任何一个操作系统中都是正确的,接下来,我们脱离这些抽象的东西,来看一下,在Linux系统中,进程具体的状态是如何变化的。
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务,task表示任务,在Linux操作系统中PCB是task_struct,而在Linux创建时的一段时期进程就被叫做任务,可见进程和任务的关系非常紧密)。
下面的状态在kernel源代码里定义:
/*
* 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 */
};
首先,在Linux中,进程的一部分是由task_struct结构体创建的对象,而task_struct是一个结构体,内部会包含各种属性,其中就有状态。
struct task_struct
{
int status;//进程状态的变化就是修改status整数
//其他属性
};
并不意为着进程一定在运行中,它表示进程要么是在运行中要么在运行队列(队列由操作系统自己维护)里。
下面,我们通过在Linux环境下运行一段代码来查看进程的状态(创建下面两个文件并编写对应的代码)
test.c
makefile
调用下面的指令,查看进程信息
ps axj | head -n1 && ps axj | grep mytest | grep -v grep
首先执行make指令,生成可执行程序,然后使用复制的会话运行程序
如下图,我们将程序运行,程序进入死循环,然后查看进程的信息
我们看到,查看到的进程的状态大多都是S+
,只有一个是R+
(这里先不管+
,我们只看字母,后面会说的)
首先,
S
是睡眠状态,是阻塞状态的一种这是因为,程序需要进行循环打印,需要频繁的访问显示器设备,而我是在在Xshell中使用centos7云服务器,其主机可能在离我千里之外的地方,printf打印结果想要出现在我的显示器上,需要有访问外设的行为 ,这导致进程在运行的过程中需要显示器资源,此时就处于阻塞 状态。
CPU的运行速度是很快的,而等待外设时间相对于CPU就慢的多,所以我们查看进程,看到大多数时候都处于S 阻塞状态,很少出现R 状态。
我们将test.c 文件修改如下,之后运行查看结果
之后在使用make指令,生成可执行文件,之后运行该文件,查看其进程状态
我们看到,此处查看进程,所有的结果表明该进程的状态为R
此时运行该代码,不需要任何资源,只是在用CPU进行运行,所以我们看到它的状态一直是R
在Linux系统当作,S休眠状态又叫可中断休眠 (可以被终止,可以暂停),意为着进程在等待事件的完成,本质就是阻塞状态。
我们将test.c 文件修改如下:
之后使用make指令,创建对应可执行文件,然后运行该文件
我们看到此时所处的状态为S状态(+ 之后讲)
此时进程运行到scanf,需要输入数据,进程需要键盘提供资源,该进程的task_struct对象在键盘资源所对应的队列中排队等待资源,处于阻塞状态
在Linux系统中也叫做不可中断休眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
这个状态主要应对特殊的情况,这里不好演示,我们通过下面的例子来讲解:
一个进程在正常的运行,某一刻它需要将一段数据存储在磁盘的某个位置,而磁盘的运行速度比较慢,它在存储的这段时间,该进程无法在继续推进代码,只能等数据存储完成,此时这个进程的task_struct在磁盘对应的队列中排队,CPU去执行其他的进程。
如果是正常的情况,当磁盘将数据存储后,该进程继续使用CPU运行,但在该进程处于阻塞状态时,内存已经无法在存储其他的数据,操作系统此时就会想办法释放一定内存空间内不被使用的数据,释放部分内存,而正在S 状态的进程就会被操作系统杀掉,若在进程被杀掉后磁盘存储数据时发生了一定问题(比如磁盘空间不足),磁盘无法解决他就会返回给对应的进程,告诉它数据存储失败,在由进程告诉用户存储数据失败,但此时进程已经被杀死,那磁盘只能拿着数据在风中凌乱
那对这些数据磁盘该如何处理,丢弃吗?若数据是用户的账号密码或银行的转账信息呢,自然不能丢。保留下来呢?对应的进程又没办法联系,不知道保留在哪里。
首先这种情况是可能存在的,而且这种情况十分危险的,想要解决它只能让操作系统不去杀死进程,这就需要为该进程复以新的状态D 状态。
可以通过发送 SIGSTOP 信号给进程来停止(T)进程(用户想要让一个进程暂停)。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。 T状态也是一种阻塞状态。
我们将test.c 文件修改如下:
想要让一个进程处于T状态需要向其发送 SIGSTOP 信号,而该信号是通过kill
指令发送如下:
使用kill -l
查看 SIGSTOP 信号对应的序号
对应序号为19,向一个进程传递该信号使用kill -19 进程对应PID
的方式。
之后使用make指令,创建对应可执行文件,然后运行该文件,向对应进程发送 SIGSTOP 信号,查看进程状态
若我们要该进程继续运行,需要向前发送 SIGCONT 信号,同样使用kill
指令发送
使用kill -l
查看 SIGCONT 信号对应的序号
对应序号为18,向一个进程传递该信号使用kill -18 进程对应PID
的方式。
我们看到,进行继续运行,此时的状态为S 后面的**+** 没有了
其中进程状态后有**+** 表示进程在前台运行,这样的状态下,我们可以使用Ctrl + c 来关闭进程,
而没有**+** 表示进程在后台运行,此时我们可以正常执行shell指令,但不能在使用Ctrl + c 关闭进程
想要关闭该状态下的进程,我们有两种方法,一是需要向该进程发送 SIGKILL 信号
对应的序号为9,向一个进程传递该信号使用kill -18 进程对应PID
的方式。(在我的操作系统中需要在使用Ctrl + c 才能看到命令行,而有的是不用的)
二是直接使用下面的指令来关闭该进程
killall 进程名
注意:
在我们调试代码时,让程序在断点处停止,本质就是暂停状态,而这种状态就是追踪式暂停。
如下图,我们使用gdb设置断点并运行,之后查看进程的状态
一个进程为gdb调试对应可执行文件的进程,第二个为我们写的程序对应的可执行文件在内存中对应的进程,此时这个进程的状态为t 表示该进程为暂停状态
所以我们打断点,调试时在断点处暂停,本质就是gdb向目标进程发生暂停信息,使其暂停
这个状态表面进程已经死亡,进程中的PCB、代码和数据都已经被回收,你不会在任务列表里看到这个状态。
虽然这个状态我们看不到,但有一个和它相关的状态,即Z状态–僵尸状态 。
进程是干什么的,进程是为我们解决事情的,既然是解决事情,那必然会有结果的产生,我们要不关心结果,要不不关心,不关心结果,进程运行完也就完了,要是我们关心结果呢?
- 僵尸进程是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码(使用wait()系统调用获取。返回代码即返回时的状态,是运行成功后正常结束还是被OS杀死或是其它)时就会产生僵尸进程。(一个进程具有独立性,我们没有办法将其中的数据返回给父进程)
- 僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
我们修改test.c文件如下
该程序运行后会产生两个进程,一个父进程,一个子进程,我们杀死子进程,父进程是不会读取到子进程状态的,此时子进程就属于僵尸状态,如下:
我们看到,子进程被杀死后,是不会直接被回收的,而是由父进程接收,状态为Z ,方便获取返回代码
- 父进程先退出,子进程就称为“孤儿进程 ”
- 孤儿进程被1号init进程领养,由init进程回收
我们通过下面的实验来证明孤儿进程的情况
在myprocess.c 文件中编写如下程序
该程序功能为创建子进程,并于父进程一起运行,当父进程在循环内运行6次后便会退出,只剩下子进程,此时我们在来查看子进程的情况
在makefile 文件中编写如下代码
通过make指令快速创建可执行文件myprocess ,其中**$@** 表示依赖关系中冒号左边的目标myprocess,$^ 表示依赖关系中冒号右边的依赖myprocess.c
当生成的可执行文件运行后,使用下面的指令,时其在页面循环打印出对应的进程情况
while :; do ps axj | head -n1 && ps axj | grep -v grep | grep myprocess; sleep 1; echo " -----------"; done
执行结果如下图:
通过上面的图画,我们看到,当父进程退出后,只有子进程在运行,而子进程对应的PPID由原本的5890变为1
我们知道一个进程退出后,若其父进程没有接收其返回代码,该进程会变为僵尸进程,而我们写的代码运行后产生了父子进程,父进程循环6此后退出,为什么没有变为僵尸进程呢?
首先,父进程5890(PID)也是有父进程的,它的父进程就是bash(命令行解释器),虽然父进程5890没有接收其子进程的返回值(我们没有写让父进程5809接收),但是bash3200却自动接收了父进程5890的返回代码,处理其僵尸状态,使进程5890被回收。
其次,我们看到,当父进程5809退出后,子进程5891由进程1领养,这个进程1就是init,也可以看作是操作系统领养了子进程5891,这种被领养的进程就称为孤儿进程。
那为什么父进程退出后,其子进程变为孤儿进程被操作系统领养呢?
如果不领养,这个子进程就没有父进程,在其退出是,就会变为僵尸进程,并且没有父进程将其回收,造成内存泄漏,所以子进程必须被领养。
注意:
父进程退出后,子进程变为孤儿进程,其的状态变为S 没有**+** ,表示其在后台运行,想要杀掉该进程只能是否两种方法,来完成对该进程的退出
//方法1 kill -9 进程PID //方法2 killall 进程名
注意:方法2中进程名表示运行的可执行文件名,不是PID
- CPU资源分配的先后顺序,就是值进程的优先权(priority)
- 优先权高的进程有优先执行的权力。配置进程优先权对多任务环境的Linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体的性能。
为什么要有优先级?
因为CPU资源有限,一台普通的电脑上CPU是4~8个,而要执行的进程少说也要20个以上,所以要让重要的进程优先执行,保证利益的最大化。
在Linux或unix系统中,用ps -al
指令则会类似输出以下几个内容:
其中,重要信息如下:
- UID:代表执行者的身份,用户标识符
如上图,当我们查看当前文件下的信息时,可以看到两种表示文件所属用户或所属组的方式,其中,用户使用用户名来标识彼此,而操作系统使用用户标识符(UID,数字)来标识用户
- PID:代表这个进程的代号
- PPID:代表这个进程是由那个进程发展衍生而来的,亦即父进程的代号
- PRI:代表这个进程可被执行的优先级,其值越小越早被执行
- NI:代表这个进程的nice值
调整方法非常多,可以使用代码去调整,也可以用指令去调,这里我们讲一下使用
top
去调整进程的优先级
首先,我们编写如下test.c 文件
修改makefile 如下
使用make mytest
指令生成可执行文件mytest,之后按照下面的步骤做
ps -la
查看对应进程的PIDtop
指令打开top编号 | 名称 | 功能 | 快捷键 |
---|---|---|---|
2 | SITINT | 程序终止信号,用于通知前台终止进程 | Ctrl+c |
3 | SIGQUIT | 与SIGINT相似,进程终止后会生成文件core | Ctrl+\ |
9 | SIGKILL | 强行终止某个进程,该进程不能被封锁 | |
18 | SIGCONT | 恢复执行被SIGSTOP或SIGTSTP信号暂停的进程 | |
19 | SIGSTOP | 通知操作系统停止进程的运行,该信号不可忽略 | |
20 | SIGTSTP | 暂停进程,但该信号可以被处理和忽略 | Ctrl+z |