【Linux】进程概念

目录

一、进程的基础认知
    1.1 冯诺依曼体系结构
    1.2 操作系统的核心定位
    1.3 进程的本质定义
二、进程的核心描述 - PCB
    2.1 PCB的概念与Linux实现
    2.2 task_struct的核心内容
    2.3 进程的组织方式
三、进程的状态与特殊进程
    3.1 Linux进程的七种状态
    3.2 僵尸进程的形成与危害
    3.3 孤儿进程的处理机制
四、进程的创建与地址空间深度解析
    4.1 进程的查看方式
    4.2 fork系统调用:一次调用,两次返回
        4.2.1 fork的基本用法与返回值之谜
        4.2.2 if-else分流执行的原理
        4.2.3 写时拷贝(COW):子进程修改数据为何父进程不受影响
五、进程地址空间
    5.1 虚拟地址空间的本质
    5.2 进程地址空间的结构
    5.3 虚拟地址空间的优势
六、总结


一、进程的基础认知

要理解Linux进程,首先需要掌握计算机系统的底层架构和操作系统的核心作用,这是进程概念建立的基础。进程作为操作系统资源分配的基本单位,其存在和运行依赖于硬件架构的支撑和操作系统的管理。

1.1 冯诺依曼体系结构

我们日常使用的笔记本、服务器等绝大多数计算机,都遵循冯诺依曼体系结构。该体系将计算机硬件划分为输入设备、存储器、运算器、控制器和输出设备五大核心组件,其中核心要点是所有设备都只能直接与内存打交道
在这里插入图片描述

核心组件的功能分工如下:

  • 输入设备:键盘、鼠标、扫描仪等,负责将外部数据传入内存;
  • 存储器:此处特指内存,是数据和指令的临时存储中心,CPU只能对内存进行读写;
  • 中央处理器(CPU):包含运算器和控制器,运算器负责数据计算,控制器协调各组件工作;
  • 输出设备:显示器、打印机等,负责将内存中的处理结果输出到外部。

数据流动实例:以QQ聊天为例,发送文字消息时,键盘输入的字符先传入内存,CPU读取内存中的数据进行处理后,再通过网卡写入内存,网卡从内存读取数据发送给对方;对方接收时,网卡将数据写入内存,CPU处理后,内存中的数据再传输到显示器显示。发送文件的过程同理,文件数据需先加载到内存,再通过网络设备传输,全程不允许外设与CPU直接交互。

1.2 操作系统的核心定位

操作系统是计算机软硬件之间的桥梁,是一款纯正的“管理型”软件,其核心作用是承上启下。
从组成来看,操作系统分为广义和狭义两种:

  • 狭义OS:仅包含内核(Kernel),负责进程管理、内存管理、文件管理和驱动管理四大核心功能;
  • 广义OS:除内核外,还包括shell程序、函数库、系统级预装软件等。

操作系统的设计目的有两个层面:对下,直接与硬件交互,统一管理所有软硬件资源;对上,为应用程序提供稳定、高效的执行环境,屏蔽底层硬件的复杂性。
在这里插入图片描述

内核中“管理”的本质是先描述,再组织

  1. 描述被管理对象:用struct结构体
  2. 组织被管理对象:用链表或其他合适的数据结构

1.3 进程的本质定义

进程是操作系统资源分配的最小单位,对其定义有三个视角:

  • 课本视角:程序的一个执行实例,是正在运行的程序;
  • 内核视角:担当分配系统资源(CPU时间、内存空间等)的实体;
  • 实操视角:进程 = 内核数据结构(task_struct) + 程序自身的代码和数据。

这里需要明确程序与进程的区别:程序是静态的,是存放在磁盘上的指令和数据集合(如test.c编译后的./test文件);而进程是动态的,是程序加载到内存后运行的过程,具有生命周期,会随着执行完成而终止。一个程序可以对应多个进程,例如多次运行./test会产生多个独立的进程。

