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

Linux:进程的认识

前言:对于操作系统,先建立一种思想,凡是遇到一个新事物前要先描述,再组织,有了这样的思想也可以用于学习进程的概念。

进程的定义和基本概念

进程的感性理解

        Process:在 Linux 中,每个执行的程序都称为一个进程。每一个进程都分配一个 ID 号(PID,进程号)。与 Windows 下的任务管理器中的进程意思相同。

        在官方的定义中,进程通常被解释为“运行起来的程序”“在内存中的程序”。这种定义虽然简单,但严格来说并不容易理解,因为进程和程序显然是不同的概念。程序是静态的,它是一组指令和数据的集合,存储在磁盘中;而进程是动态的,它是程序在内存中的执行实例,拥有独立的内存空间和系统资源。仅仅通过“运行起来的程序”这样的定义,我们无法真正理解进程的本质。因此,我们需要从其他角度来深入理解进程。

        首先,我们需要思考一个问题:当程序运行时,它是从哪里开始的?程序的运行起点取决于它的存储位置。程序本质上是一个文件,而文件通常存储在磁盘中。因此,程序要运行起来,就必须从磁盘加载到内存中。这个过程被称为“加载”。加载完成后,程序就进入了内存,但此时内存中只是一个个独立的程序,如何对它们进行有效的管理呢?这就引出了另一个关键概念——操作系统。

        操作系统是一个纯粹的管理类软件,同时也是计算机启动时第一个运行的软件。无论何时将磁盘中的程序加载到内存中,内存中早已有一个软件在运行,这个软件就是操作系统。因此,操作系统的一个重要功能就是管理这些从磁盘加载到内存中的程序。当这些程序进入内存后,它们就不再是静态的文件,而是变成了动态的进程。那么,操作系统如何管理这些进程呢?这就需要遵循“先描述,再组织”的原则。

        具体来说,操作系统会为每个进程创建一个数据结构,称为进程控制块(PCB),用来描述进程的各种属性和状态。PCB中包含了进程的标识符、状态、优先级、程序计数器、寄存器内容、内存分配信息、打开的文件列表等。通过PCB,操作系统可以全面掌握每个进程的状态和资源使用情况。这就是“描述”的过程。

        在“描述”的基础上,操作系统会将这些进程的PCB组织起来,形成一个进程表或进程队列。通过这种方式,操作系统可以高效地进行进程调度、资源分配和状态转换等操作。这就是“组织”的过程。

        通过这种方式,操作系统能够有效地管理内存中的多个进程,确保它们能够高效、安全地并发执行。这种管理机制不仅提高了系统的利用率,还增强了系统的响应速度和稳定性。

总结来说,进程不仅仅是“运行起来的程序”或“在内存中的程序”,它是程序在内存中的动态执行实例,拥有独立的内存空间和系统资源。操作系统通过“先描述,再组织”的方式,对进程进行全面的管理,从而确保系统的正常运行和高效性能。

如何具体描述进程? 

        在日常生活中,如果我们想要管理人,首先需要对人进行描述。例如,我们可以定义一个结构体,里面存储人的各种属性,比如学号、姓名、住址、手机号等。有了这些信息,我们就能对人的数据进行管理,进而对人进行组织和实质性的管理。

        同样的道理,对于进程来说,操作系统也需要先对其进行描述,然后才能进行管理。那么,如何描述一个进程呢?进程有许多属性需要描述,比如进程的ID、进程的代码地址、进程的数据地址、进程的状态、进程的优先级、进程的链接字段等。通过这些属性,我们可以像管理人的数据一样管理进程的数据。

        例如,进程的ID(PID)是唯一标识一个进程的编号;进程的代码地址和数据地址指示了进程在内存中的存储位置;进程的状态(如运行、就绪、阻塞等)反映了进程当前的活动情况;进程的优先级决定了它在调度时的顺序;而进程的链接字段则可以帮助我们将进程像链表一样组织起来,从而快速找到某个进程。这种链接字段的实现通常依赖于指针的概念,通过指针将多个进程的PCB(进程控制块)串联起来,形成一个进程表或队列。

        所有这些属性共同组成了进程的描述信息,而这些信息被存储在一个结构体中。这个结构体有一个官方的名字,叫做进程控制块(PCB,Process Control Block)。PCB是操作系统管理进程的核心数据结构,它包含了进程的所有关键信息。

        通过PCB,操作系统可以像管理人一样对进程进行管理。例如,操作系统可以通过PCB中的状态信息决定是否调度某个进程运行,通过优先级信息决定进程的执行顺序,通过链接字段快速定位某个进程。这种“先描述,再组织”的方式,使得操作系统能够高效地管理内存中的多个进程,确保它们能够并发执行,同时避免资源冲突。

        总结来说,进程的描述是通过【进程控制块(PCB)】实现的。PCB中存储了进程的各种属性,如ID、代码地址、数据地址、状态、优先级和链接字段等。通过这些信息,操作系统可以像管理人一样对进程进行组织和管理,从而将操作系统的功能与进程的运行紧密结合在一起。理解PCB的作用,对于我们掌握进程管理的原理至关重要。

 进程的实质理解

