Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!
我的博客:<但凡.
我的专栏:《编程之路》、《数据结构与算法之美》、《C++修炼之路》、《Linux修炼:终端之内 洞悉真理》、《Git 完全手册:从入门到团队协作实战》
感谢你打开这篇博客!希望这篇博客能为你带来帮助,也欢迎一起交流探讨,共同成长。
目录
1、进程状态
1.1、操作系统中的进程状态
在操作系统中,进程是程序的一次执行实例。进程在其生命周期中会经历多种状态,这些状态反映了进程当前的活动情况和资源占用情况。以下是常见的进程状态及其说明:
新建(New)
进程正在被创建,操作系统为其分配资源(如内存、进程控制块等)。此时进程尚未完全准备好执行。
就绪(Ready)
进程已获得除CPU外的所有必要资源,等待被调度器选中并分配CPU时间。处于就绪状态的进程通常存放在就绪队列中。
运行(Running)
进程正在CPU上执行指令。同一时间,单核CPU只能有一个进程处于运行状态(多核CPU可并行运行多个进程)。
阻塞(Blocked)/等待(Waiting)
进程因等待某些事件(如I/O操作完成、信号量释放等)而暂停执行。此时进程不会占用CPU资源,直到事件发生。
终止(Terminated)
进程已完成执行或被强制终止,操作系统回收其占用的资源。进程控制块可能暂时保留以供父进程查询状态。
挂起(Suspended)
某些系统中,进程可能被暂时移出内存(换出到磁盘),以释放内存资源。挂起状态可与就绪或阻塞组合:
- 就绪挂起:进程在外存中,但已具备执行条件。
- 阻塞挂起:进程在外存中,且仍在等待事件。
1.2、状态转换示例
- 新建 → 就绪:操作系统完成进程初始化。
- 就绪 → 运行:调度器分配CPU时间。
- 运行 → 就绪:时间片用完或被更高优先级进程抢占。
- 运行 → 阻塞:进程请求I/O或等待资源。
- 阻塞 → 就绪:等待的事件发生(如I/O完成)。
- 运行 → 终止:进程执行完毕或发生错误。
每个进程的状态信息由PCB维护,包括:
- 进程ID(PID)
- 程序计数器(PC)
- 寄存器状态
- 内存分配情况
- 优先级
- 打开文件列表等。
操作系统中的状态其实就是一个整数,他是提前设定好的宏值。
进程竞争资源本质就两类资源:一是CPU资源,二是外设资源。对应的,我们写的代码就两类,一类是计算密集型,比如算法,数据结构代码。一类是IO密集型。
2、Linux中的进场状态
2.1、新建状态和运行状态
在Linux系统中,系统会通过一个全局的双链表,管理所有进程。在这个双链表中的进程都是新建状态。 task_struct之间的链接和我们之前接触到的简单的双链表并不一样。我们结合图来理解一下,注意在以下出现的所有图中我都省略了task_struct链接的代码和数据,但是我们得知道无论一下什么情况,task_struct都是连着自己的代码和数据的。
在task_struct中,我们内嵌了一个节点,这个节点就是用来链接task_struct的。如果我们想让一个进程链入运行队列,直接把运行队列的指针指向task_struct中内置的运行队列的节点就可以了,不用把task_struct之前的链断开。也就是说这个节点可以既处于全局的双链表中,也处于运行队列中。
链入到runqueue里面的pcb的状态是运行状态。运行状态并不是指正在运行,而是指已经准备好随时被cpu运行了。这个runqueue就是调度队列,pcb从全局双链表中链入到运行队列中状态就从新建状态切换到了运行状态。每个cpu都有调度队列,有几个cpu就有几个调度队列。
现在有一个问题,我们知道task_stuct中list_node这个成员的地址,我们如何得到该结构体(task_struct)变量的地址?以及我们如何访问这个结构体变量中的任意属性呢?
我们可以通过以下公式计算:
task_struct* test=&list_node - &((task_struct*)0->list_node)
思路是用node的地址,减去偏移量,就得到了这个结构体变量的起始地址。
既然都知道这个结构体起始的地址了,其他的属性也就好说了。
2.2、阻塞状态与挂起状态
接下来我们介绍一下阻塞状态。首先我们新建一个文件,然后往文件中输入一下代码,运行它:
#include<stdio.h>
int main()
{
int a=0;
scanf("%d",&a);
printf("%d",a);
return 0;
}
这时我们不输入数字。系统等待我们输入数字的状态就叫阻塞状态。
对于外设来说也有专属的等待队列,这个队列的每一个节点包含一个外设的各种信息。当运行队列中某一个进程陷入阻塞状态中,这个进程pdb就会从运行队列中断开,然后链接到对应设备的节点中。比如上面代码系统等待我们输入数字是就把这个进程链到了键盘的对应节点。
阻塞和运行的本质就是让进程的task_struct更改task_struct 状态属性,并连入不同的队列中。
当一个进程处于阻塞状态时,此时他的代码和数据白白占用空间。操作系统会把他的代码和数据交换到磁盘中,好释放内存空间给其他的进程使用。当这个进程不是阻塞状态之后就可以再把数据交换回来。那么代码和数据被存放在磁盘中的进程所处的状态就是挂起状态。
此外,对于运行队列来说,运行队列中所有的进程都是运行状态,此时如果内存空间不足,系统会主动地把当前运行队列中的一些进程的代码和数据放入磁盘中。此时这个进程的状态就是就绪挂起状态。虽然说他叫就绪挂起状态,但是在Linux系统中他是从运行状态转换到就绪挂起状态的。在Linux系统中,我们把处在运行队列的进程都叫运行状态,并没有就绪状态。但其实严格来说,只有正在运行的才是运行状态,运行队列中的其他进程都是就绪状态。
之后我们统一共识,在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 */
};
在Linux中,S是浅度休眠,他就是阻塞状态,换句话说是阻塞状态在Linux系统中的具体体现状态。我们运行上面的那串代码,看一下他的状态:
我们在写以下代码,看一下他的状态:
#include<stdio.h>
int main()
{
while(1)
{
printf("Hello Linux!");
}
return 0;
}
我们查看他的状态,发现他也是浅度休眠。其实他的状态时来回存运行状态和浅度休眠状态之间切换。由于我们cpu运转速率特别快,远快于外设,所以说我们查看状态大概率都是阻塞状态。
既然有浅度睡眠,就有深度睡眠,什么是深度睡眠呢?
在我们的进程进行磁盘级IO时,他的状态就被设置成了深度睡眠(disk sleep)。处于深度休眠的状态无法被操作系统杀死。因为进行磁盘级IO的进程一旦被杀死,很可能会造成数据的丢失。
一般不会出现D状态。就算出现D状态也不会维持特别长的时间。如果长时间D状态(半小时)说明磁盘出问题了。
浅度休眠和深度休眠统一被称为阻塞状态。
在上面的各种状态中,我们发现他都有一个+号,其实这个+号代表这个进程是处在前台的。前台任务用ctrl+c能杀掉,后台的用ctrl+c杀不掉。
2.3、追踪状态和暂停状态
追踪状态(t)是指被追踪的进程因为断点停下来,此时这个进程的状态就是 t ,追踪状态。
比如我们用gdb调试某个代码,如果我们给这串代码中打一个断点,并且r运行起来,这时候gdb其实是创建了一个子进程去运行这个程序。遇到断电之后停了下来,此时我们观察他的状态就是t状态。
当每个进程进行非法操作时,操作系统会把他暂停,此时他的状态就是暂停状态(T)。阻塞状态和暂停状态的区别是,阻塞状态是系统让进程等待资源就绪,等资源就绪之后他会让进程在运行起来的。而暂停状态只能用户去解决。
2.4、僵尸状态和结束状态
当进程结束时,进程不会直接退出,而是先把代码和数据释放,保留task_struct。目的是方便父进程读取该进程的退出信息。此时,该进程的状态就是僵尸状态(Z)。如果父进程将来不处理,这个僵尸进程会一直存在。
当进程结束时,这个进程就成了结束状态(X),但是这个处理是在一瞬间完成的,所以我们观察不到X状态。
2.5、孤儿进程
我们执行以下代码:
在上面的代码中,我们创建了一个子进程,但是我们先让父进程结束,然后然子进程一直运行,发现子进程的ppid变成了1:
1是操作系统的一部分,当一个子进程的父进程被释放,那么这个子进程就成了孤儿进程,他会被操作系统收养。
我们的父进程的状态并没有变成僵尸状态,因为他会被bash自动释放。
3、进程优先级
进程优先级是操作系统用来决定哪个进程在CPU上执行的顺序。优先级高的进程会优先获得CPU资源,而优先级低的进程需要等待。优先级机制确保关键任务能够及时响应,同时合理分配系统资源。
在Linux系统中,我们可以通过以下命令,输出几个信息:
其中,UID是执行者的身份,PRI是这个进程的优先级,越小越早执行,NI是这个进程的nice值。nice值是进程优先级的修正数据。
首先我们介绍以下如何更改nice值。进入top之后按r,接着输入进程PID,再输入nice值。
更改后我们可以发现nice值和PRI都改变了。
当然,我们可以以使用nice和renice命令更改。
PRI等于PRI(旧) - NI。需要注意的是PRI(旧)始终等于80。
nice值的取值范围是-20到19。所以PRI的范围是60到99。 nice值的范围是为了进程调度更加公平。
补充概念:
竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理的竞争相关资源,便具有了优先级。
独立性:多进程运行,需要独享各种资源,多进程之间运行期间并不互相干扰。
并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行。
并发:多个进程在一个CPU下采用进程切换的方式,在一段时间内,让多个进程都得到推进,称之为并发。
4、进程切换
当前系统都是分时系统,每一个进程都有自己的时间片。时间片其实就是一个计数器,当时间片到达,进程就被操作系统从CPU中剥离下来。
CPU上下文切换:其实际含义是任务切换,或者CPU寄存器切换。当一个进程进行到一半时系统决定切换到其他进程进行,这时候他会保存正在运行的进程的当前状态,也就是CPU寄存器中的全部内容,这些内容被保存到进程自己的堆栈中,入栈完成后就把下一个将要运行的进程的当前状况从该任务的栈中重新装入CPU寄存器,并开始运行下一个进程。
进程切换有以下触发条件:
(1)时间片用完,(2)系统调用,(3)中断发生,(4)进程阻塞。
进程切换的步骤
保存当前进程上下文
将当前进程的寄存器、程序计数器、栈指针等状态保存到其进程控制块(PCB)中。更新调度队列
若当前进程从运行态转为就绪态或阻塞态,将其PCB加入相应队列。选择下一个进程
调度器根据算法(如轮转、优先级)从就绪队列中选择目标进程。恢复目标进程上下文
从目标进程的PCB中加载寄存器、程序计数器等状态到CPU,切换到用户模式并跳转到目标进程的代码位置。
5、进程调度
说了这么半天,那么进程是如何调度的呢?我们来结合图理解一下。
在runqueue中内置了两个struct q,这两个结构体里面还有三个元素,一个是nr_active,一个是bitmap[5],一个是queue[140]。这个queue[140]其实就是个数组,其中数组的前一百个元素我们不用管,他们是为实时操作系统的进程优先级准备的。后四十个元素就是我们分时操作系统对应的优先级的范围大小。各个进程按照优先级链入数组中。
所以说,进程有基于时间片的公平调度的分时进程,也有基于优先级的实时运行。
我们可以用bitmap,也就是位图(位图的讲解以及模拟实现可以看我之前的文章),快速定位最高优先级的进程。并且直接拿走双链表中排的最靠前的进程控制块。这个操作的时间复杂度可以达到常数级别,故而这个进程调度算法被称为O(1)调度算法。
当进程运行结束后,系统会把这个进程链入过期队列中。也就是说所有进程执行完之后,当前的活跃队列为空,这时候系统会交换过期队列指针和活跃队列指针指向的内容。
我们给一个进程设置nice值,等到他的时间片结束后,根据nice值调整优先级,然后插入过期队列。这也是nice值存在的意义,如果直接调整优先级的话,他还得在活跃队列先插入一次,再在过期队列中再插入一次。
好了,今天的内容就分享到这,我们下期再见!