• 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. 信号量

    信号量的详细可参考: Linux编程之信号量

    信号量是用来保证两个或多个代码段不被并发访问,是一种保证共享资源有序访问的工具。使用信号量作为互斥锁有可能导致进程睡眠,因此,要谨慎使用,特别是对于 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编程之文件锁

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

    4. 互斥锁

    基于原子操作、信号量以及文件锁,Nginx 在更高层次封装了一个互斥锁。

    互斥锁的 5 种操作方法:

    • ngx_shmtx_create(ngx_shmtx_t *mtx, ngx_shmtx_sh_t *addr, u_char *name):
      • 参数:
        • mtx 表示待操作的 ngx_shmtx_t 类型互斥锁;
        • 当互斥锁由原子变量实现时,参数 addr 表示要操作的原子变量锁,而互斥锁由文件实现时,参数 addr 没有任何意义;
        • 参数 name 仅当互斥锁由文件实现时才有意义,它表示这个文件所在的路径及文件名。
      • 意义:初始化 mtx 互斥锁。
    • ngx_shmtx_destroy(ngx_shmtx_t *mtx):
      • 参数:mtx 表示待操作的 ngx_shmtx_t 类型互斥锁
      • 意义:销毁 mtx 互斥锁
    • ngx_shmtx_trylock(ngx_shmtx_t *mtx):
      • 参数:mtx 表示待操作的 ngx_shmtx_t 类型互斥锁
      • 意义:无阻塞地试图获取互斥锁,返回 1 表示获取互斥锁成功,返回 0 表示获取互斥锁失败。
    • ngx_shmtx_lock(ngx_shmtx_t *mtx):
      • 参数:mtx 表示待操作的 ngx_shmtx_t 类型互斥锁
      • 以阻塞进程的方式获取互斥锁,在方法返回时就已经持有互斥锁了。
    • ngx_shmtx_unlock(ngx_shmtx_t *mtx):
      • 参数:mtx 表示待操作的 ngx_shmtx_t 类型互斥锁
      • 意义:释放互斥锁。

    ngx_shmtx_t 结构体

    typedef struct {
    #if (NGX_HAVE_ATOMIC_OPS)
        // 原子变量锁
        ngx_atomic_t  *lock;
    #if (NGX_HAVE_POSIX_SEM)
        ngx_atomic_t  *wait;
        // semaphore 为 1 时表示获取锁将可能使用到信号量
        ngx_uint_t     semaphore;
        // sem 就是信号量锁
        sem_t          sem;
    #endif
    #else
        // 使用文件锁时 fd 表示使用的文件句柄
        ngx_fd_t       fd;
        // 使用文件锁时对应的文件名
        u_char        *name;
    #endif
        // 自旋次数,表示在自旋状态下等待其他处理器执行结果中释放锁的时间。
        // 由文件锁实现时,spin 没有任何意义
        ngx_uint_t     spin;
    } ngx_shmtx_t;
    

    ngx_shmtc_t 结构体涉及两个宏:NGX_HAVE_ATOMIC_OPS、NGX_HAVE_POSIX_SEM,这两个宏对应着互斥锁的 3 中不同实现。

    • 第一种实现:当不支持原子操作时,会使用文件锁来实现 ngx_shmtx_t 互斥锁,这时它仅有 fd 和 name 成员(实际上还有 spin 成员,但这时没有任何意义)。这两个成员使用上面介绍的文件锁来提供阻塞、非阻塞的互斥锁。
    • 第二种实现:支持原子操作却又不支持信号量。
    • 第三种实现:在支持原子操作的同时,操作系统也支持信号量。

    后两种实现的唯一区别是 ngx_shmtx_lock 方法执行时的效果,也就是说,支持信号量只会影响阻塞进程的 ngx_shmtx_lock 方法持有锁的方式。当不支持信号量时,ngx_shmtx_lock 取锁与自旋锁是一致的,而支持信号量后,ngx_shmtx_lock 将在 spin 指定的一段时间内自旋等待其他处理器释放锁,如果达到 spin 上限还没有获取到锁,那么僵会使用 sem_wait 使得当前进程进入睡眠状态,等其他进程释放了锁内核才会唤醒这个进程。当然,在实际实现中,Nginx 做了巧妙的设计,它使得 ngx_shmtx_lock 方法在运行一段时间后,如果其他进程始终不释放锁,那么当前进程将有可能强制性地获得到这把锁,这也是出于 Nginx 不宜使用阻塞进程的睡眠锁方面的考虑。

    4.1 文件锁实现的 ngx_shmtx_t 锁

    4.1.1 ngx_shmtx_create

    ngx_int_t 
    ngx_shmtx_create(ngx_shmtx_t *mtx, ngx_shmtx_sh_t *addr, u_char *name)
    {
        if (mtx->name) {
            // 如果 ngx_shmtx_t 中的 name 成员已经赋值了,且与 name 相同,
            // 意味着 mtx 互斥锁已经初始化过了
            if (ngx_strcmp(name, mtx->name) == 0) {
                mtx->name = name;
                reutrn NGX_OK;
            }
            
            // 不同则先销毁 mtx 中的互斥锁
            ngx_shmtx_destroy(mtx);
        }
        
        // 打开该用于文件互斥锁的文件
        mtx->fd = ngx_open_file(name, NGX_FILE_RDWR, NGX_FILE_CREATE_OR_OPEN, 
                                NGX_FILE_DEFAULT_ACCESS);
        
        if (mtx->fd == NGX_INVALID_FILE) {
            ngx_log_error(NGX_LOG_EMERG, ngx_cycle->log, ngx_errno,
                          ngx_open_file_n " "%s" failed", name);
            return NGX_ERROR;
        }
        
        // 由于只需要这个文件在内核中的 i-node 信息,所以可以把文件删除,只要
        // fd 可就行了,这里是使用 unlink 移除一个链接,但由于该文件的描述符
        // 还未关闭,因此系统实际上不会删除这个文件.
        if (ngx_delete_file(name) == NGX_FILE_ERROR) {
            ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_errno,
                          ngx_delete_file_n " "%s" failed", name);
        }
    
        mtx->name = name;
    
        return NGX_OK;
    }
    

    unlink 系统调用移除一个链接(删除一个文件名),且如果此链接是指向文件的最后一个链接,那么还将移除文件本身。内核除了为每个 i-node 维护链接计数之外,还对文件的打开文件描述符计数。当移除指向文件的最后一个链接时,如果仍有进程持有指代该文件的打开文件描述符,那么在关闭所有此类描述符之前,系统实际上将不会删除该文件。这一特性的妙用在于允许取消对文件的链接,而无需担心是否有其他进程已将其打开。(然而,对于链接数已降为 0 的打开文件,就无法将文件名与其重新关联起来。)此外,基于上述事实,还可以玩点小技巧:先创建并打开一个临时文件,随机取消对文件的链接(unlink),然后在程序中继续使用该文件。

    4.1.2 ngx_shmtx_destroy

    void
    ngx_shmtx_destroy(ngx_shmtx_t *mtx)
    {
        // 关闭 ngx_shmtx_t 结构体中的 fd 句柄
        if (ngx_close_file(mtx->fd) == NGX_FILE_ERROR) {
            ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_errno,
                          ngx_close_file_n " "%s" failed", mtx->name);
        }
    }
    

    4.1.3 ngx_shmtx_trylock

    ngx_uint_t
    ngx_shmtx_trylock(ngx_shmtx_t *mtx)
    {
        ngx_err_t  err;
        
        // 非阻塞方式获取文件锁
        err = ngx_trylock_fd(mtx->fd);
        
        // 成功获取到锁
        if (err == 0) {
            return 1;
        }
        
        // 表示现在锁已经被其他进程持有了
        if (err == NGX_EAGAIN) {
            return 0;
        }
        
    #if __osf__ /* Tru64 UNIX */    
        
        if (err == NGX_EACCES) {
            return 0;
        }
    
    #endif
    
        ngx_log_abort(err, ngx_trylock_fd_n " %s failed", mtx->name);
    
        return 0;
    }
    

    4.1.4 ngx_shmtx_lock

    void
    ngx_shmtx_lock(ngx_shmtx_t *mtx)
    {
        ngx_err_t  err;
    
        // 使用阻塞方式获取文件锁,若没有获取到锁,则会阻塞代码的继续执行,
        // 即使当前进程处于睡眠状态,等待其他进程释放锁后内核唤醒它
        err = ngx_lock_fd(mtx->fd);
    
        if (err == 0) {
            return;
        }
    
        ngx_log_abort(err, ngx_lock_fd_n " %s failed", mtx->name);
    }
    

    该函数没有返回值,因为它一旦返回就相当于获取到锁了.

    4.1.5 ngx_shmtx_unlock

    void
    ngx_shmtx_unlock(ngx_shmtx_t *mtx)
    {
        ngx_err_t  err;
    
        // 删除该文件锁
        err = ngx_unlock_fd(mtx->fd);
    
        if (err == 0) {
            return;
        }
    
        ngx_log_abort(err, ngx_unlock_fd_n " %s failed", mtx->name);
    }
    

    4.2 原子变量实现的 ngx_shmtx 锁

    ngx_shmtx_t 结构中的 lock 原子变量表示当前锁的状态。mtx->lock 当前值为 0 表示当前没有进程持有该锁,大于 0 则表示有进程持有该锁。

    4.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;
    
        // 当 spin 为 -1 时,表示不能使用信号量,直接返回成功
        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) {
            ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_errno,
                          "sem_init() failed");
        } else {
            // 信号量初始化成功后,设置 semaphore 标志位为 1
            mtx->semaphore = 1;
        }
    
    #endif
    
        return NGX_OK;
    }
    

    spin 和 semaphore 成员都将决定 ngx_shmtx_lock 阻塞锁的行为。

    4.2.2 ngx_shmtx_destroy

    void
    ngx_shmtx_destroy(ngx_shmtx_t *mtx)
    {
    #if (NGX_HAVE_POSIX_SEM)
    
        // 当这把锁的 spin 不为 -1,且初始化信号量成功,
        // semaphore 标志位才为 1
        if (mtx->semaphore) {
            // 销毁信号量
            if (sem_destroy(&mtx->sem) == -1) {
                ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_errno,
                              "sem_destroy() failed");
            }
        }
    
    #endif
    }
    

    4.2.3 ngx_shmtx_trylock

    ngx_uint_t
    ngx_shmtx_trylock(ngx_shmtx_t *mtx)
    {
        // 若当前 lock 为 0,即表示没有进程持有该锁,则
        // 调用 ngx_atomic_cmp_set 函数比较 mtx->lock 的值与 0 是否
        // 相等,若相等,则将 ngx_pid 的值赋给 mtx->lock,返回 1,
        return (*mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid));
    }
    

    该函数返回 1 表示获取锁成功。

    4.2.4 ngx_shmtx_lock

    void
    ngx_shmtx_lock(ngx_shmtx_t *mtx)
    {
        ngx_uint_t         i, n;
    
        ngx_log_debug0(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0, "shmtx lock");
    
        // 若没有拿到锁是不会跳出循环的
        for ( ;; ) {
    
            // 若当前 lock 为 0(即还未有进程持有该锁),则获取该锁,并将 mtx->lock 
            // 的值设置为 ngx_pid
            if (*mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid)) {
                return;
            }
    
            // 若已经有进程持有该锁了,且在多处理器下,spin 值才有意义
            if (ngx_ncpu > 1) {
        
                // 如下为执行自旋的代码,这里不会使当前进程进入睡眠,而是始终
                // 保持进程的可执行状态,每当内核调度到这个进程执行时就持续
                // 检查是否已经可以获取锁.
    
                // 循环执行 pause,检查锁是否已经释放
                for (n = 1; n < mtx->spin; n <<= 1) {
    
                    // 随着长时间没有获取到锁,将会执行更多次的pause才会检查锁
                    for (i = 0; i < n; i++) {
                        // 对于多处理器系统,执行 ngx_cpu_pause 可以降低功耗
                        ngx_cpu_pause();
                    }
    
                    // 检测当前锁是否已经被释放了,若已经为 0,即释放了,
                    // 则获取该锁,并将 mtx->lock 设置为 ngx_pid
                    if (*mtx->lock == 0
                        && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid))
                    {
                        // 持有锁成功后立即返回
                        return;
                    }
                }
            }
    
    // 若当前系统支持信号量
    #if (NGX_HAVE_POSIX_SEM)
    
            // semaphore 标志位为 1 时才使用信号量
            if (mtx->semaphore) {
                // 将该原子变量 mtx->wait 值加上 1
                (void) ngx_atomic_fetch_add(mtx->wait, 1);
    
                // 若 lock 为 0,则调用 ngx_atomic_cmp_set 立即获取该锁
                if (*mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid)) {
                    // 若获取锁成功后,则将 mtx->wait 的值加上 -1,即减 1,然后返回
                    (void) ngx_atomic_fetch_add(mtx->wait, -1);
                    return;
                }
    
                ngx_log_debug1(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0,
                               "shmtx wait %uA", *mtx->wait);
    
                // 若上面检测到已经有其他进程持有锁了,则这里调用
                // sem_wait 递减当前信号量mtx->sem 的值,若当前信号量的值
                // 大于 0,则 sem_wait() 会立即返回;若当前信号量为0,则
                // sem_wait() 会阻塞直到信号量的值大于 0,当信号量值大于
                // 0时该信号量就被递减并且sem_wait会返回.
                while (sem_wait(&mtx->sem) == -1) {
                    ngx_err_t  err;
    
                    err = ngx_errno;
    
                    // sem_wait除了被信号处理器中断返回 EINTR 错误外,其他的
                    // 的错误码都表示sem_wait发生了未知的错误
                    if (err != NGX_EINTR) {
                        ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, err,
                                      "sem_wait() failed while waiting on shmtx");
                        break;
                    }
                }
    
                ngx_log_debug0(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0,
                               "shmtx awoke");
    
                continue;
            }
    
    #endif
    
            // 当前进程仍然处于可执行状态,但暂时"让出"处理器,使得处理器优先调度
            // 其他可执行状态的进程,这样,在进程被内核再次调度时,在 for 循环
            // 代码中可以期望其他进程释放锁。
            ngx_sched_yield();
        }
    }
    

    4.2.5 ngx_shmtx_unlock

    void
    ngx_shmtx_unlock(ngx_shmtx_t *mtx)
    {
        if (mtx->spin != (ngx_uint_t) -1) {
            ngx_log_debug0(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0, "shmtx unlock");
        }
    
        // 将mtx->lock中的值与 ngx_pid比较,相等,则设置为0,
        // 即表示释放当前信号量
        if (ngx_atomic_cmp_set(mtx->lock, ngx_pid, 0)) {
            ngx_shmtx_wakeup(mtx);
        }
    }
    

    该函数中信号量释放成功后,即嗲用 ngx_shmtx_wakeup 函数.

    static void
    ngx_shmtx_wakeup(ngx_shmtx_t *mtx)
    {
    #if (NGX_HAVE_POSIX_SEM)
        ngx_atomic_uint_t  wait;
    
        // 若当前没有初始化好信号量,则直接返回
        if (!mtx->semaphore) {
            return;
        }
    
        for ( ;; ) {
    
            wait = *mtx->wait;
    
            // 若原子变量 wait 小于等于 0的话,表示当前没有进程在等待获取
            // 该信号量,则直接返回
            if ((ngx_atomic_int_t) wait <= 0) {
                return;
            }
    
            // 否则有进程正在睡眠等待获取该信号量,则将该信号量wait值减1
            if (ngx_atomic_cmp_set(mtx->wait, wait, wait - 1)) {
                // 成功,直接退出循环
                break;
            }
        }
    
        ngx_log_debug1(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0,
                       "shmtx wake %uA", wait);
    
        // 发布该信号量,即将该信号量值加1
        // 若sem_post调用之前信号量的值为 0,并且其他某个进程正在因等待递减这个
        // 信号量而阻塞,那么该进程会被唤醒,它的sem_wait调用会继续往前执行来
        // 递减这个信号量。如果多个进程在sem_wait中阻塞了,并且这写进程的调度
        // 采用的是默认的循环时间分享策略,则哪个进程被唤醒并允许递减这个信号
        // 量是不确定的.
        if (sem_post(&mtx->sem) == -1) {
            ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_errno,
                          "sem_post() failed while wake shmtx");
        }
    
    #endif
    }
    

    注:Nginx 封装的锁不会直接使用信号量,因为一旦获取信号量互斥锁失败,进程就会进入睡眠状态,这会导致其他请求 "饿死".

  • 相关阅读:
    内存寻址:逻辑地址到物理地址的转化
    变量类型,变量作用域,变量存储空间,变量生命周期
    位运算计算与位运算应用
    sizeof()计算
    位域(位段)
    自然对齐和强制对齐
    内存中的数据对齐
    用汇编编写子程序,可以显示字符串到屏幕指定位置
    汇编语言 实验9 根据材料编程
    80x25彩色字符模式
  • 原文地址:https://www.cnblogs.com/jimodetiantang/p/9192489.html
Copyright © 2020-2023  润新知