3、Ashmem与Linux传统共享内存的异同
说实话,每次面试Android工程师,我都喜欢问这个问题。很多人能把Ashmem背得滚瓜烂熟,但一问到POSIX共享内存和System V共享内存,就开始支支吾吾了。其实搞懂了这三者的关系,你对Android内存管理的理解才算真正入门。
3.1 先说说Linux传统共享内存
Linux下有两套经典的共享内存方案,都是POSIX标准的一部分。我最早接触的是System V共享内存,那时候还在做嵌入式Linux开发。
3.1.1 System V共享内存
这套接口比较老,API设计得有点「Unix味」。核心函数就这几个:
// 创建或获取共享内存
int shmget(key_t key, size_t size, int shmflg);
// 附加到进程地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg);
// 分离共享内存
int shmdt(const void *shmaddr);
// 控制操作(删除、查询等)
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
用起来其实挺直接的。先通过shmget拿到一个ID,然后用shmat映射到进程空间,用完再shmdt分离。嗯,这里有个坑——System V共享内存是持久化的。什么意思?就算所有进程都detach了,这块内存还在内核里待着,除非你显式调用shmctl删除它。
我曾经在一个项目里吃过这个亏。进程退出时忘了清理共享内存,结果系统里残留了一堆废弃的共享内存段,把内核内存耗光了。从那以后,我养成了在程序启动时检查并清理旧共享内存的习惯。
3.1.2 POSIX共享内存
这套接口更现代一些,用文件描述符的方式来操作:
// 创建或打开共享内存对象
int shm_open(const char *name, int oflag, mode_t mode);
// 设置大小
int ftruncate(int fd, off_t length);
// 映射到进程地址空间
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
// 解除映射
int munmap(void *addr, size_t length);
// 删除共享内存对象
int shm_unlink(const char *name);
你看,它把共享内存抽象成了一个文件对象。我个人更喜欢这套接口,因为mmap的语义更清晰,而且文件描述符的管理方式跟其他I/O操作统一了。
关键区别:POSIX共享内存是引用计数的。当所有进程都unmap后,内核会自动回收。除非你显式调用shm_unlink,否则对象名会一直存在。
3.2 Ashmem的独特之处
好了,现在说回我们的主角——Ashmem。它是在Android内核里新增的一个驱动,专门为移动设备设计的。你想想看,手机的内存本来就金贵,传统共享内存那种「占着茅坑不拉屎」的行为,在Android上绝对不行。
3.2.1 核心差异:内存回收机制
这是Ashmem最牛的地方。传统共享内存一旦分配,内核就拿不走了。但Ashmem不一样——它支持内存回收。
具体怎么做的?Ashmem维护了一个pin/unpin机制:
- Pin:锁定内存,保证不会被回收
- Unpin:解锁内存,系统内存紧张时可以回收
说白了,Ashmem给共享内存加了一个「可回收」的标签。当系统内存不够时,内核会优先回收那些unpin状态的Ashmem内存。这在传统共享内存里是做不到的。
我个人的经验:在开发SurfaceFlinger时,大量使用了Ashmem来传递图形缓冲区。因为图形数据是流式的,用完就可以unpin,让系统在需要时回收。如果换成传统共享内存,手机用不了多久就会OOM。
3.2.2 文件描述符传递
Ashmem的另一个设计亮点是:它通过ashmem_create_region返回一个文件描述符。这个fd可以在进程间通过Binder传递。你想想看,这比System V那种用key来标识的方式灵活多了。
// Ashmem核心API(用户空间)
int ashmem_create_region(const char *name, size_t size);
int ashmem_set_prot_region(int fd, int prot);
int ashmem_pin_region(int fd, size_t offset, size_t len);
int ashmem_unpin_region(int fd, size_t offset, size_t len);
3.3 对比表格
我整理了一个对比表,方便你一目了然:
| 特性 | System V共享内存 | POSIX共享内存 | Ashmem |
|---|---|---|---|
| 标识方式 | key值(整数) | 文件名(字符串) | 文件描述符 |
| 内存回收 | 不支持 | 不支持 | 支持pin/unpin回收 |
| 生命周期 | 显式删除 | 引用计数 | 引用计数+可回收 |
| 进程间传递 | 通过key | 通过文件名 | 通过Binder传fd |
| 权限控制 | 粗粒度 | 文件权限 | 支持设置保护位 |
| 内核实现 | 标准内核 | 标准内核 | Android特有驱动 |
3.4 什么时候选哪个?
这个问题我经常被问到。我的建议是这样的:
- 纯Linux环境:用POSIX共享内存,接口更现代,行为更可控
- Android系统服务:优先用Ashmem,特别是需要传递大块数据时
- 需要持久化共享内存:System V的持久化特性反而成了优势
- 高性能场景:三者性能差异不大,瓶颈通常在内存拷贝上
注意:从Android 10开始,Google推荐使用MemoryFile替代直接操作Ashmem。MemoryFile是对Ashmem的封装,用起来更安全。但底层原理还是Ashmem那一套。
3.5 一个实际案例
我记得有一次调试一个多媒体播放器,画面总是卡顿。排查了半天,发现是SurfaceFlinger和MediaServer之间用传统共享内存传递解码后的帧数据。每次传递都要拷贝一次,而且内存一直pin着,系统内存很快就吃紧了。
后来改成Ashmem,配合unpin机制,内存压力明显下降。而且通过Binder传递fd,省去了中间的内存拷贝。嗯,这就是为什么Android选择Ashmem的原因——移动设备上,内存就是命根子。
所以你看,Ashmem不是凭空造出来的轮子,而是针对移动场景做了精准的优化。理解了这一点,你就能明白Android在设计上的取舍了。