Linux进程管理之子进程的创建(fork函数)、子进程与线程的区别、fork函数的简单使用例子、子进程的典型应用场景、父进程等待子进程结束后自己再结束

news2025/3/31 22:55:09

收尾

进程终止:子进程通过exit()或_exit()终止,父进程通过wait()或waitpid()等待子进程终止,并获取其退出状态。?其实可以考虑在另一篇博文中来写

fork函数讲解

fork函数概述

fork() 是 Linux 中用于创建新进程的系统调用。当一个进程调用 fork() 时,系统会创建一个与原进程几乎完全相同的子进程。
新的子进程在有相关写操作时,会复制父进程的资源(即写时复制的概念)。
父进程的PID和子进程的PID是不同的。
父进程和子进程会从 fork() 调用的返回值处开始继续执行,但返回值在父进程和子进程中是不同的。

fork函数的工作原理和流程

其具体工作原理和流程如下:
当某个进程调用fork() 创建子进程后,系统会创建一个与原进程几乎完全相同的子进程,然后父进程和子进程会从fork()返回的地方继续执行。
不过两个进程得到的fork()的返回值不一样:

  • 对于父进程,它得到的fork()的返回值是子进程PID;
  • 对于子进程,它得到的fork()的返回值是0;

即:
如果 fork() 成功,它会返回两次:在父进程中返回子进程的 PID,在子进程中返回 0。
如果 fork() 失败(如资源不足),返回 -1,并设置 errno 以说明错误。

子进程的特点

  • 父子进程的几乎相同:子进程是父进程的副本,它们俩几平是相同的,除了fork() 的返回值和 PID。
  • 资源复制:父子进程共享文件描述符、内存映射等资源,但会使用“写时复制”(Copy-On-Write, COW)技术优化内存使用。这意味着在父子进程开始执行时,内存不会立即复制,而是在修改内存时才复制。
  • 进程独立性:父进程和子进程是独立的,执行过程中互不影响,但它们会共享某些资源(如打开的文件)。

进程与线程的区别

fork() 创建的子进程在某些方面与线程(特别是线程的创建和管理)有相似之处,但它们在操作系统级别的实现和资源管理上有一些关键的区别。

  1. 进程(Process)
  • 进程是操作系统分配资源的基本单位。每个进程都有独立的地址空间、文件描述符、栈、堆等资源。
  • 进程之间是独立的,它们不会共享内存空间(除非显式使用共享内存或其他进程间通信机制),每个进程有自己的 PID 和独立的资源。
  • 进程的创建(通过 fork())是相对重的操作,需要操作系统为子进程分配新的资源(如内存)。
  1. 线程(Thread)
  • 线程是进程内部的执行单元,同一个进程中的多个线程共享进程的资源(如内存、文件描述符等),它们共享相同的地址空间。
  • 线程之间是轻量级的,因为它们共享进程的资源,而不需要像进程那样拥有完全独立的资源。
  • 线程的创建通常比进程轻量,操作系统管理线程的开销较小,线程之间可以很容易地进行共享数据和通信(如使用互斥锁、条件变量等)。

二者的关键差异

  1. 地址空间和资源

    • 进程:子进程有独立的地址空间。父进程与子进程之间没有直接共享内存,除非使用共享内存(mmap())或其他进程间通信(IPC)方式。
    • 线程:线程在同一进程内共享内存、文件描述符等资源。线程间的通信非常高效,因为它们共享相同的地址空间。
  2. 创建开销

    • 进程:通过 fork() 创建子进程时,操作系统需要为新进程分配独立的资源(如内存空间)。这使得进程创建的开销相对较大。
    • 线程:创建线程时,操作系统只需要分配线程控制块(TCB)等较小的资源,不需要分配独立的内存空间,因此线程创建比进程轻量。
  3. 执行独立性

    • 进程:父进程与子进程相互独立,父进程的退出不会影响子进程,反之亦然。它们各自拥有独立的控制流。
    • 线程:同一进程中的多个线程共享控制流,互相协作。线程的退出会影响到进程的状态,甚至可能导致整个进程退出。
  4. 调度和切换

    • 进程:操作系统调度时,进程之间的切换需要保存和恢复更多的状态,因为每个进程有独立的地址空间。
    • 线程:线程之间的切换较轻量,操作系统只需要保存和恢复少量状态(如寄存器、栈指针等),因为线程共享地址空间。

