进程创建fork
fork 之后发生了什么
- 将给子进程分配新的内存块和内核数据结构(形成了新的页表映射)
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork 返回,开始调度器调度
父进程创建子进程,代码是共享的,数据不修改时也是共享的,子进程数据修改时发生写时拷贝
写时拷贝
父进程创建子进程之前,先把数据权限改为只读的,子进程将数据修改时,触发系统错误,触发缺页中断,系统检测,系统检测如果代码段发生错误,就杀掉进程,如果是数据段发生错误,系统就判定要发生写时拷贝,系统申请内存,发生拷贝,修改页表,恢复执行,恢复权限为只读
进程终止
错误码
为什么要使用return ?
return返回的数字表示是否错误 : 0表示成功,非0表示错误
进程中,父进程会关心我的运行情况
main函数的返回值本质:表示进程运行完成时,是否是正确的结果,如果不是,我们可以用不同的数字表示出错的原因
系统给我们提供了一批错误码 :
C 语言当中有个的 string.h 中有一个 strerror 接口 以及 errno.h 中有一个errno接口
写linux中以下代码可以将所有错误码信息打印出来,一共有133条错误码
总结:
main函数结束表示进程退出,main函数的返回值表示进程的退出码
进程的退出码可以由系统默认提供,也可以自定义约定
进程终止的方式
1.main函数return (只有main函数return表示进程结束,其他函数结束表示当前函数返回)
2.exit (在代码的任何地方,表示进程结束)
3._exit
exit和_exit
exit会刷新缓冲区,_exit不会刷新缓冲区
缓冲区 -- 叫做语言级缓冲区 -- 在C/C++内部,不在操作系统中
exit在man 3 手册,属于库函数 , _exit在man 2 手册,属于系统调用
exit和_exit属于上下层关系,exit就是调用的_exit并封装
进程等待
父进程创建子进程,看以下代码以及运行结果和监视窗口
父进程创建子进程后,如果不管他,子进程运行结束就会变成僵尸进程
所以,父进程创建了子进程,父进程就要等待子进程,直到子进程结束。等待的时候,子进程如果不退,父进程就要阻塞在wait函数内部,wait函数等待任意一个子进程
使用一下wait函数,发现子进程结束后,直接被父进程回收了,监视窗口中也看不到子进程变成僵尸进程了,直接被回收
waitpid
pid > 0 指定一个子进程 , pid == -1 任意子进程
*status 帮助父进程获取子进程的退出信息 (输出型参数)
status不能简单的当作整形来看待,可以当作位图来看待,只研究status低16位比特位
根据以上代码,父进程可以拿到子进程的退出信息
可不可以使用全局变量来获得子进程的退出码?不可以,只能通过系统调用来获取退出信息
重谈进程退出:
1.代码跑完,结果对,return0
2.代码跑完,结果不对,return非0
3.进程异常 OS提前使用信号终止进程
status最低7位提取子进程的退出信号
阻塞/非阻塞等待
options决定是否阻塞,0 :阻塞等待 WNOHANG :非阻塞等待
写以下代码及运行结果
1 #include<iostream>
2 #include<unistd.h>
3 #include<cstdlib>
4 #include<string>
5 #include<string.h>
6 #include<errno.h>
7 #include<vector>
8 #include<sys/types.h>
9 #include<sys/wait.h>
10 #include<functional>
11 #include"task.h"
12
13 using task_t = std::function<void()>;
14
15
16 // 非阻塞等待
17
18 void LoadTask(std::vector<task_t> &tasks)
19 {
20 tasks.push_back(PrintLog);
21 tasks.push_back(Download);
22 tasks.push_back(BackUp);
23 }
24 int main()
25 {
26 std::vector<task_t> tasks;
27 LoadTask(tasks);
28
29 pid_t id = fork();
30 if(id == 0) // child
31 {
32 while(1)
33 {
34 printf("我是子进程,pid: %d\n",getpid());
35 sleep(1);
36 }
37 exit(0);
38 }
39
40 // father
41 while(1)
42 {
43 pid_t rid = waitpid(id,nullptr,WNOHANG);
44 if(rid > 0)
45 {
46 printf("等待子进程%d成功\n",rid);
47 sleep(1);
48 break;
49 }
50 else if(rid < 0)
51 {
52 printf("等待子进程失败\n");
53 sleep(1);
54 break;
55 }
56 else
57 {
58 printf("子进程尚未退出\n");
59 sleep(1);
60
61 //阻塞期间父进程做自己的事情
62 for(auto &task : tasks)
63 {
64 task();
65 }
66 }
67 }
68 return 0;
69 }
可以看到父进程不等待子进程,子进程在完成任务时,父进程也可以做自己的事情
进程程序替换
程序替换是通过特定的接⼝,加载磁盘上的⼀个全新的程序(代码和数据),加载到调⽤进程的地址空间中,看以下代码,我们的myexec程序就执行了ls命令
execl不仅可以替换系统命令,也可以替换我们的可执行程序
看以下代码:验证了程序替换没有创建新进程
返回值:成功时没有返回值,失败返回-1
只要返回,就是失败
可以理解为execl将可执行程序加载到内存
认识全部接口
execv的使用方法即运行结果
execlp使用可以不带路径,为什么可以不带路径?
因为execlp会从环境变量PATH中找到路径(有重复的路径就找到第一个)
execvp
对于环境变量:子进程继承父进程的所有环境变量,如果要传递新的环境变量(自己定义,自己传递) execvpe可以自己导入新的环境变量
程序替换不影响命令行参数和环境变量
这六个不是系统调用接口,而是C标准库封装的接口。这六个的区别只有传参方式的差别
真正意义上的系统调用接口是execve