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

Linux进程 -fork(初识),进程状态和进程优先级

目录

一、通过系统调用创建进程-fork

1.fork的介绍

 2.fork的理解

3.fork常规用法

4.fork的三个问题 

5.创建多个子进程 

二、进程状态 

(1)Linux内核源代码

(2)进程的状态 

R运行状态(运行态)

S 睡眠状态(sleeping)和 D 磁盘休眠状态(disk sleep)

T 停止状态(stopped)

Z 僵尸状态(zombie)-- 僵尸进程 

 孤儿进程

三、进程优先级 

(1)基本概念

 (2)查看进程优先级的命令


一、通过系统调用创建进程-fork

1.fork的介绍

平时创建进程一般是通过 ./myproc 运行某个存储在磁盘上的可执行程序来创建。而我们还可以通过系统调用接口来创建进程。

pid_t是无符号整型。我们先看一段代码。

 1 #include<stdio.h>2 #include <unistd.h>3 int main()4 {5   printf("I am a father: %u\n", getpid());6     fork();7 8     while(1)9     {10         printf("I am a process, pid: %u, ppid: %u\n", getpid(), getppid());11         sleep(1);12     }13 14     return 0;                                                                                      15 }

 pid为31318即为创建的子进程id

当我们查看进程

此时有三个进程:分别为父进程和fork创建的子进程和grep进程

 2.fork的理解

从代码的角度看

父子进程共享用户代码(代码是只读的,不可写),而用户数据各自私有一份(为了不让进程互相干扰),采用写时拷贝技术。

fork 之后子进程会被创建成功,然后父子进程都会继续运行,但谁先运行是不确定的,由系统调度优先级决定。

从内核的角度看 

对于操作系统来说,通过fork后,系统多了一个进程。

具体是,fork后以父进程为模板,操作系统创建新的PCB,把父进程PCB的内容属性拷贝过来,他们共享代码和数据。

3.fork常规用法

我们创建子进程的目的是为了让子进程给我们完成任务,所以 fork 之后通常要用 if 进行分流,让父子进程执行不同的代码,实现一个并行的效果。(比如父进程播放音乐,子进程下载文件)

通过 fork 的两个返回值来进行分流:

  • 如果 fork 执行成功,在父进程中返回子进程的 pid,在子进程中返回 0。
  • 如果 fork 执行失败,在父进程中返回 -1,不创建子进程,并适当地设置 errno。
#include <stdio.h>  
#include <sys/types.h> // getpid, getppid  
#include <unistd.h>    // getpid, getppid, forkint main()  
{  printf("I'm a father: %u\n", getpid());pid_t ret = fork();if (ret == 0){  // child processwhile (1){printf("child process, pid:%u, ppid:%u\n", getpid(), getppid());sleep(1);}}else if (ret > 0){// father processwhile (1){printf("father process, pid:%u, ppid:%u\n", getpid(), getppid());sleep(1);}}else{// failureperror("fork");return 1;}return 0;            
}

  这一份代码为什么会出现父进程和子进程一起循环呢?

这里给大家抛出三个问题

  • fork为什么有两个返回值?
  • 为什么上述代码中,fork 的返回值 ret 有两个值,既等于 0 又大于 0 呢?fork 之后,父子进程如何做到共享用户代码,如何做到用户数据各自私有的呢?
  • 如果 fork 执行成功,为什么在父进程中返回子进程的 pid,在子进程中返回的是 0 呢?

4.fork的三个问题 

(1)两个返回值问题

fork函数一直往下执行

  • 在执行到最后ret之前,子进程已经被创建出来了,在上面我们说父进程和子进程的代码是共享的,那么这个return ret是不是一份代码呢?
  • 答案肯定是的,那么是代码子进程也会执行return ret。
  • 所以这就是为什么有两个返回值

(2)一个变量为什么会存在两个值呢? 

这个在我们后面讲进程地址空间的时候会给大家介绍,暂时不多做解释。

(3) 为什么在父进程中返回子进程的 pid,在子进程中返回的是 0 呢?

举一个例子,一位父亲有很多孩子,那么该怎么辨别这些孩子呢?这就需要给孩子标识并记住它。而每个孩子只有唯一一个父亲,所以能很好的辨别父亲。

所以在父进程中需要返回子进程的 pid,因为得让父进程知道自己的子进程(儿子)是谁。

而子进程只需要知道自己被创建成功了就行,所以在子进程中返回 0 即可。

5.创建多个子进程 

#include <stdlib.h>68 void runchild()69 {70   int cnt=10;71   while(cnt)72   {73     printf("i am a child:%d,ppid:%d",getpid(),getppid());74     sleep(1);75     cnt--;76   }77 }78 int main()79 {80   int i=0;                                                                                         81   for(i=0;i<5;i++)82   {83     pid_t id=fork();84     if(id==0)85     {86       runchild();87       exit(0);88     }89     sleep(100);90   }91 }

二、进程状态 

进程的状态体现一个进程的生命状态。

(1)Linux内核源代码

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)进程的状态 

