Android存储系统成长记

news2024/12/23 10:37:18

用心坚持输出易读、有趣、有深度、高质量、体系化的技术文章

本文概要

您一定使用过ContextgetFileStreamPath方法或者EnvironmentgetExternalStoragePublicDirectory方法,甚至还有别的方法把数据存储到文件中,这些都是存储系统提供的服务,那本文从存储系统“成长”的角度,来揭开存储系统的秘密。(文中的代码是基于android13

Android系列的前六篇文章如下:
Android系统native进程之我是init进程
Android系统native进程之属性能力的设计魅力
Android系统native进程之进程杀手–lmkd
Android系统native进程之日志系统–logd、logcat
Android系统native进程之我是installd进程
Apk安装之谜

我是存储系统

“大家好,很高兴认识大家,我的名字叫Android存储系统,大家可千万别被系统这高大上的词语吓到啊,我做的工作非常的普通平凡,我主要是为app提供文件存储服务的,听了这些概念性的东西,是不是有种味同嚼蜡的感觉,那我就结合例子来介绍下。”

在app运行中,会使用Context的getFileStreamPath、getFilesDir等方法把数据存储到文件中,或者使用Environment类的getExternalStoragePublicDirectory等方法把数据存储到共享文件中。比如相机app在拍完照后会调用Environment类的getExternalStoragePublicDirectory方法把拍的照片存储到 /storage/emulated/0/Pictures/ 目录下。微信、淘宝等app会把关键重要的数据调用Context的getFileStreamPath、getFilesDir等方法把这些核心数据以文件的形式存储下来。

上面举的这些例子都体现了我为app提供文件存储服务,而对于存储的文件又分为两种:一种是对于安全性要求非常高的文件,即文件仅且只能被当前app进程访问读写并且肯定是不能共享的;另外一种对于安全性要求并不是很高,这些文件是可以共享给别的进程使用的。对于安全性极高的文件,我给存放它的区域起了一个响亮的名字内部存储;对于可以共享、安全性低的文件,我同样也给存放它的区域起了个名字外部存储

既然提到了内部存储外部存储,那就来介绍下它们。

内部存储

存储了app运行过程中的私有数据,内部存储的根目录是**/data/user/userid**(如果设备支持多用户的话userid是当前的用户id,一般情况下是0),每个app存储的文件都存放在以包名命名的根目录(这个目录的uid和gid都是当前app的appid)下面,而这个目录的根目录是**/data/user/userid**,如下图
image.png
由于包名命名的根目录的uid和gid都是当前app的appid(appid是app安装成功后的唯一标识),这也就保证了只有app进程的uid与包名命名的根目录的uid一样才可以访问读写该目录下的文件,其他的进程想都不要想。
通过ContextgetFileStreamPath、getFilesDir等方法存储的文件,以及数据库,通过SharedPreferences写入的数据文件都是位于内部存储。

外部存储

位于外部存储的文件是可以被共享的。它的根目录是**/storage/emulated/userid (如果设备支持多用户的话userid是当前的用户id,一般情况下是0),外部存储又可以划分为app专有目录其他目录**。

app专有目录

它的目录路径是 /storage/emulated/userid/Android/data/packageName (packageName是app的包名)、/storage/emulated/userid/Android/obb/packageName,该目录下的文件的uid是当前app的appid,gid是sdcard_rw,也就是除了app的uid与目录uid一致的进程和sdcard_rw进程,其他的进程是不可以访问该目录下的文件。下图显示了app专有目录
image.png
通过Context类的getExternalXXX等方法,可以把文件存放在app专有目录。

其他目录

除了app专有目录外,剩下的这些目录它们都是可以被任何进程访问的(在获取对应权限后),这些目录的uid都是root,gid都是everybody。下图显示了一些目录
image.png
如上图,在**/storage/emulated/0根目录下有Alarms、Android、DCIM等这些目录,而这些目录的uid是root**,也就是说只有root用户才可以创建这些目录,而这些目录的gid是everybody,也就是说每个进程都是可以访问、读写、创建这些目录下面的文件。
通过Environment类的各种getXXX方法,可以把文件存放在其他目录。

我是Android存储系统,我为app提供文件存储服务,app存储的文件有些是私密性的、有些是可共享的,私密性的文件存放于内部存储区域,可共享的文件存放于外部存储区域。不管是外部存储还是内部存储都需要增加权限控制,没有权限控制那岂不是乱套、安全性没有任何保证了。对于内部存储的权限控制,使用文件的uid和gid非常容易做到的,而对于外部存储的权限控制就有些棘手了,虽然棘手我也还是做到了,先悄悄的给大家透漏下采用了FUSE机制,大家千万别着急在后面会详细的介绍到它(好饭不怕晚)。
image.png

多用户

一个Android设备上正常是支持多用户登录的,一般打开Android设备,默认会存在userid是0的用户。当您创建了一个访客用户后 (它的userid一般为10) ,访客有它自己的一套文件和目录的,它是不能访问 useid:0的用户的文件和目录的。

那我存储系统针对多用户也提供了支持,针对多用户不管是内部存储还是外部存储都有它们自己各自的目录,比如内部存储会有 /data/user/0、/data/user/10、/data/user_de/0、/data/user_de/10 这些目录 (0和10分别是对应的userid) ,外部存储也会有对应的 /storage/emulated/0、/storage/emulated/10 这些目录。

大家对我应该有了一定的了解,为app提供的存储服务可不是我一个“人”就能够搞定的啊,我可是有团队的啊,我的团队成员才是我坚实的后盾,它们一直默默的做“幕后者”,那我今天就把它们介绍给大家。

我的团队成员

作为一个系统,自然是由多个模块之间的默契的配合才可以保证系统的正常运行,我作为存储系统当然也不例外。我的团队成员主要由 **vold 进程、StorageManagerService、MediaProvider进程 **组成,我先有请它们做一个简单的自我介绍。

vold不服气的说:“存储系统啊,你这也太小气了吧,霸占舞台这么久了,让我们只是做一个简短的自我介绍,你这种行为就是台霸。”
存储系统:“vold你可是误会我了,让你们做简短介绍的原因是因为在讲到后面的内容的时候,还需要把你们请出来,那时候会给你们充足的时间和舞台来展现你们的全部给大家。”
vold:“sorry,我错怪你的好意了。大家好啊,我的名字是vold,我可是一个拥有root权限系统native进程,看到我前面的定语是不是都是王炸级别的,首先系统native进程就已经很厉害了,我的进程优先级是非常高的,同时我有了root权限后真的就可以‘为所欲为’了。我的工作任务很多如外部存储设备的热插拔事件,后面我还会详细介绍我。”
StorageManagerService:“大家好啊,看到我的名字是不是很长,大家可以叫我SMS,我不像vold是native进程,我所在的进程是systemserver进程,它是java世界的系统进程。我的很多事情其实都是vold在帮我做,比如挂载 (mount) 其实最终是交给vold来帮我完成的,我感觉我就是个‘傀儡’,不‘傀儡’这个词太不好听了,还是叫vold的代理人比较好。”
MediaProvider:“大家好啊,看到我的名字后缀是不是有人想到了ContentProvider,其实也没错我是一个媒体数据提供者,我也是java世界的进程,我不仅提供了文件共享我还提供了权限校验,如若访问外部存储区域的文件可以找我啊。”

前面提到过我主要是为app提供文件存储的服务,那我们来看下app进程往app专有目录外部存储目录下的文件写数据时候,我的团队成员在app文件存储中都做了哪些事情。

app专有目录下的文件写数据
app专有目录下的文件写数据 (专有目录有/storage/emulated/userid/Android/data/packageName (packageName是app的包名)、/storage/emulated/userid/Android/obb/packageName ),在app进程调用getExternalFilesDir方法获取该app专有目录,而app专有目录的根目录 /storage/emulated/ 是从StorageManagerService获取的,StorageManagerService会保存从vold进程获取的目录为 **/storage/emulated **的卷。在往app专有目录下的文件写数据是不需要经过MediaProvider进行权限校验的。

外部存储目录的文件写数据
在app进程调用EnvironmentgetExternalStoragePublicDirectory方法的时候,同样也是从StorageManagerService获取根目录/storage/emulated/ 。但是往外部存储目录 (除了app专有目录外) 下的文件写数据是需要去MediaProvider进程进行一系列的权限校验,如果权限校验通过了则可以写数据,否则会抛异常。

App进程在往外部存储目录存储文件时,StorageManagerService的作用是为App提供可用的卷,而vold进程会创建、挂载可用卷,并且把可用卷告知StorageManagerServiceMediaProvider进程的作用是进行一系列权限校验。当然了这只是展示它们的一小部分作用,它们还有别的作用呢。

app专有目录存储文件是不需要经过MediaProvider进程进行校验的,而往外部存储其他目录是需要MediaProvider进程进行一系列的权限校验的。这又是为啥呢?您肯定还有一系列的别的问题比如MediaProvider是怎样进行权限校验的、vold进程创建的目录为**/storage/emualted的**卷到底是个啥等等,那我就跟随我的脚步来揭开这些谜底吧。

我的成长阶段

我在为app提供存储服务的时候,整个心情是非常美丽的,别问我为啥,因为赠人玫瑰手留余香。俗话说台上一分钟台下十年功,我具有这些能力并不是“一蹴而就”,而是经历了多个成长阶段,最终才“长大成人”的。

我把我的成长阶段分为 vold进程启动、挂载userdata分区、解密DE类型目录、StorageManagerService启动、解密CE类型目录、 创建虚拟卷、挂载虚拟卷、MediaProvider启动这几个阶段。随着每个阶段的完成,我也具有了相应的阶段能力,那就来看下我的“成长记”吧。

注:下面提到的目录中带有userid的,可以把userid先当作0,比如 /storage/emulated/userid 则对应 /storage/emulated/0 这个目录

vold进程启动

存储系统:“vold进程启动作为第一阶段的原因是vold进程是整个存储系统中最核心的存在,很多的能力都需要它来提供,并且它确实也是我的三个团队成员中启动最早的一个,因此它最先启动后才可以保证后面的阶段正常进行。vold现在我把舞台交给你,你自信、大胆的把自己展示给大家吧!”

自我介绍

“大家好啊,我也像lmkdlinstalld一样也是一个系统native进程,我的名字叫vold,相信有很多的朋友会不认识vold到底是啥意思,vold其实是volume deamon的缩写,翻译为中文是‘卷守护’进程。"

“这里的卷是啥子意思吗?”一个进程问到。

卷通常指的是存储设备或存储介质上的一个独立区域,用于存储文件和数据。在操作系统中,一个硬盘可以被分为多个分区,每个分区可以被格式化为一个独立的卷。用直白的话说就是卷是存储设备上的一部分,是可以直接拿来存储文件的。我管理着存储设备 (比如SD卡、U盘) 所有的卷,其中又有虚拟卷 (EmulatedVolume) 、obb卷 (ObbVolume) 、私有卷 (PrivateVolume)、公有卷 (public Volume)、stub卷 (StubVolume),咱们只需要关注虚拟卷即可,(关于如何收集外部存储设备的所有卷用到了监听外部存储设备的热插拔事件,内容与本节无关就不在这讨论)。

我是一个系统native进程,我的工作职责除了管理存储设备的卷监听外部存储设备的热插拔事件 外,还会对存储设备进行挂载、卸载、格式化等操作;对磁盘数据的安全加密(FDE/FBE)和解密,开机过程对data分区进行挂载这些工作,如果要在外部存储上进行文件存储,必须从我这拿到可用的

启动

我的父亲是init进程,因为它的子进程是非常非常多的,这么多子进程何时创建、创建之前需要执行哪些命令又更是多上加多,这么多的信息它完全是无招架之力,为了解决这个问题它创建了init脚本语言,哪个子进程需要创建,则配置自己的init脚本语言即可,下面是我的脚本语言:

service vold /system/bin/vold \
        --blkid_context=u:r:blkid:s0 --blkid_untrusted_context=u:r:blkid_untrusted:s0 \
        --fsck_context=u:r:fsck:s0 --fsck_untrusted_context=u:r:fsck_untrusted:s0
    class core
    ioprio be 2
    task_profiles ProcessCapacityHigh
    shutdown critical
    group root reserved_disk
    reboot_on_failure reboot,vold-failed

上面的脚步语言会告诉init进程,我的进程名字叫vold,在fork成功后会执行/system/bin/vold可执行文件 (后面是需要传递的参数),会执行下面的方法

//文件路径:system/vold/main.cpp

int main(int argc, char** argv) {
    
    省略代码......

    //初始化VolumeManager、NetlinkManager
    VolumeManager* vm;
    NetlinkManager* nm;

    //解析脚本文件中传递的参数
    parse_args(argc, argv);

    省略代码......

    /* Create our singleton managers */
    if (!(vm = VolumeManager::Instance())) {
        LOG(ERROR) << "Unable to create VolumeManager";
        exit(1);
    }

    if (!(nm = NetlinkManager::Instance())) {
        LOG(ERROR) << "Unable to create NetlinkManager";
        exit(1);
    }

    if (android::base::GetBoolProperty("vold.debug", false)) {
        vm->setDebug(true);
    }

    if (vm->start()) {
        PLOG(ERROR) << "Unable to start VolumeManager";
        exit(1);
    }

    VoldConfigs configs = {};

    省略代码......

    //VoldNativeService它是一个binder服务,start方法会把它发布到ServiceManager中
    if (android::vold::VoldNativeService::start() != android::OK) {
        LOG(ERROR) << "Unable to start VoldNativeService";
        exit(1);
    }

    //监听外部存储设备
    if (nm->start()) {
        PLOG(ERROR) << "Unable to start NetlinkManager";
        exit(1);
    }

    省略代码......

    android::IPCThreadState::self()->joinThreadPool();
    LOG(INFO) << "vold shutting down";

    exit(0);
}

执行完上面的方法后,就代表具有root权限的系统native类型的 vold进程 已经启动了,VoldNativeService是我的“接口人”,它是一个binder服务,Android进程的各位大佬们,如果你们想使用我提供的能力,请从ServiceManager中通过**"vold"可以找到我的binder代理,进而可以与VoldNativeService**通信。(监听设备热插拔事件与本章没有关系,因此在这不会涉及)

提供的能力

VoldNativeService:“作为接口人,我有必要介绍下我提供了哪些能力。不是我骄傲我提供的能力确实很多,为了节约大家时间,只介绍与本章有关的能力。”

挂载Fstab中定义的分区或者存储设备

对应的接口:mountFstab(const std::string& blkDevice、const std::string& mountPoint),blkDevice是fstab中定义的分区或者存储设备,mountPoint挂载点

fstab : 是 Linux 和其他类 Unix 系统中的一个重要文件,它用于存储文件系统的静态信息。具体来说,fstab 文件列出了系统上所有的文件系统(包括交换分区)以及它们应该如何被挂载到文件系统中

如下是fstab内容的例子:

# Android fstab file.

#<src>                                                  <mnt_point>            <type>  <mnt_flags and options>                              <fs_mgr_flags>
# system分区挂载于 /system 目录
system                                                  /system                ext4    ro,barrier=1                                         wait,slotselect,avb=vbmeta_system,logical,first_stage_mount
system_ext                                              /system_ext            ext4    ro,barrier=1                                         wait,slotselect,avb=vbmeta_system,logical,first_stage_mount
vendor                                                  /vendor                ext4    ro,barrier=1                                         wait,slotselect,avb=vbmeta,logical,first_stage_mount
product                                                 /product               ext4    ro,barrier=1                                         wait,slotselect,avb,logical,first_stage_mount
/dev/block/by-name/metadata                             /metadata              ext4    noatime,nosuid,nodev,discard,data=journal,commit=1   wait,formattable,first_stage_mount,check
/dev/block/bootdevice/by-name/modem                     /vendor/firmware_mnt   vfat    ro,shortname=lower,uid=0,gid=1000,dmask=227,fmask=337,context=u:object_r:firmware_file:s0 wait,slotselect
/dev/block/bootdevice/by-name/misc                      /misc                  emmc    defaults                                             defaults

# userdata分区挂载于 /data 目录
/dev/block/bootdevice/by-name/userdata                  /data                  f2fs    noatime,nosuid,nodev,discard,reserve_root=32768,resgid=1065,fsync_mode=nobarrier       latemount,wait,check,quota,formattable,fileencryption=ice,reservedsize=128M,sysfs_path=/dev/sys/block/bootdevice,keydirectory=/metadata/vold/metadata_encryption,checkpoint=fs
/devices/platform/soc/a600000.ssusb/a600000.dwc3*       auto                   vfat    defaults                                             voldmanaged=usb:auto
/dev/block/zram0                                        none                   swap    defaults                                             zramsize=2147483648,max_comp_streams=8,zram_backingdev_size=512M

解密设备加密存储空间 (DE)

对应接口:**initUser0 (**关于该接口会在后面详细的介绍)

阶段能力

vold进程启动标志着我存储系统具有了可以监听外部存储设备热插拔事件,创建、挂载、卸载卷,格式化存储设备的能力。

挂载userdata分区

存储系统:“我想大家应该会有个疑问挂载userdata分区从名字上就感觉和存储系统没有任何的关系,为啥会有它呢?并且它为啥作为第二阶段呢?”

在讲原因之前,我需要告诉大家一个惊天的秘密,可注意了这可是惊天的秘密不是惊人的秘密,大家千万不要慌、要镇定。
image.png
在前面介绍存储系统的时候提到把存储区域划分为内部存储外部存储,还记得外部存储的根目录是**/storage/emulated/userid** ** **(如果设备支持多用户的话userid是当前的用户id,正常情况下是0)

外部存储的根目录虽然是**/storage/emulated/userid**,但是该目录下所有的文件、目录等 真正来源于 /data/media/userid 根目录。也就是 **/storage/emulated/userid **只是一个“傀儡”而已, **/data/media/userid **才是“真身”。

如果大家不信,下面两幅图就是铁证 (除了目录的uid和gid不一样外,目录的大小、修改时间等等都是一样的)
/data/media/0目录下的目录
image.png
/storage/emulated/0目录下的目录
image.png

挂载userdata分区其实就是把 **userdata.img **镜像文件挂载到 /data/ 目录,如上外部存储的真正根目录是 **/data/media/userid **,而也只有挂载userdata分区后,才可以访问 **/data/media/userid **目录。

init进程会触发挂载userdata分区的操作,最终会调用到了vold进程mountFstab接口 (参数blkDevice的值为 /dev/block/bootdevice/by-name/userdata ,参数mountPoint的值为 /data) 来进行挂载操作,这样就把 **userdata.img **镜像文件挂载到 **/data **目录了,但是此时 /data 目录是处于加密状态

下面是相关代码,有兴趣可以看下

//文件路径:VoldNativeService.cpp
binder::Status VoldNativeService::mountFstab(const std::string& blkDevice,
                                             const std::string& mountPoint) {
    ENFORCE_SYSTEM_OR_ROOT;
    ACQUIRE_LOCK;
    //调用到下面的fscrypt_mount_metadata_encrypted方法
    return translateBool(
            fscrypt_mount_metadata_encrypted(blkDevice, mountPoint, false, false, "null"));
}

//文件路径:MetadataCrypt.cpp
bool fscrypt_mount_metadata_encrypted(const std::string& blk_device, const std::string& mount_point,
                                      bool needs_encrypt, bool should_format,
                                      const std::string& fs_type) {
    省略代码......

    //获取加密状态,拿到的值一把是 encrypted
    auto encrypted_state = android::base::GetProperty("ro.crypto.state", "");
    if (encrypted_state != "" && encrypted_state != "encrypted") {
        LOG(ERROR) << "fscrypt_mount_metadata_encrypted got unexpected starting state: "
                   << encrypted_state;
        return false;
    }

    //从fstab_default中依据mount_point:/data 获取对应的 挂载信息
    auto data_rec = GetEntryForMountPoint(&fstab_default, mount_point);
    if (!data_rec) {
        LOG(ERROR) << "Failed to get data_rec for " << mount_point;
        return false;
    }

    省略代码......

    LOG(DEBUG) << "Mounting metadata-encrypted filesystem:" << mount_point;

    //进行挂载操作
    mount_via_fs_mgr(mount_point.c_str(), crypto_blkdev.c_str());

    省略代码......
    
    return true;
}


阶段能力

挂载userdata分区标志着可以访问 /data 目录,外部存储的“真身” /data/media/userid 也同样可以访问了,挂载userdata分区用到了vold进程的mountFstab接口,但是此时 /data 目录是处于加密状态,下图就是铁证
/data/media/userid 目录处于加密状态 (红框部分内都是乱码)
image.png

解密DE类型目录

存储系统:“上一阶段提到过 /data 目录下的子目录都处于加密状态,这不该阶段就来对** /data** 目录下的DE类型的目录进行解密。肯定会有人疑惑DE类型目录是啥?为啥只对DE类型目录解密?那我就来解答下。”

先来解释下 **设备加密 (DE) **、**凭据加密 (CE) **

**凭据加密:**它通过在用户锁屏的情况下加密用户数据来提高设备的安全性。凭证加密使用用户在设备上设置的锁屏密码作为加密密钥,因此只有在用户正确输入锁屏密码之后,系统才能解密用户的数据。
设备加密:是在设备启动时就会生效的加密方式,它将整个设备的存储空间都加密起来,包括系统分区和用户分区。这意味着即使在设备被盗取或者遗失的情况下,盗取者也无法轻易访问设备上的数据。

也就是说DE设备加密的简称**,CE凭据加密**的简称,DE类型目录也就是使用设备加密加密的目录,有DE类型目录那自然有CE类型目录。DE类型目录的格式一般都是 **xxx_de **(如 /data/system_de、/data/misc_de、/data/user_de) ,CE类型目录的格式一般是 xxx_cexxx 两种 (如 /data/system_ce、/data/user、/data/app)

为啥只针对DE类型目录解密呢?主要原因是CE类型目录解密是需要用户正确输入锁屏密码后,使用解锁密码作为解密密钥才可以解密。

init进程会发起 解密DE类型目录 的操作,最终会调用到vold进程“接口人”VoldNativeServiceinitUser0方法来完成解密。

下面是对应代码,有兴趣可以看下

//文件路径:VoldNativeService.cpp
binder::Status VoldNativeService::initUser0() {//niu 该方法主要是对user为0时候,在 /data/misc/vold/user_keys/ce/userid/current 和 /data/misc/vold/user_keys/de/userid/current下面生成各种对应目录和文件(秘钥信息),并且把de类型的目录用秘钥进行解密,但是由于ce类型的秘钥需要依赖锁屏信息因此回退后对ce类型目录解密
    ENFORCE_SYSTEM_OR_ROOT;
    ACQUIRE_CRYPT_LOCK;
    //调用fscrypt_init_user0方法
    return translateBool(fscrypt_init_user0());
}

//文件路径:FsCrypt.cpp
bool fscrypt_init_user0() {
    LOG(DEBUG) << "fscrypt_init_user0";

    //下面的代码会把DE和CE类型的关键信息存起来,为后面解密做准备
    if (fscrypt_is_native()) {
        //创建data/misc/vold/user_keys、/data/misc/vold/user_keys/ce、/data/misc/vold/user_keys/de目录
        if (!prepare_dir(user_key_dir, 0700, AID_ROOT, AID_ROOT)) return false; 
        if (!prepare_dir(user_key_dir + "/ce", 0700, AID_ROOT, AID_ROOT)) return false;
        if (!prepare_dir(user_key_dir + "/de", 0700, AID_ROOT, AID_ROOT)) return false;
        if (!android::vold::pathExists(get_de_key_path(0))) {
            if (!create_and_install_user_keys(0, false)) return false;
        }
        // TODO: switch to loading only DE_0 here once framework makes
        // explicit calls to install DE keys for secondary users
        if (!load_all_de_keys()) return false;
    }

    //对DE类型目录开始解密
    if (!fscrypt_prepare_user_storage("", 0, 0, android::os::IVold::STORAGE_FLAG_DE)) {
        LOG(ERROR) << "Failed to prepare user 0 storage";
        return false;
    }

    省略代码......

    fscrypt_init_user0_done = true;
    return true;
}


阶段能力

该阶段标志着我存储系统 /data 目录下的DE类型目录已经解密,只有解密了才可以让存储系统正常运行,但是千万别忘记了, /data 目录下的CE类型的目录还处于加密状态。下图是铁证 (data/user/userid、/data/app都是CE类型目录)

/data/user/0 目录处于加密状态 (红框部分内都是乱码)
image.png
/data/app 目录处于加密状态 (红框部分内都是乱码)
image.png

vold虽然有对CE类型目录具有解密的能力,但是它缺密钥,而这个密钥需要framework层来通知vold。

StorageManagerService启动

存储系统:“有请我团队的第二个成员StorageManagerService上台,我把StorageManagerService比作一个代理人的角色,既然有代理人,那就有被代理人,被代理人就是vold,也就是说在java世界如果想要调用vold的能力,可以直接调用StorageManagerService,把舞台交给StorageManagerService吧。”

StorageManagerService:“存储系统你把我比作代理人,你这可是有点过分了,这是完全看不上我的能力啊,在这么重要的场合和你计较显得我很low。”

“大家好啊,从我的名字可以看出我与ActivityManagerService、WindowManagerService一样也是一个binder服务,如果想调用我的能力可以从ServiceManager中通过mount获取到我的binder代理对象,我位于systemserver进程,我除了代理vold的能力外,我还会监听vold发出的各种事件比如Disk被创建、销毁,卷 (volume)被创建、挂载、卸载。我保存了从vold获取到的可用的卷 (volume)、Disk这些信息。App在往外部存储存储文件的时候需要从我这获取可用的卷,当然我还有别的功能就不在这炫耀了。那来看下我的启动过程吧。”

启动

我的启动过程也非常简单,在启动的时候会调用ServiceManager的getService方法,参数为vold,这样就可以获取到vold进程的“接口人”VoldNativeService的binder代理对象了,同时我也会初始化一个IVoldListener对象,把它当作VoldNativeService的binder代理对象的setListener方法的参数,这样vold进程在有比如Disk创建、销毁事件,卷创建、挂载、卸载等事件的时候,我StorageManagerService就完全可以知道了。

vold进程还给我提了个要求,需要把当前Android设备上正在运行的userid告知它 (因为它没这个能力,只能我来通知它了) ,我会调用VoldNativeService的binder代理对象的onUserAdded方法 (参数就是当前的userid)。

下面是相关代码,有兴趣看下

//文件路径:StorageManagerService.java

private void connectVold() {
        //获取VoldNativeService的binder代理对象
        IBinder binder = ServiceManager.getService("vold");
        if (binder != null) {
            try {
                //注册binder死掉的事件
                binder.linkToDeath(new DeathRecipient() {
                    @Override
                    public void binderDied() {
                        Slog.w(TAG, "vold died; reconnecting");
                        mVold = null;
                        connectVold();
                    }
                }, 0);
            } catch (RemoteException e) {
                binder = null;
            }
        }

        if (binder != null) {
            //转换为IVold
            mVold = IVold.Stub.asInterface(binder);
            try {
                //设置listener
                mVold.setListener(mListener); 
            } catch (RemoteException e) {
                mVold = null;
                Slog.w(TAG, "vold listener rejected; trying again", e);
            }
        }

        省略代码......
    }



阶段能力

此阶段标志着vold进程可以感知framework的一些事件了比如用户解锁,这些事件都可以通过StorageManagerService告知vold进程进而做出相应的动作。同时StorageManagerService也可以监听vold进程的一些事件比如卷 (volume)创建、挂载、卸载。StorageManagerService会把这些信息保存比如已经创建的卷,这样就不用每次都和vold进程通过跨进程的方式获取了,大大的节省了时间。

解密CE类型目录

/data 根目录下CE类型目录还处于加密状态,那该阶段就是来处理这个事情的,解密动作何时发起需要分两种情况:设置锁屏密码不设置。前者需要用户第一次正确输入解锁密码后发起解密操作 ,千万记得只有第一次,如果已经成功解锁并且解密了,后面的解锁操作都不会发起解密操作;后者在StorageManagerService启动后就会发起。

解密CE类型目录的操作需要分unlockUserKeyprepareUserStorage两步进行。

unlockUserKey

可以理解为安装解密key,解密key是与userid、密钥 (若存在解锁密码) 是一一对应关系。解密key及加密相关的信息还有挂载点 /data 会传递给内核。是不是有些懵逼啊,那就用大白话讲就是通知内核关于 /data 目录下面的CE类型目录的解密操作都用我传递给你的key就行了,你要相信我。
unlockUserKey的具体操作是
userid和密钥 (若存在解锁密码) ,生成解密相关的信息 (EncryptionOptions、KeyBuffer、EncryptionPolicy) ,这些信息会通过ioctl
方法传递到内核,并且会把EncryptionPolicy保存在当前userid上。

prepareUserStorage

它的主要作用是会尝试创建 /data/system_ce/userid、/data/misc_ce/userid、/data/vendor_ce/userid、
/data/media/userid、/data/user/userid这些目录 (userid是当前Android设备的userid,一般为0) ,这些目录都是CE类型目录,并且使用unlockUserKey保存的EncryptionPolicy对这些CE类型目录解密。

StorageManagerService会把unlockUserKeyprepareUserStorage操作通过binder告知vold进程VoldNativeService,具体的事项就交给VoldNativeService来完成。

下面是部分相关代码,有兴趣看下

//文件路径:VoldNativeService.cpp
//unlockUserKey操作
binder::Status VoldNativeService::unlockUserKey(int32_t userId, int32_t userSerial,
                                                const std::string& secret) { //niu /data目录下面所有ce类型的目录都被解密了,就都可以用了
    ENFORCE_SYSTEM_OR_ROOT;
    ACQUIRE_CRYPT_LOCK;
    //调用下面的fscrypt_unlock_user_key方法
    return translateBool(fscrypt_unlock_user_key(userId, userSerial, secret));
}

//文件路径:FsCrypt.cpp
bool fscrypt_unlock_user_key(userid_t user_id, int serial, const std::string& secret_hex) {
    LOG(DEBUG) << "fscrypt_unlock_user_key " << user_id << " serial=" << serial << " fscrypt_is_native():" << fscrypt_is_native();
    if (fscrypt_is_native()) {
        //已经安装了policy,直接返回
        if (s_ce_policies.count(user_id) != 0) { 
            LOG(WARNING) << "Tried to unlock already-unlocked key for user " << user_id;
            return true;
        }
        //通过secret拿到auth
        auto auth = authentication_from_hex(secret_hex); 
        if (!auth) return false;
        //调用下面的read_and_install_user_ce_key方法
        if (!read_and_install_user_ce_key(user_id, *auth)) {
            LOG(ERROR) << "Couldn't read key for " << user_id;
            return false;
        }
    } 
    省略代码......
    return true;
}

static bool read_and_install_user_ce_key(userid_t user_id,
                                         const android::vold::KeyAuthentication& auth) {
    //已经安装了policy,直接返回
    if (s_ce_policies.count(user_id) != 0) return true; 
    EncryptionOptions options;
    if (!get_data_file_encryption_options(&options)) return false;
    KeyBuffer ce_key;
    if (!read_and_fixate_user_ce_key(user_id, auth, &ce_key)) return false;
    EncryptionPolicy ce_policy;
    //DATA_MNT_POINT是/data 目录,把挂载点、options、ce_key传递给内核,拿到ce_policy
    if (!install_storage_key(DATA_MNT_POINT, options, ce_key, &ce_policy)) return false;
    //保存ce_policy
    s_ce_policies[user_id] = ce_policy;
    LOG(DEBUG) << "Installed ce key for user " << user_id;
    return true;
}


//文件路径:VoldNativeService.cpp
//prepareUserStorage
binder::Status VoldNativeService::prepareUserStorage(const std::optional<std::string>& uuid,
                                                     int32_t userId, int32_t userSerial,
                                                     int32_t flags) {//niu 准备 /data下面的各种目录,并且通知kernel用key对这些目录解密(好像除了app和一些之外的目录)
    ENFORCE_SYSTEM_OR_ROOT;
    std::string empty_string = "";
    auto uuid_ = uuid ? *uuid : empty_string;
    CHECK_ARGUMENT_HEX(uuid_);

    ACQUIRE_CRYPT_LOCK;
    //调用下面的fscrypt_prepare_user_storage方法
    return translateBool(fscrypt_prepare_user_storage(uuid_, userId, userSerial, flags));
}

//文件路径:FsCrypt.cpp
bool fscrypt_prepare_user_storage(const std::string& volume_uuid, userid_t user_id, int serial,
                                  int flags) {
    LOG(DEBUG) << "fscrypt_prepare_user_storage for volume " << escape_empty(volume_uuid)
               << ", user " << user_id << ", serial " << serial << ", flags " << flags;

    省略代码......

    if (flags & android::os::IVold::STORAGE_FLAG_CE) {

        //尝试创建 data/system_ce/userid /data/misc_ce/userid /data/vendor_ce/userid /data/media/userid 这些目录
        // CE_n key
        auto system_ce_path = android::vold::BuildDataSystemCePath(user_id);
        auto misc_ce_path = android::vold::BuildDataMiscCePath(volume_uuid, user_id);
        auto vendor_ce_path = android::vold::BuildDataVendorCePath(user_id); 
        auto media_ce_path = android::vold::BuildDataMediaCePath(volume_uuid, user_id);

        // 若volume_uuid为null并且userid为0 则返回 /data/data ;否则 /data/user/userid
        auto user_ce_path = android::vold::BuildDataUserCePath(volume_uuid, user_id); 

        省略创建目录代码......
        
        if (fscrypt_is_native()) {
            EncryptionPolicy ce_policy;
            LOG(INFO) << "niu vold fscrypt_prepare_user_storage 解密ce类型  volume_uuid:" << volume_uuid;
            //对下面目录解密
            if (volume_uuid.empty()) {
                if (!lookup_policy(s_ce_policies, user_id, &ce_policy)) return false; //niu 在VoldNativeService::unlockUserKey的时候已经把秘钥存储了,因此从s_ce_policies拿
                if (!EnsurePolicy(ce_policy, system_ce_path)) return false;
                if (!EnsurePolicy(ce_policy, vendor_ce_path)) return false;
            } 

            省略代码......
            //对下面目录解密
            if (!EnsurePolicy(ce_policy, media_ce_path)) return false;
            if (!EnsurePolicy(ce_policy, misc_ce_path)) return false;
            if (!EnsurePolicy(ce_policy, user_ce_path)) return false;
        }

        省略代码......
    }

    省略代码......
    
    return true;
}

阶段能力

此阶段的产物是/data/system_ce/userid、/data/misc_ce/userid、/data/vendor_ce/userid、
/data/media/userid、/data/user/userid这些**CE类型目录,**并且这些CE类型目录也解密了。

歇一歇

咱们从vold进程启动解密CE类型目录阶段讲解的东西确实有点多,学东西不能囫囵吞枣只图快,要反复多咀嚼咀嚼。因此我觉得有必要先小憩一下,整理整理思路,盘点盘点是否存在疑惑。

image.png

先总结下从vold进程启动解密CE类型目录阶段,我存储系统都拥有了哪些能力:

  1. vold进程启动,监听外部存储设备热插拔事件,创建、挂载、卸载卷,格式化存储设备,管理存储设备的卷这是咱们本章用得着的最有用的能力。要想使用vold进程提供的能力,可以使用binder通信与它的“接口人”VoldNativeService联系
  2. 挂载userdata分区,userdata.img镜像文件挂载到 /data 目录,内部存储的根目录是 /data/data/ 外部存储的真正根目录是 /data/media/,这一阶段就可以保证可以访问内部存储外部存储,但是 /data 目录下的CE类型目录和DE类型目录处于加密状态
  3. 解密DE类型目录,DE类型目录被解密后就可以被使用了
  4. StorageManagerService启动,vold进程它处于native层它有一颗想要了解framework层发生哪些事情的“心”,同时framework层也需要使用到vold进程提供的能力,那StorageManagerService它就启动了,StorageManagerService可以监听到vold进程创建了哪些Disk、创建了哪些卷,也可以把framework层的事情比如解锁、哪个user启动了发送给vold进程
  5. 解密CE类型目录,会在 **/data **根目录下尝试创建 **/data/data/ 或者 /data/user/userid/ (它们都是内部存储的根目录) 和 /data/media (真正外部存储根目录) ** 等其他CE类型目录,并且会对这些CE类型目录解密,从此 **/data **目录下的所有的目录都已经被解密了。内部存储完全可以使用了。

存储系统:“我的总结说完了,那大家把自己的疑惑点抛出来吧,我帮大家解惑下。”
image.png
一个进程说到:“您好,我这有一些疑惑 首先我理解的外部存储应该是SD卡之类的存储设备,而现在看来不是这样的;其次既然外部存储的真正根目录是 **/data/media/ **,那‘傀儡’目录 **/storage/emulated **它的作用是啥? ”

存储系统:“谢谢你提问,第一个问题 早期的Android设备外部存储确实是SD卡,但是由于SD卡它的性能要慢于内部存储因此逐渐被淘汰了,而现在的Android设备都是在内部存储空间中划分出一部分来作为外部存储,这时候的外部存储我觉得叫它虚拟外部存储更好,因为它没有真正对应的外部存储设备。”

“第二个问题外部存储的真正根目录是 /data/media/ ,而该目录下的文件或目录是需要被共享的并且还需要有权限管理机制,也就是只有经过权限验证通过才可以共享/data/media/ 目录下的文件。要实现这个需求用到了fuse机制。那我们就先来简单的了解下fuse机制。了解了实现方案也就能解答 **/storage/emulated **的作用了。”

fuse机制

fuse:“大家好啊,我是fuse,fuse是Filesystem in Userspace的简称,翻译为中文是用户空间文件系统,也就是说我可以在用户空间实现一套文件系统,这是不是打破了大家的认知啊,大家的普遍的认知文件系统应该是存在于内核,哈哈。我的存在自有好处,那就是我非常的灵活,毫不夸张的说比孙悟空的72变还灵活。”

那我先用图来介绍下我的工作流程
image.png

图解:
VFS:Virtual File System(虚拟文件系统),因为会存在各种各样的文件系统如ext2、ext4、sdcardfs等,VFS的作用就是制定统一的标准接口,不同的文件系统实现统一的接口即可接入。屏蔽掉不同文件系统的差异,对于上层来说不需要关心是哪种文件系统。
fs:代表各种文件系统。
page cache:页缓存,数据会先写入页缓存,随后被更新到磁盘。
FUSE daemon:用户空间fuse类型的应用,它的主要作用就是处理FUSE driver传递上来的各种信息比如create、open等。
FUSE driver:在内核层FUSE驱动,其实可以简单的把它理解为一种文件系统。

上面两幅图,左边图展示了正常情况下App往文件写入数据的流程:VFS–>fs–>page cache;右边图展示了经过fuse App往文件写入数据的流程:VFS–>FUSE driver–>FUSE daemon–>VFS–>fs–page cache。聪明的你肯定一眼就能发现不同,后者需要把信息通过FUSE driver传递给用户空间FUSE daemon,FUSE daemon经过处理后在把流程转交给VFS。FUSE daemon就是一个中间层,在中间层就可以非常灵活的做各种事情了,想象空间非常大。

上图只是展示了写的过程,其他的读、创建文件/目录、访问目录/文件等操作也是如此。

fuse有三个关键组成:挂载点FUSE daemon、/dev/fuse

挂载点
可不是随便访问一个目录都会被转到FUSE daemon,我可不是一个随便的人,那什么样的目录才会被转到FUSE daemon呢?答案是mount (挂载) 到 /dev/fuse 上的目录,比如 mount /dev/fuse /demo/hello,/dev/fuse挂载到了 /demo/hello 目录,那在访问 /demo/hello 目录及它下面的子目录和文件时,都会被转到FUSE daemon。

FUSE daemon
接收来自内核层的消息,并且处理这些消息,比如监听到open file操作,则可以在open file之前加入一些逻辑比如权限校验。FUSE daemon可做的事情很多,只要你有足够的想象力。

/dev/fuse
它作为FUSE daemon与FUSE driver互相通信的桥梁,FUSE daemon会从 /dev/fuse 把 FUSE driver传递的消息读取到,也会通过它把消息发送给 FUSE driver。

外部存储雏形

介绍完毕fuse机制,那我来介绍下外部存储的雏形,这样在介绍后面阶段的时候,大家心里就有底了。
image.png
上图展示了外部存储的流程,那就来解说下:

  1. vold进程创建、挂载虚拟卷 (虚拟卷说白了就是没有对应真正的外部存储设备,它是虚拟出来的) ,**/dev/fuse **挂载到了虚拟卷的 /storage/emualted/userid 目录 (userid是当前设备的用户id,一般为0)
  2. StorageManagerService会保存虚拟卷
  3. App会通过binder通信从StorageManagerService中获取虚拟卷
  4. App往 /storage/emualated/userid 目录下的某个文件写入数据 (除了外部存储专有目录) 会经过:VFS–> FUSE driver–> MediaProvider进程 --> VFS --> fs --> page cache
  5. MediaProvider进程就是FUSE daemon,MediaProvider会进行权限校验,校验通过后这时候是往 **/data/meida **目录下的对应文件中写数据

外部存储就是基于fuse机制进行了扩展,创建了虚拟卷 (因为现在的Android设备的外部存储是从内部存储划分出了一片区域,不是真正的外部存储),/dev/fuse 挂载到 /storage/emulated/userid 目录 。而MediaProvider进程就是FUSE daemon,MediaProvider进行会进行权限校验等,最后转到了 /data/media 目录下的对应文件。对于
/storage/emulated/userid 目录下文件的访问 (除了外部存储专有目录) 都会被转到MediaProvider进程中。

好了,咱们趁着歇息的间隙,做了阶段性总结,同时也介绍了fuse机制外部存储的雏形,有了这些基础后,在介绍后面的阶段的时候就轻而易举了。

创建虚拟卷

存储系统:“到了创建虚拟卷的阶段了,那就有请虚拟卷EmulatedVolume来给大家介绍。”

EmulatedVolume:“大家好啊,我是虚拟卷对应的类,创建虚拟卷的工作非常简单,只需要把 /data/media 和 **0 **交给我即可,对了我会生成一个 emulated:0 的id。”

创建虚拟卷的工作由vold进程完成,而具体啥时候开始创建虚拟卷的这个时机是由StorageManagerService告知vold的,这个时机一般是 设备的当前user第一次启动的时候或者设备切换为一个新的user,这时候StorageManagerService会调用VoldNativeService的binder代理对象的onUserStarted方法,只需要把userid传递过去即可。

还记得StorageManagerService可以监听卷的创建吗?在虚拟卷创建的时候会通过callback把虚拟卷的信息发送给StorageManagerService。

下面是部分代码,有兴趣可以看下

//文件路径:VoldNativeService.cpp

binder::Status VoldNativeService::onUserStarted(int32_t userId) {
    ENFORCE_SYSTEM_OR_ROOT;
    ACQUIRE_LOCK;
    return translate(VolumeManager::Instance()->onUserStarted(userId));
}

//文件路径:VolumeManager.cpp
int VolumeManager::onUserStarted(userid_t userId) {
    LOG(INFO) << "onUserStarted: " << userId;

    //user第一次启动,则会执行createEmulatedVolumesForUser方法,开始创建虚拟卷
    if (mStartedUsers.find(userId) == mStartedUsers.end()) { 
        createEmulatedVolumesForUser(userId);
    }

    mStartedUsers.insert(userId);

    createPendingDisksIfNeeded();
    return 0;
}


void VolumeManager::createEmulatedVolumesForUser(userid_t userId) {

    // /data/media 、userid作为参数传递给EmulatedVolume
    auto vol = std::shared_ptr<android::vold::VolumeBase>(
            new android::vold::EmulatedVolume("/data/media", userId));
    vol->setMountUserId(userId);
    mInternalEmulatedVolumes.push_back(vol);
    //调用create方法
    vol->create();

    省略代码......
}

//文件路径:model/VolumeBase.cpp
status_t VolumeBase::create() {
    CHECK(!mCreated);

    mCreated = true;
    status_t res = doCreate();

    auto listener = getListener();
    if (listener) {
        //通知StorageManagerService
        listener->onVolumeCreated(getId(), static_cast<int32_t>(mType), mDiskId, mPartGuid,
                                  mMountUserId);
    }
    //把卷的状态设置为 没挂载
    setState(State::kUnmounted);
    return res;
}

阶段能力

此阶段,rawpath为 /data/media、userid为0 的虚拟卷创建成功,该虚拟卷会发送给StorageManagerService。

挂载虚拟卷

别看我虚拟卷只是一个“虚拟”的卷,凡是卷该有的功能我都有,卷是只有挂载了 (mounted) 了后才能被使用,我虚拟卷也不例外,vold进程具有挂载卷的能力,但是具体啥时候挂载得由StorageManagerService通知。

StorageManagerService在收到虚拟卷创建的事件后,会根据系统是否支持外部存储及其他条件来判断是否挂载虚拟卷,如果挂载则会调用VoldNativeService的binder代理对象的mount方法 (参数有虚拟卷id: emulated:0,mountflags,mountUserid: 0,IVoldMountCallback: 接收mount回调事件) 。

挂载虚拟卷主要分为两步工作:sdcardfs相关处理fuse相关处理,那就从这两步来介绍挂载虚拟卷。

StorageManagerService调用mount的相关代码如下,有兴趣看下

//文件路径:StorageManagerService.java
private void mount(VolumeInfo vol) {
        try {
            // TODO(b/135341433): Remove cautious logging when FUSE is stable
            Slog.i(TAG, "Mounting volume " + vol);
            //对mVold的mount方法调用,最终会调用到VoldNativeService的mount方法,注册挂载回调
            mVold.mount(vol.id, vol.mountFlags, vol.mountUserId, new IVoldMountCallback.Stub() {
                @Override
                public boolean onVolumeChecking(FileDescriptor fd, String path,
                        String internalPath) {//niu fd指向 /dev/fuse  path:/storage/emulated  internalPath:/data/media
                    省略代码......
                }
            });
            Slog.i(TAG, "Mounted volume " + vol);
        } catch (Exception e) {
            Slog.wtf(TAG, e);
        }
    }


//文件路径:VoldNativeService.cpp
//mount方法
binder::Status VoldNativeService::mount(
        const std::string& volId, int32_t mountFlags, int32_t mountUserId,
        const android::sp<android::os::IVoldMountCallback>& callback) {
    ENFORCE_SYSTEM_OR_ROOT;
    CHECK_ARGUMENT_ID(volId);
    ACQUIRE_LOCK;

    //从VolumeManager中根据volid查找对应的volume,这时候的 volId是 emulated:0 是一个EmulatedVolume
    auto vol = VolumeManager::Instance()->findVolume(volId);
    if (vol == nullptr) {
        return error("Failed to find volume " + volId);
    }
    
    vol->setMountFlags(mountFlags);
    vol->setMountUserId(mountUserId);

    vol->setMountCallback(callback);
    //开始挂载
    int res = vol->mount();
    vol->setMountCallback(nullptr);

    if (res != OK) {
        return translate(res);
    }

    return translate(OK);
}

sdcardfs相关处理

sdcardfs的主要功能是管理Android系统中的/sdcard目录,该目录被用作“外部”存储,在android13上sdcard虽然已经不用了,但是一部分sdcardfs的代码还保留着。那就来介绍下sdcardfs相关处理的内容吧。

首先会创建 /mnt/runtime/default/emulated、/mnt/runtime/read/emulated、/mnt/runtime/write/emulated、
/mnt/runtime/full/emulated 这几个目录,这几个目录原先的作用主要是支持sdcardfs运行时权限管理的,在android13已经没有这些作用了,其中 /mnt/runtime/full/emulated 在后面会使用到它。

如果当前android系统sdcardfs可用,并且当前的请求mount的userid为0,则会调用 /system/bin/sdcard 可执行文件把 /data/media 分别挂载到上面创建的几个目录。如下图 (红框内) :
image.png

下面是部分相关代码,有兴趣可以看下:

//文件路径:model/EmulatedVolume.cpp
status_t EmulatedVolume::doMount() {
    std::string label = getLabel();
    bool isVisible = isVisibleForWrite();

    mSdcardFsDefault = StringPrintf("/mnt/runtime/default/%s", label.c_str());
    mSdcardFsRead = StringPrintf("/mnt/runtime/read/%s", label.c_str());
    mSdcardFsWrite = StringPrintf("/mnt/runtime/write/%s", label.c_str());
    mSdcardFsFull = StringPrintf("/mnt/runtime/full/%s", label.c_str());

    //mRawPath为 /data/media
    setInternalPath(mRawPath);
    //path为/storage/emulated
    setPath(StringPrintf("/storage/%s", label.c_str()));

    //创建sdcardfs相关的目录
    if (fs_prepare_dir(mSdcardFsDefault.c_str(), 0700, AID_ROOT, AID_ROOT) ||
        fs_prepare_dir(mSdcardFsRead.c_str(), 0700, AID_ROOT, AID_ROOT) ||
        fs_prepare_dir(mSdcardFsWrite.c_str(), 0700, AID_ROOT, AID_ROOT) ||
        fs_prepare_dir(mSdcardFsFull.c_str(), 0700, AID_ROOT, AID_ROOT)) {
        PLOG(ERROR) << getId() << " failed to create mount points";
        return -errno;
    }

    省略代码......

    //可以使用sdcardfs并且userid为0,则执行下面逻辑
    if (mUseSdcardFs && getMountUserId() == 0) { 
        LOG(INFO) << "Executing sdcardfs";
        int sdcardFsPid;

        //fork一个子进程
        if (!(sdcardFsPid = fork())) {

            // kSdcardFsPath为/system/bin/sdcard,执行该可执行文件
            // clang-format off
            if (execl(kSdcardFsPath, kSdcardFsPath,
                    "-u", "1023", // AID_MEDIA_RW
                    "-g", "1023", // AID_MEDIA_RW
                    "-m",
                    "-w",
                    "-G",
                    "-i",
                    "-o",
                    mRawPath.c_str(),  //niu mRawPath /data/media
                    label.c_str(),     //niu label为 emulated
                    NULL)) { //niu execl主要的工作是 把/mnt/runtime/default/emulated,/mnt/runtime/read/emulated,/mnt/runtime/write/emulated,/mnt/runtime/full/emulated绑定到/data/media
                // clang-format on
                PLOG(ERROR) << "Failed to exec";
            }

            LOG(ERROR) << "sdcardfs exiting";
            _exit(1);
        }

        省略代码......
    }

    省略代码......

}

fuse相关处理

首先会尝试创建 /data/media/userid、/data/media/userid/Android、/data/media/userid/Android/data (外部存储专有目录根目录) 、/data/media/userid/Android/obb (外部存储专有目录根目录) 、
/data/media/userid/Android/media 这些目录 (userid只有一个用户情况下,它的值是0) 。

介绍个概念**bind mount **它是Linux系统中的一个特性,允许你将一个目录或者文件,而不是整个设备,挂载到一个指定的目录上。

挂载fuse

还记得fuse机制在使用的时候需要把 /dev/fuse 挂载到 某个目录吗?挂载fuse就是来做这个事情了,这也是fuse机制非常重要的一环,在挂载之前需要尝试创建 /mnt/user/userid/emulated、/mnt/pass_through/userid/emulated 这几个目录,最后把 **/dev/fuse 挂载到 /mnt/user/userid/emulated **目录。

是不是发现和外部存储雏形中介绍的不一样,那里介绍 **/dev/fuse 是挂载到 /storage/emulated/userid **目录的,而这里却没有发现 **/storage/emulated/userid **的“身影”。

在解释原因之前,先来介绍个概念挂载命名空间 它是Linux内核提供的一种隔离机制,它允许每个命名空间拥有自己独立的文件系统挂载点列表。这意味着,在一个挂载命名空间中进行的挂载或卸载操作,对于其他挂载命名空间是不可见的),举个例子比如 A进程 它的 /dev/fuse 是挂载到 /mnt/testA 目录的,则B进程 是可以把 它的 /dev/fuse 是挂载到 /mnt/testB 目录的,并且是不会影响A进程的挂载,即使A进程把 /mnt/testA 目录卸载了,也不会影响到B进程。

