当前位置: 首页 > news >正文

【把Linux“聊”明白】进程的概念与状态

在这里插入图片描述

进程

友情专栏:【把Linux“聊”明白】


文章目录

  • 进程
  • 前言
  • 一、基本概念与操作
    • 1-1 基本概念
    • 1-2 PCB
    • 1-3 task_struct
    • 1-4 查看进程
    • 1-5 通过系统调用获取进程标示符
    • 1-6 通过系统调用创建进程
    • 1-7 进程具有独立性
  • 二、进程状态
    • 2-1 运行-阻塞-挂起
    • 2-2 Linux内核源代码怎么说
    • 2-2 进程状态查看
    • 2-3 僵尸进程——Z(zombie)
    • 2-4 僵尸进程的危害
    • 2-5 孤儿进程
  • 总结


前言

有了上篇文章的基础上,我们在学习进程之前,要知道,操作系统是怎么管理进行进程管理的呢?很简单,先把进程描述起来,再把进程组织起来!
通过将每个进程的属性信息封装成数据结构,并以链表等组织形式进行管理,操作系统实现了对进程的创建、调度、终止等全生命周期管理。


一、基本概念与操作

1-1 基本概念

先来看课本与内核对于进程的解释:

课本概念:程序的一个执行实例,正在执行的程序等;
内核观点:担当分配系统资源(CPU时间,内存)的实体。

听起来都太抽象,在这里,我们可以理解为进程 = 内核数据结构对象 + 自己的代码和数据,如下图所示:

在这里插入图片描述

可以看到,每个进程都对应着其内核数据结构对象和自己的代码和数据,我们可以把其内核数据结构用类似链表的结构连接起来,那么对于进程的管理,就变成对链表的增删查改了。

1-2 PCB

内核数据结构我们又称为PCB (Process Control Block),即进程控制块。可以理解为进程属性的集合。简单说,它就是描述进程的结构体
我们通过PCB,就可以直接或者间接的找到进程的所有属性。
在Linux中,具体的PCB是task_struct,PCB是课本的说法。
task_struct是Linux内核的⼀种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。

1-3 task_struct

既然task_struct是描述进程的结构体,那么它里面详细有什么呢?简单看一下:

标示符:描述本进程的唯一标示符,用来区别其他进程。
状态:任务状态,退出代码,退出信号等。
优先级:相对于其他进程的优先级。
程序计数器:程序中即将被执行的下⼀条指令的地址。
内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
上下文数据:进程执行时处理器的寄存器中的数据。
I∕O状态信息:包括显示的I/O请求,分配给进程的I∕O设备和被进程使用的⽂文件列表。
记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息……

1-4 查看进程

在这里,我们要知道,我们历史上执行的所有指令(内建命令除外)、工具、自己的程序,运行起来,都是进程!
很好理解,现在我们来了解一下如何查看进程呢?

  1. 通过/proc系统文件夹查看

如:要获取PID为1的进程信息,你需要查看 /proc/1 这个⽂件夹。
在这里插入图片描述

  1. 使用top和ps这些用户级工具
    我们可以先建一个一直循环的程序来进行测试:
#include <stdio.h>
#include <unistd.h>int main()
{while (1) {printf("hello world\n");sleep(1);}return 0;
}

在这里插入图片描述

ps ajx | head -1 && ps ajx | grep myproc | grep -v grep

ps ajx:使用特定的格式选项显示进程信息
其它命令组合,就是帮助我们既能清晰看到表头信息,又能准确找到目标进程,排除干扰项所用。

补充:
在这里插入图片描述

1-5 通过系统调用获取进程标示符

在这里插入图片描述

pidppid

进程id(PID)
父进程id(PPID)

使用一下:

#include <stdio.h>
#include <unistd.h>int main()
{printf("pid:%d\n",getpid());printf("ppid:%d\n",getppid());return 0;
}

输出:
在这里插入图片描述

我们可以对ppid进行搜索

在这里插入图片描述

哦,bash
说明:bash(命令行解释器)也是一个进程
我们要知道,OS会为每一个登录的用户,分配一个bash

补充: exe和cwd
它们是进程对应的两个属性,我们可以在 /proc 下查看进程对应的属性:
在这里插入图片描述
执行命令后可以看到:
在这里插入图片描述
解释:

exe :指向启动该进程的可执行文件的完整路径。它告诉你这个进程是由哪个程序文件创建的。
cwd :Current Working Directory 的缩写,代表进程的当前工作目录。像我们在C语言中的fopen函数,以写的形式打开文件…,如果我们没有指定路径的话,就会默认在当前路径,即根据cwd来确定路径。

1-6 通过系统调用创建进程

我们是通过fork函数来进行创建子进程的,我们可以来查看一下man手册:

在这里插入图片描述
在这里插入图片描述

