Android 多用户挂载技术

2014/12/09

本文原创作者:Cloud Chou. 欢迎转载,请注明出处和本文链接

引言

emultaed技术,即多用户挂载技术,是Android 4.2引入的一项新的Sd卡挂载技术,不同的用户看到的Sd卡挂载目录不一样。

比如使用adb shell查看sd卡目录可能得到的Sd卡目录是/storage/emulated/legacy,它实际上是一个链接目录:

1
/storage/emulated/legacy -> /mnt/shell/emulated/0

这样/mnt/shell/emulated/0是sd卡的实际目录,但是在应用程序里即使用Root权限查看/storage/emulated/legacy目录,会发现该目录根本不存在。

应用程序通过系统api获取Sd卡路径时得到的Sd卡路径是/storage/emulated/0,同样我们使用adb shell去查看该目录时也会发现该目录不存在。

linux内核从2.6.x引入了共享挂载,从属挂载,私有挂载,绑定挂载,挂载传播技术。

Android 4.2就是利用了共享挂载和从属挂载来隔离挂载的命名空间,不同用户的挂载对别的进程或者用户不可见。

关于挂载技术的资料:

https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt

命名空间相关资料:

http://book.51cto.com/art/201005/200881.htm

多用户挂载原理

init.rc

首先我们看一下init.rc里storage,mnt目录的创建:

1
2
3
4
5
6
7
8
9
10
11
12
export EXTERNAL_STORAGE /storage/emulated/legacy
export EMULATED_STORAGE_SOURCE /mnt/shell/emulated
export EMULATED_STORAGE_TARGET /storage/emulated 
# Support legacy paths
symlink /storage/emulated/legacy /sdcard
symlink /storage/emulated/legacy /mnt/sdcard
symlink /storage/emulated/legacy /storage/sdcard0
symlink /mnt/shell/emulated/0 /storage/emulated/legacy	
mkdir /storage/external_storage 0666 system system
mount tmpfs tmpfs /storage/external_storage rec mode=0775,gid=1000
# Backward compatibility
symlink /storage/external_storage /mnt/usb  

可以看到有如下链接关系:

1
2
3
/sdcard  -> /storage/emulated/legacy          ->/mnt/shell/emulated/0
/mnt/sdcard -> /storage/emulated/legacy       ->/mnt/shell/emulated/0
/storage/sdcard0 -> /storage/emulated/legacy  ->/mnt/shell/emulated/0

通过mount命令可以看到:

1
/dev/fuse /mnt/shell/emulated fuse rw,nosuid,nodev,relatime,user_id=1023,group_id=1023,default_permissions,allow_other 0 0

我们可以看到真实的挂载目录其实是/mnt/shell/emulated,不过使用的是fuse设备。

由此我们可以理解为什么使用 adb shell ls -l /storage/emulated 看到的是:

1
2
ls -l /storage/emulated 
lrwxrwxrwx root     root              1969-12-31 16:00 legacy -> /mnt/shell/emulated/0 

android应用程序使用Root权限看/storage/emulated

再看一下在应用程序里使用SuperUser执行命令 ls -l /storage/emulated/得到的结果:

1
2
 drwxrwxr-x   22 0        1015          4096 May  7 07:04 0
 drwxrwxr-x   22 0        1015          4096 May  7 07:04 legacy   

为什么会多了一个0目录呢? 为什么legacy目录成为真实目录, 而不是软链接了。

知道Android应用启动过程的读者一定知道应用启动时是通过zygote使用fork孵化出来的进程,

