Nginx源码分析 - 基础数据结构篇 - 内存池 ngx_palloc.c

Nginx源码分析 - 基础数据结构篇 - 内存池 ngx_palloc.c

。 https://blog.csdn.net/initphp/article/details/50588790

Nginx的内存管理是通过内存池来实现的。Nginx的内存池的设计非常的精巧,很多场景下,我们可以将Nginx的内存池实现抽象出来改造成我们开发中的内存池。

 

内存池

一般我们使用malloc/alloc/free等函数来分配和释放内存。但是直接使用这些函数会有一些弊端:

1. 虽然系统自带的ptmalloc内存分配管理器,也有自己的内存优化管理方案(申请内存块以及将内存交还给系统都有自己的优化方案,具体可以研究一下ptmalloc的源码),但是直接使用malloc/alloc/free,仍然会导致内存分配的性能比较低。

2. 频繁使用这些函数分配和释放内存,会导致内存碎片,不容易让系统直接回收内存。典型的例子就是大并发频繁分配和回收内存,会导致进程的内存产生碎片,并且不会立马被系统回收。

3. 容易产生内存泄露。

 

使用内存池分配内存有几点好处:

1. 提升内存分配效率。不需要每次分配内存都执行malloc/alloc等函数。

2. 让内存的管理变得更加简单。内存的分配都会在一块大的内存上,回收的时候只需要回收大块内存就能将所有的内存回收,防止了内存管理混乱和内存泄露问题。

 

数据结构定义

ngx_pool_t 内存池主结构

 

 
  1. /**

  2. * Nginx 内存池数据结构

  3. */

  4. struct ngx_pool_s {

  5. ngx_pool_data_t d; /* 内存池的数据区域*/

  6. size_t max; /* 最大每次可分配内存 */

  7. ngx_pool_t *current; /* 指向当前的内存池指针地址。ngx_pool_t链表上最后一个缓存池结构*/

  8. ngx_chain_t *chain; /* 缓冲区链表 */

  9. ngx_pool_large_t *large; /* 存储大数据的链表 */

  10. ngx_pool_cleanup_t *cleanup; /* 可自定义回调函数,清除内存块分配的内存 */

  11. ngx_log_t *log; /* 日志 */

  12. };

ngx_pool_data_t 数据区域结构

 

 
  1. typedef struct {

  2. u_char *last; /* 内存池中未使用内存的开始节点地址 */

  3. u_char *end; /* 内存池的结束地址 */

  4. ngx_pool_t *next; /* 指向下一个内存池 */

  5. ngx_uint_t failed;/* 失败次数 */

  6. } ngx_pool_data_t;

ngx_pool_large_t 大数据块结构

 

 
  1. struct ngx_pool_large_s {

  2. ngx_pool_large_t *next; /* 指向下一个存储地址 通过这个地址可以知道当前块长度 */

  3. void *alloc; /* 数据块指针地址 */

  4. };

 

ngx_pool_cleanup_t 自定义清理回调的数据结构

 

 
  1. struct ngx_pool_cleanup_s {

  2. ngx_pool_cleanup_pt handler; /* 清理的回调函数 */

  3. void *data; /* 指向存储的数据地址 */

  4. ngx_pool_cleanup_t *next; /* 下一个ngx_pool_cleanup_t */

  5. };

 

数据结构图

说明:

1. Nginx的内存池会放在ngx_pool_t的数据结构上(ngx_pool_data_t用于记录内存块block的可用地址空间和内存块尾部)。当初始化分配的内存块大小不能满足需求的时候,Nginx就会调用ngx_palloc_block函数来分配一个新的内存块,通过链表的形式连接起来。

2. 当申请的内存大于pool->max的值的时候,Nginx就会单独分配一块large的内存块,会放置在pool->large的链表结构上。

3. pool->cleanup的链表结构主要存放需要通过回调函数清理的内存数据。(例如文件描述符)

具体函数实现

内存分配 ngx_alloc和ngx_calloc

