如何成功运营一个在线社区 -欧洲杯足彩官网

2顶
0踩

如何成功运营一个在线社区

2015-01-16 09:33 by 正式编辑 cao345657340 评论(0) 有6712人浏览

要不要建立一个在线社区?当你了解了在线社区的力量,你一定会给出肯定的回答。

 

的ceo vanessa dimauro 会告诉你如今在线社区是连接目标用户的最有力方式。

 

在最近的一次网络研讨会上,vanessa dimaura针对爱德曼信任度调查报告( edelman trust barometer)的诸多要点,指出网络社区如何得以解决这些问题。vanessa dimaura写了一篇可谓是“社交时代的虚拟戒律碑”的《》。

 

为此,本文作者采访了vanessa dimaura:


 

社区要有一个专注的愿景

 

在列表的顶端,vanessa 写道,社区建设者必须提出一个清晰的、积极的愿景陈述,并与会员分享。她告诉我,愿景陈述将决定社区最终的样子,以及社区成员互动的方式。

 

“愿景陈述为社区的文化定下了基调,”她说。

 

“每个社区都有自己的一套规范的行为,体现了个人对个人和个人对公司的关系风格。大家都知道,无论我们是否刻意设计,文化都会产生。”

 

所以,那就写一个吧,易如反掌,不是么?貌似不是……

 

“写一个愿景描述是一个痛苦的过程,要考虑集体意识,”vanessa 说。

 

“最终结果往往是一些平淡的或者夸张的声明,没有任何特别,吸引不了任何人的兴趣。”

 

她给出了这个问题的解决方法:

 

“我经常跟我的客户玩一个叫'mad-lib‘的社区游戏,帮助他们表达自己的意图,并保持精神活力。它大概是这样的:

 

我们的社区是______的地方。

 

成员将从 __________ 和 __________ 中受益。

 

而且最激动人心的是,他们可以在线____________ 。”

 

vanessa 承认这不总是完美的办法,但是一个良好的开端。


(图:vanessa 引用了“get satisfaction”的一项调查,显示了关于客户需要什么及品牌如何满足其需求的统计数据。)

 

运营计划

 

vanessa的另一个戒律是为社区创建一个90天的运营计划 ,并且每季度修改。

 

为什么呢?

 

“机构的优先级往往每个季度改变,而整体的经营战略一般保持不变。每个季度从产品线负责人、销售、市场及merry band 获得信息并复查计划,社区可以更好地响应客户和机构的需求。"

 

她接着说,这个计划可以帮助不同的部门一起工作。vanessa 举例说,假设市场部正在举行一个活动,社区需要及时将会员们请进来。如果产品开发部推出新产品,需要提前告诉会员,早期反馈将有助于支持发布。

 

创建用户生成的内容(ugc)

 

10号规则:

 

首先把重心放在机构的内容上,然后要计划转移到原生(用户生成)的内容。

 

我请vanessa 说明b2b公司如何才能做到,产生非常吸引人的用户生成内容。这个难题一直困扰着我。

 

她告诉我,对于在线社区,成员为内容而来,因社区而留。

 

“每一块内容或每一次互动都要有它存在的理由。即使用户生成的内容是通过自愿(用户可选)的编辑过程完成的,该过程有助于用户尽可能写出有力的内容。这既可以是长尾内容,如信息类文章或方法指南,也可以是为时下而设计的一次性专题的内容,”她说。

 

不用畏惧“追逐引爆点”和触及当下最热的事情。”

 

“seo友好的社区指南,或由社区管理者撰写的关于社区利益及需要新人参与的主题的文章,通常都是成功的在线内容。” 她解释道,有些社区专注于讨论生活方式,或一些与某个品牌的产品无直接关联的话题,但这个品牌却能从这些内容中获益。

 

b2b还是b2c,不可等同对待

 

在该文的第23号规则中,vanessa写道,你需要知道,b2b和b2c社区有截然不同的最佳实践。

 

“b2b在线社区是专业性的网络,围绕一个共享的基于业务的体验,包含一系列内容和合作机会。他们一般是建立在企业与客户、职业协会或志趣相投的专业人士之间。”

 

vanessa 说,许多典型的b2b社区,对于普通大众是不可见的。它们为特定的受众服务,为这些会员提供“封闭式的体验”。相比同类的b2c社区,b2b社区往往小得多,从一百多个会员以上的都有。

 

而对于b2c来说,社区可能非常大,就某个品牌的产品使用,都能集中进行相关的特定活动。

 

“b2c在线社区是专注于品牌-消费者和消费者-消费者的网络。它们提供了一个平台,增强品牌的用户粘性,或者支持消费者使用该品牌的产品。”

 

据vanessa说,绝大多数的在线社区归为此类,不论这个在线社区是一个与顾客互动的twitter号,还是用于讨论产品使用的客服论坛。

 

找到核心用户群

 

从vanessa那里,我认识到每一个成功的在线社区都依赖于一个稳固的核心团队,方能持续成长。我问vanessa如何去寻找和开发日益重要的“小圈子”或核心。

 

她的回答是:“组建一个内测组呀,亲!”

 

她建议内测组可以由50-250名成员组成,这些人可以使公司的朋友,客户,或有兴趣成为“社区先锋”的伙伴们。

 

“要确保他们是被正式邀请来参加这个内测项目的,并且要有明确的起始点和清晰的价值定位(对他们的),”她说。她指出,启动前,很重要的事情是使内测组能够在正式上线的时候引导社区的其他所有人的行为。

 

“他们填写自己的个人资料,参与彼此的讨论,提交内容,参加一个或多个面试,给社区带来活动……于是其他人就明白可以做什么和如何在线互动。任何一个高级社区,在这条路上要走很远,但这是必经之路。”

作者简介:

 

barry feldman,运营着feldman creative,为客户提供富有时尚感和创意的内容营销策略。barry 最近被online marketing institute提名为前40名数字媒体战略师,及被linkedin称为“您需要知道的25位社会化媒体营销专家”之一。访问feldman creative及其博客(the point)。

 

英文原文:

  • 大小: 20.3 kb
  • 大小: 107.4 kb
来自:
2
0
评论 共 0 条 请登录后发表评论

发表评论

您还没有登录,请您登录后再发表评论

