block:cfq 学习02
From: https://blog.csdn.net/vanbreaker/article/details/8308766
前文介绍了CFQ调度器的一些概念和结构之间的关系,这里再结合实际的代码,来分析CFQ的工作流程。CFQ调度器的定义如下:
static struct elevator_type iosched_cfq = {
.ops = {
.elevator_merge_fn = cfq_merge,
.elevator_merged_fn = cfq_merged_request,
.elevator_merge_req_fn = cfq_merged_requests,
.elevator_allow_merge_fn = cfq_allow_merge,
.elevator_dispatch_fn = cfq_dispatch_requests,
.elevator_add_req_fn = cfq_insert_request,
.elevator_activate_req_fn = cfq_activate_request,
.elevator_deactivate_req_fn = cfq_deactivate_request,
.elevator_queue_empty_fn = cfq_queue_empty,
.elevator_completed_req_fn = cfq_completed_request,
.elevator_former_req_fn = elv_rb_former_request,
.elevator_latter_req_fn = elv_rb_latter_request,
.elevator_set_req_fn = cfq_set_request,
.elevator_put_req_fn = cfq_put_request,
.elevator_may_queue_fn = cfq_may_queue,
.elevator_init_fn = cfq_init_queue,
.elevator_exit_fn = cfq_exit_queue,
.trim = cfq_free_io_context,
},
.elevator_attrs = cfq_attrs,
.elevator_name = "cfq",
.elevator_owner = THIS_MODULE,
};
可以看到CFQ调度器涉及到的操作函数还是比较多的,这里我只打算选一些和提交bio以及request相关的函数进行分析。在提交bio的时候,如果在通用层寻找可以合并bio的途径失败,要通过cfq_merge()来判断是否能够将bio插入到某个request的bio链表首部
static struct request *
cfq_find_rq_fmerge(struct cfq_data *cfqd, struct bio *bio)
{
struct task_struct *tsk = current;
struct cfq_io_context *cic;
struct cfq_queue *cfqq;
//在进程的io_context中,找到进程特定于块设备的cfq_io_context
cic = cfq_cic_lookup(cfqd, tsk->io_context);
if (!cic)
return NULL;
//根据同步还是异步,确定cfq_queue
cfqq = cic_to_cfqq(cic, cfq_bio_sync(bio));
if (cfqq) {
sector_t sector = bio->bi_sector + bio_sectors(bio);//得到末尾扇区号
//从cfq_queue的红黑树中查找对应的节点
return elv_rb_find(&cfqq->sort_list, sector);
}
return NULL;
}
cfq_find_rq_fmerge()进行实际的搜索工作,要确定bio的归属request,必须先确定进程的通信对象是谁(因为一个进程有可能和多个块设备通信),也就是要找到进程对应的cfq_io_context结构,其中包含了进程的同步请求队列和异步请求队列的地址,只要找到了相应的cfq_io_context,就可以通过bio的同异步性确定对应的cfq_queue了,最后再判断对应的cfq_queue中是否存在可以容纳bio的request。推导cfq_io_context的关键在于以块设备CFQ调度器的描述结构cfq_data的地址为关键值,在进程的io_context的基数树中进行搜索
static struct request *
cfq_find_rq_fmerge(struct cfq_data *cfqd, struct bio *bio)
{
struct task_struct *tsk = current;
struct cfq_io_context *cic;
struct cfq_queue *cfqq;
//在进程的io_context中,找到进程特定于块设备的cfq_io_context
cic = cfq_cic_lookup(cfqd, tsk->io_context);
if (!cic)
return NULL;
//根据同步还是异步,确定cfq_queue
cfqq = cic_to_cfqq(cic, cfq_bio_sync(bio));
if (cfqq) {
sector_t sector = bio->bi_sector + bio_sectors(bio);//得到末尾扇区号
//从cfq_queue的红黑树中查找对应的节点
return elv_rb_find(&cfqq->sort_list, sector);
}
return NULL;
}
通过基数树寻找对应设备的cfq_io_context:
static struct cfq_io_context *
cfq_cic_lookup(struct cfq_data *cfqd, struct io_context *ioc)
{
struct cfq_io_context *cic;
unsigned long flags;
void *k;
if (unlikely(!ioc))
return NULL;
rcu_read_lock();
/*
* we maintain a last-hit cache, to avoid browsing over the tree
*/
//由于进程很有可能连续访问同一块设备,因此先将cic中的关键值直接与cfqd比较
cic = rcu_dereference(ioc->ioc_data);
if (cic && cic->key == cfqd) {
rcu_read_unlock();
return cic;
}
do {//在进程io_context的基数树中寻找对应访问的块设备的cfq_data结构
cic = radix_tree_lookup(&ioc->radix_root, (unsigned long) cfqd);
rcu_read_unlock();
if (!cic)
break;
/* ->key must be copied to avoid race with cfq_exit_queue() */
k = cic->key;
if (unlikely(!k)) {
cfq_drop_dead_cic(cfqd, ioc, cic);
rcu_read_lock();
continue;
}
//保存cic到ioc->ioc_data
spin_lock_irqsave(&ioc->lock, flags);
rcu_assign_pointer(ioc->ioc_data, cic);
spin_unlock_irqrestore(&ioc->lock, flags);
break;
} while (1);
return cic;
}
在通用层__make_request()函数中,如果bio找不到接收对象,那么就要重新创建一个request来接收它。分配一个request必须进行初始化,需要调用cfq_set_request()函数。和前面的过程比较类似的是,request要先找到接收它的cfq_queue,然后这里出现了一个问题,如果这个request是进程的第一个同步请求或者异步请求,那么就要分配新的cfq_queue来接收request。同步请求和异步请求的处理情况有点区别,因为进程独自拥有自己的同步请求队列,而进程的异步请求都是共享cfq_data中的异步请求队列的,所以只有当request是同步请求时才需要进行cfq_queue的分配
static int
cfq_set_request(struct request_queue *q, struct request *rq, gfp_t gfp_mask)
{
struct cfq_data *cfqd = q->elevator->elevator_data;
struct cfq_io_context *cic;
const int rw = rq_data_dir(rq);
const bool is_sync = rq_is_sync(rq);
struct cfq_queue *cfqq;
unsigned long flags;
might_sleep_if(gfp_mask & __GFP_WAIT);
//获取进程特定于块设备的cfq_io_context结构
cic = cfq_get_io_context(cfqd, gfp_mask);
spin_lock_irqsave(q->queue_lock, flags);
if (!cic)
goto queue_fail;
new_queue:
cfqq = cic_to_cfqq(cic, is_sync);//根据同异步情况,获取进程中对应的cfq_queue
if (!cfqq || cfqq == &cfqd->oom_cfqq) {//如果还没有相应的cfqq则进行分配
cfqq = cfq_get_queue(cfqd, is_sync, cic->ioc, gfp_mask);//分配cfq_queue
cic_set_cfqq(cic, cfqq, is_sync);//设置cic->cfqq[is_sync] = cfqq
} else {
/*
* If the queue was seeky for too long, break it apart.
*/
if (cfq_cfqq_coop(cfqq) && should_split_cfqq(cfqq)) {
cfq_log_cfqq(cfqd, cfqq, "breaking apart cfqq");
cfqq = split_cfqq(cic, cfqq);
if (!cfqq)
goto new_queue;
}
/*
* Check to see if this queue is scheduled to merge with
* another, closely cooperating queue. The merging of
* queues happens here as it must be done in process context.
* The reference on new_cfqq was taken in merge_cfqqs.
*/
if (cfqq->new_cfqq)
cfqq = cfq_merge_cfqqs(cfqd, cic, cfqq);
}
cfqq->allocated[rw]++;
atomic_inc(&cfqq->ref);
spin_unlock_irqrestore(q->queue_lock, flags);
rq->elevator_private = cic;//保存cfq_io_context到request
rq->elevator_private2 = cfqq;//保存cfq_queue到request
return 0;
queue_fail:
if (cic)
put_io_context(cic->ioc);
cfq_schedule_dispatch(cfqd);
spin_unlock_irqrestore(q->queue_lock, flags);
cfq_log(cfqd, "set_request fail");
return 1;
}
当一个request创建初始化,就要将其插入到相应的队列中,cfq_insert_request()函数完成这个功能,和deadline调度器相似,request会被放入两个队列里,一个是按照起始扇区号排列的红黑树(sort_list),一个是按响应期限排列的链表(fifo)
static void cfq_insert_request(struct request_queue *q, struct request *rq)
{
struct cfq_data *cfqd = q->elevator->elevator_data;
struct cfq_queue *cfqq = RQ_CFQQ(rq);
cfq_log_cfqq(cfqd, cfqq, "insert_request");
cfq_init_prio_data(cfqq, RQ_CIC(rq)->ioc);//根据ioc设定cfq_queue的优先级类和优先级
cfq_add_rq_rb(rq);//将rq添加到cfq_queue的红黑树
//设置期限值
rq_set_fifo_time(rq, jiffies + cfqd->cfq_fifo_expire[rq_is_sync(rq)]);
list_add_tail(&rq->queuelist, &cfqq->fifo);//添加至fifo
cfq_rq_enqueued(cfqd, cfqq, rq);
}
当一个request纳入一个新的bio后,要考虑能否和其他request进行合并,假如是将bio插入到链表的末尾,那么就要在sort_list中获取request后面的一个request(通过函数elv_rb_latter_request()来获取),然后判断前者的结束扇区和前者的起始扇区是否一样,这些工作都是再通用层代码中完成的,当两个request通过了通用层的审核和,并完成合并操作后,将调用cfq_merged_requests进行一些针对队列的额外工作
tatic void
cfq_merged_requests(struct request_queue *q, struct request *rq,
struct request *next)
{
/*
* reposition in fifo if next is older than rq
*/
if (!list_empty(&rq->queuelist) && !list_empty(&next->queuelist) &&
time_before(rq_fifo_time(next), rq_fifo_time(rq))) {
list_move(&rq->queuelist, &next->queuelist);
rq_set_fifo_time(rq, rq_fifo_time(next));
}
cfq_remove_request(next);
}
这里主要是考察两个request的期限,因为合并时都是将后者合并入前者,因此当后者的期限时间小于前者时,要进行相应的调整,这一点和deadline调度器是一样的。
elevator_dispatch_fn
最后分析的依然是每个调度器最重要的函数--elevator_dispatch_fn
CFQ调度器在发送request到底层块设备时的流程大致如下:
1.选择一个cfq_queue
2.从cfq_queue中选择一个request进行发送
选择cfq_queue的思路如下:
1.如果当前的cfq_queue的时间片还没用完,则继续当前的cfq_queue
2.如果当前的cfq_queue的时间片消耗完了,则优先在对应的prio_tree中选择一个cfq_queue,该cfq_queue的第一个访问扇区与整个调度器最后处理的扇区之间的差值必须小于一个阈值,如果OK的话就选择这个cfq_queue
3.如果找不到这样的cfq_queue,再从service_tree中调度其他的cfq_queue
static int cfq_dispatch_requests(struct request_queue *q, int force)
{
struct cfq_data *cfqd = q->elevator->elevator_data;
struct cfq_queue *cfqq;
if (!cfqd->busy_queues)
return 0;
if (unlikely(force))
return cfq_forced_dispatch(cfqd);
//先要选择一个队列
cfqq = cfq_select_queue(cfqd);
if (!cfqq)
return 0;
/*
* Dispatch a request from this cfqq, if it is allowed
*/
//从选择的cfq_queue中选择request进行分派
if (!cfq_dispatch_request(cfqd, cfqq))
return 0;
cfqq->slice_dispatch++;
cfq_clear_cfqq_must_dispatch(cfqq);
/*
* expire an async queue immediately if it has used up its slice. idle
* queue always expire after 1 dispatch round.
*/
/*如果service_tree中有其他队列等待调度,而刚刚发送请求的
队列是异步队列并且发送数已经超过了时间片的内最大请求数,
则将异步队列挂起,如果是idle队列则在发送一个请求后直接将队列挂起
*/
if (cfqd->busy_queues > 1 && ((!cfq_cfqq_sync(cfqq) &&
cfqq->slice_dispatch >= cfq_prio_to_maxrq(cfqd, cfqq)) ||
cfq_class_idle(cfqq))) {
cfqq->slice_end = jiffies + 1;
cfq_slice_expired(cfqd, 0);
}
cfq_log_cfqq(cfqd, cfqq, "dispatched a request");
return 1;
}
cfq_select_queue()用来选择一个队列,active_queue为当前运行的队列
static struct cfq_queue *cfq_select_queue(struct cfq_data *cfqd)
{
struct cfq_queue *cfqq, *new_cfqq = NULL;
/*先检查active_queue*/
cfqq = cfqd->active_queue;
if (!cfqq)//没有指定active_queue则跳转到new_queue去选择新的队列
goto new_queue;
/*
* The active queue has run out of time, expire it and select new.
*/
//指定了active_queue,这里检查该队列的时间片是否已经过去
if (cfq_slice_used(cfqq) && !cfq_cfqq_must_dispatch(cfqq))
goto expire;
/*
* The active queue has requests and isn't expired, allow it to
* dispatch.
*/
/*走到这里表示时间片尚在,这里检查cfq_queue的sort_list是否为空*/
if (!RB_EMPTY_ROOT(&cfqq->sort_list))
goto keep_queue;
/*
* If another queue has a request waiting within our mean seek
* distance, let it run. The expire code will check for close
* cooperators and put the close queue at the front of the service
* tree. If possible, merge the expiring queue with the new cfqq.
*/
//走到这里表示active_queue内已经没有请求了,因此要找一个最适合的cfq_queue
new_cfqq = cfq_close_cooperator(cfqd, cfqq);
if (new_cfqq) {
if (!cfqq->new_cfqq)
cfq_setup_merge(cfqq, new_cfqq);
goto expire;
}
/*
* No requests pending. If the active queue still has requests in
* flight or is idling for a new request, allow either of these
* conditions to happen (or time out) before selecting a new queue.
*/
if (timer_pending(&cfqd->idle_slice_timer) ||
(cfqq->dispatched && cfq_cfqq_idle_window(cfqq))) {
cfqq = NULL;
goto keep_queue;
}
expire:
cfq_slice_expired(cfqd, 0);//将时间片消耗完的active_queue重新插入service_tree
new_queue:
cfqq = cfq_set_active_queue(cfqd, new_cfqq);//设定new_cfqq为新的active_tree接收时间片
keep_queue:
return cfqq;
}
cfq_close_cooperator查找一个在扇区地址上满足要求的队列,这个操作只对同步请求队列有效
static struct cfq_queue *cfq_close_cooperator(struct cfq_data *cfqd,
struct cfq_queue *cur_cfqq)
{
struct cfq_queue *cfqq;
if (!cfq_cfqq_sync(cur_cfqq))
return NULL;
if (CFQQ_SEEKY(cur_cfqq))
return NULL;
/*
* We should notice if some of the queues are cooperating, eg
* working closely on the same area of the disk. In that case,
* we can group them together and don't waste time idling.
*/
//根据扇区的差值,寻找一个最接近的节点
cfqq = cfqq_close(cfqd, cur_cfqq);
if (!cfqq)
return NULL;
/*
* It only makes sense to merge sync queues.
*/
if (!cfq_cfqq_sync(cfqq))
return NULL;
if (CFQQ_SEEKY(cfqq))
return NULL;
return cfqq;
}
cfqq_close执行实际的工作
static struct cfq_queue *cfqq_close(struct cfq_data *cfqd,
struct cfq_queue *cur_cfqq)
{
struct rb_root *root = &cfqd->prio_trees[cur_cfqq->org_ioprio];
struct rb_node *parent, *node;
struct cfq_queue *__cfqq;
sector_t sector = cfqd->last_position;//这里区最后访问的扇区
if (RB_EMPTY_ROOT(root))
return NULL;
/*
* First, if we find a request starting at the end of the last
* request, choose it.
*/
//首先试图在同优先级的红黑树中寻找与最近访问的扇区相接的另一个cfq_queue
__cfqq = cfq_prio_tree_lookup(cfqd, root, sector, &parent, NULL);
if (__cfqq)//如果找到了的话则选定该cfq_queue进行调度
return __cfqq;
/*
* If the exact sector wasn't found, the parent of the NULL leaf
* will contain the closest sector.
*/
//parent保存了最接近的节点的父节点,如果没找到相邻的cfq_queue,获取父节点对应的cfq_queue
__cfqq = rb_entry(parent, struct cfq_queue, p_node);
/*如果__cfqq的下一个派送请求(next_rq指定)的扇区地址和调度器最后派送的请求的结束扇区之间的间隙满足要求,
则选定该cfq_queue*/
if (cfq_rq_close(cfqd, cur_cfqq, __cfqq->next_rq))
return __cfqq;
/* 前面都不成功,则根据next_rq和sector的大小关系,
来选择最接近的比__cfqq的起始扇区大的节点或者小的节点*/
if (blk_rq_pos(__cfqq->next_rq) < sector)
node = rb_next(&__cfqq->p_node);
else
node = rb_prev(&__cfqq->p_node);
if (!node)
return NULL;
//这里再把选择的节点进行一次间隙的判断,如果间隙小于阈值,则选择该节点,否则返回NULL
__cfqq = rb_entry(node, struct cfq_queue, p_node);
if (cfq_rq_close(cfqd, cur_cfqq, __cfqq->next_rq))
return __cfqq;
return NULL;
}
如果找到了这样的队列,则在cfq_set_active_queue()函数中设定该队列为运行队列,否则就从service_tree中调度时间点最邻近的队列
static struct cfq_queue *cfq_set_active_queue(struct cfq_data *cfqd,
struct cfq_queue *cfqq)
{
if (!cfqq)//假如没指定cfqq,则从service_tree中最前的节点
cfqq = cfq_get_next_queue(cfqd);
__cfq_set_active_queue(cfqd, cfqq);//设定active_queue为cfqq
return cfqq;
}
队列选择完毕,下面要通过cfq_dispatch_request()函数在队列中选择合适的request进行发送
static bool cfq_dispatch_request(struct cfq_data *cfqd, struct cfq_queue *cfqq)
{
struct request *rq;
BUG_ON(RB_EMPTY_ROOT(&cfqq->sort_list));
//先判断是否确定发放cfqq里的请求
if (!cfq_may_dispatch(cfqd, cfqq))
return false;
/*
* follow expired path, else get first next available
*/
/*这里先检查fifo中的请求,如果fifo为空或者fifo中第一个请求的期限还没到,则不会获取到request*/
rq = cfq_check_fifo(cfqq);
if (!rq)//如果fifo中获取rq失败,则考虑备选的next_rq,next_rq总是从扇区的连续性上考虑的
rq = cfqq->next_rq;
/*
* insert request into driver dispatch list
*/
cfq_dispatch_insert(cfqd->queue, rq);//将rq插入到设备的请求队列
if (!cfqd->active_cic) {
struct cfq_io_context *cic = RQ_CIC(rq);
atomic_long_inc(&cic->ioc->refcount);
cfqd->active_cic = cic;
}
return true;
}
在发送具体的request之前,先要确定是否适合为该队列发放请求,主要是判断队列在时间片内发送的请求数有没有超额
static bool cfq_may_dispatch(struct cfq_data *cfqd, struct cfq_queue *cfqq)
{
unsigned int max_dispatch;
/*
* Drain async requests before we start sync IO
*/
//如果cfqq可以被idle并且设备有异步请求处理,则不进行新的同步请求的发放
if (cfq_cfqq_idle_window(cfqq) && cfqd->rq_in_driver[BLK_RW_ASYNC])
return false;
/*
* If this is an async queue and we have sync IO in flight, let it wait
*/
//如果发放的是异步请求并且request_queue中还有同步请求在等待提交,则不方法
if (cfqd->sync_flight && !cfq_cfqq_sync(cfqq))
return false;
//默认max_dispatch为预定的cfq_quantum,也就是说cfqq在时间片内最多dispatch
//cfq_quantum个请求
max_dispatch = cfqd->cfq_quantum;
//如果cfqq的优先级类为idle,则在时间片内只能dispatch一个请求
if (cfq_class_idle(cfqq))
max_dispatch = 1;
/*
* Does this cfqq already have too much IO in flight?
*/
//如果队列发放下去的请求数超额
if (cfqq->dispatched >= max_dispatch) {
/*
* idle queue must always only have a single IO in flight
*/
if (cfq_class_idle(cfqq))//如果是idle优先级,则不能进行发放
return false;
/*
* We have other queues, don't allow more IO from this one
*/
if (cfqd->busy_queues > 1)//如果有其他的队列在等待发放请求,则不进行发放
return false;
/*
* Sole queue user, allow bigger slice
*/
//如果service_tree中只有该队列,且优先级高于idle,则扩宽max_dispatch的限制
max_dispatch *= 4;
}
/*
* Async queues must wait a bit before being allowed dispatch.
* We also ramp up the dispatch depth gradually for async IO,
* based on the last sync IO we serviced
*/
/*下面根据最后一次发送的同步请求和现在的时间间隔以及同步请求时间片的值,计算出
depth,根据depth重置max_dispatch,这里的意图应该是在从同步请求的发送转为
异步请求的发送时,延迟一下异步请求的发送*/
if (!cfq_cfqq_sync(cfqq) && cfqd->cfq_latency) {
unsigned long last_sync = jiffies - cfqd->last_end_sync_rq;
unsigned int depth;
depth = last_sync / cfqd->cfq_slice[1];
if (!depth && !cfqq->dispatched)
depth = 1;
if (depth < max_dispatch)
max_dispatch = depth;
}
/*
* If we're below the current max, allow a dispatch
*/
return cfqq->dispatched < max_dispatch;
}
通过检查后,则要正式开始选择request,选择的原则是:如果fifo中的第一个请求期限已到,则选择该请求,否则选择next_rq指针指定的请求,该指针指向的请求总是从物理扇区的连续性上考虑的。最后调用cfq_dispatch_insert()函数将request插入到request_queue中去,等待块设备的响应
static void cfq_dispatch_insert(struct request_queue *q, struct request *rq)
{
struct cfq_data *cfqd = q->elevator->elevator_data;
struct cfq_queue *cfqq = RQ_CFQQ(rq);
cfq_log_cfqq(cfqd, cfqq, "dispatch_insert");
cfqq->next_rq = cfq_find_next_rq(cfqd, cfqq, rq);//选择一个扇区间隙最近的请求赋给next_rq作为下一个请求的备选
cfq_remove_request(rq);//将rq从sort_list和fifo中移除
cfqq->dispatched++;
elv_dispatch_sort(q, rq);//将rq分派到request queue的请求队列等候响应
if (cfq_cfqq_sync(cfqq))
cfqd->sync_flight++;
}
block:cfq 学习02的更多相关文章
- block: cfq 学习01
CFQ,即Completely Fair Queueing绝对公平调度器,力图为竞争块设备使用权的所有进程分配一个等同的时间片,在调度器分配给进程的时间片内,进程可以将其读写请求发送给底层块设备,当进 ...
- JavaScript学习02 基础语法
JavaScript学习02 基础语法 JavaScript中很多基础内容和Java中大体上基本一样,所以不需要再单独重复讲了,包括: 各种算术运算符.比较运算符.逻辑运算符: if else语句.s ...
- Java虚拟机JVM学习02 类的加载概述
Java虚拟机JVM学习02 类的加载概述 类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对 ...
- Python学习02 列表 List
Python学习02 列表 List Python列表 List Python中的列表(List)用逗号分隔,方括号包围(comma-separated values (items) between ...
- Android Testing学习02 HelloTesting 项目建立与执行
Android Testing学习02 HelloTesting 项目建立与执行 Android测试,分为待测试的项目和测试项目,这两个项目会生成两个独立的apk,但是内部,它们会共享同一个进程. 下 ...
- Java学习02
Java学习02 1.导入内部的包 一.在包的下面加入下面一句话: import java.util.Scanner; 二.在类中 Scanner input=new Sanner(Sy ...
- ThinkPhp学习02
原文:ThinkPhp学习02 一.什么是MVC M -Model 编写model类 对数据进行操作 V -View 编写html文件,页面呈现 C -Controll ...
- JVM学习02:GC垃圾回收和内存分配
JVM学习02:GC垃圾回收和内存分配 写在前面:本系列分享主要参考资料是 周志明老师的<深入理解Java虚拟机>第二版. GC垃圾回收和内存分配知识要点Xmind梳理 案例分析1-(G ...
- 2018.12.22 Spring学习02
Spring学习02 1.使用注解配置Spring 1.1 为主配置文件引入新的命名空间(约束) 添加约束文件xxx-xxx-context.xml 添加到主配置文件中 选择刚才的context.xm ...
随机推荐
- Python常用数据处理函数
1.基本统计特征函数 方法名 函数功能 所属库 使用格式 sum() 计算数据样本综合(按列计算) Pandas D.sum() mean() 计算数据样本算数平均数 Pandas D.mean() ...
- POJ3254 状压dp
Corn ...
- 【bzoj2282】[Sdoi2011]消防
两次bfs可得直径,答案一定不会小于所有点到直径的距离最大值,只要把直径上的边权设为0,任选直径上一点bfs可得将最大值作为二分下界,二分直径左右端点的舍弃部分 #include<algorit ...
- HTTP要点概述:六,HTTP报文
一,HTTP报文: 用于HTTP交互的信息称为HTTP报文.请求端(客户端)的HTTP报文叫做请求报文,响应端(服务器)的叫做响应报文.HTTP报文本身是由多行(用CR+LF换行)数据构成的字符串文本 ...
- Analyzing with SonarScanner for MSBuild
https://docs.sonarqube.org/display/SCAN/Analyzing+with+SonarQube+Scanner+for+MSBuild Features The So ...
- 埃森哲杯第十六届上海大学程序设计联赛春季赛暨上海高校金马五校赛 C序列变换
链接:https://www.nowcoder.com/acm/contest/91/C来源:牛客网没有账号的同学这样注册,支持博主 题目描述 给定两个长度为n的序列,ai, bi(1<=i&l ...
- POJ2352 star
传送门 这道题有个非常好听的名字,求二维偏序! 听起来似乎很高端,但就是让求满足对于每个i,xi < xj && yi < yj的个数. 这道题特别良心,给的顺序都是y递增 ...
- Lexer and parser generators (ocamllex, ocamlyacc)
Chapter 12 Lexer and parser generators (ocamllex, ocamlyacc) This chapter describes two program gene ...
- the odb manual
http://www.codesynthesis.com/products/odb/doc/manual.xhtml#18.4
- 2017 Pycharm激活码
BIG3CLIK6F-eyJsaWNlbnNlSWQiOiJCSUczQ0xJSzZGIiwibGljZW5zZWVOYW1lIjoibGFuIHl1IiwiYXNzaWduZWVOYW1lIjoiI ...