了解了这个概念后,一切就都好说了,在app进程被**zygote **fork后,会执行下面的一段代码

//文件路径:com_android_internal_os_Zygote

static void MountEmulatedStorage(uid_t uid, jint mount_mode,
        bool force_mount_namespace,
        fail_fn_t fail_fn) {
  
  省略代码......

  //创建私有的挂载命名空间
  // Create a second private mount namespace for our process
  ensureInAppMountNamespace(fail_fn);

  // Handle force_mount_namespace with MOUNT_EXTERNAL_NONE.
  if (mount_mode == MOUNT_EXTERNAL_NONE) {
    return;
  }

  const userid_t user_id = multiuser_get_user_id(uid);
  const std::string user_source = StringPrintf("/mnt/user/%d", user_id);
  

  //尝试创建 /mnt/user/userid 目录
  PrepareDir(user_source, 0710, user_id ? AID_ROOT : AID_SHELL,
             multiuser_get_uid(user_id, AID_EVERYBODY), fail_fn);

  bool isAppDataIsolationEnabled = GetBoolProperty(kVoldAppDataIsolation, false);

  //MediaProvider进程会走这
  if (mount_mode == MOUNT_EXTERNAL_PASS_THROUGH) {
      const std::string pass_through_source = StringPrintf("/mnt/pass_through/%d", user_id);
      PrepareDir(pass_through_source, 0710, AID_ROOT, AID_MEDIA_RW, fail_fn);
      BindMount(pass_through_source, "/storage", fail_fn);
  } else if (mount_mode == MOUNT_EXTERNAL_INSTALLER) {
      const std::string installer_source = StringPrintf("/mnt/installer/%d", user_id);
      BindMount(installer_source, "/storage", fail_fn);
  } else if (isAppDataIsolationEnabled && mount_mode == MOUNT_EXTERNAL_ANDROID_WRITABLE) {
      const std::string writable_source = StringPrintf("/mnt/androidwritable/%d", user_id);
      BindMount(writable_source, "/storage", fail_fn);
  } else {
      //正常app是走这,把 /storage  bind mount /mnt/user/userid
      BindMount(user_source, "/storage", fail_fn); 
  }
}

