随机数真的是随机的么? ---让菠菜、彩票等裤衩赔穿的漏洞
日常业务中,经常会生成随机数,这里以C++为例,常见的随机数生成方法如下:原理很简单,先生成一个随机数生成器,指定种子,然后生成随机数;
#include <iostream> #include <windows.h> int main() { printf("time(0)=%d",time(0)); srand(0); for(int i=0;i<=10;i++){ printf("%d\n",rand()); } system("pause"); }
根据随机数的定义,每次生成的数不一样才能叫随机数。然而上面这段代码真是这样的么?我们看看效果了:生产的10个数确实不一样啊,这不就是我们想要的效果么? 感兴趣的读者可以自行尝试多生成一些数,看看每次生成的数是不是都不一样!
然而这样就结束了么?图样图森破!同样的代码,我们多运行几次实例看看了,效果如下:不同的时间点我生成了4个不同的实例,然后每次生成实例产生的随机数都是一样的,这是不是很“毁三观”啊!
我明明想生成随机数,为啥每次生成实例产生的数据都是一样的了?换句话说:srand生成随机数的原理是啥了?
计算机的随机数都是由伪随机数,即是由小M多项式序列生成的,其中产生每个小序列都有一个初始值,即随机种子。(注意: 小M多项式序列的周期是65535,即每次利用一个随机种子生成的随机数的周期是65535,当你取得65535个随机数后它们又重复出现了。)我们知道 rand() 函数可以用来产生随机数,但是这不是真正意义上的随机数,是一个伪随机数,是根据一个数(我们可以称它为种子)为基准以某个递推公式推算出来的一系列数,当这系列数很大的时候,就符合正态公布,从而相当于产生了随机数,但这不是真正的随机数,所以我们运行不同实例时产生的“随机数”居然是一样的,都是相同种子惹的祸!所以我们把种子用当前时间来代替,重新更改后的代码如下:
#include <iostream> #include <windows.h> int main() { int seed = time(0); printf("seed=%d", seed); srand(seed); for(int i=0;i<=10;i++){ printf("%d\n",rand()); } system("pause"); }
多运行几个实例,每次产生的数确实不一样了:这样就万事大吉、高枕无忧了?
前面说了:相同的种子,产生的伪随机数是一样的,是不是也可以反过来猜想: 根据伪随机数倒推种子了?比如我先在看到的数字是22843、6380、25403、7962,是不是能倒推出种子是16210637719041了?一旦倒推出种子,我是不是也能成功预测第5个数字是13894了?穷举找seed的代码如下:
int num[] = { 22843,6380,25403,7962 }; int seed = time(0); bool bfind = false; while (seed--) { srand(seed); for (int i=0;i<4;i++) { if (num[i]!=rand()) { bfind = false; break; } bfind = true; } printf("%d is not the seed,continue!\n", seed); if (bfind) { printf("find the seed:%d \n", seed); break; } }
一旦找到seed,就能用这个seed继续生成剩下的数字,准确预测了!
注意:实战时,因为不知道菠菜站点启动服务器的时间,所以这个计算量较大,一般都是用服务器+多线程跑的,这里只是介绍最核心的原理,感兴趣的小伙伴可以自行尝试!
===================================分割线====================================
怎么才能得到尽可能随机的数了?这就要回到随机数的定义了!我个人简单理解:所谓随机数,就是没有规律、无法精准预测、琢磨不透生成规律的数!为了达到这个效果,这里“不走寻常路”地用多线程(不考虑同步)去读写数据,原因很简单:
多个线程之间如果不用互斥、信号量等同步,强行在“同一时间”读写同一个内存的数据,那么问题来了:哪个线程先读写?哪个线程后读写?内存被读写后的结果是啥?就我个人浅薄的知识理解,影响最终结果的关键因素如下:
- cpu的内部缓存:cpu有3级缓存,每级缓存存放的数据和共享的范围是不一样的,内存数据都会被先读到缓存后再进入cpu的运算单元处理
- 线程的调度:这个是由操作系统实现的;调度的算法,包括但不限于先进先出、最短耗时任务优先、时间片轮转、最大最小公平、multi-level feedback、多CPU核场景下MFQ等,调度机制异常复杂!
机制越复杂,人为预测(甚至是猜测)准确的可能性就越小,这不就是“随机”要达到的效果么?所以这里尝试用多线程读写同一内存的方式生产随机数,核心代码如下:
#include <pthread.h> int num = 0; pthread_mutex_t mutex; void* inc_num(void* arg) { //pthread_mutex_lock(&mutex); for (int i= 0; i <= 10000; i++) { num++; } //pthread_mutex_unlock(&mutex); return NULL; } void* dec_num(void* arg) { //pthread_mutex_lock(&mutex); for (int i = 0; i <= 10000; i++) { num--; } //pthread_mutex_unlock(&mutex); return NULL; } void main() { pthread_mutex_init(&mutex,NULL); pthread_t thread_id[50]; for (int i = 0; i < 50; i++) { if (i % 2) { pthread_create(thread_id + 1, NULL, inc_num, NULL); } else { pthread_create(thread_id + 1, NULL, dec_num, NULL); } } for (int i = 0; i < 50; i++) { pthread_join(thread_id[i],NULL); } printf("num is %d",num); pthread_mutex_destroy(&mutex); }
代码的原理很简单:生成50个线程,奇数号线程增加num,偶数号线程减少num,但是线程完全不使用同步机制!由于上述的cpu缓存、os线程调度机制很复杂,人为预测准确的概率较小,反正我尝试过多次后还没遇到重复的结果!
1、https://www.runoob.com/w3cnote/cpp-rand-srand.html C++ rand 与 srand 的用法
2、https://zhuanlan.zhihu.com/p/97071815 内核调度算法