你在终端启动的进程,最后都是什么下场?(上)

news2024/11/26 22:51:12

你在终端启动的进程,最后都是什么下场?(上)

前言

在本篇文章当中,主要给大家介绍我们在终端启动的进程都是怎么结束的,在我们登录终端和退出终端都发生了什么?

基本介绍

首先我们需要了解的概念就是当我们使用 ssh 登录服务器的时候就会产生一个会话。当我们在终端启动一个程序之后会创建一个新的进程组,进程组的首进程为要执行的程序,这个进程组可以是一个进程也可以是多个进程,整个进程组在 shell 看来也是一个作业(Job)。如果你不使用 & 符号去执行一个程序,那么 shell 就会执行这个程序,然后这个程序就成为一个前台进程组(可以是一个进程也可以是多个进程),当我们在终端当中执行的命令加上 & 符号,那么这个作业(Job)就会成为一个后台进程组。

会话、前台进程组和后台进程组他们之间的关系大致如下图所示:

从上面的图当中我们可以看出,一个会话当中有很多个进程组,分为前台进程组和后台进程组,但是后台进程组可以有多个,前台呢进程组最多只能有一个,而且每一个进程组当中可以有多个进程(当你在执行的程序当中 fork 出子进程的时候,就是一个含有多个进程的进程组)。

现在我们使用一个例子具体去了解一下:

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

int main()
{
  // 打印进程的 id 号
  printf("process id = %d\n", getpid());
  // 打印进程的进程组号 0 表示返回当前进程的进程组号
  printf("process group id = %d\n", getpgid(0));
  // 打印进程的父进程号
  printf("parent process id = %d\n", getppid());
  // 打印父进程的进程组号
  printf("parent process group id = %d\n", getpgid(getppid()));
  return 0;
}

上面的程序的输出结果如下所示:

➜  daemon git:(master) ✗ ./job1.out 
process id = 3773445
process group id = 3773445
parent process id = 3766993
parent process group id = 3766993

从上面程序的输出结果我们可以看到程序的进程组号和父进程的进程组号是不一样的,我们需要了解到的是,job1 的父进程就是 shell ,但是他与 shell 的进程组是不一样的,shell 在执行新的程序的时候会创建一个子进程,然后修改子进程的进程组号,而且新的进程组的组号为子进程的进程号。

或者我们直接在终端输入命令也可以发现 shell 的子进程的进程组号和 shell 的进程组号是不一样的:

➜  daemon git:(master)ps -o pid,ppid,pgid,tty,cmd
    PID    PPID    PGID TT       CMD
3766993 3757891 3766993 pts/1    /usr/bin/zsh -i
3772829 3766993 3772829 pts/1    ps -o pid,ppid,pgid,tty,cmd

在上面的输出当中,PID,PPID,PGID 分别表示进程的进程号,父进程号和进程组号,CMD 表示执行程序时候的命令。首先我们知道的是 ps 命令进程是 shell 进程的子进程(上面的进程号等于 3766993 的进程就是 shell 进程),从上面的输出结果也可以得知这一点( ps 的 PPID 就是 shell 的 PID)。

通过上面两个例子我们可以知道,确实当我们执行程序的时候 shell 会创建一个新的进程组,事实上只要是在终端里面执行的程序,都会创建一个新的进程组。如果你熟悉 linux 的话,那么肯定用过 & 符号,这个符号就是将任务放在后台执行,这样创建的进程组就是后台进程组。

前台进程的死亡情况列表

前台进程组的死亡一般来说有四种情况:

  • 程序正常结束,比如你在终端输入 ls 命令,执行完成✅之后他就正常结束了。
  • 当你在终端输入 ctrl + c 之类的字符的时候内核会给前台进程组发送 SIGINT 之类的信号。
  • 当控制进程 也就是 shell 进程终止(比如说被杀掉了)的时候,内核会发送 SIGHUP 信号给前台进程组中的所有进程。
  • 当退出终端的时候,shell 会发送 SIGHUP 信号给前台进程组。

初探信号

