【Linux】进程状态

Linux进程状态

1、操作系统的进程状态

Linux内核中每一个进程唯一对应一个task_struct结构体,结构体中存储进程的状态,进程状态的本质是int类型整数,进程状态决定了进程接下来要执行什么任务,进程的状态有7种:

  • 创建状态:创建进程
  • 就绪状态:进程已经拿到所有资源,只差 CPU 时间片,随时可被调度运行
  • 运行状态:调度器选中该进程,CPU 正在执行它的代码
  • 阻塞状态:主动脱离调度队列,挂载到对应硬件的等待队列wait_queue;事件完成后回到就绪
  • 结束状态:进程执行完毕return/exit(),释放资源

1.1 运行状态

一般计算机中只有一个CPU,但进程很多,CPU遵循先进先出(FIFO)的规则,所以CPU会维护一个运行队列,以队列的形式调度进程,队列的每个成员都是进程的PCB对象,凡是在运行队列中的进程,所处状态都是运行状态

1.2 阻塞状态

写一段代码并运行:

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
	int a = 0;
	scanf("%d",&a);
	printf("a = %d\n",a);
	return 0;
}

运行程序后执行到scanf时程序会停下等待键盘输入,此时进程阻塞

常见的阻塞是进程等待键盘输入数据的过程,键盘是一种硬件,操作系统对硬件的管理方式是先描述再组织,所以每一个硬件在内核中都有一个自己的结构体对象,结构体对象中会维护一个等待队列,用来存放所有等待该硬件事件的阻塞进程

所以阻塞本质是将当前进程从CPU的运行队列摘除,挂载到了硬件结构体对象中的等待队列

当用户敲击键盘输入时,操作系统从键盘的等待队列中取出改进程,放回CPU运行队列

所以阻塞与运行的本质:看你的task_struct在谁提供的队列中

1.3 挂起状态

  • swap

swap是磁盘上划分的一块专用分区,作用是内存不足时,把进程的代码或数据临时存到磁盘,用磁盘的空间换内存的空间,过度的swap会导致系统变慢

swap out表示把进程代码段、数据段 写入磁盘 swap 分区

swap in表示把 swap 分区里的进程代码或数据重新加载回物理内存,进程恢复普通就绪或阻塞状态

当内存空间不足时,操作系统会把没有被CPU调度的代码和数据放到swap分区,只留PCB在队列中排队,这种进程就处于挂起状态,如果进程处于阻塞状态时被挂起成我阻塞挂起,如果进程处于运行状态时被挂起成我运行挂起,如果内存还是不够,那么操作系统会选择性杀掉特定的进程

2、Linux中具体的进程状态

为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。

2.1 状态在kernel源代码里定义