ngx_alloc和ngx_calloc 主要封装了Nginx的内存分配函数malloc。

 

 
  1. /**

  2. * 封装了malloc函数,并且添加了日志

  3. */

  4. void *

  5. ngx_alloc(size_t size, ngx_log_t *log)

  6. {

  7. void *p;

  8. //分配一块内存

  9. p = malloc(size);

  10. if (p == NULL) {

  11. ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,

  12. "malloc(%uz) failed", size);

  13. }

  14.  
  15. ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, log, 0, "malloc: %p:%uz", p, size);

  16.  
  17. return p;

  18. }

  19.  
  20. /**

  21. * 调用ngx_alloc方法,如果分配成,则调用ngx_memzero方法,将内存块设置为0

  22. * #define ngx_memzero(buf, n) (void) memset(buf, 0, n)

  23. */

  24. void *

  25. ngx_calloc(size_t size, ngx_log_t *log)

  26. {

  27. void *p;

  28.  
  29. //调用内存分配函数

  30. p = ngx_alloc(size, log);

  31.  
  32. if (p) {

  33. //将内存块全部设置为0

  34. ngx_memzero(p, size);

  35. }

  36.  
  37. return p;

  38. }

创建内存池ngx_create_pool

调用ngx_create_pool这个方法就可以创建一个内存池。

 

 
  1. /**

  2. * 创建一个内存池

  3. */

  4. ngx_pool_t *

  5. ngx_create_pool(size_t size, ngx_log_t *log) {

  6. ngx_pool_t *p;

  7.  
  8. /**

  9. * 相当于分配一块内存 ngx_alloc(size, log)

  10. */

  11. p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);

  12. if (p == NULL) {

  13. return NULL;

  14. }

  15.  
  16. /**

  17. * Nginx会分配一块大内存,其中内存头部存放ngx_pool_t本身内存池的数据结构

  18. * ngx_pool_data_t p->d 存放内存池的数据部分(适合小于p->max的内存块存储)

  19. * p->large 存放大内存块列表

  20. * p->cleanup 存放可以被回调函数清理的内存块(该内存块不一定会在内存池上面分配)

  21. */

  22. p->d.last = (u_char *) p + sizeof(ngx_pool_t); //内存开始地址,指向ngx_pool_t结构体之后数据取起始位置

  23. p->d.end = (u_char *) p + size; //内存结束地址

  24. p->d.next = NULL; //下一个ngx_pool_t 内存池地址

  25. p->d.failed = 0; //失败次数

  26.  
  27. size = size - sizeof(ngx_pool_t);

  28. p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;

  29.  
  30. /* 只有缓存池的父节点,才会用到下面的这些 ,子节点只挂载在p->d.next,并且只负责p->d的数据内容*/

  31. p->current = p;

  32. p->chain = NULL;

  33. p->large = NULL;

  34. p->cleanup = NULL;

  35. p->log = log;

  36.  
  37. return p;

  38. }

 

销毁内存池ngx_destroy_pool

 

 
  1. /**

  2. * 销毁内存池。

  3. */

  4. void ngx_destroy_pool(ngx_pool_t *pool) {

  5. ngx_pool_t *p, *n;

  6. ngx_pool_large_t *l;

  7. ngx_pool_cleanup_t *c;

  8.  
  9. /* 首先清理pool->cleanup链表 */

  10. for (c = pool->cleanup; c; c = c->next) {

  11. /* handler 为一个清理的回调函数 */

  12. if (c->handler) {

  13. ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,

  14. "run cleanup: %p", c);

  15. c->handler(c->data);

  16. }

  17. }

  18.  
  19. /* 清理pool->large链表(pool->large为单独的大数据内存块) */

  20. for (l = pool->large; l; l = l->next) {

  21.  
  22. ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);

  23.  
  24. if (l->alloc) {

  25. ngx_free(l->alloc);

  26. }

  27. }

  28.  
  29. #if (NGX_DEBUG)

  30.  
  31. /*

  32. * we could allocate the pool->log from this pool

  33. * so we cannot use this log while free()ing the pool

  34. */

  35.  
  36. for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {

  37. ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, pool->log, 0,

  38. "free: %p, unused: %uz", p, p->d.end - p->d.last);

  39.  
  40. if (n == NULL) {

  41. break;

  42. }

  43. }

  44.  
  45. #endif

  46.  
  47. /* 对内存池的data数据区域进行释放 */

  48. for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {

  49. ngx_free(p);

  50.  
  51. if (n == NULL) {

  52. break;

  53. }

  54. }

  55. }

 

