C++技能系列
Linux通信架构系列
C++高性能优化编程系列
深入理解软件架构设计系列
高级C++并发线程编程
期待你的关注哦!!!
现在的一切都是为将来的梦想编织翅膀,让梦想在现实中展翅高飞。
Now everything is for the future of dream weaving wings, let the dream fly in reality.
进程之间是一种属性的结构关系,如图:
最上面的init进程是“老祖宗”进程(操作系统启动时内核自己创建出来的,具有超级用户特权)。init进程的进程ID(PID)是1,系统内的其他进程应该都是由它或者它的子进程fork(创建)出来的。
进程关系进一步分析:
进程有进程ID(PID),有父进程ID(PPID)。还要知道一点:每个进程还可以属于一个进程组(分成组方便管理,例如可以给整个组发送一条信息等)。 “进程组”代表一个或多个进程的集合
。每个进程组有一个唯一的进程组ID,一个进程组中的各个进程可以独立接收来自终端的各种信号。可以通过调用系统函数创建进程组、加入进程组等。
了解进程组概念之后,我们引入一个新的概念: “会话”(Sessiond)是一个或多个进程组的集合。
一般来讲,如果不调用特殊的系统函数进行特殊处理,1个bash(shell)上运行的所有程序将属于一个会话(session),而这个会话有1个会话首进程(session leader,就是创建该会话的进程),这个shell(bash)通常就是session leader。另外,还可以调用函数创建新的session。
启动Ubuntu Linux虚拟机,用Xshell链接虚拟机,输入如下命令:
ps -ef | grep bash
进程ID为 19501就是bash进程(既Xshell 链接到Ubuntu Linux虚拟机产生的进程),如图2.1所示:
再建一个Xshell链接虚拟机会话窗口,输入**ps -ef | grep bash
**命令,如图2.12所示:
可以看出,此时有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
在VSCode自己编写nginx.c代码,内容如下:
#include
#include
int main(int argc, char *const *argv)
{
printf("hello nginx\n");
for(;;)
{
sleep(1);
printf("休息1s\n");
}
printf("程序退出!再见!\n");
}
运行./nginx
, 并在另一个终端窗口输入:
ps -la
-l代表比较长的输出显示格式;-a代表显示终端上的所有进程(包括其他终端的进程)
现在,把自己写的这个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:
(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信号的进程默认动作就是退出。
分析到这里应该就明白了 - 为什么终端一退,进程就退出了。
strace是Linux中的调试分析诊断工具,功能强大,可以用于跟踪程序执行时进程的系统调用(system call)和所接收到的信号。现在来看strace工具如何使用。
(1)执行如下命令:sudo strace -e trace=signal -p 23099
这条命令用于跟踪PID为23099的进程(nginx进程)上与信号(signal)有关的系统调用。也就是说,这条命令把trace工具附着到进程23099上了。如图所示:
(2)现在打开第三个终端窗口,再使用strace附着一下nginx进程PPID为3253的父进程(bash)
sudo strace -e trace=signal -p 3253
(3)现在关闭运行nginx的进程窗口会发现另外2个运行strace的窗口都有内容输出。
从图2.4.3中可以看出,信号是SIGHUP,sid_pid表示谁发来的信号,3253就是bash进程(nginx进程的父进程)。
图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”也就说明了杀死了自己
当终端关闭时,终端上运行的nginx进程就退出了。现在要解决的问题是,终端关闭时,如何让nginx进程不退出。设想一下,如果nginx进程拦截SIGHUP信号,是否可以?
修改一下nginx.c。信号相关内容后面讲,这里写一小段代码,把该信号忽略掉。收到SIGHUP信号后,操作系统默认结束nginx进程,现在通过修改代码,告诉操作系统忽略掉该信号,操作系统就不会执行默认处理行为(不会结束该进程)。
#include
#include
#include
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");
}
关闭了nginc进程所在的bash父进程后,我们发现6408 bash进程没了,但是nginx进程依旧在,如图所示:
nginx依旧存在,不过它的TT(终端)列显示变成了“?”,这表示没有对应的终端了,而且它的PPID(父进程ID)变成了1,正是前面介绍的老祖宗进程init的PID(进程ID),本来nginx进程的父进程是bash,但是bash被结束了,nginx进程变成孤儿进程,这种孤儿进程就被init进程收养了。
这就涉及调用系统函数setid了。该函数用于建立一个新会话。修改nginx.c代码如下:
#include
#include
#include
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,如图,可以看到父进程和子进程都在运行之中。
可以清楚地看到,PID为16658的nginx进程的父进程ID(PID)是16657,但是16658和16657两个进程的SID并不相同。还可以发现调用fork函数创建出的子进程没有关联的终端(TT/TTY列显示为“?”)。
可以发现PID15806的bash没有了,PID为16657的父进程也没了,但PID为166658的子进程还在,子进程变成了孤儿进程,被老祖宗init收养了,所以子进程的PPID变成了1。
setsid不但是个函数(可以在代码中使用),也是个命令(可以在命令行中使用)。该命令用于启动一个进程,而且能够使启动的进程在一个新的session中,这样终端关闭后进程就不会退出。
现在,我们"kill 166658" , 把剩余的nginx杀死,并把代码复原:
#include
#include
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"
可以看出,关闭后依然存在。
是否觉得这个命令似曾相识?前面介绍了一个终端断开信号(SIGHUP),看起来与这个命令很相似,都是hup。是的,使用nohup命令启动的程序会忽略(SIGHUP信号),与用程序编写忽略SIGHUP信号同理。
nohup命令有点与众不同,执行程序,我们看看:
nohup ./nginx
可以发现,本来屏幕上输出的内容,现在不输出了,光标停在屏幕上。这是nohup命令的特点,该命令会把输出重定向到当前目录的nohup.out文件中,程序需要执行10~20s才能发现nohup的尺寸有变化(变得大于0了)。所以,这种nohup文件的输出程序执行结果并不是实时输出,而是有一定的延迟。
nginx依然存在,PPID变成了1,子进程变成了孤儿进程,被老祖宗init收养了。
后台执行(运行)的命令:
./nginx &
此时,程序在后台执行了(直接输入./nginx执行方式可理解为在前台运行)。
那么,程序在后台运行和在前台运行有什么区别呢?
(1)如果程序在前台执行,在终端输入ls命令,是无法列出目录的;程序在前台执行终端只只能等待该程序指向完成才能继续执行其他程序。
(2)如果程序在前台执行,则按Ctl + C 组合键能够停止该进程的执行;如果程序在后台执行,按Ctl + C 组合键则无法停止该进程的执行。
(3)可以把前台执行的进程切换到前台,使用fg命令即可进行切换:
fg
nginx进程被切换到了前台,这时按Ctl + C组合键就可以停止该进程了。
(4)如果把终端连接断开,就会发现,就算nignx是后台进程,它也被关闭(退出)了。