R运行状态(运行态)

当我们写一段代码,为什么状态时是S+而不是R呢?

  • 因为CPU的运行速度非常快,而进程绝大多数时间都在休眠(sleep(1)),只有极少数的时间在运行 。
  • 因为 printf 是往显示器上打印,涉及到 IO,所以效率比较低,该进程需要等待操作系统把数据刷新到显示器中。

那有一个问题了?如果该进程的状态是R,那它一定在CPU上运行吗?

不一定,在CPU上运行一定是R状态,但一个进程状态是R,也有可能在运行队列中。 

 如果想看到R状态,我们只需要写一个while(1)即可。

S 睡眠状态(sleeping)和 D 磁盘休眠状态(disk sleep)

S:休眠状态(sleeping)(也可以叫阻塞状态

  • 表示进程虽然是一种休眠状态,但随时可以接受外部的信号,处理外部的请求,被唤醒。

当键盘还未输入数据时,却在内存中,如图在等待队列中等待,可以叫做阻塞状态,可以随时接受外接的信号,被唤醒。

当在等待队列中,如果操作系统内部的内存资源严重不足时,在保证正常的情况,队列中只存在PCB,把对应的代码和数据返回到外设中,当存在响应的时候,再把代码和数据换入,再放到运行队列中。这种状态叫做挂起状态 

D:磁盘休眠状态(disk sleep)深度休眠) 

比如:进程 A 想要把一些数据写入磁盘中,因为 IO 需要时间,所以进程 A 需要等待。但因为内存资源不足,在等待期间进程 A 被操作系统 kill 掉了,而此时磁盘因为空间不足,写入这些数据失败了,却不能把情况汇报给进程 A,那这些数据该如何处理呢?很可能导致这些数据被丢失,操作系统 kill 掉进程 A 导致了此次事故的发生。所以诞生了 D 状态,不可以被杀掉,即便是操作系统。只能等待 D 状态自动醒来,或者是关机重启

 S状态和S+状态有什么区别呢?

S+ 状态:表示前台进程。(前台进程一旦运行,bash 就无法进行命令行解释,使用 Ctrl+C 可以终止前台进程)
S 状态:表示后台进程。(后台进程在运行时,bash 可以进行命令行解释,使用 Ctrl+C 无法终止后台进程)

T 停止状态(stopped)

我们可以通过kill命令,让进程进入T状态也就是停止状态,停止运行了。

举个例子:

  • 我们给进程发 19 号信号 SIGSTOP,可以让进程进入 T 停止状态。停止运行。

  • 我们给进程发 18 号信号 SIGCONT,可以让进程停止 T 停止状态。恢复运行。

Z 僵尸状态(zombie)-- 僵尸进程 

我们先看一段代码

#include <stdio.h>
#include <stdlib.h>    // exit
#include <sys/types.h> // getpid, getppid  
#include <unistd.h>    // getpid, getppid, fork, sleepint main()
{// 创建5个子进程for (int i = 0; i < 5; i++){pid_t ret = fork();if (ret == 0){// child processprintf("child%d, pid:%u, ppid:%u\n", i, getpid(), getppid());sleep(1);exit(1); // 子进程退出}}getchar(); // getchar()目的是不让父进程退出,则无法回收子进程。return 0;
}

 成功创建了 5 个子进程。但程序会一直卡在这里,不会自己退出。

我们发现五个子进程全部变僵尸进程了(Z状态) 

那什么是僵尸状态呢?

要知道,进程退出,一般不是立马就让操作系统回收进程的所有资源。

因为创建进程的目的,是为了让它完成某个任务和工作。当它退出时,我们得知道它把任务完成的怎么样,所以需要知道这个进程是正常还是异常退出的。

如果进程是正常退出的,那么交给进程的任务有没有正常完成呢?
所以,进程退出时,会自动将自己的退出信息,保存到进程的 PCB 中,供 OS 或者父进程来进行读取。

进程退出但父进程还没有读取,进程此时就处于僵尸状态。
读取成功后,该进程才算是真正的死亡,变成 X 死亡状态。

僵尸状态的概念:

  • 僵死状态(Zombies)是一个比较特殊的状态。当子进程退出,并且父进程没有读取到子进程退出时的返回代码时就会产生僵死(尸)进程。(父进程使用系统调用 wait() 让 OS 回收子进程)
  • 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出代码。
  • 所以,只要子进程退出,父进程还在运行,但父进程没有读取到子进程状态,子进程就会进入 Z 状态。
  • 父进程通过调用 getchar() 函数来等待用户输入,这样做可以防止父进程过早退出,在大多数情况下,这也意味着父进程不会立即回收结束的子进程资源,因为没有调用 wait / waitpid 函数来等待子进程结束。 
  • 虽然父进程通过 getchar() 等待,但这并不是处理僵尸进程(已结束但未被父进程回收的子进程)的正确做法。在实际应用中,父进程应该使用 wait / waitpid 函数来等待子进程结束,并回收它们的资源,以避免僵尸进程的产生。
  • 进程处于Z状态,资源会被一直占用,进程相关资源task_struct不能被释放,导致内存泄漏。等后面给大家介绍进程等待能很好的解决内存泄露问题。

 孤儿进程

