注:下文中表述的不可能完成的“自动增长”功能已经实现了,LRU缓存。
Active共享内存公共基础系统
计费产品部 张博
1 共享内存概述
1.1 什么是共享内存
共享内存是UNIX系统进程间共享信息的最灵活、最高效的方式。
一般实现为同一块物理内存通过IPC函数映射到不同的进程的地址空间,每个进程对共享内存的修改都直接反映到其它进程。共享内存可以在进程终止后持续存在,只要没有用IPC函数删除或关闭主机,共享内存将一直存在。由于多个进程可以同时修改共享内存,因此还需要使用适当的机制进行同步(例如信号量)。
1.2 使用共享内存的好处
在大型业务系统中,对于时间和空间的要求可能同时趋于极限,既要求尽可能高的处理速度,又要求不超出给定的内存需求。实际情况可能是时间和空间都逼近于主机极限。
能够在时间和空间上同时提高性能的手段就是使用共享内存。共享内存是普通内存,访问机制和私有内存相同,因此具有相同的访问效率。共享内存是对进程间共享的,相同的数据只需要在共享内存中存储一份,不需要每个进程一份拷贝,节省了内存空间。共享内存超越进程存在,因此可以一次构造数据然后不断访问,不需要每次都构造,也可以把构造数据的程序和读取数据的程序分开,对于实时系统而言,这是相当关键的特性。
1.3 使用共享内存的难点
使用共享内存的代价是编程复杂度的增加。使用共享内存所需的技术绝不仅仅是几个IPC函数那么简单。
怎样保证多进程修改不冲突?使用一把锁还是多把锁?使用读优先还是写优先?
怎样保证代码版本正确?使用错误格式的程序可能会毁掉数据。
怎样保证系统可靠?任何一个程序的地址访问错误都可能错误涂改共享内存数据。
由于共享内存连接入进程的地址是不确定的,因此一个普通指针存储在共享内存是无意义的,任何使用指针的数据类型都不能存储在共享内存,基本上说,只有基本数据类型int、float、char等和它们的数组可以放入共享内存,通常使用的C++类型string、vector、set、map等等都是无法使用的。
难道需要退回到C时代吗?
本系统的实践表明,使用一些高级编程技巧可以在共享内存中实现标准的STL容器从而极大地减低客户代码的编程复杂度,而经过仔细规划的安全机制可以显著地提高系统可靠性。本系统所用的部分技巧是独创的。
2 Active共享内存系统概述
2.1 历史
从最早的计费帐务系统到LIBS再到MBOSS,共享内存从最初的局部辅助使用到成为整个系统最重要的基础部分,经历了漫长的发展过程,如今已经形成了一套比较完善成熟的共享内存管理和操作的公共基础系统。
最初的计费帐务系统较少使用共享内存,仅对账目采用共享内存存储,因此使用直接的方式操作共享内存,代码没有模块化,完全没有考虑通用性。
LIBS系统需要将基础资料和账目、积量等系统主要数据全部存入共享内存,因此必须采用模块化的公共代码,因此设计了一套完整的共享内存系统,由一组可移植的代码——包括几套模版和辅助功能类——以及少量数据库表构成,可以在代码无修改的情况下简单地加入其它项目代码中。使用功能所需的配置简单,代码使用方式基本与STL的array和set相同,非常大地简化了相关功能的实现。
MBOSS系统增加了实时性的要求,不仅系统主要数据存储在共享内存,程序运行调度、监控也采用了共享内存。共享内存系统增加了完善的界面管理,并支持可关闭的互斥功能,可以在代码完全相同的情况下采用或不采用多进程互斥(互斥访问极耗系统资源,在已经确知无需互斥的情况下可以关闭互斥)。
现在的共享内存系统实现了配置需求极小化,除了共享内存名称和编号之外仅需要配置最大记录数即可。
2.2 功能
实现了包装的共享内存管理和访问接口,实现了一般用到的共享内存的创建、备份(保存)、基于共享内存的字符串类和set集合,
创建共享内存,共享内存ID存储在数据库中
共享内存块的保存和读取,只保存和读取有效数据
共享内存连接检查,通过检查共享内存的创建时间确认共享内存是否正确
适用于共享内存的字符串类sstring
访问共享内存的数组模版,模拟了STL的array
访问共享内存的二叉树模版,模拟了STL的set,并支持部分关键字的lower_bound,其中对iterator的实现是具有独创性的
访问共享内存的字符串池类,可以通过共享字符串减少字符串占用的空间
2.3 系统环境要求
本系统已经在Sun、HP、IBM的UNIX小型机上使用,采用主机自带的C++编译器。需要使用STL。数据库使用很少,仅有几个函数访问数据库,可以根据需要修改,比如修改为使用文件或使用其它数据库接口。可以移植到任何UNIX系统。
2.4 包含的内容
一组C++源代码,全部为头文件:
Shm_array.h 数组模板
shm_set.h set模板
shm_stringpool.h 字符串池模板
zbshm.h 包装基本共享内存功能
zbmutex.h 包装基本信号量功能
3张数据库表
Billing_shm_config 配置共享内存大小,配置表
Billing_shm 共享内存ID和CTIME,动态,程序控制
Billing_sem 信号量ID和CTIME,动态,程序控制
一个名字空间:
ns_shm_data
2.5 设计者
本系统主要由计费产品部张博设计和编写,其中用于T_SHMSET的有序二叉树算法由计费产品部王列编写。
3 Active共享内存系统设计说明
3.1 关键技巧
本系统采用IPC_PRIVATE方式创建共享内存,共享内存ID和shm_ctime存储在数据库中,用ID连接并检查shm_ctime是否一致。该方式避免了KEY冲突或依赖路径生成KEY的复杂方法。由于大型系统总是存在数据库或文件的配置存储,因此保存共享ID就不是额外的负担。对于用于互斥的信号量也采用同样的方式处理。
本系统借助全局宏GET_PP获得一个全局指针地址来辅助实现iterator的++和->操作,该iterator可以存储在共享内存中。GET_PP的参数为共享内存编号,由模板实现给定,因此客户系统必须维护一个全局的共享内存编号清单以避免编号冲突。该技巧以及小的代价在共享内存中实现了iterator。
本系统实现了统一的共享内存接口和共享内存组接口,共享内存组接口自动调用每个共享内存的接口,方便共享内存的分组管理。
统一实现了文件保存,对共享内存组自动增加子目录。
存储在共享内存的信息不能是指针,也不能是STL::string这样的含有指针的对象,只能是简单类型。本系统包含一些辅助类简化数据存储。Ssting类内置了固定的长度检查。
3.2 体系结构
物理共享内存: 元数据 数据头 记录 记录 。。。 |
数据库: SHM_ID CTIME |
管理者接口: IShmActiveObject共享内存管理接口 CShmActiveObjects共享内存组管理接口 功能: 创建、删除、连接、断开、保存、加载 |
使用者接口(全部实现了IShmActiveObject): T_ARRAY<typename T,int PI_N,typename T_USER_HEAD >数组模板,类似STL::array T_SHMSET<typename T_DATA,int PI_N,typename T_USER_HEAD,int VER>set模板,类似STL::set StringPool<typename T,int PI_N,typename T_USER_HEAD>字符串池模板 |
3.3 规划和配置
全局统一规划的共享内存名称和编号,需要在源代码中硬编码,将作为模板实现的参数
Billing_shm_config表配置的共享内存最大记录数。R_SIZE为最大记录数,对于字符串池是字节数,对于T_ARRAY和T_SHMSET都是记录大小。实际创建的共享内存的大小为元数据大小+数据头大小+记录大小*R_SIZE。
3.4 可靠性设计和指导
本系统特别注重了可靠性设计。
本系统唯一的配置点是共享内存记录数,该记录数仅在创建共享内存时使用,而共享内存的实际大小存储在共享内存中,访问者完全根据共享内存自身的描述信息来访问共享内存。
共享内存自身存在一个称为元数据的区域,包含了数据格式的特征信息,如基本数据的长度(long是8字节还是4字节)、记录长度、主机字节序(不同主机可能不同)、共享内存格式版本、记录结构版本,还包含一个GUID,用来确保共享内存是本系统创建的。访问者连接共享内存时检查以上所有信息确认数据结构一致。
记录结构只能使用基本数据类型,不能使用指针。作为替代,使用本系统提供的sstring代替STL::string,使用T_SHMSET::iterator代替指针。
T_SHMSET使用单一锁,该锁为写优先的读写锁,大量写时读进程可能一直等待。使用锁也会显著降低系统效率。
本系统的锁支持禁用。如有可能,尽可能将系统设计为无需用锁,在应用的层面上设法保证修改数据时不会有读进程。
尽可能使用只读连接。使用只读连接时如果程序错误误改写共享内存时会导致进程出错,从而避免共享内存数据损坏。
3.5 节省空间指导
本系统尽可能地节省结构本身占用的空间,T_SHMSET每个记录所需的额外空间是3个long,如有可能,尽可能使用T_ARRAY而不是T_SHMSET。
本系统的iterator可以存储在共享内存,因此可以构建多极指向的系统。尽可能避免存储重复信息。对于存在大量重复的数据尽量存储在独立的共享内存块中,通过iterator指向。不定长的字符串数据尽量使用字符串池。
实际使用中发现,数百万数据、每个数据有10个属性,经过统计这10个属性的组合只有几千个,将属性的组合独立存储后空间省去了90%。字符串池节省空间的比例也可能高达99%。
这些措施对于编程复杂度的增加是很少的。
4 Active共享内存系统使用方法
4.1 加入系统
将本系统的源代码文件加入项目中,因为没有cpp文件,所以不需要修改makefile。
在需要使用功能的源代码中包含合适的头文件。本系统的头文件自身都包含了本身需要的其它头文件。使用数组模板只需要包含shm_array.h,不需要包含shm_set.h,其它也是类似。
4.2 定义
ns_shm_data::T_SHMSET<ns_shm_data::CDemoData ,MAX_PP-1 > datas("tmp",0);
ns_shm_data::T_SHMSET<ns_shm_data::CDemoData ,MAX_PP-1 >::const_iterator it;
以上代码定义了一个共享内存和iterator
数据类型为ns_shm_data::CDemoData,名称为“tmp”,版本为0。版本有助于防止连接到错误的数据版本上。
4.3 创建
if(!datas.CreateShm())return __LINE__;
4.4 连接和断开
if(!datas.Attach(false))return __LINE__;
。。。。。。访问。。。。。。。。
if(!datas.Detach())return __LINE__;
4.5 访问
T_ARRAY是个数组,T_SHMSET基本兼容STL::set
具体见接口详解和示例代码
5 Active共享内存系统接口详解
5.1 用户数据结构
//T_DATA范例
struct CDemoData
{
long n;
bool operator < (CDemoData const & tmp)const{return n<tmp.n;}
string & toString(string & str)const
{
char buf[2048];
sprintf(buf,"%ld",n);
return str=buf;
}
};
如果需要排序,必须定义“operator <”
如果需要调用Report(),必须定义“toString()”
如果需要使用lower_bound()和upper_bound(),还需要定义部分比较函数
用户数据结构不能包括含有指针的数据类型,可以包括简单数据类型和专门提供的sstring和iterator(不是STL的,是本系统定义的)。
5.2 字符串模板sstring
template <long BUFSIZE >
class sstring
模板参数为存储空间长度,能存储的字符串的最大长度为存储空间长度减一
支持和STL::string、char *的所有构造、赋值和比较。
支持c_str()和size()。
5.3 管理接口IShmActiveObject
实现情况A为T_ARRAY,S为T_SHM_SET,P为StringPool
T_ARRAY为另外两个的基类,可以看到大多数功能都继承自T_ARRAY,只有T_SHM_SET支持互斥,数据库存储则都没有实现(只能由业务代码实现)
接口 | 功能 | 实现情况 |
virtual char const * GetName()const | 获得共享内存的名称,该名称必须全局唯一 | A |
virtual bool disableMutex()const | 如果可能,禁用互斥 | S |
virtual bool CreateShm() | 创建共享内存(不连接) | A |
virtual bool Attach(bool isReadOnly) | 连接到共享内存,可选是否只读,推荐尽可能只读访问 | A |
virtual bool LoadFromDB() | 从数据库加载,模板未实现,需要在业务级定义 |
|
virtual bool SaveToDB() | 保存到数据库,模板未实现,需要在业务级定义 |
|
virtual bool LoadFromDir(char const * dir_name) | 从目录加载,参数为目录名,目前均由T_ARRAY模板实现 | A |
virtual bool SaveToDir(char const * dir_name)const | 保存到目录,参数为目录名,目前均由T_ARRAY模板实现 | A |
virtual bool Report()const | 打印共享内存报告,目前的实现包括了共享内存元数据、数据头和若干条数据 | ASP |
接口中并未包含删除共享内存。为了减少频繁创建共享内存,T_ARRAY设计为仅当创建时发现现有共享内存不够大的情况下删除旧的创建新的。由于共享内存的特殊性,共享内存自动增长功能无法在这一级接口实现,只能在更高级访问接口中实现。
5.4 组接口CShmActiveObjects
用于管理一组IShmActiveObject对象或CShmActiveObjects对象,自动对每个对象或对象组调用IShmActiveObject功能,可以简化编写多个共享内存的管理代码
增加的接口:
virtual bool AddTable(IShmActiveObject * p)
增加一个对象
virtual bool AddTable(CShmActiveObjects * p)
增加一个对象组
virtual bool ShowRet(string & ret,long level=0)const
显示操作结果,按照级别关系树形显示操作结果成功还是失败
5.5 数组接口T_ARRAY
5.5.1 定义
//T 元素类型
//PI_N 预定义的整数,用于支持直接指针操作,若此参数为PI_NULL则不可以使用直接指针功能(此类及子类型的句柄或迭代器的*和->)
//T_USER_HEAD 用户定义的数组头类型
template<typename T,int PI_N,typename T_USER_HEAD >
class T_ARRAY : public IShmActiveObject
T是作为数组内容的数据结构,T_USER_HEAD为用户附加存储在数组头部的信息
5.5.2 构造
T_ARRAY(char const * _name,int _version)
_name为统一规划的共享内存名称,_version为版本,有助于防止连接到错误的数据
5.5.3 杂项功能
T_USER_HEAD * GetUserHead()获得用户附加的数组头
T const * GetData()const返回数据指针
long Size()const返回数据个数
long Capacity()const返回最大容量
5.5.4 句柄迭代
T_ARRAY::HANDLE提供类似iterator的功能,由于较少用,因为尚未改为iterator的形式
HANDLE Begin()const
HANDLE End()const
5.5.5 访问
bool Add(T const & data,HANDLE & h)
bool Add(T const * pData,T_SHM_SIZE count,HANDLE & h)
添加一个数据或一组数据,h返回最后添加的元素的位置
void Sort()
排序,使用operator <
HANDLE & LowerBound(T const & data,HANDLE & h)const
HANDLE & LowerBound(T const & data,HANDLE & h,bool (* less)(T const &,T const &))const
搜索,使用operator <
T * Get(HANDLE const & h)const
获得数据
5.5.6 管理
见IShmActiveObject
5.6 字符串池接口StringPool
5.6.1 定义
//字符串池,模板参数T用于区分不同用途的池
template<typename T,int PI_N,typename T_USER_HEAD>
class StringPool : public T_ARRAY<char,PI_N,T_USER_HEAD >
5.6.2 构造
StringPool(char const * name,int version=0)
Name为统一规划的共享内存名称,version用来防止访问错误的数据版本
5.6.3 操作
字符串缓存设计为最小存储,无重复的字符串一个接一个地存放在共享内存中,添加字符串时必须先重建缓存获得现有字符串的句柄,然后添加。
void RebuildCache()重建缓存,为了快速搜索已经存在的字符串,用于添加字符串之前
void ClearCache()清除缓存,用于释放空间
bool AddString(char const * str,HANDLE & h)添加一个字符串,h返回句柄,这个句柄可以存储在共享内存
char const * GetString(HANDLE const & h)const获得一个字符串
5.6.4 管理
见IShmActiveObject
5.7 Set接口T_SHMSET
5.7.1 定义
template<typename T_DATA,int PI_N,typename T_USER_HEAD=CDemoData,int VER=0>
class T_SHMSET
5.7.2 构造
T_SHMSET(char const * name,int version)
5.7.3 方法
类别 | 名称 | 功能 | 互斥 |
杂项 | T_USER_HEAD * GetUserHead() | 获得用户附加的头结构 |
|
| long size()const | 兼容STL::set |
|
| long capacity()const | 兼容STL::set |
|
Iterator | const_iterator begin()const | 兼容STL::set | Y |
| const_iterator end()const | 兼容STL::set |
|
| const_iterator rbegin()const | 兼容STL::set | Y |
| const_iterator rend()const | 兼容STL::set |
|
操作 | bool clear() | 兼容STL::set | Y |
| pair<iterator, bool> insert(T_DATA const & data) | 兼容STL::set | Y |
| const_iterator find(T_DATA const & tmp)const | 兼容STL::set | Y |
| bool erase(const_iterator it) | 兼容STL::set | Y |
| const_iterator lower_bound(T_DATA const & tmp,bool comp(T_DATA const &,T_DATA const &))const | 必须符合比较顺序,如operator<的比较顺序为ABCD,则comp可以是ABCD、ABC、AB或A | Y |
| const_iterator upper_bound(T_DATA const & tmp,bool comp(T_DATA const &,T_DATA const &))const | Y |
5.7.4 管理
见IShmActiveObject
6 完整示例
这个事例完整演示了从定义到访问的全过程,包括:
定义共享内存对象、定义iterator、创建、连接、访问、断开
访问包括了添加、删除、查找、清空、lower_bound以及iterator的遍历和反向遍历
bool less_CDemoData(ns_shm_data::CDemoData const & a,ns_shm_data::CDemoData const & b){return a.n<b.n;}
int test(int argc,char ** argv)
{
ns_shm_data::T_SHMSET<ns_shm_data::CDemoData ,MAX_PP-1 > datas("tmp",0);
ns_shm_data::CDemoData tmp;
ns_shm_data::T_SHMSET<ns_shm_data::CDemoData ,MAX_PP-1 >::const_iterator it;
if(!datas.CreateShm())return __LINE__;
if(!datas.Attach(false))return __LINE__;
string str;
long maxcount=10;
long i;
for(i=0;i<maxcount;++i)
{
tmp.n=i;
datas.insert(tmp);
}
thelog<<datas.Report(str)<<endi;
for(i=0;i<=maxcount;++i)
{
tmp.n=i;
if(datas.find(tmp)==datas.end())thelog<<i<<" not found"<<endi;
else thelog<<tmp.toString(str)<<endi;
}
thelog<<"遍历:"<<endi;
for(it=datas.begin();it!=datas.end();++it)
{
thelog<<it.handle<<" : "<<(*it).toString(str)<<endi;
}
while(true)
{
thelog<<endl<<"1 删除2添加3清空4 lower_bound 5 upper_bound 6反向遍历"<<endi;
str=UIInput("请输入命令:b=break",-1);
if("b"==str)break;
switch(atol(str.c_str()))
{
case 1:
str=UIInput("请输入要删除的值:b=break",-1);
if("b"==str)break;
tmp.n=atol(str.c_str());
datas.erase(datas.find(tmp));
break;
case 2:
str=UIInput("请输入要添加的值:b=break",-1);
if("b"==str)break;
tmp.n=atol(str.c_str());
datas.insert(tmp);
break;
case 3:
datas.clear();
break;
case 4:
str=UIInput("请输入值:b=break",-1);
if("b"==str)break;
tmp.n=atol(str.c_str());
if(datas.end()!=(it=datas.lower_bound(tmp)))thelog<<it.handle<<" : "<<(*it).toString(str)<<endi;
else thelog<<"end()"<<endi;
break;
case 5:
str=UIInput("请输入值:b=break",-1);
if("b"==str)break;
tmp.n=atol(str.c_str());
if(datas.end()!=(it=datas.upper_bound(tmp,less_CDemoData)))thelog<<it.handle<<" : "<<(*it).toString(str)<<endi;
else thelog<<"end()"<<endi;
break;
case 6:
for(it=datas.rbegin();it!=datas.rend();--it)
{
thelog<<it.handle<<" : "<<(*it).toString(str)<<endi;
}
break;
default:
thelog<<"无效命令:"<<str<<"("<<atol(str.c_str())<<")"<<ende;
break;
}
thelog<<datas.Report(str)<<endi;
thelog<<"遍历:"<<endi;
for(it=datas.begin();it!=datas.end();++it)
{
thelog<<it.handle<<" : "<<(*it).toString(str)<<endi;
}
}
if(!datas.Detach())return __LINE__;
return 0;
}