熟悉进程虚拟地址空间、进程创建 / 销毁、基础文件 IO、管道 / 共享内存 / 消息队列等进程通信方式、信号处理机制

一、进程虚拟地址空间

虚拟地址空间是现代操作系统的核心设计,也是理解进程隔离、内存管理、fork 写时复制的基础。

1. 核心概念与设计意义

每个进程启动后,操作系统都会为其分配一个独立的、连续的虚拟地址空间,进程只能访问自己的虚拟地址,无法直接操作物理内存。操作系统通过页表将虚拟地址映射到真实的物理内存。

核心价值:

  • 进程隔离:每个进程地址空间独立,一个进程崩溃不会影响其他进程,保障系统稳定性
  • 内存安全:用户态无法直接访问内核空间,非法地址访问会触发段错误
  • 高效利用:通过分页 + 换页机制,虚拟内存可以远大于物理内存,同时多进程可以共享同一块物理内存
  • 简化编程:程序无需关心物理内存分配,统一使用线性虚拟地址开发

2. 地址空间整体布局

以 Linux 系统为例:

  • 32 位系统:总地址空间 4GB,其中 0~3GB 为用户空间,3GB~4GB 为内核空间
  • 64 位系统:仅使用低 48 位地址,低地址为用户空间,高地址为内核空间,中间存在大片不可访问的地址空洞

3. 用户空间核心分段(从低地址到高地址)

表格

分段作用与特性
代码段(.text)只读,存放程序的机器指令、字符串常量。修改此区域会触发段错误
数据段(.data)存放已初始化的全局变量、静态变量,可读可写
BSS 段(.bss)存放未初始化的全局变量、静态变量,程序加载时会被自动清零,不占用磁盘空间
堆(Heap)动态内存分配区,malloc/new 分配的内存位于此区域,从低地址向高地址增长
共享映射区存放动态链接库、mmap 映射的文件 / 共享内存,位于堆和栈之间
栈(Stack)存放局部变量、函数参数、返回地址、寄存器上下文,从高地址向低地址增长,有大小限制(默认 8MB)

4. 分页映射机制

虚拟地址到物理地址的转换以为单位(通常 4KB):

  1. 虚拟地址拆分为「页号 + 页内偏移」
  2. 操作系统通过每个进程独立的页表,将虚拟页号映射为物理页框号
  3. 物理页框号 + 页内偏移 = 最终物理地址

当访问的虚拟页不在物理内存中时,会触发缺页中断,内核将数据从磁盘加载到物理内存,更新页表后继续执行。

5. 写时复制(Copy-On-Write, COW)

这是 fork 创建子进程的核心优化机制:

  • fork 刚完成时,父子进程共享同一块物理内存,页表标记为只读
  • 当任意一方尝试修改内存时,才会触发缺页中断,内核复制一份物理页给修改方,双方各自拥有独立副本
  • 优势:大幅降低 fork 的开销,避免无意义的内存拷贝

二、进程创建与销毁

1. 进程本质与 PCB

进程 = 程序 + 资源分配的最小单位。内核通过 task_struct(进程控制块 PCB) 描述一个进程,记录进程的 PID、状态、虚拟地址空间、文件描述符表、信号、优先级等所有信息。

进程五大基础状态:

  • 就绪态:等待 CPU 调度
  • 运行态:占用 CPU 执行
  • 阻塞态:等待事件(IO、锁、信号),不参与调度
  • 停止态:收到暂停信号,暂停执行
  • 僵尸态:进程已退出,但父进程未回收其资源

2. 进程创建:fork /vfork/clone

(1)fork ():最常用的进程创建

c

运行

#include <unistd.h>
pid_t fork(void);
  • 返回值:父进程返回子进程 PID,子进程返回 0,出错返回 -1
  • 子进程继承内容:虚拟地址空间副本、文件描述符表、信号处理方式、工作目录、用户 ID 等
  • 子进程独有:PID、父进程 ID、运行时间统计、锁等
  • 核心特性:写时复制,刚创建时父子共享物理内存
