百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 优雅编程 > 正文

Nginx之进程间的通信机制(共享内存、原子操作)

sinye56 2024-11-24 21:29 12 浏览 0 评论

1. 概述

详细教程资料+课件 关注+后台私信;资料;两个字可以免费视频领取+文档+各大厂面试题 资料内容包括:C/C++,Linux,golang,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,嵌入式 等。

Linux 提供了多种进程间传递消息的方式,如共享内存、套接字、管道、消息队列、信号等,而 Nginx 框架使用了 3 种传递消息的传递方式:共享内存、套接字、信号。

Nginx 主要使用了 3 种同步方式:原子操作、信号量、文件锁。

由于 Nginx 的每个 worker 进程都会同时处理千万个请求,所以处理任何一个请求时都不应该阻塞当前进程处理后续的其他请求。

2. 共享内存

共享内存是 Linux 下提供的最基本的进程间通信方法,它通过 mmap 或者 shmgat 系统调用在内存中创建了一块连续的线性地址空间,而通过 munmap 或者 shmdt 系统调用可以释放这块内存。

虽然 mmap 可以以磁盘文件的方式映射共享内存,但在 Nginx 封装的共享内存操作方法中是没有使用到映射文件功能的。

Nginx 定义了 ngx_shm_t 结构体,用于描述一块共享内存:

typedef struct {
    /* 执行共享内存的起始地址 */
    u_char      *addr;
    /* 共享内存的长度 */
    size_t       size;
    /* 这块共享内存的名称 */
    ngx_str_t    name;
    /* 记录日志的 ngx_log_t 对象 */
    ngx_log_t   *log;
    /* 表示共享内存是否已经分配过的标志位,为 1 时表示已经存在 */
    ngx_uint_t   exists; /* unsigned exists:1 */
}ngx_shm_t;

操作 ngx_shm_t 结构体的方法有以下两个:

  • ngx_shm_alloc:用于分配新的共享内存;
  • ngx_shm_free:用于释放已经存在的共享内存。

详细教程资料+课件 关注+后台私信;资料;两个字可以免费视频领取+文档+各大厂面试题 资料内容包括:C/C++,Linux,golang,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,嵌入式 等。

mmap 系统调用简述

void *mmap(void *start, size_t length, int prot, int flags, 
                int fd, off_t offset);

mmap 可以将磁盘文件映射到内存中,直接操作内存时 Linux 内核将负责同步内存和磁盘文件中的数据:

  • fd 参数就指向需要同步的磁盘文件
  • offset 则代表从文件的这个偏移量开始共享。
  • 当 flags 参数中加入 MAP_ANON 或者 MAP_ANONYMOUS 参数时表示不使用文件映射方式,这时 fd 和 offset 参数就没有意义了,也不需要传递,此时的 mmap 方法和 ngx_shm_alloc 的功能几乎完全相同。
  • length 参数就是将要在内存中开辟的线性地址空间大小。
  • prot 参数则是操作这段共享内存的方式(如只读或可读可写)。
  • start 参数说明希望的共享内存起始映射地址,通常设为 NULL,即由内存选择映射的起始地址。

MAP_ANON 是 MAP_ANONYMOUS 的同义词,已过时。表示不使用文件映射方式,并且共享内存被初始化为0,因此忽略 mmap 中的 fd 和 offset 参数,但是为了可移植性,当 MAP_ANONYMOUS(或 MAP_ANON)被指定时,fd 应该设置为 -1。

如下为使用 mmap 实现的 ngx_shm_alloc 方法:

ngx_int_t ngx_shm_alloc(ngx_shm_t *shm) { /* 开辟一块 shm->size 大小且可读/写的共享内存,内存首地址存放在 shm->addr 中 */ shm->addr = (u_char *)mmap(NULL, shm->size, PROT_READ|PROT_WRITE, MAP_ANON|MAP_SHARED, -1, 0); if (shm->addr == MAP_FAILED) { ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno, "mmap(MAP_ANON|MAP_SHARED, %uz) failed", shm->size); return NGX_ERROR; } return NGX_OK; }

当不在使用共享内存时,需要调用 munmap 或者 shmdt 来释放共享内存:

  • start:指向共享内存的首地址
  • length:表示这段共享内存的长度