二、进程的核心描述 - PCB

操作系统要管理进程,首先需要对进程进行描述组织。描述进程的核心数据结构就是进程控制块(PCB),Linux系统中具体实现为task_struct

2.1 PCB的概念与Linux实现

PCB(Process Control Block) 是进程属性的集合,是操作系统感知进程存在的唯一标识。操作系统通过PCB掌握进程的所有状态信息,实现对进程的调度、管理和控制。
在Linux内核中,PCB的具体实现是task_struct结构体。该结构体被装载到内存中,包含了进程运行所需的全部信息。无论是系统进程还是用户进程,运行时都会在内核中创建对应的task_struct实例。

2.2 task_struct的核心内容

task_struct是一个庞大的结构体,其内容可分为八大核心类别,每一类都服务于特定的管理需求:

  1. 标识符(PID/PPID):进程的唯一标识,PID是进程自身ID,PPID是父进程ID,用于区分不同进程;
  2. 状态:记录进程当前的运行状态(如运行、睡眠、停止等),以及退出代码、退出信号等信息;
  3. 优先级:决定进程获取CPU资源的先后顺序,优先级越高,越容易被调度执行;
  4. 程序计数器:存储程序即将执行的下一条指令的地址,进程切换时需保存该值,恢复时可继续执行;
  5. 内存指针:指向程序代码、进程数据的内存地址,以及与其他进程共享的内存块指针;
  6. 上下文数据:进程执行时CPU寄存器中的数据,类似学生休学保存的学习进度,进程切换时需完整保存,恢复时原样加载;
  7. I/O状态信息:记录进程的I/O请求、占用的I/O设备(如打印机)和打开的文件列表;
  8. 记账信息:统计进程占用的CPU时间、时钟数、时间限制等,用于资源调度和计费。

2.3 进程的组织方式

Linux内核中,所有运行的进程通过task_struct双向链表的形式组织起来。这个链表贯穿了系统中所有的进程,内核通过遍历该链表实现对所有进程的批量管理(如遍历所有进程查找特定PID的进程)。
除了双向链表,内核还会通过其他数据结构辅助管理进程,例如用于快速查找的红黑树等,但双向链表是最基础、最核心的组织方式,确保了进程管理的稳定性和遍历效率。

三、进程的状态与特殊进程

在操作系统中,进程在生命周期中会不断切换状态。
在这里插入图片描述

Linux内核定义了七种进程状态,每种状态对应特定的运行场景。其中僵尸进程和孤儿进程是两种特殊状态的进程,需要重点关注其形成原因和处理方式。

3.1 Linux进程的七种状态

Linux内核源代码中通过task_state_array数组定义了七种进程状态,每种状态都有明确的语义和适用场景,具体如下:

状态标识状态名称核心含义
R运行状态(running)进程要么正在CPU上执行,要么在运行队列中等待CPU调度
S睡眠状态(sleeping)可中断睡眠,进程等待某个事件完成(如I/O完成、信号触发),可被信号唤醒
D磁盘休眠状态(disk sleep)不可中断睡眠,通常等待磁盘I/O完成,期间不响应任何信号,避免数据丢失
T停止状态(stopped)进程被暂停,可通过SIGSTOP信号触发,SIGCONT信号恢复运行
t追踪停止状态(tracing stop)进程被调试工具(如gdb)追踪时的停止状态
X死亡状态(dead)进程执行完成或异常终止,仅为返回状态,不会出现在进程列表中
Z僵尸状态(zombie)进程退出后,父进程未读取其退出状态,PCB仍保留在进程表中

查看进程状态的常用命令是ps auxps axj,通过命令输出的STAT字段可直接看到进程当前的状态。

3.2 僵尸进程的形成与危害

僵尸进程是进程退出后未能被父进程回收的产物,其形成的核心条件是:子进程退出,父进程未通过wait()等系统调用读取子进程的退出状态