重设内存池ngx_reset_pool

 

 
  1. /**

  2. * 重设内存池

  3. */

  4. void ngx_reset_pool(ngx_pool_t *pool) {

  5. ngx_pool_t *p;

  6. ngx_pool_large_t *l;

  7.  
  8. /* 清理pool->large链表(pool->large为单独的大数据内存块) */

  9. for (l = pool->large; l; l = l->next) {

  10. if (l->alloc) {

  11. ngx_free(l->alloc);

  12. }

  13. }

  14.  
  15. pool->large = NULL;

  16.  
  17. /* 循环重新设置内存池data区域的 p->d.last;data区域数据并不擦除*/

  18. for (p = pool; p; p = p->d.next) {

  19. p->d.last = (u_char *) p + sizeof(ngx_pool_t);

  20. }

  21. }

 

使用内存池分配一块内存ngx_palloc和ngx_pnalloc

 

 
  1. /**

  2. * 内存池分配一块内存,返回void类型指针

  3. */

  4. void *

  5. ngx_palloc(ngx_pool_t *pool, size_t size) {

  6. u_char *m;

  7. ngx_pool_t *p;

  8.  
  9. /* 判断每次分配的内存大小,如果超出pool->max的限制,则需要走大数据内存分配策略 */

  10. if (size <= pool->max) {

  11.  
  12. p = pool->current;

  13.  
  14. /*

  15. * 循环读取缓存池链p->d.next的各个的ngx_pool_t节点,

  16. * 如果剩余的空间可以容纳size,则返回指针地址

  17. *

  18. * 这边的循环,实际上最多只有4次,具体可以看ngx_palloc_block函数

  19. * */

  20. do {

  21. /* 对齐操作,会损失内存,但是提高内存使用速度 */

  22. m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);

  23.  
  24. if ((size_t)(p->d.end - m) >= size) {

  25. p->d.last = m + size;

  26.  
  27. return m;

  28. }

  29.  
  30. p = p->d.next;

  31.  
  32. } while (p);

  33.  
  34. /* 如果没有缓存池空间没有可以容纳大小为size的内存块,则需要重新申请一个缓存池pool节点 */

  35. return ngx_palloc_block(pool, size);

  36. }

  37.  
  38. /* 走大数据分配策略 ,在pool->large链表上分配 */

  39. return ngx_palloc_large(pool, size);

  40. }

  41.  
  42. /**

  43. * 内存池分配一块内存,返回void类型指针

  44. * 不考虑对齐情况

  45. */

  46. void *

  47. ngx_pnalloc(ngx_pool_t *pool, size_t size) {

  48. u_char *m;

  49. ngx_pool_t *p;

  50.  
  51. /* 判断每次分配的内存大小,如果超出pool->max的限制,则需要走大数据内存分配策略 */

  52. if (size <= pool->max) {

  53.  
  54. p = pool->current;

  55.  
  56. /* 循环读取数据区域的各个ngx_pool_t缓存池链,如果剩余的空间可以容纳size,则返回指针地址*/

  57. do {

  58. m = p->d.last; //分配的内存块的地址

  59.  
  60. if ((size_t)(p->d.end - m) >= size) {

  61. p->d.last = m + size;

  62.  
  63. return m;

  64. }

  65.  
  66. p = p->d.next;

  67.  
  68. } while (p);

  69.  
  70. /* 如果没有缓存池空间没有可以容纳大小为size的内存块,则需要重新申请一个缓存池*/

  71. return ngx_palloc_block(pool, size);

  72. }

  73.  
  74. /* 走大数据分配策略 */

  75. return ngx_palloc_large(pool, size);

  76. }


