当前位置:首页>Linux>一次搞懂 Linux 进程管理:从创建到进程间通信

一次搞懂 Linux 进程管理:从创建到进程间通信

  • 2026-09-10 12:04:26
一次搞懂 Linux 进程管理:从创建到进程间通信
你好!今天聊聊 Linux 进程管理。

进程管理是操作系统最核心的功能之一,理解起来不难,但很多新手看完教材还是一知半解。这篇文章把核心概念、系统调用、实战案例串起来,看完就能上手。


一、进程管理

1.1 什么是进程?

进程就是正在运行的程序的实例。

进程就像厨师在厨房里做菜,每个厨师有自己独立的灶台(CPU)、食材(内存)、工具(文件描述符),互不干扰。

在 Linux 里,每个进程拥有:

  • 独立的内存空间
  • 独立的文件描述符
  • 独立的系统资源

它是操作系统进行资源分配和调度的基本单位。

1.2 进程的生命周期

一个进程从生到死,一共经历五个阶段:

状态
说明
类比
创建
进程被创建出来,比如调用 fork()
招聘厨师,办入职手续
就绪
拿到了除 CPU 之外的所有资源,等待 CPU
厨师准备好一切,就等灶台空出来
运行
正在 CPU 上执行指令
厨师在灶台炒菜
阻塞/等待
等待某个事件发生,不占用 CPU
厨师等食材送到,去旁边休息
终止
执行完或被杀掉,释放所有资源
厨师下班,收拾灶台离开

1.3 核心系统调用

fork() - 创建进程

Linux 里最有意思的函数:调用一次,返回两次。

#include<unistd.h>pid_tfork(void);

返回值规则:

  • > 0:父进程中,返回子进程的 PID
  • = 0:子进程中,返回 0

一次调用分出两个进程,父进程需要知道子进程是谁,子进程只需要知道自己是新的。

就像复制一份文件,原件得到副本的名字(PID),副本知道自己是个新的副本。

execvp() - 替换进程映像

intexecvp(constchar *file, char *const argv[]);

用新程序替换当前进程的代码段、数据段和堆栈。

调用成功不会返回——原来的代码已经被替换了。只有失败才返回 -1。

厨师突然换了个菜谱,继续做菜,但做的是完全不同的菜。

wait() / waitpid() - 等待子进程

父进程必须调用这个函数回收子进程资源,不然会产生僵尸进程。

#include<sys/wait.h>pid_twait(int *wstatus);pid_twaitpid(pid_t pid, int *wstatus, int options);
  • wait():阻塞父进程,直到任意子进程结束
  • waitpid():等待特定子进程,可用 WNOHANG 非阻塞轮询

wstatus 存储子进程退出状态,可以用宏解析:

  • WIFEXITED(status):检查是否正常退出
  • WEXITSTATUS(status):获取退出码

类比:家长等孩子放学回家,还得问孩子今天怎么样(退出状态)。

1.4 特殊情况:僵尸进程与孤儿进程

这两个概念经常搞混。

僵尸进程

子进程终止了,父进程还没调用 wait() 读取退出状态。

子进程不运行了,但 PCB 还占着内核内存。如果僵尸进程太多,系统可能无法创建新进程。

类比:孩子不做饭了但还没收拾灶台,灶台一直占着,别人没法用。

解决方法:父进程调用 wait() 或 waitpid() 回收子进程。

孤儿进程

父进程比子进程先退出,子进程变成孤儿。

在 Linux 中,孤儿进程会被 init 进程(PID = 1)收养。init 会自动调用 wait() 回收,孤儿进程一般没危害。

家长下班走了,孩子被食堂经理(init)收养,经理会帮孩子收拾灶台。

1.5 实战:实现简易 Shell

写个极简 Shell 把知识点串起来。

流程:父进程提示输入 → fork() 创建子进程 → 子进程 execvp() 执行命令 → 父进程 waitpid() 等待结束。

