学习资料https://pan.baidu.com/s/1NZpC4NyH0OHGOPPbO9J10g?pwd=h9m2 提取码: h9m2
一. 进程基础
1.1 进程基本概念
1. 程序 vs 进程:
- 程序 是静态的指令集合,不占用系统运行资源。
- 进程 是程序在系统中的一次动态执行过程,占用系统资源(CPU、内存、文件等),一个程序运行多次会产生多个进程。
2. Linux 进程的三种类型:
- 交互进程 :由 Shell 启动,可在前台(可与用户交互)或后台(加
&运行)运行。 - 批处理进程:与终端无直接联系,是一个按指定方式执行的进程序列。
- 守护进程:Linux 启动时运行的后台服务,屏幕不显示任何信息,在后台默默为用户服务(如各类网络服务)。
3. 执行模式与 Shell:
- 进程在用户模式 (用户空间)下运行,当发生系统调用或中断时切换为内核模式(核心程序运行,具有特权指令,不受用户干预)。
- 用户登录后打开的终端会建立一个 Shell 进程,接收用户命令并创建子进程执行;进程结束后,返回当前 Shell 继续接收新命令。
1.2 进程创建和命令执行
1. fork() 系统调用 ------ 创建子进程
- 头文件 :
#include <unistd.h> - 函数原型 :
pid_t result = fork(void); - 核心机制(返回两次) :
- 父进程 :返回新创建子进程的 PID(>0)。
- 子进程 :返回 0。
- 出错 :返回 -1。
- 内核完成的工作 :
① 为子进程分配新内存块和内核数据结构;
② 复制父进程信息(代码段、数据段等),二者拥有相同可执行代码;
③ 将新进程加入运行进程集合;
④ 执行完毕,控制权返回给两个进程,由调度器决定执行顺序。 - 关键特性(数据独立性) :
fork之后,父子进程的全局变量和局部变量虽然名称和初始值相同,但它们在物理内存中是相互独立的 ,互不影响。- 示例分析 :父进程执行
glob++(变为11),子进程执行local--(变为7),两者输出互不干扰。
- 示例分析 :父进程执行
2. exec 系列调用 ------ 执行新程序
- 当父进程希望子进程执行特定任务时,会使用
exec家族函数,用新程序的指令代码替换子进程原有的代码段、数据段和堆栈(相当于给子进程"换脑")。 - 函数分类 :
execl/execv:要求提供可执行文件的绝对或相对路径。execlp/execvp:使用PATH环境变量查找可执行文件(p代表 PATH)。- 传参方式:
l(list)逐个列举参数;v(vector)使用字符串数组(指针数组)传递参数。
- 执行流程 :将指定程序复制到调用它的进程 → 将字符串数组作为
argv[]传递 → 运行新程序。 - 核心特性 :
exec成功调用后不返回任何值,子进程原有的后续代码(如printf)将被新程序替换而不再执行;但如果exec调用失败,返回 -1,子进程会继续执行后续代码。
二. 进程退出
进程退出主要分为正常退出 和异常退出两类。
1. 正常退出的 3 种方式:
- 程序正常运行结束,进程自动消失。
- 程序调用
exit()函数退出。 - 程序调用
_exit()退出。
2. exit() 与 _exit() 的核心区别(图 5.3):
exit():属于库函数。会先刷新缓冲区 中的所有流,调用用户通过atexit和on_exit注册的函数,执行完其他与退出相关的操作后,最后才调用_exit()。_exit():属于系统调用。会直接终止进程,不刷新缓冲区,也不执行用户注册的清理函数。
3. 异常退出:
进程在运行时收到系统传来的外部信号而终止执行。例如:kill -9 pid 就是向进程号为 pid 的进程发送强制结束信号。
2.1 守护进程(Daemon)
1. 概念:
守护进程是脱离终端且在后台运行的进程。它不受终端控制,即使终端退出,它依然在后台运行(如 ftpd、crond、httpd)。系统引导时会开启很多这种服务为用户提供功能。
2. 核心概念:进程组与会话期(图 5.4)
- 进程组:一组进程的集合,由进程组 PID 表示。每个进程除了进程 ID 外,还必须属于某个进程组。通常一个终端就是进程组组长。
- 会话期:由一个或多个进程组组成的集合。一个会话期开始于用户登录,结束于用户退出。
3. 创建守护进程的 5 个步骤(图 5.5 + 例 5-5):
① 创建子进程,父进程退出 :调用 fork() 创建子进程,然后让父进程 exit()。这样子进程就变成了孤儿进程,被系统的 init 进程(PID 1)收养,从而脱离原终端的控制。
② 让子进程脱离控制终端 :在子进程中调用 setsid() 系统调用(见表 5.3)。它会创建一个新的会话,并让调用它的进程成为该会话的组长。作用是让进程摆脱原会话、原进程组、原控制终端的控制。
③ 改变当前目录为根目录 :子进程执行 chdir("/")。防止当前目录(如某个挂载点)因为守护进程的存在而无法被卸载。
④ 修改文件权限掩码 :调用 umask(0)。将文件权限掩码设置为 0,即取消子进程对文件操作的屏蔽限制,增强守护进程的灵活性。
⑤ 关闭文件描述符 :关闭从父进程继承的已打开的文件描述符。因为守护进程已与控制终端失去联系,标准输入输出(0、1、2)等文件描述符不再被使用,且会耗费系统资源,应使用 close(fd) 关闭。
2.2 僵尸进程(Zombie)
1. 产生原因:
当子进程先于父进程结束 ,而父进程没有使用 wait() 系统调用 获取子进程的结束状态时,子进程就会变成僵尸进程。
注意:如果是父进程先结束,则不会产生僵尸进程。
2. 特点与危害:
僵尸进程没有任何代码、数据或堆栈,占用资源极少,但它依然存在于系统的任务列表中 (进程表中占据一个位置)。如果大量产生,会耗尽系统的进程表资源。
识别方法 :使用 ps 命令查看时,如果进程名称旁边出现 <defunct>,则表明该进程为僵尸进程(例 5-6 演示了如何通过让父进程长时间睡眠来观察僵尸进程)。
2.3 进程退出状态与等待
为了防止子进程变成僵尸进程,父进程需要调用 wait() 或 waitpid() 来等待子进程结束并获取其状态。
1. wait() 系统调用(表 5.4)
- 头文件 :
#include <sys/wait.h>、#include <sys/types.h> - 原型 :
pid_t pid = wait(int *status); - 功能:暂停调用它的进程(父进程),直到有子进程结束。
- 返回值 :如果不存在子进程,返回 -1;若有任何一个子进程结束,则返回该子进程的 PID 并保存其返回状态在
status中。
2. waitpid() 系统调用(表 5.5)与 wait() 的区别
wait()的局限性 :只能等待任意一个子进程结束。当一个父进程有多个子进程时,若某个子进程结束,wait只能得到其结束状态,无法再得到其他子进程的状态,容易导致其他子进程变成僵尸进程。waitpid()的改进 :- 提供了非阻塞 调用方式(通过设置
Options选项,如WNOHANG:如果没有已结束的子进程则马上返回,不等待)。 - 可以等待指定的子进程 (通过传入特定的
pid参数,如pid > 0等待指定 ID 的子进程;pid = 0等待同组任意子进程等)。 status参数用于保存返回状态,options参数用于控制等待行为。
- 提供了非阻塞 调用方式(通过设置
3. 退出状态检测宏(表 5.6)
系统提供了专门的宏来检测 status 变量,判断子进程的退出情况:
WIFEXITED(status):如果子进程正常结束 ,则此宏为真,此时可调用WEXITSTATUS(status)取得子进程exit()返回的结束代码。WIFSIGNALED(status):如果子进程是因为信号而结束 ,则此宏为真,此时可调用WTERMSIG(status)取得导致子进程终止的信号代码。WIFSTOPPED(status):如果子进程处于暂停执行 情况(如收到 SIGSTOP),则此宏为真,此时可调用WSTOPSIG(status)取得引发子进程暂停的信号代码。