继上回书Linux进程概念(一),我们初步了解了进程的一些相关概念以及如何创建和查看进程,对其原理和一些进程现象进行了分析和解释,那么今天,我们学习下一个进程知识-进程概念。
目录
1.操作系统的进程状态
常见的进程状态
1.运行状态
2.阻塞状态
3.挂起状态
2.Linux中的进程状态
进程状态引入
前台与后台进程
浅度睡眠与深度睡眠
暂停/继续状态
死亡状态与僵尸进程
孤儿进程
3.Linux进程优先级
基本概念
查看系统进程
PRI and NI
修改进程优先级
其他的概念
1.操作系统的进程状态
进程状态是描述进程当前状态的概念。在操作系统中,每个正在运行的程序都是一个进程,而进程状态用于跟踪和管理进程的执行情况。进程状态可以告诉我们进程当前在做什么以及它是否需要执行或等待某些事件。进程状态,其实就是PCB中的一个变量(status整数变量),这个变量可以记录不同的整形值用以表示不同的状态,所谓的状态变化,其实本质上就是修改了变量值。
常见的进程状态
1.运行状态
运行队列与运行状态:
运行队列是操作系统中存放处于就绪状态的进程的数据结构,用于调度程序选择下一个应该执行的进程。运行状态则是进程的一种状态,表示当前进程正在执行或等待CPU执行。
运行队列和运行状态是两个不同的概念,但它们之间存在关联:
运行队列:运行队列是一个数据结构(通常是队列),用于存放处于就绪状态的进程。当进程准备好执行(就绪状态)时,它会被添加到运行队列中等待调度。运行队列中的进程可能包含不同的优先级和调度策略,调度程序会从中选择合适的进程来分配CPU时间片进行执行。
运行状态:运行状态是进程的一种状态,表示当前进程正在执行或等待CPU执行。当调度程序从运行队列中选择一个进程时,该进程的状态将从就绪状态转变为运行状态,它将占用CPU资源并开始执行其指令。在多任务操作系统中,可能有多个进程同时处于运行状态,通过操作系统的时间分片机制,CPU时间会被分配给不同的进程。
总结来说,运行队列是一个存放就绪进程的数据结构,用于调度程序选择下一个应该执行的进程。运行状态是进程的一种状态,表示当前进程正在执行或等待CPU执行。运行队列中的进程可以处于就绪状态,等待CPU执行,一旦选择执行,进程的状态将变为运行状态。
2.阻塞状态
阻塞状态,也称为等待状态,是进程可能处于的一种状态。当进程暂时停止执行,因为它正在等待某些事件的发生时,就会进入阻塞状态。进程在阻塞状态时,通常是因为以下原因之一:
I/O操作:进程发起了一个I/O操作(如读取文件、发送网络请求等),但需要等待I/O操作完成才能继续执行。在这种情况下,进程会进入阻塞状态,等待设备或操作系统完成请求的I/O操作,常见的比如我们经常在scanf或者读取磁盘文件等访问外设的操作。
等待资源:进程可能因为等待某个资源的可用性而进入阻塞状态。例如,进程可能等待获取一个共享的锁或等待其他进程释放某个资源。
等待信号:进程可能在等待接收一个特定的信号。例如,进程可能等待某个特定的信号量或信号来继续执行。
访问的资源有没有就绪,或者具不具备访问条件,操作系统怎么能知道呢?也就是说,操作系统是通过什么措施来管理各个底层硬件(包括各种外设)的呢?其实,每个设备都有对应的设备结构体,操作系统通过“先描述,再组织”的策略,将各设备通过链表连接起来,形成了设备链表。当运行队列中的进程由于访问外部资源等原因而停下来,操作系统就会将这个进程从运行队列中拿出来,放在对应的等待设备的等待队列中去,直至访问该设备结束并且成功取得所需的资源后,操作系统才会将其从对应的设备等待队列中放回到原来的运行队列继续排队,进程由于需要访问资源而从将该进程从运行队列中移出到对应设备的等待队列中,我们把该进程存在于设备的等待队列的状态称之为阻塞状态。
操作系统中会同时存在多个队列,比如运行队列,等待硬件的设备队列等,我们之前有提到,所有的进程都是采用双链表的形式链接起来的,但是我们可以单独设置一个结构体来表示进程链表,这样,我们就可以通过设置许多该对象的不同成员,用同一份task_struct来表示出不同的队列关系,
3.挂起状态
如果当前一个进程被阻塞了,那么就代表,这个进程在等待外部资源就绪前是不会被调度的,如果此时,恰逢内存资源空间已经严重不足了,那么操作系统就会将这个进程对应的代码段和数据段移动到外设(磁盘)中去,用来一定程度的缓解内存资源占用,这个过程就叫做挂起,而由于该进程是由于在阻塞状态中被挂起的,所以这个过程就叫做阻塞挂起。当进程被OS调度时,操作系统会将曾经被置换出去的代码和数据再重新加载回来以继续执行进程。
这个将内存中的阻塞进程的数据和代码文件置换到外设,是针对所有的被阻塞进程的,这样做会一定程度上导致拖慢操作系统的速度,但是这也是无奈之举,与其内存不足而导致操作无法进行,慢一点可以把损害降到更低。磁盘上的交换空间通常是一个预留给操作系统的特定磁盘区域,用于存储被挂起或交换出去的页面。当进程的页面被交换出内存时,它们的内容被写入交换空间,并在需要时可以再次加载到内存中。具体的交换空间位置和管理方式取决于操作系统的实现。在Linux系统中,交换空间通常是一个专门的交换分区(swap 分区)或交换文件(swap file)。在Windows系统中,交换空间称为页面文件(page file)。
拓展:为什么一些大型企业或者公司中,要求磁盘的swap分区尽量设置的较小一些,最大不超过内存大小?
性能优化:较小的交换分区可以鼓励系统操作系统更积极地使用内存而不是交换空间。内存访问速度比磁盘访问速度快得多,因此减少对交换分区的依赖可以提高系统的性能和响应速度。
成本控制:磁盘空间是一种有限的资源,较小的交换分区可以减少对磁盘空间的占用。对于大型企业或公司而言,管理和维护大量的交换分区可能需要更多的存储设备和成本,因此通过限制交换分区的大小可以控制成本。
防止过度交换:较小的交换分区可以防止系统过度依赖交换空间。过度使用交换分区可能导致系统性能下降,因为频繁的磁盘访问会引入延迟。通过限制交换分区的大小,可以鼓励系统更有效地管理内存,并避免过度交换的情况发生。
2.Linux中的进程状态
Linux操作系统作为一种具体的操作系统实现,自然也涵盖了操作系统基础中的各个方面。它实现了操作系统基础中描述的核心功能,如进程管理、内存管理、文件系统和设备驱动程序等。现在我们想要查看进程的不同状态,一个进程可以有几个状态(在 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 */
};
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠)也叫作浅度睡眠,进程可以被终止。
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),也叫作深度睡眠,在这个状态的进程通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可 以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
进程状态引入
下面,我们来快速的体会Linux操作系统的捕捉到的进程状态的过程:
下面是一段死循环代码,为的是时刻能看到这个进程的状态:
这里我们需要介绍一个进程监视语句脚本:
while :; do ps -axj | head -1 && ps -axj | grep mycode | grep -v "grep"; sleep 1; echo "##################"; done
//这种语句并不常用,所以我们只要知道查看进程的部分语句,也就是ps 语句部分即可,至于 grep -v “grep” 就是不显示grep进程,因为我们知道,grep命令本质上也是一个进程,在者就是echo可以打印符号作为监视过程之间的分隔行,以便于我们查看,while循环的用法了解即可,这里不做过多解释,其余的我们前面都有提到过
运行,我们查看监视窗口下的结果:
我们发现监视窗口显示同一个进程PID,说明此时我们只是在执行同一个进程,我们注意到STAT这一列其实就是进程状态表示列,我们发现,进程状态多数以“S+”居多,并且极小概率会出现“R+”的状态,这个S就代表当前进程状态是阻塞状态,R则代表运行状态。
这不对啊,我们写的明明是一个死循环,按理来说程序应该一直在运行状态,为什么打印出来确实大多数情况下在阻塞状态呢?
这是因为啊,printf语句的本质是向显示器打印语句,显示器是外设呀,所以这就相当于进程在等待printf向外设打印完语句,进程才能再次进入运行状态,而我们知道,访问内存的速度比访问外设的速度快的多,所以这也就会导致进程相对的大部分时间都在被printf语句所耽误,导致进程大部分时间都是在等待printf打印到显示器的阻塞状态下,只有极少情况下,操作系统能捕捉到进程正在运行队列中,所以才会造成这一现象,当我们将printf语句去掉,只剩下一个死循环,而while的本质是不断地使用cpu资源做检测,这样就可以正常看到进程一直在运行状态了。
前台与后台进程
在Linux中,前台进程和后台进程是指正在运行的进程的不同状态。
前台进程是指当前正在与用户交互的进程。当用户在终端上执行一个命令或启动一个程序时,默认情况下,该进程将成为前台进程。前台进程会占用终端,接收用户输入并将输出结果显示在终端上。在前台运行的进程通常会阻塞终端,直到它完成或被用户中断(例如通过Ctrl+C)。
后台进程是指在后台运行的进程,它们不会占用终端,并且不会接收终端的输入。后台进程通常用于执行长时间运行的任务或不需要与用户交互的程序。在启动进程时,可以使用特殊的技巧将其置于后台运行。例如,在命令行中,在命令的末尾添加一个&符号,即可将该命令置于后台运行。需要终止后天进程时,可以使用kill 命令强制将进程杀死。
我们将我们之前写的代码改为后台进程看看效果:
浅度睡眠与深度睡眠
浅度睡眠就是我们前面提到过的,浅度睡眠是指进程在等待某个条件满足时,仍然保持部分的活跃性,能够及时响应其他事件。在浅度睡眠状态下,进程仍然占用CPU资源,并能够快速响应其他事件。比如我们可以随时将其终止。下面,我们重点来看一下深度睡眠的形成原理,
深度睡眠,也就是我们前面的D磁盘休眠状态(Disk sleep) ,这里的深度为什么不是Deep,而是Disk(磁盘)呢?其实,这个深度睡眠就是专门针对磁盘来设计的,
我们来看一个小故事:
最终,操作系统和进程就在一起商量着,单独规定哟一种休眠状态,表示当前我这个进程正在往磁盘中写入关键数据,操作系统不能把其当做普通的阻塞进程杀掉,也就是D磁盘休眠状态(Disk sleep),该状态不可以被杀掉,操作系统也不行,一旦出现了深度睡眠,唯一的办法就是等待,等待其出来一个结果,甚至连关机都不行, 事实上,当进程在深度睡眠状态时,如果它所依赖的设备或资源出现异常,可能会导致进程无法正常唤醒或执行。例如,如果进程等待某个设备的输入,但设备出现故障或断开连接,进程可能无法恢复正常运行。某些操作系统中可能存在错误或缺陷,导致进程在深度睡眠状态下出现异常。这可能是由于操作系统内核的问题,或者与特定硬件或软件环境的兼容性问题有关。所以,深度睡眠状态某些时候可以说明我们的计算机出现了问题。
暂停/继续状态
在Linux系统中有许多的信号,我们可以通过命令 :
kill -l
来查看这些信号和对应的编号,每一个信号对应的编号,其实都是采用宏定义的方式定义出来的,比如我们熟悉的 9号就是杀死进程的信号。
我们现在来看的是第18和第19号的信号,他们分别代表的是 signal continue 和signal stop ,也就是继续和暂停信号,我们来简单的演示一下,
事实上,暂停状态和继续状态会自动让前台进程变为后台进程,并且会一直延续后台进程的状态不会再发生改变。
当我们在使用gdb进程系统调试的时候,也会导致程序进程进入一种暂停状态,只是这种状态下显示的进程个状态是 t 符号,而不是T,但是 t 和 T 是不分家的,这两个状态没有本质的区别。
初始状态,我们启动gdb,可以看到我们的监视窗口gdb前台进程已经启动,但是此时代码还未启动,
接着,我们在gdb中打上断点并开始尝试运行代码,发现代码进程以 “t” 状态加入进程监视窗口,
如果此时我们开始按行进行单步运行调试,此时程序进程会发生瞬间变化然后恢复暂停状态,
死亡状态与僵尸进程
在Linux中,"死亡状态"通常指的是进程的终止状态,也称为"终止状态"或"Terminated"状态。当一个进程终止时,它不再执行,并且释放了占用的系统资源。
当一个进程进入死亡状态后,它的对应的PCB和代码还有数据都要被从内存中释放掉。进程的终止状态可以通过"ps"命令的输出中的"STAT"列来查看。死亡状态是进程的终止状态,表示进程已经停止执行并且不再占用系统资源。进程可以通过正常终止、异常终止或被其他进程终止进入死亡状态。
僵尸进程是一种特殊的死亡状态,,通常以"Z"表示僵尸状态,它们已经终止,但其父进程尚未通过"wait"系统调用来获取其退出状态,这个wait我们后续还会提到。僵尸进程在系统中占用资源,因此它们应该被父进程处理并释放。
创建一个进程的目的,本质上就是为了完成某个任务,当这个任务被该进程完成或者意外终止时,我们的进程就该退出了,为了让创建该进程的父进程(也可以理解为操作系统)知道该子进程任务的完成情况,子进程在退出时,操作系统会将任务的完成结果信息(例如我们main函数的方返回值是0,0在Linux中就表示任务完成状态),写入到自己的PCB中作为退出信息的一部分,并且可以允许该进程对应的代码和数据立即进行释放,但是该子进程的PCB却不能被立即释放,在该子进程让其父进程或者操作系统读取到该进程的任务完成的返回信息之前,操作系统必须维护这个已经退出的进程的PCB结构(这个读取方式我们后续也会再提),此时,这个进程已经不能被被调度了(因为此时该进程已经没有对应的代码和数据了),这个状态算是终止状态但是也不能算彻底退出,我们就把这个状态叫做僵尸状态。
我们再来举一个例子,假如高速公路上一个人出了车祸导致意外死亡,此时他的尸体不会立即被送往火葬场火化,而是需要警察先来调查死亡原因和联系家属等等工作,此时这个人的状态就是僵尸状态。
当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程,僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
这里简单演示一个子进程退出等待父进程读取的代码:
其中,exit函数的作用就是退出一个进程:
运行这段代码和监视脚本,可以发现,当子进程结束退出后由于没有给其父进程传递返回信息变成了僵尸进程。
僵尸进程的危害
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。如果父进程一直不读取子进程的退出状态,子进程将一直处于僵尸状态,占用系统资源。 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要被维护,如果一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费,因为数据结构对象本身就要占用内存,同样的,僵尸进程也会造成内存泄漏内存泄漏问题。
孤儿进程
孤儿进程是指在父进程终止或者父进程提前结束时,仍然在系统中运行的子进程。孤儿进程的处理是由操作系统负责的,它将孤儿进程的父进程设置为init进程(进程ID为1的进程,这个进程其实就是操作系统)。当init进程接管孤儿进程后,它将会成为孤儿进程的新的父进程,并负责管理和回收这些进程。一旦孤儿进程终止,它的资源将被释放,并且其进程表项也会被清理。
我们就以上面的代码为例,现在来模拟父进程比子进程先退出的场景,并监视进程的情况:
3.Linux进程优先级
基本概念
cpu资源分配的先后顺序,就是指进程的优先权(priority)。 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
查看系统进程
在linux系统中,用ps –la 命令则会类似输出以下几个内容:
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行,Linux中的文件默认优先级都是80
NI :代表这个进程的nice值
PRI and NI
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值 ,PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行,所以,调整进程优先级,在Linux下,就是调整进程nice值,nice其取值范围是-20至19,一共40个级别。这么做的目的是要把进程优先级控制在一定的范围内,使得操作系统在调度时,能够均衡的调度每一个进程,使得其他优先级较低的进程也有机会得到cpu资源,优先级较低的进程长时间得不到cpu资源的状态,也叫作进程饥饿。
修改进程优先级
1. top
2.进入top后按“r”–>输入进程PID–>输入nice值
这里需要注意,普通用户可以将自己的进程的优先级降低,但是如果想要提高某个进程的优先级,那么就要使用提权指令sudo 来执行,还有一个细节需要注意,假如此时我们的进程PRI被改为了90,我们将nice改为-10后,PRI会变为70而不是80,PRI每次都是根据Linux中的进程默认权限80和nice值进行修正运算的,与PRI当前的值是多少无关。
其他的概念
下面的这些概念我们简单一提,后续也会继续讲解的
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高 效完成任务,更合理竞争相关资源,便具有了优先级
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
并发: 多个进程在一个CPU下采用进程切换的方式(这种方式进行进程切换绝对不会失败),在一段时间之内,让多个进程都得以推进,称之为并发
欲知后事如何,且听下回分解......会尽快更新的......