⭐️这篇博客就要开始聊一聊进程相关的内容了,在聊这个之前,我们还需要了解一下操作系统相关和管理的概念,这样更加有助于我们了解进程的内容了。


🌏冯若依曼体系结构

冯·诺伊曼体系结构是现代计算机的基础,现在大多计算机仍是冯·诺伊曼计算机的组织结构。(下面是一张冯若依曼体系结构的图片)

  • 存储器: 对应的是我们电脑中的内存
  • 中央处理器CPU: 其中由运算器和控制器两个部分构成
  • 输入设备: 包括键盘、硬盘、鼠标等
  • 输出设备: 硬盘、显示器等(输入设备和输出设备统称为外设)

在这里插入图片描述
从这张图片中我们可以得出几个结论:

  1. 外设并不是直接和CPU进行交互,而是先与内存进行交互,再与CPU进行交互,因为外设运行速度比较慢,CPU的运算速度是很快的,为了平衡二者之间的速度,会让CPU与介于二者运行速度之间的内存先进行交互
  2. 读入数据时,输入设备将数据写入到中介内存中,然后内存把数据写入到CPU中,让CPU进行数据的处理,处理完后,CPU将数据写回到内存中,最后由内存把数据写入到输出设备中
  3. 有了内存,CPU不需要和外设直接打交道
  4. 冯若依曼的原理是存储程序和程序控制

🌏操作系统

操作系统是什么?

概念: 管理计算机硬件与软件资源的计算机程序。英文名称是Operator System(简称OS)

为什么要有操作系统?
大体分为两点原因:

  1. 对上(用户、程序员):给用户提供稳定、高效和安全的运行环境,为程序员提供各种基本功能(OS不信任任何用户,不让用户或程序员之间与硬件进行交互)
  2. 对下:管理好各种软硬件资源

如何管理?

管理: 管理就是对被管理对象进行先描述,再组织这么两个步骤的操作。

  • 描述: 在C/C++中,对一个对象进行描述一般是把这个对象的所有属性放在一个结构体或类中来进行描述,这样一遍我们更好地组织它们
  • 组织: 用一些高效的数据结构来把这些对象组织起来,一般是链表、队列等一下高效的数据结构。

下面是操作系统对软硬件
在这里插入图片描述
从这张图我们可以看到几点内容:

  1. OS管理的硬件部分: 网卡、硬盘等
  2. OS管理的软件部分: 内存管理、驱动管理、进程管理和文件管理,还有驱动和系统调用接口(下面介绍)

系统调用和库函数的概念

  • 系统调用接口: OS不信任任何用户,会提供系统调用接口给用户提供服务,其中的细节我们不关心。(比如:我们平时写的printf函数要往显示屏上打印数据,这时候就要涉及硬件的访问,因为OS不信任任何用户,所以我们需要调用系统调用接口来完成,其中这个这个printf函数底层会帮我们调用需要用到的的系统调用接口来实现,帮程序员完成打印的操作)
  • 系统调用: 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口
  • 库函数: 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以有些开发者会对部分系统调用进行十度封装,这样就形成了库,方便上层用户使用

🌏进程

🌲概念

进程: 计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

根据上面对管理的介绍,我们知道,要想了解操作系统如何管理进程,我们应该先对进程进行描述
如何描述进程?

  • 进程的所有属性信息都被放在一个叫做进程控制块的结构体中,可以理解为进程属性的集合。
  • 这个数据结构的英文名称是PCB(process control block),在Linux的OS下的PCB是task_struct(Linux内核中的一种数据结构,它会被装载到RAM(内存)中并且包含并包含进程的信息)

简单的语言描述进程

进程 = 程序文件内容 + 由操作系统自动创建的相关数据结构

