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

news/2024/11/24 5:02:53/

系列文章目录

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); //休息1sprintf("休息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); //休息1sprintf("我是子进程,我的进程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.ppmy.cn/news/534731.html

相关文章

blfs:为lfs虚拟机增加桌面02

参考书籍&#xff1a; BLFS11.3 LFS11.3&#xff08;这里面有软件安装的详细说明&#xff09; 树莓派Linux操作系统移植&#xff08;这里面有桌面系统的脉络梳理&#xff09; 参考视频 https://www.youtube.com/watch?vcavxyXBgJ6Q&listPLyc5xVO2uDsBK_3VZOek8ICsxewOO4DU…

看完这篇 教你玩转渗透测试靶机vulnhub—Emplre: Lupinone

Vulnhub靶机Emplre: Lupinone渗透测试详解 Vulnhub靶机介绍&#xff1a;Vulnhub靶机下载&#xff1a;Vulnhub靶机安装&#xff1a;Vulnhub靶机漏洞详解&#xff1a;①&#xff1a;信息收集&#xff1a;②&#xff1a;SSH私钥爆破登入&#xff1a;③&#xff1a;pip提权&#xf…

领导力启程 - 从个人贡献者华丽转身成为领导者

1、例如:的领导好难、好难: 上面领导要求多、下面95 00员工好难带、 2、举例: 英国体育怎么样、 足球还行、其他都不行 2004 -2016 突飞猛进、 通过DDI的培训、 发现:只要领导力大幅提升、 整个团队就会完全不一样 最大的价值: 团队因有你而不同 …

一个在线五笔的例子的代码,很不错,转载过来共享

<HTML> <HEAD> <META http-equivContent-Type contenttext/html; charsetgb2312> <TITLE>在线五笔输入</TITLE> </HEAD> <BODY > 脚本说明: 把如下代码加入<body>区域中 <STYLE>TD { FONT-SIZE: 9pt } A:link …

嵌入式异质结体二极管的3.3 kV 4H-SiC MOSFET用于低开关损耗

题目&#xff1a;3.3 kV 4H-SiC MOSFET with embeded hetero junction body diode for low switching loss 阅读日期&#xff1a;2023.6.23 受到的启发&#xff1a; The Ron-sp of the SH-HJD MOSFET is 9.60 mΩ∙cm2, which is about 21.6% lower than the 12.25 mΩ∙cm2…

第四章 机器学习

文章目录 第四章 决策树4.1基本流程4.2划分选择4.2.1信息增益4.2.2增益率4.2.3基尼指数 4.3剪枝处理4.3.1预剪枝4.3.2后剪枝 4.4连续与缺失值4.4.1连续值处理4.4.2缺失值处理 4.5多变量决策树 第四章 决策树 4.1基本流程 决策过程&#xff1a; 基本算法&#xff1a; 4.2划…

python统计文件某一列相同数据出现的个数并插入柱状图

import xlsxwriter import csv import pandas as pd from collections import Counter import numpy import time # 新建一个表格&#xff0c;并添加表及柱状图 def generate_excel(dic): workbook xlsxwriter.Workbook(Books score statistics.xlsx) worksheet wo…

旗舰店 flagship store

近日&#xff0c;动画片《麦兜武当》的主创计划推出一系列基于该电影的产品&#xff0c;并打算把麦兜打造成有竞争力的国内卡通品牌。今后我们不仅能够在大荧屏上看到可爱的小猪麦兜&#xff0c;我们还能去听麦兜音乐会&#xff0c;看麦兜的书和麦兜音乐剧&#xff0c;还能买到…