(2)vfork ():历史遗留接口
  • 子进程直接共享父进程地址空间,不复制页表
  • 会阻塞父进程,直到子进程执行 execexit
  • 现在基本被淘汰,仅用于极端性能敏感且子进程立即 exec 的场景
(3)clone ():底层系统调用
  • 可以精细化控制父子进程共享的资源(地址空间、文件描述符、信号等)
  • Linux 线程库 pthread 底层就是基于 clone 实现的

3. 进程映像替换:exec 函数族

exec 不会创建新进程,而是替换当前进程的代码段、数据段、栈堆等全部资源,执行一个全新的程序,进程 PID 保持不变。

常用变体:

  • execl:参数以列表形式传入,以 NULL 结尾
  • execv:参数以数组形式传入
  • execle:可自定义环境变量
  • execvp:支持从 PATH 环境变量中查找程序

经典模式:fork + exec 是 shell 执行外部命令的标准流程:shell fork 出子进程,子进程调用 exec 加载目标程序,父进程 wait 等待子进程退出。

4. 进程终止方式

正常终止
  1. main 函数 return,等价于调用 exit
  2. 调用标准库 exit():执行用户注册的清理函数,刷新 IO 缓冲区,然后进入内核终止
  3. 调用系统调用 _exit() / _Exit():直接进入内核终止,不做用户态清理
  4. 进程最后一个线程返回
异常终止
  1. 收到终止信号(如 SIGKILL、SIGSEGV)
  2. 调用 abort() 主动异常终止

5. 僵尸进程与孤儿进程

僵尸进程
  • 成因:子进程先于父进程退出,父进程未调用 wait/waitpid 回收子进程资源,子进程的 PCB 残留在系统中
  • 危害:占用进程 PID 资源,大量僵尸进程会导致系统无法创建新进程
  • 解决方案:
    1. 父进程主动调用 wait/waitpid 回收
    2. 注册 SIGCHLD 信号处理函数,子进程退出时触发回调,在回调中调用 wait 回收
    3. 两次 fork:父进程 fork 子进程,子进程再 fork 孙进程后立即退出,孙进程成为孤儿进程由 init 收养
孤儿进程
  • 成因:父进程先退出,子进程仍在运行
  • 处理:系统会将孤儿进程交由 init 进程(PID=1)收养,由 init 负责回收资源
  • 影响:无危害,属于正常场景

三、基础文件 IO(系统调用级)

这里讲的是内核提供的原生 IO 系统调用,区别于 C 标准库的 fopen/fread 等带缓冲区的封装。

1. 文件描述符与三张表

文件描述符(fd)是一个非负整数,是进程访问文件的句柄。内核通过三张表管理文件:

  1. 进程文件描述符表:每个进程独立,位于 PCB 中,下标是 fd,值是指向系统打开文件表的指针
  2. 系统打开文件表:全局共享,记录文件状态标志、当前读写偏移量、inode 指针
  3. inode 表:全局共享,每个文件唯一,记录文件物理位置、权限、大小等元数据

关键结论:fork 后父子进程共享同一个系统打开文件表,因此共享文件偏移量;dup 复制的 fd 也指向同一个打开文件表。

2. 核心 IO 函数

(1)open:打开 / 创建文件

c

运行

int open(const char *pathname, int flags, mode_t mode);
  • 常用 flags:O_RDONLY 只读、O_WRONLY 只写、O_RDWR 读写、O_CREAT 创建、O_TRUNC 截断清空、O_APPEND 追加、O_NONBLOCK 非阻塞
  • mode:创建文件时指定权限,如 0644
  • 返回值:成功返回最小可用的文件描述符,失败返回 -1
(2)read /write:读写数据

c

运行

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
  • 返回值:成功返回实际读写的字节数;read 返回 0 表示读到文件末尾;失败返回 -1
  • 注意:write 返回成功仅代表数据写入内核缓冲区,不代表已落盘,需 fsync 强制刷盘
