系列文章目录

第三章 进程概念



前言

  在上一篇文章中,我们了解了计算机的硬件体系、操作系统的作用以及程序的加载过程。我们知道了操作系统是“搞管理”的软件,管理的本质是“先描述,再组织”。本节我们将在此基础上,深入探讨操作系统的核心管理对象之一,进程

  进程是操作系统中最基本、最重要的概念之一。无论是我们日常使用的浏览器、编辑器,还是服务器上运行的各种服务,本质上都是一个或多个进程在背后默默工作。理解进程,是理解操作系统如何管理计算机资源、如何实现多任务并发、如何保证系统稳定运行的关键一步。接下来,我们将从进程的定义出发,逐步揭开它的神秘面纱。


一、进程的基本概念

1.1 什么是进程?

  我们平时在电脑上双击一个程序图标,或者在终端输入一个命令,这个程序就开始运行了。这个“运行中的程序”,在操作系统中就被称为进程

  举个例子:当你双击打开一个文本编辑器时,操作系统就会为这个编辑器创建一个进程。如果你同时打开两个文本编辑器窗口,那么系统中就会存在两个独立的进程,它们各自拥有自己的内存空间、运行状态和资源,互不干扰。即使你关闭了其中一个窗口,另一个窗口依然可以正常工作,这正是因为它们是两个相互独立的进程。
  从操作系统的角度来看,进程是程序的一次执行过程,是系统进行资源分配和调度的基本单位。换句话说,程序是存放在磁盘上的静态文件,而进程是程序在内存中动态执行的过程。

  这里需要特别强调的是“动态”二字。程序本身只是一堆指令和数据的集合,它静静地躺在硬盘上,不占用任何 CPU 时间,也不消耗内存资源。只有当用户启动它、操作系统把它加载到内存并开始执行时,它才“活”起来,成为一个进程。进程从创建到消亡,经历了一系列状态变化,如就绪、运行、阻塞等,这些状态转换正是操作系统调度和管理的核心内容。

1.2 进程与程序的区别

对比项程序进程
存在形式静态文件(存储在磁盘)动态执行(运行在内存)
生命周期长期存在有创建、运行、消亡的过程
资源占用不占用CPU和内存占用CPU、内存等系统资源
数量关系一个程序可以对应多个进程每个进程对应一个程序实例

  从上表可以看出,程序与进程有着本质的区别。程序是“死”的,它只是存储在磁盘上的一堆指令和数据;而进程是“活”的,它是程序在内存中动态执行的过程,拥有自己的生命周期和资源占用。

  一个典型的例子是:同一个浏览器程序,你可以同时打开多个窗口,每个窗口都是一个独立的进程。它们共享同一个程序代码(即磁盘上的浏览器可执行文件),但各自拥有独立的内存空间和运行状态。这就是“一个程序可以对应多个进程”的直观体现。

1.3 进程的本质

  进程即程序的调用是怎么样的呢? 为了保证进程的有序执行,os必须对这些进程做管理,那具体形式是什么? 结构体完美的承担了这个角色。

  在操作系统中,每个进程都需要被 “描述”和“记录”。操作系统需要知道:这个进程叫什么名字?它占用了多少内存?它当前处于什么状态?它运行到哪一条指令了?这些信息如果散落在各处,操作系统将无法统一管理。
  因此,操作系统为每个进程都维护了一个专门的数据结构,用来集中存放该进程的所有信息,这个数据结构就是进程控制块(PCB)

  在上一篇文章中我们提到,操作系统管理的本质是 “先描述,再组织”。那么对于进程而言:

  • 描述:操作系统用一个数据结构来记录进程的所有信息,这个结构就是PCB(Process Control Block,进程控制块)。

  • 组织:操作系统将所有的PCB通过一定的数据结构组织起来,以便高效地查找、调度和管理。

  因此,我们可以说:进程 = 程序代码 + 数据 + PCB。

1.4 进程的加载

  进程的本质是:进程 = 程序代码 + 数据 + PCB。

程序平时存放在硬盘中,运行时需要加载到内存。整个加载过程分为四步:

  • 第一步:在硬盘中找到可执行文件:
      操作系统根据文件名或路径,找到硬盘上的可执行文件(如 .exe)。它包含程序代码、数据、文件头等信息,此时还只是一个静态文件。

  • 第二步:加载到内存:
      操作系统读取文件,解析格式,为程序分配内存空间(代码段、数据段、堆、栈等),然后把程序代码和数据从硬盘复制到内存中。

  • 第三步:创建 PCB(进程控制块):
      操作系统为该进程创建一个 PCB,记录进程 ID、状态、程序计数器、寄存器、优先级等信息。创建 PCB 后,进程被放入就绪队列,等待 CPU 调度。

  • 第四步:CPU 运行进程:
      CPU 调度器从就绪队列中选择该进程,加载 PCB 中的寄存器等信息,从内存中取指令执行。进程开始运行,执行过程中不断切换(保存/恢复 PCB)。

在这里插入图片描述


二、PCB——进程控制块

2.1 PCB的作用

  PCB是操作系统感知进程存在的唯一标志。在Linux系统中,PCB的具体实现是 task_struct 结构体。这个结构体中包含了操作系统管理进程所需的全部信息。

2.2 task_struct的主要内容

  PCB作为结构体,它的属性是管理着这个进程的运行,我们需要认识的有:

  • 标识符:每个进程都有唯一的PID(进程ID),用于区分不同的进程。此外还有PPID(父进程ID)等。
  • 状态:进程当前处于什么状态(运行、就绪、阻塞等)。
  • 优先级:进程被调度执行的优先级别。
  • 程序计数器:记录下一条要执行的指令地址。
  • 内存指针:指向进程的代码、数据、堆栈等在内存中的位置。
  • 上下文数据:进程切换时需要保存的CPU寄存器状态。
  • I/O状态信息:进程打开的文件列表、使用的I/O设备等。
  • 记账信息:进程使用的CPU时间、内存大小等统计信息。

2.3 进程的组织方式

  在Linux内核中,所有进程的task_struct通过多种方式组织:

  1. 链表: 所有进程通过一个双向链表连接在一起,方便遍历所有进程。

  2. 树形结构: 通过父子关系组织,形成一个进程树,方便管理进程的创建和终止关系。

  3. 哈希表: 通过PID快速定位到对应的进程。

这种“描述+组织”的方式,正是操作系统管理进程的核心机制。操作系统通过对PCB的增删查改,就实现了对进程的管理。
在这里插入图片描述


三、进程的查看

3.1 ps命令

  ps命令用于查看当前系统中的进程状态。常用的选项有:

bash
ps axj        # 查看系统中所有进程的详细信息
ps -ef        # 以完整格式显示所有进程
ps -l         # 显示当前用户的进程

  当然这样直接的使用不方便我们的查看,可以结合之前学过的“管道” 和 “grep过滤”知识:

  • 基础过滤:使用 grep '文件名' 筛选特定进程。
  • 去除干扰:加上 grep -v grep 过滤掉过滤指令本身的进程。
  • 保留标题栏:使用 head -1 显示 ps 输出的首行属性说明。
  • 实时监控脚本:
    使用 while 循环构建一个简单的自动化脚本:
while :; do ps axj | head -1 && ps axj | grep 'myprocess' | grep -v grep; sleep 1; done

3.2 top命令

  top命令可以动态地查看系统中进程的运行状态,包括CPU占用率、内存占用率等信息,类似于Windows的任务管理器。

top

3.3 /proc文件系统

  Linux提供了一个特殊的文件系统/proc,它不是一个真实的磁盘文件系统,而是内核数据结构的映射。每个进程在 /proc 下都有一个以PID命名的目录,里面包含了该进程的各种信息。

# 查看PID为1的进程信息
ls /proc/1/

在这里插入图片描述
  用这样的指令,查看进程的详细信息会发现两个特殊的文件格式:在这里插入图片描述

cwd -> /home/sxr/code/Socket/UDP
exe -> /home/sxr/code/Socket/UDP/udpserver
  • cwd:
      这表示这个进程运行时的工作目录是/home/sxr/code/Socket/UDP,这个信息记录在进程的 PCB 中,是操作系统管理进程的重要上下文之一。
  • exe:
      这表示这个进程是从 /home/sxr/code/Socket/UDP/udpserver 这个文件加载到内存后运行起来的。在进程加载过程中,操作系统会在硬盘上找到这个可执行文件,读取它的代码和数据,并创建 PCB。exe 这个符号链接就指向了那个原始的可执行文件

四、进程的创建——fork函数

4.1 fork函数的基本使用

  在Linux中,创建新进程的主要方式是调用fork()系统调用。fork()函数会创建一个新的进程,称为子进程,原来的进程称为父进程。

头文件:#include <unistd.h>

  fork的作用就像是 “克隆”,复制出与该进程一模一样的子进程,有着这样的特点:

  • 和你长得一样(代码一样)

  • 手里拿的东西一样(数据一样)

  • 站在同样的位置(当前工作目录一样)

  但你们是两个独立的个体,之后可以各干各的活。

  给出代码进行讲解:

#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    
    if (pid == 0) {
        // 子进程执行
        printf("我是子进程,PID = %d,子进程PID = %d\n", getpid(),getppid());
    } else if (pid > 0) {
        // 父进程执行
        printf("我是父进程,PID = %d,子进程PID = %d\n", getpid(), pid);
    } else {
        // fork失败
        perror("fork");
    }
    
    return 0;
}

在这里插入图片描述
  我们可知 进程 = PCB + 代码数据,作为子进程那么它必然具有属于自己的pcb和代码数据,而该进程是由父进程修改后自己的两者后拷贝赋予的。最终,子进程也就会执行父进程之后的代码,相当于有两个进程同时执行代码。

对于这样的代码,无异于是超出了我们C/C++ 使用的常规语法的,我们会有着这样的疑问:

  • pid 为什么存在不同的值
  • 函数为什么会存在两个返回值
  • if语句中执行两者不同的执行逻辑

4.1.1 父子进程的变量存储

  我们可以清楚的看到:pid这个变量同时表达了两个值,大于0和等于0。这个现象的产生需要我们了解fork这个调用的底层逻辑: 写实拷贝

  1. 创建与共享: 父进程调用fork函数后,os不复制物理内存, 它将给子进程创建一个新的PCB,并将父进程和子进程的页表同时指向父进程的内存空间 (页表即虚拟地址与物理内存的映射表)
  2. 操作: 在进行只读操作期间,共享状态并不会改变,而一旦发生修改操作,就会立刻发生保护异常
  3. 拷贝: 当父子进程某一方要执行修改操作时,os就会给一方创建一个新的物理内存,并将原有的代码数据等拷贝,同时修改页表的映射关系,实现两者代码修改的独立。

  因此,父进程的pid和子进程的pid虽然名字相同,但本质是两块不同的物理内存资源,因此有不同的值。

4.1.2 函数存在多个返回值

  要明白这个问题,我们首先要清楚一件事:函数要实现的目的是return语句执行前就已经完成了。而通过上面写实拷贝过程中的事情,我们可知:在fork函数返回前就已经创建好子进程了。
  那就意味着如果要赋值这类修改操作发生,就会有不同的物理内存存储这个值,因此存在多个返回值。

4.1.2 else 和 if else 为什么可以同时执行?

  我们要明确一个概念:代码是共享的,但执行流是独立的。fork形成的父子进程具有同样的执行代码,只是两者存储的修改后值不同,因此同时执行是我们肉眼看到的,本质是:有两个执行相同代码的进程同时进入到if判断中并作出相应的回应。


  本节的进程有关的知识就到这里了,下一节我们将学习关于进程的更多执行过程中的状态变化。

总结

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