该文章参考宋宝华老师的进程视频课程,详细可以去听阅码场宋老师的课程。
-
Linux Deadline调度器(全新内容)
-
Linux 为什么不是硬实时的
-
preempt-rt 对 Linux 实时性的改造
-
多核下负载均衡
-
中断负载均衡、RPS软中断负载均衡
1、多核之间的负载均衡
目前 linux 会自动帮你做 负载均衡,举个例子,你要连续拷贝 一个 1080P 的视频流,每一帧图像都是 1080P 的,你可以开4个线程,分别拷贝不同的区域,linux 会自动的将这4个线程分布到4个核上去跑。
RT进程
比如 4核的CPU , 现在有 6个RT进程,linux 会选择4个优先级最高的进程,分布到这4个核上。
普通进程 :CFS调度的,有nice 值的进程,
周期负载均衡:根据系统的 节拍,系统的核会判断自己是不是闲着,别的核是不是忙,如果闲着 就反之 也可以push任务到别的核,每个核都以劳动为乐。
IDLE时负载均衡 :当一个核进入IDLE状态时,会从别的核上pull 任务自己来做,进行负载均衡
fork exec 时:会有新的进程,task_struct ,这时候会选一个最闲的核来跑,实现负载均衡。
比如你一下跑两个a.out CPU会自动帮你分布到两个 核上。
但是 有时候,我们不想在某个CPU上跑,这时候有办法的:
写代码的时候,可以给线程,进程设置 CPU的 affinity
taskset -a 进程 所有的线程, -p 第几个CPU ,最后是 pid
跑一个例子,一个进程 创建两个线程
#include <stdio.h>
#include <pthread.h>
#include <stdio.h>
#include <linux/unistd.h>
#include <sys/syscall.h>
static pid_t gettid( void )
{
return syscall(__NR_gettid);
}
static void *thread_fun(void *param)
{
printf("thread pid:%d, tid:%d pthread_self:%lu\n", getpid(), gettid(),pthread_self());
while(1);
return NULL;
}
int main(void)
{
pthread_t tid1, tid2;
int ret;
printf("thread pid:%d, tid:%d pthread_self:%lu\n", getpid(), gettid(),pthread_self());
ret = pthread_create(&tid1, NULL, thread_fun, NULL);
if (ret == -1) {
perror("cannot create new thread");
return -1;
}
ret = pthread_create(&tid2, NULL, thread_fun, NULL);
if (ret == -1) {
perror("cannot create new thread");
return -1;
}
if (pthread_join(tid1, NULL) != 0) {
perror("call pthread_join function fail");
return -1;
}
if (pthread_join(tid2, NULL) != 0) {
perror("call pthread_join function fail");
return -1;
}
return 0;
}
在 有 2个核的 Linux 上跑:
运行 a.out 之后, top 命令查看,可以看到 稳定后 ,大约在 200% 。这是因为 top 命令是以进程视角看的 CPU利用率,1个核 100%, 两个核 最大 200%, 所以 a.out 被平均分到两个核上跑。
使用 taskset 可以看出 3=011,是跑在两个核上的。
此时 可以用 top -H 切换为 线程视角,然后按1显示 CPU信息来确认一下
执行完 taskset -a -p 1 24276 之后, cpu 利用率就下来了,因为现在就一个核了,最大100%
top 命令上 有 si hi 代表的是 软中断 硬中断的时间比例。wa 是等硬盘的时间。id 是空闲时间。
有时候 负载均衡不单单是 线程的开销,有时候大量的中断处理的开销也是需要做负载均衡的。
在Linux 里面 中断不支持嵌套,当大量的中断来的时候,需要注意中断的负载均衡。
对于一个多核的CPU来说,都有一个 中断控制器(可配置),中断控制器下可以接很多的中断源,
关于 网卡的中断到底发给哪个核,其实是可以配置的, Linux 有api的 ,
有可能会遇到一些高端的网卡,一个网卡 有 1个 ip 地址,但是硬件上就有几个队列(multi queue),每个队列都可以产生一个中断,这种就是 网卡硬件上就支持负载均衡的,这样的情况,就可以将中断分配到不同的核上去,可以通过下面的方法来设置。需要先找到中断号。
cat /proc/interrupt 指的都是硬中断
在虚拟机上,用 qmeu 模拟跑一个 cotex-A9 的 4核的板子,其中 将 主机的tcp 1235端口,udp 5001端口 都 映射到ARM板子上,这样在主机的 tcp 1235 udp 5001端口收到数据的时候,ARM板子的 对应端口也可以收到数据。
在 虚拟机主机上 执行 nc 127.0.0.1 1235 ,多发几次包。
然后在 arm 板子上 观察 cat /proc/interrupt ,发现所有的中断都在 cpu0上。
现在 要设置 在 CPU1 上收中断,需要执行下面命令,2就是 二进制的10 ,设置的是CPU1
echo 2 > /proc/irq/31/smp_affinity
在虚拟主机上重新发包,然后在arm 板子上 观察
下面的命令是设置CPU3 来收中断
echo 4 > /proc/irq/31/smp_affinity
如果要想4个CPU 都可以接收中断,可以将掩码设置为
echo f > /proc/irq/31/smp_affinity
一般来说,CPU0接收到的硬中断,抛出来的软中断,也是在CPU0上的。
CPU1接收到的硬中断,抛出来的软中断,是在CPU1上的
如果有些网卡,不支持这种 硬中断 的负载均衡呢, 谷歌工程师曾经向 linux内核 提了一个非常重要的补丁,可以调整 软中断的负载均衡。让多个核来处理协议栈,收包的程序。
下面设置 让4个核来进行处理软中断
echo 0xF > /sys/class/net/eth1/queues/rx-0/rps_cpus
测试:
echo 1 > /proc/irq/31/smp_affinity
echo 0xF > /sys/class/net/eth1/queues/rx-0/rps_cpus
发包收包之后,可以看到 软中断已经分布到4个核了
当我们在做一个网络产品的时候,比如 CPU 有 16个核,但是 有 4个网卡(4个队列),这个时候是需要做 软中断的负载均衡的,rps的补丁的。
2 硬实时
可以使用 cyclictest 来测试 系统的延迟
注意,应用程序设置的 优先级、top命令显示的优先级、内核的优先级 是不同的,是有一个换算关系的。
应用程序最高的优先级 99,在 top 命令显示 RT 进程
应用程序优先级 98 ,top命令显示 -99
应用程序优先级 97 ,top命令显示 -98
普通进程,使用nice 值的,top命令显示 20
linux 是一个抢占的操作系统,但不是一个硬实时的操作系统,因为在linux 系统中,有一些地方是不可以抢占的,比如下图,红色代表不可以抢占的,绿色可以抢占。
那什么时候不可抢占呢?
中断、软中断、内核中的spin_lock 这三种情况是不可以抢占的。
这部分可以参考我写的另外一篇文章:
如果想在 Linux 上 实现 硬实时,需要打上 RT补丁,RT补丁就是要攻克上面的几种情况。
把本来不能抢占的,变的可以抢占了,这是因为 中断线程化,软中断线程化,spinlock的改变。
有时候 优先级并不能解决问题,在嵌入式领域,有一些场景是 周期性任务 ,这时候就可以考虑使用 sche_deadline 的调度策略。
下面的文档是专门介绍 这个调度策略的
https://elinux.org/images/f/fe/Using_SCHED_DEADLINE.pdf
现在的调度策略 有三种
sche_deadline 涉及三个参数
period 就是周期
runtime 就是在一个周期内,你要运行的时间
dealine 就是 runtime 要在 deadline 之前跑完, deadline 可以 等于 period,大部分情况是相等。
可参考 Documentation/scheduler/sched-deadline.txt 中 给线程设置 deadline 策略的方法
#define _GNU_SOURCE
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <linux/unistd.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <sys/syscall.h>
#include <pthread.h>
#define gettid() syscall(__NR_gettid)
#define SCHED_DEADLINE 6
/* XXX use the proper syscall numbers */
#ifdef __x86_64__
#define __NR_sched_setattr 314
#define __NR_sched_getattr 315
#endif
#ifdef __i386__
#define __NR_sched_setattr 351
#define __NR_sched_getattr 352
#endif
#ifdef __arm__
#define __NR_sched_setattr 380
#define __NR_sched_getattr 381
#endif
static volatile int done;
struct sched_attr {
__u32 size;
__u32 sched_policy;
__u64 sched_flags;
/* SCHED_NORMAL, SCHED_BATCH */
__s32 sched_nice;
/* SCHED_FIFO, SCHED_RR */
__u32 sched_priority;
/* SCHED_DEADLINE (nsec) */
__u64 sched_runtime;
__u64 sched_deadline;
__u64 sched_period;
};
int sched_setattr(pid_t pid,
const struct sched_attr *attr,
unsigned int flags)
{
return syscall(__NR_sched_setattr, pid, attr, flags);
}
int sched_getattr(pid_t pid,
struct sched_attr *attr,
unsigned int size,
unsigned int flags)
{
return syscall(__NR_sched_getattr, pid, attr, size, flags);
}
void *run_deadline(void *data)
{
struct sched_attr attr;
int x = 0;
int ret;
unsigned int flags = 0;
printf("deadline thread started [%ld]\n", gettid());
attr.size = sizeof(attr);
attr.sched_flags = 0;
attr.sched_nice = 0;
attr.sched_priority = 0;
/* This creates a 10ms/30ms reservation */
attr.sched_policy = SCHED_DEADLINE;
attr.sched_runtime = 10 * 1000 * 1000;
attr.sched_period = attr.sched_deadline = 30 * 1000 * 1000;
ret = sched_setattr(0, &attr, flags);
if (ret < 0) {
done = 0;
perror("sched_setattr");
exit(-1);
}
while (!done) {
x++;
}
printf("deadline thread dies [%ld]\n", gettid());
return NULL;
}
int main (int argc, char **argv)
{
pthread_t thread;
printf("main thread [%ld]\n", gettid());
pthread_create(&thread, NULL, run_deadline, NULL);
sleep(10);
done = 1;
pthread_join(thread, NULL);
printf("main dies [%ld]\n", gettid());
return 0;
}
使用 sched_setattr 的时候 要用 root 权限,或者是给普通用户,用 setcap 设置能力才可以运行。
也可以使用 chrt 命令来修改 DEADLINE 策略。