(3)lseek:移动文件指针

c

运行

off_t lseek(int fd, off_t offset, int whence);
  • whence:SEEK_SET 文件开头、SEEK_CUR 当前位置、SEEK_END 文件末尾
  • 可用于拓展文件、获取文件大小,对管道、套接字无效
(4)close:关闭文件

关闭文件描述符,释放资源;进程退出时内核会自动关闭所有打开的文件。

3. 文件重定向:dup /dup2

c

运行

int dup(int oldfd);
int dup2(int oldfd, int newfd);
  • 作用:复制文件描述符,新旧 fd 指向同一个系统打开文件表
  • dup2:将 newfd 指向 oldfd 对应的文件,如果 newfd 已打开则先关闭
  • 典型应用:实现 shell 的输入输出重定向、管道通信

4. 阻塞与非阻塞 IO

  • 阻塞 IO:文件无数据可读 / 缓冲区满时,read/write 会挂起进程,直到条件满足
  • 非阻塞 IO:open 时加 O_NONBLOCK 标志,read/write 不满足条件时立即返回 -1,错误码为 EAGAIN
  • 非阻塞 IO 通常配合多路复用(select/poll/epoll)使用,是高并发网络编程的基础

四、进程间通信(IPC)

Linux 提供了多种进程间数据交换与同步机制,各有适用场景。

1. 匿名管道(Pipe)

核心特性
  • 本质:内核中的一块环形缓冲区,半双工通信,数据只能单向流动
  • 限制:仅支持有亲缘关系的进程(父子、兄弟进程)
  • 面向字节流:无消息边界,读写数据长度不固定
基础使用

c

运行

int pipe(int pipefd[2]);
// pipefd[0] 读端,pipefd[1] 写端

标准流程:

  1. 父进程创建管道
  2. fork 出子进程,父子各自继承读写两端
  3. 父进程关闭读端、子进程关闭写端(单向通信),开始数据传输
读写行为
  • 读管道:写端全部关闭 → 读返回 0;写端存在且管道空 → 读阻塞
  • 写管道:读端全部关闭 → 进程收到 SIGPIPE 信号,默认终止;读端存在且管道满 → 写阻塞

2. 命名管道(FIFO)

核心特性
  • 本质:文件系统中一个特殊的管道文件,有真实路径名
  • 优势:无亲缘关系的进程也可以通信
  • 底层:和匿名管道一样,基于内核缓冲区,半双工
基础使用

c

运行

// 创建命名管道文件
int mkfifo(const char *pathname, mode_t mode);

创建后,进程通过 open 打开文件,用 read/write 读写,用法和普通文件一致。

3. 共享内存(Shared Memory)

核心原理

将同一块物理内存映射到多个进程的虚拟地址空间,进程直接读写内存地址,无需任何数据拷贝,是速度最快的 IPC 方式。

核心 API
  1. shmget:创建或获取共享内存段,返回共享内存 ID
  2. shmat:将共享内存挂载到当前进程的虚拟地址空间,返回内存首地址
  3. shmdt:卸载当前进程的共享内存映射
  4. shmctl:控制共享内存,如删除、获取状态
关键注意事项
  • 无同步机制:多进程同时读写会出现数据竞态,必须配合信号量、互斥锁等同步工具使用
  • 生命周期随内核:除非手动删除,否则一直存在,程序退出不会自动销毁
  • 适合场景:大数据量、高频次的进程间数据传输

4. 消息队列(Message Queue)

核心原理

内核维护的一个消息链表,每条消息包含消息类型 + 数据内容,进程可以按类型选择性读取消息,支持优先级。

核心 API
  1. msgget:创建或获取消息队列
  2. msgsnd:向队列尾部添加一条消息
  3. msgrcv:从队列读取消息,可指定消息类型
  4. msgctl:控制消息队列,删除、获取状态
