ovs-dpdk:revalidator源码解析
revalidator是做什么的?需要知道哪些东西?
- 通过ovs-vsctl list open_vs可以看到other_config里面有两个变量线程数配置:n-handler-threads和n-revalidator-threads,很明显这是两个线程数量的配置,但这两个线程都是做什么的做什么的?
- 除了设置线程数的配置,还有两个:max-idle和max-revalidator,这两个是做什么的?
- ovs-appctl upcall/show里显示的dump duration时间和上边的有什么关系?
- flow-limit也会说到,这个flow-limit是如何限制的?
答案在文尾。
需要记得的东西
概括:
- 为什么要重验证呢?实际上真正有效的规则是openflow规则,datapath里面存的megaflow只不过是pmd进行upcall之后翻译的openflow规则的缓存,当openflow规则发生改变时,如果修改megaflow缓存呢,megaflow如果判断缓存是否还是正确的呢,这个就是revalidate做的,那么可以用几个revalidator线程去做这种事。
- revalidator检查快速路径中的流缓存(pmd->flow_table),检查流及其action是否还继续有效(max-idle就是超时时间,默认10s)。若继续有效则保留它不做修改。若能对流及其action做部分修改后保持其有效则修改后保留它。若流已完全无效则从快速路径的流缓存中删除流。
- 一是周期性触发,这是通过leader线程每500ms改变一次udpif->dump_seq进行的在revalidate(revalidator)函数中会检查ukey->dump_seq是否等于udpif->dump_seq相等时不验证。因为leader线程会周期性改变udpif->dump_seq,所以流也会被周期性验证每一轮revalidation之后,ukey->dump_seq会被赋值为udpif->dump_seq。
- 另一种是主动通知。当ofproto层改变了流表规则,会调用udpif_revalidate()主动改变udpif->reval_seq。在revalidate_ukey()中会检查ukey->reval_seq是否等于udpif->reval_seq,如果不相等则必须重验证流。reval_seq不相等的另一层含义是我们不能再使用之前创建的ukey->xcache,而必须新创建一个。一般来说,udpif->reval_seq由某些上层的事件触发,它的变化频率没有udpif->dump_seq那么高在revalidate_ukey()中验证完流以后会把ukey->reval_seq 赋值为udpif->reval_seq。
配置生效过程
set config
bridge_run
bridge_init_ofproto // 这个里面很巧妙,使用了一个static bool变量,只会被init一次
ofproto_init // 绑定ofproto_class, ofproto-dpif.c里
ofproto_set_flow_restore_wait // flow_restore_wait,没见过other_config里设置的,都是默认值0
bridge_run__
ofproto_enumerate_types // 绑定dpif_netdev_class
ofproto_type_run
ofproto_class->type_run // 上边绑定的,ofproto-dpif.c里
//在dpif创建前不会走到这,进来判断没有就返回了
if (backer->recv_set_enable)
udpif_set_threads(backer->udpif, n_handlers, n_revalidators);
bridge_reconfigure
// 这四个变量,在dpif层就是从这里拿的
ofproto_set_flow_limit(ofproto_flow_limit)
ofproto_set_max_idle(ofproto_max_idle)
ofproto_set_max_revalidator(ofproto_max_revalidator)
ofproto_set_threads(n_handlers, n_revalidators)
ofproto_create // 进入dpif层
ofproto->ofproto_class->construct // 这个ofproto_class就是上边绑定的,ofproto-dpif.c里
open_dpif_backer
dpif_create_and_open
udpif_create // 这里创建之后,上边的udpif_set_threads才会执行
atomic_init(&udpif->flow_limit, ofproto_flow_limit); // 顺便提一下,flow-limit是这里设置的
backer->recv_set_enable = !ofproto_get_flow_restore_wait(); // 默认为1,之后bridge_run__里判断就会命中
udpif_set_threads(backer->udpif, n_handlers, n_revalidators) // 这里设置了
// ofproto层就单纯的根据other_config设置一下
// 如果没有设置revalidator,根据max(cpu核数,2)和handler线程数来确定设为几。
// handle也类似,代码一看就懂
ofproto_set_threads(int n_handlers_, int n_revalidators_)
{
int threads = MAX(count_cpu_cores(), 2); n_revalidators = MAX(n_revalidators_, 0);
n_handlers = MAX(n_handlers_, 0); if (!n_revalidators) {
n_revalidators = n_handlers
? MAX(threads - (int) n_handlers, 1)
: threads / 4 + 1;
} if (!n_handlers) {
n_handlers = MAX(threads - (int) n_revalidators, 1);
}
} // dpif这里有三个函数重要,加粗了,分为三部分,暂停thread,设置thread数量,开始运行thread
// 但实际上设置和运行都在一个函数里面
void
udpif_set_threads(struct udpif *udpif, size_t n_handlers_,
size_t n_revalidators_)
{
ovs_assert(udpif);
ovs_assert(n_handlers_ && n_revalidators_); if (udpif->n_handlers != n_handlers_
|| udpif->n_revalidators != n_revalidators_) {
udpif_stop_threads(udpif, true);
} if (!udpif->handlers && !udpif->revalidators) {
int error;
// 不执行,因为dpif-netdev.c里面这个函数为NULL
error = dpif_handlers_set(udpif->dpif, n_handlers_);
if (error) {
VLOG_ERR("failed to configure handlers in dpif %s: %s",
dpif_name(udpif->dpif), ovs_strerror(error));
return;
} udpif_start_threads(udpif, n_handlers_, n_revalidators_);
}
} // 在运行线程数的和设置的不同就会执行 udpif_stop_threads:
udpif->n_handlers != 0 || udpif->n_revalidators != 0
latch_set(&udpif->exit_latch); // 一个通讯机制,set后正在运行的handle和revalidator线程,会判断然后退出
//先别管什么rcu,ovs_barrier,因为我也不懂,之后再看
xpthread_join(udpif->handlers[i].thread, NULL);
xpthread_join(udpif->revalidators[i].thread, NULL);
dpif_disable_upcall(udpif->dpif); // 在dpif-netdev里面,把upcall的读写锁加写锁,这样就不会执行upcall了
revalidator_purge(&udpif->revalidators); // 把megaflow清光
latch_poll(&udpif->exit_latch);
// 把参数和指针重置
free(udpif->revalidators); free(udpif->handlers);
udpif->revalidators = NULL; udpif->handlers = NULL;
udpif->n_revalidators = 0; udpif->n_handlers = 0; // start当!udpif->handlers && !udpif->revalidators才会执行
udpif_start_threads:
udpif->n_handlers = n_handlers_;
udpif->n_revalidators = n_revalidators_;
dpif_enable_upcall(udpif->dpif); // 把upcall的读锁释放
// 然后就会ovs_thread_create()对应个数个handle和revalidator
// 相当于这里就启动了
handler线程:dpdk版是个空线程
- 上边最后一部就生成了对应属两个线程:udpif_upcall_handler and udpif_revalidator
- 这两个线程通过udpif->exit_latch感知到是否需要退出
static void *
udpif_upcall_handler(void *arg)
{
struct handler *handler = arg;
struct udpif *udpif = handler->udpif;
// 上边如果stop里面set了就会退出
while (!latch_is_set(&handler->udpif->exit_latch)) {
//recv_upcalls用于接收upcall消息。只有kernel path提供了recv函数dpif_netlink_recv,所以说handler线程对dpdk path不生效。
//recv_upcalls返回值大于0,说明已经处理过upcall消息,防止
//丢失后续的upcall消息,线程不能堵塞,需要再次调用recv_upcalls
//尝试接收upcall消息,所以需要调用poll_immediate_wake将
//timeout_when设置为最小值LLONG_MIN,这样poll_block调用poll函数就能立即返回,继续执行recv_upcalls。
//recv_upcalls返回值小于0,说明没有upcall消息,dpif_recv_wait调用等待upcall消息即可。
if (recv_upcalls(handler)) {
poll_immediate_wake();
} else {
//等待upcall消息
dpif_recv_wait(udpif->dpif, handler->handler_id);
//等待exit消息
latch_wait(&udpif->exit_latch);
}
//堵塞在poll函数上,超时时间timeout_when为最大值,等待事件发生
poll_block();
} return NULL;
} recv_upcalls(struct handler *handler){
n_upcalls = 0;
// UPCALL_MAX_BATCH = 64
while (n_upcalls < UPCALL_MAX_BATCH) {
// 调用udpif->dpif->dpif_class->recv,因为并没有实现recv,这里return的是EAGAIN
// 相当于没有upcall
if (dpif_recv(udpif->dpif, handler->handler_id, dupcall, recv_buf)) {
// free
ofpbuf_uninit(recv_buf);
break;
}
...
}
}
revalidator线程
ovs barrier
void
ovs_barrier_block(struct ovs_barrier *barrier)
{
uint64_t seq = seq_read(barrier->seq);
uint32_t orig; orig = atomic_count_inc(&barrier->count);
if (orig + 1 == barrier->size) {
atomic_count_set(&barrier->count, 0);
/* seq_change() serves as a release barrier against the other threads,
* so the zeroed count is visible to them as they continue. */
seq_change(barrier->seq);
} else {
/* To prevent thread from waking up by other event,
* keeps waiting for the change of 'barrier->seq'. */
while (seq == seq_read(barrier->seq)) {
seq_wait(barrier->seq, seq);
poll_block();
}
}
}
主函数udpif_revalidator
udpif_revalidator(void *arg)
{
/* Used by all revalidators. */
struct revalidator *revalidator = arg;
struct udpif *udpif = revalidator->udpif;
bool leader = revalidator == &udpif->revalidators[0]; /* Used only by the leader. */
long long int start_time = 0;
uint64_t last_reval_seq = 0;
size_t n_flows = 0; revalidator->id = ovsthread_id_self();
for (;;) {
if (leader) {
// recirc_run()回收释放struct recirc_id_node内的struct recirc_state结构
recirc_run();
// 在dpif层(datapath)读取流的总数,udpif->n_flows_timestamp记录了上一次读取操作的
// 时间,udpif->n_flows记录了上一次读取的值,如果当前时间与上一次读取操作的时间间隔
// 不超过100ms,则继续使用上一次读取的值。否则执行一次新的读取操作
n_flows = udpif_get_n_flows(udpif); udpif->pause = latch_is_set(&udpif->pause_latch);
udpif->reval_exit = latch_is_set(&udpif->exit_latch);
start_time = time_msec();
if (!udpif->reval_exit) {
bool terse_dump; terse_dump = udpif_use_ufid(udpif);
// dpif_flow_dump_create()创建一个dump上下文
udpif->dump = dpif_flow_dump_create(udpif->dpif, terse_dump,
NULL);
}
}
/* Wait for the leader to start the flow dump. */
ovs_barrier_block(&udpif->reval_barrier);
if (udpif->pause) {
revalidator_pause(revalidator);
}
// 更改线程数量时候,这里会退出
if (udpif->reval_exit) {
break;
}
// 最重要的地方,进行验证,决定megaflow是否删除,修改
revalidate(revalidator); /* Wait for all flows to have been dumped before we garbage collect. */
ovs_barrier_block(&udpif->reval_barrier);
// 上边的revalidate只能重验证datapath里面有的flow,但是有些被删了,
// 可是ukey还存在,sweep就是清除残余无用的ukey
revalidator_sweep(revalidator); ovs_barrier_block(&udpif->reval_barrier); if (leader) {
unsigned int flow_limit;
long long int duration; atomic_read_relaxed(&udpif->flow_limit, &flow_limit); dpif_flow_dump_destroy(udpif->dump);
// 这里会dump_seq++
seq_change(udpif->dump_seq);
// 默认ofproto_flow_reduce_point=2000ms, ofproto_flow_quater_point=1300ms
// 1. 如果revalidate时间大于2秒,就将flow_limit调小duration/1000倍
// 2. 如果revalidate时间大于1.3秒,就将flow_limit调成3/4
// 3. 如果revalidate时间小于1秒并且flow_limit小于实际上1秒可以revalidate的数量,就+1000
duration = MAX(time_msec() - start_time, 1);
udpif->dump_duration = duration;
if (duration > ofproto_flow_reduce_point) {
flow_limit /= duration / 1000;
} else if (duration > ofproto_flow_quater_point) {
flow_limit = flow_limit * 3 / 4;
} else if (duration < 1000 &&
flow_limit < n_flows * 1000 / duration) {
flow_limit += 1000;
}
flow_limit = MIN(ofproto_flow_limit, MAX(flow_limit, 1000));
atomic_store_relaxed(&udpif->flow_limit, flow_limit); if (duration > 2000) {
VLOG_INFO("Spent an unreasonably long %lldms dumping flows",
duration);
}
// 这里就会用上设置的max_revalidator,比如设置了4秒,其中进行revalidate花费了2秒,就会再睡眠2秒
// 表示4秒进行一次revalidate
poll_timer_wait_until(start_time + MIN(ofproto_max_idle,
ofproto_max_revalidator));
seq_wait(udpif->reval_seq, last_reval_seq);
latch_wait(&udpif->exit_latch);
latch_wait(&udpif->pause_latch);
poll_block(); if (!latch_is_set(&udpif->pause_latch) &&
!latch_is_set(&udpif->exit_latch)) {
long long int now = time_msec();
/* Block again if we are woken up within 5ms of the last start
* time. */
start_time += 5;
// 如果上边的revalidate什么的加一起5ms内搞完,就再block着
if (now < start_time) {
poll_timer_wait_until(start_time);
latch_wait(&udpif->exit_latch);
latch_wait(&udpif->pause_latch);
poll_block();
}
}
}
} return NULL;
}
1. revalidate:重验证
revalidate(struct revalidator *revalidator)
{
uint64_t odp_actions_stub[1024 / 8];
struct ofpbuf odp_actions = OFPBUF_STUB_INITIALIZER(odp_actions_stub); struct udpif *udpif = revalidator->udpif;
struct dpif_flow_dump_thread *dump_thread;
uint64_t dump_seq, reval_seq;
unsigned int flow_limit; dump_seq = seq_read(udpif->dump_seq);
reval_seq = seq_read(udpif->reval_seq);
atomic_read_relaxed(&udpif->flow_limit, &flow_limit);
dump_thread = dpif_flow_dump_thread_create(udpif->dump);
for (;;) {
struct ukey_op ops[REVALIDATE_MAX_BATCH];
int n_ops = 0; struct dpif_flow flows[REVALIDATE_MAX_BATCH];
const struct dpif_flow *f;
int n_dumped; long long int max_idle;
long long int now;
size_t n_dp_flows;
bool kill_them_all;
//批量dump流
n_dumped = dpif_flow_dump_next(dump_thread, flows, ARRAY_SIZE(flows));
if (!n_dumped) {
break;
} now = time_msec();
// 1. megaflow>flow_limit*2就全删
// 2. megaflow大于flow_limit但小于2倍flow_limit,把max-idle改成100ms,加快删除
n_dp_flows = udpif_get_n_flows(udpif);
kill_them_all = n_dp_flows > flow_limit * 2;
max_idle = n_dp_flows > flow_limit ? 100 : ofproto_max_idle; udpif->dpif->current_ms = time_msec();
for (f = flows; f < &flows[n_dumped]; f++) {
// 上一次被命中的时间
long long int used = f->stats.used;
struct recirc_refs recircs = RECIRC_REFS_EMPTY_INITIALIZER;
enum reval_result result;
struct udpif_key *ukey;
bool already_dumped;
int error;
// 对于dump的每一条流,lock它的ukey,如果流还没有ukey就为它分配一个
if (ukey_acquire(udpif, f, &ukey, &error)) {
if (error == EBUSY) {
/* Another thread is processing this flow, so don't bother
* processing it.*/
COVERAGE_INC(upcall_ukey_contention);
} else {
log_unexpected_flow(f, error);
if (error != ENOENT) {
delete_op_init__(udpif, &ops[n_ops++], f);
}
}
continue;
} // 发现ukey和udpif的dump_seq相等,说明被dump过了,别的线程就不会dump了
already_dumped = ukey->dump_seq == dump_seq;
if (already_dumped) {
continue;
} ukey_update_meter_stats(ukey, f);
if (!used) {
used = ukey->created;
}
// 全删或者超过的max-idle就删除
if (kill_them_all || (used && used < now - max_idle)) {
result = UKEY_DELETE;
} else {
result = revalidate_ukey(udpif, ukey, &f->stats, &odp_actions,
reval_seq, &recircs);
}
// 更新ukey的dump_seq
ukey->dump_seq = dump_seq; if (result != UKEY_KEEP) {
/* Takes ownership of 'recircs'. */
reval_op_init(&ops[n_ops++], result, udpif, ukey, &recircs,
&odp_actions);
}
ovs_mutex_unlock(&ukey->mutex);
} if (n_ops) {
/* Push datapath ops but defer ukey deletion to 'sweep' phase. */
push_dp_ops(udpif, ops, n_ops);
}
ovsrcu_quiesce();
}
dpif_flow_dump_thread_destroy(dump_thread);
ofpbuf_uninit(&odp_actions);
}
revalidate_ukey:真正对ukey进行重验证
static enum reval_result
revalidate_ukey(struct udpif *udpif, struct udpif_key *ukey,
const struct dpif_flow_stats *stats,
struct ofpbuf *odp_actions, uint64_t reval_seq,
struct recirc_refs *recircs)
OVS_REQUIRES(ukey->mutex)
{
// 每一轮dump,udpif->reval_seq会加1,每个ukey也有一个ukey->reval_seq
// 表示上一次验证时的序号当两者不同时说明需要对流进行重验证了。
bool need_revalidate = ukey->reval_seq != reval_seq;
enum reval_result result = UKEY_DELETE;
struct dpif_flow_stats push; ofpbuf_clear(odp_actions); push.used = stats->used;
push.tcp_flags = stats->tcp_flags;
push.n_packets = (stats->n_packets > ukey->stats.n_packets
? stats->n_packets - ukey->stats.n_packets
: 0);
push.n_bytes = (stats->n_bytes > ukey->stats.n_bytes
? stats->n_bytes - ukey->stats.n_bytes
: 0); if (need_revalidate) {
// 如果命中这条流的数据包的速率不超过5pps则删除它,如果自上次重验证以来还没有包经过这条流则保留这条流
// 如果自上次重验证以来ukey->xcache还没有超时删除则保留这条流。
if (should_revalidate(udpif, push.n_packets, ukey->stats.used)) {
if (!ukey->xcache) {
ukey->xcache = xlate_cache_new();
} else {
xlate_cache_clear(ukey->xcache);
}
result = revalidate_ukey__(udpif, ukey, push.tcp_flags,
odp_actions, recircs, ukey->xcache);
} /* else delete; too expensive to revalidate */
} else if (!push.n_packets || ukey->xcache
|| !populate_xcache(udpif, ukey, push.tcp_flags)) {
result = UKEY_KEEP;
} /* Stats for deleted flows will be attributed upon flow deletion. Skip. */
if (result != UKEY_DELETE) {
if (ukey->xcache)
xlate_cache_push_meter_stats(ukey->xcache, ukey);
xlate_push_stats(ukey->xcache, &push);
ukey->stats = *stats;
ukey->reval_seq = reval_seq;
} return result;
}
push_dp_ops:根据重验证结果修改dp的megaflow
static void
dpif_netdev_operate(struct dpif *dpif, struct dpif_op **ops, size_t n_ops,
enum dpif_offload_type offload_type OVS_UNUSED)
{
size_t i; for (i = 0; i < n_ops; i++) {
struct dpif_op *op = ops[i]; switch (op->type) {
// 对应的就是revalidate里面的UKEY_MODIFY
case DPIF_OP_FLOW_PUT:
op->error = dpif_netdev_flow_put(dpif, &op->flow_put);
break; // 删除megaflow,对应UKEY_DELETE
case DPIF_OP_FLOW_DEL:
op->error = dpif_netdev_flow_del(dpif, &op->flow_del);
break; //这两个都是pmd自己upcall做的
case DPIF_OP_EXECUTE:
op->error = dpif_netdev_execute(dpif, &op->execute);
break; case DPIF_OP_FLOW_GET:
op->error = dpif_netdev_flow_get(dpif, &op->flow_get);
break;
}
}
}
2.revalidator_sweep 清处多余的ukey
revalidator_sweep__最后会走push_ukey_ops,发现如果datapath里面是删除操作,则把ukey也删掉 push_ukey_ops(struct udpif *udpif, struct umap *umap,
struct ukey_op *ops, size_t n_ops)
{
int i; push_dp_ops(udpif, ops, n_ops);
ovs_mutex_lock(&umap->mutex);
for (i = 0; i < n_ops; i++) {
// 如果datapath里是删除,则把ukey删除
if (ops[i].dop.type == DPIF_OP_FLOW_DEL) {
ukey_delete(umap, ops[i].ukey);
}
}
ovs_mutex_unlock(&umap->mutex);
}
3.更新flow_limit,休眠:leader线程
最开始的问题
- 通过ovs-vsctl list open_vs可以看到other_config里面有两个变量线程数配置:n-handler-threads和n-revalidator-threads,很明显这是两个线程数量的配置,但这两个线程都是做什么的做什么的?
- OVS-DPDK里面handler是个空线程,revalidator才是做重验证的,为的就是维护flow的的相关信息,当openflow规则发生改变,当flow超时等,去更新datapath里面的megaflow缓存。
- 除了设置线程数的配置,还有两个:max-idle和max-revalidator,这两个是做什么的?
- max-idle是flow的超时时间,当megfalow数量超过flow_limit这个值会变成100ms,使其快速删除,默认是10s。
- max-revalidator有俩作用,一个是进行完revalidate的休眠时间:为max-revalidator减去revalidate耗时。另一个是当revalidate的时间小于超过max-revalidator/2,表示耗时不会太多,就对flow进行revalidate。否则直接就删除了。
- ovs-appctl upcall/show里显示的dump duration时间和上边的有什么关系?
- 就是执行完重验证revalidate耗时。
- flow-limit也会说到,这个flow-limit是如何限制的?
- 默认ofproto_flow_reduce_point=2000ms, ofproto_flow_quater_point=1300ms
- 初始化时udpif_create设置为ofproto_flow_limit。社区里的是MIN(ofproto_flow_limit, 10000)
- 1. 如果revalidate时间大于2秒,就将flow_limit调小duration/1000倍
- 2. 如果revalidate时间大于1.3秒,就将flow_limit调成3/4
- 3. 如果revalidate时间小于1秒并且flow_limit小于实际上1秒可以revalidate的数量,就+1000
- 如果当前megaflow > flow_limit*2,则将megaflow全删除
参考:
ovs-dpdk:revalidator源码解析的更多相关文章
- 【原】Android热更新开源项目Tinker源码解析系列之三:so热更新
本系列将从以下三个方面对Tinker进行源码解析: Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Android热更新开源项目Tinker源码解析系列之二:资源文件热更新 A ...
- 【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新
[原]Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Tinker是微信的第一个开源项目,主要用于安卓应用bug的热修复和功能的迭代. Tinker github地址:http ...
- 【原】Android热更新开源项目Tinker源码解析系列之二:资源文件热更新
上一篇文章介绍了Dex文件的热更新流程,本文将会分析Tinker中对资源文件的热更新流程. 同Dex,资源文件的热更新同样包括三个部分:资源补丁生成,资源补丁合成及资源补丁加载. 本系列将从以下三个方 ...
- 多线程爬坑之路-Thread和Runable源码解析之基本方法的运用实例
前面的文章:多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类) 多线程爬坑之路-Thread和Runable源码解析 前面 ...
- jQuery2.x源码解析(缓存篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 缓存是jQuery中的又一核心设计,jQuery ...
- Spring IoC源码解析——Bean的创建和初始化
Spring介绍 Spring(http://spring.io/)是一个轻量级的Java 开发框架,同时也是轻量级的IoC和AOP的容器框架,主要是针对JavaBean的生命周期进行管理的轻量级容器 ...
- jQuery2.x源码解析(构建篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 笔者阅读了园友艾伦 Aaron的系列博客< ...
- jQuery2.x源码解析(设计篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 这一篇笔者主要以设计的角度探索jQuery的源代 ...
- jQuery2.x源码解析(回调篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 通过艾伦的博客,我们能看出,jQuery的pro ...
- HashMap 源码解析
HashMap简介: HashMap在日常的开发中应用的非常之广泛,它是基于Hash表,实现了Map接口,以键值对(key-value)形式进行数据存储,HashMap在数据结构上使用的是数组+链表. ...
随机推荐
- 部署及配置Mycat数据库中间件
Mycat关键特性关键特性支持SQL92标准支持MySQL.Oracle.DB2.SQL Server.PostgreSQL等DB的常见SQL语法遵守Mysql原生协议,跨语言,跨平台,跨数据库的通用 ...
- 攻防世界Web进阶篇——warmup
打开链接,发现是一张滑稽 查看页面源代码,发现文件 于是打开source.php,发现 打开hint.php,根据提示得知flag在ffffllllaaaagggg文件中 回到source.php,检 ...
- 2N2218仿真估算静态工作点
(在找到的2N2218技术手册中没有发现输入输出特性曲线,只能自己估算了) 共射极直流通路电路 #静态工作点表达式 #IBQ = (Vcc - UBEQ)/RB #ICQ = (Vcc - UCEQ) ...
- RGB以及文档流
继承 继承 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF ...
- css3 旋转 八仙桌
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- git拉取远程主支内容,在本地进行修改后,重新提交到新建分支的过程
git拉取远程主支内容,在本地进行修改后,重新提交到新建分支的过程 在本地找一个干净的文件夹 git init 进行初始化 git clone 复制拉取远程的地址 在文件夹中打开,进入复制下来的项 ...
- 在Unity3D中开发的Toon Shader
SwordMaster Toon Shader 特点 此卡通渲染风格的Shader是顶点片元Shader,由本人手动编写完成 此卡通渲染风格的Shader已经在移动设备真机上进行过测试,可以直接应用到 ...
- 基于python-nmap的扫描代码
本次代码只利于人员进行分析,没有啥用,小学期作业,被迫工作. 1 import tkinter 2 from tkinter import * 3 import time 4 import nmap ...
- CentOS 8.x下编译php 7.4、php5.6、php5.3多版本报错处理教程
一.编译安装php 7.4.x 参考CentOS 8.0.1905编译安装Nginx1.16.1+MySQL8.0.18+PHP7.3.10 1.安装编译工具及库文件(使用yum命令安装) yum i ...
- selenium webdriver 无法选中元素,修改元素属性可见
<ul data-v-6529428e="" class="el-dropdown-menu el-popper filter-dropdown el-dropdo ...