Nginx服务器在使用Master-Worker模型时,会涉及到三类通信:Linux系统与Nginx通信,Master进程与Worker进程通信,Worker进程间通信,也采用了三种不同的通信机制。
Linux信号
Linux 系统与Nginx是通过信号进行通信的,例如在Linux命令行敲下 ./nginx -s stop ,实际系统会新开一个Master进程,该进程负责向原Master发送信号,发送完信号该进程就挂了,原Master进程接收到信号后执行相应的操作。
1.信号定义与注册
Nginx定义了一个ngx_signal_t结构体用于描述接收到信号的行为
typedef struct { //signals
int signo; //需要处理的信号
char *signame; //信号对应的字符串名称
char *name; //这个信号对应着的Nginx命令
void (*handler)(int signo); //收到signo信号后就会回调handler方法
} ngx_signal_t;
Nginx在启动时会调用ngx_init_signals函数,该函数通过sigaction系统调用初始化所有信号,并注册相应的信号处理函数。完成所有初始化工作后,Master进程调用ngx_master_process_cycle函数进入自身事件循环,负责监听信号,一旦收到信号后,执行对应信号处理函数。
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) {
#if (NGX_VALGRIND)
ngx_log_error(NGX_LOG_ALERT, log, ngx_errno,
"sigaction(%s) failed, ignored", sig->signame);
#else
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
"sigaction(%s) failed", sig->signame);
return NGX_ERROR;
#endif
}
}
return NGX_OK;
}
2.信号发送
执行 ./nginx -s stop 命令将会开启新的Master进程,该进程根据-s参数知道用户要给Nginx发送信号,通过ngx_get_options()函数解析出所要发送的信号,然后将执行ngx_signal_process()函数,该函数首先获取原Master进程的pid,然后给原Master进程发送具体的信号,完成通信。
case 's':
if (*p) {
ngx_signal = (char *) p;
} else if (argv[++i]) {
ngx_signal = argv[i];
} else {
ngx_log_stderr(0, "option \"-s\" requires parameter");
return NGX_ERROR;
}
if (ngx_strcmp(ngx_signal, "stop") == 0
|| ngx_strcmp(ngx_signal, "quit") == 0
|| ngx_strcmp(ngx_signal, "reopen") == 0
/*
reload实际上是执行reload的nginx进程向原master+worker中的master进程发送reload信号,源master收到后,启动新的worker进程,同时向源worker
进程发送quit信号,等他们处理完已有的数据信息后,退出,这样就只有新的worker进程运行。见ngx_signal_handler
*/
|| ngx_strcmp(ngx_signal, "reload") == 0)
{
ngx_process = NGX_PROCESS_SIGNALLER;
goto next;
}
/*
该函数作用:
读取ngx_core_module模块的配置结构ngx_core_conf_t;
根据配置结构找到其工作进程文件,如"/usr/local/nginx/logs/nginx.pid"(该文件保存nginx进程ID,即pid);
打开该文件,读取pid;
调用ngx_os_signal_process()发送信号;
*/
ngx_int_t ngx_signal_process(ngx_cycle_t *cycle, char *sig)
{
...
//打开存放master进程的文件NGX_PID_PATH
file.name = ccf->pid;
file.log = cycle->log;
file.fd = ngx_open_file(file.name.data, NGX_FILE_RDONLY,
NGX_FILE_OPEN, NGX_FILE_DEFAULT_ACCESS);
if (file.fd == NGX_INVALID_FILE) {
ngx_log_error(NGX_LOG_ERR, cycle->log, ngx_errno,
ngx_open_file_n " \"%s\" failed", file.name.data);
return 1;
}
n = ngx_read_file(&file, buf, NGX_INT64_LEN + 2, 0);
if (ngx_close_file(file.fd) == NGX_FILE_ERROR) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
ngx_close_file_n " \"%s\" failed", file.name.data);
}
if (n == NGX_ERROR) {
return 1;
}
while (n-- && (buf[n] == CR || buf[n] == LF)) { /* void */ }
pid = ngx_atoi(buf, ++n); //master进程id
if (pid == NGX_ERROR) {
ngx_log_error(NGX_LOG_ERR, cycle->log, 0,
"invalid PID number \"%*s\" in \"%s\"",
n, buf, file.name.data);
return 1;
}
return ngx_os_signal_process(cycle, sig, pid);
}
Channel
Nginx中利用socketpair()函数,创建一对相互连接的socket,实现父子进程的通信,即Master进程与Worker进程通信,在Nginx中,将这种通信定义为频道——channel。
1.channel定义
typedef struct {
ngx_uint_t command; //对端将要做得命令 取值为NGX_CMD_OPEN_CHANNEL等
ngx_pid_t pid; //当前的子进程id 进程ID,一般是发送命令方的进程ID
ngx_int_t slot; //在全局进程表中的位置 表示发送命令方在ngx_processes进程数组间的序号
ngx_fd_t fd; //传递的fd 通信的套接字句柄
} ngx_channel_t;
2.channel注册
Master进程通过fork()函数创建子进程,在fork之前首先调用socketpair()创建一对关联的套接字,用于父子进程间的通信,子进程也将会获得该套接字,在子进程初始化的时候,将套接字加入epoll等待父进程的消息,并注册消息处理函数。父进程通过ngx_write_channel()发送消息,子进程通过ngx_read_channel()读取消息。
ngx_pid_t ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,
char *name, ngx_int_t respawn) //respawn取值为NGX_PROCESS_RESPAWN等,或者为进程在ngx_processes[]中的序号
{
...
if (socketpair(AF_UNIX, SOCK_STREAM, 0, ngx_processes[s].channel) == -1) //在ngx_worker_process_init中添加到事件集
{
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
"socketpair() failed while spawning \"%s\"", name);
return NGX_INVALID_PID;
}
/* 设置master的channel[0](即写端口),channel[1](即读端口)均为非阻塞方式 */
if (ngx_nonblocking(ngx_processes[s].channel[0]) == -1) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
ngx_nonblocking_n " failed while spawning \"%s\"",
name);
ngx_close_channel(ngx_processes[s].channel, cycle->log);
return NGX_INVALID_PID;
}
if (ngx_nonblocking(ngx_processes[s].channel[1]) == -1) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
ngx_nonblocking_n " failed while spawning \"%s\"",
name);
ngx_close_channel(ngx_processes[s].channel, cycle->log);
return NGX_INVALID_PID;
}
...
pid = fork();
}
static void ngx_worker_process_init(ngx_cycle_t *cycle, ngx_int_t worker)
{
//调用epoll add 把ngx_chanel 加入epoll 中
if (ngx_add_channel_event(cycle, ngx_channel, NGX_READ_EVENT,
ngx_channel_handler) //在ngx_spawn_process中赋值
== NGX_ERROR)
{
/* fatal */
exit(2);
}
}
共享内存
Nginx中各Worker进程是通过共享内存进行通信的,共享内存是Linux下提供的最基本的进程间通信方式,它通过mmap和shmget系统调用在内存中创建了一块连续的线性地址空间,而通过munmap或者shmdt系统调用可以释放这块内存。使用共享内存的好处是当多个进程使用同一块共享内存时,在任何一个进程修改了共享内存中的内容后,其他进程通过访问这段共享内存都能够得到修改后的内容。
Nginx定义了ngx_shm_t结构体,用于描述一块共享内存
typedef struct {
u_char *addr; //共享内存起始地址
size_t size; //共享内存空间大小
ngx_str_t name; //这块共享内存的名称
ngx_log_t *log; //shm.log = cycle->log; 记录日志的ngx_log_t对象
ngx_uint_t exists; /* unsigned exists:1; */ //表示共享内存是否已经分配过的标志位,为1时表示已经存在
} ngx_shm_t;
操作ngx_shm_t结构体的方法有两个:ngx_shm_alloc(基于mmap实现)用于分配新的共享内存,而ngx_shm_free(基于munmap实现)用于释放已经存在的共享内存。共享内存由Master进程创建,Worker进程共享。Nginx 解决惊群问题,是通过设置互斥锁,只有拥有互斥锁的工作进程才能担负与客户建立连接的任务,这个互斥锁就放于共享内存中。另外,ngin统计连接数,这个全局变量也放于共享内存中。