内存分配逻辑:

 

1. 分配一块内存,如果分配的内存size小于内存池的pool->max的限制,则属于小内存块分配,走小内存块分配逻辑;否则走大内存分配逻辑。

2. 小内存分配逻辑:循环读取pool->d上的内存块,是否有足够的空间容纳需要分配的size,如果可以容纳,则直接分配内存;否则内存池需要申请新的内存块,调用ngx_palloc_block。

3. 大内存分配逻辑:当分配的内存size大于内存池的pool->max的限制,则会直接调用ngx_palloc_large方法申请一块独立的内存块,并且将内存块挂载到pool->large的链表上进行统一管理。

 

ngx_palloc_block,内存池扩容:

 

 
  1. /**

  2. * 申请一个新的缓存池 ngx_pool_t

  3. * 新的缓存池会挂载在主缓存池的 数据区域 (pool->d->next)

  4. */

  5. static void *

  6. ngx_palloc_block(ngx_pool_t *pool, size_t size) {

  7. u_char *m;

  8. size_t psize;

  9. ngx_pool_t *p, *new, *current;

  10.  
  11. psize = (size_t)(pool->d.end - (u_char *) pool);

  12.  
  13. /* 申请新的块 */

  14. m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);

  15. if (m == NULL) {

  16. return NULL;

  17. }

  18.  
  19. new = (ngx_pool_t *) m;

  20.  
  21. new->d.end = m + psize;

  22. new->d.next = NULL;

  23. new->d.failed = 0;

  24.  
  25. /* 分配size大小的内存块,返回m指针地址 */

  26. m += sizeof(ngx_pool_data_t);

  27. m = ngx_align_ptr(m, NGX_ALIGNMENT);

  28. new->d.last = m + size;

  29.  
  30. current = pool->current;

  31.  
  32. /**

  33. * 缓存池的pool数据结构会挂载子节点的ngx_pool_t数据结构

  34. * 子节点的ngx_pool_t数据结构中只用到pool->d的结构,只保存数据

  35. * 每添加一个子节点,p->d.failed就会+1,当添加超过4个子节点的时候,

  36. * pool->current会指向到最新的子节点地址

  37. *

  38. * 这个逻辑主要是为了防止pool上的子节点过多,导致每次ngx_palloc循环pool->d.next链表

  39. * 将pool->current设置成最新的子节点之后,每次最大循环4次,不会去遍历整个缓存池链表

  40. */

  41. for (p = current; p->d.next; p = p->d.next) {

  42. if (p->d.failed++ > 4) {

  43. current = p->d.next;

  44. }

  45. }

  46.  
  47. p->d.next = new;

  48.  
  49. /* 最终这个还是没变 */

  50. pool->current = current ? current : new;

  51.  
  52. return m;

  53. }


分配一块大内存,挂载到pool->large链表上ngx_palloc_large:

 

 

 
  1. /**

  2. * 当分配的内存块大小超出pool->max限制的时候,需要分配在pool->large上

  3. */

  4. static void *

  5. ngx_palloc_large(ngx_pool_t *pool, size_t size) {

  6. void *p;

  7. ngx_uint_t n;

  8. ngx_pool_large_t *large;

  9.  
  10. /* 分配一块新的大内存块 */

  11. p = ngx_alloc(size, pool->log);

  12. if (p == NULL) {

  13. return NULL;

  14. }

  15.  
  16. n = 0;

  17.  
  18. /* 去pool->large链表上查询是否有NULL的,只在链表上往下查询3次,主要判断大数据块是否有被释放的,如果没有则只能跳出*/

  19. for (large = pool->large; large; large = large->next) {

  20. if (large->alloc == NULL) {

  21. large->alloc = p;

  22. return p;

  23. }

  24.  
  25. if (n++ > 3) {

  26. break;

  27. }

  28. }

  29.  
  30. /* 分配一个ngx_pool_large_t 数据结构 */

  31. large = ngx_palloc(pool, sizeof(ngx_pool_large_t));

  32. if (large == NULL) {

  33. ngx_free(p); //如果分配失败,删除内存块

  34. return NULL;

  35. }

  36.  
  37. large->alloc = p;

  38. large->next = pool->large;

  39. pool->large = large;

  40.  
  41. return p;

  42. }

 

