Nginx之进程间的通信机制(信号信号量文件锁)

Posted 季末的天堂

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Nginx之进程间的通信机制(信号信号量文件锁)相关的知识,希望对你有一定的参考价值。

1. 信号

nginx 在管理 master 进程和 worker 进程时大量使用了信号。Linux 定义的前 31 个信号是最常用的,Nginx 则通过重定义其中一些信号的处理方法来使用吸纳后,如接收到 SIGUSR1 信号就意味着需要重新打开文件。

使用信号时 Nginx 定义了一个 ngx_signal_t 结构体用于描述接收到的信号时的行为:

typedef struct {
    // 需要处理的信号
    int     signo;
    // 信号对应的字符串名称
    char   *signame;
    // 这个信号对应着的 Nginx 命令
    char   *name;
    // 收到 signo 信号后就会回调 handler 方法
    void  (*handler)(int signo, siginfo_t *siginfo, void *ucontext);
}ngx_signal_t;

根据该 ngx_signal_t 结构体,Nginx 定义了一个数组,用来定义进程将会处理的所有信号。如:

#define NGX_RECONFIGURE_SIGNAL HTP

ngx_signal_t  signals[] = {
    { ngx_signal_value(NGX_RECONFIGURE_SIGNAL),
      "SIG" ngx_value(NGX_RECONFIGURE_SIGNAL),
      "reload",
      ngx_signal_handler },
    
    { ngx_signal_value(NGX_REOPEN_SIGNAL),
      "SIG" ngx_value(NGX_REOPEN_SIGNAL),
      "reopen",
      ngx_signal_handler },

    { ngx_signal_value(NGX_NOACCEPT_SIGNAL),
      "SIG" ngx_value(NGX_NOACCEPT_SIGNAL),
      "",
      ngx_signal_handler },

    { ngx_signal_value(NGX_TERMINATE_SIGNAL),
      "SIG" ngx_value(NGX_TERMINATE_SIGNAL),
      "stop",
      ngx_signal_handler },

    { ngx_signal_value(NGX_SHUTDOWN_SIGNAL),
      "SIG" ngx_value(NGX_SHUTDOWN_SIGNAL),
      "quit",
      ngx_signal_handler },

    { ngx_signal_value(NGX_CHANGEBIN_SIGNAL),
      "SIG" ngx_value(NGX_CHANGEBIN_SIGNAL),
      "",
      ngx_signal_handler },

    { SIGALRM, "SIGALRM", "", ngx_signal_handler },

    { SIGINT, "SIGINT", "", ngx_signal_handler },

    { SIGIO, "SIGIO", "", ngx_signal_handler },

    { SIGCHLD, "SIGCHLD", "", ngx_signal_handler },

    { SIGSYS, "SIGSYS, SIG_IGN", "", NULL },

    { SIGPIPE, "SIGPIPE, SIG_IGN", "", NULL },

    { 0, NULL, "", NULL }
};

从该数组知,加入接收到 SIGHUP 信号,将调用 ngx_signal_handler 方法进行处理,以便重新读取配置文件,或者说,当收到用户发来的如下命令时:

./nginx -s reload

这个新启动的 Nginx 进程会向实际运行的 Nginx 服务器进程发送 SIGHUP 信号(执行这个命令后拉起的 Nginx 进程并不会重新启动服务器,而是仅用于发送信号,在 ngx_get_options 方法中会重置 ngx_signal 全局变量,而 main 方法中检查其非 0 时就会调用 ngx_signal_process 方法向正在运行的 Nginx 服务器发送信号,之后 main 方法就会返回,新启动的 Nginx 进程退出),这样运行中的服务进程也会调用 ngx_signal_handler 方法来处理这个信号。具体代码流程如下.

新启动的 Nginx 进程:

int ngx_cdecl
main(int argc, char *const *argv)
{
    ...
    // 这个 ngx_signal 全局变量会在 ngx_get_options 函数中被赋值
    // (检测到输入的命令行参数中有 -s,则 ngx_signal 的值即为 "reload")
    if (ngx_signal) {
        return ngx_signal_process(cycle, ngx_signal);
    }
    ...
}

接着调用 ngx_signal_process 函数,调用结束后该新启动的 Nginx 直接退出。