#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<sys/wait.h>#define MAX_CMD_LEN 1024intmain(){char input[MAX_CMD_LEN];char *args[64];pid_t pid;int status;printf("=== 简易 Shell 启动 ===\n");printf("输入 'exit' 退出\n\n");while (1) {printf("myshell> ");if (fgets(input, MAX_CMD_LEN, stdin) == NULL) {break;        }        input[strcspn(input, "\n")] = 0;if (strcmp(input, "exit") == 0) {break;        }int i = 0;        args[i] = strtok(input, " ");while (args[i] != NULL) {            i++;            args[i] = strtok(NULL, " ");        }        args[i] = NULL;if (args[0] == NULL) continue;        pid = fork();if (pid < 0) {            perror("fork failed");continue;        }elseif (pid == 0) {            execvp(args[0], args);            perror("execvp failed");exit(1);        }else {            waitpid(pid, &status, 0);printf("\n命令退出,退出码:%d\n\n", WEXITSTATUS(status));        }    }printf("Bye!\n");return0;}

编译运行

gcc shell.c -o myshell./myshell

二、进程间通信

每个进程有独立地址空间,无法直接访问其他进程内存。操作系统提供 IPC 机制让进程交换数据。

好比两家独立的小餐馆,不能直接用对方的食材,需要专门的传递方式。

常用方式:

  • 管道(匿名、有名)
  • 信号处理
  • 消息队列
  • 共享内存

2.1 管道

匿名管道

最简单的 IPC 方式,Shell 里的 cmd1 | cmd2 就是管道。

就像两家餐馆之间有个传菜窗口,只够一个人用,而且有方向性。

特点:

  • 半双工:同一时间只能单向流动
  • 只能用于有血缘关系的进程
  • 生命周期随进程

创建接口:

#include<unistd.h>intpipe(int pipefd[2]);
  • pipefd[0]:读端
  • pipefd[1]:写端
  • 成功返回 0,失败返回 -1

父进程写、子进程读

#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<sys/wait.h>intmain(){int pipefd[2];pid_t pid;char buf[1024];constchar *msg = "Hello from parent!";// 1. 创建管道,传菜窗口建好了if (pipe(pipefd) < 0) {        perror("pipe failed");exit(1);    }// 2. 创建子进程    pid = fork();if (pid < 0) {        perror("fork failed");exit(1);    }if (pid > 0) {// 父进程:写数据        close(pipefd[0]);  // 关闭不用的读端,像不用传菜窗口接收        write(pipefd[1], msg, strlen(msg));        close(pipefd[1]);  // 写完关闭,传完菜关窗户        wait(NULL);        // 等子进程读完    } else {// 子进程:读数据        close(pipefd[1]);  // 关闭不用的写端ssize_t n = read(pipefd[0], buf, sizeof(buf)-1);if (n > 0) {            buf[n] = '\0';printf("Child: %s\n", buf);        }        close(pipefd[0]);exit(0);    }return0;}

关键点:

  • 一定要关闭不用的那一端,不然会导致死锁
  • 写端关闭后 read 返回 0(读到 EOF)
  • 读端关闭后写触发 SIGPIPE 信号

有名管道(FIFO)

用于非亲缘进程通信,是文件系统中的特殊文件。

就像在公共区域设个公用信箱,谁都可以往里面塞纸条。

创建:

#include<sys/stat.h>intmkfifo(constchar *pathname, mode_t mode);

命令行创建:

mkfifo myfifo

写进程:

#include<stdio.h>#include<stdlib.h>#include<string.h>#include<fcntl.h>#include<unistd.h>#include<sys/stat.h>intmain(){constchar *fifo_path = "/tmp/myfifo";// 创建信箱    mkfifo(fifo_path, 0666);// 打开信箱写(会阻塞直到有人来读)int fd = open(fifo_path, O_WRONLY);if (fd < 0) {        perror("open failed");exit(1);    }char *messages[] = {"Hello", "Second", "Bye"};for (int i = 0; i < 3; i++) {        write(fd, messages[i], strlen(messages[i]));        sleep(1);    }    close(fd);    unlink(fifo_path);  // 删除信箱return0;}

