当前位置: 首页 > article >正文

20242817李臻《Linux⾼级编程实践》第四周

20242817李臻《Linux⾼级编程实践》第4周

一、AI对学习内容的总结

第5章 Linux进程管理

5.1 进程基本概念

进程与程序的区别

  • 程序:静态的二进制文件(如/bin/ls),存储在磁盘中,不占用运行资源。
  • 进程:程序的动态实例,拥有独立的地址空间、资源(CPU、内存、文件句柄等),生命周期包括创建、运行、阻塞、终止。
    关键差异
  • 程序是“菜谱”,进程是“烹饪过程”。
  • 同一程序可产生多个进程(如同时运行多个ls命令)。

进程类型

  • 交互进程:由用户通过Shell启动(如./a.out)。前台进程占用终端(用户需等待完成),后台进程加&(如./a.out &)。
  • 批处理进程:无交互任务序列(如cron定时任务)。
  • 守护进程(Daemon):系统启动时加载(如sshdhttpd),脱离终端,长期运行。

进程状态

  • 运行(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):直接终止进程,不清理缓冲区。
    异常退出
  • 收到信号(如SIGKILLSIGSEGV)。

守护进程创建步骤

详细流程

  1. fork()后父进程退出:子进程成为孤儿进程,由init接管。
  2. setsid()创建新会话:脱离原控制终端,成为会话组长。
  3. chdir("/")修改工作目录:避免占用挂载点。
  4. umask(0)重置文件掩码:取消权限限制。
  5. 关闭文件描述符:
for (int i = 0; i < sysconf(_SC_OPEN_MAX); i++) close(i);
  1. 重定向标准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非阻塞轮询)。

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 进程基本概念

  1. 守护进程类型描述不完整
    • 原文描述:“系统启动时加载”
    • 修正:守护进程分为两类:系统引导时启动的常驻型(如sshd)和按需启动的任务型(如crond)。建议补充说明"守护进程可通过systemd等机制按需启动"。
  2. 进程状态术语偏差
    • 原文使用"睡眠(Sleeping)"状态
    • 修正:Linux官方术语中分为"阻塞态(Blocked)“和"睡眠态(Sleep)”:
      • 阻塞态:被动等待I/O或资源(如read()系统调用)
      • 睡眠态:主动调用sleep()进入挂起

5.2 进程创建与命令执行

  1. exec系列函数的文件描述符继承
    • 原文描述:“父子进程共享打开的文件描述符”
    • 修正:“若文件描述符设置了FD_CLOEXEC标志,则exec时会自动关闭该描述符”。可通过fcntl(fd, F_SETFD, FD_CLOEXEC)设置。

5.3 进程退出

  1. 僵尸进程处理方案补充
    • 原文未提及SIGCHLD信号特性
    • 修正:需补充说明"若父进程注册了SIGCHLD信号处理函数,应使用循环while(waitpid(-1, NULL, WNOHANG) > 0)避免信号丢失"。
  2. 守护进程创建步骤优化
    • 原文代码示例直接关闭所有文件描述符
    • 修正:更安全的做法是遍历/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 进程开发实例

  1. 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非阻塞模式轮询僵尸进程。

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/Oopen("/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允许自定义环境变量,适用于特殊执行环境需求。
非阻塞waitpidWNOHANG选项实现异步子进程状态查询,避免父进程阻塞。

三、学习思维导图

第五章

在这里插入图片描述

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,操作系统还会为每个进程维护以下关键信息:
  1. PPID(父进程ID):标识创建当前进程的父进程,帮助操作系统管理进程的层次结构。
  2. 进程状态:如运行、就绪、阻塞等,帮助操作系统调度进程。
  3. CPU寄存器状态:保存进程的上下文,以便在进程切换时恢复执行。
  4. 内存分配信息:记录进程使用的内存区域,确保内存的合理分配和回收。
  5. 文件描述符表:跟踪进程打开的文件和I/O资源。
    这些信息帮助操作系统有效地管理、调度和监控进程,确保系统的稳定性和效率。现在,你的理解是什么呢?

  • 我:
    针对我该知识点,我理解了吗?我的回答结束了,请对我的回答进行评价总结。

  • AI:
    你对Linux进程与程序的基础知识已经有了较好的理解!以下是对你回答的总结和评价:

优点:

  1. 程序与进程的区别:你清楚地解释了程序是静态的指令集合,而进程是程序在运行时的动态实体。这种区分非常重要,表明你理解了核心概念。
  2. PID的作用:你准确地指出了PID是操作系统用来唯一标识和管理进程的关键信息,这显示你对进程管理的基本机制有清晰的认识。
  3. 资源分配:你提到操作系统为进程分配内存,并强调了内存空间的独立性,这表明你理解了进程运行时的资源管理。

改进建议:

  1. 深入进程信息:虽然你提到了PID和内存分配,但对其他关键信息(如PPID、进程状态、CPU寄存器、文件描述符等)的讨论可以更深入。这些信息在进程管理和调度中同样重要。
  2. 操作系统的调度机制:你可以进一步探讨操作系统如何利用这些信息来调度进程,例如进程状态如何影响CPU的分配。
  3. 实例与场景:通过具体的例子或场景来说明这些概念,可以帮助你更好地理解和记忆。例如,可以思考在多任务环境中,操作系统如何通过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 行。以下是详细解释:

  1. 初始输出
    • 程序开始执行时,主进程(父进程)会执行第一个 printf("my pid is %d\n", getpid());,输出其进程 ID,这是第 1 行 输出。
  2. 第一次 fork
    • 调用 fork() 后,系统创建一个子进程。此时,父进程和子进程都会继续执行后续代码。
    • 父进程和子进程都会执行到第二个 printf("my pid is %d\n", getpid());,各自输出自己的进程 ID,产生 2 行 输出。
  3. 第二次 fork
    • 第一次 fork 后,父进程和子进程各自再次调用 fork(),分别创建新的子进程。此时,总共有 4 个进程(1 个最初的父进程,3 个子进程)。
    • 这 4 个进程都会执行最后一个 printf("my pid is %d\n", getpid());,各自输出自己的进程 ID,产生 4 行 输出。
  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高级编程实践》”*


http://www.kler.cn/a/591848.html

相关文章:

  • [spring]集成junit
  • 在 Vue 项目中引入静态图片有多种方式
  • 从Excel到搭贝的转变过程
  • VSTO(C#)Excel开发13:实现定时器
  • 【模拟面试】计算机考研复试集训(第八天)
  • 免费看付费电影网站制作,高清电影集合搜索引擎网站
  • 【Json-RPC框架】:Json::CharReader,parse函数反序列化的返回值
  • sparksql的Transformation与 Action操作
  • Redis 小记
  • LeetCode[42] 接雨水
  • HarmonyOS开发,A持有B,B引用A的场景会不会导致内存泄漏,代码示例告诉你答案
  • Ext系列文件系统
  • 全网首创/纯Qt/C++实现国标GB28181服务/实时视频/云台控制/预置位/录像回放和下载/事件订阅/语音对讲
  • 飞腾2000+/64核加固服务器
  • ruoyi-vue部署
  • 虚幻基础:组件组件通信
  • PreparedStatement:Java 数据库操作的安全与高效之道
  • STM32---FreeRTOS任务通知
  • SpringBoot实现发邮件功能+邮件内容带模版
  • 深入浅出DBSCAN:基于密度的聚类算法