当前位置:   article > 正文

Linux系统编程·进程创建及终止_实验四 进程管理实验 通过实际上机调试和运行程序了解linux系统中的进程基本编程:

实验四 进程管理实验 通过实际上机调试和运行程序了解linux系统中的进程基本编程:

在这里插入图片描述

你好,我是安然无虞。

自学网站

推荐给老铁们两款学习网站:
面试利器&算法学习:牛客网
风趣幽默的学人工智能:人工智能学习
首个付费专栏:《C++入门核心技术》

进程创建

fork函数

fork函数在Linux中是一个很重要的函数, 它是从已经存在的进程中创建一个新的进程. 新进程为子进程, 而原进程为父进程. 所以, fork函数是用来创建子进程的.

在这里插入图片描述
返回值: fork 函数比较特殊, 它有两个返回值, 给子进程返回0, 给父进程返回子进程的pid, 出错返回-1

调用 fork 函数, 当控制转移到内核中的 fork 代码后, 内核做了几件事:

  • 分配新的内存块和内核数据结构给子进程(task_struct, mm_struct等);
  • 将父进程部分数据结构内容拷贝至子进程;
  • 添加子进程到系统进程列表当中;
  • fork 返回, 开始调度器调度.

当一个进程调用 fork 函数之后, 就会有两个二进制相同的代码的进程, 而且它们都运行到相同的地方, 但是每个进程都可以执行自己的代码.
做个小实验:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>

