011 Linux进程

发布于:2025-06-24 ⋅ 阅读:(17) ⋅ 点赞:(0)

🦄 个人主页: 小米里的大麦-CSDN博客
🎏 所属专栏: Linux_小米里的大麦的博客-CSDN博客
🎁 GitHub主页: 小米里的大麦的 GitHub
⚙️ 操作环境: Visual Studio 2022

在这里插入图片描述

深入理解 Linux 进程管理

一、管理哲学/本质:先描述,再组织(校长如何管理学校?)

一个操作系统不仅仅只能运行一个进程,可以同时运行多个进程。操作系统的进程管理:先描述,在组织 → 任何一个进程。在操作系统中,管理任何对象,最终都可以转化成为对某种数据结构的增删查改。想象你是一所大学的校长,管理数万名学生。你不需要认识每个学生,只需通过 学生档案系统 管理:

  • 描述:每个学生有专属档案(学号、姓名、专业、成绩、宿舍号…)
  • 组织:档案按学院 → 专业 → 班级形成 链表结构
  • 管理:调整专业只需修改档案中的“专业”字段,开除学生只需删除对应档案(增删查改

操作系统管理进程同理

  1. 描述:为每个进程创建 task_struct(进程的“档案”)
  2. 组织:通过链表、队列等数据结构管理所有 task_struct
  3. 控制:调整优先级、终止进程等操作只需修改对应结构体

数据结构的作用

  • 学校:学生档案链表 → 管理学生 增删查改(如入学、转班、退学)。
  • 操作系统:进程 PCB 链表 → 管理进程的创建、调度、终止等。

二、进程的定义与核心概念

1. 什么是进程?
  • 进程 = 程序员自己写的代码和数据 + 内核 PCB 数据结构对象(描述进程的所有数据,process ctrl block → 进程控制块)
    • 程序:程序员编写的代码(如 hello.c 编译后的可执行文件 hello)。
    • 进程:程序被加载到内存中运行的实例(如运行 ./hello 后生成的进程)。
  • 关键点
    • 进程是动态的:程序本身是静态文件,进程是运行时的实体。
    • 每个进程都有自己的 PCB(进程控制块),存储进程的所有信息。
2. 进程管理的核心:进程控制块(PCB)
  • PCB 的作用:记录进程的所有属性,帮助内核管理进程。
  • Linux 中的 PCB:名为 task_struct 的结构体(C 语言中的结构体)。
  • 类比:就像学生的学籍档案,记录了学号、成绩、班级、状态等。

三、Linux 的 task_struct 详解(进程控制块的实现)

1. task_struct 的结构(进程的 DNA)

在 Linux 内核中,进程属性的集合 task_struct 是一个庞大的结构体(约有数百个字段),task_struct 是 Linux 内核的一种数据结构,它会被装载到 RAM(内存)里并且包含着进程的信息。

// 简化版 task_struct 结构(Linux内核真实结构复杂得多)
struct task_struct
{
    long pid;                		// 身份证号(进程唯一标识)
    volatile long state;      		// 当前状态(睡觉/跑步/待机)
    int priority;            		// VIP等级(优先级)
    struct mm_struct *mm;    		// 家庭住址(内存分配情况)
    struct files_struct *files; 	// 随身物品(打开的文件)
    
    // 相关指针信息:
    structPCB *next
    struct PCB *queue
    struct PCB *xxxxx
        
    // ... 上百个字段
};
关键字段解析(下面详解):
字段 类比 作用
pid 学号 唯一标识进程(ps -ef 查看)
state 学生状态(上课/请假) 进程状态:运行(R)、睡眠(S)、僵尸(Z)等
priority 奖学金等级 优先级高的进程优先使用 CPU(nice 命令调整)
mm_struct 宿舍分配表 记录进程使用的内存空间(代码段、数据段、堆栈等)
files_struct 书包里的书本 记录进程打开的所有文件(lsof -p <pid> 查看)
上下文数据 课堂笔记 保存 CPU 寄存器的值,用于中断后恢复现场

1. 标识符(Identity)
  • pid(进程 ID):唯一标识符,类似学生的学号。每个进程有唯一的 PID,通过 ps 命令可见。
  • ppid(父进程 ID):记录该进程的父进程 PID,形成父子进程关系。类比:学生档案中记录班主任的工号。
2. 状态(State)
  • 进程状态:进程当前所处的阶段,包括:

    • 运行态(Running):正在 CPU 上执行。
    • 就绪态(Runnable):等待调度到 CPU 上运行。
    • 阻塞态(Blocked):等待 I/O(如读写文件)、等待信号等。
    • 终止态(Zombie):进程已结束,但父进程未回收其资源(成为“僵尸”)。
  • 字段示例

    volatile long state;  // 进程状态(如TASK_RUNNING, TASK_INTERRUPTIBLE)
    int exit_code;        // 进程退出时的返回值
    
3. 优先级(Priority)
  • 调度优先级:决定进程获得 CPU 时间的“权重”。

    • nice 值:用户可调整的优先级(值越低,优先级越高)。
    • 实时优先级:用于需要严格实时性的进程(如音频播放)。
  • 字段示例

    int nice;             // nice值(-20到19,默认0)
    unsigned long policy; // 调度策略(如SCHED_OTHER普通进程)
    
4. 程序计数器(Program Counter)
  • 寄存器状态:记录进程执行到的位置。
    • pc(程序计数器):指向当前指令的下一条指令的地址。
    • 其他寄存器:如栈指针(rsp)、基指针(rbp)等,保存进程执行现场。
  • 作用:当进程被中断(如切换到其他进程),内核会保存这些寄存器的值,以便恢复执行。
5. 上下文(Context)
  • 上下文数据:进程执行时的所有硬件寄存器的值。
    • 类比:学生临时离开教室前,记录自己写到哪页笔记、用哪支笔。
    • 保存场景:当进程被中断(如 I/O 请求),内核将寄存器值保存到 task_struct
    • 恢复场景:当进程重新调度到 CPU 时,内核恢复这些寄存器值,继续执行。
6. 内存与资源信息
  • 内存指针:记录进程的代码、数据、堆栈的内存地址。
    • 代码段(text 段):程序指令的内存区域。
    • 数据段(data 段):全局变量、堆栈等。
    • 共享内存:与其他进程共享的内存块指针。
  • 资源列表
    • 打开的文件描述符(如 /dev/sda)。
    • 分配的 I/O 设备(如 USB 设备)。
7. I/O 状态
  • I/O 请求队列:记录进程等待的 I/O 操作(如读取硬盘)。
  • 分配的设备:进程当前占用的硬件设备。
8. 记账信息(Accounting)
  • CPU 时间统计
    • utime:用户模式下消耗的 CPU 时间。
    • stime:内核模式下消耗的 CPU 时间。
  • 时间限制:如进程允许的最大运行时间。
  • 记账用户:记录进程属于哪个用户(如 root 或普通用户)。
9. 其他信息
  • 信号处理:记录进程如何处理信号(如 SIGTERM 终止信号)。
  • 线程信息:如果是多线程程序,记录线程组信息。
  • 调度队列指针:指向进程所在的就绪队列或阻塞队列。

四、组织进程

可以在内核源代码里找到它。所有运行在系统里的进程都以 task_struct 链表等非常复杂的数据结构的形式存在内核里。


五、查看进程

1. 查看进程方法

在 Linux 中,进程(Process)是正在运行的程序实例。查看进程的常用命令有:

  • ps:最常用的进程查看工具,适合快速查看进程快照(静态结果输出)。
  • top:动态监控进程,适合实时观察系统状态(动态查看进程变化,默认 5 秒更新)。
  • htop:增强版 top,界面更友好,适合交互式操作。
  • 其他命令:如 pgreppidof 等,适合精准定位进程。

1. ps 相关命令

当直接输入 ps 而不带任何参数时,命令会:

  • 仅显示当前终端会话中的进程,即与当前终端关联的进程。
  • 输出内容较为简洁,仅包含以下字段:
    • PID:进程 ID。
    • TTY:进程关联的终端(如 pts/0 表示伪终端)。
    • TIME:进程占用的 CPU 时间。
    • CMD:启动进程的命令名及参数。
命令/选项 说明 示例 常用度
ps aux 显示所有用户的进程,包含详细信息(用户、CPU、内存等) ps aux ⭐⭐⭐⭐⭐
ps -ef 显示全格式进程信息(更详细的进程信息,常用于系统管理和脚本,含 PPID) ps -ef ⭐⭐⭐⭐⭐
ps -e 显示所有进程 ps -e ⭐⭐⭐
ps -u <用户> 显示指定用户的进程 ps -u root ⭐⭐⭐
ps -p <PID> 显示指定 PID 的进程信息 ps -p 1234 ⭐⭐⭐⭐
ps -o 按需展示特定字段,常与 -p 结合使用(如 PID、PPID、CMD 等) ps -o pid,ppid,cmd,%mem,%cpu ⭐⭐⭐⭐
ps aux --sort 按指定字段排序(如内存、CPU) ps aux --sort=-%mem ⭐⭐⭐⭐
ps auxf 以树状结构显示进程 ps auxf ⭐⭐⭐⭐
2. top 相关命令(按 q 退出)
命令/选项 说明 示例 常用度
top 实时动态查看进程状态(类似任务管理器) top ⭐⭐⭐⭐⭐
top -c 显示完整命令路径 top -c ⭐⭐⭐⭐
top -p <PID> 监控指定 PID 的进程 top -p 1234 ⭐⭐⭐
top -u <用户> 监控指定用户的进程 top -u root ⭐⭐⭐
top -b 以批处理模式运行,输出到文件 top -b -n 1 > top.log ⭐⭐⭐
top -n <次数> 指定刷新次数后退出 top -n 3 ⭐⭐⭐
3. htop 相关命令
命令/选项 说明 示例 常用度
htop 增强版 top,支持鼠标操作和颜色高亮(需安装:yum install htop htop ⭐⭐⭐⭐⭐
htop -u <用户> 显示指定用户的进程 htop -u root ⭐⭐⭐⭐
htop -p <PID> 显示指定 PID 的进程 htop -p 1234 ⭐⭐⭐
htop -d <秒> 设置刷新间隔时间 htop -d 5 ⭐⭐⭐
4. 其他进程相关命令
命令/选项 说明 示例 常用度
pgrep 根据名称查找 PID(支持模糊匹配) pgrep sshd ⭐⭐⭐⭐
pgrep -l 显示进程名和 PID pgrep -l ssh ⭐⭐⭐⭐
pidof 根据精确进程名查找 PID pidof nginx ⭐⭐⭐
pstree 以树状结构显示进程关系(需安装:yum install psmisc pstree -p ⭐⭐⭐
kill <PID> 终止指定进程 kill 1234 ⭐⭐⭐
kill -9 <PID> 强制杀死进程 kill -9 1234 ⭐⭐⭐⭐
5. /proc 文件系统
命令/路径 说明 示例 常用度
/proc/[PID]/status 查看进程的详细状态信息 cat /proc/1234/status ⭐⭐⭐⭐
/proc/[PID]/cmdline 查看进程的启动命令 cat /proc/1234/cmdline ⭐⭐⭐
/proc/[PID]/exe 查看进程对应的可执行文件 ls -l /proc/1234/exe ⭐⭐⭐
/proc/[PID]/fd 查看进程打开的文件描述符 ls -l /proc/1234/fd ⭐⭐⭐

2. 通过系统调用获取进程标识符
  1. C 程序示例
#include <unistd.h>  // 提供 getpid(), getppid(), 和 sleep()
#include <stdio.h>

int main()
{
    while (1)
    {
        // 获取当前进程 ID
        pid_t pid = getpid();
        printf("当前进程 ID (PID): %d\n", pid);

        // 获取父进程 ID
        pid_t ppid = getppid();
        printf("父进程 ID (PPID): %d\n", ppid);
        
        // 睡眠 1 秒
        sleep(1);  
    }

    return 0;
}
  1. 编译
gcc -o getpid getpid.c
  1. 运行
./getpid
  1. 示例输出
当前进程 ID (PID): 1195
父进程 ID (PPID): 481

3. fork() 创建进程详解

摘自鸟哥的 Linux 私房菜基础学习篇(第四版)517页:

Linux 的程序调用通常成为 fork-and-exec 的流程。进程都会借父进程以复制(fork)的方式产生一个一模一样的子进程,然后被复制出来的子进程再以 exec 的方式来执行实际要执行的进程,最终就成为一个子进程。

  1. 基础示例代码
#include <unistd.h>
#include <stdio.h>

int main()
{
    pid_t pid = fork();

    if (pid < 0)
    {
        fprintf(stderr, "创建子进程失败");                             // 将标准错误输出的信息改为中文
        return 1;
    }
    else if (pid == 0)
    {
        printf("子进程PID=%d, 父进程PPID=%d\n", getpid(), getppid());  // 子进程中打印的信息改为中文
    }
    else
    {
        printf("父进程PID=%d, 创建的子进程PID=%d\n", getpid(), pid);   // 父进程中打印的信息改为中文
    }

    return 0;
}
  1. 示例输出:
父进程PID=2744, 创建的子进程PID=2745
子进程PID=2745, 父进程PPID=2744

注意:输出顺序可能不固定,取决于系统调度,再看下面一个示例:

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

int main()
{
    printf("begin: 我是一个进程,pid: %d, ppid: %d\n", getpid(), getppid());
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        while (1)
        {
            printf("我是子进程,pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else if (id > 0)
    {
        // 父进程
        while (1)
        {
            printf("我是父进程,pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else
    {
        //error
    }

    return 0;
}

示例输出:

begin: 我是一个进程,pid: 3721, ppid: 481
我是父进程,pid: 3721, ppid: 481
我是子进程,pid: 3722, ppid: 3721
我是父进程,pid: 3721, ppid: 481
我是子进程,pid: 3722, ppid: 3721
我是子进程,pid: 3722, ppid: 3721
……

[!IMPORTANT]

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

fork() 的目的是创建一个 几乎完全相同 的子进程(一般而言 fork 之后的代码父子共享)。让父子进程执行不同的代码分支。通过返回值区分父子进程的身份:

  • 父进程:需要知道子进程的 PID,以便后续管理(如等待子进程退出、发送信号等)。
  • 子进程:无需知道自己的 PID(可通过 getpid() 获取),但需要明确自己是子进程(返回 0 是最直接的方式)。

实现意义

  • 父进程通过子进程 PID 可以追踪、控制子进程(如 waitpid())。
  • 子进程通过返回值 0 知道自己是被创建的一方,从而执行子进程独有的逻辑。

总结:父进程通过返回值区分子进程,子进程通过返回值确认自己是子进程。


2. fork() 作为一个函数为什么能返回两次?如何理解?

fork() 本质上只调用一次,但返回两次! fork() 调用后,操作系统会复制父进程,创建一个新的子进程。

  • 父进程:继续从 fork() 返回,得到子进程的 PID。

  • 子进程:从 fork() 返回,得到 0。

  • 示意图

    +----------------+
    | 父进程执行 fork() | --> 子进程诞生
    +----------------+
           |                  |
           v                  v
    返回子进程 PID            返回 0
    

看似一次调用返回两次,实际是两个独立的进程分别执行了 fork() 后的代码。可以把 fork() 想象成 一颗分叉的树:调用时是同一条路径,但执行后分成了两条路,各自独立运行。


3. 为什么同一个变量在父子进程里内容不同?

调用 fork() 时,子进程不会立即复制父进程的内存,而是共享同一份物理内存(标记为只读)。当父子进程 修改内存时,操作系统才会复制被修改的页(内存页的最小单位)。具体来说就是当 fork() 被调用时:

  1. 代码段(Code Segment): 父子进程共享这部分内存,不需要复制。毕竟代码是静态的,读操作不会影响对方。
  2. 数据段、堆、栈(Data, Heap, Stack): 父子进程“看起来”各自独立,但实际上,系统不会立刻复制这部分内存!取而代之的是:
    • COW 机制: 父子进程最开始共享同一片物理内存,这些内存页被标记为 只读(Read-Only)
    • 写时复制: 当父或子进程试图写入这片内存时,操作系统才会触发“页面复制”,分配新的内存页,供写操作独享。

这种机制大幅优化了 fork() 的性能,因为如果父子进程不修改内存,根本就不用复制,节省了大量资源。综上:fork() 之后,父子进程共享代码段,但数据、堆、栈在写操作时会触发“写时复制”。

举个例子:

#include <stdio.h>
#include <unistd.h>
int main()
{
    int x = 100;
    pid_t pid = fork();

    if (pid == 0)
    {
        // 子进程
        printf("子进程 x = %d\n", x);                // 共享内存,输出 100
        x = 200;                                    // 写操作,触发写时复制,子进程独享这片内存
        printf("子进程修改后 x = %d\n", x);
    }
    else
    {
        // 父进程
        sleep(1);                                   // 让子进程先跑
        printf("父进程 x = %d\n", x);               // 父进程的 x 仍然是 100,未受子进程影响
    }

    return 0;
}

输出:
子进程 x = 100
子进程修改后 x = 200
父进程 x = 100

解释:

  1. fork() 之后,父子进程共享内存,x 的初始值对父子都可见(100)。
  2. 子进程写入 x 时,触发 COW:
    • 系统为子进程分配新内存页,把原来内存页的值(100)复制过来。
    • 子进程修改自己的副本,父进程继续使用原来的内存页。
  3. 所以,父子进程的 x 从此各自独立,互不干扰。因为它们的内存空间已经分开,各玩各的!

注:如果想让父子进程共享数据,得用专门的共享内存机制(如 mmap())。


4. fork() 究竟在干什么?系统中多了一个进程吗?

是的,fork() 让系统里多了一个新进程!可通过 ps 命令观察到子进程的存在:ps -ef | grep <程序名>。它做了这几件事:

  1. 复制父进程的内存空间: 代码段、数据段、堆栈等全部复制,子进程看起来和父进程几乎一模一样。

    • 复制父进程的 进程控制块(PCB),生成子进程的 PCB。
    • 复制父进程的 虚拟地址空间(通过写时复制优化)。
  2. 分配新的进程 ID(PID): 子进程有独立的 PID,同时保留父进程的 PPID(Parent PID)。

  3. 独立调度: 子进程作为独立的进程被调度,和父进程各自执行,互不干扰。

所以,fork() 之后,系统里确实多了一个进程!但它们起点一致,只是接下来的执行路径可能不同。


5. 为什么要创建子进程?

创建子进程的目的是 并行执行不同的任务,常见用途有:

  1. 后台任务: 如守护进程、Web 服务器,父进程处理请求,子进程负责具体工作。
  2. 任务分解: 父进程分配工作,子进程完成任务。
  3. 隔离风险: 子进程失败不会影响父进程,避免整个程序崩溃。

简单来说,父进程管理,子进程干活,井井有条!


6. fork() 后,谁先运行?

这个是 不确定的!谁先运行完全取决于操作系统的调度策略:

  • 父子进程创建完成后,由操作系统的调度器决定执行顺序。
  • 可能父进程先运行,也可能子进程先运行,甚至交替执行

所以,谁先跑?随缘!操作系统说了算,让操作系统的调度器决定,它才是“调度大哥”!


六、扩展思考

Q1:为什么进程不需要直接与内核交互?

答:内核通过 task_struct 间接管理进程,就像校长通过学生档案管理学生。进程只需执行代码,内核通过数据结构记录状态并决策。

Q2:如何查看进程的 task_struct

答:用户态无法直接访问 task_struct,但可以通过以下命令间接查看:

  • ps:显示 PID、状态、CPU/内存使用。
  • /proc/[pid]/stat:虚拟文件系统,如 cat /proc/1234/stat 包含进程的详细信息(映射 task_struct 字段)。
Q3:多个进程如何共享 CPU

答:通过调度算法在 task_struct 间切换。

  • 完全公平调度(CFS):基于虚拟运行时间(vruntime)决定进程优先级。
  • 实时调度策略:如 SCHED_FIFO(先进先出)和 SCHED_RR(时间片轮转)。
  • 调度相关字段task_struct 中的 policy(调度策略)、prio(动态优先级)、static_prio(静态优先级)。

共勉

在这里插入图片描述
在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到