nginx服务器在启动后,产生一个主进程,主进程执行一系列工作后产生一个或者多个工作进程。
主进程主要完成nginx配置文件解析,数据结构初始化,模块配置和注册、信号处理、网络监听生成,工作进程生成和管理工作;工作进程主要进行进程初始化、模块调用和请求处理工作,是nginx提供服务的主体。
nginx作为反向代理服务器的时候可以将动态的网络请求转发到后端服务器,在后端服务器完成处理后将结果返回给客户端,同时支持缓存
。
1.nginx 服务器进程
1.1主进程
- 读取配置文件,并验证有效性和正确性。
-
建立、绑定、关闭socket
。 - 按照配置生成、管理、结束工作进程。
- 接收外部指令,重启,升级退出服务器指令。
- 不中断服务,实现平滑重启,应用新配置。
- 不中断服务,实现平滑升级,升级失败回滚。
-
开启日志,获取文件描述符
。 编译处理perl脚本。
1.2.工作进程
- 接收客户端请求。
将请求依次送入各个模块进行过滤。
IO调用,获取响应数据
- 与后端服务器通信,接收后端服务器的处理结果。
数据缓存,访问缓存索引、查询和调用缓存
- 发送请求结果,响应客户端请求。
- 接收master进程的指令,重启,退出等指令。
1.3.缓存索引重建及管理进程
缓存索引重建进程(Cache Loader)和缓存管理进程(Cache Manage)两类进程完成工作,Cache Loader进程在nginx启动一段时间后,由主进程生成,在缓存元数据重建完成后就自动退出。管理进程处于nginx整个生命周期。
2.进程的交互
2.1Master进程和Work进程交互
worker进程由master进程使用fork函数生成,nginx服务器启动后,主进程根据配置文件决定生成工作进程的数量,然后建立一张全局的工作进程表用于存放当前未退出的所有工作进程。
生成工作进程后,将新生成的工作进程加入到工作进程表,建立一个单向管道
,将进程表发送给新的工作进程。管道包含了主进程向工作进程发出的指定、工作进程ID、工作进程在工作进程表中的索引和必要的文件描述符。
主进程和外界通过信号机制通信
,当接收到需要处理的信号的时候,通过管道向工作进程发送指令。每个工作进程都有能力捕获管道里面的事件,工作进程从管道读取并解析指令,然后采取措施。
2.2 Worker进程之间的交互
worker交互实现原理和master-wroker交互基本一致,只要工作进程能够得到彼此之间的信息,建立管道,即可通信。工作进程之间彼此隔离,需要通过主进程获得另一个进程的信息。
主进程在生成工作进程后,在工作进程表中进行遍历,将该新进程的ID以及针对该进程建立的管道句柄传递给工作进程表中的其他进程,为工作进程之间的交互做准备。每个工作进程捕获管道中的可读事件,根据指令采取措施。
当工作进程W1需要向W2发送 指令时,首先在主进程给其他的工作进程信息中找到W2的进程ID,然后将正确的指令写入指向W2的通道。工作进程W2捕获管道中的事件后,解析指令并采取措施。
2.3 RunLoops
暂时略
3.管道和信号
上面在介绍nginx进程通信的时候,大量的提到的信号和管道的概念。如果在不理解操作系统进程相关知识的情况下,是很难理解真正含义的。下面对管道和信号做一个初步介绍和代码实验,需要详情理解可以参考linux 编程的教材。
3.1 管道
管道是两个进程的连接器,单向,一端作为输入。普通管道只能用于父子进程的进程间通信,命名管道可以实现不同进程间的通信。
首先创建了一个管道,然后创建了子进程,利用该管道从父进程向子进程发送信息。消息内容是程序的命令行参数,子进程收到父进程传送的消息后输出。本例里面每个文件描述符都被关闭了两次,是因为在多进程的环境下,一个有效的管道描述符在并发子进程时会将被所有进程共享的文件表里面的引用次数+1,这里的每一次都是将其引用计数-1,在一个描述符的引用次数为0情况下才会真正的关闭一个描述符。
//普通管道的案例,父进程向子进程发送消息
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<signal.h>
#include <setjmp.h>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/stat.h>
/*int pipe(int pipefd[2]);
参数保存系统返回的描述符号,0读,1写
返回0表示成功
返回-1表示函数执行失败
*/
int main(int argc, char *argv[])
{
int pfd[2]; //保存管道的文件描述符
pid_t cpid;//保存进程标示符
char buf;//定义变量
if(argc != 2) {
fprintf(stderr,"Usage: %s <string>\n",argv[0]);//提示用法信息
exit(0);
}
if (pipe(pfd) == -1){
perror("pipe");
exit(EXIT_FAILURE);
}
cpid = fork();//创建进程
if (cpid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (cpid == 0)//子进程
{
close(pfd[1]);//关闭写管道描述符
while (read(pfd[0], &buf, 1) > 0)//管道循环读数据
write(STDOUT_FILENO, &buf, 1);//输出读到数据
write(STDOUT_FILENO, "\n", 1);//输出从管道里读取的数据
close(pfd[0]);//关闭读文件描述符
exit(EXIT_SUCCESS);
}
else {
close(pfd[0]);//关闭读文件描述符
char const* pramStr = argv[1];
write(pfd[1], argv[1], strlen(pramStr));//向管道写入命令行参数1
close(pfd[1]);//关闭写管道描述符
wait(NULL);//等待子进程退出
exit(EXIT_SUCCESS);
}
return 0;
}
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<signal.h>
#include <setjmp.h>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/stat.h>
/**/
`这是一个利用管道创建函数pipe和描述符重定向函数dup2,从子进程向父进程传递消息的过程`
/**
* 首先创建了管道,然后创建子进程,在子进程里面用execlp函数执行ls命令,
* 利用函数dup2将子进程的标准输出重定向到管道描述符,使得本来应该输出到屏幕的ls命令结果
* 输出到了管道的写描述符,而在父进程里面调用execlp函数执行wc命令,
* 同样调用dup2将父进程的标准输入重定向由标准输入变成了管道的读描述符,这样父进程执行wc命令时
* 其输入就是从管道读取的数据,恰好是由子进程执行ls命令输出的数据,
* 巧妙实现了利用管道编程实现了shell功能ls-l|wc-l这个功能创建数组保存管道返回的文件描述符号
* @return
*/
int main() {
int pfds[2]; //保存管道的文件描述符
if (pipe(pfds) == 0) {
if (fork() == 0){//子进程
close(STDOUT_FILENO);
dup2(pfds[1], STDOUT_FILENO);//将管道的写文件描述符号复制到进程的标准输出
close(pfds[0]);//关闭管道的读
execlp("ls", "ls", "-l", NULL);//执行ls -l命令,将输出写入管道
} else{ //父进程
close(STDIN_FILENO);//关闭标准输入
dup2(pfds[0], 0);//将管道的读文件描述服复制到进程的标准输入
close(pfds[1]);//关闭管道的写
execlp("wc", "wc", "-l", NULL);//执行wc -l,将从管道读去的数据作为输出
}
}
return 0;
}
对于没有亲缘关系的进程,可以利用系统的提供函数创建一个管道文件,也可以用shell提供的命令mkfifo -m 0644 p或者mknod p;
int mkfifo (__const char *__path, __mode_t __mode);
int main() {
int fd;
if ((fd = open("/Users/guodong/p", O_RDONLY, 0)) < 0) {
perror("open");
exit(-1);
}
char buff[100];
int size = 5;
while ((size = read(fd, buff, size))>0){
printf("%s",buff);
memset(buff,0, sizeof(buff));
}
return 0;
}
//这两个可执行文件同时执行的时候上面的进程终端会看到下面进程打印的信息
int main() {
int fd;
if ((fd = open("/Users/guodong/p", O_WRONLY, 0)) < 0) {
perror("open");
exit(-1);
}
char *str = "hello world\0";
write(fd,str,strlen(str));
close(fd);
return 0;
}
3.2信号
Linxu系统支持30种不同种信号,每种信号都是对应系统的某种事件,底层硬件的异常是由内核异常处理程序处理的,正常情况下,对于用户进程是不可见的。信号提供了一种机制,
通知用户进程发生了这些异常。
例如:当进程在前台运行的时候,按下ctr+c内核向当前进程发送SIGINT的信号给前台进程,默认操作是终止运行。
- 发送信号
内核通过更新目的进程的上下文的某个状态,发送信号给目的进程,原因有两个
- 内核检测到了系统事件,比如除数为0.子进程终止
- 一个进程调用了kill函数,显示的要求内核发送信号给目的进程
2.接收信号:目的进程被内核强迫以某种方式对信号的发送做出反应的时候
,就接收了信号。进程可以选择忽视,也可以通过信号处理程序捕获信号---signal函数
3.待处理信号:发出还未接收的信号。
注意:任何时刻一种类型的信号至多有一个待处理信号。如果一个进程有一个类型为k的待处理信号,接下来发送的任何为k的信号不会排队等待,会被丢弃.
一个进程可以选择性的阻塞某种信号,当一个信号被阻塞时,但是产生的待处理信号不会被接收,直到进程取消对这种信号的阻塞。如何发送信号呢,linux系统提供了大量的机制,但是都是基于进程组这个概念的,默认的子进程和它的父进程属于同一个进程组。每个进程都只属于一个进程组,进程组是一个正整数进程组ID来标识的。可以通过getpgrp和setpgid获取或者改变进程组
4.发送信号方法:
-
/bin/kill程序可以向另外的进程发送任意的信号,比如 /bin/kill -9 -15211发送信号SIGKILL给进程15213,如果PID为负数,会认为是进程组号,给进程组所有进程发送信号。
-
键盘发送,比如常见的ctrl-c,内核发送SIGINT信号给前台进程组的所有的进程,linux外壳使用job的抽象概念表示一个对一个命令行求值创建的进程,任何时刻至多有一个前台作业和0或者多个后台作业。外壳为每个作业创建了一个独立的进程组。典型的进程组ID是取自作业中父进程ID的一个,组长进程。
-
kill函数发送信号,这里的kill和第一个提到的不一样。这里的是一个底层函数,另一个是shell的内置命令。Kill(pid,sig);
-
用alarm函数发送信号。
案例:通过alarm产生产生的1s中断,计时5秒。
void handler(int sig) {
static int count = 0;//定义静态局部变量
printf("%d\n", count);
if (++count < 5) {
alarm(1);
} else {
printf("end");
exit(0);
}
}
int main() {
signal(SIGALRM, handler);
alarm(1);//使得内核在1s内发送信号SIGALRM给进程
while (1) { ;
}
exit(0);
}
5.接收信号:
内核从一个异常的处理程序返回,准备将控制传递给进程p的时候,它会检查p未被阻塞的待处理的信号的集合,如果为空,内核将控制传递到p的逻辑控制流的下一条指令。如果非空,内核选择集合的某个信号k通常k是最小值,并强制p接收信号k,收到这个信号会触发进程的某个行为,完成后,控制就传递回p的逻辑控制流中的下一个指令。(就是说,中断函数处理完成后,内核会看看是否还有没有没有被阻塞但是还有处理的中断时间,比如在处理中断函数的时候又来信号了,来了不止一个,那么就从里面选择编号最小的处理,当然这样可能性不大,通常就是处理完后返回main函数继续运行)。
6.信号的处理方式
每个信号都有相关联的默认行为,信号的默认行为可以改变,但是SIGSTOP和SIGKILL不可以。signal的第二个参数可以决定忽略。
信号处理问题:对于只捕获一个信号就终止
的程序来说信号的处理非常简单,捕获多个的时候就产生细微的问题了。
-
待处理信号被阻塞:linux信号处理程序会阻塞当前处理程序正在处理的类型的待处理信号,比如正在处理一个SIGINT触发的函数,过程中又来了一个SIGINT函数,那么这个SIGINT将变成待处理的信号了,但是不会接收,直到程序返回。
-
待处理信号不会排队等待:任意类型的信号至多有一个待处理信号。
-
系统调用可以被中断。read,write这类函数会潜在的阻塞进程一段较长时间,成为慢速系统调用,当处理程序捕获一个信号的时候,被中断的慢速系统调用在信号处理程序返回时不再继续,而是返回给用户一个错误条件,并且将errno设置为EINTR
案例代码:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<signal.h>
pid_t Fork(void) {
pid_t pid;
if ((pid = fork()) < 0) {
perror("Fork error");
}
return pid;
}
void handler(int sig) {
pid_t pid;
if ((pid = waitpid(-1, NULL, 0)) < 0) {
printf("waitpid error");
}
printf("Handler reaped child %d\n", (int) pid);
sleep(2);
return;
}
int main() {
int i, n;
char buf[255];
//只要有一个子进程终止或者停止内核就发送SIGCHLD信号给父进程
if (signal(SIGCHLD, handler) == SIG_ERR) {
printf("signal error");
}
for (i = 0; i < 3; i++) {
if (Fork() == 0) {
//创建三个子进程,并且休眠1秒后退出
printf("Hello from child %d\n", (int) getpid());
sleep(1);
exit(0);
}
}
//父进程等待来自终端的一个输入行。当每个子进程终止时,父进程
//根据信号处理子进程回收工作,并且可以处理一些其他的事情想sleep(2) handler函数
if ((n = read(STDIN_FILENO, buf, sizeof(buf))) < 0) {
//从输入缓冲区读数据到数组buf
printf("read error");
}
printf("Parent processing input\n");
while (1);
exit(0);
}
上述代码第三个信号会丢失,因为linux最多只能同时处理两个相同类型的信号,一个是正在处理,另一个是阻塞等待,再来第三个就直接丢弃。所以不可以用信号对其他进程中发生的事件计数
,下面的代码会解决上面出现的问题,把if改成了while由父进程进行一直等待,直到其他的子进程都退出。
void handler2(int sig) {
pid_t pid;
while ((pid = waitpid(-1, NULL, 0)) > 0) {
printf("Handler reaped child %d\n", (int) pid);
}
if (errno != ECHILD) {
print("waitpid error");
}
sleep(2);
return;
}
网友评论