转自: http://www.d2school.com/bhcpp_book/5_4.php
5.4. 进程与内存
前面讲“库”文件时说到:动态库的“函数”,有“函数地址”。其实不仅函数有地址,程序中的各种数据,都有地址。
5.4.1.什么叫进程
当程序安静地躺在硬盘上时,它是一个“文件”,非要给点区别的话,它是一个“可执行文件”;而当程序运行起来,它就从硬盘上一跃进入内存,这时它就被叫做一个“进程/process”了。
〖课堂作业〗:学习通过“任务管理器”观察进程
请同时按下Ctrl + Alt + Del 键,或者在任务栏空白处点右键,弹出菜单中选“任务管理器”,并切换到“进程”页,我们可以看到各个进程的一些指标。提示:通过菜单“查看->选择列”,可以配置更多观察选项,下图是一个示例:
图 5-6 通过“任务管理器”,观察进程
5.4.2.进程的内存空间
硬盘上的文件,是“死”的,内存的进程,才是“活”的,可以说,内存就是程序生存的天地,那么每个进程的拥有天地有多大呢?回答是:“4G”。
“4G”又是多大?通常程序员习惯用十六进制来思考内存大小问题,不过我们还没有学习十六进制呢,还是用10进制回答问题吧:4G的大小是4294967295。
或许有些读者想打开机箱:明明只插了1G,甚至只有512兆的内存啊?操作系统如何给每个进程都分配出4 G的内存空间呢?
在本章前部的《从代码到程序》一节,我们已经说过,程序运行在操作系统之上,要访问硬件,通过要假手操作系统。内存可是最重要的硬件了,元老级的操作系统确实允许我们直接操纵内存,但现代的操作系统都不允许了。这样,操作系统就有了玩猫腻的机会——当然,是在硬件支持的情况下。
首先,预备的并不是真实的内存空间大小,而是“内存地址”大小,更严格地说,是内存的“虚拟地址”(真实内存地址称为“物理地址”)。
如果我们把内存空间的最小单位想像成一个格子。那么内存地址就是格子的门牌号码。这里的4G,是指有4G个内存“虚拟地址”——再直白一点,就是每个进程可以获得4G个门牌号,而不是4G个真实的内存空间。
房子少,门牌却多,这些门牌最终要挂在哪里呢?答曰:一部分挂到真实的物理地址上去,一部分可能被挂到“虚拟内存(硬盘空间)”上,还有很大一部分,哪儿也不挂,整个儿就是操作系统给进程开的白条。
“虚拟内存”和“虚拟地址”没多大关系。后者是指操作系统分配给每个进程一致的,从“0”到“4294967295”的内存地址。前者则是指操作系统经常性地拿硬盘空间假装成内存空间欺骗进程。
〖课堂作业〗:观察本机的虚拟内存配置
右键点击“我的电脑”,弹出菜单选“属性”,出现“系统属性”对话框中,切换到“高级”页;找到“性能”分组框,点击其内“设置”按钮,出现“性能选项”对话框,同样切换到“高级”页。请在上面查找有关“虚拟内存”的配置。
有关4G内存空间的几个迷底我们一并揭开:
第一、“内存不够,硬盘来凑”。但是,请注意:访问真实内存的速度,是“纳秒(十亿分之一秒)/ns”级,访问硬盘的速度,却是“毫秒(千分之一秒)/ms”级。
〖重要〗: 程序设计原则:去除不必要读写文件操作
程序需要读写(物理)文件很正常,但如果一个程序需要非常频繁地读写文件,就需要考虑其设计思路是否有问题。
比如读取配置文件,一般被设计成在程序启动期间,读入内存,之后一直使用内存数据。如果在程序运行过程中,配置文件会被外部修改,则应争取设置成“通知”机制,即外部有修改后,程序能够收到一个通知,然后重读文件,而不应设计成每次需要配置,就从文件中读取(配置数据的使用频度往往很高)。
第二、 虽然承诺是4G,但实际上这是一个按需分配的过程,绝大多数进程,需要的最多不过数百兆。操作系统分给每个进程是4G的内存虚拟地址(门牌),绝不是直接就给出4G内存(那样的话,系统直接挂掉了)。
第三、 在4G里,还需要分出2G用来预留和其操作系统、和其它进程共享使用,这部分内存,当前进程默认状态下没有访问权限。
第四、最后一迷底很重要:谁负责内存“虚拟地址”与“物理地址”的变换呢?是硬件,主要是CPU。物理内存是兵家必争之地——至少未来十年之内还是——读写内存必须非常迅速,这件要求软件(包括操作系统)实现不了,只能交给硬件去做。而操作系统所做的,是屏蔽掉多数应用程序直接读写物理内存的权限。
上面的迷底一开,会不会让读者您很鄙视“4G空间”这一说了?且莫!
通过“虚拟地址”的内存访问模式,称为“保护模式”,对应的,允许程序直接访问物理内存,称为“实模式”。“实模式”存在不少问题。
比如,随便一个程序对物理内存胡作非为了,整个操作系统——甚至就是整台机器, 直接就宕掉了。
再如,没有系统实现的虚拟内存时,程序必须自己处理如何将暂时不用的数据,存到硬盘上,等需要时,又如何读出来,令程序员烦不胜烦;现在有了4G空间,程序员一口气写6个程序,脸不红心不跳的,真是方便多了!
5.4.3. 内存分配测试程序
下面我们写一个程序,以观察一个C++程序大致能向操作系统要到多少内存。这是一很小的,同时也很不准确的程序。仅粗略测试程序运行期,能够动态分配到多少内存,程序自身已经占用的内存并不计算。
打开Code::Blocks,新建一个控制台项目,项目名称为“MemoryAllocTest”。打开项目中默认存在的main.cpp文件,完成以下代码:
#include <iostream>
using namespace std;
int main()
{
007 unsigned int bytes = 0;
009 while(true)
{
011 try
{
013 new char [1024 * 4]; //每次分配4K内存
014 bytes += 1024 * 4;
}
016 catch(std::bad_alloc const & e)
{
018 std::cout << e.what() << std::endl;
019 break;
}
}
023 std::cout << bytes << "bytes" << std::endl;
return 0;
}
〖危险〗: 一个对内存无比贪婪的程序!
切莫着急编译,然后运行这个程序。因为它会尽全力去吞噬你的内存(物理的,或者虚拟的),直到操作系统忍无可忍地拒绝了它。在此过程中,你的电脑会变得反应迟钝,很多其它进程变得不能及时响应,甚至有些进程会出错。所以,请在执行本程序之前,先保存你修改中任何内容,并尽量关闭一些不需要的程序。
内存的最小单位,叫“字节/byte”。在007行,我们定义一个“正整数/unsigned int”的变量,我们就把它取名为bytes了。这个变量一会儿用来记录我们分配了多少个字节的内存。现在,它被初始化为0。
009行是我们熟悉的“死循环”,既然是死循环,通常就得有个出口来解救:在019行,我们看到一个break。
这个break什么时候起作用呢?这里我们要认识新朋友了:异常。请看从011行和016行,组成这样的一个语句结构
011 try
{
//代码块-1
}
016 catch(std::bad_alloc const & e)
{
//代码块-2
}
在try{} 的范围内的代码(代码块-1),如果发生C++定义的异常,程序将跳转catch() {}范围内的代码(代码块-2)继续执行。
异常是C++支持的一种程序流程结构化跳转,我们将在以后学习,本例中将要发生的事情是:
013 行的代码,将在循环内一次又一次分配内存,(并且后面从不释放),终有一次,这个贪婪的行为将会失败。C++规定,出现内存分配失败时,默认行为是抛出一个C++的异常数据,并且中止其后代码运行,然后跳转到某一种catch代码块中,在本例,就是018行和019行。
018行输出所捕获到异常的简单说明。019行则是重要的“break”,没错,程序内存被耗尽,循环进行不下去,此时不break,更待何时?
下面是该程序在我的电脑上运行的结果,费时超过6分钟,分配了近2 G字节的内存。
图 5-7 C++程序内存分配试验结果
读者可能对上述代码中,C++如何申请内存有兴趣。我们也简单说一下。
C++中的“new”命令,可以向系统申请分配内存,并且它根据其后所接的“数据类型”,来决定要申请多个字节。“char/字符”类型在C++中的大小正好是1个字节,所以,如果013行代码写的是:
013 new char;
那么每一次循环步骤中,将只分配1个字节,这会让这个测试更准确一些,但代价可能是变成费时1个小时,所以本例每次申请4K 个字节(1K是1024字节)。在C++,要一次分配连续的,多个char大小的内存,方法是使用中括号,正如本例所示:
new char [N];
其中N是所要分配的个数。
〖小提示〗:内存个数单位
内存最小单位为:字节/byte。
1K:2的10次方,1024个字节。
1M(兆):2的20次方,1024*1024个字节。
1G:2的30次方:1024*1024*1024个字节。
前面描述内存地址时的4G,即2的32次方(1024*1024*1024 * 4)。
5.4.4.32位机器
现在,本小节最后一个问题是:为什么系统给每个进程,预备的是4G个字节,而不是更多或更少?
以下是回答过程: 我们现在常用的电脑,是32位机。这32位,对应到硬件上,就是32条电路,这32路要么通电,要么断电,对应成二进制,就是32个1或0。
用这32位来表达内存地址,最大数是一个32位全为1的二进制数,即2的32次方,正好是4G。
哪一天我们全用上64位机,这个4G的数值,可就要改成多大2的64次方: 4G个4G!