特点
  • 消息结构化,支持按类型读取,无需按顺序接收
  • 生命周期随内核
  • 有系统级大小限制,不适合传输大数据

5. 各类 IPC 对比

表格

通信方式有无亲缘限制传输速度数据形式同步性适用场景
匿名管道较快字节流天然阻塞同步父子进程简单数据传输
命名管道较快字节流天然阻塞同步无亲缘进程简单通信
共享内存最快内存块无,需额外同步大数据量高频传输
消息队列中等结构化消息自带阻塞带类型的消息交互

五、信号处理机制

信号是 Linux 的软件中断机制,用于异步通知进程某个事件发生,是进程间最简单的异步通信方式。

1. 信号基础

  • 本质:内核向进程发送的一个整数编号通知,进程收到后暂停当前执行,处理信号事件
  • 不可捕获 / 不可忽略的信号:SIGKILL(9)SIGSTOP(19),是系统强制控制进程的终极手段
常见核心信号

表格

信号编号触发场景默认行为
SIGINT2键盘 Ctrl+C终止进程
SIGQUIT3键盘 Ctrl+\终止并生成 core
SIGKILL9强制杀死进程终止,不可捕获
SIGSEGV11非法内存访问(段错误)终止并生成 core
SIGPIPE13管道读端关闭后仍写入终止进程
SIGALRM14alarm 闹钟超时终止进程
SIGCHLD17子进程退出 / 暂停忽略
SIGSTOP19暂停进程暂停,不可捕获

2. 信号的生命周期

  1. 产生:由键盘、硬件异常、系统调用、软件条件触发
  2. 未决:信号产生后,尚未被进程处理的状态
  3. 阻塞:信号被进程屏蔽,暂时不递达,解除阻塞后才会处理
  4. 递达:信号送达进程,开始执行处理动作
  5. 处理:执行默认动作、忽略,或执行用户注册的捕获函数

3. 信号注册与处理

(1)signal:简单但不可靠

c

运行

sighandler_t signal(int signum, sighandler_t handler);
  • handler 可选值:SIG_DFL 默认处理、SIG_IGN 忽略、自定义函数指针
  • 缺点:不同 Unix 系统行为不一致,信号处理完后会重置为默认行为,不推荐使用
(2)sigaction:标准可靠接口(推荐)

c

运行

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

struct sigaction 核心成员:

  • sa_handler:信号处理函数指针
  • sa_mask:处理信号期间临时阻塞的信号集
  • sa_flags:行为标志,如 SA_RESTART 自动重启被信号打断的系统调用
(3)发送信号
  • kill(pid, sig):向指定进程发送信号
  • raise(sig):向自己发送信号
  • alarm(seconds):设置闹钟,超时后向自己发送 SIGALRM

4. 信号集与信号阻塞

内核为每个进程维护两个位图:

  • 阻塞信号集(信号屏蔽字):标记哪些信号被暂时屏蔽
  • 未决信号集:标记哪些信号已经产生但尚未处理
核心操作
  1. 定义信号集:sigset_t set
  2. 信号集操作:sigemptyset 清空、sigfillset 全置、sigaddset 添加信号、sigdelset 删除信号
  3. sigprocmask:设置进程的阻塞信号集
  4. sigpending:获取当前进程的未决信号集

注意:信号不支持排队,同一信号在阻塞期间多次产生,解除阻塞后只会递达一次。

5. 可重入函数与信号安全

信号处理函数会打断主程序的执行流,如果处理函数内部调用了不可重入函数,可能导致数据损坏、程序崩溃。

  • 可重入函数:可以被多个执行流同时安全调用,不依赖全局 / 静态变量,如 readwritememcpy
  • 不可重入函数:使用了全局变量、静态变量,或涉及动态内存分配,如 mallocfreeprintfstrtok

最佳实践:信号处理函数中只做最简单的操作(如设置标志位),复杂逻辑放到主程序中处理,避免调用标准 IO、内存分配等函数。

谢谢
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

c23856

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值