fork() 创建的子线程和线程的相似性和差异

相似性:

  • 并行执行:无论是 fork() 创建的子进程还是线程,它们都可以并行执行(多核 CPU 上);
  • 并发性:父进程与子进程之间的调度是并发的,线程间的调度也是如此。

差异:

  • 资源分配fork() 创建的子进程拥有独立的资源(如地址空间、PID),而线程共享进程的资源。
  • 进程控制:父进程与子进程是完全独立的,退出父进程不会直接影响子进程;线程则不同,进程退出时会导致所有线程结束。

何时选择使用进程,何时使用线程?

  • 如果需要完全隔离的执行环境,或者需要实现进程间的严格隔离,应该选择进程(使用 fork())。例如,fork() 在服务器中常用于创建多个独立的工作进程,每个进程可以处理独立的任务。
  • 如果需要多个轻量级的并发任务,并且共享资源是必须的,应该选择线程。线程通常适用于需要大量并发操作且共享内存的场景,例如 Web 服务器的请求处理。

小结

虽然子进程和线程在某些方面表现得有点相似——例如它们可以并行执行,但它们本质上是不同的:子进程具有独立的资源和地址空间,而线程则是共享同一进程的资源。fork() 创建的子进程是“重型”的,而线程则是“轻型”的。因此,子进程和线程虽然在并发执行上有相似之处,但它们的实现和适用场景有很大区别。

fork函数使用的简单例子

下面是一个简单的示例,展示如何使用 fork() 创建一个子进程:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
    pid_t pid = fork();  // 创建新进程

    if (pid == -1) {
        // 错误处理:fork()失败
        perror("fork failed");
        return 1;
    } else if (pid > 0) {
        // 父进程
        printf("This is the parent process, PID: %d, child PID: %d\n", getpid(), pid);
        sleep(2);  // 父进程睡眠 2 秒,确保子进程有时间执行,否则有可能出现子进程还没执行完父进程就执行完的情况
    } else {
        // 子进程
        printf("This is the child process, PID: %d, parent PID: %d\n", getpid(), getppid());
    }

    return 0;
}

代码解释:
fork() 被调用时,操作系统会在内核中创建一个新进程,子进程会复制父进程的大部分资源(包括内存、文件描述符等)。然后父进程和子进程都会从 fork() 语句的下一行代码开始继续执行,但它们的执行路径有所不同:

  • 父进程:父进程得到的fork() 返回值是子进程的 PID(大于 0 的值),父进程会执行属于它的if条件分支代码块。
  • 子进程:子进程得到的fork() 返回值的值是 0,子进程会执行属于它的if分支条件代码块。

由于父进程和子进程得到的fork() 的返回值不一样,即各自的pid变量的值不一样,所以在后续的代码中各自执行了不同的条件分支。

在Linux开发板上测试上面这个例子

代码文件复制到Ubuntu中

在这里插入图片描述

交叉编译

运行下面的命令进行交叉编译

cd /home/book/mycode/C0034_fork
arm-buildroot-linux-gnueabihf-gcc -o fort_test fort_test.c

在这里插入图片描述
复制到NFS目录中,备用。
在这里插入图片描述

在开发板上测试

打开串口终端→打开开发板→挂载网络文件系统

执行下面的代码运行测试程序

/mnt/fork_test/fort_test

运行结果如下:
在这里插入图片描述
可见,符合我们的预期,所以测试成功。

子进程的应用场景

子进程的创建通常是为了实现进程间的隔离并行化或者并发处理,以及提升程序的响应性资源管理。虽然有时线程可能更合适,但子进程在某些场景下非常重要。下面我将列举一些典型的子进程应用场景,帮助你更好地理解它们的使用场景。

