关于系统缓存的问题-物理内存消耗远远多于实际占用物理内存

【 某某提到: 】
: 一台服务器装有windows server 2008 r2,安装16G内存并设置16G虚拟内存。最近在运行一个用C#编写的大规模计算程序时发现,有很大一部分物理内存被莫名其妙地消耗了。资源监视器显示该程序占用物理内存不到5G,但是总的物理内存消耗接近10G,可用物理内存仅剩6G。随着运?
: 除了这个程序之外没有其它程序大量占用内存。这个程序有大量磁盘IO操作,在运行中会不时地调用GC.Collect()以及时清理不用的内存。这个实验中用到的一系列程序的结构基本相同,都会不时调用GC清理,但其它程序的内存使用都正常,只有这个程序会出现占用内存是实际使用的
: 请问为什么会出现这样莫名其妙多占用内存的情况呢?谢谢大家


这个既不是应用本身的bug,也不是系统的memory leak。

当前资源监视器中关于系统物理内存,有这么几个统计项,可用、缓存、总数、已安装;其中"缓存"这项,代表着已用于文件系统、网络等等子系统的数据缓冲存储的内存容量,其中包含数量巨大的驻留在物理内存中的数据页面。而这样的物理内存消耗并没有归入任何一个进程列表显示的进程所占用的物理内存。这就是为什么下面公式,

进程列表显示的所有进程所占用的物理内存之和 + 可用物理内存 < 物理内存总数

,成立的原因所在。

导致这一现象的原因,从这个大规模计算程序的行为描述看,基本可以断定是由于以下两点,
1)应用本身的大规模数据驻留物理内存,导致parser.exe进程庞大的working set;
2)大量频繁的IO操作,引起大量的物理内存为系统缓存所占用;

对于1),必须注意,GC.Collect()只是设置使能垃圾收集的标志位,并没有立即启动垃圾收集过程,这个过程的实际启动时刻由CLR来动态决议;

所以如果要获得即时的托管内存的释放,并进一步释放物理内存以减小当前进程的working set,可以使用AppDomain这个.net下可以用来资源划分、获取和释放的,在概念上近似于轻量级进程的编程语义;在AppDomain中获取的各种资源,包括托管内存、加载其中的各个assembly以及CCW等,在此AppDomain被释放时都被相应的及时释放(或者引用计数递减)。

对于2),重新观察先前的设计实现和模型,考虑是否能把一些分散的IO操作合并起来进行,比如,
for(long i=0; i < Count; ++i)
{
  ...
  objIO.Operation(Data[i], 1);
  ...
}
修改为
for(long i=0; i < Count; ++i)
{
  ...
  ...
}
objIO.Operation(Data, Count);
这样对于提高应用的IO效率以及提升系统缓存利用率应当会有帮助。


对于2),系统缓存随着这个大规模计算应用的进行而逐步增大,并最后导致整个系统无法获取的物理内存而无法继续运行的现象,估计即使采用了在上文提出的,在应用程序代码中尽可能合并IO操作,减少IO次数的方法,也不会改善系统缓存占用物理内存数量过大的问题。这个问题本质上是Windows操作系统本身从NT时代到现在,一直存在的问题,主要是围绕着Windows kernel中的Cache mananger以及memory manager核心态组件的实现机制而产生的。

根据目前的Cc(对Cache manager的简称,在WindowsResourceKernel开源项目中,Cache manager相关模块的函数都以Cc作为前缀,比如CcCopyRead,CcFlushCache等,Memory manager也同样简称Mm)的实现机制,所有对文件系统的访问,包括本地和网络,都会首先由Cc对相关页面作缓存映射,随着频繁的IO的操作,被Cc缓存的页面也迅速递增,而被缓存页面占用多少物理内存,这是由Windows kernel中的Memory manager决定。目前在64位平台上,系统缓存最高可达1TB,所以这个应用进程的运行中出现分配8G的缓存是完全可能的,但同时问题也随之而来,那就是系统缓存占用了过多的物理内存,导致其他进程以及内核本身无法申请足够的物理内存,最后致使系统“僵死”;

对于这个问题,微软提供了“Microsoft Windows Dynamic Cache Service”工具来提供对系统缓存的工作集working set容量(也就是驻留物理内存的大小)的控制,这个工具主要是对SetSystemFileCacheSize的封装,可以设置系统缓存容量的上下限。

但这只是一种临时的解决方案,因为应用虽然可以通过上面这个Dynamic Cache Service来设置和限制系统缓存容量的大小,但是如何确定缓存容量大小的非常困难,如果过小,所有IO性能大受影响,整个Cc如同虚设;如果过大(等价于不受限),那么系统缓存占用过多物理内存导致系统僵死的现象就会重现。

