image

32 位 CPU 最大只能操作 4G 内存吗?

两个基本概念:

  • CPU 位宽
  • 地址总线 位宽

地址总线:

  • 如果地址总线只有 1 条,只能表示:0、1 两种地址
  • 如果地址总线有 2 条,能表示:00、01、10、11 四种地址

CPU 想要操作内存地址,需要 地址总线

由于 CPU 能操作的地址总线长度 不应该 超出 CPU 的位宽,因此,32 位的 CPU 能操作的地址总线条数最多为 32 条,寻址空间为 2^32,也就是 4G

如果超过 32 条,32 位 CPU 一次性不能完全计算出内存地址,操作十分繁琐,严重耗费性能!

因此,32 位 CPU 理论 操作的最大内存为 4G

PAE 技术 可以使 32 位的 CPU 支持超过 4GB 的物理内存。此技术是通过在地址翻译过程中增加 额外的一级页表 来实现的,这使得 CPU 可以使用多达 36 位的物理地址,支持的物理内存 理论上可以达到最大 64GB

64 位相比 32 位 CPU 的优势在哪?64 位 CPU 的计算性能一定比 32 位 CPU 高很多吗?

问题一:

  • 单次可以计算的数字最大为 64 位,可以计算更大的数据;而 32 位 CPU 要想计算超过 32 位的数据,就需要多次分步骤计算,性能较差
  • 内存寻址空间更大:64 位 CPU 的理论寻址空间为 2^64,一般的 64 位 CPU 的地址总线为 48 位,可以允许更大的内存

问题二:

不一定,只有计算大数(超过 32 位),64 位 CPU 的性能优势才能体现出来

软件的 32 位和 64 位之间的区别?

  • 软件的 32 位和 64 位之间的区别?
  • 32 位的操作系统可以运行在 64 位的电脑上吗?
  • 64 位的操作系统可以运行在 32 位的电脑上吗?如果不行,原因是什么?

区别在于指令是 32 位还是 64 位

OS 本质也是一种软件,32 位的 OS 经过一个「兼容层」就可以在 64 位的电脑上运行

相反,64 位 OS 无法在 32 位的电脑上运行,因为 32 位 CPU 的寄存器无法存储 64 位的指令

CPU Cache

结构:

图片来自小林 coding

首先要讲一下 Cache line 的概念:

CPU Cache 是由很多个 Cache Line 组成的,Cache Line 是 CPU 从内存读取数据的 基本单位

CPU 从内存读取数据,并缓存到 L1、L2 缓存的过程,一次是读取「一批」数据,这一批数据会被存放到 Cache line 中

图片来自小林 coding

在 Linux 上,可以使用以下指令查看每一级缓存的 cache line 的大小:

root@SkyLee:~# cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
64

上面的结果表示 L1 缓存的 cache line 的大小为 64 字节

如何写出让 CPU 跑得更快的代码?

要想让 CPU 跑得更「快」,就得提高 CPU Cache 的命中率,减少内存的访问次数

从 L1 缓存的结构可以看出,有两个优化点:

  • 数据缓存
  • 指令缓存

提高数据缓存命中率

分析下面的代码,哪个更快?

// code 1
const int N = 10000;
int main(void)
{
    int arr[N][N];
    for (int i = 0; i < N; ++i)
        for (int j = 0; j < N; ++j)
            arr[i][j] = 0;
}
// code 2
const int N = 10000;
int main(void)
{
    int arr[N][N];
    for (int i = 0; i < N; ++i)
        for (int j = 0; j < N; ++j)
            arr[j][i] = 0;
}

对于 c 来说,答案是第一种方式更快

因为 c 的二维数组的存储方式是 按行存储,例如 arr[2][2] 的存储顺序为: arr[0][0]arr[0][1]arr[1][0]arr[1][1]

前面提到,CPU 从内存读取数据的基本单位是 Cache Line

那么,如果 Cache Line 为 64 字节,第一次就可以读取 arr[0][0] ~ arr[0][15] 共 64 字节的数据到 L1 Cache 的数据缓存部分,后续访问 arr[0][1] ~ arr[0][15] 时,就不用读取内存,直接读 L1 Cache 即可

如果采取按行遍历,就可以大大提高数据缓存的命中率,进而提升性能

提高指令缓存命中率

观察下面代码:

const int N = 100;
std::vector<int> arr(N);

