免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
12下一页
最近访问板块 发新帖
查看: 6367 | 回复: 12

[FreeBSD] 一个真实的FreeBSD进程的地址空间的探察分析 [复制链接]

论坛徽章:
0
发表于 2006-05-09 15:42 |显示全部楼层
一个真实的FreeBSD进程的地址空间的探察分析

雨丝风片:chinaunix.net


为了形象地描述一个真实的FreeBSD进程的地址空间的组织结构,我写了一个简单的程序,然后使用FreeBSD的kvm接口探察组成它的虚拟地址空间的数据结构,希望通过形象的图示和对真实数据的分析来讲述相关的知识,以便和代码阅读、分析形成有效互补。内核空间原是一篇漆黑,一切都只能停留在由内核源代码产生的想象中。现在有了kvm这个手电筒,就可以借着这点亮光探索前进了。希望大家能够多提意见和问题,多指出方向,以便我们能探索出更大的未知领域。

参考文献:
【FreeBSD操作系统设计与实现】第五章:Memory Management
【FreeBSD虚存系统splay树的基本原理】
【FreeBSD虚存系统splay树的代码分析】
【使用kvm接口探察内核空间的方法】

作为测试进程的小程序很简单:

  1.       1    int main(void)
  2.       2    {
  3.       3        for(;;)
  4.       4            ;
  5.       5    }
复制代码


一旦运行,这个进程便会陷入无限循环中,处于一种相对静止的状态,方便我们使用kvm接口对其进行探察。我们从描述其地址空间的vmspace结构体开始,把vm_map结构体和描述其地址空间中的连续区域的所有vm_map_entry结构体的内容都打印了出来,如下图1所示:


图1 一个真实进程的地址空间的组成结构图示


图1中蓝色小方块就是vm_map_entry结构体,除了位于vm_map结构体中的一个是用来作为头节点之外,实际用来描述测试进程地址空间的vm_map_entry结构体共有11个,它们分别表示地址空间中的一段连续区域。这11个vm_map_entry结构体是按照splay树的形态绘制的,树中的左右孩子指针在图1中用黑色箭头表示。我们知道,vm_map_entry结构体除了组织成splay树之外,还组织成了一个双向循环链表,其中,prev指针在图1中用红色箭头表示,而next指针则用蓝色箭头表示。注意,vm_map结构体里面的header也是这个双向循环链表中的一员。

图1中下方的方块分别给出了每个vm_map_entry结构体所表示的内存区域的大小,地址相邻的vm_map_entry结构体画在一排,我们可以看出,地址空间实际上由三大块组成。

下面我们来看看所打印出来的每个数字的实际含义。

首先来看看vmspace结构体:

  1.     space.vm_swrss = 0
  2.     space.vm_tsize = 1
  3.     space.vm_dsize = 1
  4.     space.vm_ssize = 32
  5.     space.vm_taddr = 0x8048000
  6.     space.vm_daddr = 0x8049000
  7.     space.vm_maxsaddr = 0xbbc00000
  8.     space.vm_exitingcnt = 0
  9.     space.vm_refcnt = 1
复制代码

  • vm_swrss是一个以页面为单位的数字,表示在最近的一次swap操作之前这个进程的驻留集大小,实际上就是说在上次swap之前这个进程有多少个页面驻留在内存中。这个值在创建vmspace结构体的时候被初始化为0,仅在swap操作中被赋予vmspace结构体中的pmap结构体中的pmap_statistics结构体中的resident_count字段的值,这个字段记录的是当前已被映射的页面总数。在我们的例子里,resident_count的值为101,而vm_swrss的值为0,说明这个进程还没有被swap out过。
  • vm_tsize表示以页面为单位的text段的大小。实际上,图1中标记为1的vm_map_entry结构体所表示的内存区域就是text段,我们可以看到它的大小是4K,也就是一个页面,因此这里vm_tsize的值为1。
  • vm_dsize表示以页面为单位的data段的大小。图1中标记为2的vm_map_entry结构体就是用来表示data段的。data段是一个可以动态增长的区域,但它目前的大小为4K,因此这里vm_dsize的值为1。
  • vm_ssize表示以页面为单位的stack段的大小。图1中标记为11的,也就是最后一个vm_map_entry结构体就是用来表示stack段的,这也是一个可以动态增长的区域,但它目前的大小为128K,因此这里vm_ssize的值就是32。
  • vm_taddr表示text段的起始地址,我们稍后可以看到,这和标记为1的vm_map_entry结构体的start地址是一致的。
  • vm_daddr表示data段的起始地址,同样,它和标记为2的vm_map_entry结构体的start地址是一致的。
  • vm_maxsaddr表示用户stack可以增长的最大地址。实际上,第11个vm_map_entry结构体的end地址就是用户stack的起始地址,我们稍后可以看到,它的值是0xbfc00000,而此处vm_maxsaddr的值为0xbbc00000,其间一共有64M的空间,这就是用户stack段的最大尺寸。这和系统对MAXSSIZ参数的设置是保持一致的。
  • vm_exitingcnt字段的含义尚不清楚,应和进程的退出状态有关。
  • vm_refcnt表示这个地址空间的引用计数,因为有可能会有多个进程共享同一个地址空间,比如vfork的情况,而此处vm_refcnt的值为1,表示这个地址空间目前只属于一个进程。

