多核的负载均衡
Linux 在多线程的情况下,进程是如何调度的呢?我们先来看看下面流程图:
要理解多核首先要理解单核,前面的文章中我已经对单核的调度算法学习了。其实多核的情况下,需要理解负载均衡的概念。所以,
- 在多核的 CPU 上,每个 core 上面的还是使用FIFO、RR、CFS算法调度进程的
- 在core与core之间,使用一些算法进行负载均衡。
- 对于 RT 的进程,core和core之间使用 pull_rt_request 和 push_rt_request 的进行负载均衡。这是一种主动的平衡策略。
- 对于 normal 的进程,采用的是的跟随 check 的方式,比较被动。
pull_rt_request:是当一个 core 闲下来的时候,会主动的去忙的 core 里面拉去进程,帮繁忙的 core 来运行进程。
push_rt_request:是当一个 core 繁忙的时候,它会主动的将它上面的进程推送给闲暇的 core,让闲暇的 core 帮忙来执行。
普通进程会在三个时机触发 core 负责平衡:
- 当一个时钟周期结束的时候
- 当一个 core 进入 IDLE 状态的时候
- 当一个 core 执行 fork 或者 exec 的时候
来来,我们来测试一下,请看下面的代码:
1 #include <stdio.h>
2 #include <pthread.h>
3 #include <sys/types.h>
4
5 void *thread_fun(void *param)
6 {
7 printf("thread pid:%d, tid:%lu\n", getpid(), pthread_self());
8 while (1) ;
9 return NULL;
10 }
11
12 int main(void)
13 {
14 pthread_t tid1, tid2;
15 int ret;
16
17 printf("main pid:%d, tid:%lu\n", getpid(), pthread_self());
18
19 ret = pthread_create(&tid1, NULL, thread_fun, NULL);
20 if (ret == -1) {
21 perror("cannot create new thread");
22 return 1;
23 }
24
25 ret = pthread_create(&tid2, NULL, thread_fun, NULL);
26 if (ret == -1) {
27 perror("cannot create new thread");
28 return 1;
29 }
30
31 if (pthread_join(tid1, NULL) != 0) {
32 perror("call pthread_join function fail");
33 return 1;
34 }
35
36 if (pthread_join(tid2, NULL) != 0) {
37 perror("call pthread_join function fail");
38 return 1;
39 }
40
41 return 0;
42 }
~> gcc show_cores.c -pthread
~> ./a.out
执行结果如下所示:
在上图中可以看到,a.out 的进程 cpu 占用率是 200% ,这是因为 Linux 操作系统会帮我们将 a.out 里面两个线程的负载平均到这两个 core 里面。
使用 time 这个命令,我们也可以看到一些 Linux 做 core 之间负载均衡的痕迹,下图所示,real 是程序真正在 cpu 执行的时间,user 是站在用户的角度执行的时候,user 大概是 real 的两倍,这说明 Linux 将两个线程执行时间平衡到两个 core 里面执行了,因为为我们节省了一半的时间了。
我们可以使用 taskset
这个 shell 命令来设置进程在那个 core 里面跑。例如
taskset -a -p 01 2000 # 将 2000 进程所有的线程放到 01 这个 core 里面跑
taskset -a -p 01 2000 # 将 2000 进程所有的线程放到 01 这个 core 里面跑
中断的负载均衡
我们以网卡的中断来说明,如何做到网络的中断的负载均衡的。
如何下图所示:
在上图中,可以发现我的虚拟机上,有两个网卡驱动,我们看看 ens33 的队列的负载均衡是怎么做的。 rps_cpus 里面可以设置中断队列的中断发送给那个 cpu 来处理。这是 linux 的 RPS(Receive Packet Steering) 补丁,这个补丁解决了中断分配不均匀的问题。我们可以做下面的修改:
~> echo 'fffe' > /sys/class/net/lo/queues/rx-0/rps_cpus
这样设计就把中断分给了 01、02、03 三个 core 里面了。
可以使用下面的 shell 语句进行观察:
watch -d "cat /proc/softirqs | grep 'NET_RX'"
cgroup
cgroup 是 Linux 提供一种 cpu 资源隔离的机制。它是为了解决用户级的进程调度公平。
假如有一个用户底下有 1024 个进程,另外一个用户下面有 52 个进程,那A用户很明显会占用更多的资源,因为 CFS 尽量的让每个进程公平得获得 CPU 资源,这样积累下来第一个用户获得比第二个进程获得更多得资源,如果,如果第二个进程是 CPU 消耗型的虽然进程数量少,但是需要的 CPU资源更多。所以CFS可能能会造成用户级的不公平。
那咋办呢?当然是是有办法了,这就是 cgroup 了。
先不解释概念,先一波如下一顿操作:
# 启动三个 a.out 程序
~> ./a.out &
~> ./a.out &
~> ./a.out &
~> cd /sys/fs/cgroup/cpu
~> mkdir A
~> mkdir B
~> echo 1834 > A/cgroup.procs
~> echo 1837 > A/cgroup.procs
~> echo 1840 > B/cgroup.procs
在这一波操作前三个 a.out 的 cpu 占用率如下所示:
进行完这波操作完,再来看看三个 a.out 的 cpu 占用率:
在路径 /sys/fs/cgroup/cpu 下面我们我们可以像新建文件夹一样的创建一个 cgroup ,然后我们可以在 cgroup.proc 里面写入进程ID好,这样就把 pid 进程放到了这个 cgroup 里面了。
可以调整几个参数来调整 cgroup 里面进程的 cpu 占用率。
先来看看看 pid 和 cgroup 的关系。如下所示:
pid | cgroup |
---|---|
1840 | B |
1834 | A |
1837 | A |
它们的 cpu 占用率如下所示:
我们进行如下设置:
~> echo 512 > B/cpu.shares
这个命令会将 B 的CPU占用的优先级设置为 512 , A 的 cpu.shares 的值为 1024 。A 是 B的二倍,所以 A 可以分到三个二的 CPU ,B可以分到三分之一的 CPU时间。
结果如下下图所示:
在来看看其的参数:
- cpu.rt_period_us: RT型进程一个周期的微秒数
- cpu.rt_runtime_us:RT型进程能在 cpu.rt_period_us 中跑多长时间
- cpu.cfs_period_us:normal 型进程一个周期的微秒数
- cpu.cfs_quota_us:normal 型进程能在 cpu.rt_period_us 中跑多长时间
来看看 cpu.cfs_quota 是咋用的:
~> echo 10000 > A/cpu.cfs_quota_us
在执行之前:
在执行之后:
我们知道,1837和1834 是在 A 中执行的,我们把 A 中 normal 类型的进程的运行时间减少后,CPU 资源大多给了 A , 所以 1840 这个进程就获得大部分的 CPU资源。
这就是cgroup.
Linux 不是一个实时操作系统
什么样子的系统称之为实时操作系统呢?真相只有一个,那就是进程从就绪到执行的等待时间是可预期的。
怎么理解可预期呢?就是一个进程 RT 从唤醒到获得 CPU 资源的时间不会超过一个阈值。如果超过了那这个操作系统就不能称为实时操作系统。
原因是 RT 进程在好三类 CPU 区间下不能抢到 CPU 资源。
不能抢的情况是有下面几个:
- 进程上下切换过程中拿到 spinlock,进程中有 spinlock
- 中断和软中断
- 内核进程不能抢
这三类 CPU区间 CPU 占用时长都是不确定的。所以 Linux 不是硬实时操作系统。
可以安装 preempt_rt 这补丁来把 Linux 做成一个近似的硬实时操作系统。
这个补丁实现下面的功能:
- spinlock 迁移为可调度的 mutex ,其中 mutex 是可以抢的。
- 实现优先级继承协议,举个例子, A B C 三个进程, 他们优先级是 1 2 3 , A 和 C 两个之间存在锁,C 控制了锁,但是 C 的优先级比较低,C 总是抢不到 CPU,所以 A 有等 C 好长时间。优先级继承协议是将 C 从优先级设置成 1 ,这样的话,C 就能比较快速的干完自己的事情,干完后就可以把锁给 A了
- 中断和软中断线程化,什么是中断线程化,我也不知道?需要进一步的了解。
番外知识点
fork 我已经知道是个什么东西了,exce 不执行,exce 说白了就是挂羊头买狗肉的做法,首先就是用 fork 出来进程的内存、代码端、程序计数器等一个完整的进程;然后使用把这些都擦除掉,然后将我们希望运行的程序放到刚才说的进程结构里面去,酒瓶子就装上了新酒。