for (int i = 0; i < N; ++i)
    arr[i] = rand() % 100;

std::sort(arr.begin(), arr.end()); // 1

for (int i = 0; i < N; ++i) // 2
{
    if (arr[i] < 50)
        arr[i] = 0;
}
  • 先执行 1 后执行 2
  • 先执行 2 后执行 1

哪个更快?

答案是:先执行 1 后执行 2

CPU 有一个功能叫做 分支预测

如果分支预测可以预测到接下来要执行 if 里的指令,还是 else 指令的话,就可以「提前」把这些指令放在指令缓存中 ,这样 CPU 可以直接从 Cache 读取到指令,于是执行速度就会很快。

因此,先升序排序,那么小于 50 的元素就会被 集中 放在前面,大概率 会命中指令缓存,性能更高

提高多核 CPU 的缓存命中率

现在的 CPU 基本上都是多核的,每个核有独立的 L1、L2 Cache

而线程可以在不同核上交替执行,如果一个线程在多个核之间来回切换,缓存命中率就会下降

如果想避免这种情况,可以将某个线程「绑定」在一个核心上

Linux 提供了 sched_setaffinity 方法做到这一点

缓存一致性问题

数据写入内存的步骤?

首先说说数据读取的步骤:

  • 检查 L1 Cache 有没有该数据,如果没有:
  • 检查 L2 Cache 有没有该数据,如果没有:
  • 检查 L3 Cache 有没有该数据,如果没有:
  • 从内存读取数据

再来讨论数据写入的步骤

写直达

写直达会将数据 同时 写到 CPU Cache(如果数据存在)和 Memory

优点就是 确保了数据的一致性

但这种方式,会 浪费很多时间、性能(内存写入速率远小于 CPU Cache)

写回

在写入前,先检查 CPU Cache 中有没有该数据:

  • 如果没有,那直接写到内存
  • 如果有,写到 CPU Cache,并打上「脏」标记,代表该数据被修改过

「写回」方式不会将新的数据直接写到内存,真正的写入内存时机,是在 该 Cache Block 被替换时

图片来自小林 coding

这就是「懒更新」的思想,线段树也用到了该思想

优点就是性能高,不用每次都写内存

缺点就是存在数据不一致问题

假设有一个变量 i(初始值为 0),且核心 A、B 的 L1 Cache 都有变量 i,初始,线程 T 在核心 A 上运行:

  • T 修改 i 的值为 1
  • 发生调度,T 下处理机
  • 发生调度,T 上处理机,在核心 B 运行
  • T 想要读取 i,但是,B 的 L1 Cache 并没有更新,此时,T 就读到了脏数据!

那么,如何保证 CPU Cache 之间的一致性呢?有两种方式:

  • 总线嗅探
  • MESI 协议

总线嗅探

总线嗅探的基本原理是:

  • 每个 CPU 核心监听总线上的事件
  • 当 CPU 更新修改变量 i 的值时,将这个事件广播到总线
  • 其它 CPU 监听到该事件,检查自己的缓存有没有该数据,如果有,那么更新

由于 CPU 需要每时每刻监听总线上的一切活动,但是不管别的核心的 Cache 是否缓存相同的数据,都需要发出一个广播事件,这无疑会 加重总线的负载

此外,总线嗅探看似解决了上面的数据不一致,但是,如果核心 B 的 L1 Cache 一开始没有变量 i:

  • T 修改 i 的值为 1,广播该事件到总线
  • 核心 B 监听到该事件,但自己的缓存没有 i,忽略
  • 发生调度,T 下处理机
  • 发生调度,T 上处理机,在核心 B 运行
  • T 想要读取 i,由于核心 B 的缓存没有变量 i,于是访问内存
  • 但是,修改后的 i,还没有写入内存,此时,T 就读到了脏数据!

因此,总线嗅探 不能完全解决 CPU Cache 的一致性问题

事实上,要解决 CPU Cache 的一致性问题,需要满足两个条件:

  • 写传播:修改 L1 Cache 的消息,必须传播到其它核心
  • 事务串行化:某个变量在一个核心的操作顺序,在其它核心看起来必须是一致的

图片来自小林 coding

总线嗅探仅满足了第一个条件

MESI 协议

MESI 是:Modified, Exclusive, Shared, and Invalid 的缩写,具体含义如下

  • M:已修改
  • E:独占
  • S:共享
  • I:无效