注意:要理解下面的应用场景关键要明白子进程与父进程为何能执行不同的代码,其实在实际代码中就是通过判断fork的返回值来进行的,通过前面的例子我们已经知道fork()函数对父进程和子进程的返回值不一样嘛。

1. 服务器模型中的进程池

  • 场景:Web 服务器(如 Apache)或数据库服务器常常需要处理大量并发请求。这时,创建一个子进程来处理每一个请求,可以保证每个请求都在独立的进程中运行,互不干扰。
  • 原因:使用子进程可以实现进程间隔离,每个请求都在独立的进程中执行,即使其中一个请求出现崩溃或问题,也不会影响其他请求。父进程只负责接收和分发任务,而实际处理任务的工作交给多个子进程。
  • 举例
    • Web 服务器:比如 Apache 使用子进程处理不同的客户端请求,这样即使一个子进程崩溃,其他请求仍然能继续处理。
    • 数据库服务:一些数据库管理系统通过子进程处理不同的查询,确保查询之间不会互相干扰。

2. 任务调度与并行处理

  • 场景:当某个程序需要同时进行多个独立的计算任务时,创建子进程可以将这些任务分配给不同的进程,使它们并行执行,从而提高执行效率。
  • 原因:多核 CPU 环境下,父进程可以通过 fork() 创建多个子进程,这样每个子进程都可以在不同的 CPU 核心上并行执行任务,从而提高计算效率。
  • 举例
    • 科学计算:例如,处理大规模数据分析时,可以将数据分成多个块,每个子进程处理一个数据块,最后将结果合并。
    • 视频处理:在进行视频转码或图片渲染时,创建多个子进程来并行处理不同帧或不同部分的图像。

3. 资源隔离与安全性

  • 场景:一些需要高安全性的应用会将不同的功能模块分离成不同的进程,这样即使某个进程被攻击或崩溃,其他进程的运行不会受到影响。
  • 原因:子进程的隔离性使得每个进程拥有自己的内存空间和资源,避免了进程间的干扰。这对于处理敏感信息或者确保系统稳定性和安全性至关重要。
  • 举例
    • 浏览器:现代浏览器会为每个标签页、插件或扩展创建不同的进程,这样一个标签页崩溃不会导致整个浏览器崩溃。
    • 操作系统安全:如一些操作系统使用子进程来执行高权限操作,避免给系统带来潜在风险。

4. 守护进程(Daemon)

  • 场景:守护进程是一种长期在后台运行的进程,通常在系统启动时启动,并在系统关闭时停止。守护进程需要创建一个子进程来执行具体的工作。
  • 原因:守护进程通常不与用户直接交互,它们负责监视某些任务或提供某些服务。通过创建子进程,守护进程可以独立处理各种工作,保持系统的高效和稳定。
  • 举例
    • 系统守护进程:例如 cron 进程定期运行任务,sshd 进程监听远程连接。
    • 文件服务器:一个守护进程可以监听文件的变化并对文件进行同步或备份。

5. 进程控制与协作

  • 场景:父进程与子进程之间的协作。父进程创建子进程后,可以与子进程进行通信,通过管道、消息队列等机制协作完成任务。
  • 原因:父子进程之间可以通过进程间通信(IPC)进行数据传递,子进程执行任务的结果会影响父进程的执行。父进程和子进程之间的关系通常是控制与执行的关系。
  • 举例
    • 编译工具:在某些构建系统(如 make)中,父进程通过创建子进程来执行不同的编译任务,最后将结果汇总。
    • 日志管理:父进程可以创建多个子进程来处理日志文件,每个子进程独立地处理不同的日志任务,最后通过 IPC 汇总结果。

6. 多重任务并发处理

  • 场景:当一个应用程序需要并发执行多个不同的任务时,可以通过 fork() 创建多个子进程,分配任务给不同的子进程。
  • 原因:这种方式适合处理需要大量并行工作的任务,例如并行搜索、并行计算等。
  • 举例
    • 多线程下载管理器:如果一个文件很大,可以通过 fork() 创建多个子进程并行下载文件的不同部分,最后合并成一个完整的文件。
    • 分布式计算:某些分布式计算任务会将计算工作分配给多个进程来并行处理,提高整体计算速度。

