Linux进程创建,终止与等待
目录
一:进程创建
1.1认识fork
1.2fork返回值
1.3写时拷贝
1.4fork常规使用
二:进程终止
2.1进程退出的场景
2.2进程常见退出方法
2.2.1退出码
2.2.2_exit函数
2.2.3exit函数
2.2.4return退出
三:进程等待
3.1等待的必要性
3.2进程等待的方法
3.2.1wait方法
3.2.2waitpid方法
3.2.3获取子进程status
3.2.4阻塞与非阻塞等待
阻塞等待
非阻塞等待
一:进程创建
1.1认识fork
在之前的文章中,我们已经认识了fork的基础用法和返回值特点,现在我们再回顾一下
在 linux 中 fork 函数是非常重要的函数,它从已存在进程中创建⼀个新进程。新进程为子进程, 子原进程为父进程。
进程调用 fork ,当控制转移到内核中的 fork 代码后,内核做:
1.分配新的内存块和内核数据结构给子进程
2.将父进程部分数据结构内容拷贝至子进程
3.添加子进程到系统进程列表当中
4.fork 返回,开始调度器调度
当⼀个进程调用fork之后,就有两个⼆进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序。
int main()
{printf("Before pid : %d\n",getpid());int id = fork();if(id < 0){perror("fork");return 1;}printf("After pid : %d,fork return : %d\n",getpid(),id);return 0;
}
这里看到了三行输出,一行before,两行after。进程3378929先打印before消息,然后它有打印after。 另⼀个after消息由3378930打印的。注意到进程3378930没有打印before,为什么呢?如下图所示:
所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
1.2fork返回值
根据前面man手册中,对fork的介绍可以看出fork有两个返回值
子进程返回0
父进程返回的是子进程的pid
为什么会有两个返回值的具体原因可以看我之前的文章:Linux进程创建与状态管理-CSDN博客
1.3写时拷贝
通常,父子代码共享,父子在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自⼀份副本。具体见下图:
因为有写时拷贝技术的存在,所以父子进程得以彻底分离离!完成了进程独立性的技术保证!写时拷贝,是⼀种延时申请技术,可以提高整机内存的使用率。
发生写时拷贝的具体流程:
父进程创建子进程时,会更改权限为只读 ---> 子进程写入 --->触发系统错误 ---> 发生缺页中断 --->系统检测:1.在代码段写入,杀掉进程 2.在数据段写入,写时拷贝--->申请内存发生拷贝,修改页表,继续执行程序,恢复权限
问:为什么是从父进程中写时拷贝原有的数据,而不是直接让子进程申请空间,自己填入数据?
写入操作 != 对区域进行覆盖 ,可能是cont++类似的操作
1.4fork常规使用
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
一个进程要执行⼀个不同的程序。例如子进程从fork返回后,调用exec函数。
fork调用失败的原因:1.系统中有太多的进程 2. 实际用户的进程数超过了限制
二:进程终止
进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。
2.1进程退出的场景
代码运行完毕,结果正确
代码运行完毕,结果不正确
代码异常终止
2.2进程常见退出方法
正常终止(可以通过 echo $? 查看进程退出码)
1. 从main返回(main函数的返回值,也就是我们常写的return 0,这个0就是main函数的退出码)
2. 调用exit
3. _exit
异常退出:ctrl+c,信号终止
2.2.1退出码
退出码(退出状态)可以告诉我们最后⼀次执行的命令的状态。在命令结束以后,我们可以知道命令 是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码0 时表示执行成功,没有问题。 代码1 或0 以外的任何代码都被视为不成功。
Linux Shell 中的主要退出码:
一般使用不同的数字,约定或者表示出错的原因,系统提供了一批错误码,Linux规定了0~133,我们也可以使用erron自己规定错误码
可以使用strerror翻译错误码
常见错误报错和错误码:
错误码打印查看:
for (int i = 0; i < 200; i++){std::cout << "code :" << i << ", errstring: " << strerror(i) << std::endl;}printf("before: errno : %d, errstring: %s\n", errno, strerror(errno));
退出码0 表示命令执行无误,这是完成命令的理想状态。
退出码1: 我们也可以将其解释为“不被允许的操作”。例如在没有sudo权限的情况下使用yum;再例如除以0 等操作也会返回错误码1 ,对应的命令为 let a=1/0
130 ( SIGINT 或^C )和143 ( SIGTERM )等终止信号是非常典型的,它们属于128+n 信号,其中n 代表示终止码。
2.2.2_exit函数
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。
2.2.3exit函数
exit最后也会调用_exit,但在调用_exit之前,还做了其他工作:
1. 执行用户通过atexit或on_exit定义的清理函数。
2. 关闭所有打开的流,所有的缓存数据均被写入
3. 调用_exit
代码实例感受两个函数的区别;
2.2.4return退出
return是⼀种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做exit的参数。
三:进程等待
3.1等待的必要性
之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
另外,进程⼀旦变成僵尸状态,那就刀枪不入,kill-9 +pid也无能为力,因为谁也没有办法杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是 不对,或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
3.2进程等待的方法
3.2.1wait方法
返回值: 成功返回被等待进程pid,失败返回-1。
参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
父进程等待子进程的时候,如果子进程不退,父进程就一直阻塞在wait函数内部,等待子进程退出,回收子进程信息。
3.2.2waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);返回值:当正常返回的时候waitpid返回收集到的⼦进程的进程ID;如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0;如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在;
参数:pid:Pid=-1,等待任⼀个⼦进程。与wait等效。waitpid(-1,nullptr,0) == waitPid>0.等待其进程ID与pid相等的⼦进程。status: 输出型参数WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程
是否是正常退出)WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程
的退出码)options:默认为0,表示阻塞等待WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等
待。若正常结束,则返回该⼦进程的ID。即非阻塞等待,由父进程循环调用非阻塞接口,完成轮询检测,这种方式可以让父进程做更多的事
进程退出:
1.代码跑完,结果对,return 0
2.代码跑完,结果不对,return 非0
3.进程异常,os提前使用信号终止了进程,进程退出的信息会保存在自己的的退出信号里
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。
3.2.3获取子进程status
wait和waitpid,都有一个status参数,该参数是⼀个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给⽗进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):(图片的两个位图实际上是一张位图,最后两个要重叠在一块)
int main()
{int id = fork();if(id < 0){perror("fork");return 1;}else if(id == 0){sleep(20);exit(123);}else{int st;int ret = wait(&st);if(ret > 0 && (st & 0x7f) == 0)//正常退出printf("child exit code : %d\n",(st>>8)&0xFF);elseprintf("sig code : %d\n",st&0x7F);//异常退出,打印终止信号}return 0;
}
正常退出:
信号终止进程:
3.2.4阻塞与非阻塞等待
阻塞等待
// 阻塞式等待
int main()
{int id = fork();if (id < 0){printf("%s fork error\n", __FUNCTION__);return 1;}else if (id == 0){printf("child is running,pid : %d\n", getpid());sleep(2);exit(456);}else{int status = 0;//我们只能通过系统调用获得进程退出信息pid_t ret = waitpid(-1, &status, 0); // 阻塞式等待,相当于wait(nulllptr)printf("this is test for wait\n");if (WIFEXITED(status) && ret == id){printf("wait child 5s success,child return code is: %d\n", WEXITSTATUS(status));}else{printf("wait child fail,return.\n");return 1;}}return 0;
}
非阻塞等待
typedef void (*handler_t )();//函数指针
vector<handler_t> handlers;//函数指针数组void func_one()
{printf("这是临时任务1\n");
}void func_two()
{printf("这是临时任务2\n");
}void Load()
{handlers.push_back(func_one);handlers.push_back(func_two);
}void handler()
{if(handlers.empty())Load();for(auto func : handlers){func();}
}// 非阻塞式等待
int main()
{int id = fork();if (id < 0){printf("%s fork error\n", __FUNCTION__);return 1;}else if (id == 0){printf("child is running,pid : %d\n", getpid());sleep(2);exit(456);}else{int status = 0;pid_t ret = 0;do{// 我们只能通过系统调用获得进程退出信息ret = waitpid(-1, &status, WNOHANG); // 非阻塞式等待if(ret == 0)printf("this is test for wait\n");handler();} while (ret == 0);if (WIFEXITED(status) && ret == id){printf("wait child 5s success,child return code is: %d\n", WEXITSTATUS(status));}else{printf("wait child fail,return.\n");return 1;}}return 0;
}
通过代码可以明显看出非阻塞等待,子进程在退出之前,父进程可以先做其他任务,在子进程退出时,父进程立即去回收子进程信息。