Nginx 的 ngx_shm_free 方法封装了该 munmap 方法

void ngx_shm_free(ngx_shm_t *shm) { if (munmap((void*) shm->addr, shm->size) == -1) { ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno, "munmap(%p, %uz) failed", shm->addr, shm->size); } }

Nginx 各进程间共享数据的主要方式就是使用共享内存(在使用共享内存时,Nginx 一般是由 master 进程创建,在 master 进程 fork 出 worker 子进程后,所有的进程开始使用这块内存中的数据)。

Nginx 的共享内存有三种实现:

  • 不映射文件使用 mmap 分配共享内存(即上面的代码)
  • 以 /dev/zero 文件使用 mmap 映射共享内存
  • 用 shmget 调用来分配共享内存

3. 原子操作

原子操作(atomic operation)指的是由多步操作组成的一个操作。如果该操作不能原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。

typedef volatile ngx_atomic_uint_t  ngx_atomic_t;

Nginx 提供了两个方法来修改原子变量的值.

ngx_atomic_cmp_set

static ngx_inline ngx_atomic_uint_t
ngx_atomic_cmp_set(ngx_atomic_t *lock, ngx_atomic_uint_t old,
    ngx_atomic_uint_t set)

该方法会将 old 参数与原子变量 lock 的值进行比较,若相等,则将 lock 设为参数 set,同时返回 1;若不等,则直接返回 0。

ngx_atomic_fetch_add

static ngx_inline ngx_atomic_int_t
ngx_atomic_fetch_add(ngx_atomic_t *value, ngx_atomic_int_t add)

该方法会把原子变量 value 的值加上参数 add,同时返回之前 value 的值。

由于各种硬件体系架构,原子操作的实现不尽相同,如下为 Nginx 基于几个硬件体系关于原子操作的实现。

当无法实现原子操作时,就只能用 volatile 关键字在 C 语言级别上模拟原子操作了。事实上,绝大多数体系架构都支持原子操作。

ngx_atomic_cmp_set 的实现如下:

static ngx_inline ngx_atomic_uint_t
ngx_atomic_cmp_set(ngx_atomic_t *lock, ngx_atomic_uint_t old,
    ngx_atomic_uint_t set)
{
    /* 当原子变量 lock 与 old 相等时,才能把 set 设置到 lock 中 */
    if (*lock == old) {
        *lock = set;
        return 1;
    }

    /* 若 lock 与 set 不等,返回 0 */
    return 0;
}

ngx_atomic_fetch_add 的实现如下:

static ngx_inline ngx_atomic_int_t
ngx_atomic_fetch_add(ngx_atomic_t *value, ngx_atomic_int_t add)
{
    ngx_atomic_int_t  old;

    /* 将原子变量 value 加上 add 后,返回原先 value 的值 */
    old = *value;
    *value += add;

    return old;

x86 架构下的原子操作

gnu lib提供原子操作的实现
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
 
static int count = 0;
 
 
void *test_func(void *arg)
{
        int i=0;
        for(i=0;i<20000;++i){
//                __sync_fetch_and_add(&count,1);
                count ++;
        }
        return NULL;
}
 
int main(int argc, const char *argv[])
{
        pthread_t id[20];
        int i = 0;
 
        for(i=0;i<20;++i){
                pthread_create(&id[i],NULL,test_func,NULL);
        }
 
        for(i=0;i<20;++i){
                pthread_join(id[i],NULL);
        }
 
        printf("%d\n",count);
        return 0;
}

2.利用汇编自己实现

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#define LOCK "lock ; "
typedef struct { volatile int counter; } atomic_t;
 
static __inline__ void atomic_inc(atomic_t *v)
 
{
 
    __asm__ __volatile__(
 
       LOCK "incl %0"
 
       :"=m" (v->counter)
 
       :"m" (v->counter));
 
}
 
static atomic_t count = {0};
void *test_func(void *arg)
{
        int i=0;
        for(i=0;i<20000;++i){
                atomic_inc(&count);
 
        }
        return NULL;
}
 
int main(int argc, const char *argv[])
{
        pthread_t id[20];
        int i = 0;
 
        for(i=0;i<20;++i){
                pthread_create(&id[i],NULL,test_func,NULL);
        }
 
        for(i=0;i<20;++i){
                pthread_join(id[i],NULL);
        }
 
        printf("%d\n",count.counter);
        return 0;
}

3.3 自旋锁

基于原子的操作,Nginx 实现了一个自旋锁。当发现锁已经被其他进程获得时,那么不会使得当前进程进入睡眠状态,始终保持进程的可执行状态,每当内核调度到这个进程执行时就持续检查是否可以获取到锁。

自旋锁主要是为多处理器操作系统而设置的,它要解决的共享资源保护场景就是进程使用锁的时间非常短(如果锁的使用时间很久,自旋锁就不合适,会占用大量的 CPU 资源)。如果使用锁的进程不太希望自己进入睡眠状态,特别它处理的是非常核心的事件时,这时就应该使用自旋锁,其实大部分情况下 Nginx 的 worker 进程最好不要进入睡眠状态,因为它非常繁忙,在这个进程的 epoll 上可能会有十万甚至百万的 TCP 连接等待着处理,进程一旦睡眠后必须等待其他事件的唤醒,这中间及其频繁的进程间切换带来的负载消耗可能无法让用户接受。

自旋锁对于单处理器操作系统来说一样是有效的,不进入睡眠状态并不意味着其他可执行状态的进程得不到执行。Linux 内核中对于每个处理器都有一个运行队列,自旋锁可以仅仅调整当前进程在运行队列中的顺序,或者调整进程的时间片,这都会为当前处理器上的其他进程提供被调度的机会,以使得锁被其他进程释放

如下为 Nginx 实现的基于原子操作的自旋锁方法 ngx_spinlock:

void
ngx_spinlock(ngx_atomic_t *lock, ngx_atomic_int_t value, ngx_uint_t spin)
{
    ngx_uint_t i, n;
    
    // 无法获取锁时进程的代码将一直在这个循环中执行
    for ( ;; ) {
        // lock 为 0 表示锁是没有被其他进程持有的,这时将 lock 值设为 value 
        // 参数表示当前进程持有了锁
        if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, value)) {
            // 获取到锁后 ngx_spinlock 方法才会返回
            return;
        }
        
        // 该变量是处理器的个数,当它大于 1 时表示处理多处理器系统中
        if (ngx_ncpu > 1) {
            // 在多处理器下,更好的做法是当前进程不要立刻"让出"正在使用的 CPU 
            // 处理器,而是等待一段时间,看看其他处理器上的进程是否会释放锁,
            // 这会减少进程间切换的次数
            for (n = 1; n < spin; n <<= 1) {
            
                // 注意,随着等待的次数越来越多,实际去检查 lock 是否被释放
                // 的频繁会越来越小。为什么?因为检查 lock 值更消耗 CPU,
                // 而执行 ngx_cpu_pause 对于 CPU 的能耗来说更为省电
                for (i = 0; i < n; i++) {
                
                    // ngx_cpu_pause 是在许多架构体系中专门为了自旋锁而提供的
                    // 指令,它会告诉CPU现在处于自旋锁等待状态,通常一些CPU
                    // 会将自己置于节能状态,降低功耗。注意,在执行
                    // ngx_cpu_pause 后,当前进程没有 "让出" 正使用的处理器
                    ngx_cpu_pasue();
                }
                
                // 检查锁是否被释放了,如果 lock 值为0且释放了锁后,就把它的值设为
                // value,当前进程持有锁成功并返回
                if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, value)) {
                    return;
                }
            }
        }
        
        // 当前进程仍然处理可执行状态,但暂时"让出"处理器,使得处理器优先调度其他
        // 可执行状态的进程,这样,在进程被内核再次调度时,在 for 循环代码中可以期望
        // 其他进程释放锁。注意,不同的内核版本对于 sched_yield 系统调用的实现可能
        // 不同,但它们的目的都是暂时 "让出" 处理器
        ngx_sched_yield();
    }
}

总结:

释放锁时需要 Nginx 模块通过 ngx_atomic_cmp_set 方法将原子变量 lock 值设为 0。

详细教程资料+课件 关注+后台私信;资料;两个字可以免费视频领取+文档+各大厂面试题 资料内容包括:C/C++,Linux,golang,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,嵌入式 等。

相关推荐

程序员:JDK的安装与配置(完整版)_jdk的安装方法