上面代码会创建私有挂载命名空间,而对于正常app来说会把 **/storage 目录 bind mount /mnt/user/userid **目录,而 **/dev/fuse **挂载到 /mnt/user/userid/emulated 目录,进而推导出 /dev/fuse 挂载到 /storage/emulated 目录。

也就是说在app子进程创建成功后,会创建它的私有挂载命名空间/dev/fuse 也会被挂载到 /storage/emulated 目录。

挂载pass_through

如果系统支持使用sdcardfs的话,会把 **/mnt/pass_through/userid/emulated **目录 bind mount 到 **/mnt/runtime/full/emulated **目录,而之前 /data/media 是mount到了 **/mnt/runtime/full/emulated **目录,进而可以推导出 /data/media 是mount到了 /mnt/pass_through/userid/emulated 目录。
若不支持sdcardfs的话,会直接把
/data/media
是mount到了 /mnt/pass_through/userid/emulated 目录。请记住/mnt/pass_through/userid/emulated这个目录后面会用到。

app专有目录特殊处理

还记得外部存储的app专有目录吗?app专有目录是 /storage/emulated/userid/Android/data/ (packageName是app的包名)、/storage/emulated/userid/Android/obb/packageName,而app专有目录它们的uid与自己对应app的appid是一样的,也就是只有app才能访问自己app专有目录,如下图
image.png
既然app专有目录的uid和gid就已经携带了权限校验的功能,那在访问它们的时候肯定是完全没必要在经过MediaProvider进程进行权限校验了,那如何做到呢?

