【Linux】进程控制——创建,终止,等待回收

ops/2024/11/8 13:17:34/

目录

  • 进程创建
    • fork再介绍
    • 写时拷贝
  • 进程终止
    • 退出码
    • 退出方式
  • 进程等待
    • 获取子进程status
    • wait
    • waitpid

在前两篇进程概念中,对进程进行了介绍,进行了初步认识,也认识到了与之相关联的进程地址空间;本文则对进程的生命周期——创建,终止,回收进行介绍,加深对进程的学习。如果对进程相关概念不了解的可以参考——进程概念一,进程概念二

进程创建

进程创建中,fork函数是不可或缺的。

fork再介绍

在Linux中fork函数是非常重要的函数:它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程

FORK(2)                                                                                                                                         FORK(2)NAMEfork - create a child processSYNOPSIS#include <sys/types.h>#include <unistd.h>pid_t fork(void);
  • fork为系统级调用
  • 类型为pid_t,本质为int
  • 返回值:自进程中返回0,父进程返回子进程id,出错返回-1

进程调用fork,当控制转移到内核中的fork代码后,内核做以下工作:

  1. 分配新的内存块和内核数据结构给子进程
  2. 将父进程部分数据结构内容拷贝至子进程
  3. 添加子进程到系统进程列表当中
  4. fork返回,开始调度器调度

fork调用
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>int main()
{printf("before fork pid:%d\n",getpid());pid_t pid = fork();printf("after fork pid:%d\n",getpid());
}

fork调用
这里看到了三行输出,一行before,两行after。进程35244先打印before消息,然后它又打印after。另一个after
消息有35245打印的。注意到进程35245没有打印before,为什么呢?如下图所示
fork执行流
所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。

  • 注意,fork之后,父子进程谁先执行完全由调度器决定。

fork函数返回值

  • 子进程返回0
  • 父进程返回的是子进程的pid

写时拷贝

进程具有独立性,父子进程理论上都具有独立的代码和数据,但是实际中,在不修改数据时,父子进程看到的是同一份代码和数据,只有在子进程修改数据时,才会通过写时拷贝机制,存放这份子进程独有的数据;如下:

int g_val=100;//进程创建
int main()
{pid_t pid=fork();if(pid>0)//父进程{int num=4;while(num--){printf("I am father pid: %d  g_val:%d g_val_addr: %p\n",getpid(),g_val,&g_val);sleep(1);}}else if(pid==0)//子进程{int num=3;while (num--){printf("I am child ppid: %d  pid:%d  g_val: %d g_val_addr: %p\n",getppid(),getpid(),g_val,&g_val);sleep(1);if(num==1){g_val=200;printf("child g_val change to %d\n",g_val);}}}else{perror("fork\n");}return 0;
}

写时拷贝

可以看到当子进程对g_val进行修改后,父子进程的g_val不再是同一个值,这就是通过写时拷贝实现的。

  • 不解地址为什么还是相同的原因请参考——进程地址空间

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
写实拷贝机制

fork常规用法

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

进程终止

事物有始有终,一个进程既然被创建,那么就会有终止的时候;例如父进程创建子进程去执行相关任务,但是父子进程是独立的,父进程如何知道子进程有没有完成下派的任务呢?这就需要进程退出码了。

退出码

进程退出场景

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果不正确
  3. 代码异常终止

进程退出有以上三种状况;而这三种状况中,前两种的退出码是有意义的,可参考的。代码异常终止一般是收到信号,程序并没有安设想的方式退出,退出码也就没有参考意义了。

正常退出

例如:

int main()
{return 99;
}

查看退出码

使用指令echo $?可以查看上一次程序的退出码

查看退出码
可以看到,在运行程序后查看退出码确实是myproc的退出码;但是再次查看时,退出码却是0;这是因为指令本身也是可执行程序,这个退出码0是指令的退出码。

异常终止

异常终止一般是收到了信号,如直接使用kill -9指令直接终止该进程

int main()
{while(1){printf("pid:%d\n",getpid());sleep(1);}return 99;
}

异常终止
可以看到该退出码并不是我们设想的,所以异常退出的退出码是没有意义的。

而不同的退出码一般会被定义为不同的问题,在C语言中可以使用strerror查看;在C语言中:退出码0代表成功,这也是为什么在main函数中总是return 0的原因,而1到133都对应着不同的问题。

int main()
{for(int i=0;i<150;i++){printf("%d :%s\n",i,strerror(i));}return 0;
}

退出码

退出方式

正常终止

  1. 从main返回
  2. 调用exit
  3. _exit

main返回

退出码是给父进程看的,可以判断子进程是否成功运行,在C/C++中。main函数是程序的开始,所以main函数返回会使用return,告诉父进程(OS)子进程的完成情况。

