热门标签 | 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;

本章完~







推荐阅读
  • 本文详细探讨了 Android Service 组件中 onStartCommand 方法的四种不同返回值及其应用场景。Service 可以在后台执行长时间的操作,无需提供用户界面,支持通过启动和绑定两种方式创建。 ... [详细]
  • Hadoop MapReduce 实战案例:手机流量使用统计分析
    本文通过一个具体的Hadoop MapReduce案例,详细介绍了如何利用MapReduce框架来统计和分析手机用户的流量使用情况,包括上行和下行流量的计算以及总流量的汇总。 ... [详细]
  • Java中提取字符串的最后一部分
    本文介绍了如何使用Java中的substring()和split()方法来提取字符串的最后一部分,特别是在处理包含特殊字符的路径时的方法与技巧。 ... [详细]
  • 题目概述:Sereja 拥有一个由 n 个整数组成的数组 a1, a2, ..., an。他计划执行 m 项操作,这些操作包括更新数组中的特定元素、增加数组中所有元素的值,以及查询数组中的特定元素。 ... [详细]
  • 题目描述:Balala Power! 时间限制:4000/2000 MS (Java/Other) 内存限制:131072/131072 K (Java/Other)。题目背景及问题描述详见正文。 ... [详细]
  • 本文探讨了Linux环境下线程私有数据(Thread-Specific Data, TSD)的概念及其重要性,介绍了如何通过TSD技术避免多线程间全局变量冲突的问题,并提供了具体的实现方法和示例代码。 ... [详细]
  • C/C++ 应用程序的安装与卸载解决方案
    本文介绍了如何使用Inno Setup来创建C/C++应用程序的安装程序,包括自动检测并安装所需的运行库,确保应用能够顺利安装和卸载。 ... [详细]
  • 本文详细介绍如何在SSM(Spring + Spring MVC + MyBatis)框架中实现分页功能。包括分页的基本概念、数据准备、前端分页栏的设计与实现、后端分页逻辑的编写以及最终的测试步骤。 ... [详细]
  • Go语言实现文件读取与终端输出
    本文介绍如何使用Go语言编写程序,通过命令行参数指定文件路径,读取文件内容并将其输出到控制台。代码示例中包含了错误处理和资源管理的最佳实践。 ... [详细]
  • 如何使用Maven将依赖插件一并打包进JAR文件
    本文详细介绍了在使用Maven构建项目时,如何将所需的依赖插件一同打包进最终的JAR文件中,以避免手动部署依赖库的麻烦。 ... [详细]
  • Java连接MySQL数据库的方法及测试示例
    本文详细介绍了如何安装MySQL数据库,并通过Java编程语言实现与MySQL数据库的连接,包括环境搭建、数据库创建以及简单的查询操作。 ... [详细]
  • 本文详细探讨了PHP中使用const和define定义常量的方法及其差异。了解这些区别有助于开发者根据具体需求选择合适的方式定义常量。 ... [详细]
  • 本文旨在探讨Swift中的Closure与Objective-C中的Block之间的区别与联系,通过定义、使用方式以及外部变量捕获等方面的比较,帮助开发者更好地理解这两种机制的特点及应用场景。 ... [详细]
  • 本文介绍了如何使用 Python 的 Pyglet 库加载并显示图像。Pyglet 是一个用于开发图形用户界面应用的强大工具,特别适用于游戏和多媒体项目。 ... [详细]
  • 本文分享了作者在使用LaTeX过程中的几点心得,涵盖了从文档编辑、代码高亮、图形绘制到3D模型展示等多个方面的内容。适合希望深入了解LaTeX高级功能的用户。 ... [详细]
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社区 版权所有