(学习总结29)Linux 进程概念和进程状态

Linux 进程概念

  • 冯·诺依曼体系结构
    • 软件运行与存储分级
    • 数据流动的理论过程
  • 操作系统
    • 操作系统(Operator System) 概念
    • 操作系统的功能与作用
    • 系统调用和库函数概念
  • 进程概念
    • 描述进程 - PCB
    • task_struct
    • 查看进程
    • 通过系统调用获取进程标示符 PID
    • 通过系统调用 fork 函数创建进程
      • 简单使用
      • 区分父子进程操作
      • 父子进程的写时拷贝
  • 进程状态
    • 具体的 Linux 内核解释
      • 运行、阻塞 和 挂起状态
      • 进程如何被转移
    • Linux 进程状态
    • 僵尸进程与其危害
      • 僵尸进程危害
    • 孤儿进程

以下代码环境为 Linux Ubuntu 22.04.5 gcc C语言

冯·诺依曼体系结构

我们生活中的计算机大部分都遵守冯·诺依曼体系,如笔记本、服务器等等。

在这里插入图片描述

而计算机都是由一个个的硬件组件组成的:

  • 输入设备:包括键盘、鼠标、扫描仪、写板等。
  • 存储器:内存。
  • 中央处理器(CPU):含有运算器和控制器等。
  • 输出设备:显示器、打印机等。

如果再精确一点说明,则有:

  • 这里的存储器确切是指内存。
  • 不考虑缓存情况,这里的 CPU 只能对内存进行读写,不能访问外设(输入或输出设备)。
  • 外部设备(输入或输出设备) 要输入或者输出数据,也只能写入内存或者从内存中读取。
  • 可以肯定的是,所有设备都只能直接和内存打交道。

软件运行与存储分级

软件在运行之前,会先存储在磁盘或其它外部存储设备中。当需要运行软件时,会将软件的程序加载到内存当中,然后由 CPU 获取来执行程序,处理程序逻辑,最后由显示器等输出设备显示结果。

考虑到 CPU 的处理速度非常快,这个体系之下还会细分很多的存储设备,目的就是为了尽可能不拖慢 CPU 的速度。

在这里插入图片描述

数据流动的理论过程

当我们使用这个体系结构的计算机进行信息交流,就一定会通过 输入设备 -> 载入内存 + CPU运算 -> 输出设备 这个步骤:

在这里插入图片描述

操作系统

操作系统(Operator System) 概念

任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。

更广泛上的操作系统包括:

  • 内核(包括:进程管理、内存管理、文件管理、驱动管理等)
  • 其他程序(例如函数库,shell程序 等等)

在这里插入图片描述

操作系统的功能与作用

在整个计算机软硬件架构中,操作系统的功能定位是一款搞 " 管理 " 的软件,它的作用是让应用程序正常执行,具体为:

  • 对下,与硬件交互和管理所有的软硬件资源。
  • 对上,为用户程序(应用程序)提供一个良好的执行环境。

在这里插入图片描述
在上图中可以看到:

  • 软硬件体系结构为层状结构,各层也设计成 高内聚低耦合,方便各个部分自己更新迭代。

  • 访问操作系统就必须使用系统提供的系统调用接口。

  • 若用户程序访问硬件,则一定会贯穿整个软硬件体系结构!

那操作系统如何 " 管理 " 呢?

  1. 先描述被管理对象:使用 struct 结构体(使用的是 C语言)构建被管理对象的数据。

  2. 再组织被管理对象:使用高效的数据结构组织被管理对象。

总结起来就是一句话:先描述,再组织

系统调用和库函数概念

在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用

系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以有的开发者对部分系统调用进行适度封装,形成库。有了库,就利于更上层用户或者开发者进行二次开发。

进程概念

从课本概念出发:程序的一个执行实例,正在执行的程序等。

从内核观点出发:担当分配系统资源(CPU时间,内存)的实体。

更具体的说进程:进程 = 内核数据结构元素 + 进程的代码和数据

描述进程 - PCB