僵尸进程的示例代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    pid_t id = fork();
    if (id < 0) {
        perror("fork");
        return 1;
    } else if (id > 0) { // 父进程
        printf("parent[%d] is sleeping...\n", getpid());
        sleep(30); // 父进程睡眠30秒,期间不回收子进程
    } else { // 子进程
        printf("child[%d] is begin Z...\n", getpid());
        sleep(5); // 子进程运行5秒后退出
        exit(EXIT_SUCCESS);
    }
    return 0;
}

编译运行后,用ps aux | grep test可观察到子进程状态为Z+,即僵尸状态。
在这里插入图片描述

僵尸进程的危害

僵尸进程的本质是PCB未被释放,而PCB存储在内存中,持续占用内存资源。如果父进程长期不回收子进程,大量僵尸进程会导致内存资源被耗尽,引发内存泄漏,最终影响系统正常运行。

3.3 孤儿进程的处理机制

孤儿进程是指父进程提前退出,而子进程仍在运行的进程。由于父进程已经终止,无法回收子进程的退出状态,此时Linux系统会启动1号init/systemd进程领养孤儿进程,负责回收其资源。

孤儿进程的示例代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("子进程 - PID: %d, 父进程PID: %d\n", getpid(), getppid());
        // 子进程睡眠2秒,确保父进程先退出
        sleep(2);
        // 此时父进程应该已经退出,子进程成为孤儿进程
        printf("子进程 - 现在我是孤儿进程\n");
        printf("子进程 - PID: %d, 新的父进程PID: %d\n", 
               getpid(), getppid());
        printf("子进程退出\n");
    } else {
        // 父进程
        printf("父进程 - PID: %d, 子进程PID: %d\n", getpid(), pid);
        // 父进程立即退出,使子进程成为孤儿
        printf("父进程退出\n");
        // 这里没有wait(),所以父进程不会等待子进程
    }
    return 0;
}

父进程退出后,子进程的PPID会变为1,成为init进程的子进程,待子进程执行完成后,init进程会自动回收其资源,因此孤儿进程不会像僵尸进程那样造成内存泄漏。
在这里插入图片描述

四、进程的创建与地址空间深度解析

进程的操作包括查看、创建等基础功能。其中,fork系统调用是创建新进程的核心,其行为特性与进程地址空间的设计紧密相关。而进程优先级则决定了进程获取CPU资源的先后顺序,是进程管理的另一个重要方面。

4.1 进程的查看方式

Linux提供了多种查看进程信息的方式,涵盖内核级、工具级等不同层面:

  1. /proc文件系统:内核将进程信息以文件形式存储在/proc目录下,每个进程对应一个以PID命名的文件夹。例如查看PID为1的进程信息,可访问/proc/1目录;
  2. ps命令:常用参数组合ps aux(以用户为中心显示所有进程)和ps axj(显示进程组ID、会话ID等信息);
  3. top命令:实时动态显示进程状态,可查看进程的CPU使用率、内存使用率等,支持交互式操作(如调整优先级)。

4.2 fork系统调用:一次调用,两次返回

fork()是Linux中创建新进程的核心系统调用。它的行为非常独特:调用一次,返回两次。它会创建一个与父进程(调用fork的进程)几乎完全相同的子进程。

4.2.1 fork的基本用法与返回值之谜

fork的返回值是理解其行为的关键:

  • 在父进程中fork返回新创建的子进程的PID(一个大于0的整数)。
  • 在子进程中fork返回0。
  • 如果创建失败fork返回-1(通常在父进程中返回-1)。

为什么会返回两个不同的值?
这是fork最核心的特性。当fork被调用时,内核会执行以下操作:

  1. 在内核中创建一个新的task_struct(PCB)。
  2. 将父进程的task_struct内容几乎完全复制到子进程的task_struct中。这包括进程状态、优先级、内存指针、文件描述符表等。
  3. 为子进程分配一个唯一的PID。
  4. 将子进程的task_struct加入到内核的进程链表中。
  5. 关键一步:内核会让父子进程都从fork函数调用的下一条指令开始继续执行