exit返回

NAMEexit - cause normal process terminationSYNOPSIS#include <stdlib.h>void exit(int status);

exit为库函数,用于退出当前进程,同时会将退出码设置。如:

int main()
{exit(1);printf("can u see me?\n");return 0;
}

exit
可以看到退出码确实为1,而且并没有执行exit后面的代码,所以exit的作用就是:直接退出当前进程并设置退出码

_exit

NAME_exit, _Exit - terminate the calling processSYNOPSIS#include <unistd.h>void _exit(int status);

_exit为系统级调用,实际上库函数exit就是对_exit的封装,_exit的用法和exit一致;那有什么区别呢?看下面的例子
_exit
可以看到_exit的代码中,u can see me这句话并没有打印出来。

实际上exit最后也会调用exit,但在调用exit之前,还做了其他工作:

  • _exit 就只是单纯的退出程序
  • exit在退出之前还会做一些事,比如冲刷缓冲区,再调用_exit
    _exit对比exit
    所以推荐使用exit

进程等待

为什么要进行进程等待?谁在等待?

首先需要明确:如果父进程不对子进程进行回收,子进程结束后就会变成僵尸进程,有可能导致内存泄漏。
wait

进程等待必要性

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

所以,当子进程创建出来后,父进程需要进行等待,目的是获取进程退出码(次要)和释放子进程的资源(主要),避免僵尸问题。

获取子进程status

系统提供的父进程等待函数有两个waitwaitpid,后者可操作项更多,比较常用;
waitpid
如何获取进程退出码的方式就是通过第二个参数status,所以,在介绍wait waitpid前,先了解父进程是如何获取子进程退出码的。

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

status
只研究低16位比特位:从0号开始到7号为信号部分;8到15才是具体的进程退出码。(位图结构)

退出方式
如何获取退出码呢?虽然status是整型,但使用时却是当作位图来使用的,所以需要进行一些位操作,而系统也已经提供了对应的宏解析status。

WEXITSTATUS(status);//查看进程退出码
WIFEXITED(status);//检测进程是否收到信号退出;若正常退出,则为真,否则为假

WEXITSTATUS就是一个宏:

#define	__WEXITSTATUS(status)	(((status) & 0xff00) >> 8)

注意:WIFEXITED仅用来判断是否收到信号而退出进程,不解析收到几号信号;若收到信号退出则为假,正常退出为真。

WTERMSIG才是用来查看收到几号信号的。

WTERMSIG(status)

宏定义

#define	__WTERMSIG(status)	((status) & 0x7f)

wait

函数原型

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);

返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,若不关心则可以设置成为NULL

int main()
{//WNOHANG;//宏,为1pid_t pid=fork();if(pid==0){// childint cnt = 5;while (cnt--){printf("I am child pid:%d ppid:%d\n", getpid(), getppid());sleep(1);if (cnt == 0){printf("child over\n");}}exit(99);}pid_t rid=wait(NULL);printf("father wait success rid:%d\n",rid);//注意,父子进程本应是各自执行自己的代码的,但是wait会阻塞父进程,直至回收才会执行后面的代码。return 0;
}

注意:使用wait回收子进程,父进程会阻塞在wait处,直到回收成功才继续执行后续代码。
wait

waitpid

函数原型

#include<sys/types.h>
#include<sys/wait.h>
pid_ t waitpid(pid_t pid, int *status, int options);

返回值:

  • 当正常返回的时候waitpid返回收集到的子进程的进程ID;
  • 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
  • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数:

pid:

Pid=-1,等待任一个子进程。与wait等效。
Pid>0,等待其进程ID与pid相等的子进程。

status:

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
WTERMSIG(status):提取子进程收到的信号。

options:

0:将option设为0,等待方式和wait一样,为阻塞等待。
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进
程的ID。

  • 这也是另一种等待方式——非阻塞轮询

第三个参数options;提供不同的等待方式;一般为0:阻塞等待;或者WNOHANG:非阻塞轮询——父进程可以执行自己的代码。

阻塞等待
waitpid(pid,&status,0);

int main()
{//WNOHANG;//宏,为1pid_t pid=fork();if(pid==0){// childint cnt = 3;while (cnt--){printf("I am child pid:%d ppid:%d\n", getpid(), getppid());sleep(1);if (cnt == 0){printf("child over\n");}}exit(99);}// fatherint status=0;pid_t rid=waitpid(pid,&status,0);int cnt = 5;while (cnt--){printf("I am father pid:%d\n", getpid());sleep(1);}if(rid>0 && WIFEXITED(status))//若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出,不能看出是几号信号){printf("wait success:%d exit_code:%d\n",rid,WEXITSTATUS(status));}else{printf("kill by sig:%d\n", WTERMSIG(status));//((status) & 0x7f)}return 0;
}