基本概念

  • 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。

  • 概念上称之为 PCB(process control block),在 Linux 操作系统下的 PCB 是: task_struct

task_struct 为 PCB 的一种

  • 在 Linux 中描述进程的结构体叫做 task_struct

  • task_struct 是 Linux 内核的一种数据结构,它会被装载到 RAM(内存) 里并且包含着进程的属性信息。

在这里插入图片描述

task_struct

内容分类

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

组织进程

可以在 Linux 的内核源代码里找到它,所有运行在系统里的进程都以 task_struct 链表的形式存在内核里。

查看进程

  1. 进程的信息可以通过 /proc 系统文件夹查看。如:要获取 PID 为 1 的进程信息,则需要查看 /proc/1 这个文件夹。
    在这里插入图片描述
    在这里插入图片描述

  2. 大多数进程信息同样可以使用 topps 这些用户级工具命令来获取。

另外可以注意到 OS 会给每个登录用户分配一个 bash 进程。

通过系统调用获取进程标示符 PID

通过查看 man 手册可以知道在代码层面进程的 PID 如何获取:
在这里插入图片描述
我们可以用代码测试看看:

#include <stdio.h>                                                                                                                                                       
#include <unistd.h>int main()
{printf("当前进程的 PID 为 %d\n", getpid());printf("当前进程父进程的 PID 为 %d\n", getppid());return 0;
}   

在这里插入图片描述

通过系统调用 fork 函数创建进程

简单使用

通过查看 man 手册可以知道在代码层面创建进程的 fork 函数信息:
在这里插入图片描述
我们可以测试下面的代码,父进程进入 fork 函数时,会创建子进程,最后两者一起从 fork 函数出来执行 PID 的打印:

#include <stdio.h>
#include <unistd.h>int main()
{printf("父进程 PID 为 %d\n", getpid());fork();									// 父进程进入创建子进程,fork() 调用完后两者同时出来   	printf("进程 PID 为 %d\n", getpid());  	// 父子都独自打印自己的 PID                                                                                                                          return 0;
}

在这里插入图片描述

区分父子进程操作

这也就意味着,当父进程进入 fork 函数创建子进程时,两者代码一样,执行代码命令一样,在上面的代码中 fork 函数后的执行操作就是一样的。

为了区分父子进程,如果是子进程 fork 的返回值规定为 0,如果是父进程则返回大于 0 的数,小于 0 说明创建子进程失败。

接下来测试返回值是否是这样规定的:

#include <stdio.h>
#include <unistd.h>int main()
{int ret = fork();           // ret 同时接收 fork 返回的两个值if (ret < 0)    {   perror("fork");         // 小于 0 表示调用失败return 0;}   else if (ret == 0)			// 0 为子进程{   int child = 2;while (child--){   printf("我是子进程,我的 PID 为 %d\n", getpid());printf("我的父进程 PID 为 %d\n", getppid());};  }   else						// 大于 0 为父进程{   sleep(3);int parent = 3;while (parent--){   printf("我是父进程,我的 PID 为 %d\n", getpid());}                                                                                                                                                                    }   return 0;
}

在这里插入图片描述
可以看到,通过分支语句可以让两个进程共享代码的情况下去执行不同的代码。

这意味着:

  1. fork 函数有两个返回值,父进程有一个,子进程有一个

  2. 父子进程代码共享,数据各自开辟(写时拷贝节省空间)

  3. fork 调用之后通常需要使用 if 分支语句进行分流

父子进程的写时拷贝

另外我们注意到 ret 变量接收 fork 函数返回值,竟然出现不一样的 if 语句跳转。事实上,如果修改父子任何一方的数据,OS 会将被修改数据在底层拷贝一份,让目标进程修改这个拷贝,这种做法叫写时拷贝

