【Atheros】minstrel速率调整算法源码走读
先说几个辅助的宏,因为内核不支持浮点运算,当然还有实现需要,minstrel对很多浮点值做了缩放:
/* scaled fraction values */
#define MINSTREL_SCALE 16
#define MINSTREL_FRAC(val, div) (((val) << MINSTREL_SCALE) / div)
#define MINSTREL_TRUNC(val) ((val) >> MINSTREL_SCALE)
MINSTREL_SCALE是一个放大的倍数,minstrel设定的是16,缩放16位也就是2^16倍,我不知道为什么要设置成这么大的数,不过没有关系不影响理解。MINSTREL_FRAC是两个数相除之后放大,MINSTREL_TRUNC则是逆运算,将某个被放大的数缩小。
下面进行源码分析,根据对外暴露的结构体对指针函数的定义:
static struct rate_control_ops mac80211_minstrel_ht = {
.name = "minstrel_ht",
.tx_status = minstrel_ht_tx_status,
.get_rate = minstrel_ht_get_rate,
.rate_init = minstrel_ht_rate_init,
.rate_update = minstrel_ht_rate_update,
.alloc_sta = minstrel_ht_alloc_sta,
.free_sta = minstrel_ht_free_sta,
.alloc = minstrel_ht_alloc,
.free = minstrel_ht_free,
#ifdef CONFIG_MAC80211_DEBUGFS
.add_sta_debugfs = minstrel_ht_add_sta_debugfs,
.remove_sta_debugfs = minstrel_ht_remove_sta_debugfs,
#endif
};
我们需要着重关注核心的三个函数:tx_status负责每次发送完聚合帧之后根据ACK的状况更新各个速率状态,get_rate负责每次要发新的数据包的时候指定发送速率,rate_init在与另一站点建立连接的时候初始化相关参数。下面首先介绍一个抽随机速率用的表的生成,然后按照rate_init、get_rate、tx_status的顺序介绍minstrel的原理。
1. 初始化探测速率表
minstrel对速率的管理是通过速率组来管理的,这关乎几个重要的变量:
const struct mcs_group minstrel_mcs_groups[];
static u8 sample_table[SAMPLE_COLUMNS][MCS_GROUP_RATES];
mi->groups[]
先说minstrel_mcs_groups,我的实验环境最多支持双流,minstrel_mcs_groups也就是一个长度为8的数组,如果是三流就是长度为12的数组,这8个group的配置按顺序分别是:
组号 | 空间流数 | 是否支持SGI | 20MHz/40MHz |
0 | 1 | 否 | 20 |
1 | 2 | 是 | 20 |
2 | 1 | 否 | 20 |
3 | 2 | 是 | 20 |
4 | 1 | 否 | 40 |
5 | 2 | 是 | 40 |
6 | 1 | 否 | 40 |
7 | 2 | 是 | 40 |
这个变量存的是这几个速率组不变的配置信息,mi->groups这个数组和minstrel_mcs_groups相对应,存储在对应配置下8个速率的动态数据统计。
sample_table是随机生成的一个速率表,本节就是讲这个表的生成和作用。
当Minstrel模块加载的时候,首先初始化速率表,也就是sample_table,当需要进行探测的时候,探测顺序就是以这个速率表做参考:
static u8 sample_table[SAMPLE_COLUMNS][MCS_GROUP_RATES];
static void
init_sample_table(void)
{
int col, i, new_idx;
u8 rnd[MCS_GROUP_RATES]; memset(sample_table, 0xff, sizeof(sample_table));
for (col = ; col < SAMPLE_COLUMNS; col++) {
for (i = ; i < MCS_GROUP_RATES; i++) {
get_random_bytes(rnd, sizeof(rnd));
new_idx = (i + rnd[i]) % MCS_GROUP_RATES; while (sample_table[col][new_idx] != 0xff)
new_idx = (new_idx + ) % MCS_GROUP_RATES; sample_table[col][new_idx] = i;
}
}
}
SAMPLE_COLUMNS的默认取值是10,MCS_GROUP_RATES是每组MCS中有几个速率,也就是8(单流、双流、三流里面各有8个速率),这个速率取样表就是一个10*8的方阵。这段函数生成的速率表,一共10行,每一行都是0-7这七个数字的随机分布,minstrel是随机探测,但是用哪个速率不是在发送的时候才随机获取的,而是提前随机生成这个表,发送的时候依次遍历这个表,所以说这个速率表其实没有什么讲究,就是避免了在运行过程中频繁的抽随机数罢了,这个表已经提前生成了10组MCS随机排列的序列,运行过程中只要顺序读取,就是随机探测了。
2. 初始化探测的相关参数
minstrel_ht_rate_init和minstrel_ht_rate_update分别在连接站点初始化或者需要更新的时候被调用,他们的本质都是调用了minstrel_ht_update_caps,其中和速率调整相关的呢是这么几句:
mi->avg_ampdu_len = MINSTREL_FRAC(1, 1); /* When using MRR, sample more on the first attempt, without delay */
if (mp->has_mrr) {
mi->sample_count = 16;
mi->sample_wait = 0;
} else {
mi->sample_count = ;
mi->sample_wait = ;
}
mi->sample_tries = 4;
……
for (i = ; i < ARRAY_SIZE(mi->groups); i++) {
u16 req = ; mi->groups[i].supported = ;
if (minstrel_mcs_groups[i].flags & IEEE80211_TX_RC_SHORT_GI) {
if (minstrel_mcs_groups[i].flags & IEEE80211_TX_RC_40_MHZ_WIDTH)
req |= IEEE80211_HT_CAP_SGI_40;
else
req |= IEEE80211_HT_CAP_SGI_20;
} if (minstrel_mcs_groups[i].flags & IEEE80211_TX_RC_40_MHZ_WIDTH)
req |= IEEE80211_HT_CAP_SUP_WIDTH_20_40; if ((sta_cap & req) != req)
continue; mi->groups[i].supported =
mcs->rx_mask[minstrel_mcs_groups[i].streams - ]; if (mi->groups[i].supported)
n_supported++;
} if (!n_supported)
goto use_legacy;
这一段的主要作用呢,就是初始化平均AMPDU长度(聚合帧长度)为1,因为我们需要MRR(Multi-rate retry,多速率重传),设置sample_count=16\sample_wait=0\sample_tries=4,这三个参数和探测的频率相关,后面会介绍,最后,确定本文最前面介绍的关于(空间流数、SGI、带宽)设置的数组中的每一数组项是不是被硬件支持。
3. 发送时确定发送速率
minstrel_ht_get_rate是速率调整最重要的两个函数之一,它决定了当前待发送数据包的发送速率。
if (rate_control_send_low(sta, priv_sta, txrc))
return;
首先,当目标站点不存在,或者本次发送不需要等ACK的时候,为了确保数据包尽可能被对方正确接收,那么会直接用传统速率来发送,不给它分配MCS速率。
下面获取本次取样的速率:
sample_idx = minstrel_get_sample_rate(mp, mi);
深入minstrel_get_sample_rate:
mg = &mi->groups[mi->sample_group];
sample_idx = sample_table[mg->column][mg->index];
mr = &mg->rates[sample_idx];
sample_idx += mi->sample_group * MCS_GROUP_RATES;
minstrel_next_sample_idx(mi);
前面已经介绍,有一个随机生成的速率表用作获取随机数用,首先在这个速率表中选取下一个sample_idx,之后,调用minstrel_next_sample_idx把mi->sample_group指向下一个可用的group,这个group就是前面介绍的拥有空间流数、SGI、带宽配置的组了。然后sample_idx在自身取值的基础上加上了sample_group*MCS_GROUP_RATES,所以这个sample_idx可以用来得到当前用的第几个取样组,比如sample_idx=27,那么就是8*3+4,它表示的就是第4个速率组(双流、SGI、20MHz)的第四个速率,因为是双流,就表示MCS11。正常情况下,minstrel_get_sample_rate这个函数会把sample_idx返回,如果不探测,则返回-1,下面继续看minstrel_get_sample_rate的几个返回-1的情况:
if (!mp->has_mrr && (mr->probability > MINSTREL_FRAC(, )))
return -;
如果不支持mrr(多速率重传),并且当前速率的投递率已经达到了95%以上,就不再取样。
if (minstrel_get_duration(sample_idx) >
minstrel_get_duration(mi->max_tp_rate)) {
if (mr->sample_skipped < )
return -; if (mi->sample_slow++ > )
return -;
}
这里把刚刚得到的这个要取样的速率sample_idx和当前吞吐率最高的速率进行比较,那么这个duration是个什么东西?这个duration就是对网卡用当前速率发送一个数据包所用时间的估算,duration越大,基本等效于速率值越低,具体含义如下:
#define AVG_PKT_SIZE 1200
#define MCS_NBITS (AVG_PKT_SIZE << 3)
#define MCS_NSYMS(bps) ((MCS_NBITS + (bps) - 1) / (bps)) #define MCS_SYMBOL_TIME(sgi, syms) \
(sgi ? \
((syms) * + ) / : /* syms * 3.6 us */ \
(syms) << /* syms * 4 us */ \
) #define MCS_DURATION(streams, sgi, bps) MCS_SYMBOL_TIME(sgi, MCS_NSYMS((streams) * (bps))) /*
* Define group sort order: HT40 -> SGI -> #streams
*/
#define GROUP_IDX(_streams, _sgi, _ht40) \
MINSTREL_MAX_STREAMS * * _ht40 + \
MINSTREL_MAX_STREAMS * _sgi + \
_streams - /* MCS rate information for an MCS group */
#define MCS_GROUP(_streams, _sgi, _ht40) \
[GROUP_IDX(_streams, _sgi, _ht40)] = { \
.streams = _streams, \
.flags = \
(_sgi ? IEEE80211_TX_RC_SHORT_GI : ) | \
(_ht40 ? IEEE80211_TX_RC_40_MHZ_WIDTH : ), \
.duration = { \
MCS_DURATION(_streams, _sgi, _ht40 ? : ), \
MCS_DURATION(_streams, _sgi, _ht40 ? : ), \
MCS_DURATION(_streams, _sgi, _ht40 ? : ), \
MCS_DURATION(_streams, _sgi, _ht40 ? : ), \
MCS_DURATION(_streams, _sgi, _ht40 ? : ), \
MCS_DURATION(_streams, _sgi, _ht40 ? : ), \
MCS_DURATION(_streams, _sgi, _ht40 ? : ), \
MCS_DURATION(_streams, _sgi, _ht40 ? : ) \
} \
}
这一段宏的定义比较复杂,简单来说,假定平均每个数据包长度是1200字节,也就是(1200<<3)即(1200*8)bits,根据每个码元(symbol)包含几个bit算出来这个数据包一共有多少码元(MCS_NSYMS),最后根据数据率计算数据包在每一个MCS参数下的发送延时。
花了这么多时间介绍minstrel_get_sample_rate,因为这个函数包括了最主要的,探测速率从哪儿来的问题,现在回到minstrel_ht_get_rate,刚才是从:
sample_idx = minstrel_get_sample_rate(mp, mi);
展开的,获取到sample_idx,后面就是重头戏了:
if (sample_idx >= ) {
sample = true;
minstrel_ht_set_rate(mp, mi, &ar[0], sample_idx, true, false);
info->flags |= IEEE80211_TX_CTL_RATE_CTRL_PROBE;
} else {
minstrel_ht_set_rate(mp, mi, &ar[0], mi->max_tp_rate, false, false);
}
如果sample_idx为-1,那么就不探测,仍然用之前确定的最高吞吐率的速率mi->max_tp_rate来发送,除了前面说的几个情况下会返回-1之外,还有一个重要的控制,就是关于探测周期的控制,本文最后一部分会做介绍,下面继续看需要探测的情况下怎么选择速率,minstrel_ht_set_rate这个函数的其他参数可以不关注,重点是我标红的这三个,第一个是一个(struct ieee80211_tx_rate)类型的参数,最后驱动底层就是从这个结构体里面拿到发送的MCS、重传次数、发送带宽等参数,minstrel用一个ar数组来存,ar[0]是第一个速率,重传多次仍然失败的话就用ar[1],以此类推,第二个参数就是速率索引sample_idx/max_tp_rate,第三个参数代表当前帧是不是探测采样用的帧。对这个函数要重点说的,是重传策略:
if (sample)
rate->count = ;
else if (mr->probability < MINSTREL_FRAC(, ))
rate->count = ;
else if (rtscts)
rate->count = mr->retry_count_rtscts;
else
rate->count = mr->retry_count;
如果当前速率用作探测,则只发送一次,如果当前速率的投递率小于20%,则发两次,其他情况下重传次数依赖于mr->retry_count,这个变量是在每次更新各个速率状态之后更新的,如果该速率的投递率小于10%,初始为1,否则初始为2,然后根据帧平均发送时间还要适当增加。
最后就是MRR,也就是多速率重传的其它几个速率怎么设置了:
if (mp->hw->max_rates >= ) {
if (sample_idx >= )
minstrel_ht_set_rate(mp, mi, &ar[], mi->max_tp_rate, false, false);
else
minstrel_ht_set_rate(mp, mi, &ar[], mi->max_tp_rate2, false, true); minstrel_ht_set_rate(mp, mi, &ar[], mi->max_prob_rate, false, !sample); ar[].count = ;
ar[].idx = -;
} else if (mp->hw->max_rates == ) {
minstrel_ht_set_rate(mp, mi, &ar[], mi->max_prob_rate, false, !sample); ar[].count = ;
ar[].idx = -;
} else {
ar[].count = ;
ar[].idx = -;
}
如果驱动支持的速率个数多于3,那么按照max_tp_rate>max_prob_rate的顺序来设置剩下两个速率,如果第一个速率不作为探测速率,也就是说第一个速率是用max_tp_rate,那么第二三个速率就用max_tp_rate2>max_prob_rate,如果底层支持的多速率就是能支持两个,则第二速率就用max_prob_rate发送,这个速率是投递率最高的速率,确保在有限次发送后正确传输。
4. 更新各个速率状态
minstrel_ht_tx_status函数是当每个帧发送完成时的回调函数,但是每个聚合帧中只有一个帧携带了该聚合帧都使用了哪些速率发送、哪一次发送才成功等我们需要的信息,因此对于未携带这些信息的帧,直接返回不予处理:
if ((info->flags & IEEE80211_TX_CTL_AMPDU) &&
!(info->flags & IEEE80211_TX_STAT_AMPDU))
return;
之后根据ar[0]、ar[1]和ar[2]里面携带的实际发送次数,得到每个速率在本次发送中共发送了多少个帧(指单帧)、成功了多少个(单帧)。然后进入核心处理逻辑:
rate = minstrel_get_ratestats(mi, mi->max_tp_rate);
if (rate->attempts > &&
MINSTREL_FRAC(rate->success, rate->attempts) <
MINSTREL_FRAC(, ))
minstrel_downgrade_rate(mi, &mi->max_tp_rate, true); rate2 = minstrel_get_ratestats(mi, mi->max_tp_rate2);
if (rate2->attempts > &&
MINSTREL_FRAC(rate2->success, rate2->attempts) <
MINSTREL_FRAC(, ))
minstrel_downgrade_rate(mi, &mi->max_tp_rate2, false); if (time_after(jiffies, mi->stats_update + (mp->update_interval / * HZ) / )) {
minstrel_ht_update_stats(mp, mi);
if (!(info->flags & IEEE80211_TX_CTL_AMPDU))
minstrel_aggr_check(sta, skb);
}
minstrel_downgrade_rate是把系统当前的max_tp_rate或者max_tp_rate2切换到比原来速率所在组的前一个较低或相同流数的组的对应值上,比如,现在的max_tp_rate是第3个group的max_tp_rate,此时就会判断,第2组包含的速率都是几个空间流的,如果组3是双流的,则组2是单流或双流的话,就把组2的max_tp_rate作为当前系统的max_tp_rate,如果组3是单流,但组2是双流,就会再看组1是不是单流,以此类推。
根据驱动的注释,是为了防止空间流突然不能用,比如本来用的双流,有一个流突然不起作用了,所以这里判断一下,如果max_tp_rate或者max_tp_rate2已经尝试30次以上,投递率还不超过20%,则降空间流,最后要解决的一个问题就是,每个组的max_tp_rate和max_tp_rate2是怎么算出来的,看代码的最后几行,每过(mp->update_interval / 2 * HZ) / 1000 这些时间(单位是jiffies),就会更新整个速率表中各个mcs_group,针对每个mcs_group,会遍历8个MCS:
for (i = ; i < MCS_GROUP_RATES; i++) {
if (!(mg->supported & BIT(i)))
continue; mr = &mg->rates[i];
mr->retry_updated = false;
index = MCS_GROUP_RATES * group + i;
minstrel_calc_rate_ewma(mr);
minstrel_ht_calc_tp(mi, group, i);
针对每一个速率,也就是mr,都会调用minstrel_calc_rate_ewma去计算吞吐率和投递率的加权平均:
mr->sample_skipped = ;
mr->cur_prob = MINSTREL_FRAC(mr->success, mr->attempts);
if (!mr->att_hist)
mr->probability = mr->cur_prob;
else
mr->probability = minstrel_ewma(mr->probability, mr->cur_prob, EWMA_LEVEL);
mr->att_hist += mr->attempts;
mr->succ_hist += mr->success;
计算这个速率在这个计算周期内的投递率cur_prob,EWMA_LEVEL默认为75,如果之前没有发送成功过,也就是历史尝试数等于0,就直接更新该速率的投递率,否则调用minstrel_ewma以(原投递率*75%+本次投递率*25%)的计算方式更新投递率。算完了prob的ewma之后就调用minstrel_ht_calc_tp计算各个速率的吞吐率,这个吞吐率结合了实际数据包的长度、各个编码方案的数据率,应该是比较准确的算法。
在每个循环中,在算完当前速率的投递率和吞吐率之后,就会和当前组的最佳值进行比较,如果优于最佳值,就进行交换,再把以前的最佳值和以前的max_tp_rate2比较:
if ((mr->cur_tp > cur_prob_tp && mr->probability >
MINSTREL_FRAC(, )) || mr->probability > cur_prob) {
mg->max_prob_rate = index;
cur_prob = mr->probability;
cur_prob_tp = mr->cur_tp;
} if (mr->cur_tp > cur_tp) {
swap(index, mg->max_tp_rate);
cur_tp = mr->cur_tp;
mr = minstrel_get_ratestats(mi, index);
} if (index >= mg->max_tp_rate)
continue; if (mr->cur_tp > cur_tp2) {
mg->max_tp_rate2 = index;
cur_tp2 = mr->cur_tp;
}
最后再用同样的方法遍历各个组,更新整个系统的max_tp_rate、max_tp_rate2等域,不再重复帖代码。
5. 探测频率
那么minstrel多久或者说在什么条件下会进行探测呢,这和前文提到的三个重要的变量有关,就是sample_wait、sample_tries和sample_count。我的理解,sample_wait是说再过多少个数据包之后进行探测,sample_tries是说拿几个帧来探测,但不是说经过一个sample_wait+sample_tries之后就开始计算那个速率最好,什么时候重新计算各个速率的吞吐率是用时间来控制的,在下一次计算之前,最多进行sample_count组(sample_wait+sample_tries)的循环。
在聚合帧发送完成的回掉函数minstrel_ht_tx_status中,对这几个值进行判断:
if (!mi->sample_wait && !mi->sample_tries && mi->sample_count > ) {
mi->sample_wait = + * MINSTREL_TRUNC(mi->avg_ampdu_len);
mi->sample_tries = ;
mi->sample_count--;
}
mi->avg_ampdu_len是每个聚合帧聚合长度的加权平均:
mi->avg_ampdu_len = minstrel_ewma(mi->avg_ampdu_len, MINSTREL_FRAC(mi->ampdu_len, mi->ampdu_packets), EWMA_LEVEL);
而在一个数据包将要发送请求采样速率sample_idx的minstrel_get_sample_rate函数中,一开始会做如下处理:
if (mi->sample_wait > ) {
mi->sample_wait--;
return -;
} if (!mi->sample_tries)
return -; mi->sample_tries--;
因为要等sample_wait个包之后探测,因此先判断这个值是不是大于0,如果是,则把这个计数器减1,之后返回-1不进行探测。每次探测时会把控制探测数量的计数器sample_tries减1,当这个数已经减到0的时候返回-1不探测。
以上就是Minstrel速率调整算法的基本流程。
【Atheros】minstrel速率调整算法源码走读的更多相关文章
- 【Atheros】Ath9k速率调整算法源码走读
上一篇文章介绍了驱动中minstrel_ht速率调整算法,atheros中提供了可选的的两种速率调整算法,分别是ath9k和minstrel,这两个算法分别位于: drivers\net\wirele ...
- ConcurrentHashMap源码走读
目录 ConcurrentHashMap源码走读 简介 放入数据 容器元素总数更新 容器扩容 协助扩容 遍历 ConcurrentHashMap源码走读 简介 在从JDK8开始,为了提高并发度,Con ...
- Apache Spark源码走读之23 -- Spark MLLib中拟牛顿法L-BFGS的源码实现
欢迎转载,转载请注明出处,徽沪一郎. 概要 本文就拟牛顿法L-BFGS的由来做一个简要的回顾,然后就其在spark mllib中的实现进行源码走读. 拟牛顿法 数学原理 代码实现 L-BFGS算法中使 ...
- storm-kafka源码走读之KafkaSpout
from: http://blog.csdn.net/wzhg0508/article/details/40903919 (五)storm-kafka源码走读之KafkaSpout 原创 2014年1 ...
- Atitit 图像清晰度 模糊度 检测 识别 评价算法 源码实现attilax总结
Atitit 图像清晰度 模糊度 检测 识别 评价算法 源码实现attilax总结 1.1. 原理,主要使用像素模糊后的差别会变小1 1.2. 具体流程1 1.3. 提升性能 可以使用采样法即可..1 ...
- Apache Spark源码走读之16 -- spark repl实现详解
欢迎转载,转载请注明出处,徽沪一郎. 概要 之所以对spark shell的内部实现产生兴趣全部缘于好奇代码的编译加载过程,scala是需要编译才能执行的语言,但提供的scala repl可以实现代码 ...
- Apache Spark源码走读之13 -- hiveql on spark实现详解
欢迎转载,转载请注明出处,徽沪一郎 概要 在新近发布的spark 1.0中新加了sql的模块,更为引人注意的是对hive中的hiveql也提供了良好的支持,作为一个源码分析控,了解一下spark是如何 ...
- Apache Spark源码走读之7 -- Standalone部署方式分析
欢迎转载,转载请注明出处,徽沪一郎. 楔子 在Spark源码走读系列之2中曾经提到Spark能以Standalone的方式来运行cluster,但没有对Application的提交与具体运行流程做详细 ...
- twitter storm 源码走读之5 -- worker进程内部消息传递处理和数据结构分析
欢迎转载,转载请注明出处,徽沪一郎. 本文从外部消息在worker进程内部的转化,传递及处理过程入手,一步步分析在worker-data中的数据项存在的原因和意义.试图从代码实现的角度来回答,如果是从 ...
随机推荐
- Linux文本过滤常用命令(转)
01 cat命令 通常用来显示文本文件的内容 一般用来查看比较短的文本文件,因为其缓冲区有限 -s选项可以用来合并文件中多余的空行,多个空行将被压缩为一个空行; -n选项可以显示行号 -b选项可以跳过 ...
- PHP switch的“高级”用法详解
只所以称为“高级”用法,是因为我连switch的最基础的用法都还没有掌握,so,接下来讲的其实还是它的基础用法! switch 语句和具有同样表达式的一系列的 IF 语句相似.很多场合下需要把同一个变 ...
- 【Mysql】字段排序中文排序
在mysql中 如果字段的值是中文的话,排序结果往往不符合人意. 所以如果要中文排序正常的话,可以使用如下函数 SELECT huayangare0_.id AS id1_0_, huayangare ...
- JAVA常见算法题(二十三)
package com.xiaowu.demo; /** * 给一个不多于5位的正整数,要求:①求它是几位数:②逆序打印出各位数字. * * * @author WQ * */ public clas ...
- hdu4099 Revenge of Fibonacci
题意:给定fibonacci数列,输入前缀,求出下标.题目中fibonacci数量达到100000,而题目输入的前缀顶多为40位数字,这说明我们只需要精确计算fibinacci数前40位即可.查询时使 ...
- 用ASP实现JS的decodeURIComponent()函数
<% response.write jsDecodeURIComponent( "%E6%B5%8B%E8%AF%95" ) %> <script languag ...
- C# HttpWebRequest 绝技 【转】
原文地址:http://www.sufeinet.com/thread-6-1-1.html 在线测试工具http://www.sufeinet.com/thread-3690-1-1.html c# ...
- account for 与led to和result in的区别
account for sth:be the explanation of sth; explain the cause of sth 作某事物的解释; 解释某事物的原因:His illness ac ...
- 【共享单车】—— React后台管理系统开发手记:AntD Table高级表格
前言:以下内容基于React全家桶+AntD实战课程的学习实践过程记录.最终成果github地址:https://github.com/66Web/react-antd-manager,欢迎star. ...
- 2017.7.1 mysql安装与启动(已验证可以使用)
下载地址:http://learning.happymmall.com/ 之前一直用解压版安装,启动mysql服务的时候总是失败,这次用mysql installer安装一遍,终于成功启动. 1.下载 ...