【Linux】进程创建、进程终止和进程等待

简介: 【Linux】进程创建、进程终止和进程等待

👉进程创建👈


fork 函数的理解


fork 函数能够从已存在进程中创建一个新进程,新进程为子进程,而原进程为父进程。进程调用 fork 函数后,操作系统会做一下的事情:


分配新的内存块和内核数据结构给子进程

将父进程部分数据结构内容拷贝至子进程

将子进程添加到系统进程列表当中

fork 返回,开始调度器调度

262ef4057c8f46a3bf41d703b71249c2.png


当一个进程调用 fork 函数之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方,但每个进程都将可以开始它们自己的旅程。fork 之前由父进程独立执行,fork 之后父子进程分别执行。注意:fork 之后,谁先执行完全由调度器决定。


72a0c4f1afe3475caf8da5f1937de9c5.png


fork 函数的返回值:fork 失败返回 -1;fork 成功给父进程返回子进程的 ID,给子进程返回 0。那如何理解 fork 函数给父进程返回子进程的 ID,给子进程返回 0 呢?其实是因为父进程可能有多个子进程,而子进程只有一个父进程,所以不要给子进程返回父进程的 ID,但需要给父进程返回子进程的 ID,将子进程管理起来。


如何理解 fork 函数有两个返回值呢?又如何理解同一个变量 id,怎么可能保存两个不同的值,让 if - else if - else 分支结构同时执行呢?见下图解释!


88b9cb7a69fc436f92ff89f175b1a9ee.png


写时拷贝


父子进程共享 fork 函数之后的代码。当父子进程都不进行数据写入时,数据也是共享的。当任意一方试图写入时,便以写时拷贝的方式各自一份副本。具体过程见下图:

46f0ead9c4dc4786ab81f09c2582f435.png


fork 的常规用法


  • 一个父进程希望复制自己,使父子进程同时执行不同的代码。例如:父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如:子进程从 fork 返回后,调用 exec 函数。


fork调用失败的原因


  • 系统中有太多的进程
  • 实际用户的进程数超过了限制


死循环创建进程代码


#include <stdio.h>
#include <unistd.h>
int main()
{
    int cnt = 0;
    while(1)
    {
        int id = fork();
        if(id < 0)
        {
            printf("fork fail! cnt:%d\n", cnt);
            break;
        }
        else if(id == 0)
        {
            // child
            while(1) sleep(1);
        }
        // parent
        ++cnt;
    }
}

c1feabcbfc994eb791da7bcb504276a6.png


👉进程终止👈


进程退出码


main 函数调用结束总会要 return 一个值,其实这个值是进程退出的时候对应的退出码,标定进程执行的结果是否正确。


验证进程退出码的存在


bf4c7a15113d40c6a48e336930c2a377.png

echo $? #输出最近一个进程执行完毕时的退出码


093ec8eb6744491b82a1d50f983706ac.png


33284c0146fb45a484d487dd26470c94.png


那如何设定 main 函数的返回值呢?如果不关心进出退出码, return 0 就行了。如果关心进程退出码,那么就需要返回特定的数据表面特定的执行结果。


退出码的意义:0 表示标识成功,非 0 标识失败,不同的数字标识不同的错误。


退出码对人是不友好的,而对计算机比较友好。一般而言,退出码都必须有对应退出码的文字描述。


strerror 函数能够将退出码转化成对应的退出码信息。

5f88f6439c734041b727f9657e838ab2.png


#include <stdio.h>
#include <string.h>
int main()
{
    for(int i = 0; i < 200; ++i)
    {
        printf("%d:%s\n", i, strerror(i));
    }
    return 0;
}

01991d8ac83047be9871965cf36e6513.png


Linux 系统一共提供了 134 个退出码信息。


进程退出场景


  • 代码运行完毕,结果正确(return 0)
  • 代码运行完毕,结果不正确(return 非0)
  • 代码异常终止(退出码无意义)


进程常见退出方法


正常终止(可以通过 echo $? 查看进程退出码)


  1. 从 main 函数 return 返回
  2. 任意位置调用 exit 函数
  3. _exit 系统调用


异常退出:Ctrl + c,信号终止进程


1. exit 函数


exit 函数的使用


3ec5638d7da2472b98156d9a596b10b0.png

c0bb47ddab71443493cb3cebb4c3182b.png

25724cccf9b04161ad96d1673db3fc08.png

2. _exit 系统调用


_exit 系统调用的使用



a601174393834186826be58530bab590.png



b6fd3ef72fc646ad95216de4f06b79e5.png

676d207cf8be45a0a62f9e64f851536e.png


3. exit 和 _exit 的区别


exit 是库函数,而 _exit 是系统调用。exit 库函数相对于 _exit 系统调用更加上层,eixt 函数本质也是调用了 _exit 但是 exit 函数还做了别的处理。


通过代码来体会 exit 和 _exit 的区别

d0baaf1ae0d7450bba0285933fabe2ef.png

2eefcc3f60914df1b6aae7d39d68b529.png


ea7dc7108ec9445bbbc0220b9feb51bd.png