所以从根本上看,这个问题应由包括Cc和Mm在内的整个Windows kernel作出完整一致的调整,但从目前的实现看要完成整个方案改动很大,据称这个改进可能会考虑包含在Win7中发布。

Microsoft Windows Dynamic Cache Service下载,
http://www.microsoft.com/downloads/en/details.aspx?FamilyID=e24ade0a-5efe-43c8-b9c3-5d0ecb2f39af&displaylang=en

Microsoft Windows Dynamic Cache Service相关的介绍,
http://blogs.msdn.com/b/ntdebugging/archive/2009/02/06/microsoft-windows-dynamic-cache-service.aspx

posted @ 2010-12-11 11:19 flagman 阅读(6152) | 评论 (1)编辑 收藏

一个抽号码问题

前些天在论坛上看到一个看似简单,其实挺有意思的问题:

【20个连续的号码中抽出6个来,要求6个号码不能相连,有多少种抽法?】

这问题的本意应该是两两不相连的情况。

首先定义一个函数,F(m,p), m是确定抽出几个号码,p是总共有几个号码,那么
F(m,p)的值域就是代表在p个连续号码中,抽出两两不相连的m个号码,总共有几种组合;

接着确定状态转移方程,经过观察,p必须满足条件p >= m*2-1,否则F就为0,同时
F(6,20) = F(5,18) + F(5,17) + F(5,16) + ... + F(5,9);

因此可以得出如下状态转移方程,
当 p > m*2-1,F(m,p) = Sigma(F(m-1,q)) + 1;其中q 从(m-1)*2 到 p-2;
当 p == m*2-1,F(m,p) = 1;
当 p < m*2-1,F(m,p) = 0;

虽然分析到此,已可以着手具体实现,但是还是有些问题值得进一步分析,比如F(m,p)和F(m,p-1)之间存在何种关系,若使用递归,就当前这个问题效率估计会是问题;

因此对此方程进一步分析,
F(5,18) = Sigma(F(4,q))+ F(4,7);q从8到16
F(5,17) = Sigma(F(4,q))+ F(4,7);q从8到8;
...
可进一步推出,
当 p > m*2-1, F(m,p) = F(m,p-1) + F(m-1,p-2);

这样我们就得到了可以进行递推实现的转态转移方程;
另外,对于m == 1的情形,显然F(1,p) = p ;


#include<stdio.h>
#include<conio.h>

#define MAXLEN 10000

static int F[MAXLEN];
static int R[MAXLEN];

int Compute(
    const int cM,
    const int cP)
{
  if (cM <= 0 || cP < (cM*2-1))
    return 0;
  if (cM == 1)
    return cP;
  if (cP == cM*2-1)
    return 1;

  for(int i = 0; i < MAXLEN; ++i) R[i] = i;

  for(int m = 2; m <= cM; ++m)
  {
    int floof = 2*m;
    int ceiling = cP-2*(cM-m);
    F[2*m-1] = 1;
    for(int p = floof; p <= ceiling; ++p)
        F[p] = F[p-1] + R[p-2];
    for(int j = floof; j <= ceiling; ++j)
        R[j] = F[j];
  }
  return F[cP];
}

main()
{
  Compute(6,20);
//  Compute(6,19);
//  Compute(5,18);
//  Compute(5,17);
//  Compute(4,16);
//  Compute(6,13);
//  Compute(6,12);

//  Compute(5,11);
//  Compute(5,10);
//  Compute(4,9);
//  Compute(4,8);
//  Compute(3,7);
  return 0;
}

接着再对目前的整个实现做下复杂度分析,主要处理部分基本上由两个循环构成,对于R数组的初始化可作为常数项不计,那么

大O( F(m,p) ) = O( m*(ceiling-floor) )
              = O( m*(p-2*m) )
              近似于O( m*p ),
若m << p,显然O(F(m,p)) = p;
若m 近似 p, 但事实上必须p >= 2*m - 1,否则F值就接近0或1,因此O(F(m,p)) 近似于const;
所以综合来看上面的这个实现在时间上是个线性复杂度的实现;在空间上,使用了两个长度至少为p的数组,个人认为可以对此进行进一步优化。

对于F(6,20) = 5005

整个实现在TC++ 3.0上验证通过。


posted @ 2010-12-03 10:53 flagman 阅读(1303) | 评论 (1)编辑 收藏

思考系统API设计的问题

最近正好在思考系统API设计中考量的一些问题,

