现状介绍

        由于技术和成本的问题,早期Android系统一般配置有较小的内置存储,另外还提供存储卡的插槽作为主存储(外置存储)。 随着技术升级和内置存储成本的降低,大部分Android系统已经取消了存储卡的插槽,取而代之的是更大的内置存储。 但是很多程序已经习惯了使用外置存储,为了兼容老的程序和开发者的使用习惯,即使在没有存储卡插槽的手机上,我们也能看到/sdcard目录,主存储也是存在的。另外一方面Android也支持外接越来越多类型的设备来作为外置存储,所以Android需要对这些外置设备进行管理。 这就是vold 和MountService的基本作用:对外置存储进行管理。

         Android有个主存储(Primary)的概念,主存储作为主要的外置存储,是提供给应用程序使用的,一般通过Context的几个获取外置存储路径的api,基本上返回的都是主存储的磁盘路径。主存储对于Android手机是必须的,除非要求所有应用都别使用读写外置存储的api,这显然是不现实的。那对于目前没有外置存储的手机是怎么做到这点的呢? Android引入了一个模拟存储的概念,就是用内置存储的空间内来模拟外置存储。不但可以使用外置存储来模拟内置存储,Android同样提供了使用外置存储来模拟内置存储的能力,这样既可以通过外接设备来扩展外置存储空间,还可以通过外接设备来扩展内置存储。但是出于安全考虑,内置存储的数据需要进行加密。在Android 5.0开始Android默认开启内置存储的全盘加密,Android 7.0开始内置存储默认开启基于文件的加密。

         对于Android系统来说,存储的配置有三种:
         1 只有一个内置存储,使用内置存储来模拟主外置存储。
         2 有一个外置存储和内置存储,使用真实的外置存储设备作为主外置存储。
         3 使用内置存储来模拟主外置存储,支持其他外置存储设备。

         针对这三种配置感兴趣的读者可以阅读一下这篇文档。
         http://androidxref.com/6.0.1_r10/xref/docs/source.android.com/src/devices/storage/config-example.jd

         在Android 4.0开始支持多用户,对于不同用户应该做到存储空间隔离,这也是Android在设计存储管理系统时要考虑的。

         下面我们以一个真实的pixel举例来看下常见的设备配置(pixel属于上述的第三种配置)。

PIXEL举例

首先df命令看下盘的挂载情况

sailfish:/ # df
/dev/root          1999708  782936   1216772  40% /
tmpfs              1898544     436   1898108   1% /dev
tmpfs              1898544       0   1898544   0% /mnt
/dev/block/sda31    292868  224200     68668  77% /vendor
/dev/block/sda25     65488   57008      8480  88% /firmware/radio
/dev/block/sda35  25497924 1306056  24191868   6% /data
/dev/block/sdd3      28144     516     27628   2% /persist
/dev/fuse         25497924 1306056  24191868   6% /mnt/runtime/default/emulated
/dev/fuse         25497924 1306056  24191868   6% /mnt/runtime/read/emulated
/dev/fuse         25497924 1306056  24191868   6% /mnt/runtime/write/emulated
  • /dev/root 对应system分区
  • /dev/block/sda* 是一个设备,分了三个分区,分别挂载到目录/vendor, /data和 /firmware/radio。
  • /dev/block/sdd3 是一个很小的设备挂载到目录 /persist, 这个干啥的我也没有研究过。

        下面/dev/fuse 对应的几个目录,就是使用外置存储来模拟内置存储,这里主要用到了fuse文件系统,三个目录对应有不同权限的应用能看到的目录,有不同的读写权限,后面我会说明。

  • /dev/fuse 25497924 1306056 24191868 6% /mnt/runtime/default/emulated
  • /dev/fuse 25497924 1306056 24191868 6% /mnt/runtime/read/emulated
  • /dev/fuse 25497924 1306056 24191868 6% /mnt/runtime/write/emulated

        下面来看下pixel的fstab文件。它就是指导系统如何挂载磁盘的文件。

sailfish:/ # cat fstab.sailfish                                                                                                                                                                                                                                                   
\# Android fstab file.
\# The filesystem that contains the filesystem checker binary (typically /system) cannot
\# specify MF_CHECK, and must come before any filesystems that do specify MF_CHECK\#TODO: Add 'check' as fs_mgr_flags with data partition.# Currently we dont have e2fsck compiled. So fs check would failed.#<src>                                  <mnt_point>       <type>  <mnt_flags and options>                     <fs_mgr_flags>
/dev/block/bootdevice/by-name/system    /           ext4    ro,barrier=1,discard                                wait,slotselect,verify
/dev/block/bootdevice/by-name/vendor    /vendor     ext4    ro,barrier=1,discard                                wait,slotselect
/dev/block/bootdevice/by-name/modem     /firmware/radio    vfat    ro,shortname=lower,uid=1000,gid=0,dmask=227,fmask=337,context=u:object_r:firmware_file:s0   wait,slotselect
/dev/block/bootdevice/by-name/userdata    /data             ext4    nosuid,nodev,barrier=1,noauto_da_alloc      wait,check,formattable,fileencryption=ice
/dev/block/zram0                        none              swap    defaults                                            zramsize=536870912
/dev/block/bootdevice/by-name/misc        /misc           emmc  defaults defaults
/devices/*/xhci-hcd.0.auto/usb*           auto              vfat    defaults                                            voldmanaged=usb:auto
  • /dev/block/bootdevice/by-name/system 挂载到根目录,也就是/dev/root 这个设备。
  • /dev/block/bootdevice/by-name/vendor vendor挂载到/vender目录也就是/dev/block/sda31。
  • /dev/block/bootdevice/by-name/userdata 挂载到/data目录,也就是/dev/block/sda35。
  • /dev/block/zram0 这个是一个交换分区。
  • /dev/block/bootdevice/by-name/misc 挂载到misc目录,这应该是一个内置的emmc设备。

        前面这些都是内置的设备,或者虚拟的分区。

  • /devices/*/xhci-hcd.0.auto/usb* usb设备,属于外置设备,可以再开机后进行插拔,这一行设备了voldmanaged标志,说明该设备被vold进行管理, auto代表vold自动读取分区进行挂载。

        另外vold没有设置系统属性ro.vold.primary_physical 为1,表示使用内置存储/data分区来模拟主存储(后面代码分析中会看到为什么这样)。
        到这里相信读者对内置存储,外置存储和主存储有了基本的概念,以及知道了可以使用内置存储来模拟主存储。下面咱们再来看下Android系统对目录的管理。还是以pixel为例。

sailfish:/ # ls -ld /data/user/0                                                                                                                                                                                                                                                  
lrwxrwxrwx 1 root root 10 1970-03-15 16:41 /data/user/0 -> /data/datasailfish:/ # ls -ld /sdcard                                                                                                                                                                                                                                                       
lrw-r--r-- 1 root root 21 2020-07-12 14:00 /sdcard -> /storage/self/primarysailfish:/ # ls -ld /storage/self/primary
lrwxrwxrwx 1 root root 19 1970-05-08 02:11 /storage/self/primary -> /mnt/user/0/primarysailfish:/ # ls -ld /mnt/user/0/primary                                                                                                                                                                                                                                           
lrwxrwxrwx 1 root root 19 1970-05-08 02:11 /mnt/user/0/primary -> /storage/emulated/0

        目前Android系统已经支持多用户,主用户的用户id为0,所以我们可以看到一般应用程序的内置数据目录为 /data/data/,这对单用户显然没什么问题,但是要支持多用户就应该为每个用户提供不同的数据存储位置。所以Android为每个用户创建一个 用户独有的数据目录,这就是 /data/user/${userid}, 目录,对于主用户,它指向 /data/data目录。

        为了兼容旧应用程序,还必须提供一个/sdcard存储目录,指向主外置存储( /storage/self/primary), 对于不同的用户/storage/self/primary又指向不同的主外置存储目录 /mnt/user/${uid}/primary, 由于pixel是使用内置存储模拟的外置存储,所以主存储的位置为 /storage/emulated/${uid}, 这里的emulated就是代表该目录是模拟的。

        后面我们分析vold和MountService就是要弄明白vold如何挂载外置存储,确定主存储分区,对多用户如何进行管理,以及对上层应用提供怎样的api来访问外置存储。

        关于上述目录的特性,可以参考Android存储系统-开篇 这篇文章,下面我们就从代码的角度来分析下MountService和vold如何来管理Android手机上的外置存储。

整体架构

vold/MountService系统架构
        如上图,整个架构分为kernel层,system层和framework层。

        kernel层主要发出udev的通知给vold,通知设备的热插拔事件,对于不支持热插拔的设备,udev支持写uevent文件,比如给/sys/block/sda/uevent文件写一个add字符串,内核就会通过netlink发出一个udev的add事件给用户空间的发送一个消息。用户空间就可以通过监听netlink的消息来处理磁盘设备的热插拔事件。

        vold进程是system层的代表,主要负责监听前面说的uevent事件。vold只关注块(磁盘)设备的事件,在vold启动之初,会创建一个模拟分区,路径为/data/media,也就是在userdata里面划出来一块空间做为模拟主设备的分区,如果需要使用模拟设备来作为主外置存储,就使用这个模拟分区来创建主存储。 之后向/sys/block下面的子目录的uevent文件写入add,冷启这些设备。vold只关心fstab文件中设置了 voldmanaged标志的设备,该标志表示该设备归vold管理,其他设备的挂载都通过init进程初始化阶段进行挂载。(参考 fstab.sailfish,/data, /system, /vender 这些内置存储都不需要vold来管理)。vold启动的时候来会读取fstab文件来确定自己要管理的磁盘,vold要管理的磁盘使用DiskSource数据结构来描述。vold会使用NetLinkManager和内核建立链接,监听udev事件。vold进程收到udev事件的时候,读取设备信息,对于需要vold管理的设备会创建Disk数据结构来代表一块设备,然后读取分区表(使用/system/bin/sgdisk命令读取),最后创建Volume结构描述分区,并放到Disk下的volumes集合中,来建立关系。vold就是通过DiskSource来确认设备是否由自己管理。

        在vold管理的Disk和Volume创建过程中,会通过CommandListener通知MountService进程中的NativeDaemonConnector。 所以vold和MountService使用NativeDaemonConnector进行通信,NativeDaemonConnector代表一个unix 域套接字的链接,该套接字在vold创建之初进行创建。MountService启动的时候通过NativeDaemonConnector链接该套接字。MountService中的NativeDaemonConnector收到磁盘和分区相关的状态消息后,会在framework层建立相应的Disk和Volume关系。 当收到volume的创建消息后,会通过NativeDaemonConnector发送命令给vold进程,通知挂载Volume。这样在framework层就建立起来了Volume数据结构,以供系统相关api返回Volume信息给应用程序使用。另外MountService和会给vold来发送多用户相关的信息,以便vold进程为不同用户准备目录信息。在vold的这端使用CommandListener来接收MountService发来的命令,最终调用vold相关函数来处理命令。

        下面我们来看一下vold的类图:

类图

