一、进程基础与IPC概述

大家好,我是你们的嵌入式老兵。今天咱们聊聊进程间通信,但别急着上手代码——先得把地基打牢。进程是什么?程序又是什么?这两个问题,我当年刚入行时也傻傻分不清。

1.1 进程与程序的区别

程序,说白了就是硬盘上躺着的一堆指令。你写好的C代码,编译成ELF文件,它就静静待在那里,啥也不干。

进程呢?是程序跑起来之后的“活物”。它有生命,有状态,会生会死。

核心区别一句话:程序是静态的,进程是动态的。程序是菜谱,进程是炒菜的过程。

我在项目中遇到过一件事:同事把程序拷贝了好几份,以为这样就能多任务处理。结果发现所有“副本”其实指向同一个可执行文件,跑起来却是多个进程。嗯,这就是程序与进程的区别——一个程序可以产生多个进程。

1.2 进程的状态转换

进程不是一直跑着的。它有三种基本状态:就绪、运行、阻塞。你想想看,CPU就那么几个核,进程却成百上千,总得排队吧?

  • 就绪态:万事俱备,只欠CPU。进程已经准备好,就等调度器翻牌子。
  • 运行态:正在占用CPU执行指令。注意,单核CPU上同一时刻只能有一个进程在运行。
  • 阻塞态:进程在等某个事件,比如等磁盘I/O、等网络数据、等用户输入。这时候它不消耗CPU。

状态之间怎么转换?就绪→运行(调度器选中它),运行→就绪(时间片用完或被更高优先级进程抢占),运行→阻塞(发起I/O请求),阻塞→就绪(I/O完成)。

我的经验:调试多进程程序时,最头疼的就是死锁。两个进程互相等对方释放资源,双双进入阻塞态,谁也动不了。我曾经花了一整个下午才找到问题——就是一个信号量没释放。

1.3 进程控制块PCB

操作系统怎么管理这么多进程?靠的就是PCB——进程控制块。每个进程都有一个PCB,里面装着它的“户口本”。

PCB里有什么?我列几个关键的:

字段 说明
PID 进程标识符,唯一的数字ID
状态 当前处于就绪、运行还是阻塞
程序计数器 下一条要执行的指令地址
CPU寄存器 上下文切换时保存的寄存器值
内存限制 进程能访问的内存范围
打开文件列表 进程打开了哪些文件

在Linux内核里,PCB就是task_struct结构体。这玩意儿很大,几百个字段。你想想看,操作系统为了管理一个进程,得记多少东西?

1.4 进程标识符PID

每个进程都有一个唯一的PID。你可以用ps命令查看:

$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 10:00 ?        00:00:01 init
root       100     1  0 10:01 ?        00:00:00 sshd
myuser     200   100  0 10:02 pts/0    00:00:00 bash
myuser     300   200  0 10:03 pts/0    00:00:00 ps -ef

PID 0是空闲进程,PID 1是init进程(现在很多系统是systemd)。PID是有限的,最大值可以看/proc/sys/kernel/pid_max。我遇到过PID耗尽的情况——进程创建太多,系统再也fork不出新进程了。那次是在一个压力测试场景下,没控制好并发数。

1.5 进程的创建——fork()

创建进程最核心的系统调用就是fork()。它的行为很特别:调用一次,返回两次。

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

int main() {
    pid_t pid = fork();
    
    if (pid == 0) {
        // 子进程
        printf("我是子进程,PID=%d\n", getpid());
    } else if (pid > 0) {
        // 父进程
        printf("我是父进程,PID=%d,子进程PID=%d\n", getpid(), pid);
    } else {
        // fork失败
        perror("fork");
        return 1;
    }
    
    return 0;
}

fork之后,子进程是父进程的副本。它有自己的地址空间,但内容完全一样。你想想看,这就像细胞分裂——两个一模一样的个体,但从此各自独立。

注意:fork之后,父子进程的执行顺序是不确定的。谁先跑,取决于调度器。别指望父进程一定先执行。

1.6 进程的终止——exit()与_exit()

进程终止有两种方式:正常终止和异常终止。正常终止就是调用exit()_exit(),或者在main函数里return。

exit()_exit()有什么区别?

  • _exit():直接进入内核,立即终止进程。不会清理stdio缓冲区。
  • exit():先调用atexit注册的清理函数,刷新stdio缓冲区,然后再调用_exit()