此时,内存中有两个几乎一模一样的进程在运行。它们都刚刚执行完fork系统调用。为了让这两个进程能够区分彼此并执行不同的逻辑,fork系统调用本身会根据当前运行的是父进程还是子进程,返回不同的值。父进程看到的是子进程的PID,而子进程看到的是0。

4.2.2 if-else分流执行的原理

正因为fork在父子进程中返回了不同的值,我们才能通过一个简单的if-else结构让它们执行不同的代码路径。

示例代码:

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

int main()
{
    pid_t ret = fork(); 
    if (ret < 0) { // fork失败
        perror("fork");
        return 1;
    } else if (ret == 0) { // 子进程执行路径
        printf("I am child process. My PID: %d, Parent PID: %d\n", getpid(), getppid());
    } else { // 父进程执行路径
        printf("I am parent process. My PID: %d, Child PID: %d\n", getpid(), ret);
        wait(NULL); // 等待子进程结束
    }

    printf("After fork\n"); // 父子进程都会执行这里
    return 0;
}

在这里插入图片描述

执行分析:

  1. 调用fork()。内核创建子进程。
  2. 父进程fork返回,ret的值是子进程的PID(例如12345)。由于ret > 0,它进入else分支,打印父进程信息。
  3. 子进程也从fork返回,ret的值是0。它进入else if (ret == 0)分支,打印子进程信息。
  4. 两个进程都会继续执行if-else结构之后的代码,打印出After fork

所以,if-else并不是“同时执行”,而是两个独立的进程在各自的地址空间中,根据fork返回给它们的不同值,分别执行了if-else结构中的不同分支。

4.2.3 写时拷贝(COW):子进程修改数据为何父进程不受影响

初学者常常困惑于,既然子进程是父进程的副本,为什么子进程修改了一个变量,父进程的变量值却不变。这就需要引入写时拷贝(Copy-On-Write, COW)机制。

传统fork的问题:
在早期的Unix系统中,fork会完整复制父进程的地址空间(代码、数据、堆、栈等)。这意味着创建一个子进程需要大量的内存和CPU时间。但实际上,很多子进程创建后会立即调用exec系列函数加载新程序,之前的复制工作就白费了,造成了巨大的浪费。

写时拷贝(COW)的优化:
为了解决这个问题,现代操作系统(包括Linux)都采用了COW技术。其核心思想是:

  1. fork时不复制fork创建子进程时,内核并不立即复制父进程的整个地址空间。相反,父子进程共享同一份物理内存页。
  2. 设置只读权限:内核会将这些共享的内存页标记为只读
  3. 写操作触发拷贝:当任何一个进程(父或子)试图写入这些共享的只读页面时,CPU的内存管理单元(MMU)会检测到一个“写时拷贝”冲突,并触发一个中断。内核接收到中断后,会为发生写操作的进程复制该内存页的一个私有副本,并将虚拟地址映射到这个新的物理页上,然后恢复写操作。

与进程地址空间的关联:
这正是进程地址空间(虚拟地址空间)发挥关键作用的地方。每个进程都有自己独立的虚拟地址空间和页表。

  • 初始状态:父子进程的虚拟地址空间看起来是一样的,它们的页表都指向相同的物理内存页。
  • 写操作发生时:当子进程尝试修改一个变量(例如g_val = 100),它实际上是在尝试写入自己虚拟地址空间中的一个地址。由于该地址对应的物理页被标记为只读,触发COW。内核为子进程分配一个新的物理页,将原页的数据复制过来,然后更新子进程的页表,使其虚拟地址指向这个新的物理页。
  • 最终结果:子进程现在修改的是自己私有的物理页,而父进程的页表仍然指向原来的物理页。因此,子进程的修改不会影响到父进程。

示例代码(展示COW效果):

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

int g_val = 10; // 全局变量

