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

本章完~







推荐阅读
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • C语言注释工具及快捷键,删除C语言注释工具的实现思路
    本文介绍了C语言中注释的两种方式以及注释的作用,提供了删除C语言注释的工具实现思路,并分享了C语言中注释的快捷键操作方法。 ... [详细]
  • Android源码深入理解JNI技术的概述和应用
    本文介绍了Android源码中的JNI技术,包括概述和应用。JNI是Java Native Interface的缩写,是一种技术,可以实现Java程序调用Native语言写的函数,以及Native程序调用Java层的函数。在Android平台上,JNI充当了连接Java世界和Native世界的桥梁。本文通过分析Android源码中的相关文件和位置,深入探讨了JNI技术在Android开发中的重要性和应用场景。 ... [详细]
  • C++字符字符串处理及字符集编码方案
    本文介绍了C++中字符字符串处理的问题,并详细解释了字符集编码方案,包括UNICODE、Windows apps采用的UTF-16编码、ASCII、SBCS和DBCS编码方案。同时说明了ANSI C标准和Windows中的字符/字符串数据类型实现。文章还提到了在编译时需要定义UNICODE宏以支持unicode编码,否则将使用windows code page编译。最后,给出了相关的头文件和数据类型定义。 ... [详细]
  • 本文介绍了Linux Shell中括号和整数扩展的使用方法,包括命令组、命令替换、初始化数组以及算术表达式和逻辑判断的相关内容。括号中的命令将会在新开的子shell中顺序执行,括号中的变量不能被脚本余下的部分使用。命令替换可以用于将命令的标准输出作为另一个命令的输入。括号中的运算符和表达式符合C语言运算规则,可以用在整数扩展中进行算术计算和逻辑判断。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • 树莓派语音控制的配置方法和步骤
    本文介绍了在树莓派上实现语音控制的配置方法和步骤。首先感谢博主Eoman的帮助,文章参考了他的内容。树莓派的配置需要通过sudo raspi-config进行,然后使用Eoman的控制方法,即安装wiringPi库并编写控制引脚的脚本。具体的安装步骤和脚本编写方法在文章中详细介绍。 ... [详细]
  • 本文介绍了在Android开发中使用软引用和弱引用的应用。如果一个对象只具有软引用,那么只有在内存不够的情况下才会被回收,可以用来实现内存敏感的高速缓存;而如果一个对象只具有弱引用,不管内存是否足够,都会被垃圾回收器回收。软引用和弱引用还可以与引用队列联合使用,当被引用的对象被回收时,会将引用加入到关联的引用队列中。软引用和弱引用的根本区别在于生命周期的长短,弱引用的对象可能随时被回收,而软引用的对象只有在内存不够时才会被回收。 ... [详细]
  • 本文介绍了GTK+中的GObject对象系统,该系统是基于GLib和C语言完成的面向对象的框架,提供了灵活、可扩展且易于映射到其他语言的特性。其中最重要的是GType,它是GLib运行时类型认证和管理系统的基础,通过注册和管理基本数据类型、用户定义对象和界面类型来实现对象的继承。文章详细解释了GObject系统中对象的三个部分:唯一的ID标识、类结构和实例结构。 ... [详细]
  • BZOJ1233 干草堆单调队列优化DP
    本文介绍了一个关于干草堆摆放的问题,通过使用单调队列来优化DP算法,求解最多可以叠几层干草堆。具体的解题思路和转移方程在文章中进行了详细说明,并给出了相应的代码示例。 ... [详细]
  • linux进阶50——无锁CAS
    1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰ ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • VScode格式化文档换行或不换行的设置方法
    本文介绍了在VScode中设置格式化文档换行或不换行的方法,包括使用插件和修改settings.json文件的内容。详细步骤为:找到settings.json文件,将其中的代码替换为指定的代码。 ... [详细]
  • ejava,刘聪dejava
    本文目录一览:1、什么是Java?2、java ... [详细]
  • 《2017年3月全国计算机等级考试二级C语言上机题库完全版》由会员分享,可在线阅读,更多相关《2017年3月全国计算机等级考试二级C语言上机题库完全版( ... [详细]
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社区 版权所有