1 基于进程的cpu配置
基于进程的cpu配置技术是指在linux操作系统中,通过调整进程的cpu使用率来实现对系统资源的合理分配和管理。这种技术可以用于限制特定进程的cpu使用,防止其占用过多的cpu资源,从而保证系统的稳定性和性能。
基于进程的cpu配置是指将一个进程绑定到特定的cpu核心或者cpu集合上运行。这样可以控制进程在特定的cpu资源上执行,以提高性能或实现特定的调度策略。
1.1 对cpu亲和力的配置
对于centos 8下的linux系统,首先可以使用top命令来查看当前的cpu占用率,如下图所示:
键盘上按数字1,可以以数据化的形式看到具体的使用情况,如下图:
键盘上按字母t,可以以图形化的形式看到具体的使用情况,如下图:
可以看到,当前的cpu基本是空闲的状态。
这时候,我们写一个测试代码,让其挂在后台运行,例如下面的一个死循环函数:
#include <iostream> int main() { while (true) { // 在这里编写你的代码 // 例如,输出一条信息 std::cout << "hello, i am running in an infinite loop!" << std::endl; } return 0; }
让其挂在后台运行着,暂时不管。
如下图:
此时,我们再次使用top命令,来查看当前的cpu 状态,如下图:
可以看到,此时循环代码这个进程正在后台不停运行,此时的cpu占用率相比开始,非常的高。
记住当前的进程pid,然后使用taskset -c -p 13265 命令查看当前进程的cpu亲和力。13265是当前进程的pid。
可以从输出的信息看出,pid 为13265的进程(即当前的测试进程)的亲和力cpu为0-3,即它同时运行在了0,1,2,3这四个cpu上面。
现在我们更改pid 为13265的进程(即当前的测试进程)的亲和力,将其改成只在0,1这两个cpu上面运行。如下图:
这里由于我使用了ctrl + c暂停了刚才的进程,然后重新启动该进程时,进程的pid变成了13495,不过这没有任何影响。可以看到,当前的进程新的亲和力列表为0,1,说明设置新的cpu亲和力成功。而pid 13495 的当前亲和力掩码为3,同样能说明亲和力设置成功。使用taskset命令来查看进程运行在哪个cpu上。
使用以下命令:
taskset -p < pid >
【请将< pid >替换为你要查看的进程的实际pid。】
该命令将显示进程的当前亲和力掩码,其中每个位表示一个cpu核心。例如,如果输出为pid 's current affinity mask: 3,表示进程当前在cpu核心0和1上运行,因为二进制表示为11。
再次实验一下,这次我们将cpu亲和力设置在cpu 1,cpu2上,如下图:
由当前亲和力掩码可以看到,为6,说明此时运行在cpu1和cpu2上。(2^1 + 2^2 = 6)
1.2 绑定进程到指定cpu核上运行
查看cpu有几个核
使用 cat /proc/cpuinfo 查看cpu信息,如下两个信息:
- ·processor:指明第几个cpu处理器
- ·cpu cores:指明每个处理器的核心数
以本机中虚拟机为例,有4个cpu(分别为:cpu0, cpu1, cpu2, cpu3),每个cpu有1个核。
也可以使用系统调用sysconf获取cpu核心数:
#include <unistd.h> int sysconf(_sc_nprocessors_conf);/* 返回系统可以使用的核数,但是其值会包括系统中禁用的核的数目,因 此该值并不代表当前系统中可用的核数 */ int sysconf(_sc_nprocessors_onln);/* 返回值真正的代表了系统当前可用的核数 */ /* 以下两个函数与上述类似 */ #include <sys/sysinfo.h> int get_nprocs_conf (void);/* 可用核数 */ int get_nprocs (void);/* 真正的反映了当前可用核数 */
使用 taskset 指令
- 获取进程pid:
- 查看进程当前运行在哪个cpu上:
显示的十六进制f转换为二进制为最低四个是1,每个1对应一个cpu,所以进程运行在4个cpu上。
- 指定进程10770运行在cpu0上:
注意,cpu的标号是从0开始的,所以cpu0表示第1个cpu(第一个cpu的标号是0)。
至此,就把应用程序绑定到了cpu0上运行,查看如下:
- 启动程序时绑定cpu:
例如启动时绑定到第二个cpu上,即cpu1:
使用sched_setaffinity系统调用
通过系统调用sched_setaffinity进行绑定,通过sched_getaffinity获取绑定关系。注意这对方法是进程级别的绑定。代码中指定cpu0和cpu3,我们可以通过top查看,两个cpu使用达到了100%,其他的cpu均不会(正常场景)。
sched_setaffinity可以将某个进程绑定到一个特定的cpu。
#define _gnu_source /* see feature_test_macros(7) */ #include <sched.h> /* 设置进程号为pid的进程运行在mask所设定的cpu上 * 第二个参数cpusetsize是mask所指定的数的长度 * 通常设定为sizeof(cpu_set_t) * 如果pid的值为0,则表示指定的是当前进程 */ int sched_setaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask); int sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask);/* 获得pid所指示的进程的cpu位掩码,并将该掩码返回到mask所指向的结构中 */
代码示例:
/* *该程序演示了如何使用sched_setaffinity函数将线程绑定到特定的cpu核心上运行。 *程序首先创建了两个线程,然后使用sched_setaffinity函数将线程1绑定到cpu 0上,将线程2绑定到cpu 3上。 *运行时,可以通过查看输出的pid来确定程序的进程id。 *然后,程序将cpu_zero宏应用于一个cpu_set_t类型的变量mask,以将其初始化为空集。 *接下来,程序将cpu_set宏应用于mask,将cpu 0和cpu 3添加到集合中。 *最后,程序调用sched_setaffinity函数将mask应用于当前进程,将线程1绑定到cpu 0上,将线程2绑定到cpu 3上。 *线程创建成功后,程序使用pthread_join函数等待线程1和线程2的结束。 */ #define _gnu_source /* see feature_test_macros(7) */ #include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <sched.h> #include <pthread.h> void* testfunc(void* t) { while(1); return null; } int main() { cpu_set_t mask; // 定义cpu_set_t类型的变量mask,用于存储cpu集合 printf("pid=%d\n", getpid()); // 打印进程id cpu_zero(&mask); // 将mask初始化为空集 cpu_set(0, &mask);//将cpu0绑定到mask中 cpu_set(3, &mask);//将cpu3绑定到mask中 // 将mask应用于当前进程,绑定线程到指定的cpu核心 sched_setaffinity(0, sizeof(cpu_set_t), &mask) ; pthread_t tid1;//创建线程1 if (pthread_create(&tid1, null, (void *)testfunc, null) != 0) { fprintf(stderr, "thread create failed\n"); // 线程创建失败,打印错误信息 return -1; } pthread_t tid2;//创建线程2 if (pthread_create(&tid2, null, (void *)testfunc, null) != 0) { fprintf(stderr, "thread create failed\n"); // 线程创建失败,打印错误信息 return -1; } pthread_join(tid1, null); // 等待线程1结束 pthread_join(tid1, null); // 等待线程2结束 return 0; }
执行结果如下图所示:
- 执行前:
- 执行后:
2 基于线程的cpu配置
2.1 线程绑定:使用函数pthread_setaffinity_np
线程绑定cpu核心的意义:
- 在多核cpu中合理的调度线程在各个核上运行可以获得更高的性能。
- 在多线程编程中,每个线程处理的任务优先级是不一样的,对于要求实时性比较高的线程或者是主线程,对于这种线程我们可以在创建线程时指定其绑定到某个cpu核上,以后这个核就专门处理该线程。
- 这样可以使得该线程的任务可以得到较快的处理,特别是和用户直接交互的任务,较短的响应时间可以提升用户的体验感。
几个重要的宏操作:
一个线程的cpu亲合力掩码用一个cpu_set_t结构体来表示一个cpu集合,下面的几个宏分别对这个掩码集进行操作:
cpu_zero() 清空一个集合 cpu_set()与cpu_clr()分别对将一个给定的cpu号加到一个集合或者从一个集合中去掉 cpu_isset()检查一个cpu号是否在这个集合中
设置获取线程cpu亲和力状态:
sched_setaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask)
该函数设置线程为pid的这个线程,让它运行在mask所设定的cpu上。
如果pid的值为0,则表示指定的是当前线程,使当前线程运行在mask所设定的那些cpu上。
第二个参数cpusetsize是mask所指定的数的长度。通常设定为sizeof(cpu_set_t)。
如果当前pid所指定的线程此时没有运行在mask所指定的任意一个cpu上,则该指定的线程会从其它cpu上迁移到mask的指定的一个cpu上运行。
sched_getaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask)
该函数获得pid所指示的线程的cpu位掩码,并将该掩码返回到mask所指向的结构中。即获得指定pid当前可以运行在哪些cpu上。同样,如果pid的值为0。也表示的是当前进程。
简单的实例:
// 此代码不完整,只是帮助理解绑定过程 // 在创建线程时添加以下代码,可以将该线程绑定到1核 cpu_set_t mask; // 将掩码清零 cpu_zero(&mask); // 将1添加到掩码中 cpu_set(1, &mask); // #将本线程绑定到1核 sched_setaffinity(0, sizeof(cpu_set_t), &mask);
查看线程是否运行在指定的核上:
实际工作中,为了方便查看线程的情况,会在创建线程时将相关信息保存到一个文件中,需要时用cat命令查看,内容包括创建了哪些线程、线程名称、线程id和pid、绑定的cpu核、优先级、调度方式等。
使用“top”命令查看:
- top -d 2:查看线程的运行情况和cpu状态
- 按’h’ 和 1:在上一句的基础上可以查看更详细的信息。
从文件中得到线程pid和ppid,通过top命令,查看线程在哪个cpu核上运行,验证核绑定的核是否一样。
对于线程绑定,我们需要借助pthread库,通过函数pthread_setaffinity_np来设置绑定cpu关系。我们通过top查看,会发现cpu0和cpu3使用率达到100%。
代码实例:
/* *该程序演示了如何使用pthread_setaffinity_np函数将线程绑定到特定的cpu核心上运行。 *程序首先创建了两个线程,然后使用pthread_setaffinity_np函数将线程1绑定到cpu 0上,将线程2绑定到cpu 3上。 *运行时,可以通过查看输出的pid来确定程序的进程id。 *然后,程序将cpu_zero宏应用于一个cpu_set_t类型的变量mask,以将其初始化为空集。 *接下来,程序使用pthread_create函数创建线程1和线程2,并检查线程创建是否成功。 *然后,程序打印出线程1和线程2的id。 *程序使用cpu_set宏将cpu 0添加到mask中,并使用pthread_setaffinity_np函数将mask应用于线程1,将线程1绑定到cpu 0上。 *然后,程序清除之前设置的mask,并将cpu 3添加到mask中,并使用pthread_setaffinity_np函数将mask应用于线程2,将线程2绑定到cpu 3上。 *最后,程序使用pthread_join函数等待线程1和线程2的结束。 */ #define _gnu_source /* see feature_test_macros(7) */ #include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <sched.h> #include <pthread.h> void* testfunc(void* t) { int i = 3; // 初始化循环计数器为3 while(i) { // 进入循环,条件为i非零 sleep(5); // 休眠5秒 printf("tid=%d,cpu=%d\n",pthread_self(), sched_getcpu()); // 打印线程id和cpu编号 i--; // 计数器减一 } while(1); // 进入无限循环 return null; } int main() { cpu_set_t mask; // 定义cpu集合 printf("pid=%d\n", getpid()); // 打印进程id cpu_zero(&mask); // 清空cpu集合 pthread_t tid1; // 定义线程tid1 if (pthread_create(&tid1, null, (void *)testfunc, null) != 0) { fprintf(stderr, "thread create failed\n"); // 线程创建失败,打印错误信息 return -1; } pthread_t tid2; // 定义线程tid2 if (pthread_create(&tid2, null, (void *)testfunc, null) != 0) { fprintf(stderr, "thread create failed\n"); // 线程创建失败,打印错误信息 return -1; } printf("tid1=%d,tid2=%d\n", tid1,tid2); // 打印线程tid1和tid2的值 cpu_set(0, &mask); // 将cpu0加入cpu集合 pthread_setaffinity_np(tid1, sizeof(cpu_set_t), &mask) ; // 设置线程tid1的cpu亲和性为cpu0 // 清除之前设置,重新设置绑定cpu3 cpu_zero(&mask); // 清空cpu集合 cpu_set(3, &mask); // 将cpu3加入cpu集合 pthread_setaffinity_np(tid2, sizeof(cpu_set_t), &mask) ; // 设置线程tid2的cpu亲和性为cpu3 pthread_join(tid1, null); // 等待线程tid1结束 pthread_join(tid1, null); // 等待线程tid2结束 return 0; }
- 执行之后:
- 将其kill,恢复:
建议:进行配置之前先将虚拟机拍摄快照,以防配置不当出现意外情况。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论