《Linux操作系统编程》 第六章 Linux中的进程监控: fork函数的使用,以及父子进程间的关系,掌握exec系列函数

简介: 《Linux操作系统编程》 第六章 Linux中的进程监控: fork函数的使用,以及父子进程间的关系,掌握exec系列函数

🌷🍁 博主 libin9iOak带您 Go to New World.✨🍁

🦄 个人主页——libin9iOak的博客🎐
🐳 《面试题大全》 文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺
🌊 《IDEA开发秘籍》学会IDEA常用操作,工作效率翻倍~💐
🪁🍁 希望本文能够给您带来一定的帮助🌸文章粗浅,敬请批评指正!🍁🐥


第六章 Linux中的进程监控

学习目的

使学生理解Linux中进程控制块的数据结构,Linux进程的创建、执行、终止、等待以及监控方法。并重点掌握fork函数的使用以及exec系列函数。

学习要求

了解:Linux进程控制块的数据结构以及进程的状态,进程的内存空间布局,特殊进程。

理解:Linux进程创建时环境变量、命令行参数的设置,理解父进程等待子进程结束和获得子进程返回值的原理;

掌握:fork函数的使用,以及父子进程间的关系,掌握exec系列函数。

学习方法

通过对进程运行与监控的相关知识点的编程学习和锻炼,培养学生们对进程相关实例问题进行分析与解决的能力

概念和原理

6.1 Linux的进程控制块

进程在内核中的表现形式:进程控制块(PCB)

Linux系统中的每个进程都有一个名为task_struct的数据结构,它相当于“进程控制块”。 task_struct是Linux内核的一种数据结构,它会被装载到内存中并且包含着进程的信息。

指向task_struct数据结构的指针形成了一个task数组。当建立新进程的时候,Linux为新进程分配一个task_struct结构,然后将指针保存在task数组中。调度程序一直维护着一个current指针,它指向当前正在运行的进程。

6.1.1 task_struct结构包含的信息

▪ 标识符 :描述本进程的唯一标识符,用来区别其他进程。

▪ 优先级 :相对于其他进程的优先级。

▪ 程序计数器:程序中即将被执行的下一条指令的地址。

▪ 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。

▪ 上下文数据:进程执行时处理器的寄存器中的数据。

▪ I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。

▪ 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等

6.1.2 task_struct:进程标识符

pid_t pid; //进程的唯一标识

pid_t tgid; // 线程组中领头线程的pid值

每个进程都必须拥有它自己的进程描述符pid。

pid是32位无符号整型数据,最大值取32767。

进程标识符是内核提供给用户程序的接口,用户程序通过pid操作程序。

6.1.3 task_struct:进程状态

volatile long state

进程状态切换

图6-1 进程状态切换

6.2 Linux中进程的创建

6.2.1 Linux中进程的创建

(1) 创建进程

▪ Linux中创建进程的方式:

- 在shell中执行命令或可执行文件

- 在代码中(已经存在的进程中)调用函数创建子进程

(2) 创建子进程-fork函数

▪ 函数原型:pid_t fork(void);

▪ 返回值:

- fork函数被正确调用后,可能会在子进程中或父进程中分别返回

- 在子进程中返回值为0(不合法的PID,提示当前运行在子进程中)

- 在父进程中返回值为子进程ID(让父进程掌握所创建子进程的ID号)

- 出错返回-1

(3) fork函数工作流程

▪ 子进程是父进程的副本

- 子进程复制/拷贝父进程的PCB、用户空间(数据段、堆和栈)

- 父子进程共享正文段(只读)

▪ 父进程继续执行fork函数调用之后的代码,子进程也从fork函数调用之后的代码开始执行

▪ 为了提高效率,fork后,子进程并不立即复制父进程数据段、堆和栈,采用了写时复制机制(Copy-On-Write)

(4) fork函数执行后父子进程的主要异同

▪ 相同

- 真实用户ID,真实组ID

- 有效用户ID,有效组ID

- 环境变量

- 堆

- 栈

- 打开的文件

▪ 不同

- fork的返回值

- 子进程ID及父进程ID

- 子进程的

- tms_utime,

- tms_stime,

- tms_cutime,

- tms_ustime

(5) fork函数的用法1