大家如果经常使用 linux 的话,一定会有过这种情况:当你在终端执行一个程序的时候,你突然遇到某些问题不想执行他了,然后你会疯狂按 ctrl + c ,让这个程序退出。那当你在终端按下 ctrl + c 的时候程序一定会停止嘛?如果程序退出了,那是什么原因导致他退出的呢?事实上,当你在终端按下 ctrl + c 的时候,内核会想前台进程组所有的进程发送一个 SIGINT 信号,注意这里是前台进程组中的所有进程,但是通常我们在终端里执行的就是一个单进程任务,但是如果我们执行的程序是多进程的话,那么这个进程组里面的所有进程都会收到一个来自操作系统内核的 SIGINT 信号。

为了后面我们进行验证的时候大家能够了解清楚程序的行为,我们首先介绍一下信号处理函数的使用,所谓信号处理函数就是,当进程收到一个由其他进程或者操作系统内核发送的信号的时候,我们可以定义一个函数去处理信号,也就是定义收到信号的行为:

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

void sig(int signo) // signo 这个参数就是对应信号的数字表示 SIGINT 信号对应的数字为 2
{
  char* s = "received a signal\n";
  write(STDOUT_FILENO, s, strlen(s));
}

int main()
{
  // 注册收到 SIGINT 信号的时候,我们应该使用什么处理函数
  // 当进程收到 SIGINT 信号的时候,会调用函数 sig 
  signal(SIGINT, sig);
  while(1);
  return 0;
}

上面的程序的输出结果如下所示:

➜  daemon git:(master) ✗ ./job4.out 
^Creceived a signal
^Creceived a signal
^Creceived a signal
^Creceived a signal
^Creceived a signal
^Creceived a signal
^Creceived a signal

从上面的终端的输出结果我们可以知道,当我们在终端输入 SIGINT 的时候,进程会收到一个 SIGINT 信号,然后会调用信号处理函数 sig ,并且执行函数体。

现在我们执行一个多进程的任务试试:

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

void sig(int signo) // signo 这个参数就是对应信号的数字表示 SIGINT 信号对应的数字为 2
{
  char s[1024];
  sprintf(s, "received a signal %d\n", getpid()); // 输出内容并且答应进程的进程号
  write(STDOUT_FILENO, s, strlen(s));
}

int main()
{
  // 注册收到 SIGINT 信号的时候,我们应该使用什么处理函数
  // 当进程收到 SIGINT 信号的时候,会调用函数 sig 
  signal(SIGINT, sig);
  fork();
  fork();
  while(1);
  return 0;
}

在上面的程序当中,我们 fork 的两次,一共有四个进程,上面的程序输出的结果如下所示:

➜  daemon git:(master) ✗ ./job5.out 
^Creceived a signal 3702
received a signal 3703
received a signal 3705
received a signal 3704
^Creceived a signal 3703
received a signal 3702
received a signal 3705
received a signal 3704
^Creceived a signal 3703
received a signal 3704
received a signal 3705
received a signal 3702

从上面的输出结果我们可以看到,前台进程组的所有进程都收到了 SIGINT 信号。

事实上,我们有如下规则:

在任一时刻,会话(当我们登录服务器的时候就会产生一个会话,我们使用一些远程登录软件的时候通常会看到 session 的字样们就是表示会话)中的其中一个进程组会成为终端的前台进程组,其他进程组会成为后台进程组。只有前台进程组中的进程才能从控制终端中读取输入。当用户在控制终端中输入其中一个信号生成终端字符之后,该信号会被发送到前台进程组中的所有成员。这些字符包括生成 SIGINT 的中断字符(通常是 Control-C)、生成 SIGQUIT 的退出字符(通常是 Control-\)、生成 SIGSTP 的挂起字符(通常是 Control-Z)。

是谁给前台进程组发送的 SIGINT 信号

在上面的内容当中我们提到了:

  • 当我们按下中断字符(ctrl + c)的时候,所有的前台进程都会收到一个 SIGINT 信号。
  • 当我们按下退出字符(ctrl + \)的时候,所有的前台进程都会收到一个 SIGQUIT 信号。
  • 当我们按下挂起字符(ctrl + Z)的时候,所有的前台进程都会收到一个 SIGTSTP 信号。

现在的问题来了,是谁发送的这些信号呢?事实上是 0 号进程发送的这些信号,我们可以使用程序去验证这一点。

#define _GNU_SOURCE
#include <unistd.h>
#include <error.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>

void my_handler (int signo, siginfo_t *si, void*ucontext)
{
  char s[1024];
  // si->si_pid 是发送信号的进程的进程号
  sprintf(s, "发送信号的进程号 = %d 信号 = %d\n", si->si_pid, signo);
  write(STDOUT_FILENO, s, strlen(s));
  _exit(0);
}

int main()
{
  printf("pid = %d\n", getpid());
  struct sigaction demo;
  demo.sa_sigaction = my_handler; // 保存信号处理函数
  demo.sa_flags |= SA_SIGINFO; // 这个表示使用三个参数的信号处理函数 之前使用 signal 的信号处理函数值有一个参数
  sigaction(SIGINT, &demo, NULL);
  sigaction(SIGQUIT, &demo, NULL);
  sigaction(SIGTSTP, &demo, NULL);
  while(1);
  return 0;
}

上面的程序的输出结果如下所示:

➜  daemon git:(master) ✗ ./job2.out 
pid = 12842
^C发送信号的进程号 = 0 信号 = 2

在上面的程序当中我们使用 sigaction 去定义我们自己的信号处理函数,在之前我们是使用 signal 这个函数去定义信号处理函数,其实 signal 也是通过 sigaction 实现的,sigaction 可以让我们定义一些更加细节的处理。

从上面的函数定义来看,sigaction 和 signal 不一样的地方在于信号处理函数有三个参数,然后我们定义了三个信号 SIGINT、SIGQUIT和SIGTSTP,他们的信号处理函数都是 my_handler 。

从上面程序的输出结果我们可以知道是进程号等于 0 的进程发送的,哎我们知道的 init 的进程号是等于 1 ,那么进程号等于 0 的进程是啥呢?

事实上这个 0 号进程就是位于内核的终端驱动程序,init 的进程号是 1 ,内核的进程号等于 0 ,是不是可以理解呢?😂

Shell 被杀掉导致前台进程死亡

当 shell 进程被杀掉退出的时候,内核会发送 SIGHUP 给所有的前台进程,接下来我们复现一下这个结果。

在前面的文章当中我们提到了,当一个终端终止执行,比如说被 kill -9 杀死,那么内核就会发送 SIGHUP 信号给前台进程组当中的所有的进程,现在我们使用下面的程序来复现这个现象:

#define _GNU_SOURCE
#include <unistd.h>
#include <error.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <assert.h>

void my_handler (int signo, siginfo_t *si, void*ucontext)
{
  char s[1024];
  int fd = open("text.txt", O_APPEND | O_RDWR | O_CREAT, 0644);
  if(fd == -1)
  {
    perror("");
    abort();
  }
  sprintf(s, "发送信号的进程号 = %d 信号 = %d\n", si->si_pid, signo);
  write(fd, s, strlen(s));
  close(fd);
  fsync(fd);
  _exit(0);
}

int main()
{
  printf("pid = %d\n", getpid());
  struct sigaction demo;
  demo.sa_sigaction = my_handler;
  demo.sa_flags |= SA_SIGINFO;
  demo.sa_flags &= ~SA_RESETHAND;
  sigaction(SIGINT, &demo, NULL);
  sigaction(SIGHUP, &demo, NULL);
  while(1);
  return 0;
}

在上面的程序当中我们给 SIGHUP 定义了一个信号处理器 my_handler ,当进程收到 SIGHUP 信号的时候就会调用这个函数,然后往 text.txt 文件当中写入数据,我们再次查看文件就能够知道是哪个进程给前台进程组发送的信号了。

在下面的图片当中,首先我们启动一个 shell 进程,进程号等于 2842,然后启动程序 job6.out(就是上面的代码),然后在右侧的终端执行 kill -9 命令,杀死左侧的终端程序,最终 job6.out 会收到一个来自内核的 SIGHUP 信号,因此会在 text.txt 文件当中写入信息。

我们现在再次查看 text.txt 文件当中的信息:

从上面的 text.txt 文件的输出结果我们就可以知道了,确实是内核发送的 SIGHUP 信号,SIGHUP 信号对应的信号数值就是 1,这个输出结果符合我们的预期。

退出终端导致前台进程组死亡

我们已经在前文当中提到了,当我们退出终端的时候 shell 会给所有的前台进程组发送 SIGHUP 信号,现在我们来复现一下这个现象。