cpp 复制代码
/*
 * 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):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))
  • D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束
  • T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行
  • X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态

2.2 查看进程状态

指令:ps ajx | head -1 && ps ajx | grep myprocess

先写一段代码

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
	while(1)
	{
		printf("我是一个进程:%d\n", getpid());
	}
	
	return 0;
}

运行后查看进程的状态

从上面可以看出代码执行时处于S睡眠状态,将printf打印语句删除后再运行

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
	while(1);
	
	return 0;
}

此时进程状态变成了R状态

因为CPU的速度很快,在持续输出时,进程的大部分时间都用来等显示器输出,所以最终是睡眠状态,去掉printf后,进程不会去访问显示器,进程始终运行,所以最终是R状态

另外加号表示在前台运行,没有+表示在后台运行

./myprocess后加上& 可以切换到后台运行

如果想停止在后台运行的进程可以用指令kill -9 + 进程PID 结束进程

2.3 D(disk sleep)不可中断休眠

如果进程需要磁盘写入数据,那么进程把数据交给磁盘后要等磁盘把数据全部写入,而在等待时间内进程不能结束,但操作系统检测到该进程在等待磁盘后,为了腾出更多的内存空间,会直接终止该进程,此时如果磁盘写入数据失败,磁盘找不到进程,无法告诉用户数据写入结果,磁盘会直接把数据丢掉,造成数据丢失

所以D状态存在的意义是让进程等待磁盘 IO 硬件操作时,屏蔽所有信号,避免操作系统打断读写磁盘的进程,避免造成数据损坏

2.4 T(stopped)停止状态

进程被外部信号冻结,完全停止执行,不等待资源

使用指令:kill -19 进程PID 可以使进程变成T状态

执行命令后,进程由R+状态变为T状态,并且进程会到后台运行

使用指令kill -18 进程PID 可以使进程结束T状态

结束停止状态的进程会到后台运行,此时只能通过kill -9 + 进程PID 指令将进程终止

T状态和S状态的不同之处是S状态下,资源就绪会自动唤醒,而T状态不会自动恢复,只能手动操作

3、僵尸进程

当一个进程结束时,退出信息有两种:main函数的返回值或收到的信号值,那么进程退出后退出的信息保存在哪里?答案是保存在进程自己的task_struct内核结构体 里,结构体中有两个关键成员:exit_state:标记进程状态,exit_codeexit_signal:存储退出码、杀死它的信号值

当一个子进程终止时,父进程没有读取子进程结构体中的退出信息,此时子进程就会进入僵尸状态

操作系统不会直接销毁它的task_struct,会把退出信息(exit_codeexit_signal)保存在这个结构体里,专门留给父进程读取。

只要父进程不去读取这份信息,task_struct就不会被内核释放,进程将一直处于僵尸状态

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
	pid_t pid = fork();
	if(pid == 0)
	{
		printf("子进程退出\n");
		return 0; // 子进程退出,变僵尸
	}
    sleep(30); // 父进程休眠30秒,不调用wait回收
    return 0;
}

当程序运行时,查看程序的运行状态

当子进程结束后,子进程的用户态代码内存全部释放,父进程卡在sleep(30),没有读取子进程退出状态,子进程无法被清理,成为僵尸状态

僵尸进程的危害

  • 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程将一直处于 Z 状态
  • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护
  • 那一个父进程创建了很多子进程,就是不回收,会造成内存资源的浪费因为数据结构对象本身就要占用内存
  • 造成内存泄漏

4、孤儿进程

僵尸进程是让子进程先退出,父进程运行,那么孤儿进程是让父进程先退出,子进程运行

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // 子进程代码段
        while(1)
        {
            printf("我是子进程, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else
    {
        // 父进程代码段
        int cnt = 5;
        while(cnt)
        {
            cnt--;
            printf("我是父进程, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            sleep(1);
        }
    }
    return 0;
}

当父进程运行时查看运行状态

父进程和子进程都处于S状态

当父进程退出后查看运行状态

当父进程退出后,父进程被bash回收,所以不会出现僵尸状态,当父进程结束后,子进程的父进程会变成1号进程,这种进程就是孤儿进程,孤儿进程会被系统领养,给孤儿进程退出善后,孤儿进程会自动变成后台进程

补充:前后台进程的区分:谁能从键盘获取数据输入,谁就是前台,键盘只有一个,所以在获取输入时,只能有一个进程获取,前台进程任何时刻只能有一个,后台进程可以有多个,所以后台进程不能获取键盘数据,不能用ctrl + c终止,根据这一点可以区分前后台进程

相关推荐
三言老师4 小时前
文本工具组合统计服务器日志数据
linux·运维·服务器
mounter6255 小时前
认识 Tetragon:基于 eBPF 的安全监控与强制执行工具
linux·ebpf·cve·kernel
aixingkong9216 小时前
AI超节点Scale Up域总线各层优化设计
linux·服务器·网络
kidwjb6 小时前
Linux内核-文件系统-文件系统目录和文件操作
linux·内核·文件系统
_艾伦 耶格尔.7 小时前
进程控制
linux
寒水馨8 小时前
Linux下载、安装PowerShell-v7.6.4(附安装包powershell-7.6.4-linux-x64.tar.gz)
linux·跨平台·自动化运维·devops·命令行·powershell·脚本语言
lsh曙光8 小时前
scp和rsync远程同步
linux
Kina_C8 小时前
HAProxy 负载均衡算法全解析:从静态轮询到一致性哈希的实战指南
linux·算法·负载均衡·哈希算法·haproxy
~光~~8 小时前
【嵌入式linux学习_OV8858 bring up】L2_V4L2与Ov8858
linux·数据库·学习
Kina_C9 小时前
HAProxy 负载均衡实战:从安装配置到高级参数详解
linux·运维·负载均衡·haproxy