Apollo Scheduler 模块的工作原理如下图所示:
原图引用链接:【架构分析】Apollo CyberRT Framework分析 - Scheduler调度器_HaoBBNuanMM的博客-CSDN博客_cyberrt scheduler
以SchedulerClassic为例,在SchedulerClassic构造函数中会读取调度配置文件信息并将配置文件中的groups条目下信息记录到classic_conf_成员变量内。
在构造函数结尾调用CreateProcessor()函数创建processor_num 个Processor,CreateProcessor()函数代码如下:
void SchedulerClassic::CreateProcessor() {
for (auto& group : classic_conf_.groups()) {
auto& group_name = group.name();
auto proc_num = group.processor_num();
if (task_pool_size_ == 0) {
task_pool_size_ = proc_num;
}
auto& affinity = group.affinity();
auto& processor_policy = group.processor_policy();
auto processor_prio = group.processor_prio();
std::vector<int> cpuset;
ParseCpuset(group.cpuset(), &cpuset);
for (uint32_t i = 0; i < proc_num; i++) {
auto ctx = std::make_shared<ClassicContext>(group_name);
pctxs_.emplace_back(ctx);
auto proc = std::make_shared<Processor>();
proc->BindContext(ctx);
SetSchedAffinity(proc->Thread(), cpuset, affinity, i);
SetSchedPolicy(proc->Thread(), processor_policy, processor_prio,
proc->Tid());
processors_.emplace_back(proc);
}
}
}
Apollo中的Processor实际对应于Linux中的thread,CreateProcessor()首先读取要创建Processor(也就是thread)的优先级及绑定的CPU核的配置信息,然后在for循环中依次创建proc_num个thread并将其与ClassicContext和CPU核的绑定。
绑定完成的后每个thread将不断从ClassicContext中获取可执行的Component,调用Component中的Proc()函数,实现Component运行。
另一方面,在创建Component时,Component初始化过程中会调用SchedulerClassic中DispatchTask()函数将不同的Component分配到ClassicContext中,DispatchTask()函数代码如下:
bool SchedulerClassic::DispatchTask(const std::shared_ptr<CRoutine>& cr) {
// we use multi-key mutex to prevent race condition
// when del && add cr with same crid
MutexWrapper* wrapper = nullptr;
if (!id_map_mutex_.Get(cr->id(), &wrapper)) {
{
std::lock_guard<std::mutex> wl_lg(cr_wl_mtx_);
if (!id_map_mutex_.Get(cr->id(), &wrapper)) {
wrapper = new MutexWrapper();
id_map_mutex_.Set(cr->id(), wrapper);
}
}
}
std::lock_guard<std::mutex> lg(wrapper->Mutex());
{
WriteLockGuard<AtomicRWLock> lk(id_cr_lock_);
if (id_cr_.find(cr->id()) != id_cr_.end()) {
return false;
}
id_cr_[cr->id()] = cr;
}
if (cr_confs_.find(cr->name()) != cr_confs_.end()) {
ClassicTask task = cr_confs_[cr->name()];
cr->set_priority(task.prio());
cr->set_group_name(task.group_name());
} else {
// croutine that not exist in conf
cr->set_group_name(classic_conf_.groups(0).name());
}
if (cr->priority() >= MAX_PRIO) {
AWARN << cr->name() << " prio is greater than MAX_PRIO[ << " << MAX_PRIO
<< "].";
cr->set_priority(MAX_PRIO - 1);
}
// Enqueue task.
{
WriteLockGuard<AtomicRWLock> lk(
ClassicContext::rq_locks_[cr->group_name()].at(cr->priority()));
ClassicContext::cr_group_[cr->group_name()]
.at(cr->priority())
.emplace_back(cr);
}
ClassicContext::Notify(cr->group_name());
return true;
}
上述代码中最核心的部分其实只有一句:
ClassicContext::cr_group_[cr->group_name()]
.at(cr->priority())
.emplace_back(cr);
上述代码实现了根据group_name及Component对应的task(调度配置文件中的tasks条目)的优先级放入到ClassicContext中对应的cr_group_变量内。
从代码上看,整个功能实现比较怪异,直接在一个类中使用的一个类的成员变量,不太符合常规的软件设计原则。另一方面,SchedulerClassic的CreateProcessor()函数是如下创建ClassicContext并绑定到thread的:
for (uint32_t i = 0; i < proc_num; i++) {
auto ctx = std::make_shared<ClassicContext>(group_name);
pctxs_.emplace_back(ctx);
auto proc = std::make_shared<Processor>();
proc->BindContext(ctx);
SetSchedAffinity(proc->Thread(), cpuset, affinity, i);
SetSchedPolicy(proc->Thread(), processor_policy, processor_prio,
proc->Tid());
processors_.emplace_back(proc);
}
从代码上看似乎是为每一个thead创建了一个ClassicContext,如果是这样的话将不太好理解Component是如何选择thread对应的ClassicContext的。
答案就在ClassicContext的类定义及构造函数中:
ClassicContext定义了几个静态成员变量:
alignas(CACHELINE_SIZE) static CR_GROUP cr_group_;
alignas(CACHELINE_SIZE) static RQ_LOCK_GROUP rq_locks_;
alignas(CACHELINE_SIZE) static GRP_WQ_CV cv_wq_;
alignas(CACHELINE_SIZE) static GRP_WQ_MUTEX mtx_wq_;
alignas(CACHELINE_SIZE) static NOTIFY_GRP notify_grp_;
并在构造函数中调用InitGroup()函数实现如下功能:
void ClassicContext::InitGroup(const std::string& group_name) {
multi_pri_rq_ = &cr_group_[group_name];
lq_ = &rq_locks_[group_name];
mtx_wrapper_ = &mtx_wq_[group_name];
cw_ = &cv_wq_[group_name];
notify_grp_[group_name] = 0;
current_grp = group_name;
}
我们可以看出核心问题是cr_group_是静态成员变量,(这点其实从ClassicContext::cr_group_这种使用方法也可推断出来),所以本质上是所有的ClassicContext类对象共享CR_GROUP cr_group_这个静态变量,所以在执行
std::make_shared<ClassicContext>(group_name)
时,只要group_name不变,就可以得到相同的ClassicContext::cr_group_[cr->group_name()],从而使thread绑定到同一个MULTI_PRIO_QUEUE中。
至此,我们可以总结出:Component对应的task根据调度配置文件的group name分配到MULTI_PRIO_QUEUE中;Processor(即thread)根据group name绑定到相同group name的MULTI_PRIO_QUEUE,同时Processor根据调度配置文件中的优先级及内核绑定配置配置线程优先级并绑定到特定内核。