在下面的图片当中我们使用 ps 命令得到当前 shell 的进程号,从图片的结果来看当前的 shell 的进程号等于 3892,然后我们在终端执行程序 job6.out ,和上个例子的是同一个程序,然后我们退出终端,根据前面我们所谈到的在退出终端之后 shell 会给所有的前台进程组发送 SIGHUP 信号,那么 job6.out 就会调用信号处理函数,然后将信息写入 text.txt 。

我们现在来看一下 text.txt 当中的内容:

从上面的输出结果来看 job6.out 确实收到了一个 SIGHUP 信号,对应的信号值等于 1,而且发送信号的进程号等于 3892,确实是上图当中显示的 shell 的进程号。

总结

在本篇文章当中主要给大家介绍了前台进程组当中的进程退出的几种情况,并且使用程序复现了他们,主要有以下四种情况:

  • 程序正常结束,比如你在终端输入 ls 命令,执行完成之后他就正常结束了。
  • 当你在终端输入 ctrl + c 之类的字符的时候内核会给前台进程组发送 SIGINT 之类的信号。
  • 当控制进程 也就是 shell 进程终止(比如说被杀掉了)的时候,内核会发送 SIGHUP 信号给前台进程组中的所有进程。
  • 当退出终端的时候,shell 会发送 SIGHUP 信号给前台进程组。

总的来说深入去了解其中的过程对我们来说还是很有裨益的,希望大家有所收获!


以上就是本篇文章的所有内容了,我是LeHung,我们下期再见!!!更多精彩内容合集可访问项目:https://github.com/Chang-LeHung/CSCore

关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。

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

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

相关文章

从Android系统启动→app启动→activity启动和渲染的整个流程

引言 本文讲解从开机到app显示画面的流程&#xff0c;但不分析源码&#xff0c;如果想阅读源码请到参考文章中查阅。 本文把这段流程分为三部分&#xff1a; 从开机到显示应用列表从点击应用图标到Activity创建成功从Activity创建成功到显示画面 从开机到显示应用列表 先看…

Java-CC

漏洞原理 TransformedMap这个类的decorate函数可以将一个普通的Map转换为一个TransformedMap&#xff0c;其第2、3参数分别对应当key改变和value改变时需要做的操作。所以此时如果修改其中的任意key或value&#xff0c;就会触发我们预先定义好的某些操作来对Map进行处理&#…

pytorch初学笔记(十四):损失函数

目录 一、损失函数 1.1 L1损失函数 1.1.1 简介 1.1.2 参数设定 1.1.3 代码实现 1.2 MSE损失函数&#xff08;平方和&#xff09; 1.2.1 简介 1.2.2 参数介绍 1.2.3 代码实现 1.3 损失函数的作用 二、在神经网络中使用loss function 2.1 使用交叉熵损失函数 2.2 …

【软件测试】资深测试聊一聊,测试架构师是怎么样的,做一名成功的测试工程师......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 测试架构师 测试架…

利尔达5G模组NE16U-CN通过华为OpenLab基于R16标准的认证测试

近日&#xff0c;利尔达5G R16模组NE16U-CN 率先顺利通过了华为OpenLab的认证测试&#xff0c;成为首批基于展锐V516芯片平台通过华为认证测试的5G模组&#xff0c;实现了基于3GPP R16协议版本的业务验证。 这表明&#xff0c;利尔达NE16U-CN模组已支持3GPP R16所具有的5G LAN、…

Overview of Computer Graphics

ContentsWhat is Computer Graphics?Why study Computer Graphics?ApplicationsFundamental Intellectual ChallengesTechnical ChallengesCourse TopicsRasterization (光栅化)Curves and Meshes (曲线和曲面)Ray Tracing (光线追踪)Animation / Simulation (动画 / 模拟)Re…

ANACONDA的进阶理解和思考

0. 继续深入了解anaconda 0.1 Anaconda 是 Python 的一个开源发行版本 里面集成了很多关于 python 科学计算的第三方库&#xff0c;主要面向科学计算且安装方便&#xff0c;而 python 是一个编译器 如果不使用 anaconda&#xff0c;那么安装库的时候&#xff0c;库的依赖安装起…

力扣LeetCode算法题 第6题-Z 字形变换