7.小结

子进程通常用于以下几种情况:

  • 进程隔离和独立执行:避免不同任务间的干扰,提高系统稳定性。
  • 并行化计算:充分利用多核 CPU 提高计算效率。
  • 长期运行的守护进程:独立执行后台任务。
  • 提高安全性:隔离不同的任务和数据,防止安全问题扩散。

子进程的优势通常体现在资源隔离任务并行化上,尤其在需要并发处理多个独立任务或确保系统高可靠性和稳定性时,使用子进程可以大大提高系统性能和安全性。

父进程等待子进程结束后自己再结束

原因分析

当 fork() 创建子进程后,子进程是独立的进程,和父进程共享部分资源,但运行是相互独立的。
如果父进程提前结束,子进程仍然可以继续执行,直到它自己终止或被系统杀死。
如果父进程退出,而子进程仍在运行,那么子进程会成为孤儿进程。
孤儿进程会被 init 进程(PID=1) 领养,init 进程会负责等待它结束,回收它的资源。

但通常我们还是会让父进程等待子进程结束后自己再结束,原因如下:

1. 防止子进程变成僵尸进程(Zombie Process)

  • 问题
    • 当子进程结束时,它的退出状态信息会保留在系统的进程表中,直到父进程使用 wait()waitpid() 读取该信息。
    • 如果父进程没有处理这个信息,子进程的进程表项不会被释放,导致僵尸进程(Zombie Process)。
    • 如果系统中存在大量僵尸进程,会占用进程表,可能导致新进程无法创建。

2. 确保父进程在子进程完成任务后再退出

  • 有时候子进程的任务和父进程相关,比如:
    • 子进程负责处理数据,父进程等待结果
    • 子进程执行重要任务,父进程需要等它完成才能继续

3. 避免创建大量孤儿进程

  • 虽然孤儿进程会被 init 进程接管,但不是所有情况下都希望这样
    • 如果有多个子进程,可能会导致 进程管理变得混乱
    • 如果子进程运行时间较长,可能会造成 不必要的资源占用
    • 更好的方法
      • 直接让父进程 wait() 等待子进程。
      • 或者 让子进程调用 setsid() 使自己变成独立的进程组。

4. 让父进程控制子进程的执行

  • 例如:
    • 父进程动态分配任务给子进程
    • 父进程需要获取子进程的退出状态来决定下一步操作

5 最佳实践

  1. 如果子进程需要完成某些任务,父进程应该 wait() 等待,避免进程管理混乱。
  2. 如果不关心子进程的状态,但不想产生僵尸进程,可以用 SIGCHLD 信号处理
    signal(SIGCHLD, SIG_IGN);
    
    这样,子进程退出后,系统会自动回收它们的资源。

使用wait让父进程等待子进程的示例代码

下面是一个简单的 C 代码示例,演示 Linux 父进程如何等待子进程终止:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();  // 创建子进程

    if (pid < 0) {
        perror("fork failed");
        exit(1);
    } else if (pid == 0) {
        // 子进程执行的代码
        printf("Child process (PID: %d) is running...\n", getpid());
        sleep(2); // 模拟子进程执行任务
        printf("Child process (PID: %d) is exiting...\n", getpid());
        exit(0);
    } else {
        // 父进程执行的代码
        printf("Parent process (PID: %d) is waiting for child (PID: %d)...\n", getpid(), pid);
        int status;
        waitpid(pid, &status, 0); // 等待子进程结束
        if (WIFEXITED(status)) {
            printf("Child process exited with status %d\n", WEXITSTATUS(status));
        }
        printf("Parent process is exiting...\n");
    }

    return 0;
}

代码说明:

  1. fork() 创建一个子进程,返回两次:
    • 在父进程中返回子进程的 PID。
    • 在子进程中返回 0。
  2. 子进程执行自己的任务,sleep(2) 模拟耗时操作,然后退出。
  3. 父进程调用 waitpid() 等待子进程结束,并获取其退出状态。
  4. WIFEXITED(status) 判断子进程是否正常退出,WEXITSTATUS(status) 获取其退出码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2324495.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