#include <stdio.h>
#include <unistd.h>int main()
{int a = 10; int b = 20; int ret = fork();if (ret < 0){   perror("fork");return 0;}   else if (ret == 0){   b = 10; printf("我是子进程,我的 PID 为 %d\n", getpid());printf("我的 ret 变量的值为 %d\n", ret);                                                                                                                             printf("子进程的 a == %d, &a == %p\n", a, &a);printf("子进程的 b == %d, &b == %p\n", b, &b);}   else{   sleep(2);   // 父进程等待 2 秒,让子进程先修改。                                                                                                                     printf("我是父进程,我的 PID 为 %d\n", getpid());printf("我的 ret 变量的值为 %d\n", ret);printf("父进程的 a == %d, &a == %p\n", a, &a);printf("父进程的 b == %d, &b == %p\n", b, &b);}   return 0;
}

在这里插入图片描述

可以看到,两个进程的 b 地址一样,子进程修改后,父进程的 b 变量竟然没有变!

这说明进程变量的地址不是物理地址,而是虚拟的地址!虽然两者地址一样,但是底层的物理地址一定不一样。

这可以说明接收 fork 函数的 ret 变量在被修改时,两个进程的 ret 已经不一样了(写时拷贝执行),if 判断的值自然不一样。

也就是说,进程具有独立性,在大部分运行情况不受其它进程影响。

但为什么 fork 函数返回的值大于 0 为父进程,等于 0 为子进程呢?

因为大于 0 的值实际是子进程的 PID,一个父进程可以有多个子进程,其 PID 拿给父进程用于管理(上述子进程的 PID 由父进程的 ret 保管)。而子进程可以使用 getpid() 函数拿到自己的 PID,使用 getppid() 函数拿到父进程的 PID。其 ret 拿取没有必要,则规定 0 为子进程。

进程状态

在操作系统的概念上说,进程状态有:创建、就绪、运行、阻塞、挂起、结束等状态。
在这里插入图片描述

具体的 Linux 内核解释

运行、阻塞 和 挂起状态

在具体的操作系统也就是 Linux 中,每个 CPU 有一个调度队列 runqueue,只要在 runqueue 调度队列中的进程就算在运行中(也就是包含就绪和运行状态)。
在这里插入图片描述

当进程进入阻塞状态,通常是在等待某种设备或资源就绪,如:C语言的 scanf 函数会等待用户在键盘输入内容加回车后再继续向下执行,若用户不输入,则一直处于阻塞状态。

在阻塞状态时,进程会脱离调度队列被分配到其它等待队列:
在这里插入图片描述

如果键盘输入数据后,OS 会第一时间知道并将获取数据的进程加入调度队列,让该进程进入运行状态。

那什么是挂起状态呢?当内存空间不够时,为保证 OS 本身正常运行,OS 不得不将部分没有使用的进程的代码和数据部分,临时的放入磁盘交换分区。此时在内存中的进程只有 task_struct,代码和数据却放在了磁盘,这就叫做挂起状态。
在这里插入图片描述
如果轮到当前的进程运行、获取数据,OS 又会将对应进程的代码和数据归还给进程。

进程如何被转移

上述中我们注意到一个问题,当进程从 runqueue 分配到 wait queue 时,进程是如何被转移的?

事实上,Linux 进程的 PCB(tast_struct) 使用的是特殊的双链表结构 struct list_task,这个结构只包含 next 和 prev,用于指向后一个节点(task_struct)和前一个节点。在一个 task_struct 中包含多个 struct list_task 就可以在不同队列转移。

当然,这个结构本身不能获取整个 task_struct 结构的信息,但可以使用 C语言的 offsetof 找到其相对于 task_struct 的偏移值,通过 地址移动 + 强制类型转换,即可在不同的队列中获取 task_struct 完整的结构。

在这里插入图片描述

这意味着进程被操作时,删除与插入到其它队列的时间复杂度都是 O(1) 级别,极大的提高了效率。

Linux 进程状态

Linux 对进程的状态具体规定了以下几种(在 Linux 内核里,进程也叫做任务 task)。以下的状态是在 Linux 内核源代码里定义的,可能不适用于其它 OS:

  1. R(running)运行状态:并不意味着进程一定在运行中,只表明进程要么是在运行中要么在运行队列(调度队列)里。

  2. S(sleeping)睡眠状态:意味着进程在等待事件完成(这里的睡眠有时候也叫做 可中断睡眠(interruptible sleep) )。

  3. D(Disk sleep)磁盘休眠状态:有时候也叫不可中断睡眠状态(uninterruptible sleep) ,在这个状态的进程通常会等待 I/O 的结束。

  4. T(stopped)停止状态:可以通过发送 SIGSTOP 信号让进程停止下来,再发送 SIGCONT 信号让进程继续运行。

  5. t(tracing stop)跟踪停止状态:当进程被调试如 gdb 调试代码程序,在断点处停止时的状态。

  6. X(dead)死亡状态:这个状态只是一个返回状态,不能在任务列表里看到这个状态。

  7. Z(zombie)僵尸状态:为了获取进程退出信息的临时状态。

我们着重查看僵尸状态,让子进程只打印自己的 PID 就退出,而父进程则执行死循环:

#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>int main()
{int ret = fork();if (ret < 0){   perror("fork");return 0;}   else if (ret == 0){   printf("我是子进程,我的 PID 为: %d\n", getpid()); // 子进程执行后退出,但父进程没有回收,会一直保持僵尸状态}   else {   printf("我是父进程,我的 PID 为: %d\n", getpid()); // 父进程执行死循环while (true){                                                                                                                                                                    ;   }   }   return 0;
}

在这里插入图片描述
可以看到,子进程状态处于 Z 也就是僵尸状态,而父进程使用 Ctrl + Z 快捷键正处于暂停状态,grep 命令也是一个进程,ps 命令查看正好将其打印出来,它此时处于睡眠状态,S++ 号表示是前台进程,没有则表示后台进程。

另外,D 状态下的进程是无法被 OS 杀掉的,这是为保证重要的数据正常处理,不受 OS 在内存不足时的干扰。但这意味着除非它自己的任务完成,然后自己结束,想要手动结束就只能让电脑关机或断电了。

僵尸进程与其危害

上述的僵尸状态(zombie)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程。

僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。

僵尸进程危害

进程的退出状态必须被维持下去,因为它要告诉与它联系的进程(父进程)自己任务处理的情况。可父进程一直不读取,那子进程就会一直处于 Z 状态。

维护退出状态本身就是要用数据维护(此时进程的代码和数据已经没有了),也属于进程基本信息,所以保存在 task_struct 中。换句话说,Z 状态一直不退出,task_struct 就一直要维护,这就导致内存被浪费,也就是内存泄漏。

如果一个父进程创建很多子进程,但不读取,自己也不退出,就会造成大量的内存资源浪费。

如何让父进程读取子进程状态?使用 wait() 系统调用(有机会再剖析解释)。

孤儿进程

父进程如果提前退出,而子进程后退出,进入 Z 之后,子进程就称之为 " 孤儿进程 "

孤儿进程会被 1 号 init 或 systemd 进程领养,此时由 init / systemd 进程处理子进程,防止内存泄漏:

#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>int main()
{int ret = fork();if (ret < 0){   perror("fork");return 0;}   else if (ret == 0){   printf("我是子进程,我的 PID 为: %d\n", getpid());  // 子进程死循环                                                                                                  while (true){   ;   }   }   else {   printf("我是父进程,我的 PID 为: %d\n", getpid()); // 父进程打印退出}   return 0;
}

在这里插入图片描述
可以看到子进程被 1 号进程 " 领养 ",子进程退出变为僵尸状态,而 1 号进程会定期调用 wait() 回收所有孤儿进程的状态信息,确保其不会长期滞留为僵尸进程。

当然,这种方法只是兜底策略,如果在父进程长期运行的环境下(如服务器),不注意回收子进程的内存泄漏风险依然存在。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/38293.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LLM - CentOS上离线部署Ollama+Qwen2.5-coder模型完全指南

文章目录 离线安装OllamaOllama下载Ollama硬件需求Ollama 常用命令参考Ollama安装Ollama 服务管理&开机启动开启局域网访问 Ollama 服务 离线安装模型gguf 文件格式下载Qwen2.5-Coder-7B-Instruct-GGUF格式选择 ( gguf 版本 )构建Modelfile文件加载并运行离线模型测试 集成…

Linux——信号

目录 Linux——信号1.信号的基础了解2.技术应用角度的信号3.产生信号3.1按键组合3.2系统调用产生信号3.2.1 kill()3.2.2 raise()3.2.3 abort() 3.3**.** 软件条件产生信号3.4硬件异常产生信号3.4.1 /0异常3.4.2 内存越界异常 4.理解信号的存在5.总结一下6.核心转储7.全部信号都…

向量叉积的应用——正反画画

1 解题思路 解题思路涉及的向量积相关知识 c实现 #include<iostream> #include<vector>using namespace std;struct TrianglePoint {int x;int y; };int momentForce(TrianglePoint A, TrianglePoint B, TrianglePoint C) {//AB向量&#xff1a;(B.x-A.x, B.y-A.…

构建自定义MCP天气服务器:集成Claude for Desktop与实时天气数据

构建自定义MCP天气服务器:集成Claude for Desktop与实时天气数据 概述 本文将指导开发者构建一个MCP(Model Control Protocol)天气服务器,通过暴露get-alerts和get-forecast工具,为Claude for Desktop等客户端提供实时天气数据支持。该方案解决了传统LLM无法直接获取天气…

Web安全策略CSP详解与实践

引言 &#xff1a;在黑客攻击频发的今天&#xff0c;你的网站是否像“裸奔”一样毫无防护&#xff1f;跨站脚本&#xff08;XSS&#xff09;、数据注入等攻击随时可能让用户数据泄露。今天我们将揭秘一个网站的隐形保镖——内容安全策略&#xff08;CSP&#xff09;&#xff0c…

HC-05与HC-06蓝牙配对零基础教程 以及openmv识别及远程传输项目的概述

这个是上一年的项目&#xff0c;之前弄得不怎么完整&#xff0c;只有一个openmv的&#xff0c;所以openmv自己去我主页找&#xff0c;这篇主要讲蓝牙 这个是我在使用openmv连接单片机1然后单片机1与单片机2通过蓝牙进行通信 最终实现的效果是&#xff1a;openmv识别到图形和数…

点云分割方法

点云分割 通过判断三维距离&#xff0c;实现对创建3团点云的分割 通过判断三维距离&#xff0c;实现对创建3团点云的分割 * 点云1 gen_object_model_3d_from_points (rand(100), rand(100),rand(100), Points1)* 点云2 gen_object_model_3d_from_points (rand(100), 2rand(100…

SpringBoot3使用CompletableFuture时java.util.ConcurrentModificationException异常解决方案

问题描述 在Spring Boot 3项目中&#xff0c;使用CompletableFuture进行异步编程时&#xff0c;偶发{"code":500,"msg":"java.util.ConcurrentModificationException"}异常&#xff0c;但代码中并未直接操作List或CopyOnWriteArrayList等集合类…

细说卫星导航:测距定位原理

测距定位原理 1. 伪距测量技术 核心原理&#xff1a;卫星发射信号&#xff0c;用户接收并记录传播时间&#xff0c;乘以光速得到距离&#xff08;伪距&#xff09;。 技术细节&#xff1a; 信号传播路径分析 信号结构&#xff1a; 卫星信号包含三部分&#xff1a; 载波&…

Linux系统管理与编程09:任务驱动综合应用

兰生幽谷&#xff0c;不为莫服而不芳&#xff1b; 君子行义&#xff0c;不为莫知而止休。 [环境] windows11、centos9.9.2207、zabbix6、MobaXterm、Internet环境 [要求] zabbix6.0安装环境&#xff1a;Lamp&#xff08;linux httpd mysql8.0 php&#xff09; [步骤] 5 …

RAG(Retrieval-Augmented Generation)基建之PDF解析的“魔法”与“陷阱”

嘿&#xff0c;亲爱的算法工程师们&#xff01;今天咱们聊一聊PDF解析的那些事儿&#xff0c;简直就像是在玩一场“信息捉迷藏”游戏&#xff01;PDF文档就像是个调皮的小精灵&#xff0c;表面上看起来规规矩矩&#xff0c;但当你想要从它那里提取信息时&#xff0c;它就开始跟…

RK3568 I2C底层驱动详解

前提须知&#xff1a;I2C协议不懂的话就去看之前的内容吧&#xff0c;这个文章需要读者一定的基础。 RK3568 I2C 简介 RK3568 支持 6 个独立 I2C: I2C0、I2C1、I2C2、I2C3、I2C4、I2C5。I2C 控制器支持以下特性: ① 兼容 i2c 总线 ② AMBA APB 从接口 ③ 支持 I2C 总线主模式…

UNIX网络编程笔记:基本TCP套接字编程

一、socket函数 一、socket函数核心参数与协议组合 函数原型与基本功能 #include <sys/socket.h> int socket(int family, int type, int protocol);• 功能&#xff1a;创建通信端点&#xff08;套接字&#xff09;&#xff0c;返回描述符供后续操作。 • 返回值&#…

JSON在AutoCAD二次开发中应用场景及具体案例

配置文件的读取 在AutoCAD插件开发中&#xff0c;可能需要生成、修改、读取配置文件中一些参数或设置。JSON格式的配置文件易于编写和修改&#xff0c;且可以方便地反序列化为对象进行使用。 运行后效果如下 using Autodesk.AutoCAD.ApplicationServices; using Autodesk.Au…

自由学习记录(46)

CG语法的数据类型 // uint : 无符号整数&#xff08;32位&#xff09; // int : 有符号整数&#xff08;32位&#xff09; // float : 单精度浮点数&#xff08;32位&#xff09;&#xff0c;通常带后缀 f&#xff08;如 1.0f&#xff09; // half : 半精度浮…

解决Selenium滑动页面到指定元素,点击失效的问题

White graces&#xff1a;个人主页 &#x1f649;专栏推荐:Java入门知识&#x1f649; &#x1f439;今日诗词:君失臣兮龙为鱼&#xff0c;权归臣兮鼠变虎&#x1f439; ⛳️点赞 ☀️收藏⭐️关注&#x1f4ac;卑微小博主&#x1f64f; ⛳️点赞 ☀️收藏⭐️关注&#x1f4…

Vue基础

目录 -Vue基础- 1、插值表达式 {{}} 2、Vue核心特性&#xff1a;响应式 3、开发者工具Vue Devtools(极简插件下载) 4、Vue指令 v-text v-html v-bind v-on v-if v-show v-for v-model 5、Vue指令修饰符 .stop .prevent .capture .self .once .enter、.tab、…

收数据花式画图plt实战

目录 Python plt想把纵坐标化成对数形式代码 子图ax. 我又有ax scatter&#xff0c;又有ax plot&#xff0c;都要去对数 数字接近0&#xff0c;取对数没有定义&#xff0c;怎么办 创建数据 添加一个小的常数以避免对数未定义的问题 创建一个figure和一个子图ax 在子图a…

二项式分布(Binomial Distribution)

二项式分布&#xff08;Binomial Distribution&#xff09; 定义 让我们来看看玩板球这个例子。假设你今天赢了一场比赛&#xff0c;这表示一个成功的事件。你再比了一场&#xff0c;但你输了。如果你今天赢了一场比赛&#xff0c;但这并不表示你明天肯定会赢。我们来分配一个…

【算法工程】大模型开发之windows环境的各种安装

1. 背景 最近由于研究需要&#xff0c;我购置了两块3090显卡&#xff0c;以便在家中进行一些小规模的实验。为此&#xff0c;还更换了主机。当然&#xff0c;新系统上少不了要安装各种开发环境。从开发体验来看&#xff0c;macOS无疑更为流畅&#xff0c;但为了确保所有环境都能…