▪ 父进程希望子进程复制自己(共享代码,复制数据空间),但父子进程执行相同代码中的不同分支。

▪ 例如:网络并发服务器中,父进程等待客户端的服务请求。当请求达到,父进程调用fork创建子进程处理该请求,而父进程继续等待下一个服务请求

(6) fork函数的用法2

▪ 父子进程执行不同的可执行文件(父子进程具有完全不同的代码段和数据空间)

(7) 创建子进程-vfork函数

▪ vfork用于创建新进程,而该新进程的目的是执行另外一个可执行文件

- 子进程在调用exec或exit之前,在父进程的地址空间中运行

- vfork函数保证子进程先执行,在它调用exec或者exit之后,父进程才会继续被调度执行(父进程处于TASK_UNINTERRUPTIBLE状态)

(8) 创建进程-clone函数

▪ clone():有选择的复制父进程的资源。(带参数)

▪ int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);

- fn为函数指针,此指针指向一个函数体;

- child_stack为给子进程分配系统堆栈的指针(在linux下系统堆栈空间是2页面,就是8K的内存,其中在这块内存中,低地址上放入了值,这个值就是进程控制块task_struct的值);

- arg 就是传给子进程的参数,一般为(0);

- flags 为要复制资源的标志,描述需要从父进程继承那些资源。

(9) 创建进程流程

图6-2 创建进程流程

(10) Linux进程空间的获取

▪ 申请空白PCB详细阐述:

- do_fork() à alloc_task_struct(void)

(11) 区别fork(),vfork(),clone()

▪ 拷贝内容

- fork():子进程拷贝父进程的数据段,代码段 。

- vfork():子进程与父进程共享数据段 ;

- clone(): 有选择的复制父进程的资源。(带参数)

- 通过参数clone_flags的设置来决定哪些资源共享,哪些资源拷贝

- CLONE_VM( 共 享 地 址 空间)

- CLONE_FS|CLONE_FILES(共享文件描述符集)

- CLONE_SIGCHLD(共享信号)

▪ 访问次序控制

- fork():父子进程的执行次序不确定 。

- vfork():保证子进程先运行,在调用execve()或exit()之前,与父进程数据是共享的。

- clone():由标志CLONE_VFORK 来决定子进程在执行时父进程是阻塞还是运行,若没有设置该标志,则父子进程同时运行; 若设置了该标志,则父进程挂起,直到子进程结束为止。

6.3 Linux中进程的执行

6.3.1 Linux中进程的执行

(1) exec系列函数

fork、vfork和clone三个函数主要是Linux用来创建新的进程(线程)而设计的,exec系列函数则是用来用指定的程序替换当前进程的所有内容。exec系列函数经常在前三个函数使用之后调用,来创建一个全新的程序运行环境。exec函数簇提供了一个在进程中启动另一个程序执行的方法。

进程调用exec系列函数在进程中加载执行另外一个可执行文件

▪ execl execv execle execve execlp execvp

▪ 六个函数开头均为exec,所以称为exec系列函数

- l:表示list,每个命令行参数都说明为一个单独的参数

- v:表示vector,命令行参数放在数组中

- e:表示由函数调用者提供环境变量表

- p:表示通过环境变量PATH来指定路径,查找可执行文件

▪ PATH环境变量说明

- PATH 环境变量包含了一张目录表,系统通过 PATH 环境变量定义的路径搜索执行码

- PATH 环境变量定义时目录之间要用“:”分隔,以“.”号表示结束。

- PATH 环境变量定义在用户的.profile或.bash_profile 中

(2) execl函数

▪ 函数原型

- 头文件:unistd.h

- int execl(const char *pathname, const char *arg0, …,NULL);

▪ 参数

- pathname:要执行程序的绝对路径名

- 可变参数:要执行程序的命令行参数,以空指针结束

▪ 返回值

- 出错返回-1

- 成功该函数不返回!

(3) execv函数

▪ 函数原型

- 头文件:unistd.h

- int execv(const char *pathname, char *const argv[]);

▪ 参数

- pathname:要执行程序的绝对路径名

- argv:数组指针维护的程序命令行参数列表,该数组的最后一个成员必须为空指针

▪ 返回值

- 出错返回-1

- 成功该函数不返回

(4) execle函数

▪ 函数原型

- 头文件:unistd.h

