热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【Linux】进程创建、退出和等待(fork、exit和_exit、waitpid和wait、阻塞和非阻塞)

文章目录1、进程创建1.1理解fork函数1.2fork函数的细节2、进程退出2.1退出码2.2exit函数和_exit系统调用3、进程等待3.1wait和waitpid3.2阻塞




文章目录


    • 1、进程创建
      • 1.1 理解fork函数
      • 1.2 fork函数的细节

    • 2、进程退出
      • 2.1 退出码
      • 2.2 exit函数和_exit系统调用

    • 3、进程等待
      • 3.1 wait和waitpid
      • 3.2 阻塞和非阻塞





1、进程创建

进程的创建主要依靠系统接口fork函数

fork函数从已存在的一个进程中,创建一个子进程,原进程为父进程。

#include
#include //用pid_t 需要包括这个文件
pid_t fork(void);

父进程返回子进程pid, 子进程返回0,出错返回-1。


1.1 理解fork函数

先从一个小程序看看fork函数的效果。

1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/type.h> //用pid_t 需要包括这个文件
4 int main()
5 {
6 pid_t id &#61; fork();
7 if(id < 0)
8 {
9 printf("fork error!\n");
10 }
11
12 if(id > 0)
13 {
14 printf("当前进程的PID为: %d, 父进程PID是: %d, id: %d\n", getpid(), getppid(), id);
15 }
16
17 else
18 {
19 printf("当前进程的PID为: %d, 父进程PID是: %d, id: %d\n", getpid(), getppid(), id);
20 }
21 sleep(2);
22
23 return 0;
24 }

在这里插入图片描述



pid_t 是什么&#xff1f;
首先在/usr/include/sys/types.h中&#xff0c;通过通过/pid_t查询
这里是引用
再到/usr/include/bits/types.h中&#xff0c;通过/__pid_t查询


在这里插入图片描述
再到/usr/include/bits/typesizes.h中&#xff0c;通过/__PID_T_TYPE查询
在这里插入图片描述
回到/usr/include/bits/types.h中&#xff0c;通过/__S32_TYPE查询&#xff0c;发现其实就是int。
在这里插入图片描述

为什么要弄这么麻烦&#xff1f; 其实为了代码在不同平台上跑&#xff0c;可能其它平台是long&#xff0c;而不是int。&#xff08;为了可移植性&#xff09;




可能你会有疑惑&#xff0c;为什么会有两次打印&#xff1f;打印为什么是这个结果&#xff1f;代码是怎么走的&#xff1f;

我们的程序代码执行前
首先&#xff0c;我们所写的程序&#xff0c;在运行后加载到内存就成了Linux系统中的一个进程。
当我们运行编译好的程序后&#xff0c;程序加载到内存称为了一个Linux进程。
该进程&#xff08;对应pid:31200&#xff09;由命令行解释器bash&#xff08;bash是一个系统进程&#xff0c;这里对应31107&#xff09;创建&#xff0c;作为其子进程执行代码。

代码执行过程
进入main函数&#xff0c;执行pid_t id &#61; fork(); 此时转到操作系统内核fork定义处&#xff0c;执行fork函数代码。
&#xff08;下图的子进程&#xff0c;其实不是在fork中马上创了一个空间&#xff0c;这里为了更好理解&#xff0c;下面会解释&#xff09;
在这里插入图片描述

所以其实很简单&#xff0c;就是fork之后&#xff0c;有了两个执行流&#xff0c;通过返回值的不同走不同的代码路径。



1.2 fork函数的细节

有几个细节&#xff0c;能让我们更好理解fork。
在前面我们解释了fork函数为什么有两个返回值的问题&#xff0c;就是通过fork创建子进程&#xff0c;有了两个执行流。

首先


  • 如何理解fork之后&#xff0c;父进程返回子进程id&#xff0c;子进程返回0&#xff1f;
    我们都知道&#xff0c;一个父亲可以对应着多个孩子&#xff0c;而多个孩子只能对应一个父亲。
    进程也一样&#xff0c;我们可以通过getpid和getppid得到唯一的自己和父亲&#xff0c;但对于孩子&#xff0c;如果我们需要找其中一个就需要有一个确定值。

其次


  • 为什么会有一个变量id&#xff0c;储存两个不同的值?


pid id &#61; fork(); 首先对于一个进程&#xff0c;我们并不确定父子进程哪个先执行完。
返回的本质&#xff0c;其实就是写入值到id&#xff0c;所以谁先返回谁就先写入id。
后写入的进程&#xff0c;因为进程独立性&#xff0c;为了不影响前面的一个进程就会发生写时拷贝
在这里插入图片描述



  • 我们看到fork失败会返回-1&#xff0c;那么什么情况会发生呢&#xff1f;
    1、当系统中有太多进程&#xff0c;通常意味着某方面出了问题&#xff08;比如 死循环调用fork&#xff09;。
    2、当该用户ID的进程数超过了系统的限制数。&#xff08;CHILD_MAX&#xff09;

  • fork的通常用法
    1、通过创建子进程&#xff0c;继承父进程的代码&#xff0c;运行和父进程运行的不同代码路径。
    2、创建子进程&#xff0c;运行其它的进程&#xff08;比如后续进程程序替换中的exec系列的函数&#xff09;。


2、进程退出


2.1 退出码

从main函数开始。
我们之前写的程序很多在最后都会有一个return 0&#xff1b;
这个0其实就是退出码&#xff0c;它标识着程序运行成功。

int main()
{
return 0;
}


通过echo $? 可以查看记录最近一个进程在命令行执行完毕时对应的退出码。

在这里插入图片描述
在这里插入图片描述


  • 进程退出的情况&#xff1f;
    1、代码跑完&#xff0c;结果正确 — return 0;
    2、代码跑完了&#xff0c;结果不正确 — return !0;
    3、代码没跑完&#xff0c;程序异常了&#xff0c;退出码无意义。


如果我们关心退出码&#xff0c;可以通过不同的数字&#xff0c;表述不同的错误。
如果我们并不知道退出码对应的退出信息是什么&#xff0c;可以通过strerror(errno)。

如果熟悉个别退出码对应的信息&#xff0c;可以通过strerror(num) 打印退出信息。


1 #include <stdio.h>
2 #include <string.h>
3 #include <errno.h>
4
5 int main()
6 {
7 int i &#61; 0;
8 for(i &#61; 0; i < 200; &#43;&#43;i)
9 {
10 printf("[%d]: %s\n", i, strerror(i));
11 }
12 printf("return infor: %s\n", strerror(errno));
13 return 0;
14 }

由于结果太长&#xff0c;只截了开头一段和结尾一段。(通过运行看结果 可以看到退出码只有0-133)
在这里插入图片描述
在这里插入图片描述


2.2 exit函数和_exit系统调用


  • exit()
    exit函数终止进程&#xff0c;返回对应退出码。

    #include
    void exit(int status);

    exit虽然并没有返回值&#xff0c;但是会将status传给父进程接收退出码。&#xff08;这个后面进程等待会解释&#xff0c;先了解&#xff09;
    通过man 3 exit
    在这里插入图片描述
    在C语言阶段&#xff0c;我们会在一些地方使用exit(num)&#xff0c;里面的num其实就是退出码&#xff0c;退出码可以根据需要自己定义。

    #include
    #include
    void fun()
    {
    exit(10);//从这里程序退出。
    }
    int main()
    {
    fun();
    printf("hello world");
    }

  • _exit
    _exit作为一个系统接口&#xff0c;在操作系统层。以及exit其实就是调_exit实现的。

    #include
    void _exit(int status);

  • exit和_exit的区别

    先通过一个小程序看exit

    1 #include <stdio.h>
    2 #include <unistd.h>
    3 #include <stdlib.h>
    4
    5 int main()
    6 {
    7 printf("process");
    8 sleep(2);
    9 exit(1);
    10 }

    通过运行
    在这里插入图片描述

    再经过小小修改

    1 #include <stdio.h>
    2 #include <unistd.h>
    3 #include <stdlib.h>
    4
    5 int main()
    6 {
    7 printf("process");
    8 sleep(2);
    9 _exit(1);
    10 }

    在这里插入图片描述



    其实exit和_exit的区别就是exit刷新缓冲区&#xff0c;但是_exit不刷新缓冲区。
    sleep后&#xff0c;进程放入等待队列&#xff0c;输出在缓冲区&#xff0c;等进程重新回到运行队列_exit直接退出了&#xff0c;exit会刷新缓冲区&#xff0c;所以有这两个结果。


    根据这个结果我们也可以推出&#xff1a;缓存区其实是一个用户级的缓冲区。
    在这里插入图片描述


3、进程等待

一个子进程在退出后&#xff0c;操作系统回收它的数据与代码&#xff0c;但是进程一定是为了什么目的才存在的&#xff0c;一个进程完成后可以不将结果汇报给创造它的父进程&#xff0c;但是不能没有结果。

其实&#xff0c;一个进程在退出后&#xff0c;操作系统依旧会保留其PCB&#xff0c;等待父进程或系统对该进程进行回收。
子进程在这个PCB被保留的状态就是一个僵尸进程&#xff0c;父进程通过进程等待的方式对子进程回收并且获得子进程退出信息


3.1 wait和waitpid


  • wait()

#include
#include
pid_t wait(int* status);


当status值设为NULL时&#xff0c;只回收子进程&#xff0c;代表不在意回收的子进程的退出码。
当status不为NULL时&#xff0c;回收子进程&#xff0c;并且获得子进程的退出信息&#xff0c;存放在status中。

假设status不为NULL&#xff0c;status不是简单的存一个值&#xff0c;下面解释它如何保存信息。
在这里插入图片描述


1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <assert.h>
5 #include <sys/wait.h>
6 #include <sys/type.h>
7
8 int main()
9 {
10 pid_t id &#61; fork();
11 assert(id >&#61; 0);
12 if(id &#61;&#61; 0)
13 {
14 //子进程
15 printf("我是子进程: %d, 父进程: %d, id: %d\n", getpid(), getppid(), id);
16 exit(10); //随意设置
17 }
18
19 //父进程
20 sleep(2); //让子进程先运行完
21 int status &#61; 0;
22 pid_t ret &#61; wait(&status);
23 printf("return code : %d, sig : %d\n", (status >> 8), (status & 0x7F));
24 if(id > 0)
25 {
26 printf("wait success: %d\n", ret);
27
28 }
29 }

在这里插入图片描述


  • waitpid()

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

pid&#xff1a;进行等待的进程pid。
status&#xff1a;记录回收进程的退出信息。
options&#xff1a;一般选择是阻塞还是非阻塞两个状态。&#xff08;下面会说啥是阻塞&#xff09;
返回值返回回收的子进程pid&#xff0c;如果子进程还没退出返回0&#xff0c;如果waitpid调用失败返回-1。


稍稍修改一下代码

1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <assert.h>
5 #include <sys/wait.h>
6 #include <sys/types.h>
7
8 int main()
9 {
10 pid_t id &#61; fork();
11 assert(id >&#61; 0);
12 if(id &#61;&#61; 0)
13 {
14 //子进程
15 printf("我是子进程: %d, 父进程: %d, id: %d\n", getpid(), getppid(), id);
16 exit(10);
17 }
18
19 //父进程
20 sleep(2); //让子进程先运行完
21 int status &#61; 0;
22 pid_t ret &#61; waitpid(id, &status, 0);// 0 代表阻塞式等待 WNOHANG代表非阻塞式等待
23 printf("return code : %d, sig : %d\n", (status >> 8), (status & 0x7F));
24 if(id > 0)
25 {
26 printf("wait success: %d\n", ret);
27
28 }
29 }

  • 子进程退出的退出信息存放在哪&#xff1f;
    在这里插入图片描述

  • 补充&#xff1a;宏函数
    WIFEXITED(status)。W-wait&#xff0c;wait是否退出&#xff0c;若正常退出子进程&#xff0c;返回真。
    WEXITSTATUS(status)。查看进程退出码&#xff0c;若WIFEXITED非零&#xff0c;提取子进程退出码。

    //是否正常退出
    if(WIFEXITED(status))
    {
    // 判断子进程运行结果是否ok
    printf("exit code: %d\n", WEXITSTATUS(status);
    }


3.2 阻塞和非阻塞

前面wait相关的测试都是在子进程已经退出的前提下进行的。

阻塞和非阻塞很简单&#xff0c;将waitpid设置为阻塞后如果子进程没有退出&#xff0c;那么父进程就会一直等待&#xff0c;直到子进程退出。

父进程查看子进程状态&#xff0c;子进程没有退出&#xff0c;父进程立即返回去执行其它任务&#xff0c;这一次的过程叫做非阻塞。&#xff08;而父进程多次来回确认子进程有没有退出的过程称为轮询&#xff09;

一个测试非阻塞的程序

1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/wait.h>
4 #include <sys/types.h>
5 #include <stdlib.h>
6 #include <string.h>
7 #define NUM 10
8
9 typedef void (*func_t)();
10 func_t handlerTask[NUM];
11
12 void task1()
13 {
14 printf("do task1!\n");
15 }
16
17 void task2()
18 {
19 printf("do task2!\n");
20 }
21
22 void loadTask()
23 {
24 memset(handlerTask, 0, sizeof(handlerTask));
25 handlerTask[0] &#61; task1;
26 handlerTask[1] &#61; task2;
27 }
28
29 int main()
30 {
31 pid_t id &#61; fork();
32 if(id &#61;&#61; 0)
33 {
34 while(1)
35 {
36 //child
37 int cnt &#61; 3;
38 while(cnt)
39 {
40 printf("child running, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
41 sleep(1);
42 }
43
44 exit(10);
45 }
46 }
47 //parent
48 loadTask();
49 int status &#61; 0;
50 while(1)
51 {
52 pid_t ret &#61; waitpid(id, &status, WNOHANG); //WNOHANG: 非阻塞 -> 子进程没有退出&#xff0c; 父进程检测时候&#xff0c; 立即返回.
53 if(ret &#61;&#61; 0)
54 {
55 //waitpid调用成功 && 子进程没退出
56 //子进程没有退出&#xff0c;我的waitpid没有等待失败&#xff0c;仅仅是检测到了子进程没退出
57 printf("wait done, but child is runing , parent do :\n");
58 int i &#61; 0;
59 for(i &#61; 0; handlerTask[i]!&#61;NULL; &#43;&#43;i)
60 {
61 handlerTask[i]();
62 }
63 }
64 else if(ret > 0)
65 {
66 //waitpid调用成功 && 子进程退出了
67 printf("wait success, exit code: %d, sig: %d\n", (status >> 8), (status & 0x7F));
68 break;
69 }
70 else
71 {
72 //waitpid调用失败
73 printf("waitpid call failed\n");
74 break;
75 }
76 sleep(1);
77 }
78
79 return 0;
80 }

在这里插入图片描述
非阻塞不会占用父进程所有精力&#xff0c;可以在轮询期间干点别的&#xff01;

本章完~







推荐阅读
  • 如何在Java中使用DButils类
    这期内容当中小编将会给大家带来有关如何在Java中使用DButils类,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。D ... [详细]
  • WinMain 函数详解及示例
    本文详细介绍了 WinMain 函数的参数及其用途,并提供了一个具体的示例代码来解析 WinMain 函数的实现。 ... [详细]
  • 开机自启动的几种方式
    0x01快速自启动目录快速启动目录自启动方式源于Windows中的一个目录,这个目录一般叫启动或者Startup。位于该目录下的PE文件会在开机后进行自启动 ... [详细]
  • 本文探讨了如何利用Java代码获取当前本地操作系统中正在运行的进程列表及其详细信息。通过引入必要的包和类,开发者可以轻松地实现这一功能,为系统监控和管理提供有力支持。示例代码展示了具体实现方法,适用于需要了解系统进程状态的开发人员。 ... [详细]
  • JVM钩子函数的应用场景详解
    本文详细介绍了JVM钩子函数的多种应用场景,包括正常关闭、异常关闭和强制关闭。通过具体示例和代码演示,帮助读者更好地理解和应用这一机制。适合对Java编程和JVM有一定基础的开发者阅读。 ... [详细]
  • 本文详细介绍了Java反射机制的基本概念、获取Class对象的方法、反射的主要功能及其在实际开发中的应用。通过具体示例,帮助读者更好地理解和使用Java反射。 ... [详细]
  • IOS Run loop详解
    为什么80%的码农都做不了架构师?转自http:blog.csdn.netztp800201articledetails9240913感谢作者分享Objecti ... [详细]
  • Ihavetwomethodsofgeneratingmdistinctrandomnumbersintherange[0..n-1]我有两种方法在范围[0.n-1]中生 ... [详细]
  • 本文介绍如何使用线段树解决洛谷 P1531 我讨厌它问题,重点在于单点更新和区间查询最大值。 ... [详细]
  • 在多线程并发环境中,普通变量的操作往往是线程不安全的。本文通过一个简单的例子,展示了如何使用 AtomicInteger 类及其核心的 CAS 无锁算法来保证线程安全。 ... [详细]
  • [转]doc,ppt,xls文件格式转PDF格式http:blog.csdn.netlee353086articledetails7920355确实好用。需要注意的是#import ... [详细]
  • 在JavaWeb开发中,文件上传是一个常见的需求。无论是通过表单还是其他方式上传文件,都必须使用POST请求。前端部分通常采用HTML表单来实现文件选择和提交功能。后端则利用Apache Commons FileUpload库来处理上传的文件,该库提供了强大的文件解析和存储能力,能够高效地处理各种文件类型。此外,为了提高系统的安全性和稳定性,还需要对上传文件的大小、格式等进行严格的校验和限制。 ... [详细]
  • Flowable 流程图路径与节点展示:已执行节点高亮红色标记,增强可视化效果
    在Flowable流程图中,通常仅显示当前节点,而路径则需自行获取。特别是在多次驳回的情况下,节点可能会出现混乱。本文重点探讨了如何准确地展示流程图效果,包括已结束的流程和正在执行的流程。具体实现方法包括生成带有高亮红色标记的图片,以增强可视化效果,确保用户能够清晰地了解每个节点的状态。 ... [详细]
  • ### 优化后的摘要本文对 HDU ACM 1073 题目进行了详细解析,该题属于基础字符串处理范畴。通过分析题目要求,我们可以发现这是一道较为简单的题目。代码实现中使用了 C++ 语言,并定义了一个常量 `N` 用于字符串长度的限制。主要操作包括字符串的输入、处理和输出,具体步骤涉及字符数组的初始化和字符串的逆序操作。通过对该题目的深入探讨,读者可以更好地理解字符串处理的基本方法和技巧。 ... [详细]
  • 在C语言中,指针的高级应用及其实例分析具有重要意义。通过使用 `&` 符号可以获取变量的内存地址,而 `*` 符号则用于定义指针变量。例如,`int *p;` 定义了一个指向整型的指针变量 `p`。其中,`p` 代表指针变量本身,而 `*p` 则表示指针所指向的内存地址中的内容。此外,指针在不同函数中可以具有相同的变量名,但其作用域和生命周期会有所不同。指针的灵活运用能够有效提升程序的效率和可维护性。 ... [详细]
author-avatar
奔跑的饼干的饼干桶_698
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有