读进程:

#include<stdio.h>#include<stdlib.h>#include<fcntl.h>#include<unistd.h>#include<sys/stat.h>intmain(){constchar *fifo_path = "/tmp/myfifo";// 打开信箱读(会阻塞直到有人来写)int fd = open(fifo_path, O_RDONLY);if (fd < 0) {        perror("open failed");exit(1);    }char buf[1024];ssize_t n;while ((n = read(fd, buf, sizeof(buf)-1)) > 0) {        buf[n] = '\0';printf("Reader: %s\n", buf);    }    close(fd);return0;}

匿名管道 vs 有名管道

特性
匿名管道
有名管道
通信范围
亲缘进程
任意进程
存在形式
内核中
文件系统特殊文件
创建方式
pipe()
mkfifo()

2.2 信号处理

常见信号

信号名
编号
含义
类比
SIGINT(2)
2
Ctrl+C 请求终止
老板拍桌子喊停
SIGSEGV(11)
11
段错误
厨师摔了盘子
SIGCHLD(17)
17
子进程状态改变
孩子喊"好了"
SIGKILL(9)
9
强制杀死(不可忽略)
直接开除
SIGTERM(15)
15
请求终止(可捕获)
礼貌地请下班

信号处理接口

#include<signal.h>sighandler_tsignal(int signum, sighandler_t handler);

handler 可选值:

  • SIG_DFL:默认处理
  • SIG_IGN:忽略
  • 自定义函数指针

捕获 SIGINT 信号

#include<stdio.h>#include<stdlib.h>#include<signal.h>#include<unistd.h>voidhandler(int sig){printf("\n收到 SIGINT(%d),不退出\n", sig);printf("用 SIGKILL 才能杀掉我\n");}intmain(){// 注册 SIGINT 的处理函数    signal(SIGINT, handler);printf("PID: %d\n", getpid());printf("试试 Ctrl+C\n");while (1) {        sleep(1);    }return0;}

推荐使用 sigaction

signal 是老接口,现在推荐更安全的 sigaction:

intsigaction(int signum, const struct sigaction *act,              struct sigaction *oldact);

示例:

structsigactionsa;sa.sa_handler = handler;sigemptyset(&sa.sa_mask);sa.sa_flags = 0;sigaction(SIGINT, &sa, NULL);

2.3 消息队列

消息队列是内核维护的消息链表,每个消息都有明确的边界,不会像管道那样粘在一起。

就像两家餐馆之间有个带编号的小信箱,每封信都独立,不会混在一起。

POSIX 消息队列接口详解

1. mq_open - 打开或创建消息队列

mqd_tmq_open(constchar *name, int oflag, ...);

参数说明:

  • name:消息队列名称,必须以 / 开头,比如 /my_queue
  • oflag:打开标志
    • O_RDONLY:只读
    • O_WRONLY:只写
    • O_RDWR:读写
    • O_CREAT:不存在则创建
    • O_EXCL:配合 O_CREAT,已存在则失败
  • mode_t mode(可选):创建时的权限,如 0666
  • struct mq_attr *attr(可选):创建时的队列属性

返回值: 成功返回消息队列描述符,失败返回 -1

示例:

// 创建或打开消息队列,只写模式mqd_t mq = mq_open("/my_queue", O_WRONLY | O_CREAT, 0666, NULL);

2. mq_send - 发送消息

intmq_send(mqd_t mqdes, constchar *msg_ptr,size_t msg_len, unsignedint msg_prio);

参数说明:

  • mqdes:消息队列描述符(mq_open 返回的)
  • msg_ptr:消息内容指针
  • msg_len:消息长度(字节数)
  • msg_prio:消息优先级(0 最低,数字越大优先级越高)

返回值: 成功返回 0,失败返回 -1

示例:

// 发送一条高优先级消息mq_send(mq, "Hello", 5, 10);

3. mq_receive - 接收消息

