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

Linux图文理解进程

目录

    • 书上对于进程的介绍
    • 进程和程序的差别
    • 为什么说进程对比程序是动态的,动态怎么体现?
    • 创建进程
      • fork原理
    • 进程状态
      • 运行状态
      • 阻塞状态
      • 挂起
      • Linux中的进程状态
      • 僵尸状态
      • 孤儿进程
    • 进程终止
      • _exit函数
      • exit函数
      • return退出
    • 进程等待
      • 阻塞等待
        • wait方法
        • waitpid方法
        • 等待多个子进程
      • 非阻塞等待
        • waitpid方法
    • 进场地址空间文章跳转

书上对于进程的介绍

书本上对于进程的介绍:

  • 运行起来的程序
  • 在内存中的程序
  • 进程是动态的
  • 程序是静态的

不好理解。
想知道什么是进程,就要能说的出进程和程序的差别是什么?

进程和程序的差别

我们自己编程好的C、C++代码程序在哪?
磁盘。
在这里插入图片描述
这样就是进程?静态的程序加载到内存就变成动态的进程了吗?
体现在哪里?

在加载到内存之前,操作系统是最先存在的,也就是操作系统帮忙把进程加载到内存。如果说我们刚刚的加载形成了一个进程,在那之前,操作系统就已经同时存在了非常多的进程。(比如一边听音乐,一边写博客)
所以操作系统要管理所有的进程。
管理的方法:先描述,再组织。
在这里插入图片描述

每个进程被加载的时候,先描述进程,每个进程用一个结构体描述
把每个进程连接起来,对进程的操作就类似于对链表的增删查改了。

对于这个结构体,教材上喜欢称为进程的PCB
在这里插入图片描述

在这里插入图片描述
理解下来就明白,教材里说的进程在排队是什么意思了:
和可执行程序是没有关系的,排队排的是PCB

为什么说进程对比程序是动态的,动态怎么体现?

从上我们可以理解了进程和程序的区别。
那么说进程是动态的,动态体现在哪?
各位在写代码的时候,最简单的是while(1)死循环,难道这个进程在跑的时候一直占着CPU吗?
我们观察发现跑的时候确实有时候会卡一会,可以正常运行但又不会影响别的进程运行。就像我们边听音乐边看博客。
进程是有状态的,进程在内存里,可能被调度,也可能不被调度,也就是看进程的状态,CPU调度的时候它就跑一下,不调度的时候就不跑,就像有生命一般,也就是动态体现。
我的代码可能现在或者即将被CPU一行一行的执行,执行可能并不会执行完,过一会再继续上次位置继续执行,进程和程序相比就具有了动态的特性。

在未来的学习中,情况可能会更复杂,我们可能会看到通过指针的方式:一个进程里的PCB在多个链表(多个数据结构),这个PCB可能同时存在于多个位置(阻塞队列、挂起队列等)

在这里插入图片描述

创建进程

在这里插入图片描述
在创建进程前,我们需要学会怎么获得当前进程自身的ID和父进程的ID

如何获得pid,ppid?
在上面我们学会了每个进程都有自身的PCB,同时
每个进程都有唯一的 PID,用于标识自身。
每个进程(除初始化进程外)都有一个 PPID,用于标识其创建者(父进程)。

系统调用接口:
getpid() 和 getppid()

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

启动一个进程,如何理解这种行为呢?本质就是系统多了一个进程,os要管理的进程也就多了一个,进程=可执行程序 + task struct 对象(内核对象),
Linux中,进程的PCB具体是struct task struct{} ,知道说的是什么即可

创建一个进程,就是系统中要申请内存,保存当前进程的可执行程序+task struct 对象,并将task struct对象添加到进程列表中!

代码创建进程
在这里插入图片描述

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{printf("我是一个父进程,我的pid是: %d\n", getpid());//创建子进程fork();while(1){printf("我是一个进程,我的pid是: %d, ppid: %d\n", getpid(),getppid());sleep(1);}return 0;
}

在这里插入图片描述
可以看到父进程的pid是:2447
子进程的pid是2448

可以得出结论:只有父进程执行了fork之前的代码,在fork之后,父进程和子进程都会执行后续的代码。