相关推荐

  • 四种常见的内存分配算法,简要介绍其优缺点以及代码实现

  • 内存是用于存放数据的硬件。程序在执行前需要先放到内存中才能被cpu处理。如果字长为16位的计算机“按字编址”,则每个存储单元大小为1个字;每个字的大小为16个二进制位。如果计算机“按字节编址”则每个存储单元大小为1字节,即1b,即8个二进制位。编译: 由编译程序将用户源代码编译成若干个目标模块(编译就是把高级语言翻译为机器语言)。 链接: 由链接程序将编译后形成的一组目标模块,以及所需库函数链接在一起,形成一个完整的装入模块。 装入(装载): 由装入程序将装入模块装入内存运行。1、绝对装入: 在编译时,如果

  • 简介     内存是计算机中最重要的资源之一,通常情况下,物理内存无法容纳下所有的进程。虽然物理内存的增长现在达到了n个gb,但比物理内存增长还快的是程序,所以无论物理内存如何增长,都赶不上程序增长的速度,所以操作系统如何有效的管理内存便显得尤为重要。本文讲述操作系统对于内存的管理的过去和现在,以及一些页替换的算法的介绍。   对于进程的简单介绍     在开始之前,首先从操作系统的角度简...

  • 内存空间的分配包括连续分配存储管理和非连续分配存储管理。 连续分配管理方式 首先确定外部碎片和内部碎片的定义。在内存空间中,内部碎片是指分配给某进程的内存区域中没有被用到的部分,例如一个进程5mb,操作系统为其分配了6mb,则存在1mb用不到的内部碎片。外部碎片是指还没有被分配出去(不属于任何进程),但由于太小了无法分配给申请内存空间的新进程的内存空闲区域。 单一连续分配 在单一连续分配中,内存分为系统区和用户区,系统区存放操作系统相关数据,用区去存放用户进程相关数据。在内存中只有一道用户程序。

  • 可执行程序的内存分布 gnu编译器生成的目标文件默认格式为elf(executive linked file)格式,这是linux系统所采用的可执行链接文件的通用文件格式。elf格式由若干个段(section)组成,由标准c源代码生成的目标文件中包含以下段:       .text(正文段)包含程序的指令代码,       .data(数据段)包含固定的数据,如常量,

  • 与“分页”最大的区别就是——离散分配时所分配地址空间的基本单位不同。分段管理中产生的外部碎片也可以用“紧凑”来解决,只是需要付出较大的时间代价。

  • 操作系统内存知识概括内存内存管理非连续分配管理方式虚拟内存页面置换算法页面分配策略 内存 什么是内存?有何作用? 内存可存放数据。程序执行前需要先放到内存中才能被cpu处理——缓和cpu与硬盘之间的速度矛盾。 在多道程序环境下,系统中会有多个程序并发执行,也就 是说会有多个程序的数据需要同时放到内存中。那么会给内存的存储单元编地址。 内存地址从0 开始,每个 地址对应一 个存储单元。 如果计算机“按字节编址”, 则每个存储单元大小为 1字节,即 1b,即 8个二进制位。 如果字长为16位的计算机 “按字编址

  • 动态内存分配背后的机制深刻的体现了计算机科学中的这句名言: all problem in cs can be solved by another level of indirection. — butler lampson ...

  • 在一些物理内存为8g的服务器上,主要运行一个java服务,系统内存分配如下:java服务的jvm堆大小设置为6g,一个监控进程占用大约 600m,linux自身使用大约800m。 从表面上,物理内存应该是足够使用的;但实际运行的情况是,会发生大量使用swap(说明物理内存不够使用 了),如下图所示。由于swap和gc同时发生会致使jvm严重卡顿,所以我们要追问:内存究竟去哪儿了? 要分析这个问题,理解jvm和操作系统之间的内存关系非常重要。接下来主要就linux与jvm之间的内存关系进行一些分析。 一、

  • 操作系统之内存的基础知识操作系统之内存的基础知识一、什么是内存,有何作用二、进程运行的基本原理1. 指令2. 逻辑地址vs物理地址3. 从写程序到程序运行的过程4. 链接的三种方式4.1 静态链接4.2 装入时动态链接4.3 运行时动态链接5. 装入的三种方式5.1 绝对装入5.2 静态重定位5.3 动态重定位 操作系统之内存的基础知识 一、什么是内存,有何作用 内存是用于存放数据的硬件。程序执行前需要先放到内存中才能被cpu处理。 那么,为什么必须将程序和数据先放到内存中才能被cpu处理呢?因为,cpu是

  • 原来一直对于内存分配不太清楚,尤其是jingchanne

  • 文章目录内存概述进程运行的基本原理内存管理内存空间的扩充内存空间的分配与回收动态分区分配算法 内存概述 什么是内存: 内存是用于存放数据的硬件,程序执行前需要先放到内存中才能被cpu处理。 存储单元 将内存分为一个一个的小区间,每个区间就是一个存储单元。 内存地址 给内存的存储单元编地址,有两种: 按字节编址:每个存储单元的大小为1字节(8位) 按字编址:字长为16位的计算机,每个存储单元的大...

  • 一段高级的程序语言代码要经过编译、链接、装入才能进入内存, 程序运行时创建进程 链接:将编译后的目标模块链接成一个可执行程序。有静态链接和动态链接之分。 静态链接:在程序运行前,将目标模块链接成一个完整的装入模块 需要做的两个任务:1.修改逻辑地址 2.变换外部调用符号 优缺点 优点:...

  • 内存应容纳操作系统和各种用户进程,因此应该尽可能有效地分配内存。下面介绍一种早期方法:连续内存分配。内存通常分为两个区域:一个用于驻留操作系统,另一个用于用户进程。操作系统可以放在低内存,也可放在高内存,这取决于中断向量的位置。由于中断向量通常位于低内存,因此程序员通常将操作系统也放在低内存。因此,这里只讨论操作系统位于低内存的情况,其他情况的讨论也类似。通常,我们需要将多个进程同时放在内存中。...

  • ipc的几种通信方式

  • 在多道程序当中,如果要让我们的程序运行,必须先创建进程。而创建进程的第一步便是要将程序和对应的数据装入内存——mt47h64m16hr-3:h。把用户的源程序变成可执行的程序要经历 编译 - 链接 - 装入 三个过程。   此刻我要说的就是最后的一个步骤,如何为一个用户程序分配相应的内存空间。   第一种:单一连续分配方式   适用于单用户、单任务的操作系统。没什么好讲的。   第二种:固定分区分...

  • 【操作系统-存储】存储的分配和管理方式

  • 操作系统 内存 内存使用 分页 分段 段页结合 多级页表与快表 虚拟内存 内存换入换出 cloak算法 页面置换算法 linux段页结合内存管理代码分析

  • 文将对 linux™ 程序员可以使用的内存管理技术进行概述,虽然关注的重点是 c 语言,但同样也适用于其他语言。文中将为您提供如何管理内存的细节,然后将进一步展示如何手工管理内存,如何使用引用计数或者内存池来半手工地管理内存,以及如何使用垃圾收集自动管理内存。 为什么必须管理内存 内存管理是计算机编程最为基本的领域之一。在很多脚本语言中,您不必担心内存是如何管理的,这并不能使得内存管理的重要性有一点点降低。对实际编程来说,理解您的内存管理器的能力与局限性至关重要。在大部分系统语言中,比如 c 和 c ,您必须进行内存管理。本文将介绍手工的、半手工的以及自动的内存管理实践的基本概念。 追溯到在 apple ii 上进行汇编语言编程的时代,那时内存管理还不是个大问题。您实际上在运行整个系统。系统有多少内存,您就有多少内存。您甚至不必费心思去弄明白它有多少内存,因为每一台机器的内存数量都相同。所以,如果内存需要非常固定,那么您只需要选择一个内存范围并使用它即可。 不过,即使是在这样一个简单的计算机中,您也会有问题,尤其是当您不知道程序的每个部分将需要多少内存时。如果您的空间有限,而内存需求是变化的,那么您需要一些方法来满足这些需求: 确定您是否有足够的内存来处理数据。 从可用的内存中获取一部分内存。 向可用内存池(pool)中返回部分内存,以使其可以由程序的其他部分或者其他程序使用。 实现这些需求的程序库称为 分配程序(allocators),因为它们负责分配和回收内存。程序的动态性越强,内存管理就越重要,您的内存分配程序的选择也就更重要。让我们来了解可用于内存管理的不同方法,它们的好处与不足,以及它们最适用的情形。 回页首 c 风格的内存分配程序 c 编程语言提供了两个函数来满足我们的三个需求: malloc:该函数分配给定的字节数,并返回一个指向它们的指针。如果没有足够的可用内存,那么它返回一个空指针。 free:该函数获得指向由 malloc 分配的内存片段的指针,并将其释放,以便以后的程序或操作系统使用(实际上,一些 malloc 实现只能将内存归还给程序,而无法将内存归还给操作系统)。 物理内存和虚拟内存 要理解内存在程序中是如何分配的,首先需要理解如何将内存从操作系统分配给程序。计算机上的每一个进程都认为自己可以访问所有的物理内存。显然,由于同时在运行多个程序,所以每个进程不可能拥有全部内存。实际上,这些进程使用的是 虚拟内存。 只是作为一个例子,让我们假定您的程序正在访问地址为 629 的内存。不过,虚拟内存系统不需要将其存储在位置为 629 的 ram 中。实际上,它甚至可以不在 ram 中 —— 如果物理 ram 已经满了,它甚至可能已经被转移到硬盘上!由于这类地址不必反映内存所在的物理位置,所以它们被称为虚拟内存。操作系统维持着一个虚拟地址到物理地址的转换的表,以便计算机硬件可以正确地响应地址请求。并且,如果地址在硬盘上而不是在 ram 中,那么操作系统将暂时停止您的进程,将其他内存转存到硬盘中,从硬盘上加载被请求的内存,然后再重新启动您的进程。这样,每个进程都获得了自己可以使用的地址空间,可以访问比您物理上安装的内存更多的内存。 在 32-位 x86 系统上,每一个进程可以访问 4 gb 内存。现在,大部分人的系统上并没有 4 gb 内存,即使您将 swap 也算上, 每个进程所使用的内存也肯定少于 4 gb。因此,当加载一个进程时,它会得到一个取决于某个称为 系统中断点(system break)的特定地址的初始内存分配。该地址之后是未被映射的内存 —— 用于在 ram 或者硬盘中没有分配相应物理位置的内存。因此,如果一个进程运行超出了它初始分配的内存,那么它必须请求操作系统“映射进来(map in)”更多的内存。(映射是一个表示一一对应关系的数学术语 —— 当内存的虚拟地址有一个对应的物理地址来存储内存内容时,该内存将被映射。) 基于 unix 的系统有两个可映射到附加内存中的基本系统调用: brk: brk() 是一个非常简单的系统调用。还记得系统中断点吗?该位置是进程映射的内存边界。 brk() 只是简单地将这个位置向前或者向后移动,就可以向进程添加内存或者从进程取走内存。 mmap: mmap(),或者说是“内存映像”,类似于 brk(),但是更为灵活。首先,它可以映射任何位置的内存,而不单单只局限于进程。其次,它不仅可以将虚拟地址映射到物理的 ram 或者 swap,它还可以将它们映射到文件和文件位置,这样,读写内存将对文件中的数据进行读写。不过,在这里,我们只关心 mmap 向进程添加被映射的内存的能力。 munmap() 所做的事情与 mmap() 相反。 如您所见, brk() 或者 mmap() 都可以用来向我们的进程添加额外的虚拟内存。在我们的例子中将使用 brk(),因为它更简单,更通用。 实现一个简单的分配程序 如果您曾经编写过很多 c 程序,那么您可能曾多次使用过 malloc() 和 free()。不过,您可能没有用一些时间去思考它们在您的操作系统中是如何实现的。本节将向您展示 malloc 和 free 的一个最简化实现的代码,来帮助说明管理内存时都涉及到了哪些事情。 要试着运行这些示例,需要先 复制本代码清单,并将其粘贴到一个名为 malloc.c 的文件中。接下来,我将一次一个部分地对该清单进行解释。 在大部分操作系统中,内存分配由以下两个简单的函数来处理: void *malloc(long numbytes):该函数负责分配 numbytes 大小的内存,并返回指向第一个字节的指针。 void free(void *firstbyte):如果给定一个由先前的 malloc 返回的指针,那么该函数会将分配的空间归还给进程的“空闲空间”。 malloc_init 将是初始化内存分配程序的函数。它要完成以下三件事:将分配程序标识为已经初始化,找到系统中最后一个有效内存地址,然后建立起指向我们管理的内存的指针。这三个变量都是全局变量: 清单 1. 我们的简单分配程序的全局变量 int has_initialized = 0; void *managed_memory_start; void *last_valid_address; 如前所述,被映射的内存的边界(最后一个有效地址)常被称为系统中断点或者 当前中断点。在很多 unix® 系统中,为了指出当前系统中断点,必须使用 sbrk(0) 函数。 sbrk 根据参数中给出的字节数移动当前系统中断点,然后返回新的系统中断点。使用参数 0 只是返回当前中断点。这里是我们的 malloc 初始化代码,它将找到当前中断点并初始化我们的变量: 清单 2. 分配程序初始化函数 /* include the sbrk function */ #include void malloc_init() { /* grab the last valid address from the os */ last_valid_address = sbrk(0); /* we don't have any memory to manage yet, so *just set the beginning to be last_valid_address */ managed_memory_start = last_valid_address; /* okay, we're initialized and ready to go */ has_initialized = 1; } 现在,为了完全地管理内存,我们需要能够追踪要分配和回收哪些内存。在对内存块进行了 free 调用之后,我们需要做的是诸如将它们标记为未被使用的等事情,并且,在调用 malloc 时,我们要能够定位未被使用的内存块。因此, malloc 返回的每块内存的起始处首先要有这个结构: 清单 3. 内存控制块结构定义 struct mem_control_block { int is_available; int size; }; 现在,您可能会认为当程序调用 malloc 时这会引发问题 —— 它们如何知道这个结构?答案是它们不必知道;在返回指针之前,我们会将其移动到这个结构之后,把它隐藏起来。这使得返回的指针指向没有用于任何其他用途的内存。那样,从调用程序的角度来看,它们所得到的全部是空闲的、开放的内存。然后,当通过 free() 将该指针传递回来时,我们只需要倒退几个内存字节就可以再次找到这个结构。 在讨论分配内存之前,我们将先讨论释放,因为它更简单。为了释放内存,我们必须要做的惟一一件事情就是,获得我们给出的指针,回退 sizeof(struct mem_control_block) 个字节,并将其标记为可用的。这里是对应的代码: 清单 4. 解除分配函数 void free(void *firstbyte) { struct mem_control_block *mcb; /* backup from the given pointer to find the * mem_control_block */ mcb = firstbyte - sizeof(struct mem_control_block); /* mark the block as being available */ mcb->is_available = 1; /* that's it! we're done. */ return; } 如您所见,在这个分配程序中,内存的释放使用了一个非常简单的机制,在固定时间内完成内存释放。分配内存稍微困难一些。以下是该算法的略述: 清单 5. 主分配程序的伪代码 1. if our allocator has not been initialized, initialize it. 2. add sizeof(struct mem_control_block) to the size requested. 3. start at managed_memory_start. 4. are we at last_valid address? 5. if we are: a. we didn't find any existing space that was large enough -- ask the operating system for more and return that. 6. otherwise: a. is the current space available (check is_available from the mem_control_block)? b. if it is: i) is it large enough (check "size" from the mem_control_block)? ii) if so: a. mark it as unavailable b. move past mem_control_block and return the pointer iii) otherwise: a. move forward "size" bytes b. go back go step 4 c. otherwise: i) move forward "size" bytes ii) go back to step 4 我们主要使用连接的指针遍历内存来寻找开放的内存块。这里是代码: 清单 6. 主分配程序 void *malloc(long numbytes) { /* holds where we are looking in memory */ void *current_location; /* this is the same as current_location, but cast to a * memory_control_block */ struct mem_control_block *current_location_mcb; /* this is the memory location we will return. it will * be set to 0 until we find something suitable */ void *memory_location; /* initialize if we haven't already done so */ if(! has_initialized) { malloc_init(); } /* the memory we search for has to include the memory * control block, but the users of malloc don't need * to know this, so we'll just add it in for them. */ numbytes = numbytes sizeof(struct mem_control_block); /* set memory_location to 0 until we find a suitable * location */ memory_location = 0; /* begin searching at the start of managed memory */ current_location = managed_memory_start; /* keep going until we have searched all allocated space */ while(current_location != last_valid_address) { /* current_location and current_location_mcb point * to the same address. however, current_location_mcb * is of the correct type, so we can use it as a struct. * current_location is a void pointer so we can use it * to calculate addresses. */ current_location_mcb = (struct mem_control_block *)current_location; if(current_location_mcb->is_available) { if(current_location_mcb->size >= numbytes) { /* woohoo! we've found an open, * appropriately-size location. */ /* it is no longer available */ current_location_mcb->is_available = 0; /* we own it */ memory_location = current_location; /* leave the loop */ break; } } /* if we made it here, it's because the current memory * block not suitable; move to the next one */ current_location = current_location current_location_mcb->size; } /* if we still don't have a valid location, we'll * have to ask the operating system for more memory */ if(! memory_location) { /* move the program break numbytes further */ sbrk(numbytes); /* the new memory will be where the last valid * address left off */ memory_location = last_valid_address; /* we'll move the last valid address forward * numbytes */ last_valid_address = last_valid_address numbytes; /* we need to initialize the mem_control_block */ current_location_mcb = memory_location; current_location_mcb->is_available = 0; current_location_mcb->size = numbytes; } /* now, no matter what (well, except for error conditions), * memory_location has the address of the memory, including * the mem_control_block */ /* move the pointer past the mem_control_block */ memory_location = memory_location sizeof(struct mem_control_block); /* return the pointer */ return memory_location; } 这就是我们的内存管理器。现在,我们只需要构建它,并在程序中使用它即可。 运行下面的命令来构建 malloc 兼容的分配程序(实际上,我们忽略了 realloc() 等一些函数,不过, malloc() 和 free() 才是最主要的函数): 清单 7. 编译分配程序 gcc -shared -fpic malloc.c -o malloc.so 该程序将生成一个名为 malloc.so 的文件,它是一个包含有我们的代码的共享库。 在 unix 系统中,现在您可以用您的分配程序来取代系统的 malloc(),做法如下: 清单 8. 替换您的标准的 malloc ld_preload=/path/to/malloc.so export ld_preload ld_preload 环境变量使动态链接器在加载任何可执行程序之前,先加载给定的共享库的符号。它还为特定库中的符号赋予优先权。因此,从现在起,该会话中的任何应用程序都将使用我们的 malloc(),而不是只有系统的应用程序能够使用。有一些应用程序不使用 malloc(),不过它们是例外。其他使用 realloc() 等其他内存管理函数的应用程序,或者错误地假定 malloc() 内部行为的那些应用程序,很可能会崩溃。ash shell 似乎可以使用我们的新 malloc() 很好地工作。 如果您想确保 malloc() 正在被使用,那么您应该通过向函数的入口点添加 write() 调用来进行测试。 我们的内存管理器在很多方面都还存在欠缺,但它可以有效地展示内存管理需要做什么事情。它的某些缺点包括: 由于它对系统中断点(一个全局变量)进行操作,所以它不能与其他分配程序或者 mmap 一起使用。 当分配内存时,在最坏的情形下,它将不得不遍历 全部进程内存;其中可能包括位于硬盘上的很多内存,这意味着操作系统将不得不花时间去向硬盘移入数据和从硬盘中移出数据。 没有很好的内存不足处理方案( malloc 只假定内存分配是成功的)。 它没有实现很多其他的内存函数,比如 realloc()。 由于 sbrk() 可能会交回比我们请求的更多的内存,所以在堆(heap)的末端会遗漏一些内存。 虽然 is_available 标记只包含一位信息,但它要使用完整的 4-字节 的字。 分配程序不是线程安全的。 分配程序不能将空闲空间拼合为更大的内存块。 分配程序的过于简单的匹配算法会导致产生很多潜在的内存碎片。 我确信还有很多其他问题。这就是为什么它只是一个例子! 其他 malloc 实现 malloc() 的实现有很多,这些实现各有优点与缺点。在设计一个分配程序时,要面临许多需要折衷的选择,其中包括: 分配的速度。 回收的速度。 有线程的环境的行为。 内存将要被用光时的行为。 局部缓存。 簿记(bookkeeping)内存开销。 虚拟内存环境中的行为。 小的或者大的对象。 实时保证。 每一个实现都有其自身的优缺点集合。在我们的简单的分配程序中,分配非常慢,而回收非常快。另外,由于它在使用虚拟内存系统方面较差,所以它最适于处理大的对象。 还有其他许多分配程序可以使用。其中包括: doug lea malloc:doug lea malloc 实际上是完整的一组分配程序,其中包括 doug lea 的原始分配程序,gnu libc 分配程序和 ptmalloc。 doug lea 的分配程序有着与我们的版本非常类似的基本结构,但是它加入了索引,这使得搜索速度更快,并且可以将多个没有被使用的块组合为一个大的块。它还支持缓存,以便更快地再次使用最近释放的内存。 ptmalloc 是 doug lea malloc 的一个扩展版本,支持多线程。在本文后面的 参考资料部分中,有一篇描述 doug lea 的 malloc 实现的文章。 bsd malloc:bsd malloc 是随 4.2 bsd 发行的实现,包含在 freebsd 之中,这个分配程序可以从预先确实大小的对象构成的池中分配对象。它有一些用于对象大小的 size 类,这些对象的大小为 2 的若干次幂减去某一常数。所以,如果您请求给定大小的一个对象,它就简单地分配一个与之匹配的 size 类。这样就提供了一个快速的实现,但是可能会浪费内存。在 参考资料部分中,有一篇描述该实现的文章。 hoard:编写 hoard 的目标是使内存分配在多线程环境中进行得非常快。因此,它的构造以锁的使用为中心,从而使所有进程不必等待分配内存。它可以显著地加快那些进行很多分配和回收的多线程进程的速度。在 参考资料部分中,有一篇描述该实现的文章。 众多可用的分配程序中最有名的就是上述这些分配程序。如果您的程序有特别的分配需求,那么您可能更愿意编写一个定制的能匹配您的程序内存分配方式的分配程序。不过,如果不熟悉分配程序的设计,那么定制分配程序通常会带来比它们解决的问题更多的问题。要获得关于该主题的适当的介绍,请参阅 donald knuth 撰写的 the art of computer programming volume 1: fundamental algorithms 中的第 2.5 节“dynamic storage allocation”(请参阅 参考资料中的链接)。它有点过时,因为它没有考虑虚拟内存环境,不过大部分算法都是基于前面给出的函数。 在 c 中,通过重载 operator new(),您可以以每个类或者每个模板为单位实现自己的分配程序。在 andrei alexandrescu 撰写的 modern c design 的第 4 章(“small object allocation”)中,描述了一个小对象分配程序(请参阅 参考资料中的链接)。 基于 malloc() 的内存管理的缺点 不只是我们的内存管理器有缺点,基于 malloc() 的内存管理器仍然也有很多缺点,不管您使用的是哪个分配程序。对于那些需要保持长期存储的程序使用 malloc() 来管理内存可能会非常令人失望。如果您有大量的不固定的内存引用,经常难以知道它们何时被释放。生存期局限于当前函数的内存非常容易管理,但是对于生存期超出该范围的内存来说,管理内存则困难得多。而且,关于内存管理是由进行调用的程序还是由被调用的函数来负责这一问题,很多 api 都不是很明确。 因为管理内存的问题,很多程序倾向于使用它们自己的内存管理规则。c 的异常处理使得这项任务更成问题。有时好像致力于管理内存分配和清理的代码比实际完成计算任务的代码还要多!因此,我们将研究内存管理的其他选择。 回页首 半自动内存管理策略 引用计数 引用计数是一种 半自动(semi-automated)的内存管理技术,这表示它需要一些编程支持,但是它不需要您确切知道某一对象何时不再被使用。引用计数机制为您完成内存管理任务。 在引用计数中,所有共享的数据结构都有一个域来包含当前活动“引用”结构的次数。当向一个程序传递一个指向某个数据结构指针时,该程序会将引用计数增加 1。实质上,您是在告诉数据结构,它正在被存储在多少个位置上。然后,当您的进程完成对它的使用后,该程序就会将引用计数减少 1。结束这个动作之后,它还会检查计数是否已经减到零。如果是,那么它将释放内存。 这样做的好处是,您不必追踪程序中某个给定的数据结构可能会遵循的每一条路径。每次对其局部的引用,都将导致计数的适当增加或减少。这样可以防止在使用数据结构时释放该结构。不过,当您使用某个采用引用计数的数据结构时,您必须记得运行引用计数函数。另外,内置函数和第三方的库不会知道或者可以使用您的引用计数机制。引用计数也难以处理发生循环引用的数据结构。 要实现引用计数,您只需要两个函数 —— 一个增加引用计数,一个减少引用计数并当计数减少到零时释放内存。 一个示例引用计数函数集可能看起来如下所示: 清单 9. 基本的引用计数函数 /* structure definitions*/ /* base structure that holds a refcount */ struct refcountedstruct { int refcount; } /* all refcounted structures must mirror struct * refcountedstruct for their first variables */ /* refcount maintenance functions */ /* increase reference count */ void ref(void *data) { struct refcountedstruct *rstruct; rstruct = (struct refcountedstruct *) data; rstruct->refcount ; } /* decrease reference count */ void unref(void *data) { struct refcountedstruct *rstruct; rstruct = (struct refcountedstruct *) data; rstruct->refcount--; /* free the structure if there are no more users */ if(rstruct->refcount == 0) { free(rstruct); } } ref 和 unref 可能会更复杂,这取决于您想要做的事情。例如,您可能想要为多线程程序增加锁,那么您可能想扩展 refcountedstruct,使它同样包含一个指向某个在释放内存之前要调用的函数的指针(类似于面向对象语言中的析构函数 —— 如果您的结构中包含这些指针,那么这是 必需的)。 当使用 ref 和 unref 时,您需要遵守这些指针的分配规则: unref 分配前左端指针(left-hand-side pointer)指向的值。 ref 分配后左端指针(left-hand-side pointer)指向的值。 在传递使用引用计数的结构的函数中,函数需要遵循以下这些规则: 在函数的起始处 ref 每一个指针。 在函数的结束处 unref 第一个指针。 以下是一个使用引用计数的生动的代码示例: 清单 10. 使用引用计数的示例 /* examples of usage */ /* data type to be refcounted */ struct mydata { int refcount; /* same as refcountedstruct */ int datafield1; /* fields specific to this struct */ int datafield2; /* other declarations would go here as appropriate */ }; /* use the functions in code */ void dosomething(struct mydata *data) { ref(data); /* process data */ /* when we are through */ unref(data); } struct mydata *globalvar1; /* note that in this one, we don't decrease the * refcount since we are maintaining the reference * past the end of the function call through the * global variable */ void storesomething(struct mydata *data) { ref(data); /* passed as a parameter */ globalvar1 = data; ref(data); /* ref because of assignment */ unref(data); /* function finished */ } 由于引用计数是如此简单,大部分程序员都自已去实现它,而不是使用库。不过,它们依赖于 malloc 和 free 等低层的分配程序来实际地分配和释放它们的内存。 在 perl 等高级语言中,进行内存管理时使用引用计数非常广泛。在这些语言中,引用计数由语言自动地处理,所以您根本不必担心它,除非要编写扩展模块。由于所有内容都必须进行引用计数,所以这会对速度产生一些影响,但它极大地提高了编程的安全性和方便性。以下是引用计数的益处: 实现简单。 易于使用。 由于引用是数据结构的一部分,所以它有一个好的缓存位置。 不过,它也有其不足之处: 要求您永远不要忘记调用引用计数函数。 无法释放作为循环数据结构的一部分的结构。 减缓几乎每一个指针的分配。 尽管所使用的对象采用了引用计数,但是当使用异常处理(比如 try 或 setjmp()/ longjmp())时,您必须采取其他方法。 需要额外的内存来处理引用。 引用计数占用了结构中的第一个位置,在大部分机器中最快可以访问到的就是这个位置。 在多线程环境中更慢也更难以使用。 c 可以通过使用 智能指针(smart pointers)来容忍程序员所犯的一些错误,智能指针可以为您处理引用计数等指针处理细节。不过,如果不得不使用任何先前的不能处理智能指针的代码(比如对 c 库的联接),实际上,使用它们的后果通实比不使用它们更为困难和复杂。因此,它通常只是有益于纯 c 项目。如果您想使用智能指针,那么您实在应该去阅读 alexandrescu 撰写的 modern c design 一书中的“smart pointers”那一章。 内存池 内存池是另一种半自动内存管理方法。内存池帮助某些程序进行自动内存管理,这些程序会经历一些特定的阶段,而且每个阶段中都有分配给进程的特定阶段的内存。例如,很多网络服务器进程都会分配很多针对每个连接的内存 —— 内存的最大生存期限为当前连接的存在期。apache 使用了池式内存(pooled memory),将其连接拆分为各个阶段,每个阶段都有自己的内存池。在结束每个阶段时,会一次释放所有内存。 在池式内存管理中,每次内存分配都会指定内存池,从中分配内存。每个内存池都有不同的生存期限。在 apache 中,有一个持续时间为服务器存在期的内存池,还有一个持续时间为连接的存在期的内存池,以及一个持续时间为请求的存在期的池,另外还有其他一些内存池。因此,如果我的一系列函数不会生成比连接持续时间更长的数据,那么我就可以完全从连接池中分配内存,并知道在连接结束时,这些内存会被自动释放。另外,有一些实现允许注册 清除函数(cleanup functions),在清除内存池之前,恰好可以调用它,来完成在内存被清理前需要完成的其他所有任务(类似于面向对象中的析构函数)。 要在自己的程序中使用池,您既可以使用 gnu libc 的 obstack 实现,也可以使用 apache 的 apache portable runtime。gnu obstack 的好处在于,基于 gnu 的 linux 发行版本中默认会包括它们。apache portable runtime 的好处在于它有很多其他工具,可以处理编写多平台服务器软件所有方面的事情。要深入了解 gnu obstack 和 apache 的池式内存实现,请参阅 参考资料部分中指向这些实现的文档的链接。 下面的假想代码列表展示了如何使用 obstack: 清单 11. obstack 的示例代码 #include #include /* example code listing for using obstacks */ /* used for obstack macros (xmalloc is a malloc function that exits if memory is exhausted */ #define obstack_chunk_alloc xmalloc #define obstack_chunk_free free /* pools */ /* only permanent allocations should go in this pool */ struct obstack *global_pool; /* this pool is for per-connection data */ struct obstack *connection_pool; /* this pool is for per-request data */ struct obstack *request_pool; void allocation_failed() { exit(1); } int main() { /* initialize pools */ global_pool = (struct obstack *) xmalloc (sizeof (struct obstack)); obstack_init(global_pool); connection_pool = (struct obstack *) xmalloc (sizeof (struct obstack)); obstack_init(connection_pool); request_pool = (struct obstack *) xmalloc (sizeof (struct obstack)); obstack_init(request_pool); /* set the error handling function */ obstack_alloc_failed_handler = &allocation_failed; /* server main loop */ while(1) { wait_for_connection(); /* we are in a connection */ while(more_requests_available()) { /* handle request */ handle_request(); /* free all of the memory allocated * in the request pool */ obstack_free(request_pool, null); } /* we're finished with the connection, time * to free that pool */ obstack_free(connection_pool, null); } } int handle_request() { /* be sure that all object allocations are allocated * from the request pool */ int bytes_i_need = 400; void *data1 = obstack_alloc(request_pool, bytes_i_need); /* do stuff to process the request */ /* return */ return 0; } 基本上,在操作的每一个主要阶段结束之后,这个阶段的 obstack 会被释放。不过,要注意的是,如果一个过程需要分配持续时间比当前阶段更长的内存,那么它也可以使用更长期限的 obstack,比如连接或者全局内存。传递给 obstack_free() 的 null 指出它应该释放 obstack 的全部内容。可以用其他的值,但是它们通常不怎么实用。 使用池式内存分配的益处如下所示: 应用程序可以简单地管理内存。 内存分配和回收更快,因为每次都是在一个池中完成的。分配可以在 o(1) 时间内完成,释放内存池所需时间也差不多(实际上是 o(n) 时间,不过在大部分情况下会除以一个大的因数,使其变成 o(1))。 可以预先分配错误处理池(error-handling pools),以便程序在常规内存被耗尽时仍可以恢复。 有非常易于使用的标准实现。 池式内存的缺点是: 内存池只适用于操作可以分阶段的程序。 内存池通常不能与第三方库很好地合作。 如果程序的结构发生变化,则不得不修改内存池,这可能会导致内存管理系统的重新设计。 您必须记住需要从哪个池进行分配。另外,如果在这里出错,就很难捕获该内存池。 回页首 垃圾收集 垃圾收集(garbage collection)是全自动地检测并移除不再使用的数据对象。垃圾收集器通常会在当可用内存减少到少于一个具体的阈值时运行。通常,它们以程序所知的可用的一组“基本”数据 —— 栈数据、全局变量、寄存器 —— 作为出发点。然后它们尝试去追踪通过这些数据连接到每一块数据。收集器找到的都是有用的数据;它没有找到的就是垃圾,可以被销毁并重新使用这些无用的数据。为了有效地管理内存,很多类型的垃圾收集器都需要知道数据结构内部指针的规划,所以,为了正确运行垃圾收集器,它们必须是语言本身的一部分。 收集器的类型 复制(copying): 这些收集器将内存存储器分为两部分,只允许数据驻留在其中一部分上。它们定时地从“基本”的元素开始将数据从一部分复制到另一部分。内存新近被占用的部分现在成为活动的,另一部分上的所有内容都认为是垃圾。另外,当进行这项复制操作时,所有指针都必须被更新为指向每个内存条目的新位置。因此,为使用这种垃圾收集方法,垃圾收集器必须与编程语言集成在一起。 标记并清理(mark and sweep):每一块数据都被加上一个标签。不定期的,所有标签都被设置为 0,收集器从“基本”的元素开始遍历数据。当它遇到内存时,就将标签标记为 1。最后没有被标记为 1 的所有内容都认为是垃圾,以后分配内存时会重新使用它们。 增量的(incremental):增量垃圾收集器不需要遍历全部数据对象。因为在收集期间的突然等待,也因为与访问所有当前数据相关的缓存问题(所有内容都不得不被页入(page-in)),遍历所有内存会引发问题。增量收集器避免了这些问题。 保守的(conservative):保守的垃圾收集器在管理内存时不需要知道与数据结构相关的任何信息。它们只查看所有数据类型,并假定它们 可以全部都是指针。所以,如果一个字节序列可以是一个指向一块被分配的内存的指针,那么收集器就将其标记为正在被引用。有时没有被引用的内存会被收集,这样会引发问题,例如,如果一个整数域中包含一个值,该值是已分配内存的地址。不过,这种情况极少发生,而且它只会浪费少量内存。保守的收集器的优势是,它们可以与任何编程语言相集成。 hans boehm 的保守垃圾收集器是可用的最流行的垃圾收集器之一,因为它是免费的,而且既是保守的又是增量的,可以使用 --enable-redirect-malloc 选项来构建它,并且可以将它用作系统分配程序的简易替代者(drop-in replacement)(用 malloc/ free 代替它自己的 api)。实际上,如果这样做,您就可以使用与我们在示例分配程序中所使用的相同的 ld_preload 技巧,在系统上的几乎任何程序中启用垃圾收集。如果您怀疑某个程序正在泄漏内存,那么您可以使用这个垃圾收集器来控制进程。在早期,当 mozilla 严重地泄漏内存时,很多人在其中使用了这项技术。这种垃圾收集器既可以在 windows® 下运行,也可以在 unix 下运行。 垃圾收集的一些优点: 您永远不必担心内存的双重释放或者对象的生命周期。 使用某些收集器,您可以使用与常规分配相同的 api。 其缺点包括: 使用大部分收集器时,您都无法干涉何时释放内存。 在多数情况下,垃圾收集比其他形式的内存管理更慢。 垃圾收集错误引发的缺陷难于调试。 如果您忘记将不再使用的指针设置为 null,那么仍然会有内存泄漏。 回页首 结束语 一切都需要折衷:性能、易用、易于实现、支持线程的能力等,这里只列出了其中的一些。为了满足项目的要求,有很多内存管理模式可以供您使用。每种模式都有大量的实现,各有其优缺点。对很多项目来说,使用编程环境默认的技术就足够了,不过,当您的项目有特殊的需要时,了解可用的选择将会有帮助。下表对比了本文中涉及的内存管理策略。 表 1. 内存分配策略的对比 策略 分配速度 回收速度 局部缓存 易用性 通用性 实时可用 smp 线程友好 定制分配程序 取决于实现 取决于实现 取决于实现 很难 无 取决于实现 取决于实现 简单分配程序 内存使用少时较快 很快 差 容易 高 否 否 gnu malloc 中 快 中 容易 高 否 中 hoard 中 中 中 容易 高 否 是 引用计数 n/a n/a 非常好 中 中 是(取决于 malloc 实现) 取决于实现 池 中 非常快 极好 中 中 是(取决于 malloc 实现) 取决于实现 垃圾收集 中(进行收集时慢) 中 差 中 中 否 几乎不 增量垃圾收集 中 中 中 中 中 否 几乎不 增量保守垃圾收集 中 中 中 容易 高 否 几乎不 参考资料 您可以参阅本文在 developerworks 全球站点上的 英文原文。 web 上的文档 gnu c library 手册的 obstacks 部分 提供了 obstacks 编程接口。 apache portable runtime 文档 描述了它们的池式分配程序的接口。 基本的分配程序 doug lea 的 malloc 是最流行的内存分配程序之一。 bsd malloc 用于大部分基于 bsd 的系统中。 ptmalloc 起源于 doug lea 的 malloc,用于 glibc 之中。 hoard 是一个为多线程应用程序优化的 malloc 实现。 gnu memory-mapped malloc(gdb 的组成部分) 是一个基于 mmap() 的 malloc 实现。 池式分配程序 gnu obstacks(gnu libc 的组成部分)是安装最多的池式分配程序,因为在每一个基于 glibc 的系统中都有它。 apache 的池式分配程序(apache portable runtime 中) 是应用最为广泛的池式分配程序。 squid 有其自己的池式分配程序。 netbsd 也有其自己的池式分配程序。 talloc 是一个池式分配程序,是 samba 的组成部分。 智能指针和定制分配程序 loki c library 有很多为 c 实现的通用模式,包括智能指针和一个定制的小对象分配程序。 垃圾收集器 hahns boehm conservative garbage collector 是最流行的开源垃圾收集器,它可以用于常规的 c/c 程序。 关于现代操作系统中的虚拟内存的文章 marshall kirk mckusick 和 michael j. karels 合著的 a new virtual memory implementation for berkeley unix 讨论了 bsd 的 vm 系统。 mel gorman's linux vm documentation 讨论了 linux vm 系统。 关于 malloc 的文章 poul-henning kamp 撰写的 malloc in modern virtual memory environments 讨论的是 malloc 以及它如何与 bsd 虚拟内存交互。 berger、mckinley、blumofe 和 wilson 合著的 hoard -- a scalable memory allocator for multithreaded environments 讨论了 hoard 分配程序的实现。 marshall kirk mckusick 和 michael j. karels 合著的 design of a general purpose memory allocator for the 4.3bsd unix kernel 讨论了内核级的分配程序。 doug lea 撰写的 a memory allocator 给出了一个关于设计和实现分配程序的概述,其中包括设计选择与折衷。 emery d. berger 撰写的 memory management for high-performance applications 讨论的是定制内存管理以及它如何影响高性能应用程序。 关于定制分配程序的文章 doug lea 撰写的 some storage management techniques for container classes 描述的是为 c 类编写定制分配程序。 berger、zorn 和 mckinley 合著的 composing high-performance memory allocators 讨论了如何编写定制分配程序来加快具体工作的速度。 berger、zorn 和 mckinley 合著的 reconsidering custom memory allocation 再次提及了定制分配的主题,看是否真正值得为其费心。 关于垃圾收集的文章 paul r. wilson 撰写的 uniprocessor garbage collection techniques 给出了垃圾收集的一个基本概述。 benjamin zorn 撰写的 the measured cost of garbage collection 给出了关于垃圾收集和性能的硬数据(hard data)。 hans-juergen boehm 撰写的 memory allocation myths and half-truths 给出了关于垃圾收集的神话(myths)。 hans-juergen boehm 撰写的 space efficient conservative garbage collection 是一篇描述他的用于 c/c 的垃圾收集器的文章。 web 上的通用参考资料 内存管理参考 中有很多关于内存管理参考资料和技术文章的链接。 关于内存管理和内存层级的 oops group papers 是非常好的一组关于此主题的技术文章。 c 中的内存管理讨论的是为 c 编写定制的分配程序。 programming alternatives: memory management 讨论了程序员进行内存管理时的一些选择。 垃圾收集 faq 讨论了关于垃圾收集您需要了解的所有内容。 richard jones 的 garbage collection bibliography 有指向任何您想要的关于垃圾收集的文章的链接。 书籍 michael daconta 撰写的 c pointers and dynamic memory management 介绍了关于内存管理的很多技术。 frantisek franek 撰写的 memory as a programming concept in c and c 讨论了有效使用内存的技术与工具,并给出了在计算机编程中应当引起注意的内存相关错误的角色。 richard jones 和 rafael lins 合著的 garbage collection: algorithms for automatic dynamic memory management 描述了当前使用的最常见的垃圾收集算法。 在 donald knuth 撰写的 the art of computer programming 第 1 卷 fundamental algorithms 的第 2.5 节“dynamic storage allocation”中,描述了实现基本的分配程序的一些技术。 在 donald knuth 撰写的 the art of computer programming 第 1 卷 fundamental algorithms 的第 2.3.5 节“lists and garbage collection”中,讨论了用于列表的垃圾收集算法。 andrei alexandrescu 撰写的 modern c design 第 4 章“small object allocation”描述了一个比 c 标准分配程序效率高得多的一个高速小对象分配程序。 andrei alexandrescu 撰写的 modern c design 第 7 章“smart pointers”描述了在 c 中智能指针的实现。 jonathan 撰写的 programming from the ground up 第 8 章“intermediate memory topics”中有本文使用的简单分配程序的一个汇编语言版本。 来自 developerworks 自我管理数据缓冲区内存 (developerworks,2004 年 1 月)略述了一个用于管理内存的自管理的抽象数据缓存器的伪 c (pseudo-c)实现。 a framework for the user defined malloc replacement feature (developerworks,2002 年 2 月)展示了如何利用 aix 中的一个工具,使用自己设计的内存子系统取代原有的内存子系统。 掌握 linux 调试技术 (developerworks,2002 年 8 月)描述了可以使用调试方法的 4 种不同情形:段错误、内存溢出、内存泄漏和挂起。 在 处理 java 程序中的内存漏洞 (developerworks,2001 年 2 月)中,了解导致 java 内存泄漏的原因,以及何时需要考虑它们。 在 developerworks linux 专区中,可以找到更多为 linux 开发人员准备的参考资料。 从 developerworks 的 speed-start your linux app 专区中,可以下载运行于 linux 之上的 ibm 中间件产品的免费测试版本,其中包括 websphere® studio application developer、websphere application server、db2® universal database、tivoli® access manager 和 tivoli directory server,查找 how-to 文章和凯发推荐的技术支持。 通过参与 developerworks blogs 加入到 developerworks 社区。 可以在 developer bookstore linux 专栏中定购 打折出售的 linux 书籍。 关于作者 jonathan bartlett 是 programming from the ground up 一书的作者,这本书介绍的是 linux 汇编语言编程。jonathan bartlett 是 new media worx 的总开发师,负责为客户开发 web、视频、kiosk 和桌面应用程序。您可以通过 johnnyb@eskimo.com 与 jonathan 联系。

global site tag (gtag.js) - google analytics
网站地图