文章目录

  • 一、进程创建
    • 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等待即可。
这样,一旦命令本身有问题,也不会波及父进程。


进程控制(进程创建与终止 | 进程等待 | 程序替换)相关推荐

  1. Linux——进程控制:创建、终止、等待、替换

    进程创建 fork #include <unistd.h> pid_t fork(void); 操作系统做了什么? 调用fork之后,内核的工作: 分配新的内存块和内核数据结构给子进程 将 ...

  2. linux——进程(创建、终止、等待、替换)

    进程的基本操作 概念 程序运行的一个实例,其占有一定的空间. 查询某一进程当前情况 ps aux | grep 进程名 终止进程 kill -9 pid: //pid指需要终止的进程pid 创建 pi ...

  3. 【Linux】Linux进程控制 --- 进程创建、终止、等待、替换、shell派生子进程的理解…

    柴犬: 你好啊,屏幕前的大帅哥or大美女,和我一起享受美好的今天叭

  4. 进程控制(2):进程操作

    前言: 关于进程控制这块的好文很多,下面转载的这篇内容很丰富,也会举适当的栗子,与其写的一知半解不如参读学习别人的优秀博文,感谢原作,本系列摘录自:https://www.cnblogs.com/xi ...

  5. Linux无法终止进程,如何在Linux中终止进程

    您是否曾经遇到过启动应用程序,而在使用该应用程序时突然变得无响应并意外崩溃的情况?您尝试再次启动该应用程序,但没有任何反应,因为原始应用程序进程从未真正完全关闭. 好吧,这件事发生在我们所有人身上,不 ...

  6. 【Linux】进程控制(创建、终止、等待)

    环境:centos7.6,腾讯云服务器 Linux文章都放在了专栏:[Linux]欢迎支持订阅 相关文章推荐: [Linux]冯.诺依曼体系结构与操作系统 [Linux]进程理解与学习Ⅰ-进程概念 [ ...

  7. 进程控制-创建、退出、等待、替换

    目录 进程创建 1.子进程继承 2.写时拷贝 进程退出 echo $? 退出码 进程异常退出的情况模拟: 退出进程的方式 退出码的意义: 进程退出,在系统中发生了什么? 进程等待 为什么要有进程等待呢 ...

  8. Linux系统调用:创建和终止进程

    1.进程的三种状态 1.运行.要么在被CPU执行,要么等待被执行且最终会被内核调度. 2.停止.执行被挂起且不会被调度.收到特定信号后才能继续运行. 3.终止.进程永远地停止了.可能的原因有三种:(1 ...

  9. windows下 C 程序 调用其他程序常见新的进程CreateProcess以及通过TerminateProcess终止进程

    WinAPI执行外部程序和创建新进程: CreateProcess(NULL,cmdOp,NULL,NULL,FALSE,CREATE_NEW_CONSOLE,NULL,NULL,&si,&a ...

最新文章

  1. 【Linux 内核 内存管理】内存管理架构 ⑤ ( sbrk 内存分配系统调用代码示例 | 在 /proc/pid/maps 中查看进程堆内存详情 )
  2. matlab条件判断配合输出
  3. Pycharm、Idea、Goland 官方汉化来了
  4. Booting ARM Linux SMP on MPCore
  5. python安装pyecharts清华_基于Python安装pyecharts所遇的问题及解决方法
  6. Spring Boot 学习(一) 快速搭建SpringBoot 项目
  7. 信息学奥赛一本通(1114:白细胞计数)
  8. js脚本捕获页面 GET 方式请求的参数?其实直接使用 window.location.search 获得
  9. 第 15 章 垃圾回收相关算法
  10. 官方文档---ubuntu 安装OpenStack
  11. areact中组件antd中checkbox_19GW光伏组件中/开标价格一览!
  12. mysql主从复制1064_mysql主从复制或其他操作报错ERROR 1064 (42000): You have an er
  13. 微信jssdk开发 java_Java微信公众平台开发(十一)--微信JSSDK中Config配置
  14. 笔记本电脑频繁自动重启_电脑一直自动重启怎么办 电脑一直自动重启的原因和解决办法...
  15. Oliver Wyman的一年制PTA面试
  16. 困惑很久的解微分方程时绝对值取舍问题(必看)
  17. 让人懵逼的宏定义赋值
  18. mysql 正则 查询 手机号,移动手机号码段 正则
  19. 巧用计算机方法,第四课 巧用计算器教案.doc
  20. 服务器的固态硬盘使用raid非ssd,在VMware ESXi中使用固态硬盘的注意事项

热门文章

  1. mysql忘记密码如何重置密码,以及修改root密码的方法
  2. mysql8.0JDBC驱动下载以及JDBC连接时遇到的问题
  3. Flash游戏存档文件 .sol文件替换+拷贝教程 附sol Editor下载
  4. project 2013 删除资源
  5. 模板_单调栈_AcWing_830. 单调栈_底顶递增栈
  6. 蚂蚁全媒体中心总编刘鑫炜解答:律师如何打造个人品牌
  7. 我的数据分析秋招经历和学习心得【万字长文】
  8. 兼容IE浏览器 媒体查询方法
  9. 英雄联盟男爵领域服务器位置,LOL男爵领域是几区?英雄联盟新区怎么升级快?
  10. 蚁群算法及TSP问题的matlab代码记录