解密随机数生成器

从小就一直很好奇,MP3播放器的随机播放功能是如何实现的,今天读到一篇关于随机数的文章,又勾起了我的那时好奇心,索性上下求索,了解了随机数背后的很多知识,顿觉豁然开朗,特意写这篇文章和大家总结分享一下。

其实,随机数在我们身边无处不在。无论是玩扑克牌麻将骰子时的点数,玩LOL时的玩家匹配,还是高大上的量子物理,核聚变,都无一例外地随机数有关,在混沌理论中,这个世界本身就是一系列随机过程的产物——好吧,有点激动,扯得太远了——作为编程爱好者,应该会发现,每一门编程语言必然会有自己的随机数生成函数,常用的比如:C语言stdlib库中的rand()函数,java中Random类中的nextInt () 方法,Python中random模块的randint()方法等等。作为各种编程语言的“官方标配”,这小小的随机函数作用那也是大大的,不光而这看似简单的东西背后学问还真不少。

好了,废话不多讲,现在就让我们走近随机数,看看它的“庐山真面目”!

三种随机数生成器

你们有没有想过这个问题——计算机到底是怎么得到随机数的?作为人类,我们大可提笔随便在纸上写一大串数字,也许就算是随机数了,但是计算机可没有这本事,它必须有一个科学稳定的随机数来源,才能得到随机数,这个来源,我们称为随机数生成器。

常见的计算机随机数生成器有三种:

  • 一是使用物理方法,称为真随机数生成器(True Random Number Generator),生成的算是真正意义上的随机数,无法预测且无周期性;
  • 与真随机数对应的是伪随机数生成器(Pseudo Random Number Generator),它是由算法计算得来的,但这种方法生成的随机数是可预测、有周期的,并不能算真的随机数,因此得名伪随机数;
  • 还有第三种方法,叫随机数表法,就是用真随机数生成器事先生成好大量随机数,存到数据库中,使用时再从库中调用。记得高中数学书第三册后附有一个叫随机数表的东西,使用时直接查阅就行,这种方法简单,但缺点是内存占用大,因此不常采用,我也就不展开讲了,在此我只详细介绍一下前两种:真随机数生成器与伪随机数生成器。

真随机数生成器

程序员都是完美主义者,我们自然希望有一个能产生真正随机数的程序。遗憾的是,生成真随机数的程序,就像永动机一样无法实现,要得到真正的随机数目前来讲只能看老天的眼色,比如噪声(Noise),量子效应(Quantum effects),人品(RP)这些物理现象。

第一个真随机数发生器是1955年由Rand公司创造的,而在1999年,Intel发布Intel810芯片组时,就配备了硬件随机数发生器,原理利用的是电阻和振荡器生成的热噪声。目前,大部分芯片厂商都集成了硬件随机数发生器,使用十分方便,而一系列为科研和信息安全设计的真随机数发生器也层出不穷,发展到今天,真随机数生成器(以下简称TRNG)大体可分为以下三种:

1、基于电路的TRNG:

1 振荡器采样:如上文中提到的Intel810RNG芯片,利用热噪声(是由导体中电子的热震动引起的)放大后,影响一个由电压控制的振荡器,再通过另一个高频振荡器来收集数据,得到随机数。在Intel 815E芯片组的个人电脑上安装Intel Security Driver(ISD)后,就可以通过编程读取寄存器获取RNG中的随机数。

2 直接放大电路噪声:直接以热噪声等电路噪声为随机源,通过运算放大,统计一定时间内达到阈值的信号数以此来得到随机数。

3电路亚稳态: 2010年,德国的研究团队现在开发出一种真随机数发生器,它使用的计算机内存双态触发器作为随机的一个额外层,触发器可随机的在1或0状态中切换,在切换之前,触发器处于行为无法预测的“亚稳态”。在亚稳态结束时,内存中的内容为完全随机。研究人员对一个触发器单元阵列的实验显示,这种方法产生的随机数比传统方法“随机”约20倍。

4 混沌电路:混沌电路的输出的结果对初始条件很敏感,不可预测,且在IC芯片中易集成,可产生效果不错的真随机数。

5 根据。。。质量?:劣质内存芯片工作在高温下,其数据是不可预测的,读取这里面的数据,就会得到难以预测的随机数,人们采用这种技术,制作了随机数发生器板卡。。。(O(∩_∩)O呵呵 ~)

2、基于物理的TRNG:

如今的量子物理,从本质上讲就是真正随机的,是不可预测的——比较著名的就是薛定谔的猫啦——因此很适合用来做TRNG,当然,这并不是说基于经典宏观物理学的TRNG就不存在,比如http://random.org/这个网站,从1998年开始就在Internet上提供真随机数服务了,它用的是大气噪音来生成真随机数(很不可思议吧)。下面举几个近年来基于量子物理发明的TRNG:

1 http://random.irb.hr/这是一个与克罗地亚计算机科学家发明的TRNG,全名是Quantum Random Bit Generator Service (QRBGS),它依赖于半导体光子发散量子物理过程中内在的随机性,通过光电效应检测光子得到随机数。

2 2010年,比利时物理学家S. Pironio和同事利用纠缠粒子的随机性和非局域性属性(别问我,我也不懂- -)创造出了真随机数。

3 2011年,加拿大渥太华的物理学家Ben Sussman利用激光脉冲和钻石创造了真随机数。Sussman的实验室使用持续几万亿分之一秒的激光脉冲照射钻石,激光进入和出来的方向发生了变化。Sussman称改变与量子真空涨落的相互作用有关,在量子法则中这种作用是不可知的,他认为这可以用于创造真正的随机数。

4 2012年,澳大利亚国立大学的科学家从真空中的亚原子噪音获取随机数,创造了世界上最快的随机数发生器。量子力学中,亚原子对会持续自发的产生和湮灭,通过监听真空内亚原子粒子量子涨落产生的噪音,可以得到真正的随机数。

3、基于其他因素的TRNG:

1 PuTTYgen:它的随机数是让用户移动鼠标达到一定的长度,之后把鼠标的运动轨迹转化为种子,由此产生随机数

2 Linux自1.3.30版就在内核提供了真随机数生成器(至少是理论上),它利用机器的噪音生成随机数,噪音源包括各种硬件运行时速,用户和计算机交互时速。比如击键的间隔时间、鼠标移动速度、特定中断的时间间隔和块IO请求的响应时间等。

3 人可不可以生成随机数呢?嘿嘿,掷骰子斗地主啥的我就不说了,据说某些HR选简历的方式是,往天上一扔,掉在桌子上的简历就通过。。。这个可是真随机啊!

另外:
用Java可以使用java.security.SecureRandom 产生真随机数(待查);
Linux系统有/dev/random,/dev/urandom向用户提供真随机数;
Windows系统有CryptGenRandom 函数生成真随机数(待查)
感兴趣的可以研究一下。

基于物理的随机数生成器,生成的随机数无周期,不可预测,分布均匀,然而,这种随机数生成器技术要求高,而且随机数生成效率不高,难以满足计算机高速计算的需要,因此为了提高数据产生效率,它们都常被用来生成伪随机数生成器的“种子”(seed),并以此生成伪随机的输出序列。

伪随机数生成器

上面我们了解了基于物理现象的真随机数生成器,然而,真随机数产生速度较慢,为了实际计算需要,计算机中的随机数都是由程序算法,也就是某些公式函数生成的,只不过对于同一随机种子与函数,得到的随机数列是一定的,因此得到的随机数可预测且有周期,不能算是真正的随机数,因此称为伪随机数(Pseudo Random Number)。

不过,别看到伪字就瞧不起,这里面也是有学问的,看似几个简简单单的公式可能是前辈们努力了几代的成果,相关的研究可以写好几本书了!顺便提一下,亚裔唯一图灵奖得主姚期智,研究的就是伪随机数生成论(The pseudo random number generating theory)。在这里,我重点介绍两个常用的算法:同余法(Congruential method)和梅森旋转算法(Mersenne twister)

1、同余法

同余法(Congruential method)是很常用的一种随机数生成方法,在很多编程语言中有应用,最明显的就是java了,java.util.Random类中用的就是同余法中的一种——线性同余法(Linear congruential method),除此之外还有乘同余法(Multiplicative congruential method)和混合同余法(Mixed congruential method)。好了,现在我们就打开java的源代码,看一看线性同余法的真面目!

在Eclipse中输入java.util.Random,按F3转到Random类的源代码:

首先,我们看到这样一段说明:

这里写图片描述

翻译过来是:

这个类的一个实现是用来生成一串伪随机数。这个类用了一个48位的种子,被线性同余公式修改用来生成随机数。(见Donald Kunth《计算机编程的艺术》第二卷,章节3.2.1)

显然,java的Random类使用的是线性同余法来得到随机数的。

接着往下看,我们找到了它的构造函数与几个方法,里面包含了获得48位种子的过程:

/** 
     * Creates a new random number generator. This constructor sets 
     * the seed of the random number generator to a value very likely 
     * to be distinct from any other invocation of this constructor. 
     */  
    public Random() {  
        this(seedUniquifier() ^ System.nanoTime());  
    }  

    private static long seedUniquifier() {  
        // L'Ecuyer, "Tables of Linear Congruential Generators of  
        // Different Sizes and Good Lattice Structure", 1999  
        for (;;) {  
            long current = seedUniquifier.get();  
            long next = current * 181783497276652981L;  
            if (seedUniquifier.compareAndSet(current, next))  
                return next;  
        }  
    }  

    private static final AtomicLong seedUniquifier  
        = new AtomicLong(8682522807148012L);  
    public Random(long seed) {  
        if (getClass() == Random.class)  
            this.seed = new AtomicLong(initialScramble(seed));  
        else {  
            // subclass might have overriden setSeed  
            this.seed = new AtomicLong();  
            setSeed(seed);  
        }  
    }  
    private static long initialScramble(long seed) {  
        return (seed ^ multiplier) & mask;  
    }  
    。。。  

这里使用了System.nanoTime()方法来得到一个纳秒级的时间量,参与48位种子的构成,然后还进行了一个很变态的运算——不断乘以181783497276652981L,直到某一次相乘前后结果相同——来进一步增大随机性,这里的nanotime可以算是一个真随机数,不过有必要提的是,nanoTime和我们常用的currenttime方法不同,返回的不是从1970年1月1日到现在的时间,而是一个随机的数——只用来前后比较计算一个时间段,比如一行代码的运行时间,数据库导入的时间等,而不能用来计算今天是哪一天。

好了,现在我不得不佩服这位工程师的变态了:到目前为止,这个程序已经至少进行了三次随机:

1、获得一个长整形数作为“初始种子”(系统默认的是8682522807148012L)

2、不断与一个变态的数——181783497276652981L相乘(天知道这些数是不是工程师随便滚键盘滚出来的-.-)直到某一次相乘前后数值相等

3、与系统随机出来的nanotime值作异或运算,得到最终的种子

再往下看,就是我们常用的得到随机数的方法了,我首先找到了最常用的nextInt()函数,代码如下:

public int nextInt() {  
    return next(32);  
}  

代码很简洁,直接跳到了next函数:

protected int next(int bits) {  
    long oldseed, nextseed;  
    AtomicLong seed = this.seed;  
    do {  
        oldseed = seed.get();  
        nextseed = (oldseed * multiplier + addend) & mask;  
    } while (!seed.compareAndSet(oldseed, nextseed));  
    return (int)(nextseed >>> (48 - bits));  
}  

OK,祝贺一下怎么样,因为我们已经深入到的线性同余法的核心了——没错,就是这几行代码!

在分析这段代码前,先来简要介绍一下线性同余法。

在程序中为了使表达式的结果小于某个值,我们常常采用取余的操作,结果是同一个除数的余数,这种方法叫同余法(Congruential method)。

线性同余法是一个很古老的随机数生成算法,它的数学形式如下:

Xn+1 = (a*Xn+c)(mod m) 
其中,
m>0,0<a<m,0<c<m

这里Xn这个序列生成一系列的随机数,X0是种子。随机数产生的质量与m,a,c三个参数的选取有很大关系。这些随机数并不是真正的随机,而是满足在某一周期内随机分布,这个周期的最长为m。根据Hull-Dobell Theorem,当且仅当:

  1. c和m互素;

  2. a-1可被所有m的质因数整除;

  3. 当m是4的整数倍,a-1也是4的整数倍

时,周期为m。所以m一般都设置的很大,以延长周期。

现在我们回过头来看刚才的程序,注意这行代码:

nextseed = (oldseed * multiplier + addend) & mask;  

和Xn+1=(a*Xn+c)(mod m)的形式很像有木有!

没错,就是这一行代码应用到了线性同余法公式!不过还有一个问题:怎么没见取余符号?嘿嘿,先让我们看看三个变量的数值声明:

private static final long multiplier = 0x5DEECE66DL;  
private static final long addend = 0xBL;  
private static final long mask = (1L << 48) - 1;  

其中multiplier和addend分别代表公式中的a和c,很好理解,但mask代表什么呢?其实,x & [(1L << 48)–1]与 x(mod 2^48)等价。解释如下:

x对于2的N次幂取余,由于除数是2的N次幂,如:

0001,0010,0100,1000。。。。

相当于把x的二进制形式向右移N位,此时移到小数点右侧的就是余数,如:

13 = 1101 8 = 1000

13 / 8 = 1.101,所以小数点右侧的101就是余数,化成十进制就是5

然而,无论是C语言还是java,位运算移走的数显然都一去不复返了。(什么,你说在CF寄存器中?好吧,太高端了点,其实还有更给力的方法)有什么好办法保护这些即将逝去的数据呢?

学着上面的mask,我们不妨试着把2的N次幂减一:

0000,0001,0011,0111,01111,011111。。。

怎么样,有启发了吗?

我们知道,某个数(限0和1)与1作与(&)操作,结果还是它本身;而与0作与操作结果总是0,即:

a & 1 = a, a & 0 = 0

而我们将x对2^N取余操作希望达到的目的可以理解为:

1、所有比2^N位(包括2^N那一位)高的位全都为0

2、所有比2^N低的位保持原样

因此, x & (2^N-1)与x(mod 2^N)运算等价,还是13与8的例子:

1101 % 1000 = 0101 1101 & 0111 = 0101

二者结果一致。

嘿嘿,讲明白了这个与运算的含义,我想上面那行代码的含义应该很明了了,就是线性同余公式的直接套用,其中a = 0x5DEECE66DL, c = 0xBL, m = 2^48,就可以得到一个48位的随机数,而且这个谨慎的工程师进行了迭代,增加结果的随机性。再把结果移位,就可以得到指定位数的随机数。

接下来我们研究一下更常用的一个函数——带参数n的nextInt:

public int nextInt(int n) {  
    if (n <= 0)  
        throw new IllegalArgumentException("n must be positive");  

    if ((n & -n) == n)  // i.e., n is a power of 2  
        return (int)((n * (long)next(31)) >> 31);  

    int bits, val;  
    do {  
        bits = next(31);  
        val = bits % n;  
    } while (bits - val + (n-1) < 0);  
    return val;  
}  

显然,这里基本的思路还是一样的,先调用next函数生成一个31位的随机数(int类型的范围),再对参数n进行判断,如果n恰好为2的方幂,那么直接移位就可以得到想要的结果;如果不是2的方幂,那么就关于n取余,最终使结果在[0,n)范围内。另外,do-while语句的目的应该是防止结果为负数。

你也许会好奇为什么(n & -n) == n可以判断一个数是不是2的次方幂,其实我也是研究了一番才弄明白的,其实,这主要与补码的特性有关:

众所周知,计算机中负数使用补码储存的(不懂什么是补码的自己百度恶补),举几组例子:

2 :0000 0010 -2 :1111 1110

8 :0000 1000 -8 :1111 1000

18 :0001 0010 -18 :1110 1110

20 :0001 0100 -20 :1110 1100

不知道大家有没有注意到,补码有一个特性,就是可以对于两个相反数n与-n,有且只有最低一个为1的位数字相同且都为1,而更低的位全为0,更高的位各不相同。因此两数作按位与操作后只有一位为1,而能满足这个结果仍为n的只能是原本就只有一位是1的数,也就是恰好是2的次方幂的数了。

不过个人觉得还有一种更好的判断2的次方幂的方法:

n & (n-1) == 0

感兴趣的也可以自己研究一下^o^。

好了,线性同余法就介绍到这了,下面简要介绍一下另一种同余法——乘同余法(Multiplicative congruential method)。

上文中的线性同余法,主要用来生成整数,而某些情景下,比如科研中,常常只需要(0,1)之间的小数,这时,乘同余法是更好的选择,它的基本公式和线性同余法很像:

Xn+1=(a*Xn )(mod m )
其实只是令线性公式中的c=0而已。只不过,为了得到小数,我们多做一步:

Yn = Xn/m

由于Xn是m的余数,所以Yn的值介于0与1之间,由此到(0,1)区间上的随机数列。

除此之外,还有混合同余法,二次同余法,三次同余法等类似的方法,公式类似,也各有优劣,在此不详细介绍了。

同余法优势在计算速度快,内存消耗少。但是,因为相邻的随机数并不独立,序列关联性较大。所以,对于随机数质量要求高的应用,特别是很多科研领域,并不适合用这种方法。

不要走开,下篇博客介绍一个更给力的算法——梅森旋转算法(Mersenne Twister),持续关注啊!


转自:
http://t1174779123.iteye.com/blog/2036709
http://t1174779123.iteye.com/blog/2037719

  • 8
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值