我们再来看看vmspace结构体中的vm_map结构体的内容:

  1.     vm_map.nentries = 11
  2.     vm_map.size = 1196032
  3.     vm_map.timestamp = 0x28
  4.     vm_map.needs_wakeup = 0
  5.     vm_map.system_map = 0
  6.     vm_map.flags = 0x0
  7.     vm_map.root = 0xc209a660
  8.     vm_map.pmap = 0xc1de9da4
复制代码

  • nentries表示的是用来描述地址空间的vm_map_entry结构体的数目(不包括header),从图1中我们可以看到,这样的vm_map_entry结构体一共有11个,因此nentries的值为11。
  • size表示的是以字节为单位的现有虚拟地址空间的大小,实际上就是所有vm_map_entry结构体所表示的连续内存区域尺寸的总和,因此,我们把图1下方给出的所有vm_map_entry结构体中的内存尺寸加起来就是1168K,也就是此处size的值。
  • timestamp和needs_wakeup的用途尚不清楚。
  • system_map用来表示这个映射是一个系统映射还是一个用户映射,由于这个映射是一个用户进程的映射,因此system_map的值为0。
  • flags的用途尚不清楚。
  • root指向的是由vm_map_entry结构体组成的splay树中的根节点,从图1可以看到,当前的根节点就是标记为10的vm_map_entry结构体,因此,此处root的值就是那个vm_map_entry结构体的地址。当需要在这个地址空间里寻找一块空闲区域的时候,就是从这个root节点出发的。
  • pmap指向的是表示物理映射的pmap结构体,这属于另一条探察主线,稍后再叙。