task_struct内容有哪些?

  • 标识符:描述本进程的唯一标识符(就像是我们每个人的身份证)
    在这里插入图片描述

  • 状态:任务状态、退出代码、退出信号等(可以用$?查看上一次执行命令的退出代码)
    在这里插入图片描述

  • 优先级: 程序被CPU执行的顺序(后面会单独介绍)

  • 程序计数器: 一个寄存器中存放了一个pc指针,这个指针永远指向即将被执行的下一条指令的地址

  • 内存指针: 包含程序代码和进程相关的数据的指针,还有和其它进程共享的内存快的指针。这样就可以PCB找到进程的实体

  • 上下文数据: 在单核CPU中,进程需要在运行队列(run_queue) 中排队,等待CPU调度,每个进程在CPU中执行时间是在一个时间片内的,时间片到了,就要从CPU上下来,继续去运行队列中排队
    在这里插入图片描述

  • I/O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表

  • 记账信息: 能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等

  • 其他信息

组织进程
在内核源代码中发现,所有运行在系统里的进程都以task_struct链表形式存在内核中。

🌲通过系统调用获取进程标识符

下面是两个系统调用的函数:

  • getpid: 获取进程id(PID)
  • getppid: 获取父进程id(PPID)

在这里插入图片描述
实例演示:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
  printf("pid:%d ppid:%d\n", getpid(), getppid());
  return 0;
}

代码运行结果如下:
在这里插入图片描述
注意: 普通进程的父进程基本都是bash

🌲通过系统调用创建子进程

了解fork函数
在这里插入图片描述

  • 功能: 通过复制当前进程,为当前进程创建一个子进程
  • 返回值: 有两个返回值,一个是父进程返回子进程的id,还有一个是子进程返回的0(fork失败返回值为-1)

先看下面一段代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
  pid_t ret = fork();
  printf("pid:%d, ppid%d\n", getpid(), getppid());
  sleep(1);

  return 0;
}

代码运行结果如下:
在这里插入图片描述
这里只用一条输出语句,但是却打印了两句话,这是为什么?

这里父进程创建子进程成功后,两个进程具有独立性,会分别执行fork后面的代码,完成打印,所以有两条语句。

分析下面几个问题:

  1. 如何理解进程的创建?
    创建进程,系统就会多一个进程,所以系统就要多一分管理进程的数据结构和该进程对应的代码和数据,父子进程代码共享,数据默认是一样的,但是当任一方试图写入数据,便以写时拷贝的方式各自一份副本,数据各自私有,具有独立性。
  2. 为什么fork有两个返回值?
    在fork函数体内,函数返回id前已经完成了子进程的创建,既然完成了子进程的创建,那么子进程就也会去到运行队列中,等待CPU调度,父子进程共享代码,数据各自开空间。由于返回值id是数据,所以虽然id的变量名相同,但是内存地址不同,所以返回的id是不同的。
  3. 父子进程执行的顺序是怎样的?
    这是不确定的,两个进程都在运行队列中等待CPU调度,由Linux下的调度器决定,所以这里是不确定的。
  4. 为什么父进程返回子进程pid,子进程返回0?
    因为父进程返回子进程pid,父进程可以直接找到子进程,子进程返回0代表创建进程成功

再看一个实例 父子进程实现分流,同时进入if和else的两个分支

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
  pid_t ret = fork();
  
  if (ret < 0)
  {
    perror("fork");
    return 1;
  }
  else if (ret == 0)// 子进程
  {
    printf("I am child-pid:%d, ppid:%d\n", getpid(), getppid());
    sleep(1);
  }
  else if (ret > 0)// 父进程
  {
    printf("I am parent-pid:%d, ppid:%d\n", getpid(), getppid());
    sleep(1);
  }

  sleep(1);

  return 0;
}

代码运行结果如下:
在这里插入图片描述

🌲进程状态和进程状态的查看