int main()
{
    pid_t id = fork();
    if (id < 0) {
        perror("fork");
        return 1;
    } else if (id == 0) { // 子进程
        printf("Child: Before change, g_val = %d, Address: %p\n", g_val, &g_val);
        g_val = 100; // 子进程修改全局变量
        printf("Child: After change, g_val = %d, Address: %p\n", g_val, &g_val);
    } else { // 父进程
        sleep(1); // 等待子进程先修改
        printf("Parent: g_val = %d, Address: %p\n", g_val, &g_val); // 父进程的值不变
        wait(NULL); // 回收子进程
    }
    return 0;
}

在这里插入图片描述

可以看到,父子进程中g_val虚拟地址是相同的,但子进程修改后,父进程的值并未改变。这正是COW机制和独立虚拟地址空间共同作用的结果。

五、进程地址空间

我们在C语言中看到的地址并非物理内存地址,而是虚拟地址。Linux通过虚拟地址空间机制,实现了进程内存的隔离和高效管理,这是现代操作系统内存管理的核心。

5.1 虚拟地址空间的本质

虚拟地址空间是操作系统为每个进程分配的独立地址空间,进程看到的是连续的虚拟地址,而非实际的物理内存地址。虚拟地址与物理地址通过页表建立映射关系,由内存管理单元(MMU)负责地址转换。

核心验证示例

通过fork()创建子进程,修改全局变量后观察地址变化:

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

int g_val = 0;
int main() {
    pid_t id = fork();
    if (id < 0) { perror("fork"); return 0; }
    else if (id == 0) { // 子进程
        g_val = 100;
        printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
    } else { // 父进程
        sleep(3);
        printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
    }
    sleep(1);
    return 0;
}

在这里插入图片描述

运行结果显示,父子进程中g_val的虚拟地址相同,但值不同。这证明虚拟地址并非物理地址,父子进程的虚拟地址映射到了不同的物理地址(这背后就是写时拷贝机制在起作用)。
在这里插入图片描述
上图就足矣说明问题,同⼀个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!

5.2 进程地址空间的结构

Linux中32位系统的虚拟地址空间大小为4GB,分为内核空间(1GB)和用户空间(3GB)。
在这里插入图片描述

用户空间从高地址到低地址依次分为以下区域:

  1. 命令行参数与环境变量:存储argvenv等数据;
  2. 栈区:存储局部变量、函数参数,栈空间自动增长,向下扩展;
  3. 共享区:存储动态链接库等共享资源;
  4. 堆区:动态内存分配区域,需手动申请和释放,向上扩展;
  5. 未初始化数据区(BSS):存储未初始化的全局变量和静态变量,初始值为0;
  6. 初始化数据区(DATA):存储已初始化的全局变量和静态变量;
  7. 正文代码区:存储程序的指令,只读属性。

进程地址空间由mm_struct结构体描述,每个进程有独立的mm_struct,通过vm_area_struct结构体描述各个虚拟内存区域,确保内存管理的精细化。
在这里插入图片描述

5.3 虚拟地址空间的优势

虚拟地址空间解决了直接使用物理内存的三大痛点,是现代操作系统的关键设计:

  1. 提升安全性:进程只能访问自身虚拟地址空间,无法直接操作物理内存,避免了恶意进程修改系统数据;
  2. 地址确定性:程序编译时使用固定的虚拟地址,运行时由OS负责映射到物理内存,无需关心物理内存的实际使用情况;
  3. 提高效率:通过分页机制,可将进程的部分内存换入换出磁盘,无需整体迁移,减少了内存与磁盘的IO开销;同时支持延迟分配,程序申请内存时仅分配虚拟地址,实际使用时才分配物理内存。

六、总结

核心认知层面

  • 进程本质:进程 = 内核数据结构(task_struct) + 程序代码和数据,是程序执行的动态实例
  • 管理机制先描述,再组织,PCB是进程存在的唯一标识
  • 状态流转:进程在运行、睡眠、停止、僵尸等状态间不断切换,理解状态转换是掌握进程行为的关键