答案是:因为在app进程fork后,zygote已经把 /storage 目录 bind mout到了 /mnt/user/userid 目录,在EmulatedVolume::mountFuseBindMounts方法中会把 /mnt/user/userid/emulated/Android/data目录 bind mount到 /data/media/userid/Android/data目录,
把/mnt/user/userid/emulated/Android/obb目录bind mount到 /data/media/userid/Android/obb 目录。而上面的bind mount。进而可以推导出 /storage/emulated/userid/Android/data bind mount 到 /data/media/userid/Android/data目录,/storage/emulated/userid/Android/obb bind mount 到 /data/media/userid/Android/obb目录。

最后会把挂载的结果通过IVoldMountCallback告知StorageManagerService。

下面是部分相关代码,有兴趣看下

//文件路径:model/EmulatedVolume.cpp
status_t EmulatedVolume::doMount() {
    std::string label = getLabel();
    bool isVisible = isVisibleForWrite();

    省略代码......

    //isVisible为true
    if (isVisible) {
        省略代码......
        
        LOG(INFO) << "Mounting emulated fuse volume";
        android::base::unique_fd fd;
        int user_id = getMountUserId();
        // /data/media/userid
        auto volumeRoot = getRootPath(); 

        // 尝试创建/data/media/userid/Android、创建/data/media/userid/Android/data、
        //创建/data/media/userid/Android/obb、创建/data/media/userid/Android/media 目录
        status_t res = PrepareAndroidDirs(volumeRoot); 
        if (res != OK) {
            LOG(ERROR) << "Failed to prepare Android/ directories";
            return res;
        }

        //挂载fuse,执行下面MountUserFuse方法
        res = MountUserFuse(user_id, getInternalPath(), label, &fd); //niu getInternalPath()为 /data/media ,  fd指向 /dev/fuse
        if (res != 0) {
            PLOG(ERROR) << "Failed to mount emulated fuse volume";
            return res;
        }

        省略代码......
        
        auto callback = getMountCallback();
        if (callback) {
            bool is_ready = false;
            //把结果告知StorageManagerService
            callback->onVolumeChecking(std::move(fd), getPath(), getInternalPath(), &is_ready);
            if (!is_ready) {
                return -EIO;
            }
        }
        //进入下面逻辑
        if (!mFuseBpfEnabled) {
            // Only do the bind-mounts when we know for sure the FUSE daemon can resolve the path.
            //mount data、obb目录
            res = mountFuseBindMounts();
            if (res != OK) {
                return res;
            }
        }

        省略代码......
    }

    return OK;
}