这四个状态是 Cache Line 的基本状态,在运行时,四个状态之间可以相互转换(状态机)

  • 「独占」的含义是:该数据只有当前的 CPU Cache 有,其它没有
  • 「共享」的含义是:该数据除了当前的 CPU 有以外,其它某些 CPU 也有
  • 「已修改」的含义是:该数据在当前的 CPU 有过修改(与内存不一致了)
  • 「无效」的含义是:该 Cache Block 无效

处于「独占」和「已修改」状态的 Cache Line,可以直接修改数据而不通知其它核心,解决了 总线嗅探 的性能浪费问题

处于「共享」状态的 Cache Line,若要修改数据,需要 通知 其它共享该 Cache Line 的核心,将「共享」状态修改为「无效」状态,并且,本核心的状态修改为「已修改」

处于「无效」的 Cache Line,核心 不可以读取

对于上面的示例,如果使用 MESI 协议:

如果 Core1 想读取 i 的值:

补充:

状态转换规则:

图片来自小林 coding

此外,还有一个 网站 帮忙理解 MESI

CPU 是如何执行任务的?

读写数据时的问题

假设现在有一个双核 CPU

我们假设有两个线程 a、b,分别运行在核心 A、B 上

有两个 long 类型的变量 v0、v1,二者之间 没有任何关系

由于 v0、v1 的 存储位置恰好十分接近,导致 CPU 读取 v0 的时候,恰好 将 v1 也一并读入到 同一个 Cache Line 中,问题就产生了:

如果核心 A 修改 v0 的值,由于核心 B 也有 v0 的值,因此,核心 B 的 v0、v1 所对应的 Cache Line 的状态为 I(无效)

此时,线程 b 想要读取 v1 的值,由于 v1 所在的 Cache Line 的状态为 I,因此需要:

  • 核心 A 将 v0、v1 所在的 Cache Line 写回内存,状态修改为 S
  • 核心 B 从内存中读取 v1,状态也为 S

如果线程 b 修改 v1 的值,也会影响线程 a 对 v0 读取

可以发现,这种情况的出现,会 十分影响性能,因为 Cache miss

什么是伪共享?

上面提到的场景就是伪共享问题:这种因为多个线程同时读写同一个 Cache Line 的不同变量时,而导致 CPU Cache 失效的现象称为伪共享(False Sharing)

怎么解决伪共享带来的性能损失?

要想解决伪共享问题,需要避免一些不相关变量位于同一个 Cache Line 中

例如,对于下面这个结构体:

struct Foo{
    int a;
    int b;
};

有可能 a、b 会因为位于同一个 Cache Line,出现伪共享问题

在 Linux 中,可以通过 __cacheline_aligned_in_smp 宏定义来避免:

struct Foo{
    int a;
    int b __cacheline_aligned_in_smp;
};

本质上还是 填充字节以达到一个 Cache Line 的大小 来避免的,典型的用空间换时间的思想

再来看一个 Java 的一个并发框架的部分实现:

图片来自小林 coding

p1 ~ p7 的大小加起来为 56 个字节,起到填充字节的作用,避免 其它不相关变量 与 final 常量处于一个 Cache Line,导致读取 final 常量 cache miss

CPU 是如何选择任务的?

无论是进程还是线程(轻量级进程),都是用 task_struct 来表示

内核选择任务的过程,就是选择 task_struct 的过程

调度类

Linux 有三个调度类:

  • DeadLine
  • RealTime
  • Fair

图片来自小林 coding

  • SCHED_DEADLINE:选择 deadline 离当前时间最近的 task
  • SCHED_FIFO:先来先服务,但是优先级高的可以抢占
  • SCHED_RR:轮转法,但是优先级高的可以抢占
  • SCHED_NORMAL:普通任务调度策略
  • SCHED_BATCH:后台任务调度策略(优先级比前台任务低)

DeadLine 和 RealTime 调度器,都是用于 实时任务

而 Fair 用于 普通任务

完全公平调度(CFS)

对于普通任务,公平最重要,Fair 的调度,又叫做完全公平调度

算法的基本思想:

  • 为每个任务分配一个虚拟运行时间 vruntime
  • 调度时,选择 vruntime 少的,尽可能保证每个任务都有相似的时间执行

vruntime 的计算规则:

  • 对于优先级一致的任务,实际运行时间越大,vruntime 越大
  • 对于实际运行时间越大的任务,优先级越高,vruntime 越小

CPU Runtime Queue

图片来自小林 coding,csf_rq 应为 cfs_rq

三个队列的优先级为:DeadLine > RealTime > Fair

也就是说:实时任务总是比普通任务先执行

调整优先级

任务的优先级 priority 的范围为 [0, 139],priority 越小,优先级越大

  • 实时任务 priority 的范围为 [0, 99]
  • 普通任务 priority 的范围为 [100, 139]

对于普通任务,可以通过调整 Nice 值,来调整优先级

nice 值的范围为 [-20, 19]

普通任务 new_priority = old_priority + nice

因此,如果要想提高普通任务的优先级,可以降低 nice 值

在启动任务的时候,可以指定 nice 值

nice -n -1 ./helloworld

也可以调整一个运行中的进程的 nice 值

# 查看原 nice 值
root@SkyLee:~# ps -o nice -p 1050
 NI
  0
# 修改 nice 值
root@SkyLee:~# renice -1 -p 1050
1050 (process ID) old priority 0, new priority -1
# 查看新 nice 值
root@SkyLee:~# ps -o nice -p 1050
 NI
 -1

当然,普通任务不管怎么调整,优先级也不会比实时任务高,如果对实时性有要求,可以修改一个运行中的进程为实时任务:

# 将 pid 为 1145 的进程改为实时任务,并且优先级为 1
chrt -f 1 -p 1145

什么是中断?

中断是一种事件处理机制,OS 在收到中断请求后,会中断正在执行的进程,然后调用内核中断程序处理

但是,如果中断时间过长,会导致之前执行的进程一直等待,用户体验不好

并且,在处理中断时,OS 往往会屏蔽其它的中断,如果中断时间过长,会导致其它中断请求漏掉

什么是软中断?

内核处理中断请求的过程叫做「硬中断」,为了避免硬中断的时间过长,内核会触发一个「软中断」来处理接下来的任务,当次硬中断就结束了

  • 例如,网卡接收到数据以后,会向 OS 发起一个硬中断,OS 收到该中断,就来处理这个事件
  • OS 先屏蔽其它的中断请求,避免频繁中断降低性能
  • OS 触发软中断
  • OS 取消屏蔽其它中断,硬中断结束,由软中断处理接下来的任务
  • 软中断处理程序负责将网卡 ringbuffer 的数据取走,放到内核的接收缓冲区

可以发现,中断分为上下两部分:

  • 上部分:硬中断,快速 处理中断请求
  • 下部分:软中断,延迟 处理上部分未处理的事件,内核线程 的形式

软中断有哪些类型?

可以在 Linux 下查看 /proc/softirqs 的内容:

root@SkyLee:~# cat /proc/softirqs
                    CPU0       CPU1       
          HI:          2          0
       TIMER:   66975830   81145851
      NET_TX:          5          7
      NET_RX:    7477893    7523257
       BLOCK:       2728    3216316
    IRQ_POLL:          0          0
     TASKLET:        691      13839
       SCHED:  189534698  190142385
     HRTIMER:       1785          0
         RCU:  147754429  147986682

这里表示有 10 中软中断,例如:NET_RX 表示网络接收中断,NET_TX 表示网络发送中断、TIMER 表示定时中断、RCU 表示 RCU 锁中断、SCHED 表示内核调度中断。

如果软中断占用了较高的系统资源,怎么定位,解决?

首先使用 top 查看当前软中断占用了多少资源:

top - 10:16:08 up 38 days, 15:33,  1 user,  load average: 0.01, 0.01, 0.00
Tasks: 132 total,   1 running, 131 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.5 us,  0.5 sy,  0.0 ni, 99.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st

si 就是软中断占用 CPU 的比例,如果发现占用较高,可以使用 watch 来查看软中断次数的变化频率:

watch -d cat /proc/softirqs

如果发现某个软中断的变化频率较高,例如对于网络 IO 比较频繁的 Server,NET_RX 的变化频率需要格外注意,如果频率过高,可以使用 sar -n DEV 查看哪个网卡的数据包比较多

这里的 eth0 网卡的包比较多,可以使用 tcpdump 抓包判断有没有非法的报文,可以考虑加防火墙,或者升级带宽等等