20242817李臻《Linux⾼级编程实践》第四周
20242817李臻《Linux⾼级编程实践》第4周
一、AI对学习内容的总结
第5章 Linux进程管理
5.1 进程基本概念
进程与程序的区别
- 程序:静态的二进制文件(如
/bin/ls
),存储在磁盘中,不占用运行资源。 - 进程:程序的动态实例,拥有独立的地址空间、资源(CPU、内存、文件句柄等),生命周期包括创建、运行、阻塞、终止。
关键差异: - 程序是“菜谱”,进程是“烹饪过程”。
- 同一程序可产生多个进程(如同时运行多个
ls
命令)。
进程类型
- 交互进程:由用户通过Shell启动(如
./a.out
)。前台进程占用终端(用户需等待完成),后台进程加&
(如./a.out &
)。 - 批处理进程:无交互任务序列(如
cron
定时任务)。 - 守护进程(Daemon):系统启动时加载(如
sshd
、httpd
),脱离终端,长期运行。
进程状态
- 运行(Running):占用CPU执行指令。
- 就绪(Ready):等待调度器分配CPU时间片。
- 睡眠(Sleeping):等待I/O或信号唤醒。
- 停止(Stopped):收到
SIGSTOP
信号暂停。 - 僵尸(Zombie):已终止但未被父进程回收。
执行模式
- 用户模式:运行用户程序,权限受限(无法直接访问硬件)。
- 内核模式:执行系统调用或中断处理,可操作特权指令(如修改内存页表)。
切换场景: - 系统调用(如
read()
)。 - 硬件中断(如时钟中断)。
5.2 进程创建与命令执行
fork()系统调用
核心机制:
- 写时复制(Copy-On-Write):父子进程共享物理内存,仅在实际修改时复制。
- 复制父进程的代码段、数据段、堆栈、文件描述符表、信号处理等。
返回值: - 父进程返回子进程PID(>0)。
- 子进程返回0。
- 失败返回-1(如进程数超限)。
示例分析:
pid_t pid = fork();
if (pid == 0) {
// 子进程代码
} else if (pid > 0) {
// 父进程代码
} else {
perror("fork failed");
}
exec系列函数
功能:替换当前进程的代码段为新程序(“换脑”)。
常用变体:
函数 | 参数传递方式 | 路径查找方式 |
---|---|---|
execl | 列表(arg0, arg1, ..., NULL ) | 需完整路径 |
execv | 数组(argv[] ) | 需完整路径 |
execlp | 列表 | 通过PATH 环境变量 |
execvp | 数组 | 通过PATH 环境变量 |
关键特性: |
- 成功调用后不返回,失败返回-1。
- 保留原进程的PID、文件描述符、信号掩码等属性。
文件描述符继承: - 父子进程共享打开的文件描述符,文件偏移量同步更新。
潜在问题: - 并发写入冲突(需使用
O_APPEND
或文件锁)。
示例:父进程读取文件后fork()
,子进程写入时继承同一偏移量。
5.3 进程退出
退出方式
正常退出:
exit(int status)
:刷新缓冲区,调用atexit()
注册的函数。_exit(int status)
:直接终止进程,不清理缓冲区。
异常退出:- 收到信号(如
SIGKILL
、SIGSEGV
)。
守护进程创建步骤
详细流程:
fork()
后父进程退出:子进程成为孤儿进程,由init
接管。setsid()
创建新会话:脱离原控制终端,成为会话组长。chdir("/")
修改工作目录:避免占用挂载点。umask(0)
重置文件掩码:取消权限限制。- 关闭文件描述符:
for (int i = 0; i < sysconf(_SC_OPEN_MAX); i++) close(i);
- 重定向标准I/O(可选):
open("/dev/null", O_RDWR); // stdin
dup(0); // stdout
dup(0); // stderr
僵尸进程处理
产生原因:子进程终止后,父进程未调用wait()
/waitpid()
。
解决方法:
- 阻塞等待:父进程调用
wait(&status)
。 - 非阻塞等待:使用
waitpid(pid, &status, WNOHANG)
。 - 信号处理:捕获
SIGCHLD
信号,异步回收子进程。
signal(SIGCHLD, [](int sig) {
while (waitpid(-1, NULL, WNOHANG) > 0);
});
wait()
与waitpid()
对比:
wait()
:等待任一子进程结束,阻塞父进程。waitpid()
:- 可指定子进程PID(如
pid=-1
等待所有子进程)。 - 支持选项(如
WNOHANG
非阻塞轮询)。
- 可指定子进程PID(如
5.4 进程开发实例
简易Shell实现
核心逻辑:
while (1) {
printf("> ");
fgets(cmd, sizeof(cmd), stdin);
if (strcmp(cmd, "exit\n") == 0) break;
pid_t pid = fork();
if (pid == 0) {
// 子进程解析命令并执行
execvp(args[0], args);
perror("exec failed");
exit(1);
} else {
wait(NULL); // 等待子进程结束
}
}
扩展功能:
- 支持管道(
pipe()
+dup2()
)。 - 支持重定向(
open()
+dup2()
)。
多进程同步
父子进程协作示例:父进程等待所有子进程完成。
pid_t child1 = fork();
if (child1 == 0) { /* 输出10个B */ exit(0); }
pid_t child2 = fork();
if (child2 == 0) { /* 输出10个C */ exit(0); }
waitpid(child1, NULL, 0);
waitpid(child2, NULL, 0);
// 父进程输出A
总结表格
主题 | 关键点 |
---|---|
fork() | 写时复制、父子进程独立执行、返回值区分角色。 |
exec系列 | 替换进程映像、参数传递方式(列表/数组)、路径查找(完整路径/PATH )。 |
守护进程 | setsid() 脱离终端、chdir("/") 、umask(0) 、关闭文件描述符。 |
僵尸进程 | 父进程未调用wait() 、通过信号或非阻塞等待回收。 |
Shell实现 | fork() +execvp() +wait() 循环,支持命令解析与执行。 |
二、对AI总结的反思与补充
1.反思
原内容存在问题
5.1 进程基本概念
- 守护进程类型描述不完整
- 原文描述:“系统启动时加载”
- 修正:守护进程分为两类:系统引导时启动的常驻型(如
sshd
)和按需启动的任务型(如crond
)。建议补充说明"守护进程可通过systemd
等机制按需启动"。
- 进程状态术语偏差
- 原文使用"睡眠(Sleeping)"状态
- 修正:Linux官方术语中分为"阻塞态(Blocked)“和"睡眠态(Sleep)”:
- 阻塞态:被动等待I/O或资源(如
read()
系统调用) - 睡眠态:主动调用
sleep()
进入挂起
- 阻塞态:被动等待I/O或资源(如
5.2 进程创建与命令执行
- exec系列函数的文件描述符继承
- 原文描述:“父子进程共享打开的文件描述符”
- 修正:“若文件描述符设置了
FD_CLOEXEC
标志,则exec时会自动关闭该描述符”。可通过fcntl(fd, F_SETFD, FD_CLOEXEC)
设置。
5.3 进程退出
- 僵尸进程处理方案补充
- 原文未提及
SIGCHLD
信号特性 - 修正:需补充说明"若父进程注册了
SIGCHLD
信号处理函数,应使用循环while(waitpid(-1, NULL, WNOHANG) > 0)
避免信号丢失"。
- 原文未提及
- 守护进程创建步骤优化
- 原文代码示例直接关闭所有文件描述符
- 修正:更安全的做法是遍历
/proc/self/fd
目录关闭文件描述符,避免漏关新增fd:
DIR *dir = opendir("/proc/self/fd"); while ((entry = readdir(dir)) != NULL) { int fd = atoi(entry->d_name); if (fd > STDERR_FILENO) close(fd); } closedir(dir); ```。
5.4 进程开发实例
- Shell实现的重定向漏洞
- 原文未处理重定向竞争条件
- 修正:在
fork()
前完成文件打开操作,避免父子进程并发操作:
int fd = open(file, O_RDWR); if (fork() == 0) { dup2(fd, STDOUT_FILENO); // 子进程重定向 close(fd); // 避免fd泄漏 }
2.补充
5.1 进程基本概念
进程控制块(PCB)
- 数据结构:每个进程对应一个
task_struct
结构体,包含:- 进程标识符(PID/PPID):唯一标识进程及其父进程
- 进程状态:R(运行)、S(休眠)、D(不可中断)、Z(僵尸)等
- 调度信息:优先级、时间片、调度策略
- 内存管理:虚拟内存地址空间、页表指针
- 文件系统:打开的文件描述符表(FD Table)
- 信号处理:信号掩码、未决信号队列
进程状态扩展
- 不可中断睡眠(D):等待磁盘I/O等硬件操作,无法被信号唤醒。
- 僵尸进程处理:
- SIGCHLD信号处理:通过
signal(SIGCHLD, handler)
注册信号处理器,异步回收子进程。 - waitpid选项:
WNOHANG
非阻塞模式轮询僵尸进程。
- SIGCHLD信号处理:通过
5.2 进程创建与命令执行
exec函数族扩展
- 环境变量控制:
execle
:可自定义环境变量数组(char *envp[]
)。execve
:系统级底层函数,支持完整参数和环境变量控制。// execle示例:传递自定义环境变量 char *env[] = {"PATH=/usr/bin", "LANG=en_US", NULL}; execle("/bin/ls", "ls", "-l", NULL, env);
- 文件描述符继承:
- 默认继承:所有打开的文件描述符(除
O_CLOEXEC
标记)。 - 关闭技巧:
fcntl(fd, F_SETFD, FD_CLOEXEC)
标记在exec时自动关闭。
- 默认继承:所有打开的文件描述符(除
高级进程创建
- 双重fork技巧:防止守护进程意外获取控制终端。
if (fork() > 0) exit(0); // 第一次fork setsid(); // 创建新会话 if (fork() > 0) exit(0); // 第二次fork
5.3 进程退出
守护进程优化
- 资源清理:
- 关闭所有文件描述符:
for(int i=sysconf(_SC_OPEN_MAX); i>=0; i--) close(i);
- 重定向标准I/O:
open("/dev/null", O_RDWR)
后执行dup2()
。
- 关闭所有文件描述符:
- 快速创建:通过
nohup command &
直接创建守护进程。
僵尸进程深度处理
- 多子进程回收:在信号处理函数中使用循环
while(waitpid(-1, NULL, WNOHANG) > 0)
防止信号丢失。void sigchld_handler(int sig) { while(waitpid(-1, NULL, WNOHANG) > 0); } signal(SIGCHLD, sigchld_handler);
5.4 进程开发实例
进程间通信(IPC)
机制 | 特点 | 适用场景 |
---|---|---|
管道 | 单向通信,血缘进程间使用 | 父子进程简单数据传递 |
共享内存 | 零拷贝高效,需同步机制 | 大数据量高频次通信 |
信号量 | 计数器实现同步 | 资源竞争控制 |
消息队列 | 结构化消息,支持优先级 | 异步通信 |
// 共享内存示例
int shm_id = shmget(IPC_PRIVATE, size, 0666);
char *ptr = shmat(shm_id, NULL, 0);
sprintf(ptr, "Hello from PID %d", getpid());
shmdt(ptr);
进程替换特性
- 属性保留:
- PID不变:exec后仍保持原进程ID。
- 信号处理:已忽略的信号保持忽略,其他重置为默认动作。
- 资源限制:继承原进程的
rlimit
设置。
总结
补充主题 | 新增关键点 |
---|---|
PCB结构 | task_struct包含进程所有元数据,如内存映射、文件表、信号处理等。 |
双重fork | 防止守护进程重新获得控制终端的关键技术。 |
信号量IPC | 通过semget/semctl实现进程间同步,解决资源竞争问题。 |
环境变量传递 | execle允许自定义环境变量,适用于特殊执行环境需求。 |
非阻塞waitpid | WNOHANG选项实现异步子进程状态查询,避免父进程阻塞。 |
三、学习思维导图
第五章
mindmap
root((Linux进程管理))
1 进程基本概念
1.1 进程定义
- 动态执行的程序实例
- 包含程序代码、数据、栈及运行状态
1.2 进程类型
- 交互进程(Shell启动,前后台运行)
- 批处理进程(按指定方式执行的进程序列)
- 守护进程(系统启动时运行,后台服务)
1.3 执行模式
- 用户模式(运行用户程序代码)
- 内核模式(执行系统调用或处理中断)
2 进程创建和命令执行
2.1 fork系统调用
- 创建子进程,复制父进程资源
- 返回值区分父子进程(0给子,PID给父)
- 示例代码及输出分析
2.2 exec家族调用
- 替换进程代码执行新程序
- 包括execl、execv、execlp、execvp等
- 参数传递方式及查找路径区别
2.3 fork与exec结合
- 父进程创建子进程后,子进程调用exec执行新任务
- 实现命令执行的典型模式
3 进程退出
3.1 正常退出
- 程序运行结束自动退出
- 调用exit函数刷新缓冲区并清理资源
- _exit与exit区别(是否刷新缓冲区及调用清理函数)
3.2 异常退出
- 接收外部信号终止执行(如kill -9)
3.3 守护进程
- 脱离终端后台运行的服务进程
- 创建步骤:fork子进程、setsid脱离终端、改变工作目录、修改文件权限掩码、关闭文件描述符
- 示例代码及运行验证
3.4 僵尸进程
- 子进程结束但父进程未回收其资源
- 使用wait系统调用避免僵尸进程
- wait与waitpid区别(等待任意或指定子进程)
4 进程开发实例
4.1 myshell程序
- 模拟Shell功能,等待用户输入命令
- 父进程处理用户输入,子进程执行命令
- 使用fork、exec、wait等系统调用实现命令执行流程
4.2 实例运行模型
- 父进程循环等待输入,子进程执行命令后退出
- 展示了进程创建、执行、等待、退出的完整生命周期
四、基于AI的学习
1、关于符进程与程序
- 我:
我在学习Linux进程与程序知识点,请你以苏格拉底的方式对我进行提问,一次一个问题 - AI:
很好!让我们从基础开始。请告诉我,你认为什么是Linux中的“程序”? - 我:
在Linux系统中,程序可以有很多种形式。比如,有些程序是直接在命令行里运行的,你输入一个命令,它就开始工作;还有一些程序是图形界面的,就像你平时用的软件一样,点点鼠标、按按按钮就能操作。简单来说,程序就是让计算机帮你做事的工具。 - AI:
很好的描述!那么,既然你已经对程序有了一定的理解,我想进一步问你:当一个程序在Linux系统中运行时,它变成了什么?或者说,程序和进程之间有什么关系? - 我:
程序就是一堆写好的指令,告诉计算机要做什么事情,它是一个静态的东西。
当我们在Linux系统中运行一个程序的时候,程序就变成了一个进程。进程是程序在运行时的“活生生”的状态。它包含了程序的代码、程序运行时需要的数据,以及程序运行时的状态 - AI:
非常清晰的解释!既然你已经理解了程序和进程的区别,那么让我继续追问:当一个进程在Linux系统中运行时,操作系统是如何管理和跟踪它的?具体来说,操作系统会为进程分配哪些资源? - 我:
当一个进程在Linux系统里运行的时候,操作系统会像“管家”一样管理它。操作系统会给进程分配一些资源,让它能顺利运行。
操作系统会给进程分配内存。内存就像是进程的“工作台”,进程需要在这个地方存储自己的代码和数据。每个进程都有自己的一块独立的内存空间,这样它们才不会互相干扰。 - AI:
很好!你提到了内存分配,这是非常重要的一个方面。那么,既然操作系统为进程分配了内存,我想进一步探讨的是:操作系统是如何区分和管理不同进程的?或者说,每个进程在系统中是否有唯一的标识? - 我:
当一个进程在Linux系统中运行时,操作系统会创建这个进程,并给它分配一个独一无二的PID。这个PID在整个系统运行期间都是唯一的,不会和其他进程的PID重复。操作系统用PID来跟踪每个进程的状态,比如它正在运行、暂停,还是已经结束。 - AI:
非常准确!既然你已经知道PID(进程ID)是操作系统用来区分和管理进程的关键,那么让我们深入一点:除了PID,操作系统还会为每个进程维护哪些关键信息?这些信息在进程管理中起到了什么作用? - 我:
你的理解是什么?
- AI:
我的理解是,除了PID,操作系统还会为每个进程维护以下关键信息:
- PPID(父进程ID):标识创建当前进程的父进程,帮助操作系统管理进程的层次结构。
- 进程状态:如运行、就绪、阻塞等,帮助操作系统调度进程。
- CPU寄存器状态:保存进程的上下文,以便在进程切换时恢复执行。
- 内存分配信息:记录进程使用的内存区域,确保内存的合理分配和回收。
- 文件描述符表:跟踪进程打开的文件和I/O资源。
这些信息帮助操作系统有效地管理、调度和监控进程,确保系统的稳定性和效率。现在,你的理解是什么呢?
- 我:
针对我该知识点,我理解了吗?我的回答结束了,请对我的回答进行评价总结。
- AI:
你对Linux进程与程序的基础知识已经有了较好的理解!以下是对你回答的总结和评价:
优点:
- 程序与进程的区别:你清楚地解释了程序是静态的指令集合,而进程是程序在运行时的动态实体。这种区分非常重要,表明你理解了核心概念。
- PID的作用:你准确地指出了PID是操作系统用来唯一标识和管理进程的关键信息,这显示你对进程管理的基本机制有清晰的认识。
- 资源分配:你提到操作系统为进程分配内存,并强调了内存空间的独立性,这表明你理解了进程运行时的资源管理。
改进建议:
- 深入进程信息:虽然你提到了PID和内存分配,但对其他关键信息(如PPID、进程状态、CPU寄存器、文件描述符等)的讨论可以更深入。这些信息在进程管理和调度中同样重要。
- 操作系统的调度机制:你可以进一步探讨操作系统如何利用这些信息来调度进程,例如进程状态如何影响CPU的分配。
- 实例与场景:通过具体的例子或场景来说明这些概念,可以帮助你更好地理解和记忆。例如,可以思考在多任务环境中,操作系统如何通过PID和进程状态来管理多个进程。
总结:
你对Linux进程与程序的基础知识掌握得不错,已经理解了核心概念。接下来,可以通过深入探讨进程的其他关键信息、操作系统的调度机制,以及结合实际场景来进一步巩固和扩展你的知识。继续努力,你的理解会越来越全面!
2、关于僵尸进程处理
- 我:
我在学习僵尸进程知识点,请你以苏格拉底的方式对我进行提问,一次一个问题 - AI:
好的,让我们从基础开始。你知道什么是进程吗? - 我:
进程是程序的运行实例,是操作系统进行资源分配和调度的基本单位。 - AI:
很好!既然你理解进程的概念,那么你能告诉我进程在终止时,通常会发生什么吗? - 我:
进程终止时,通常会释放占用的资源,如内存、文件句柄等;操作系统会清理其数据结构,并通知父进程其终止状态。 - AI:
非常清晰!既然进程终止时会通知父进程,那么如果一个父进程没有接收到子进程的终止状态,你认为会发生什么情况? - 我:
如果父进程没有接收子进程的终止状态,子进程会变成僵尸进程,占用系统资源,直到父进程读取其状态信息。 - AI:
没错!那么,现在你已经知道僵尸进程的产生原因,你能想到哪些方法可以避免或处理僵尸进程吗?
- 我:
避免或处理僵尸进程的方法如下:
父进程调用 wait 或 waitpid:父进程通过调用 wait 或 waitpid 函数回收子进程的状态。
捕获 SIGCHLD 信号:在父进程中捕获 SIGCHLD 信号,并在信号处理函数中调用 wait 或 waitpid。
使用双重 fork 技术:通过双重 fork 使子进程成为孤儿进程,由 init 进程接管并回收。
设置信号忽略:在父进程中设置 signal(SIGCHLD, SIG_IGN),忽略 SIGCHLD 信号,让子进程直接被 init 进程接管。
处理僵尸进程的方法
杀死父进程:找到产生僵尸进程的父进程并杀死它,僵尸进程会被 init 进程接管并清理。
重启服务:如果僵尸进程是由某个服务产生的,尝试重启该服务以清理僵尸进程。
定期检查和清理:编写脚本定期检查并清理僵尸进程。
- AI:
非常全面!既然你已经掌握了这些方法,那么你认为在这些方法中,哪一种是最有效且最常用的?为什么?
- 我:
在避免和处理僵尸进程的方法中,最有效且最常用的方法是父进程正确使用 wait() 或 waitpid() 函数,或者通过捕获 SIGCHLD 信号来回收子进程的状态信息。
针对我该知识点,我理解了吗?我的回答结束了,请对我的回答进行评价总结。
- AI:
你的回答非常出色!你对僵尸进程的理解非常全面,并且能够清晰地解释各种避免和处理僵尸进程的方法。你不仅指出了最有效且最常用的方法,还详细说明了其原因,这显示了你对该知识点的深入理解。
总结一下,你已经掌握了以下关键点:
僵尸进程的产生原因。
避免僵尸进程的多种方法,包括使用 wait() 或 waitpid(),捕获 SIGCHLD 信号,双重 fork 技术,以及设置信号忽略。
处理僵尸进程的方法,如杀死父进程、重启服务和定期检查清理。
能够分析并指出最有效且最常用的方法,并解释其原因。
总体而言,你对僵尸进程的知识点掌握得非常扎实,能够进行全面的分析和有效的应用。继续保持这种深入学习和思考的习惯,你在操作系统和进程管理方面的知识将会更加深厚。如果你有其他问题或需要进一步探讨,随时可以继续提问!
五、基于AI的代码实践
使用Gitee进行代码托管
仓库链接:
https://gitee.com/li-zhen1215/homework/tree/master/Week4
例5-1
fork.c
#include <stdio.h>
#include <unistd.h> // 包含 getpid(), fork(), sleep() 函数的声明
int main() // 明确指定 main 函数的返回类型为 int
{
int ret_from_fork, mypid;
mypid = getpid();
printf("Before: my pid is %d\n", mypid); // 修正格式字符串和参数顺序
ret_from_fork = fork();
sleep(1); // 包含 unistd.h 后,sleep 函数可以正常使用
printf("After: my pid is %d, fork() said %d\n", getpid(), ret_from_fork);
return 0; // main 函数返回 0
}
实践截图:
例5-2
vartest.c
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h> // 包含 exit 函数的声明
int glob = 10; // 全局变量
int main(void)
{
int local;
pid_t pid;
local = 8;
if ((pid = fork()) == 0)
{ // 子进程执行该代码
sleep(2);
}
else if (pid > 0) // 父进程要执行的代码
{
glob++;
local--;
sleep(10);
printf("glob = %d, local = %d, mypid=%d\n", glob, local, getpid()); // 父子进程都要执行的代码
exit(0); // 使用 exit 函数正确退出程序
}
}
实践截图:
例5-3
#include <unistd.h>
#include <stdio.h>
int main() // 明确指定 main 函数的返回类型为 int
{
char *arglist[3];
pid_t pid;
arglist[0] = "ls";
arglist[1] = "-l";
arglist[2] = NULL; // 使用 NULL 而不是 0 来表示字符串数组的结束
pid = fork();
if (pid == 0) {
execvp("ls", arglist);
// 如果 execvp 返回,说明调用失败,可以打印错误信息
perror("execvp failed");
return 1; // 返回非零值表示错误
}
printf("*** program is over. bye\n");
return 0; // 正常结束返回 0
}
实践截图:
例5-4
forkfd.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
char buf[10];
char *str1="This is child process";
char *str2="This is parent process";
pid_t pid;
int fd, readsize;
fd = open("test.txt", O_WRONLY);
if (fd == -1) {
perror("open failed");
exit(0);
}
readsize = read(fd, buf, 5);
// 读取aaaa字符串到buf,此时,与 fd 关联的文件位置指针指向了第二行的第一个字符
pid = fork();
// 创建子进程,该子进程此时复制了 fd 的属性,其与父进程的 fd 的文件位置指针指向同一个地方
switch(pid){
case -1: perror("fork failed"); exit(0); break; // 创建失败
case 0: write(fd, str1, strlen(str1)); // 子进程
break;
default: write(fd, str2, strlen(str2)); // 父进程
}
}
例5-5
daemon.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h> // 包含 umask 函数的声明
#define MAXFILE 65535
int main(void)
{
pid_t pc;
int i, fd, len;
char *buf = "Hello, everybody!\n";
len = strlen(buf);
pc = fork();
if (pc < 0) {
printf("fork error\n");
exit(1);
}
else if (pc > 0)
exit(0);
setuid(0); // 第二步,下面都是子进程需要执行的代码
chdir("/"); // 第三步
umask(0); // 第四步
for (i = 0; i < MAXFILE; i++) // 第五步
// 以下代码是守护进程真正长期执行的代码或功能
while (1)
{
if((fd = open("/tmp/daemon.log", O_CREAT | O_WRONLY | O_APPEND, 0600)) < 0)
{
perror("open");
exit(1);
}
write(fd, buf, len+1);
close(fd);
sleep(10);
}
}
实践截图:
例5-6
zombie.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h> // 包含 exit 函数的声明
void parent_code(int delay){
sleep(delay); // 让子进程先结束,产生僵尸进程
}
int main(void)
{
pid_t pid;
int status;
pid = fork();
if (pid == 0) { // 子进程什么都不做,尽快结束
exit(0);
}
if (pid > 0) parent_code(100000); // 父进程睡眠足够长,可通过命令查看僵尸进程信息
return 0; // 正常结束返回 0
}
实践截图:
例5-7
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h> // 包含 exit 函数的声明
void child(int delay){
sleep(delay);
exit(0); // 子进程结束
}
void parent(int *status) {
wait(status); // 父进程等待子进程结束
}
int main(){
pid_t pid;
int status;
printf("before : my pid is %d\n", getpid());
pid=fork();
if (pid == 0) {
child(1000); // 子进程调用 child 函数
}
if (pid > 0) {
parent(&status); // 父进程调用 parent 函数
}
return 0;
}
实践截图:
例5-8
myshell.c
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stddef.h>
void parent(int *status){
wait(status);
}
void split(char **arr, char *str, const char *del) // 字符串分割函数
{
char *s = strtok(str, del);
while(s != NULL){
*arr++ = s;
s = strtok(NULL, del);
}
*arr = NULL; // 确保字符串数组以 NULL 结尾
}
int main(){
char *args[10];
char arg[100]; // 保存用户输入的命令
pid_t pid;
int status;
while (1)
{
printf("please input command:\n");
memset(args, 0x0, sizeof(args)); // 清空字符串数组
fgets(arg, sizeof(arg), stdin); // 使用 fgets 代替 gets
arg[strcspn(arg, "\n")] = 0; // 去除换行符
if (strcmp("#", arg) == 0) break;
split(args, arg, " ");
pid = fork();
if (pid < 0) {
printf("fork failed\n");
exit(0);
}
else if (pid == 0) {
if (execvp(args[0], args) == -1) printf("exec error!\n");
exit(1); // 子进程执行失败时退出
}
else wait(&status);
}
return 0;
}
实践截图:
习题一
这段代码将输出 6 行。以下是详细解释:
- 初始输出:
- 程序开始执行时,主进程(父进程)会执行第一个
printf("my pid is %d\n", getpid());
,输出其进程 ID,这是第 1 行 输出。
- 程序开始执行时,主进程(父进程)会执行第一个
- 第一次
fork
:- 调用
fork()
后,系统创建一个子进程。此时,父进程和子进程都会继续执行后续代码。 - 父进程和子进程都会执行到第二个
printf("my pid is %d\n", getpid());
,各自输出自己的进程 ID,产生 2 行 输出。
- 调用
- 第二次
fork
:- 第一次
fork
后,父进程和子进程各自再次调用fork()
,分别创建新的子进程。此时,总共有 4 个进程(1 个最初的父进程,3 个子进程)。 - 这 4 个进程都会执行最后一个
printf("my pid is %d\n", getpid());
,各自输出自己的进程 ID,产生 4 行 输出。
- 第一次
- 总输出行数:
- 初始输出:1 行
- 第一次
fork
后的输出:2 行 - 第二次
fork
后的输出:4 行 - 总计:1 + 2 + 4 = 7 行
(注:由于代码中第二次fork
后的printf
也会被执行,因此总行数应为 7 行。)
总结:代码输出 7 行,原因是主进程初始输出一次,两次fork
调用分别增加了 2 行和 4 行输出。
习题二
mindmap
root((进程创建过程))
Initial_Process
fork1
Child_Process_1
fork2
Child_Process_3
fork3
Child_Process_7
Child_Process_4
fork3
Child_Process_8
Parent_Process
fork2
Child_Process_2
fork3
Child_Process_6
Child_Process_5
fork3
Child_Process_9
习题三
ys.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
void create_process_tree() {
// 第一层创建 (A -> B)
pid_t pid_b = fork();
if (pid_b < 0) {
perror("fork error");
exit(EXIT_FAILURE);
}
else if (pid_b == 0) { // 子进程B的代码块
printf("Process B (PID:%d) created by A (PPID:%d)\n",
getpid(), getppid());
// 第二层创建 (B -> C)
pid_t pid_c = fork();
if (pid_c < 0) {
perror("fork error");
exit(EXIT_FAILURE);
}
else if (pid_c == 0) { // 孙子进程C的代码块
printf("Process C (PID:%d) created by B (PPID:%d)\n",
getpid(), getppid());
exit(EXIT_SUCCESS); // 最底层立即退出
}
waitpid(pid_c, NULL, 0); // B等待C结束
exit(EXIT_SUCCESS); // B完成任务退出
}
// 主进程A等待B结束
waitpid(pid_b, NULL, 0);
}
int main() {
printf("Root Process A (PID:%d) starts\n", getpid());
create_process_tree();
printf("Process A completed all tasks\n");
return 0;
}
实践截图:
习题四
zjc.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main() {
pid_t pid1, pid2;
int status;
// 创建第一个子进程
pid1 = fork();
if (pid1 == 0) {
// 子进程1输出B
setbuf(stdout, NULL); // 禁用缓冲区
for (int i = 0; i < 10; i++) {
putchar('B');
}
exit(0); // 确保退出
} else if (pid1 < 0) {
perror("fork failed");
return 1;
}
// 创建第二个子进程
pid2 = fork();
if (pid2 == 0) {
// 子进程2输出C
setbuf(stdout, NULL);
for (int i = 0; i < 10; i++) {
putchar('C');
}
exit(0);
} else if (pid2 < 0) {
perror("fork failed");
return 1;
}
// 父进程等待两个子进程结束
waitpid(pid1, &status, 0);
waitpid(pid2, &status, 0);
// 父进程输出A
setbuf(stdout, NULL);
for (int i = 0; i < 10; i++) {
putchar('A');
}
putchar('\n');
return 0;
}
实践截图:
六、学习实践过程遇到的问题与解决方式
1. fork()函数的使用与理解
问题描述:
在编写多进程程序时,对fork()
函数的返回值理解不够清晰,导致程序逻辑混乱。
解决方式:
通过查阅文档和示例代码,明确了fork()
函数的返回值:
- 父进程中返回子进程的PID。
- 子进程中返回0。
- 失败时返回-1。
通过编写简单的测试程序,验证了fork()
的行为,并确保在父进程和子进程中正确区分逻辑。
2. 僵尸进程的产生与处理
问题描述:
在编写多进程程序时,发现子进程结束后变成了僵尸进程,导致系统资源无法释放。
解决方式:
学习了僵尸进程的产生原因,即父进程未调用wait()
或waitpid()
回收子进程的状态信息。通过以下方法解决了问题:
- 在父进程中显式调用
wait()
或waitpid()
。 - 使用SIGCHLD信号处理函数异步回收子进程。
3. 守护进程的创建
问题描述:
在创建守护进程时,发现进程未能正确脱离终端,且文件描述符未正确关闭。
解决方式:
通过查阅守护进程的创建步骤,修正了以下问题:
- 使用
setsid()
创建新会话,脱离终端。 - 关闭所有打开的文件描述符,避免资源泄漏。
- 重定向标准输入、输出和错误到
/dev/null
。
参考资料
- AI工具(你使用的AI工具及其链接)
- 问小白
- Kimi
- 图书
- 《Linux编程基础.李养群》电子版
- 网站
- CSDN
“20242817李臻 原创作品转载请注明出处 《Linux高级编程实践》”*