status_t MountUserFuse(userid_t user_id, const std::string& absolute_lower_path,
                       const std::string& relative_upper_path, android::base::unique_fd* fuse_fd) { //niu absolute_lower_path:/data/media  relative_upper_path:emualted

    省略掉创建 /mnt/user/userid/emulated、/mnt/pass_through/userid/emulated 这几个目录的代码......

    //进入下面逻辑
    if (relative_upper_path == "emulated") {
        省略掉创建软链接的代码......
    }

    // Open fuse fd.
    fuse_fd->reset(open("/dev/fuse", O_RDWR | O_CLOEXEC));
    if (fuse_fd->get() == -1) {
        PLOG(ERROR) << "Failed to open /dev/fuse";
        return -1;
    }

    // Note: leaving out default_permissions since we don't want kernel to do lower filesystem
    // permission checks before routing to FUSE daemon.
    const auto opts = StringPrintf(
        "fd=%i,"
        "rootmode=40000,"
        "allow_other,"
        "user_id=0,group_id=0,",
        fuse_fd->get());

    //把 /dev/fuse 挂载到 /mnt/user/userid/emulated 目录
    result = TEMP_FAILURE_RETRY(mount("/dev/fuse", fuse_path.c_str(), "fuse",
                                      MS_NOSUID | MS_NODEV | MS_NOEXEC | MS_NOATIME | MS_LAZYTIME,
                                      opts.c_str())); //niu /dev/fuse 挂载到 /mnt/user/userid/emulated
    省略代码.......

    if (IsSdcardfsUsed()) {
        //进入这里
        std::string sdcardfs_path(
                StringPrintf("/mnt/runtime/full/%s", relative_upper_path.c_str())); //niu /mnt/runtime/full/emulated

        LOG(INFO) << "Bind mounting " << sdcardfs_path << " to " << pass_through_path;
        //sdcardfs_path在之前已经bint mound到了 /data/media,因此 pass_through_path bind mount 到了 /data/media
        return BindMount(sdcardfs_path, pass_through_path); 
    } else {
        省略代码......
    }
}

