【Linux系统】进程概念

发布于:2025-07-14 ⋅ 阅读:(20) ⋅ 点赞:(0)

1. 进程概念

1.1 进程的本质

核心定义

  • 用户视角:程序的动态执行实例(如同时运行多个Chrome窗口即多个进程)。

  • 内核视角:资源分配的最小实体单位,独享CPU时间片、内存空间和文件资源。

  • 现代定义
    进程 = 内核数据结构(task_struct) + 程序代码和数据段

进程不仅是“程序的执行实例”或“正在执行的程序”,从内核角度看,它是分配系统资源(CPU时间、内存)的实体。更精确地说,进程 = 内核数据结构(task_struct) + 程序代码和数据。当程序被加载到内存时,操作系统为其创建一个task_struct实例,该结构体封装了进程的所有属性和状态信息。生动示例:想象一个C程序(如hello.c)被编译执行时,操作系统会动态分配内存和CPU时间片,并将程序指令映射到虚拟地址空间,同时初始化task_struct来跟踪其状态,就像给每个运行的程序贴上一个“身份证”和“健康记录卡”。

示例:启动两个vim编辑不同文件时,系统创建两个独立进程,各自拥有独立的代码执行流和内存空间,互不干扰。

 我们先创建一个myprocess.c文件,然后死循环,每隔一秒打印

这里直接运行这个可执行程序,当我们把这个程序运行起来,其实就是一个进程


1.2 进程控制块 (PCB)

