Linux进程编程、fork函数范例详解 ( 5 ) -【Linux通信架构系列 】

news2024/11/16 6:33:47

系列文章目录

C++技能系列
Linux通信架构系列
C++高性能优化编程系列
深入理解软件架构设计系列
高级C++并发线程编程

期待你的关注哦!!!
在这里插入图片描述

现在的一切都是为将来的梦想编织翅膀,让梦想在现实中展翅高飞。
Now everything is for the future of dream weaving wings, let the dream fly in reality.

Linux进程编程、fork函数范例详解

  • 系列文章目录
  • 一、认识fork函数及简单范例
  • 二、僵尸进程的产生、解决,SIGCHLD
  • 三、进程的内存空间及进程的产生
  • 四、判断父进程进程和子进程的执行分支
  • 五、一个和fork执行有关的逻辑判断
  • 六、fork失败的可能原因总结

进程的概念

一个可执行程序执行一次就是一个进程,再执行一次就有是一个进程(多个进程共享同一个可执行文件),换句话说,进程一般定义为程序为程序执行的一个实例。

一、认识fork函数及简单范例

在一个进程中可以使用fork创建一个子进程,当该子进程创建时, 它从fork函数的下一条语句(或者说fork的返回处)开始执行与父进程相同的代码。 换句话说, fork函数产生一个和当前进程完全一样的新进程,并和当前进程一样从fork函数调用中返回。

试想,原来只有1个父进程在运行,是1条执行通路,调用fork之后,就变成了2条执行通路(父进程一条,子进程1条)。如图1.1所示:
在这里插入图片描述

图1.1 调用fork后,程序执行通路从原来的1条变成2条(父进程1条,子进程1条)

看如下范例:

#include <stdio.h>
#include <stdlib.h> //malloc,exit
#include <unistd.h> //fork
#include <signal.h>

//信号处理函数
void sig_usr(int signo)
{
	printf("收到了SIGUSR1信号,进程ID = %d!\n", getpid());
}

int main(int argc, char *const *argv)
{
	pid_t pid;
	printf("进程开始执行!\n");
	//先简单处理一个信号
	if(signal(SIGUSR1, sig_usr) == SIG_ERR)
	{
		printf("无法捕捉SIGUSR1信号!\n");
		exit(1);
	}
	//创建1个子线程
	pid = fork();
	//要判断子进程是否创建成功
	if(pid < 0)
	{
		printf("子进程创建失败,很遗憾!\n");
		exit(1);
	}
	//现在,父进程和子进程同时开始运行了
	for(;;)
	{
		sleep(1);
		printf("休息1s,进程ID = %d!\n", getpid());
	}
	printf("再见了!\n");
	return 0;
}

编译、链接并运行,结果如下:

在这里插入图片描述

图1.2 父进程调用fork创建子进程,并杀死子进程后父进程收到SIGCHLD信号

在这里插入图片描述

图1.3 将子进程杀死后,依旧可以看到子进程(僵尸进程)

(1)可以注意到,进程ID为1183的父进程ID是1182,说明1182这个进程调用fork函数创建1183进程。另外注意这2个进程的状态都是S+。S是休眠因为进程大部分时间执行的是sleep,所以休眠是正常的;+表示位于前台进程组。

(2)但这里注意一点,调用fork函数创建出一个子进程后,后续的代码是父进程先执行还是子进程先执行并不确定,不代表父进程一定快,因为存在进程的时间片调度问题(这与内核调度算法有关)。

(3)从打印结果中可以看出,父进程和子进程都能收到这个信号,说明信号捕捉这段代码,是子进程和父进程的公共代码(对父进程和子进程都有效,或者说这段代码既在父进程中,也在子进程中 - 虽然子进程是后面fork函数创建出来的,但在子进程创建出来之前父进程执行的所有代码都相当于子进程执行过了)。

(4)也可以注意到,我们通过kill -9 命令(-9代表SIGKILL信号,该信号不能被拦截,不能被捕获)把子进程杀掉,父进程收到了SIGCHLD信号。