阶段能力

此阶段创建了一些目录,在app fork成功后,zygote会把**/dev/fuse **间接的挂载到了 **/storage/emulated/userid 目录,这样就可以保证对 /storage/emulated/userid (除了app专有目录外) **下面子目录及文件的访问都可以转到MediaProvider进程。
同时把 **app专有目录 ** /storage/emulated/userid/Android/data bind mount 到 /data/media/userid/Android/data目录,/storage/emulated/userid/Android/obb bind mount 到 /data/media/userid/Android/obb目录,来做到在访问app专有目录的时候不会经过MediaProvider进程进行权限校验。

MediaProvider进程启动

存储系统:“MediaProvider进程启动,是我存储系统成长的最后一个阶段,经过此阶段后,我终于‘长大成人’了。”
MediaProvider进程的作用就是fuse机制中的FUSE daemon,它的主要作用就是进行权限校验、文件共享等,那就简单介绍下它的一个启动过程。

虚拟卷挂载成功后,StorageManagerService会通过IVoldMountCallback收到挂载成功的事件,其中的参数有FileDescriptor fd、String path、String internalPath,它们的值分别为 fd对应打开的**/dev/fuse **文件 (这样MediaProvider进程就可以从 通过/dev/fuse与FUSE driver互相通信了) 、/storage/emulated、/data/media,这些参数都最终会FuseDaemon类,FuseDaemon会把这些参数传递给native,进而MediaProvider进程的fuse功能启动完成,由于MediaProvider的篇幅过长就不在这赘述了。

