使用lscpu,我们可以看到机器有1个物理核,上面有两个core,然后开起来超线程,所以每个core有两个thread。操作系统就可以看到4个核。


我们使用如下简单的代码来看下我们的程序会使用几个CPU。

可以看到输出NumCpu为4,GOMAXPROCS也为4,看起来一起都正常。那么go runtime是用的什么方法来得到可以使用的核数的呢,是使用的类似lscpu或者读取/proc/cpuinfo之类的方法么?

继续看代码,在runtime/debug.go中,可以看到是直接读取的ncpu这个全局变量得到的,而且注释里面很明确的说明了这个值是在进程初始化的时候读取的,后面就不会再变了。

那么ncpu又是在哪儿赋值的呢,这个时候就可以使用我们万能的grep命令了,直接在/d/go/src/runtime目录下面找ncpu赋值的地方。
grep -Er 'ncpu = ' .
能看到好几个文件都有,我们直接看os_linux.go

可以看到是在osinit中进行赋值的,调用了getproccount函数

getproccount使用了sche_getaffinity这个函数,对其输出buf这个值,直接统计里面的为1的bit的个数,有多个少个bit为1,就表示有多少个CPU。

如果对Linux熟悉的同学可能马上就反应过来了,这个sche_getaffinity不是一个系统调用么,用来得到进程的CPU亲和性的。其值反应在/proc/$pid/status的Cpus_allowed里面的。

我们看下这函数的实现,发现其如下,只有一个函数的声明,并没有函数的实现。

在go中,当发现一个函数只有声明函数原型,但是没有函数体的时候,说明这个函数的实现是用go汇编实现的。我们再用grep看下这个函数在哪儿实现的,发现在runtime/sys_linux_amd64.s里面。

其实现如下,注意go的汇编是plan9汇编语言,这个和我们平时看得比较多的GNU汇编语法差别还是挺大的(go语言是自举的,有自己的编译器,汇编器和链接器,所以用自己的汇编语法是完全没有问题的。)

go自己的编译工具
这个汇编就是将pid,len,buf这三个变量依次放到DI,SI,DX三个寄存器中,然后把sched_getaffinity这个系统调用对应的编号放到AX寄存器中,然后调用SYSCALL指令。这个流程就是LINUX里面标准的系统调用流程。完成之后返回码放到AX寄存器,关心的值从buf出来。

到这儿整个逻辑就通了。go得到NumCpu不是当前集群的lscpu输出的个数,而是系统分配给当前进程的CPU个数。那么为什么我们大部分情况下看到的就是当前系统的CPU个数呢?那是因为默认情况下,系统就是让进程可以使用所有CPU的。但是其实我们可以通过cgroup里面的cpuset来让当前进程只可以使用一部分CPU的。
使用 cgcreate -g cpuset:/gocpu 创建一个cpuset类型的cgroup,名字为gocpu

可以看到当前cpuset.cpus为空的,表示没有亲和性,也就是所有的cpu都可以使用。
我们通过如下命令让在gocpu这cgroup里面的进程只能使用0-1这两个cpu,然后让当前的bash进入这个cgroup中
#echo '0' > cpuset.mems
#echo '0-1' > cpuset.cpus
#echo $$ > tasks

可以看到当前bash进程只会在0,1两个cpu上执行了。

然后再启动我们的程序,可以看到NumCpu只有2了。因为当前子进程和继承父进程的cpu亲和性。
