Linux 多线程 / 多进程编程、同步互斥、信号机制完整梳理
一、多进程编程(进程:独立地址空间,资源隔离)
1. 创建进程核心 API
- fork()
- 调用一次返回两次:父进程返回子进程 PID,子进程返回 0;失败返回 - 1。
- 写时复制 COW:初期父子共享页表,修改内存才拷贝,开销大。
- 示例逻辑:
pid_t pid = fork(); if(pid < 0) { perror("fork"); } else if(pid == 0) { /* 子进程 */ } else { /* 父进程,pid是子进程号 */ } - vfork()不拷贝地址空间,子进程直接复用父进程内存,必须立刻
exec/_exit,现已基本废弃。 - exec 系列(替换进程镜像)
execl/execlp/execle/execv/execvp/execve,fork 后调用执行新程序。 - 进程回收 wait /waitpid
wait():阻塞等待任意子进程退出,获取退出状态,处理僵尸进程。waitpid(pid, &status, WNOHANG):非阻塞,可指定等待某一子进程。- 僵尸进程:子进程先退出、父进程未回收,PCB 残留;孤儿进程:父进程先退出,子进程由
init/systemd收养。
2. 进程间通信 IPC(多进程数据交换)
- 管道 pipe:单向,仅父子进程通信
- 命名管道 FIFO:无亲缘进程可用
- 共享内存 shmget/shmat/shmdt/shmctl:最快 IPC,需手动同步
- 消息队列 msgget/msgsnd/msgrcv:带类型消息缓冲
- 信号量 semget/semop/semctl:专门用于进程同步互斥
- Socket:本地域 socket,跨进程 / 跨机器通用
二、多线程编程(线程:进程内执行流,共享地址空间)
1. pthread 核心 API
- 创建:
pthread_create(&tid, attr, start_routine, arg) - 等待回收:
pthread_join(tid, &retval)(阻塞回收) - 分离线程:
pthread_detach(tid),线程退出自动释放资源,无需 join - 线程退出:
pthread_exit(void *ret);主线程exit()会杀死所有子线程,pthread_exit()仅结束主线程、子线程继续运行 - 获取自身 tid:
pthread_self()
2. 线程独有 / 共享资源
- 共享:全局变量、堆、文件描述符、信号处理函数、代码段
- 独有:栈、寄存器、errno、线程私有数据 TSD、调度优先级
三、同步与互斥(解决竞态条件:多执行流并发修改共享资源)
(一)互斥:同一时刻仅一个执行流访问临界区
1. 互斥锁 pthread_mutex_t(线程专用)
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_lock(&mtx); // 加锁,阻塞 pthread_mutex_unlock(&mtx); // 解锁 pthread_mutex_trylock(&mtx);// 非阻塞加锁,失败返回EBUSY类型:普通锁、递归锁(同一线程可多次 lock)、错误检测锁。
2. 进程间互斥:信号量 semaphore
- System V 信号量:
semget/semop/semctl,支持多计数器,常用于多进程互斥同步 - POSIX 无名信号量:存于共享内存,多进程 / 线程可用
- POSIX 有名信号量:
sem_open,磁盘持久化,无亲缘进程通信
(二)同步:控制执行流先后顺序
条件变量 pthread_cond_t(配合互斥锁使用)
pthread_cond_wait(cond, mtx):释放锁并阻塞,被唤醒后重新加锁pthread_cond_signal:唤醒一个等待线程pthread_cond_broadcast:唤醒全部等待线程典型场景:生产者消费者模型。
读写锁 pthread_rwlock_t
- 读共享、写独占:大量读少量写场景提升并发
rdlock多线程同时持有,wrlock独占。
自旋锁 pthread_spinlock_t忙等循环,不切换上下文;适合临界区极短、CPU 充足场景。
(三)进程同步补充
- 文件锁 fcntl:文件级互斥,简单轻量
- 共享内存 + 信号量:高性能多进程并发读写
四、Linux 信号处理机制
1. 信号基础
信号是内核 / 进程发给另一进程的异步通知,中断程序正常执行:
- 常见信号:
- SIGINT (2) Ctrl+C;SIGKILL (9) 强制杀死,不可捕获;SIGSEGV (11) 段错误;SIGCHLD 子进程退出;SIGALRM 定时器信号
- 信号状态:递达、未决、阻塞(信号屏蔽字 mask)
2. 信号注册 API
signal(int sig, void (*handler)(int)):简易接口,跨平台兼容性差sigaction(int sig, struct sigaction *act, oldact):推荐,精细控制- sa_handler:普通信号处理函数
- sa_sigaction:带附加信息的处理函数(支持 siginfo_t)
- sa_mask:执行 handler 时临时屏蔽的信号集
- sa_flags:SA_RESTART(系统调用自动重启)、SA_NOCLDWAIT(SIGCHLD 不产生僵尸)
3. 信号集操作(屏蔽 / 解除屏蔽信号)
sigset_t set; sigemptyset(&set); // 清空集合 sigaddset(&set, SIGINT); // 添加信号 sigprocmask(SIG_BLOCK, &set, NULL); // 阻塞指定信号 sigprocmask(SIG_UNBLOCK, &set, NULL); // 解除阻塞4. 信号等待
pause():阻塞直到收到任意信号sigsuspend(&mask):临时替换屏蔽字并阻塞,原子操作,避免竞态
5. 多进程 / 多线程信号特性
- 进程信号:SIGCHLD 用于父进程异步回收子进程,避免轮询 waitpid
- 线程信号规则
- 信号屏蔽字是线程私有,每个线程可独立屏蔽不同信号
- 同步信号(SIGSEGV/SIGFPE)发送到触发线程;异步信号随机发给任意未屏蔽该信号的线程
- 多线程程序推荐:单独创建一个专用线程统一处理所有异步信号,其余线程全部屏蔽信号,简化逻辑
6. 信号处理限制
信号 handler 内只能调用异步安全函数(write、sigaddset 等),禁止 malloc、printf、pthread 系列等非安全函数,易死锁 / 内存损坏。
五、面试高频总结(可直接用于简历 / 口述)
- 进程与线程区别:进程资源隔离、切换开销大;线程共享地址空间、切换轻量,线程依赖进程存在。
- 互斥 vs 同步:互斥保证同一时间唯一访问临界区;同步控制执行先后顺序,条件变量 + 互斥锁是标准组合。
- 锁选择:短临界区用自旋锁;普通线程互斥用 mutex;多读少写用读写锁;多进程同步用信号量。
- 信号关键点:SIGKILL 无法捕获;线程独立屏蔽字;handler 仅可调用异步安全函数;SIGCHLD 解决僵尸进程。
- 生产者消费者经典模型实现:线程版 (mutex+cond)、进程版 (共享内存 + 信号量)。