ngx_int_t
ngx_signal_process(ngx_cycle_t *cycle, char *sig)
{
    ssize_t           n;
    ngx_pid_t         pid;
    ngx_file_t        file;
    ngx_core_conf_t  *ccf;
    u_char            buf[NGX_INT64_LEN + 2];
    
    ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
    
    ngx_memzero(&file, sizeof(ngx_file_t));
    
    // logs/nginx.pid 文件,保存着master进程的进程 ID
    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) {
        return 1;
    }
    
    n = ngx_read_file(&file, buf, NGX_INT64_LEN + 2, 0);
    
    if (ngx_close_file(file.fd) == NGX_FILE_ERROR) {
        
    }
    
    if (n == NGX_ERROR) {
        return 1;
    }
    
    while (n-- && (buf[n] == CR || buf[n] == LF)) { /* void */ }
    
    // 得到正在运行的Nginx服务器的 master 进程 ID 
    pid = ngx_atoi(buf, ++n);
    
    if (pid == (ngx_pid_t) NGX_ERROR) {
        return 1;
    }
    
    return ngx_os_signal_process(cycle, sig, pid);
}

该函数从 logs/nginx.pid 文件中获取到正在运行的 Nginx 服务器的 master 进程的 ID 后,调用 ngx_os_signal_process 函数进行处理。

ngx_int_t 
ngx_os_signal_process(ngx_cycle_t *cycle, char *name, ngx_pid_t pid)
{
    ngx_signal_t *sig;
    
    // 首先确定将要发送给 pid 的命令 name 在 signals 数组中存在
    for (sig == signals; sig->signo != 0; sig++) {
        if (ngx_strcmp(name, sig->name) == 0) {
            // 找到后向该 pid 发送 name 命令对应的信号
            if (kill(pid, sig->signo) != -1) {
                return 0;
            }
            
        }
    }
    
    return 1;
}

发送完信号后,这个新启动的 Nginx(即 ./nginx -s reload) 就直接退出了.接着,正在运行的 Nginx 服务器接收到该 reload 命令对应的信号(SIGHUP)后,将会调用 ngx_signal_handler 函数进行处理.

Nginx 在定义了 ngx_signal_t 类型的 signals 数组后,ngx_init_signals 方法会初始化所有的信号:

ngx_int_t
ngx_init_signals(ngx_log_t *log)
{
    ngx_signal_t      *sig;
    struct sigaction   sa;
    
    // 遍历 signals 数组,处理每一个 ngx_signal_t 类型的结构体
    for (sig = signals; sig->signo != 0; sig++) {
        ngx_memzero(&sa, sizeof(struct sigaction));
        
        if (sig->handler) {
            // 设置信号的处理方法为 handler
            sa.sa_sigaction = sig->handler;
            sa.sa_flags = SA_SIGINFO;
            
        } else {
            sa.sa_handler = SIG_IGN;
        }
        
        // 将信号掩码 sa.sa_mask 全部置为 0
        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;
}

调用该函数为所有指定的信号设置好信号处理函数后,进程就可以处理信号了。若需要 Nginx 处理新的信号,则可以直接向 signals 数组中添加新的 ngx_signal_t 成员。

2. 信号量

信号量是用来保证两个或多个代码段不被并发访问,是一种保证共享资源有序访问的工具。使用信号量作为互斥锁有可能导致进程睡眠,因此,要谨慎使用,特别是对于 Nginx 这种每一个进程同时处理着数以万计请求的服务器来说,这种导致睡眠的操作将有可能造成性能大幅降低。

Nginx 仅把信号量作为简单的互斥锁来使用。使用信号量前,首先调用 sem_init 方法初始化信号量。

2.1 ngx_shmtx_create: 初始化一个信号量

ngx_int_t 
ngx_shmtx_create(ngx_shmtx_t *mtx, ngx_shmtx_sh_t *addr, u_char *name)
{
    mtx->lock = &addr->lock;
    
    if (mtx->spin == (ngx_uint_t) -1) {
        return NGX_OK;
    }
    
    mtx->spin = 2048;

#if (NGX_HAVE_POSIX_SEM)
    
    mtx->wait = &addr->wait;
    
    // 初始化一个未命名的信号量,并且该信号量是在进程间共享,
    // 信号量的初值为 0
    if (sem_init(&mtx->sem, 1, 0) == -1) {
        
    } else {
        mtx->semaphort = 1;
    }

#endif

    return NGX_OK;
}

2.2 ngx_shmtx_destroy: 销毁信号量

void
ngx_shmtx_destroy(ngx_shmtx_t *mtx)
{
#if (NGX_HAVE_POSIX_SEM)

    if (mtx->semaphore) {
        // 销毁该信号量,
        // 注,只有在不存在进程在等待一个信号量时才能安全销毁信号量
        if (sem_destroy(&mtx->sem) == -1) {
            ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_errno,
                          "sem_destroy() failed");
        }
    }

#endif
}

3. 文件锁

Linux 内核提供了基于文件的互斥锁,而 Nginx 框架封装了 3 个方法, 提供给 Nginx 模块和文件互斥锁来保护共享数据。

Nginx 中基于文件的互斥锁是通过 fcntl 方法实现的:

int fcntl(int fd, int cmd, struct flock *lock);
  • fd: 必须是已经打开成功的文件句柄。实际上,nginx.conf 文件中的 lock_file 配置项指定的文件路径,就是用于文件互斥锁的,这个文件被打开后得到的句柄,将会作为 fd 参数传递给 fcntl 方法,提供一种锁机制。
  • cmd:表示执行的锁操作。在 Nginx 中只会有两个值:F_SETLK 和 F_SETLKW,它们都表示试图获得互斥锁,但:
    • 使用 F_SETLK 时如果互斥锁已经被其他进程占用,fcntl 方法不会等待其他进程释放锁且自己拿到锁后才返回,而是立即返回获取互斥锁失败;
    • 使用 F_SETLKW 时则不同,锁被占用后 fcntl 方法会一直等待,在其他进程没有释放锁时,当前进程就会阻塞在 fcntl 方法中,这种阻塞会导致当前进程由可执行状态转为睡眠状态。
  • lock:描述了这个锁的信息。类型为 flock 结构体,如下:

    struct flock {
    ...
    // 锁类型,取值为F_RDLCK, F_WRLCK, F_UNLCK
    short l_type;
    
    // 锁区域起始地址的相对位置
    short l_whence;
    
    // 锁区域起始地址偏移量,同 l_whence 共同确定锁区域
    long l_start;
    
    // 锁的长度, 0 表示锁至文件末
    long l_len;
    
    // 拥有锁的进程 ID
    long l_pid;
    }

    从 flock 结构体中可以看出,文件锁的功能不仅仅局限于普通的互斥锁,它还可以锁住文件中的部分内容。但 Nginx 封装的文件锁仅仅用于保护代码段的顺序执行(如,在进行负载均衡时,使用互斥锁保证同一时刻仅有一个 worker 进程可以处理新的 TCP 连接),使用方式要简单得多:一个 lock_file 文件对应一个全局互斥锁,而且它对 master 进程或者 worker 进程都生效。因此,对于 l_start、l_len、l_Pid,都填为 0,而 l_whence 则填为 SEEK_SET,只需要这个文件提供一个锁。l_type 的值则取决于用户是想实现阻塞睡眠锁还是想实现非阻塞不会睡眠的锁。

对于文件锁,Nginx 封装了 3 个方法:

  • ngx_trylock_fd:实现了不会阻塞进程、不会使得进程进入睡眠状态的互斥锁;
  • ngx_lock_fd:提供的互斥锁在锁已经被其他进程拿到时将会导致当前进程进入睡眠状态,直到顺利拿到这个锁后,当前进程才会被 Linux 内核重新调度,所以它是阻塞操作;
  • ngx_unlock_fd:用于释放互斥锁。

3.1 ngx_trylock_fd

ngx_err_t 
ngx_trylock_fd(ngx_fd_t fd)
{
    struct flock fl;
    
    ngx_memzero(&fl, sizeof(struct flock));
    fl.l_type = F_WRLCK;
    fl.l_whence = SEEK_SET;
    
    // F_SETLK 表示若获取写锁失败则立刻返回,不会导致当前进程阻塞
    if (fcntl(fd, F_SETLK, &fl) == -1) {
        // 若返回的错误码为 NGX_EAGAIN 或 NGX_EACCESS 时表示
        // 当前没有拿到互斥锁,否则认为 fcntl 执行错误
        return ngx_errno;
    }
    
    return 0;
}

3.2 ngx_lock_fd

ngx_err_t
ngx_lock_fd(ngx_fd_t fd)
{
    struct flock  fl;

    ngx_memzero(&fl, sizeof(struct flock));
    fl.l_type = F_WRLCK;
    fl.l_whence = SEEK_SET;

    // F_SETLKW 表示当没有获取到锁时将会导致当前进程进入阻塞,
    // 直到获取到锁时才会再次获得调度
    if (fcntl(fd, F_SETLKW, &fl) == -1) {
        return ngx_errno;
    }

    return 0;
}

3.3 ngx_unlock_fd

ngx_err_t
ngx_unlock_fd(ngx_fd_t fd)
{
    struct flock  fl;

    ngx_memzero(&fl, sizeof(struct flock));
    fl.l_type = F_UNLCK;
    fl.l_whence = SEEK_SET;

    // 释放当前进程已经拿到的互斥锁
    if (fcntl(fd, F_SETLK, &fl) == -1) {
        return  ngx_errno;
    }

    return 0;
}

以上是关于Nginx之进程间的通信机制(信号信号量文件锁)的主要内容,如果未能解决你的问题,请参考以下文章

请教一个Linux下C语言的进程间的信号问题

进程间的通信方式

几种进程间的通信方式

用于进程间通信的机制

python网络编程--管道,信号量,Event,进程池,回调函数

进程间的几种通信方式的比较和线程间的几种