目录

一、进程创建

1.1 深入 fork 函数

1.2 写时拷贝

二、进程终止

2.1 进程退出码

2.2 exit 与 _exit

三、进程等待

3.1 进程等待必要性

3.2 进程等待

3.2 wait 与 waitpid

3.3 获取子进程 status

3.4 非阻塞等待


一、进程创建

1.1 深入 fork 函数

在 Linux 中 fork 函数是一个非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

fork 函数的返回值:

  • 给父进程返回子进程的 pid
  • 给子进程返回0

接下来我们举例使用一下fork函数 ()

我们编译,然后运行一下:

fork 的常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

fork调用失败的原因

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

在重温了一下 fork 函数的使用后,接下来我们来研究一个话题:

fork() 创建子进程,操作系统做了哪些操作?

进程调用 fork,当控制转移到内核中的fork代码后,内核做了以下操作:

  1. 分配新得内存块和内核数据结构给子进程。
  2. 将父进程部分数据结构内容拷贝至子进程。
  3. 添加子进程到系统进程列表中。
  4. fork返回,开始调度器调度。

父进程执行完 fork之前的代码(before)后,调用 fork 创建子进程,父子两个执行流分别执行。注意:fork 之后,谁先执行完全由调度器决定

这里还有一个问题,当fork之后,父子进程代码共享是 after 共享,还是所有代码都进行共享?为什么子进程总准确地执行 fork 之后对应的代码?

答案: 所有代码共享,因为CPU记录了进程的执行位置。

  • 代码进行汇编之后,会有很多行代码,而且每行代码加载到内存之后,都有对应的地址。
  • 因为进程随时都可能被中断(可能并没有执行完),下次继续执行时,还必须从之前的位置继续运行(并不是程序最开始或main函数处),这就要求 CPU 必须实时记录下当前进程执行的位置。
  • 所以,CPU内有对应的寄存器数据,用来记录当前进程的执行位置,此寄存器叫做EIP,也称作为pc(point code 程序计数器),用来记录正在执行代码的下一行代码的地址(上下文数据)。
  • 当子进程创建时,会修改其EIP。此时子进程便会认为EIP的中保存下的数据,就是要执行的代码。

创建子进程时,操作系统给子进程分配对应的数据结构,子进程独立运行,因为进程具有独立性。

理论上,子进程也要有自己的代码和数据,但是一般而言,创建子进程没有加载的过程,子进程本身并没有自己的代码和数据。

所以,子进程只能 "使用" 父进程的代码和数据,而代码是只读的,父子共享不会冲突;而数据是可能被修改的,必须进行分离。

这时,操作系统便采用写时拷贝的策略。

1.2 写时拷贝

OS 为何采用写时拷贝技术,对父子进程进行分离

  1. 写入时再进行拷贝,是高效使用内存的一种表现。
  2. 提高了系统的运行效率。
  3. OS无法在代码执行前预知哪些空间会被访问。

二、进程终止

当进程终止时,操作系统释放了进程申请的相关内核数据结构和对应的代码的数据,其本质就是释放系统资源,

2.1 进程退出码

进程终止的常见方式:

  1. 代码跑完,结果正确。
  2. 代码跑完,结果不正确。
  3. 代码没有跑完,程序崩溃了。

区分第一种情况和第二种情况我们可以通过进程的退出码很清晰的辨别。

关于学习的 C语言中 main 函数的返回值,其中 main 函数的返回值就是进程的退出码。其意义是返回给上一级进程,用来评判该进程执行结果。

现在我们编写一个简单的 C 程序。

然后我们可以通过 echo $? 获取最近一个进程的退出码。

进程的返回值有 0 和非0两种情况,其中 0 表示程序成功运行并结果正确,而非0表示成功运行但结果有误,非零值有无数个,不同的非零值就可以就表示着不同的错误,方便我们定义错误的原因。

那常见的错误信息有哪些呢?

我们可以使用 strerror 将其打印出来

结果如下:

发现 linux 下,共有133条错误码

当然,程序崩溃的时候,退出码没有意义。

众所周知,Linux 是用C语言写的,其中命令本质就是C语言程序,所以我们可以简单的拿 ls 命令来举例

而 2 号退出码对应的报错信息:

2.2 exit 与 _exit

关于终止一个进程可以使用 return 语句,还可以调用 exit 和 _exit 函数

exit函数:

_exit函数:

关于这两个函数的区别有很多,我们先举一个小例:

我们接下来使用 printf 打印一条信息,然后sleep三秒,再使用 exit 退出,并观察结果

因为我们带上了 \n ,加上 \n 会刷新缓冲区,屏幕上即出现我们打印的内容。

如果我们不带上 \n ,我们再观察结果:

发现:因为没有 \n ,所以 printf 中的内容并没有在休眠前被打印出来,而是调用 exit 后将缓冲区的内容刷新出来输出在屏幕上。

接下来我们使用 _exit 函数

运行可执行文件 b:

发现什么内容都没有被打印出来,使用 echo $? 打印最近进程退出码,发现 b 文件确实被执行了。

这说明,exit是库函数,而_exit 是系统调用,其退出进程时并没有刷新缓冲区中的内容。

此时我们便能得出一个结论:

printf 数据是保存在"缓冲区"中的,exit可以将其刷新,而系统调用接口_exit不能将其刷新。所以,缓冲区必定不在操作系统内部,而是由C标准库维护的。

三、进程等待

3.1 进程等待必要性

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成"僵尸进程"的问题,进而造成内存泄漏。
  • 另外,进程一旦编程僵尸状态,那就刀枪不入,即使 kill -9 命令也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成得如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程的资源,获取子进程的退出信息。

3.2 进程等待

接下来我们编写一个示例,子进程退出,父进程仍然在运行的程序。

3.2 wait 与 waitpid

以上子进程就进入了僵尸状态,既然kill -9都终止不了的进程,我们应该采取什么样的方法才能让子进程退出呢?

接下来介绍的两个系统调用接口——wait与waitpid。其功能是等待一个进程的状态改变。

 pid_t wait(int*status);
  • 返回值: 成功返回被等待进程pid,失败返回-1.
  • 参数:输出型参数,获取子进程的退出状态(即status),不关心则可以设置为NULL 。

pid_t  waitpid(pid_t pid,int* status,int options);

  • 返回值:​​​​​​​​​​​​​​​​​​​​​​​​​​​​

    • 正常返回的时候 waitpid 返回收集到的子进程的进程ID。
    • 如果设置了选项WNOHANG,而调用 waitpid 发现没有已退出的子进程,则返回0
    • 如果调用中出错,则返回-1。
  • 参数
    • pid

      • pid = -1,等待任一子进程。与wait等效。
      • pid > 0 ,等待其进程ID与pid相等的子进程。
    • status
      • WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
      • WEXITSTATUS(status): 若 WIFEXITED 非零,提取子进程退出码。(查看进程的退出码。
    • options
      • ​​​​​​​传入 0 ,表示阻塞式等待。
      • 传入 WNOHANG,表示非阻塞等待,若 pid 指定的子进程没有结束,则waitpid()函数返回0,不予以等待,若正常结束,返回子进程的pid。

接下来,我们就可以使用wait函数将上面的代码进行修改了。

运行结果:

接下来我们使用waitpid()接口 

waitpid中的option选项默认为0,表示阻塞等待,WNOHANG为非阻塞等待。

结果如下:​​​​​​​

这样,使用wait和waitpid的方法就成功解决了子进程运行结束进入僵尸进程无法回收的问题。

  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立刻返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在却正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。

3.3 获取子进程 status

  • wait 和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

status 不能简单的当作整体来看待,可以当作位图来看待,具体细节如下图:(只研究)

其中status的低7位,是子进程收到的信号,可以使用 status&0x7f 取出。

status的次低8位,是子进程的退出码吗,可以使用 (status>>8)&0xff 取出。

让我们来看看结果:

除了以上的位操作,还可以使用系统为我们预设的宏来提取信号和退出码:

  • WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否正常退出)
  • WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

写段代码来使用一下:

结果如下:

其中这个信号是什么东西呢?我们接下来简要的了解一下。

进程退出可以分为三种情况:

  1. 代码跑完,结果正确。即退出码为0。
  2. 代码跑完,结果有误,即退出码非0。
  3. 代码运行,程序异常退出或奔溃,操作系统杀死进程。

那既然程序异常退出或者奔溃是操作系统杀掉了进程,那操作系统是如何杀掉进程的呢?

本质就是通过发送信号的方式来杀死进程,那常见的信号有哪些呢?

接下来我们来展示一下子进程收到的信号。

比如我们在子进程中进行整数除以0、指针的操作,然后从父进程中提取出子进程的信号。

11号信号,就是上图中的11) SIGSEGV,linux中最常见的段错误报错(在windows下就是红色弹窗的形式)。

程序异常,除了上面程序内部出现问题的情况,还有一种也可能是外力直接杀掉。例如,现在给子进程设置死循环,然后外部使用kill -9命令杀死子进程,观察其信号:

结果:

注意!!!程序既然奔溃了,退出码便没有意义,即使上图显示子进程退出码显示为0,其实也没有任何意义了。

3.4 非阻塞等待

