Chinaunix首页 | 论坛 | 博客
  • 博客访问: 519734
  • 博文数量: 78
  • 博客积分: 995
  • 博客等级: 准尉
  • 技术积分: 1462
  • 用 户 组: 普通用户
  • 注册时间: 2011-11-15 20:22
个人简介

技术中沉思的时候最快乐,问题得到完美解决的时候最有成就感!

文章分类

全部博文(78)

文章存档

2013年(39)

2012年(37)

2011年(2)

分类: C/C++

2012-07-14 14:19:23

内存中的数据结构中最基础的部件就是指针了,指针是一切内存数据结构的灵魂;但内存指针只能
用于内存,导致内存数据结构一旦脱离其所依赖的进程内存,就立马失去了意义;这导致了几个问题:

1,不能在内存和其他介质(比如磁盘,socket等等)之间交换数据,所以才有了持久化+反持久化,
以及编码+解码等一系列衍生问题;这些问题应该说自从内存诞生那天就产生了,但直到今天也未能有完美
通用的解决方案;有的是通用性问题,有的产生了严重的性能问题;(我自己的一个项目就因为采用
thrift做编解码而遇到了非常严重的性能问题,所以才有了整个小型项目的诞生)

2,共享内存使用严重制约,共享内存可以说等价于内存+磁盘的一个玩意,具有内存的快速访问,
同时具有磁盘等介质拥有的交换共享数据的作用;共享内存不能使用指针,指针对于共享内存没有任何意义,
就像在磁盘上用指针一样!
共享内存有各种问题,一般来说不应该成为解决问题的首选甚至中间方案,只有最终迫不得已才能
采用的方案,除了共享内存本身的各种问题外,数据结构的极度缺乏也是一个极大的问题;至少我用c和c++
6年来,还没看到过非常成熟的工业级的共享内存的数据结构(应该是通用性的问题);所以我们用共享内存
都一直是自造各种内存数据结构;

以前在项目中使用共享内存来维护一些超出进程生存范围的数据;为了使用共享内存,基础部件是
偏移,即地址相对于共享内存起始地址的偏移值;在此基础上写了一些比较简单的数据结构,比如:单双向
链表,hashlist,数组,字符串等等;
写这样一个玩意起始不复杂,数据结构稍微有点基础的开发都可以很快的弄出来;可能也可以很快
使用;我当初就写过这样一些玩意,而且也在正式线上环境中使用了,甚至有些在线上服务了相当长的时间;
看起来共享内存的数据结构貌似挺简单,但后来偶发的一些问题让我重新认识到了这种数据结构的
严重问题:严重缺乏容错性,缺乏错误发现机制,以及相关的错误纠正机制;

比如:某进程正在改共享内存上的一个包含几个子变量的数据结构,这几个子变量中包含了一些“
指针”(实为偏移值),而这一个过程可能包含好几个连续的操作,只有在操作都完成后,这一操作才有意义
否则就是中间未定义的状态;
可能 99.99% 的情况下都可以正常完成,但如果碰到另外的 0.01% 的情况下就会完蛋,这 0.01% 
的情况包括:你的代码有bug,core了;或者进程被强行kill掉了;甚至机器直接掉电了;或者是共享内存
所依赖的磁盘down了(比如我们过去为了更“保险”而把共享内存用mmap和磁盘挂钩);很明显,这 0.01%
的情况一般不会出现,但一旦出现就是致命的,且出现的时间完全不可控的;
一旦出现这种问题,共享内存将立即处于一个未定义的状态,然而因为共享内存的持久性,导致问题
不会像内存出问题一样进程重启就会解决,而会一直保留下去,导致程序会出现真正的内存bug,比如:无限
循环(如果共享内存上保存了链表,那么很有可能出现无限循环),或者core 掉...反正就是无尽的bug;这
时候唯一能做的就是人工干预:手动清除共享内存得到一个干净的世界,然后重启进程;而这种人工干预是
非常令人难受的,而且经常令人措手不及;

近来项目中因一些问题而使我重新思考这一方案到底问题出在哪里;想来想去,得到的一些想法:
我们过去的方案缺乏对异常的考虑,自然缺乏对异常的处理;而一般系统中最麻烦也最复杂的
地方就是对异常的处理;我们既不能对异常视而不见,觉得异常一般情况不会出现而坐视不理;但我们也不
能将异常情况处理的过于复杂;两个极端都是不可取的;最佳的方案应该是:对所有的异常都加以考虑,然
后尝试用最简单最透明最傻瓜的方法解决掉;

回到这个方案上,需要考虑的几个问题:
1,一旦出现问题的时候,能否尽早的检测到这些异常或者未定义状态;
2,检测到了,该怎么解决?或者说怎么清除这些未定义状态?


下面的想法都是基于这两条来考虑的;
对于第一条:可能比较简单的方法是:引入事务;我们需要保证的是:程序需要修改共享内存之后,
要么成功,要么失败,而不是中间那种未定义的状态;这个不就是事务么?简单还有效的方法:首先保证不能
直接修改共享内存,提供一组简练独立的api,然后在api中引入日志,在操作开始前和结束后,都记录日志;
只有前后日志都完整的操作才能说操作成功,才能说状态处于已知可控状态;
记日志一般也不会影响到程序性能,磁盘对于这种顺序写入的支持性能还是挺强劲绝对可接受的;