进程=可执行程序+内核数据结构(PCB)

        由于我们的研究对象都是在Linux环境下进行研究,那么重点是讲述在Linux下的进程概念,在Linux操作系统下,进程的PCB被叫做是task_struct,也就是说,在Linux下描述进程的结构体不叫PCB,叫做task_struct,这是Linux内核中的一种数据结构,它会被装载到内存中,并且当中会包含着进程的信息,用下图来简答说明这段概念:

        这张图片清晰地展示了磁盘上的可执行程序被加载到内存中的过程。内存中早已加载好了操作系统,等待管理这些程序。当程序进入内存后,操作系统会为每个进程创建一个独特的 task_struct,这个结构体用来描述该进程的各种信息。

        每个进程都有自己的 task_struct,操作系统通过这些结构体的数据来管理进程。在内部,操作系统通常使用双向链表的形式来组织这些进程。当有进程被终止时,操作系统会将其从链表中删除;当有新进程进入时,操作系统会将其插入链表中。这种方式不仅方便管理,还能提高效率。

        因此,进程可以简单理解为:可执行程序加上内核的数据结构。通过这种方式,操作系统能够高效地管理和调度多个进程,确保它们正常运行。

【进程的管理被建模成了数据结构】

        数据结构是一种抽象的概念,它的实际意义在于帮助我们更方便地管理数据。以图中展示的进程为例,操作系统作为管理类软件,需要通过数据结构来管理进程。通过使用双向链表这种数据结构,操作系统可以将进程的管理简化为对链表的增删查改操作。有了这种思维方式,管理和处理数据就变得简单了许多。 

        进程的PCB可以存在于多个链表中,而不仅限于图中的一条链表。本质上,链表中的节点只是数据的一部分,这些数据可以同时存在于多条链表中。

        进程的管理并不局限于链表。虽然图中展示了用链表管理进程的方式,但实际上,进程还可以通过队列等其他数据结构进行建模和管理。数据结构的选择取决于具体的需求和场景。

【小结】

我们对进程的理解已经有了初步的框架。然而,这些知识还停留在理论层面,真正的实践操作将帮助我们更深入地理解进程的概念和运行机制。

查看系统进程

命令:ps ajx

  • a:所有
  • j:任务
  • x:把所有的信息全部输出

一般搭配管道使用,如:ps ajx | head - l && ps ajx | grep test,其中 ps ajx | head - l 是把 ps ajx 输出的信息中的第一行信息(属性列)输出。

[root@iZbp1157ft1ib0ydj8jqtzZ ~]# ps ajx | head -1 && ps ajx | grep test
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 5274  5487  5486  5274 pts/0     5486 S+       0   0:00 grep --color=auto test

Linux中,如果想要查看进程,可以在/proc系统文件夹中查看

也可以使用ps和top来获取进程信息 

 

实践进程

通过代码实践来验证:

//创建test.c文件
#include <stdio.h>
#include <unistd.h>

int main()
{
    while(1)
    {
        printf("正在打印进程\n");
        sleep(1);
    }
    return 0;
}

[root@iZbp1157ft1ib0ydj8jqtzZ Linux操作]# ps aux| grep test
root      6563  0.0  0.0   4216   356 pts/0    S+   10:48   0:00 ./test
root      6890  0.0  0.0 112812   980 pts/2    R+   10:50   0:00 grep --color=auto test

从中看出,test进程是确实存在的,并且其中还包含一个grep --color=auto test进程,因为调用grep管道指令也算一个进程 。

系统调用

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

  • 进程 ID(PID)
  • 父进程 ID(PPID)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
 
int main()
{
    while(1)
    {
        printf("I am a process, my pid is:%u\n", getpid());  //返回正在调用进程的进程ID
        printf("I am a process, my ppid is:%u\n", getppid()); //返回正在调用进程的父进程ID
        sleep(1);    
    }
    return 0;
}
I am a process, my pid is:7520
I am a process, my ppid is:7191
[root@iZbp1157ft1ib0ydj8jqtzZ 222]# ps ajx | head -1 && ps ajx | grep 7191
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 5225  7191  7191  7191 pts/3     7520 Ss       0   0:00 /bin/bash --init-......
 7191  7520  7520  7191 pts/3     7520 S+       0   0:00 ./test
 8438  8631  8630  8438 pts/0     8630 R+       0   0:00 grep --color=auto 7191

 

./test的父进程是命令行解释器--bash.

Linux中创建进程的方式通常有两种:

  1. 命令行中直接启动进程
  2. 通过代码来创建进程

对于第一种很好理解,执行程序./process其实就是启动进程,那第二种是什么?如何理解这个概念?就引出了本篇的核心,系统调用fork.

创建进程fork

        首先,我们需要对 fork 有一个初步认知。fork 是一个系统级别的调用,它可以为当前进程创建一个子进程。通过代码调用 fork,我们可以在系统中动态地创建新的进程。

        当用户启动一个进程时,实际上是在系统中新增了一个进程。这意味着操作系统需要管理的进程数量增加了一个。创建一个进程的过程包括向系统申请内存,保存该进程的可执行程序,并创建该进程的 PCB(即 task_struct 对象)。最后,操作系统会将这个 task_struct 对象添加到进程列表中,以便进行管理和调度。

        简单来说,创建进程就是向系统申请资源、保存程序数据、创建 PCB,并将其纳入操作系统的管理范围。

#include <unistd.h>
 
// pid_t是无符号整数
pid_t fork(void); // fork函数功能:创建一个子进程
  • 运行 man fork 认识 fork。

  • 通过复制调用进程创建一个新进程。

  • fork 有两个返回值

  • 父子进程代码共享,数据各自私有一份(采用写时拷贝)。

 fork 之后,如果不做任何的分流,fork 下面的所有代码是被父子进程共享的。

#include <stdio.h>
#include <sys/types.h> // getpid, getppid
#include <unistd.h>    // getpid, getppid, fork, sleep
 
int main()
{
    printf("I am a father: %u\n", getpid());
    fork();
 
    while(1)
    {
        printf("I am a process, pid: %u, ppid: %u\n", getpid(), getppid());
        sleep(1);
    }
 
    return 0;
}

fork的理解

站在程序员的角度:

fork 是操作系统中用于创建新进程的系统调用。当调用 fork 时,操作系统会创建一个与父进程几乎完全相同的子进程。父子进程共享用户代码(因为代码是只读的,不可修改),但用户数据是各自私有的。为了确保进程之间的独立性,操作系统采用了【写时拷贝(Copy-On-Write, COW)】技术。这意味着只有当某个进程尝试修改数据时,操作系统才会为该进程创建一份独立的数据副本,从而避免进程之间的相互干扰。

举个例子,打开 Windows 的任务管理器,可以看到许多进程在运行。如果关闭微信进程,QQ 进程不会受到影响。这是因为操作系统中所有进程都是相互独立的,进程具有独立性。为了确保进程之间不会互相干扰,操作系统通过 fork 创建子进程后,父子进程会继续运行,但谁先运行是由系统的调度优先级决定的,顺序并不固定。