妙用《甄嬛传》中的选妃来记忆概率论中的乘法公式

强烈推荐最近在看的不错的B站概率论课程 《概率统计》正课&#xff0c;零废话&#xff0c;超精讲&#xff01;【孔祥仁】 《概率统计》正课&#xff0c;零废话&#xff0c;超精讲&#xff01;【孔祥仁】_哔哩哔哩_bilibili 其中概率论中的乘法公式&#xff0c;老师用了《甄嬛传…

【MySQL篇】事务管理,事务的特性及深入理解隔离级别

目录 一&#xff0c;什么是事务 二&#xff0c;事务的版本支持 三&#xff0c;事务的提交方式 四&#xff0c;事务常见操作方式 五&#xff0c;隔离级别 1&#xff0c;理解隔离性 2&#xff0c;查看与设置隔离级别 3&#xff0c;读未提交&#xff08;read uncommitted&a…

项目实战-角色列表

抄上一次写过的代码&#xff1a; import React, { useState, useEffect } from "react"; import axios from axios; import { Button, Table, Modal } from antd; import { BarsOutlined, DeleteOutlined, ExclamationCircleOutlined } from ant-design/icons;const…

26_ajax

目录 了解 接口 前后端交互 一、安装服务器环境 nodejs ajax发起请求 渲染响应结果 get方式传递参数 post方式传递参数 封装ajax_上 封装ajax下 了解 清楚前后端交互就可以写一些后端代码了。小项目 现在写项目开发的时候都是前后端分离 之前都没有前端这个东西&a…

Kafka中的消息是如何存储的?

大家好&#xff0c;我是锋哥。今天分享关于【Kafka中的消息是如何存储的&#xff1f;】面试题。希望对大家有帮助&#xff1b; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在 Kafka 中&#xff0c;消息是通过 日志&#xff08;Log&#xff09; 的方式进行存储的。…

Altium Designer——同时更改多个元素的属性(名称、网络标签、字符串标识)

右键要更改的其中一个对象&#xff0c;选择查找相似… 进入到筛选界面&#xff0c;就是选择你要多选的对象的共同特点&#xff08;名字、大小等等&#xff09;&#xff0c;我这里要更改的是网络标签&#xff0c;所以我选择Text设置为一样。 点击应用就是应用该筛选调节&#…

当模板方法模式遇上工厂模式:一道优雅的烹饪架构设计

当模板方法模式遇上工厂模式&#xff1a;一道优雅的烹饪架构设计 模式交响曲的实现模板方法模式搭建烹饪骨架&#xff08;抽象类&#xff09;具体菜品&#xff08;子类&#xff09; 工厂模式 模式协作的优势呈现扩展性演示运行时流程控制 完整代码 如果在学习 设计模式的过程中…

企业级知识库建设:自建与开源产品集成的全景解析 —— 产品经理、CTO 与 CDO 的深度对话

文章目录 一、引言二、主流产品与方案对比表三、自建方案 vs. 开源产品集成&#xff1a;技术路径对比3.1 自建方案3.2 开源产品集成方案 四、结论与个人观点 一、引言 在当今数据驱动的商业环境中&#xff0c;构建高质量的知识库已成为企业数字化转型的关键一环。本博客分别从…

vue3项目配置别名

vue3项目配置别名 src别名的配置TypeScript 编译配置如果出现/别名引入报找不到的问题 src别名的配置 在开发项目的时候文件与文件关系可能很复杂&#xff0c;因此我们需要给src文件夹配置一个别名&#xff01;&#xff01;&#xff01; // vite.config.ts import {defineCon…

[ C语言 ] | 从0到1?

目录 认识计算机语言 C语言 工欲善其事必先利其器 第一个C语言代码 这一些列 [ C语言 ] &#xff0c;就来分享一下 C语言 相关的知识点~ 认识计算机语言 我们说到计算机语言&#xff0c;语言&#xff0c;就是用来沟通的工具&#xff0c;计算机语言呢&#xff1f;就是我们…

[Mac]利用Hexo+Github Pages搭建个人博客