ssize_tmq_receive(mqd_t mqdes, char *msg_ptr,size_t msg_len, unsignedint *msg_prio);

参数说明:

  • mqdes:消息队列描述符
  • msg_ptr:接收消息的缓冲区
  • msg_len:缓冲区大小
  • msg_prio:传出参数,接收到的消息优先级(可传 NULL 忽略)

返回值: 成功返回接收到的消息字节数,失败返回 -1

示例:

char buf[1024];unsignedint prio;ssize_t n = mq_receive(mq, buf, sizeof(buf), &prio);

4. mq_close - 关闭消息队列

intmq_close(mqd_t mqdes);

参数说明:

  • mqdes:消息队列描述符

返回值: 成功返回 0,失败返回 -1

5. mq_unlink - 删除消息队列

intmq_unlink(constchar *name);

参数说明:

  • name:消息队列名称

返回值: 成功返回 0,失败返回 -1

发送端完整代码

#include<stdio.h>#include<stdlib.h>#include<string.h>#include<mqueue.h>#include<unistd.h>#define QUEUE_NAME "/my_queue"#define MAX_SIZE 1024intmain(){// 1. 创建或打开消息队列(只写模式)// O_CREAT:不存在则创建// O_WRONLY:只写// 0666:读写权限// NULL:使用默认队列属性mqd_t mq = mq_open(QUEUE_NAME, O_WRONLY | O_CREAT, 0666, NULL);if (mq == (mqd_t)-1) {        perror("mq_open failed");exit(1);    }char *messages[] = {"First", "Second", "Bye"};for (int i = 0; i < 3; i++) {// 发送消息:优先级 0// 注意:mq_send 会阻塞直到队列有空间if (mq_send(mq, messages[i], strlen(messages[i]), 0) < 0) {            perror("mq_send failed");        }        sleep(1);    }// 关闭消息队列描述符    mq_close(mq);return0;}

注意:

  • 消息名称必须以 / 开头
  • mq_send 会阻塞如果队列满了
  • 每个消息是独立的,不用担心粘包

接收端完整代码

#include<stdio.h>#include<stdlib.h>#include<mqueue.h>#define QUEUE_NAME "/my_queue"#define MAX_SIZE 1024intmain(){// 1. 创建或打开消息队列(只读模式)mqd_t mq = mq_open(QUEUE_NAME, O_RDONLY | O_CREAT, 0666, NULL);if (mq == (mqd_t)-1) {        perror("mq_open failed");exit(1);    }char buf[MAX_SIZE];for (int i = 0; i < 3; i++) {// 接收消息// 注意:mq_receive 会阻塞直到有消息// NULL 表示不关心优先级ssize_t n = mq_receive(mq, buf, MAX_SIZE, NULL);if (n > 0) {            buf[n] = '\0';  // 手动添加字符串结束符printf("Received: %s\n", buf);        }    }    mq_close(mq);// 删除消息队列(最后一个使用者负责)    mq_unlink(QUEUE_NAME);return0;}

避坑指南:

  1. 缓冲区大小:接收缓冲区必须大于等于消息最大长度,否则 mq_receive 返回 EMSGSIZE 错误
  2. 字符串结束符:mq_receive 返回的是字节数,不会自动加 \0,必须手动添加
  3. 删除时机:mq_unlink 应该由最后一个使用者调用,否则其他进程可能找不到队列
  4. 非阻塞模式:如果不想阻塞,需要在 mq_open 时设置 mq_attr 的 mq_flags 为 O_NONBLOCK

编译需要链接 pthread:

gcc sender.c -o sender -lpthreadgcc receiver.c -o receiver -lpthread

2.4 共享内存

共享内存让多个进程直接访问同一块物理内存,不需要任何数据拷贝。

就像两家餐馆共用一个大仓库,都直接去仓库取东西,不用传来传去,速度最快。

必须自己做同步,通常搭配信号量使用。

POSIX 共享内存接口详解

1. shm_open - 打开或创建共享内存对象

intshm_open(constchar *name, int oflag, mode_t mode);