以上的这些方式都是阻塞式等待,父进程什么都不做,进入阻塞队列等待子进程状态结束。这种等待方式很明显是不好的,所以这里我们来学习非阻塞等待。

设置为阻塞等待还是非阻塞等待就是根据 waitpid 中的第三个参数 option 来决定的。

接下来我们写一段代码来感受一下非阻塞等待。

结果如下:

不能发现,非阻塞等待的最大优势就在于,不会傻等子进程状态;而是调用waitpid时,发现子进程未退出,就可以暂时去执行自己本身的代码。

这里还有最后两个问题:

1.父进程通过wait/waitpid可以拿到子进程的退出结果,为什么要用wait/waitpid函数?

答:因为进程具有独立性,那么数据会发生写时拷贝,父进程无法拿到。

2.既然进程具有独立性,进程退出码,不也是子进程的数据吗?wait/waitpid是如何拿到的?

答:task_struct中保留了任何进程退出时的退出结果信息,而wait/waitpid就是从子进程的task_struct中取出数据然后位操作放入status中。

【Linux】进程创建、进程终止、进程等待相关推荐

  1. 进程控制(进程创建与终止 | 进程等待 | 程序替换)

    文章目录 一.进程创建 1. fork函数 2. fork创建进程 3. 写时拷贝 二.进程终止 1. 进程退出有三种情况 2. 常见进程终止方法 三.进程等待 背景(必要性) 1. 进程等待的方法 ...

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

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

  3. 模拟进程创建、终止、阻塞、唤醒原语_轻松搞定进程原理

    进程简介 并发和并行 并发:在一个时间段中多个程序都启动运行在用一个处理机中 并行:两个进程分别由不同的CPU管理执行,两个进程不抢占CPU的资源,且可以同时运行,叫做并行 区别在于是否同时 多进程的 ...

  4. 模拟进程创建、终止、阻塞、唤醒原语_操作系统第二章--进程的描述与控制

    操作系统第二章--进程的描述与控制 前趋图和程序执行 前趋图 前趋图是一个有向无循环图DAG,用来描述进程之间执行的前后关系 初始结点:没有前趋的结点 终止结点:没有后继的结点 重量:表示该结点所含有 ...

  5. 实验六:分析Linux内核创建一个新进程的过程

    20135108 李泽源 阅读理解task_struct数据结构http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h ...

  6. 如何在 Linux 命令行中终止进程?

    如果你想在linux上停止某个进程,你会怎么操作? 如果命令/进程在前台运行,您可以使用 Ctrl+C 终端快捷方式,但是,如果进程不可见(在后台运行),您可以使用专用命令"杀死它" ...

  7. 父进程创建五个子进程

    父进程创建五个子进程 #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<sys ...

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

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

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

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

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

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

最新文章

  1. python 常用内置函数_Python小白必备的8个最常用的内置函数(推荐)
  2. Linux——进程控制(总结)
  3. CGAL window 10安装、Demo使用步骤以及问题解决记录
  4. 17种常用的JS正则表达式 非负浮点数 非负正数
  5. Android官方开发文档Training系列课程中文版:连接无线设备之通过WIFI创建P2P连接
  6. iatf16949内审员_申请IATF16949认证有什么要求
  7. poi导出Excel(分行单元格颜色设置,字体设置,合并单元格,插入图片)
  8. 解密昇腾AI处理器--DaVinci架构(控制单元)
  9. mysql update case when和where之间的注意事项
  10. java timer指定线程池_Java 定时器(Timer)及线程池里使用定时器实例代码
  11. 「每天一道面试题」对象和GC Roots引用链没连接时一定会被回收吗
  12. C盘空间太小,建议删除如下文件以释放空间!
  13. Google亲儿子 Nexus/Pixel 手机刷机Root之旅
  14. 讨论8QAM及16QAM的星座模型
  15. java jdk9_jdk9下载-jdk9下载9.0.4 官方最新版-西西软件下载
  16. IP协议及IPV4地址
  17. net share列出了Windows的默认共享(包括C盘)
  18. 2018年,硅谷的P2P公司们为啥没跑路?
  19. CloudPhone真的快要来了
  20. Linux配置sendmail实现PHP发送邮件

热门文章

  1. 关于南通大学教务管理系统微信公众号的个人看法:
  2. PTA习题7-1 选择法排序 (20 分)
  3. html设置内容居中,设置div内容居中
  4. FBEC2020 | 第五届金陀螺奖投票倒计时5天,专家评审团明星阵容豪华亮相
  5. Redis的高可用性
  6. 微信参数 sha1 加密
  7. 【番外】利用Excel表格绘制数据透视图
  8. 实现短信验证码登录——学习笔记
  9. STUN/TURN协议
  10. Java工程师学习路线