task_struct:Linux的PCB实现

  • 存储位置:常驻内存(RAM),由内核动态管理。

  • 关键字段分类(扩展版):

    字段类别 具体内容
    标识符 PID(进程ID)、PPID(父进程ID)、PGID(进程组ID)
    状态 运行态(TASK_RUNNING)、睡眠态(TASK_INTERRUPTIBLE)、僵尸态(EXIT_ZOMBIE)等
    内存指针 代码段(mm_struct->code_start)、数据段(mm_struct->data_start)指针
    上下文数据 保存暂停时的CPU寄存器值(eip, eax等),用于恢复执行
    文件描述符表 记录打开的文件(files_struct结构体)
    资源限制 最大文件打开数、CPU时间配额(struct rlimit

进程组织方式

// 内核源码示例(简化版)
struct task_struct {
    volatile long state;          // 进程状态
    struct mm_struct *mm;         // 内存管理结构体
    pid_t pid;                    // 进程ID
    struct files_struct *files;   // 打开文件表
    struct list_head tasks;       // 双向链表指针
    // ... 其他字段
};
  • 全局进程链表:内核通过struct list_head tasks所有进程组成双向链表,头节点为init_task(PID=1的init进程)。


1.3 查看进程的实战方法

1. /proc文件系统

  • 动态虚拟文件系统:以目录形式暴露内核进程信息。

    # 查看PID为1的进程信息
    $ ls /proc/1
    exe -> /usr/lib/systemd/systemd  # 可执行文件链接
    cwd                              # 当前工作目录
    fd/                              # 打开的文件描述符
    status                           # 进程状态摘要

2. 命令行工具

# 显示进程树(含父子关系)
$ pstree -p
systemd(1)─┬─sshd(1234)───bash(5678)───vim(9012)
           └─crond(2345)

# 动态监控进程
$ top -p 9012  # 监控PID 9012(vim进程)的资源占用

3. ps指令

一、ps 命令核心功能

作用:捕捉系统当前进程快照(非实时),用于:

  • 查看进程状态(运行/睡眠/僵尸)

  • 分析资源占用(CPU/内存)

  • 定位问题进程

  • 查看进程间关系


二、参数详解与使用场景
1. 基础查看
命令 作用 示例输出片段
ps aux 查看所有用户的所有进程 USER PID %CPU %MEM VSZ RSS TTY...
ps ajx 显示进程树关系(含PPID/PGID) PPID PID PGID SID TTY COMMAND...

输出字段解析

  • VSZ:虚拟内存大小 (KB)

  • RSS:实际物理内存 (KB)

  • TTY:关联终端(? 表示无终端)

  • STAT:进程状态(后文详解)


2. 进程状态(STAT)解码
状态码 含义 说明
R 运行中 (Running) 正在执行或就绪状态
S 可中断睡眠 (Sleeping) 等待事件完成(如 I/O 操作)
D 不可中断睡眠 (Disk sleep) 通常发生在磁盘 I/O,不可被信号中断
T 暂停状态 (Stopped) 被信号暂停(如 SIGSTOP
Z 僵尸进程 (Zombie) 进程已终止,但父进程未回收
< 高优先级进程 优先级高于默认值
N 低优先级进程 优先级低于默认值
s 会话领导者 (Session leader) 控制终端的进程
+ 前台进程组 (Foreground group) 与终端交互的进程

示例Ss+ = 会话领导者 + 可中断睡眠 + 前台进程


3. 高级过滤与显示
参数组合 作用 示例应用场景
ps -e | grep ssh 查找特定进程 检查 SSH 服务是否运行
ps -fC nginx 显示进程完整命令行 (-f) + 按名称过滤 查看 Nginx 配置参数
ps -p 1234 -o pid,ppid,cmd 自定义输出字段 查看指定进程的父子关系
ps --forest 树形显示进程层级 分析进程派生关系
ps -eo pid,ppid,cmd --sort=-%mem 按内存排序 找出内存消耗最大的进程

1.4 获取进程标识符(代码解析)

我们可以通过man手册来查看一下getpid

基本定义

  • 进程ID(PID)

    • 定义:进程ID(Process ID)是操作系统分配给每个进程的唯一标识符。它用于区分系统中的不同进程。
    • 作用
      • 资源分配:PID是分配系统资源(如内存、CPU时间等)的重要依据。

      • 进程控制:操作系统可以使用PID来对进程进行操作,例如启动、停止、暂停或终止进程。

      • 唯一标识:每个进程在系统中都有一个唯一的PID,操作系统通过PID来管理进程。

    • 示例cat进程的PID为3538。

  • 父进程ID(PPID)

    • 定义:父进程ID(Parent Process ID)是指创建当前进程的进程的ID。每个进程都有一个父进程(除了初始进程)。

    • 作用

      • 进程关系:PPID用于表示进程之间的父子关系。通过PPID,可以追踪进程的创建过程。

      • 资源继承:子进程通常会继承父进程的资源(如文件描述符、环境变量等)。

      • 进程管理:操作系统可以通过PPID来管理进程树结构,例如在父进程终止时,清理其子进程。

    • 示例bash进程(PPID=2686)创建了cat进程(PID=3538),因此cat的PPID为2686。


核心特性

(1) 父子进程关系
  • 创建机制:父进程通过系统调用(如fork())创建子进程。子进程继承父进程环境,但操作系统为其分配新PID,同时将其PPID设为父进程的PID。
    代码示例(C++):

    pid_t t = fork();  // 创建子进程
    if (t == 0) {
        // 子进程:getpid()返回自身PID,getppid()返回父进程PID
        cout << "子进程 PID:" << getpid() << " PPID:" << getppid() << endl; 
    } else {
        // 父进程:getpid()返回自身PID
        cout << "父进程 PID:" << getpid() << endl; 
    }
    
  • 关系规则

    • 一个父进程可创建多个子进程,所有子进程共享同一PPID(即父进程PID)。
    • 子进程退出后,父进程需回收其资源,否则可能产生僵尸进程。(至于什么是僵尸进程后面章节会讲)
(2) 特殊进程
  • init进程(PID=1)
    系统启动的第一个进程(Linux中通常为systemd),是所有用户进程的最终祖先,其PPID为0或-1(表示无父进程)。

    示例:进程表中PID=1的进程PPID为-1。
  • 内核进程(PID=0)
    管理内存交换等核心任务,无PPID。

注意:

数据类型本质
pid_t 是一个带符号整数类型signed int),在 Linux 系统中被明确定义为 int 的别名。

  • 设计目的
    提供进程 ID 的抽象表示,屏蔽不同操作系统(如 Linux/Windows)或硬件架构(32/64 位)的底层差异,增强代码可移植性。例如:

    • Linux 使用 pid_t 表示 PID,而 Windows 使用 HANDLE
    • 直接使用 int 可能导致平台兼容性问题。

进程id示例:

我们修改一下之前的代码

运行结果:

ltx@hcss-ecs-d90d:~/lesson2$ make
gcc -o myprocess myprocess.c
ltx@hcss-ecs-d90d:~/lesson2$ ./myprocess
我是一个进程!我的pid:453288
我是一个进程!我的pid:453288
我是一个进程!我的pid:453288
我是一个进程!我的pid:453288
我是一个进程!我的pid:453288
我是一个进程!我的pid:453288
我是一个进程!我的pid:453288
我是一个进程!我的pid:453288
我是一个进程!我的pid:453288

...

运行后我们可以发现可执行程序myprocess的pid是453288,我们还可以来验证一下

使用ps指令来查看:

ps ajx | head -1 && ps ajx | grep myprocess | grep -v grep
  • ps ajx

    • ps 是进程查看命令,用于显示当前系统中的进程快照(非动态更新)。
    • 选项 ajx 指定输出格式:a 显示所有用户进程,j 以作业控制格式显示,x 包括无终端的进程(如后台进程)。
    • 示例输出:包含 PID(进程ID)、PPID(父进程ID)、COMMAND(命令名称)等列。
  • | head -1

    • | 是管道符,将前一个命令的输出作为后一个命令的输入。
    • head -1 只保留输出的第一行,即进程信息的表头(如 PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND)。
    • 作用:确保后续进程信息有清晰的列名,便于阅读。
  • &&

    • 逻辑与运算符,表示只有前一个命令(ps ajx | head -1)成功执行(返回状态码 0)时,才执行后续命令。
    • 这里用于分隔两个独立操作:先显示表头,再显示进程详情。
  • ps ajx | grep myprocess

    • 再次运行 ps ajx 获取所有进程信息。
    • grep myprocess 过滤出包含关键字 "myprocess" 的行(通常是目标进程的命令名称)。
    • 问题grep 命令自身在运行时也会被列为进程,且其命令中包含 "myprocess",因此会被错误地包含在结果中(例如输出 grep --color=auto myprocess)。
  • | grep -v grep

    • grep -v 表示反向过滤,排除包含指定关键字 "grep" 的行。
    • 作用:移除 grep myprocess 自身产生的进程条目,避免干扰。例如,如果未加此部分,输出会多出一行 grep --color=auto myprocess

如果我们想杀掉这个进程可以使用快捷键CTRL + c(左边的Shell),或者在右边的Shell中使用

kill -9 [想要杀掉的pid]

至于kill这个指令的是如何杀掉这个进程的,我们在后面的信号章节也会讲到

父进程id示例:

再次修改一下代码

运行

ltx@hcss-ecs-d90d:~/lesson2$ make
gcc -o myprocess myprocess.c
ltx@hcss-ecs-d90d:~/lesson2$ ./myprocess
我是一个进程!我的pid:451964,我的父进程id:450425
我是一个进程!我的pid:451964,我的父进程id:450425
我是一个进程!我的pid:451964,我的父进程id:450425
我是一个进程!我的pid:451964,我的父进程id:450425
我是一个进程!我的pid:451964,我的父进程id:450425
我是一个进程!我的pid:451964,我的父进程id:450425
我是一个进程!我的pid:451964,我的父进程id:450425
我是一个进程!我的pid:451964,我的父进程id:450425
...

运行结果可看到进程pid为451964,父进程ppid为450425

同样也可以在验证一下

这里我们可以看到怎么这次的父进程ppid和上次的父进程ppid一样,都是450425.

我们可以多次运行看一下

父进程id一直不变,这是什么情况呢?

我们也可以用ps来查一下

我们可以看到原来我们的父进程是bash

因为 你每次都是在同一个交互式 Bash 会话里手动启动程序,所以:

  • Bash 是 Linux 默认的命令行解释器(Shell),用户通过终端输入命令时,Bash 会创建子进程来执行这些命令

  • 父进程就是当前这个 Bash 进程

  • Bash 进程的 PID 在你退出或关闭终端之前不会改变

  • 于是你看到的 PPID 始终就是 那个 Bash 的 PID-bashbash)。