总结来说,fork 创建的子进程与父进程共享代码但拥有独立的数据空间,通过写时拷贝技术确保进程独立性。父子进程的运行顺序由系统调度决定,进程之间互不干扰。

站在操作系统的角度:

fork 之后,从操作系统的角度来看,系统中确实多了一个新的进程。fork 创建的子进程通常以父进程为模板,子进程默认会使用父进程的代码和数据,但通过【写时拷贝(Copy-On-Write, COW)】技术,确保父子进程的数据在修改时是独立的。

由于系统中多了一个进程,操作系统会为子进程创建一个新的 PCB(进程控制块),并将父进程 PCB 中的部分内容(如代码段指针、寄存器状态等)拷贝到子进程的 PCB 中。这样,子进程就有了自己的独立描述信息,同时继承了父进程的运行环境。

总结来说,fork 创建子进程后,操作系统会为其分配新的 PCB,并通过写时拷贝技术确保父子进程的数据独立性。子进程继承了父进程的运行环境,但拥有独立的进程描述信息。

fork的用法

fork 的常规用法是通过创建子进程来实现任务的并行执行。通常,我们使用 fork 后,会利用 if 语句进行分流,让父进程和子进程执行不同的代码,从而实现并行效果。例如,父进程可以播放音乐,而子进程可以下载文件。

fork 的返回值用于区分父子进程:

  • 如果 fork 执行成功,父进程会返回子进程的 PID(进程 ID),而子进程会返回 0。

  • 如果 fork 执行失败,父进程会返回 -1,并且不会创建子进程,同时会设置适当的 errno 来指示错误原因。

通过这种方式,我们可以利用 fork 的返回值来控制父子进程的执行路径,从而实现并行任务的处理。

#include <stdio.h>  
#include <sys/types.h> // getpid, getppid  
#include <unistd.h>    // getpid, getppid, fork
 
int main()  
{  
    printf("I'm a father: %u\n", getpid());
    pid_t ret = fork();
    if (ret == 0)
    {  
        // child process
        while (1)
        {
            printf("child process, pid:%u, ppid:%u\n", getpid(), getppid());
            sleep(1);
        }
    }
    else if (ret > 0)
    {
        // father process
        while (1)
        {
            printf("father process, pid:%u, ppid:%u\n", getpid(), getppid());
            sleep(1);
        }
    }
    else
    {
        // failure
        perror("fork");
        return 1;
    }
    return 0;            
}

站在语言的角度,是不可能同时进入两个执行流的,既进入 if 也进入 else if 的,即不可能同时执行两个死循环。

但实际的运行结果:

理解fork的返回值

fork 函数有两个返回值的原因在于它的特殊行为:它会在调用进程中创建一个新的子进程。调用 fork 后,操作系统会将当前进程(父进程)复制一份,生成一个新的进程(子进程)。由于父子进程是独立的,它们会从 fork 调用处继续执行,但返回值不同:

  • 父进程fork 返回子进程的 PID(进程 ID)。

  • 子进程fork 返回 0。

如果 fork 失败,父进程会返回 -1,表示没有创建子进程。

这种设计是为了让父进程和子进程能够区分自己的身份,从而执行不同的逻辑。

调用一个函数时,这个函数准备 return 了,请问这个函数的功能执行完成了吗?

        当一个函数准备 return 时,通常意味着它的主要功能已经执行完成。return 的作用是结束函数的执行,并将控制权交还给调用者,同时可以返回一个值(如果有返回值的话)。

        不过,fork 是一个特殊情况。fork 的 return 并不是简单的结束函数,而是标志着父子进程的分叉点。在 fork 返回时,父进程和子进程都会从 fork 调用处继续执行,但返回值不同。

+-------------------+
| 调用 fork()        |
| 父进程继续执行     |
+-------------------+
         |
         v
+-------------------+       +-------------------+
| 操作系统复制进程   | ----> | 创建子进程         |
| 生成子进程         |       | 子进程从 fork 处继续 |
+-------------------+       +-------------------+
         |                           |
         v                           v
