**
进程与线程1
**
📖今日学习内容:程序与进程概念、进程状态、fork创建子进程、进程回收wait/waitpid、exec函数族、环境变量;线程概念、线程创建回收、进程线程对比。
🔍文章大纲
- 基础概念:程序 vs 进程
- Linux进程常用shell命令
- 进程虚拟地址空间
- 进程五大状态
- 进程创建 fork()
- 进程退出 exit/_exit
- 进程回收 wait / waitpid,僵尸进程、孤儿进程
- exec函数族
- 环境变量 setenv / environ
- 线程基础:线程概念、进程与线程区别
- 线程API pthread_create / pthread_exit / pthread_join
- ✨学习重点汇总
- ⚠️易错点&踩坑记录
- 📝今日学习小结
1. 基础概念:程序 vs 进程
- 程序:存放在外存(磁盘)上的可执行文件,静态,没有运行。
- 进程:程序的一次动态执行实例,包含创建、调度、运行、消亡整个生命周期。>
PID:进程唯一标识ID;PPID:父进程ID。
2. Linux进程常用shell命令
| 命令 | 功能 |
|---|---|
ps -ef |
查看系统全部进程,PID、PPID |
ps -aux |
查看进程CPU、内存占用 |
pstree |
查看进程父子树关系 |
top |
实时查看进程资源占用 |
kill -9 PID |
强制杀死指定进程 |
killall -9 进程名 |
按名字批量杀进程 |
./a.out & |
程序放到后台运行 |
jobs |
查看后台任务 |
fg 编号 |
后台任务切回前台运行 |
nice / renice |
修改进程优先级 |
3. 进程虚拟地址空间(32位0~4G)
每个进程拥有独立虚拟地址空间,MMU内存管理单元映射到物理内存。
分区:
- 文本段(.text):存放可执行代码、指令,只读
- 数据段
.data:已经初始化全局变量、static静态变量.bss:未初始化全局、静态变量,程序启动自动置0.rodata:字符串常量,只读
- 栈区(.stack):局部变量、函数形参、函数调用栈;函数退出自动回收;空间较小。
- 堆区(.heap) :程序员手动管理,
malloc/free申请释放,存放大数据。
进程之间虚拟地址相互隔离,一个进程不能直接读写另一个进程的地址空间。
4. 进程6种状态
| 状态 | 标记 | 说明 |
|---|---|---|
| 运行态 | R | CPU正在执行该进程 |
| 就绪态 | R | 等待CPU时间片,资源就绪等待调度 |
| 可中断睡眠态(S) | S | 等待资源,可以被信号唤醒 |
| 不可中断睡眠态(D) | D | 等待硬件IO,不能被信号唤醒 |
| 停止态(T) | T | 收到暂停信号,进程暂停 |
| 僵尸态(Z) | Z | 子进程代码执行结束,资源未被父进程回收,残留PCB |
| 结束态(X) | X | 资源全部回收,彻底消亡 |
僵尸进程 :子进程已经退出,父进程没有调用wait/waitpid回收子进程资源,产生僵尸进程,占用PCB资源。
孤儿进程 :父进程先退出,子进程被init/systemd进程收养,init会自动回收孤儿子进程,孤儿进程不会成为僵尸进程。
5. 进程创建 fork()
#include <unistd.h>
pid_t fork(void);
返回值
- 父进程:返回子进程PID(>0)
- 子进程:返回 0
- 失败:返回 ‑1
fork工作原理:
- 调用fork之后,创建子进程,拷贝父进程的虚拟地址空间(写时复制COW)。
- fork之后,父子进程从fork返回处,同时向下执行代码。
- 文件描述符会被复制,父子共享同一个文件偏移。
- 父子进程是完全独立进程,变量互不干扰。
写时复制COW:刚fork时父子共享物理内存;只有任意一方对内存做写操作,才会真正拷贝内存页面。
6. 进程退出
#include <stdlib.h>
void exit(int status); //库函数,会刷新缓冲区,执行atexit注册钩子,再退出进程
#include <unistd.h>
void _exit(int status); //系统调用,直接结束进程,**不刷新IO缓冲区**
- status:退出状态,取值0‑255;父进程wait可以拿到这个退出值。
- main函数
return n等价于exit(n);普通函数return只是结束函数,不会结束进程。
7. 进程回收 wait / waitpid
wait --- 阻塞回收子进程
pid_t wait(int *wstatus);
- 阻塞等待任意一个子进程结束;
- wstatus保存子进程退出状态,传NULL代表不关心状态。
- 返回值:成功返回退出子进程PID;失败返回‑1。
状态解析宏:
WIFEXITED(wstatus):非0 → 子进程正常退出WEXITSTATUS(wstatus):获取子进程exit的退出码WIFSIGNALED(wstatus):非0 → 子进程被信号杀死WTERMSIG(wstatus):获取杀死进程的信号编号
waitpid --- 指定PID回收,可以非阻塞
pid_t waitpid(pid_t pid, int *wstatus, int options);
参数pid:
pid > 0:回收指定PID的子进程pid = -1:回收任意子进程,等价wait
options选项:0:阻塞模式WNOHANG:非阻塞,子进程未结束返回0,不卡住父进程
wait/waitpid作用:回收子进程PCB资源,消除僵尸进程。
8. exec函数族
fork创建子进程后,子进程复制父进程代码;exec可以让子进程加载执行全新程序,替换当前进程地址空间,PID保持不变。
常用原型:
// l:参数列表传参;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调用成功之后,后面代码全部不再执行,地址空间被新程序覆盖。
- exec只有失败才有返回值,成功无返回。
- 文件描述符默认继承。
9. 环境变量
-
全局变量
extern char **environ;保存全部环境变量。int setenv(const char *name, const char *value, int overwrite);
-
overwrite=1:存在就覆盖;overwrite=0,存在则不修改。
10. 线程基础
线程:轻量级进程LWP,进程是资源分配最小单位;线程是CPU调度最小单位。
进程与线程对比
| 对比项 | 进程 | 线程 |
|---|---|---|
| 资源分配 | 每个进程拥有独立完整虚拟地址空间 | 线程隶属于进程,共享进程的堆、全局、data、bss、rodata ;每个线程拥有独立栈 |
| 调度单位 | 进程 | 线程(CPU调度) |
| 切换开销 | 大,切换页表,刷新cache | 很小,只切换栈、寄存器 |
| 隔离性 | 进程完全隔离,进程间默认不能直接访问 | 同进程内线程共享内存,直接读写全局变量,存在竞态条件 |
| 销毁影响 | 一个进程崩溃不影响别的进程 | 一个线程崩溃,整个进程全部直接崩溃 |
一个进程至少有1个主线程(main线程)。
11. POSIX线程API(pthread库,编译需要加 -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,主线程退出,其他子线程可以继续运行。pthread_join:阻塞等待指定线程结束,回收线程资源;如果不join,会产生僵尸线程。>
编译命令示例:
gcc test.c -o test -pthread
✨学习重点
- 程序静态,进程动态;PID进程唯一标识,PPID父进程ID。
- 进程虚拟空间分区:text、data、bss、rodata、栈、堆;栈自动回收,堆需要malloc/free手动释放。
- fork:写时复制COW;返回三次(父>0,子=0,失败‑1),父子进程从fork之后并行执行。
- 僵尸进程:子进程退出父进程没有wait回收;孤儿进程父进程先死,被init收养,不会僵尸。
- exit库函数刷新缓冲区;_exit系统调用直接退出,不刷新缓冲区;main return等价exit。
- wait阻塞回收任意子进程;waitpid支持指定PID,支持WNOHANG非阻塞回收,解决僵尸进程。
- exec函数族:替换进程地址空间,PID不变,成功之后后续代码不运行,只有失败返回。
- 进程vs线程:进程是资源分配最小单位;线程是CPU调度最小单位。
- 同进程线程:共享堆、全局变量;每个线程有自己独立栈;一个线程崩溃整个进程崩溃。
- pthread库编译必须加
-pthread;pthread_join回收线程,否则僵尸线程。
⚠️易错点汇总
- 混淆
_exit()与exit():_exit()系统调用,直接退出不刷新缓冲区,容易丢失输出。 - fork之后误以为父子进程先后执行,实际父子进程调度顺序由操作系统决定,顺序不确定。
- fork之后父子进程变量拷贝,子进程修改变量不会影响父进程,写时复制,各自独立。
- 僵尸进程不等于孤儿进程:孤儿进程被init自动回收,不会产生僵尸;只有子结束父不回收才僵尸。
- wait会阻塞父进程;想要不阻塞必须使用
waitpid(..., WNOHANG)。 - exec执行成功,后面代码不会跑;不要在exec后面写业务逻辑,只写错误处理。
- 线程易错:main函数
return 0是结束整个进程,所有子线程全部直接销毁;pthread_exit(NULL)只退出主线程,子线程继续运行。 - pthread编译忘记加
-pthread,编译报错未定义引用。 - 不调用
pthread_join,线程结束资源不释放,产生僵尸线程内存泄露。 - 同进程多个线程直接读写全局共享变量,没有同步保护,出现数据竞争、乱码逻辑错误。
- 误以为线程崩溃只杀死当前线程:同一个进程任意一个线程崩溃,整个进程全部退出。
- fork复制文件描述符,父子进程共享同一个文件偏移,父子同时write会出现内容交织错乱。
📝今日学习小结
今天学习Linux进程与线程核心知识点:
- 进程是程序动态运行实例,拥有独立虚拟地址空间;掌握进程6种状态,分清僵尸进程、孤儿进程产生原因。
- fork创建子进程,写时复制;exit/_exit进程退出;wait/waitpid回收子进程消除僵尸进程。
- exec函数族替换进程镜像,在不改变PID情况下执行全新程序。
- 线程是轻量级进程,CPU调度最小单位;线程共享进程大部分资源,每个线程拥有独立栈。
- pthread线程库,
pthread_create创建,pthread_exit退出,pthread_join回收,编译链接必须带上‑pthread。