换句话说,只要你不关掉这个终端(或显式 exit 掉这个 Bash),它就是所有你手动启动命令的父进程,PPID 自然看起来“不变”。


1.5 fork() 机制深度解析

同样我们可以使用man手册来查一下

关键特性

  1. 一次调用,两次返回

    • 父进程返回子进程PID(>0)

    • 子进程返回0

    • 失败返回-1(如进程数超限)

  2. 写时拷贝(Copy-On-Write)

    • 初始状态:父子进程共享同一物理内存。

    • 修改触发:当任一进程尝试写入数据时,内核为该进程复制新内存页。

我们先来修改一下代码,浅尝一下fork

运行结果:

fork之后的代码被执行了两次,why?

fork: 如何呢?又能怎?

核心原理:一次调用,两次返回

当程序执行到 fork() 系统调用时,操作系统会创建一个与原进程(父进程)几乎完全相同的副本(子进程)。这个副本包括:

  1. 代码段的复制(共享只读)

  2. 数据段和堆栈的复制(写时拷贝)

  3. 程序计数器(PC)位置 - 指向 fork() 之后的下一条指令

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

int main() {
    printf("父进程开始运行,pid:%d\n", getpid());  // 步骤1:父进程执行
    fork();  // 步骤2:分水岭!
    // ↓ 步骤3:此处开始有两个独立的执行流
    printf("进程开始运行,pid:%d\n", getpid());  // 步骤4:父子进程各执行一次
    return 0;
}

