🌷🍁 博主 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 命令的三种常用格式为:
- kill PID
正常结束进程, 自动完成所有善后工作, 作用类似于按 Del 键.
- kill -1 PID
先挂起该进程, 终止子进程, 完成善后工作, 再终止该进程.
- kill -9 PID
立即强行终止该进程, 不作任何善后工作. 可能出现资源浪费和"孤儿"进程.
重点
进程创建、执行程序和等待进程退出。
难点
进程创建过程。
这部分过于抽象难以理解,通过代码导读能够更加形象一些,但是学生可能并不熟悉Linux源代码,因此需要在Linux入门章节增加代码结构介绍和阅读指导,然后布置和鼓励学生们自行阅读源代码,从而在讲解时更加方便,也更能激发学生的学习意愿。
习题
- 在Linux中创建进程主要有哪几种方式?
答:(1)在shell中执行命令或可执行文件。(2)在代码中(已经存在的进程中)调用函数创建子进程。
- 用fork()创建新进程时,子进程从父进程继承了哪些资源?
答:子进程复制/拷贝父进程的PCB、用户空间(数据段、堆和栈),父子进程共享正文段(只读)。
- waitpid函数的pid参数怎样设置表示等待任一子进程终止?
答:pid设置为 -1代表等待任意子进程状态改变(同wait)。
4.在Linux中什么情况下使用exec函数簇?
答:exec函数簇是用来用指定的程序替换当前进程的所有内容。exec系列函数经常在fork、vfork和clone三个函数使用之后调用,来创建一个全新的程序运行环境。exec函数簇提供了一个在进程中启动另一个程序执行的方法。
原创声明
=======
作者: [ libin9iOak ]
本文为原创文章,版权归作者所有。未经许可,禁止转载、复制或引用。
作者保证信息真实可靠,但不对准确性和完整性承担责任。
未经许可,禁止商业用途。
如有疑问或建议,请联系作者。
感谢您的支持与尊重。
点击
下方名片
,加入IT技术核心学习团队。一起探索科技的未来,共同成长。