0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

nginx内存池源码设计

科技绿洲 来源:Linux开发架构之路 作者:Linux开发架构之路 2023-11-13 11:51 次阅读

造轮子内存池原因引入

作为C/C++程序员, 相较JAVA程序员的一个重大特征是我们可以直接访问内存, 自己管理内存, 这个可以说是我们的特色, 也是我们的苦楚了.

java可以有虚拟机帮助管理内存, 但是我们只能自己管理内存, 一不小心产生了内存泄漏问题, 又特别是服务器的内存泄漏问题, 进程不死去, 泄漏的内存就一直无法回收.

所以对于内存的管理一直是我们C系列程序员深挖的事情.

所以对于C++有智能指针这个东西. 还有内存池组件. 内存池组件也不能完全避免内存泄漏, 但是它可以很好的帮助我们定位内存泄漏的点, 以及可以减少内存申请和释放的次数, 提高效率

大量的malloc/free小内存所带来的弊端

弊端

  1. malloc/free的底层是调用系统调用, 这两者库函数是对于系统调用的封装, 频繁的系统调用所带来的用户内核态切换花费大量时间, 大大降低系统执行效率
  2. 频繁的申请小内存, 带来的大量内存碎片, 内存使用率低下且导致无法申请大块的内存
  3. 没有内存回收机制, 很容易造成内存泄漏

内存碎片出现原因解释

  1. 内部内存碎片定义: 已经被分配出去了(明确分配到一个进程), 但是无法被利用的空间
  2. 内存分配的起始地址 一定要是 4, 8, 16整除地址
  3. 内存是按照页进行分配的, 中间会产生外部内存碎片, 无法分配给进程
  4. 内部内存碎片:频繁的申请小块内存导致了内存不连续性,中间的小内存间隙又不足以满足我们的内存申请要求, 无法申请出去利用起来, 这个就是内部内存碎片.

出现场景

最为典型的场景就是高并发是的频繁内存申请, 释放. (http请求) (tcp连接)

大牛解决措施(nginx内存池)

nginx内存池, 公认的设计方式非常巧妙的一款内存池设计组件, 专门针对高并发下面的大量的内存申请释放而产生的.

在系统层,我们可以使用高性能内存管理组件 Tcmalloc Jemalloc(优化效率和碎片问题)

在应用层: 我们可以根据需求设计内存池进行管理 (高并发可以借助nginx内存池设计)

图片

内存池技术

啥叫作内存池技术

就是说在真正使用内存之前, 先提前申请分配一定数量的、大小相等(一般情况下)的内存块留作备用, 当需要分配内存的时候, 直接从内存块中获取. 如果内存块不够了, 再申请新的内存块.

内存池: 就是将这些提前申请的内存块组织管理起来的数据结构

优势何在:统一对程序所使用的内存进行统一的分配和回收, 提前申请的块, 然后将块中的内存合理的分配出去, 极大的减少了系统调用的次数. 提高了内存利用率. 统一的内存分配回收使得内存泄漏出现的概率大大降低

内存池技术为啥可以解决上文弊端

高并发时系统调用频繁(malloc free频繁),降低了系统的执行效率

  • 内存池提前预先分配大块内存,统一释放,极大的减少了malloc 和 free 等函数的调用。

频繁使用时增加了系统内存的碎片,降低内存使用效率

  • 内存池每次请求分配大小适度的内存块,最大避免了碎片的产生

没有内存回收机制,容易造成内存泄漏

  • 在生命周期结束后统一释放内存,极大的避免了内存泄露的发生

高并发内存池nginx内存池源码刨析

啥是高并发

系统能够同时并行处理很多请求就是高并发

高并发具备的特征

  • 响应时间短
  • 支持并发用户数高
  • 支持用户接入量高
  • 连接建立时间短

nginx_memory_pool为啥就适合高并发

内存池生存时间应该尽可能短,与请求或者连接具有相同的周期

减少碎片堆积和内存泄漏

避免不同请求连接之间互相影响

一个连接或者一个请求就创建一个内存池专门为其服务, 内存池的生命周期和连接的生命周期保持一致.

仿写nginx内存池

