【Linux C学习笔记】进程、线程、线程同步、进程间通信总结
📖今日学习:进程状态、fork、wait/waitpid、exec函数族;线程pthread全套API;线程互斥锁、死锁;进程间通信:无名管道、有名管道、信号。
🔍文章大纲
- 进程基础回顾
- fork创建子进程、写时复制COW
- 进程退出、僵尸进程、孤儿进程
- 进程回收 wait / waitpid
- exec函数族,环境变量setenv
- 线程基础:进程与线程对比
- POSIX线程API:pthread_create / pthread_exit / pthread_join
- 线程属性:分离属性
- 线程同步:互斥锁pthread_mutex
- 死锁产生四大条件与避免方法
- 进程间通信IPC:无名管道pipe、有名管道mkfifo
- 信号基础,signal信号捕捉
- ✨学习重点汇总
- ⚠️易错点&踩坑记录
- 📝今日学习小结
1. 进程基础回顾
- 程序:存放在磁盘上静态可执行文件。
- 进程:程序一次动态运行实例,拥有0~4G独立虚拟地址空间,由MMU映射到物理内存。
- PID:进程唯一ID;PPID:父进程ID。
进程虚拟内存分区:
.text文本段:存放代码指令,只读.rodata:字符串常量,只读.data:已初始化全局/静态变量.bss:未初始化全局、静态变量,程序启动自动置0.stack栈区:局部变量、函数栈帧,函数退出自动回收.heap堆区:malloc/free手动申请释放,程序员管理
进程6种状态:
| 状态 | 标记 | 说明 |
|---|---|---|
| 运行态 | R | CPU正在运行 |
| 就绪态 | R | 资源就绪,等待CPU时间片 |
| 可中断睡眠 | S | 等待资源,可以被信号唤醒 |
| 不可中断睡眠 | D | 硬件IO等待,不能被信号唤醒 |
| 停止态 | T | 收到信号暂停运行 |
| 僵尸态 | Z | 子进程结束,PCB资源未被父进程回收 |
孤儿进程:父进程先退出,子进程被
init/systemd收养,孤儿不会成为僵尸进程。
2. fork 创建子进程
#include <unistd.h>
pid_t fork(void);
返回值
- 父进程返回:子进程PID(大于0)
- 子进程返回:
0 - 出错返回:
‑1
写时复制 COW(Copy‑On‑Write)
- fork之后,父子进程一开始共享同一份物理内存页;
- 只有任意一方发生写操作的时候,才真正拷贝内存页面;读操作完全共享。
- 文件描述符会被复制,父子共享同一个文件偏移。
fork之后父子执行顺序完全由操作系统调度决定,代码不能假设谁先运行。
3. 进程退出
#include <stdlib.h>
void exit(int status); //库函数:刷新IO缓冲区,执行atexit钩子,再退出
#include <unistd.h>
void _exit(int status); //系统调用:直接结束进程,**不刷新缓冲区**
- status取值范围
0~255,退出状态,可以被父进程wait获取。 - main函数内
return n等价于exit(n);普通函数return只是退出函数,不会结束整个进程。
4. 进程回收 wait / waitpid
wait --- 阻塞回收任意子进程
pid_t wait(int *wstatus);
- 阻塞,直到任意一个子进程结束;
- wstatus保存子进程退出状态,传
NULL代表不关心状态。
状态解析宏:
WIFEXITED(wstatus) //非0:子进程正常退出
WEXITSTATUS(wstatus) //拿到子进程exit的退出码
WIFSIGNALED(wstatus)//非0:子进程被信号杀死
WTERMSIG(wstatus) //获取杀死进程的信号编号
waitpid --- 指定PID回收,支持非阻塞
pid_t waitpid(pid_t pid, int *wstatus, int options);
pid>0:回收指定pid子进程;pid=-1等价wait回收任意子进程- options:
0阻塞;WNOHANG非阻塞 ,子进程未结束返回0,不会卡住程序。
wait / waitpid作用:回收子进程PCB,消除僵尸进程。
5. exec函数族
fork复制父进程地址空间;exec用来替换当前进程的地址空间,PID保持不变,加载执行全新程序。
// l:列表传参;v:数组传参;p:搜索PATH;e:自定义环境变量
int execl(const char *path, const char *arg, ... , (char*)NULL);
int execlp(const char *file, const char *arg, ... , (char*)NULL);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
- exec调用成功之后,后面所有代码不会执行;只有失败才会返回‑1。
环境变量操作
extern char **environ; //全部环境变量
int setenv(const char *name, const char *value, int overwrite);
// overwrite=1存在就覆盖;0存在则不修改
6. 线程基础
进程:操作系统资源分配最小单位 ;线程:CPU调度最小单位。
进程与线程对比
| 对比项 | 进程 | 线程 |
|---|---|---|
| 资源 | 拥有独立完整虚拟地址空间 | 共享进程text/data/bss/heap;每个线程拥有独立栈 |
| 切换开销 | 大,切换页表,刷新cache | 很小,只切换寄存器、栈 |
| 隔离性 | 完全隔离,一个崩溃不影响别的进程 | 同进程任意线程崩溃 → 整个进程直接崩溃 |
| 通信 | 需要IPC进程间通信 | 直接读写全局变量,存在资源竞争 |
- 一个进程至少存在1个主线程(main线程)。
7. POSIX线程API
编译必须加
-pthread链接线程库
// 创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
// 线程退出
void pthread_exit(void *retval);
// 阻塞回收线程
int pthread_join(pthread_t thread, void **retval);
pthread_create:attr传NULL使用默认属性;arg传给线程入口函数。pthread_exit():只结束当前线程;main函数调用pthread_exit(NULL),主线程退出,子线程可以继续运行。pthread_join():阻塞等待线程结束,回收线程资源;不join会产生僵尸线程内存泄漏。
8. 线程分离属性 detach
-
分离状态的线程:线程退出后系统自动回收资源,不需要调用pthread_join。
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
9. 线程同步:互斥锁 mutex
多线程同时修改共享全局变量,产生资源竞争(竞态条件),需要互斥锁保护临界区。
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL); //初始化锁
pthread_mutex_lock(&mutex); //加锁,拿不到锁阻塞
//临界区:访问共享资源
pthread_mutex_unlock(&mutex); //解锁
pthread_mutex_destroy(&mutex); //销毁锁
10. 死锁
死锁产生4个必要条件(全部满足才会死锁)
- 互斥条件:资源同一时刻只允许一个线程占有
- 请求保持:线程占有部分资源,同时请求别的被占用资源
- 不可剥夺:已经占有的资源,别人不能强行抢走
- 循环等待:多个线程形成环路等待资源
避免死锁方案
- 保证多个锁加锁顺序统一
- 使用
pthread_mutex_trylock()非阻塞尝试加锁,拿不到直接释放已有锁 - 减少锁粒度,尽量少同时持有多把锁
11. IPC进程间通信
无名管道 pipe
只支持**有亲缘关系进程(父子、兄弟进程)**通信;半双工,一端读一端写。
int pipe(int pipefd[2]);
// pipefd[0]:读端;pipefd[1]:写端
管道行为特点:
- 管道无数据,读操作阻塞;管道写端全部关闭,read返回0(读到末尾)
- 管道写满,write阻塞;管道读端全部关闭,继续写会产生管道破裂SIGPIPE信号,进程直接终止。
有名管道 FIFO mkfifo
int mkfifo(const char *pathname, mode_t mode);
- 可以用于任意两个进程通信,无关进程也可以使用;
- 必须读端、写端同时打开FIFO,否则open会阻塞等待另一端。
12. 信号 signal
信号是内核给应用程序的异步通知机制。
进程收到信号三种处理方式:
-
默认处理SIG_DFL
-
忽略信号SIG_IGN
-
捕捉信号,执行自定义信号处理函数
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
SIGKILL(9)、SIGSTOP(19) 不能被忽略、不能被捕捉。
常用信号:
SIGINT:Ctrl + cSIGQUIT:Ctrl + \SIGTSTP:Ctrl + zSIGPIPE:管道破裂SIGALRM:闹钟定时信号SIGCHLD:子进程退出信号
✨学习重点
- 进程虚拟内存分区:text / rodata / data / bss / stack / heap;栈自动回收,堆需要手动
malloc/free。 - fork写时复制COW;父子PID区分;父子调度顺序不确定。
- 僵尸进程:子结束父不回收PCB;孤儿进程被init收养不会僵尸;
wait/waitpid回收消除僵尸。 exit()刷新缓冲区;_exit()直接退出,不刷新缓冲区。- exec替换进程地址空间,PID不变,成功后后续代码不执行。
- 进程是资源分配最小单位,线程是CPU调度最小单位;线程共享进程大部分资源,每个线程拥有独立栈;一个线程崩溃整个进程崩溃。
- pthread库编译必须带
‑pthread;pthread_join回收线程,否则僵尸线程内存泄漏。 - 多线程访问共享全局变量必须互斥锁保护临界区,防止竞态条件。
- 死锁四大必要条件,掌握避免死锁手段。
- 无名管道pipe:仅亲缘进程,半双工;有名管道mkfifo支持任意进程。
- signal信号捕捉,SIGKILL、SIGSTOP无法捕捉忽略。
⚠️易错点汇总
- fork之后不要想当然认为父进程一定先运行,调度顺序由操作系统决定。
- 区分
exit()和_exit(),_exit()不会刷新stdout缓冲区,printf内容丢失。 - wait会阻塞父进程;想要非阻塞回收必须使用
waitpid(...,WNOHANG)。 - exec执行成功之后,后面代码不会运行,exec后面只写错误处理。
- 线程易错:main函数写
return 0会直接结束整个进程,所有子线程全部销毁 ;pthread_exit(NULL)只退出主线程,子线程继续运行。 - 编译pthread代码忘记加
‑pthread,报未定义引用编译错误。 - 不调用
pthread_join,线程结束资源不释放,产生僵尸线程内存泄露;可以设置分离属性避免join。 - 互斥锁忘记unlock;异常分支路径没有解锁,造成死锁。
- 死锁:多把锁加锁顺序不一致,极易触发死锁。
- pipe管道:读端全部关闭还继续写,触发SIGPIPE信号,进程直接退出。
- mkfifo有名管道open的时候,只有一端打开会阻塞,读写两端必须都存在。
- 信号:SIGKILL(9)、SIGSTOP(19)不能用signal忽略或者捕捉,写了也无效。
📝今日学习小结
今天学习Linux进程线程与基础IPC通信:
- 掌握进程完整生命周期:fork创建、exit退出、wait/waitpid回收;分清僵尸进程、孤儿进程产生条件;exec替换进程镜像。
- 线程是轻量级进程,共享进程资源,栈私有;学会pthread创建、退出、回收;理解线程资源竞争,使用互斥锁保护临界区,理解死锁四大条件。
- IPC通信:无名管道适用于亲缘进程;有名管道FIFO支持任意进程;信号是内核异步通知,注意部分信号不能捕捉忽略。