vold类图
        vold的基本类图如上图, DiskSource为解析fstab文件创建的数据结构,用于描述fstab中带有voldmanaged标志的行, 其中mSysPattern为要挂载的设备。 mNikname则表示要挂载到的文件目录,mFlags 是根据fstab解析出来的挂载标志。针对插入的外置设备, vold会根据对应的DiskSource来创建Disk数据结构,然后根据该设备的分区信息创建VolumeBase数据结构,VolumeBase数据结构用来描述一个分区。 Android支持三种分区:

  • PublicVolume分区:代表公共分区,也就非Android系统也可以支持挂载的分区。如果一个Disk设备是可加密的,则该设备可以被格式化成PrivateVolume。因为PublicVolume分区可以跨设备使用,所以被成为公有(Public)分区,公有分区只能作为外置存储使用。
  • PrivateVolume:Android私有的分区,这种类型的分区是加密分区,只有创建该分区的设备才可以挂载这个分区,私有分区是用于扩展内置存储的。
  • EmulatedVolume分区: 表示模拟分区,使用fuse文件系统在内置存储上模拟外置存储。

        关于PublicVolume和PrivateVolume可以参考Android M能让外部存储变成内部存储 支持U盘热插拔。 主要分区的创建请看下图。
分区创建
        当一个正常可以挂载的设备被插入Android系统后,可以对该设备重新格式化, 格式化有两个选项:

  • Use as portable storage:格式化成可移动存储,也就是可以将该磁盘拔下来挂载到其他设备使用。这种请看创建一个PublicVolume。
  • Use as internal storage: 格式化成内置存储, 将一个外置存储格式化成内置存储,格式化后分区是加密分区,只能在创建分区的设备上使用, 对应PrivateVolume。

         对于PrivateVolume,挂载后作为内置存储的扩展还需要创建一个EmulatedVolume,在该分区上模拟一个外置存储,用于存储对应的媒体文件。
         那么主存储如何确定呢,Android提供了三种方式。

  • userdata分区是Android必须的一个分区,作为内置存储,一般也是加密后的分区。 但是userdata需要在vold启动之前挂载,所以它并不归vold管理,而是由init进程挂载。如果没有外接设备,Android系统应该在data分区上模拟主存储。

         有些设备本为了减少成本,内置设备做的很少,会使用外接存储卡作为主存存储, 在这种情况下又分为两种情况:

  • PublicVolume分区作为外置存储。
  • PrivateVolume上的EmulatedVolume作为外置存储。

多用户backup.vpn.baidu.com

关于多用户部分,vold进程提供四个api,如下:

    int onUserAdded(userid_t userId, int userSerialNumber);int onUserRemoved(userid_t userId);int onUserStarted(userid_t userId);int onUserStopped(userid_t userId);

        MountService会通过unix域套接字(NativeDaemonConnector)来发送命令给vold进程的CommandListener,最终调用这四个api来处理多用户事件。

        下面我们就直接进入代码分析。

代码分析

1. 分区挂载

system/core/rootdir/init.rc

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:s0class coresocket vold stream 0660 root mountsocket cryptd stream 0660 root mountioprio be 2on post-fs-data......# Make sure we have the device encryption keystart logdstart vold

        在init进程挂载完内置分区后,就会启动vold进程来处理外置分区。vold可能需要在内置分区/data下面来模拟外置主分区,是依赖内置分区的,所以必须等待/data分区挂载好之后才会启动vold进程。另外vold进程启动的时候来创建了两个stream类型的unix域套接字,用于和MountService通信。

        vold进程的启动在system/vold/main.cpp 代码文件中(后面均以Android 7.0代码为例)

int main(int argc, char** argv) {......VolumeManager *vm;CommandListener *cl;CryptCommandListener *ccl;NetlinkManager *nm;......mkdir("/dev/block/vold", 0755); // 创建/dev/block/vold目录,用于创建设备文件/* For when cryptfs checks and mounts an encrypted filesystem */klog_set_level(6);/* Create our singleton managers */if (!(vm = VolumeManager::Instance())) {   //初始化VolumeManagerLOG(ERROR) << "Unable to create VolumeManager";exit(1);}if (!(nm = NetlinkManager::Instance())) { // 初始化NetlinkManagerLOG(ERROR) << "Unable to create NetlinkManager";exit(1);}if (property_get_bool("vold.debug", false)) { // 设置debug参数vm->setDebug(true);}cl = new CommandListener(); //初始化CommandListener 用于监听MountService管理卷的消息。ccl = new CryptCommandListener(); //初始化CryptCommandListener 用于监听MountServic分区加密解析的消息。vm->setBroadcaster((SocketListener *) cl);nm->setBroadcaster((SocketListener *) cl);if (vm->start()) { // 启动VolumeManagerPLOG(ERROR) << "Unable to start VolumeManager";exit(1);}if (process_config(vm)) { //读取fstab文件,创建DiskSource数据结构PLOG(ERROR) << "Error reading configuration... continuing anyways";}if (nm->start()) { //启动NetlinkManager,监听udev事件。PLOG(ERROR) << "Unable to start NetlinkManager";exit(1);}coldboot("/sys/block"); //冷启动/sys/block对应的块设备
//    coldboot("/sys/class/switch");/** Now that we're up, we can respond to commands*/if (cl->startListener()) { //启动监听MountService下发的消息PLOG(ERROR) << "Unable to start CommandListener";exit(1);}if (ccl->startListener()) { //启动监听MountService下发的消息PLOG(ERROR) << "Unable to start CryptCommandListener";exit(1);}// Eventually we'll become the monitoring threadwhile(1) {sleep(1000);}LOG(ERROR) << "Vold exiting";exit(0);
}

总结下启动的主要几个步骤:

  1. 启动VolumeManager。
  2. 解析fstab生成DiskSource数据结构。
  3. 启动NetlinkManager,监听udev事件。
  4. 冷启动/sys/block下对应的块设备。
  5. 启动监听MountService下发的消息。

        这里我们不关系加密解析分区相关消息处理,后面单拿出来一篇文章进行分析,按照上面五个步骤来分析vold启动做的事情。

