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在设计上的取舍了。