Q: SQL中聚合函数和GROUP BY是如何实现的?
GROUP BY的两种核心实现方式:
1. Hash Aggregation(最常用):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 执行流程: 表扫描 → 对每行计算group key的hash → 在hash table中查找对应的group → 找到: 更新该group的聚合累加器 → 未找到: 创建新group entry
Hash Table结构: ┌─────────┬─────────────────────────┐ │ Hash Key│ Aggregation State │ ├─────────┼─────────────────────────┤ │ "北京" │ count=1523, sum=456789 │ │ "上海" │ count=2341, sum=789012 │ │ "深圳" │ count=891, sum=234567 │ └─────────┴─────────────────────────┘
优点: O(N)时间复杂度, 适合大量分组 缺点: 内存开销(hash table可能很大),结果无序
|
2. Sort Aggregation:
1 2 3 4 5 6 7 8 9 10
| 执行流程: 表扫描 → 按group key排序 → 顺序扫描,相邻相同key的行聚合
适用场景: - 数据已按group key有序(如聚簇索引) - 结果需要有序输出(ORDER BY与GROUP BY相同) - 分组数很少时排序开销可接受
优点: 结果天然有序,内存开销小(只需维护当前group的状态) 缺点: 排序代价O(N log N)
|
聚合函数的执行机制:
| 聚合函数 |
内部状态 |
更新操作 |
最终结果 |
| COUNT(*) |
int64 counter |
counter++ |
counter |
| SUM(x) |
running_sum |
sum += x |
sum |
| AVG(x) |
sum + count |
sum+=x, count++ |
sum/count |
| MAX(x) |
current_max |
max = max(max, x) |
max |
| MIN(x) |
current_min |
min = min(min, x) |
min |
| COUNT(DISTINCT x) |
HashSet |
set.insert(x) |
set.size() |
分布式场景的两阶段聚合:
1 2 3 4 5 6 7 8 9
| SELECT city, SUM(amount) FROM orders GROUP BY city;
|
注意: COUNT(DISTINCT)不能简单分阶段合并(各节点的distinct集合可能有重叠),需要用HyperLogLog近似或全局shuffle后去重。
Q: SQL中JOIN是如何实现的?有什么优化方法?
三种JOIN实现算法详解:
1. Nested Loop Join (NLJ):
1 2 3 4 5 6 7 8 9
| for each row r in outer_table: // 外表(驱动表) for each row s in inner_table: // 内表(被驱动表) if r.key == s.key: emit(r, s)
复杂度: O(M × N) 优化: Index Nested Loop Join → 内表的join key有索引时, 内层改为index lookup: O(M × logN) 适用: 外表很小(几行) + 内表有索引
|
2. Hash Join(等值连接最高效):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| // Build阶段: 对小表建hash table build_table = smaller_table hash_table = {} for each row r in build_table: hash_table[hash(r.key)].append(r)
// Probe阶段: 扫描大表做探测 for each row s in probe_table: bucket = hash_table[hash(s.key)] for each matching row r in bucket: emit(r, s)
复杂度: O(M + N) 内存: O(min(M,N)) — build阶段需要内存容纳小表的hash table 溢出处理: Grace Hash Join — hash table放不下时分区写磁盘
|
3. Sort-Merge Join:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| // Sort阶段: 两表分别按join key排序 sort(table_A, by=key) sort(table_B, by=key)
// Merge阶段: 双指针归并 i, j = 0, 0 while i < |A| and j < |B|: if A[i].key == B[j].key: emit match, advance both elif A[i].key < B[j].key: i++ else: j++
复杂度: O(M logM + N logN + M + N) 优点: 如果数据已有序(索引/上一步排序结果),只需O(M+N) 适用: 非等值连接(range join)、数据已有序
|
优化器如何选择JOIN策略:
| 条件 |
选择的算法 |
原因 |
| 小表(<1000行) + 大表有索引 |
Index NLJ |
索引查找快 |
| 等值连接 + 内存足够 |
Hash Join |
O(M+N)最快 |
| 数据已有序 |
Sort-Merge |
免排序 |
| 非等值连接(range) |
Sort-Merge或NLJ |
Hash无法处理范围 |
| 内存不足 |
Grace Hash / External Sort-Merge |
分区溢出 |
高级JOIN优化:
- Join Reorder:多表JOIN时优化连接顺序(NP-hard问题,优化器用动态规划或贪心)
- Bloom Filter:probe前先用bloom filter快速过滤不可能匹配的行
- Partition-wise Join:分区表之间只需对应分区做join
- Semi-Join Reduction:分布式场景先传输distinct key列表过滤
Q: 数据库优化器的实现原理?如何选择执行计划?
优化器的完整工作流程:
1 2 3 4 5 6 7 8 9 10 11
| SQL语句 ↓ Parser (语法分析) AST (抽象语法树) ↓ Binder (语义分析: 解析表名、列名、类型检查) 逻辑计划 (关系代数表达式) ↓ Logical Optimizer (规则优化: 谓词下推、列裁剪等) 优化后的逻辑计划 ↓ Physical Planner (枚举物理实现 + Cost Model选择) 物理执行计划 ↓ Executor (执行) 结果集
|
Cost Model的组成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| Cost = IO_Cost + CPU_Cost + Network_Cost(分布式)
IO_Cost: - 顺序扫描: pages × seq_page_cost - 随机读取: rows × random_page_cost (通常 4× seq_page_cost) CPU_Cost: - 行处理: rows × cpu_tuple_cost - 索引比较: rows × cpu_index_cost - 表达式计算: rows × cpu_operator_cost
关键输入 — 统计信息: - 表行数(pg_class.reltuples) - 列基数(n_distinct) - 值分布直方图(most_common_vals + histogram_bounds) - 空值比例(null_frac) - 平均列宽(avg_width)
|
选择率(Selectivity)估算:
1 2 3 4 5 6 7 8 9 10 11
| WHERE age > 30
WHERE city = '北京'
WHERE age > 30 AND city = '北京'
|
物理计划枚举:
| 逻辑算子 |
可选物理实现 |
| Scan |
SeqScan, IndexScan, IndexOnlyScan, BitmapScan |
| Join |
NestedLoop, HashJoin, MergeJoin |
| Aggregate |
HashAggregate, SortAggregate |
| Sort |
External Sort, In-memory Sort, Index利用有序性 |
如何看执行计划(MySQL/PostgreSQL):
1 2 3 4 5 6 7 8 9 10 11
| EXPLAIN ANALYZE SELECT * FROM orders JOIN customers ON orders.cust_id = customers.id WHERE orders.amount > 1000;
|
Q: Buffer Pool中脏页如何管理?数据丢失如何解决?
Buffer Pool脏页管理机制:
1 2 3 4 5 6 7 8 9 10
| Buffer Pool结构: ┌──────────────────────────────────────────┐ │ Page 1 (clean) │ Page 2 (dirty) │ ... │ │ Page N (dirty) │ Page M (clean) │ ... │ └──────────────────────────────────────────┘ ↕ LRU链表管理 ↕ 脏页标记: 每个buffer frame有dirty bit - 修改页时: 标记dirty = true - 刷盘后: dirty = false
|
脏页刷盘策略(以InnoDB为例):
| 策略 |
触发条件 |
说明 |
| LRU淘汰 |
Free list为空需要新页 |
淘汰LRU尾部页,脏页先刷盘再淘汰 |
| Checkpoint |
定期或redo log空间不足 |
批量刷脏页,推进LSN |
| Flush List |
后台线程(page cleaner) |
按oldest_modification排序刷最旧的脏页 |
| 紧急刷盘 |
redo log即将写满 |
同步刷脏页直到释放足够redo空间 |
WAL保证数据不丢失:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Write-Ahead Logging原则: 修改数据前,先将修改内容写入redo log并确保持久化
事务提交流程: 1. 修改Buffer Pool中的页 (内存中修改, 快) 2. 生成redo log record写入Log Buffer 3. commit时: Log Buffer fsync到磁盘 (顺序IO, 快) 4. 返回commit成功给用户 5. 脏页可以延后刷盘 (随时可以从redo log恢复)
崩溃恢复: 1. 找到最后一个checkpoint的LSN 2. 从该LSN开始重放redo log (前滚, redo) 3. 对未提交事务回滚 (用undo log)
|
关键概念:
| 概念 |
含义 |
| LSN (Log Sequence Number) |
redo log中每条记录的唯一递增编号 |
| Checkpoint |
已刷盘的脏页对应的最大LSN,恢复只需从此处开始 |
| Doublewrite Buffer |
防止partial page write导致数据损坏 |
| fsync |
确保数据从OS缓存写到磁盘持久化 |
Q: LRU算法有什么缺陷?如何改进?
LRU的经典问题:
问题1: 预读失效(Prefetch Pollution)
1 2 3 4 5 6
| 正常访问模式: 热页 [A, B, C, D] 在LRU前部
预读触发: 读取page X, OS预读了X+1, X+2, X+3到LRU头部 LRU变化: [X+3, X+2, X+1, X, A, B, C, D] ↑ 热页被挤到尾部 如果X+1/X+2/X+3从未被真正访问 → 白白占据热位置
|
问题2: 缓存污染(Buffer Pool Pollution)
1 2 3 4 5 6
| 全表扫描(100万行): 顺序读取每个page,每个page只用一次 LRU: [page_1M, ..., page_2, page_1, 之前的热页全被挤出] 结果: 一次全表扫描把整个buffer pool的热数据冲走 后续正常查询全部cache miss → 性能骤降
|
改进方案:
1. MySQL InnoDB的分段LRU (Young/Old):
1 2 3 4 5 6 7 8 9 10 11 12
| Buffer Pool LRU链表: ┌──── Young区 (5/8) ────┐┌──── Old区 (3/8) ────┐ │ 真正的热页 ││ 新进入/待观察的页 │ │ (频繁访问才能待在这) ││ (首次访问进入这里) │ └────────────────────────┘└─────────────────────┘ ↑ ↑ 被再次访问且距上次访问>1s 新页进入点(midpoint insertion) 才从Old提升到Young
全表扫描时: 页进入Old区 → 短时间内不会被再次访问 → 不会提升到Young → 被快速淘汰 → 不影响Young区的热页!
|
2. LRU-K (数据库学术界方案):
1 2 3 4 5 6
| LRU-1: 标准LRU, 一次访问就进入缓存 LRU-2: 页被访问2次才进入正式缓存, 第一次只进入历史队列 LRU-K: 页被访问K次才进入, 用第K次的访问时间排序
优点: 有效过滤一次性访问 缺点: 需要维护访问历史,复杂度高
|
3. CLOCK算法(Linux页面置换):
1 2 3 4 5 6 7
| 环形缓冲 + reference bit: - 页被访问时: reference bit = 1 - 需要淘汰时: 指针扫描 - bit=1: 改为0 (给第二次机会), 跳过 - bit=0: 淘汰该页
优点: 近似LRU效果,开销低(不需要维护链表)
|
4. ARC (Adaptive Replacement Cache):
1 2 3 4 5
| 自适应在LRU和LFU之间平衡: - T1: 最近访问一次的页(LRU顺序) - T2: 最近访问多次的页(LFU顺序) - 根据命中模式动态调整T1/T2的大小比例 - ZFS和一些数据库使用
|
Q: 并发B+树如何实现?
Latch Crabbing(蟹行协议)——最经典方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 查找(读操作): 1. 加Root的S-latch(共享锁) 2. 进入子节点, 加子节点S-latch 3. 释放父节点S-latch (已经不需要了) 4. 重复直到叶节点
插入(写操作): 1. 加Root的X-latch(排他锁) 2. 进入子节点, 加子节点X-latch 3. 如果子节点"安全"(不会分裂: 当前key数 < max-1): → 释放所有祖先节点的X-latch (它们不会被修改) 4. 如果子节点"不安全": → 保持所有祖先的锁 (可能需要分裂传播到父节点)
"安全"判定: 插入安全: 节点未满 (不会分裂) 删除安全: 节点超过半满 (不会合并)
|
Latch Crabbing的问题和优化:
| 方案 |
思路 |
优缺点 |
| 基本Crabbing |
写操作自顶向下加X-latch |
Root锁竞争严重(高并发瓶颈) |
| 乐观Latch |
写时先乐观用S-latch下行,到叶节点才加X-latch |
减少上层锁竞争,分裂时需要重试 |
| B-link Tree |
每个节点加right-link指针 |
分裂时只锁当前节点(半个节点通过right-link可达) |
| Latch-Free B-Tree |
CAS + epoch-based reclamation |
最高并发,实现复杂 |
B-link Tree的巧妙设计:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 标准B+树分裂需要同时锁父和子: [Parent: ..., 20, ...] ↓ [Child: 1,3,5,7,9,12,15,18] → 满了需要分裂
B-link Tree: 节点带right pointer 分裂步骤: 1. 只锁Child,分裂为 [1,3,5,7] →right→ [9,12,15,18] 2. 释放Child锁 3. 锁Parent,插入新key和指针 如果步骤2-3之间有读者: 读者到达旧Child,发现要找的key > high_key → 跟随right pointer到新节点 → 无需锁父节点也能找到数据!
|
Q: 火山模型和其他执行模型的区别?
三种数据库执行模型对比:
1. 火山模型 (Volcano/Iterator/Tuple-at-a-time):
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Operator { virtual void open() = 0; virtual Tuple* next() = 0; virtual void close() = 0; };
auto* project = new ProjectOp(new FilterOp(new SeqScanOp("table"))); project->open(); while (auto* tuple = project->next()) { output(tuple); }
|
| 优点 |
缺点 |
| 通用:任意算子可组合 |
虚函数调用开销(每行多次) |
| 内存友好(一次一行) |
CPU cache不友好(数据不连续) |
| 实现简单 |
解释执行开销大 |
2. 物化模型 (Materialization/Operator-at-a-time):
1 2 3 4 5 6 7 8
| vector<Tuple> SeqScan::execute() { return read_all_from_table(); } vector<Tuple> Filter::execute() { auto input = child->execute(); return filter(input, predicate); }
|
| 优点 |
缺点 |
| 无虚函数调用开销 |
中间结果可能很大(内存问题) |
| 可批量优化(SIMD) |
不适合大数据量 |
3. 向量化模型 (Vectorized/Batch-at-a-time) — 现代OLAP主流:
1 2 3 4 5 6 7 8
| class Operator { virtual ColumnBatch* next_batch() = 0; };
|
| 维度 |
火山模型 |
向量化模型 |
加速比 |
| 虚函数开销/行 |
1次 |
1/1024次 |
1024x |
| SIMD利用 |
无 |
列式batch天然适合 |
4-8x |
| Cache利用 |
差(行式散乱) |
好(列连续) |
2-5x |
| 总体性能 |
baseline |
10-100x(OLAP) |
- |
典型数据库的选择:
- 火山模型:MySQL、PostgreSQL(传统OLTP)
- 向量化模型:ClickHouse、DuckDB、Velox(现代OLAP)
- JIT编译:Hyper、MemSQL(编译执行计划为机器码,消除解释开销)
Q: 行存和列存的应用场景?
核心设计差异:
1 2 3 4 5 6 7 8
| 行存(Row Store): 列存(Column Store): ┌─────────────────────────┐ ┌──────┬───────┬────────┐ │ id=1, name="张", age=25 │ │ id列 │ name列│ age列 │ │ id=2, name="李", age=30 │ │ 1 │ 张 │ 25 │ │ id=3, name="王", age=28 │ │ 2 │ 李 │ 30 │ └─────────────────────────┘ │ 3 │ 王 │ 28 │ └──────┴───────┴────────┘ 一行数据物理连续 一列数据物理连续
|
性能对比:
| 操作 |
行存 |
列存 |
原因 |
| 单行读取(SELECT * WHERE id=1) |
快(一次IO读完整行) |
慢(需读多个列文件) |
行连续 vs 列分散 |
| 单行写入(INSERT/UPDATE) |
快(写入一个位置) |
慢(修改多个列文件) |
一个 vs 多个位置 |
| 聚合查询(SELECT AVG(age)) |
慢(读整行但只用1列) |
快(只读age列) |
列裁剪 |
| 扫描分析(WHERE age>25) |
慢(读大量无关数据) |
快(只扫age列) |
数据量减少 |
| 压缩率 |
低(混合类型) |
高(同类型连续) |
同类型更易压缩 |
| SIMD加速 |
困难 |
容易(列连续处理) |
数据布局对齐 |
典型应用场景:
| 场景 |
推荐存储 |
代表系统 |
| OLTP(事务处理) |
行存 |
MySQL, PostgreSQL |
| OLAP(分析) |
列存 |
ClickHouse, Snowflake |
| 时序数据 |
列存 |
InfluxDB, TimescaleDB |
| 数据湖 |
列存文件格式 |
Parquet, ORC |
| 混合负载(HTAP) |
行列混合 |
TiDB(行+TiFlash列), SAP HANA |
| AI特征存储 |
列存 |
快速读取特征子集 |
列存的压缩优势:
- 同一列数据类型相同,pattern重复 → Run-Length Encoding, Dictionary Encoding有效
- 典型压缩率:列存5-10x vs 行存2-3x
- 压缩后IO更少 → 进一步加速分析查询
Q: Linux中断处理模块如何实现?CPU如何处理多个中断?
中断处理的完整流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 硬件设备产生中断信号 ↓ 中断控制器(APIC/GIC)接收 ↓ (IRQ编号 + 优先级仲裁) CPU响应中断: 1. 关闭本CPU中断(cli) 2. 保存当前上下文(寄存器压栈) 3. 跳转到中断向量表[IRQ]对应的handler ↓ ┌──────── 上半部(Top Half) ────────┐ │ 硬中断处理(irq_handler): │ │ - 读取硬件状态/数据 │ │ - ACK中断控制器 │ │ - 标记下半部工作 │ │ - 执行时间: 必须<100us │ └──────────────────────────────────┘ ↓ (开中断, 恢复执行) ┌──────── 下半部(Bottom Half) ──────┐ │ 延后执行的耗时工作: │ │ - Softirq: 高频(如网络包处理) │ │ - Tasklet: 基于softirq的封装 │ │ - Workqueue: 可睡眠(如磁盘IO) │ │ - Threaded IRQ: 中断线程化 │ └──────────────────────────────────┘
|
上下半部的对比:
| 维度 |
上半部(硬中断) |
下半部(软中断/workqueue) |
| 执行时机 |
立即 |
延后(软中断在硬中断返回后, WQ在内核线程中) |
| 可否睡眠 |
不可 |
Softirq不可, Workqueue可以 |
| 抢占 |
不可被同级抢占 |
可被硬中断抢占 |
| 执行时间 |
极短(<100us) |
可较长 |
| 典型操作 |
读硬件寄存器、ACK |
数据处理、协议栈 |
多中断处理机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 场景: CPU正在处理IRQ_A时,IRQ_B到来
处理策略: 1. 同优先级/低优先级: 排队等待(中断被关闭或标记pending) 2. 高优先级: - x86: 处理硬中断时关闭同级中断,不支持嵌套 - ARM GIC: 支持优先级抢占(高优先级中断可打断低优先级handler)
中断亲和性(Affinity): /proc/irq/N/smp_affinity → 指定哪些CPU处理该中断 用途: 网卡中断绑定到特定核,避免cache bounce
多队列网卡(MSI-X): 一块网卡有多个中断向量(如64个) 每个队列的中断分发到不同CPU核 → 并行处理网络包
|
NAPI(网络中断优化):
1 2 3 4 5 6 7 8 9
| 高速网络场景: 每秒百万包,每包一个中断 → CPU被中断淹没
NAPI解决方案: 中断+轮询混合模式 1. 首个包到达: 触发硬中断 2. 硬中断handler: 关闭网卡中断, 启动轮询(poll) 3. 软中断中: 批量从网卡buffer读取数据包(一次处理多个) 4. 读完后: 重新开启网卡中断
效果: 高负载时接近轮询效率,低负载时保持中断的低延迟
|
Q: 进程和线程的区别?用户态线程的问题?
进程 vs 线程的本质区别:
| 维度 |
进程 |
线程 |
| 定义 |
资源分配的基本单位 |
CPU调度的基本单位 |
| 地址空间 |
独立(4GB/128TB虚拟空间) |
共享进程地址空间 |
| 创建开销 |
大(~1ms, fork需复制页表/fd表) |
小(~10-100us, 共享一切) |
| 上下文切换 |
慢(需切换页表/刷TLB/切换寄存器) |
快(只切换寄存器和栈指针) |
| 通信 |
需IPC(pipe/socket/shm) |
直接访问共享内存 |
| 隔离性 |
强(一个崩不影响另一个) |
弱(一个线程段错误整个进程崩) |
| 内核表示(Linux) |
task_struct + 独立mm_struct |
task_struct + 共享mm_struct |
Linux中进程和线程的统一表示:
1 2 3 4 5 6 7 8
|
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | ...)
|
用户态线程(Green Thread/Coroutine)的问题:
| 问题 |
原因 |
后果 |
| 阻塞IO阻塞整个进程 |
内核只看到一个线程(1:N模型) |
一个协程read()阻塞,所有协程停止 |
| 无法利用多核 |
所有协程在一个内核线程上 |
CPU密集任务无并行 |
| 无抢占式调度 |
用户态调度器无硬件timer中断 |
一个协程死循环则饿死其他协程 |
| 系统调用问题 |
系统调用是线程粒度的 |
page fault等阻塞所有协程 |
现代解决方案(M:N模型):
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Go goroutine / Rust tokio / Java虚拟线程: M个用户态协程 映射到 N个内核线程
Go的GMP模型: G (Goroutine): 用户态协程(2KB栈) M (Machine): 内核线程 P (Processor): 逻辑处理器(通常=CPU核数)
G → P → M → CPU核 关键设计: - G阻塞IO时: runtime将G挂起, P切换到另一个G(非阻塞) - G计算密集: sysmon检测长时间不让出的G,强制抢占(1.14+) - 工作窃取: P空闲时从其他P的队列偷G执行
|
Q: 用户态向内核态切换的过程?
切换的触发方式和完整过程:
触发方式:
| 触发方式 |
示例 |
特点 |
| 系统调用(syscall) |
read(), write(), mmap() |
用户主动请求内核服务 |
| 中断(interrupt) |
定时器中断、网卡中断 |
外部设备异步通知 |
| 异常(exception) |
页错误(page fault)、除零、段错误 |
CPU执行时检测到的错误 |
x86-64 syscall指令的完整过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 用户态(Ring 3): mov rax, __NR_read ; 系统调用号 mov rdi, fd ; 参数1 mov rsi, buf ; 参数2 mov rdx, count ; 参数3 syscall ; 触发切换!
CPU硬件自动完成(~100 cycles): 1. RCX = 保存用户态RIP(返回地址) 2. R11 = 保存用户态RFLAGS 3. RIP = MSR_LSTAR(内核入口地址, 即syscall handler) 4. CPL = 0 (Ring3→Ring0, 权限提升) 5. 段选择器变为内核段 6. 关闭中断(IF=0) 内核入口(entry_SYSCALL_64): ; 切换到内核栈 movq %rsp, PER_CPU(rsp_scratch) ; 保存用户栈 movq PER_CPU(kernel_stack), %rsp ; 加载内核栈(从TSS) ; 保存用户态寄存器到内核栈(pt_regs结构) pushq %rcx ; 用户RIP pushq %r11 ; 用户RFLAGS pushq %rdi, %rsi, %rdx, ... ; 保存所有寄存器 ; 根据rax系统调用号查sys_call_table call sys_call_table[rax] ; 执行实际的内核函数 ; 返回用户态 sysretq ; RIP = RCX(之前保存的返回地址) ; RFLAGS = R11 ; CPL = 3 (Ring0→Ring3)
|
完整开销分析:
| 操作 |
开销 |
说明 |
| syscall/sysret指令 |
~50-100 cycles |
硬件特权级切换 |
| 保存/恢复寄存器 |
~50 cycles |
压栈/弹栈操作 |
| 栈切换 |
~20 cycles |
用户栈→内核栈 |
| TLB/Cache影响 |
0-数千cycles |
内核代码可能导致cache eviction |
| KPTI(Meltdown缓解) |
+~500 cycles |
需要切换页表(隔离内核映射) |
| 总计 |
200-1000 cycles (100-500ns) |
取决于硬件和安全缓解措施 |
为什么vDSO可以避免切换?
1 2 3 4 5 6 7 8
| vDSO (Virtual Dynamic Shared Object): 内核将某些只读数据映射到用户空间(如时间戳) gettimeofday() 不需要真正进入内核: - 内核定期更新共享页中的时间值 - 用户态直接读取共享页 → 无syscall开销 类似: clock_gettime(), getcpu()
|
Q: 手撕:实现一个基于内存的文件系统(字典树变体)?
(编程题)
Q: 手撕:快慢指针找中点+反转链表?
(编程题)
Q: 手撕:字符串大写转小写后按字典序排序(不用库函数)?
(编程题)