计算机是确定性的机器,因此它无法直接生成真正的随机数,而浑沌系统的随机数生成速度又比较慢,在许多情况下不适合作为快速的(伪)随机数库函数算法。快速的伪随机数生成算法中最著名的要数linear-congruential method(线性同余法),也就是:
Xn+1 =(a Xn + b )% c // % 就是C/C++ 中的MOD(同余)运算符
这种方法可以从一个种子X0 = seed开始,连续生成任意长的伪随机数序列Xn 。它的运算过程极其简单,并且如果令c = 2m,其中m为Xn的字长,则连MOD运算都直接省掉了—— Xn+1 ≥2m 时高位自动溢出而被截除。用这种办法生成的伪随机数序列,在给定范围和精度内确实满足均匀分布的要求,但是并非连续分布,因为计算机存放数据的精度不是无限!正是由于最小数据间隙的存在,该序列将会以一个相当长的周期循环。
更为重要的是,上述方法为代表的伪随机数生成器,其生成的数字序列并非拥有不可辨别的模式,相反,它们的规律都极其容易由少量数据反推得到。这里有两种具体情况,一类是数据加密中要应用到伪随机数,则此种“易于反推出规律的”伪随机数生成器很不合适;另一类是科学计算中用到的伪随机数,这时并不要求一定得掩盖住其内在规律,但是却有另外的要求:伪随机数的内在规律不能与所研究问题的自然规律相似,并且在必要的时候需要使用高精度的方法减小“最小数据间隙”。
之所以称其为“线性同余法”,是因为Xn+1 =(a Xn + b )% c生成的伪随机数实际上是分布在一条经MOD运算截断的直线上,其近似具有“随机性”的核心也就是这个同余运算。但是如果将相继的一对数(x=X2i ,y=X2i+1)作为一对二维坐标确定平面上的一个点,则在伪随机数数列上的一个小区段内显然会连续出现几对数,它们各自确定的点在平面上沿一直线排列,这也就是文章开头我所提到的现象。其中直线的方程——
设该小区段为Xp,Xp+1,Xp+2,……Xp+ m
则相应的点对为(Xp,Xp+1),(Xp+2,Xp+3),……
直线y = Ax + B =(aX + b)MOD c =(aX MOD c)+ b
而(aX MOD c)= aX - Floor[ aX /c ]·c ,
∵根据算法要求,c相比于aX是一个较大的常数,
∴在一个小区段内总有Floor [ aX /c ]≡Floor [ aXp /c ]。
用常数P记Floor [ aXp /c ] ,则直线方程可写为:
y = Ax + B = aX + b - cP ,
此方程当且仅当 cP ≤ aXp < aXp+ m < cP+c时成立。
这种二维平面上的规律性严重影响到我的模拟计算,因此不得不反复调用randomize()函数来初始化伪随机数生成器——即使是使用两个独立的伪随机数生成器也不能避免类似的规律性。但是randomize()函数是利用系统时钟作为种子实现初始化的,这导致“不断初始化”的办法存在严重的问题:
其一,读取系统时钟的函数需要访问CPU-RAM总线以外的慢速设备,因此执行时间相当慢,会严重影响高性能计算的速度。当然,外围时钟可以利用现代操作系统的虚拟时钟设备在CPU内部仿真实现,这部分的缓解了此问题。
其二,系统时钟的计时精度并不高,但是今日的计算机“CPU-高速缓存”子系统运行速度非常高,以至于CPU在系统时钟计时精度的最小间隙(微秒级)之内就可以完成上千次操作。如果运行速度很快,因而非常频繁地读取系统时钟来初始化的话,可能会导致相继的几次读取获得完全相同的时钟值,换句话说,就是连续多次使用同一个种子来做初始化,那么显然会获得完全相同的伪随机数数值。
其三,以线性同余法为代表的简单伪随机数生成算法,最初生成的几个数受初始化种子的影响很大,所以随机性都不好,一般需要舍弃伪随机数序列的前几个值——这种不均匀性问题在上述高速运行的情况下特别严重!因为在非常频繁地做初始化时,每次采用的种子即使不完全相同,也是紧密相邻的。如果舍弃数列中的前几个数,却又会增加运算量、降低效率。
总之,频繁利用系统时间初始化这一方法在高速运算时的效果相当差。
但是从好的方面看,只要研究者对自己处理的科学计算问题思路明确、图像清晰,一般来说总是有办法避免伪随机数序列的内在规律与所研究问题的自然规律相似;同时只要略微施加一些编程上的技巧,就能有效地以存储空间换运行时间,避免反复调用系统时间做初始化的动作。例如对于上述例子,可以开辟一个二维数组T[2][max](max≠2n),然后:
randomize() ;
int i ;
for (i=0 ; i
T [1][ i ] = Random(Range_We_Need) ;
/* 这里的Random函数根据具体情况返回需要的数据类型 */
randomize() ;
for (i=0 ; i
T [2][ i ] = Random(Range_We_Need) ;
依据此算法存放一组伪随机的二维坐标数据对,而后在调用时按列读取数据对(T[1][i] ,T[2][i]),在“max”不大时(例如5000对于双精度浮点数组),整个数组(78.125KB)可以完全缓存在CPU的L2 cache中,这样就能迅速的生成和读取一系列的坐标数据对。同时,由于T[1][i] 与T[2][i]并非相继生成一对随机数,它们之间的联系因为两次非密集的randomize()调用而几乎完全不相关。如果所需的数据对非常多以至于超过max对,也可以根据情况再次生成这么多对数据后取用,但是最好不要随意加大数组的最大大小。
上面是对于特殊用法的一个解决方案特例,其它各种各样的情况也可以类似的具体问题具体对待,比如一维随机行走问题就完全可以直接使用连续的“线性同余”伪随机数序列,最多不过需要在走过相当长的一段时间之后重新初始化一下伪随机数生成器。事实上,如果科学计算时所需的大量随机数是各自独立使用的,那么大多可以直接连续应用线性同余法产生的序列而无需顾虑太多,间或插入一个“初始化”操作。但是,线性同余法只能确保在“各自独立使用”的一维情况下,随机数数列上每一个小区段内局部没有明显的相关规律,二维及其以上的高维就需要具体分析——所谓“二维或高维”,也就是前述那种需要连续产生随机向量的情况。
为什么要过一段时间插入一个初始化操作呢?
除非加上一些程序外的数据——例如系统时间,否则任何伪随机数生成器一定是循环的,只是循环周期的长短不同。因为伪随机数生成器是一个固定的运算程序,并且前一个输出是下一个运算的输入,但计算机的数值储存状态是有限的,因此必然会在循环到某一时刻产生与前面相同的结果,从此开始重复前面的一个循环。要延长周期,最简单的办法就是过一些循环后重新拿系统时间来做一下初始化;当然还可组合两个以上的伪随机数生成器一起工作,有许多种组合方式,例如:用第一个生成器产生h个随机数,用第二个随机数生成器随机扰乱它们的顺序后输出;或者可以用第一个随机数生成器产生一个seed和一个整数h,而第二个生成器以seed为种子连续产生h个随机数……
对于一般的科学研究来说,只要保证伪随机数的内在规律不与所研究的自然规律相似,并且在所需数据精度下呈现“准连续的均匀分布”即可,而对“由少量数据反推生成规律”这种反向工程的困难程度不作限制。有时甚至还需要一种简单清晰的生成规律,以便确认这种规律是否与所研究的自然规律相似。
但是,在数据加密的时候,往往对这种反向工程极为担心,因此需要设计一种难于被反推出的生成规律。由大质数组合出巨大合数的乘法对反向工程来说相当困难,但是它需要用特殊的、巨大的数据结构来存放数据和进行运算,因此不适合用于扩展成为快速产生伪随机数的算法。
而混沌动力学系统演化过程中生成伪随机数的办法却相当合适。选取一组好的非线性方程组,只要对程序中所用非线性方程的几个常数最初的值保密,并且根据自身输出的伪随机数,每一些循环之后调用系统时间来悄悄更改这些常数,那么哪怕整个算法都公开给全世界,也几乎不可能被反向工程找出规律(也就是找出那些常数值的变化规律)。只要在运行一段时间之后才开始对外发布这些伪随机数序列,那么即使是暴力穷举法也没有可能正向破解出其内在规律。算法如下:
+++++++++++++++++++++++++++++++++++++++++++++++++++++
本算法未经密码安全性检验,使用者须自行承担可能的安全性风险
+++++++++++++++++++++++++++++++++++++++++++++++++++++
FirstFill( Constant[ n ] ) ; // 生成几个常数的初值,这需要硬件随机数
/* Constant[ ] 数组除第一个外存放的是各个常数的值,均为整数,并且
sizeof( Constant[ n ] ) ≥ 安全所需的加密强度,例如512bits */
do { Refill( Constant[ n ], SYSTIME, RandomOutput( Constant[ n ] ) ) ;
/* 读取系统时间,并产生几个伪随机数,
然后以这几个数据混合运算出新的Constant[ n ] 值 */
for (count=Constant[0] ; count>0 ; count - - )
RandomOutput( Constant[ n ] ) ; // 连续输出伪随机数
Ask( ENDorNOT ) ;
} until ( ENDorNOT==Yes ) ;
这就是利用混沌动力学系统的初值敏感性进行“反反向工程”的伪随机数产生算法,初值上的微小差异将在一小段时间之后导致非常巨大的分歧,所以“由运行一段时间之后才开始发布的伪随机数序列反推初值”的做法几乎不可能实现;至于如何应付暴力穷举法的正向破解——只要数组Constant[n] 的总长度足够长,并且严密保护好其中任何一个初值都不被窃取,那么穷举所需时间的数学期望也是不可思议的漫长。
困难的是,如何对这些即时生成的伪随机数作归一化校正,使任意一小段很短的伪随机数序列也能呈现均匀分布。另外,虽然混沌动力学系统所产生的伪随机数其内在规律不容易与一般应用中的规律相混淆,(即使同样是研究混沌动力学系统,只要模型略有差异就不会有问题,)但是万一有所混淆,查找问题根源就变得极为困难。这里我提供一个权宜之计:收集伪随机数子程序的输出,待收集到足够多的值以后对它们进行FFT,可以尝试一维、二维……任何一种你觉得必要的傅里叶变换;若发现变换出的结果有显著的规律性,那么立即再次收集一批不同的输出数据,重复刚才的测试过程数次。假如类似的规律始终存在,那么就说明此伪随机数子程序有明显的规律性,不宜使用;另一方面,如果你没有发现任何显著的规律,也不表明这个子程序就很优秀——只是你暂时还没发现缺陷罢了。
如何使伪随机数的内在规律变得更加隐蔽?如何使它看起来(并且用起来)更像是一个真正的随机数而不是可预测的结果?这些问题,不仅在科学计算领域被经常提出,并且更为迫切的出现在电子商务、机密通讯等涉及到高度保密的信息传递的领域。这是因为现今比较流行的加密算法几乎都基于一些来自于随机数的密钥,而如果采用伪随机数的话,其内禀规律一旦被别有用心的人发现,那么就有可能推算出密钥,进而破译相应的密文!上述的混沌动力系统可以产生基本符合要求的伪随机数序列,但是更符合需要的随机数应该是来自于硬件设备的真随机数……
伪随机数并非是“坏随机数”,只是某些情况下为稳妥起见我们不得不使用“真随机数”,产生真随机数的办法必须依赖硬件,其原理不外乎于量子力学和混沌动力系统。量子力学的随机现象迄今没有更深层的规律性,因此利用放射源衰变等过程得到的随机数序列肯定是真正随机的,在经过归一化校正后,便可以作为优秀的随机数序列发布了。但是,混沌动力系统不是“伪随机”的确定性系统吗?它怎么能产生“真随机数”呢?
我们说纯软件的混沌动力学系统伪随机数生成器产生的是伪随机数,这是因为现有软件运行在完全确定性的计算机上,并且计算机可以存放的数值虽然离散、但却准确无误——例如因为有限位二进制存储方式离散的缘故不能准确存放实数X=3.00000……,可是却能准确存放例如X=2.9999983 E+00这样一个完全确定的近似值。那么,在各种条件和变化过程都完全确定的计算机中,产生的随机数当然是可以预测的“伪随机数”。
现实世界中的硬件设备就不同了,例如有些硬件利用的是电子元件温度的随机波动经电子学放大而产生随机数,理论上它应该满足一个“热传导-流体力学”复合的非线性动力学方程组,但实际上并没有可能利用这样的方程组来预测随机数产生情况:
首先,我们无法精确确定该模型的初始条件,许多物理量最多只能测到4~6位有效数字。这一方面是因为测量工具自身精度的局限性;另一方面是因为客观世界的微观涨落使这些初始值快速小幅变化着;甚至在足够小的微观尺度上还会撞上量子的“不确定性原理”。种种原因都导致很难在客观世界中同时测出一系列初值的精确值,相比之下,我们可以在计算机中的模型上指定任意多个足够准确(高达8~15位有效数字)的初始值,并强制令其为“同一时刻”的值。
其次,计算机模拟过程中很容易限定环境条件(在8~15位有效数字的精度上)保持稳定,而在现实世界中这是不可能的!以PCI卡形式的随机数生成器为例,机箱内空气的湿度、温度和速度矢量分布上的微小波动,都可能显著影响到它的散热情况,又如果把它们都考虑为扩展模型的变量,那又会大大增加模型的初始条件数目,使“同时测出一系列初值的精确值”变得更加不可能。
可见,利用现实世界中测不准初值的混沌动力学系统,因为它的初值敏感性,我们也可以获得几乎完全不能预测的随机数序列。对于基于量子力学的硬件产生的无限随机数序列,预测它是不可能事件;而基于混沌动力系统的硬件产生的无限随机数序列,预测它是零概率事件,亦即说,对于可以预见的未来的任何一种应用,都可以放心的视为“不可能事件”!总之,我们把依赖量子力学或者混沌动力系统的硬件随机数生成器统称为“真正的”随机数生成器。
一个长半衰期的放射源(如T1/2=30年的Cs-137)加上一个盖格计数器,辅以有效的防护设备和计算机数据接口卡,还要再加上一个调试好参数的归一化校正程序,就可以组成一套不错的硬件随机数生成器,并且是基于量子力学的完全不可预测的随机数产生装置。但随着放射源的嬗变和仪器设备的慢慢老化,这个归一化校正程序需要每过一段时间就校正一次参数,这倒不是什么大问题;真正的大问题在于,它产生随机数的效率太低了!为保证足够的测量精度,探测器的增益不可以太大,这样该装置的随机数输出带宽受放射源辐射强度的限制,可能只有每秒几个bit甚至几十秒才有一个bit !
(电阻或者二极管中)电子热运动的Johnson噪声是量子效应,根据这个原理产生随机数的硬件装置可以更快地输出随机数,例如ComScire或者Tundra公司的一些设备,可以以20 kb/s的带宽输出质量优异的真随机数。Intel RNG的带宽为75kb/s,并且几乎不需要花费额外的费用,因为这个装置所需的硬件是Intel 82802 Firmware Hub(也就是BIOS设备)中的标准配件。从2003年起,威盛电子(VIA)在其CPU中嵌入了PadLock RNG模块,由于采用CPU内核级别的设计和生产工艺,它的访问速度很快,并且号称能以800~1600 kb/s的速率生成真随机数——折合成单精度浮点数却也不过25~50 kHz 。VIA PadLock RNG的技术原理是联合使用一组四个集体管震荡器,其随机性本源还是来自Johnson噪声。
正因为硬件随机数生成器产生的数字完全是随机的,即使是设计和使用它的人也无法找到这些真随机数的生成规律,所以用一个软件来精确的校正其分布变得相当困难,必须使用大量统计数据获得经验校正函数,而且还不能保持非常好的校正精度,这一点对加密应用来说问题不大,但是对于科学计算而言就会大大降低计算结果的精度。
硬件随机数生成器最大的缺点在于:它们都太慢了!即使是VIA PadLock RNG的25~50 kHz单精度浮点数产生速率,也远远跟不上当前主流x86系统的2~5GFLOPS / CPU的浮点计算速度:单CPU工作时差不多每十万次浮点操作才能等候到一个单精度随机数,用于计算密集型的科学研究工作显然不够。
说到这里,就不得不提一些半硬件半软件的解决方案,它们不需要专门的硬件设备,可以跨平台的应用于几乎任何一种计算机上,比Intel或者VIA的RNG还容易获得;但它们又不是完全的软件伪随机数生成器,而是至少近似不可预测的(半真半伪的)随机数来源,从本质上来说,它们是混沌动力系统硬件随机数生成器的一类:
这些生成器中,有些捕捉键盘、鼠标、硬盘驱动器的动作以及外设中断信号(DMA等),并由此作为随机数的来源;有些则把互联网上的不确定行为收集起来作为随机数来源,例如本机的网络信号流量波动情况;还有一些会从系统硬盘上任意选定的声音、图像、动画文件中提取随机信号,或者从声卡、调制解调器之类的AD/DA转换器中提取背景噪音。这些不确定行为的最根本来源是个人或者人类社会的活动,而人或者人类社会是非线性的耗散系统,显然可以认为上述随机数生成方法都是基于混沌动力系统的。
被采样的行为虽然看起来将会随机发生,但其实这种随机性却无法证明,如果这些物理动作是静止的(比如说服务器的鼠标和键盘可能根本就没连上)或者是简单重复的,就可能生成一系列低质量的随机数,由此导致随机数硬件源的质量远远低于设计者在最理想条件下的预期值。不过,虽然质量较低且不稳定,这些硬件却比那些直接利用放射源计数或者电子热运动等现象制造的专用硬件廉价、常见得多,并且已经在系统层或者驱动程序层实现了通用的随机数池设备,从使用者的角度来看更接近软件解决方案——无论是它的易用性还是实现方式。
它们当中最著名的一种,就是Linux 1.3.30版以上内核开始在系统层提供的随机数池虚拟设备“/dev/random”。“/dev/random”设备驱动程序收集来自其它硬件的动作信息:鼠标、键盘、磁盘驱动器、各种接口卡等等,将这些未经校正的随机信息填充到一个通用的“(信息)熵池”中,然后在需要访问“/dev/random”设备时提供合适的统计学函数校正和转换。当然,它收集信息的速率也很慢,甚至比那些基于量子效应的专门硬件还慢!不过一般而言质量确实也能接近基于量子效应的硬件随机数生成器。
由于运作速度慢,“/dev/random”的“熵池”在频繁读取时将很快枯竭,因此我们不应该把这个设备当作快速获取大量高质量随机数的来源,而应该从另一个相关的虚拟设备“/dev/urandom”中获取。“/dev/urandom”设备以“/dev/random”中的随机数做种子,利用MD5算法生成纯软件的伪随机数,其效果显然比简单的线性同余法高许多,相应的速度也会低一些。调用这两个虚拟设备的方法很简单,本文最后的附录中给出了相应的C99程序实例。
为了高速的产生大量(伪)随机数,我们不得不使用纯软件的伪随机数生成器,而任何纯软件随机数生成器本身是不能自发生成信息熵的,它必须有一个来自硬件的输入——也就是一个来自“熵源”的数据。按通常的算法描述,就是要先对伪随机数生成器进行初始化,例如用系统时间作种子来初始化,这一操作在32位的C99标准中会输入32bits的信息熵,此后每一次调用系统时间“重新初始化”理论上都会引入32bits信息熵。这里有两点需要注意:
第一,两次初始化之间,该伪随机数生成器输出的序列总共只有32bits的信息熵,而无论到底输出了多少bit的信息!当然,并不妨碍在许多情况下正常的使用这些低熵的数据。
第二,如果非常频繁的调用系统时间“重新初始化”,则前后两次获取的系统时间数值之间的相关性太大,就不会重新引入32bits信息熵了。因此有必要适当降低调用系统时间的频率,或者使用其它更快的硬件随机数生成器作为“熵源”。