我建议在子进程里用_exit(),父进程里用exit()。为什么?因为子进程通常不需要清理父进程的资源。我曾经见过一个bug:子进程用exit(),结果把父进程的缓冲区也刷了,导致输出乱掉。

1.7 孤儿进程与僵尸进程

这两个概念,面试常考,实际开发也常踩坑。

孤儿进程:父进程先挂了,子进程还在跑。这时候子进程会被init进程(PID=1)收养。init会负责回收它。说白了,孤儿不可怕,有人管。

僵尸进程:子进程先终止了,但父进程没有调用wait()waitpid()来获取子进程的退出状态。子进程的PCB还留在内核里,成了一个“僵尸”。

避坑指南:我曾经在服务器程序里忘了处理SIGCHLD信号,结果跑了一天后,系统里堆了几千个僵尸进程。最后只能重启。记住:父进程一定要wait()子进程,或者设置SIGCHLD处理函数为SIG_IGN。

// 正确的做法:捕获SIGCHLD信号
#include <signal.h>
#include <sys/wait.h>

void sigchld_handler(int sig) {
    while (waitpid(-1, NULL, WNOHANG) > 0);
}

int main() {
    signal(SIGCHLD, sigchld_handler);
    // ... 创建子进程 ...
}

1.8 进程间通信的概念与分类

进程之间是相互隔离的。每个进程有自己的地址空间,谁也看不见谁的数据。但实际应用中,进程之间需要交换数据、同步动作。这就引出了IPC——进程间通信。

Linux下有哪些IPC方式?我按发展顺序给你捋一捋:

  1. 管道(Pipe):最古老的IPC方式。有亲缘关系的进程才能用。无名管道用于父子进程,有名管道(FIFO)可用于任意进程。
  2. 信号(Signal):异步通信方式。进程收到信号后,会中断当前执行,去处理信号。简单但携带信息少。
  3. System V IPC:包括消息队列、共享内存、信号量。这是AT&T System V引入的,功能强大,但接口有点古老。
  4. POSIX IPC:对System V的改进。接口更简洁,支持更好的实时性。包括POSIX消息队列、POSIX共享内存、POSIX信号量。
  5. 套接字(Socket):不仅能用于同一台机器上的进程通信,还能跨网络。最灵活,但开销也最大。
  6. D-Bus:桌面应用常用的IPC机制,基于消息总线。Qt、GNOME都在用。

你可能会问:这么多方式,我该选哪个?我的建议是:

  • 简单场景用管道
  • 需要大量数据交换用共享内存
  • 需要同步用信号量
  • 跨网络用Socket

1.9 Linux下IPC的发展历程

Linux的IPC机制是逐步完善的。早期Unix只有管道和信号。System V引入了消息队列、共享内存、信号量。POSIX标准又对这些做了改进。

我记得Linux 2.4内核之前,System V IPC的性能还不错。后来随着硬件发展,共享内存成了高性能场景的首选。现在很多数据库、中间件都用共享内存做数据交换。

近几年,Linux还引入了memfd_create()eventfd()等新机制。但说实话,最常用的还是那几样:管道、信号、共享内存、Socket。

我的建议:初学者先掌握管道和信号,这两个最简单。然后学共享内存和信号量,这是高性能场景的基石。Socket最后学,因为它涉及网络编程,内容最多。

知识体系总览

下面这张图,是我自己整理的本章知识结构。你看一眼,心里就有谱了。

第一章:进程基础与IPC概述 进程基础 • 进程 vs 程序:静态 vs 动态 • 三种状态:就绪、运行、阻塞 • PCB(task_struct):进程的户口本 • PID:唯一标识符 • fork():调用一次,返回两次 • exit() vs _exit():清理 vs 直接终止 特殊进程与IPC • 孤儿进程:父死子存,init收养 • 僵尸进程:子死父未wait,PCB残留 • 避坑:必须处理SIGCHLD IPC分类 • 管道(Pipe/FIFO) • 信号(Signal) Linux IPC发展历程 管道+信号 Unix早期 System V IPC 消息队列/共享内存/信号量 POSIX IPC 接口更简洁 Socket 跨网络通信 D-Bus/eventfd 现代机制 核心原则:简单场景用管道,大量数据用共享内存,跨网络用Socket

好了,第一章的内容就到这里。进程是操作系统的核心概念,IPC是进程间协作的桥梁。后面的章节,我们会逐一深入每种IPC机制。到时候,我会带着实际项目中的案例,跟你一起踩坑、填坑。


公众号:蓝海资料掘金营,微信deep3321