本文地址:http://blog.youkuaiyun.com/spch2008/article/details/38945033
nginx的进程通信分为三种类别:linux 系统与nginx 通信, master 进程与worker进程通信, worker进程间通信。
master进程管理worker进程,本文将追溯nginx 的退出过程。
Linux信号
linux 系统与nginx是通过信号才进行通信的,通过信号控制nginx重启、关闭以及加载配置文件等。
信号发送流程
1. 发送信号
./nginx –s quit 向master进程发送信号
这里有一点是:执行 ./nginx –s quit 实际上是新开了一个master进程,只不过它半路夭折了,即向原master发送信号后,
它就死掉啦。它存在的意义就是向原master发送信号。
2. 获取参数
nginx 通过 -s 知道用户要给nginx 发送信号,会有相应的动作。
ngx_get_options
case 's':
if (*p) {
ngx_signal = (char *) p;
}
if (ngx_strcmp(ngx_signal, "stop") == 0
|| ngx_strcmp(ngx_signal, "quit") == 0
|| ngx_strcmp(ngx_signal, "reopen") == 0
|| ngx_strcmp(ngx_signal, "reload") == 0)
{
ngx_process = NGX_PROCESS_SIGNALLER;
goto next;
}
3. 获取pid
要发送信号,需要知道master进程的pid,那如何获得呢?nginx 启动的时候将其写入了本地文件中。
ngx_int_t
ngx_signal_process(ngx_cycle_t *cycle, char *sig)
{
ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
file.name = ccf->pid;
file.fd = ngx_open_file(file.name.data, NGX_FILE_RDONLY,
NGX_FILE_OPEN, NGX_FILE_DEFAULT_ACCESS);
n = ngx_read_file(&file, buf, NGX_INT64_LEN + 2, 0);
pid = ngx_atoi(buf, ++n);
return ngx_os_signal_process(cycle, sig, pid);
}
file.name即为配置文件中指定pid所在的文件,该文件存放master的pid。通过配置文件中的pid字段,指明存放进程id文件的地址。
4. 发送信号
ngx_int_t
ngx_os_signal_process(ngx_cycle_t *cycle, char *name, ngx_int_t pid)
{
ngx_signal_t *sig;
for (sig = signals; sig->signo != 0; sig++) {
if (ngx_strcmp(name, sig->name) == 0) {
if (kill(pid, sig->signo) != -1) {
return 0;
}
}
}
return 1;
}
根据name到ngx_signal_t的名字中去匹配,找到信号,然后通过kill向master发送消息。
信号注册流程
1. 信号定义
typedef struct {
int signo;
char *signame;
char *name;
void (*handler)(int signo);
} ngx_signal_t;
简单解释一下,signo为信号的数字表示,而signame为信号的名字,name为nginx定义的信号名,handler为回调函数。比如:
{ ngx_signal_value(NGX_SHUTDOWN_SIGNAL),
"SIG" ngx_value(NGX_SHUTDOWN_SIGNAL),
"quit",
ngx_signal_handler }
转换后为:{ SIGQUIT, "SIGQUIT", "quit", ngx_signal_handler }, 具体语法规则见附录。然后SIGQUIT会借助于预处理器进行数字的对应,
SIGQUIT为在signal.h中的宏定义。
2. 信号注册
ngx_int_t
ngx_init_signals(ngx_log_t *log)
{
ngx_signal_t *sig;
struct sigaction sa;
for (sig = signals; sig->signo != 0; sig++) {
ngx_memzero(&sa, sizeof(struct sigaction));
sa.sa_handler = sig->handler;
sigemptyset(&sa.sa_mask);
if (sigaction(sig->signo, &sa, NULL) == -1) {
return NGX_ERROR;
}
}
return NGX_OK;
}
signals 为ngx_signal_t 数组,内部存放所有nginx会处理的信号。上述中,通过sigaction注册信号。
3. 信号处理
void
ngx_signal_handler(int signo)
{
switch (ngx_process)
case NGX_PROCESS_MASTER:
switch (signo)
case ngx_signal_value(NGX_SHUTDOWN_SIGNAL):
ngx_quit = 1;
}
设置全局变量ngx_quit 标志为1.
4. 响应信号
void
ngx_master_process_cycle(ngx_cycle_t *cycle)
{
sigemptyset(&set);
sigaddset(&set, ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
sigemptyset(&set);
for ( ;; ) {
sigsuspend(&set);
if (ngx_quit) {
ngx_signal_worker_processes(cycle,
ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
continue;
}
}
}
sigsuspend将阻塞进程,直到set中的信号到达为止。当有信号达到,调用handler函数,然后ngx_quit被设置为1,
此时,通过ngx_signal_worker_processes 向工作进程传递信号。
Socket通信
master进程与worker进程通过sockpair进行通信。在nginx中,将这种通信定义为频道,channel。
master就是通过频道与worker进程通信滴。
1. 频道定义
typedef struct {
ngx_uint_t command;
ngx_pid_t pid;
ngx_int_t slot;
ngx_fd_t fd;
} ngx_channel_t;
2. 注册频道
其一:创建频道,其实就是socketpair,在启动worker进程的时候创建频道
ngx_pid_t
ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,
char *name, ngx_int_t respawn)
{
socketpair(AF_UNIX, SOCK_STREAM, 0, ngx_processes[s].channel) == -1);
ngx_nonblocking(ngx_processes[s].channel[0]) == -1);
ngx_nonblocking(ngx_processes[s].channel[1]) == -1);
ngx_channel = ngx_processes[s].channel[1];
}
设置非阻塞,同时,将ngx_channel赋值。
其二:加入epoll等待数据到来,在worker进程初始化的时候加入
static void
ngx_worker_process_init(ngx_cycle_t *cycle, ngx_uint_t priority)
{
if (ngx_add_channel_event(cycle, ngx_channel, NGX_READ_EVENT,
ngx_channel_handler)
== NGX_ERROR)
{
exit(2);
}
}
ngx_channel_handler为回调函数。
3. 发送消息
static void
ngx_signal_worker_processes(ngx_cycle_t *cycle, int signo)
{
ngx_channel_t ch;
switch (signo) {
case ngx_signal_value(NGX_SHUTDOWN_SIGNAL):
ch.command = NGX_CMD_QUIT;
break;
}
for (i = 0; i < ngx_last_process; i++)
ngx_write_channel(ngx_processes[i].channel[0],
&ch, sizeof(ngx_channel_t), cycle->log)
}
master进程通过ngx_signal_woker_processes向worker进程发送消息。
4. 响应消息
static void
ngx_channel_handler(ngx_event_t *ev)
{
c = ev->data;
for ( ;; ) {
n = ngx_read_channel(c->fd, &ch, sizeof(ngx_channel_t), ev->log);
switch (ch.command) {
case NGX_CMD_QUIT:
ngx_quit = 1;
break;
}
}
}
worker进程通过ngx_read_channel读取消息,然后根据command判断是什么消息,同时设置worker进程的ngx_quit变量。
5. woker进程受理
static void
ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
{
ngx_uint_t i;
ngx_connection_t *c;
for ( ;; ) {
ngx_process_events_and_timers(cycle);
if (ngx_quit) {
ngx_quit = 0;
if (!ngx_exiting) {
ngx_close_listening_sockets(cycle);
ngx_exiting = 1;
}
}
worker内部ngx_worker_process_cycle为一个循环,处理事件,当检测到退出标志后,做相应处理
共享内存
worker进程间则是通过比较快速的共享内存进行通信。
1. mmap 匿名
即不与文件关联,不需要创建文件删除文件,简单高效。但有可能有些系统不支持,所以若不支持,采用第二种方案。
2.mmap /dev/zero
与字符设备/dev/zero关联,相当于打开一个文件,但mac os不支持,所以采用第三种方式。
fd = open(“/dev/zero”, O_RDWR);
mmap(NULL, sizeof(int), PROT_READ |PROT_WRITE, MAP_SHARED, fd, 0).
3.shmget 古老的system v获取
繁琐,但所有系统都支持
由master进程创建共享变量,worker进程共享。nginx 解决惊群问题,是通过设置互斥锁,
只有拥有互斥锁的工作进程才能担负与客户建立连接的任务,这个互斥锁就放于共享内存中。
另外,ngin统计连接数,这个全局变量也放于共享内存中,多个工作进程可以去改写这个变量,
当然,需要一些互斥机制。
共享内存一个小例子:
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
int
main(int argc, char *argv[])
{
int *num;
pid_t pid;
num = (int*) mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, 0, 0);
if ((pid = fork()) < 0) {
perror("fork error");
exit(0);
} else if (pid == 0) {
*num += 5;
exit(0);
}
waitpid(pid, NULL, 0);
printf("father get data: %d\n", *num);
return 0;
}
附录
1. #转换为字符
在define宏定义中,#代表将参数转换成字符串, 即“ ”.
#definevalue(n) #n
char *str = value(12345); è str = “12345”
2. ##连接成一个token
在define宏定义中,两个##表示将参数连接起来构成一个标示符
#define value(n) val##n
intval1 = 3;
printf(“%d\n”, value(1)); è val1
3. 字符串连接
//C语言中,“ ”表示空串 不准确
char *str = “123” “456”;
等效
char *str = “123456”;
解析: 相当“123” “456” 红色是一对,空串省略
4. 为什么worker 与master采用socketpair进行通信
worker进程有一个循环,在不停的处理请求,怎样接收master发送的数据呢。而socketpair为一种套接字,将它加入epoll中,通过事件模块获取该套接字,进行处理。ngx_write_channel将数据写入channel。而在ngx_process_cycle.c->ngx_worker_process_init初始化的时候,已经调用ngx_add_channel_event将套接字写入epoll中,后续向套接字写数据,epoll能检测到,并调用注册的回调函数ngx_channel_handler,将相应标志位设1(ngx_quit)。