解决死循环问题

前面提到过app进程fork后,zygote会把**/dev/fuse **间接的挂载到了 **/storage/emulated/userid 目录,这样就可以保证对 /storage/emulated/userid (除了app专有目录外) **下面子目录及文件的访问都可以转到MediaProvider进程。

不知道大家发现没存在一个问题,那如果一个app访问了 **/storage/emulated/userid 目录下的某个文件则会转移到MediaProvider进程,因为MediaProvider进程的/dev/fuse **间接的挂载到了 /storage/emulated/userid 目录的,那MediaProvider进程对该文件的访问也会再次转移到MediaProvider进程,这样就是一个无限死循环了。那这种该咋解决呢?

答案是:MediaProvider进程在fork后,同样也会执行下面的代码

//文件路径:com_android_internal_os_Zygote

static void MountEmulatedStorage(uid_t uid, jint mount_mode,
        bool force_mount_namespace,
        fail_fn_t fail_fn) {
  
  省略代码......

  //MediaProvider进程会走这
  if (mount_mode == MOUNT_EXTERNAL_PASS_THROUGH) {
      const std::string pass_through_source = StringPrintf("/mnt/pass_through/%d", user_id);
      PrepareDir(pass_through_source, 0710, AID_ROOT, AID_MEDIA_RW, fail_fn);
      BindMount(pass_through_source, "/storage", fail_fn);
  } 

  省略其他代码......
}

执行了上面代码后,MediaProvider进程会把 **/storage **目录bind mount到 **/mnt/pass_through/userid/ 目录,而 /data/media 是挂载到 /mnt/pass_through/userid/ 目录的,进而可以推导出MediaProvider进程 data/media 挂载到 /storage/emulated/userid 目录。这样就可以做到 app访问/storage/emulated/userid **目录的某个文件–> MediaProvider进程 --> 经过权限校验,访问 **/data/media/userid **目录下对应的文件。

/storage/emulated目录uid和gid

外部存储的真正目录 /data/media 它的uid和gid都是media_rw,就是因为这个原因导致其他的app进程是没有办法访问该目录下面的任何文件,这就是一个“天然屏障”。
而 /storage/emulated 目录 (除了app专有目录外) 的uid和gid分别是root和everybody,所有的app进程都是在everybody组,这也是app进程能访问该目录下任何文件的原因。

阶段能力

经过此阶段后,存储系统的外部存储既可以做到文件共享又可以做到权限控制,app在访问 /storage/emulated/userid/ 目录下除了app专有目录外的任何文件都会被转移到MediaProvider进程进行权限校验,校验通过后会访问“真身” **/data/media/userid **目录下的对应文件。

总结

我存储系统整个“成长”经过了以下阶段,在每个阶段都拥有不同的能力

  1. vold进程启动,监听外部存储设备热插拔事件,创建、挂载、卸载卷,格式化存储设备,管理存储设备的卷这是咱们本章用得着的最有用的能力。要想使用vold进程提供的能力,可以使用binder通信与它的“接口人”VoldNativeService联系
  2. 挂载userdata分区,userdata.img镜像文件挂载到 /data 目录,内部存储的根目录是 /data/data/ 外部存储的真正根目录是 /data/media/,这一阶段就可以保证可以访问内部存储外部存储,但是 /data 目录下的CE类型目录和DE类型目录处于加密状态
  3. 解密DE类型目录,DE类型目录被解密后就可以被使用了
  4. StorageManagerService启动,vold进程它处于native层它有一颗想要了解framework层发生哪些事情的“心”,同时framework层也需要使用到vold进程提供的能力,那StorageManagerService它就启动了,StorageManagerService可以监听到vold进程创建了哪些Disk、创建了哪些卷,也可以把framework层的事情比如解锁、哪个user启动了发送给vold进程
  5. 解密CE类型目录,会在 **/data **根目录下尝试创建 **/data/data/ 或者 /data/user/userid/ (它们都是内部存储的根目录) 和 /data/media (真正外部存储根目录) ** 等其他CE类型目录,并且会对这些CE类型目录解密,从此 **/data **目录下的所有的目录都已经被解密了。内部存储完全可以使用了。
  6. 创建虚拟卷,创建了rawPath为 /data/media,id为emulated:0 的虚拟卷 (EmulatedVolume),并且会通知StorageManagerService该卷创建成功了。
  7. 挂载虚拟卷,只有卷挂载成功后才能供app使用,把 **/dev/fuse 挂载到 /mnt/user/userid/emulated 目录,在app fork成功后,zygote会把/dev/fuse **间接的挂载到了 **/storage/emulated/userid 目录,这样就可以保证对 /storage/emulated/userid (除了app专有目录外) **下面子目录及文件的访问都可以转到MediaProvider进程。
    同时把 **app专有目录 ** /storage/emulated/userid/Android/data bind mount 到 /data/media/userid/Android/data目录,/storage/emulated/userid/Android/obb bind mount 到 /data/media/userid/Android/obb目录,来做到在访问app专有目录的时候不会经过MediaProvider进程进行权限校验。
  8. MediaProvider进程启动,zygote会把MediaProvider进程的 /data/media 挂载到 /storage/emulated/userid 目录,这样就能保证MediaProvider既能做到权限校验又能访问“真身” /data/media 目录下对应的文件了。