fork有两个返回值,我们可以这样理解,父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝),现阶段只能这样理解。

使用:

#include <stdio.h>
#include <unistd.h>int main()
{pid_t ret = fork();if(ret == -1){perror("fork");return 1;}else if(ret == 0){printf("我是子进程,我的pid是:%d\n",getpid());}else if(ret > 0){printf("我是父进程,我的pid是:%d\n",getpid());}return 0;
}

输出:
在这里插入图片描述
啊,两个判断条件都成立嘛,和我们以前的说法不一样呀,一组if else只能同时成立一个呀!!!

不着急,分析:

在这里插入图片描述
上述的说法不是很标准,但是对于我们现阶段理解是足够了,因为我们现在什么都不懂,讲太深直接绕进去了。

所以,在fork之后,父子未来执行不同的代码逻辑。

有了刚才的说明,下面,我们再来对会出现的疑问进行解释:

  1. fork为什么会有两个返回值?

    在父进程中:fork() 返回子进程的ID(一个大于0的数)
    相当于说:“我是爸爸,我得到了儿子的身份证号”
    在子进程中:fork() 返回 0
    相当于说:“我是儿子,我的身份标识是0”
    方便后续管理。

  2. 两个返回值各自是如何返回给父子?

    在这里插入图片描述

  3. 一个变量如何能让if和else if 同时成立这个问题,需要在后面才能解释清楚,我们这里简单澄清几个点

    实际上,一个变量不可能同时让 if 和 else if 条件成立,只是表面看起来一样,底层已经发生了很大的变化了;
    也就是说,两个独立的进程各自执行自己的代码路径,都有ret这个变量。
    目前,只能说这些,后续的虚拟地址空间会给我们揭晓答案的。

1-7 进程具有独立性

看代码:

#include <stdio.h>
#include <unistd.h>int main()
{int cnt = 100;pid_t ret = fork();if(ret == -1){perror("fork");return 1;}else if(ret == 0){cnt+=100;printf("我是子进程,我的pid是:%d,cnt的值为%d\n",getpid(),cnt);}else if(ret > 0){printf("我是父进程,我的pid是:%d,cnt的值为%d\n",getpid(),cnt);}return 0;
}

输出:
在这里插入图片描述

可以看出,进程具有独立性。
现阶段可以这样理解:把父子任何一方,进行数据修改,OS会把被修改的数据在底层拷贝一份,让目标进程修改这个拷贝。(写时拷贝)

有了进程的基本概念,我们来看一下进程的状态吧。

二、进程状态

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

2-1 运行-阻塞-挂起

首先,我们要知道,一个CPU(单核),一个调度队列(运行队列),如下图:
在这里插入图片描述

也可以说,只要挂载到调度队列,即处于运行状态。

对于阻塞,我们来想一个在C语言中写的scanf函数,它在等待用户输入的时候,进程就处于阻塞。此时,该进程就会被从运行队列中删除,从而挂载到等待队列(wait_queue)中,等待用户输入。阻塞就是等待某种设备或资源就绪。

但是,在有些时候,内存的空间严重不足,我这个进程又处于阻塞状态,那我们可不可以把此进程对应的代码和数据放到磁盘上呢(唤入),在需要的时候又加载进内存呢(唤出),这就叫做阻塞挂起,此时,进程的task_struct又会被挂载到类似于阻塞挂起的队列。

都挺合理,但是,一个结构体(task_struct)怎么会链接在多个队列中呢?
其实,这种链接形式并不是我们在链表中学的那种,在结构体中直接定义一个结构体指针,而是存在一个struct list_head结构体:

struct list_head
{struct list_head *next, *prev;
}

在进程的task_struct中存在多个struct list_head,从而实现一个进程挂载在多个队列中。类似于:
在这里插入图片描述
要通过struct task_struct 中的 struct list_head找struct task_struct,也很简单,利用相关结构体的知识来计算。不做说明了。

可见,进程状态的变化,表现之一,就是进程在不同的队列中流动,本质就是链表的增删查改。

2-2 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):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。浅睡眠。
D磁盘休眠状态(Disksleep):有时候也叫不可中断睡眠状态(uninterruptiblesleep),在这个状态的进程通常会等待IO的结束。深度睡眠。
T停止状态(stopped):可以通过发送SIGSTOP信号给进程来停止(T)进程。这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
Z 僵尸状态 (Zombie):进程已经执行完毕,但其退出状态还没有被父进程读取。(后面会重点说)

在这里插入图片描述

2-2 进程状态查看

命令:ps aux / ps ajx
解释:

a:显示一个终端所有的进程,包括其他用户的进程。
x:显示没有控制终端的进程,例如后台运行的守护进程。
j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息。
u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU和内存使用情况等。

2-3 僵尸进程——Z(zombie)