若子进程先退出,父进程没回收,则子进程为僵尸进程

若父进程先退出,子进程将被1号进程领养(父进程改为1号进程),子进程称作孤儿进程

我们能看到,父进程退出后,子进程的父进程变成了1。 

我们能看到1号进程就是我们的操作系统。

三、进程优先级 

优先级 vs 权限,两者有什么区别呢?

  • 优先级:在资源有限的前提下,确立多个进程中谁先访问资源,谁后访问资源。
  • 权限:决定能不能得到某种资源。

(1)基本概念

在 Linux 或者 Unix 系统中,使用命令 ps -al 查看当前系统进程的信息:

  • PRI:优先级,值越小,优先级越大。
  • NI:nice,进程优先级的修正数据,范围调整[-20,19]。
  • UID:用户的ID名,执行者ID。
  • 进程新的优先级:PRI(new) = PRI(old, 默认都是 80) + nice
  •  优先级不可能一味的高,也不可能一味的低。因为 OS 的调度器也要考虑公平问题。
  • 进程的 nice 值不是进程的优先级,他们不是一个概念,但是进程的 nice 值会影响到进程的优先级变化。

 (2)查看进程优先级的命令

 通过 top 命令(类似于 Windows 的任务管理器)更改已存在进程的 nice:

  • 执行 top 命令后,按 r 键,输入进程的 PID,输入 nice 值。

 

每次输入 nice 值调整进程优先级,都是默认从 PRI = 80 开始调整的。
输入的 nice 值如果超过 [-20, 19] 这个范围,默认是按照最左/最右范围来取的。

为什么每次都要默认从 PRI = 80 开始调整呢?

  • 有一个基准值,方便调整。
  • 在设计上,实现比较简单。

为什么 nice 值的范围是 [-20, 19] 呢?

是一种可控状态,保证了进程的优先级始终在 [60, 99] 这个范围内,保证了 OS 调度器的公平。但公平并不是平均。根据每个进程的特性尽可能公平的去调度它们,而不是指每个进程的调度时间必须完全一样。 

  • 竞争性:系统进程数目众多,而 CPU 的资源很少,甚至只有一个,所以进程之间是具有竞争属性的。为了更高效的完成任务,更合理的竞争相关资源,便有了优先级。
  • 独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰。(这也是 OS 设计进程的一个原则)
  • 并发:多个进程在一个 CPU 下采用进程切换的方式,在同一段时间内,让多个进程都得以推进。(描述的时间段)
  • 并行:多个进程在多个 CPU 下同时运行。(描述的是时刻,任何一个时刻,都可能有多个进程在运行)
http://www.lryc.cn/news/526619.html

相关文章:

  • 数据从前端传到后端入库过程分析
  • macOS如何进入 Application Support 目录(cd: string not in pwd: Application)
  • 第38周:猫狗识别 (Tensorflow实战第八周)
  • 【2024年华为OD机试】 (A卷,200分)- 计算网络信号、信号强度(JavaScriptJava PythonC/C++)
  • 【go语言】数组和切片
  • 2025美赛MCM数学建模A题:《石头台阶的“记忆”:如何用数学揭开历史的足迹》(全网最全思路+模型)
  • 使用 Docker Compose 一键启动 Redis、MySQL 和 RabbitMQ
  • 新增自定义数据功能|UWA Gears V1.0.7
  • docker 简要笔记
  • 在Ubuntu上使用Apache+MariaDB安装部署Nextcloud并修改默认存储路径
  • 【JavaEE】-- 计算机是如何工作的
  • 政安晨的AI大模型训练实践三:熟悉一下LF训练模型的WebUI
  • 基于微信小程序的网上订餐管理系统
  • 科技快讯 | 理想官宣:正式收费!WeChat 港币钱包拓宽商户网络;百川智能发布深度思考模型Baichuan-M1-preview
  • 【java数据结构】map和set
  • 飞牛NAS安装过程中的docker源问题
  • Linux(Centos 7.6)命令详解:dos2unix
  • Linux MySQL离线安装
  • 声明,这些内容和我无关
  • ISO:摄影中的光线敏感度密码
  • 长短期记忆网络LSTM
  • 2. 握手问题python解法——2024年省赛蓝桥杯真题
  • poi在word中打开本地文件
  • 国产编辑器EverEdit - 输出窗口
  • 整数的个数(信息学奥赛一本通-1067)
  • ios swift画中画技术尝试
  • MyBatis 写法
  • Three城市引擎地图插件Geo-3d
  • 【贪心算法】洛谷P1106 - 删数问题
  • WPS计算机二级•幻灯片的页面布局