我存储系统主要为app存储数据服务,对于安全级别高的文件可以把文件存储于内部存储,对于可共享、安全级别不高的文件可以把文件存储于外部存储,为外部存储又可分为app专有目录其他目录,在往app专有目录存储文件的时候是不需要经过MediaProvider进程进行权限校验的,而往其他目录存储文件的时候是需要经过MediaProvider进行权限校验的。同时我也支持多用户,针对多用户,内部存储和外部存储都会有对应的不同目录。

对于app来说拿到的外部存储的根目录是 /storage/emulated/userid,但是真正的目录是 /data/media/userid,前者是一个“傀儡”,后者才是“真身”。

app拿到的外部存储的根目录信息是从StorageManagerService获取到的,StorageManagerService会把vold进程创建、挂载的目录为 **/storage/emulated **的虚拟卷 (EmulatedVolume) 保存起来供app来使用。

这就是我的“成长记”,谢谢大家。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1705037.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【代码随想录——回溯算法——三周目】

1. 子集2 这题需要先进行排序&#xff0c;和候选人那题类似。防止出现重复的子集。 func subsetsWithDup(nums []int) [][]int {path : make([]int, 0)res : make([][]int, 0)sort.Ints(nums)var dfs func(nums []int, start int)dfs func(nums []int, start int) {res app…

08Django项目--用户管理系统--查(前后端)

对应视频链接点击直达 TOC 一些朋友加我Q反馈&#xff0c;希望有每个阶段的完整项目代码&#xff0c;那从今天开始&#xff0c;我会上传完整的项目代码。 用户管理&#xff0c;简而言之就是用户的增删改查。 08项目点击下载&#xff0c;可直接运行&#xff08;含数据库&…

1967python多媒体素材管理系统mysql数据库Django结构layUI布局计算机软件工程网页

一、源码特点 python Django多媒体素材管理系统是一套完善的web设计系统mysql数据库 &#xff0c;对理解python编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 开发环境pycharm mysql 5.0 到5.5 依赖包 Dj…

推荐丨 IP地址如何申请SSL证书实现https

为IP地址申请SSL证书可以让用户通过HTTPS协议安全地访问直接绑定到IP地址的网站或服务。以下是申请IP地址SSL证书的一般步骤&#xff1a; 1 选择支持IP证书的CA&#xff1a;直接为IP地址颁发SSL/TLS证书并不常见&#xff0c;国内厂商JoySSL有提供IP证书&#xff0c;登录其官网…

东莞MES管理系统在电子工厂的益处

东莞MES管理系统对东莞电子企业带来了许多好处&#xff0c;包括但不限于以下几点&#xff1a; 提高生产效率&#xff1a;MES系统可以优化生产计划、监控生产过程&#xff0c;提高生产效率&#xff0c;减少生产中的浪费和停机时间&#xff0c;提高产能利用率。 优化库存管理&a…

R可视化:另类的柱状图

介绍 方格状态的柱状图 加载R包 knitr::opts_chunk$set(echo TRUE, message FALSE, warning FALSE) library(patternplot) library(png) library(ggplot2) library(gridExtra)rm(list ls()) options(stringsAsFactors F)导入数据 data <- read.csv(system.file(&qu…

M00238-固定翼无人机集群飞行仿真平台MATLAB完整代码含效果

一个小型无人机集群仿真演示平台&#xff0c;使用matlab和simulink搭建。 给出的例子是5架的&#xff0c;当然如果你愿意花时间&#xff0c;也可以把它扩展到10架&#xff0c;20架甚至更多。 输入&#xff1a;5架飞机的规划路径 输出&#xff1a;每架无人机每个时刻的13个状态量…

如何将天猫内容保存为PDF格式?详细步骤与实战解析

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、引言&#xff1a;保存天猫内容的重要性 二、环境准备与工具安装 1. 安装必要的Python包…

2024年电工杯高校数学建模竞赛(B题) 建模解析| 大学生平衡膳食食谱的优化设计

问题重述及方法概述 问题1&#xff1a;膳食食谱的营养分析评价及调整 数学方法&#xff1a;线性规划模型、营养素评价模型、比较分析 可视化数据图&#xff1a;营养素含量表、营养素摄入量对比图、营养素缺乏情况图 问题2&#xff1a;基于附件3的日平衡膳食食谱的优化设计 数…

idea 中配置 Java 注释模板

引言&#xff1a; 在软件工程中&#xff0c;良好的代码注释习惯对于项目的可维护性和可读性至关重要。IntelliJ IDEA&#xff0c;作为一款强大的Java开发IDE&#xff0c;提供了灵活的注释模板配置功能&#xff0c;帮助开发者快速生成规范的代码注释。本文将详细介绍如何在Inte…

2024年上半年信息系统项目管理师下午真题及答案(第二批)

试题一 某项目计划工期为10个月&#xff0c;预算210万元&#xff0c;第7个月结束时&#xff0c;项目经理进行了绩效评估&#xff0c;发现实际完成了总计划进度的70%。项目的实际数据如表所示&#xff1a; 单击下面头像图片领取更多软考独家资料

【Ambari】Docker 安装Ambari 大数据单机版本

目录 一、前期准备 1.1 部署 docker 1.2 部署 docker-compose 1.3 版本说明 二 、镜像构建启动 2.1 系统镜像构建 2.2 安装包源镜像构建 2.3 kdc镜像构建 2.4 集群安装 2.5 容器导出为镜像 三、Ubuntu环境安装测试 3.1 环境准备 3.2 集群容器启动 一、前期准备 1.…

【C++题解】1125. 删除字符串中间的*

问题&#xff1a;1125. 删除字符串中间的* 类型&#xff1a;字符串 题目描述&#xff1a; 输入一个字符串&#xff0c;将串前和串后的*保留&#xff0c;而将中间的 * 删除。 输入&#xff1a; 一个含*的字符串。 输出&#xff1a; 删除了串中的*的字符串。 样例&#xf…

夏日防晒笔记

1 防晒霜 使用方法&#xff1a;使用前上下摇晃瓶身4至5次&#xff0c;在距离肌肤10至15cm处均匀喷上。如在面部使用&#xff0c;请先喷在掌心再均匀涂抹于面部。排汗量较多时或擦拭肌肤后&#xff0c;请重复涂抹以确保防晒效果。卸除时使用普通洁肤产品洗净即可。

C++:STL容器的学习-->string

C:STL容器的学习-->string 1. 构造方法2. string的赋值操作3. string字符串的拼接4. string 查找和替换5. string字符串的比较6. string字符存取7. string 插入和删除8. string截取 需要添加头文件#include <string> 1. 构造方法 string() 创建空的字符串 string(c…

kafka3.6.1版本学习

kafka目录结构 bin linux系统下可执行脚本文件 bin/windows windows系统下可执行脚本文件 config 配置文件 libs 依赖类库 licenses 许可信息 site-docs 文档 logs 服务日志 启动ZooKeeper 进入Kafka解压缩文件夹的config目录&#xff0c;修改zookeeper.properties配置文件 #t…

深度学习模型在OCR中的可解释性问题与提升探讨

摘要&#xff1a; 随着深度学习技术在光学字符识别&#xff08;OCR&#xff09;领域的广泛应用&#xff0c;人们对深度学习模型的可解释性问题日益关注。本文将探讨OCR中深度学习模型的可解释性概念及其作用&#xff0c;以及如何提高可解释性&#xff0c;使其在实际应用中更可…

企业微信hook接口协议,ipad协议http,语音转文字

语音转文字 参数名必选类型说明uuid是String每个实例的唯一标识&#xff0c;根据uuid操作具体企业微信msgid是int要转文字的语音消息id 请求示例 {"uuid":"a4ea6a39-4b3a-4098-a250-2a07bef57355","msgid":1063645 } 返回示例 {"data&…

App Inventor 2 Encrypt.Security 安全性扩展:MD5哈希,SHA/AES/RSA/BASE64

这是关于App Inventor和Thunkable安全性的扩展&#xff0c;它提供MD5哈希&#xff0c;SHA1和SHA256哈希&#xff0c;AES加密/解密&#xff0c;RSA加密/解密&#xff0c;BASE64编码/解码方法。 权限 此扩展程序不需要任何权限。 事件 OnErrorOccured 抛出任何异常时将触发此事件…

20240527画图-筛选较长、均长、正交的基向量

1. LLM网址和prompt deepseek网址 prompt 请用python写出以下程序&#xff1a; 1、在x属于&#xff08;0&#xff0c;1920&#xff09;、y属于&#xff08;0&#xff0c;1080&#xff09;范围内&#xff0c;随机生成100个点&#xff0c;并画图 2、从这些点中选取3个点&#…