- int execle(const char *pathname, const char *arg0,… NULL, char *const envp[]);

▪ 参数

- pathname:要执行程序的绝对路径名

- 可变参数:要执行程序的命令行参数,以空指针结束

- envp::指向环境字符串数组的指针,该数组的最后一个成员必须为空指针

▪ 返回值

- 出错返回-1

- 成功该函数不返回

(5) 其他exec函数

▪ execve函数

- int execve(const char *pathname,char *const argv[], char *const envp[]);

▪ execlp函数

- int execlp(const char *filename,const char *arg0, …,NULL);

- filename参数可以是相对路径(路径信息从环境变量PATH中获取)

- 例如默认环境变量中包含的PATH=/bin:/usr/bin:/usr/local/bin/

▪ execvp函数

- int execvp(const char *filename,char *const argv[]);

6.4 Linux中进程的终止

6.4.1 Linux中进程的终止

(1) 进程的启动与退出

▪ 进程启动

子进程和父进程共享代码段,从fork函数执行之后的代码处开始执行;exec类函数会让进程从可执行文件的main函数开始重新执行

▪ 进程退出

当一个进程结束了运行或在半途终止了运行,那么内核就需要回收该进程除进程控制块之外所占用的系统资源(包括进程运行时打开的文件,申请的内存等),并让其他进程从进程控制块中收集有关信息

(2) Linux中进程的退出方式

▪ 正常退出

- 在main函数中执行return返回

- 在任意代码中调用exit函数或_exit函数

▪ 异常退出

- 在任意代码中调用abort函数

- 进程接收到终止信号

(3) exit和return的区别

▪ exit和_exit是函数,有参数。 exit 和_exit执行完后把控制权交给内核。

▪ return是函数执行完后的返回。renturn执行完后把控制权交给调用函数。

(4) 进程正常退出函数

▪ 头文件stdlib.h ,函数定义:void exit( int status )

▪ 头文件unistd.h,函数定义:void _exit (int status )

▪ 调用这两个函数均会正常地终止一个进程

▪ 调用_exit 函数将会立即返回内核

▪ 调用exit 函数:

- 执行一些预先注册的终止处理函数

- 执行文件I/O操作的善后工作,使得所有缓冲的输出数据被更新到相应的设备

- 返回内核

(5) 缓冲I/O方式

