文章目录

  • 一、进程创建
    • 1.1 系统调用 fork
    • 1.2 理解 fork 的返回值
    • 1.3 写时拷贝策略
  • 二、进程终止
    • 2.1 main 函数的返回值
    • 2.2 进程退出的几种情况
    • 2.3 进程退出码
    • 2.4 终止正常进程:return、exit、_exit
    • 2.5 站在 OS 角度:理解进程终止
  • 三、进程等待
    • 3.1 进程等待的必要性
    • 3.2 如何「进程等待」:wait、waitpid 函数
      • 3.1.1 wait 函数
      • 3.1.2 waitpid 函数 ⭐
        • ① status 参数:
          • 1、获取子进程的退出码
          • 2、获取子进程的终止信号
          • 3、代码实现:一个完整的进程等待 ⭐
        • ② options 参数:
          • waitpid 的两种等待方式:阻塞 & 非阻塞
      • 补充:如何理解阻塞/等待
      • 补充:内核源码中的退出码和终止信号
      • 3.1.3 总结
  • 四、进程的程序替换
    • 4.1 前言
    • 4.2 替换原理
    • 4.3 如何替换:exec 系列函数
      • execl 函数
      • execv 函数
      • execlp 函数
      • execle 函数(用的很少)

一、进程创建

目前学习到的进程创建的两种方式:

  1. 命令行启动命令(程序、指令等) 。
  2. 通过程序自身,调用 fork 函数创建出子进程。

1.1 系统调用 fork

Linux 中的系统接口 fork 函数是非常重要的函数,它从已存在进程(父进程)中创建一个新进程(子进程):

#include<unistd.h>
pid_t fork(void);  // 返回值:子进程中返回0,父进程中返回子进程id,出错返回-1

进程调用 fork,当控制转移到内核中的 fork 函数代码后,操作系统内核会做

  • 分配新的内存块和内核数据结构(task_struct)给子进程。
  • 以父进程为模板)将父进程的内核数据结构中的部分内容拷贝至子进程。
  • 添加子进程到系统进程列表当中(因为进程要被调度和执行)。
  • fork 函数返回后,开始调度器调度。

fork 的常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。

    例如:父进程等待客户端请求,生成子进程来处理请求。

  • 一个进程要执行一个不同的程序。

    例如:子进程从 fork 返回后,调用 exec 函数。

fork 调用失败的原因:

  • 系统中有太多的进程,系统资源不足。

  • 实际用户的进程数超过了限制。


1.2 理解 fork 的返回值

当一个进程调用 fork 之后,在不写入的情况下,用户的代码和数据是父子进程共享的。就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序。

#include<stdio.h>  // perror
#include<unistd.h> // getpid, getppid, forkint main()
{  // ...pid_t ret = fork(); // 返回时发生了写时拷贝if (ret == 0) {// child processwhile (1) {printf("child process, pid:%u, ppid:%u\n", getpid(), getppid());sleep(1);}}else if (ret > 0) {// father processwhile (1) {printf("father process, pid:%u, ppid:%u\n", getpid(), getppid());sleep(1);}}else {// failureperror("fork");}return 0;
}

fork 之前父进程独立执行,fork 之后父子进程分别执行。注意:fork 之后谁先执行完全由调度器决定。

画图理解 fork 函数:

思考

  1. 为什么 fork 有两个返回值,从而使父子进程进入不同的业务逻辑。为什么 fork 的返回值会返回两次呢?

    fork 函数中的 return 语句是被父子进程共享的,所以都会被父子进程执行。当 frok 返回时,会往变量 ret 中写入数据(如:pid_t ret = fork(); ),发生了写时拷贝,导致 ret 有两份,分别被父子进程私有。(代码共享,数据各自私有)

  2. 返回值 ret 变量名相同,为什么会有两个不同的值呢?

    变量名相同,有两个不同的值,本质是因为被映射到了不同的物理地址处。


1.3 写时拷贝策略

写时拷贝是一种延时操作的策略,为什么要有写时拷贝呢?写时拷贝的好处是什么?

  1. 为了保证父子进程的独立性!(数据各自私有一份)
  2. 不是所有的数据,都有必要被拷贝一份(比如只读的数据)。写时拷贝可以节约资源
  3. fork 时,如果把所有的数据都拷贝一份,是需要花费时间的,降低了效率。写时拷贝可以提高 fork 执行的效率。
  4. fork 创建子进程本身就是向操作系统要资源,如果把所有的数据都拷贝一份,要更多的资源,更容易导致 fork 失败。写时拷贝可以减少 fork 失败的概率。

二、进程终止

2.1 main 函数的返回值

我们在写 C/C++ 代码时,main 函数里面我们总是会返回 0,比如:

#include<stdio.h>
int main()
{printf("hello world\n");return 0;
}

思考:为什么 main 函数中总是会返回 0 ( return 0; )呢?

  • main 函数中的这个返回值叫做:「进程退出码」,用来表示进程退出时,其执行结果是否正确。
  • 返回的 0 是给操作系统看的,来确认进程的执行结果是否正确。(0 通常表示成功)