要求&#xff1a; 一开始看到题目&#xff0c;第一想到的思路&#xff0c;就被题目要求的思路给带偏了。 内容是Z字型输出内容 就一直想着把字符串输出成上面这种格式 总是想着把字符串放入到二维数组中进行展示。 这样一来思路就受到了限制。 一直使用先写入数组中。 //将…

直播邀请函 | 第12届亚洲知识产权营商论坛:共建创新价值 开拓崭新领域

由香港特别行政区政府、香港贸易发展局及香港设计中心共同举办的亚洲知识产权营商论坛&#xff0c;每年为世界各地知识产权业界专家、商界领袖提供一个理想平台&#xff0c;共同探讨亚洲知识产权市场的最新发展&#xff0c;发掘更多商机。 去年&#xff0c;论坛共邀请70余位国…

使用HBuilder X开发Vue3+node+element-plus(一)

开发Vue3有很多的工具&#xff0c;比如VSCode&#xff0c;它也非常的好用&#xff0c;本文主要使用HBuilder X开发。 环境3个&#xff1a; Windows10 Node安装 1.打开官网&#xff0c;选择一个版本&#xff0c;进行安装 Node.js 2.选择路径&#xff0c;下一步就行了 3. 输…

【深度学习】torch.argmax()函数讲解 | pytorch

文章目录前言一、两个维度的张量使用torch.argmax()函数二、三个维度的张量使用torch.argmax()函数前言 这篇博客也是属于看了好久一直没写&#xff0c;终于写了。 一、两个维度的张量使用torch.argmax()函数 我们直接先举一个例子吧&#xff0c;我们随机生成一个2X3的张量&…

[附源码]SSM计算机毕业设计亿上汽车在线销售管理系统JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

11 Daemonset:忠实可靠的看门狗

文章目录1. 前言2. 为什么要有 DaemonSet(看门狗)&#xff1f;3. 如何使用 YAML 描述 DaemonSe?3.1 参考官网创建DaemonSet YAML3.1.1 DaemonSet YAML 和 Deployment YAML 文件对比3.1.2 DaemonSet YAML 和 Deployment YAML 文件对比图示3.2 用变通的方法来创建 DaemonSet 的 …

【Python模块】图形化编程模块-turtle

Turtle&#xff0c;也称海龟渲染器&#xff0c;是 Python 内置的图形化模块&#xff0c;它使用 tkinter 实现基本图形界面&#xff0c;因此 当前使用的 Python 环境需要支持 tkinter。 Turtle 提供了面向对象和面向过程两种形式的海龟绘图基本组件。使用它可以轻松的实现图形的…

初探Golang语法巩固复习

最近在家&#xff0c;重新拾起Go语言&#xff0c;搭建环境可参考之前博客【初探Golang语言之环境搭建】&#xff0c;本文是基本语法熟悉与练习&#xff0c;方便备查。 判断语句 if && switch if 通过指定一个或多个条件&#xff0c;并通过测试条件是否为true来决定是…

[附源码]计算机毕业设计springboot驾校预约管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

xcode swift 单元测试 test

XCTest是苹果官方的测试框架&#xff0c;是基于OCUnit的传统测试框架&#xff0c;测试编写起来非常简单。 测试案例一 创建一个单元测试 func testExample() throws {let personID:String "0123456789"let count personID.countXCTAssert(count < 10, "I…

aPaaS是什么(aPaaS平台和IPaaS的区别是啥?大白话解释)

依题&#xff1a;aPaaS是什么&#xff1f;aPaaS与iPaaS二者之间的区别在哪&#xff1f;要想了解区别&#xff0c;首先得搞清概念&#xff0c;不然就是在耍流氓&#xff01;下面本人就从概念到区别用大白话给你一次性讲清楚。 一、什么是aPaaS&#xff1f; 应用程序平台即服务&…

freeswitch配置SBC的方案

概述 freeswitch 是一款好用的开源软交换平台。 但是&#xff0c;fs不是专为SBC而开发的&#xff0c;所以需要做一些定制化的配置和开发。 本文主要介绍如何利用fs的基本功能配置一个简单的SBC方案&#xff0c;满足一般化需求&#xff0c;如果有定制化的需求需要定制开发。 …

QT简单串口通信终端实现

1.工程文件 工程文件中添加serilport QT serialport 2.主程序 主程序文件main.cpp #include "mainwindow.h" #include <QApplication> int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); return a.exec(); } …