执行流程:

关键机制解析:

  1. 写时拷贝 (Copy-On-Write)

    • 子进程创建时不立即复制物理内存

    • 父子共享相同物理内存页(标记为只读)

    • 当任一进程尝试写入内存时,触发缺页异常

    • 内核再为该进程复制新的内存页

  2. 程序计数器继承

    • 子进程创建时复制父进程的CPU寄存器状态

    • 包括指向 fork() 后下一条指令的EIP寄存器

    • 因此子进程从 fork() 返回处开始执行

  3. 返回值差异

    返回值 含义 执行进程
    >0 子进程PID 父进程
    0 成功创建标志 子进程
    -1 创建失败 父进程

要是我们想让父子进程执行不同的代码逻辑,应该怎么做呢

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

int main()
{
    printf("父进程开始运行,pid:%d\n", getpid());
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        return 1;
    }
    else if(id == 0)
    {
        // child
        while(1)
        {
            printf("我是一个子进程!我的pid:%d,我的父进程id:%d\n", getpid(), getppid());
            sleep(1);             
        }
    }
    else
    {
        // father
        while(1)
        {
            printf("我是一个父进程!我的pid:%d,我的父进程id:%d\n", getpid(), getppid());
            sleep(1);             
        }
    }
    //printf("进程开始运行,pid:%d\n", getpid());
    

    
    //while(1)
    //{
    //    printf("我是一个进程!我的pid:%d\n", getpid());
    //    printf("我是一个进程!我的pid:%d,我的父进程id:%d\n", getpid(), getppid());
    //    sleep(1);
    //}
    return 0;
}

修改完代码我们来运行一下