vm->start()
/system/vold/VolumeManager.cpp

 255 int VolumeManager::start() {256     // Always start from a clean slate by unmounting everything in257     // directories that we own, in case we crashed.258     unmountAll();259 260     // Assume that we always have an emulated volume on internal261     // storage; the framework will decide if it should be mounted.262     CHECK(mInternalEmulated == nullptr);263     mInternalEmulated = std::shared_ptr<android::vold::VolumeBase>(264             new android::vold::EmulatedVolume("/data/media"));265     mInternalEmulated->create();266 267     return 0;268 }

        函数首先调用umountAll() 函数来卸载所有归vold服务管理的分区,然后创建一个内部的模拟分区,对应的内置目录为/data/media。这里为什么要创建一个EmulatedVolume类型的分区呢,是因为这个分区可能做为模拟主外置分区来挂载,模拟分区没有对应的真实设备,所以这里硬编码创建一个EmulatedVolume,对应VolumeManager的mInternalEmulated成员变量。
        unmountAll() 函数相同的文件下面,代码如下

int VolumeManager::unmountAll() {std::lock_guard<std::mutex> lock(mLock);// First, try gracefully unmounting all known devicesif (mInternalEmulated != nullptr) {mInternalEmulated->unmount();}for (auto disk : mDisks) {disk->unmountAll();}// Worst case we might have some stale mounts lurking around, so// force unmount those just to be safe.FILE* fp = setmntent("/proc/mounts", "r");if (fp == NULL) {SLOGE("Error opening /proc/mounts: %s", strerror(errno));return -errno;}// Some volumes can be stacked on each other, so force unmount in// reverse order to give us the best chance of success.std::list<std::string> toUnmount;mntent* mentry;while ((mentry = getmntent(fp)) != NULL) {if (strncmp(mentry->mnt_dir, "/mnt/", 5) == 0|| strncmp(mentry->mnt_dir, "/storage/", 9) == 0) {toUnmount.push_front(std::string(mentry->mnt_dir));}}endmntent(fp);for (auto path : toUnmount) {SLOGW("Tearing down stale mount %s", path.c_str());android::vold::ForceUnmount(path);}return 0;
}

         函数不但要卸载自己管理的多个Disk和相关的分区,还要卸载/mnt和/storage/相关的目录,是因为mInternalEmulated如果被挂载,它会被挂载到/mnt/目录或者/sdcard/目录,但是它不属于任何一个Disk,所以要单独进行卸载。

         我们先跳过EmulatedVolume的创建,后面和其他类型卷的创建一起分析。先来分析DiskSource的创建。process_config(vm)函数在system/vold/main.cpp文件中。

static int process_config(VolumeManager *vm) {std::string path(android::vold::DefaultFstabPath());fstab = fs_mgr_read_fstab(path.c_str()); // 解析fstab文件if (!fstab) {PLOG(ERROR) << "Failed to open default fstab " << path;return -1;}/* Loop through entries looking for ones that vold manages */bool has_adoptable = false;for (int i = 0; i < fstab->num_entries; i++) {if (fs_mgr_is_voldmanaged(&fstab->recs[i])) {  //针对有voldmanaged标志的磁盘创建DiskSourceif (fs_mgr_is_nonremovable(&fstab->recs[i])) {LOG(WARNING) << "nonremovable no longer supported; ignoring volume";continue;}std::string sysPattern(fstab->recs[i].blk_device);std::string nickname(fstab->recs[i].label);int flags = 0;if (fs_mgr_is_encryptable(&fstab->recs[i])) { //加密设备的盘添加kAdoptable标志flags |= android::vold::Disk::Flags::kAdoptable;has_adoptable = true;}if (fs_mgr_is_noemulatedsd(&fstab->recs[i])|| property_get_bool("vold.debug.default_primary", false)) { //添加kDefaultPrimary标志,为默认的主外置存储flags |= android::vold::Disk::Flags::kDefaultPrimary;}vm->addDiskSource(std::shared_ptr<VolumeManager::DiskSource>(new VolumeManager::DiskSource(sysPattern, nickname, flags)));}}property_set("vold.has_adoptable", has_adoptable ? "1" : "0");return 0;
}

         process_config 函数如前面所说,解析fstab文件,然后创建DiskSource。 这里可能设置两个标志:

  • kAdoptable 表示该设备是加密设备。
  • kDefaultPrimary 表示该设备是默认的主存储分区。

        我们继续分析启动流程,下面NetlinkManager的启动过程。
system/vold/NetlinkManager.cpp

int NetlinkManager::start() {....if ((mSock = socket(PF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC,NETLINK_KOBJECT_UEVENT)) < 0) {SLOGE("Unable to create uevent socket: %s", strerror(errno));return -1;}if (setsockopt(mSock, SOL_SOCKET, SO_RCVBUFFORCE, &sz, sizeof(sz)) < 0) {SLOGE("Unable to set uevent socket SO_RCVBUFFORCE option: %s", strerror(errno));goto out;}if (setsockopt(mSock, SOL_SOCKET, SO_PASSCRED, &on, sizeof(on)) < 0) {SLOGE("Unable to set uevent socket SO_PASSCRED option: %s", strerror(errno));goto out;}if (bind(mSock, (struct sockaddr *) &nladdr, sizeof(nladdr)) < 0) {SLOGE("Unable to bind uevent socket: %s", strerror(errno));goto out;}mHandler = new NetlinkHandler(mSock);if (mHandler->start()) {SLOGE("Unable to start NetlinkHandler: %s", strerror(errno));goto out;}return 0;out:close(mSock);return -1;
}

        NetLinkManager主要设置套接字,然后传递给NetlinkHanler,调用NetllinkHandler来进行监听。 关于netlink主要就是通过套接字的形式和内核建立起链接,来接收内核发送上来的事件。对于Netlink事件的监听,在Android系统的libsysutils库里面提供了一套基础的实现,这个实现不是我们关注的重点,我们只简单进行介绍。 最关键的类为NetlinkListener类,这个类启动后监听netlink套接字,收到数据后会将数据解析封装成数据结构NetlinkEvent,然后调用该类下的虚函数 virtual void onEvent(NetlinkEvent *evt)来对事件进行处理。vold的NetlinkManager创建的NetlinkHandler就是NetlinkListener的子类,它实现了onEvent函数,我们来看下它的实现。

system/vold/NetlinkHandler.cpp

void NetlinkHandler::onEvent(NetlinkEvent *evt) {VolumeManager *vm = VolumeManager::Instance();const char *subsys = evt->getSubsystem();if (!subsys) {SLOGW("No subsystem found in netlink event");return;}if (!strcmp(subsys, "block")) {vm->handleBlockEvent(evt);}
}

        它是显现很简单。就是将块设备的事件交由VolumeManager来处理,也就是调用VolumeManager的handleBlockEvent函数。

        我们先放一下handleBlockEvent函数,顺着启动流程往下看。coldboot("/sys/block")函数,从函数名称来看是冷启的设备,冷启哪些设备呢?
system/vold/main.cpp

167 static void do_coldboot(DIR *d, int lvl) {
168     struct dirent *de;
169     int dfd, fd;
170 
171     dfd = dirfd(d);
172 
173     fd = openat(dfd, "uevent", O_WRONLY | O_CLOEXEC);
174     if(fd >= 0) {
175         write(fd, "add\n", 4);
176         close(fd);
177     }
178 
179     while((de = readdir(d))) {
180         DIR *d2;
181 
182         if (de->d_name[0] == '.')
183             continue;
184 
185         if (de->d_type != DT_DIR && lvl > 0)
186             continue;
187 
188         fd = openat(dfd, de->d_name, O_RDONLY | O_DIRECTORY);
189         if(fd < 0)
190             continue;
191 
192         d2 = fdopendir(fd);
193         if(d2 == 0)
194             close(fd);
195         else {
196             do_coldboot(d2, lvl + 1);
197             closedir(d2);
198         }
199     }
200 }
201 
202 static void coldboot(const char *path) {
203     DIR *d = opendir(path);
204     if(d) {
205         do_coldboot(d, 0);
206         closedir(d);
207     }
208 }

        coldboot实现的功能就是遍历path下所有的uevent文件,然后写入”add\n“四个字节到uevent文件(175行)。uevent主要是udev协议的一部分,当设备进行热插拔的时候,上层应用程序可以通过netlink收到相应的add,remove,change等消息,这样就可以调用mknod在/dev/目录下创建对应的驱动设备文件,这样才可以使用设备文件来操作设备。 在vold的启动过程中,由于块设备早就准备好了,不会收到设备插拔的消息,没有办法对固有的设备来创建对应的Disk和Volume结构。好在udev提供了一套机制,就是可以通过写uevent来触发热插拔事件,这里写入add就代表触发一个设备插入事件。前面我们已经启动了NetlinkManager来监听netlink事件,这里来触发uevent的设备插入事件,VolumeManager就可以创建相应的Disk和Volume数据结构了。

        最后就是启动监听MountService消息了。CommandListener->startListener()

        CommandListener类主要的作用是通过unix域套接字来和MountService进行通行,它借助了Android的libsysutils库提供的api来对命令进行分发。 这里我们也不打算深入分析,只是简单介绍下和我们分析代码相关的细节。 CommandListener的构造函数如下:
system/vold/CommandListener.cpp

CommandListener::CommandListener() :FrameworkListener("vold", true) {registerCmd(new DumpCmd());registerCmd(new VolumeCmd());registerCmd(new AsecCmd());registerCmd(new ObbCmd());registerCmd(new StorageCmd());registerCmd(new FstrimCmd());
}

        CommandListener在创建的时候注册了一些列支持的命令处理类,当通过socket收到命令后就会匹配相应的命令处理类来处理命令。我们这里来看下VolumeCmd类如何处理命令。
system/vold/CommandListener.cpp

int CommandListener::VolumeCmd::runCommand(SocketClient *cli,int argc, char **argv) {......VolumeManager *vm = VolumeManager::Instance();std::lock_guard<std::mutex> lock(vm->getLock());// TODO: tease out methods not directly related to volumesstd::string cmd(argv[1]);if (cmd == "reset") {return sendGenericOkFail(cli, vm->reset());......} else if (cmd == "user_added" && argc > 3) {// user_added [user] [serial]return sendGenericOkFail(cli, vm->onUserAdded(atoi(argv[2]), atoi(argv[3])));} else if (cmd == "user_removed" && argc > 2) {......return cli->sendMsg(ResponseCode::CommandSyntaxError, nullptr, false);
}

        这里省略了大量具体命令处理的分之,但是我们可以看到VolumeCmd主要调用VolumeManager来处理MountService发出的命令。

        看完vold启动的流程后我们留下了三件事要分析:

  1. VolumeManager.handleBlockEvent处理设备插拔事件。
  2. MountService和Vold通信的建立。
  3. 卷的挂载。
  4. 多用户的管理。

        下面我们逐一分析。

 277 void VolumeManager::handleBlockEvent(NetlinkEvent *evt) {278     std::lock_guard<std::mutex> lock(mLock);279 ......286     std::string eventPath(evt->findParam("DEVPATH"));287     std::string devType(evt->findParam("DEVTYPE"));288 289     if (devType != "disk") return;290 291     int major = atoi(evt->findParam("MAJOR"));292     int minor = atoi(evt->findParam("MINOR"));293     dev_t device = makedev(major, minor);294 295     switch (evt->getAction()) {296     case NetlinkEvent::Action::kAdd: {297         for (auto source : mDiskSources) {298             if (source->matches(eventPath)) {299                 // For now, assume that MMC devices are SD, and that300                 // everything else is USB301                 int flags = source->getFlags();302                 if (major == kMajorBlockMmc) {303                     flags |= android::vold::Disk::Flags::kSd;304                 } else {305                     flags |= android::vold::Disk::Flags::kUsb;306                 }307 308                 auto disk = new android::vold::Disk(eventPath, device,309                         source->getNickname(), flags);310                 disk->create();311                 mDisks.push_back(std::shared_ptr<android::vold::Disk>(disk));312                 break;313             }314         }315         break;316     }317     case NetlinkEvent::Action::kChange: {318         LOG(DEBUG) << "Disk at " << major << ":" << minor << " changed";319         for (auto disk : mDisks) {320             if (disk->getDevice() == device) {321                 disk->readMetadata();322                 disk->readPartitions();323             }324         }325         break;326     }327     case NetlinkEvent::Action::kRemove: {328         auto i = mDisks.begin();329         while (i != mDisks.end()) {330             if ((*i)->getDevice() == device) {331                 (*i)->destroy();332                 i = mDisks.erase(i);333             } else {334                 ++i;335             }336         }337         break;338     }339     default: {340         LOG(WARNING) << "Unexpected block event action " << (int) evt->getAction();341         break;342     }343     }344 }

         handleBlockEvent的逻辑实际上是比较简单的,从NetlinkEvent中或者主,次设备号,和ueventuevent对应的设备文件的/sys/block路径。

  • 对于add消息, 根据主设备号确定设备的类型是usb还是sd卡。从NetlinkEvent中获取uevent对应的设备文件的/sys/block路径,然后和DiskSource做比较,确定是不是对应voldmanaged标志的设备,如果是voldmanaged的设备就会创建Disk数据结构,再调用Disk.create()函数执行初始化。将初始化好的Disk数据结构保存起来。
  • 对于change消息,则重新读取设备的meta信息和分区信息。
  • remove消息则调用Disk.destory()函数,从mDisks中移除Disk数据结构。

         先来看看Disk的构造函数
system/vold/Disk.cpp

Disk::Disk(const std::string& eventPath, dev_t device,const std::string& nickname, int flags) :mDevice(device), mSize(-1), mNickname(nickname), mFlags(flags), mCreated(false), mJustPartitioned(false) {mId = StringPrintf("disk:%u,%u", major(device), minor(device));mEventPath = eventPath;mSysPath = StringPrintf("/sys/%s", eventPath.c_str());mDevPath = StringPrintf("/dev/block/vold/%s", mId.c_str());CreateDeviceNode(mDevPath, mDevice);
}status_t CreateDeviceNode(const std::string& path, dev_t dev) {const char* cpath = path.c_str();status_t res = 0;......mode_t mode = 0660 | S_IFBLK;if (mknod(cpath, mode, dev) < 0) {......}......return res;
}

         前面还没说明Disk的成员变量,这里简答说说:

  • mId :设备的唯一id,默认disk:${主设备号}😒{次设备号}。
  • mEventPath: uevent文件路径。
  • mSysPath: /sys/block 下的设备路径。
  • mDevPath: /dev下的设备路径。

         CreateDeviceNode函数调用mknod 创建dev而被文件,目录为/dev/block/vold/。相当于vold实现了部分udev的功能。

        Disk的主要的初始化工作都在Disk.create()函数下面。

status_t Disk::create() {CHECK(!mCreated);mCreated = true;notifyEvent(ResponseCode::DiskCreated, StringPrintf("%d", mFlags));readMetadata();readPartitions();return OK;
}

notifyEvent函数向MountService发送消息。
readMetadata函数读取磁盘的meta信息。
readPartitions()读取设备的分区信息,创建Volume。

        先来看看读取设备的meta信息都读了哪些信息。

status_t Disk::readMetadata() {mSize = -1;mLabel.clear();int fd = open(mDevPath.c_str(), O_RDONLY | O_CLOEXEC);if (fd != -1) {if (ioctl(fd, BLKGETSIZE64, &mSize)) {mSize = -1;}close(fd);}switch (major(mDevice)) {case kMajorBlockScsiA: case kMajorBlockScsiB: case kMajorBlockScsiC: case kMajorBlockScsiD:case kMajorBlockScsiE: case kMajorBlockScsiF: case kMajorBlockScsiG: case kMajorBlockScsiH:case kMajorBlockScsiI: case kMajorBlockScsiJ: case kMajorBlockScsiK: case kMajorBlockScsiL:case kMajorBlockScsiM: case kMajorBlockScsiN: case kMajorBlockScsiO: case kMajorBlockScsiP: {std::string path(mSysPath + "/device/vendor");std::string tmp;if (!ReadFileToString(path, &tmp)) {PLOG(WARNING) << "Failed to read vendor from " << path;return -errno;}mLabel = tmp;break;}case kMajorBlockMmc: {std::string path(mSysPath + "/device/manfid");std::string tmp;if (!ReadFileToString(path, &tmp)) {PLOG(WARNING) << "Failed to read manufacturer from " << path;return -errno;}uint64_t manfid = strtoll(tmp.c_str(), nullptr, 16);// Our goal here is to give the user a meaningful label, ideally// matching whatever is silk-screened on the card.  To reduce// user confusion, this list doesn't contain white-label manfid.switch (manfid) {case 0x000003: mLabel = "SanDisk"; break;case 0x00001b: mLabel = "Samsung"; break;case 0x000028: mLabel = "Lexar"; break;case 0x000074: mLabel = "Transcend"; break;}break;}default: {LOG(WARNING) << "Unsupported block major type" << major(mDevice);return -ENOTSUP;}}notifyEvent(ResponseCode::DiskSizeChanged, StringPrintf("%" PRId64, mSize));notifyEvent(ResponseCode::DiskLabelChanged, mLabel);notifyEvent(ResponseCode::DiskSysPathChanged, mSysPath);return OK;
}

         readMetadata函数主要通过ioctl函数获取磁盘的大小,保存到mSize变量中,然后根据设备vendor或者manfid 获取Disk的mLabel。

         Disk::readPartitions() 函数用于读取分区创建Volume数据结构。是磁盘挂载的最重要步骤。

245 status_t Disk::readPartitions() {
246     int8_t maxMinors = getMaxMinors();
247     if (maxMinors < 0) {
248         return -ENOTSUP;
249     }
250 
251     destroyAllVolumes();  // 卸载所有卷
252 
253     // Parse partition table
254 
255     std::vector<std::string> cmd;
256     cmd.push_back(kSgdiskPath);
257     cmd.push_back("--android-dump");
258     cmd.push_back(mDevPath);
259 
260     std::vector<std::string> output;
261     status_t res = ForkExecvp(cmd, output); //调用/system/bin/sgdisk --android-dump /dev/block/vold/${disk_id}读取分区信息
262     if (res != OK) {
263         LOG(WARNING) << "sgdisk failed to scan " << mDevPath;
264         notifyEvent(ResponseCode::DiskScanned);
265         mJustPartitioned = false;
266         return res;
267     }
268 
269     Table table = Table::kUnknown;
270     bool foundParts = false;
271     for (auto line : output) {
272         char* cline = (char*) line.c_str();
273         char* token = strtok(cline, kSgdiskToken);
274         if (token == nullptr) continue;
275 
276         if (!strcmp(token, "DISK")) { //获取分区类型,mbr或者gtk
277             const char* type = strtok(nullptr, kSgdiskToken);
278             if (!strcmp(type, "mbr")) {
279                 table = Table::kMbr;
280             } else if (!strcmp(type, "gpt")) {
281                 table = Table::kGpt;
282             }
283         } else if (!strcmp(token, "PART")) {
284             foundParts = true;
285             int i = strtol(strtok(nullptr, kSgdiskToken), nullptr, 10);
286             if (i <= 0 || i > maxMinors) { //次设备号不合理, 处理下一个分区
287                 LOG(WARNING) << mId << " is ignoring partition " << i
288                         << " beyond max supported devices";
289                 continue;
290             }
291             dev_t partDevice = makedev(major(mDevice), minor(mDevice) + i); //获取分区设备号。
292 
293             if (table == Table::kMbr) { //根据mbr分区表设置分区信息
294                 const char* type = strtok(nullptr, kSgdiskToken);
295 
296                 switch (strtol(type, nullptr, 16)) {
297                 case 0x06: // FAT16
298                 case 0x0b: // W95 FAT32 (LBA)
299                 case 0x0c: // W95 FAT32 (LBA)
300                 case 0x0e: // W95 FAT16 (LBA)
301                     createPublicVolume(partDevice); //mbr分区表直接创建PublicVolume
302                     break;
303                 }
304             } else if (table == Table::kGpt) { //根据gpt分区表设置分区信息
305                 const char* typeGuid = strtok(nullptr, kSgdiskToken);
306                 const char* partGuid = strtok(nullptr, kSgdiskToken);
307 
308                 if (!strcasecmp(typeGuid, kGptBasicData)) { //分区类型为基础数据分区,代表一个PublicVolume。
309                     createPublicVolume(partDevice);
310                 } else if (!strcasecmp(typeGuid, kGptAndroidExpand)) {//分区类型为Android扩展,创建一个私有分区。
311                     createPrivateVolume(partDevice, partGuid);
312                 }
313             }
314         }
315     }
316     //最后如果没有找到分区类型或者没有找到分区,尝试将整个设备作为一个分区去创建PublicVolume。
317     // Ugly last ditch effort, treat entire disk as partition
318     if (table == Table::kUnknown || !foundParts) {
319         LOG(WARNING) << mId << " has unknown partition table; trying entire device";
320 
321         std::string fsType;
322         std::string unused;
323         if (ReadMetadataUntrusted(mDevPath, fsType, unused, unused) == OK) {
324             createPublicVolume(mDevice);
325         } else {
326             LOG(WARNING) << mId << " failed to identify, giving up";
327         }
328     }
329 
330     notifyEvent(ResponseCode::DiskScanned);
331     mJustPartitioned = false;
332     return OK;
333 }

         关于代码,我们可以知道readPartitions函数通过读取mbr或者gpt分区表,还获取分区的信息,创建相应的卷。对于mbr分区,全都调用createPublicVolume函数来创建PublicVolume分区。对于gpt分区,要根据分区的typeGuid来创建Volume, 如果typeGuid为kGptBasicData表示这是一个基础数据分区,所以并非一个Android特有的卷,使用createPublicVolume来创建PublicVolume。 对于typeGuid为kGptAndroidExpand的卷则表示这个卷为一个Android扩展的卷,在其他系统上是无法处理的,这对应一个vold的PrivateVolume,使用createPrivateVolume函数来创建。回过头来想想,我们对于EmulatedVolume的创建也还没有分析,现在时机成熟了,我们来一起分析下三种卷的创建。

先来看PublicVomule的创建。
system/vold/Disk.cpp

void Disk::createPublicVolume(dev_t device) {auto vol = std::shared_ptr<VolumeBase>(new PublicVolume(device));if (mJustPartitioned) {LOG(DEBUG) << "Device just partitioned; silently formatting";vol->setSilent(true);vol->create();vol->format("auto");vol->destroy();vol->setSilent(false);}mVolumes.push_back(vol);vol->setDiskId(getId());vol->create();
}

         PublicVolume的创建首先创建一个PublicVolume实例,然后如果Disk刚刚进行分区,要先进行格式化。mJustPartitioned为true的情况进行格式化,这不是我们关注的重点。创建完PublicVolume实例后最重要的是调用create函数来创建, 这其实和我们的EmulatedVolume的创建基本是一致的,先创建实例再调用create()函数。

再来看下PrivateVolume的创建。
system/vold/Disk.cpp

void Disk::createPrivateVolume(dev_t device, const std::string& partGuid) {std::string normalizedGuid;if (NormalizeHex(partGuid, normalizedGuid)) {LOG(WARNING) << "Invalid GUID " << partGuid;return;}std::string keyRaw;if (!ReadFileToString(BuildKeyPath(normalizedGuid), &keyRaw)) {PLOG(ERROR) << "Failed to load key for GUID " << normalizedGuid;return;}LOG(DEBUG) << "Found key for GUID " << normalizedGuid;auto vol = std::shared_ptr<VolumeBase>(new PrivateVolume(device, keyRaw));if (mJustPartitioned) {LOG(DEBUG) << "Device just partitioned; silently formatting";vol->setSilent(true);vol->create();vol->format("auto");vol->destroy();vol->setSilent(false);}mVolumes.push_back(vol);vol->setDiskId(getId());vol->setPartGuid(partGuid);vol->create();
}

        私有卷的创建略微有点麻烦。要先读取/data/misc/vold/expand_${hex(partguid)}.key读取用于解密的key,然后将这个key作为参数用于创建PrivateVolume实例子,后面的流程和创建PublicVolume一样,就是调用create()函数。

        所以无论PublicVolume,PrivateVolume还是EmulatedVolue的创建都是两步:

  1. 构造函数。
  2. create()函数。

        我们逐一分析。
system/vold/VolumeBase.cpp

VolumeBase::VolumeBase(Type type) :mType(type), mMountFlags(0), mMountUserId(-1), mCreated(false), mState(State::kUnmounted), mSilent(false) {
}

system/vold/PublicVolume.cpp

PublicVolume::PublicVolume(dev_t device) :VolumeBase(Type::kPublic), mDevice(device), mFusePid(0) {setId(StringPrintf("public:%u,%u", major(device), minor(device)));mDevPath = StringPrintf("/dev/block/vold/%s", getId().c_str());
}

        PublicVolume的的几个成员变量:

  • mType: kPublic。
  • mId为public:${major},${minor}。
  • mDevPath: /dev/block/vold/${mId}
  • mDevice: major:minor

PrivateVolume构造如下:
system/vold/PrivateVolume.cpp

PrivateVolume::PrivateVolume(dev_t device, const std::string& keyRaw) :VolumeBase(Type::kPrivate), mRawDevice(device), mKeyRaw(keyRaw) {setId(StringPrintf("private:%u,%u", major(device), minor(device)));mRawDevPath = StringPrintf("/dev/block/vold/%s", getId().c_str());
}

        PrivateVolume的的几个成员变量:

  • mType: kPrivate。
  • mId为private:${major},${minor}。
  • mDevPath: /dev/block/vold/${mId}
  • mRawDevice:major:minor
  • mKeyRaw: /data/misc/vold/expand_${hex(partguid)}.key 度到的key。

EmulatedVolume构造如下:
system/vold/EmulatedVolume.cpp

EmulatedVolume::EmulatedVolume(const std::string& rawPath) :VolumeBase(Type::kEmulated), mFusePid(0) {setId("emulated");mRawPath = rawPath;mLabel = "emulated";
}EmulatedVolume::EmulatedVolume(const std::string& rawPath, dev_t device,const std::string& fsUuid) : VolumeBase(Type::kEmulated), mFusePid(0) {setId(StringPrintf("emulated:%u,%u", major(device), minor(device)));mRawPath = rawPath;mLabel = fsUuid;
}

        EmulatedVolume有两个构造函数,第一个构造函数对应VolumeManager的mInternalEmulated模拟分区,第二个构造函数对应为PrivateVolume创建的EmulatedVolume创建的模拟分区,后面我们会看到,每个PrivateVolume分区都会创建一个EmulatedVolume分区,用来控制读写权限:

        EmulatedVolume成员变量:

  • mType: kEmulated
  • mId: emulated 或者emulated:${major}😒{minor}
  • mRawPath: /data/media| ${rawPath}。
  • mLabel: emulated | fsUuid。

         构造函数我们看完了,主要就是设置了几个成员变量。下面来看下create()函数。
system/vold/VolumeBase.cpp

status_t VolumeBase::create() {CHECK(!mCreated);mCreated = true;status_t res = doCreate();notifyEvent(ResponseCode::VolumeCreated,StringPrintf("%d \"%s\" \"%s\"", mType, mDiskId.c_str(), mPartGuid.c_str()));setState(State::kUnmounted);return res;
}

         create()函数并非一个虚函数,所以只有VolumeBase实现了该函数。函数首先调用虚函数doCreate()去真正创建不同类型的分区。然后调用notifyEvent通知MountService新卷的创建,最后更新状态。所以我们真正要关心的函数为doCreate函数。
system/vold/PublicVolume.cpp

status_t PublicVolume::doCreate() {return CreateDeviceNode(mDevPath, mDevice);
}status_t CreateDeviceNode(const std::string& path, dev_t dev) {const char* cpath = path.c_str();......mode_t mode = 0660 | S_IFBLK;if (mknod(cpath, mode, dev) < 0) {if (errno != EEXIST) {PLOG(ERROR) << "Failed to create device node for " << major(dev)<< ":" << minor(dev) << " at " << path;res = -errno;}}......return res;
}

         函数主要调用mknod来创建卷对应的/dev/block/vold/${mId}文件。

        再来看下PrivateVolume的doCreate函数。

status_t PrivateVolume::doCreate() {if (CreateDeviceNode(mRawDevPath, mRawDevice)) {  // 创建return -EIO;}// Recover from stale vold by tearing down any old mappingscryptfs_revert_ext_volume(getId().c_str());// TODO: figure out better SELinux labels for private volumesunsigned char* key = (unsigned char*) mKeyRaw.data();char crypto_blkdev[MAXPATHLEN];int res = cryptfs_setup_ext_volume(getId().c_str(), mRawDevPath.c_str(),key, mKeyRaw.size(), crypto_blkdev);mDmDevPath = crypto_blkdev;if (res != 0) {PLOG(ERROR) << getId() << " failed to setup cryptfs";return -EIO;}return OK;
}

        PrivateVolume的创建首先调用mknode创建 /dev/block/vold/${mId}设备。然后使用主秘钥去调用cryptfs_setup_ext_volume函数创建一个加密的设备,这个设备的路径通过crypto_blkdev变量返回,保存在mDmDevPath中,以后挂载mDmDevPath对应的设备后,写入的数据就会被加密的存到mRawDevPath对应的卷上,读数据则会被解密后返回给调用方。 主密钥的生成主要在设备格式化过程中(也就是创建PrivateVolume分区时)。这里对于PrivateVolume再多说几句,我们知道可以使用外置存储设备来模拟内置存储设备,这种情况就会创建PrivateVolume分区,PrivateVolume分区的加密使用全盘加密,PrivateVolume对应两个设备文件,分别是mRawDevPath对应的设备文件,这个文件代表一个原始设备,通过device-mapping技术映射到mDmDevPath对应的逻辑设备,对mDmDevPath设备的读写最终都会被转发到mRawDevPath设备,但是在转发之前要对数据进行加解密,所以mRawDevPath设备上的数据都是加密的,如果直接挂载mRawDevPath设备是无法看到正常的文件内容的。后面我们会专门拿出一篇文章来写磁盘加密这块技术。

        最后看下EmulatedVolume没有实现doCreare函数,所以这一步它什么都没做。

        Disk和Volume的创建到这里就完成了,但是我们并没有看到分区的挂载。挂载的命令主要由MountService来发起。所以下面我们开对MountService来进行分析。

先简单看下MountService的构造函数:
frameworks/base/services/core/java/com/android/server/MountService.java

1360     /**
1361      * Constructs a new MountService instance
1362      *
1363      * @param context  Binder context for this service
1364      */
1365     public MountService(Context context) {
1366         sSelf = this;
1367 
1368         mContext = context;
1369         mCallbacks = new Callbacks(FgThread.get().getLooper());
1370 
1371         // XXX: This will go away soon in favor of IMountServiceObserver
1372         mPms = (PackageManagerService) ServiceManager.getService("package");
1373         //1 创建Handler,用于处理vold消息
1374         HandlerThread hthread = new HandlerThread(TAG);
1375         hthread.start();
1376         mHandler = new MountServiceHandler(hthread.getLooper());
1377 
1378         // Add OBB Action Handler to MountService thread.
1379         mObbActionHandler = new ObbActionHandler(IoThread.get().getLooper());
1380 
1381         // Initialize the last-fstrim tracking if necessary
1382         File dataDir = Environment.getDataDirectory();
1383         File systemDir = new File(dataDir, "system");......
1396 
1397         mSettingsFile = new AtomicFile(
1398                 new File(Environment.getSystemSecureDirectory(), "storage.xml"));
1399 
1400         synchronized (mLock) { // 2读取配置文件
1401             readSettingsLocked();
1402         }
1403 
1404         LocalServices.addService(MountServiceInternal.class, mMountServiceInternal);
1405 
1406         /*
1407          * Create the connection to vold with a maximum queue of twice the
1408          * amount of containers we'd ever expect to have. This keeps an
1409          * "asec list" from blocking a thread repeatedly.
1410          */
1411         //3 与vold建立链接,处理卷相关消息和发送命令
1412         mConnector = new NativeDaemonConnector(this, "vold", MAX_CONTAINERS * 2, VOLD_TAG, 25,
1413                 null);
1414         mConnector.setDebug(true);
1415 
1416         Thread thread = new Thread(mConnector, VOLD_TAG);
1417         thread.start();
1418          //4 与vold建立链接, 处理加密相关命令和消息
1419         // Reuse parameters from first connector since they are tested and safe
1420         mCryptConnector = new NativeDaemonConnector(this, "cryptd",
1421                 MAX_CONTAINERS * 2, CRYPTD_TAG, 25, null);
1422         mCryptConnector.setDebug(true);
1423 
1424         Thread crypt_thread = new Thread(mCryptConnector, CRYPTD_TAG);
1425         crypt_thread.start();
1426         // 5注册广播接受者用于处理用户相关消息
1427         final IntentFilter userFilter = new IntentFilter();
1428         userFilter.addAction(Intent.ACTION_USER_ADDED);
1429         userFilter.addAction(Intent.ACTION_USER_REMOVED);
1430         mContext.registerReceiver(mUserReceiver, userFilter, null, mHandler);
1431 // 6 创建内置卷对应vold的内置EmulatedVolume("/data/media")
1432         addInternalVolume();
1433         // 7 添加到watchdog监控
1434         // Add ourself to the Watchdog monitors if enabled.
1435         if (WATCHDOG_ENABLE) {
1436             Watchdog.getInstance().addMonitor(this);
1437         }
1438     }private void addInternalVolume() {// Create a stub volume that represents internal storagefinal VolumeInfo internal = new VolumeInfo(VolumeInfo.ID_PRIVATE_INTERNAL,VolumeInfo.TYPE_PRIVATE, null, null);internal.state = VolumeInfo.STATE_MOUNTED;internal.path = Environment.getDataDirectory().getAbsolutePath();mVolumes.put(internal.id, internal);}private void readSettingsLocked() {mRecords.clear();mPrimaryStorageUuid = getDefaultPrimaryStorageUuid();mForceAdoptable = false;FileInputStream fis = null;try {fis = mSettingsFile.openRead();final XmlPullParser in = Xml.newPullParser();in.setInput(fis, StandardCharsets.UTF_8.name());int type;while ((type = in.next()) != END_DOCUMENT) {if (type == START_TAG) {final String tag = in.getName();if (TAG_VOLUMES.equals(tag)) {final int version = readIntAttribute(in, ATTR_VERSION, VERSION_INIT);final boolean primaryPhysical = SystemProperties.getBoolean(StorageManager.PROP_PRIMARY_PHYSICAL, false);final boolean validAttr = (version >= VERSION_FIX_PRIMARY)|| (version >= VERSION_ADD_PRIMARY && !primaryPhysical);if (validAttr) {mPrimaryStorageUuid = readStringAttribute(in,ATTR_PRIMARY_STORAGE_UUID);}mForceAdoptable = readBooleanAttribute(in, ATTR_FORCE_ADOPTABLE, false);} else if (TAG_VOLUME.equals(tag)) {final VolumeRecord rec = readVolumeRecord(in);mRecords.put(rec.fsUuid, rec);}}}} catch (FileNotFoundException e) {// Missing metadata is okay, probably first boot} catch (IOException e) {Slog.wtf(TAG, "Failed reading metadata", e);} catch (XmlPullParserException e) {Slog.wtf(TAG, "Failed reading metadata", e);} finally {IoUtils.closeQuietly(fis);}}

        MountService的初始化主要经过一下7个步骤:

  1. 创建Handler,用于处理vold消息 (1373-1376),对应MountService的一个独立线程。
  2. 读取配置文件,1401行readSettingsLocked(),我们稍后分析。
  3. 与vold建立链接,用于处理vold卷相关消息和给vold发送命令。(1142-1147)
  4. 与vold建立链接, 处理vold加密相关消息和给vold发送命令。(1420-1425)
  5. 注册广播接收者用于处理多用户相关消息。(1427-1430)
  6. 创建内置卷。对应vold的内置EmulatedVolume("/data/media")。1432
  7. 添加watchdog监控。

        我们不重点分析和vold链接的代码,这不是我们关注的重点。对于添加内置卷的代码也比较简单,不去进行分析了。 读取配置文件这步有一些关键代码,来看下。

    private void readSettingsLocked() {mRecords.clear();mPrimaryStorageUuid = getDefaultPrimaryStorageUuid();mForceAdoptable = false;......}

        读取配置文件的代码也比较枯燥,另外配置文件只是为了兼容老版本。就不分析肋。 主要需要关注的是mPrimaryStorageUuid变量的设置。

    private String getDefaultPrimaryStorageUuid() {if (SystemProperties.getBoolean(StorageManager.PROP_PRIMARY_PHYSICAL, false)) {return StorageManager.UUID_PRIMARY_PHYSICAL;} else {return StorageManager.UUID_PRIVATE_INTERNAL;}}public static final String PROP_PRIMARY_PHYSICAL = "ro.vold.primary_physical"

        也就是说如果设置了ro.vold.primary_physica系统属性为真表示使用物理私有的卷作为主存储。否则使用私有内置卷作为主存储。 我们后面还会结合代码来分析主存储的确定。在这之前先来看下systemReady做了啥

    private void systemReady() {mSystemReady = true;mHandler.obtainMessage(H_SYSTEM_READY).sendToTarget();}private void handleSystemReady() {synchronized (mLock) {resetIfReadyAndConnectedLocked();}// Start scheduling nominally-daily fstrim operationsMountServiceIdler.scheduleIdlePass(mContext);}private void resetIfReadyAndConnectedLocked() {Slog.d(TAG, "Thinking about reset, mSystemReady=" + mSystemReady+ ", mDaemonConnected=" + mDaemonConnected);if (mSystemReady && mDaemonConnected) {killMediaProvider();mDisks.clear();mVolumes.clear();addInternalVolume();try {mConnector.execute("volume", "reset");// Tell vold about all existing and started usersfinal UserManager um = mContext.getSystemService(UserManager.class);final List<UserInfo> users = um.getUsers();for (UserInfo user : users) {mConnector.execute("volume", "user_added", user.id, user.serialNumber);}for (int userId : mStartedUsers) {mConnector.execute("volume", "user_started", userId);}} catch (NativeDaemonConnectorException e) {Slog.w(TAG, "Failed to reset vold", e);}}}

主要做了三件事:

  1. 下发volume的reset命令。
  2. 下发volume的user_added命令。
  3. volume的user_started命令。

        回到vold先看下reset命令,多用户的命令留到后面分析。

int VolumeManager::reset() {// Tear down all existing disks/volumes and start from a blank slate so// newly connected framework hears all events.mInternalEmulated->destroy();mInternalEmulated->create();for (auto disk : mDisks) {disk->destroy();disk->create();}mAddedUsers.clear();mStartedUsers.clear();return 0;
}

        这里做的事情我们已经比较熟悉了,就是重新创建了Disk和Volume。 在Disk创建过程会发送DISK_CREATED消息给MountService。我们再来回顾下Disk的创建:
system/vold/Disk.cpp

status_t Disk::create() {......notifyEvent(ResponseCode::DiskCreated, StringPrintf("%d", mFlags));......return OK;
}

        具体怎么发到MountService就不说明了,读者可以自己研究下。我们来关注MountService如何处理这个消息。
framework/base/services/core/java/com/android/server/MountService.java

private boolean onEventLocked(int code, String raw, String[] cooked) {switch (code) {case VoldResponseCode.DISK_CREATED: {if (cooked.length != 3) break;final String id = cooked[1];int flags = Integer.parseInt(cooked[2]);if (SystemProperties.getBoolean(StorageManager.PROP_FORCE_ADOPTABLE, false)|| mForceAdoptable) {flags |= DiskInfo.FLAG_ADOPTABLE;}mDisks.put(id, new DiskInfo(id, flags));break;}

        只不过创建了一个DiskInfo数据结构。

        接下来我们再看下新加卷的处理。同样在vold中创建卷会通知MountService。framework/base/services/core/java/com/android/server/MountService.java

private boolean onEventLocked(int code, String raw, String[] cooked) {switch (code) {......case VoldResponseCode.VOLUME_CREATED: {final String id = cooked[1];final int type = Integer.parseInt(cooked[2]);final String diskId = TextUtils.nullIfEmpty(cooked[3]);final String partGuid = TextUtils.nullIfEmpty(cooked[4]);final DiskInfo disk = mDisks.get(diskId);final VolumeInfo vol = new VolumeInfo(id, type, disk, partGuid);mVolumes.put(id, vol);onVolumeCreatedLocked(vol);break;1257     private void onVolumeCreatedLocked(VolumeInfo vol) {
1258         if (mPms.isOnlyCoreApps()) { //OnlyCore 模式不挂载外置存储
1259             Slog.d(TAG, "System booted in core-only mode; ignoring volume " + vol.getId());
1260             return;
1261         }
1262 
1263         if (vol.type == VolumeInfo.TYPE_EMULATED) { //模拟存储
1264             final StorageManager storage = mContext.getSystemService(StorageManager.class);
1265             final VolumeInfo privateVol = storage.findPrivateForEmulated(vol);
1266 
1267             if (Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, mPrimaryStorageUuid)
1268                     && VolumeInfo.ID_PRIVATE_INTERNAL.equals(privateVol.id)) { //使用内置存储作为主分区
1269                 Slog.v(TAG, "Found primary storage at " + vol);
1270                 vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY;
1271                 vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE;
1272                 mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
1273 
1274             } else if (Objects.equals(privateVol.fsUuid, mPrimaryStorageUuid)) {//指定私有存储作为主存储
1275                 Slog.v(TAG, "Found primary storage at " + vol);
1276                 vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY;
1277                 vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE;
1278                 mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
1279             }
1280 
1281         } else if (vol.type == VolumeInfo.TYPE_PUBLIC) {
1282             // TODO: only look at first public partition
1283             if (Objects.equals(StorageManager.UUID_PRIMARY_PHYSICAL, mPrimaryStorageUuid) //使用外置存储作为主分区
1284                     && vol.disk.isDefaultPrimary()) {
1285                 Slog.v(TAG, "Found primary storage at " + vol);
1286                 vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY;
1287                 vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE;
1288             }
1289 
1290             // Adoptable public disks are visible to apps, since they meet
1291             // public API requirement of being in a stable location.
1292             if (vol.disk.isAdoptable()) {
1293                 vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE;
1294             }
1295 
1296             vol.mountUserId = mCurrentUserId;
1297             mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
1298 
1299         } else if (vol.type == VolumeInfo.TYPE_PRIVATE) {
1300             mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
1301 
1302         } else {
1303             Slog.d(TAG, "Skipping automatic mounting of " + vol);
1304         }
1305     }

        注意这里首先会确定主存储,给主存储盘设置VolumeInfo.MOUNT_FLAG_PRIMARY标志,另外 VolumeInfo.MOUNT_FLAG_VISIBLE代表该盘是否对应用程序可见,也就是Context的那些api能否获取到该路径。 对于非PublicVolume,只有主存储对app可见。 PublicVolume则如果是加密的盘驱或者主存储,对应用程序可见,因为加密的盘驱有固定的存储位置。无论选择哪个卷都会发送H_VOLUME_MOUNT消息来挂载卷

 608         public void handleMessage(Message msg) {....668                 case H_VOLUME_MOUNT: {669                     final VolumeInfo vol = (VolumeInfo) msg.obj;670                     if (isMountDisallowed(vol)) {671                         Slog.i(TAG, "Ignoring mount " + vol.getId() + " due to policy");672                         break;673                     }674                     try {675                         mConnector.execute("volume", "mount", vol.id, vol.mountFlags,676                                 vol.mountUserId);677                     } catch (NativeDaemonConnectorException ignored) {678                     }679                     break;680                 }

         再次回到vold

int CommandListener::VolumeCmd::runCommand(SocketClient *cli,int argc, char **argv) {....} else if (cmd == "mount" && argc > 2) {// mount [volId] [flags] [user].....vol->setMountFlags(mountFlags);vol->setMountUserId(mountUserId);int res = vol->mount();if (mountFlags & android::vold::VolumeBase::MountFlags::kPrimary) {vm->setPrimary(vol);}return sendGenericOkFail(cli, res);}

         对于mount命令。首先根据命令找到对应卷,然后调用VolumeBase的mount方法进行挂载。
system/vold/VolumeBase.cpp

status_t VolumeBase::mount() {......setState(State::kChecking);status_t res = doMount();......return res;
}

         VolumeBase的mount方法调用doMount方法,该方法是一个虚函数,对于EmulatedVolume,PublicVolume和PrivateVolume有不同的实现,老规矩我们一一分析。先来分析PublicVolume

system/vold/PublicVolume.cpp

status_t PublicVolume::doMount() {// TODO: expand to support mounting other filesystems//读取卷的信息readMetadata();if (mFsType != "vfat") { //Android 只支持vfat格式的PublicVolume,其他类型直接返回LOG(ERROR) << getId() << " unsupported filesystem " << mFsType;return -EIO;}if (vfat::Check(mDevPath)) { //再次确认格式无误LOG(ERROR) << getId() << " failed filesystem check";return -EIO;}// Use UUID as stable name, if availablestd::string stableName = getId(); // 使用fsuuid作为稳定名称if (!mFsUuid.empty()) { stableName = mFsUuid;}// 准备fuse相关文件夹mRawPath = StringPrintf("/mnt/media_rw/%s", stableName.c_str());mFuseDefault = StringPrintf("/mnt/runtime/default/%s", stableName.c_str());mFuseRead = StringPrintf("/mnt/runtime/read/%s", stableName.c_str());mFuseWrite = StringPrintf("/mnt/runtime/write/%s", stableName.c_str());setInternalPath(mRawPath);if (getMountFlags() & MountFlags::kVisible) {setPath(StringPrintf("/storage/%s", stableName.c_str()));} else {setPath(mRawPath);}// 创建相关文件夹if (fs_prepare_dir(mRawPath.c_str(), 0700, AID_ROOT, AID_ROOT) ||fs_prepare_dir(mFuseDefault.c_str(), 0700, AID_ROOT, AID_ROOT) ||fs_prepare_dir(mFuseRead.c_str(), 0700, AID_ROOT, AID_ROOT) ||fs_prepare_dir(mFuseWrite.c_str(), 0700, AID_ROOT, AID_ROOT)) {PLOG(ERROR) << getId() << " failed to create mount points";return -errno;}//挂载设备if (vfat::Mount(mDevPath, mRawPath, false, false, false,AID_MEDIA_RW, AID_MEDIA_RW, 0007, true)) {PLOG(ERROR) << getId() << " failed to mount " << mDevPath;return -EIO;}if (getMountFlags() & MountFlags::kPrimary) {initAsecStage();}// 对app不可见直接返回if (!(getMountFlags() & MountFlags::kVisible)) {// Not visible to apps, so no need to spin up FUSEreturn OK;}dev_t before = GetDevice(mFuseWrite);if (!(mFusePid = fork())) { if (getMountFlags() & MountFlags::kPrimary) {//对于主存储使用fuse文件系统挂载,使用参数-w,fullwirte模式if (execl(kFusePath, kFusePath,"-u", "1023", // AID_MEDIA_RW"-g", "1023", // AID_MEDIA_RW"-U", std::to_string(getMountUserId()).c_str(),"-w",mRawPath.c_str(),stableName.c_str(),NULL)) {PLOG(ERROR) << "Failed to exec";}} else { // 非主存储,不使用fullwrite模式if (execl(kFusePath, kFusePath,"-u", "1023", // AID_MEDIA_RW"-g", "1023", // AID_MEDIA_RW"-U", std::to_string(getMountUserId()).c_str(),mRawPath.c_str(),stableName.c_str(),NULL)) {PLOG(ERROR) << "Failed to exec";}}LOG(ERROR) << "FUSE exiting";_exit(1);}if (mFusePid == -1) {PLOG(ERROR) << getId() << " failed to fork";return -errno;}while (before == GetDevice(mFuseWrite)) { //确保挂载完成。LOG(VERBOSE) << "Waiting for FUSE to spin up...";usleep(50000); // 50ms}return OK;
}

         PublicVolume作为外置存储,先通过mount进行挂载,然后为了实现外置存储的读写权限,使用fuse来进行挂载。另外如果设备要对用户可见,就将/storage/${stable} 作为对外使用的路径,我们分析外置存储的时候会对这个路径加以说明。如果这个PublicVolume作为主存储,需要使用fuse的fullwrite模式,否则不使用该模式。

         PrivateVolume
system/vold/PrivateVolume.cpp

status_t PrivateVolume::doMount() {if (readMetadata()) {LOG(ERROR) << getId() << " failed to read metadata";return -EIO;}mPath = StringPrintf("/mnt/expand/%s", mFsUuid.c_str());setPath(mPath);if (PrepareDir(mPath, 0700, AID_ROOT, AID_ROOT)) {PLOG(ERROR) << getId() << " failed to create mount point " << mPath;return -EIO;}if (mFsType == "ext4") { //改在ext4格式的dm设备int res = ext4::Check(mDmDevPath, mPath);if (res == 0 || res == 1) {LOG(DEBUG) << getId() << " passed filesystem check";} else {PLOG(ERROR) << getId() << " failed filesystem check";return -EIO;}if (ext4::Mount(mDmDevPath, mPath, false, false, true)) {PLOG(ERROR) << getId() << " failed to mount";return -EIO;}} else if (mFsType == "f2fs") {// 挂载fsfs类型的dm设备int res = f2fs::Check(mDmDevPath);if (res == 0) {LOG(DEBUG) << getId() << " passed filesystem check";} else {PLOG(ERROR) << getId() << " failed filesystem check";return -EIO;}if (f2fs::Mount(mDmDevPath, mPath)) {PLOG(ERROR) << getId() << " failed to mount";return -EIO;}} else {LOG(ERROR) << getId() << " unsupported filesystem " << mFsType;return -EIO;}LOG(VERBOSE) << "Starting restorecon of " << mPath;// TODO: find a cleaner way of waiting for restorecon to finishproperty_set("selinux.restorecon_recursive", "");property_set("selinux.restorecon_recursive", mPath.c_str());char value[PROPERTY_VALUE_MAX];while (true) {property_get("selinux.restorecon_recursive", value, "");if (strcmp(mPath.c_str(), value) == 0) {break;}sleep(1);LOG(VERBOSE) << "Waiting for restorecon...";}LOG(VERBOSE) << "Finished restorecon of " << mPath;// 准备目录// Verify that common directories are ready to rollif (PrepareDir(mPath + "/app", 0771, AID_SYSTEM, AID_SYSTEM) ||PrepareDir(mPath + "/user", 0711, AID_SYSTEM, AID_SYSTEM) ||PrepareDir(mPath + "/media", 0770, AID_MEDIA_RW, AID_MEDIA_RW) ||PrepareDir(mPath + "/media/0", 0770, AID_MEDIA_RW, AID_MEDIA_RW) ||PrepareDir(mPath + "/local", 0751, AID_ROOT, AID_ROOT) ||PrepareDir(mPath + "/local/tmp", 0771, AID_SHELL, AID_SHELL)) {PLOG(ERROR) << getId() << " failed to prepare";return -EIO;}// Create a new emulated volume stacked above us, it will automatically// be destroyed during unmount// 准备media目录。生成模拟卷来模拟meida目录。std::string mediaPath(mPath + "/media");auto vol = std::shared_ptr<VolumeBase>(new EmulatedVolume(mediaPath, mRawDevice, mFsUuid));addVolume(vol);vol->create();return OK;
}

        PrivateVolume使用外置存储来模拟内置存储,需要挂载mDmDevPath作为存储设备(需要对数据进行加密/解密),然后对为该设备的media节点创建一个EmulatedVolume,来模拟外置存储使用。这里总结下EmulatedVolume, /data分区作为一个内置存储,如果使用模拟分区作为主存储则会创建一个EmulatedVolume来模拟外置存储,目录为/data/media。 其他的PrivateVolume下再这里创建一个EmulatedVolume,会通过MountService有模拟分区创建,如果该模拟分区是主存储分区,则会挂载该模拟分区,如果不是主分区,则不会挂载该模拟分区,为PrivateVolume创建EmulatedVolume,并不一定会被挂载,只有模拟的主分区才会被挂载。读者可以回过头来看看MountService中处理新家卷的时候对EmulatedVolume的处理。
system/vold/EmulatedVolume.cpp

status_t EmulatedVolume::doMount() {// We could have migrated storage to an adopted private volume, so always// call primary storage "emulated" to avoid media rescans.std::string label = mLabel;if (getMountFlags() & MountFlags::kPrimary) {label = "emulated";}mFuseDefault = StringPrintf("/mnt/runtime/default/%s", label.c_str());mFuseRead = StringPrintf("/mnt/runtime/read/%s", label.c_str());mFuseWrite = StringPrintf("/mnt/runtime/write/%s", label.c_str());setInternalPath(mRawPath);setPath(StringPrintf("/storage/%s", label.c_str()));if (fs_prepare_dir(mFuseDefault.c_str(), 0700, AID_ROOT, AID_ROOT) ||fs_prepare_dir(mFuseRead.c_str(), 0700, AID_ROOT, AID_ROOT) ||fs_prepare_dir(mFuseWrite.c_str(), 0700, AID_ROOT, AID_ROOT)) {PLOG(ERROR) << getId() << " failed to create mount points";return -errno;}dev_t before = GetDevice(mFuseWrite);if (!(mFusePid = fork())) {if (execl(kFusePath, kFusePath,"-u", "1023", // AID_MEDIA_RW"-g", "1023", // AID_MEDIA_RW"-m","-w",mRawPath.c_str(),label.c_str(),NULL)) {PLOG(ERROR) << "Failed to exec";}LOG(ERROR) << "FUSE exiting";_exit(1);}if (mFusePid == -1) {PLOG(ERROR) << getId() << " failed to fork";return -errno;}while (before == GetDevice(mFuseWrite)) {LOG(VERBOSE) << "Waiting for FUSE to spin up...";usleep(50000); // 50ms}return OK;
}

如果一个EmulatedVolume被挂载,它一定是一个主分区,没啥可说对mRawPath目录做fuse模拟。

到这里外接存储的挂载就基本解释完了,由于篇幅的关系,对于外接磁盘的格式化和多用户的管理我们再分两篇文章来完成。

2. 格式化

3. 多用户

查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. HTML锚点链接

    什么是锚点链接? 锚点链接(也叫书签链接)常常用于那些内容庞大繁琐的网页,通过点击命名锚点,不仅让我们能指向文档,还能指向页面里的特定段落,更能当作"精准链接"的便利工具,让链接对象接近焦点。便于浏览者查看网页内容。 其实就是定位器 当前页面设置锚点首…...

    2024/4/26 8:05:27
  2. 记录最近整树莓派遇到的那些坑

    树莓派4b ,教之前的版本来看运算速度上已经有了相当大的改进,但是升级改进带来了一些问题,解决方法不可以参照之前的版本,这里我就我遇到的问题来进行一个总结。 1.关于固件 作为一个渴望成为机器人工程师的人,我拿到树莓派4b安装的第一个系统不是对应的raspberry官方系统…...

    2024/4/29 22:48:52
  3. 培训日记三

    流程控制: 在一个程序执行过程中,每一个语句都是顺序执行的,对程序的结果有直接的影响,也就是说,程序的流程对我们 的结果有直接的影响,所以我们必须要搞清楚,我们的各个语句的执行流程,很多时候我们要通过控制语句的执行 顺序来实现我们的实现我们的功能。 1、顺序执行…...

    2024/4/26 3:08:31
  4. 志愿填报在即,店宝宝告诉你,高薪的专业有哪些?

    专业是从市场中来,又到市场中去。相信每一个应届高考生都对未来要学三年、四年甚至六七年的专业非常重视,同时大家都会特别关注一个问题:自己学的专业毕业后薪酬待遇怎么样?前段时间网上有报道一则新闻,说是现在的送外卖小哥里面,硕士学历的小哥就有7万之众(700万外卖小…...

    2024/4/22 9:24:06
  5. android基础控件(7)日期选择器(DataPicker)

    android基础控件(1)TabHost实现选项卡 android基础控件(2)TabLayout+ViewPager实现选项卡 android基础控件(3)BottomNavigationView+FragmentLayout实现底层导航栏 android基础控件(4)GridView实现网格视图 android基础控件(5)Spinner实现下列列表框 android基础控件…...

    2024/4/23 0:44:52
  6. 论文阅读:What Do We Understand About Convolutional Networks? 对卷积的了解

    What Do We Understand About Convolutional Networks? 目录What Do We Understand About Convolutional Networks?1 引言1.1动机1.2目标1.3 报告提纲2 多层网络2.1 多层架构2.1.1 神经网络2.1.2 循环神经网络2.1.3 卷积网络2.1.4 生成对抗网络2.1.5 多层网络的训练2.1.6 简单…...

    2024/5/3 23:12:39
  7. JavaWeb自学笔记[Filter][自用]

    1、 Filter 什么是过滤器 1、Filter过滤器它是JavaWeb的三大组件之一。三大组件分别是:Servlet程序、Listener监听器、Filter过滤器。 2、Filter过滤器它是JavaEE的规范。也就是接口。 3、Filter过滤器它的作用是:拦截请求,过滤响应。 截请求常见的应用场景:1、权限检查2、日记…...

    2024/4/21 15:52:36
  8. 混淆矩阵中的 精确率 和 召回率 与 置信度 之间有什么关系

    True 表示正样本,False 表示负样本,Positive 表示预测为 真,Negative 表示预测为 假 —— 题记 混淆矩阵假阳性FPFPFP 在 左下角,假阴性FNFNFN 在右上角。 精确率 Precision=TPTP+FP \mathrm{Precision} = \frac{TP}{TP+FP} Precision=TP+FPTP​ 精确率以 Positive 的个数为…...

    2024/5/3 23:10:12
  9. quartus II 13.1 编译报错

    我的电脑版本 win10 64 今天编译出现的错误:Error (119013): Current license file does not support the EP4CE10F17C8 device 我下载的是软件管家的13.1版本 破解教程:https://mp.weixin.qq.com/s/LVtdNGDZJNC-4859vM8k7g 破解crack文件:链接:https://pan.baidu.com/s/1E…...

    2024/5/2 3:37:15
  10. linux的静态IP设置

    linux的静态IP设置 linux的静态IP设置 当虚拟机拥有多种系统后,开机后可能导致自动IP每次都不同。因此需要我们来设置静态IP。 VMware Workstatio 首先打开VMware Workstatio ,在编辑下找到虚拟网络编辑器,选择VMnet8如图所示,在NAT设置中找到网关(很关键)如图为192.168.…...

    2024/4/21 5:14:02
  11. 10.单点登录(jwt)

    1. 用户管理提供数据接口 搭建gmall-ums略。。。。 参照课前资料中的《前端商城接口文档.md》编写数据接口 1.1. 数据验证功能 根据接口文档知:请求方式:GET 请求路径:check/{param}/{type} 请求参数:param,type 返回结果:true或false1.1.2. UserController /*** 校验数据…...

    2024/4/15 17:06:14
  12. Web前端21届面经_个人汇总

    CSS CSS 3有什么新特征?过渡——transition动画——animation形变——transform滤镜——filter渐变阴影——box-shadow图片边框——border-image背景——background-clip文字多行省略弹性布局CSS有哪几种选择器标签 类名 id属性 伪类 伪元素如何实现垂直居中line-height 等于 …...

    2024/4/15 17:06:11
  13. python ------ 正则表达式

    文章目录匹配单个字符匹配多个字符匹配开头结尾re模块操作匹配分组re模块的⾼级⽤法python贪婪和非贪婪r的作⽤ 正则表达式的作用: 测试字符串的某个模式,即数据有效性验证 实现按照某种规则替换文本 根据模式匹配从字符串中提取一个子字符串 正则表达式的构成: 原子(普通字…...

    2024/5/3 23:36:47
  14. vue 关于样式类动态绑定(推荐方法二)

    方法一:通过 :class="+item.no <= 3 ? ‘top-no’ : ‘’"三木运算添加样式类 <div class="list-item" v-for="item in rankData" :key="item.no"><div classs="list-item-no" :class="+item.no <= 3…...

    2024/4/18 19:51:40
  15. 重建二叉树

    题目描述 : 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。 首先,先分析题目,属于二叉树的遍历.回顾一下,…...

    2024/4/20 3:33:28
  16. 移动互联网的下半场,Android 开发还有前景吗?

    “现在学习 Android 开发还有前景吗?” “Android 开发还有什么可以研究的?” 近半年来,许多移动开发者都问过我这样的问题。大家对于职业的未来,都有一些迷茫和焦虑。为什么会这样呢? 放眼现在互联网行业,各大公司已经从争夺用户转为争夺用户时长。而小程序、快应用的崛…...

    2024/4/15 15:41:50
  17. 使用递归函数计算1到n之和 (10分)

    int sum( int n ) {int result;if(n<=0) return 0;else{if(n==1){result=1;}else result=n+sum(n-1);return result;} }...

    2024/5/3 23:10:18
  18. 一篇日记

    一直以来,我都是个很怂的人,即使很生气,也不会去跟别人吵架,因为怕自己打不过,不敢去接受别人的示好,因为不想生活变的太好或者太坏,在一个学姐的鼓励下,我做的最勇敢的事是跟前男友表白,可是我还是跟他说了无数次对不起,打了无数次没人接的电话,因为他哭过无数次,…...

    2024/4/20 5:19:52
  19. java学习笔记 基础篇02

    类型转换由于Java是强类型语言,所以要进行有些运算的时候,需要用到类型转换。运算中,不同类型的数据先转换为同一类型,然后计算强制类型转换 语法:(类型)变量名 高—低自动类型转换 可以直接转换 低—高注意点:不能对布尔类型进行转换不能把对象类型转换为不相干的类型在…...

    2024/4/15 15:41:48
  20. 记录gitee再次提交和错误提示

    接上次的gitee首次提交,再次提交 1git init 2git add . 3git commit -m “add_second” 4git push 中途遇见一次错误 failed to push some refs to git 原因是readme文件不在本地 运行git pull --rebase origin master 再次git push解决 记录一下...

    2024/4/20 14:03:02

最新文章

  1. 快讯! MySQL 8.4.0 LTS 发布(MySQL 第一个长期支持版本)

    MySQL 第一个长期支持版本 8.4.0 LTS 发布&#xff0c;社区版下载地址&#xff1a; https://dev.mysql.com/downloads/mysql/ 功能变更 添加或更改的功能 组复制&#xff1a;与组复制相关的两个服务器系统变量的默认值已更改&#xff1a; 系统变量的默认值为 group_replication…...

    2024/5/4 5:05:36
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. 利用Spark将Kafka数据流写入HDFS

    利用Spark将Kafka数据流写入HDFS 在当今的大数据时代&#xff0c;实时数据处理和分析变得越来越重要。Apache Kafka作为一个分布式流处理平台&#xff0c;已经成为处理实时数据的事实标准。而Apache Spark则是一个强大的大数据处理框架&#xff0c;它提供了对数据进行复杂处理…...

    2024/4/30 15:37:05
  4. 学习鸿蒙基础(11)

    目录 一、Navigation容器 二、web组件 三、video视频组件 四、动画 1、属性动画 .animation() 2、 转场动画 transition() 配合animateTo() 3、页面间转场动画 一、Navigation容器 Navigation组件一般作为页面的根容器&#xff0c;包括单页面、分栏和自适应三种显示模式…...

    2024/5/2 13:09:22
  5. 416. 分割等和子集问题(动态规划)

    题目 题解 class Solution:def canPartition(self, nums: List[int]) -> bool:# badcaseif not nums:return True# 不能被2整除if sum(nums) % 2 ! 0:return False# 状态定义&#xff1a;dp[i][j]表示当背包容量为j&#xff0c;用前i个物品是否正好可以将背包填满&#xff…...

    2024/5/3 11:50:27
  6. 【Java】ExcelWriter自适应宽度工具类(支持中文)

    工具类 import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet;/*** Excel工具类** author xiaoming* date 2023/11/17 10:40*/ public class ExcelUti…...

    2024/5/2 16:04:58
  7. Spring cloud负载均衡@LoadBalanced LoadBalancerClient

    LoadBalance vs Ribbon 由于Spring cloud2020之后移除了Ribbon&#xff0c;直接使用Spring Cloud LoadBalancer作为客户端负载均衡组件&#xff0c;我们讨论Spring负载均衡以Spring Cloud2020之后版本为主&#xff0c;学习Spring Cloud LoadBalance&#xff0c;暂不讨论Ribbon…...

    2024/5/2 23:55:17
  8. TSINGSEE青犀AI智能分析+视频监控工业园区周界安全防范方案

    一、背景需求分析 在工业产业园、化工园或生产制造园区中&#xff0c;周界防范意义重大&#xff0c;对园区的安全起到重要的作用。常规的安防方式是采用人员巡查&#xff0c;人力投入成本大而且效率低。周界一旦被破坏或入侵&#xff0c;会影响园区人员和资产安全&#xff0c;…...

    2024/5/3 16:00:51
  9. VB.net WebBrowser网页元素抓取分析方法

    在用WebBrowser编程实现网页操作自动化时&#xff0c;常要分析网页Html&#xff0c;例如网页在加载数据时&#xff0c;常会显示“系统处理中&#xff0c;请稍候..”&#xff0c;我们需要在数据加载完成后才能继续下一步操作&#xff0c;如何抓取这个信息的网页html元素变化&…...

    2024/5/3 11:10:49
  10. 【Objective-C】Objective-C汇总

    方法定义 参考&#xff1a;https://www.yiibai.com/objective_c/objective_c_functions.html Objective-C编程语言中方法定义的一般形式如下 - (return_type) method_name:( argumentType1 )argumentName1 joiningArgument2:( argumentType2 )argumentName2 ... joiningArgu…...

    2024/5/3 21:22:01
  11. 【洛谷算法题】P5713-洛谷团队系统【入门2分支结构】

    &#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【洛谷算法题】 文章目录 【洛谷算法题】P5713-洛谷团队系统【入门2分支结构】&#x1f30f;题目描述&#x1f30f;输入格…...

    2024/5/3 23:17:01
  12. 【ES6.0】- 扩展运算符(...)

    【ES6.0】- 扩展运算符... 文章目录 【ES6.0】- 扩展运算符...一、概述二、拷贝数组对象三、合并操作四、参数传递五、数组去重六、字符串转字符数组七、NodeList转数组八、解构变量九、打印日志十、总结 一、概述 **扩展运算符(...)**允许一个表达式在期望多个参数&#xff0…...

    2024/5/2 23:47:43
  13. 摩根看好的前智能硬件头部品牌双11交易数据极度异常!——是模式创新还是饮鸩止渴?

    文 | 螳螂观察 作者 | 李燃 双11狂欢已落下帷幕&#xff0c;各大品牌纷纷晒出优异的成绩单&#xff0c;摩根士丹利投资的智能硬件头部品牌凯迪仕也不例外。然而有爆料称&#xff0c;在自媒体平台发布霸榜各大榜单喜讯的凯迪仕智能锁&#xff0c;多个平台数据都表现出极度异常…...

    2024/5/3 13:26:06
  14. Go语言常用命令详解(二)

    文章目录 前言常用命令go bug示例参数说明 go doc示例参数说明 go env示例 go fix示例 go fmt示例 go generate示例 总结写在最后 前言 接着上一篇继续介绍Go语言的常用命令 常用命令 以下是一些常用的Go命令&#xff0c;这些命令可以帮助您在Go开发中进行编译、测试、运行和…...

    2024/5/3 1:55:15
  15. 用欧拉路径判断图同构推出reverse合法性:1116T4

    http://cplusoj.com/d/senior/p/SS231116D 假设我们要把 a a a 变成 b b b&#xff0c;我们在 a i a_i ai​ 和 a i 1 a_{i1} ai1​ 之间连边&#xff0c; b b b 同理&#xff0c;则 a a a 能变成 b b b 的充要条件是两图 A , B A,B A,B 同构。 必要性显然&#xff0…...

    2024/5/4 2:14:16
  16. 【NGINX--1】基础知识

    1、在 Debian/Ubuntu 上安装 NGINX 在 Debian 或 Ubuntu 机器上安装 NGINX 开源版。 更新已配置源的软件包信息&#xff0c;并安装一些有助于配置官方 NGINX 软件包仓库的软件包&#xff1a; apt-get update apt install -y curl gnupg2 ca-certificates lsb-release debian-…...

    2024/5/3 16:23:03
  17. Hive默认分割符、存储格式与数据压缩

    目录 1、Hive默认分割符2、Hive存储格式3、Hive数据压缩 1、Hive默认分割符 Hive创建表时指定的行受限&#xff08;ROW FORMAT&#xff09;配置标准HQL为&#xff1a; ... ROW FORMAT DELIMITED FIELDS TERMINATED BY \u0001 COLLECTION ITEMS TERMINATED BY , MAP KEYS TERMI…...

    2024/5/3 1:55:09
  18. 【论文阅读】MAG:一种用于航天器遥测数据中有效异常检测的新方法

    文章目录 摘要1 引言2 问题描述3 拟议框架4 所提出方法的细节A.数据预处理B.变量相关分析C.MAG模型D.异常分数 5 实验A.数据集和性能指标B.实验设置与平台C.结果和比较 6 结论 摘要 异常检测是保证航天器稳定性的关键。在航天器运行过程中&#xff0c;传感器和控制器产生大量周…...

    2024/5/2 8:37:00
  19. --max-old-space-size=8192报错

    vue项目运行时&#xff0c;如果经常运行慢&#xff0c;崩溃停止服务&#xff0c;报如下错误 FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory 因为在 Node 中&#xff0c;通过JavaScript使用内存时只能使用部分内存&#xff08;64位系统&…...

    2024/5/3 14:57:24
  20. 基于深度学习的恶意软件检测

    恶意软件是指恶意软件犯罪者用来感染个人计算机或整个组织的网络的软件。 它利用目标系统漏洞&#xff0c;例如可以被劫持的合法软件&#xff08;例如浏览器或 Web 应用程序插件&#xff09;中的错误。 恶意软件渗透可能会造成灾难性的后果&#xff0c;包括数据被盗、勒索或网…...

    2024/5/2 9:47:25
  21. JS原型对象prototype

    让我简单的为大家介绍一下原型对象prototype吧&#xff01; 使用原型实现方法共享 1.构造函数通过原型分配的函数是所有对象所 共享的。 2.JavaScript 规定&#xff0c;每一个构造函数都有一个 prototype 属性&#xff0c;指向另一个对象&#xff0c;所以我们也称为原型对象…...

    2024/5/4 2:00:16
  22. C++中只能有一个实例的单例类

    C中只能有一个实例的单例类 前面讨论的 President 类很不错&#xff0c;但存在一个缺陷&#xff1a;无法禁止通过实例化多个对象来创建多名总统&#xff1a; President One, Two, Three; 由于复制构造函数是私有的&#xff0c;其中每个对象都是不可复制的&#xff0c;但您的目…...

    2024/5/3 22:03:11
  23. python django 小程序图书借阅源码

    开发工具&#xff1a; PyCharm&#xff0c;mysql5.7&#xff0c;微信开发者工具 技术说明&#xff1a; python django html 小程序 功能介绍&#xff1a; 用户端&#xff1a; 登录注册&#xff08;含授权登录&#xff09; 首页显示搜索图书&#xff0c;轮播图&#xff0…...

    2024/5/3 7:43:42
  24. 电子学会C/C++编程等级考试2022年03月(一级)真题解析

    C/C++等级考试(1~8级)全部真题・点这里 第1题:双精度浮点数的输入输出 输入一个双精度浮点数,保留8位小数,输出这个浮点数。 时间限制:1000 内存限制:65536输入 只有一行,一个双精度浮点数。输出 一行,保留8位小数的浮点数。样例输入 3.1415926535798932样例输出 3.1…...

    2024/5/3 1:54:59
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57