对于第二条:可能简单的方案比较暴力:全部清除,干干净净;除了整个方法,还有其他方法么?
可能对于一些零散的且数据特别分散的数据块是很难恢复的;
想想操作系统,操作系统为各个进程提供实际的内存分配,进程退出的时候,状态可能n种,但不管
怎样,操作系统能完整回收分配给进程的所有的内存,而不会有任何泄漏;为什么?因为各进程所分配的内存
是完全处于操作系统管理的,且这种管理非常简单;操作系统提供给进程的内存都是页为单位,至于进程怎么
用这些内存页操作系统根本不管,但操作系统能很轻易的知道此进程得到了哪些内存页;当回收进程资源的时
候,自然也就非常简单了;
呃,如果各个独立的数据都比较大(假设超过512字节,比如,需要保存用户的一些阅读历史记
录),是不是可以把这些业务数据类比成操作系统的进程;对于每个独立的业务数据,程序提供共享内存分
配和回收接口,且这些分配和回收以比较大的块为单位;业务数据得到内存,怎么用这些大块的内存,程序
不需要管,就好比操作系统的进程一样;
好处来了,当检测到某业务数据处于某未定义状态的时候,进程只需要得到此业务数据占有的所有
的内存页面,简单回收就可以了;

呃,还有阁问题?如果进程操作目标业务数据的时候,因为程序bug之类的原因操作了属于其他业务
数据的内存资源(表现为内存被写乱了)怎么办?
操作系统完全可以防止进程读写其他进程的内存;即:操作系统从底层保证了各进程的绝对独立性;
同理,因为各业务数据所拥有资源(内存页面)是完全独立的,那完全可以利用操作系统提供的一些手段来
隔离这些业务数据,就像隔离各进程一样;怎么隔离?哦,操作系统有页保护api,这样,首先保证所有的
业务数据的内存页面都是受保护,不可改写的;当进程有明确的操作业务数据目标后,单独对目标业务数据
所拥有的页面进行解保护即可;


于是:一个小项目诞生了:基于块和偏移的内存管理+数据架构 / chunk_offset_mem_struct_lib
这个小项目做什么的?
1,这个项目为进程提供了一种能够在各种介质上通用的数据结构,基于偏移的“指针”保证了数据
结构的通用(当然,这些通用是相对的--必须相同的机器架构)
2,且提供了简单但绝对有用的的资源管理+资源回收等等功能,且保证了一定的容错性,保证了整个
数据的坚固性,稳定性;


项目代码一瞥:拿最基础的“指针”一说:

点击(此处)折叠或打开

  1. struct ChkPtr
  2. {
  3.         //最大为2047为所指目标所在的块编号 
  4.         uint32_t _chk_no:11;

  5.         //按4对其,最大为16K,为所指目标在其块内之偏移
  6.         uint32_t _offset:12;

  7.         //按8对其,为所指目标的大小,最大为 4096
  8. uint32_t _size:9;

  9.         bool operator == (const ChkPtr& rh) const
  10.         {
  11.                 return _chk_no == rh._chk_no && _offset == rh._offset;
  12.         }
  13.         bool operator != (const ChkPtr& rh) const
  14.         {
  15.                 return !(*this == rh);
  16.         }
  17.         void add(size_t offset)
  18.         {
  19.                 _offset += SET_CHK_OFFSET(offset);
  20.         }
  21.         void sub(size_t offset)
  22.         {
  23.                 _offset -= SET_CHK_OFFSET(offset);
  24.         }
  25.         bool is_null() const
  26.         {
  27.                 return _chk_no == CHK_MAX_CHK_NUM;
  28.         }
  29.         void set_null()
  30.         {
  31.                 _chk_no = CHK_MAX_CHK_NUM;
  32.         }
  33. };
  34. ChkPtr CHK_NULL = {CHK_MAX_CHK_NUM, 0, 0};

注释下:
1,因项目中的业务数据确实比较大,为节省内存,所以“指针”采用了4字节来做存储;这样就有了一些限制
2,限制:
指针+总内存限制:

指标

决定于

限制

可类比(32sys

块总数量

uint32_t _chk_no:11

<= 2047

 

块大小 

uint32_t _offset:12

<= 16k

 

每业务最大内存

_chk_no _offset

<= 2047*16k < 32M

每进程最大内存4G

指针范围/能力

_chk_no _offset

<32M

可指最大4G地址

被指目标大小

uint32_t _size:9及对齐

<= 4K

理论为4G??

被指目标大小对齐

 

8

4

被指目标地址对齐

 

4

4

块起始地址对齐

 

4096

操作系统的内存页一样

导致的一些数据结构限制:

指标

限制

字符串长度

<=4K

Hash 桶数目

<1024


有上述一些限制,非常不舒服,但考虑到这是一条业务数据的限制,一般来说够用;如果一定要扩展
会非常麻烦,底层的设计改动会导致上层的改动会有点大;

3,空指针(相当于c中的 NULL)为:
CHK_NULL = {CHK_MAX_CHK_NUM, 0, 0}; //即 chk_no=2047 为空指针


项目情况:
项目 github 地址:
项目进展:内存分配回收管理,指针设计都完成;另外加上了压缩
项目TODO:各类数据结构的编写,以及单元测试编写;
阅读(2334) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~