( 原文地址:https://0xffff.one/d/262-srand-yuan-ma-fen-xi-fei-zhuan-ye )

这篇帖子源于群里的一个提问:

1
为什么srand((unsigned int)time())的种子要设置成unsigned?

最直接的回答就是因为在头文件里srand的函数定义里规定了参数类型是unsigned int吧,于是就去翻了一下glibc的源码。

Warnning: 这是一篇偏技术的文章,会涉及比较多的代码!


首先glibc的源码可以在这里拿到。

srand()是在stdlib库里的,所以先看stdlib/stdlib.h这个文件,在451~456行的地方(不同版本的库可能会不一样)可以找到srand()/rand()的定义:

1
2
3
4
/* Return a random integer between 0 and RAND_MAX inclusive.  */
extern int rand (void) __THROW;
/* Seed the random number generator with the given number. */
extern void srand (unsigned int __seed) __THROW;

从定义就可以直接看出srand()传进去的seed是规定unsigned int类型的了。

然后找一下这两个函数的实现,发现没有找到,然后在stdlib.h的395~405行看到这样的一段东西:

1
2
3
4
5
6
7
8
9
/* These are the functions that actually do things.  The `random', `srandom',
`initstate' and `setstate' functions are those from BSD Unices.
The `rand' and `srand' functions are required by the ANSI standard.
We provide both interfaces to the same random number generator. */
/* Return a random long integer between 0 and RAND_MAX inclusive. */
extern long int random (void) __THROW;

/* Seed the random number generator with the given number. */
extern void srandom (unsigned int __seed) __THROW;

大概意思是srand/rand是假的,srandom/random才是真正干事情的。用gdb跑了一下,发现真的是这样(图片大概意思是在call了srand函数后,马上就有一条jmp指令跳转到srandom函数了,rand函数也类似):

在stdlib/random.c里面可以找到函数的实现:

1
2
3
4
5
6
void __srandom (unsigned int x)
{
__libc_lock_lock (lock);
(void) __srandom_r (x, &unsafe_state);
__libc_lock_unlock (lock);
}

函数里面用到了,所以有用到多线程的怀疑(?),真正执行的是一个叫 __srandom_r 的递归函数,在文件stdlib/random_r.c里面可以找到(前方高能):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
  /* Initialize the random number generator based on the given seed.  If the
type is the trivial no-state-information type, just remember the seed.
Otherwise, initializes state[] based on the given "seed" via a linear
congruential generator. Then, the pointers are set to known locations
that are exactly rand_sep places apart. Lastly, it cycles the state
information a given number of times to get rid of any initial dependencies
introduced by the L.C.R.N.G. Note that the initialization of randtbl[]
for default usage relies on values produced by this routine. */

int __srandom_r (unsigned int seed, struct random_data *buf)
{
int type;
int32_t *state;
long int i;
int32_t word;
int32_t *dst;
int kc;

if (buf == NULL)
goto fail;
type = buf->rand_type;
if ((unsigned int) type >= MAX_TYPES)
goto fail;

state = buf->state;
/* We must make sure the seed is not 0. Take arbitrarily 1 in this case. */
if (seed == 0)
seed = 1;
state[0] = seed;
if (type == TYPE_0)
goto done;

dst = state;
word = seed;
kc = buf->rand_deg;
for (i = 1; i < kc; ++i)
{
/* This does:
state[i] = (16807 * state[i - 1]) % 2147483647;
but avoids overflowing 31 bits. */
long int hi = word / 127773;
long int lo = word % 127773;
word = 16807 * lo - 2836 * hi;
if (word < 0)
word += 2147483647;
*++dst = word;
}

buf->fptr = &state[buf->rand_sep];
buf->rptr = &state[0];
kc *= 10;
while (--kc >= 0)
{
int32_t discard;
(void) __random_r (buf, &discard);
}

done:
return 0;
fail:
return -1;
}

首先,这个函数的作用是通过一个随机数种子生成一串随机数,存到buf->state(通过参数传入,在函数中是局部变量state)中。这一串数(state)的第一个为传入的种子(seed)。比较有意思的是seed是不能为0的,如果是0的话会强行变成1:

1
2
3
/* We must make sure the seed is not 0.  Take arbitrarily 1 in this case.  */
if (seed == 0)
seed = 1;

可以用一下的一段代码进行一下测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* test.c */
#include <stdlib.h>
#include <stdio.h>
void test(unsigned int seed){
srand(seed);
for(int i=0;i<10;i++){printf("%d ",rand()); }
printf("\n");
}
int main(){
test(0);
test(1);
return 0;
}

至于为什么不能是0可以接着往下看,函数主要作用的是这个for循环:

1
2
3
4
5
6
7
8
9
10
11
12
for (i = 1; i < kc; ++i)                                                                             
{
/* This does:
state[i] = (16807 * state[i - 1]) % 2147483647;
but avoids overflowing 31 bits. */
long int hi = word / 127773;
long int lo = word % 127773;
word = 16807 * lo - 2836 * hi;
if (word < 0)
word += 2147483647;
*++dst = word;
}

如果seed是0的话,整个随机数序列就都是0了。这个循环做的其实就是一个递归式的运算

1
state[i] = (16807 * state[i - 1]) % 2147483647

但是就拆分成了几行代码(运算结果应该是一样的,但是却拆分成hi和lo两个部分计算后再合成word),估计就是考虑到提升速度的方面的:一个猜想是取余运算其实是用除法来做的,当数比较大的时候会花费比较多的时间,所以就拆分成两个较小的数来进行运算;另一个猜想是因为在汇编层面来看,进行除法运算后会产生两个结果,商和余数,分别放在两个不同的寄存器,所以回到C的层面看,做除法的话就是拿商的结果,做取余的话就是拿余数的结果,所以word / 127773和word % 127773其实只用进行一次除法运算就可以同时得到hi和lo的结果了。

由srand的实现可以看出C语言的随机数是伪随机数,只要种子确定了,根据上面的递归式生成的随机数序列就是确定的了。

对比一下C11的random,翻了一下C11的源码(看不懂- -,但是发现了/dev/urandom这个东西),发现C++11是通过随机熵来实现随机数的,(估计会比递归式的更好?)