文章目录
Nginx环境配置
ubuntu@VM-20-6-ubuntu:~$ uname -a
Linux VM-20-6-ubuntu 4.15.0-213-generic #224-Ubuntu SMP Mon Jun 19 13:30:12 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
ubuntu@VM-20-6-ubuntu:~$ sudo apt-get install libpcre3-dev
ubuntu@VM-20-6-ubuntu:~$ sudo apt-get install zlib-dev
ubuntu@VM-20-6-ubuntu:~$ sudo apt-get install libssl-dev
ubuntu@VM-20-6-ubuntu:~$ mkdir nginxsourcecode
ubuntu@VM-20-6-ubuntu:~$ cd nginxsourcecode/
ubuntu@VM-20-6-ubuntu:~/nginxsourcecode$ wget https://nginx.org/download/nginx-1.26.0.tar.gz
ubuntu@VM-20-6-ubuntu:~/nginxsourcecode$ tar -zxvf nginx-1.26.0.tar.gz
源码目录
misc :辅助代码,测试C++头 的兼容性,以及对Google_PerfTools 的支持;
配置vim高亮显示
ubuntu@VM-20-6-ubuntu:~/nginxsourcecode/nginx-1.26.0$ cp -r contrib/vim ~/.vim
ubuntu@VM-20-6-ubuntu:~/nginxsourcecode/nginx-1.26.0/conf$ vim nginx.conf
html目录
50x.html
index.html
man目录
ubuntu@VM-20-6-ubuntu:~/nginxsourcecode/nginx-1.26.0/man$ man ./nginx.8
Nginx的编译和安装
ubuntu@VM-20-6-ubuntu:~/nginxsourcecode/nginx-1.26.0$ ./configure
ubuntu@VM-20-6-ubuntu:~/nginxsourcecode/nginx-1.26.0/objs$ cat ngx_modules.c
ubuntu@VM-20-6-ubuntu:~/nginxsourcecode/nginx-1.26.0$ sudo make install
Nginx的启动和简单使用
ubuntu@VM-20-6-ubuntu:/usr/local/nginx$ ps -ef | grep nginx
ps是进程查看命令,用于查看进程是否正在运行。其中参数e用于显示所有进程;参数f意为全格式显示,就是把所有能显示出来的列都显示出来让结果更全面清晰,所以一般与f连起来使用;grep也是一个命令——查找命令,用来在文本中搜索指定内容;“|”是个管道符,ps后面接管道符再接grep就表示ps和grep同时使用,该管道符的含义是把ps输出的内容作为grep的操作对象。
在本地计算机发送数据包请求
http://203.195.208.44
80端口不可以被监听两次,因为运行两次Nginx服务器等于监听了2次80端口
Nginx整体结构、进程模型
CPU、内核、处理器
worker进程合适数量
Nginx中worker进程的数量与Processor的数量一样,效率一般就会达到最高
查看云服务器Processor数量
ubuntu@VM-20-6-ubuntu:/usr/local/nginx/sbin$ grep -c processor /proc/cpuinfo
修改worker进程数量
Nginx进程模型细说
ubuntu@VM-20-6-ubuntu:/usr/local/nginx/sbin$ ./nginx -?
Nginx重载配置文件
Nginx热升级
Nginx的关闭
如果把master进程杀掉(用kill命令),那么Nginx的所有worker进程也就都没有了,相当于Nginx停止运行了
当然,上面这种关闭Nginx的方法太粗暴了,一般都是使用命令行来关闭。
ubuntu@VM-20-6-ubuntu:/usr/local/nginx/sbin$ sudo ./nginx -s quit
ubuntu@VM-20-6-ubuntu:/usr/local/nginx/sbin$ sudo ./nginx -s stop
小结
Nginx源码总述
Nginx源码入口函数定位
文件和目录权限的区别
gcc一下
终端和进程的关系
终端与bash进程
进程21015就是当前终端的bash进程
虚拟终端
pts就是虚拟终端,一个SecureCRT终端连接到Ubuntu Linux虚拟机,编号就是pts/0;又一个SecureCRT连接到Ubuntu Linux虚拟机,编号就是pts/1;继续连接,编号就是pts/2……以此类推。每连上一个终端,这个编号都增加一个数字。这里读者掌握了一个概念,叫作“虚拟终端”。
每个虚拟终端连到Ubuntu Linux虚拟机上,都会打开一个bash进程(也叫shell——壳)。向其中输入各种命令的黑窗口就是bash ,用于解释用户输人的命令。在SecureCRT中输入一条命令并按回车键后,命令就会被bash进程解释并执行(bash就是shell,也就是命令行解释器是个可执行程序)。
可以使用whereis命令查找bash这个可执行程序位置
ubuntu@VM-20-6-ubuntu:/usr/local/nginx/sbin$ whereis bash
bash: /bin/bash /etc/bash.bashrc /usr/share/man/man1/bash.1.gz
sleep函数
man 3 sleep
编写简易程序
nginx.c
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
/* code */
printf("520, fangshuang!\n");
for(;;) {
sleep(1);
printf("rest 1 second\n");
}
printf("program exit! bye!\n");
return 0;
}
上面的ps命令中,参数I(这是字母1,不是数字1,注意区别)的意思是一种比较长的显示输出格式;参数a的意思是显示终端上的所有进程,包括其他终端上的进程。
可以看到,pts/0 终端运行着nginx进程,该进程的代码在不断循环,也就是说该进程一直在运行。
注意观察,现在把运行nginx的终端关闭(只需要在SecureCRT中单击终端窗口上面标题右侧的x即可)
可以发现一个问题:随着终端的退出,该终端上运行的进程(nginx)也退出了。
一个终端对应一个bash,并且是使用./nginx来执行nginx可执行程序,那么该可执行程序就是bash的子进程。
不难看到nginx是bash的子进程,因为nginx的父进程ID(PPID)是31941,而31941恰好是bash进程的进程D (PID)
进程关系图
一般来讲,启动一个子进程用fork函数。最上面的init进程是“老祖宗”进程(操作系统启动时内核自己创建出来的,具有超级用户特权),init进程的进程ID (PID)是1,系统内的其他进程应该都是由它或者它的子进程fork(创建)出来的。可以输入ps-ef命令查看结果(如图3.40所示),其中PID为1的进程就是init进程。
如果直接在VMware Workstation Pro中登录该Ubuntu Linux 虚拟机(就是用终端tty进行登录), 就用login进程处理;而用SecureCRT登录Ubuntu Linux虚拟机属于通过ssh服务进行远程登录,这种情况下则用sshd 远程登录服务进程进行处理(只要观看完整的ps -ef命令的输出,观看各种进程之间的父子关系,就不难得到这个结论)。在bash(黑窗口)中执行的nginx进程,就成了该bash的子进程(可以理解为bash调用fork函数创建的进程)。如果希望详细了解进程的层次关系,推荐阅读《UNIX环境高级编程》第3版第9 章“进程关系”。
进程之间虽然有父子关系,但毕竟是2个进程,彼此独立,并不是父进程退出了,子进程就得退出。换句话说,并不是bash进程退出了,nginx子进程就得退出。
进程有进程ID(PID),有父进程ID(PPID)。还要知道一点:每个进程还可以属于一个进程组(分成组方便管理,例如可以给整个组发送一条信息等)。进程组是个新概念,表示1个或多个进程的集合。每个进程组有一个唯一的进程组ID,一个进程组中的各个进程可以独立接收来自终端的各种信号。可以通过调用系统函数创建进程组、加入进程组等。
会话Session
“会话”(session),是1个或者多个进程组的集合
1个会话(session)由多个进程组构成,1个进程组又由多个进程构成。一般来讲,如果不调用特殊的系统函数进行特殊处理,1个bash(shell)上运行的所有程序都将属于1个会话(session),而这个会话会有1个会话首进程(session leader,就是创建该会话的进程),这个shell(bash)通常就是session leader。另外,可以调用函数创建新的session
ubuntu@VM-20-6-ubuntu:~$ ps -eo pid,ppid,sid,tty,pgrp,comm | grep -E 'bash|PID|nginx'
上面这条ps命令中,e表示显示所有进程;o表示可以自己指定显示哪些列(sid表示session id,tty表示终端,pgrp表示进程组,comm表示执行的命令)。grep命令中,-E表示要开启扩展正则表达式功能,用于配合后面的bash|PIDlnginx以显示特定名字的进程;l表示或者关系;bash|PIDlnginx表示bash、PID、nginx这几个字符串中的某个出现就会被显示。
- (1)nginx进程的PPID是19649,而19649是某个bash的PID,说明nginx是某个bash 的子进程。
- (2)nginx进程的SID是19649,而19649是这个bash的PID,同时还是这个bash的SID,也就是说这个bash的SID等于PID,这就意味着这个bash是整个会话(session)的会话首进程(session leader)。
nginx进程的PID和PGRP都是4608,说明nginx自成一组 - (3)PID=19649的bash进程和nginx进程,它们的TT(tty)都是pts/0(编号为0的终端)
- (4)如果SecureCRT终端断开,系统就会给这个session leader(也就是这个bash进程)发送SIGHUP信号(终端断开)
- (5)bash进程收到SIGHUP信号后,就会把这个信号发送给session里面的所有进程,收到这个SIGHUP信号的进程默认动作就是退出。
strace工具的使用
strace是Linux中的调试分析诊断工具,功能强大,可用于跟踪程序执行时进程的系统调用(system call)和所接收到的信号
ubuntu@VM-20-6-ubuntu:~$ sudo strace -e trace=signal -p 4608
这条命令用于跟踪PID为4608的进程(nginx进程)上与信号(signal)有关的系统调用。也就是说,这条命令把strace工具附着到进程4608上
现在一个终端运行着nginx,另外一个终端附着了nginx进程,这2个终端窗口就都无法做其他事情了。现在打开第3个终端窗口,再使用strace附着一下该nginx进程PID为19649的父进程(bash)
ubuntu@VM-20-6-ubuntu:~$ sudo strace -e trace=signal -p 19649
现在;在SecureCRT中关闭运行nginx进程的窗口,会发现另外2个运行strace的窗口都有内容输出。
首先看一下附着了nginx进程的终端窗口的输出内容
可以看到,信号是SIGHUP, si_pid表示谁发来的信号,19649就是bash进程(nginx进程的父进程)。
接着看一下附着了bash进程的终端窗口的输出内容
- “kill(-4608, SIGHUP) = 0”这行。nginx进程的进程组(PGRP)ID就是4608,所以这行表示发送信号给这个数字的绝对值所在的进程组(kill后面如果是负值,一般代表发送信号给一个进程组),也就是说,进程组ID为4608的所有进程都会收到SIGHUP信号。
- kill(19649, SIGHUP) = 0这行,19649是bash进程自己的PID,bash在第1次收到SIGHUP信号时先把该信号发给session内的其他进程(不仅是上面的4608进程组中的进程,如果有其他进程组,其他进程组中的进程也会收到SIGHUP信号——只要这些进程的sessionID相同),然后再次发送SIGHUP命令给自己,将自己杀死。下一行的“si_pid=19649”也说明杀死了自己
终端关闭时让进程不退出
当终端关闭时,终端上运行的nginx进程就退出了。现在要解决的问题是,终端关闭时,如何让nginx进程不退出。设想一下,如果nginx进程拦截SIGHUP信号,是否可以;如果nginx进程和·bash进程在不同的session里(有不同的session ID),是否可以
方法一:拦截SIGHUP信号
收到SIGHUP信号后,操作系统默认结束nginx进程,现在通过修改代码,告诉操作系统忽略掉该信号,操作系统就不会执行默认处理行为(不会结束该进程)
nginx.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int main(int argc, char const *argv[])
{
/* code */
printf("520, fangshuang!\n");
//系统函数,设置收到某个信号时的处理程序(用哪个函数来处理)
//SIG_IGN代表: 我要求忽略这个信号,请求操作系统不要执行默认的该信号处理动作(不要把本进程杀掉)
signal(SIGHUP, SIG_IGN);
for(;;) {
sleep(1);
printf("rest 1 second\n");
}
printf("program exit! bye!\n");
return 0;
}
现在关闭第1个SecureCRT终端(运行nginx进程的终端),在第2个终端再次利用ps命令查看,会发现PID为636的bash进程没了,但nginx进程依旧在。不过它的TT(终端)列显示变成了“?”,这表示没有对应的终端了,而且它的PPID(父进程ID)变成了1,1正是前面介绍过的老祖宗进程init 的PID(进程ID)。本来nginx进程的父进程是bash,但是bash被结束了,nginx 进程就变成孤儿进程,这种孤儿进程就被init进程收养了。
方法二:nginx进程和bash进程在不同的session中
setsid()
ubuntu@VM-20-6-ubuntu:~/myProj$ man 2 setsid
如果调用进程不是进程组负责人,setsid() 将创建一个新会话。调用进程是新会话的负责人(即,其会话 ID 与其进程 ID 相同)。调用进程还将成为会话中新进程组的进程组负责人(即,其进程组 ID 与其进程 ID 相同)。
调用进程将是新进程组和新会话中的唯一进程。最初,新会话没有控制终端。有关会话如何获取控制终端的详细信息,请参阅 credentials(7)。
第一版程序
nginx.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
/* code */
printf("520, fangshuang!\n");
//系统函数,设置收到某个信号时的处理程序(用哪个函数来处理)
//SIG_IGN代表: 我要求忽略这个信号,请求操作系统不要执行默认的该信号处理动作(不要把本进程杀掉)
// signal(SIGHUP, SIG_IGN);
setsid();
for(;;) {
sleep(1);
printf("rest 1 second\n");
}
printf("program exit! bye!\n");
return 0;
}
看起来不太对,因为bash和nginx这两个进程的SID一样,没有达到要求的效果(希望它们不一样),这说明程序写得不对
可以发现这个nginx进程的PID是20400,PGRP也是20400,说明进程nginx是编号为20400这个进程组的组长,组长调用setsid函数是无效的
第二版程序
nginx.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
/* code */
pid_t pid;
printf("520, fangshuang!\n");
// 系统函数 用来创建新进程
// 子进程会从这句fork调用之后开始执行,原来的父进程也会接着往下执行
pid = fork();
if(pid < 0) {
perror("fork");
} else if(pid == 0) {
printf("child process begin!\n");
// 创建新的session
setsid();
for(;;) {
sleep(1);
printf("child process rest 1 second\n");
}
return 0;
} else {
// 父进程
for(;;) {
sleep(1);
printf("parent process rest 1 second\n");
}
return 0;
}
printf("program exit! bye!\n");
return 0;
}
与以往的代码不同,这里调用fork创建了一个子进程(子进程创建成功后会和父进程并行运行),然后在子进程中调用setsid。上面已经演示了,主进程里调用setsid是没有达到效果的,因为主进程是进程组的组长,不允许调用setsid来建立新会话。
可以清楚地看到,PID为26125的nginx进程的父进程ID(PID)是26124,但是26125和26124这2个进程的SID并不相同。还可以发现调用fork函数创建出的子进程没有关联的终端(TT/TTY列显示为“?”)。
现在,把正在显示nginx执行结果的SecureCRT终端断开连接。在另外一个SecureCRT终端再执行上述ps命令
可以发现PID为2944的bash没有了,PID为26124的父nginx 进程也没了,但PID为26125的子进程还在,子进程变成了孤儿进程,被老祖宗init收养了,所以子进程的PPID变成了1
ubuntu@VM-20-6-ubuntu:~$ sudo kill 26125
方法三:setsid命令
setsid不但是个函数(可以在代码中使用),也是个命令(可以在命令行中使用)。该命令用于启动一个进程,而且能够使启动的进程在一个新的session中,这样终端关闭时进程就不会退出
ubuntu@VM-20-6-ubuntu:~$ setsid ./nginx
ubuntu@VM-20-6-ubuntu:~$ ps -eo pid,ppid,sid,tty,pgrp,comm,cmd | grep -E 'bash|PID|nginx'
nginx进程的父进程ID是1,SID(session id)是25622,和其他2个bash的SID都不同。TT(终端)显示的是“?”,表示该进程不隶属于任何终端
现在关闭刚刚执行setsid ./nginx命令的SecureCRT终端窗口。再次使用上面的ps命令查看进程信息,可以看到nginx进程依然存在
方法四:nohup(no hang up不挂断)
前面用到一个终端断开信号(SIGHUP),看起来与这个命令很相似,都有hup。是的,使用nohup命令启动的程序会忽略SICHUP信号,与用程序编写忽略SIGHUP信号同理
ubuntu@VM-20-6-ubuntu:~/myProj$ nohup ./nginx
可以发现,本来向屏幕上输出的内容,现在不输出了,光标停在屏幕上。这是nohup命令的特点,该命令会把输出重定向到当前目录的nohup.out文件中,程序需要执行10~20s 才能发现nohup.out的尺寸有变化(变得大于0了)。所以,这种向nohup.out文件的输出程序执行结果并不是实时输出,而是有一定的延迟。可以在新的SecureCRT终端窗口中输入ls 命令查看,也可以用cat命令查看。
可以看到,nginx的父进程是bash。现在关闭这个bash(关闭运行nohup命令的这个终端窗口),用另一个SecureCRT终端窗口再次输入ps命令查看
可以看到,nginx进程仍然存在,其PPID变成了1,说明其变成了孤儿进程,被init老祖宗进程收养了
后台执行(运行)的简单理解
如果执行程序时在末尾增加一个“&”,即输入如下命令(确保在nginx可执行程序所在的目录)
ubuntu@VM-20-6-ubuntu:~/myProj$ ./nginx &
此时程序就在后台执行了(直接输入./nginx执行的方式可以理解成在前台执行)
程序在后台执行和前台执行的区别
程序在后台执行,执行该程序的同时,该终端同时还能做其他事情;程序在前台执行终端只能等待该程序执行完成才能继续执行其他程序。
信号的概念、认识、处理动作
信号的基本概念
信号,在很多大型应用程序中都经常出现。信号就是一个通知(事件通知),用来通知某一个进程发生了某一件事。当然,这些事情或者说这些信号一般都是突然事件,或者说都是突然到来的,进程本身并不知道这些信号在什么时候发生。换句话说,信号是异步发生的,又称“软件中断”。
在Nginx中,可以把信号想象成是master进程和worker进程之间的一个很有效的通信手段,所以,还可以把信号看成一种非常简单的短消息,
- (1) 某个进程发送给另外一个进程或者发送给自己
注意:进程能把信号发送给自己。某进程把自己执行起来后,再执行一个自己并向原来的自己发送信号是可以的。如Nginx在运行过程中(1个master进程,多个worker进程),要做热升级。热升级是要启动新master进程的,那么这个新启动的master进程启动时,通过增加一些命令行参数的手段就可以向旧的master进程发送一些信号,从而控制旧的master进程做一些动作。 - (2) 由内核发送给某个进程
①通过在键盘上输入一些命令动作,如按Ctrl+C组合键(中断信号)、使用kill命令等。②内存访问异常。除数为0等,硬件会检测到并通知内核等。
信号是有名字的,这些名字都以SIG开头(如SIGHUP信号——终端断开信号),UNIX操作系统以及所有类UNIX操作系统(类,在这里是类似的意思,与UNIX类似的操作系统,就像UNIX的操作系统,如Linux、FreeBSD、Solaris等)支持的信号数量各不相同,少则支持十几个,多则支持50~60个。
信号虽然有名字,其实也都是一些数字。准确地说,信号是一些正整数,定义在系统的头文件里,一般在编程的时候包含signal.h(/usr/include/)头文件即可,该头文件中有对具体的信号定义头文件的包含,不用操心太多。
另外gcc在编译程序的时候也有一个搜索头文件及库文件的顺序。类UNIX操作系统默认的搜索路径为:头文件搜索的一些路径,/usr/local/include/或/usr/include/,等等;库文件链接的一些路径,/usr/local/lib/或/usr/lib/等。
xargs在文件系统中查找名为"signal.h"的文件
ubuntu@VM-20-6-ubuntu:~$ sudo find / -name "signal.h" | xargs grep -in "SIGHUP"
这个命令的含义是从根目录开始,寻找一个名字叫作signal.h的文件,-name就是根据名字来查找。在某些文件中查找某行是否包含某个字符串时,可以用上面这条命令
grep命令是文本搜索工具,这里用-i参数表示查找时忽略大小写,n参数表示显示查找到的行号,要查找的文本字符串是“SIGHUP”。
注意xargs在这里所起的作用。xargs是一个比较玄的东西,其实也是个命令,用于向其他命令传递参数。只需要知道用了xargs之后,find命令找到的文件内容就能够传递到grep中,所以grep实际是在找到的文件内容中进行搜索。如果不使用xargs,就不是在文件内容中进行搜索,而是在文件名中进行搜索了。总之,上面这种命令行的写法比较固定。
这段代码是在Linux系统中使用find命令查找所有文件系统中名为"signal.h"的文件,并将结果传递给xargs命令。xargs命令会将找到的文件路径作为参数传递给grep命令,然后在这些文件中搜索"SIGHUP"字符串,不区分大小写(使用-i选项),并输出包含该字符串的行及其行号(使用-n选项)。
简而言之,这段代码的作用是在文件系统中查找名为"signal.h"的文件,并在这些文件中搜索包含"SIGHUP"字符串的行,并显示该行及其行号。
从找到的结果中随便找一个signal.h文件,利用vim查看内容
signal.h中信号宏定义
在vim编辑器中查看signal.h的结果,看起来大概有32个信号
可以看到,这些SIG开头的信号其实就是一些宏定义,信号名称被定义成了一些数字。同时可以发现,这些信号是从1开始编号的,编号0保留作其他用途,先不用考虑,了解信号对应的数字编号是从1开始的即可
通过kill命令认识一些信号
nginx.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
/* code */
pid_t pid;
printf("520, fangshuang!\n");
for(;;) {
sleep(1);
printf("process rest 1 second!\n");
}
printf("program exit! bye!\n");
return 0;
}
ubuntu@VM-20-6-ubuntu:/usr/include/x86_64-linux-gnu/asm$ sudo strace -e trace=signal -p 15976
strace: Process 15976 attached
ubuntu@VM-20-6-ubuntu:/usr/local/nginx/sbin$ kill 15976
得到第1条结论:如果只是单纯输入“kill进程ID”,就是向该进程发送SIGTERM终止信号(哪个SecureCRT终端窗口执行的kill命令,si_pid就是该SecureCRT终端窗口bash进程的进程ID),同时进程被终止(不能用ps列出了),显示killed by SIGTERM,说明被SIGTERM信号杀死了。
ubuntu@VM-20-6-ubuntu:~$ ps -eo pid,ppid,sid,tty,pgrp,comm,cmd | grep -E 'bash|PID|nginx'
ubuntu@VM-20-6-ubuntu:~$ sudo strace -e trace=signal -p 24106
strace: Process 24106 attached
ubuntu@VM-20-6-ubuntu:~$ kill -1 24106
得到第2条结论:如果用“kill-1进程ID”命令来杀进程,就是向该进程发送SIGHUP挂断信号(哪个终端窗口执行的kill命令,si_pid就是该终端窗口bash进程的进程ID),同时进程被终止(不能用ps列出了),显示killed by SIGHUP,说明被SIGHUP信号杀死了
继续测试,步骤基本同前,只是这次输入的kill命令如下
ubuntu@VM-20-6-ubuntu:~$ kill -2 31163
得到第3条结论:如果用“kill -2 进程ID”命令来杀进程,就是向该进程发送SIGINT中断信号(哪个终端窗口执行的kill命令,si_pid就是该终端窗口bash进程的进程ID),同时进程被终止(不能用ps列出了),显示killed by SIGINT,说明被SIGINT信号杀死了。
结论就是:用“kill-数字进程ID”的方式可以发送多种信号。kill后面直接跟进程ID而不带“-数字”时,那个数字应该默认是15,即“-15”
值得一提的是-19对应的SIGSTOP信号,该信号用于停止进程,但进程还在,和以往的信号(比如SIGKILL)不一样(SIGKILL是终止进程,进程不在了)。
进程的状态
要查看进程的状态,还是使用ps命令,在显示的列中增加一个stat列即可。将nginx 进程运行起来,在另一个SecureCRT终端窗口输入如下命令:
ubuntu@VM-20-6-ubuntu:~$ ps -eo pid,ppid,sid,tty,pgrp,comm,cmd,stat | grep -E 'bash|PID|nginx'
当然,ps命令配合aux也可以显示进程状态(aux代表一种BSD风格的显示格式)。输入如下命令:
ubuntu@VM-20-6-ubuntu:~$ ps aux | grep -E 'bash|PID|nginx'
进程状态字母
可以看到nginx进程的状态是S+状态。S意为sleeping(睡眠),十意为在前台运行
如果现在用kill-19命令发送SIGSTOP信号来停止nginx进程(不是终止),则进程还在
ubuntu@VM-20-6-ubuntu:~$ kill -19 2939
可以看到nginx进程依旧存在,进程的状态从“S+”变成了“T”(被停止/暂停的进程)。如果此时向nginx进程发送kill -18命令,会是什么情形呢? -18是SIGCONT(使停止/暂停的进程继续)信号,现在尝试向nginx进程发送SIGCONT信号,输入:
ubuntu@VM-20-6-ubuntu:~$ kill -18 2939
继续运行nginx进程后,它似乎切换到后台继续运行了,因为该进程原来的状态是“S+”,现在状态变成“S”,缺少了“+”。用fg命令可以将nginx进程切换到前台运行
常用信号列举
信号处理的相关动作
UNIX/Linux体系结构、信号编程初步
UNIX/Linux操作系统体系结构
一般情况下,类UNIX操作系统体系结构分为用户态和内核态
- (1)操作系统/内核——软件。用于控制计算机硬件资源,提供应用程序运行的环境。之所以称为内核,是因为比较小,且位于整个体系结构的核心。
从程序开发人员的角度来理解用户态和内核态这2个概念。程序员编写的程序,要么运行在用户态,要么运行在内核态,一般情况下运行在用户态,当程序要执行一些特殊代码时,程序就可能切换到内核态。这种切换由操作系统控制,不需要人为介入
再试着换一种角度去理解。用户态可以理解成最外圈应用程序的活动空间。但是,程序员所开发的应用程序的运行,往往需要访问一些资源(如读写文件要访问磁盘,调用malloc要申请内存,程序中可能还要访问传真机、打印机等外部设备),为了让应用程序能够访问这些资源,内核必须提供供应用程序访问的接口,也就是系统调用(内核的对外接口)。
- (2)系统调用其实就是一些函数(库函数),写代码时调用即可。一般情况,操作系统会提供200~300个库函数供程序员调用,这些库函数在系统内部高度封装,无须关心细节,只需要调用即可。
- (3)shell是老朋友了,前面已经多次涉及。当用SecureCRT连接Ubuntu Linux虚拟机时,使用ps命令查看,就能看到一个bash进程(这个bash进程是自动开启的,用SecureCRT连接虚拟机时,系统就启动了一个bash。图中可以看到,bash是被login/sshd用fork函数开启的。fork函数,用于创建进程)
参考前面的图, 可以看到COMMAND列的bash字样。bash是什么呢,这里阐述一下:bash是borne again shell(重新装配的shell)的缩写,是shell的一种,Linux上默认采用的是bash这种shell
通俗地说,bash也是一个可执行程序,这种可执行程序的主要作用是:把用户输入的命令翻译给操作系统,所以bash相当于一个命令解释器。
bash是由login/sshd进程调用创建进程的函数fork执行起来的。也可以手动执行bash,因为它是个可执行程序
尝试用whereis找到bash的位置,直接执行,然后使用exit退出当前的bash
如果第2次输人exit,则会从当前使用的bash中退出,SecureCRT会与Ubuntu Linux虚拟机断开连接(因为第1次exit退出的是自己执行的bash,第2次exit退出的是login/sshd进程用函数fork创建的bash进程)
观察图中shell的位置,它正好夹在系统调用和应用程序之间,起分隔系统调用和应用程序的作用,同时也有胶水(将系统调用和应用程序粘在一起)的感觉。
通俗地说,一个SecureCRT终端连接了Ubuntu Linux虚拟机,就开启了一个shell(也就是经常看到的这个大黑窗口,操作者在该窗口中输入命令,执行各种程序等),一般一按回车键,shell就对输入的内容进行解释、执行。
用户态内核态之间的切换
程序员编写的程序,要么运行在用户态,要么运行在内核态,2种状态必居其一。运行于用户态的进程可执行的操作和可访问的资源都受到极大的限制(用户态权限极小),而运行在内核态的进程则可以执行任何操作并且在资源的使用上没有限制(内核态权限较大)。
一个程序执行过程中,大部分时间都处于用户态下,只有需要内核所提供的服务时才切换到内核态,内核态的任务处理完成后,又切换回用户态。如C函数库中的内存分配函数malloc,其内部会使用sbrk系统调用来分配内存,当malloc调用sbrk的时候就涉及一次从用户态到内核态的切换。类似的函数还有printf使用write系统调用来输出字符串,等等。这种状态的转换由操作系统来完成,不需要程序员介入。
为什么要分用户态,内核态?
- ①用户态权限小,内核态权限大。权限大意味着能做一些危险的事(如处理内存、处理时钟等), 如果所有用户程序都能做这些事,就可能经常出现错用乱用,导致系统处于危险状态甚至崩溃。所以,一般情况下,程序都处于用户态,只有需要做一些危险的事情时,系统才提供一些调用接口,让程序切换到内核态。
- ②这些调用接口是系统提供并统一管理的。操作系统的资源是有限的,如果许多程序同时访问这些资源,一旦产生访问冲突,或资源耗尽,就可能导致系统崩溃,所以,系统提供这些接口,就是为了减少有限资源的访问以及使用上的冲突。
什么时候会从用户态跳到内核态呢?
- ①系统调用。如上述的malloc
- ②异常事件。如接收到某个信号,代码中写了一段信号处理函数,系统就可能需要跳到内核态去做一些调用该信号处理函数的准备工作
- ③外围设备的中断。外围设备完成用户的请求操作后,会向CPU发出中断信号,此时CPU就会暂停执行下一条即将执行的指令,转而去执行中断信号对应的处理程序,如果先前执行的指令是在用户态下,就会发生从用户态到内核态的转换
signal函数范例
通过2次调用signal函数,分别注册信号SIGUSR1和信号SIGUSR2对应的信号处理函数(sig_usr),当收到这2个信号时,sig_usr就会被调用。在sig_usr信号处理函数过程中,只是做了一些信息输出的简单工作
(1)signal函数捕捉了系统的SIGUSR1和SIGUSR2信号,并用自己的函数来处理,获得了成功。如果程序中不捕捉SIGUSR1或者SIGUSR2信号,用kill向该进程发送SIGUSR1或者SIGUSR2信号,进程会有什么表现呢?当然是终止执行——因为这两个信号的系统默认动作都是终止进程
修改nginx.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
void sig_usr(int signo) {
if(signo == SIGUSR1) {
printf("receive SIGUSR1 signal!\n");
} else if(signo == SIGUSR2) {
printf("receive SIGUSR2 signal!\n");
} else {
printf("receive other signal %d!\n", signo);
}
}
int main(int argc, char const *argv[])
{
/* code */
// if(signal(SIGUSR1, sig_usr) == SIG_ERR) {
// printf("can not capture SIGUSR1 signal!\n");
// }
// if(signal(SIGUSR2, sig_usr) == SIG_ERR) {
// printf("can not capture SIGUSR2 signal!\n");
// }
for(;;) {
sleep(1);
printf("rest 1 second\n");
}
printf("program exit! bye!\n");
return 0;
}
(2)根据发送信号时显示的程序结果,可以发现如下一些问题。信号可能是某个进程发出的,也可能是内核发出的,但不管是怎么发出的,总之目标进程(nginx)收到了这个信号。目标进程收到信号这件事,会被内核注意到,这时内核就有动作了。内核的动作是什么呢?如下图所示。不难看到,进程nginx本来正在正常执行,突然到来的信号导致nginx进程的流程被打断,进程从用户态切换到了内核态;在内核态中,因为要调用程序员所写的信号处理函数sig_usr,所以进程nginx又会从内核态切换回用户态调用sig_usr;sig_usr执行完毕后,进程nginx又会切换回内核态处理一些收尾工作;最后从内核态切换回用户态,从前被打断的执行流程处开始继续执行(这一切都由操作系统来控制,无须程序员介入)
修改代码nginx.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
int g_mysign = 0;
void muNEfunc(int value) {
//....其他处理
//函数muNEfunc能够修改全局变量g_mysign的值
g_mysign = value;
//....其他处理
}
void sig_usr(int signo) {
muNEfunc(22);
if(signo == SIGUSR1) {
printf("receive SIGUSR1 signal!\n");
} else if(signo == SIGUSR2) {
printf("receive SIGUSR2 signal!\n");
} else {
printf("receive other signal %d!\n", signo);
}
}
int main(int argc, char const *argv[])
{
/* code */
if(signal(SIGUSR1, sig_usr) == SIG_ERR) {
printf("can not capture SIGUSR1 signal!\n");
}
if(signal(SIGUSR2, sig_usr) == SIG_ERR) {
printf("can not capture SIGUSR2 signal!\n");
}
for(;;) {
sleep(1);
printf("rest 1 second\n");
muNEfunc(31);
printf("g_mysign = %d\n", g_mysign);
}
printf("program exit! bye!\n");
return 0;
}
仔细分析一下就不难看出,如果刚刚执行完for循环中的muNEfunc函数调用,全局变量g_mysign的值应该等于15,但此时突然收到一个SIGUSR1信号执行信号处理函数sig_usr,刚好sig_usr也调用了muNEfunc函数,全局变量g_mysign的值就变成了22。(当信号处理函数sig_usr调用完毕,整个程序流程回归调用信号处理函数之前的代码,应该是main函数里面的代码行printf(“g_mysign=%d\n”,g_mysign);,输出的g_mysign值就是22了。)
简而言之,本来期望每次输出的g_mysign值都是15,但是偏偏收到一个信号,信号处理程序中改变了g_mysign的值,导致printf输出的g_mysign变成了22。这个结果,是不是出乎预料呢!
可重入函数
可重入函数又称可重入的函数或异步信号安全的函数,指在信号处理函数中调用是安全的函数。显然,muNEfunc不安全,因为信号处理函数中一调用,就改变了g_mysign的值,当信号处理函数执行完毕,回归正常程序流程时,g_mysign值被改变了,所以muNEfunc不是一个可重入函数
有些周知的函数都是不可重人(在信号处理函数中不要调用)的,如malloc分配内存函数(如果在正常代码中malloc,然后突然转到信号处理函数中再调用malloc,就可能出问题。虽然出问题的概率非常小,但毕竟也是隐患)、printf屏幕输出函数等。前面演示的范例(nginx.c)中,在信号处理函数sig_usr里面调用了printf函数,实际的商业代码中请避免在信号处理函数中调用printf函数。
刚才举的例子muNEfunc也比较典型,因为这个函数修改了全局变量的值。在Linux中有与该函数类似的,如在Linux编程中有一个系统定义的变量errno,很多系统函数如果出现错误,就会给这个变量赋值。试想一下,假如在main主函数中调用了一个系统函数A,恰好出错,这个系统函数给变量errno赋一个值(假设为10),此时突然收到一个信号,系统调用对应的信号处理函数去了,信号处理函数中调用了一个系统函数B,也出错了,这个系统函数也给变量errno赋了一个值(假设为20),原来的errno中的值10就被覆盖了。
当信号处理函数执行完毕返回正常的程序流程并继续执行的时候,明明errno应该是10,此时却变成了20,如果此时正常的执行代码中用这个errno来做一些判断,很可能导致程序代码出现逻辑错误。
小结论
- (1)在信号处理函数中,应尽量使用简单的语句做简单的事情,尽量不要调用系统函数以免引起麻烦(这是避免麻烦的最有效方法)。
- (2)如果必须在信号处理函数中调用系统函数,只调用可重入函数,不要调用不可重入的系统函数。
- (3)如果必须在信号处理函数中调用那些可能修改errno值的不可重入系统函数,应考虑事先备份errno的值,事后再从信号处理函数返回之前恢复errno的值。实际上刚才的muNEfunc就应该这样处理,只是把muNEfunc认为是不可重入函数,但是有一些修改errno值的系统函数被认为是可重入函数
修改nginx.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <sys/types.h>
int g_mysign = 0;
void muNEfunc(int value) {
//....其他处理
//函数muNEfunc能够修改全局变量g_mysign的值
g_mysign = value;
//....其他处理
}
void sig_usr(int signo) {
//备份errno值
int myerrno = errno;
muNEfunc(22);
if(signo == SIGUSR1) {
printf("receive SIGUSR1 signal!\n");
} else if(signo == SIGUSR2) {
printf("receive SIGUSR2 signal!\n");
} else {
printf("receive other signal %d!\n", signo);
}
//还原errno值
errno = myerrno;
}
int main(int argc, char const *argv[])
{
/* code */
if(signal(SIGUSR1, sig_usr) == SIG_ERR) {
printf("can not capture SIGUSR1 signal!\n");
}
if(signal(SIGUSR2, sig_usr) == SIG_ERR) {
printf("can not capture SIGUSR2 signal!\n");
}
for(;;) {
sleep(1);
printf("rest 1 second\n");
muNEfunc(31);
printf("g_mysign = %d\n", g_mysign);
}
printf("program exit! bye!\n");
return 0;
}
不可重入函数的错用演示
nginx3.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/types.h>
void sig_usr(int signo) {
if(signo == SIGUSR1) {
printf("receive SIGUSR1 signal!\n");
} else if(signo == SIGUSR2) {
printf("receive SIGUSR2 signal!\n");
} else {
printf("receive other signal %d!\n", signo);
}
}
int main(int argc, char const *argv[])
{
/* code */
if(signal(SIGUSR1, sig_usr) == SIG_ERR) {
printf("can not capture SIGUSR1 signal!\n");
}
if(signal(SIGUSR2, sig_usr) == SIG_ERR) {
printf("can not capture SIGUSR2 signal!\n");
}
for(;;) {
int* p;
p = (int*)malloc(sizeof(int));
free(p);
}
printf("program exit! bye!\n");
return 0;
}
ubuntu@VM-20-6-ubuntu:~$ ps -eo pid,ppid,sid,tty,pgrp,comm,cmd,stat | grep -E 'bash|PID|nginx'
修改nginx3.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/types.h>
void sig_usr(int signo) {
//这里也malloc,这是错用,不可重入函数不能用在信号处理函数中
int* p;
p = (int *) malloc (sizeof(int)); //用了不可重入函数
free(p);
if(signo == SIGUSR1) {
printf("receive SIGUSR1 signal!\n");
} else if(signo == SIGUSR2) {
printf("receive SIGUSR2 signal!\n");
} else {
printf("receive other signal %d!\n", signo);
}
}
int main(int argc, char const *argv[])
{
/* code */
if(signal(SIGUSR1, sig_usr) == SIG_ERR) {
printf("can not capture SIGUSR1 signal!\n");
}
if(signal(SIGUSR2, sig_usr) == SIG_ERR) {
printf("can not capture SIGUSR2 signal!\n");
}
for(;;) {
int* p;
p = (int*)malloc(sizeof(int));
free(p);
}
printf("program exit! bye!\n");
return 0;
}
进程状态是R+状态(R表示处于运行之中,十表示表示位于前台进程组)
信号编程进阶、sigprocmask范例
信号集
收到一个SIGUSR1信号,开始执行信号处理函数sig_usr,尚未执行完时,突然又收到一个SIGUSR1信号,系统会不会再次触发sig_usr函数并开始执行呢?一般不会,也就是说,当收到某个信号,启动执行信号处理函数的时候,通常会“屏蔽/阻塞”其后的相同信号,直到信号处理函数执行结束(系统自动处理,程序员不需要干预)
一个进程必须记住当前阻塞了哪些信号。如收到信号SIGUSR1时,系统将标记正在处理该信号的标志设置为1,然后去执行信号处理函数,如果信号处理函数未执行完时再次收到该信号,系统检测到该信号的标志已经为1,后来的SIGUSR1信号就需要排队等待(等待调用信号处理函数来处理)或直接被忽略(丢失)。后面会演示排队等待和被忽略的情形。当信号处理函数执行完毕,再把信号SIGUSR1对应的标志设置回0,此时如果有排队的SIGUSR1信号或者新收到的SIGUSR1信号,就可以继续调用信号处理函数处理了
信号相关函数(sigemptyset、sigfillset、sigaddset、sigdelset、sigprocmask、sigismember)
sigprocmask等信号函数范例演示
sigprocmask信号函数
信号程序
nginx.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <stdlib.h>
//信号处理函数
void sig_quit(int signo) {
printf("received SIGQUIT signal!\n");
}
int main(int argc, char const *argv[])
{
/* code */
//定义信号集:新的信号集, 原有的信号集
sigset_t newmask, oldmask, pendmask;
//注册信号对应的信号处理函数
if(signal(SIGQUIT, sig_quit) == SIG_ERR) {
printf("can not capture SIGUSR1 signal!\n");
//退出程序, 参数是错误代码, 0表示正常退出, 非0表示错误, 但具体什么错误, 没有特别规定
exit(1);
}
//newmask信号集中所有信号都清0(表示这些信号都没有来)
sigemptyset(&newmask);
//设置newmask信号集中的SIGQUIT信号位为1, 再来SIGQUIT信号时, 进程就收不到, 设置为1就是该信号被阻塞掉
sigaddset(&newmask, SIGQUIT);
//设置该进程所对应的信号集
//第一个参数用了SIG_BLOCK表明设置进程新的信号屏蔽字为 当前信号屏蔽字 和 第二个参数指向的信号集的并集
//一个 "进程" 的当前信号屏蔽字,刚开始全部都是0的; 相当于把当前 "进程"的信号屏蔽字设置成 newmask(屏蔽了SIGQUIT);
//第三个参数不为空,则进程老的(调用本sigprocmask()之前的)信号集会保存到第三个参数里,用于后续,这样后续可以恢复老的信号集给进程
if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {
perror("sigprocmask(SIG_BLOCK)");
exit(1);
}
printf("I begin sleep 10s---------------begin---------------, during this time, I can't receive SIGQUIT signal!\n");
sleep(10);
printf("I've slept 10s---------------end---------------!\n");
//测试一个指定的信号位是否被置位,这里测试的是newmask
if(sigismember(&newmask, SIGQUIT)) {
printf("SIGQUIT signal has been shielded!\n");
} else {
printf("SIGQUIT signal has not been shielded!\n");
}
//测试另外一个指定的信号位是否被置位,测试的是newmask
if(sigismember(&newmask, SIGHUP)) {
printf("SIGHUP信号被屏蔽了!\n");
} else {
printf("SIGHUP信号没有被屏蔽!\n");
}
//现在取消对SIGQUIT信号的屏蔽(阻塞)--把信号集还原回去
//第一个参数用了SIGSETMASK表明设置进程新的信号屏蔽字为 第二个参数 指向的信号集,第三个参数没用
if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) {
perror("sigprocmask(SIG_SETMASK)");
exit(1);
}
printf("sigprocmask(SIG_SETMASK)成功!\n");
if(sigismember(&oldmask, SIGQUIT)) {
printf("SIGQUIT信号被屏蔽了!\n");
} else {
printf("SIGQUIT信号没有被屏蔽, 您可以发送SIGQUIT信号了, 我要sleep(10)秒钟!!!!!!\n");
int mysl = sleep(10);
if(mysl > 0)
{
printf("sleep还没睡够, 剩余%d秒\n", mysl);
}
}
printf("program exit! bye!\n");
return 0;
}
修改程序
nginx.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <stdlib.h>
//信号处理函数
void sig_quit(int signo) {
printf("received SIGQUIT signal!\n");
if(signal(SIGQUIT, SIG_DFL) == SIG_ERR) {
printf("无法为SIGQUIT信号设置缺省处理!\n");
exit(1);
}
}
int main(int argc, char const *argv[])
{
/* code */
//定义信号集:新的信号集, 原有的信号集
sigset_t newmask, oldmask, pendmask;
//注册信号对应的信号处理函数
if(signal(SIGQUIT, sig_quit) == SIG_ERR) {
printf("can not capture SIGUSR1 signal!\n");
//退出程序, 参数是错误代码, 0表示正常退出, 非0表示错误, 但具体什么错误, 没有特别规定
exit(1);
}
//newmask信号集中所有信号都清0(表示这些信号都没有来)
sigemptyset(&newmask);
//设置newmask信号集中的SIGQUIT信号位为1, 再来SIGQUIT信号时, 进程就收不到, 设置为1就是该信号被阻塞掉
sigaddset(&newmask, SIGQUIT);
//设置该进程所对应的信号集
//第一个参数用了SIG_BLOCK表明设置进程新的信号屏蔽字为 当前信号屏蔽字 和 第二个参数指向的信号集的并集
//一个 "进程" 的当前信号屏蔽字,刚开始全部都是0的; 相当于把当前 "进程"的信号屏蔽字设置成 newmask(屏蔽了SIGQUIT);
//第三个参数不为空,则进程老的(调用本sigprocmask()之前的)信号集会保存到第三个参数里,用于后续,这样后续可以恢复老的信号集给进程
if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {
perror("sigprocmask(SIG_BLOCK)");
exit(1);
}
printf("I begin sleep 10s---------------begin---------------, during this time, I can't receive SIGQUIT signal!\n");
sleep(10);
printf("I've slept 10s---------------end---------------!\n");
//测试一个指定的信号位是否被置位,这里测试的是newmask
if(sigismember(&newmask, SIGQUIT)) {
printf("SIGQUIT signal has been shielded!\n");
} else {
printf("SIGQUIT signal has not been shielded!\n");
}
//测试另外一个指定的信号位是否被置位,测试的是newmask
if(sigismember(&newmask, SIGHUP)) {
printf("SIGHUP信号被屏蔽了!\n");
} else {
printf("SIGHUP信号没有被屏蔽!\n");
}
//现在取消对SIGQUIT信号的屏蔽(阻塞)--把信号集还原回去
//第一个参数用了SIGSETMASK表明设置进程新的信号屏蔽字为 第二个参数 指向的信号集,第三个参数没用
if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) {
perror("sigprocmask(SIG_SETMASK)");
exit(1);
}
printf("sigprocmask(SIG_SETMASK)成功!\n");
if(sigismember(&oldmask, SIGQUIT)) {
printf("SIGQUIT信号被屏蔽了!\n");
} else {
printf("SIGQUIT信号没有被屏蔽, 您可以发送SIGQUIT信号了, 我要sleep(10)秒钟!!!!!!\n");
int mysl = sleep(10);
if(mysl > 0)
{
printf("sleep还没睡够, 剩余%d秒\n", mysl);
}
}
printf("program exit! bye!\n");
return 0;
}
fork函数详解、范例演示
简单认识fork函数
一个可执行程序执行1次就是1个进程,再执行1次就又是1个进程(多个进程共享同一个可执行文件)。换句话说,进程一般定义为程序执行的一个实例。
在一个进程中可以使用fork创建一个子进程,当该子进程创建时,它从fork函数的下一条语句(或者说从fork的返回处)开始执行与父进程相同的代码。换句话说,fork函数产生一个和当前进程完全一样的新进程,并和当前进程一样从fork函数调用中返回
nginx.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/types.h>
//信号处理函数
void sig_usr(int signo) {
printf("收到了SIGUSR1信号, 进程id = %d !\n", getpid());
}
int main(int argc, char const *argv[])
{
/* code */
pid_t pid;
printf("program begin execute!\n");
//先简单处理一个信号
//系统函数,参数1:是个信号,参数2:是个函数指针,代表一个针对该信号的捕捉处理函数
if(signal(SIGUSR1, sig_usr) == SIG_ERR) {
perror("signal");
exit(1);
}
//创建一个子进程
pid = fork();
if(pid < 0) {
perror("fork");
exit(1);
}
//现在,父进程和子进程同时开始运行了
for(;;) {
sleep(1);
printf("休息1秒, 进程id = %d !\n", getpid());
}
printf("program exit! bye!\n");
return 0;
}
可以注意到,进程ID为23456的父进程ID是23455,说明是23455这个进程调用fork函数创建的23456进程。另外注意这2个进程的状态都是S+。S是休眠,因为程序中大部分时间执行的是sleep,所以休眠是正常的;+表示进程位于前台进程组
但这里要注意一点,调用fork函数创建出一个子进程后,后续的代码是父进程先执行还是子进程先执行并不确定,不代表父进程一定快,因为存在进程的时间片调度问题(这与内核调度算法有关)。
ubuntu@VM-20-6-ubuntu:~$ kill -usr1 23455
ubuntu@VM-20-6-ubuntu:~$ kill -usr1 23456
从上面的结果中可以看到,父进程和子进程都能收到这个信号,说明信号捕捉这段代码,是子进程和父进程的公共代码(对父进程和子进程都有效,或者说这段代码既在父进程中,也在子进程中——虽然子进程是后面用fork函数创建出来的,但在子进程创建出来之前父进程执行的所有代码都相当于在子进程中执行过了)。
监视一下父进程收到的各种信号
ubuntu@VM-20-6-ubuntu:~$ sudo strace -e trace=signal -p 23455
子进程被杀死后,父进程收到了SIGCHLD信号。另外,子进程被杀死后,nginx进程输出窗口显示内容如下,不再显示子进程的输出信息“休息1s,进程id = 23456 !”了
ubuntu@VM-20-6-ubuntu:~$ ps -eo pid,ppid,sid,tty,pgrp,comm,cmd,stat | grep -E 'bash|PID|nginx'
可以看到,被杀掉的23456子进程仍旧存在于ps命令的列表中,但其COMMAND列显示defunct(翻译成中文是“失效”的意思),而STAT列显示Z+(Z状态表示僵尸进程)。总之,无论是Z状态,还是defunct字样,都是僵尸进程的典型标记。
僵尸进程的产生、解决,SIGCHILD
在类UNIX操作系统中,如果一个子进程终止了,但父进程还活着,但该父进程没有调用(wait / waitpid)函数来进行一些额外处置(处置子进程终止这件事),那么这个子进程将会变成一个僵尸进程。
这种僵尸进程已经被终止了,不工作了,但是依旧没被内核丢弃,因为内核认为父进程可能还需要该子进程的一些信息。
僵尸进程是占用资源的,至少会占用进程ID(PID)。整个操作系统的进程ID号是有限的,所以,作为开发者不应该允许僵尸进程的存在
怎样让僵尸进程消失呢?①重启计算机;②手动把这个僵尸进程的父进程杀掉,这个僵尸进程会自动消失
看起来这2个都不是好办法,相信实际工作中,没有谁会这样做。实际上,应该从写代码的角度来避免僵尸进程的产生。
可以看到,当子进程被杀掉时,父进程收到了一个SIGCHLD信号。这个信号是,一个进程终止或者停止时,这个信号会被发送给该进程的父进程。
对于源码中有fork行为(会创建子进程)的进程,程序员应该拦截并处理SIGCHLD信号。
waitpid函数
完善后的程序
nginx.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
//信号处理函数
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;
}
//这表示这个waitpid调用有错误,有错误也立即返回
if(pid == -1) {
return;
}
//走到这里,表示成功,那也return吧
return;
break;
}
}
}
int main(int argc, char const *argv[])
{
/* code */
pid_t pid;
printf("program begin execute!\n");
//先简单处理一个信号
//系统函数,参数1:是个信号,参数2:是个函数指针,代表一个针对该信号的捕捉处理函数
if(signal(SIGUSR1, sig_usr) == SIG_ERR) {
perror("signal(SIGUSR1)");
exit(1);
}
if(signal(SIGCHLD,sig_usr) == SIG_ERR)
{
perror("signal(SIGCHLD)");
exit(1);
}
//创建一个子进程
pid = fork();
if(pid < 0) {
perror("fork");
exit(1);
}
//现在,父进程和子进程同时开始运行了
for(;;) {
sleep(1);
printf("休息1秒, 进程id = %d !\n", getpid());
}
printf("program exit! bye!\n");
return 0;
}
重复前面的步骤,把子进程杀掉并用ps查看进程列表
这次可以看到,子进程被彻底杀掉了,消失不见了,而不再是僵尸进程。同时,在进程输出窗口,也看到了父进程收到了子进程发来的SIGCHLD信号
进一步认识fork函数
fork产生新进程的速度非常快,产生的新进程并不复制原进程的内存空间,而是和原进程(父进程)一起共享一个内存空间。这个内存空间的特性是“写时复制”,也就是说,原来的进程和fork出来的子进程可以同时自由读取内存,但如果子进程(或者父进程)对内存进行修改,这个内存就会复制一份给该进程单独使用,以免影响共享该内存空间的其他进程的使用。就是因为这种特性,fork的执行效率才能这么高。
sysconf(_SC_CHILD_MAX)
nginx2.c
#include <stdio.h>
#include <stdlib.h> //malloc,exit
#include <unistd.h> //fork
#include <signal.h>
int main(int argc, char const *argv[])
{
/* code */
printf("每个用户允许创建的最大进程数 = %ld\n", sysconf(_SC_CHILD_MAX));
fork(); //一般fork都会成功所以不判断返回值了
fork();
for(;;)
{
sleep(1); //休息1秒
printf("休息1秒, 进程id = %d !\n", getpid());
}
printf("再见了!\n");
return 0;
}
完善fork代码
回顾nginx.c源码,在其中做一个基本的fork调用。调用fork函数,就可以创建一个子进程,子进程的进程ID一般都紧邻主进程,如果主进程的进程ID是1000,子进程的进程ID就可能是1001。现在就相当于有2个进程,一个进程ID为1000,另一个进程ID为1001,进程ID为1001的进程是进程ID为1000进程的子进程。最终,fork的执行结果就是1条执行路线变成了2条
nginx3.c
#include <stdio.h>
#include <stdlib.h> //malloc,exit
#include <unistd.h> //fork
#include <signal.h>
int g_mygbltest = 0;
int main(int argc, char const *argv[])
{
/* code */
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);
printf("真是太高兴了, 我是子进程, 我的进程id = %d, g_mygbltest = %d !\n",getpid(), g_mygbltest);
}
} else {
//这里就是父进程, 因为父进程的fork()返回值会 > 0
//这是专门针对父进程的处理代码
while(1)
{
g_mygbltest++;
sleep(5); //休息5秒
printf("......, 我是父进程, 我的进程id = %d, g_mygbltest = %d!\n", getpid(), g_mygbltest);
}
}
return 0;
}
注意观察上面的结果,重点关注g_mygbltest全局变量的值,可以看到,父进程和子进程的该全局变量的值不同,每个进程都是单独计数的
fork对于子进程,返回值为0;对于父进程,返回值是新建立的子进程的ID。
父进程和子进程的全局量mygbltest值也不同,每个进程都有不同的值,因为这2个进程都有写的动作(改写全局量g_mygbltest的值,也就是改写内存),内核会给每个进程单独分配一块内存供其单独使用,所以每个进程的mygbltest值互不干扰。
一个和fork执行有关的逻辑判断
nginx4.c
#include <stdio.h>
#include <stdlib.h> //malloc,exit
#include <unistd.h> //fork
#include <signal.h>
int main(int argc, char const *argv[])
{
/* code */
((fork() && fork()) || (fork() && fork()));
for(;;)
{
sleep(1); //休息1秒
printf("休息1秒, 进程id=%d!\n",getpid());
}
printf("再见了!\n");
return 0;
}
ubuntu@VM-20-6-ubuntu:~$ ps -eo pid,ppid,sid,tty,pgrp,comm,cmd,stat | grep -E 'bash|PID|nginx'
fork失败的可能原因
- (1)系统中进程太多:
系统肯定出了问题,如僵尸进程太多。整个系统中,使用ps命令列出进程时看到的进程ID(PID)是有限的,创建子进程时一般子进程ID值比父进程ID值大1,进程ID是可以复用的,例如某个进程运行结束(终止)之后,过一段时间,操作系统又会把这个进程的ID分配给其他的新创建的进程使用(循环使用)。
默认情况下,最大的进程ID值一般是32767,如果0~32767这些数字全都被占用,fork 就会失败,当然,这是一种比较极端的情况。 - (2)创建的进程数超过了当前用户允许创建的最大进程数。
每个用户会有一个允许开启的进程总数。可以在文件nginx3_6_2.c中加入如下代码:
守护进程详解、范例演示
守护进程基本概念
ubuntu@VM-20-6-ubuntu:~/myProj$ ps -efj
其中:e表示显示所有进程;f表示完整格式信息;j表示显示与任务或者作业有关的信息
- (1)PPID为0的是内核进程,跟随系统启动而启动,生命周期也是贯穿整个系统。它们不与终端挂钩,属于超级用户特权进程。
- (2)注意CMD这列的名字,很多带中括号,这种叫内核守护进程。其中kthreadd这个内核守护进程很特殊,因为其他内核守护进程都是由这个进程创建的,所以其他内核守护进程的PPD都是2,即这些进程的父进程都是kthreadd
- (3)老祖宗进程init,是系统守护进程,负责启动各运行层次特定的系统服务。所以很多进程的PPID是init,而且init也负责收养孤儿进程。
- (4)还有很多不带中括号的普通守护进程(用户级守护进程),比如:
①rsyslogd,和系统消息日志有关的进程;
②cron,在某个时间执行命令;
③sshd,提供安全的远程登录(现在运行的Xshell就是连接到这个服务,从而实现远程操作Ubuntu Linux系统)。
一般这些守护进程多数是进程组的组长进程以及会话的首进程,而且是这些进程组和会话中的唯一进程。
守护进程的共同点
(1)大多数守护进程都以超级用户特权运行(因为如权限不够,很多事情就做不了)。
(2)守护进程是没有控制终端的,TTY那列显示的都是“?”。内核守护进程以无控制终端方式启动,普通守护进程可能是守护进程调用了setsid的结果(无控制终端)。
守护进程编写规则
1.调用umask(0);
umask实际是个函数,用来限制(屏蔽)一些文件权限。无须理会太多,只需要知道,正常使用,传递进去的参数就是0,目的是不让它限制文件权限(因为很多时候程序员自己创建的守护进程可能要创建文件),并给这个文件设置一定的权限。设置权限时不能涉及umask,否则很可能屏蔽设置的权限,所以直接记住这种写法:umask(0);
umask函数
2.fork一个子进程出来,然后父进程退出
(1)如果程序员编写的进程是通过命令行(shell)来启动的,那么父进程终止会让shell 以为这条命令执行完毕,会把这个终端空出来,终端就能做其他事。程序在后台运行时,终端就能够解释从键盘输入的命令。
(2)fork一个子进程之后,之前用过fork函数,当时的主要目的是调用setsid函数来建立新会话。主进程(父进程)是进程组的组长,无法成功调用setsid; fork出来的子进程也在该进程组中,但不是组长,所以能够成功调用setsid。
(3)为子进程调用setsid创建新会话,可以发现:
- ①该子进程有了一个单独的SID;
- ②该子进程成了一个新进程组的组长进程;
- ③用ps显示时,终端列(TTY/TT)中显示的是“?”,表示它不关联终端。
文件描述符
文件描述符是0和正整数,用于标识一个文件。每当打开一个已经存在的文件或创建一个新文件,操作系统都会返回文件描述符(用来代表这个文件);后续对该文件操作的函数,也都会用文件描述符作为参数。
有3个特殊的文件描述符,数字分别为0、1、2
①0。标准输入(键盘输入),对应的符号常量是STDIN_FILENO。
②1。标准输出(屏幕显示)对应的符号常量是STDOUT_FILENO。
③2。标准错误(一般也是屏幕显示),对应的符号常量是STDERR_FILENO。
换句话说,类UNIX操作系统默认从STDIN_FILENO读数据,向STDOUT_FILENO 写数据,向STDERR_FILENO写错误信息。
类Unix.操作系统有一个说法叫“一切皆文件”——它把标准输入、标准输出、标准错误都看成是文件。
程序开始运行时,这3个文件描述符(0、1、2)就会被自动打开(自动指向对应的设备)。文件描述符虽然是个数字,但可以将其理解成指针(指针里面保存地址——地址本身就是个数字)
输入输出重定向
输出重定向
输入输出重定向就是标准输出文件描述符,不指向屏幕了。假如重定向到一个文件(指向一个文件),那么结果就会输出到文件中去(而不是显示在屏幕上)
举例说明。正常情况下,输入ls-la命令,结果会显示在屏幕上
现在,如果把输出从默认的屏幕重定向到文件中,那么ls-la的执行结果就会保存到文件中。看看下面的命令,使用“>”符号(大于号/重定向符)把ls-la命令的执行结果重定向到文件中(注意重定向符是“>”,不要记错,尖的方向指向哪里就表示重定向到哪里),输入ls-la> myoutfile(用cat命令显示文件内容)
ubuntu@VM-20-6-ubuntu:~/myProj$ ls -lha > myoutfile
输入重定向
从键盘读入数据。用键盘输人一个字母,就等于读入了一个字母
输入重定向的应用场景可以如下:有一个文件,里面有20个字符,如果把输入重定向到文件,就相当于把这20个字符从文件中读到文件描述符0(取代从键盘输入20个字符到文件描述符0)
以cat命令为例。输入cat myoutfile,将在屏幕上显示myoutfile文件的内容。如果单独执行cat命令,会发现用键盘输入一个字符,按回车键,就直接在屏幕上输出该字符。如果不从键盘输入,而是把一个文件中的内容作为输入,这就叫输入重定向。这里把刚才的myoutfile文件内容作为输入。标准输入重定向用“<”(小于号)
ubuntu@VM-20-6-ubuntu:~/myProj$ cat < myoutfile
这个cat命令中多了一个“<”,意思是从文件myoutfile中读取内容(而不再是从键盘输人),然后将读取的内容显示到标准输出(屏幕上)。所以,表面看起来输出结果和cat myoutfile命令一样,但意思却是完全不同的
ubuntu@VM-20-6-ubuntu:~/myProj$ cat < myoutfile > myoutfile2
这行命令的意思是将标准输人重定向到myoutfile(表示从这个文件中读取内容),读取的内容不显示到屏幕上,而是将输出重定向到myoutfile2文件。执行后,将会得到文件myoutfile2,而myoutfile2的内容将和myoutfile的内容相同
空设备
“/dev/null”称为空设备(黑洞设备),是一个特殊的设备文件,它丢弃(吞噬)一切写入其中的数据(像个黑洞一样)。
当输入命令cat myoutfile来正常显示一个文件时,可以看到文件的内容输出到了屏幕上。但如果输入命令cat nginx.c > /dev/null将输出重定向到/dev/null,执行后就会发现没有任何输出显示,这是因为/dev/null(空设备)把输出吞噬了。
守护进程规则
守护进程,是脱离终端运行的,和终端不挂钩(尽管可以通过终端来运行该守护进程)。守护进程是在后台运行的,它不应该从键盘上接收任何内容,也不应该把结果输出到屏幕上。
所以按照规则,程序员应该把标准输入和标准输出重定向到/dev/null这个空设备上,以确保守护进程不从键盘上接收任何内容,也不把输出结果输出到屏幕上。即使在守护进程中调用了printf这种打印输出语句,因为标准输出重定向到了/dev/null,执行printf也没有效果。
换句话说,把标准输入和标准输出重定向到/dev/null这个空设备上去的目的,就是不希望通过某个终端登录Ubuntu Linux虚拟机后,突然发现某个守护进程竟然输出了一些信息到终端窗口中,这是比较诡异的事情。同理,程序员也不会希望通过某个终端输入几个字符会被守护进程读取,这同样是比较诡异的事情。
dup2函数
守护进程实现范例
nginx3.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>
//创建守护进程
int ngx_daemon()
{
int fd;
switch(fork()){
case -1:
//创建子进程失败, 这里可以写日志......
return -1;
case 0:
//子进程, 走到这里, 直接break;
break;
default:
//父进程, 直接退出
exit(0);
}
//只有子进程流程才能走到这里
if (setsid() == -1) //脱离终端, 终端关闭, 将跟此子进程无关
{
//记录错误日志......
return -1;
}
umask(0); //设置为0, 不要让它来限制文件权限, 以免引起混乱
fd = open("/dev/null", O_RDWR); //打开黑洞设备, 以读写方式打开
if (fd == -1)
{
//记录错误日志......
return -1;
}
//先关闭STDIN_FILENO[这是规矩, 已经打开的描述符, 动他之前, 先close], 类似于指针指向null, 让/dev/null成为标准输入
if (dup2(fd, STDIN_FILENO) == -1)
{
//记录错误日志......
return -1;
}
//先关闭STDIN_FILENO, 类似于指针指向null, 让/dev/null成为标准输出
if (dup2(fd, STDOUT_FILENO) == -1)
{
//记录错误日志......
return -1;
}
//fd应该是3, 这个应该成立
if (fd > STDERR_FILENO)
{
//释放资源这样这个文件描述符就可以被复用;不然这个数字(文件描述符)会被一直占着
if (close(fd) == -1)
{
//记录错误日志......
return -1;
}
}
return 1;
}
int main(int argc, char const *argv[])
{
/* code */
if(ngx_daemon() != 1)
{
//创建守护进程失败, 可以做失败后的处理比如写日志等等
return 1;
}
else
{
//创建守护进程成功,执行守护进程中要干的活
for(;;)
{
sleep(1);
//即使打印也没用, 现在标准输出指向黑洞(/dev/null), 打印不出任何结果(不显示任何结果)
printf("休息1秒, 进程id = %d !\n", getpid());
}
}
return 0;
}
进程执行后,就出现了“$”提示符,整个终端不会被占用,可以在终端上继续执行其他命令。
可以看到,守护进程nginx3 正在运行,其PPID是1,说明它是孤儿进程,被init老祖宗进程收养。TT类显示的是“?”,表示该进程不与任何终端关联。stat列中的S表示休眠中(sleep语句所致),s表示session leader(会话首进程)。
启动官方Nginx可执行程序,然后使用ps命令观察其中的master进程,ps命令如下:
ubuntu@VM-20-6-ubuntu:/usr/local/nginx/sbin$ ps -eo pid,ppid,sid,tty,pgrp,comm,cmd,stat | grep -E 'bash|PID|nginx'
master进程,与这里自己写的守护进程一样,所以官方Nginx中的master进程也是以守护进程的方式启动的。
可以看到,守护进程在这里是以命令行的方式启动的。如果希望开机就启动,可以借助系统初始化脚本来启动。一般情况下,守护进程多通过系统初始化脚本启动,因为守护进程一般都是开机启动,这样一开始就可以拥有超级用户特权。
守护进程不会收到的信号
某个进程收到的信号,要么是内核发出来的,要么是另外的进程发出来的。
1、SIGHUP信号
首先,守护进程不会收到来自内核的SIGHUP信号,潜台词就是如果守护进程收到了SIGHUP信号,肯定是进程发送过来的。
SIGHUP信号,是“连接(终端)断开”信号。如果终端检测到一个连接断开,会发送此信号到该终端所在的会话首进程。
守护进程和终端并不关联,所以正常情况下,守护进程是不会收到SIGHUP信号的。这个信号对于守护进程来说,就可以挪作他用。有些守护进程把该信号作为一个通知,表示配置文件已经发生改动,守护进程应该重新读入配置文件。
官方Nginx里,当配置文件修改时,可以执行sudo ./nginx -s reload命令,该命令的作用就向已经启动的master进程发送一个SIGHUP信号(与执行“sudo kill -1进程ID”效果一样),然后master进程就会有一系列动作。这里可以实际操作一下,先启动官方的Nginx,再次执行sudo ./nginx -s reload命令,会发现master进程把旧的worker进程全部杀掉,然后创建新的worker进程来适应新配置文件
2、SIGINT、SIGWINCH信号
守护进程也不会收到来自内核的SIGINT信号,潜台词就是如果守护进程收到了SIGINT信号,同样肯定是进程发送过来的。
SIGINT是终端中断符,按Ctrl+C组合键(中断键)就产生该信号。
另外还有SIGWINCH信号,这是个“终端窗口大小改变”信号,守护进程与该信号也没有关系,同样不会收到内核发出的这种信号
写守护进程时,这些信号都可以拿来自用,作为和守护进程通信的手段。
守护进程和后台进程的区别
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!