第一章:Golang索引文件内存映射实战:何时该用MAP_PRIVATE vs MAP_SHARED?3个OOM惨案复盘
内存映射(mmap)是高性能索引系统(如倒排索引、LSM-tree元数据缓存)的关键优化手段,但在Go中通过syscall.Mmap直接操作时,MAP_PRIVATE与MAP_SHARED的误用极易引发静默OOM——内核无法及时回收写时复制页,导致RSS持续暴涨直至被OOM Killer终结。
mmap行为差异的本质
MAP_PRIVATE启用写时复制(COW):对映射区域的写入不落盘,仅在页表层面复制物理页;而MAP_SHARED写入直接同步至底层文件,且多个进程映射同一文件时可见彼此修改。关键陷阱在于:MAP_PRIVATE下频繁写入大索引文件(如1GB+的term dictionary),会触发大量COW页分配,但Go runtime无法感知这些内核页,runtime.ReadMemStats()完全不统计,监控告警彻底失灵。
三个真实OOM惨案
- 日志索引服务崩溃:使用
MAP_PRIVATE映射2.4GB索引文件,批量更新term位置指针,RSS从800MB飙升至16GB,被OOM Killer杀死;切换为MAP_SHARED后稳定在950MB。 - 搜索网关内存泄漏:
MAP_PRIVATE映射只读索引却意外调用unsafe.Slice并写入,触发COW,72小时后耗尽节点内存;修复:mprotect(PROT_READ)强制只读保护。 - 多实例共享索引OOM:3个Worker进程
MAP_PRIVATE映射同一索引文件并各自构建in-memory cache,物理内存占用达3×索引大小;改用MAP_SHARED+MADV_DONTFORK后降至1.1×。
正确实践代码片段
// 推荐:只读索引 → MAP_PRIVATE + PROT_READ(安全且零COW)
fd, _ := os.Open("/data/index.dat")
defer fd.Close()
data, _ := syscall.Mmap(int(fd.Fd()), 0, int64(size),
syscall.PROT_READ, syscall.MAP_PRIVATE|syscall.MAP_POPULATE)
// 关键:显式禁止写入,避免隐式COW
syscall.Mprotect(data, syscall.PROT_READ)
// 写入型索引更新 → MAP_SHARED + 同步刷盘
fd, _ := os.OpenFile("/data/index.dat", os.O_RDWR, 0)
data, _ := syscall.Mmap(int(fd.Fd()), 0, int64(size),
syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
// 修改后必须msync,确保落盘且释放COW压力
syscall.Msync(data, syscall.MS_SYNC)
第二章:内存映射底层机制与Go运行时交互原理
2.1 mmap系统调用在Linux内核中的语义差异:MAP_PRIVATE与MAP_SHARED的页表行为剖析
mmap() 的 MAP_PRIVATE 与 MAP_SHARED 在页表映射层面触发截然不同的内核路径:
页表项(PTE)标记差异
MAP_PRIVATE:设置VM_MAYWRITE | VM_SHARED=0,启用写时复制(COW)机制MAP_SHARED:设置VM_MAYWRITE | VM_SHARED=1,允许脏页直接回写到 backing file
写操作行为对比
| 行为 | MAP_PRIVATE | MAP_SHARED |
|---|---|---|
| 首次写入 | 触发 COW,分配新物理页 | 直接修改原页,标记 dirty |
| 同步到文件 | 不同步(仅父进程可见) | 调用 file_operations->writepage |
msync(MS_SYNC) |
无实际效果 | 强制回写脏页至磁盘 |
// 典型调用示例
void *addr = mmap(NULL, SIZE, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); // 匿名私有映射
该调用不关联任何文件,内核为其分配 anon_vma 结构,所有写操作均触发 COW 分配新页,且 mm_struct 中的 nr_ptes 与 nr_pmds 统计仅反映本进程视图。
graph TD
A[mmap with MAP_PRIVATE] --> B[建立只读PTE指向原始页]
B --> C[写访问触发缺页异常]
C --> D[do_wp_page: 分配新页、复制数据、更新PTE]
D --> E[后续写仅操作新页]
2.2 Go runtime对mmap区域的GC可见性约束与msync时机陷阱
Go runtime 默认不扫描mmap映射的匿名内存页,除非显式调用runtime.SetFinalizer或通过指针链路可达。这导致未被GC根引用的mmap区域中分配的Go对象(如unsafe.Slice包装的结构体)可能被提前回收。
数据同步机制
msync(MS_SYNC)强制刷回磁盘,但Go GC仅在STW阶段扫描栈、全局变量和堆指针——mmap内存若无强引用,会被视为“不可达”。
// 示例:危险的 mmap + struct 布局
data, _ := syscall.Mmap(-1, 0, 4096,
syscall.PROT_READ|syscall.PROT_WRITE,
syscall.MAP_PRIVATE|syscall.MAP_ANONYMOUS)
defer syscall.Munmap(data)
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&data))
slice := unsafe.Slice((*int)(unsafe.Pointer(&data[0])), 1024)
// ⚠️ slice 无栈/堆引用 → GC 可能回收其底层内存!
逻辑分析:
unsafe.Slice返回的切片未被任何Go变量持有,data字节切片本身不携带类型信息;GC无法识别其中嵌套的指针字段,导致悬挂引用。
关键约束对比
| 约束维度 | heap 分配 |
mmap 匿名映射 |
|---|---|---|
| GC 可见性 | ✅ 自动扫描 | ❌ 需手动注册为 root |
| 内存屏障要求 | runtime 自动插入 | 调用方需 atomic.StorePointer |
msync 时机 |
无关 | 若映射为 MAP_SHARED,须在 GC 前同步 |
graph TD
A[Go 程序分配 mmap 内存] --> B{是否建立强引用链?}
B -->|否| C[GC 视为不可达 → 回收物理页]
B -->|是| D[需确保 msync 在写入后、GC 前完成]
D --> E[否则脏页丢失或读到陈旧数据]
2.3 索引文件场景下写时复制(COW)引发的隐式内存膨胀实测分析
在 LSM-Tree 类存储引擎(如 RocksDB)中,索引文件常采用 COW 策略维护内存中 IndexBlock 的一致性。当并发写入触发频繁的 MemTable 切换时,旧索引结构不会立即释放,而是滞留至所有引用其的 SST 文件完成 compaction。
数据同步机制
COW 期间,新写入生成全新索引副本,而旧副本仍被 pending read snapshot 持有:
// 示例:RocksDB 中 IndexReader 的弱引用生命周期管理
std::shared_ptr<const IndexBlock> current_index =
index_cache_->Lookup(key, &handle); // 引用计数 +1
// …… 后续写入触发 COW → 新 shared_ptr 构造,旧对象未析构
逻辑分析:
shared_ptr延迟释放导致多个历史索引副本共存;index_cache_默认 LRU 驱逐策略无法及时清理跨 snapshot 的“幽灵引用”。
内存膨胀量化对比(16GB 内存压测)
| 场景 | 峰值 RSS (MB) | 索引副本均值数量 |
|---|---|---|
| 单 snapshot 低写入 | 1,240 | 1.1 |
| 多 snapshot 高写入 | 3,890 | 4.7 |
graph TD
A[Write Request] --> B{MemTable Full?}
B -->|Yes| C[Trigger COW: clone IndexBlock]
C --> D[Old Index held by Snapshot N]
C --> E[New Index bound to Snapshot N+1]
D --> F[Delayed release until all Snapshots drop]
该延迟释放链是隐式内存膨胀的核心动因。
2.4 /proc//smaps中Private_Dirty与Shared_Clean字段的精准解读与监控脚本
Private_Dirty 表示进程独占且已被修改、尚未写回磁盘的内存页(单位:KB),直接影响OOM Killer决策;Shared_Clean 指多进程共享、未被修改的只读页(如共享库代码段),不计入进程独占内存。
字段语义对比
| 字段 | 所属内存类型 | 是否计入RSS | 是否触发写时复制 | 典型来源 |
|---|---|---|---|---|
Private_Dirty |
私有匿名页 | ✅ | ❌(已脏) | malloc+写入、堆变量 |
Shared_Clean |
共享文件映射 | ❌(共享) | ✅(首次写触发COW) | /usr/lib/x86_64-linux-gnu/libc.so.6 |
实时监控脚本(带注释)
#!/bin/bash
# 监控指定PID的Private_Dirty与Shared_Clean变化(单位:KB)
PID=$1
grep -E "^(Private_Dirty|Shared_Clean):" "/proc/$PID/smaps" 2>/dev/null | \
awk '{print $1, $2}' | \
sed 's/Private_Dirty:/Private_Dirty KB:/; s/Shared_Clean:/Shared_Clean KB:/'
逻辑分析:
grep精准提取两行关键字段 →awk输出字段名与数值 →sed标准化标签。参数$1为必需的进程PID,避免空输入导致/proc//smaps路径错误。
内存生命周期示意
graph TD
A[进程mmap共享库] --> B[Shared_Clean页]
C[进程写私有堆内存] --> D[Private_Dirty页]
D --> E[OOM Killer候选]
2.5 unsafe.MapView与runtime.SetFinalizer协同管理映射生命周期的工程实践
在高频写入+弱引用语义场景中,unsafe.MapView 提供零拷贝键值视图,而 runtime.SetFinalizer 可绑定资源清理逻辑,二者协同实现映射生命周期自治。
数据同步机制
MapView 返回的 []byte 指针直连底层哈希桶内存,需确保映射未被 GC 回收:
type ManagedMap struct {
m sync.Map
}
func (mm *ManagedMap) View(key string) []byte {
if v, ok := mm.m.Load(key); ok {
if b, ok := v.([]byte); ok {
// 绑定 finalizer 防止底层数组提前回收
runtime.SetFinalizer(&b, func(p *[]byte) {
fmt.Printf("finalizing view for key %s\n", key)
})
return b // 返回非复制切片
}
}
return nil
}
逻辑分析:
SetFinalizer(&b, ...)中传入的是*[]byte地址,确保该切片对象存活时其底层数组不被 GC;但注意 finalizer 不保证执行时机,仅作兜底。
生命周期约束对照表
| 约束维度 | unsafe.MapView | runtime.SetFinalizer |
|---|---|---|
| 内存安全边界 | 无 bounds check | 不阻止 GC,仅延迟回收 |
| 执行确定性 | 即时返回指针 | 异步、非确定执行时机 |
| 适用场景 | 短期只读视图 | 资源释放/日志归档等终态操作 |
graph TD
A[MapView 获取字节视图] --> B{是否已注册 Finalizer?}
B -->|否| C[调用 SetFinalizer 绑定清理逻辑]
B -->|是| D[复用已有 finalizer]
C --> E[视图使用中,GC 保留底层数组]
D --> E
第三章:MAP_PRIVATE在索引构建阶段的典型误用模式
3.1 基于B+树索引批量写入时私有映射导致RSS暴增的复现与火焰图定位
数据同步机制
批量写入时,每个线程通过 mmap(MAP_PRIVATE) 映射同一底层文件以构建 B+ 树节点缓存,触发写时复制(COW)——物理页在首次写入时被复制,导致 RSS 线性飙升。
复现关键代码
// 模拟批量写入线程中私有映射行为
void* addr = mmap(NULL, 4096 * 1024, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_POPULATE, fd, 0);
for (int i = 0; i < 1024; i++) {
*(char*)(addr + i * 4096) = 1; // 触发 COW,每页一次复制
}
MAP_PRIVATE 使修改不落盘但强制内核为每次页写入分配新物理页;MAP_POPULATE 预加载页表,放大 RSS 增长可见性。
火焰图核心线索
| 函数名 | 占比 | 关键路径 |
|---|---|---|
do_wp_page |
68% | 写保护缺页 → 分配新页 |
__handle_mm_fault |
22% | 触发 COW 的入口 |
内存行为链路
graph TD
A[批量写入线程] --> B[mmap MAP_PRIVATE]
B --> C[首次写入某页]
C --> D[触发缺页异常]
D --> E[do_wp_page复制物理页]
E --> F[RSS +4KB/页]
3.2 mmap+MADV_DONTFORK规避子进程继承引发的重复内存占用案例
在多进程服务(如 Nginx、Redis fork 模型)中,父进程通过 mmap() 分配的大页共享内存默认会被 fork() 继承,导致子进程虚存映射重复、RSS 虚高——虽不立即分配物理页,但 COW 触发后实际内存翻倍。
核心机制:MADV_DONTFORK 的作用
void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr != MAP_FAILED) {
madvise(addr, size, MADV_DONTFORK); // 关键:子进程不继承该映射
}
madvise(..., MADV_DONTFORK) 告知内核:此虚拟内存区域不参与 fork 复制。子进程 vma 链表中完全剔除该段,彻底避免映射冗余与潜在 COW 开销。
对比效果(相同 128MB mmap 区域)
| 行为 | 父进程 RSS | 子进程 RSS(fork 后) | 是否触发 COW |
|---|---|---|---|
| 默认 mmap | 128 MB | 0 → 可能飙升至 128 MB | 是 |
MADV_DONTFORK 后 |
128 MB | 0(无映射) | 否 |
数据同步机制
父子进程需改用 AF_UNIX 或 eventfd 显式传递地址/偏移,确保子进程按需重新 mmap() —— 以可控性换内存确定性。
3.3 使用memmap库实现只读索引加载与写时映射分离的架构重构方案
传统索引加载常将全部结构载入内存,造成冷启延迟与内存冗余。本方案通过 numpy.memmap 实现物理隔离:索引文件以只读模式映射,更新操作则通过独立写时复制(Copy-on-Write)缓冲区完成。
核心设计原则
- 索引数据(如倒排表偏移数组)全程
mode='r'映射,零拷贝访问 - 写操作不修改原文件,而是异步提交至 WAL 日志 + 内存增量索引
初始化只读 memmap 示例
import numpy as np
# 假设 index_offsets.dat 是 uint64 类型、10M 元素的偏移数组
index_map = np.memmap(
"index_offsets.dat",
dtype=np.uint64,
mode="r", # 关键:强制只读,避免意外覆写
shape=(10_000_000,) # 必须显式指定 shape,否则无法索引
)
逻辑分析:
mode="r"确保 OS 层面拒绝写入权限;shape参数使 NumPy 能正确计算内存偏移,缺失将导致ValueError: buffer size does not match array size。该映射仅占用页表空间,实际页在首次访问时按需加载(lazy loading)。
写时映射协同流程
graph TD
A[查询请求] --> B{命中只读 memmap?}
B -->|是| C[直接返回 offset]
B -->|否| D[查增量索引]
D --> E[合并结果]
F[写入请求] --> G[追加至 WAL + 更新内存增量树]
| 组件 | 访问模式 | 持久化策略 |
|---|---|---|
| 主索引文件 | 只读 mmap | 静态,永不修改 |
| WAL 日志 | 追加写 | 定期 compact 合并 |
| 增量索引 | 读写内存 | LRU 缓存淘汰 |
第四章:MAP_SHARED在索引服务化场景下的高危边界条件
4.1 多goroutine并发msync+munmap引发的SIGBUS与页故障重试死循环调试实录
数据同步机制
当多个 goroutine 并发调用 msync(MS_SYNC) 后立即执行 munmap,内核可能尚未完成脏页回写,而 VMA(虚拟内存区域)已被销毁,导致后续页故障处理时访问已释放的 vm_area_struct。
关键复现代码
// goroutine A
_, err := unix.Msync(ptr, unix.MS_SYNC) // 阻塞等待回写完成
if err != nil { panic(err) }
// goroutine B(几乎同时)
unix.Munmap(ptr, size) // VMA 解除映射
MS_SYNC仅保证回写启动,不保证完成;munmap会立即释放 VMA 及其页表项。若此时发生缺页异常,内核尝试通过vma->vm_ops->fault重试,但vma已为NULL,触发SIGBUS。
死循环诱因
- 缺页异常 →
do_user_addr_fault→ 尝试vma操作 →NULL指针解引用 →SIGBUS - Go runtime 捕获
SIGBUS后尝试恢复(如栈增长检查),再次触发相同页故障
| 状态 | 表现 | 根本原因 |
|---|---|---|
msync 返回 |
回写队列已提交,但未完成 | writeback_control 异步性 |
munmap 完成 |
vma 从 mm_struct 移除 |
内存管理无同步屏障 |
| 页故障重入 | vma_lookup() 返回 NULL |
缺页路径未校验 vma 有效性 |
graph TD
A[缺页异常] --> B{vma = vma_lookup()}
B -->|vma != NULL| C[调用 vm_ops->fault]
B -->|vma == NULL| D[SIGBUS]
D --> E[Go runtime trap handler]
E --> A
4.2 文件截断(ftruncate)后MAP_SHARED映射区未同步失效导致的脏页回写崩溃
数据同步机制
ftruncate() 缩小文件时,内核仅更新 i_size,但 不主动使已建立的 MAP_SHARED 映射页表项失效。后续对截断区域的写入仍会触发页错误并成功写入页缓存,形成“越界脏页”。
崩溃触发路径
当进程退出或显式 msync() 时,内核尝试将这些脏页回写至文件——但目标偏移已超出新文件大小,generic_file_write_iter() 返回 -EFBIG,而 pageout() 未妥善处理该错误,最终触发 BUG_ON(PageWriteback(page))。
// 关键内核路径(mm/vmscan.c)
if (pageout(page, mapping, gfp_mask) == PAGE_KEEP) {
// 若 writepage() 返回失败(如 -EFBIG),PageDirty 仍置位
// 下次回收时再次调用 pageout → 无限重试 → 栈溢出/panic
}
pageout()返回PAGE_KEEP表示写入失败但页仍需保留;若底层writepage因i_size不匹配返回-EFBIG,该页将持续处于“脏但不可写”矛盾状态。
典型场景对比
| 操作 | MAP_SHARED 是否失效 | 截断后写入行为 | 回写结果 |
|---|---|---|---|
ftruncate(0) |
❌ 否 | 成功修改页缓存 | -EFBIG panic |
munmap() + 重映射 |
✅ 是 | SIGBUS(访问非法区域) |
安全终止 |
graph TD
A[ftruncate to 4KB] --> B[MAP_SHARED 仍覆盖 8KB]
B --> C[写入 offset=6KB]
C --> D[页标记为 dirty]
D --> E[进程退出触发 writepages]
E --> F{writepage: offset > i_size?}
F -->|Yes → -EFBIG| G[Page remains dirty & re-queued]
G --> E
4.3 基于flock+msync的强一致性索引更新协议设计与etcd v3存储层类比分析
数据同步机制
采用 flock() 排他锁保障单写者语义,配合 msync(MS_SYNC) 强制刷盘,规避页缓存延迟导致的可见性不一致。
int fd = open("index.dat", O_RDWR);
struct flock fl = {.l_type = F_WRLCK, .l_whence = SEEK_SET};
fcntl(fd, F_SETLKW, &fl); // 阻塞获取写锁
// ... 更新 mmap 映射区中的B+树节点 ...
msync(addr, len, MS_SYNC); // 同步至磁盘,确保持久化
fl.l_type = F_UNLCK;
fcntl(fd, F_SETLK, &fl); // 释放锁
F_SETLKW确保原子性临界区;MS_SYNC比MS_ASYNC更严格,避免因内核延迟引发索引与数据页分裂不一致。
与 etcd v3 的类比要点
| 维度 | flock+msync 方案 | etcd v3(bbolt + raft) |
|---|---|---|
| 一致性模型 | 单机强一致(本地文件系统) | 分布式线性一致(Multi-Raft) |
| 持久化原语 | msync(MS_SYNC) |
fsync() on WAL + backend |
关键约束
- 仅适用于单实例索引服务,不可直接扩展为分布式;
- 必须禁用 ext4 的
data=writeback模式,推荐data=ordered。
4.4 使用memfd_create创建匿名共享映射替代临时文件,规避NFS挂载点兼容性问题
传统进程间共享大块数据常依赖临时文件(如 /tmp/data.bin),但在 NFS 挂载点上面临 O_TMPFILE 不支持、mmap 同步异常及 unlink 语义不一致等问题。
核心优势对比
| 特性 | 临时文件(NFS) | memfd_create 映射 |
|---|---|---|
| 文件系统依赖 | 强(需支持 O_TMPFILE) | 无(内核内存对象) |
mmap 共享一致性 |
❌(缓存/锁机制差异) | ✅(页表直连 anon_vma) |
| 生命周期管理 | 手动 unlink() + close() |
close() 即释放 |
创建与映射示例
#include <linux/memfd.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <fcntl.h>
int fd = syscall(SYS_memfd_create, "shared_buf", MFD_CLOEXEC);
ftruncate(fd, 1024 * 1024); // 分配 1MB 内存
void *addr = mmap(NULL, 1024*1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
MFD_CLOEXEC:避免子进程继承 fd,提升安全性;ftruncate:设定内存对象逻辑大小(非物理分配,按需页故障);MAP_SHARED:确保多进程写入可见性,无需msync强制刷盘。
数据同步机制
memfd_create 对象本质是 shmem 的封装,所有 mmap 映射共享同一 struct page 链,写操作直接更新内存页,天然规避 NFS 缓存一致性风险。
第五章:总结与展望
核心技术栈的生产验证结果
在2023年Q3至2024年Q2的12个关键业务系统迁移项目中,基于Kubernetes+Istio+Prometheus的技术栈实现平均故障恢复时间(MTTR)从47分钟降至6.3分钟,服务可用率从99.23%提升至99.992%。下表为三个典型场景的压测对比数据:
| 场景 | 原架构TPS | 新架构TPS | 资源成本降幅 | 配置变更生效延迟 |
|---|---|---|---|---|
| 订单履约服务 | 1,840 | 5,210 | 38% | 从8.2s→1.4s |
| 用户画像API | 3,150 | 9,670 | 41% | 从12.6s→0.9s |
| 实时风控引擎 | 2,420 | 7,380 | 33% | 从15.3s→2.1s |
真实故障处置案例复盘
2024年3月17日,某省级医保结算平台突发流量洪峰(峰值达设计容量217%),传统负载均衡器触发熔断。新架构通过Envoy的动态速率限制+自动扩缩容策略,在23秒内完成Pod水平扩容(从12→47实例),同时利用Jaeger链路追踪定位到第三方证书校验模块存在线程阻塞,运维团队依据TraceID精准热修复,全程业务无中断。该事件被记录为集团级SRE最佳实践案例。
# 生产环境实时诊断命令(已脱敏)
kubectl get pods -n healthcare-prod | grep "cert-validator" | awk '{print $1}' | xargs -I{} kubectl logs {} -n healthcare-prod --since=2m | grep -E "(timeout|deadlock)"
多云协同治理落地路径
当前已实现阿里云ACK集群与华为云CCE集群的跨云服务网格互通,通过自研的ServiceMesh Federation Controller统一管理142个微服务实例。关键突破包括:① TLS证书跨云自动同步机制(基于HashiCorp Vault + 自定义Operator);② 流量染色路由策略支持按地域标签(region=gd-shenzhen、region=hb-wuhan)智能分发;③ 跨云链路追踪ID全局唯一性保障(采用Snowflake算法生成TraceID前缀)。
技术债偿还进度看板
截至2024年6月,历史遗留的Java 8单体应用改造完成度达89%,剩余11个系统中,7个已完成容器化封装并接入服务网格,4个处于灰度发布阶段。重点攻坚的“核心计费引擎”重构项目,采用Strangler Pattern模式,通过API网关路由将新老系统并行运行,目前已承载63%的生产流量,日均处理交易1,280万笔,错误率稳定在0.0017%以下。
下一代可观测性演进方向
正在构建eBPF驱动的零侵入式监控体系,已在测试环境部署Calico eBPF数据平面替代iptables,网络策略执行延迟降低至亚微秒级。下一步将集成Pixie平台实现数据库查询语句级性能分析,目标在2024年Q4前实现SQL慢查询自动归因到具体代码行(需结合OpenTelemetry SDK注入的span属性)。Mermaid流程图展示当前告警闭环机制:
graph LR
A[Prometheus Alert] --> B{Rule Engine}
B -->|P0级| C[Slack机器人+电话通知]
B -->|P1级| D[自动创建Jira工单]
B -->|P2级| E[触发Ansible Playbook]
C --> F[值班工程师确认]
D --> G[关联GitLab MR链接]
E --> H[执行数据库连接池扩容]
F --> I[标记SLA状态]
G --> I
H --> I 