To be a better coder
分类: LINUX
2020-11-03 09:39:40
8个核心的CPU ID: 7 6 5 4 3 2 1 0
对应10的十进制数位: 128 64 32 16 8 4 2 1
十六进制的16个数是:0、1、2、3、4、5、6、7、8、9、A、B、C、D、E、F
对应每一个16进制的二进制位:0=0000,1=0001,2=0010,3=0011,4=0100,5=0101,6=0110,7=0111,8=1000,9=1001,A=1010,
B=1011,C=1100,D=1101,E=1110,F=1111
通常情况下,在 SMP 系统中,Linux 内核的进程调度器根据自有的调度策略将系统中的一个进程调度到某个 CPU
上执行。一个进程在前一个执行时间是在 cpuM(M 为系统中的某 CPU 的 ID)上运行,而在后一个执行时间是在 cpuN(N 为系统中另一
CPU 的 ID)上运行。这样的情况在 Linux 中是很可能发生的,因为 Linux
对进程执行的调度采用时间片法则(即进行用完自己的时间片即被暂停执行),而默认情况下,一个普通进程或线程的处理器亲和性是在所有可用的 CPU
上,有可能在它们之中的任何一个 CPU(包括超线程)上执行。
进程的处理器亲和性(Processor Affinity),即是 CPU 的绑定设置,是指将进程绑定到特定的一个或多个 CPU 上去执行,而不允许调度到其他的 CPU 上。Linux 内核对进程的调度算法也是遵守进程的处理器亲和性设置的。设置进程的处理器亲和性带来的好处是可以减少进程在多个 CPU 之间交换运行带来的缓存命中失效(cache missing),从该进程运行的角度来看,可能带来一定程度上的性能提升。换个角度来看,对进程亲和性的设置也可能带来一定的问题,如破坏了原有 SMP 系统中各个 CPU 的负载均衡(load balance),这可能会导致整个系统的进程调度变得低效。特别是在多处理器、多核、多线程技术使用的情况下,在 NUMA(Non-Uniform Memory Access)[3] 结构的系统中,如果不能基于对系统的 CPU、内存等有深入的了解,对进程的处理器亲和性进行设置是可能导致系统的整体性能的下降而非提升。
每个 vCPU 都是宿主机中的一个普通的 QEMU 线程,可以使用 taskset 工具对其设置处理器亲和性,使其绑定到某一个或几个固定的 CPU 上去调度。尽管 Linux 内核的进程调度算法已经非常高效了,在多数情况下不需要对进程的调度进行干预,不过,在虚拟化环境中有时却有必要对客户机的 QEMU 进程或线程绑定到固定的逻辑 CPU 上。下面举一个云计算应用中需要绑定 vCPU 的实例。
作为 IAAS(Infrastructure As A Service)类型的云计算提供商的 A 公司(如 Amazon、Google、阿里云、盛大云等),为客户提供一个有 2 个逻辑 CPU 计算能力的一个客户机。要求 CPU 资源独立被占用,不受宿主机中其他客户机的负载水平的影响。为了满足这个需求,可以分为如下两个步骤来实现。
第一步,启动宿主机时隔离出两个逻辑 CPU 专门供一个客户机使用。在 Linux 内核启动的命令行加上 “isolcpus=” 参数,可以实现 CPU 的隔离,让系统启动后普通进程默认都不会调度到被隔离的 CPU 上执行。例如,隔离了 cpu2 和 cpu3 的 grub 的配置文件如下:
title Red Hat Enterprise Linux Server (3.5.0)
root (hd0,0)
kernel /boot/vmlinuz-3.5.0 ro root=UUID=1a65b4bb-cd9b-4bbf-97ff-7e1f7698d3db isolcpus=2,3
initrd /boot/initramfs-3.5.0.img
系统启动后,在宿主机中检查是否隔离成功,命令行如下:
[root@jay-linux ~]# ps -eLo psr | grep 0 | wc -l
106
[root@jay-linux ~]# ps -eLo psr | grep 1 | wc -l
107
[root@jay-linux ~]# ps -eLo psr | grep 2 | wc -l
4
[root@jay-linux ~]# ps -eLo psr | grep 3 | wc -l
4
[root@jay-linux ~]# ps -eLo ruser,pid,ppid,lwp,psr,args | awk ‘{if($5==2) print $0}’
root 10 2 10 2 [migration/2]
root 11 2 11 2 [kworker/2:0]
root 12 2 12 2 [ksoftirqd/2]
root 245 2 245 2 [kworker/2:1]
[root@jay-linux ~]# ps –eLo ruser,pid,ppid,lwp,psr,args | awk ‘{if($5==3) print $0}’
root 13 2 13 3 [migration/3]
root 14 2 14 3 [kworker/3:0]
root 15 2 15 3 [ksoftirqd/3]
root 246 2 246 3 [kworker/3:1]
从上面的命令行输出信息可知,cpu0 和 cpu1 上分别有 106 和 107 个线程在运行,而 cpu2 和 cpu3 上都分别只有 4 个线程在运行。而且,根据输出信息中 cpu2 和 cpu3 上运行的线程信息(也包括进程在内),分别有 migration 进程(用于进程在不同 CPU 间迁移)、两个 kworker 进程(用于处理 workqueues)、ksoftirqd 进程(用于调度 CPU 软中断的进程),这些进程都是内核对各个 CPU 的一些守护进程,而没有其他的普通进程在 cup2 和 cpu3 上运行,说明对其的隔离是生效的。
另外,简单解释一下上面的一些命令行工具及其参数的意义。ps 命令显示当前系统的进程信息的状态,它的 “-e” 参数用于显示所有的进程,“-L” 参数用于将线程(LWP,light-weight process)也显示出来,“-o” 参数表示以用户自定义的格式输出(其中 “psr” 这列表示当前分配给进程运行的处理器编号,“lwp” 列表示线程的 ID,“ruser” 表示运行进程的用户,“pid” 表示进程的 ID,“ppid” 表示父进程的 ID,“args” 表示运行的命令及其参数)。结合 ps 和 awk 工具的使用,是为了分别将在处理器 cpu2 和 cpu3 上运行的进程打印出来。
第二步,启动一个拥有 2 个 vCPU 的客户机并将其 vCPU 绑定到宿主机中两个 CPU 上。此操作过程的命令行如下:
#(启动一个客户机)
[root@jay-linux kvm_demo]# qemu-system-x86_64 rhel6u3.img -smp 2 -m 512 -daemonize
VNC server running on ‘::1:5900’
#(查看代表 vCPU 的 QEMU 线程)
[root@jay-linux ~]# ps -eLo ruser,pid,ppid,lwp,psr,args | grep qemu | grep -v grep
root 3963 1 3963 0 qemu-system-x86_64 rhel6u3.img -smp 2 -m 512 -daemonize
root 3963 1 3967 0 qemu-system-x86_64 rhel6u3.img -smp 2 -m 512 -daemonize
root 3963 1 3968 1 qemu-system-x86_64 rhel6u3.img -smp 2 -m 512 –daemonize
#(绑定代表整个客户机的 QEMU 进程,使其运行在 cpu2 上)
[root@jay-linux ~]# taskset -p 0×4 3963
pid 3963′s current affinity mask: 3
pid 3963′s new affinity mask: 4
#(绑定第一个 vCPU 的线程,使其运行在 cpu2 上)
[root@jay-linux ~]# taskset -p 0×4 3967
pid 3967′s current affinity mask: 3
pid 3967′s new affinity mask: 4
#(绑定第二个 vCPU 的线程,使其运行在 cpu3 上)
[root@jay-linux ~]# taskset -p 0×8 3968
pid 3968′s current affinity mask: 4
pid 3968′s new affinity mask: 8
#(查看 QEMU 线程的绑定是否生效,如下的第 5 列为处理器亲和性)
[root@jay-linux ~]# ps -eLo ruser,pid,ppid,lwp,psr,args | grep qemu | grep -v grep
root 3963 1 3963 2 qemu-system-x86_64 rhel6u3.img -smp 2 -m 512 -daemonize
root 3963 1 3967 2 qemu-system-x86_64 rhel6u3.img -smp 2 -m 512 -daemonize
root 3963 1 3968 3 qemu-system-x86_64 rhel6u3.img -smp 2 -m 512 –daemonize
#(执行 vCPU 的绑定后,查看在 cpu2 上运行的线程)
[root@jay-linux ~]# ps -eLo ruser,pid,ppid,lwp,psr,args | awk ‘{if($5==2) print $0}’
root 10 2 10 2 [migration/2]
root 11 2 11 2 [kworker/2:0]
root 12 2 12 2 [ksoftirqd/2]
root 245 2 245 2 [kworker/2:1]
root 3963 1 3963 2 qemu-system-x86_64 rhel6u3.img -smp 2 -m 512 -daemonize
root 3963 1 3967 2 qemu-system-x86_64 rhel6u3.img -smp 2 -m 512 -daemonize
#(执行 vCPU 的绑定后,查看在 cpu3 上运行的线程)
[root@jay-linux ~]# ps –eLo ruser,pid,ppid,lwp,psr,args | awk ‘{if($5==3) print $0}’
root 13 2 13 3 [migration/3]
root 14 2 14 3 [kworker/3:0]
root 15 2 15 3 [ksoftirqd/3]
root 246 2 246 3 [kworker/3:1]
root 3963 1 3968 3 qemu-system-x86_64 rhel6u3.img -smp 2 -m 512 -daemonize
由上面的命令行及其输出信息可知,CPU 绑定之前,代表这个客户机的 QEMU 进程和代表各个 vCPU 的 QEMU 线程分别被调度到 cpu0 和 cpu1 上。使用 taskset 命令将 QEMU 进程和第一个 vCPU 的线程绑定到 cpu2,将第二个 vCPU 线程绑定到 cpu3 上。绑定之后,即可查看到绑定的结果是生效的,代表两个 vCPU 的 QEMU 线程分别运行在 cpu2 和 cpu3 上(即使再过一段时间后,它们也不会被调度到其他 CPU 上去)。
对 taskset 命令解释一下,此处使用的语法是:taskset -p [mask] pid 。其中,mask 是一个代表了处理器亲和性的掩码数字,转化为二进制表示后,它的值从最低位到最高位分别代表了第一个逻辑 CPU 到最后一个逻辑 CPU,进程调度器可能将该进程调度到所有标为 “1” 的位代表的 CPU 上去运行。根据上面的输出,taskset 运行之前,QEMU 线程的处理器亲和性 mask 值是 0×3(其二进制值为:0011),可知其可能会被调度到 cpu0 和 cpu1 上运行;而运行 “taskset -p 0×4 3967” 命令后,提示新的 mask 值被设为 0×4(其二进制值为:0100),所以该进程就只能被调度到 cpu2 上去运行,即通过 taskset 工具实现了 vCPU 进程绑定到特定的 CPU 上。
上面命令行中,根据 ps 命令可以看到 QEMU 的线程和进程的关系,但如何查看 vCPU 与 QEMU 线程之间的关系呢?可以切换(“Ctrl+Alt+2” 快捷键)到 QEMU monitor 中进行查看,运行 “info cpus” 命令即可(还记得 3.6 节中运行过的 “info kvm” 命令吧),其输出结果如下:
(qemu) info cpus
* CPU #0: pc=0xffffffff810375ab thread_id=3967
CPU #1: pc=0xffffffff812b2594 thread_id=3968
从上面的输出信息可知,客户机中的 cpu0 对应的线程 ID 为 3967,cpu1 对应的线程 ID 为 3968。另外,“CPU #0” 前面有一个星号(*),是标识 cpu0 是 BSP(Boot Strap Processor,系统最初启动时在 SMP 生效前使用的 CPU)。
总的来说,在 KVM 环境中,一般并不推荐手动地人为设置 QEMU 进程的处理器亲和性来绑定 vCPU,但是,在非常了解系统硬件架构的基础上,根据实际应用的需求,是可以将其绑定到特定的 CPU 上去从而提高客户机中的 CPU 执行效率或者实现 CPU 资源独享的隔离性。
添加几个关于 CPU 亲和性的小知识点:
1. 限制 CPU 亲和性的原因一般有如下 3 个:
1.1 任务中有大量计算存在;
1.2 测试复杂的应用程序(随着 CPU 个数的正常,程序的处理能力可以线性地扩展);
1.3 运行时间敏感的进程(实时性要求很高)。
2. 子进程会继承父进程的 affinity 属性(其实用 taskset 方式启动一个进程就是一次 fork+exec)。
3. 在进程的代码中,使用 sched_setaffinity 函数可以设置该进程的 CPU 亲和性。
#include
int sched_setaffinity(pid_t pid, unsigned int len, unsigned long *mask);
int sched_getaffinity(pid_t pid, unsigned int len, unsigned long *mask);
4. 使用 Nginx 时,其配置文件 conf/nginx.conf 中支持一个名为 worker_cpu_affinity 的配置项,也就是说,nginx 可以为每个工作进程绑定 CPU。
如下配置:
worker_processes 3;
worker_cpu_affinity 0010 0100 1000;
这里 0010 0100 1000 是掩码,分别代表第 2、3、4 颗 CPU 核心(或超线程)。
重启 nginx 后,3 个工作进程就可以各自用各自的 CPU 了。
5. 在 Windows 系统中的 “任务管理器” 中,也可以对一个进程设置 CPU 亲和性 “set affinity”。
逻辑 CPU 个数:
逻辑 CPU 个数是指 cat /proc/cpuinfo 所显示的 processor 的个数
# cat /proc/cpuinfo | grep "processor" | wc -l
物理 CPU 个数:
物理 CPU 个数,是指 physical id(的值)的数量
# cat /proc/cpuinfo | grep "physical id" | sort | uniq | wc -l
每个物理 CPU 中 Core 的个数:
每个相同的 physical id 都有其对应的 core id。如 core id 分别为 1、2、3、4,则表示是 Quad-Core CPU,若 core id 分别是 1、2,则表示是 Dual-Core。
# cat /proc/cpuinfo | grep "cpu cores" | wc -l
是否为超线程?
如果有两个逻辑 CPU 具有相同的 "core id",那么超线程是打开的。
每个物理 CPU 中逻辑 CPU(可能是 core, threads 或 both) 的个数:
# cat /proc/cpuinfo | grep "siblings"
逻辑 cpu 既可能是 cores 的个数,也可能是 core 的倍数。当它和 core 的个数相等时,表示每一个 core 就是一个逻辑 CPU,若它时 core 的 2 倍时,表示每个 core 又 enable 了超线程(Hyper-Thread)。比如:一个双核的启用了超线程的物理 cpu,其 core id 分别为 1、2,但是 sibling 是 4,也就是如果有两个逻辑 CPU 具有相同的 "core id",那么超线程是打开的。