问题
内核对进程调度时发生了什么?
进程调度的本质
任务 / 进程 切换
- 即:上下文切换,内核对处理器上的执行的进程进行切换
- "上下文" 指:寄存器的值
- "上下文切换" 指:
- 将寄存器的值保存在内存中 (进程被剥夺处理器,停止执行)
- 将另一组寄存器的值从内存中加载到寄存器 (调度下一个进程执行)
进程调度的本质
当时间片耗完,不管进程正在执行什么代码,都一定会发生上下文切换!
上下文切换必然导致进程状态的转换
上下文切换由中断触发 (时钟中断,IO中断,等)
上下文切换是在中断触发的,中断触发的时候一般会把中断给关闭,等中断服务程序执行结束再打开中断
详解 Linux 进程状态 (ps au)
helloworld.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <pthread.h>
#include <unistd.h>
static void* thread_entry(void* arg)
{
while(1);
return arg;
}
static void make_thread()
{
pthread_t tid = 0;
pthread_create(&tid, NULL, thread_entry, NULL);
}
int main(void)
{
printf("pid = %d, ppid = %d, pgid = %d\n",
getpid(), getppid(), getpgrp());
printf("hello world\n");
// make_thread();
while(1) sleep(1);
return 0;
}
编译后运行,使用 ps au 命令查看该进程的状态
进程状态为 S+,表示该进程处于可中断的睡眠状态,并且该进程位于前台进程组中
修改程序,将主进程的 sleep 去掉,查看进程的状态
helloworld.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <pthread.h>
#include <unistd.h>
static void* thread_entry(void* arg)
{
while(1);
return arg;
}
static void make_thread()
{
pthread_t tid = 0;
pthread_create(&tid, NULL, thread_entry, NULL);
}
int main(void)
{
printf("pid = %d, ppid = %d, pgid = %d\n",
getpid(), getppid(), getpgrp());
printf("hello world\n");
// make_thread();
while(1);
return 0;
}
编译后运行,使用 ps au 命令查看该进程的状态
进程的状态为 R+,表示进程正在执行,处于前台进程组中,此时该进程的 CPU 占用率是 100%
修改程序,在主进程中创建一个线程,在这个线程中也做死循环操作
helloworld.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <pthread.h>
#include <unistd.h>
static void* thread_entry(void* arg)
{
while(1);
return arg;
}
static void make_thread()
{
pthread_t tid = 0;
pthread_create(&tid, NULL, thread_entry, NULL);
}
int main(void)
{
printf("pid = %d, ppid = %d, pgid = %d\n",
getpid(), getppid(), getpgrp());
printf("hello world\n");
make_thread();
while(1);
return 0;
}
编译后运行,使用 ps au 命令查看该进程的状态
进程的状态为 Rl+,表示该进程正在执行,处于前台进程组中,并且有多个线程
此时该进程的 CPU 占用率为 189%,超过了 100%
我们使用 lscpu 命令,来查看 cpu 的信息
该 cpu 是个双核 cpu,我们猜想 helloworld.out 这个进程 cpu 占用率为 189%,是因为这个 cpu 的两个核同时在执行这两个线程
为了验证这个猜想,我们使用 taskset -c 0 helloworld.out 这个命令,让 helloworld.out 这个进程只能运行在 cpu0 上,然后使用 ps au 命令查看该进程的状态
可以看出 cpu 占用率变为了 95.5%,现在该进程的两个线程都在 cpu0 上执行
这个实验说明了线程是最小的执行单位,并且不同的线程可以同时运行于多个 cpu 上
细说空闲状态
处理器上电后,开始一直不停的向下执行指令
当系统中没有进程时,会执行一个 "不执行任何操作的" 的空闲进程
空闲进程的职责:执行特殊指令使处理器进入休眠状态 (低功耗状态)
空闲状态是一种暂态,但凡出现就绪进程,空闲状态立即结束
Linux 性能工具简介
ps -- 查看进程运行时数据 (ps au)
top -- Linux 整体性能监测工具 (类似任务管理器)
sar -- Linux 活动情况报告 (系统性能分析工具)
Linux 系统平均负载
即:Linux 系统负载平均值 (Linux System Load Averages)
该值表示的是一段时间内任务对系统资源需求的平均值 (1、5 和 15 分钟)
- 如果平均值接近 0,意味着系统处于空闲状态
- 如果平均值大于 1,意味着系统繁忙,任务需要等待,无法及时执行
- 如果 1min 平均值高于 5min 或 15min 平均值,则负载正在增加
- 如果 1min 平均值低于 5min 或 15min 平均值,则负载正在减少
详解 sar -q
runq-sz:执行队列的长度
plist-sz:运行中的任务 (进程 & 线程) 总数
ldavg-1:最近 1min 系统平均负载
ldavg-5:最近 5min 系统平均负载
ldavg-15:最近 15min 系统平均负载
系统调度观察实验
通过 Linux 性能工具观察进程调度
- 单处理器运行过程
- 多处理器运行过程
helloworld.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <pthread.h>
#include <unistd.h>
static void* thread_entry(void* arg)
{
while(1);
return arg;
}
static void make_thread()
{
pthread_t tid = 0;
pthread_create(&tid, NULL, thread_entry, NULL);
}
int main(void)
{
printf("pid = %d, ppid = %d, pgid = %d\n",
getpid(), getppid(), getpgrp());
printf("hello world\n");
// make_thread();
while(1);
return 0;
}
当前的 cpu 是双核 cpu,可以同时运行两个线程,我们使用 taskset -c 0 helloworld.out & 和 taskset -c 1 helloworld.out & 命令,分别在两个 cpu 上面运行该程序,然后使用 top -d 1 命令和 sar -q 1 命令来查看当前系统性能相关信息
可以看出最近 1min 的平均负载为 2.04,大于了处理器的数量,2 个 cpu 的占用率接近了 100%,当前系统运行繁忙,无法及时执行任务
然后将 cpu1 上运行的 helloworld.out 进程 kill 掉,再次查看结果
可以看出最近 1min 的平均负载为 1.16,cpu0 的占用率接近 100%,cpu1的占用率接近 0%
系统调度核心性能指标
吞吐量:单位时间内的工作总量 (越大越好)
- 处理器资源消耗越多 (空闲状态占比越低),吞吐量越大
- 对于进程调度而言,吞吐量指单位时间处理的进程数量
延迟:从开始处理任务到结束处理任务所耗费的时间 (越短越好)
- 对于进程而言,延迟即生命期,指进程从运行到结束所经历的时间
- 注意:运行 和 执行 不同,运行时间可能很长,但执行时间可能很短
吞吐量计算一
吞吐量计算二
吞吐量计算三
假设:每个进程固定执行 60ms
则:进程运行结束时
结论:
处理器的能力由硬件设计决定,吞吐量决定一个上限
当吞吐量未达上限,进程的延迟取决于进程自身
当吞吐量达到上限,随着进程数量增加,总延迟增加,但平均延迟不变
思考
多核吞吐量计算
现实中的系统
理想状态:进程正在执行,并且没有就绪状态的进程
空闲状态:处理器占用率低,吞吐量低
繁忙状态:
多个进程同时运行,但存在多个就绪状态的进程
此时,吞吐量很高(可能达到峰值),但总延迟会变长