ltx@hcss-ecs-d90d:~/lesson2$ make
gcc -o myprocess myprocess.c
ltx@hcss-ecs-d90d:~/lesson2$ ./myprocess
父进程开始运行,pid:457987
我是一个父进程!我的pid:457987,我的父进程id:450425
我是一个子进程!我的pid:457988,我的父进程id:457987
我是一个父进程!我的pid:457987,我的父进程id:450425
我是一个子进程!我的pid:457988,我的父进程id:457987
我是一个父进程!我的pid:457987,我的父进程id:450425
我是一个子进程!我的pid:457988,我的父进程id:457987
我是一个父进程!我的pid:457987,我的父进程id:450425
我是一个子进程!我的pid:457988,我的父进程id:457987
我是一个父进程!我的pid:457987,我的父进程id:450425
我是一个子进程!我的pid:457988,我的父进程id:457987
我是一个父进程!我的pid:457987,我的父进程id:450425
我是一个子进程!我的pid:457988,我的父进程id:457987
我是一个父进程!我的pid:457987,我的父进程id:450425

...

运行可以看到父进程myprocess可执行程序id为457987,父进程的父进程bash的id为450425,父进程的子进程id为457988。

1. 为什么fork要给父子进程返回不同的值?

这是为了在代码中区分父子进程的执行路径,让程序员能编写不同的逻辑分支:

设计考量

  • 父进程需要知道子进程ID:用于后续管理(等待、发送信号等)

  • 子进程需要明确自身身份:避免递归创建进程

  • 错误处理统一:只有父进程能处理fork失败

类比:就像双胞胎出生时获得不同的名字,虽然基因相同但身份不同

2. 为什么fork会"返回两次"?

实际上不是函数返回两次,而是创建了两个独立的执行流

关键机制

  1. 调用fork时,内核复制父进程的:

    • 寄存器状态(包括程序计数器PC)

    • 页表(通过写时拷贝)

    • 文件描述符表

  2. 返回用户空间前,内核修改:

    • 父进程的EAX寄存器 = 子进程PID

    • 子进程的EAX寄存器 = 0

  3. 两个进程从相同的代码位置继续执行:

    • 父进程:从fork()调用后继续

    • 子进程:"诞生"后的第一条指令就是fork()之后的代码

3. 为什么同一个变量既等于0又大于0?

核心原理:两个进程拥有独立的地址空间

int ret = fork();  // 这行代码在两个进程中都有!

// 内存布局示意:
// 父进程内存空间:ret_addr = 0x1000, 值=457988(子进程PID)
// 子进程内存空间:ret_addr = 0x1000, 值=0

执行过程

时间线        父进程(PID=457987)                子进程(PID=457988)
---------------------------------------------------------------
 T1         执行 fork() 系统调用
 T2             |                            内核创建子进程
 T3             |                            设置父进程返回值=457988
 T4             |                            设置子进程返回值=0
 T5         从fork返回 ↓
 T6         ret = 457988 (>0)                从"诞生点"开始执行 ↓
 T7         执行 else 分支                    ret = 0 (==0)
 T8         printf("Parent...")              执行 else if 分支
 T9                                          printf("Child...")

注意:ret 不是同一个物理内存位置!父子进程有各自独立的变量副本。

技术本质:写时拷贝(COW)的作用

int global = 100;  // 全局变量

pid_t pid = fork();

if (pid == 0) {
    global = 200;  // 子进程修改
} else {
    sleep(1);
    printf("%d", global); // 父进程仍输出100
}

内存变化

  1. fork时:父子共享同一物理内存页(标记为只读)

  2. 子进程写global:触发页错误

  3. 内核复制该内存页给子进程

  4. 子进程在新页上修改值

  5. 父进程仍访问原内存页

总结要点

  1. 进程是资源容器:通过task_struct实现资源隔离与调度。

  2. fork()是进程分身术:通过写时拷贝高效复制,双返回值区分父子。


网站公告

今日签到

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