我们创建子进程的目的,就是为了让子进程完成某种事情的。那子进程完成了吗?对应结果相关的信息,父进程得知道吧。这些信息在哪里呢?子进程对应的task_struct中。
所以,在子进程执行完相关的代码后,子进程对应的代码和数据会被清理,但是,子进程对应的task_struct不会被清理,而是将子进程的状态设置为Z。然后,等待被父进程接受相关退出信息。

所以,

僵死状态(Zombies)是⼀个比较特殊的状态。当子进程退出并且父进程(使用wait()系统调用,后续说)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。

现在,我们来模拟一个僵尸进程。

#include <stdio.h>
#include <unistd.h>int main()
{pid_t ret = fork();if(ret == -1){perror("fork");return 1;}else if(ret == 0){//childint cnt = 5;while(cnt--){printf("我是子进程,我的pid是:%d,父进程pid是%d\n",getpid(),getppid());sleep(1);}}else if(ret > 0){while(1){printf("我是父进程,我的pid是:%d\n",getpid());sleep(1);}}return 0;
}

编译执行
在这里插入图片描述
在这里插入图片描述
此时的defunct指的就是:进程实体已死亡,但残骸(task_struct)还在。

如果父进程一直不接受呢?——僵尸进程的危害

2-4 僵尸进程的危害

明确几点:

  1. 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程进程如果⼀直不读取,那子进程就⼀直处于Z状态?
    是的!

  2. 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态⼀直不退出,PCB⼀直都要维护?
    是的!

  3. 那⼀个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!

  4. 会造成内存泄漏?
    是的!

至于如何避免?到进程等待时会说明。

2-5 孤儿进程

僵尸进程指的是子进程想退出发生的情况,那如果父进程先退出呢?
父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
父进程先退出,子进程就称之为“孤儿进程”
孤儿进程被1号init进程领养,当然要有init进程回收喽。

模拟孤儿进程

#include <stdio.h>
#include <unistd.h>int main()
{pid_t ret = fork();if(ret == -1){perror("fork");return 1;}else if(ret == 0){//childwhile(1){printf("我是子进程,我的pid是:%d,父进程pid是%d\n",getpid(),getppid());sleep(1);}}else if(ret > 0){int cnt = 5;while(cnt--){printf("我是父进程,我的pid是:%d\n",getpid());sleep(1);}}return 0;
}

在这里插入图片描述

在这里插入图片描述

我们可以用信号来杀。
在这里插入图片描述


总结

理解进程状态转换和生命周期管理,是掌握操作系统工作原理的关键基础。后续我们将继续深入进程通信、进程调度等高级话题。


如果本文对您有启发:
点赞 - 让更多人看到这篇硬核技术解析 !
收藏 - 实战代码随时复现
关注 - 获取Linux系列深度更新
您的每一个[三连]都是我们持续创作的动力!

请添加图片描述

http://www.dtcms.com/a/600669.html

相关文章:

  • GIT版本管理工具轻松入门 | TortoiseGit,本地 Git 仓库和 Git 概念,笔记02
  • 什么是美颜sdk?美型功能开发与用户体验优化实战
  • 在 React 项目中使用 Ky 与 TanStack Query 构建现代化数据请求层
  • 计算机网络---传输层安全 SSL与TLS
  • 【Linux篇】信号机制深度剖析:从信号捕捉到SIGCHLD信号处理
  • C语言编译软件选择及优化建议
  • Linux 之 【冯诺依曼体系结构与操作系统的简介】
  • 潍坊建设gc局网站windows优化软件
  • Java虚拟机(JVM)面试题(51道含答案)
  • [27] cuda 应用之 核函数实现图像通道变换
  • Aurora RDS MySQL The table ‘/rdsdbdata/tmp/#sql14b_df16d_1bd‘ is full
  • 手机响应式网站怎么做how to use wordpress
  • 网易云音乐回应“不适配鸿蒙”:推动相关部门加快步伐
  • C语言在线编译练习 | 提高编程技能与实战能力
  • 人工智能分支——深度学习、机器学习与神经网络初概览
  • C++ STL 关联式容器:map 与 set 深度解析与应用实践
  • 鄂尔多斯网站制作 建设推广网站建设前台功能
  • 策划书模板免费下载的网站免费获客平台
  • 如何搭建IoT机器视觉
  • 几分钟学会飞书多维表格开发
  • 11.12 脚本APP 手机如何开发简单APP
  • C++17常用新特性
  • oj题 ——— 链式二叉树oj题
  • 数据库项目实战五
  • Python调用Java接口失败(Java日志打印警告:JSON parse error:xxxx)
  • 没有网站如何做SEO推广有用吗怎么不花钱自己开网店
  • ArkTS分布式设计模式浅析
  • 倍福PGV100-F200A-R4-V19使用手册
  • FD2000/4的UEFI编译和烧录文件打包过程记录
  • 微信小程序map自定义气泡customCallout