进程控制(进程创建与终止 | 进程等待 | 程序替换)
文章目录
- 一、进程创建
- 1. fork函数
- 2. fork创建进程
- 3. 写时拷贝
- 二、进程终止
- 1. 进程退出有三种情况
- 2. 常见进程终止方法
- 三、进程等待
- 背景(必要性)
- 1. 进程等待的方法
- (1)wait
- (2)waitpid
- 研究第二个参数:
- 四、进程的程序替换
- 1. 替换意义
- 2. 替换函数
- 命名理解:
- 3. 清晰理解程序替换
在之前的学习中,我们至少应该明确进程的概念:是一些数据结构(PCB+地址空间+页表)+ 代码和数据这样的一个集合。
一、进程创建
1. fork函数
初识进程时,我们对fork也有过一些简单的了解。
#include<unistd.h> //头文件pid_t fork()
fork返回值有两个:为什么给子进程返回0,给父进程返回子进程的pid呢?
- 父与子是一对多的关系,任何的孩子都知道自己的父亲,但父亲不一定知道哪个是自己的孩子,所以需要给父进程返回子进程的pid,来需要标识孩子。
2. fork创建进程
进程调用fork,当控制转移到内核中的fork代码后,内核:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构(PBC+地址空间+页表)拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
fork创建子进程是以父进程为“模板”的。
我们来看一段代码:
在以前的程序中,我们知道,if 、else if、else只会执行一个。但在该程序中,却都执行了。
- 单执行流道:本质只有一个进程
- 多执行流道:fork之前,父进程独立执行。fork之后,创建了子进程,有了两个进程,就有了两个执行流。父子进程分别开始执行从fork之后的语句。因为返回值的不同,执行不同的代码块。
下面的例子更能说明情况)
注意,fork之后,谁先执行完全由调度器决定。
3. 写时拷贝
程序 = 代码(逻辑)+ 数据
默认情况下,父子进程共享代码,但是数据各自私有一份。
代码共享:所有代码共享,一般都是从fork之后开始执行。
为啥代码是共享?
- 代码不可被修改,各自私有会浪费空间,没必要一个进程一份
为啥数据要私有?
- 因为进程之间具有独立性。一个进程的运行不能影响另一个,也就是说,如果一个进程数据被修改,会影响其他进程。
对一个进程来说,数据很多且并不是所有数据都立刻要使用,因此,就不是所有的数据都需要拷贝。
但是如果马上要独立的话,就需要将数据全部拷贝。这样就把原本可以后续再拷贝甚至不需要拷贝的数据都拷贝了,浪费时间和空间。
(如果父子都只会对数据进行读取,不需要进行数据私有化的。)
因此,拷贝的过程不是立刻做的!
写时拷贝
父子进程代码共享。
数据在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各持一份。
写时拷贝的识别(是否需要拷贝)、 操作(内存管理、申请空间、拷贝数据)是由操作系统来完成的。
总结:如何理解子进程创建?如何理解fork?
- 本质是系统多了一个进程。操作也要管理它,所以也要给它创建PCB,地址空间,页表等等。
- 父子进程的相关性体现在,一般我们会把父进程PCB,地址空间,页表的大部分内容(不是全部,比如pid这种私有属性不会)都拷给子进程。
- 子进程要以父进程为模板。它们大部分的信息是一样的。子进程是能够看到父进程所有代码的【解释:因为父进程通过自己的页表可以看到自己的代码,那么子进程的页表就是从父进程拷贝过来的,所以子进程也可以看到父进程的所有代码】
- 进程的独立性,体现在:有各自独立的PCB、地址空间、页表。代码是共享的,数据是各自私有的。
二、进程终止
1. 进程退出有三种情况
1、代码跑完了,结果是对的
2、代码跑完了,结果是错的
3、代码没跑完,程序崩溃
比如下图:
代码没有运行完,当执行到int x = 1/0这条语句时,就被系统杀掉了。
操作系统向该进程发送八号信号 SIGFPE,FPE(floating point exception)浮点数异常。
所以,1,2这种情况都是代码运行完毕,一般用退出码进行标识。
而第3种是发生了异常,进程退出的标识就没有意义了。而更关注的是退出的原因:我们如何获得原因呢,涉及进程等待相关知识。
2. 常见进程终止方法
- 正常退出(通过 echo $? 查看最近一个进程的退出码)
(1)从main返回
main函数退出时,返回的数字叫做进程的退出码。
为什么main的return一般写成0呢?0 在函数设计中,一般代表正确,代表程序正常跑完;而非0代表运行出错,每一个非0数字代表一种运行后的出错原因。
总结:return的返回值在main函数中代表这个进程的退出码;而return在其他函数中,代表的是这个函数的返回值。
(2)调用exit()函数
exit的参数就是这个进程的退出码。
- exit和return的区别:
exit:终止整个进程,任何地方调用都会终止整个进程
return:结束一个函数。(main函数中return代表进程退出)
(3)_exit
_eixt 是系统调用接口,用法几乎和exit一模一样。
_exit与eixt的区别:
第五秒才会打印。
exit(C的库函数)是能够把缓冲区数据打印出来的。
_exit(系统调用)在进程结束时,不会把缓冲区数据刷新出来。
补充:缓冲区刷新的条件
1.进程结束。
2.遇到\n。
3.缓冲区满。
4.手动刷新缓冲区fflush(stdout)。
5.调用exit();但调用_exit(0),不刷新缓冲区。
区别:_exit是直接进到系统里把进程杀死;而exit会进行一些清理工作,比如说刷新缓冲,关闭流等。
- 异常终止:ctrl + c,信号终止
三、进程等待
背景(必要性)
子进程被创建出来,父子谁先运行,是由调度器决定。
那么谁先退出?
一般而言,我们通常要让子进程先退出。
因为父进程可以很容易对子进程进行管理(垃圾回收等)。创建子进程是为了处理业务,需要让父进程帮我们拿到子进程执行的结果。因此,父进程就需要知道子进程退出的情况。
1. 进程等待的方法
(1)wait
#include<sys/types.h>
#include<sys/wait.h>pid_t wait(int*status);
//返回值://成功返回被等待进程pid,失败返回-1。
//参数://输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
我们写一段代码:模拟父子进程同时运行,然后子进程退出,父进程没有做任何回收,那么子进程就变成僵尸(造成资源浪费);
int main()
{pid_t id = fork();if (id == 0){int count = 0;while (1){sleep(1);cout << "child...: " << count << endl;if (count >= 15){break;}count++;}exit(0);}else{while (1){sleep(1);cout << "father...." << endl;}}return 0;
}
脚本监视进程状态:while :; do ps axj | head -1 && ps axj | grep myproc | grep -v grep;sleep 1;echo "###########";done
我们发现,15秒后,子进程变为僵尸状态。
接着父进程调用wait,观察子进程状态。
int main()
{pid_t id = fork();if (id == 0){int count = 0;while (1){sleep(1);cout << "child...: " << count << endl;if (count >= 15){break;}count++;}exit(0);}else{//cout<<"father before..."<<endl;//wait(NULL); //阻塞了父进程,一直等待,等待子进程退出,获取它的资源!//cout<<"father after..."<<endl;int count = 0;while (1){sleep(1);cout << "father....." << count << endl;if (count == 20){wait(NULL);}count++;}//20s后进行wait ,在子进程后面}return 0;
}
我们发现当父进程调用wait等待之后,将子进程回收,子进程的僵尸状态也消失了。
(2)waitpid
pid_ t waitpid(pid_t pid, int *status, int options);
- 第一个参数表示所要等待进程的pid:
pid=-1, 等待任一个子进程,与wait等效
pid=0, 等待其进程id与pid相等的子进程 - 第二个表示读取子进程的状态
- 第三个表示等待的方式:
如果等待成功,返回值与第一个参数相同;
如果失败,返回值-1
返回值:
- 当正常返回的时候waitpid返回收集到的子进程的进程ID;
- 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
waitpid(id,NULL,0)与wait(NULL)无差别,都是以阻塞方式等待特定的进程,且waitpid和wait一样能够将子进程进行回收释放,效果类似。
研究第二个参数:
子进程退出的情况有三种,获取的信息:
1、运行时正常运行完,拿到退出码
2、运行时发生异常,拿到收到的信号
Status:是一个整型指针。在传参时,该参数是一个输出型参数。
在调用时,
int st = 0;
waitpid(pid, &st, 0);
然后开始等待,子进程退出后,操作系统就会从子进程的PCB中读取退出信息,保存在status指向的变量中(st中)。
返回之后,st中保存的就是进程退出的信息。
要确认是否正常运行,退出码是多少,或退出信号是多少。
所以这个整数是要被按照某种特定方式去使用的。int 是 32bit,我们只研究它的低16位。
正常终止(第1,2种情况):我们只检测低16位当中的高8位,这8位是给正常退出时的退出状态作参考的。
被信号所杀(第3种情况):检测低16位中的低7位,确认是否为信号所杀,如果为0,那么就是正常。
如果子进程正常退出,且退出码是1,那么第二个参数对应的二进制就是10000 0000,低八位都是0,高八位的第一个是1。此时该整数打印出来的数字为256。
所以我们如果想获取当前进程的退出码,那么 (st>>8)&0xFF 即可。
status:
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)与
!(status&ox7f)
等价。 - WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)与
(status>>8)&0xff)
等价。
总结:
waitpid的作用:当一个子进程退出时,父进程没有回收,那么子进程变为僵尸。父进程通过waitpid释放子进程的僵尸状态,并读取子进程退出时的退出码和信号。
一个进程终止分两种情况:彻底被释放完或僵尸状态。
当僵尸状态时:代码数据被释放,部分页表也可能被释放,但它的PCB一定是被保留下来,因为内含退出信息,还需被读取。
四、进程的程序替换
1. 替换意义
首先要明确fork创建子进程的目的:
1、想让子进程执行父进程代码的一部分(子承父业)——代码共享
2、想让子进程执行和父进程完全不同的事情(创业)——这时代码也要发生写时拷贝,父子完全独立。
进程的程序替换,有没有创建新的进程呢?
- 没有创建新进程,因为PCB没有被重新创建,pid没有被重新生成
2. 替换函数
完成程序替换的接口(操作系统所提供的):
#include <unistd.h>`int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
exec系列函数没有返回值,一旦有返回值就代表替换出错。
其中,只有execve是系统调用的接口,其余五个都是对它的一个封装,以满足不同的需求。
参数说明:
- 第一个参数代表你要执行的对象
- 第二个参数代表要怎么执行(在命令行中怎么执行调用,在参数中就怎么传递)
命名理解:
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
调用举例:
#include <unistd.h>
int main()
{char *const argv[] = {"ls", "-a" "-l", "-f", NULL};char *const envp[] = {"/usr/bin/ls", NULL};execl("/usr/bin/ls", "ls", "-a", "-l", "-f", NULL);// 带p的,可以使用环境变量PATH,无需写全路径// 第一个ls代表你要执行的是ls这条命令,第二个ls代表你在命令行中的怎么执行execlp("ls", "ls", "-a", "-l", "-f", NULL);execvp("ls", argv);// 带e的,需要自己组装环境变量execle("ls", "ls", "-a", "-l", "-f", NULL, envp);execv("/usr/bin/ls", argv);execve("/usr/bin/ls", argv, envp);exit(0);
}
3. 清晰理解程序替换
上图是调用execl将代码替换成系统程序对应的代码。同时,exec系列函数也可以调用我们自己写的程序,也就是可以实现一个进程调另一个进程。
当我们想执行替换又不影响本身时,就需要多进程,通常是父子进程分流进行。
一般exec系列函数,我们不会自己调用。而是在fork之后,让子进程调用exec函数,父进程只需要wait等待即可。
这样,一旦命令本身有问题,也不会波及父进程。
进程控制(进程创建与终止 | 进程等待 | 程序替换)相关推荐
- Linux——进程控制:创建、终止、等待、替换
进程创建 fork #include <unistd.h> pid_t fork(void); 操作系统做了什么? 调用fork之后,内核的工作: 分配新的内存块和内核数据结构给子进程 将 ...
- linux——进程(创建、终止、等待、替换)
进程的基本操作 概念 程序运行的一个实例,其占有一定的空间. 查询某一进程当前情况 ps aux | grep 进程名 终止进程 kill -9 pid: //pid指需要终止的进程pid 创建 pi ...
- 【Linux】Linux进程控制 --- 进程创建、终止、等待、替换、shell派生子进程的理解…
柴犬: 你好啊,屏幕前的大帅哥or大美女,和我一起享受美好的今天叭
- 进程控制(2):进程操作
前言: 关于进程控制这块的好文很多,下面转载的这篇内容很丰富,也会举适当的栗子,与其写的一知半解不如参读学习别人的优秀博文,感谢原作,本系列摘录自:https://www.cnblogs.com/xi ...
- Linux无法终止进程,如何在Linux中终止进程
您是否曾经遇到过启动应用程序,而在使用该应用程序时突然变得无响应并意外崩溃的情况?您尝试再次启动该应用程序,但没有任何反应,因为原始应用程序进程从未真正完全关闭. 好吧,这件事发生在我们所有人身上,不 ...
- 【Linux】进程控制(创建、终止、等待)
环境:centos7.6,腾讯云服务器 Linux文章都放在了专栏:[Linux]欢迎支持订阅 相关文章推荐: [Linux]冯.诺依曼体系结构与操作系统 [Linux]进程理解与学习Ⅰ-进程概念 [ ...
- 进程控制-创建、退出、等待、替换
目录 进程创建 1.子进程继承 2.写时拷贝 进程退出 echo $? 退出码 进程异常退出的情况模拟: 退出进程的方式 退出码的意义: 进程退出,在系统中发生了什么? 进程等待 为什么要有进程等待呢 ...
- Linux系统调用:创建和终止进程
1.进程的三种状态 1.运行.要么在被CPU执行,要么等待被执行且最终会被内核调度. 2.停止.执行被挂起且不会被调度.收到特定信号后才能继续运行. 3.终止.进程永远地停止了.可能的原因有三种:(1 ...
- windows下 C 程序 调用其他程序常见新的进程CreateProcess以及通过TerminateProcess终止进程
WinAPI执行外部程序和创建新进程: CreateProcess(NULL,cmdOp,NULL,NULL,FALSE,CREATE_NEW_CONSOLE,NULL,NULL,&si,&a ...
最新文章
- 【Linux 内核 内存管理】内存管理架构 ⑤ ( sbrk 内存分配系统调用代码示例 | 在 /proc/pid/maps 中查看进程堆内存详情 )
- matlab条件判断配合输出
- Pycharm、Idea、Goland 官方汉化来了
- Booting ARM Linux SMP on MPCore
- python安装pyecharts清华_基于Python安装pyecharts所遇的问题及解决方法
- Spring Boot 学习(一) 快速搭建SpringBoot 项目
- 信息学奥赛一本通(1114:白细胞计数)
- js脚本捕获页面 GET 方式请求的参数?其实直接使用 window.location.search 获得
- 第 15 章 垃圾回收相关算法
- 官方文档---ubuntu 安装OpenStack
- areact中组件antd中checkbox_19GW光伏组件中/开标价格一览!
- mysql主从复制1064_mysql主从复制或其他操作报错ERROR 1064 (42000): You have an er
- 微信jssdk开发 java_Java微信公众平台开发(十一)--微信JSSDK中Config配置
- 笔记本电脑频繁自动重启_电脑一直自动重启怎么办 电脑一直自动重启的原因和解决办法...
- Oliver Wyman的一年制PTA面试
- 困惑很久的解微分方程时绝对值取舍问题(必看)
- 让人懵逼的宏定义赋值
- mysql 正则 查询 手机号,移动手机号码段 正则
- 巧用计算机方法,第四课 巧用计算器教案.doc
- 服务器的固态硬盘使用raid非ssd,在VMware ESXi中使用固态硬盘的注意事项
热门文章
- mysql忘记密码如何重置密码,以及修改root密码的方法
- mysql8.0JDBC驱动下载以及JDBC连接时遇到的问题
- Flash游戏存档文件 .sol文件替换+拷贝教程 附sol Editor下载
- project 2013 删除资源
- 模板_单调栈_AcWing_830. 单调栈_底顶递增栈
- 蚂蚁全媒体中心总编刘鑫炜解答:律师如何打造个人品牌
- 我的数据分析秋招经历和学习心得【万字长文】
- 兼容IE浏览器 媒体查询方法
- 英雄联盟男爵领域服务器位置,LOL男爵领域是几区?英雄联盟新区怎么升级快?
- 蚁群算法及TSP问题的matlab代码记录