下面我们就来看看图1中的那些vm_map_entry结构体的内容:

  1. header.prev = 0xc209a61c
  2. header.next = 0xc209a5d8
  3. header.left = 0x0
  4. header.right = 0x0
  5. header.start = 0x0
  6. header.end = 0xbfc00000
  7. header.avail_ssize = 0
  8. header.adj_free = 0
  9. header.max_free = 0
  10. header.offset = 0
  11. header.eflags = 0x0

  12. sn = 1
  13. entry addr = 0xc209a5d8
  14. vm_map_entry.prev = 0xc1de9ce4
  15. vm_map_entry.next = 0xc209a594
  16. vm_map_entry.left = 0x0
  17. vm_map_entry.right = 0x0
  18. vm_map_entry.start = 0x8048000
  19. vm_map_entry.end = 0x8049000
  20. vm_map_entry.avail_ssize = 0x0
  21. vm_map_entry.adj_free = 0
  22. vm_map_entry.max_free = 0
  23. vm_map_entry.offset = 0
  24. vm_map_entry.eflags = 0x40c


  25. sn = 2
  26. entry addr = 0xc209a594
  27. vm_map_entry.prev = 0xc209a5d8
  28. vm_map_entry.next = 0xc2057220
  29. vm_map_entry.left = 0xc209a5d8
  30. vm_map_entry.right = 0x0
  31. vm_map_entry.start = 0x8049000
  32. vm_map_entry.end = 0x804a000
  33. vm_map_entry.avail_ssize = 0x0
  34. vm_map_entry.adj_free = 536866816
  35. vm_map_entry.max_free = 536866816
  36. vm_map_entry.offset = 0
  37. vm_map_entry.eflags = 0x0


  38. sn = 3
  39. entry addr = 0xc2057220
  40. vm_map_entry.prev = 0xc209a594
  41. vm_map_entry.next = 0xc209a484
  42. vm_map_entry.left = 0xc209a594
  43. vm_map_entry.right = 0xc209a484
  44. vm_map_entry.start = 0x28049000
  45. vm_map_entry.end = 0x28066000
  46. vm_map_entry.avail_ssize = 0x0
  47. vm_map_entry.adj_free = 0
  48. vm_map_entry.max_free = 536866816
  49. vm_map_entry.offset = 0
  50. vm_map_entry.eflags = 0x40c


  51. sn = 4
  52. entry addr = 0xc209a484
  53. vm_map_entry.prev = 0xc2057220
  54. vm_map_entry.next = 0xc1fd0e58
  55. vm_map_entry.left = 0x0
  56. vm_map_entry.right = 0x0
  57. vm_map_entry.start = 0x28066000
  58. vm_map_entry.end = 0x28067000
  59. vm_map_entry.avail_ssize = 0x0
  60. vm_map_entry.adj_free = 0
  61. vm_map_entry.max_free = 0
  62. vm_map_entry.offset = 0
  63. vm_map_entry.eflags = 0x4


  64. sn = 5
  65. entry addr = 0xc1fd0e58
  66. vm_map_entry.prev = 0xc209a484
  67. vm_map_entry.next = 0xc1fdfa5c
  68. vm_map_entry.left = 0xc2057220
  69. vm_map_entry.right = 0x0
  70. vm_map_entry.start = 0x28067000
  71. vm_map_entry.end = 0x2806c000
  72. vm_map_entry.avail_ssize = 0x0
  73. vm_map_entry.adj_free = 0
  74. vm_map_entry.max_free = 536866816
  75. vm_map_entry.offset = 0
  76. vm_map_entry.eflags = 0x0


  77. sn = 6
  78. entry addr = 0xc1fdfa5c
  79. vm_map_entry.prev = 0xc1fd0e58
  80. vm_map_entry.next = 0xc209a50c
  81. vm_map_entry.left = 0xc1fd0e58
  82. vm_map_entry.right = 0x0
  83. vm_map_entry.start = 0x2806c000
  84. vm_map_entry.end = 0x28074000
  85. vm_map_entry.avail_ssize = 0x0
  86. vm_map_entry.adj_free = 0
  87. vm_map_entry.max_free = 536866816
  88. vm_map_entry.offset = 20480
  89. vm_map_entry.eflags = 0x0


  90. sn = 7
  91. entry addr = 0xc209a50c
  92. vm_map_entry.prev = 0xc1fdfa5c
  93. vm_map_entry.next = 0xc209a374
  94. vm_map_entry.left = 0xc1fdfa5c
  95. vm_map_entry.right = 0xc209a374
  96. vm_map_entry.start = 0x28074000
  97. vm_map_entry.end = 0x28132000
  98. vm_map_entry.avail_ssize = 0x0
  99. vm_map_entry.adj_free = 0
  100. vm_map_entry.max_free = 536866816
  101. vm_map_entry.offset = 0
  102. vm_map_entry.eflags = 0x404


  103. sn = 8
  104. entry addr = 0xc209a374
  105. vm_map_entry.prev = 0xc209a50c
  106. vm_map_entry.next = 0xc209a3fc
  107. vm_map_entry.left = 0x0
  108. vm_map_entry.right = 0x0
  109. vm_map_entry.start = 0x28132000
  110. vm_map_entry.end = 0x28133000
  111. vm_map_entry.avail_ssize = 0x0
  112. vm_map_entry.adj_free = 0
  113. vm_map_entry.max_free = 0
  114. vm_map_entry.offset = 0
  115. vm_map_entry.eflags = 0x404


  116. sn = 9
  117. entry addr = 0xc209a3fc
  118. vm_map_entry.prev = 0xc209a374
  119. vm_map_entry.next = 0xc209a660
  120. vm_map_entry.left = 0xc209a50c
  121. vm_map_entry.right = 0x0
  122. vm_map_entry.start = 0x28133000
  123. vm_map_entry.end = 0x28138000
  124. vm_map_entry.avail_ssize = 0x0
  125. vm_map_entry.adj_free = 0
  126. vm_map_entry.max_free = 536866816
  127. vm_map_entry.offset = 0
  128. vm_map_entry.eflags = 0x4


  129. sn = 10
  130. entry addr = 0xc209a660
  131. vm_map_entry.prev = 0xc209a3fc
  132. vm_map_entry.next = 0xc209a61c
  133. vm_map_entry.left = 0xc209a3fc
  134. vm_map_entry.right = 0xc209a61c
  135. vm_map_entry.start = 0x28138000
  136. vm_map_entry.end = 0x2814b000
  137. vm_map_entry.avail_ssize = 0x0
  138. vm_map_entry.adj_free = 2544455680
  139. vm_map_entry.max_free = 2544455680
  140. vm_map_entry.offset = 0
  141. vm_map_entry.eflags = 0x0


  142. sn = 11
  143. entry addr = 0xc209a61c
  144. vm_map_entry.prev = 0xc209a660
  145. vm_map_entry.next = 0xc1de9ce4
  146. vm_map_entry.left = 0x0
  147. vm_map_entry.right = 0x0
  148. vm_map_entry.start = 0xbfbe0000
  149. vm_map_entry.end = 0xbfc00000
  150. vm_map_entry.avail_ssize = 0x3fe0000
  151. vm_map_entry.adj_free = 0
  152. vm_map_entry.max_free = 0
  153. vm_map_entry.offset = 0
  154. vm_map_entry.eflags = 0x1000