对于Java程序员来说,jdk是必不陌生的一个词。但怎么安装配置jdk,对新手来说确实头疼的一件事情。我这里以jdk10为例,详细的说明讲解了jdk的安装和配置,如果有不明白的小伙伴可以评论区留言哦下...

Linux中安装jdk并配置环境变量_linux jdk安装教程及环境变量配置

一、通过连接工具登录到Linux(我这里使用的Centos7.6版本)服务器连接工具有很多我就不一一介绍了今天使用比较常用的XShell工具登录成功如下:二、上传jdk安装包到Linux服务器jdk...

麒麟系统安装JAVA JDK教程_麒麟系统配置jdk

检查检查系统是否自带java在麒麟系统桌面空白处,右键“在终端打开”,打开shell对话框输入:java–version查看是否自带java及版本如图所示,系统自带OpenJDK,要先卸载自带JDK...

学习笔记-Linux JDK - 安装&amp;配置

前提条件#检查是否存在JDKrpm-qa|grepjava#删除现存JDKyum-yremovejava*安装OracleJDK不分系统#进入安装文件目...

Linux新手入门系列:Linux下jdk安装配置

本系列文章是把作者刚接触和学习Linux时候的实操记录分享出来,内容主要包括Linux入门的一些理论概念知识、Web程序、mysql数据库的简单安装部署,希望能够帮到一些初学者,少走一些弯路。注意:L...

测试员必备:Linux下安装JDK 1.8你必须知道的那些事

1.简介在Oracle收购Sun后,Java的一系列产品就被整合到Oracle官网中,打开官网乍眼一看也不知道去哪里下载,还得一个一个的摸索尝试,而且网上大多数都是一些Oracle收购Sun前,或者就...

Linux 下安装JDK17_linux 安装jdk1.8 yum

一、安装环境操作系统:JDK版本:17二、安装步骤第一步:下载安装包下载Linux环境下的jdk1.8,请去官网(https://www.oracle.com/java/technologies/do...

在Ubuntu系统中安装JDK 17并配置环境变量教程

在Ubuntu系统上安装JDK17并配置环境变量是Java开发环境搭建的重要步骤。JDK17是Oracle提供的长期支持版本,广泛用于开发Java应用程序。以下是详细的步骤,帮助你在Ubuntu系...

如何在 Linux 上安装 Java_linux安装java的步骤

在桌面上拥抱Java应用程序,然后在所有桌面上运行它们。--SethKenlon(作者)无论你运行的是哪种操作系统,通常都有几种安装应用程序的方法。有时你可能会在应用程序商店中找到一个应用程序...

Windows和Linux环境下的JDK安装教程

JavaDevelopmentKit(简称JDK),是Java开发的核心工具包,提供了Java应用程序的编译、运行和开发所需的各类工具和类库。它包括了JRE(JavaRuntimeEnviro...

linux安装jdk_linux安装jdk软连接

JDK是啥就不用多介绍了哈,外行的人也不会进来看我的博文。依然记得读大学那会,第一次实验课就是在机房安装jdk,编写HelloWorld程序。时光飞逝啊,一下过了十多年了,挣了不少钱,买了跑车,娶了富...

linux安装jdk,全局配置,不同用户不同jdk

jdk1.8安装包链接:https://pan.baidu.com/s/14qBrh6ZpLK04QS8ogCepwg提取码:09zs上传文件解压tar-zxvfjdk-8u152-linux-...

运维大神教你在linux下安装jdk8_linux安装jdk1.7

1.到官网下载适合自己机器的版本。楼主下载的是jdk-8u66-linux-i586.tar.gzhttp://www.oracle.com/technetwork/java/javase/downl...

window和linux安装JDK1.8_linux 安装jdk1.8.tar

Windows安装JDK1.8的步骤:步骤1:下载JDK打开浏览器,找到JDK下载页面https://d.injdk.cn/download/oraclejdk/8在页面中找到并点击“下载...

最全的linux下安装JavaJDK的教程(图文详解)不会安装你来打我?

默认已经有了linux服务器,且有root账号首先检查一下是否已经安装过java的jdk任意位置输入命令:whichjava像我这个已经安装过了,就会提示在哪个位置,你的肯定是找不到。一般我们在...

取消回复欢迎 发表评论: