Linux中的随机数

本文介绍了Linux中随机数的概念,包括真随机数和伪随机数的生成原理,并详细阐述了用户态下srand()和rand()函数的使用。同时,探讨了内核态下利用硬件事件作为噪声源生成真随机数的方法,以及/dev/random和/dev/urandom设备的区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、概念

随机数都是由随机数生成器(Random Number Generator)生成的。

1.真随机数 TRUE Random Number

真正的随机数是使用物理现象产生的:比如掷钱币、骰子、转轮、使用电子元件的噪音、核裂变等等,这样的随机数发生器叫做物理性随机数发生器,它们的缺点是技术要求比较高。

根据百科上的定义可以看到,真随机数是依赖于物理随机数生成器的。使用较多的就是电子元件中的噪音等较为高级、复杂的物理过程来生成。

至于“宇宙中不存在真正的随机”这种言论已经属于哲学范畴,在此不做讨论。在此我们默认存在随机。

使用物理性随机数发生器生成的真随机数,可以说是完美再现了生活中的真正的“随机”,也可以称为绝对的公平。

 

2.伪随机数 Pseudo-Random Number

真正意义上的随机数(或者随机事件)在某次产生过程中是按照实验过程中表现的分布概率随机产生的,其结果是不可预测的,是不可见的。而计算机中的随机函数是按照一定算法模拟产生的,其结果是确定的,是可见的。我们可以这样认为这个可预见的结果其出现的概率是100%。所以用计算机随机函数所产生的“随机数”并不随机,是伪随机数。

 

从定义我们可以了解到,伪随机数其实是有规律的。只不过这个规律周期比较长,但还是可以预测的。主要原因就是伪随机数是计算机使用算法模拟出来的,这个过程并不涉及到物理过程,所以自然不可能具有真随机数的特性。

 

二、实现

(一)、用户态

用户态主要使用srand()和rand()函数来产生随机数。

 

1、rand()函数:

头文件<stdlib.h>

定义函数:int rand(void)

函数功能:产生随机数:

    函数说明:因为rand的内部是用线性同余法做的,不是真的随机数,只不过因为其周期特别长,所以在一定范围内可以看成是随机的,rand()会返回一随机值,范围在0到RAND_MAX间,在调用此函数产生随机数前,必须利用srand()设好随机数种子,若没有设随机数种子,rand()在调用时会自动设随机数种子为1。

   返回值:返回0到RAND_MAX之间的整数值,RAND_MAX的范围最少在32767之间(int),即双字节(16位)。若unsigned int双字节是65535,且0-RAND_MAX每个数字被选中的随机率是相同的。  rand()产生的是假随机数,每次执行时是相同的,若要不同以不同的值来初始化,初始化的函数就是srand()。

 

2、srand()函数:

头文件 <stdlib.h>

定义函数:void srand(unsigned int seed);

     函数声明:srand()用来设置rand()产生随机数时的随机数种子,参数seed必须是整数,通常可以用time(0)的返回值作为seed.如果每次seed都设置相同的值,rand()产生的随机数值每次都一样。

 

   srand(unsigned)time(NULL))使用系统定时/计数器的值作为随机种子每个种子对应一组根据算法预先生成的随机数,所以在相同平台的环境下,不同时间产生的随机数是不同的,相应的若将srand(unsigned)tima(NULL)改为任一常量,则无论何时运行,运行多少次得到的随机数都是一组特定的序列,所以srand生成的随机数是伪随机数。但是,所谓的“伪随机数”指的并不是假的随机数,其实绝对的对技术只是一种假想状态的随机数,计算机只能生成相对的随机数,而这些随机数既是随机的又是有规律的,一部分遵守一定规律,一部分则不遵守任何规律,总结来说就是:计算机产生伪随机数而不是绝对的随机数。

 

在每次产生随机序列前,先指定不同的种子,这样计算出来的随机序列就不完全相同了,而使用同种子相同的数调用rand()会导致相同的随机数序列被生成。

 

案例:

#include <stdlib.h>

#include <stdio.h>

#include <time.h>

main()

{

int i,k;

srand( (unsigned)time( NULL ) );

for( i = 0; i < 10;i++ )

{

     k=rand()%100+1; 

     printf( " k=%d\n", k );

  }

}

 

由于rand产生的随机数是0到rand_max,而rand_max是一个很大的数,那么要产生一个从X到Y的随机数,可以这样:s=rand()%(x-Y+1)+Y,这表示从X到Y范围内的随机数

 

     系统在调用rand()之后就自动调用srand(),如果用户在rand()之前调用srand()给参数seed指定一个值,那么rand()就会将seed的值作为产生伪随机数的初始值,如果用户在rand()前没有调用srand(),系统会默认将1作为伪随机数的初始值,如果给了一个定值,每次rand()产生的随机数序列就一样了,所以为了避免发生上述情况,通常用srand((unsigned)time(0))或者srand((unsigned)time(NULL))来产生种子,如果觉得时间间隔太小,可以在(unsigned)time(0)或者(unsigned)time(NULL)后面乘以某个合适值,如srand((unsigned)time(NULL)*10)。

 

 另外,还可以通过 j=(int)(n*rand()/RAND_MAX+1),用来产生0到N之间的整数:

#include<stdio.h>

#include<time.h>

#include<stdlib.h>

int main(void)

{

    int i,j;

    for(i=0;i<10;i++)

    {

        j=1+(int)(10*rand()/(RAND_MAX+1));

        printf("%d  ",j);

    }

    printf("\n");

    return 0;

   

}

关于int x = rand() % n和 j=(int)(n*rand()/(RAND_MAX+1.0))的问题:

 

j=(int)(n*rand()/(RAND_MAX+1.0))就是随机一个0到n之间不包括n的浮点数,然后强制转换为就是0到9之间的整数了,这个跟x = rand() % n不同的地方就是,在多次随机出来的结果,前者理论更平均一些,后者只是和n求余得到的结果,没有前面的平均。

 

     取模操作%是为了避免在某些情况下,某些伪随机数生成器产生的数,低位不够随机的问题,这里涉及到二进制问题,因为取模在二进制意义上可能代表取得低位。

 

不过在针对自己的需要下,随机数可以满足所需的情况下,int x = rand() % n是完全可以代替j=(int)(n*rand()/(RAND_MAX+1.0)),毕竟前者的时间性能要好。

 

rand函数是真正的随机数生成器,而srand()会设置提供rand()使用的随机数种子。如果第一次调用rand()之前没有调用srand(),那么系统会为你自动调用srand()。如下程序:

#include <stdlib.h>

  #include <stdio.h>

  #include <time.h>

  main()

  {

    int i,k;

  

   for( i = 0; i < 10;i++ )

  {

     k=rand()%100+1;   //rand()%100表示取100以内的随机数,即取了随机数后再对100取余  x=rand()%(Y-X+1)+X

     printf( " k=%d\n", k );

  }

}

一样可以产生0到100间的随机数。

 

另外,srand这个函数要放到循环外面,或者循环调用的外面,否则调用得到的是相同的数字。看下面例子:

#include<time.h>

#include<stdlib.h>

#include<stdio.h>

main()

{

    int i,j;

  

    for(i=0;i<10;i++)

    {

        srand((int)time(0));

        j=1+int(rand()*100.0/(RAND_MAX+1.0));

        printf("%d  ",j);

    }

}

 

在执行结束后,会发现所有a[i]是一样的,srand放在循环里面,每产生一个随机数之前,都调用srand,由于计算机运行很快,这段代码总共执行不到1s,而srand()返回是以秒为单位,所以每次用time得到时间都是一样的,这相当于使用同一个种子产生产生随机序列,所以每次产生的随机数相同,于是出现所有a[i]是一样的,应该把srand放在循环外面。

 

如果计算伪随机数序列的初始值(种子)相同,那么计算出来的伪随机序列也完全相同,这个特性被有些软件加密解密,加密时,用某个种子生产一个伪随机数序列对数据进行处理;解密时,再利用种子数生产一个伪随机序列对加密数据进行还原。

 

(二)、内核态

Linux内核实现了一个随机数产生器,从理论上说这个随机数产生器产生的是真随机数。与标准C库中的rand(),srand()产生的伪随机数不同,尽管伪随机数带有一定的随机特征,但这些数字序列并非统计意义上的随机数。也就是说它们是可重现的--只要每次使用相同的seed值,就能得到相同的伪随机数列。通常通过使用time()的返回值来改变seed,以此得到不同的伪随机数序列,但time()返回值的结果并不是不确定的(可预测),也就是这里仍然缺少一个不确定的噪声源。对于需要真随机数的程序,都不能允许使用伪随机数。