用户可以通过命令 echo $? 查看最近一次执行的程序的「进程退出码」,比如:

[ll@VM-0-12-centos 12]$ ./test
hello world
[ll@VM-0-12-centos 12]$ echo $?  # 查看最近一次执行的程序的退出码
0

2.2 进程退出的几种情况

  1. 代码跑完,结果正确。(退出码:0)
  2. 代码跑完,结果不正确。(一般是代码逻辑有问题,但没有导致程序崩溃,退出码:非0)
  3. 代码没跑完,程序非正常终止了。(这种情况下,退出码已经没有意义了,是由信号来终止,比如 ctrl+c)

2.3 进程退出码

父进程创建子进程的目的是为了让子进程给我们完成任务,父进程需要通过「子进程的退出码」知道子进程把任务完成的怎么样。

比如在生活中,网页打不开时,用户需要通过返回的一串错误代码得知网页出错的原因:

退出码可以人为的定义,也可以使用系统的错误码列表(错误码 (int) 与错误码描述 (string) 之间的映射表)

比如:C 语言库中提供一个接口,可以把「错误码」转换成对应的「错误码描述」,程序如下:

#include<stdio.h>
#include<string.h> // strerrorint main()
{for (int i = 0; i < 10; i++) {printf("%d -- %s\n", i, strerror(i)); // char *strerror(int errnum);} return 0;
}

运行结果:

[ll@VM-0-12-centos 12]$ ./test
0 -- Success
1 -- Operation not permitted
2 -- No such file or directory
3 -- No such process
4 -- Interrupted system call
5 -- Input/output error
6 -- No such device or address
7 -- Argument list too long
8 -- Exec format error
9 -- Bad file descriptor

2.4 终止正常进程:return、exit、_exit

注意:⭐

  • 只有 main 函数中的 return 表示的是终止进程,非 main 函数中的 return 不是终止进程,而是结束函数。
  • 在任何函数中调用 exit 函数,都表示直接终止该进程。

库函数:exit

#include <stdlib.h>
void exit(int status);  // 终止正常进程
// 参数 status: 定义了进程的终止状态,父进程通过 wait 函数来获取该值

系统调用:_exit

#include <unistd.h>
void _exit(int status);  // 终止正在调用的进程

系统调用接口 _exit 的功能也是终止正在调用的进程,它和库函数 exit 有什么区别呢?

  • exit:在进程退出的时候,会进行后续资源处理(比如刷新缓冲区)。
  • _exit:在进程退出的时候,不会进行后续资源处理,直接终止进程。

补充

其实,库函数 exit 最后也会调用系统接口 _exit,但在调用 _exit 之前,还做了其他工作:

  1. 执行用户通过 atexit 或 on_exit 定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入。
  3. 调用 _exit


2.5 站在 OS 角度:理解进程终止

站在操作系统角度,如何理解进程终止?

  1. “ 释放 ” 曾经为了管理该进程,在内核中维护的所有数据结构对象。

    注意:这里的 “ 释放 ” 不是真的把这些数据结构对象销毁,即占用的内核空间还给 OS;而是设置成不用状态,把相同类型的对象归为一类(如进程控制块就是一类),保存到一个 “ 数据结构池 ” 中,凡是有不用的对象,就链入该池子中。

    我们知道在内核空间中维护一个内存池,减少了用户频繁申请和释放空间的操作,提高了用户使用内存的效率,但每次从内存池中申请和使用一块空间时,还需要先对这块空间进行类型强转,再初始化。

    现在有了这些 “ 数据结构池 ” ,比如:当创建新进程时,需要创建新的 PCB,不需要再从内存池中申请一块空间,进行类型强转并初始化,而是从 “ 数据结构池 ” 中直接获取一块不用的 PCB 覆盖初始化即可,减少了频繁申请和释放空间的过程,提高了使用内存的效率。

    这种内存分配机制在 Linux 中叫做 slab 分配器。

  2. 释放程序代码和数据占用的内存空间。

    注意:这里的释放不是把代码和数据清空,而是把占用的那部分内存设置成「未使用」就可以了。

  3. 取消曾经该进程的链接关系。


三、进程等待

3.1 进程等待的必要性

  • 子进程退出,父进程还在运行,但父进程没有读取到子进程状态,就可能造成「僵尸进程」的问题,进而导致内存泄漏。

    退出状态本身要用数据维护,也属于进程的基本信息,所以保存在 task_struct(PCB) 中,换句话说,僵尸进程一直不退出,它对应的 PCB 就要一直维护。

  • 另外,进程一旦变成僵尸状态,命令 kill -9 也无能为力,因为没有办法杀死一个已经死去的进程。

  • 最后,父进程需要知道派给子进程的任务完成的如何。(如:子进程运行完成,运行结果对不对,有没有正常退出,还有根据进程退出信息制定出错时的一些策略)


思考:为什么要有进程等待?

  1. 等待子进程终止,回收僵尸进程,从而解决内存泄露问题。

  2. 获取子进程的退出信息。—— 不是必须的,需要就获取,不需要就不获取。

    因为父进程需要知道派给子进程的任务完成的如何,有没有正常退出,还可以根据进程退出信息制定出错时的一些策略。

  3. 尽量保证父进程要晚于子进程退出,可以规范化的进行资源回收。—— 这是编码方面的要求,并非系统。

总结:父进程通过进程等待的方式:回收子进程资源,防止内存泄漏获取子进程的退出信息。⭐


3.2 如何「进程等待」:wait、waitpid 函数

系统调用 waitwaitpid - 等待任意一个子进程改变状态,子进程终止时,函数才会返回。(其实就是等待进程由 R/S(运行/睡眠) 状态变成 Z(僵尸) 状态,然后父进程读取子进程的状态,操作系统回收子进程)

3.1.1 wait 函数

#include <sys/types.h>
#include <sys/wait.h>pid_t wait(int *status);
/*
* wait() 系统调用:暂停正在调用进程的执行,直到它的一个子进程终止。
* 调用 wait(&status) 等价于 waitpid(-1, &status, 0);
*/

参数:

  • status:输出型参数,获取子进程退出状态,不关心则可以设置成为 NULL。

返回值:

  • 成功时,返回终止子进程的进程 ID,出错时,返回 -1。

【Linux】进程控制(进程创建、进程终止、进程等待、进程替换)相关推荐

  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. 【Linux】进程控制(创建、终止、等待)

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

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

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

  6. MFC关于进程使用:创建、关闭及查询进程

    // 启动进程 bool StartProgress(CString& strError) {CString strExeName;strExeName.Format(_T("%s& ...

  7. Linux系统编程36:多线程之线程控制之pthread线程库(线程创建,终止,等待和分离)

    文章目录 (1)POSIX线程库 (2)pthread_create--创建线程 A:关于Linux线程的再理解 B:线程ID及地址空间布局 (3)pthread_exit--线程终止 (4)pthr ...

  8. Linux_进程控制(创建进程,等待进程,进程终止)

    文章目录 1.创建进程 1.1 fork()函数初识 1.2 fork()创建进程代码示例 2.等待进程 2.1 进程等待概念 2.2进程等待必要性 2.3 进程等待方法 2.3.1 wait 2.3 ...

  9. (王道408考研操作系统)第二章进程管理-第一节3:进程控制(配合Linux讲解)

    文章目录 一:如何实现进程控制 二:进程控制原语 (1)进程创建 A:概述 B:补充-Linux中的创建进程操作 ①:fork() ②:fork()相关问题 (2)进程终止 A:概述 B:补充-僵尸进 ...

  10. Linux进程控制(一)

    文章目录 进程创建 fork函数进一步探讨 写时拷贝 进程终止 进程退出场景 进程终止时,操作系统做了什么? 三大终止进程函数 进程等待(阻塞) 进程等待的必要性 进程等待的两种函数 获取子进程参数s ...

最新文章

  1. 人脸检测识别文献代码
  2. PaddlePaddle 中的若干基础命令中的问题
  3. 最好的浏览器排行榜_PG是最好的数据库;TiDB 4.0前瞻;SequoiaDB高可用原理;20c DG新特性... 数据库周刊第18期...
  4. swift与OC之间不得不知道的21点
  5. C++什么是内存泄漏
  6. 206块积木,72套进阶玩法!玩转STEAM教育,帮你省掉上万块的乐高课
  7. uint8 转换为 float
  8. c语言计算机猜数字100以内,求一个猜数字C语言代码,要求如下 计算机生成一个100以内的随机数,玩家来猜 记录猜的次数,最后打...
  9. dsd语言证书c1是什么,DSD一级德语语言证书考试在嘉兴高级中学举行
  10. CSDN西安分站俱乐部聚会归来记
  11. 204页数字化转型:集团企业信息化规划方案
  12. Max Script|加密写法
  13. APP中使用UI交互设计动效的三个好处
  14. 第一章概述-------第一节--1.1计算机网络在信息时代中的作用
  15. jMonkeyEngine译文 FlagRush1——通过SimpleGame创建你的第一个应用程序
  16. 国庆节,一天开发一个小程序+Web系统。2.5K到手。【分享开发经验】【收藏起来】
  17. VSCode更新失败无法打开,快捷方式无法正常工作
  18. MAC 部分目录作用
  19. 小程序FMP优化实录,已拿offer附真题解析
  20. HGAME 2022 WEB

热门文章

  1. 3des解密 mysql_加密解密
  2. python 人工智能培训班(python的诠释)
  3. 60mph和kmh换算_100mph等于多少kmh
  4. Linux系统将几块磁盘制作lvm_linux卷并分区挂载
  5. 米家小相机最新固件_#本站首晒#699元的运动相机 — 小米 米家小相机开箱简评...
  6. springboot 链接elasticsearch
  7. python练习10
  8. Linux系统性能监控
  9. 推荐一个可以下载任何插件的网站
  10. Java方法类的重载和使用