作者:月在杏花枝 | 来源:互联网 | 2023-09-08 10:32
对于CPUSET子系统我们主要解释对cpuset.cpus的操作。Read操作根据《cgroup学习(三)——伪文件》可以很容易的跟踪到代码,并解读,所以我们这里就不赘述,直接解释write及attach操作。首先我们看一下write操作的bt(使用systemtap见《使用systemtap获得内核函数的局部变量》):
- sudo sh -c "echo 0-3 > cpuset.cpus"
- 13219 (sh) cpuset_change_cpumask call trace:
- 0xffffffff810c2fc0 :cpuset_change_cpumask+0x0/0x20 [kernel]
- 0xffffffff810bfc6b :cgroup_scan_tasks+0x17b/0x270 [kernel]
- 0xffffffff810c4c2c :cpuset_write_resmask+0x17c/0x350 [kernel]
- 0xffffffff810be14f :cgroup_file_write+0x16f/0x320 [kernel]
- 0xffffffff81177e68 :vfs_write+0xb8/0x1a0 [kernel]
- 0xffffffff81178871 : sys_write+0x51/0x90[kernel]
- 0xffffffff8100b0f2 :system_call_fastpath+0x16/0x1b [kernel]
注:cpuset在attach之前要求先设置cpus及mems的值,如果tasks里没有task的话,也不会调用到cpuset_change_cpumask,会在前面返回。
因为代码被优化的原因所有在cpuset_write_resmask与cgroup_scan_tasks之间的update_cpumask及update_tasks_cpumask函数被优化掉了,其中前者主要完成参数的解析,验证(validate_change这个新的值必须是它的parent的子集,并且它的所有children还必须是新值的子集,以及exclusive的排除),然后才将新的值cpumask_copy(cs->cpus_allowed,trialcs->cpus_allowed);最后才更新它下面的所有进程的cpus_allowed(这里只是更新它这级的进程,并没有更新它下级的cgroup里面的进程,因为这些进程由这个子cgroup自己管理,它们也还是新值的一个子集)update_tasks_cpumask(该函数主要初始化一个cgroup_scanner结构,cgroup用它来遍历cgroup管理的每个进程,并定义对每个进程的test及process操作),然后就到了cgroup_scan_tasks函数,它就是扫描cgroup_scanner(即cgroup下的每个进程,使用cgroup_iter_{start,next,end}三个接口),这里使用了一个大顶堆(key为进程的create_time)来缓存需要更新的进程,这样做的原因是:保证在更新的过程中新创建的进程也会被更新到,所以这里有一个goto语句,直到heap->size=0才跳出,这样就可以减少在fork的代码里加锁。最后就是对heap里面的所有进程进行process处理cpuset_change_cpumask,该函数最终就把cgroup的cpus_allowed复制到task->cpus_allowed,并且判断这个进程是否从旧的CPU运行队列,迁移到新的CPUSET的运行队列(该过程由migrate_task完成)。
注:这里用到了cgroup最重要的几结构之间的转换,所以我们再画一张简易的图来表示一下:
图 cgroup 与task的转换
第一行从cgroup查找CPUSET子系统对应的控制体实现类,subsys[]数组保存了该cgroup所属的所有子系统抽象类,最后通过container_of获得实现类(注:container_of是中task_group内存储了整个cgroup_subsys_state内容,而不是指向cgroup_subsys_state的指针);第二行是从cgroup控制体的CPUSET子系统实现类到它所属的cgroup再到该cgroup所管理的所有tasks,这里cgroup_iter->cg_link则保存cgroup的css_set list,而cgroup_iter->task则分别保存每次css_set的task list,即task会因为遍历的css_set而变化。(这些结构关系见《cgroup学习(二)——cgroup框架结构》)
可以看出整个wirte过程,最终的目的就是在于更新该cgroup下的每个进程的cpus_allowed,并对已在运行队列里或正在运行的进程进行迁移。下面我们再来看一下cpuset的attach过程:
通过《cgroup学习(三)——伪文件》表格的cpuset_subsys全局变量,我们可以找到CPUSET子系统的attach函数为:cpuset_attach,可以想象代码应该就是把新的cgroup的cpu_allowed赋到将要attach的task的cpu_allowed,然后该迁移的迁移,不过如果你看代码的话会发现其实不是这样的,多了一个guarantee_online_cpus函数,一开始百想不得其解,后来查了一下online cpu的概念才明白,原来linux可以把物理cpu直接禁止掉(修改/sys/devices/system/cpu下的所有core目录下的online文件,这也就是CPU的热插拔),所以这个函数的目的就是排除掉所有offline的cpu后的cpu_allowed。有了cpu_allowed进程在被创建时wake_up_new_task(继承自父进程的cpu_allowed)或者被唤醒时try_to_wake_up,都会通过select_task_rq(该函数后面调用CFS调度器里的select_task_rq_fair来选择cpu_allowed里的某一个cpu,可能是load最低的)来确定它应该被置于哪个CPU的运行队列及运行,一个进程在某一时刻只能存在于一个CPU的运行队列里。