关键技术特性

  • fork机制:一次调用两次返回的独特设计,配合if-else分流实现父子进程差异化执行
  • 写时拷贝:内存优化的核心技术,避免不必要的内存复制,提升系统性能
  • 地址空间:虚拟地址空间机制实现了进程间内存隔离,保障系统安全稳定运行
  • 特殊进程:僵尸进程需要及时回收避免资源泄漏,孤儿进程由init自动接管确保系统整洁
### Linux 进程基本概念详解 Linux 系统中的进程是操作系统进行资源分配和调度的基本单位,每个运行中的程序都对应一个进程进程的管理与调度是操作系统的核心功能之一[^1]。以下将从多个方面对 Linux 进程的基本概念进行详细说明。 #### 1. 进程的状态 在 Linux 系统中,进程具有多种状态,这些状态反映了进程当前的行为和生命周期。通常,Linux 中的进程有以下几个主要状态: - **创建状态**:当一个新的进程被创建时,它处于此状态。 - **就绪状态**:进程已经准备好运行,等待 CPU 资源分配。 - **执行状态**:进程正在 CPU 上运行。 - **阻塞状态(等待状态)**:进程因等待某些事件(如 I/O 操作完成)而暂停运行。 - **终止状态**:进程已完成或被终止。 此外,Linux 特有的状态还包括: - **D状态(不可中断的睡眠状态)**:这是一种深度睡眠状态,通常发生在进程与硬件交互时,例如等待慢速设备完成操作。该状态下的进程无法被信号唤醒或终止,只能通过等待其完成或重启系统来解决[^2]。 - **Z状态(僵尸状态)**:当子进程退出后,父进程尚未回收其退出状态时,子进程会进入僵尸状态。这种状态的进程仅保留在进程表中,不占用任何实际资源[^5]。 #### 2. 进程控制块(PCB) 每个进程都有一个对应的进程控制块(Process Control Block, PCB),它是操作系统用来描述和管理进程的数据结构。PCB 包含了进程的所有信息,包括但不限于: - 进程标识符(PID) - 内存管理信息 - CPU 寄存器状态 - 文件描述符表 - 其他资源使用情况 PCB 是操作系统管理和调度进程的基础,通过它,操作系统可以跟踪和控制每个进程的运行状态[^4]。 #### 3. 进程的优先级与竞争性 由于系统中可能存在大量的进程,而 CPU 的资源有限,因此进程之间存在竞争关系。为了更高效地完成任务,操作系统引入了进程优先级的概念进程优先级决定了进程在 CPU 调度中的顺序,高优先级的进程通常会优先获得 CPU 资源。可以通过 `nice` 命令调整进程的优先级[^3]。 #### 4. 进程的并行与并发 - **并行**:在多核或多 CPU 系统中,多个进程可以同时运行,这称为并行。 - **并发**:在单核 CPU 系统中,操作系统通过时间片轮转等调度策略,在一段时间内让多个进程轮流运行,从而实现并发的效果[^3]。 #### 5. 创建子进程Linux 系统中,可以通过 `fork()` 系统调用创建子进程。`fork()` 函数会复制当前进程的所有资源,并生成一个几乎完全相同的子进程。子进程拥有独立的地址空间和资源,但继承了父进程的部分属性,例如文件描述符和环境变量[^4]。 ```python import os pid = os.fork() if pid == 0: # 子进程代码 print("子进程 PID:", os.getpid()) else: # 父进程代码 print("父进程 PID:", os.getpid(), "子进程 PID:", pid) ``` #### 6. 查看进程 Linux 提供了多种工具用于查看和管理进程,例如: - `ps` 命令:显示当前系统的进程状态。 - `top` 命令:实时监控系统中各个进程的资源使用情况。 - `kill` 命令:向指定进程发送信号以终止或控制其行为。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值