Android7 多用户源码解析(六)数据隔离与存储机制详解

系列目录 :第一篇:功能演进与系统架构总览 | 第二篇:核心数据结构深度剖析 | 第三篇:用户创建与删除流程全链路分析 | 第四篇:用户切换机制源码剖析 | 第五篇:应用安装与管理隔离机制 | 第六篇:数据隔离与存储机制详解 | 第七篇:系统服务的多用户感知处理 | [第八篇:广播分发与 Intent 路由的用户隔离](#第八篇:广播分发与 Intent 路由的用户隔离) | 第九篇:权限系统与多用户的交互机制 | [第十篇:Work Profile 企业模式与开发实战指南](#第十篇:Work Profile 企业模式与开发实战指南)


一、核心问题

  • /data/user/<id> 到底是真实目录还是挂载点?CE/DE 分离后应用数据落在哪?
  • 每个用户的 FBE 密钥由谁生成、何时销毁?
  • 共享存储("SD 卡")在多用户下如何做到"每人看到自己的文件"?

原稿把 AOSP 7 的存储描述成"纯目录 + sdcard FUSE 守护进程"------这与 FBE(File-Based Encryption)时代的真实布局差距很大。本篇按 Environment 目录布局 → FBE 密钥 → 共享存储 → 应用路径 的顺序展开。


二、目录布局:CE/DE 分离

源码路径 :frameworks/base/core/java/android/os/Environment.java

java 复制代码
public class Environment {
    private static final File DIR_ANDROID_DATA = getDirectory(ENV_ANDROID_DATA, "/data");
    private static final File DIR_ANDROID_STORAGE = getDirectory(ENV_ANDROID_STORAGE, "/storage");

    /** Return the system directory for a user. ... */
    @Deprecated
    public static File getUserSystemDirectory(int userId) {
        return new File(new File(getDataSystemDirectory(), "users"), Integer.toString(userId));
    }

    /** Returns the base directory for per-user system directory, credential encrypted. */
    public static File getDataSystemCeDirectory(int userId) {
        return buildPath(getDataDirectory(), "system_ce", String.valueOf(userId));
    }
    public static File getDataSystemDeDirectory(int userId) {
        return buildPath(getDataDirectory(), "system_de", String.valueOf(userId));
    }

    public static File getDataMiscCeDirectory(int userId) {
        return buildPath(getDataDirectory(), "misc_ce", String.valueOf(userId));
    }
    public static File getDataMiscDeDirectory(int userId) {
        return buildPath(getDataDirectory(), "misc_de", String.valueOf(userId));
    }

    public static File getDataUserCeDirectory(String volumeUuid, int userId) {
        return new File(getDataUserCeDirectory(volumeUuid), String.valueOf(userId));
    }
    public static File getDataUserDeDirectory(String volumeUuid, int userId) {
        return new File(getDataUserDeDirectory(volumeUuid), String.valueOf(userId));
    }
    // ...
}

/data 下多用户相关布局:

复制代码
/data
├── system/users/<id>/          每用户系统配置(UMS 的 <id>.xml 在 system/users/ 下)
├── system_ce/<id>/             每用户系统配置(CE:解锁后可访问)
├── system_de/<id>/             每用户系统配置(DE:开机即可访问)
├── misc_ce/<id>/  misc_de/<id>/    每用户服务杂项数据
├── user/<id>/<pkg>/            应用数据 CE 区(真实目录)
└── user_de/<id>/<pkg>/         应用数据 DE 区(真实目录)

关键设计 :CE(Credential Encrypted)目录只有用户解锁后 才可访问;DE(Device Encrypted)目录开机即可访问 (但用设备密钥加密)。应用常规数据在 CE 区,开机即需运行的服务数据放 DE 区。getUserSystemDirectory 已被标记 @Deprecated,注释明确建议迁移到 CE/DE 版本。

应用看到的 /data/user/

应用代码里拿到的 Environment.getDataUserCeDirectory 是 /data/user/<id>,但它不是真实目录------/data/user 本身是一个符号链接/挂载点 ,指向 FBE 卷上的 <id>_ce 分区(ext4 独立分区,FBE 密钥保护)。对应用透明:应用写 /data/user/<id>/<pkg>/files,内核落盘到 FBE 分区。

关键设计 :"目录"只是视图,"FBE 分区"才是实体。这保证:密钥销毁(第三篇 destroyUserKey)后,整个分区即不可读------数据删除是"销毁密钥"级别的彻底。


三、FBE 用户密钥:MountService 与 vold

第三篇创建链路里的 storage.createUserKey(userId, serialNumber, isEphemeral),实际由 MountService 实现(AOSP 7 中存储管理服务即 MountService,不是原稿写的 StorageManagerService):

源码路径 :frameworks/base/services/core/java/com/android/server/MountService.java

java 复制代码
final class MountService extends IStorageManager.Stub
        implements Handler.Callback {
    // ...
    @Override
    public void createUserKey(int userId, int serialNumber, boolean ephemeral) {
        enforcePermission(android.Manifest.permission.STORAGE_INTERNAL);
        waitForReady();
        try {
            mCryptConnector.execute("cryptfs", "create_user_key", userId, serialNumber,
                ephemeral ? 1 : 0);
        } catch (NativeDaemonConnectorException e) {
            throw e.rethrowAsParcelableException();
        }
    }

    @Override
    public void destroyUserKey(int userId) {
        enforcePermission(android.Manifest.permission.STORAGE_INTERNAL);
        waitForReady();
        try {
            mCryptConnector.execute("cryptfs", "destroy_user_key", userId);
        } catch (NativeDaemonConnectorException e) {
            throw e.rethrowAsParcelableException();
        }
    }

    @Override
    public void prepareUserStorage(String volumeUuid, int userId, int serialNumber, int flags) {
        enforcePermission(android.Manifest.permission.STORAGE_INTERNAL);
        waitForReady();
        try {
            mCryptConnector.execute("cryptfs", "prepare_user_storage", escapeNull(volumeUuid),
                    userId, serialNumber, flags);
        } catch (NativeDaemonConnectorException e) {
            throw e.rethrowAsParcelableException();
        }
    }
}

关键设计 :Java 侧的存储管理是薄壳 ------全部加密操作通过 mCryptConnector 委托 native 的 vold (cryptfs 命令)。serialNumber 随密钥生成传入(密钥与 serialNumber 绑定,恢复出厂/恢复备份时用于识别);ephemeral 标记访客密钥(访客退出即销毁,数据不留盘)。

创建/删除用户时的密钥生命周期与第三篇流程对应:

复制代码
createUser   → cryptfs create_user_key(userId, serial, ephemeral)
           → mPm.prepareUserData → cryptfs prepare_user_storage(uuid, userId, serial, CE|DE)
removeUserState → cryptfs destroy_user_key(userId)

四、共享存储:每人一个视图

AOSP 7 的内置"SD 卡"(emulated storage)真实路径是 /data/media/<userId>/,每个用户一个目录,UID 归属隔离:

复制代码
/data/media/
├── 0/    主用户共享存储(UID 归属 media_rw:sdcard_rw,按用户 uid 区分)
├── 10/   用户 10 共享存储
└── 11/   用户 11 共享存储

用户可见路径 /storage/emulated/<userId> 由 init 的 fstab 挂载(/mnt/media_rw/emulated bind 到 /data/media/<id>),切换用户时由 AM 侧 updateUserConfiguration + WMS 重新挂载对应用户的 bind mount------不是 原稿描述的"Java 层启动 /system/bin/sdcard 守护进程"(mountFuse 方法在 AOSP 7 的 MountService 中不存在)。

应用的外部路径按用户拼装:

java 复制代码
// Environment.java
public static File[] buildExternalStorageAppDirs(String packageName) {
    return buildPaths(getExternalDirs(), DIR_ANDROID, DIR_DATA, packageName);
}
// /storage/emulated/<userId>/Android/data/<pkg>/files

关键设计 :共享存储的隔离靠三件事叠加:① /data/media/<id> 目录 + UID 权限(不同用户 UID 不同,互相不可见);② 每个用户只能看到自己 userId 的 emulated 挂载;③ MediaProvider 按 userId 过滤扫描结果。原稿"sdcard FUSE 做权限过滤"是 Android 4.x 的机制,AOSP 7 已废弃。


五、应用进程内的用户感知

5.1 Context 路径

ContextImpl.getFilesDir() 返回 /data/user/<userId>/<pkg>/files,其中 userId 取进程启动时绑定的用户(Process.myUserHandle())。系统服务需要操作其他用户时,通过 createContextAsUser / createPackageContextAsUser 拿到目标用户的 Context------路径自动指向对应用户的目录:

java 复制代码
// ContextImpl.java(@hide API)
public Context createContextAsUser(UserHandle user, int flags) {
    // 校验:INTERACT_ACROSS_USERS(_FULL) 权限
    final int userId = user.getIdentifier();
    // ... 权限检查 ...
    return createPackageContextAsUser(getOpPackageName(), 0, user);
}

关键设计 :跨用户 Context 是系统服务操作他用户数据(如"为用户 10 的某应用发通知")的标准姿势,权限门槛 INTERACT_ACROSS_USERS(_FULL) 与第九篇的跨用户权限体系一致。

5.2 数据目录的属主

/data/user/<id>/<pkg> 的属主 UID 是 userId * 100000 + appId(第二篇的 UID 映射):同一应用的两个用户实例 UID 不同,文件系统权限天然隔离。SELinux 的 app_data_file 上下文 + per-app MLS 级别(mls)进一步保证"即使 UID 相同也跨不出自己的应用数据"。

关键设计:隔离是三层叠加------UID(Unix 权限)→ SELinux(MAC)→ FBE(分区密钥)。任一层被突破,其余两层仍兜底。


六、调试

bash 复制代码
# 查看 /data 下各用户目录
adb shell ls -l /data/user/ /data/media/

# 查看 FBE 卷状态(vold 管理)
adb shell dumpsys mount | grep -A5 "User"

# 查看挂载点
adb shell mount | grep -E "emulated|user"

七、本篇总结

  1. 目录布局 :CE/DE 分离(system_ce/system_de/misc_ce/misc_de/user/user_de),应用数据真实落在 FBE 独立分区;
  2. 密钥 :MountService(IStorageManager 实现)通过 vold cryptfs 命令管理用户密钥,serialNumber 绑定、ephemeral 支持访客;
  3. 共享存储 :/data/media/<id> + UID 权限 + 按用户 bind 挂载,无 FUSE 守护进程;
  4. 应用路径 :/data/user/<id>/<pkg> 属主 UID 跨用户唯一,三层隔离(UID/SELinux/FBE);
  5. 跨用户访问 :createContextAsUser + INTERACT_ACROSS_USERS 权限。

下一篇:系统服务如何"知道"该为谁干活------mH 消息路由、per-user 锁、ActivityManagerInternal 的用户维度状态。

相关推荐
AFinalStone5 小时前
Android7 多用户源码解析(一)功能演进与系统架构总览
系统架构·aosp·多用户·usermanaher
Jmyd01235 小时前
智慧文博数字化系统架构设计:从三维采集到平台应用的全链路方案
系统架构
一切皆是因缘际会9 小时前
掌控信息论:同源星际通信零延迟架构
人工智能·深度学习·ai·系统架构·信息与通信·星际通信·同源通信
AFinalStone10 小时前
Android 7 多用户源码解析(七)系统服务的多用户感知处理
android·系统架构·多用户
Runwise创新社区10 小时前
开源大模型从本地跑通到生产上线:以千问为例的五级部署阶梯与六项选型检查
人工智能·系统架构·开源
艺杯羹11 小时前
240GB流量击穿与频繁OOM:从云端瘫痪到客户端自治的本地优先架构重构实录
性能优化·系统架构·fastapi·桌面客户端·本地优先
AFinalStone13 小时前
Android7 多用户源码解析(十)Work Profile 企业模式与开发实战指南
系统架构·aosp·多用户
AFinalStone14 小时前
Android7 多用户源码解析(九)权限系统与多用户的交互机制
系统架构·aosp·多用户
数字新视界15 小时前
国产动环系统技术原理与应用实践的深入剖析
嵌入式硬件·物联网·系统架构·机房管理·动力与环境监控系统