概述

最近在看一个库的实现代码时,发现它依赖于查看进程使用的 CPU 核数,然后根据 CPU 核数做一些限制,所以我就顺便看了一下 Go 是如何实现查看 CPU 核心数的。

Golang 实现

  1. [root@liqiang.io]# cat main.go
  2. runtime.NumCPU()
  3. --> runtime/debug.go return int(ncpu)

这里返回了一个全局变量,这个全局变量的初始化是在进程起来的时候设置的,所以注释中也解释了,一旦进程运行之后,如果更改了 CPU 亲和性的配置也不会生效:

  1. [root@liqiang.io]# cat runtime/os_linux.go
  2. func osinit() {
  3. ncpu = getproccount()
  4. ---> func getproccount() int32 {
  5. r := sched_getaffinity(0, unsafe.Sizeof(buf), &buf[0])
  6. ... ...
  7. n := int32(0)
  8. for _, v := range buf[:r] {
  9. for v != 0 {
  10. n += int32(v & 1)
  11. v >>= 1
  12. }
  13. }
  14. if n == 0 {
  15. n = 1
  16. }
  17. return n

这里的 sched_getaffinity 不是一个简单的 Go 函数,而是一个系统调用,所以需要从汇编代码中看,但是也很简单,就是将函数入栈,然后调用系统调用:

  1. [root@liqiang.io]# cat runtime/sys_linux_amd64.s
  2. TEXT runtime·sched_getaffinity(SB),NOSPLIT,$0
  3. MOVQ pid+0(FP), DI
  4. MOVQ len+8(FP), SI
  5. MOVQ buf+16(FP), DX
  6. MOVL $SYS_sched_getaffinity, AX
  7. SYSCALL
  8. MOVL AX, ret+24(FP)
  9. RET

sched_getaffinity 系统调用

从 linux 的 man page(https://man7.org/linux/man-pages/man2/sched_setaffinity.2.html) 中可以看到,这个系统调用是返回 cpuset 的信息,通过掩码的方式返回。实际上返回的 cpuset 就是一个位图,如果一个 bit 是 1,那么表示这个 cpu 是可用的,如果是 0 则表示不可用,所以我们从 Go 的代码中可以看到,它遍历返回的 buffer,然后逐位地检查(这个其实有个高效的 CPU 指令可以完成),最终计算当前进程可以使用的 CPU 核数是多少。

我们用系统命令也可以查看容器可以使用的 CPU 核心数:

  1. [root@liqiang.io]# lscpu | egrep -i 'core.*:|socket'
  2. Thread(s) per core: 1
  3. Core(s) per socket: 1
  4. Socket(s): 4

一些简单的小坑

在使用 Docker 的时候,我曾经想限制 CPU 的个数(限制 Docker 容器的 CPU 内存等资源,发现 Docker 有两种不同的选项:

对于第二种限制 CPU 时间片的方式,在容器里面实际上看到的还是所有的 CPU 核心,所以 Go 也会认为他有那么多核心数,这个可能是一个坑需要注意。

一些系统底层知识

cgroups v2 限制 cpuset

cpuset 可以通过 cgroups 简单地限制,例如我这里使用 cgroupv2 为例进行:

  1. [root@liqiang.io]# sudo cgcreate -g cpuset:/liqiang2
  2. [root@liqiang.io]# sudo cgexec -g cpuset:/liqiang2 go run /tmp/main.go
  3. 16
  4. [root@liqiang.io]# echo "0-1" | sudo tee /sys/fs/cgroup/liqiang2/cpuset.cpus
  5. [root@liqiang.io]# sudo cgexec -g cpuset:/liqiang2 go run /tmp/main.go
  6. 2

NUMA

ok,这里我们知道了,Go 是通过 cpuset 来获取可用的 CPU 核数的,那么 cpuset 是什么?为什么会存在这个东西?我们在理解容器的概念之后,很自然地会认为容器之间资源隔离不是一个很正常的功能吗?这没错,但是实际上 cpuset 在容器流行之前就存在了,再次之前,它常被用于 NUMA 的环境,在一些高性能的服务器中,一个主机上其实包含不只有一个 CPU,你很平常就会看到两个 CPU 的机器,例如我这一台:

  1. [root@liqiang.io]# lscpu | grep -i numa
  2. NUMA node(s): 2
  3. NUMA node0 CPU(s): 0-23,48-71
  4. NUMA node1 CPU(s): 24-47,72-95

那么我们知道 CPU 和内存是通过北桥高速通信线路通信的,那么如何存在多个 CPU 的话,这个线路要如何设计?事实上,在 NUMA 架构中,CPU 和内存是有一个远近关系的,每个 CPU 都会有一些本地内存比较快,对于其他 CPU 的本地内存,访问速度会慢一些,于是,为了程序的运行效率,我们就会有绑定 CPU 和内存的需求了,我们可以将进程绑定在固定的 CPU 上,并且内存也使用对应的一些,这样可以保证我们的进程不会跨 CPU,这样的话就可以去除 NUMA 架构的影响了。