进程状态
下面是进程状态在源码中的定义

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运行状态: 这里并不是指进程一直在运行,而是指进程在运行队列中,可随时被CPU调度
    在这里插入图片描述

  • S睡眠状态: 进程处于等待队列中,在等待时间完成,这里的睡眠是可以中断的,也叫浅睡眠

  • D磁盘休眠状态: 这种休眠是不可以被中断的,这个时候的进程通常是在等待IO结束(此时在和磁盘进行IO,以免被OS误杀进程)

  • T停止状态: 可以通过发送SIGSTOP信号让进程停下
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  • X死亡状态: 这个状态只是一个返回状态,任务列表中是看不到的

注意: kill的1-31是普通信号,34-64属于实时信号

进程状态的查看
有下面两种命令(前者查看所用进程的名字,后者可以查看进程的父子关系):

ps aux/ps axj

这里再贴一张图:
在这里插入图片描述

🌲僵尸进程

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

特征:

  • 僵死的时候,task_struct是会被保留的,进程的退出信息是放在PCB中的
  • 父进程没有读取子进程的状态信息,子进程就会进入僵死状态
  • 父进程读取子进程状态码后,子进程会由Z状态变成X状态

实例演示:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>

int main()
{
  pid_t ret = fork();
  
  if (ret < 0)
  {
    perror("fork");
    return 1;
  }
  else if (ret == 0)// 子进程
  {
    printf("I am child-pid:%d, ppid:%d\n", getpid(), getppid());
    exit(0);// 子进程退出
  }
  else if (ret > 0)// 父进程
  {
    printf("I am parent-pid:%d, ppid:%d\n", getpid(), getppid());
    sleep(10);
  }

  sleep(1);

  return 0;
}

下面是检测进程状态的运行脚本:

while :; do ps axj | head -1 && ps axj | grep test | grep -v grep; sleep 1; echo "############"; done

运行结果如下:
在这里插入图片描述
在这里插入图片描述
危害:

  • 进程的退出状态必须被维持下去,父进程如果一直不读取,那子进程就一直处于Z状态
  • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护
  • 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费
  • 对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!

🌲孤儿进程

孤儿进程: 父进程先退出,子进程就称之为“孤儿进程”。孤儿进程会被1号systemed进程领养
如果进程没有父进程,且当前进程退出,那么当前进程进入僵死状态,该进程资源无法被回收造成内存泄漏,但是OS考虑了这个问题,孤儿进程是会被领养的
实例演示:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>

int main()
{
  pid_t ret = fork();
  
  if (ret < 0)
  {
    perror("fork");
    return 1;
  }
  else if (ret == 0)// 子进程
  {
    printf("I am child-pid:%d, ppid:%d\n", getpid(), getppid());
  }
  else if (ret > 0)// 父进程
  {
    printf("I am parent-pid:%d, ppid:%d\n", getpid(), getppid());
    sleep(10);
    exit(0);
  }

  sleep(1);

  return 0;
}

代码运行结果如下:
在这里插入图片描述
在这里插入图片描述

🌲进程优先级

概念:

  • cpu资源分配的先后顺序,就是指进程的优先权
  • 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能
  • 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能

查看系统进程
在这里插入图片描述
几个重要信息:

  • UID : 代表执行者的身份
  • PID : 代表这个进程的代号
  • PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
  • PRI :代表这个进程可被执行的优先级,其值越小越早被执行
  • NI :代表这个进程的nice值

PRI 和NI

  • PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
  • NI就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
  • PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为: PRI(new)=PRI(old)+nice
  • 当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
  • 调整进程优先级,在Linux下,就是调整进程nice值
  • nice其取值范围是-20至19,一共40个级别

修改进程优先级

进入top后按“r”–>输入进程PID–>输入nice值

几个概念:

  • 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
  • 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
  • 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
  • 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发

🌐总结

以上就是进程相关的一些基本介绍,这块的内容需要自己不断用代码验证和自己理解,这样更有助于后面的学习。喜欢的话,欢迎点赞支持和关注~
在这里插入图片描述

Logo

快速构建 Web 应用程序

更多推荐