Linux的标准函数库中,有一种被称作 “缓冲I/O”的操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续地读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区读取;同样,每次写文件的时候也仅仅是写入内存的缓冲区,等满足了一定的条件(如达到了一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。

6.5 Linux中进程的等待

6.5.1 Linux中进程的等待

(1) wait函数

▪ 功能:获取任意子进程的状态改变信息(如果是终止状态则对子进程进行善后处理)

▪ 函数原型

- 头文件:sys/wait.h

- pid_t wait(int *statloc);

▪ 参数与返回值

- statloc:用于存储子进程的状态改变信息

- 返回值:若成功返回状态信息改变的子进程ID,出错返回-1

(2) waitpid函数

▪ 功能:等待某个特定子进程状态改变

▪ 函数原型

- 头文件:sys/wait.h

- pid_t waitpid(pid_t pid, int *statloc, int options);

▪ 返回值

- 成功返回终止子进程ID,失败返回-1

▪ 参数

- pid

- pid == -1: 等待任意子进程状态改变(同wait)

- pid > 0: 等待进程ID为pid的子进程状态改变

- pid == 0: 等待其组ID等于调用进程组ID的任意子进程

- pid < -1: 等待其组ID等于pid绝对值的任意子进程

- statloc:用于存储子进程的状态改变信息

- options:可以为0,也可以是以下常量

- WNOHANG:如果没有任何已经终止的子进程则马上返回, 函数不等待,此时返回值为0

- WUNTRACED:用于跟踪调试

▪ waitpid特有的功能

- waitpid可等待一个特定的进程的状态改变信息

- waitpid可以实现非阻塞的等待操作,有时希望取得子进程的状态改变信息,但不希望阻塞父进程等待子进程状态改变

- waitpid支持作业控制(进程组控制)

(3) 获知子进程状态改变

▪ 主动获取

- 调用wait或waitpid函数等待子进程状态信息改变,并获取其状态信息

▪ 异步通知

- 当一个进程发生特定的状态变化(进程终止、暂停以及恢复)时,内核向其父进程发送SIGCHLD信号

- 父进程可以选择忽略该信号,也可以对信号进行处理(默认处理方式为忽略该信号)

(4) 僵尸进程

对于已经终止但父进程尚未对其调用wait或waitpid函数的子进程称为僵尸进程(TASK_ZOMBIE状态)。

(5) 孤儿进程

如果父进程在子进程终止之前终止,则子进程的父进程将变为init进程,保证每个进程都有父进程,由init进程调用wait函数进行善后

6.6 Linux中进程的监控

(1) 进程及进程状态

▪ UNIX/Linux系统中的“进程”是可运行程序在内存中的一次运行实例。

▪ 进程和程序的主要区别是:

- 进程是动态的, 它有自己的生命周期和不同状态; 而程序是静态的, 通常存放在某种介质(如磁盘或纸张等)上。

- 进程具有运行控制结构和作用数据区;程序没有。

- 一个程序可以同时在内存中有多个运行实例, 即同时作为多个进程的组成部分。

▪ 生命周期大致分为三种状态:

- 运行态:进程正占用CPU和其它资源进行运算.

- 就绪态:进程已做好一切准备, 等待获得CPU投入运行.

- 睡眠态:进程因等待输入输出或其它系统资源, 而让出CPU资源, 等待运行条件满足。

(2) 获取进程状态信息: ps 命令

▪ 不带参数的ps命令运行时, 显示该用户当前活动进程的基本信息:

- PID 进程标识号. 系统每个进程在其生命周期都有一个唯一的

- TTY 启动该进程的终端号

- TIME 进程累计占用CPU的时间

- COMMAND 产生该进程的命令

▪ ps命令的常用任选项 -e (或-a) 显示系统中所有活动进程的信息,而不只是本用户的进程。

▪ -f 任选项:显示每个进程的完整信息,包括完整的命令行

(3) 暂停进程运行: sleep 命令

sleep time

sleep命令使运行它的进程暂停time指定的秒数.

(4) 终止进程运行: kill 命令

用户发出 kill 命令, 强行终止后台进程或键盘锁住了的前台进程的运行.

kill 命令的三种常用格式为:

  1. kill PID

正常结束进程, 自动完成所有善后工作, 作用类似于按 Del 键.

  1. kill -1 PID

先挂起该进程, 终止子进程, 完成善后工作, 再终止该进程.

  1. kill -9 PID

立即强行终止该进程, 不作任何善后工作. 可能出现资源浪费和"孤儿"进程.

重点

进程创建、执行程序和等待进程退出。

难点

进程创建过程。

这部分过于抽象难以理解,通过代码导读能够更加形象一些,但是学生可能并不熟悉Linux源代码,因此需要在Linux入门章节增加代码结构介绍和阅读指导,然后布置和鼓励学生们自行阅读源代码,从而在讲解时更加方便,也更能激发学生的学习意愿。

习题

  1. 在Linux中创建进程主要有哪几种方式?

答:(1)在shell中执行命令或可执行文件。(2)在代码中(已经存在的进程中)调用函数创建子进程。

  1. 用fork()创建新进程时,子进程从父进程继承了哪些资源?

答:子进程复制/拷贝父进程的PCB、用户空间(数据段、堆和栈),父子进程共享正文段(只读)。

  1. waitpid函数的pid参数怎样设置表示等待任一子进程终止?

答:pid设置为 -1代表等待任意子进程状态改变(同wait)。

4.在Linux中什么情况下使用exec函数簇?

答:exec函数簇是用来用指定的程序替换当前进程的所有内容。exec系列函数经常在fork、vfork和clone三个函数使用之后调用,来创建一个全新的程序运行环境。exec函数簇提供了一个在进程中启动另一个程序执行的方法。

原创声明

=======

作者: [ libin9iOak ]


本文为原创文章,版权归作者所有。未经许可,禁止转载、复制或引用。

作者保证信息真实可靠,但不对准确性和完整性承担责任。

未经许可,禁止商业用途。

如有疑问或建议,请联系作者。

感谢您的支持与尊重。

点击下方名片,加入IT技术核心学习团队。一起探索科技的未来,共同成长。


目录
相关文章
|
15天前
|
算法 调度 UED
深入理解操作系统:进程调度与优先级队列
【10月更文挑战第31天】在计算机科学的广阔天地中,操作系统扮演着枢纽的角色,它不仅管理着硬件资源,还为应用程序提供了运行的环境。本文将深入浅出地探讨操作系统的核心概念之一——进程调度,以及如何通过优先级队列来优化资源分配。我们将从基础理论出发,逐步过渡到实际应用,最终以代码示例巩固知识点,旨在为读者揭开操作系统高效管理的神秘面纱。
|
8天前
|
消息中间件 安全 算法
深入理解操作系统:进程管理的艺术
【10月更文挑战第38天】在数字世界的心脏,操作系统扮演着至关重要的角色。它不仅是硬件与软件的桥梁,更是维持计算机运行秩序的守夜人。本文将带你走进操作系统的核心——进程管理,探索它是如何协调和优化资源的使用,确保系统的稳定与高效。我们将从进程的基本概念出发,逐步深入到进程调度、同步与通信,最后探讨进程安全的重要性。通过这篇文章,你将获得对操作系统进程管理的全新认识,为你的计算机科学之旅增添一份深刻的理解。
|
12天前
|
算法 调度 UED
深入理解操作系统:进程管理与调度策略
【10月更文挑战第34天】本文旨在探讨操作系统中至关重要的一环——进程管理及其调度策略。我们将从基础概念入手,逐步揭示进程的生命周期、状态转换以及调度算法的核心原理。文章将通过浅显易懂的语言和具体实例,引导读者理解操作系统如何高效地管理和调度进程,保证系统资源的合理分配和利用。无论你是初学者还是有一定经验的开发者,这篇文章都能为你提供新的视角和深入的理解。
34 3
|
14天前
|
Linux 调度 C语言
深入理解操作系统:进程和线程的管理
【10月更文挑战第32天】本文旨在通过浅显易懂的语言和实际代码示例,带领读者探索操作系统中进程与线程的奥秘。我们将从基础知识出发,逐步深入到它们在操作系统中的实现和管理机制,最终通过实践加深对这一核心概念的理解。无论你是编程新手还是希望复习相关知识的资深开发者,这篇文章都将为你提供有价值的见解。
|
15天前
|
算法 调度 UED
深入理解操作系统的进程调度机制
本文旨在探讨操作系统中至关重要的组成部分之一——进程调度机制。通过详细解析进程调度的概念、目的、类型以及实现方式,本文为读者提供了一个全面了解操作系统如何高效管理进程资源的视角。此外,文章还简要介绍了几种常见的进程调度算法,并分析了它们的优缺点,旨在帮助读者更好地理解操作系统内部的复杂性及其对系统性能的影响。
|
13天前
|
消息中间件 算法 调度
深入理解操作系统:进程管理的艺术
【10月更文挑战第33天】本文旨在揭示操作系统中进程管理的神秘面纱,带领读者从理论到实践,探索进程调度、同步以及通信的精妙之处。通过深入浅出的解释和直观的代码示例,我们将一起踏上这场技术之旅,解锁进程管理的秘密。
20 0
|
15天前
|
算法 Linux 调度
深入理解操作系统之进程调度
【10月更文挑战第31天】在操作系统的心脏跳动中,进程调度扮演着关键角色。本文将深入浅出地探讨进程调度的机制和策略,通过比喻和实例让读者轻松理解这一复杂主题。我们将一起探索不同类型的调度算法,并了解它们如何影响系统性能和用户体验。无论你是初学者还是资深开发者,这篇文章都将为你打开一扇理解操作系统深层工作机制的大门。
25 0
|
16天前
|
安全 Linux 数据安全/隐私保护
Vanilla OS:下一代安全 Linux 发行版
【10月更文挑战第30天】
38 0
Vanilla OS:下一代安全 Linux 发行版
|
19天前
|
人工智能 安全 Linux
|
1月前
|
Unix 物联网 大数据
操作系统的演化与比较:从Unix到Linux
本文将探讨操作系统的历史发展,重点关注Unix和Linux两个主要的操作系统分支。通过分析它们的起源、设计哲学、技术特点以及在现代计算中的影响,我们可以更好地理解操作系统在计算机科学中的核心地位及其未来发展趋势。