(5)从图1.3中我们可以看到被杀掉的1183子进程仍旧存在于ps命令的列表中,但这COMMAND列显示defunct(失效的意思),而STAT列显示Z+(Z状态表示僵尸进程)。总之,无论是Z状态还是defunct字样,都是僵尸进程的典型标记。

二、僵尸进程的产生、解决,SIGCHLD

⚠️(1)僵尸进程是怎么产生的能呢?

在Linux操作系统中,如果一个子进程终止了,但父进程还活着,当该父进程没有调用(wait / waitpid)函数来进行一些额外处置(处置子进程终止这件事),那么这个子进程会变成一个僵尸进程

这种僵尸进程已经被终止了,不工作了,但是依旧没有被内核丢弃,因为内核认为父进程可能还需要该子进程的一些信息。

僵尸进程是占用资源的,至少会占用进程ID(PID)。整个操作系统中进程号是有限的,所以,作为开发者不应该允许僵尸进程的存在

⚠️(2)那么怎么能让僵尸进程消失呢?

重启计算机?手动把这个僵尸进程的父进程杀掉?这两个都不是好办法。
我们应该从代码的角度来避免僵尸进程的产生

当子进程被杀掉的时候,父进程收到了一个SIGCHLD信号。所以,对于源码中fork的行为(会创建子进程)的进程,我们应该拦截并处理SIGCHLD信号。

看如下范例:

#include <stdio.h>
#include <stdlib.h> //malloc,exit
#include <unistd.h> //fork
#include <signal.h>
#include <sys/wait.h> //waitpid

//信号处理函数
void sig_usr(int signo)
{
	
	int status;
	switch(signo)
	{
		case SIGUSR1:
		{
			printf("收到了SIGUSR1信号,进程ID = %d!\n", getpid());
		}
		break;
		case SIGCHLD:
		{
			printf("收到了SIGCHLD信号,进程ID = %d!\n", getpid());
			//waitpid获取子进程的终止状态,子进程就不会成为僵尸进程了
			//第一个参数:-1,表示等待任何的子进程
			//第二个参数:保存子进程的状态信息
			//第三个参数:WNOHANG表示不要阻塞,让这个waitpid()立即返回
			pid_t pid = waitpid(-1, &status, WNOHANG);
			
			if(pid == 0)
				return;
			//子进程没结束,会立即返回该数字,但这里应该不是该数字,这里的情况是子进程结束才出发父进程的该信号
			if(pid == -1)
				return;
			//走到这里,表示成功,程序返回
			return;
		}
		break;
	}

}

int main(int argc, char *const *argv)
{
	pid_t pid;
	printf("进程开始执行!\n");
	//先简单处理一个信号
	if(signal(SIGUSR1, sig_usr) == SIG_ERR)
	{
		printf("无法捕捉SIGUSR1信号!\n");
		exit(1);
	}
	//增加SIGCHLD信号的捕捉
	if(signal(SIGCHLD, sig_usr) == SIG_ERR)
	{
		printf("无法捕捉SIGCHLD信号!\n");
		exit(1);
	}
	//创建1个子线程
	pid = fork();
	//要判断子进程是否创建成功
	if(pid < 0)
	{
		printf("子进程创建失败,很遗憾!\n");
		exit(1);
	}
	//现在,父进程和子进程同时开始运行了
	for(;;)
	{
		sleep(1);
		printf("休息1s,进程ID = %d!\n", getpid());
	}
	printf("再见了!\n");
	return 0;
}

编译、链接并运行,结果如下:
在这里插入图片描述

图2.1 将子进程杀死后,没有僵尸进程了

三、进程的内存空间及进程的产生

fork 产生新进程的速度非常快, 产生的新进程并不复制原来进程的内存空间,而是和原来进程(父进程)一起共享一个内存空间。 这个内存空间的特性是"写时复制",也就是说,原来的进程和fork出来的子进程可以同时自由读取内存,但如果子进程(或者父进程)对内存进行修改,这个内存就会复制一份给该进程单独使用,以免影响该内存空间的其他进程的使用。

看如下范例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

int main(int argc, char * const * argv)
{
	fork(); //一般fork都会成功,所以不判断返回值了
	fork();
	
	for(;;)
	{
		sleep(1); //休息1s
		printf("休息1s, 进程ID = %d!\n", getpid());
	}
	printf("再见了!\n");
	return 0;
}