复制代码


首先来看vm_map结构体中的header:
  • prev表示的是header在vm_map_entry结构体双向循环链表中的前一个节点,从图1中可以看出,header的前一个节点是11号entry,下一个节点是1号entry既next字段的值。注意,不管是双向链表还是splay树,vm_map_entry结构体都是按照起始和终止地址的大小来排序的。因此,从1一直到11号entry的地址是从小到大排列的。而1号entry作为链表上的第一个节点,自然就排在header的后面。11号entry作为链表上的最后一个节点,它的下一个节点将绕回到header处。
  • 因为header并不属于splay树,因此left和right字段无效,此处为空。
  • start和end分别表示当前地址空间的起始地址和终止地址。此处start为0,表示地址空间的可用范围从0开始。end为0xbfc00000,这是第11号entry的end地址,即用户可访问地址的上限(stack的底部)。从0到0xbfc00000,一共有3G的空间,这正是我们常说的用户空间和内核空间3:1的划分。
  • avail_ssize只对表示stack的vm_map_entry结构体有效,此处为0。
  • adj_free和max_free对于header无效,此处为0。
  • eflags用于记录vm_map_entry结构体的一些标志,此处为0。

来看1号entry:
  • 它的地址为0xc209a5d8,这正是header节点的next字段的值,表明它是链表中的第一个节点。
  • prev等于0xc1de9ce4,即header的地址,因此,这应该和11号entry的next字段的值是相等的。next等于0xc209a594,这是2号节点的地址。
  • 1号节点位于splay树的最低层,因此它的left和right孩子指针都为空。
  • start地址为0x8048000,end地址为0x8049000。前已述及,这个vm_map_entry结构体表示的是进程的text段,从start和end的值来看,这个进程的text段只占用了一个4K内存页。
  • 由于这个vm_map_entry结构体并不是用来表示stack的,因此avail_ssize等于0。
  • 1号entry它在链表中的下一个节点即2号entry的地址是紧密衔接的,中间并无空闲空间,因此adj_free为0。(一个entry的adj_free字段的值等于它在链表中的下一个entry的start地址减去它自己的end地址。)
  • 1号entry在splay树中并无子树,而它自己的adj_free又等于0,因此max_free等于0。(max_free的大小等于entry自己的adj_free和其左右子树的max_free三者中的最大值。)
  • eflags等于0x40c,这是由以下几个宏值组成的:MAP_ENTRY_COW、MAP_ENTRY_NEEDS_COPY、MAP_ENTRY_NOCOREDUMP。其中MAP_ENTRY_COW和MAP_ENTRY_NEEDS_COPY使得多个进程可以共享vm_map_entry(以及vm_object)结构体,从而避免不必要的复制操作。MAP_ENTRY_NOCOREDUMP则表示这个vm_map_entry的区域不会包括在core文件中。