由于我这台Mac基本没啥环境&#xff0c;因此需要从零开始配置&#xff0c;供各位参考。 注意⚠️&#xff1a;MacBook (M4)使用/bin/zsh作为默认Shell&#xff0c;其对应的配置文件为~/.zshrc 参考文档&#xff1a; HEXO系列教程 | 使用GitHub部署静态博客HEXO | 小白向教程 文…

Qt在IMX6ULL嵌入式系统中图片加载问题排查与解决

Qt在IMX6ULL嵌入式系统中图片加载问题排查与解决&#xff08;保姆级教学&#xff01;&#xff09; 在使用Qt开发IMX6ULL嵌入式系统的过程中&#xff0c;我遇到了图片加载的常见问题。本文将分享问题排查的详细过程和解决方案&#xff0c;希望能帮助遇到类似困难的开发者。 问题…

界面控件Telerik和Kendo UI 2025 Q1亮点——AI集成与数据可视化

Telerik DevCraft包含一个完整的产品栈来构建您下一个Web、移动和桌面应用程序。它使用HTML和每个.NET平台的UI库&#xff0c;加快开发速度。Telerik DevCraft提供完整的工具箱&#xff0c;用于构建现代和面向未来的业务应用程序&#xff0c;目前提供UI for ASP.NET MVC、Kendo…

pycharm终端操作远程服务器

pycharm项目已经连接了远程服务器&#xff0c;但是打开终端&#xff0c;却依旧显示的是本地的那个环境&#xff0c;也就是说没有操作远程的那个环境。只能再使用Xshell去操作远程环境&#xff0c;很麻烦&#xff0c;找了下教程。 来源&#xff1a;https://blog.csdn.net/maolim…

接口测试中数据库验证,怎么解决?

在接口测试中&#xff0c;通常需要在接口调用前后查询数据库&#xff0c;以验证接口操作是否正确影响了数据库状态。​这可以通过数据库断言来实现&#xff0c;PyMySQL库常用于连接和操作MySQL数据库。​通过该库&#xff0c;可以在测试中执行SQL语句&#xff0c;查询或修改数据…

Playwright从入门到实战:比Selenium更快的数据爬取案例实战

摘要 Playwright 是微软开源的下一代浏览器自动化工具&#xff0c;凭借其高性能、跨浏览器支持和现代化设计&#xff0c;迅速成为 Web 自动化领域的热门选择。本文将从 安装配置 开始&#xff0c;通过 实战演练 展示其核心功能&#xff0c;并与 Selenium 深度对比&#xff0c;…

day1_Flink基础

文章目录 Flink基础今日课程内容目标为什么要学Flink技术更新迭代市场需求 流式计算批量计算概念特点 批量计算的优势和弊端流式计算生活中流场景流式计算的概念 Flink简介Flink历史Flink介绍 Flink架构体系已学过的框架技术Flink架构 Flink集群搭建Flink的集群模式Standalone模…

使用FastExcel时的单个和批量插入的问题

在我们用excel表进行插入导出的时候&#xff0c;通常使用easyexcel或者FastExcel&#xff0c;而fastexcel是easy的升级版本&#xff0c;今天我们就对使用FastExcel时往数据库插入数据的业务场景做出一个详细的剖析 场景1 现在我们数据库有一张组织表&#xff0c;组织表的字段…

交换技术综合实验

一、实验拓扑 二、实验要求 内网IP地址使用172.16.0.0/16分配。 SW1和SW2之间互为备份。 VRRP/STP/VLAN/Eth-trunk均使用。 所有PC通过DHCP获取IP地址。 ISP只能配置IP地址。 所有电脑可以正常访问ISP路由器。 三、实验步骤 基于172.16.0.0/16进行划分 172.16.2.0/24&…

记录Jmeter 利用BeanShell 脚本解析JSON字符串

下载org.json包(文档说明) #下载地址 https://www.json.org/ # github 地址 https://github.com/stleary/JSON-java # api 文档说明 https://resources.arcgis.com/en/help/arcobjects-java/api/arcobjects/com/esri/arcgis/server/json/JSONObject.htmlBeanShell脚本 import…