42da4081ef9d4cd3ac47893b3798a14b.png


bedf206bd4014727aff8a4195bd9e297.png


调用 exit 函数的现象是休眠两秒过后,数据被打印出来了;而调用 _exit 的现象是休眠两秒后,数据并没有被打印出来。


结论:exit 终止进程会主动刷新缓冲区,而 _exit 终止进程并不会刷新缓冲区。该缓存区是用户空间的缓冲区。如果该缓冲区是在内核空间的话,那么 exit 和 _exit 都会刷新该缓冲区。该用户级的缓冲区会在基础 IO 部分里讲解!


调用 exit 函数所做的事情:

执行用户通过 atexit或on_exit定义的清理函数

关闭所有打开的流,所有的缓存数据均被写入

调用 _exit

e3ceeeb81e41470b83de97ca8e80d55f.png


👉进程等待👈


进程等待的必要性


  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入。“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法 杀死一个已经死去的进程。

最后,父进程派给子进程的任务完成的如何,我们需要知道。子进程运行完成,结果对还是不对, 或者是否正常退出。

父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。


进程等待的方法


1. wait


886d8cc013144501b3dbd1c0c496b096.png


注:wait 函数的参数是输出型参数,用来获取子进程退出的状态。如果不关心子进程退出的状态,可以设置为 NULL。wait 函数的返回值:成功返回被等待进程的 ID,失败返回 -1。


e96ec8bebf814e2fab8226ff04db9b23.png


2ae98605fec448c59afcc4e0423f3c21.png



现象:子进程执行完毕后的五秒钟,子进程处于僵尸状态。当父进程调用 wait 时,等待子进程成功,回收子进程的资源,那子进程就彻底结束了。


2. waitpid


pid_t waitpid(pid_t pid, int *status, int options);


waitpid 的参数 pid 是想要等待的进程,status 是输出型参数,记录子进程退出的信息,options 为 0 时为阻塞等待。当正常返回的时候,waitpid 返回收集到的子进程的进程 ID;如果设置了选项 WNOHANG,而调用中 waitpid 发现没有已退出的子进程可收集,则返回 0;如果调用中出错,则返回 -1,这时 errno 会被设置成相应的值以指示错误所在。

f52e3200448949178113d6c1295462b9.png

f51a28243b2740e1a286aff77b01b0b5.png

status 并不是退出码 10。因为 status 并不是整体使用,而是使用 status 的位信息。


我们知道,进程退出的场景有三种:代码运行完毕,结果正确、代码运行完毕,结果不正确以及代码异常终止。那么,status 就是用来表示以上三种情况的。


获取子进程状态 status


  • wait 和 waitpid,都有一个 status 参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递 NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status 不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究 status 低16比特位)
  • e4595db0c47841cab18dfeb34a709e91.png


注:当低七位为 0 时,表示进程正常退出。然后再看次低八位,看进程退出的结果是否正确。如果低七位不为 0,则次低八位无意义。


#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        return 1;
    }
    else if(id == 0)
    {
        // child
        int cnt = 5;
        while(cnt)
        {
            printf("我是子进程, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);
            sleep(1);
        }
        exit(10); // 子进程退出
    }
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if(ret > 0)  // 等待成功
    {
        printf("wait success:%d, signal number:%d, child exit code:%d\n", ret, (status & 0x7F), (status >> 8) & 0xFF);
    }
    return 0;
}

33c110560aa4433d83f634e3889fd992.png563419153ec64fbab78de45492c0b989.png45fdbd91d7734e51bd80c5c987aeb559.png6655d2311dc840e09f651f5ea7ebdfa4.png


进程等待的本质是父进程从子进程的进程控制块 PCB 中拿取子进程的退出信息。


c5b1773e50e74ac4917b22532de6ac81.png


除了通过位运算来拿到子进程的退出信息,还可以通过宏来拿到子进程的退出信息。


  • WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
  • WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
    pid_t id = fork();
    assert(id != -1);
    if(id == 0)
    {
        // child
        int cnt = 8;
        while(cnt)
        {
            printf("child procees running, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);
            sleep(1);
        }
        exit(0);
    }
    // parent
    int status = 0;
    // 1. 让操作系统释放子进程的僵尸状态
    // 2. 获取子进程的退出结果
    // 在子进程没有退出的时候,父进程只能阻塞等待,什么事也不干
    int ret = waitpid(id, &status, 0);  // 0表阻塞等待
    if(ret > 0) // 等待成功1
    {
        // 是否正常退出
        if(WIFEXITED(status))
        {
            // 查看子进程的退出码
            printf("exit code:%d\n", WEXITSTATUS(status));
        }
        else
        {
            printf("child eixt not normal!\n");
        }
        //printf("wait success, signal number:%d, exit code:%d\n", status & 0x7F, (status >> 8) & 0xFF);
    }
  return 0;
}

0bcec503d4f84a52959ad4d0070813af.png


非阻塞等待


子进程没有退出前,父进程一直在等待子进程退出,不执行其他代码,这种等待就是阻塞等待。而非阻塞等待是就算子进程没退出,父进程也不会一直等待子进程退出,期间父进程会去执行它的代码,过一段时间再来看子进程是否退出,这种等待就是非阻塞等待。父进程每次检查子进程是否退出都是一次非阻塞等待,多次非阻塞等待就是轮询的过程。


非阻塞等待代码


#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
    pid_t id = fork();
    assert(id != -1);
    if(id == 0)
    {
        // child
        int cnt = 8;
        while(cnt)
        {
            printf("child procees running, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);
            sleep(1);
        }
        exit(0);
    }
    // parent
    int status = 0;
    while(1)
    {
        pid_t ret = waitpid(id, &status, WNOHANG); // 非阻塞等待
        if(ret == 0)
        {
            // waitpid调用成功 && 子进程没有退出
            // 子进程没有退出,我们waitpid没有等待失败,仅仅是监测到子进程没有退出
            printf("waitpid success, but child process is running!\n");
            sleep(1);
        }
        else if(ret > 0)
        {
            // waitpid调用成功 && 子进程退出来
            printf("wait success, signal number:%d, exit code:%d\n", status & 0x7F, (status >> 8) & 0xFF);
            break;
        }
        else
        {
            // waitpid调用失败
            printf("waitpid call failed!\n");
            break;
        }
    }
  return 0;
}


04dd8a2c44d64f5ca046034ee7227918.png


waitpid 等待失败的场景


5b264f24d9ce4045b6a1ba5e2228a38e.png

db4c0b4433024dce8e984ff69487b123.png

非阻塞等待的好处


  • 不会占用父进程的所有精力,可以在轮询期间做别的时期。


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#define NUM 10
typedef void (*func_t)(); //函数指针
func_t handlerTask[NUM];
//样例任务
void task1()
{
    printf("handler task1\n");
}
void task2()
{
    printf("handler task2\n");
}
void task3()
{
    printf("handler task3\n");
}
void loadTask()
{
    memset(handlerTask, 0, sizeof(handlerTask));
    handlerTask[0] = task1;
    handlerTask[1] = task2;
    handlerTask[2] = task3;
}
int main()
{
    pid_t id = fork();
    assert(id != -1);
    if(id == 0)
    {
        //child
        int cnt = 5;
        while(cnt)
        {
            printf("child running, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
            sleep(1);
        }
        exit(10);
    }
    loadTask();  
    // parent
    int status = 0;
    while(1)
    {
        pid_t ret = waitpid(id, &status, WNOHANG); //WNOHANG: 非阻塞-> 子进程没有退出, 父进程检测时候,立即返回
        if(ret == 0)
        {
            // waitpid调用成功 && 子进程没退出
            //子进程没有退出,我的waitpid没有等待失败,仅仅是监测到了子进程没退出.
            printf("wait done, but child is running...., parent running other things\n");
            for(int i = 0; handlerTask[i] != NULL; i++)
            {
                handlerTask[i](); //采用回调的方式,执行我们想让父进程在空闲的时候做的事情
            }
        }
        else if(ret > 0)
        {
            // 1.waitpid调用成功 && 子进程退出了
            printf("wait success, exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);
            break;
        }
        else
        {
            // waitpid调用失败
            printf("waitpid call failed\n");
        //    break;
        }
        sleep(1);
    }
    return 0;
}

6eea827db0cd42e191289f631cfc6405.png


👉总结👈


本篇博客主要讲解了进程创建、进程终止和进程等待。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️








相关文章
|
6天前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
本文旨在探讨Linux操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
25 1
|
1天前
|
SQL 运维 监控
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
|
9天前
|
运维 监控 Linux
Linux操作系统的守护进程与服务管理深度剖析####
本文作为一篇技术性文章,旨在深入探讨Linux操作系统中守护进程与服务管理的机制、工具及实践策略。不同于传统的摘要概述,本文将以“守护进程的生命周期”为核心线索,串联起Linux服务管理的各个方面,从守护进程的定义与特性出发,逐步深入到Systemd的工作原理、服务单元文件编写、服务状态管理以及故障排查技巧,为读者呈现一幅Linux服务管理的全景图。 ####
|
1月前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
119 4
linux进程管理万字详解!!!
|
14天前
|
缓存 算法 Linux
Linux内核的心脏:深入理解进程调度器
本文探讨了Linux操作系统中至关重要的组成部分——进程调度器。通过分析其工作原理、调度算法以及在不同场景下的表现,揭示它是如何高效管理CPU资源,确保系统响应性和公平性的。本文旨在为读者提供一个清晰的视图,了解在多任务环境下,Linux是如何智能地分配处理器时间给各个进程的。
|
24天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
64 8
|
22天前
|
网络协议 Linux 虚拟化
如何在 Linux 系统中查看进程的详细信息?
如何在 Linux 系统中查看进程的详细信息?
41 1
|
22天前
|
Linux
如何在 Linux 系统中查看进程占用的内存?
如何在 Linux 系统中查看进程占用的内存?
|
1月前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
69 4
|
1月前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####