⚠️上面的代码执行后会产生几个进程?

fork的能力简单说就是一分二(一条路线分成两条路线 / 一个进程变成了2个进程)。

代码中,第一个fork一分二,2条线同时往下走,2条线都经历了第2个fork,每个fork又分出2个,所以二分四(最终产生了4个进程)

接下来我们看下执行结果:
在这里插入图片描述

图3.1 调用2次fork,产生4个nginx进程

四、判断父进程进程和子进程的执行分支

一执行fork,1条路线变成了2条路线,所以fork函数的实际返回了2次(父进程返回了1次;子进程中也返回了1次)。

fork函数在父进程中返回的值和在子进程中返回的值是不同的,据此可以编写代码分别识别出当前是父进程还是子进程,从而让父子进程执行不同的代码分支。

通过观察下面的代码范例,可以发现,程序正是 通过判断fork的返回值来决定父进程执行哪些代码、子进程执行哪些代码。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

int g_mygbltest = 0;
int main(int argc, char * const * argv)
{
	pid_t pid;
	printf("进程开始执行!\n");
	//创建一个子进程
	pid = fork();
	//要判断子进程是否成功
	if(pid < 0)
	{
		printf("子进程创建失败,很遗憾!\n");
		exit(1);
	}
	//走到这里,fork()成功,执行后续代码的可能是父进程,也可能是子进程
	if(pid == 0)
	{
		//子进程,因为子进程的fork()返回值会是0
		//这是专门针对子进程的处理代码
		while(1)
		{
			g_mygbltest++;
			sleep(1); //休息1s
			printf("我是子进程,我的进程ID = %d, g_mygbltest = %d\n", getpid(), g_mygbltest);
		}
	}else{
		//这里就是父进程,因为父进程的fork()返回值会 >0
		//这是专门针对父进程的处理代码
		while(1)
		{
			g_mygbltest++;
			sleep(5);
			printf("我是父进程,我的进程ID = %d, g_mygbltest = %d\n", getpid(), g_mygbltest);
		}
	}
	return 0;
}

运行结果如下:
在这里插入图片描述

图4.1 父进程进程和子进程的执行分支运行结果

观察上面的结果,重点关注g_mygbltest全局变量的值,可以看到,父进程和子进程的该全局变量的值是不同的,每个进程都是单独计数的。

通过上面的范例,可以得出一个结论

fork对于子进程,返回值0;对于父进程,返回值是新建立的子进程的ID。

父进程和子进程的全局量g_mygbltest值也不同,每个进程都有不同的值,因为这2个进程都有写的动作(改写全局变量g_mygbltest的值,也就是改写内存),内核会给每个进程单独分配一块内存供其单独使用,所以每个进程的g_mygbltest值是互不干扰的。

五、一个和fork执行有关的逻辑判断

#include<stdio.h>
#include<stdlib.h> //malloc,exit
#include<unistd.h> //fork
#include<signal.h>

int main(int argc, char * const * argv)
{
	((fork() && fork()) || (fork() && fork()));
	for(;;)
	{
		sleep(1);
		printf("休息1s, 进程id = %d!\n", getpid());
	}
	printf("再见了!\n");
	return 0;
}

运行结果如下:
在这里插入图片描述

图5.1 使用ps命令查看(fork() && fork()) || (fork() && fork());产生的7个进程

六、fork失败的可能原因总结

(1)系统中进程太多:

  • 肯定出了问题,如僵尸进程太多。整个系统中,使用ps命令列出进程时看到的进程ID(PID)是有限的,创建子进程ID值比父进程ID值大于1,进程ID是可以复用的,例如某个进程结束(终止)之后,过一段时间,操作系统又会把这个进程的ID分配给其他的新创建的进程使用(循环使用)。

  • 默认情况下,最大的进程ID值一般都是32767,如果0~32767这些数字全部都被占用,fork就会失败,当然,这是一种比较极端的情况。

(2)创建的进程数超过了当前用户允许创建的最大进程数。

  • 每个用户会有一个允许开启的进程总数。
printf("每个用户允许创建的最大进程数 = %ld\n", sysconf(_SC_CHILD_MAX));

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/677063.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring高手之路6——Bean生命周期的扩展点:BeanPostProcessor

文章目录 1. 探索Spring的后置处理器&#xff08;BeanPostProcessor&#xff09;1.1 BeanPostProcessor的设计理念1.2 BeanPostProcessor的文档说明 2. BeanPostProcessor的使用2.1 BeanPostProcessor的基础使用示例2.2 利用BeanPostProcessor修改Bean的初始化结果的返回值2.3 …

【Linux工具】yum指令、vim的使用和修改信任白名单

【Linux工具】yum指令、vim的使用和修改信任白名单 目录 【Linux工具】yum指令、vim的使用和修改信任白名单软件包rzsz查看软件包 安装软件卸载软件vim的使用vim的三种模式vim的基本操作vim命令模式命令集vim末行模式命令集vim操作总结 简单vim配置修改信任白名单 作者&#xf…

第四章 进程同步

目录 一、进程同步、进程互斥 1.1 进程同步 1.2 进程互斥 二、信号量机制 2.1 整型信号量 2.2 记录型信号量 三、用信号量实现进程互斥、同步、前驱关系 3.1 信号量机制实现进程互斥 3.2 信号量机制实现进程同步 3.3 信号量机制实现前驱关系 四、生产者-消费者问题…

【MySQL】· 一文了解四大子查询

前言 ✨欢迎来到小K的MySQL专栏&#xff0c;本节将为大家带来MySQL标量/单行子查询、列子/表子查询的讲解✨ 目录 前言一、子查询概念二、标量/单行子查询、列子/表子查询三、总结 一、子查询概念 子查询指一个查询语句嵌套在另一个查询语句内部的查询&#xff0c;这个特性从My…

MySQL - 第7节 - MySQL内置函数

1.日期函数 1.1.常用的日期函数 常用的日期函数如下&#xff1a; 1.2.current_date函数 current_date函数用于获取当前的日期。如下&#xff1a; 1.3.current_time函数 current_time函数用于获取当前的时间。如下&#xff1a; 1.4.current_timestamp函数 current_timestamp函数…

计组:各硬件工作原理

目录 ​编辑程序指令执行流程 程序执行指令前 执行各指令的顺序 程序&#xff08;每一条&#xff09;指令执行具体步骤 以第一步为例&#xff1a; 取指令&#xff08;#1~#4&#xff09; 初&#xff1a;&#xff08;PC)0&#xff0c;指向第一条指令的存储地址 #1&#x…

计算机组成原理学习笔记(学习中)

计算机系统概论 1.1计算机基本组成 冯诺依曼计算机特点&#xff1a; 计算机由五大部件组成&#xff1a;控制器&#xff0c;运算器&#xff0c;存储器&#xff0c;输入设备&#xff0c;输出设备 指令和数据以同等地位存于存储器&#xff0c;可按地址寻访 指令和数据用二进制…

一文读懂CAN总线及通信协议

CAN总线的汽车 CAN概念 CAN是控制器域网 (Controller Area Network, CAN) 的简称&#xff0c;是由研发和生产汽车电子产品著称的德国BOSCH公司开发了的&#xff0c;并最终成为国际标准&#xff08;ISO11898&#xff09;&#xff0c;是ISO国际标准化的串行通信协议。是国际上应…

Anchor-free应用一览:目标检测、实例分割、多目标跟踪

作者&#xff5c;杨阳知乎 来源&#xff5c;https://zhuanlan.zhihu.com/p/163266388 本文整理了与Anchor free相关的一些工作。一方面是分享近期在目标检测领域中一些工作&#xff0c;另一方面&#xff0c;和大家一起梳理一下非常火热的网络模型CenterNet、FCOS&#xff0c;当…

Linux网络-数据链路层,MAC帧解析

目录 数据链路层VS网络层 以太网概念 以太网的帧格式&#xff08;报文格式&#xff09;&#xff08;也可以称之为MAC帧&#xff09; MAC地址的概念 MAC帧格式 局域网通信原理 MTU MTU说明 MTU对IP协议的影响 MTU对UDP协议的影响 MTU对TCP协议的影响 数据链路层VS网…

【Linux】应用层协议:HTTP和HTTPS

每个人都可以很喜欢每个人&#xff0c;但喜欢治不了病&#xff0c;喜欢买不了东西&#xff0c;喜欢不能当饭吃&#xff0c;喜欢很廉价… 文章目录 一、HTTP协议1.URL1.1 URL的组成1.2 urlencode && urldecode 2.HTTP协议格式2.1 http请求和响应的格式2.2 通过代码来进行…

[Eigen中文文档] 求解稀疏线性系统

文档总目录 本文目录 稀疏求解器列表内置直接求解器内置迭代求解器外部求解器的包装器 稀疏求解器概念计算步骤基准测试例程 英文原文(Solving Sparse Linear Systems) 在Eigen中&#xff0c;有多种方法可用于求解稀疏系数矩阵的线性系统。由于此类矩阵的特殊表示&#xff0c…

零基础速成simulink代码生成——结合CANOE的DBC文件CAN报文代码生成 移植到硬件4

零基础速成simulink代码生成——结合CANOE的DBC文件CAN报文代码生成 移植到硬件4 本次我们将讲解如何将代码放到嵌入式硬件上运行,本次例子将dbc文件导入simulink中,生成代码,不需要我们自己实现数据库的内容。 导入DBC文件 新建一个simulink模型 MCU_CAN.slx simulink具…

开源计算机视觉库OpenCV详解

目录 1、概述 2、OpenCV详细介绍 2.1、OpenCV的起源 2.2、OpenCV开发语言 2.3、OpenCV的应用领域 3、OpenCV模块划分 4、OpenCV源码文件结构 4.1、根目录介绍 4.2、常用模块介绍 4.3、CUDA加速模块 5、OpenCV配置以及Visual Studio使用OpenCV 6、OpenCV和OpenGL的区…

SpringBoot常用操作

SpringBoot常用操作 SpringBoot启动过程 1. 生成一个SpringApplication的对象1. webApplicationType 推测web应用类型&#xff08;NONE、REACTIVE、SERVLET&#xff09;2. 从spring.factories中获取BootstrapRegistryInitializer对象3. initializers 从spring.factories中获…

A brief taste of JIFA

JIFA 是阿里贡献给 Eclipse 的一个适用于 Java 应用的问题诊断应用。 它以图形化的方式展示 Heap Dump AnalysisGC Log AnalysisThread Dump Analysis 因为云环境/生产环境的一些限制&#xff0c;应用的问题可能不能被就地分析&#xff0c;所以Jifa provides a web solution…

关于运动模糊问题的分析及处理方法

1、问题背景 前段时间有做一个化妆镜项目&#xff0c;就是一面镜子上装有一个摄像头&#xff0c;用户对着镜子化妆时&#xff0c;可同时用来采集人脸信息&#xff0c;分析人脸用的。客户反馈抓拍静止的人脸图像时&#xff0c;画面正常&#xff0c;而当人脸稍微运动时&#xff…

java springboot整合MyBatis实现分页查询以及带条件的分页查询

之前的文章 java springboot整合MyBatis做数据库查询操作操作了springboot整合MyBatis&#xff0c;然后简单做了个按id查询的操作 那么 我们按上文搭建起的环境继续 我们直接在staffDao接口中声明一个分页函数 Select("select * from staff limit #{page},#{pageSize}&q…

【Vue】Vite 组件化开发

文章目录 组件化开发一、组件化开发思想二、Vue 组件的构成2.1 组件组成结构2.2 组件 template 节点2.2.1 在 template 中使用指令2.2.2 在 template 中定义根节点 2.3 组件的 script 模板 三、组件的基本使用3.1 组件的注册3.1.1 全局组件注册3.1.2 局部组件注册 3.2 组件样式…

ffmpeg命令参数

主要参数 -i 设定输入流 -f 设定输出格式&#xff08;format&#xff09; -ss 开始时间 -t 时间长度视频参数 -vframes 设置要输出的视频帧数 -b 设定视频码率 -b:v 视频码率 -r 设定帧率 -s 设定画面的宽与高 -vn 不处理视频 -aspect aspect 设置横纵比4:3 或16:9 或1.333或…