《C++新经典Linux C++通信架构实战》第3章 Nginx开发初步
- 3.1 学习Nginx源码前的准备工作
- 3.1.1 Nginx源码总述
- 3.1.2 Nginx源码查看工具
- 3.1.3 Nginx源码入口函数定位
- 3.1.4 创建Linux下C语言程序
- 3.1.c
- 3.2 Nginx源码学法,终端和进程的关系
- 3.2.1 Nginx源码学习方法
- 3.2.2 终端和进程的关系
- 3.2.c
- 3.3.c
- 3.4.c
- 3.5.c
- 3.3 信号的概念、认识、处理动作
- 3.3.1 信号的基本概念
- 3.3.2 通过kill命令认识一些信号
- 3.3.3 进程状态
- 3.3.4 常用信号
- 3.3.5 信号处理的相关动作
- 3.4 UNIX/Linux体系结构、信号编程初步
- 3.4.1 UNIX/Linux操作系统体系结构
- 3.4.2 signal函数
- 3.5 信号编程进阶
- 3.5.1 信号集(信号屏蔽字)
- 3.5.2 信号相关函数
- 3.5.3 sigprocmask等信号函数
- 3.6 fork函数
- 3.6.1 简单认识
- 3.6.2 进一步认识
- 3.6.3 完善fork代码
- 3.6.4 fork执行相关的逻辑判断
- 3.6.5 fork失败的可能原因
- 3.7 守护进程
- 3.7.1 普通进程
- 3.7.2 守护进程基本概念
- 3.7.3 守护进程编写规则
- 3.7.4 守护进程不会收到的信号
- 3.7.5 守护进程与后台进程的区别
3.1 学习Nginx源码前的准备工作
3.1.1 Nginx源码总述
src目录,源代码目录1. core,核心代码2. event,event(事件)模块相关代码3. http,http(Web服务)模块相关代码4. mail,邮件模块相关代码5. os,操作系统相关代码6. stream,流处理相关代码
3.1.2 Nginx源码查看工具
Visual Studio Code
3.1.3 Nginx源码入口函数定位
搜索“main(”
3.1.4 创建Linux下C语言程序
3.1.c
#include int main(int argc, char *const *argv){printf("study together!\n");return 0;}# gcc 3.1.c -o 3.1 && ./3.1
Linux下文件权限符”drwxrwxrwx”
- d表示目录
- read(
r
), write(w
), execute(x
) - 3个字符一组,共3组,表示当前用户权限,同组用户权限,其他用户权限
- 目录权限,r能被浏览(ls);w能新建、删除、修改、移动目录内的文件;x进入目录权限(cd)
3.2 Nginx源码学法,终端和进程的关系
3.2.1 Nginx源码学习方法
泛读Nginx中重要代码,提取使用。
3.2.2 终端和进程的关系
- 终端与bash进程
每个虚拟终端(pts)连到虚拟机,都会打开一个bash进程(shell壳)。输入命令黑窗就是bash(shell,命令行解释器是个可执行程序),用于解释用户输入的命令。
ps -ef | grep bashwhereis bash
- 终端上开启进程
3.2.c
#include #include int main(int argc, char *const *argv){printf("开始!\n");while(1){sleep(1);printf("休息1秒!\n");}printf("退出!\n");return 0;}
gcc -o 3.2 3.2.c && ./3.2ps -la#l,长显示输出格式#a,显示终端上的所有进程,包括其他终端上的进程
随着终端退出,该终端上运行的进程(nginx)也退出了。
init进程(操作系统启动时内核自己创建出来的,具有超级用户特权),进程ID(PID)是1。
- init进程(初始进程)
—fork➡
- getty进程(打开终端设备【文件描述符0,1,2】,读取用户和环境信息)
—exec➡
- login进程/sshd远程登录服务进程(处理登录信息:虚拟机用终端tty登录通过login进程处理,XShell登录通过sshd远程登录服务进程处理)
—fork➡
- bash(shell)进程(执行bash进程)
—fork➡
- cp、ps、ls、rm、mkdir、vim、nginx等进程(执行各种用户命令、用户进程)
- 进程关系进一步分析
- 会话(session),进程组的集合。
- 进程组(分组方便管理,例如给整个组发消息),进程的集合。
- 进程组中各个进程,可以独立接收来自终端的各种信号。
进程ID(PID),父进程ID(PPID),进程组ID(PGRP,与进程组ID相同的进程叫进程组组长),会话ID(SID,等于会话首进程ID)。
通常一个bash(shell)上所有进程都属于同一个会话。
会话首进程(session leader,创建会话的进程),通常是这个shell(bash)。
ps -eo pid,ppid,sid,tty,pgrp,comm | grep -E 'bash|PID|nginx'#e,所有进程#o,指定显示列#sid,session id;tty,终端;pgrp,进程组;comm,执行的命令。#-E,开启扩展正则表达式,'bash|PID|nginx'中‘|’表示或者
Xshell终端断开,系统会给会话首进程(这个bash进程)发送SIGHUP信号(终端断开),bash进程收到SIGHUP信号后,将这个信号发给session里的所有进程(最后发给自己),收到SIGHUP信号的进程默认动作就是退出。
- strace工具的使用
调试分析诊断工具,跟踪程序执行时进程的系统调用(system call)和所接收到的信号。
sudo strace -e trace=signal -p 1157#si_pid,发送信号者(谁发来的信号)#kill,发送信号
跟踪PID1157的进程上与信号(signal)有关的系统调用。
int abc = 4294966139;
等价于
int abc = -1157;
kill -1157(负值表示给进程组发送信号)表示给进程组1157发送信号。
kill sid(通常会话id和bash进程id一致),bash收到SIGHUP信号时,先将该信号发给session内其它进程(各个进程组内进程,只要session ID相同),然后将SIGHUP信号发送给自己。
- 终端关闭时不退出进程
gcc nginx.c -o nginxps -eo pid,ppid,sid,tty,pgrp,comm | grep -E 'bash|PID|nginx'ps -ef | grep -E 'bash|PID|nginx'
(1)拦截SIGHUP信号
3.3.c
#include #include #include int main(int argc, char *const *argv){signal(SIGHUP, SIG_IGN);//ignore SIGUPprintf("begin!\n");while(1){sleep(1);printf("sleep 1s!\n");}printf("end!\n");return 0;}
gcc 3.3.c -o 3.3 && ./3.3ps -ef | grep '3\.3'
终端(bash)断开后,该进程TT变为?(孤儿进程),PPID变为1(init进程收养)。
(2)进程和终端bash进程在不同session中
3.4.c
#include #include #include int main(int argc, char *const *argv) {setsid();printf("begin!\n");while(1){sleep(1);printf("sleep 1s!\n");}printf("end!\n");return 0;}
进程组组长调用setsid()无效。
gcc 3.4.c -o 3.4 && ./3.4ps -ef | grep '3\.4'
3.5.c
#include #include #include int main(int argc, char *const *argv) {printf("开始!\n");pid_t pid = fork();if(pid<0){printf("fork进程出错!\n");} else if(pid==0) { //子进程setsid();//子进程调用setsid()有效,TT变为?。while(1) {sleep(1);printf("子进程休息1s!\n");//注意即使TT为?,当前终端(关联终端)仍会输出}} else {//父进程//setsid();//父进程,进程组组长,调用setsid()无效。while(1) {sleep(1);printf("父进程休息1s!\n");}}printf("退出!\n");return 0;}
gcc 3.5.c -o 3.5 && ./3.5ps -ef | grep '3\.5'
关闭当前终端后,父进程退出,子进程仍运行,变成孤儿进程。
(3)setsid命令启动进程
使启动的进程在新的session中,终端关闭时不会退出。
nginx.c
#include #include #include int main(int argc, char *const *argv){printf("begin!\n");while(1){sleep(1);printf("sleep 1s!\n");}printf("end!\n");return 0;}
gcc nginx.c -o nginxsetsid ./nginx
PPID为1,TT为?。
(4)nohup(no hang up不挂断)
nohup命令启动的程序会忽略SIGHUP信号,与程序内编写忽略SIGHUP信号等价。
nohup ./nginx
nohup命令将输出重定向到当前目录的nohup.out文件中。
关闭终端后,进程PPID为1,TT为?。
- 后台执行(运行)
./nginx &#fg,切换当前后台进程到前台#fg jobnumber(是命令编号),选定进程#bg jobnumber,后台暂停进程后台继续执行#jobs,查看后台进程#kill %jobnumber(jobs)#kill pid(ps)#Ctrl+C,终止前台#Ctrl+Z,前台变后台且暂停#nohup ./nginx &,后台运行且终端断开进程不退出
后台执行,终端能同时做其他事情;
Ctrl+C(中断键)能停止前台进程,无法终止后台进程;
终端连接断开,后台进程被关闭(退出)。
3.3 信号的概念、认识、处理动作
3.3.1 信号的基本概念
信号(突然事件,异步发生,“软件中断”),就是一个通知(事件通知),用来通知进程发生了事件。
信号产生:
进程发送给其他进程或自己
ngnix热升级启动新master进程时,向旧msater进程发送信号。内核发送给进程
①键盘输入命令动作,Ctrl+C(中断信号);kill命令。
②内存访问异常。除数为0,硬件检测到并通知内核。
信号名,SIG开头(宏定义),数字(1开始编号),signal.h。
sudo find / -name "signal.h" | xargs grep -in "SIGHUP"#xargs,打开文件找#i,忽略大小写#n,显示行号
3.3.2 通过kill命令认识一些信号
向进程发送信号。
绝大多数信号的默认动作,是终止收到信号的进程。
kill pid(默认-15),向进程发送SIGTERM终止信号
kill -1 pid,向进程发送SIGHUP挂断信号
kill -2 pid,向进程发送SIGINT中断信号
kill参数 | 该参数发出信号 | 内核默认动作 |
---|---|---|
-1 | SIGHUP(连接断开) | 终止掉进程(进程没了) |
-2 | SIGINT(终端中断符,Ctrl+C) | 终止掉进程(进程没了) |
-3 | SIGQUIT(终端退出符,Ctrl+\) | 终止掉进程(进程没了) |
-9 | SIGHUPKILL(终止) | 终止掉进程(进程没了) |
-18 | SIGCONT(暂停进程继续) | 忽略(进程运行不受影响) |
-19 | SIGSTOP(停止),可用SIGCONT继续,但任务被放到了后台 | 停止进程(不是终止,进程还在) |
-20 | SIGSTP(终端停止符,Ctrl+Z),可用SIGCONT继续,但任务被放到了后台 | 停止进程(不是终止,进程还在) |
3.3.3 进程状态
ps -eo pid,ppid,sid,tty,pgrp,comm,stat | grep -E "bash|PID|nginx"# stat,查看进程状态ps -ef| grep -E "bash|PID|nginx"ps aux| grep -E "bash|PID|nginx"#aux,BSD风格的显示格式
进程状态 | 含义 |
---|---|
D | 不可中断的休眠状态(通常是I/O的进程),可以处理信号,有延迟 |
R | 可执行状态 & 运行状态(在运行队列里的状态) |
S | 可中断的休眠状态之中(等待某事件完成),可以处理信号 |
T | 停止或被追踪(被作业控制信号锁停止) |
Z | 僵尸进程 |
X | 死掉的进程 |
< | 高优先级进程 |
N | 低优先级进程 |
L | 有些页被锁进内存 |
s | session leader,其下有子进程 |
t | 追踪期间被调试器停止 |
+ | 位于前台的进程组 |
3.3.4 常用信号
信号名称 | 信号含义 |
---|---|
SIGHUP(连接断开) | 终端断开信号。Xshell断开发送,-1,发送到终端所在的会话首进程。 |
SIGALRM(定时器超时) | alarm创建定时器,超时产生。 |
SIGINT(中断) | Ctrl+C,终止进程。后台进程忽略。 |
SIGSEGV(无效内存) | SEGV,段违例(segmentation violation),内存访问异常,除数为0等,硬件检测到并通知内核。 |
SIGIO(异步I/O) | 通信套接口有数据到达或发生异步错误,内核向进程通知该信号。 |
SIGCHILD(子进程改变) | 进程终止或停止,该信号发送给父进程。 |
SIGUSR1、SIGUSR2(用户定义信号) | 用于应用程序。 |
SIGTERM(终止) | kill pid产生,退出前处理工作,优雅退出。 |
SIGKILL(终止) | 不能被忽略,不能被进程本身捕捉,杀死任意进程。 |
SIGSTOP(停止) | 不能被忽略,不能被进程本身捕捉,停止执行进程。 SIGCONT信号继续执行,但会放入后台。 |
SIGQUIT(终端退出符) | Ctrl+\,shell会将后台进程对该信号的处理设置为忽略(后台进程不会收到该信号)。 |
SIGCONT(使暂停进程继续) | 继续运行暂停的进程。 |
SIGTSTP(终端停止符) | Ctrl+Z,进程被停止且放入后台,可以用SIGCONT继续执行。 |
3.3.5 信号处理的相关动作
执行系统默认动作
绝大多数情况,内核(操作系统)会杀死(或暂停)进程。忽略此信号
2个信号(特权信号)不能忽略,SIGKILL(-9,终止)和SIGSTOP(-19,暂停)。
signal(SIGHUP, SIG_IGN);
- 捕捉该信号
写信号处理函数,当系统收到该信号时,会自动调用信号处理函数。
2个信号(特权信号)不能被捕捉,SIGKILL和SIGSTOP。
3.4 UNIX/Linux体系结构、信号编程初步
3.4.1 UNIX/Linux操作系统体系结构
(1)操作系统/内核——软件。用于控制计算机硬件资源,提供应用程序运行的环境。程序,要么运行在用户态,要么运行在内核态。一般情况下运行在用户态,程序执行特殊代码时,切换到内核态(操作系统控制)。
应用程序通过shell、公共函数库进行系统调用(内核的对外接口,访问磁盘,内存等资源)。
(2)系统调用函数库,系统内部高度封装,调用即可。
(3)bash(borne again shell),shell的一种,命令行解释器。位于系统调用和应用程序之间,分隔系统调用和应用程序,胶水(将系统调用和应用程序粘在一起)作用。
whereis bashpsexit
操作系统内核部分有进程管理(CPU)、内存管理(RAM)、文件系统(磁盘/存储设备)、设备驱动(各种终端设备)、网络(网卡)等各种资源的管理。操作系统内核和用户空间(应用程序),通过系统调用(system call)接口实现对各种资源功能的调用。
- (4)用户态内核态之间的切换
进程大部分时间处于用户态下,需要内核提供服务才切换到内核态,内核态任务处理完成后,又切换回用户态。
分用户态,内核态的目的:
①用户态权限小,内核态权限大。权限大,能做危险事情(处理内存、处理时钟等),交由内核态处理,避免系统处于危险状态甚至崩溃,只提供调用接口。
②系统提供并统一管理调用接口。系统资源有限,避免发生访问冲突,资源耗尽等导致系统崩溃,系统接口用于减少有限资源访问和使用的冲突。
用户态切换到内核态时间:
①系统调用。malloc
②异常事件。收到信号,调用信号处理函数,系统跳到内核态做一些调用该信号处理函数的准备工作
③外围设备的中断。外围设备完成用户的请求操作后,会向CPU发出中断信号,此时CPU就会暂停执行下一条即将执行的指令,转而去执行中断信号对应的处理程序,如果先前执行的指令处于用户态下,就会发生用户态到内核态的转换。
3.4.2 signal函数
忽略或捕捉信号。
#include #include #include void sig_usr(int signo) {if(signo == SIGUSR1) {printf("收到了SIGUSR1信号!\n");} else if(signo == SIGUSR1) {printf("收到了SIGUSR2信号!\n");} else {printf("收到了未捕捉的信号%d!\n", signo);}}int main(int argc, char *const *argv) {printf("开始!\n");if(signal(SIGUSR1, sig_usr) == SIG_ERR) {printf("无法捕捉SIGUSR1信号!\n");}if(signal(SIGUSR2, sig_usr) == SIG_ERR) {printf("无法捕捉SIGUSR2信号!\n");}while(1) {sleep(1);printf("休息1s!\n");}printf("退出!\n");return 0;}
kill -USR1 pidkill -USR2 pid
进程执行过程中,信号到来引发的用户态和内核态之间的切换过程:
- 进程nginx正常执行,突然到来信号导致nginx进程的流程被打断,进程从用户态切换到内核态(做一些调用信号处理函数的准备工作);
- 为了调用注册的信号处理函数sig_usr,又从内核态切换到用户态;
- sig_usr执行完毕后,从用户态切换到内核态(做一些信号处理函数的收尾工作);
- 从内核态切换到用户态,从之前被打断的nginx进程的执行流程处开始继续执行。
#include #include #include int g_mysign = 0;void muNEfunc(int value) {g_mysign = value;}void sig_usr(int signo) {muNEfunc(22);if(signo == SIGUSR1) {printf("收到了SIGUSR1信号!\n");} else if(signo == SIGUSR1) {printf("收到了SIGUSR2信号!\n");} else {printf("收到了未捕捉的信号%d!\n", signo);}}int main(int argc, char *const *argv) {printf("开始!\n");if(signal(SIGUSR1, sig_usr) == SIG_ERR) {printf("无法捕捉SIGUSR1信号!\n");}if(signal(SIGUSR2, sig_usr) == SIG_ERR) {printf("无法捕捉SIGUSR2信号!\n");}while(1) {sleep(1);printf("休息1s!\n");muNEfunc(15);//此时收到SIGUSR1信号会改变g_mysign的值printf("g_mysign=%d\n", g_mysign);}printf("退出!\n");return 0;}
- 可重入函数
可重入的函数(或异步信号安全的函数),指在信号处理函数中调用是安全的函数。
可重入函数(即可以被中断的函数)可以被一个以上的任务调用,而不担心数据破坏。可重入函数在任何时候都可以被中断,而一段时间之后又可以恢复运行,而相应的数据不会破坏或者丢失。
信号处理函数中处理方法:
(1)尽量调用简单语句,尽量不要调用系统函数(malloc,printf等非可重入函数)。
(2)只调用可重入系统函数(搜索Linux可重入函数)。
(3)调用了可能修改errno值的可重入系统函数,先备份errno,信号处理函数返回前恢复errno值。
#include void sig_usr(int signo) {int myerrno = errno;//备份errno//调用可能修改errno值的可重入系统函数errno = myerrno;//还原errno}
- 不可重入函数的错用示例
#include #include #include #include void sig_usr(int signo) {if(signo==SIGUSR1) {printf("SIGUSR1\n");}else if(signo==SIGUSR2) {printf("SIGUSR2\n");}else{printf("Unknown Signal: %d\n", signo);}}int main(int argc, char *const *argv) {if(signal(SIGUSR1, sig_usr) == SIG_ERR) {printf("Catch SIGUSR1 Error\n");}if(signal(SIGUSR2, sig_usr) == SIG_ERR) {printf("Catch SIGUSR2 Error\n");}while(1) {int *p;p = (int *)malloc(sizeof(int));sleep(1);printf("Sleep 1s\n");free(p);}printf("Over\n");return 0;}
kill -USR1 pidkill -USR2 pid
正常运行。
修改sig_usr。
void sig_usr(int signo) {int *p = (int *)malloc(sizeof(int)); //非可重入函数//像printf()和malloc()之类的标准库函数,它们会使用全局的数据(比如malloc()需要使用进程的堆数据结构)是不可重入的,即使加锁也无法解决,加锁可以使得库函数Tread-safe,但是无法解决可重入。//malloc并不是简单的一条指令,内部比如有加锁和开锁功能,main函数中调用malloc函数中刚加锁,然后信号处理函数中断,sig_usr调用malloc,再一次加锁,发生错误。free(p);if(signo==SIGUSR1) {printf("SIGUSR1\n");}else if(signo==SIGUSR2) {printf("SIGUSR2\n");}else{printf("Unknown Signal: %d\n", signo);}}
3.5 信号编程进阶
3.5.1 信号集(信号屏蔽字)
当收到某个信号,启动执行信号处理函数的时候,通常会“屏蔽/阻塞”其后的相同信号,直到信号处理函数执行结束。
进程会记录当前阻塞了哪些信号。收到某个信号时,系统将标记正在处理该信号的标志设置为1,然后去执行信号处理函数,若执行期间再次收到该信号,系统检测到该信号的标志已经为1,后面的相同信号就需要排队等待(等待调用信号处理函数处理)或直接被忽略(丢失)。当信号处理函数执行完毕,再把该信号对应标志设置回0。
sigset_t表示信号集数据类型。
typedef struct { unsigned long sig[2];//8*4*2个信号,来(1)或没来(0)}sigset_t;
3.5.2 信号相关函数
(1)sigemptyset
所有信号清零,所有信号没来。
(2)sigfillset
所有信号设置为1,到来的任何信号都会排队或被忽略。
(3)sigaddset增加信号(设置为1),sigdelset删除信号(设置为0)
(4)sigprocmask设置进程信号集(进程有默认信号集),sigismember检测信号集特定信号是否被置位。
3.5.3 sigprocmask等信号函数
#include #include #include #include //信号处理函数,可被中断//即信号处理函数注册多个信号,当处理第一个信号时,可被第二个信号中断void sig_quit(int signo) {printf("SIGQUIT\n");/*//第二次收到SIGQUIT信号时,采用默认处理,即显示Quit后立即终止。if(signal(SIGQUIT, SIG_DFL) == SIG_ERR) {printf("SIGQUIT Default Handle Error!\n");exit(1);}*/}int main(int argc, char *const *argv) {sigset_t newmask;//新信号集sigset_t oldmask;//旧信号集sigset_t pendmask;//Ctrl+\, SIGQUITif(signal(SIGQUIT, sig_quit) == SIG_ERR) { //注册信号对应的信号处理函数printf("Catch SIGQUIT Error!\n");exit(1);}//newmask信号集中所有信号清零(这些信号都没有来)sigemptyset(&newmask);//newmask信号集中SIGQUIT信号置1,再来SIGQUIT信号进程收不到sigaddset(&newmask, SIGQUIT);//设置进程信号集//SIG_BLOCK表示,设置进程新的信号屏蔽字newmask为进程当前信号屏蔽字(开始全部为0)和newmask的并集(或),(屏蔽了SIGQUIT信号,只有SIGQUIT位为1)//oldmask,保存老的信号集(全部为0,无信号屏蔽),用于恢复。if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {printf("sigprocmask(SIG_BLOCK) Error!\n");exit(1);}printf("sleep 10s--begin--, cannot receive SIGQUIT!\n");sleep(10);printf("sleep 10s--end--, cannot receive SIGQUIT!\n");if(sigismember(&newmask, SIGQUIT)) {printf("SIGQUIT is shielded\n");//SIGQUIT信号被进程屏蔽} else {printf("SIGQUIT is not shielded");}if(sigismember(&newmask, SIGHUP)) {printf("SIGHUP is shielded");} else {printf("SIGHUP is not shielded\n");//}//取消SIGQUIT信号的屏蔽(阻塞)——信号集还原//SIG_SETMASK表示,设置当前进程信号屏蔽字为oldmask(全0,无信号屏蔽)if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) {printf("sigprocmask(SIG_SETMASK) Error!\n");exit(1);}printf("sigprocmask(SIG_SETMASK) Success!\n");if(sigismember(&oldmask, SIGQUIT)) {printf("SIGQUIT is shielded\n");} else {printf("SIGQUIT is not shielded, sleep 10s\n");int mysl = sleep(10);//收到信号会中断休眠,返回剩下时间if(mysl>0)printf("left %ds\n", mysl);}printf("Over\n");return 0;}
运行进程时连续输入5个Ctrl+\(SIGQUIT),进程却只收到1个,说明信号有等待机制(信号SIGQUIT被屏蔽时发出SIGQUIT信号,后续该信号屏蔽被取消时能够收到),但无法允许多个相同的信号排队等待(5个SIGQUIT信号被合并成1个)
3.6 fork函数
3.6.1 简单认识
- 进程概念
多个进程可共享同一个可执行文件(程序),进程一般定义为程序执行的一个实例。fork用于进程中创建子进程,当该子进程创建时,它从fork函数的下一条语句(或者说从fork的返回处)开始执行与父进程相同的代码。fork后父子进程执行的先后顺序并不确定(与内核调度算法有关)。
- 简单范例
#include #include #include #include //信号处理函数void sig_usr(int signo) {printf("Signal=%d, PID=%d\n", signo, getpid());}int main(int argc, char *const *argv) {pid_t pid;printf("Start\n");if(signal(SIGUSR1, sig_usr) == SIG_ERR) {printf("Catch SIGUSR1 Error\n");exit(1);}pid = fork();if(pid<0) {printf("Create Process Error\n");exit(1);}//父子进程同时运行while(1) {sleep(1);printf("Sleep 1s, PID=%d\n", getpid());}printf("Over, PID=%d\n", getpid());return 0;}
子进程被杀死(SIGKILL)后,父进程收到了SIGCHILD信号,子进程变为僵尸进程(COMMAND显示defunct(失效),STAT显示Z)
- 僵尸进程的产生和解决
子进程终止了,父进程还活着,但该父进程未调用函数(wait/waitpid)来进行额外处置(处置子进程终止这件事),子进程将变成僵尸进程(未释放资源,比如PID)。
杀掉父进程,僵尸进程会自动消失;
子进程终止或停止时,父进程会收到SIGCHILD信号,可以在信号处理函数中调用wait/waitpid函数,释放僵尸进程。
#include #include #include #include #include //信号处理函数void sig_usr(int signo) {int status;switch(signo) {case SIGUSR1:printf("SIGUSR1, PID=%d\n", getpid());break;case SIGCHILD:printf("SIGCHILD, PID=%d\n", getpid());//-1,任意子进程//status,保存子进程的状态信息//WNOHANG,非阻塞pid_t pid = waitpid(-1, &status, WNOHANG);//pid==0,子进程未结束,这里不会为0,子进程结束才触发SIGCHILD信号处理函数。//pid==-1,waitpid调用有错误break;}}int main(int argc, char *const *argv) {pid_t pid;if(signal(SIGUSR1, sig_usr) == SIG_ERR) {printf("Catch SIGUSR1 Error\n");exit(1);}if(signal(SIGCHILD, sig_usr) == SIG_ERR) {printf("Catch SIGCHILD Error\n");exit(1);}pid = fork();if(pid<0) {printf("Create Process Error\n");exit(1);}//父子进程同时运行while(1) {sleep(1);printf("Sleep 1s, PID=%d\n", getpid());}printf("Over, PID=%d\n", getpid());return 0;}
3.6.2 进一步认识
fork产生的子进程和父进程共享内存空间,采用写时复制(修改内存后,复制一份内存给进程单独使用,避免相互影响),提高执行效率。
#include #include #include #include //4个进程int main(int argc, char *const *argv) {fork();fork();while(1) {sleep(1);printf("Sleep 1s, PID=%d\n", getpid());}printf("Over, PID=%d\n", getpid());return 0;}
3.6.3 完善fork代码
#include #include #include #include int g_mygbltest = 0;int main(int argc, char *const *argv) {pid_t pid;printf("Start\n");pid = fork();if(pid<0) {printf("Create Process Error\n");exit(1);}if(pid==0) {//子进程while(1) {g_mygbltest++;sleep(1);printf("Child Process, PID=%d, g_mygbltest=%d\n", getpid(), g_mygbltest);}} else {//父进程while(1) {g_mygbltest++;sleep(1);printf("Parent Process, PID=%d, g_mygbltest=%d\n", getpid(), g_mygbltest);}}printf("Over, PID=%d\n", getpid());return 0;}
3.6.4 fork执行相关的逻辑判断
#include #include #include #include //7个进程int main(int argc, char *const *argv) {(fork() && fork()) || (fork() && fork());while(1) {sleep(1);printf("Sleep 1s, PID=%d\n", getpid());}printf("Over, PID=%d\n", getpid());return 0;}
( | ( | fork() | && | fork() | ) | ) | || | ( | ( | fork() | && | fork() | ) | ) | 进程 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
①pid=0 False | ①①pid=0 False | 1 | |||||||||||||
①②pid>0 True | ①②①pid=0 False | 2 | |||||||||||||
①②②pid>0 True | 3 | ||||||||||||||
②pid>0 True | ②①pid=0 False | ②①①pid=0 False | 4 | ||||||||||||
②①②pid>0 True | ②①②①pid=0 False | 5 | |||||||||||||
②①②②pid>0 True | 6 | ||||||||||||||
②②pid>0 True | 7 |
3.6.5 fork失败的可能原因
- 系统中进程太多
系统PID有限,默认最大PID为32767,僵尸进程占据PID。 - 创建进程数超过当前用户允许创建的最大进程数
printf("每个用户允许创建的最大进程数=%ld\n", sysconf(_SC_CHILD_MAX));
3.7 守护进程
3.7.1 普通进程
#include #include #include #include //7个进程int main(int argc, char *const *argv) {while(1) {sleep(1);printf("Sleep 1s, PID=%d\n", getpid());}printf("Over, PID=%d\n", getpid());return 0;}
终端退出,进程消失。
运行进程的终端被占用,不能做其他事。
3.7.2 守护进程基本概念
一种长期在后台运行,不与任何控制终端关联的进程。
基本特点:
- 生存期长,一般操作系统启动时启动,操作系统关闭时关闭。
- 无关联控制终端,终端退出不会导致守护进程退出。
- 后台默默运行,不会占着终端。
ps -efj#e,所有进程#f,完整格式信息#j,显示与任务或者作业有关信息
PPID为0的是内核进程,超级用户特权进程,随系统启动。
CMD列中,中括号[]为内核守护进程,kthreadd是其他内核守护进程的父进程,PID为2。
老祖宗进程init,系统守护进程,收养孤儿进程,PID为1.
不带中括号[]的普通守护进程(用户级守护进程):
rsyslogd,系统消息日志有关的进程
cron,某个时间执行命令
sshd,安全远程登录进程
守护进程的共同点:
- 大多数守护进程以超级用户特权运行(做事情需要权限)
- 没有控制终端,TTY显示?。内核守护进程以无控制终端方式启动,普通守护进程可能是守护进程调用setsid启动。
3.7.3 守护进程编写规则
调用umask(0)
umask函数,限制(屏蔽)一些文件权限。umask(0),不让它限制文件权限(守护进程可能要创建文件),并给这个文件设置一定的权限。fork子进程,然后退出父进程
- 命令行(shell)启动进程,父进程终止会空出终端。
- fork的子进程,能够成功调用setsid函数。
(1)文件描述符,标识文件
0,标准输入,键盘输入,STDIN_FILENO
1,标准输出,屏幕显示,STDOUT_FILENO
2,错误输出,屏幕显示,STDERR_FILENO
程序运行时,自动打开3个文件描述符。
(2)输入输出重定向
ls -la > myoutfile
cat < myoutfile
cat < myoutfile > myoutfile2
(3)空设备
“/deb/null”,黑洞设备,特殊的设备文件,丢弃(吞噬)一切写入其中的数据。
cat nginx.c > /dev/null
//读写方式打开黑洞设备int fd = open("/dev/null", O_RDWR); //NULL//先关闭STDIN_FILENO(已经打开的文件描述符,改动之前先关闭)//让STDIN_FILENO指向fd所指的内容(/dev/null)//类似于指针指向NULL,让/dev/null成为标准输入dup2(fd, STDIN_FILENO); //close(STDIN_FILENO), STDIN_FILENO=fd;//先关闭STDOUT_FILENO(已经打开的文件描述符,改动之前先关闭)//让STDOUT_FILENO指向fd所指的内容(/dev/null)//类似于指针指向NULL,让/dev/null成为标准输出dup2(fd, STDOUT_FILENO); //close(STDOUT_FILENO), STDOUT_FILENO=fd;//通过STDIN_FILENO,STDOUT_FILENO输入输出都会被/dev/null吞噬//STDERR_FILENO未关闭if(fd>STDERR_FILENO)close(fd); //fd = null,释放资源,这个文件描述符fd可以被复用。
(4)范例
#include #include #include #include #include #include //创建守护进程int ngx_daemon() {int fd;switch(fork()) {case -1://创建子进程失败,这里可以写日志......return -1;case 0://子进程,直接跳出break;default://父进程,直接退出exit(0);}//子进程才走到这里if(setsid() == -1) { //脱离终端,终端关闭,子进程不会退出//记录错误日志......return -1;}umask(0); //设置为0,不要让它限制文件权限,以免引起混乱fd = open("/dev/null", O_RDWR); //读写方式打开黑洞设备if(fd == -1) { //记录错误日志......return -1;}if(dup2(fd, STDIN_FILENO) == -1){ //关闭STDIN_FILENO,/dev/null成为标准输入//记录错误日志......return -1;}if(dup2(fd, STDOUT_FILENO) == -1){ //关闭STDOUT_FILENO,/dev/null成为标准输出//记录错误日志......return -1;}if(fd>STDERR_FILENO) {if(close(fd) == -1){ //释放资源,文件描述符(数字)可以被复用//记录错误日志......return -1;}}return 1;}int main(int argc, char *const *argv) {if(ngx_daemon() != 1) {//创建守护进程失败,可以写日志等。return 1;}while(1) {//创建守护进程成功,执行守护进程中工作sleep(1);//无任何显示结果printf("Sleep 1s, PID=%d\n", getpid());}return 0;}
开机启动守护进程,可以拥有超级用户特权。
3.7.4 守护进程不会收到的信号
SIGHUP信号
守护进程不会收到来自内核的SIGHUP信号,如果收到,肯定是进程发送过来的。
SIGHUP信号,连接(终端)断开信号,终端检测到连接断开,会发送SIGHUP信号到终端所在的会话首进程。
守护进程和终端不关联,终端断开不会收到SIGHUP信号,可以将该信号挪作他用,比如配置文件改动,守护进程重新读入配置文件。sudo ./nginx -s reload,向已启动master进程发送SIGHUP信号(-1)。SIGINT、SIGWINCH信号
守护进程不会收到来自内核的SIGINT、SIGWINCH信号,如果收到,肯定是进程发送过来的。
SIGINT,终端中断符,Ctrl+C(中断组合键)。
SIGWINCH,终端大小改变。
这些信号可以拿来和守护进程通信。
3.7.5 守护进程与后台进程的区别
- 守护进程和终端不挂钩,不向终端输出内容(信息),后台进程能向终端输出内容(信息)。
- 守护进程在关闭终端时不受影响,后台进程会退出。