参数说明:

  • name:共享内存对象名称,必须以 / 开头,比如 /my_shm
  • oflag:打开标志
    • O_RDONLY:只读
    • O_RDWR:读写
    • O_CREAT:不存在则创建
    • O_EXCL:配合 O_CREAT,已存在则失败
    • O_TRUNC:截断到 0 长度
  • mode:创建时的权限,如 0666

返回值: 成功返回文件描述符,失败返回 -1

示例:

int fd = shm_open("/my_shm", O_RDWR | O_CREAT, 0666);

2. ftruncate - 设置共享内存大小

intftruncate(int fd, off_t length);

参数说明:

  • fd:文件描述符(shm_open 返回的)
  • length:共享内存大小(字节数)

返回值: 成功返回 0,失败返回 -1

示例:

ftruncate(fd, 4096);  // 设置为 4KB

3. mmap - 映射共享内存到进程地址空间

void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);

参数说明:

  • addr:建议的映射地址,通常传 NULL 让系统选择
  • length:要映射的字节数
  • prot:保护标志
    • PROT_READ:可读
    • PROT_WRITE:可写
    • PROT_EXEC:可执行
  • flags:映射标志
    • MAP_SHARED:共享映射,写入会反映到其他进程
    • MAP_PRIVATE:私有映射,写入不会影响其他进程
  • fd:文件描述符
  • offset:文件偏移量,通常为 0

返回值: 成功返回映射地址,失败返回 MAP_FAILED

示例:

void *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

4. munmap - 取消映射

intmunmap(void *addr, size_t length);

参数说明:

  • addr:映射地址(mmap 返回的)
  • length:映射的字节数

返回值: 成功返回 0,失败返回 -1

5. close - 关闭共享内存描述符

intclose(int fd);

参数说明:

  • fd:文件描述符

返回值: 成功返回 0,失败返回 -1

6. shm_unlink - 删除共享内存对象

intshm_unlink(constchar *name);

参数说明:

  • name:共享内存对象名称

返回值: 成功返回 0,失败返回 -1

写端完整代码

#include<stdio.h>#include<stdlib.h>#include<string.h>#include<sys/mman.h>#include<fcntl.h>#include<unistd.h>#define SHM_NAME "/my_shm"#define SHM_SIZE 4096intmain(){// 1. 创建或打开共享内存对象// O_RDWR:需要读写权限// O_CREAT:不存在则创建// 0666:权限(可读可写)int fd = shm_open(SHM_NAME, O_RDWR | O_CREAT, 0666);if (fd < 0) {        perror("shm_open failed");exit(1);    }// 2. 设置共享内存大小(必须做!)// 新创建的共享内存大小为 0,必须用 ftruncate 设置if (ftruncate(fd, SHM_SIZE) < 0) {        perror("ftruncate failed");        close(fd);exit(1);    }// 3. 映射共享内存到进程地址空间// PROT_READ | PROT_WRITE:可读可写// MAP_SHARED:共享映射,写入会反映到其他进程void *ptr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (ptr == MAP_FAILED) {        perror("mmap failed");        close(fd);exit(1);    }// 4. 写入数据constchar *msg = "Hello from shared memory writer!";// 安全拷贝,防止溢出strncpy((char *)ptr, msg, SHM_SIZE - 1);    ((char *)ptr)[SHM_SIZE - 1] = '\0';  // 确保字符串结束printf("Written: %s\n", (char *)ptr);// 5. 取消映射(不关闭文件描述符,让读端能继续用)    munmap(ptr, SHM_SIZE);// 6. 关闭文件描述符    close(fd);return0;}

注意:

  • ftruncate 必须调用,否则共享内存大小为 0,无法写入
  • 用 strncpy 而不是 strcpy,防止缓冲区溢出
  • 手动添加字符串结束符

读端完整代码