【某网友讨论到】
: 那地址是不是同一个地址呢。我现在的理解是这样的,假设有巨大的真实内存。windows首先将高2G的内存自己占了,用作各种内核对象。这2G内存共享给每个进程,但进程不能直接访问,只能通过windows给定的函数访问。
: 然后每个进程都给他2G内存,进程如果创建自己的对象就放到自己那2G内存里面,如果要建立内核对象就放到共享的那高2G里面去。
: 所以不同进程如果可以访问高2G内存的话,任何进程访问到同一个高地址实际上都是访问到同一个对象。但如果访问低2G地址的话,不同进程是对应不同的对象的。



在不同的进程中,询问同一个内核对象的实际地址(无论是线性地址还是物理地址),是无意义的:

首先,内核对象只能由在内核态下的例程才能直接访问,在我们日常的代码中,所调用的Windows API,比如CreateFile, (注意调用刚开始时是处于用户态下的),一般都会在ntdll.dll中找到对应的内核函数或例程,接着系统切换到内核态,开始调用实际对应的内核函数(KiCreateFile),这个时候才会去访问内核对象的实际地址,然后建立一个该内核对象对应当前进程的Handle,并把它返回给caller,同时切换回用户态;因此,对于用户态程序来说,只要且只能知道该内核对象在当前进程中的对应的Handle就可以对其进行操作了;

其次,这样的设计是出于对OS核心数据结构(当然包括我们正在讨论的内核对象)的保护;如果用户态程序可以轻易的获取内核数据结构的实际地址,那么对于整个OS的安全和稳定显然构成很大的问题;一个用户态的误操作可以轻易的引起整个OS的崩溃,而有了这一层的保护,崩溃的只是当前进程而不是整个系统;

接着上面这点,也可以看出,内核对象的如此设计达到了接纳OS本身的平滑演进的目的。从Windows 3.0到95/98,从NT到Win2k/XP,再到眼下的Vista/Win7,Windows操作系统本身发生了巨大的变化和进步,采纳了无数的新技术新方法,但是它基本的系统应用编程接口,也就是我们所熟知的windows API,却并没有发生太大的改变,很多Win 3.0 这个16位OS时代的程序代码只要当初设计规范编码规范,稍许修改就可以在最新版的OS上运行如飞;是什么做到了这些?也就是所谓的极为重要的向后兼容性,我个人认为,把操作系统的重要/主要功能抽象成内核对象,并通过一套极为solid的API暴露出来,达成了这个目标。

这是一种更高层次上的面向对象,把实现的细节,把系统的复杂,简单而优雅的封装了起来。你只要调用CreateFile去建个文件或管道或邮槽,不用担心当前OS是Windows 3.0还是Win7,获得的Handle,你也不用去关心它以及它所指向的内核对象是Windows 3.0的实现还是Win7的实现。

Windows上所有的精彩几乎都是基于这套通过内核对象概念抽象并暴露的API基础之上,COM/OLE,这个二十年前震撼性的ABI和IPC范畴的技术规范,其中很多的设计思路也是植根于内核对象的设计理念,如COM对象的引用计数和内核对象引用计数,IUnknown和Windows Handle(前者是指向某个二进制兼容的组件对象,后者引用或间接指向某个内核对象,都是对于某个复杂概念的一致性抽象表述),等等;

十年前的.net,本来是作为COM的升级版本推出,把COM/OLE的实现复杂性封装在了虚拟机平台CLR里面,而从这个虚拟机的开源实现SSCLI,我们可以看到大量的COM机制在.net的具体实现里面起了举足轻重的作用。在这些VM中大量symbol有着COR的前缀或者后缀,COR指代什么?Common Object Runtime, 原来CLR/SSCLI的设计思路也是把OS通过虚拟机VM的形式,并通过common object向应用程序暴露功能。

小结一下,
OS内核对象API,三十年前系统级别的对象抽象;
COM/OLE,二十年前二进制组件级别的对象抽象;
.net/CLR, 十年前虚拟机平台级别的对象抽象;

写到这里倒是引起了我其他的一些思考,软件工业界一直以来对面向对象OO是热火朝天,特别是语言层面,从C++/Java/C#到Python/JScript,不一而足;

但是我们有没有从根本性的设计理念上对面向对象,察纳雅言了呢?

如果现在设计Windows这套API的任务放在大家面前,会采用内核对象/Handle方案还是直接指向OS内部数据结构的方式来暴露功能?

从三十年前的这套API的设计中,我们真的可以学到很多。


 

posted @ 2010-12-01 21:28 flagman 阅读(10139) | 评论 (0)编辑 收藏

仅列出标题
共2页: 1 2 
<2024年4月>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

导航

统计

常用链接

留言簿(1)

随笔分类

随笔档案

搜索

最新评论

阅读排行榜

评论排行榜