waitpid

非阻塞轮询
waitpid(pid, &status, WNOHANG)

int main()
{pid_t pid = fork();if (pid == 0){// childint cnt = 3;while (cnt){printf("I am child pid:%d ppid:%d\n", getpid(), getppid());sleep(1);cnt--;if (cnt == 0){printf("child over\n");}}exit(1);}int status=0;pid_t rid;while(1){rid = waitpid(pid, &status, WNOHANG);//非阻塞轮询if(rid==0){printf("father do other things...\n");sleep(1);}else if (rid > 0){if (WIFEXITED(status)){printf("wait success:%d exit_code:%d\n", rid, WEXITSTATUS(status));}else{printf("kill by sig:%d\n", WTERMSIG(status));//((status) & 0x7f)}break;}else{printf("waitpid error...\n");break;}}printf("father over\n");//sleep(5);return 0;
}

waitpid
以非阻塞轮询的方式回收可以看到:父进程每隔一段时间就去查看子进程是否退出,若未退出,则父进程先去忙自己的事情,过一段时间再来查看,直到子进程退出后读取子进程的退出信息。

除了以上正常方式退出,当然也有可能收到信号而退出。如kill -9指令杀掉一个进程。
kill
以上就是进程创建,终止,等待回收的相关问题。


http://www.ppmy.cn/ops/131937.html

相关文章

正则表达式在Kotlin中的应用:提取图片链接

在现代的Web开发中&#xff0c;经常需要从网页内容中提取特定的数据&#xff0c;例如图片链接。Kotlin作为一种现代的编程语言&#xff0c;提供了强大的网络请求和文本处理能力。本文将介绍如何使用Kotlin结合正则表达式来提取网页中的图片链接。 正则表达式基础 正则表达式是…

Wecom酱搭建企业微信发送消息

Wecom酱 https://github.com/easychen/wecomchan 企业微信 https://work.weixin.qq.com/ 获取企业id 创建应用 获取企业微信应用id、secret 设置可信域名和可信ip 邀请用户关注 https://你的域名/wxsend.php?sendkeydyf&msg测试 发送成功

vue组件获取props中的数据并绑定到form表单 el-form-item的v-model中方法

在vue的组件的form表单中, 我们可以直接使用props中传递的数据,如: <el-form-item label"姓名:">{{ value.real_name }} </el-form-item> 这里的value是通过props传递来的 const props defineProps({value: {type: [Object, String],required: true} })…

XML标记语言

最近在学XXE-XML外部实体注入漏洞时候&#xff0c;浅浅学习了一下XML&#xff0c;谨做此学习笔记。 目录 一&#xff1a;XML概述 二&#xff1a;XML语法 XML中的CDATA 三&#xff1a;使用PHP解析XML文档 添加节点 四&#xff1a;Xpath语言 绝对查找 相对查找 使用*匹配…

如何在 Java 中使用 Canal 同步 MySQL 数据到 Redis

文章目录 一、引言二、工作原理1. MySQL主备复制原理2. canal 工作原理 三、环境准备1. 安装和配置 MySQL2. 安装和配置 Canal3. 安装和配置 Redis 四、开发 Java 应用1. 添加依赖2. 编写 Canal 客户端代码3. 运行和测试3.1 启动 Canal 服务&#xff1a;3.2 启动 Redis 服务&am…

全星魅-物联网定位终端-北斗定位便携终端-北斗有源终端

在当今快速发展的物流运输行业中&#xff0c;精准定位与实时监控已成为确保货物安全与高效运输的关键因素。为了满足这一需求&#xff0c;QMCZ10作为一款集4G&#xff08;LTE Cat1&#xff09;通讯技术与智能定位功能于一体的终端产品&#xff0c;应运而生。它不仅具备普通定位…

【算法与数据结构】【链表篇】【题1-题5】

题1.从尾到头打印链表 题目&#xff1a;输入一个链表的头结点&#xff0c;从尾到头反过来打印出每个节点的值。链表的定义如下&#xff1a; struct ListNode {int mValue;ListNode *mNext;ListNode *mPrev; }; 1.1 方法一&#xff1a;栈 思路&#xff1a;要反过来打印&…

爬虫-------字体反爬

目录 一、了解什么是字体加密 二. 定位字体位置 三. python处理字体 1. 工具库 2. 字体读取 3. 处理字体 案例1:起点 案例2:字符偏移: 5请求数据 - 发现偏移量 5.4 多套字体替换 套用模板 版本1 版本2 四.项目实战 1. 采集目标 2. 逆向结果 一、了解什么是…