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 <stdio.h>

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 终端和进程的关系

  1. 终端与bash进程

每个虚拟终端(pts)连到虚拟机,都会打开一个bash进程(shell壳)。输入命令黑窗就是bash(shell,命令行解释器是个可执行程序),用于解释用户输入的命令。

ps -ef | grep bash

whereis bash
  1. 终端上开启进程
3.2.c
#include <stdio.h>
#include <unistd.h>

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.2
ps -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等进程(执行各种用户命令、用户进程)
  1. 进程关系进一步分析
  • 会话(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信号的进程默认动作就是退出。

  1. 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信号发送给自己。

  1. 终端关闭时不退出进程
gcc nginx.c -o nginx

ps -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 <stdio.h>
#include <unistd.h>
#include <signal.h>

int main(int argc, char *const *argv){
	signal(SIGHUP, SIG_IGN);//ignore SIGUP
	printf("begin!\n");
	while(1){
		sleep(1);
		printf("sleep 1s!\n");
	}
	printf("end!\n");
	return 0;
}
gcc 3.3.c -o 3.3 && ./3.3

ps -ef | grep '3\.3'

终端(bash)断开后,该进程TT变为?(孤儿进程),PPID变为1(init进程收养)。

(2)进程和终端bash进程在不同session中

3.4.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
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.4

ps -ef | grep '3\.4'
3.5.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

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.5

ps -ef | grep '3\.5'

关闭当前终端后,父进程退出,子进程仍运行,变成孤儿进程。

(3)setsid命令启动进程
使启动的进程在新的session中,终端关闭时不会退出。
nginx.c

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

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 nginx
setsid ./nginx

PPID为1,TT为?。

(4)nohup(no hang up不挂断)
nohup命令启动的程序会忽略SIGHUP信号,与程序内编写忽略SIGHUP信号等价。

nohup ./nginx

nohup命令将输出重定向到当前目录的nohup.out文件中。

关闭终端后,进程PPID为1,TT为?。

  1. 后台执行(运行)
./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参数该参数发出信号内核默认动作
-1SIGHUP(连接断开)终止掉进程(进程没了)
-2SIGINT(终端中断符,Ctrl+C)终止掉进程(进程没了)
-3SIGQUIT(终端退出符,Ctrl+\)终止掉进程(进程没了)
-9SIGHUPKILL(终止)终止掉进程(进程没了)
-18SIGCONT(暂停进程继续)忽略(进程运行不受影响)
-19SIGSTOP(停止),可用SIGCONT继续,但任务被放到了后台停止进程(不是终止,进程还在)
-20SIGSTP(终端停止符,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有些页被锁进内存
ssession 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 bash
ps
exit

操作系统内核部分有进程管理(CPU)、内存管理(RAM)、文件系统(磁盘/存储设备)、设备驱动(各种终端设备)、网络(网卡)等各种资源的管理。操作系统内核和用户空间(应用程序),通过系统调用(system call)接口实现对各种资源功能的调用。

  • (4)用户态内核态之间的切换

进程大部分时间处于用户态下,需要内核提供服务才切换到内核态,内核态任务处理完成后,又切换回用户态。
分用户态,内核态的目的:
①用户态权限小,内核态权限大。权限大,能做危险事情(处理内存、处理时钟等),交由内核态处理,避免系统处于危险状态甚至崩溃,只提供调用接口。
②系统提供并统一管理调用接口。系统资源有限,避免发生访问冲突,资源耗尽等导致系统崩溃,系统接口用于减少有限资源访问和使用的冲突。

用户态切换到内核态时间:
①系统调用。malloc
②异常事件。收到信号,调用信号处理函数,系统跳到内核态做一些调用该信号处理函数的准备工作
③外围设备的中断。外围设备完成用户的请求操作后,会向CPU发出中断信号,此时CPU就会暂停执行下一条即将执行的指令,转而去执行中断信号对应的处理程序,如果先前执行的指令处于用户态下,就会发生用户态到内核态的转换。

3.4.2 signal函数

忽略或捕捉信号。

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

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 pid
kill -USR2 pid

进程执行过程中,信号到来引发的用户态和内核态之间的切换过程:

  • 进程nginx正常执行,突然到来信号导致nginx进程的流程被打断,进程从用户态切换到内核态(做一些调用信号处理函数的准备工作);
  • 为了调用注册的信号处理函数sig_usr,又从内核态切换到用户态;
  • sig_usr执行完毕后,从用户态切换到内核态(做一些信号处理函数的收尾工作);
  • 从内核态切换到用户态,从之前被打断的nginx进程的执行流程处开始继续执行。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

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. 可重入函数

可重入的函数(或异步信号安全的函数),指在信号处理函数中调用是安全的函数。
可重入函数(即可以被中断的函数)可以被一个以上的任务调用,而不担心数据破坏。可重入函数在任何时候都可以被中断,而一段时间之后又可以恢复运行,而相应的数据不会破坏或者丢失。

信号处理函数中处理方法:
(1)尽量调用简单语句,尽量不要调用系统函数(malloc,printf等非可重入函数)。
(2)只调用可重入系统函数(搜索Linux可重入函数)。
(3)调用了可能修改errno值的可重入系统函数,先备份errno,信号处理函数返回前恢复errno值。

#include <errno.h>

void sig_usr(int signo) {
	int myerrno = errno;//备份errno
	//调用可能修改errno值的可重入系统函数
	errno = myerrno;//还原errno
}
  1. 不可重入函数的错用示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

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 pid
kill -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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

//信号处理函数,可被中断
//即信号处理函数注册多个信号,当处理第一个信号时,可被第二个信号中断
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+\, SIGQUIT
	if(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 简单认识

  1. 进程概念

多个进程可共享同一个可执行文件(程序),进程一般定义为程序执行的一个实例。fork用于进程中创建子进程,当该子进程创建时,它从fork函数的下一条语句(或者说从fork的返回处)开始执行与父进程相同的代码。fork后父子进程执行的先后顺序并不确定(与内核调度算法有关)。

  1. 简单范例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

//信号处理函数
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)

  1. 僵尸进程的产生和解决

子进程终止了,父进程还活着,但该父进程未调用函数(wait/waitpid)来进行额外处置(处置子进程终止这件事),子进程将变成僵尸进程(未释放资源,比如PID)。

杀掉父进程,僵尸进程会自动消失;
子进程终止或停止时,父进程会收到SIGCHILD信号,可以在信号处理函数中调用wait/waitpid函数,释放僵尸进程。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

//信号处理函数
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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

//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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

//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 False1
①②pid>0 True①②①pid=0 False2
①②②pid>0 True3
②pid>0 True②①pid=0 False②①①pid=0 False4
②①②pid>0 True②①②①pid=0 False5
②①②②pid>0 True6
②②pid>0 True7

3.6.5 fork失败的可能原因

  • 系统中进程太多
    系统PID有限,默认最大PID为32767,僵尸进程占据PID。
  • 创建进程数超过当前用户允许创建的最大进程数
printf("每个用户允许创建的最大进程数=%ld\n", sysconf(_SC_CHILD_MAX));

3.7 守护进程

3.7.1 普通进程

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

//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 守护进程编写规则

  1. 调用umask(0)
    umask函数,限制(屏蔽)一些文件权限。umask(0),不让它限制文件权限(守护进程可能要创建文件),并给这个文件设置一定的权限。

  2. 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 <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;
	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 守护进程不会收到的信号

  1. SIGHUP信号
    守护进程不会收到来自内核的SIGHUP信号,如果收到,肯定是进程发送过来的。
    SIGHUP信号,连接(终端)断开信号,终端检测到连接断开,会发送SIGHUP信号到终端所在的会话首进程。
    守护进程和终端不关联,终端断开不会收到SIGHUP信号,可以将该信号挪作他用,比如配置文件改动,守护进程重新读入配置文件。sudo ./nginx -s reload,向已启动master进程发送SIGHUP信号(-1)。

  2. SIGINT、SIGWINCH信号
    守护进程不会收到来自内核的SIGINT、SIGWINCH信号,如果收到,肯定是进程发送过来的。
    SIGINT,终端中断符,Ctrl+C(中断组合键)。
    SIGWINCH,终端大小改变。

这些信号可以拿来和守护进程通信。

3.7.5 守护进程与后台进程的区别

  • 守护进程和终端不挂钩,不向终端输出内容(信息),后台进程能向终端输出内容(信息)。
  • 守护进程在关闭终端时不受影响,后台进程会退出。
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