分类: LINUX
2015-09-01 09:28:45
简单地说,CPU 亲和性(affinity) 就是进程要在某个给定的 CPU 上尽量长时间地运行而不被迁移到其他处理器的倾向性。Linux 内核进程调度器天生就具有被称为 软 CPU 亲和性(affinity) 的特性,这意味着进程通常不会在处理器之间频繁迁移。这种状态正是我们希望的,因为进程迁移的频率小就意味着产生的负载小。
2.6 版本的 Linux 内核还包含了一种机制,它让开发人员可以编程实现 硬 CPU 亲和性(affinity)。这意味着应用程序可以显式地指定进程在哪个(或哪些)处理器上运行。
在 Linux 内核中,所有的进程都有一个相关的数据结构,称为 task_struct
。这个结构非常重要,原因有很多;其中与 亲和性(affinity)相关度最高的是 cpus_allowed
位掩码。这个位掩码由 n 位组成,与系统中的 n 个逻辑处理器一一对应。
具有 4 个物理 CPU 的系统可以有 4 位。如果这些 CPU 都启用了超线程,那么这个系统就有一个 8 位的位掩码。
如果为给定的进程设置了给定的位,那么这个进程就可以在相关的 CPU 上运行。因此,如果一个进程可以在任何 CPU 上运行,并且能够根据需要在处理器之间进行迁移,那么位掩码就全是 1。实际上,这就是 Linux 中进程的缺省状态。
Linux 内核 API 提供了一些方法,让用户可以修改位掩码或查看当前的位掩码:
sched_set_affinity()
(用来修改位掩码)sched_get_affinity()
(用来查看当前的位掩码)
注意,cpu_affinity
会被传递给子线程,因此应该适当地调用 sched_set_affinity
。
通常 Linux 内核都可以很好地对进程进行调度,在应该运行的地方运行进程(这就是说,在可用的处理器上运行并获得很好的整体性能)。内核包含了一些用来检测 CPU 之间任务负载迁移的算法,可以启用进程迁移来降低繁忙的处理器的压力。
一般情况下,在应用程序中只需使用缺省的调度器行为。然而,您可能会希望修改这些缺省行为以实现性能的优化。让我们来看一下使用硬亲和性(affinity) 的 3 个原因。
基于大量计算的情形通常出现在科学和理论计算中,但是通用领域的计算也可能出现这种情况。一个常见的标志是您发现自己的应用程序要在多处理器的机器上花费大量的计算时间。
测试复杂软件是我们对内核的亲和性(affinity)技术感兴趣的另外一个原因。考虑一个需要进行线性可伸缩性测试的应用程序。有些产品声明可以在 使用更多硬件 时执行得更好。
我们不用购买多台机器(为每种处理器配置都购买一台机器),而是可以:
如果应用程序随着 CPU 的增加可以线性地伸缩,那么每秒事务数和 CPU 个数之间应该会是线性的关系(例如斜线图 —— 请参阅下一节的内容)。这样建模可以确定应用程序是否可以有效地使用底层硬件。
Amdahl 法则说明这种加速比在现实中可能并不会发生,但是可以非常接近于该值。对于通常情况来说,我们可以推论出每个程序都有一些串行的组件。随着问题集不断变大,串行组件最终会在优化解决方案时间方面达到一个上限。
Amdahl 法则在希望保持高 CPU 缓存命中率时尤其重要。如果一个给定的进程迁移到其他地方去了,那么它就失去了利用 CPU 缓存的优势。实际上,如果正在使用的 CPU 需要为自己缓存一些特殊的数据,那么所有其他 CPU 都会使这些数据在自己的缓存中失效。
因此,如果有多个线程都需要相同的数据,那么将这些线程绑定到一个特定的 CPU 上是非常有意义的,这样就确保它们可以访问相同的缓存数据(或者至少可以提高缓存的命中率)。否则,这些线程可能会在不同的 CPU 上执行,这样会频繁地使其他缓存项失效。
我们对 CPU 亲和性(affinity)感兴趣的最后一个原因是实时(对时间敏感的)进程。例如,您可能会希望使用硬亲和性(affinity)来指定一个 8 路主机上的某个处理器,而同时允许其他 7 个处理器处理所有普通的系统调度。这种做法确保长时间运行、对时间敏感的应用程序可以得到运行,同时可以允许其他应用程序独占其余的计算资源。
下面的样例应用程序显示了这是如何工作的。
现在让我们来设计一个程序,它可以让 Linux 系统非常繁忙。可以使用前面介绍的系统调用和另外一些用来说明系统中有多少处理器的 API 来构建这个应用程序。实际上,我们的目标是编写这样一个程序:它可以让系统中的每个处理器都繁忙几秒钟。可以从后面的“下载”一节中 下载样例程序。
/* This method will create threads, then bind each to its own cpu. */ bool do_cpu_stress(int numthreads) { int ret = TRUE; int created_thread = 0; /* We need a thread for each cpu we have... */ while ( created_thread < numthreads - 1 ) { int mypid = fork(); if (mypid == 0) /* Child process */ { printf("\tCreating Child Thread: #%i\n", created_thread); break; } else /* Only parent executes this */ { /* Continue looping until we spawned enough threads! */ ; created_thread++; } } /* NOTE: All threads execute code from here down! */ |
正如您可以看到的一样,这段代码只是通过 fork 调用简单地创建一组线程。每个线程都执行这个方法中后面的代码。现在我们让每个线程都将亲和性(affinity)设置为自己的 CPU。
cpu_set_t mask; /* CPU_ZERO initializes all the bits in the mask to zero. */ CPU_ZERO( &mask ); /* CPU_SET sets only the bit corresponding to cpu. */ CPU_SET( created_thread, &mask ); /* sched_setaffinity returns 0 in success */ if( sched_setaffinity( 0, sizeof(mask), &mask ) == -1 ) { printf("WARNING: Could not set CPU Affinity, continuing...\n"); } |
如果程序可以执行到这儿,那么我们的线程就已经设置了自己的亲和性(affinity)。调用 sched_setaffinity
会设置由 pid
所引用的进程的 CPU 亲和性(affinity)掩码。如果 pid
为 0,那么就使用当前进程。
亲和性(affinity)掩码是使用在 mask
中存储的位掩码来表示的。最低位对应于系统中的第一个逻辑处理器,而最高位则对应于系统中最后一个逻辑处理器。
每个设置的位都对应一个可以合法调度的 CPU,而未设置的位则对应一个不可调度的 CPU。换而言之,进程都被绑定了,只能在那些对应位被设置了的处理器上运行。通常,掩码中的所有位都被置位了。这些线程的亲和性(affinity)都会传递给从它们派生的子进程中。
注意不应该直接修改位掩码。应该使用下面的宏。虽然在我们的例子中并没有全部使用这些宏,但是在本文中还是详细列出了这些宏,您在自己的程序中可能需要这些宏。
void CPU_ZERO (cpu_set_t *set) 这个宏对 CPU 集 set 进行初始化,将其设置为空集。 void CPU_SET (int cpu, cpu_set_t *set) 这个宏将 cpu 加入 CPU 集 set 中。 void CPU_CLR (int cpu, cpu_set_t *set) 这个宏将 cpu 从 CPU 集 set 中删除。 int CPU_ISSET (int cpu, const cpu_set_t *set) 如果 cpu 是 CPU 集 set 的一员,这个宏就返回一个非零值(true),否则就返回零(false)。 |
对于本文来说,样例代码会继续让每个线程都执行某些计算量较大的操作。
/* Now we have a single thread bound to each cpu on the system */ int computation_res = do_cpu_expensive_op(41); cpu_set_t mycpuid; sched_getaffinity(0, sizeof(mycpuid), &mycpuid); if ( check_cpu_expensive_op(computation_res) ) { printf("SUCCESS: Thread completed, and PASSED integrity check!\n", mycpuid); ret = TRUE; } else { printf("FAILURE: Thread failed integrity check!\n", mycpuid); ret = FALSE; } return ret; } |
现在您已经了解了在 Linux 2.6 版本的内核中设置 CPU 亲和性(affinity)的基本知识。接下来,我们使用一个 main
程序来封装这些方法,它使用一个用户指定的参数来说明要让多少个 CPU 繁忙。我们可以使用另外一个方法来确定系统中有多少个处理器:
int NUM_PROCS = sysconf(_SC_NPROCESSORS_CONF);
这个方法让程序能够自己确定要让多少个处理器保持繁忙,例如缺省让所有的处理器都处于繁忙状态,并允许用户指定系统中实际处理器范围的一个子集。
#include
#include
#include
#include
#include
#define __USE_GNU
#include
#include
#include
int main(int argc, char* argv[])
{
int num = sysconf(_SC_NPROCESSORS_CONF);
int created_thread = 0;
int myid;
int i;
int j = 0;
cpu_set_t mask;
cpu_set_t get;
if (argc != 2)
{
printf("usage : ./cpu num\n");
exit(1);
}
myid = atoi(argv[1]);
printf("system has %i processor(s). \n", num);
CPU_ZERO(&mask);
CPU_SET(myid, &mask);
if (sched_setaffinity(0, sizeof(mask), &mask) == -1)
{
printf("warning: could not set CPU affinity, continuing...\n");
}
while (1)
{
CPU_ZERO(&get);
if (sched_getaffinity(0, sizeof(get), &get) == -1)
{
printf("warning: cound not get cpu affinity, continuing...\n");
}
for (i = 0; i < num; i++)
{
if (CPU_ISSET(i, &get))
{
printf("this process %d is running processor : %d\n",getpid(), i);
}
}
}
return 0;
}
PS: setpriority()可以设置进程优先级
时钟中断:是指在计算机CMOS中一个芯片晶片中(常被称为实时时钟)由电池供电,用于计时,而另一个芯片则根据实时时钟提供的时间定期向CPU引脚发送中断信号的中断。
时钟中断非常特殊而且在系统作用也非常重大。
1.他用做记时。这点显而易见。
2.现在流行的操作系统linux和windows他们都是多进程并发执行的,为什么多进程可以并发,这是由中断支持的。一个进程执行的脚本是代 码,而我们知道我们在写代码过程中不会每隔多少行,或者说多少命令就写一个让出CPU语句将自己停止。既然这样,就是说进程不会自己让出CPU了(让出 CPU我想除了中断其他方式用的都是有自己特殊目的的,比如异常,让他来支持多进程并发运行显然不实际,因为他将会执行的异常处理程序,确切说他可能连进 程也不会切换),那么就得靠其它中断来阻止他继续运行,让CPU停下来从新调度所有进程,然后给人造成多进程并发运行的假象。
那么只有靠硬件的中断了,(为什么不是软件呢,刚说过了正在执行的进程没有中断自己,其它进程还在内存中,连CPU都没拿到,它如何能向CPU中断 引脚发送中断信号)硬件中断都是有自己的特殊用途的,一般他们目的都是想让CPU执行它们的驱动程序给他们提供服务,绝大部分是。明显时钟中断没这个要 求,我的中断服务程序里面一个应该涉及到计时,除此之外我不需要CPU给我实时时钟提供什么服务。而且如果你操作系统不需要,甚至连前面的计时都可以不 用。现在时钟中断了当前运行的进程,然后它按理应该做的是恢复原来的现场,但如果我不恢复现场呢,做一次进程调度。这样就实现了多进程并发运行。
当然如果你在代码中每隔几行加上一次自己主动让出CPU的语句(不一定是中断),但这么做明显将进程调度的部分任务交给了程序,编写的程序将会很复杂,而且每个语句指定时间长短不一,很难做到时间很短或者很确定时间。