2号entry:
  • 它的地址为0xc209a594。
  • 它在链表中的前、后指针分别是0xc209a5d8和0xc2057220,这是1号和3号entry的地址。
  • 它在splay树中的左指针为0xc209a5d8,这是1号entry的地址。其右指针为空。
  • start地址为0x8049000,end地址为0x804a000。这个vm_map_entry结构体表示的是进程的data段,因此,目前的data段只占据了一个4K内存页。
  • avail_ssize字段无效。
  • adj_free等于536866816,这是2号entry的end地址0x804a000和3号entry的start地址0x28049000之间的空闲空间,即511.996M,在加上2号entry已经占用的0.004M,共计512M,即从2号entry的start地址到3号entry的start地址之间一共有512M的空间,这就是进程的data段的最大尺寸,这和系统的MAXDSIZ参数的设置是保持一致的。由此我们也可以推断出,3号entry的start地址就是mmap区域的起始地址。
  • 由于2号entry没有右子树,左子树的max_free又等于0,因此它的max_free就等于它自己的adj_free,也就是536866816。
  • eflags等于0,没有设置1号entry中的MAP_ENTRY_COW等标志,至少表示这个vm_map_entry表示的区域是可写的,这也正是data段的属性。

从3号到10号总共8个vm_map_entry分别表示的是8个互相邻接的mmap区域,它们之间并无本质区别,留待后面结合vm_object结构体一起进行探察分析。

11号entry:
  • 它的地址为0xc209a61c。
  • 它在链表中的前、后指针分别是0xc209a660和0xc1de9ce4,这分别是10号entry和header的地址。
  • 它在splay树中的左、右孩子指针均为0,表明它既没有左子树,也没有右子树。
  • 它的start地址为0xbfbe0000,end地址为0xbfc00000,总共128K,这是这个进程目前的stack大小。stack是可以根据需要动态增长的,增长的上限地址就是vmspace结构体中的vm_maxsaddr字段的值,在我们的例子中为0xbbc00000,即stack最大为64M,这是由系统的MAXSSIZ参数设定的。
  • 这是一个表示stack的vm_map_entry结构体,因此它的avail_ssize有效,表示stack段剩余的可供增长的空间。从最大限值64M里面除去11号entry已经占用的128K,还有63.875M,正是avail_ssize的值0x3fe0000。
  • 11号entry已经是链表中的最后一个entry了,因此adj_free自然为0。
  • 11号entry没有子树,自己的adj_free又等于0,因此max_free也就等于0了。
  • eflags等于0x1000,这是宏MAP_ENTRY_GROWS_DOWN的值,表示这个stack是从上向下生长的。


[ 本帖最后由 雨丝风片 于 2006-5-10 07:55 编辑 ]

论坛徽章:
1
寅虎
日期:2013-09-29 23:15:15
发表于 2006-05-09 16:01 |显示全部楼层
学习ing^_^

论坛徽章:
0
发表于 2006-05-09 16:31 |显示全部楼层
先收起来~~
过段时间再看了~~
现在看不懂~~

论坛徽章:
2
亥猪
日期:2014-03-19 16:36:35午马
日期:2014-11-23 23:48:46
发表于 2006-05-09 19:24 |显示全部楼层
很有分量,大手笔。
偶抽时间把你的kvm看了再说先

论坛徽章:
0
发表于 2006-05-09 20:41 |显示全部楼层
原帖由 gvim 于 2006-5-9 19:24 发表
很有分量,大手笔。
偶抽时间把你的kvm看了再说先


抽时间把你的东西也拿出来晾晾先,小心发霉了。

论坛徽章:
0
发表于 2006-05-09 22:17 |显示全部楼层
发帖时最不幸的事情是:快写完了,IE缺崩溃了.
写了一大堆后全没了,只有简单的写一点了.
看过你的文章后确实觉得不错,首先我得承认,我从没搞过VM方面的分析
在这方面我肯定是不行的.  
其次我对你文章中的所说的"5.flags的用途尚不清楚。", 我查了下VM的代码
flags和系统调用
324        AUE_NULL        MSTD        { int mlockall(int how); }
有关.是用户区对内存页面加锁用的一个系统调用(见syscalls.master).
该调用有两种方式:一个是MCL_FUTURE,另一个是MCL_CURRENT,
(SYS\SYS\mman.h第96,97行)
当然也可两种并用.他的意思分别是对将要用的页,对当前的内存页加锁,这一点是和LINUX
一样的,当方式是MCL_FUTURE时,flags标志会通过vm_map_modflags
(SYS\VM\vm_map.h地222行)
置位{加入MAP_WIREFUTURE}.当然munlockall系统调用在解锁时也要使用vm_map_modflags
函数去掉flags标志中的MAP_WIREFUTURE.
看看 MCL_FUTURE  的意思(Man Pages ):
Lock all pages mapped into the process's address space in
                  the future, at the time the mapping is established.  Note
                  that this may cause future mappings to fail if those map-
                  pings cause resource limits to be exceeded
