欢迎光临
我们一直在努力

【Linux】进程概念(一):基本概念与基本操作

摘要

本文深入探讨了 进程 的基本概念与操作系统如何管理进程,重点介绍了 进程控制块(PCB) 和 task_struct 数据结构。通过分析程序如何加载成进程,阐述了进程的动态性,以及操作系统如何通过 PCB 组织和管理大量进程的信息。此外,结合实际命令如 ps 和 /proc,讲解了如何查看进程状态,并进一步介绍了 PID 和 PPID 的概念。最后,通过详细的 fork() 示例,探讨了父进程与子进程的关系以及它们如何通过不同的返回值执行各自的任务。 在这里插入图片描述


  • 摘要
  • 目录
    • 基本概念
    • 一、描述进程—PCB
    • 二、task_struct
      • 1. 内容分类(进程信息)
      • 2. 组织进程
    • 三、查看进程
      • 1. 通过系统目录查看
      • 2. 通过ps命令来查看
    • 四、通过系统调用获取进程的PID和PPID
    • 五、通过系统调用创建进程——fork初识
      • 1. 概念
      • 2. fork有两个返回值
      • 3. 为什么 fork() 给父进程返回子进程的 PID,给子进程返回 0?
      • 4. fork的操作
  • 总结

目录

基本概念

我们都知道,在编写完一段代码后,点击执行时,代码会被编译并链接成一个可执行文件。这个可执行文件其实是存储在磁盘上的。当我们双击它时,操作系统会将其加载到内存中,因为只有加载到内存中,CPU 才能逐条指令地执行(这也是冯诺依曼体系中描述的执行原理)。不过,值得注意的是,一旦可执行程序加载到内存中,它就不再是简单的“程序”了,严格来说,它已经成为一个“进程”——这是程序在内存中的动态表现。

在这里插入图片描述


一、描述进程—PCB

我们已经了解了进程的概念,那什么是 PCB(进程控制块)呢?

系统当中可以同时存在大量进程,使用命令ps ajx便可以显示系统当中存在的进程。 在这里插入图片描述 当我们开机的时候启动的第一个程序就是操作系统(意思就是操作系统是第一个加载到内存的),我们都知道操作系统是做管理工作的,而其中就一个管理就是进程管理。而系统内是存在大量进程的,那么操作系统是如何对进程进行管理的呢?

这时我们就应该想到管理的"精髓":先描述,再组织。操作系统管理进程也是一样的,操作系统作为管理者是不需要直接和被管理者(进程)直接进行沟通的(我们说过会有一个执行者),当一个进程出现时,操作系统就立马对其进行描述,之后对该进程的管理实际上就是对其描述信息的管理。 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,课本上称之为PCB(process control block)。

PCB(Process Control Block) 是操作系统用来描述一个进程信息的数据结构。它保存了进程的各种状态、控制信息以及调度所需的所有数据。简单来说,进程 是由内核中 PCB 数据结构 和我们编写的 代码 构成的。

  • 进程控制块 用来描述进程的所有相关属性信息。它包含了一个进程在执行过程中所需要的全部数据,如进程的状态、CPU寄存器、内存管理信息等。
  • 程序 本质上是一个二进制文件,包含了我们编写的代码和数据。这个二进制文件会被操作系统加载到内存中,并在执行时形成一个 进程。程序和进程是两个不同的概念,程序是静态的,存储在磁盘上;而进程是动态的,是程序在内存中的执行实例。

二、task_struct

task_struct 是用来描述一个 进程 的重要数据结构。每个进程都有一个对应的 task_struct,它保存了该进程的各种信息和状态。

  • 上面我们提到操作系统对进程的管理其实是对进程的描述信息的管理,它的信息包含了许多,对其管理的增删查改其实最适用的task_struct就是用双向链表组织起来。
  • 这样一来,操作系统只要拿到这个双链表的头指针,便可以访问到所有的PCB。此后,操作系统对各个进程的管理就变成了对这条双链表的一系列操作。

1. 内容分类(进程信息)

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

2. 组织进程

在这里插入图片描述

三、查看进程

1. 通过系统目录查看

ls /proc 在这里插入图片描述

/proc系统目录中,储存着所有正在运行进程的目录文件以及相关的进程信息,并且进程的目录文件名是以进程对应的进程编号(PID)为名称。

