Android 多用户挂载技术(K/L)

在Android K和L版本里面,zygote会将sdcard指向/storage/emulated/0,而非从init启动的bin程序识别的/storage/emulated/legacy,也就是说如果将sdcard的路径传递给二进制服务的时候,必须要进行相应的转换。

类似于下面的code。 

    if (!strncasecmp("/storage/emulated/0", mImageUrl, 19)) {
        using std::string;
        string imagePath = string(mImageUrl);
        imagePath = imagePath.substr(19);
        imagePath = string("/storage/emulated/legacy") + imagePath;
        strncpy(mImageUrl, imagePath.c_str(), imagePath.length() + 1);
        ALOGD("setDataSource image path replaced %s", mImageUrl);
    }

原理学习主要来自下面这篇blog,也转载于此。 

http://www.cloudchou.com/android/post-689.html

引言

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目录的创建:

 

 

 

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的关键代码如下所示:
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进行模拟挂载的,我们看一下这个函数的关键代码:

/*
 * 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。

 

你可能感兴趣的:(Android 多用户挂载技术(K/L))