进程的控制(1)
本文摘要介绍了Linux进程管理的核心概念,包括进程创建、终止和等待。在进程创建部分,详细讲解了fork函数的工作原理、写时拷贝技术及其重要性;进程终止部分阐述了退出场景、常见退出方法及exit与_exit的区别;进程等待部分说明了必要性、wait/waitpid方法的使用及返回值处理,重点分析了阻塞与非阻塞等待机制。文章通过代码示例和图示,系统性地呈现了Linux进程管理的核心机制和技术原理。
1. 进程创建
1-1fork函数初识
在 linux 中 fork 函数是⾮常重要的函数,它从已存在进程中创建⼀个新进程。新进程为⼦进程,
⽽原进程为⽗进程
进程调⽤ fork ,当控制转移到内核中的 fork 代码后,内核做:
• 分配新的内存块和内核数据结构给⼦进程
• 将⽗进程部分数据结构内容拷⻉⾄⼦进程
• 添加⼦进程到系统进程列表当中
• fork 返回,开始调度器调度
1-2fork函数返回值
• ⼦进程返回0
• ⽗进程返回的是⼦进程的pid。
1-3写时拷⻉
通常,⽗⼦代码共享,⽗⼦再不写⼊时,数据也是共享的,当任意⼀⽅试图写⼊,便以写时拷⻉的⽅
式各⾃⼀份副本。
这是只有父进程时其数据段的页表权限是可读写的
当创建子进程后
数据段的页表权限改为只读的,如果不这么设置会影响进程的独立性
这时子进程尝试写入,操作系统就会"报错"
OS要对当前情况进行判断分类:
- 真的是野指针?真的错误,终止进程
- a. 写时拷贝 b. 更改权限
问题1:为什么创建子进程的之后,直接把数据分开呢?直接拷贝不就完了,为什么要写时拷贝?
本质是"按需获取"
问题2:为什么要拷贝?直接开辟对应的空间不就好了!
子进程可能要以父进程中的数据为基础
当子进程需要修改数据时,操作系统进行写实拷贝,给子进程分配新的物理内存后,权限改回可读写
总结
因为有写时拷⻉技术的存在,所以⽗⼦进程得以彻底分离离!完成了进程独⽴性的技术保证!?
写时拷⻉,是⼀种延时申请技术,可以提⾼整机内存的使⽤率
1-4fork常规⽤法
• ⼀个⽗进程希望复制⾃⼰,使⽗⼦进程同时执⾏不同的代码段。例如,⽗进程等待客⼾端请求,
⽣成⼦进程来处理请求。
• ⼀个进程要执⾏⼀个不同的程序。例如⼦进程从fork返回后,调⽤exec函数。
1-5fork调⽤失败的原因
• 系统中有太多的进程
• 实际⽤⼾的进程数超过了限制
2. 进程终⽌
进程终⽌的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。
1.引入
#include<stdio.h>
int main()
{
int a = 1;
int b = 2;
int c = a + b;
return 0;
}
return 0 会被父进程也就是系统拿到退出码,‘0’表示进程运行成功。
1,2,3……用来表示不同的错误
2-1进程退出场景
• 代码运⾏完毕,结果正确
• 代码运⾏完毕,结果不正确
• 代码异常终⽌
2-2进程常⻅退出⽅法
正常终⽌(可以通过 echo $ 查看上一个进程退出码):
- 从main返回
- 调⽤exit
#include <unistd.h>
void exit(int status);
exit最后也会调⽤_exit(只有系统调用才能访问内核使进程真正关闭)
, 但在调⽤_exit之前,还做了其他⼯作
- 执⾏⽤⼾通过atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写⼊
- 调⽤_exit
- _exit
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终⽌状态,⽗进程通过wait来获取该值
问题一:return返回 和 exit返回 有什么区别
return表示函数调用结束,main函数return 表示进程退出
exit()在你的代码任意地方调用都会导致进程退出
问题二:exit 和_exit 的区别
exit是c库函数终止进程会主动刷新缓存区
_exit 系统调用,不会主动刷新缓存区
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
printf("Hello, World!"); // 注意:没有换行符\n,数据会留在用户缓冲区
// 情况1:使用exit()
// exit(0); // 会刷新缓冲区,输出"Hello, World!"
// 情况2:使用_exit()
_exit(0); // 不会刷新缓冲区,无输出
}
3. 进程等待
3-1进程等待必要性
• 之前讲过,⼦进程退出,⽗进程如果不管不顾,就可能造成‘僵⼫进程’的问题,进⽽造成内存
泄漏。
• 另外,进程⼀旦变成僵⼫状态,那就⼑枪不⼊,“杀⼈不眨眼”的kill-9也⽆能为⼒,因为谁也
没有办法杀死⼀个已经死去的进程。
• 最后,⽗进程派给⼦进程的任务完成的如何,我们需要知道。如,⼦进程运⾏完成,结果对还是
不对,或者是否正常退出。
• ⽗进程通过进程等待的⽅式,回收⼦进程资源,获取⼦进程退出信息
3-2进程等待的⽅法
3-2-1wait⽅法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL
// 等待子进程结束,并获取其退出状态
pid_t waited_pid = wait(&status);
3-2-2waitpid⽅法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的⼦进程的进程ID;
如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0;
如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在;
参数:
1.pid:
Pid=-1,等待任⼀个⼦进程。与wait等效。
Pid>0.等待其进程ID与pid相等的⼦进程。
2.status: 输出型参数
WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程
是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程
的退出码)
3.options:默认为0,表⽰阻塞等待
WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等
待。若正常结束,则返回该⼦进程的ID。

8-15,8个比特位,所以一般而言进程的退出码范围0-255
0-6,属于信号编号,也就是导致进程异常的返回标号
• 代码运⾏完毕,结果正确 返回0, 0
• 代码运⾏完毕,结果不正确 返回0,!0
• 代码异常终⽌ 返回!0
如何处理waitpid的返回值
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>
int main() {
pid_t pid, wait_ret;
int status;
// 创建子进程
pid = fork();
if (pid < 0) {
perror("fork failed");
exit(1);
} else if (pid == 0) {
// 子进程
printf("子进程 (PID: %d) 运行中...\n", getpid());
sleep(2);
printf("子进程退出\n");
exit(42);
} else {
// 父进程
printf("父进程等待子进程 (PID: %d) 结束...\n", pid);
// 处理 waitpid 的返回值
wait_ret = waitpid(pid, &status, 0);
if (wait_ret == -1) {
// 出错
perror("waitpid failed");
exit(1);
} else if (wait_ret == 0) {
// 使用 WNOHANG 选项时可能返回 0
printf("没有子进程退出\n");
} else {
// 成功等待到子进程
printf("成功等待到子进程 (PID: %d)\n", wait_ret);
// 解析退出状态
if (WIFEXITED(status)) {
printf("子进程正常退出,退出码: %d\n", WEXITSTATUS(status));//(status>>8)&0xff
} else if (WIFSIGNALED(status)) {
printf("子进程被信号终止,信号编号: %d\n", WTERMSIG(status));//status&0x7f
}
}
}
return 0;
}

我们用两个数字来描述子进程的执行情况
1.进程退出信号 终止原因
2.进程退出码 程序执行结果

3-2.阻塞与非阻塞等待
waitpid中options:默认为0,表⽰阻塞等待**
WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等
待。若正常结束,则返回该⼦进程的ID。
更多推荐



所有评论(0)