+-------------------+       +-------------------+
| 父进程返回子进程 PID|       | 子进程返回 0        |
| (pid > 0)          |       | (pid == 0)         |
+-------------------+       +-------------------+
         |                           |
         v                           v
+-------------------+       +-------------------+
| 父进程执行后续代码 |       | 子进程执行后续代码 |
+-------------------+       +-------------------+

关键点:

  1. 调用 fork:父进程调用 fork,操作系统开始复制父进程。

  2. 创建子进程:操作系统生成子进程,子进程是父进程的副本。

  3. 返回不同值

    • 父进程返回子进程的 PID。

    • 子进程返回 0。

  4. 分流执行:父子进程从 fork 调用处继续执行,但通过返回值区分身份,执行不同的代码。

如果 fork 执行成功,为什么在父进程中返回子进程的 pid,在子进程中返回的是 0 呢? 

在人类世界中,每个孩子只有一个亲生父亲,而一个父亲可以有多个孩子。因此,孩子找父亲是非常简单的,因为父亲是唯一的;而父亲为了更方便地管理多个孩子,需要给每个孩子一个独特的标识,并记住他们(比如:张三、李四、王五等)。

类似地,在操作系统中,fork 创建子进程后:

  • 父进程需要返回子进程的 PID,因为父进程需要知道它创建的子进程是谁,以便管理和跟踪。

  • 子进程只需要知道自己被成功创建,因此在子进程中返回 0 即可。

这种设计使得父进程能够有效地管理多个子进程,而子进程只需关注自己的任务执行。

如果创建多个子进程呢?

通过循环创建,下面这段代码并不完善,只是为了简单理解如果创建多个子进程的情况:

#include <stdio.h>
#include <stdlib.h>    // exit
#include <sys/types.h> // getpid, getppid  
#include <unistd.h>    // getpid, getppid, fork, sleep
 
int main()
{
    // 创建5个子进程
    for (int i = 0; i < 5; i++)
    {
        pid_t ret = fork();
        if (ret == 0)
        {
            // child process
            printf("child%d, pid:%u, ppid:%u\n", i, getpid(), getppid());
            sleep(1);
            exit(1); // 子进程退出
        }
    }
    getchar(); // getchar()目的是不让父进程退出,否则无法回收子进程。
    return 0;
}

运行结果:成功创建了 5 个子进程。但程序会一直卡在这里,不会自己退出。 

为什么上述代码中,fork 的返回值 ret 有两个值,既等于 0 又大于 0 呢?fork 之后,父子进程如何做到共享用户代码,如何做到用户数据各自私有的呢?

这两个问题学习了进程地址空间就能够很好的理解了。


http://www.kler.cn/a/558491.html

相关文章:

  • win32汇编环境,窗口程序中使用菜单示例四
  • 【java】就近原则
  • vscode@右键文件夹或文件vscode打开一键配置
  • for循环可遍历但不可以修改列表原因分析
  • 物联网常见协议基础学习
  • 【软考】【2025年系统分析师拿证之路】【啃书】第十三章 系统设计(十四)
  • CSS基础(盒子模型的组成、内容溢出、隐藏元素的方式、样式的继承、元素的默认样式、布局技巧、元素之间的空白问题、行内块元素的幽灵空白问题)
  • 利用 AI 大模型驱动企业智能化转型:Cherry Studio 与 Anything LLM 的应用探索
  • 海康威视摄像头ISUP(原EHOME协议) 摄像头实时预览springboot 版本java实现,并可以在浏览器vue前端播放(附带源码)
  • deepseek云端部署及结合本地知识库(结合api调用)可视化界面应用
  • 【拓展】二进制的原码、补码、反码及相互转换方式
  • Linux系统管理与编程01:准备工作
  • 深度学习(3)-TensorFlow入门(梯度带)
  • `pip freeze > requirements.txt` 命令
  • Python 错误和异常处理
  • 正则表达式特殊字符
  • 腾讯SQL面试题解析:如何找出连续5天涨幅超过5%的股票
  • LSTM 与随机森林的对比
  • LeetCode216
  • Python 的 Lambda 函数及应用场景