返回顶部
首页 > 资讯 > 精选 >nginx内存池源码分析
  • 831
分享到

nginx内存池源码分析

2023-06-25 16:06:33 831人浏览 安东尼
摘要

本篇内容主要讲解“Nginx内存池源码分析”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“nginx内存池源码分析”吧!内存池概述    内存池是在真正使用内存之前,

本篇内容主要讲解“Nginx内存池源码分析”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“nginx内存池源码分析”吧!

内存池概述

    内存池是在真正使用内存之前,预先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够用时,再继续申请新的内存。

   内存池的好处有减少向系统申请和释放内存的时间开销,解决内存频繁分配产生的碎片,提示程序性能,减少程序员在编写代码中对内存的关注等

   目前一些常见的内存池实现方案有STL中的内存分配区,boost中的object_pool,nginx中的ngx_pool_t,Google的开源项目TCMalloc等。

一、nginx数据结构

// SGI STL小块和大块内存的分界点:128B// nginx(给Http服务器所有的模块分配内存)小块和大块内存的分界点:4096B#define NGX_MAX_ALLOC_FROM_POOL  (ngx_pagesize - 1) // 内存池默认大小#define NGX_DEFAULT_POOL_SIZE    (16 * 1024)// 内存池字节对齐,SGI STL对其是8B#define NGX_POOL_ALIGNMENT       16#define NGX_MIN_POOL_SIZE        ngx_align((sizeof(ngx_pool_t) + 2 * sizeof(ngx_pool_large_t)), \                         NGX_POOL_ALIGNMENT)// 将开辟的内存调整到16的整数倍#define ngx_align(d, a)          (((d) + (a - 1)) & ~(a - 1))
typedef struct ngx_pool_s ngx_pool_t;typedef struct {    u_char               *last;   // 指向可用内存的起始地址    u_char               *end;    // 指向可用内存的末尾地址    ngx_pool_t           *next;   // 指向下一个内存块      ngx_uint_t            failed; // 当前内存块分配空间失败的次数} ngx_pool_data_t;// 内存池块的类型struct ngx_pool_s {    ngx_pool_data_t       d;          // 内存池块头信息    size_t                max;    ngx_pool_t           *current;    // 指向可用于分配空间的内存块(failed < 4)的起始地址    ngx_chain_t          *chain;      // 连接所有的内存池块    ngx_pool_large_t     *large;  // 大块内存的入口指针    ngx_pool_cleanup_t   *cleanup;    // 内存池块的清理操作,用户可设置回调函数,在内存池块释放之前执行清理操作    ngx_log_t            *log;        // 日志};

nginx内存池源码分析

二、nginx向OS申请空间ngx_create_pool

// 根据size进行内存开辟ngx_pool_t * ngx_create_pool(size_t size, ngx_log_t *log){    ngx_pool_t  *p;// 根据系统平台定义的宏以及用户执行的size,调用不同平台的api开辟内存池    p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);    if (p == NULL) {        return NULL;    }    p->d.last = (u_char *) p + sizeof(ngx_pool_t);  // 指向可用内存的起始地址    p->d.end = (u_char *) p + size;                 // 指向可用内存的末尾地址    p->d.next = NULL;                               // 指向下一个内存块,当前刚申请内存块,所以置空                  p->d.failed = 0;                                // 内存块是否开辟成功    size = size - sizeof(ngx_pool_t);              // 能使用的空间 = 总空间 - 头信息    // 指定的大小若大于一个页面就用一个页面,否则用指定的大小    // max = min(size, 4096),max指的是除开头信息以外的内存块的大小    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;    p->current = p;         // 指向可用于分配空间的内存块的起始地址    p->chain = NULL;    p->large = NULL;        // 小块内存直接在内存块开辟,大块内存在large指向的内存开辟    p->cleanup = NULL;    p->log = log;    return p;}

nginx内存池源码分析

三、nginx向内存池申请空间

void *ngx_palloc(ngx_pool_t *pool, size_t size){#if !(NGX_DEBUG_PALLOC)    if (size <= pool->max) {    // 当前分配的空间小于max,小块内存的分配        return ngx_palloc_small(pool, size, 1);   // 考虑内存对齐    }#endif    return ngx_palloc_large(pool, size);}void *ngx_pnalloc(ngx_pool_t *pool, size_t size){#if !(NGX_DEBUG_PALLOC)    if (size <= pool->max) {        return ngx_palloc_small(pool, size, 0);  // 不考虑内存对齐    }#endif    return ngx_palloc_large(pool, size);}void* ngx_pcalloc(ngx_pool_t *pool, size_t size){    void *p;    p = ngx_palloc(pool, size); // 考虑内存对齐    if (p) {        ngx_memzero(p, size);   // 可以初始化内存为0    }    return p;}

ngx_palloc_small 分配效率高,只做了指针的偏移

static ngx_inline void *ngx_palloc_small(ngx_pool_t *pool, size_t size, ngx_uint_t align){    u_char      *m;    ngx_pool_t  *p;// 从第一个内存块的current指针指向的内存池进行分配    p = pool->current;    do {        m = p->d.last;  // m指向可分配内存的起始地址        if (align) {        // 把m调整为NGX_ALIGNMENT整数倍            m = ngx_align_ptr(m, NGX_ALIGNMENT);        }// 内存池分配内存的核心代码        if ((size_t) (p->d.end - m) >= size) {        // 若可分配空间 >= 申请的空间        // 偏移d.last指针,记录空闲空间的首地址            p->d.last = m + size;            return m;        }        // 当前内存块的空闲空间不够分配,若有下一个内存块则转向下一个内存块        // 若没有,p会被置空,退出while        p = p->d.next;    } while (p);    return ngx_palloc_block(pool, size);}

当前内存池的块足够分配:

nginx内存池源码分析

当前内存池的块不够分配:

  1. 开辟新的内存块,修改新内存块头信息的last、end、next、failed

  2. 前面所有内存块的failed++

  3. 连接新的内存块以及前面的内存块

static void * ngx_palloc_block(ngx_pool_t *pool, size_t size){    u_char      *m;    size_t       psize;    ngx_pool_t  *p, *new;// 开辟与上一个内存块大小相同的内存块    psize = (size_t) (pool->d.end - (u_char *) pool);// 将psize对齐为NGX_POOL_ALIGNMENT的整数倍后,向OS申请空间    m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);    if (m == NULL) {        return NULL;    }    new = (ngx_pool_t *) m;    // 指向新开辟内存块的起始地址    new->d.end = m + psize;    // 指向新开辟内存块的末尾地址    new->d.next = NULL;   // 下一块内存的地址为NULL     new->d.failed = 0;   // 当前内存块分配空间失败的次数    // 指向头信息的尾部,而max,current、chain等只在第一个内存块有    m += sizeof(ngx_pool_data_t);      m = ngx_align_ptr(m, NGX_ALIGNMENT);    new->d.last = m + size;                // last指向当前块空闲空间的起始地址// 由于每次都是从pool->current开始分配空间// 若执行到这里,除了new这个内存块分配成功,其他的内存块全部分配失败    for (p = pool->current; p->d.next != NULL; p = p->d.next) {    // 对所有的内存块的failed都++,直到该内存块分配失败的次数大于4了    // 就表示该内存块的剩余空间很小了,不能再分配空间了    // 就修改current指针,下次从current开始分配空间,再次分配的时候可以不用遍历前面的内存块        if (p->d.failed++ > 4) {            pool->current = p->d.next;        }    }    p->d.next = new;   // 连接可分配空间的首个内存块 和 新开辟的内存块    return m;}

nginx内存池源码分析

四、大块内存的分配与释放

typedef struct ngx_pool_large_s  ngx_pool_large_t;struct ngx_pool_large_s {    ngx_pool_large_t     *next;   // 下一个大块内存的起始地址    void                 *alloc;  // 大块内存的起始地址};static void * ngx_palloc_large(ngx_pool_t *pool, size_t size){    void              *p;    ngx_uint_t         n;    ngx_pool_large_t  *large;// 调用的就是malloc    p = ngx_alloc(size, pool->log);    if (p == NULL) {        return NULL;    }    n = 0;// for循环遍历存储大块内存信息的链表    for (large = pool->large; large; large = large->next) {        if (large->alloc == NULL) {        // 当大块内存被ngx_pfree时,alloc为NULL        // 遍历链表,若大块内存的首地址为空,则把当前malloc的内存地址写入alloc            large->alloc = p;            return p;        }// 遍历4次后,若还没有找到被释放过的大块内存对应的信息// 为了提高效率,直接在小块内存中申请空间保存大块内存的信息        if (n++ > 3) {            break;        }    }// 通过指针偏移在小块内存池上分配存放大块内存*next和*alloc的空间    large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);    if (large == NULL) {    // 如果在小块内存上分配存储*next和*alloc空间失败,则无法记录大块内存    // 释放大块内存p        ngx_free(p);        return NULL;    }    large->alloc = p;   // alloc指向大块内存的首地址    large->next = pool->large;   // 这两句采用头插法,将新内存块的记录信息存放于以large为头结点的链表中    pool->large = large;    return p;}

nginx内存池源码分析

大块内存的释放

// 释放p指向的大块内存ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p){    ngx_pool_large_t  *l;    for (l = pool->large; l; l = l->next) {    // 遍历存储大块内存信息的链表,找到p对应的大块内存        if (p == l->alloc) {            ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,                           "free: %p", l->alloc);            // 释放大块内存,但不释放存储信息的内存空间            ngx_free(l->alloc);  // free            l->alloc = NULL;     // alloc置空            return NGX_OK;        }    }    return NGX_DECLINED;}

五、关于小块内存不释放

就用了last和end两个指着标识空闲的空间,是无法将已经使用的空间合理归还到内存池的,只是会重置内存池。同时还存储了指向大内存块large和清理函数cleanup的头信息

考虑到nginx的效率,小块内存分配高效,同时也不回收内存

void ngx_reset_pool(ngx_pool_t *pool){    ngx_pool_t        *p;    ngx_pool_large_t  *l;// 由于需要重置小块内存,而大块内存的控制信息在小块内存中保存// 所以需要先释放大块内存,在重置小块内存    for (l = pool->large; l; l = l->next) {        if (l->alloc) {            ngx_free(l->alloc);        }    }// 遍历小块内存的链表,重置last、failed、current、chain、large等管理信息    for (p = pool; p; p = p->d.next) {    // 由于只有第一个内存块有除了ngx_pool_data_t以外的管理信息,别的内存块只有ngx_pool_data_t的信息    // 不会出错,但是会浪费空间        p->d.last = (u_char *) p + sizeof(ngx_pool_t);        p->d.failed = 0;    }// current指向可用于分配内存的内存块    pool->current = pool;    pool->chain = NULL;    pool->large = NULL;}

nginx本质是http服务器,通常处理的是短链接,间接性提供服务,需要的内存不大,所以不回收内存,重置即可。

客户端发起一个requests请求后,nginx服务器收到请求会返回response响应,若在keep-alive时间内没有收到客户的再次请求,nginx服务器会主动断开连接,此时会reset内存池。下一次客户端请求再到来时,可以复用内存池。

如果是处理长链接,只要客户端还在线,服务器的资源就无法释放,直到系统资源耗尽。长链接一般使用SGI STL内存池的方式进行内存的开辟和释放,而这种方式分配和回收空间的效率就比nginx低

六、销毁和清空内存池

假设如下情况:

// 假设内存对齐为4Btypedef struct{char* p;char data[508];}stData;ngx_pool_t *pool = ngx_create_pool(512, log);  // 创建一个总空间为512B的nginx内存块stData* data_ptr = ngx_alloc(512);            // 因为可用的实际内存大小为:512-sizeof(ngx_pool_t),所以属于大内存开辟data_ptr->p = malloc(10);                   // p指向外界堆内存,类似于c++对象中对用占用了外部资源

当回收大块内存的时候,调用ngx_free,就会导致内存泄漏

nginx内存池源码分析

以上内存泄漏的问题,可以通过回调函数进行内存释放(通过函数指针实现)

typedef void (*ngx_pool_cleanup_pt)(void *data);typedef struct ngx_pool_cleanup_s  ngx_pool_cleanup_t;// 以下结构体由ngx_pool_s.cleanup指向,也是存放在内存池的小块内存struct ngx_pool_cleanup_s {    ngx_pool_cleanup_pt   handler;    void                 *data;     // 指向需要释放的资源    ngx_pool_cleanup_t   *next;     // 释放资源的函数都放在一个链表,用next指向这个链表};

nginx提供的函数接口:

// p表示内存池的入口地址,size表示p->cleanup->data指针的大小// p->cleanup指向含有清理函数信息的结构体// ngx_pool_cleanup_add返回 含有清理函数信息的结构体 的指针ngx_pool_cleanup_t* ngx_pool_cleanup_add(ngx_pool_t *p, size_t size){    ngx_pool_cleanup_t  *c;// 开辟清理函数的结构体,实际上也是存放在内存池的小块内存    c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));    if (c == NULL) {        return NULL;    }    if (size) {    // 为c->data申请size的空间        c->data = ngx_palloc(p, size);        if (c->data == NULL) {            return NULL;        }    } else {        c->data = NULL;    }    c->handler = NULL;    // 采用头插法,将当前结构体串在pool->cleanup后    c->next = p->cleanup;    p->cleanup = c;    ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);    return c;}

使用方式:

void release(void* p){free(p);}ngx_pool_cleanup_t* clean_ptr = ngx_clean_cleanup_add(pool, sizeof(char*));clean_ptr->handler = &release;   // 用户设置销毁内存池前需要调用的函数clean_ptr->data = data_ptr->p;   // 用户设置销毁内存池前需要释放的内存的地址ngx_destroy_pool(pool);          // 用户销毁内存池

七、编译测试内存池接口功能

void ngx_destroy_pool(ngx_pool_t *pool){    ngx_pool_t          *p, *n;    ngx_pool_large_t    *l;    ngx_pool_cleanup_t  *c;// 遍历cleanup链表(存放的时释放前需要调用的函数),可释放外部占用的资源    for (c = pool->cleanup; c; c = c->next) {        if (c->handler) {            ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,                           "run cleanup: %p", c);            c->handler(c->data);        }    }// 释放大块内存    for (l = pool->large; l; l = l->next) {        if (l->alloc) {            ngx_free(l->alloc);        }    }// 释放小块内存池    for (p = pool, n = pool->d.next; ; p = n, n = n->d.next) {        ngx_free(p);                if (n == NULL) {            break;        }    }}

nginx内存池源码分析

执行configure生成Makefile文件(若报错则表示需要apt安装软件)

nginx内存池源码分析

Makefile如下:

nginx内存池源码分析

执行make命令使用Makefile编译源码,在相应目录下生成 .o文件

nginx内存池源码分析

#include <ngx_config.h>#include <nginx.h>#include <ngx_core.h>#include <ngx_palloc.h>#include <stdio.h>#include <stdlib.h>#include <string.h>void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err,            const char *fmt, ...){}typedef struct Data stData;struct Data{    char *ptr;    FILE *pfile;};void func1(char *p){    printf("free ptr mem!\n");    free(p);}void func2(FILE *pf){    printf("close file!\n");    fclose(pf);}void main(){// max = 512 - sizeof(ngx_pool_t)// 创建总空间为512字节的nginx内存块    ngx_pool_t *pool = ngx_create_pool(512, NULL);    if(pool == NULL){        printf("ngx_create_pool fail...");        return;    }    // 从小块内存池分配的    void *p1 = ngx_palloc(pool, 128);     if(p1 == NULL){        printf("ngx_palloc 128 bytes fail...");        return;    }// 从大块内存池分配的    stData *p2 = ngx_palloc(pool, 512);     if(p2 == NULL){        printf("ngx_palloc 512 bytes fail...");        return;    }        // 占用外部堆内存    p2->ptr = malloc(12);    strcpy(p2->ptr, "hello world");    // 文件描述符    p2->pfile = fopen("data.txt", "w");        ngx_pool_cleanup_t *c1 = ngx_pool_cleanup_add(pool, sizeof(char*));    c1->handler = func1;   // 设置回调函数    c1->data = p2->ptr;    // 设置资源地址    ngx_pool_cleanup_t *c2 = ngx_pool_cleanup_add(pool, sizeof(FILE*));    c2->handler = func2;    c2->data = p2->pfile;// 1.调用所有的预置的清理函数 2.释放大块内存 3.释放小块内存池所有内存    ngx_destroy_pool(pool);     return;}

nginx内存池源码分析

由于ngx_pool_cleanup_add中用头插法将创建的清理块链入pool->cleanup,所以ngx_destroy_pool的时候先清理文件后清理堆内存。

到此,相信大家对“nginx内存池源码分析”有了更深的了解,不妨来实际操作一番吧!这里是编程网网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

--结束END--

本文标题: nginx内存池源码分析

本文链接: https://lsjlt.com/news/305978.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • nginx内存池源码分析
    本篇内容主要讲解“nginx内存池源码分析”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“nginx内存池源码分析”吧!内存池概述    内存池是在真正使用内存之前,...
    99+
    2023-06-25
  • nginx内存池源码解析
    目录内存池概述一、nginx数据结构二、nginx向OS申请空间ngx_create_pool三、nginx向内存池申请空间四、大块内存的分配与释放五、关于小块内存不释放六、销毁和清...
    99+
    2024-04-02
  • nginx内存池如何实现
    这篇文章主要讲解了“nginx内存池如何实现”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“nginx内存池如何实现”吧!一、简介最新稳定版本nginx1.20.2。为了能高效、快速的分配内存...
    99+
    2023-07-02
  • nginx之内存池的实现
    目录一、简介二、数据结构2.1 内存池主要结构2.2 大内存链2.3 清理任务链三、内存结构图3.1 逻辑3.2 实际四、实现4.1 创建内存池4.2 从内存池中分配空间4.3 注册...
    99+
    2024-04-02
  • Golang内存模型实例源码分析
    这篇文章主要介绍“Golang内存模型实例源码分析”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Golang内存模型实例源码分析”文章能帮助大家解决问题。1. 简介(Introduction)Go ...
    99+
    2023-07-05
  • 如何进行Nginx内核优化的源代码分析
    如何进行Nginx内核优化的源代码分析,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。Nginx内核优化在不断的使用中有很多的问...
    99+
    2024-04-02
  • JVM堆外内存源码完全解读分析
    概述 广义的堆外内存 说到堆外内存,那大家肯定想到堆内内存,这也是我们大家接触最多的,我们在jvm参数里通常设置-Xmx来指定我们的堆的最大值,不过这还不是我们理解的Java堆,-X...
    99+
    2024-04-02
  • Java线程池ThreadPoolExecutor源码深入分析
    1.线程池Executors的简单使用 1)创建一个线程的线程池。 Executors.newSingleThreadExecutor(); //创建的源码 public...
    99+
    2024-04-02
  • Android内核wake_up源码分析
    今天小编给大家分享一下Android内核wake_up源码分析的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。内核中通常用法:...
    99+
    2023-07-05
  • 关于java连接池/线程池/内存池/进程池等汇总分析
    目录一、引言二、池技术的由来和目的三、池技术的原理四、池技术的优缺点对象池:内存池:线程池:数据库连接池:连接池:进程池:缓冲池:工作队列:六、延伸与拓展缓存(Caching):延迟...
    99+
    2023-05-16
    java池技术 java连接池 java线程池 java内存池 java进程池
  • Nginx源码编译安装的示例分析
    这篇文章将为大家详细讲解有关Nginx源码编译安装的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。rpm包安装比较简单,这里不做说明。对于大多数开源软件,如果找不到安装包,可以使用源码安装方式,源...
    99+
    2023-06-25
  • PostgreSQL存储过程源码分析
    这篇文章主要介绍了PostgreSQL存储过程源码分析的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇PostgreSQL存储过程源码分析文章都会有所收获,下面我们一起来看看吧。游标PL/pgSQL 游标允许我们...
    99+
    2023-07-05
  • java线程池的实现原理源码分析
    这篇文章主要介绍“java线程池的实现原理源码分析”,在日常操作中,相信很多人在java线程池的实现原理源码分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”java线程池的实现原理源码分析”的疑惑有所帮助!...
    99+
    2023-06-30
  • java线程池核心API源码详细分析
    目录概述源码分析ExecutorExecutorServiceScheduledExecutorServiceThreadPoolExecutorScheduledThreadPoo...
    99+
    2024-04-02
  • 从java源码分析线程池(池化技术)的实现原理
    目录线程池的起源线程池的定义和使用方案一:Executors(仅做了解,推荐使用方案二)方案二:ThreadPoolExecutor线程池的实现原理前言: 线程池是一个非常重要的知识...
    99+
    2024-04-02
  • 如何分析Linux内核源码do_fork
    本篇文章为大家展示了如何分析Linux内核源码do_fork,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。我们都知道进程是Linux内核中最为重要的一个抽象概念,那么我们平时在fork一个进程时,该...
    99+
    2023-06-16
  • Python内建类型int源码分析
    今天小编给大家分享一下Python内建类型int源码分析的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。问题:对于C语言,下面...
    99+
    2023-06-30
  • Python内建类型float源码分析
    这篇文章主要介绍“Python内建类型float源码分析”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Python内建类型float源码分析”文章能帮助大家解决问题。1 回顾float的基础知识1....
    99+
    2023-06-30
  • Python内建类型str源码分析
    这篇文章主要讲解了“Python内建类型str源码分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Python内建类型str源码分析”吧!1 Unicode计算机存储的基本单位是字节,由8...
    99+
    2023-06-30
  • Python内建类型dict源码分析
    本篇内容主要讲解“Python内建类型dict源码分析”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Python内建类型dict源码分析”吧!深入认识Python内建类型&mdash;&...
    99+
    2023-07-05
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作