Linux 进程概念 进程状态 fock函数讲解

[复制链接]
发表于 2026-10-4 18:20:08 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×

linux 下「进程」是理解一切服务端程序的起点:一个程序跑起来就是进程,fork 一次就多一个。本文把三件事串起来讲清楚——进程怎么标识自己(PID/PPID)、怎么用 fork 造子进程、以及进程在运行队列、等待队列之间的状态流转。偏基础,但每次换工作被问到进程状态,答不上 R/S/D 都很吃亏。

一、进程靠什么标识:PID 与 PPID

内核给每个进程一个编号,叫做 PID(Process ID)。拿到自己 PID 的系统调用是 getpid():

#include <unistd.h>
pid_t getpid(void);

对应的父进程编号叫 PPID,用 getppid() 取:

#include <unistd.h>
pid_t getppid(void);  // 父进程的 PID

要注意两个特性:PID 每次运行都会变(进程退出后编号被回收再分配给别人),PPID 在一次登录会话里相对稳定——你在终端里跑命令,父进程基本就是那个登录 shell。想直接看可以用 ps:

ps -ef | head
echo $$

$$ 打印当前 shell 自己的 PID,$PPID 打印它的父进程,配合 echo $? 看上一条命令的退出码,这几个变量在写脚本时天天用。

二、fork:一次调用,两个返回值

fork 的英文就是「分叉」。调用一次,原有那条执行流一分为二:父进程继续往下,子进程也从同一下一个位置往下跑,只是此后继续执行的代码是两份。

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

int main()
{
    printf("before: only one line\n");
    fork();
    printf("after: only one line\n");  // fork 之后的代码会被执行两次
    return 0;
}

运行结果是 before 出现一次、after 出现两次——这就是「fork 之前一个执行流、fork 之后两个进程」最直观的证据。两个进程的返回值分工明确:

  • 子进程里 fork() 返回 0(因为是它创建的,没有子子进程要标识);
  • 父进程里 fork() 返回新建子进程的 PID;
  • 失败时返回 -1。

之所以要返回两个值,本质是为了让两条执行流跑不同的代码块:父进程拿到子进程 PID,后续才能精确地「控制某一个子进程」(等待、杀掉、信号通知都靠这个句柄)。

一个标准的多进程骨架长这样:

pid_t id = fork();
if (id < 0) {
    perror("fork");          // 失败
} else if (id == 0) {
    while (1) {              // 子进程:干自己的活
        printf("我是子进程, pid=%d, ppid=%d\n", getpid(), getppid());
        sleep(1);
    }
} else {
    while (1) {              // 父进程:也可以继续干别的
        printf("我是父进程, pid=%d, ppid=%d\n", getpid(), getppid());
        sleep(1);
    }
}

编译运行:gcc fork_demo.c -o fork_demo && ./fork_demo,再用 ps -ef | grep fork_demo 看父子关系。实际上谁先被调度是不确定的,由调度器决定,这也是多进程程序里「时序不确定」bug 的根源。

三、fork 之后发生了什么:独立性与写时拷贝

创建子进程内核大致做四件事:

  1. 新建一个子进程的 PCB(Linux 里叫 task_struct);
  2. 填充内容——关键是让子进程的 PCB 继承父进程的大部分字段,包括指向同一份代码的指针;
  3. 让父子进程指向同一份代码;
  4. 各自有独立的 task_struct,都能被调度上 CPU。

那为什么父进程看到 id>0、子进程看到 id==0,明明是同一个变量 id?关键在于进程具有独立性:父子绝不能共享同一份可写数据,否则一方改了另一方就跟着变。

做法是数据层面的写时拷贝(copy-on-write):一开始数据区被标记为「只读、共享」,谁都不许改;只有当某一方真的要写时,内核才给它复制一份新空间、改页表映射,从此各改各的。代码共享不影响独立性,因为代码本身不可写。

顺带解释「一个函数怎么返回两次」:return 语句本身也是代码,父子共享,于是父进程执行一次 return、子进程也执行一次 return,同一个 return id; 就返回了两个不同的值。

四、进程状态:R、阻塞、挂起

4.1 运行态(R)

内核用双链表维护进程,进程控制块里记着首尾指针,凑成一个运行队列。进程排进队列、等 CPU 调度,这种状态叫运行态(R),也就是「我已经准备好了,随时能上 CPU」。

进程很多而 CPU 只有几个,所以必然竞争。每个进程分到一个时间片(典型 10ms 量级),用完就被扒下来重新排到队尾,让下一个上——这个「放上去、拿下来」的动作叫进程切换。宏观上在一个时间段里所有进程的代码都被推进过,就是并发执行。

命令看状态最直接:

ps -eo pid,ppid,stat,comm
top

STAT 列里 R 就是运行/可运行,S 是sleeping(可中断睡眠),D 是 uninterruptible sleep,Z 是僵尸。

4.2 阻塞态

进程要读键盘数据,你就是不输入,它只能干等——此时它已经不在运行队列了,而是挂到对应设备的等待队列上去。这种「等某个特定资源就绪」的状态叫阻塞态。

本质还是那条管理原则:先描述、再组织。内核用一个结构体描述每个设备,再用队列把等待该设备的进程串起来。

4.3 挂起态

进程在阻塞态等资源时,如果系统内存吃紧,内核为了保证整体可用,会把进程的常驻代码数据写到磁盘上腾空间——这叫换出,此时进程处于阻塞挂起;资源到位后再搬回内存,叫换入。

需要注意的是运行队列和等待队列都在内存里,只是挂载点不同;而「主动运行一个进程」看起来是瞬间完成,实际大概率也经历了排队,只是 CPU 太快,人感觉不到。

五、常见追问速答

  • 为什么要创建子进程?为了父、子执行不同的代码块,所以 fork 必须给出区分方式(返回值不同)。
  • 父进程为什么拿到的是子进程 PID?因为父进程要能明确控制某一个子进程。
  • 第一次循环用 if-else 还是 switch?多进程场景几乎是 if-else 骨架(0 / >0 / <0 三分支),因为分支语义天然是「身份判断」。
  • 子进程 exit 后父进程不管会怎样?父进程没 wait,子进程就变僵尸(Z),PCB 还占着,所以服务端程序里 wait/waitpid 必须成对出现。

六、小结

这一块记住四句话就够:PID 会变、PPID 是本次登录的父 shell;fork 一次返回两个值,用来区分父子执行流;父子代码共享、数据写时拷贝,独立性靠页表隔离;进程在 R 队列与设备等待队列之间流转,内存不足还会被换出到磁盘。落到实操,ps -eo pid,ppid,stat,comm 这一条命令能把你今天讲的所有状态都看明白。

qidao123.com企服评测-应用市场 编辑提示

回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表