学习资料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)取得引发子进程暂停的信号代码。