我们知道fork是有返回值的,我们更改一下代码接收一下fork的返回值,然后打印出来看一下
在这里插入图片描述

结论:fork成功之后,有两个返回值,给子进程返回0,给父进程返回子进程的pid

1.我们为什么要创建子进程?我们想让子进程协作父进程完成一些工作,这些工作是单进程解决不了的,比如我要一边听音乐一边下载游戏。
2.我们创建子进程是为了让子进程和我父进程做不一样的事情。

要让子进程和父进程做不一样的事情,就要执行不一样的代码,就要区分出谁是子进程,谁是父进程。
为什么fork会有两个返回值,就好理解了,通过fork的返回值,判断父还是子,从而执行不同的代码。

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{int ret = fork();if(ret < 0){perror("fork");return 1;}else if(ret == 0){ //childprintf("I am child : %d!, ret: %d\n", getpid(), ret);}else{ //fatherprintf("I am father : %d!, ret: %d\n", getpid(), ret);}sleep(1);return 0;
}

fork原理

在这里插入图片描述
创建完子进程之后,父进程,子进程都会在运行队列中排队
哪一个进程的PCB先被调度,哪个进程就先运行。(这是不确定的,由操作系统根据优先级等决定)

所以fork之后,父子之间谁先运行?
不确定。

fork为什么有两个返回值?
在这里插入图片描述
类似的,我们写的函数中,局部变量具有临时性,局部性,局部变量怎么被拿到函数外面,就是通过CPU的寄存器做到的,一个寄存器返回一个返回值,那两个寄存器就对应两个返回值了。

进程状态

在这里插入图片描述
介绍三个状态:

  1. 运行状态
  2. 阻塞状态
  3. 挂起

运行状态

只要在运行队列中的进程,状态都是运行状态
运行状态并不代表正在运行,而是代表运行队列中的PCB随时可以被调度
在这里插入图片描述

阻塞状态

当我们的程序开始运行
代码中一定会或多或少的访问外设,比如键盘
scanf() 或者 cin>> 本质是想从键盘读取数据

这个时候我一直不按键盘,键盘上的数据就没有就绪
进程要访问的资源没有就绪,代码无法向后执行

操作系统要管理外设,一样是通过类似结构体对象的方式管理
在这里插入图片描述
系统想看键盘有没有准备好,只需要看键盘结构对应的属性就可以确定
如果没有准备好,就把PCB链入等待队列,如果还有另一个没有就绪的进程,那它的PCB就会从运行队列中脱离,并继续链入等待队列

这就是阻塞状态

所以进程变化的本质就是:

  1. 把static变量更改
  2. 把PCB链入不同的队列之中

平时一边下东西,一边看视频,电脑卡顿也是因为,网卡资源一直被申请,就会导致进程有时候在运行队列,有时候不在运行队列

挂起

如果一个进程当前被阻塞了
注定了,这个进程在它所等待的资源没有就绪的时候,该进程是无法被调度
如果此时,恰好OS内的内存资源已经严重不足了

OS就会把处在等待队列中的进程,也就是阻塞的进程(PCB+代码数据)中的代码数据置换到外设
等到进程再次被调度的时候,再把代码和数据重新加载进来
在这里插入图片描述

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 信号让进程继续运行。
t状态
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态

S睡眠状态相当于概挂起,轻度睡眠
而D休眠状态是重度睡眠,专门针对磁盘设计的
原因是:Linux在实在没办法时候,会杀掉进程,节省资源
如果被杀掉的进程是S状态,也就是挂起队列,这个时候这个进程的代码和数据是在磁盘的,这个进程是试图往磁盘写入数据的,正在等。这个时候操作系统把这个进程杀掉,同时磁盘写入失败的话,数据就会彻底丢失。

所以D状态就是为了防止进程被杀掉设计的,当进程处于D状态,它就不可以被杀掉,操作系统也不行,只能等。
D状态一旦出现,就说明磁盘已经很忙了,很有可能写入失败,也就是磁盘要爆了,如果用户看到有进程出现了D状态,说明计算机就快挂了。

T状态:在进程访问软件资源时,可能暂时不给访问,就会把进程设为T状态
t状态:debug程序的时候,追踪程序,遇到断点,就暂停程序
这两个状态其实也就是概念里的堵塞状态

僵尸状态

Z,僵尸状态

进程的创建,代表要完成某种任务
当进程退出时,必须要有退出信息表明自己任务完成的如何,所以必须要有人接受进程的退出信息。
当进程退出,退出信息会由OS写入到退出进程的PCB中,进程的代码和数据被释放,但是PCB不会被立刻释放。
如果这个进程的父进程一直不读取退出进程的PCB,子进程就一直处于Z状态,PCB也要一直维护,占用空间,用户想用指令杀也杀不掉,因为无法杀掉一个死去的进程。

如果一个父进程创建了很多子进程,子进程退出都不读取它的状态,就会造成内存资源的浪费,也就是内存泄漏

孤儿进程

父进程先退出,子进程就称之为“孤儿进程”,为了防止大量孤儿进程占用资源,子进程就需要被领养
由一号进程(就是操作系统,有的叫systemd,有的叫initd)领养

进程终止

进程退出场景
代码运行完毕,结果正确
代码运行完毕,结果不正确
代码异常终止

_exit函数

#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
说明:虽然status是int,但是仅有低8位可以被父进程所用。
所以_exit(-1)时,在终端执行$?发现返回值是255

exit函数

#include <unistd.h>
void exit(int status);

1.exit 是库函数,_exit是系统调用
2.exit终止进程的时候,会自动刷新缓冲区。 exit终止进程的时候,不会自动刷新缓冲区

return退出

return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。

进程等待

子进程退出,父进程如果不接收子进程的退出信息,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

阻塞等待

wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:成功返回被等待进程pid,失败返回-1。
参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:当正常返回的时候waitpid返回收集到的子进程的进程ID;如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:pid:Pid=-1,等待任一个子进程。与wait等效。Pid>0.等待其进程ID与pid相等的子进程。status:输出型参数,包含了进程的退出信息options:0:阻塞等待WNOHANG: 非阻塞等待

wait是阻塞等待,意思是当父进程用了wait(),就会一直等到子进程僵尸,wait自动回收。如果子进程不退出,父进程就会一直等。
waitpid看参数options,如果是0,就也是阻塞等待。

演示时先按阻塞等待示范,然后再说非阻塞等待
在这里插入图片描述

在低16位中
低 8 位:

  • 第 0~6 位:终止信号编号(如果进程被信号终止)
  • 第 7 位:core dump 标志(如果进程产生了 core dump 文件)

高 8 位:

  • 如果进程正常终止,高 8 位存储退出状态码(exit(status) 的参数)

status&0x7F 拿到bit位的0~7位
(status>>8)&0xFF 拿到bit位的8~15位

举例

0000 0001
&
0111 1111 0x7F
0000 0001

除了用&的方法拿到bit位,也可以用函数
WIFEXITED(status): 查看进程的退出信号
WEXITSTATUS(status): 查看进程的退出码

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>//int status = 0;
void Worker(int number)
{int cnt = 10;while(cnt){printf("I am child process, pid: %d, ppid: %d, cnt: %d, number: %d\n", getpid(), getppid(), cnt--, number);sleep(1);int a = 10;a /= 0;}
}int main()
{pid_t id = fork();if(id == 0){//childWorker();exit(1);}else{// fatherprintf("wait before\n");int status = 0;pid_t rid = waitpid(id, &status, 0);printf("wait after\n");if(rid == id){// 不能对status整体使用,要读它的bit位printf("wait success, pid: %d, rpid: %d, exit sig: %d, exit code: %d\n", getpid(), rid, status&0x7F, (status>>8)&0xFF);}}return 0;
}

在这里插入图片描述
在这里插入图片描述
结论:
如果异常信号不是0,就说明程序异常退出,不用看错误码。
如果异常信号为0,错误码错误,说明程序虽然正常执行,但结果错误
如果异常信号为0,错误码正确,说明程序正常执行,结果正确

在这里插入图片描述
比如图就是,异常信号为8,说明程序异常退出

等待多个子进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>//int status = 0;
void Worker(int number)
{int *p = NULL;int cnt = 10;while(cnt){printf("I am child process, pid: %d, ppid: %d, cnt: %d, number: %d\n", getpid(), getppid(), cnt--, number);sleep(1);//*p = 100;//int a = 10;//a /= 0;}
}
const int n = 10;int main()
{for(int i = 0;i < n; i++){pid_t id = fork();if(id == 0){Worker(i);//status = i;exit(0);}}//等待多个子进程for(int i = 0; i < n; i++){int status = 0;pid_t rid = waitpid(-1, &status, 0); // pid>0:指定id pid=-1:任意一个退出的子进程if(rid > 0){printf("wait child %d success, exit code: %d\n",rid, WEXITSTATUS(status));}}return 0;
}

非阻塞等待

waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:当正常返回的时候waitpid返回收集到的子进程的进程ID;如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:pid:Pid=-1,等待任一个子进程。与wait等效。Pid>0.等待其进程ID与pid相等的子进程。status:输出型参数,包含了进程的退出信息options:0:阻塞等待WNOHANG: 非阻塞等待

刚刚阻塞等待演示时,主要介绍了pid 和 status参数
现在着重演示返回值和options中的WNOHANG

返回值:
ret > 0 当正常返回的时候waitpid返回收集到的子进程的进程ID;
ret == 0 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
ret < 0 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

非阻塞等待一般搭配循环
非阻塞等待失败就做一下自己的事情,等过会再去查看等待结果

#include <stdio.h> 
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{pid_t pid;pid = fork();if(pid < 0){printf("%s fork error\n",__FUNCTION__);return 1;}else if( pid == 0 ){ //childprintf("child is run, pid is : %d\n",getpid());sleep(5);exit(1);} else{int status = 0;pid_t ret = 0;do{ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待if( ret == 0 ){printf("child is running\n");}sleep(1);}while(ret == 0);if( WIFEXITED(status) && ret == pid ){printf("child return code is :%d.\n",WEXITSTATUS(status));}else{printf("wait child failed, return.\n");return 1;}}return 0;
}

进场地址空间文章跳转

有兴趣的可以移步我的下一篇文章学习:进场地址空间学习

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

相关文章:

  • fiddler实用用法,抓包内容导入到apipos
  • 数据库管理系统:入门需要了解的内容
  • Modbus核心参数,调试工具,接线注意事项
  • Mongodb常用命令简介
  • C++线程库的学习
  • 从Centos 9 Stream 版本切换到 Rocky Linux 9
  • MongoDB数据存储界的瑞士军刀:cpolar内网穿透实验室第513号挑战
  • IDEA-Research推出的一系列检测、分割模型:从DINO(改进版DETR)、Grounding Dino、DINO-X到Grounded SAM2
  • 串联所有单词的子串-leetcode
  • 计算机基础·linux系统
  • Linux线程学习
  • pytorch学习笔记-最大池化maxpooling的使用、搭建多层网络并验证、sequential的使用
  • golang的面向对象编程,struct的使用
  • 2.8 逻辑符号
  • Linux怎么查看时区信息?(Linux时区)(tzselect)
  • Java中接口与抽象类
  • 处理失败: module ‘fitz‘ has no attribute ‘open‘
  • 传统防火墙与下一代防火墙
  • 华为 2025 校招目标院校
  • 【2025最新】在 macOS 上构建 Flutter iOS 应用
  • 嵌入式学习---在 Linux 下的 C 语言学习 Day10
  • 可执行文件的生成与加载执行
  • 超高车辆如何影响城市立交隧道安全?预警系统如何应对?
  • [论文阅读] 软件工程 | 软件工程中的同理心:表现、动机与影响因素解析
  • oracle 11G安装大概率遇到问题
  • 大文件断点续传(vue+springboot+mysql)
  • Failed to restart docker.service: Unit docker.service is masked.
  • PostgreSQL 数据库 设置90天密码过期时间的完整方案
  • 读取了错误数据导致STM32 单片机Hard Fault
  • 智能升级革命:Deepoc具身模型开发板如何让传统除草机器人拥有“认知大脑”