实现思路

  • 对于每个请求或者连接都会建立相应的内存池,建立好内存池之后,我们可以直接从内存池中申请所需要的内存,不用去管内存的释放,当内存池使用完成之后一次性销毁内存池。
  • 区分大小内存块的申请和释放,大于内存池块最大尺寸的定义为大内存块,使用单独的大内存块链表保存,即时分配和释放
  • 小于等于池尺寸的定义为小内存块,直接从预先分配的内存块中提取,不够就扩充池中的内存,在生命周期内对小块内存不做释放,直到最后统一销毁。

图片

内存池大小, 以及内存对齐的宏定义

图片

#define MP_ALIGNMENT       		32
#define MP_PAGE_SIZE			4096
#define MP_MAX_ALLOC_FROM_POOL	(MP_PAGE_SIZE-1)

#define mp_align_ptr(p, alignment) (void *)((((size_t)p)+(alignment-1)) & ~(alignment-1))
//分配内存起点对齐

结构定义以及图解分析

typedef struct mp_large_s {
    struct mp_large_s* next;
    void* alloc;//data区
} mp_large_s;

typedef struct mp_node_s {
    unsigned char* last;//下一次内存分配的起点
    unsigned char* end;//当前内存块末尾
    size_t failed;//当前内存块分配失败的次数
    struct mp_node_s* next;
} mp_node_s;

typedef struct mp_pool_s {
    mp_large_s* large;//指向大块内存起点
    mp_node_s* current;//指向当前可分配的小内存块起点
    int max;//小块最大内存
    mp_node_s head[0];//存储地址, 不占据内存,变长结构体技巧
    //存储首块小内存块head地址
} mp_pool_s;

mp_pool_s 内存池结构

  1. large 指向第一个大块
  2. current 指向当前可分配的小块
  3. head 始终指向第一块小块

mp_node_s 小块内存结构

  1. last 下一次内存分配的起点, 本次内存分配的终点
  2. end 块内存末尾
  3. failed 当前内存块申请内存的失败次数, nginx采取的方式是失败次数达到一定程度就更换current,current是开始尝试分配的内存块, 也就是说失败达到一定次数, 就不再申请这个内存块了.

mp_large_s 大块内存块

  1. 正常的申请, 然后使用链表连接管理起来.
  2. alloc 内存块, 分配内存块

函数原型以及功能叙述

//函数申明
mp_pool_s *mp_create_pool(size_t size);//创建内存池
void mp_destory_pool( mp_pool_s *pool);//销毁内存池
void *mp_alloc(mp_pool_s *pool, size_t size);
//从内存池中申请并且进行字节对齐
void *mp_nalloc(mp_pool_s *pool, size_t size);
//从内存池中申请不进行字节对齐
void *mp_calloc(mp_pool_s *pool, size_t size);
//模拟calloc
void mp_free(mp_pool_s *pool, void *p);
void mp_reset_pool(struct mp_pool_s *pool);
//重置内存池
static void *mp_alloc_block(struct mp_pool_s *pool, size_t size);
//申请小块内存
static void *mp_alloc_large(struct mp_pool_s *pool, size_t size);
//申请大块内存

对应nginx函数原型

图片

图片

重点函数分块细节刨析

mp_create_pool: 创建线程池

第一块内存: 大小设置为 size + sizeof(node) + sizeof(pool) ?

mp_node_s head[0] 啥意思?

mp_pool_s* mp_create_pool(size_t size) {
    struct mp_pool_s *p = NULL;
	int ret = posix_memalign((void **)&p, MP_ALIGNMENT, size + sizeof(mp_pool_s) + sizeof(mp_node_s));
	if (ret) {
		return NULL;
	}
	//内存池小块的大小限制
	p- >max = (size < MP_MAX_ALLOC_FROM_POOL) ? size : MP_MAX_ALLOC_FROM_POOL;
	p- >current = p- >head;//第一块为当前块
	p- >large = NULL;

	p- >head- >last = (unsigned char *)p + sizeof( mp_pool_s) + sizeof(mp_node_s);
	p- >head- >end = p- >head- >last + size;
	p- >head- >failed = 0;

	return p;
}

看完了代码来回答一下问题

  • 为了尽可能地避免内存碎片地产生, 小内存地申请, 于是我采取地方式是将 memory pool内存池也放入到首块内存中地方式. 同时所有地node结点信息也都统一存储在每一个内存块中.
  • head[0] : 是一种常用于变长结构体地技巧, 不占用内存, 仅仅只是表示一个地址信息, 存储head node 的地址.

