• 【LInux】进程管理


    在这里插入图片描述

    👦个人主页:Weraphael
    ✍🏻作者简介:目前正在学习c++和算法
    ✈️专栏:Linux
    🐋 希望大家多多支持,咱一起进步!😁
    如果文章有啥瑕疵,希望大佬指点一二
    如果文章对你有帮助的话
    欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍


    一、进程概念

    冯诺依曼体系结构可知,程序一旦运行起来,一定会先加载到内存,然后再通过CPU对其进行逐行的语句执行。而严格意义上,当一个程序被加载到内存中(正在运行的程序),它会被操作系统视为一个进程(或者称做任务)。

    二、进程管理 — PCB

    当你开机的时候启动的第一个程序就是操作系统(即操作系统是第一个加载到内存的)。常识告诉我们,系统可以同时运行多个进程,而操作系统是做管理工作的,而其中就包括了进程管理。那么操作系统是如何对进程进行管理的呢?

    • 这时就应该想到操作系统管理的六字真言:先描述,再组织

    当一个进程出现时,操作系统会立即对其进行描述,并将这些描述信息组织起来以便管理。这种描述信息通常被放置在一个称为进程控制块PCB(process control block)的数据结构中(可以理解为进程属性的集合),并将这些PCB对象以双链表的形式组织起来。因此,操作系统对各个进程的管理就变成了对这条双链表的增、删、查、改等操作。

    注意:每个操作系统都有其独特的方式来表示进程控制块PCBLinux下,PCB被表示为task_struct结构体task_struct结构体包含了Linux内核对进程的所有必要信息,比如:

    struct task_struct
    {
        进程标识符PID: 描述本进程的唯一标识符,用来区别其他进程。
        进程状态: 任务状态,退出代码,退出信号等。
        优先级: 相对于其他进程的优先级。
        程序计数器: 程序中即将被执行的下一条指令的地址。
        内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
        上下文数据: 进程执行时处理器的寄存器中的数据
        I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
        记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
        ...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    当一个进程被加载到内存中时,操作系统需要根据该进程的属性和需求,为其创建对应的PCB对象,除此之外,操作系统还需要将该进程的代码和数据加载到内存中。如果满足这两个要求(PCB对象和程序的代码数据相结合),就形成了一个完整的进程。

    在这里插入图片描述

    然而,操作系统在进行进程管理时,通常更关注的是进程的状态、资源分配和调度,以及对进程的控制(就好比学校系统,校长管理者和被管理者学生不需要见面,只需拿到学生信息就能达到某种意义上的管理)。因此,操作系统对进程的管理其实就是对进程的PCB对象进行管理。

    三、在Linux中查看进程

    3.1 ps指令

    ps axj
    
    • 1

    测试代码如下:

    在这里插入图片描述

    当我们将以上程序运行起来,此时这个程序就变成了一个进程。而由于Linux下正在执行的程序会有点多,因此我们可以使用grep来过滤出我们想要看的进程信息

    # head -1 -> 显示进程的属性字段
    # && -> 一行执行两个指令
    ps axj | head -1 && ps axj | grep '可执行程序'
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    如上图所示,明明要的是mytest进程,为什么会出现grep相关的进程呢?

    我们自己写的代码,编译成为可执行程序,启动之后就是一个进程;同样的,基本指令本质上就是可执行文件, 这些指令在执行后也会成为进程,这也就是为什么上面会把grep显示出来。

    当然也可以过滤掉grep命令的进程信息

    ps axj | head -1 && ps axj | grep '可执行程序'' | grep -v grep
    
    • 1

    3.2 通过proc查看

    进程的信息也可以通过/proc系统文件夹查看

    ls /proc
    
    • 1

    在这里插入图片描述

    每一个进程都会存在一个唯一的标识符,就如同每个学生都有学号一样,而这个唯一标识符在Linux称为pidprocess id),而上面蓝色的目录即为进程的pid。每当执行一个程序,/proc目录就会出现进程的pid

    在这里插入图片描述

    而当我们杀死进程,再运行同样的进程时,对应的pid是会改变的。

    PID是由操作系统动态分配的,系统会确保每个进程的PID在系统中是唯一的。当一个进程启动时,操作系统会查找当前系统中未被使用的PID,并将其分配给新的进程。

    # 杀死进程
    kill -9 PID
    
    • 1
    • 2

    在这里插入图片描述

    四、通过系统调用获取进程标识符pid

    想直接获取进程的信息,如进程标识符pid是不行的,因为进程控制块PCB是由操作系统管理的。而操作系统不相信用户,一般情况下不会直接将进程的信息提供给用户。相反,操作系统提供了系统调用接口来让用户程序与操作系统交互,从而获取进程信息。

    getpid() # 获取进程pid
    
    • 1

    我们可以通过man指令来查看getpid()的详细描述

    # 2号手册是专门查看系统调用
    man 2 getpid
    
    • 1
    • 2

    在这里插入图片描述

    • 是一个系统级别的头文件,它包含了许多基本的系统数据类型的定义。如果使用 pid_tuid_tgid_t 等,那么就要使用此头文件

    • 中,则通常会包含一些系统调用的声明,例如 getpid()。因此,在使用 getpid() 函数时,需要包含 来获取函数的声明。

    我们可以来测试一下,代码如下:

    在这里插入图片描述

    验证如下

    在这里插入图片描述

    五、父进程

    在这里插入图片描述

    从上图可以观察发现:每当运行一个程序时,对应的进程都会有一个父进程标识符ppidParent Process ID),并且每次终止进程,对应的pid就会发生改变,而ppid却始终没发生变化。

    既然ppid始终不变,那么我们可以查查父进程到底是什么?

    ps axj | head -1 && ps axj | grep 29062
    
    • 1

    在这里插入图片描述

    图中可以看出父进程29062就是一个bash。因此,几乎我们在命令行上所执行的所有的指令,都是命令行bash进程的子进程!

    除此之外,我们同样可以通过系统调用接口来获取一个进程的父进程的标识符ppid

    getppid()
    
    • 1

    在这里插入图片描述

    验证结果如下:

    在这里插入图片描述

    六、通过系统调用创建进程

    6.1 fork函数

    fork()是一个系统调用,用于创建子进程。

    先来看看简单的代码样例:

    在这里插入图片描述

    运行结果如下:

    在这里插入图片描述

    为什么在fork之后的代码被执行了2次?这其实和它的返回值有关!

    • fork()创建成功,将子进程的pid作为父进程的fork函数的返回值,将0作为子进程的fork函数的返回值,
    • fork()创建失败,-1返回给父进程。不创建子进程

    下面是一个简单的C语言示例,演示了如何检查fork()的返回值:

    在这里插入图片描述

    运行结果如下:

    在这里插入图片描述

    以上执行过程可以抽象成以下图:

    在这里插入图片描述

    简单来说:当程序执行到fork()函数时,操作系统会创建一个新的PCB对象,即子进程,该子进程与父进程几乎完全相同。然后父进程和子进程都会从fork()函数的调用处开始继续执行程序(代码共享),父子进程可以根据返回值的不同来执行不同的逻辑,以满足不同的需求。

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

    首先返回不同的返回值是为了让子进程和父进程可以执行不同的代码(干不同的事);其次

    • 父进程返回子进程的PID:父进程通常需要知道它创建的子进程的PID,以便管理和与子进程进行通信。所以,将子进程的PID作为fork()函数的返回值,使得父进程可以轻松地获取到子进程的标识符PID

    • 子进程返回0:在子进程中,通常需要知道它是子进程就行。因此将0作为fork()函数在子进程中的返回值,可以方便地用于条件判断。

    6.3 fork函数做了什么事情?

    进程 = PCB对象 + 数据和代码

    1. 为子进程创建新的进程控制块PCB

    2. 复制父进程的地址空间:fork()调用成功后,操作系统将父进程的地址空间复制到新进程中,包括代码段、数据段、堆栈等。这样新进程就拥有了父进程的所有数据和程序,成为了父进程的副本。

    3. 设置子进程的状态: 设置子进程的状态为就绪状态,使其可以被调度执行。

    4. 返回值处理

    6.4 fork函数是如何做到返回2次的

    在这里插入图片描述

    fork()在内部创建完子进程后,就已经开始父子进程代码共享,会使得fork()被父进程执行一次,同时被子进程执行一次。这样就实现了fork()函数在逻辑上返回两次的效果。

    6.5 pid变量是如何做到可以接收两个返回值

    子进程共享父进程的代码和数据,而进程之间具有独立性,如果子进程修改了父进程的数据,那么势必会影响父进程,这也就是接下来我要回答:为什么变量pid可以接收两个fork()的返回值?这其实是因为系统对其做了写时拷贝

    • 在调用 fork() 函数创建子进程时,并不立即复制整个父进程的代码和数据,而是共享!这是因为如果复制整个数据段代码,而子进程在执行时不一定会对数据段的代码进行修改,那么就会导致资源浪费。

    • 如果在子进程执行期间需要修改共享的数据段,就会触发写时拷贝机制。这时,操作系统会为子进程分配一块新的内存空间,复制需要修改的数据,并且在新的内存空间上进行修改,而不会影响到父进程的数据。这样就确保了父子进程之间对内存的修改不会相互干扰,同时又避免了不必要的内存复制开销。

  • 相关阅读:
    3. 双向约瑟夫问题
    使用IDEA自带功能将WSDL转java
    07-网络篇-抓包分析TCP
    如何让JOIN跑得更快
    Java练习任务【15】
    【模型压缩】模型剪枝模块
    OpenGL:开放图形库
    深度解读:金融企业容器云平台存储如何选型
    数据采集实战:如何自动化运营微博?
    FileChannel 文件流的简单使用
  • 原文地址:https://blog.csdn.net/Weraphael/article/details/138030914