[ linux-系统 ] 进程控制
进程创建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) // child31 {32 while(1)33 {34 printf("我是子进程,pid: %d\n",getpid());35 sleep(1);36 }37 exit(0);38 }39 40 // father41 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 else57 {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