#include<stdio.h>#include<stdlib.h>#include<sys/mman.h>#include<fcntl.h>#include<unistd.h>#define SHM_NAME "/my_shm"#define SHM_SIZE 4096intmain(){// 1. 打开共享内存对象int fd = shm_open(SHM_NAME, O_RDWR, 0);if (fd < 0) {        perror("shm_open failed");exit(1);    }// 2. 映射共享内存到进程地址空间void *ptr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (ptr == MAP_FAILED) {        perror("mmap failed");        close(fd);exit(1);    }// 3. 读取数据printf("Read from shared memory: %s\n", (char *)ptr);// 4. 取消映射    munmap(ptr, SHM_SIZE);// 5. 关闭文件描述符    close(fd);// 6. 删除共享内存对象(最后一个使用者负责)    shm_unlink(SHM_NAME);return0;}

避坑指南:

  1. ftruncate 必不可少:新创建的共享内存大小默认为 0,必须用 ftruncate 设置大小,否则 mmap 会失败
  2. 内存同步:虽然 MAP_SHARED 会自动同步,但在某些情况下可能需要调用 msync 手动同步
  3. 删除时机:shm_unlink 只删除名称,不会立即释放内存,只有所有描述符都关闭了才会真正释放
  4. 权限问题:shm_open 的 mode 参数决定了谁能打开共享内存,常用 0666 让所有用户可读写
  5. 映射大小:mmap 的 length 必须小于等于 ftruncate 设置的大小

为什么共享内存最快?

  • 管道、消息队列需要两次拷贝:用户 → 内核 → 用户
  • 共享内存只需要建立映射,之后直接读写

注意事项

  • 不提供同步,需要自己用信号量或互斥锁保护
  • 不删除会一直存在内核中,直到系统重启或主动删除

2.5 IPC 方式对比

IPC 方式
速度
适用场景
优点
缺点
管道
较快
父子进程流式数据
简单易用,自动清理
半双工,亲缘进程
FIFO
较快
任意进程通信
可文件访问
半双工
信号
-
事件通知
异步,简单
信息量少
消息队列
中等
多进程消息交换
有边界,支持类型
需内核拷贝
共享内存
最快
大数据量交换
零拷贝,高性能
需自做同步

三、补充:PCB(进程控制块)

PCB 是内核用于描述和管理进程的数据结构。

一句话:PCB 是进程存在的唯一标志。

操作系统通过 PCB 感知、管理和控制进程。可以把 PCB 想象成:

  • 身份证:唯一的 PID
  • 档案袋:记录进程运行所需的所有信息

3.1 PCB 内容

进程标识信息

  • 进程 ID (PID):唯一数字标识符
  • 用户 ID (UID):进程所属用户
  • 父进程 ID (PPID):创建者的 PID

进程状态信息

  • 进程状态:运行、就绪、阻塞
  • 程序计数器(PC):下一条指令地址
  • CPU 寄存器:保存/恢复执行现场

资源管理信息

  • 内存管理信息:进程内存布局、页表
  • I/O 状态信息:分配的设备、打开的文件

进程控制信息

  • 调度信息:优先级、等待时间、运行时间
  • 进程间通信:信号量、消息队列指针
  • 会计信息:CPU 时间、创建时间

3.2 PCB 和僵尸/孤儿进程

僵尸进程

子进程终止后,代码和数据空间释放,但 PCB 还留在系统里,等待父进程调用 wait() 读取退出状态。

如果父进程不回收,PCB 一直占用内核资源。

孤儿进程

父进程先退出,子进程被 init(PID=1)收养,更新 PCB 中的 PPID。等子进程结束,init 自动回收 PCB。


总结

Linux 进程管理核心内容:

  • 进程管理:概念、生命周期、系统调用、僵尸/孤儿进程、Shell 实战
  • 进程间通信:管道、信号处理、消息队列、共享内存,每种都带完整源码
  • 补充内容:PCB,进程存在的唯一标志

所有源码可直接编译运行。

理解这些,Linux 应用层开发的进程部分就算入门了。


“

💡 觉得有用欢迎点赞收藏,关注不迷路。

最新文章

随机文章