为了获得真正意义上的随机数,需要一个外部的噪声源。Linux内核找到了一个完美的噪声源产生者--就是使用计算机的人。我们在使用计算机时敲击键盘的时间间隔,移动鼠标的距离与间隔,特定中断的时间间隔等等,这些对于计算机来讲都是属于非确定的和不可预测的。虽然计算机本身的行为完全由编程所控制,但人对外设硬件的操作具有很大的不确定性,而这些不确定性可以通过驱动程序中注册的中断处理例程(ISR)获取。内核根据这些非确定性的设备事件维护着一个熵池,池中的数据是完全随机的。当有新的设备事件到来,内核会估计新加入的数据的随机性,当我们从熵池中取出数据时,内核会减少熵的估计值。

asmlinkage int handle_IRQ_event(unsigned int irq, struct pt_regs *regs, struct irqaction *action)

{

    int status = 1;

    int retval = 0;

     

    if (!(action->flags & SA_INTERRUPT))

        local_irq_enable();

 

    do

    {

        status |= action->flags;

        retval |= action->handler(irq, action->dev_id, regs);

        action = action->next;

    }while (action);

     

    if (status & SA_SAMPLE_RANDOM)

        add_interrupt_randomness(irq);

 

    local_irq_disable();

    return retval;

}

上面这段代码是x86上用来处理某条中断线上注册的ISR例程的函数。这里我们感兴趣的地方是:如果ISR在注册期间指定了SA_SAMPLE_RANDOM标志,在处理完action后,还要调用add_interrupt_randomness()这个函数,它使用中断间隔时间为内核随机数产生器产生熵。内核就是在这里为熵池填充新数据的。

 

如果我们完全不操作计算机会如何呢?也就是作为噪声源的产生者,我们完全不去碰键盘,鼠标等外设,不让熵池获得新的数据,这个时候如果去熵池取数据内核会如何反应?

 

内核在每次从熵池中取数据后都会减少熵的估计值,如果熵估计值等于0了,内核此时可以拒绝用户对随机数的请求操作。

1、熵的输出接口

void get_random_bytes(void *buf, int nbytes)

该函数返回长度为nbytes字节的缓冲区buf,无论熵估计是否为0都将返回数据。使用这个函数时需要在内核空间。我们写一个小模块来测试一下。

#include <linux/init.h>

#include <linux/module.h>

#include <linux/kernel.h>

#define NUM 10

 

void get_random_bytes(void *buf, int nbytes);

 

static int get_random_number(void)

{

    unsigned long randNum[10];

    int i = 0;

 

    printk(KERN_ALERT "Get some real random number.\n");

    for (i=0; i<NUM; i++)

    {

        get_random_bytes(&randNum[i], sizeof(unsigned long));

        printk(KERN_ALERT "We get random number: %ld\n", randNum[i]);

    }

    return 0;

}

 

static void random_exit(void)

{

    printk(KERN_ALERT "quit get_random_num.\n");

}

 

module_init(get_random_number);

module_exit(random_exit);

MODULE_LICENSE("GPL");

MODULE_AUTHOR("Test");

 

Makefile文件如下:

obj-m = get_random_num.o

KDIR = $(shell uname -r)

PWD = $(shell pwd)

 

all:

    make -C /lib/modules/$(KDIR)/build M=$(PWD) modules

clean:

    make -C /lib/modules/$(KDIR)/build M=$(PWD) clean

 

2、/dev/random & /dev/urandom

这两个特殊设备都是字符型设备。我们可以在用户空间通过read系统调用读这两个设备文件以此获取随机数。这两个设备文件的区别在于:如果内核熵池的估计值为0时,

 

/dev/random将被阻塞,而/dev/urandom不会有这个限制。

#include <assert.h>

#include <sys/stat.h>

#include <sys/types.h>

#include <fcntl.h>

#include <unistd.h>

 

/* minmax中返回一个随机值 */

 

int random_number(int min, int max)

{

    static int dev_random_fd = -1;

    char *next_random_byte;

    int bytes_to_read;

    unsigned random_value;

     

    assert(max > min);

    

    if (dev_random_fd == -1)

    {

        dev_random_fd = open("/dev/random", O_RDONLY);

        assert(dev_random_fd != -1);

    }

    

    next_random_byte = (char *)&random_value;

    bytes_to_read = sizeof(random_value);

    

    /* 因为是从/dev/random中读取,read可能会被阻塞,一次读取可能只能得到一个字节,

     * 循环是为了让我们读取足够的字节数来填充random_value.

     */

    do

    {

        int bytes_read;

        bytes_read = read(dev_random_fd, next_random_byte, bytes_to_read);

        bytes_to_read -= bytes_read;

        next_random_byte += bytes_read;

    }while(bytes_to_read > 0);

    return min + (random_value % (max - min + 1));

}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值