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;
}

原文地址:https://www.cnblogs.com/jimodetiantang/p/9192489.html

时间: 2024-11-08 05:35:43

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

Nginx之进程间的通信机制(Nginx频道)

1. Nginx 频道 ngx_channel_t 频道是 Nginx master 进程与 worker 进程之间通信的常用工具,它是使用本机套接字实现的,即 socketpair 方法,它用于创建父子进程间使用的套接字. #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socketpair(int domain, int type, int protocol, int sv[2]); 这个方法

从AIDL开始谈Android进程间Binder通信机制

本文首先概述了Android的进程间通信的Binder机制,然后结合一个AIDL的例子,对Binder机制进行了解析. 概述 我们知道,在Android app中的众多activity,service等组件可以运行在同一进程中,也可以运行在不同进程中.当组件运行在同一进程中进行通信就显得比较简单,在之前的Android线程间通信机制中已经讲过了:而当它们运行在不同的进程中时,就需要使用我们本文中所要介绍的Binder机制了. Binder作为一种进程间通信机制,负责提供远程调用的功能(RPC),

操作系统 进程间的通信 之 信号 消息队列 共享内存 浅析

[几个易混淆的相关概念] 进程互斥:指在多道程序环境下,每次只允许一个进程对临界资源进行访问. 进程同步:指多个相关进程在执行次序上的协调. 临界资源:在一段时间内只允许一个进程访问的资源. 临界区:每个进程中访问临界资源的那段代码. [进程通信] 现在常用的进程间通信方式有信号.信号量.消息队列.共享内存.通信,是一个广义的意义,不仅仅指传递一些 message.进程通信就是指不同进程之间进程数据共享和数据交换. [信号和信号量] 信号和信号量是不同的,他们虽然都可用来实现同步和互斥,但信号是

Nginx学习——Nginx进程间的通信

nginx进程间的通信 进程间消息传递 共享内存 共享内存还是Linux下提供的最主要的进程间通信方式,它通过mmap和shmget系统调用在内存中创建了一块连续的线性地址空间,而通过munmap或者shmdt系统调用可以释放这块内存.使用共享内存的优点是当多个进程使用同一块共享内存时,在不论什么一个进程改动了共享内存中的内容后,其它进程通过訪问这段共享内存都可以得到改动后的内容. Nginx定义了ngx_shm_t结构体.用于描写叙述一块共享内存, typedef struct{ //指向共享

linux 进程间的通信

现在linux使用的进程间通信方式:(1)管道(pipe)和有名管道(FIFO)(2)信号(signal)(3)消息队列(4)共享内存(5)信号量(6)套接字(socket) 为何进行进程间的通信:A.数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几M字节之间B.共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到.C.通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程).D.资源共享

Linux进程间的通信

一.管道 管道是Linux支持的最初Unix IPC形式之一,具有以下特点: A. 管道是半双工的,数据只能向一个方向流动: B. 需要双工通信时,需要建立起两个管道: C. 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程): D. 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中. 匿名管道的创建:该函数创建的管道的两端处于一个进程中间,在实际应用中没有太大意义;因此,一

Linux/UNIX之进程间的通信(2)

进程间的通信(2) 有三种IPC我们称为XSI IPC,即消息队列.信号量以及共享存储器,它们之间有很多相似之处. 标识符和键 每个内核的IPC结构(消息队列.信号量或共享存储段)都用一个非负整数的标识符加以引用.例如,为了对一个消息队列发送或取消息,只需要知道其队列标识符.与文件描述符不同,IPC标识符不是小的整数.当一个IPC结构被创建,以后被删除时,与这种结果相关的标识符连续加1,知道达到一个整型数的最大值,然后又回到0. 标识符是IPC对象的内部名.为使多个合作进程能够在同一IPC对象上

wwwhy76888com理解容器间link通信机制199O8836661

一.什么是docker的link机制? 同一个宿主机上的多个docker容器之间如果想进行通信,可以通过使用容器的ip地址来通信,也可以通过宿主机的ip加上容器暴露出的端口号来通信,前者会导致ip地址的硬编码,不方便迁移,并且容器重启后ip地址会改变,除非使用固定的ip,后者的通信方式比较单一,只能依靠监听在暴露出的端口的进程来进行有限的通信.通过docker的link机制可以通过一个name来和另一个容器通信,link机制方便了容器去发现其它的容器并且可以安全的传递一些连接信息给其它的容器.其

【转】使用AIDL实现进程间的通信之复杂类型传递

使用AIDL实现进程间的通信之复杂类型传递 首先要了解一下AIDL对Java类型的支持. 1.AIDL支持Java原始数据类型. 2.AIDL支持String和CharSequence. 3.AIDL支持传递其他AIDL接口,但你引用的每个AIDL接口都需要一个import语句,即使位于同一个包中. 4.AIDL支持传递实现了android.os.Parcelable接口的复杂类型,同样在引用这些类型时也需要import语句.(Parcelable接口告诉Android运行时在封送(marsha