int main()
{
    printf("Before: pid is %d\n", getpid());
    pid_t id = fork();
    if(id == -1)
    {
        perror("fork()");
        exit(1);
    }
    printf("After: pid is %d, fork return %d\n", getpid(), id);
    sleep(1);

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

执行程序:
在这里插入图片描述
我们看到的现象是:
打印了三行内容, 一行before, 两行after, 注意哦, 进程28187没有打印before, 这是为什么呢?
在这里插入图片描述
fork之前只有父进程, 父进程独立执行, fork 之后, 有两个进程, 分子两个执行流分别执行. 注意哦, fork 之后, 谁先执行并没有硬性规定, 完全由调度器决定.

那么 fork 之后, 是否只有 fork 之后的代码是被父子进程共享的? 不是, 一般情况下, fork 之后, 父子进程共享所有的代码, 子进程执行的后续代码 != 共享的所有代码, 只不过子进程只能从这里开始执行, 如上图所示.

因为我们知道, CPU的内部是有一些寄存器的, 其中有一个eip寄存器, 叫程序计数器, 也叫PC指针, 它是保存当前正在执行指令的下一条指令. 所以eip程序计数器会拷贝给子进程, 子进程便从eip所指向的代码处开始执行.

写时拷贝

我们知道进程是具有独立性的, 通常情况下, 父子进程代码共享, 如若父子不再写入时, 数据也是共享的, 但是当任意一方试图写入, 数据便以写时拷贝的方式各自独立.

所以 fork 之后, OS做了什么?

进程 = 进程的内核数据结构 + 进程的代码和数据. 所以 fork 之后创建子进程的内核数据结构(task_struct + mm_struct + 页表等), 代码继承父进程, 数据以写时拷贝的方式, 来共享或独立.

为什么要写时拷贝?

我们说了这么多次写时拷贝, 为什么要发生写时拷贝呢, 创建子进程的时候, 就把数据分开不行吗?
当然不行, 原因有三:

  • 父进程的数据, 子进程不一定会用, 即便使用, 也不一定全部写入, 所以会有浪费空间的嫌疑;
  • 最理想的情况, 只有会被父子修改的数据, 进行分离拷贝, 不需要修改的共享即可, 但是这样的话, 技术角度实现复杂;
  • 如果fork的时候就无脑拷贝数据给子进程, 会增加 fork 的成本, 比如内存和时间等.

所以采用写时拷贝的方式, 只会拷贝父子修改的数据, 是拷贝数据的最小成本, 但是拷贝的成本依然是存在的, 这也可以认为是一种延时拷贝策略 (只有你真正使用的时候才给你, 你想要, 但是不立马使用的空间, 先不给你, 这也就意味着可以先给别人) 变相的提高内存的使用率.

写时拷贝本身就是由OS的内存管理模块完成的.

在这里插入图片描述

fork 函数的常规用法:

  • 一个父进程希望复制自己, 使父子进程同时执行不同的代码段;
  • 一个进程要执行不同的程序(后面程序替换时详细讲解)

fork 函数调用失败的原因:

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

进程终止

进程退出场景

进程退出总共有三种场景:

  • 代码跑完, 结果正确;
  • 代码跑完, 结果不正确;
  • 代码没跑完, 程序异常.

1,2两种情况关注的是退出码, 第3种情况关注的是退出信号, 这个进程等待的时候详细介绍.

关于进程终止的正确认识:
我们之前在编写 C/C++ 代码的时候, main函数是入口函数, 进程return 0. 下面有几个问题, 看看你知道吗:

  1. return 0, 给谁return?
  2. 为何是0? 其他值可以吗?

进程代码执行完毕, 我们想要知道结果是否正确, 常用0表征成功, 用非零表征失败.

为什么用非零表征失败呢? 因为当结果错误, 我们最想知道的是失败的原因, 所以用非零标识不同的原因, main函数中, return x 代表的是进程退出码, 用来表征进程退出的信息, 让父进程读取的. 所以return 0, 是给父进程return 的.

一般而言, 失败的非零值我该如何设置呢, 以及默认表达的含义?
错误码, 退出码可以对应不同的错误原因, 方便我们定位问题, 这个放在后面讲解.

关于进程终止的常见做法:

1.在main函数中return, 代表进程退出, 为什么其他函数中不行呢?
其他函数中return , 代表的是函数调用结束, 函数返回.

2.在自己的代码任意地点中, 调用exit()
其中exit() 用的多.
exit() 终止进程, 刷新缓冲区;
_exit() 终止进程, 不会刷新缓冲区.

在这里插入图片描述
exit 函数最后也会调用_exit 函数, 但在此之前, 还做了其他动作:
在这里插入图片描述

关于进程终止, 内核做了什么?

进程 = 进程的内核数据结构 + 进程的代码和数据

进程终止后, 代码和数据会被释放, 当时进程的内核数据结构, 如task_struct, mm_struct等, 操作系统可能并不会释放该进程的内核数据结构, 因为用task_struct, mm_struct 这些内核数据结构创建对象的时候, 要开辟空间和初始化啥的, 这些都是需要花时间的.

了解部分:
OS将不同的数据结构全部维护到一个链表中, 空间并没有释放, 只是设置为无效, 当再次创建进程时, OS会直接从这里拿出来相关的task_struct 和 mm_struct这些内核数据结构, 由此省去了开辟空间所花费的时间, 这样一来, 只要处理新进程的代码和数据的初始化工作即可.这里会提到一个概念, 叫做内核的数据结构缓冲池(slab分派器)

练习题

1.如何使一个进程退出,以下错误的是
A.在程序的任意位置调用return
B.在main函数中调用return
C.在程序的任意位置调用exit接口
D.在程序的任意位置调用_exit接口

解析:
退出进程的方式咱们讲到了三种:

  • 在main函数中return
  • 主任意位置调用库函数 exit
  • 在任意位置调用系统调用 _exit

2.关于进程退出返回值的说法中,正确的有
A.进程退出的返回值可以随便设置
B.进程的退出返回值可以在父进程中通过wait/waitpid接口获取
C.程序异常退出时,进程返回值为-1
D.进程的退出返回值可以在任意进程中通过wait/waitpid接口获取

解析:
进程的退出返回值也不能随意设置,因为进程的退出返回值实际上只用了一个字节进行存储,因此随意设置可能会导致实际保存的数据与设置的数据不同的情况,因为过大会导致数据截断存储;
waitpid(int pid, int *status, int options); 函数中 status参数 用于父进程获取退出子进程的返回值;
程序异常退出时,意味着程序并没有运行到return/exit去设置返回值,则返回值不做评判标准,因为返回值的存储位置的数据是一个未知随机值

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/秋刀鱼在做梦/article/detail/891793
推荐阅读
相关标签
  

闽ICP备14008679号