当前位置: 首页 > 科技观察

图解:如何绑定进程到CPU

时间:2023-03-14 15:20:38 科技观察

昨天群里有朋友问:如何绑定进程到某个CPU上运行。首先,让我们了解将进程绑定到CPU的好处。Process-boundCPU的好处:在多核CPU结构中,每个核都有自己的L1和L2缓存,而L3缓存是共享的。如果一个进程在核心之间来回切换,每个核心的缓存命中率都会受到影响。相反,如果一个进程无论怎么调度都能一直在一个核上执行,那么它的数据的L1和L2缓存的命中率就可以得到显着的提升。因此,将进程绑定到CPU可以提高CPU缓存的命中率,从而提高性能。进程与CPU的绑定称为:CPU亲和性。设置进程的CPU亲和力介绍完进程绑定CPU的好处,接下来介绍一下Linux系统下如何将进程绑定到CPU(即设置进程的CPU亲和力)。Linux系统提供了一个名为sched_setaffinity的系统调用,可以设置进程的CPU亲和性。我们先看一下sched_setaffinity系统调用的原型:intsched_setaffinity(pid_tpid,size_tcpusetsize,constcpu_set_t*mask);下面介绍一下sched_setaffinity系统调用的各个参数的作用:pid:进程ID,即要绑定CPU的进程ID。cputesize:掩码参数指向的CPU集合的大小。掩码:绑定到进程的一组CPU(因为一个进程可以绑定到多个CPU上运行)。参数mask的类型是cpu_set_t,cpu_set_t是位图,位图的每一位代表一个CPU,如下图所示:例如设置cpu_set_t的第0位为1,表示绑定进程运行在CPU0上,当然我们可以绑定进程在多个CPU上运行。我们用一个例子来介绍如何通过sched_setaffinity系统调用来设置进程的CPU亲和性:#define_GNU_SOURCE#include#include#include#include#include#includeintmain(intargc,char**argv){cpu_set_tcpuset;CPU_ZERO(&cpuset);//初始化CPU集,设置cpuset为空CPU_SET(2,&cpuset);//BindthisprocesstoCPU2//设置进程的CPUaffinityif(sched_setaffinity(0,sizeof(cpuset),&cpuset)==-1){printf("SetCPUaffinityfailed,error:%s\n",strerror(errno));return-1;}return0;}CPU亲和性实现知道了如何设置进程的CPU亲和性,下面我们来分析一下Linux内核是如何实现CPU亲和性功能的。本文使用的Linux内核版本为2.6.23。Linux内核为每个CPU定义了一个structrq类型的runnableprocessqueue,即每个CPU都有一个独立的runnableprocessqueue。一般来说,CPU只会从自己的可运行进程队列中选择一个进程运行。即CPU0只会从属于CPU0的可运行队列中挑选一个进程运行,而永远不会从CPU1的可运行队列中获取。因此,从上面的信息可以分析出,绑定一个进程运行在某个CPU上,只需要将该进程放到它所属的可运行进程队列中即可。我们来分析一下sched_setaffinity系统调用的实现。sched_setaffinity系统调用的调用链如下:会调用migrate_task函数完成进程绑定CPU的工作。下面分析一下migrate_task函数的实现:staticintmigrate_task(structtask_struct*p,intdest_cpu,structmigration_req*req){structrq*rq=task_rq(p);//case1//如果进程不在任何运行队列中//那么just设置进程的cpu字段为dest_cpuif(!p->se.on_rq&&!task_running(rq,p)){set_task_cpu(p,dest_cpu);return0;}//情况2://如果进程已经在某个CPU的runnablequeue//那么进程需要从之前的CPUrunnablequeue迁移到新的CPUrunnablequeue//这个迁移过程由migration_thread内核线程完成//构建进程迁移请求init_completion(&req->done);req->task=p;req->dest_cpudest_cpu=dest_cpu;list_add(&req->list,&rq->migration_queue);return1;}先介绍一下migrate_task函数的参数含义:p:要设置CPU亲和性的进程描述符。dest_cpu:绑定的CPU数量。req:进程迁移请求对象(如下所述)。所以migrate_task函数的作用就是将进程描述符为p的进程绑定到编号为dest_cpu的目标CPU上。migrate_task函数主要分两种情况将进程绑定到某个CPU:情况一:如果进程不在任何CPU的runnable队列中(unrunnable状态),那么只需要设置进程描述符的cpu字段即可它可以是dest_cpu。当一个进程变为runnable时,会根据进程描述符的cpu字段自动放入对应的CPUrunnable队列中。Case2:如果进程已经在某个CPU的runnablequeue中,需要将进程从之前的CPUrunnablequeue迁移到新的CPUrunnablequeue。迁移过程由migration_thread内核线程完成。migrate_task函数只是构造一个进程迁移请求,通知migration_thread内核线程有新的迁移请求需要处理。进程迁移过程由__migrate_task函数完成。我们来看看__migrate_task函数的实现:staticint__migrate_task(structtask_struct*p,intsrc_cpu,intdest_cpu){structrq*rq_dest,*rq_src;intret=0,on_rq;...rq_src=cpu_rq(src_cpu);//原文进程所在的可运行队列rq_dest=cpu_rq(dest_cpu);//进程要放置的目标可运行队列...on_rq=p->se.on_rq;//进程是否在可运行队列中(runnablestate)if(on_rq)deactivate_task(rq_src,p,0);//从原来的runnable队列中删除进程set_task_cpu(p,dest_cpu);if(on_rq){activate_task(rq_dest,p,0);//放入进程进入目标runnable队列...}...returnret;}__migrate_task函数主要完成以下两个任务:从原来的runnable队列中删除进程。将进程放入目标可运行队列。工作流程如下图所示(将进程从CPU0的可运行队列迁移到CPU3的可运行队列):,所以需要将进程从CPU0的可运行队列迁移到CPU3的可运行队列。迁移进程首先从CPU0的可运行队列中移除进程,然后将进程插入到CPU3的可运行队列中。当一个CPU想要运行一个进程时,它首先从它的可运行队列中挑选一个进程并调度该进程在CPU上运行。小结从上面的分析我们可以看出,实际上将一个进程绑定到某个CPU上只是把这个进程放到了CPU的runnable队列中。由于每个CPU都有一个可运行队列,因此可能存在跨CPU的可运行队列负载不平衡。例如,CPU0的可运行队列中的进程比CPU1的可运行队列中的进程多很多,导致CPU0的负载很高,而CPU1的负载很低。当出现上述情况时,需要重新平衡CPU之间的可运行队列。有兴趣的可以阅读源码或者参考相关资料。