而zygote启动时会执行dvmStartup -> initZygote (dalvik/vm/Init.cpp)

  • initZygote的关键代码如下所示:
  • 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
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    
    static bool initZygote()
    {
        /* zygote goes into its own process group */
        setpgid(0,0);
    
        // See storage config details at http://source.android.com/tech/storage/
        // Create private mount namespace shared by all children
        //先前有提到unshare,此处调用是为了创建新的命名空间,
        //CLONE_NEWNS表示mount类型的命名空间,
        //这样子进程和父进程的mount命名空间不再是同一个了
        if (unshare(CLONE_NEWNS) == -1) {
            SLOGE("Failed to unshare(): %s", strerror(errno));
            return -1;
        }
    
        // Mark ANDROID_STORAGE (e.g., /storage) as being a slave so that changes
        // from default namespace only flow into our children.  Notes:
        // 1. ANDROID_STORAGE must already serve as a mountpoint, e.g., for a tmpfs
        //    volume, as only mountpoints may be marked and rootfs cannot be bind
        //    mounted.
        // 2. EMULATED_STORAGE_TARGET (e.g., /storage/emulated), if used, must be
        //    path-prefixed by ANDROID_STORAGE.
        // 3. Multi-user sandbox mounts must be made under ANDROID_STORAGE to
        //    remain hidden from other users.
        // 4. Dalvik-based apps may mount anywhere, except under ANDROID_STORAGE,
        //    to provide a system-wide shared mount between apps and users.
        //ANDROID_STORAGE 通常是指 /storage目录
        const char* storage_base = getenv("ANDROID_STORAGE");
        if (storage_base != NULL) {
            //修改/storage的挂载属性为从属挂载,MS_SLAVE是指从属挂载,MS_REC表示递归修改
            if (mount(NULL, storage_base, NULL, (MS_SLAVE | MS_REC), NULL) == 0) {
                // ANDROID_STORAGE successfully marked as slave, leave the rest of the
                // filesystem hierarchy marked as shared.
                goto mounted_slave;
            } else {
                // Warn only, init.rc is likely missing a tmpfs mount for ANDROID_STORAGE.
                SLOGW("Failed to mount %s as MS_SLAVE: %s", storage_base, strerror(errno));
            }
        } else {
            SLOGW("ANDROID_STORAGE environment variable undefined");
        }
    
        // Fallback: Mark rootfs as being a slave so that changes from default
        // namespace only flow into our children.  All mounts under "/" will be
        // hidden from other apps and users.
        if (mount("rootfs", "/", NULL, (MS_SLAVE | MS_REC), NULL) == -1) {
            SLOGE("Failed to mount() rootfs as MS_SLAVE: %s", strerror(errno));
            return -1;
        }
    
    mounted_slave:
    
        // Create a staging tmpfs that is shared by our children; they will
        // bind mount storage into their respective private namespaces, which
        // are isolated from each other.
        //EMULATED_STORAGE_TARGET环境变量一般指/storage/emulated
        const char* target_base = getenv("EMULATED_STORAGE_TARGET"); 
       if (target_base != NULL) {
            //挂载tmpfs放到/storage/emulated目录, 
            //此时先前建立的软链接/storage/emulated/legacy不再可见,
            //只有目录/storage/emulated了
            if (mount("tmpfs", target_base, "tmpfs", MS_NOSUID | MS_NODEV,
                    "uid=0,gid=1028,mode=0050") == -1) {
                SLOGE("Failed to mount tmpfs to %s: %s", target_base, strerror(errno));
                return -1;
            }
        }
    
        return true;
    }
    

    我们再看应用启动时如何挂载/storage/emulated/0目录的,我们知道应用启动时是通过Zygote孵化,会调用到Zygote.forkAndSpecialize,再看一下它接下来的调用流程:

    Zygote.forkAndSpecialize -> Dalvik_dalvik_system_Zygote_forkAndSpecialize(dalvik_system_Zygote.cpp) -> forkAndSpecializeCommon(dalvik_system_Zygote.cpp)->mountEmulatedStorage

    由此可知道是在mountEmulatedStorage进行模拟挂载的,我们看一下这个函数的关键代码:

    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
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    
    /*
     * Create a private mount namespace and bind mount appropriate emulated
     * storage for the given user.
     */
    static int mountEmulatedStorage(uid_t uid, u4 mountMode) {
        // See storage config details at http://source.android.com/tech/storage/
        //此处获取multiuser_get_user_id其实是用uid/100000得到userid,也就是说一个用户至多只有100000个用户
        userid_t userid = multiuser_get_user_id(uid);
    
        // Create a second private mount namespace for our process
        //和initZygote时一样建立自己的mount命名空间
        if (unshare(CLONE_NEWNS) == -1) {
            ALOGE("Failed to unshare(): %s", strerror(errno));
            return -1; 
        }   
    
        // Create bind mounts to expose external storage
        //多用户环境下mountMode一般都是MOUNT_EXTERNAL_MULTIUSER或者MOUNT_EXTERNAL_MULTIUSER_ALL
        if (mountMode == MOUNT_EXTERNAL_MULTIUSER
                || mountMode == MOUNT_EXTERNAL_MULTIUSER_ALL) {
            // These paths must already be created by init.rc
            //先前我们看到的init.rc里也指出了环境变量EMULATED_STORAGE_SOURCE,
            //EMULATED_STORAGE_TARGET,EMULATED_STORAGE_TARGET的值
            //一般情况下这些环境变量的值:
            //EMULATED_STORAGE_SOURCE  /mnt/shell/emulated
            //EMULATED_STORAGE_TARGET /storage/emulated
            //EXTERNAL_STORAGE /storage/emulated/legacy
            const char* source = getenv("EMULATED_STORAGE_SOURCE");
            const char* target = getenv("EMULATED_STORAGE_TARGET");
            const char* legacy = getenv("EXTERNAL_STORAGE");
            if (source == NULL || target == NULL || legacy == NULL) {
                ALOGE("Storage environment undefined; unable to provide external storage");
                return -1;
            }
    
            // Prepare source paths      
            char source_user[PATH_MAX];
            char source_obb[PATH_MAX];
            char target_user[PATH_MAX];
    
            // /mnt/shell/emulated/0
            snprintf(source_user, PATH_MAX, "%s/%d", source, userid);
            // /mnt/shell/emulated/obb
            snprintf(source_obb, PATH_MAX, "%s/obb", source);
            // /storage/emulated/0
            snprintf(target_user, PATH_MAX, "%s/%d", target, userid);
            //建立各种目录,若目录已存在则不用重复创建
            if (fs_prepare_dir(source_user, 0000, 0, 0) == -1
                    || fs_prepare_dir(source_obb, 0000, 0, 0) == -1
                    || fs_prepare_dir(target_user, 0000, 0, 0) == -1) {
                return -1;
            }
    
            // Unfortunately bind mounts from outside ANDROID_STORAGE retain the
            // recursive-shared property (kernel bug?).  This means any additional bind
            // mounts (e.g., /storage/emulated/0/Android/obb) will also appear, shared
            // in all namespaces, at their respective source paths (e.g.,
            // /mnt/shell/emulated/0/Android/obb), leading to hundreds of
            // /proc/mounts-visible bind mounts.  As a workaround, mark
            // EMULATED_STORAGE_SOURCE (e.g., /mnt/shell/emulated) also a slave so that
            // subsequent bind mounts are confined to this namespace.  Note,
            // EMULATED_STORAGE_SOURCE must already serve as a mountpoint, which it
            // should for the "sdcard" fuse volume.
            //将/mnt/shell/emulated变成从属挂载
            if (mount(NULL, source, NULL, (MS_SLAVE | MS_REC), NULL) == -1) {      
                SLOGW("Failed to mount %s as MS_SLAVE: %s", source, strerror(errno));
    
                // Fallback: Mark rootfs as slave.  All mounts under "/" will be hidden
                // from other apps and users.  This shouldn't happen unless the sdcard
                // service is broken.
                if (mount("rootfs", "/", NULL, (MS_SLAVE | MS_REC), NULL) == -1) {
                    SLOGE("Failed to mount rootfs as MS_SLAVE: %s", strerror(errno));
                    return -1;
                }
            }
    
            if (mountMode == MOUNT_EXTERNAL_MULTIUSER_ALL) {
                // Mount entire external storage tree for all users
                //将/mnt/shell/emulated 绑定到 /storage/emulated
                if (mount(source, target, NULL, MS_BIND, NULL) == -1) {
                    ALOGE("Failed to mount %s to %s: %s", source, target, strerror(errno));
                    return -1;
                }
            } else {
                // Only mount user-specific external storage
                //将/mnt/shell/emulated/0 绑定到 /storage/emulated/0,
                //这样就有了/storage/emulated/0,因为/storage是从属挂载方式,
                //故此adb shell看不到这个0目录
                if (mount(source_user, target_user, NULL, MS_BIND, NULL) == -1) {
                    ALOGE("Failed to mount %s to %s: %s", source_user, target_user, strerror(errno));
                    return -1;
                }
            }
    
            // Now that user is mounted, prepare and mount OBB storage
            // into place for current user
                    char target_android[PATH_MAX];
            char target_obb[PATH_MAX];
    
            // /storage/emulated/0/Android
            snprintf(target_android, PATH_MAX, "%s/%d/Android", target, userid);
            // /storage/emulated/0/Android/obb
            snprintf(target_obb, PATH_MAX, "%s/%d/Android/obb", target, userid);
    
            if (fs_prepare_dir(target_android, 0000, 0, 0) == -1
                    || fs_prepare_dir(target_obb, 0000, 0, 0) == -1
                    || fs_prepare_dir(legacy, 0000, 0, 0) == -1) {
                return -1;
            }
            if (mount(source_obb, target_obb, NULL, MS_BIND, NULL) == -1) {
                ALOGE("Failed to mount %s to %s: %s", source_obb, target_obb, strerror(errno));
                return -1;
            }
    
            // Finally, mount user-specific path into place for legacy users
            //将 /storage/emulated/legacy 绑定到 /storage/emulated/0,
            //故此看到 /storage/emulated/legacy是一个真实目录,
            //不再是软链接,并且该目录下的内容和sd卡下的内容一致
            if (mount(target_user, legacy, NULL, MS_BIND | MS_REC, NULL) == -1) {
                ALOGE("Failed to mount %s to %s: %s", target_user, legacy, strerror(errno));
                return -1;
            }
    
        } else {
            ALOGE("Mount mode %d unsupported", mountMode);
            return -1;    
    	  }
    
        return 0;
    }
    

    由上可以解释: 为什么会多了一个0目录呢? 为什么legacy目录成为真实目录, 而不是软链接了

    /storage是从属挂载,应用启动时会在/storage/emulated建立0目录和legacy目录,但是因为是从属挂载,故此通过adb shell看不到该目录

    应用启动时采取绑定挂载的方式将/mnt/shell/emulated/0挂载到了/storage/emulated/0,还采取了绑定挂载的方式将/storage/emulated/0挂载到了/storage/emulated/legacy。

    ¥打赏5毛

    取消

    感谢您的支持,我会继续努力的!

    扫码支持
    赏个5毛,支持我把

    打开支付宝扫一扫,即可进行扫码打赏哦

    本篇目录