大内存块的释放ngx_pfree

内存池释放需要走ngx_destroy_pool,独立大内存块的单独释放,可以走ngx_pfree方法。

 

 
  1. /**

  2. * 大内存块释放 pool->large

  3. */

  4. ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p) {

  5. ngx_pool_large_t *l;

  6.  
  7. /* 在pool->large链上循环搜索,并且只释放内容区域,不释放ngx_pool_large_t数据结构*/

  8. for (l = pool->large; l; l = l->next) {

  9. if (p == l->alloc) {

  10. ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,

  11. "free: %p", l->alloc);

  12. ngx_free(l->alloc);

  13. l->alloc = NULL;

  14.  
  15. return NGX_OK;

  16. }

  17. }

  18.  
  19. return NGX_DECLINED;

  20. }

 

cleanup机制 可以回调函数清理数据

Nginx的内存池cleanup机制,设计的非常巧妙。pool->cleanup本身是一个链表,每个ngx_pool_cleanup_t的数据结构上,保存着内存数据的本身cleanup->data和回调清理函数cleanup->handler。

通过cleanup的机制,我们就可以在内存池上保存例如文件句柄fd的资源。当我们调用ngx_destroy_pool方法销毁内存池的时候,首先会来清理pool->cleanup,并且都会执行c->handler(c->data)回调函数,用于清理资源。

Nginx的这个机制,最显著的就是让文件描述符和需要自定义清理的数据的管理变得更加简单。

 

分配一个cleanup结构:

 

 
  1. /**

  2. * 分配一个可以用于回调函数清理内存块的内存

  3. * 内存块仍旧在p->d或p->large上

  4. *

  5. * ngx_pool_t中的cleanup字段管理着一个特殊的链表,该链表的每一项都记录着一个特殊的需要释放的资源。

  6. * 对于这个链表中每个节点所包含的资源如何去释放,是自说明的。这也就提供了非常大的灵活性。

  7. * 意味着,ngx_pool_t不仅仅可以管理内存,通过这个机制,也可以管理任何需要释放的资源,

  8. * 例如,关闭文件,或者删除文件等等的。下面我们看一下这个链表每个节点的类型

  9. *

  10. * 一般分两种情况:

  11. * 1. 文件描述符

  12. * 2. 外部自定义回调函数可以来清理内存

  13. */

  14. ngx_pool_cleanup_t *

  15. ngx_pool_cleanup_add(ngx_pool_t *p, size_t size) {

  16. ngx_pool_cleanup_t *c;

  17.  
  18. /* 分配一个ngx_pool_cleanup_t */

  19. c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));

  20. if (c == NULL) {

  21. return NULL;

  22. }

  23.  
  24. /* 如果size !=0 从pool->d或pool->large分配一个内存块 */

  25. if (size) {

  26. /* */

  27. c->data = ngx_palloc(p, size);

  28. if (c->data == NULL) {

  29. return NULL;

  30. }

  31.  
  32. } else {

  33. c->data = NULL;

  34. }

  35.  
  36. /* handler为回调函数 */

  37. c->handler = NULL;

  38. c->next = p->cleanup;

  39.  
  40. p->cleanup = c;

  41.  
  42. ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);

  43.  
  44. return c;

  45. }