ls /proc/编号查看进程信息 在这里插入图片描述

  • exe 是进程的可执行文件的路径链接,指向二进制文件的绝对位置。进程是由二进制文件加载到内存中运行而生成的实例,而 exe 则对应该二进制文件在磁盘上的位置。你可以通过 exe 了解进程对应的可执行文件的路径。
  • cwd 是进程的当前工作目录,进程的所有操作(如文件创建或读取)默认都在该目录下进行。进程在运行过程中生成的临时文件通常会以 cwd 作为基础路径,从而在该目录下创建文件。

  • 2. 通过ps命令来查看

    在这里插入图片描述 ps命令与grep命令搭配使用,即可只显示某一进程的信息。 ps ajx | head -1 && ps ajx | grep 文件名 在这里插入图片描述


    四、通过系统调用获取进程的PID和PPID

    什么是PID?什么是PPID?

    PID 是进程标识符(Process Identifier)的缩写,是操作系统分配给每个正在运行的进程的唯一编号。每个进程都有一个唯一的 PID,它帮助操作系统和用户区分不同的进程。例如,当你查看正在运行的进程时,PID 是识别进程的关键标识符。你可以通过 ps 命令查看当前进程的 PID。

    • 当你运行一个程序(如 ./test),操作系统会为这个进程分配一个 PID,如 1234。
    • 使用 ps aux 命令时,你会看到类似于 1234 这样的 PID 列在进程信息中。

    PPID 是父进程标识符(Parent Process Identifier)的缩写,它是指创建当前进程的父进程的 PID。每个进程都有一个父进程,除了系统启动时的初始化进程(通常是 init 或 systemd)。PPID 表示的是哪个进程启动了当前的进程。通过查看 PPID,我们可以了解进程之间的父子关系。

    • 如果一个进程 1234 是由进程 5678 启动的,那么进程 1234 的 PPID 就是 5678。
    • 你可以使用 ps -eo pid,ppid,cmd 来查看每个进程的 PID 和 PPID。

    在这里插入图片描述 在这里插入图片描述 我们可以通过ps命令查看该进程的信息,即可发现通过ps命令得到的进程的PID和PPID与使用系统调用函数getpid和getppid所获取的值相同。 在这里插入图片描述


    五、通过系统调用创建进程——fork初识

    1. 概念

    在这里插入图片描述

    这里我们可以看到他是用来创建子进程的(当前进程的子进程);包含在头文件unistd.h中;返回类型pid_t,它定义的遍历是进程号类型;fork创建成功会给子进程返回0,然后给父进程返回PID;创建失败则会返回一个<1的数给父进程。

    2. fork有两个返回值

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

    int main()
    {
    printf("begin\\n");
    fork();
    printf("end\\n");

    return 0;
    }

    在这里插入图片描述

  • printf("begin\\n"); 这行会首先输出 begin。无论是父进程还是子进程,它们都会执行到这一行,因此你会看到 begin 输出一次。

  • fork(); fork() 调用会创建一个新的子进程,父进程和子进程都会从 fork() 调用之后的地方开始执行。 在 父进程 中,fork() 会返回子进程的 PID。 在 子进程 中,fork() 会返回 0。 这意味着,fork() 被父进程和子进程都执行一次,导致 fork() 之后的代码会在两个进程中各自执行一次。

  • printf("end\\n"); 这行代码会在父进程和子进程中各自执行一次。因此,end 会输出两次:一次来自父进程,另一次来自子进程。

  • 1#include<stdio.h>
    2 #include<unistd.h>
    3 #include<sys/types.h>
    4
    5 int main()
    6 {
    7 printf("我是一个进程,PID:%d,PPID:%d\\n",getpid(),getppid());
    8 sleep(1);
    9 pid_t id = fork();
    10 if(id > 0)//执行父进程
    11 {
    12 while(1)
    13 {
    14 printf("父进程,PID:%d,PPID:%d\\n",getpid(),getppid());
    15 sleep(1);
    16 }
    17 }
    18 else if(id == 0)//执行子进程
    19 {
    20 while(1)
    21 {
    22 printf("子进程,PID:%d,PPID:%d\\n",getpid(),getppid());
    23 sleep(1);
    24 }
    25 }
    26 else{
    27 printf("创建失败!\\n");
    28 }
    29
    30 return 0;
    31 }

    在这里插入图片描述 这段话的核心思想是关于 fork() 系统调用为什么要给父进程返回子进程的 PID(进程 ID),而给子进程返回 0,以及如何通过这种设计区分父进程和子进程的执行流。让我们通过简单清晰的思维来总结:

    3. 为什么 fork() 给父进程返回子进程的 PID,给子进程返回 0?

    fork() 会在父进程和子进程中都执行一次,父进程需要知道自己创建了哪个子进程,以便后续管理和控制。因此,父进程需要获取子进程的 PID,即 fork() 返回子进程的 PID。如果父进程不对子进程进行区分,那么就没有办法找到特点的子进程了。

    子进程则不需要关心父进程的 PID,因为子进程只对应一个父进程,在描述信息的时候它的task_struct就已经对应初始化放置了父进程的PPID,并且它不需要对谁进行管理,所以给子进程返回0。

    在操作系统中,父进程创建子进程是为了让子进程去执行不同代码特定的任务。因为如果像第一段代码,父进程和子进程执行相同的代码块,就没啥意义了。如果父进程创建了多个子进程,父进程需要知道这些子进程,以便进行管理、等待或者控制。每个进程都有唯一的 PID,父进程通过 fork() 返回的 PID 就能知道哪个子进程对应哪个任务,方便后续对特定子进程进行管理。

    创建子进程的一个关键目的,是让子进程去执行与父进程不同的任务。父进程和子进程虽然代码是共享的,但它们可以根据 fork() 返回值的不同来决定执行不同的代码块。 通过 if (pid == 0) 判断子进程,if (pid > 0) 判断父进程,父子进程可以分别执行不同的代码,这就是 fork() 设计的核心之一。

    4. fork的操作

  • 我们使用fork函数创建子进程后,系统就多了一个子进程,它最初没有自己的PCB,所以它就复制父进程的PCB进行适量修改PPID、PID以及其它属性时候就变成了子进程自己的PCB,即子进程的进程控制块task_struct。
  • 我们知道程序文件是有对应的代码和数据的,程序文件运行之后对应的进程就是当前的父进程,所以父进程是有自己的代码和数据的,子进程什么都没有连进程控制块PCB中的大部分内容都是复制的父进程的,一个进程是由内核数据结构PCB加代码和数据构成,此时子进程已经有PCB了,那么它既然想要成为一个进程却又没有代码和数据,那么它只能从父进程的代码和数据想办法,其中对于代码,代码是不能修改的,那么父进程自然可以允许子进程和自己共用代码,即父进程和子进程指向同一块代码
  • 我们知道程序有自己的代码和数据。但是一个进程是由内核数据结构PCB和代码数据构成,我们的子进程初始是和父进程共享同一块数据内存空间的,所以它为了变成一个进程就只能从父进程那里共用代码,但是数据(如变量和内存中的动态数据)是不能共享的,因为如果他们一方修改共享数据会相互影响,从而不符合进程独立性的要求。
  • 在Linux中为了解决这个问题,采用了写时拷贝(Copy-On-Write)。他就是让父进程和子进程在最初的时候指向同一块数据空间,如果他们都不对这块空间进行修改的化,他们就会共享这块内存空间,同时也节省了资源;但是如果有一方需要对数据进行修改,操作系统将会为需要修改数据的进程创建一份独立的副本,这个副本就是“写时拷贝”。然后原来的空间就属于未进行修改数据的进程了。修改数据的进程就在自己的副本上修改,通过这种方式,父进程和子进程都能各自独立的操作数据,互不干扰,确保进程的独立性。
  • 如何让父子进程执行不同的代码? 当 fork() 被调用时,它会 返回不同的值给父进程和子进程:父进程:接收到子进程的 PID(子进程的进程ID),返回值大于 0。子进程:接收到 0,表示它是一个新创建的进程。这两者的不同返回值就是用来区分父进程和子进程的执行流。

    根据返回值分支执行代码:父进程 根据返回的子进程的 PID(大于0)来执行特定的代码块。子进程 根据返回的 0 来执行另一个代码块。通过这种方式,父子进程会 共享代码,但 根据 fork() 返回值的不同,父子进程会执行不同的代码块,确保它们各自完成不同的任务。


    总结

    通过本文的学习,我们深入理解了操作系统如何通过 PCB 和 task_struct 进行进程管理,掌握了查看和操作进程信息的技巧。了解了如何通过 PID 和 PPID 反映进程的父子关系,以及如何利用 fork() 创建并管理父子进程,确保它们独立运行并执行不同任务。这些知识为深入理解操作系统的进程管理机制提供了基础,有助于我们更高效地调试和优化程序。


    ✨ 坚持用 清晰易懂的图解 + 代码语言, 让每个知识点都 简单直观 ! 🚀 个人主页 :不呆头 · CSDN 🌱 代码仓库 :不呆头 · Gitee 📌 专栏系列

    • 📖 《C语言》
    • 🧩 《数据结构》
    • 💡 《C++》
    • 🐧 《Linux》

    💬 座右铭 : “不患无位,患所以立。”

    赞(0)
    未经允许不得转载:171主机测评 » 【Linux】进程概念(一):基本概念与基本操作
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址