mp_alloc 带字节对齐的内存申请

首先按照size大小选择内存分配方式, 小于等于线程池小块最大大小限制就从已有小块中申请, 小块不足就调用mp_alloc_block创建新的小块 否则就调用 mp_alloc_large 申请创建一个大块内存

mp_align_ptr 用于字节对齐

void *mp_alloc(mp_pool_s *pool, size_t size) {
    mp_node_s* p = NULL;
    unsigned char* m = NULL;
    if (size <= MP_MAX_ALLOC_FROM_POOL) {//从小块中分配
        p = pool- >current;
        do {//循环尝试从现有小块中申请
            m = mp_align_ptr(p- >last, MP_ALIGNMENT);
            if ((size_t)(p- >end - m) >= size) {
                p- >last = m + size;
                return m;  
            }
            p = p- >next;
        } while (p);
        //说明小块中都分配失败了, 于是从新申请一个小块
        return mp_alloc_block(pool, size);
    }
    //从大块中分配
    return mp_alloc_large(pool, size);
}

mp_alloc_block 申请创建新的小块内存

psize 大小等于mp_node_s结点内存大小 + 实际可用内存块大小

搞清楚内存块组成:结点信息 + 实际可用内存块

返回的内存是实际可用内存的起始地址

//申请小块内存
void *mp_alloc_block(struct mp_pool_s *pool, size_t size) {
    unsigned char* m = NULL;
    size_t psize = 0;//内存池每一块的大小
    psize = (size_t)((unsigned char*)pool- >head- >end - (unsigned char*)pool- >head);
    int ret = posix_memalign((void**)&m, MP_ALIGNMENT, psize);
    if (ret) return NULL;
    //此时已经分配出来一个新的块了
    mp_node_s* new_node, *p, *current;
    new_node = (mp_node_s*)m;

    new_node- >end = m + psize;
    new_node- >failed = 0;
    new_node- >next = NULL;

    m += sizeof(mp_node_s);//跳过node
    //对于m进行地址起点内存对齐
    m = mp_align_ptr(m, MP_ALIGNMENT);
    new_node- >last = m + size;

    current = pool- >current;
    //循环寻找新的可分配内存块起点current
    for (p = current; p- >next; p = p- >next) {
        if (p- >failed++ > 4) {
            current = p- >next;
        }
    }
    //将new_node连接到最后一块内存上, 并且尝试跟新pool- >current
    pool- >current = current ? current : new_node;
    p- >next = new_node;
    return m;
}

mp_alloc_large 申请创建新的大块内存

大块内存参考nginx_pool 采取采取的是malloc分配

先分配出来所需大块内存. 在pool的large链表中寻找是否存在空闲的alloc. 存在则将内存挂在上面返回. 寻找5次还没有找到就另外申请一个新的large结点挂载内存, 链接到large list中管理

mp_large_s* node 是从内存池中分配的, 也就是从小块中分配的 why? 减少内存碎片, 将大块的node信息放入小块内存中,避免小内存的申请, 减少内存碎片

留疑? 空闲的alloc从何而来?

void *mp_alloc_large(struct mp_pool_s *pool, size_t size) {
    void* p = malloc(size);
    if (p == NULL) return NULL;
    mp_large_s* l = NULL;
    size_t cnt = 0;

    for (l = pool- >large; l; l = l- >next) {
        if (l- >alloc) {
            l- >alloc = p;
            return p;
        }
        if (cnt++ > 3) {
            break;//为了提高效率, 检查前5个块, 没有空闲alloc就从新申请large
        }
    }
 	l = mp_alloc(pool, sizeof(struct mp_large_s));
	if (l == NULL) {
		free(p);
		return NULL;
	}

    l- >alloc = p;
    l- >next = pool- >large;
    pool- >large = l;
    return p;
}

空闲的alloc是被free掉了空闲出来的. 虽然nginx采取的是小块不单独回收, 最后统一回收, 因为小块的回收非常难以控制, 不清楚何时可以回收. 但是对于大块nginx提供了free回收接口.

mp_free_large 回收大块内存资源

void mp_free_large(mp_pool_s *pool, void *p) {
    mp_large_s* l = NULL;
    for (l = pool- >large; l; l = l- >next) {
        if (p == l- >alloc) {
            free(l- >alloc);

            l- >alloc = NULL;
            return ;
        }
    }

}

整体代码附下

#ifndef _MPOOL_H_
#define _MPOOL_H_

#include < stdlib.h >
#include < stdio.h >
#include < string.h >
#include < unistd.h >
#include < fcntl.h >

#define MP_ALIGNMENT       		32
#define MP_PAGE_SIZE			4096
#define MP_MAX_ALLOC_FROM_POOL	(MP_PAGE_SIZE-1)

#define mp_align_ptr(p, alignment) (void *)((((size_t)p)+(alignment-1)) & ~(alignment-1))
//内存起点对齐

typedef struct mp_large_s {
    struct mp_large_s* next;
    void* alloc;//data区
} mp_large_s;

typedef struct mp_node_s {
    unsigned char* last;//下一次内存分配的起点
    unsigned char* end;//当前内存块末尾
    size_t failed;//当前内存块分配失败的次数
    struct mp_node_s* next;
} mp_node_s;

typedef struct mp_pool_s {
    mp_large_s* large;//指向大块内存起点
    mp_node_s* current;//指向当前可分配的小内存块起点
    int max;//小块最大内存
    mp_node_s head[0];//存储地址, 不占据内存,变长结构体技巧
    //存储首块小内存块head地址
} mp_pool_s;

//函数申明
mp_pool_s *mp_create_pool(size_t size);//创建内存池
void mp_destory_pool( mp_pool_s *pool);//销毁内存池
void *mp_alloc(mp_pool_s *pool, size_t size);
//从内存池中申请并且进行字节对齐
void *mp_nalloc(mp_pool_s *pool, size_t size);
//从内存池中申请不进行字节对齐
void *mp_calloc(mp_pool_s *pool, size_t size);
//模拟calloc
void mp_free(mp_pool_s *pool, void *p);
void mp_reset_pool(struct mp_pool_s *pool);
//重置内存池
static void *mp_alloc_block(struct mp_pool_s *pool, size_t size);
//申请小块内存
static void *mp_alloc_large(struct mp_pool_s *pool, size_t size);
//申请大块内存

mp_pool_s* mp_create_pool(size_t size) {
    struct mp_pool_s *p = NULL;
	int ret = posix_memalign((void **)&p, MP_ALIGNMENT, size + sizeof(mp_pool_s) + sizeof(mp_node_s));
	if (ret) {
		return NULL;
	}
	//内存池小块的大小限制
	p- >max = (size < MP_MAX_ALLOC_FROM_POOL) ? size : MP_MAX_ALLOC_FROM_POOL;
	p- >current = p- >head;//第一块为当前块
	p- >large = NULL;

	p- >head- >last = (unsigned char *)p + sizeof( mp_pool_s) + sizeof(mp_node_s);
	p- >head- >end = p- >head- >last + size;
	p- >head- >failed = 0;

	return p;
}

void mp_destory_pool( mp_pool_s *pool) {
    //先销毁大块
    mp_large_s* l = NULL;
    mp_node_s* p = pool- >head- >next, *q = NULL;
    for (l = pool- >large; l; l = l- >next) {
        if (l- >alloc) {
            free(l- >alloc);
            l- >alloc = NULL;
        }
    }
    //然后销毁小块内存
    while (p) {
        q = p- >next;
        free(p);
        p = q;
    }
    free(pool);
}



//申请小块内存
void *mp_alloc_block(struct mp_pool_s *pool, size_t size) {
    unsigned char* m = NULL;
    size_t psize = 0;//内存池每一块的大小
    psize = (size_t)((unsigned char*)pool- >head- >end - (unsigned char*)pool- >head);
    int ret = posix_memalign((void**)&m, MP_ALIGNMENT, psize);
    if (ret) return NULL;
    //此时已经分配出来一个新的块了
    mp_node_s* new_node, *p, *current;
    new_node = (mp_node_s*)m;

    new_node- >end = m + psize;
    new_node- >failed = 0;
    new_node- >next = NULL;

    m += sizeof(mp_node_s);//跳过node
    //对于m进行地址起点内存对齐
    m = mp_align_ptr(m, MP_ALIGNMENT);
    new_node- >last = m + size;
    current = pool- >current;
    for (p = current; p- >next; p = p- >next) {
        if (p- >failed++ > 4) {
            current = p- >next;
        }
    }
    //将new_node连接到最后一块内存上, 并且尝试跟新pool- >current
    pool- >current = current ? current : new_node;
    p- >next = new_node;
    return m;
}

//申请大块内存
void *mp_alloc_large(struct mp_pool_s *pool, size_t size) {
    void* p = malloc(size);
    if (p == NULL) return NULL;
    mp_large_s* l = NULL;
    size_t cnt = 0;

    for (l = pool- >large; l; l = l- >next) {
        if (l- >alloc) {
            l- >alloc = p;
            return p;
        }
        if (cnt++ > 3) {
            break;//为了提高效率, 检查前5个块, 没有空闲alloc就从新申请large
        }
    }
 	l = mp_alloc(pool, sizeof(struct mp_large_s));
	if (l == NULL) {
		free(p);
		return NULL;
	}

    l- >alloc = p;
    l- >next = pool- >large;
    pool- >large = l;
    return p;
}


//带有字节对齐的申请
void *mp_alloc(mp_pool_s *pool, size_t size) {
    mp_node_s* p = NULL;
    unsigned char* m = NULL;
    if (size < MP_MAX_ALLOC_FROM_POOL) {//从小块中分配
        p = pool- >current;
        do {
            m = mp_align_ptr(p- >last, MP_ALIGNMENT);
            if ((size_t)(p- >end - m) >= size) {
                p- >last = m + size;
                return m;  
            }
            p = p- >next;
        } while (p);
        //说明小块中都分配失败了, 于是从新申请一个小块
        return mp_alloc_block(pool, size);
    }
    //从大块中分配
    return mp_alloc_large(pool, size);
}



//不带字节对齐的从内存池中申请内存
void *mp_nalloc(mp_pool_s *pool, size_t size) {
    mp_node_s* p = NULL;
    unsigned char* m = NULL;
    if (size < MP_MAX_ALLOC_FROM_POOL) {//从小块中分配
        p = pool- >current;
        do {
            m = p- >last;
            if ((size_t)(p- >end - m) >= size) {
                p- >last = m + size;
                return m;  
            }
            p = p- >next;
        } while (p);
        //说明小块中都分配失败了, 于是从新申请一个小块
        return mp_alloc_block(pool, size);
    }
    //从大块中分配
    return mp_alloc_large(pool, size);
}


void *mp_calloc(struct mp_pool_s *pool, size_t size) {

	void *p = mp_alloc(pool, size);
	if (p) {
		memset(p, 0, size);
	}

	return p;
	
}

void mp_free(mp_pool_s *pool, void *p) {
    mp_large_s* l = NULL;
    for (l = pool- >large; l; l = l- >next) {
        if (p == l- >alloc) {
            free(l- >alloc);

            l- >alloc = NULL;
            return ;
        }
    }

}

#endif
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 内存
    +关注

    关注

    8

    文章

    2767

    浏览量

    72779
  • JAVA
    +关注

    关注

    19

    文章

    2904

    浏览量

    103003
  • 程序
    +关注

    关注

    114

    文章

    3631

    浏览量

    79554
  • 函数
    +关注

    关注

    3

    文章

    3904

    浏览量

    61311
  • nginx
    +关注

    关注

    0

    文章

    136

    浏览量

    11989
收藏 人收藏

    评论

    相关推荐

    【每日一练】第十六节:内存的使用

    本视频为【每日一练】的第16节学习视频,注:刚开始学习的童鞋请从第一节视频开始打卡哦(本节视频在下面打卡即可)学习任务:1、删除内存时,会首先唤醒等待在该内存对象上的所有线程。(判
    发表于 09-08 09:33

    内存可以调节内存的大小吗

    嵌入式–内存直接上代码,自己体会。嵌入式设备,一般keil提供的堆很小,一般都不使用。使用内存,自己可以调节内存大小。头文件 mallo
    发表于 12-17 07:00

    内存的概念和实现原理概述

    { //一:内存的概念和实现原理概述//malloc:内存浪费,频繁分配小块内存,则浪费更加显得明显//“内存
    发表于 12-17 06:44

    关于RT-Thread内存管理的内存简析

    这篇文章继续介绍 RT-Thread 内存管理剩下的部分——内存。为何引入内存内存堆虽然方
    发表于 04-06 17:02

    RT-Thread内存管理之内存实现分析

    适合它们的高效率的内存分配算法,就将变得复杂化。RT-Thread 操作系统在内存管理上,根据上层应用及系统资源的不同,有针对性地提供了不同的内存分配管理算法。总体上可分为两类:内存
    发表于 10-17 15:06

    删除静态内存是用rt_mp_detach还是rt_mp_delete

    可否动态申请一块内存作为静态内存,然后再在这块静态内存进行相关的静态内存操作?删除静态
    发表于 11-22 14:42

    【昉·星光 2 高性能RISC-V单板计算机体验】使用nginx、ffmpeg和VLC搭建简单视频直播系统

    源码编译并安装nginx和http-flv模块,然后介绍如何使用ffmpeg推流,最后介绍如何在PC上使用VLC播放器查看视频流中的画面。在本文介绍的实验中,nginx及http-flv模块作为视频流
    发表于 08-27 19:06

    鸿蒙内核源码分析: 虚拟内存和物理内存是怎么管理的

    有了上篇鸿蒙内核源码分析(内存概念篇)的基础,本篇讲内存管理部分,本章源码超级多,很烧脑,但笔者关键处都加了注释。废话不多说,开始吧。内存
    发表于 11-23 11:45 19次下载
    鸿蒙内核<b class='flag-5'>源码</b>分析: 虚拟<b class='flag-5'>内存</b>和物理<b class='flag-5'>内存</b>是怎么管理的

    Nginx的详细知识点讲解

    Nginx是一个高性能的HTTP和反向代理服务器,特点是占用内存少,并发能力强,事实上nginx的并发能力确实在同类型的网页服务器中表现较好 nginx专为性能优化而开发,性能是其最重
    的头像 发表于 12-26 10:25 2355次阅读
    <b class='flag-5'>Nginx</b>的详细知识点讲解

    如何彻底搞懂Nginx知识网结构

    1.Nginx知识网结构图 Nginx是一个高性能的HTTP和反向代理服务器,特点是占用内存少,并发能力强,事实上nginx的并发能力确实在同类型的网页服务器中表现较好
    的头像 发表于 05-03 14:15 1469次阅读
    如何彻底搞懂<b class='flag-5'>Nginx</b>知识网结构

    Nginx如何监控

    搭建了Nginx集群后,需要继续深入研究的就是日常Nginx监控。
    的头像 发表于 08-22 10:03 1168次阅读

    Nginx的特点和作用 Nginx常用命令和核心配置

    热更新。性能是 Nginx 最重要的考量,其占用内存少、并发能力强、能支持高达 5w 个并发连接数,最重要的是, Nginx 是免费的并可以商业化,配置使用也比较简单。
    的头像 发表于 09-01 09:53 365次阅读
    <b class='flag-5'>Nginx</b>的特点和作用 <b class='flag-5'>Nginx</b>常用命令和核心配置

    如何实现一个高性能内存

    写在前面 本文的内存池代码是改编自Nginx内存源码,思路几乎一样。由于Nginx源码的变量
    的头像 发表于 11-10 11:11 252次阅读
    如何实现一个高性能<b class='flag-5'>内存</b>池

    Nginx目录结构有哪些

    有很多,例如:新浪、网易、 腾讯等。 为什么要用Nginx? 跨平台、配置简单、方向代理、高并发连接:处理2-3万并发连接数,官方监测能支持5万并发,内存消耗小:开启10个nginx才占150M
    的头像 发表于 11-11 11:27 385次阅读
    <b class='flag-5'>Nginx</b>目录结构有哪些

    Nginx 如何实现高性能低消耗

    Nginx 是一个轻量级的HTTP 服务程序,相比其他服务器程序如Apache,Nginx占用内存少,稳定性高,并发处理能力强。同时Nginx 还是一个反向代理服务程序,和邮件代理服务
    的头像 发表于 11-11 11:31 351次阅读
    <b class='flag-5'>Nginx</b> 如何实现高性能低消耗