手动清理 p->cleanup链表上的数据:(内存池销毁函数ngx_destroy_pool也会清理p->cleanup)

 

 

 
  1. /**

  2. * 清除 p->cleanup链表上的内存块(主要是文件描述符)

  3. * 回调函数:ngx_pool_cleanup_file

  4. */

  5. void ngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd) {

  6. ngx_pool_cleanup_t *c;

  7. ngx_pool_cleanup_file_t *cf;

  8.  
  9. for (c = p->cleanup; c; c = c->next) {

  10. if (c->handler == ngx_pool_cleanup_file) {

  11.  
  12. cf = c->data;

  13.  
  14. if (cf->fd == fd) {

  15. c->handler(cf); /* 调用回调函数 */

  16. c->handler = NULL;

  17. return;

  18. }

  19. }

  20. }

  21. }


关闭文件的回调函数和删除文件的回调函数。这个是文件句柄通用的回调函数,可以放置在p->cleanup->handler上。

 

 

 
  1. /**

  2. * 关闭文件回调函数

  3. * ngx_pool_run_cleanup_file方法执行的时候,用了此函数作为回调函数的,都会被清理

  4. */

  5. void ngx_pool_cleanup_file(void *data) {

  6. ngx_pool_cleanup_file_t *c = data;

  7.  
  8. ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, c->log, 0, "file cleanup: fd:%d",

  9. c->fd);

  10.  
  11. if (ngx_close_file(c->fd) == NGX_FILE_ERROR) {

  12. ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,

  13. ngx_close_file_n " \"%s\" failed", c->name);

  14. }

  15. }

 

 

 
  1. /**

  2. * 删除文件回调函数

  3. */

  4. void ngx_pool_delete_file(void *data) {

  5. ngx_pool_cleanup_file_t *c = data;

  6.  
  7. ngx_err_t err;

  8.  
  9. ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, c->log, 0, "file cleanup: fd:%d %s",

  10. c->fd, c->name);

  11.  
  12. if (ngx_delete_file(c->name) == NGX_FILE_ERROR) {

  13. err = ngx_errno;

  14.  
  15. if (err != NGX_ENOENT) {

  16. ngx_log_error(NGX_LOG_CRIT, c->log, err,

  17. ngx_delete_file_n " \"%s\" failed", c->name);

  18. }

  19. }

  20.  
  21. if (ngx_close_file(c->fd) == NGX_FILE_ERROR) {

  22. ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,

  23. ngx_close_file_n " \"%s\" failed", c->name);

  24. }

  25. }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SQLAlchemy 是一个 SQL 工具包和对象关系映射(ORM)库,用于 Python 编程语言。它提供了一个高级的 SQL 工具和对象关系映射工具,允许开发者以 Python 类和对象的形式操作数据库,而无需编写大量的 SQL 语句。SQLAlchemy 建立在 DBAPI 之上,支持多种数据库后端,如 SQLite, MySQL, PostgreSQL 等。 SQLAlchemy 的核心功能: 对象关系映射(ORM): SQLAlchemy 允许开发者使用 Python 类来表示数据库表,使用类的实例表示表中的行。 开发者可以定义类之间的关系(如一对多、多对多),SQLAlchemy 会自动处理这些关系在数据库中的映射。 通过 ORM,开发者可以像操作 Python 对象一样操作数据库,这大大简化了数据库操作的复杂性。 表达式语言: SQLAlchemy 提供了一个丰富的 SQL 表达式语言,允许开发者以 Python 表达式的方式编写复杂的 SQL 查询。 表达式语言提供了对 SQL 语句的灵活控制,同时保持了代码的可读性和可维护性。 数据库引擎和连接池: SQLAlchemy 支持多种数据库后端,并且为每种后端提供了对应的数据库引擎。 它还提供了连接池管理功能,以优化数据库连接的创建、使用和释放。 会话管理: SQLAlchemy 使用会话(Session)来管理对象的持久化状态。 会话提供了一个工作单元(unit of work)和身份映射(identity map)的概念,使得对象的状态管理和查询更加高效。 事件系统: SQLAlchemy 提供了一个事件系统,允许开发者在 ORM 的各个生命周期阶段插入自定义的钩子函数。 这使得开发者可以在对象加载、修改、删除等操作时执行额外的逻辑。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值