Linux终端与进程的关系 ( 1 ) -【Linux通信架构系列】

系列文章目录

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

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

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

一、Linux 进程关系

1.1 Linux 进程相关概念「进程关系、进程组、进程会话」

进程之间是一种属性的结构关系,如图:

在这里插入图片描述

图1.1 进程关系图

最上面的init进程是“老祖宗”进程(操作系统启动时内核自己创建出来的,具有超级用户特权)。init进程的进程ID(PID)是1,系统内的其他进程应该都是由它或者它的子进程fork(创建)出来的。

终端输入ps -ef命令查看结果:
在这里插入图片描述

图1.2 PID为1的进程就是init进程

进程关系进一步分析:

进程有进程ID(PID),有父进程ID(PPID)。还要知道一点:每个进程还可以属于一个进程组(分成组方便管理,例如可以给整个组发送一条信息等)。 “进程组”代表一个或多个进程的集合 。每个进程组有一个唯一的进程组ID,一个进程组中的各个进程可以独立接收来自终端的各种信号。可以通过调用系统函数创建进程组、加入进程组等。

了解进程组概念之后,我们引入一个新的概念: “会话”(Sessiond)是一个或多个进程组的集合。

在这里插入图片描述

图1.3 一个session包含一个或多个进程组,一个进程组包含一个或多个进程

一般来讲,如果不调用特殊的系统函数进行特殊处理,1个bash(shell)上运行的所有程序将属于一个会话(session),而这个会话有1个会话首进程(session leader,就是创建该会话的进程),这个shell(bash)通常就是session leader。另外,还可以调用函数创建新的session。

二、终端与进程的关系

2.1 终端与bash进程

启动Ubuntu Linux虚拟机,用Xshell链接虚拟机,输入如下命令:
ps -ef | grep bash

进程ID为 19501就是bash进程(既Xshell 链接到Ubuntu Linux虚拟机产生的进程),如图2.1所示:
在这里插入图片描述

图2.1.1 查找包含bash字样的进程(1个)

再建一个Xshell链接虚拟机会话窗口,输入**ps -ef | grep bash**命令,如图2.12所示:
在这里插入图片描述

图2.1.2 查找包含bash字样的进程(2个)

可以看出,此时有2个bash进程(因为有两个Xshell连入了Ubuntu Linux虚拟机),进程ID分别为 19501和3253,我们可以看到图中pts/0和pts/1,pts就是虚拟终端

每个虚拟机终端链接到Ubuntu Linux虚拟机上,都会打开一个bash进程(也叫shell - 壳)。向其中输入各种命令的黑窗口就是bash,用于解释用户输入的命令。在Xshell中输入一条命令并按回车键后,命令就会被bash进程解释并执行(bash就是shell,也就是命令行解释器是一个可执行程序)。

可以使用whereis命令查找bash可执行程序的位置,如图2.13所示:
whereis bash
在这里插入图片描述

图2.1.3 使用whereis命令查找bash可执行程序的位置

2.2 终端上的开启进程

在VSCode自己编写nginx.c代码,内容如下:

#include <stdio.h>
#include <unistd.h>
int main(int argc, char *const *argv)
{
    
    
        printf("hello nginx\n");
        for(;;)
        {
    
    
                sleep(1);
                printf("休息1s\n");
        }
        printf("程序退出!再见!\n");
}

运行./nginx, 并在另一个终端窗口输入:
ps -la
-l代表比较长的输出显示格式;-a代表显示终端上的所有进程(包括其他终端的进程)

在这里插入图片描述

图2.2.1 执行ps -la的结果
这是一个无限循环的程序,会不断重复输出“休息1秒”,可以按`Ctrl + C`组合键停止。

2.3 为什么终端一退,进程就退出了

现在,把自己写的这个nginx可执行程序在某个终端再次启动,然后再打开两个Xshell终端,保持3个终端在运行的状态。

在终端输入如下命令:
ps -eo pid,ppid,sid,tty,pgrp,comm | grep -E 'bash|PID|nginx'

e代表所有进程;o代表自己指定显示哪些列(sid表示session id, tty表示终端, pgrp表示进程组, comm表示执行的命令);grep命令中,-E代表要开启扩展正则表达式功能,用于配合后面的bash|PID|nginx以显示特定名字的进程;| 代表或者关系;bash|PID|nginx代表bash、PID、nginx这几个字符串中的某个出现就会被显示。
在这里插入图片描述

图2.3.1 用ps命令显示指定名字的进程

观察图2.3.1:

(1)nginx进程的PPID是3253,而3253是某个bash的PID,说明nginx是某个bash的子进程。

(2)nginx进程的SID是3253,而3253是这个bash的PID,同时还是这个bash的SID,也就是说这个bash的SID等于PID,这就意味着这个bash是整个会话(session)的会话首进程(session leader)。

nginx进程的PID和PGRP都是14942,说明nginx自成一组。

(3)PID=3253的bash进程和nginx进程,它们的TT(tty)都是pts/0(编号为0的终端)。

(4)bash进程收到SIGHUP信号后,就会把这个信号发送给session 里面的所有进程,收到这个SIGHUP信号的进程默认动作就是退出。

分析到这里应该就明白了 - 为什么终端一退,进程就退出了。

2.4 strace 工具的使用

strace是Linux中的调试分析诊断工具,功能强大,可以用于跟踪程序执行时进程的系统调用(system call)和所接收到的信号。现在来看strace工具如何使用。

(1)执行如下命令:sudo strace -e trace=signal -p 23099

这条命令用于跟踪PID为23099的进程(nginx进程)上与信号(signal)有关的系统调用。也就是说,这条命令把trace工具附着到进程23099上了。如图所示:
在这里插入图片描述

图2.4.1 将strace 工具附着到进程23099(nginx)进程上

(2)现在打开第三个终端窗口,再使用strace附着一下nginx进程PPID为3253的父进程(bash)
sudo strace -e trace=signal -p 3253
在这里插入图片描述

图2.4.2 将strace 工具附着到3253进程(bash)上

(3)现在关闭运行nginx的进程窗口会发现另外2个运行strace的窗口都有内容输出。

首先看下附着nginx进程的终端窗口的输出内容:
在这里插入图片描述

图2.4.3 附着到nginx进程上的strace工具有了输出

从图2.4.3中可以看出,信号是SIGHUP,sid_pid表示谁发来的信号,3253就是bash进程(nginx进程的父进程)。

接着看下附着bash进程的终端窗口的输出内容:
在这里插入图片描述

图2.4.4 附着到bash进程上的strace工具有了输出

图2.4.4所示信息有些复杂,尝试找一找有用的信息(kill在这里表示发送信号的意思)。

(1)开头为"kill(-23099, SIGHUP)"的这行,nginx进程的进程组(PGRP)ID就是23099,所以这行表示发送信号给这个数字的绝对值所在的进程组(kill后面如果有负值,一般代表发送信号给一个进程组),也就是说,进程组ID为23099的所有进程都会收到SIGHUP的信号。

(2)kill(3253, SIGHUP)的这行,3253是bash进程自己的PID,bash在第一次收到SIGHUP信号时先把该信号发给session内的其他进程(不仅是上面的23099进程组的进程,如果有其他进程组,其他进程组也会收到SIGHUP信号(只要这些进程的session ID 相同),然后再次发送SIGHUP命令给自己,将自己杀死。下一行的“sid_pid=3253”也就说明了杀死了自己

2.5 终端关闭时如何让进程不退出

当终端关闭时,终端上运行的nginx进程就退出了。现在要解决的问题是,终端关闭时,如何让nginx进程不退出。设想一下,如果nginx进程拦截SIGHUP信号,是否可以?

2.5.1 nginx进程拦截SIGHUP信号

修改一下nginx.c。信号相关内容后面讲,这里写一小段代码,把该信号忽略掉。收到SIGHUP信号后,操作系统默认结束nginx进程,现在通过修改代码,告诉操作系统忽略掉该信号,操作系统就不会执行默认处理行为(不会结束该进程)。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int main(int argc, char *const *argv)
{
    
    
        printf("hello nginx\n");
        //系统函数,设置收到某个信号时的处理程序(用哪个函数来处理)
		signal(SIGHUP, SIG_IGN); //SIG_ING:代表我要求忽略该信号,请求操作系统不要执行默认的
							     //该信号处理动作(不要把本进程杀掉)。
        for(;;)
        {
    
    
                sleep(1);
                printf("休息1s\n");
        }
        printf("程序退出!再见!\n");
}

此时编译并执行,如图所示:
在这里插入图片描述

图2.5.1.1 strace工具输出前状态

关闭了nginc进程所在的bash父进程后,我们发现6408 bash进程没了,但是nginx进程依旧在,如图所示:

在这里插入图片描述

图2.5.1.2 关闭了nginc进程所在的bash父进程后strace工具输出后状态

nginx依旧存在,不过它的TT(终端)列显示变成了“?”,这表示没有对应的终端了,而且它的PPID(父进程ID)变成了1,正是前面介绍的老祖宗进程init的PID(进程ID),本来nginx进程的父进程是bash,但是bash被结束了,nginx进程变成孤儿进程,这种孤儿进程就被init进程收养了。

2.5.2 nginx进程和bash进程在不同的session中(不同的session ID)

这就涉及调用系统函数setid了。该函数用于建立一个新会话。修改nginx.c代码如下:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int main(int argc, char *const *argv)
{
    
    
        printf("hello nginx\n");
        pid_t pid;
		
		pid = fork();//系统函数,用来创建新进程,后续会继续讲解,子进程会从这句fork调用之后开始
					 //执行,原来的父进程也会接着往下执行
					 //为什么调用fork()函数,而不直接创建会话?是因为主进程是进程组的组长,不允许调用setsid来建立会话
		printf("pid = %d \n", pid);
		if(pid < 0){
    
    
			printf("fork进程出错!\n");
		}else if(pid == 0){
    
    
			//若执行的是子进程,则该条件就会满足
			printf("子进程开始执行!\n");
			//创建新的session
			setsid();
			for(;;)
        	{
    
    
                sleep(1);
                printf("休息1s\n");
        	}
			return 0;
		}else{
    
    
			//父进程才会走到这里
			for(;;)
        	{
    
    
                sleep(1);
                printf("休息1s\n");
       		}
       		return 0;
		}
        printf("程序退出!再见!\n");
        return 0;
}

这里是调用fork创建了一个子进程(子进程创建成功后回和父进程并行运行),然后在子进程中调用setsid。

此时,子一个终端窗口编译、链接并运行nginx,如图,可以看到父进程和子进程都在运行之中。
在这里插入图片描述

图2.5.2.1 重新编译链接nginx.c并执行,此时父进程和子进程并行执行

可以清楚地看到,PID为16658的nginx进程的父进程ID(PID)是16657,但是16658和16657两个进程的SID并不相同。还可以发现调用fork函数创建出的子进程没有关联的终端(TT/TTY列显示为“?”)。

现在,把正显示nginx执行结果终端断开连接,再看下信息:
在这里插入图片描述

图2.5.2.2 关闭了nginc进程所在的bash父进程后strace工具输出后状态

可以发现PID15806的bash没有了,PID为16657的父进程也没了,但PID为166658的子进程还在,子进程变成了孤儿进程,被老祖宗init收养了,所以子进程的PPID变成了1。

2.5.3 直接使用setsid命令

setsid不但是个函数(可以在代码中使用),也是个命令(可以在命令行中使用)。该命令用于启动一个进程,而且能够使启动的进程在一个新的session中,这样终端关闭后进程就不会退出。

现在,我们"kill 166658" , 把剩余的nginx杀死,并把代码复原:

#include <stdio.h>
#include <unistd.h>
int main(int argc, char *const *argv)
{
    
    
        printf("hello nginx\n");
        for(;;)
        {
    
    
                sleep(1);
                printf("休息1s\n");
        }
        printf("程序退出!再见!\n");
        
}

再次编译gcc -o nginx nginx.c
在一个终端窗口执行命令:setsid ./nginx
在另一个终端窗口执行命令:ps -ef | grep -E "nginx|PID|bash"

在这里插入图片描述

图2.5.3.1 运行setsid ./nginx 命令后的执行情况

然后我们关闭nginx终端,依旧存在nginx进程:
在这里插入图片描述

图2.5.3.2 关闭nginx终端,显示终端进程的执行情况

可以看出,关闭后依然存在。

2.5.4 直接使用 nohup命令(no hang up 不挂断)

是否觉得这个命令似曾相识?前面介绍了一个终端断开信号(SIGHUP),看起来与这个命令很相似,都是hup。是的,使用nohup命令启动的程序会忽略(SIGHUP信号),与用程序编写忽略SIGHUP信号同理。

nohup命令有点与众不同,执行程序,我们看看:
nohup ./nginx

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

图2.5.4.1 使用nohup命令启动一个可执行程序

可以发现,本来屏幕上输出的内容,现在不输出了,光标停在屏幕上。这是nohup命令的特点,该命令会把输出重定向到当前目录的nohup.out文件中,程序需要执行10~20s才能发现nohup的尺寸有变化(变得大于0了)。所以,这种nohup文件的输出程序执行结果并不是实时输出,而是有一定的延迟。

看下当前目录结构:
在这里插入图片描述

图2.5.4.2 查看nohup命令的输出文件nohup.out的大小

查看当前的进程信息:
在这里插入图片描述

图2.5.4.3 查看当前的nginx进程信息

关闭nginx的终端后,查看当前的进程信息:
在这里插入图片描述

图2.5.4.4 关闭nginx的终端后,查看当前的进程信息

nginx依然存在,PPID变成了1,子进程变成了孤儿进程,被老祖宗init收养了。

2.6 后台执行(运行)的简单理解

后台执行(运行)的命令:
./nginx &

在这里插入图片描述

图2.6.1 使用"&"让程序在后台执行

此时,程序在后台执行了(直接输入./nginx执行方式可理解为在前台运行)。

那么,程序在后台运行和在前台运行有什么区别呢?

(1)如果程序在前台执行,在终端输入ls命令,是无法列出目录的;程序在前台执行终端只只能等待该程序指向完成才能继续执行其他程序。
(2)如果程序在前台执行,则按Ctl + C 组合键能够停止该进程的执行;如果程序在后台执行,按Ctl + C 组合键则无法停止该进程的执行。
(3)可以把前台执行的进程切换到前台,使用fg命令即可进行切换:
fg
nginx进程被切换到了前台,这时按Ctl + C组合键就可以停止该进程了。
(4)如果把终端连接断开,就会发现,就算nignx是后台进程,它也被关闭(退出)了。

猜你喜欢

转载自blog.csdn.net/weixin_30197685/article/details/131266761