而和flags没关系的MCL_CURRENT 的意思(Man Pages )
Lock all pages currently mapped into the process's address
                  space
我认为用户区如果要对连续的将要使用的内存页加锁时会用到flags标志(置MAP_WIREFUTURE)
对于用户进程已经有此标志的堆栈页,如果需要增加堆栈,必须同样注意保持堆栈的连续性.(vm_map.c中
第2899行)
参考:vm_map.c
        vm_mmap.c
        vm_map.h
syscalls.master
等等
好了,不写其他废话了

[ 本帖最后由 xie_minix 于 2006-5-9 23:40 编辑 ]

论坛徽章:
0
发表于 2006-05-09 23:00 |显示全部楼层
学习!!!!!

论坛徽章:
0
发表于 2006-05-10 04:18 |显示全部楼层
LZ知识太高深,偶一点看不懂,严重打击了我的自信心.

估计很多人感觉和我一样.

能否介绍些应用层方面的编程?

论坛徽章:
0
发表于 2006-05-10 08:55 |显示全部楼层
原帖由 xie_minix 于 2006-5-9 22:17 发表
发帖时最不幸的事情是:快写完了,IE缺崩溃了.
写了一大堆后全没了,只有简单的写一点了.
看过你的文章后确实觉得不错,首先我得承认,我从没搞过VM方面的分析
在这方面我肯定是不行的.  
其次我对你文章中的所说 ...


多谢指点!

我对vm的分析也是源于这里的一个关于top命令的讨论贴,总想搞清楚top输出中的SIZE和RES在“物理上”究竟是什么含义、是如何体现的,于是就一步步地挖了进去。。。

vm_map.c文件目前我也只看过和splay树有关的几个函数,其余的内容还没分析到呢,不过看了你指点的几个函数,比如内存加锁以及堆栈增长,都很有意思,找个时间去仔细看看!

原来还不了解mlockall系统调用,今天大致看了一下内核中的mlockall()函数,其间对vm_map结构体中的flags的设置确如你所说。
  1. _____________________________________________________________________FreeBSD6.0
  2. 938   int
  3. 939   mlockall(td, uap)
  4. 940       struct thread *td;
  5. 941       struct mlockall_args *uap;
  6. 942   {
  7.        ......
  8. 970       if (uap->how & MCL_FUTURE) {
  9. 971           vm_map_lock(map);
  10. 972           vm_map_modflags(map, MAP_WIREFUTURE, 0);
  11. 973           vm_map_unlock(map);
  12. 974           error = 0;
  13. 975       }
  14. ______________________________________________________/usr/src/sys/vm/vm_mmap.c
复制代码


也就是说,只有mlockall系统调用的入参为MCL_FUTURE时才会去设置vm_map的flags,而设置的值就是MAP_WIREFUTURE,即1。如果入参为MCL_CURRENT,则不去设置这个值。

我对此进行了试验,在前面的那个小程序里面增加了mlockall系统调用,然后再去查看flags的值,测试结果与上述分析一致。

另,老大以后还是在vim之类的东西里打草稿吧,否则我们老是只能看到冰山一角,太不爽了。

论坛徽章:
0
发表于 2006-05-10 10:51 |显示全部楼层
原帖由 eunyoo 于 2006-5-10 04:18 发表
LZ知识太高深,偶一点看不懂,严重打击了我的自信心.
估计很多人感觉和我一样.
能否介绍些应用层方面的编程?


看了你的回帖,心有感触,干脆另开一贴阐述了一下BSD程序开发版的立版宗旨:

http://bbs.chinaunix.net/viewthr ... &extra=page%3D1

欢迎多去给BSD程序开发版提一些意见和建议!具体到介绍应用层方面的编程,能不能具体谈一些要求,我们好做安排。
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

SACC2019中国系统架构师大会

【数字转型 架构演进】SACC2019中国系统架构师大会,8.5折限时优惠重磅来袭!
2019年10月31日~11月2日第11届中国系统架构师大会(SACC2019)将在北京隆重召开。四大主线并行的演讲模式,1个主会场、20个技术专场、超千人参与的会议规模,100+来自互联网、金融、制造业、电商等领域的嘉宾阵容,将为广大参会者提供一场最具价值的技术交流盛会。

限时8.5折扣期:2019年9月30日前


----------------------------------------

大会官网>>
  

北京盛拓优讯信息技术有限公司. 版权所有 16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122
中国互联网协会会员  联系我们:huangweiwei@it168.com
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP