[Linux] 进程控制之创建和终止
🪐🪐🪐欢迎来到程序员餐厅💫💫💫
主厨的主页:Chef‘s blog
所属专栏:青果大战linux
每日吐槽
不得不说,算法和linux交换着学习,真的很舒服,就是算法容易上头,得多加控制才行,还有一件事,少熬夜吧、、、、、又掉头发了,真的很emo'
进程创建
fork
功能:在linux中fork函数是非常重要的函数,包含在头文件<unistd.h>,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
返回值:类型是pid_t,包含在头文件<sys/typeds.h>,向父进程返回子进程的PID,向子进程返回0,若fork失败则向父进程返回-1
代码展示
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main(){
pid_t p=fork();
if(p>0)
{
printf("我是父进程,我的PID时:%d 我的PPID时%d\n",getpid(),getppid());
}
else if(p==0)
printf("我是子进程,我的PID时:%d 我的PPID时%d\n",getpid(),getppid());
else{
printf("创建失败\n");
}
}
fork都干了什么
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
子进程的拷贝工作
1.在子进程刚创建时,他的代码和数据都是和父进程共享的,我们上节课谈到了页表与虚拟地址,这里的共享指的就是子进程直接拷贝父进程的页表,因此二者的数据的虚拟地址一样,且物理地址一样。
2.我们知道代码是放在只读区无法被修改的,因此父子共享是没问题的,所以拷贝页表后就不用继续处理这部分内容了。
3.数据是有可能被修改的,而父进程修改数据后,有可能影响子进程的工作,(例如子进程需要通过该数据进行if判断,加减运算)同理,子进程修改也会影响父进程,为了保证进程之间的独立性,写时拷贝出现了。
4.写时拷贝:
fork()之后,OS会把父进程中页表权限都设为read-only,然后子进程将该页表进行拷贝。当父进程中要进行写入操作时,CPU中的MMU单元检测到该地址的页表属性是read-only的,于是生成一个访问权限异常,并向OS反馈,OS经过检测后就会重新开辟一片空间A,将该数据拷贝过去,并将父进程的页表中指向该数据的物理地址改为空间A的地址,设置权限为可写,并将原表中该数据的权限设置为可写。
5.缺页中断
访问了非法的地址(野指针)
访问的地址是合法的,但是该地址还未分配物理页框。
写时拷贝这样做对比直接在创建子进程时就拷贝所有数据大大提升了内存利用率,减少了不必要的空间浪费。
子进程的运行
子进程虽然代码和父进程共享,但它是从fork之后的语句(而不是main函数)开始运行的,一个正在CPU运行的进程,CPU的寄存器会记录要运行的下一条语句的地址(PC寄存器),只需要在创建子进程时把该信息存入子进程的PCB中,当子进程开始运行时就会从fork后开始运行而不是main函数了。具体可看进程切换上下文数据
fork的使用
我们通常用fork的返回值进行if else,来让父子进程跑不同的代码,如下
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main(){
pid_t p=fork();
if(p>0)
printf("记得关注啊!!\n");
else if(p==0)
printf("记得点赞收藏啊\n");
else{
printf("创建失败\n");
}
}
最后,想必大家都很关心一件事,父进程和子进程到底谁先运行
答:不知道
这件事取决于操作系统,它觉得这次应该父进程先跑,那就父进程先跑了,它觉得下次子进程先跑那就子进程先跑了。
fork创建失败
- 系统中有太多的进程
- 实际用户的进程数超过了限制
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main(){
while(1){
pid_t p=fork();
if(p<0)
printf("fork失败\n");
}
}
进程终止
有始有终,当一个进程被创建,那他在未来某一刻一定会终止,此时又该怎么做呢?任其暴毙于内存,还是说,,,操作一番
进程结束的三种情况
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
不难理解,父进程既然要创建子进程,一定是让它去完成一些任务的,那么现在这个进程结束了,这个任务到底有没有完成呢?如果没有完成又是因为什么原因失败的呢?这些问题我们是需要为父进程解答的,好让他根据具体情况采取对策。
如何向父进程反馈?靠的就是退出码这个东西。
运行成功的话,父进程只要知道成功就好了,但如果失败了,父进程还需要知道他到底错哪了
因此我们规定了退出码为0,表示运行成功
退出码非零,表示运行错误,不同的值表示不同的错误原因
echo $?//查看上一次进程的退出码
代码展示
#include<stdio.h>
int main(){
return 18;
}
错误码
不同的非0退出码可以表示不同的错误信息,那么退出码和错误信息的一一对应关系是什么呢?
其实OS已经给我们设定好了一套标准,这里我们称那些非0退出码为错误码,他们对应的信息为错误信息。c语言给我们提供了很多接口去调用它们。
- errno
表示当前进程的错误码,假如没有错误发生就是0,每次出现错误就会更新,包含在<errno.h>头文件中。
- strerror
参数是错误码,返回值是该错误码对应的错误信息(字符串)可以把输出对应的的错误信息,输入错误码
- perror
输出当前错误码的错误信息,可以自定义输出,传入字符串s,输出s+错误信息
当程序在运行时发生了运行时错误(野指针,除零等等)就会自动记录错误码,每个错误码都有对应的错误信息,我们可以演示一下
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main(){
printf("Before:错误码%d 错误信息%s\n",errno,strerror(errno));
FILE* f=fopen("./test","r");
if(f==NULL)
printf("After:错误码%d 错误信息%s\n",errno,strerror(errno));
return errno;
}
我们可以打印出所有错误吗及其错误信息来查看(我的版本下一共134条)
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main(){
for(int i=0;i<140;i++)
{
printf("[%d] :%s\n",i,strerror(i));
}
}
当然,你也可以不用这些错误码,而是自己规定一套去使用,事实上在你的代码和系统并非强相关时,我们通常会选择自己设定错误码,比如你写一个二分算法,就可以规定错误码1表示死循环,2表示超时等等,
exit
#include<stdio.h>
#include<stdlib.h>
void func(){
exit(100);
}
int main(){
func();
return 10;
}
可以发现,exit会直接退出该进程,而return只能退出该函数 。exit的参数即是该进程最后的退出码。
exit和_exit都是直接退出进程,但是exit在退出时会把缓冲区里的数据刷出来,而_exit不会,如下所示
_exit是系统提供的接口,exit是语言里的接口。exit是对_exit的封装
这里我们多说一句,事实上缓冲区不是OS创建的,而是编程语言创建的.
感觉有用的话,就点个关注吧,博主正在持续更新linux,对于想学linux的宝,可以提供很多帮助哦