DeviceStorageMonitorService是framework中实时检查当前设备是否存在底存储空间状态的服务,他继承于systemserver。我们可以通过dumpsys devicestoragemonitor来获取他的一些信息。

1 周期性任务

DeviceStorageMonitorService继承SystemService,像servicemanager注册服务字符串未devicestoragemonitor,构造方法中会发送handler消息调用checkLow和checkHigh两个方法

/system/frameworks/base/services/core/java/com/android/server/storage/DeviceStorageMonitorService.java 
/**
 * Service that monitors and maintains free space on storage volumes.
 * <p>
 * As the free space on a volume nears the threshold defined by
 * {@link StorageManager#getStorageLowBytes(File)}, this service will clear out
 * cached data to keep the disk from entering this low state.
 */
public class DeviceStorageMonitorService extends SystemService {
    private static final String TAG = "DeviceStorageMonitorService";
    static final String SERVICE = "devicestoragemonitor";
    // com.android.internal.R.string.low_internal_storage_view_text_no_boot
    // hard codes 250MB in the message as the storage space required for the
    // boot image.
    private static final long BOOT_IMAGE_STORAGE_REQUIREMENT = DataUnit.MEBIBYTES.toBytes(250);
    private static class State {
        private static final int LEVEL_UNKNOWN = -1;
        private static final int LEVEL_NORMAL = 0;  //正常状态
        private static final int LEVEL_LOW = 1;     //低空间状态
        private static final int LEVEL_FULL = 2;    //存储已满状态
        /** Last "level" that we alerted about */
        public int level = LEVEL_NORMAL;
    }
    public DeviceStorageMonitorService(Context context) {
        super(context);
        mHandlerThread = new HandlerThread(TAG, android.os.Process.THREAD_PRIORITY_BACKGROUND);
        mHandlerThread.start();
        mHandler = new Handler(mHandlerThread.getLooper()) {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case MSG_CHECK_LOW:
                        checkLow();
                        return;
                    case MSG_CHECK_HIGH:
                        checkHigh();
                        return;
                }
            }
        };
    }
    @Override
    public void onStart() {
        publishBinderService(SERVICE, mRemoteService);
        publishLocalService(DeviceStorageMonitorInternal.class, mLocalService);
    }
}

2 checkLow

检查当前设备是否处于LOW的状态:

  • storage.getWritablePrivateVolumes获取所有私有卷,通常/data就是一个私有卷,如果有多个私有卷目录需要遍历。
  • storage.getStorageFullBytes(file)获取当前目录的存储空间已满阈值,表示当前空间已满。从上面的dumpsys的fullBytes数据转换来看这里在N2XX项目上面默认设置1M大小。
  • storage.getStorageLowBytes(file)获取当前目录的低空间阈值,如果低于这个值会触发告警。从上面的dumpsys的lowBytes数据转换来看这里在N2XX项目上面默认设置512M大小。
  •  pm.freeStorage调用PMS清理应用缓存等数据,这里的触发条件是可用空间小于低空间阈值的1.5倍触发,即在N2XX项目上来看/data目录神可用空间小于512*1.5M就会触发pms清理应用缓存。
  • file.getTotalSpace()获取当前目录的总空间大小
  • file.getUsableSpace()获取当前目录的可用空间大小,即剩下空间
  • usableBytes <= fullBytes 当前目录的可用空间小于fullBytes表示当前存储已满,设置State.LEVEL_FULL
  • usableBytes <= lowBytes当前目录的可用空间小于lowBytes表示当前低存储,设置State.LEVEL_LOW
  • usableBytes < BOOT_IMAGE_STORAGE_REQUIREMENT当前目录的可用空间小于定制的值也表示当前低存储,设置State.LEVEL_LOW。这里的默认值250M
  • updateNotifications向系统发送通知
  • updateBroadcasts向系统发送广播:Intent.ACTION_DEVICE_STORAGE_FULL或者或者ACTION_DEVICE_STORAGE_LOW
  • 最后handler发延迟消息,进行下一分钟轮询此函数
/**
     * Core logic that checks the storage state of every mounted private volume and clears cache
     * under low storage state. Since this can do heavy I/O, callers should invoke indirectly using
     * {@link #MSG_CHECK_LOW}.
     */
    @WorkerThread
    private void checkLow() {
        final StorageManager storage = getContext().getSystemService(StorageManager.class);
        final int seq = mSeq.get();
        // Check every mounted private volume to see if they're low on space
        for (VolumeInfo vol : storage.getWritablePrivateVolumes()) {
            //获取可写的私有卷,例如/data就是其中之一,vol.getPath就是/data
            final File file = vol.getPath();
            //获取对应目录的爆满状态阈值
            final long fullBytes = storage.getStorageFullBytes(file);
            //获取对应目录的低内存状态阈值
            final long lowBytes = storage.getStorageLowBytes(file);
            // Automatically trim cached data when nearing the low threshold;
            // when it's within 150% of the threshold, we try trimming usage
            // back to 200% of the threshold.
            //当前目录可用空间小于低内存阈值的1.5倍触发pms进行清除应用缓存
            if (file.getUsableSpace() < (lowBytes * 3) / 2) {
                final PackageManagerInternal pm =
                        LocalServices.getService(PackageManagerInternal.class);
                try {
                    pm.freeStorage(vol.getFsUuid(), lowBytes * 2, 0);
                } catch (IOException e) {
                    Slog.w(TAG, e);
                }
            }
            // Send relevant broadcasts and show notifications based on any
            // recently noticed state transitions.
            final UUID uuid = StorageManager.convert(vol.getFsUuid());
            final State state = findOrCreateState(uuid);
            final long totalBytes = file.getTotalSpace();
            final long usableBytes = file.getUsableSpace();
            int oldLevel = state.level;
            int newLevel;
            if (mForceLevel != State.LEVEL_UNKNOWN) {
                // When in testing mode, use unknown old level to force sending
                // of any relevant broadcasts.
                oldLevel = State.LEVEL_UNKNOWN;
                newLevel = mForceLevel;
            } else if (usableBytes <= fullBytes) {
                //存储已满
                newLevel = State.LEVEL_FULL;
            } else if (usableBytes <= lowBytes) {
                newLevel = State.LEVEL_LOW;
            } else if (StorageManager.UUID_DEFAULT.equals(uuid)
                    && usableBytes < BOOT_IMAGE_STORAGE_REQUIREMENT) {
                newLevel = State.LEVEL_LOW;
            } else {
                newLevel = State.LEVEL_NORMAL;
            }
            // Log whenever we notice drastic storage changes
            if ((Math.abs(state.lastUsableBytes - usableBytes) > DEFAULT_LOG_DELTA_BYTES)
                    || oldLevel != newLevel) {
                EventLogTags.writeStorageState(uuid.toString(), oldLevel, newLevel,
                        usableBytes, totalBytes);
                state.lastUsableBytes = usableBytes;
            }
            updateNotifications(vol, oldLevel, newLevel);
            updateBroadcasts(vol, oldLevel, newLevel, seq);
            state.level = newLevel;
        }
        // Loop around to check again in future; we don't remove messages since
        // there might be an immediate request pending.
        if (!mHandler.hasMessages(MSG_CHECK_LOW)) {
            mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CHECK_LOW),
                    LOW_CHECK_INTERVAL);
        }
        if (!mHandler.hasMessages(MSG_CHECK_HIGH)) {
            mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CHECK_HIGH),
                    HIGH_CHECK_INTERVAL);
        }
    }

StorageManager.getStorageFullBytes如何计算的?

//system/frameworks/base/core/java/android/os/storage/StorageManager.java 
private static final long DEFAULT_FULL_THRESHOLD_BYTES = DataUnit.MEBIBYTES.toBytes(1);   
//即如果Settings数据库没有设置,那么就固定写死1M
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public long getStorageFullBytes(File path) {
        return Settings.Global.getLong(mResolver, Settings.Global.SYS_STORAGE_FULL_THRESHOLD_BYTES,
                DEFAULT_FULL_THRESHOLD_BYTES);
    }

StorageManager.getStorageLowBytes如何计算的?

//system/frameworks/base/core/java/android/os/storage/StorageManager.java
public static final int DEFAULT_STORAGE_THRESHOLD_PERCENT_LOW = 5;
private static final long DEFAULT_THRESHOLD_MAX_BYTES = DataUnit.MEBIBYTES.toBytes(500);
@UnsupportedAppUsage
    public long getStorageLowBytes(File path) {
        //计算对应目录的总大小百分比,这里的DEFAULT_STORAGE_THRESHOLD_PERCENT_LOW默认值为5,即5%
        final long lowPercent = Settings.Global.getInt(mResolver,
                Settings.Global.SYS_STORAGE_THRESHOLD_PERCENTAGE,
                DEFAULT_STORAGE_THRESHOLD_PERCENT_LOW);
        final long lowBytes = (path.getTotalSpace() * lowPercent) / 100;
        //这里获取最大LOW低空间阀值,DEFAULT_THRESHOLD_MAX_BYTES固定写死500M
        final long maxLowBytes = Settings.Global.getLong(mResolver,
                Settings.Global.SYS_STORAGE_THRESHOLD_MAX_BYTES, DEFAULT_THRESHOLD_MAX_BYTES);
        //取上面的最小值
        return Math.min(lowBytes, maxLowBytes);
    }

3 checkHigh

检查当前私有卷可用空间是否低于总空间的20%,触发pms释放一些app的缓存数据,逻辑如下:

  • DEFAULT_STORAGE_THRESHOLD_PERCENT_HIGH在StorageManager.java 被定义为20,所以这里的判断条件为可用空间小于总空间20%
  • pm.freeAllAppCacheAboveQuota 
    /**
     * Core logic that checks the storage state of every mounted private volume and clears cache if
     * free space is under 20% of total space. Since this can do heavy I/O, callers should invoke
     * indirectly using {@link #MSG_CHECK_HIGH}.
     */
    @WorkerThread
    private void checkHigh() {
        final StorageManager storage = getContext().getSystemService(StorageManager.class);
        // Check every mounted private volume to see if they're under the high storage threshold
        // which is storageThresholdPercentHigh of total space
        final int storageThresholdPercentHigh = DeviceConfig.getInt(
                DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
                StorageManager.STORAGE_THRESHOLD_PERCENT_HIGH_KEY,
                StorageManager.DEFAULT_STORAGE_THRESHOLD_PERCENT_HIGH);
        for (VolumeInfo vol : storage.getWritablePrivateVolumes()) {
            final File file = vol.getPath();
            if (file.getUsableSpace() < file.getTotalSpace() * storageThresholdPercentHigh / 100) {
                final PackageManagerInternal pm =
                        LocalServices.getService(PackageManagerInternal.class);
                try {
                    pm.freeAllAppCacheAboveQuota(vol.getFsUuid());
                } catch (IOException e) {
                    Slog.w(TAG, e);
                }
            }
        }
        // Loop around to check again in future; we don't remove messages since
        // there might be an immediate request pending
        if (!mHandler.hasMessages(MSG_CHECK_HIGH)) {
            mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CHECK_HIGH),
                    HIGH_CHECK_INTERVAL);
        }
    }

4 调试低存储

使用存储填充工具占满data空间,/data目录还剩下大约500M的空间,但是无法成功安装应用

adb shell dumpsys devicestoragemonitor当前存储信息,lowBytes被设置为512M,当前可用空间大约495M,因此被设置为LOW状态,触发此机制

5 如何影响PMS安装应用?

最初以为level=LOW的状态导致PMS安装应用的时候失败呢?但是经过多次测试发现上面adb install安装应用的报错和level没有什么关系,倒是和lowBytes和fullBytes的值关系很大?

搜索报错日志"Requested internal only, but not enough space",跟踪流程依次如下:

resolveInstallVolume:安装应用之前会走如下方法,并去check存储是否空间足够,这里检查的PRIVATE私有卷,即/data目录

checkFitOnVolume:params为需要安装的应用需要的空间,如果小于存储可用空间availBytes就会主动抛异常

getAllocatableBytes:计算availBytes,为/data目录可用空间减去前面设置的低存储阀值,并取最大值。例如当前可用空间400M,lowbytes被设置为500M,那么这里减了后就是-100,取0

总结:adb install应用和State.LEVEL_LOW无关,但是与lowBytes存在直接关联,即PMS在安装应用之前会通过SMS去获取/data目录的剩余空间是否够用,如果除去lowBytes之后不够直接报错

6 案例之freeStorage过程异常

问题背景:DDR测试过程中出现系统不稳定,磁盘被占满的情况

日志分析:DeviceStorageMonitorService: java.io.IOException: Failed to free 1048576000 on storage device at /data  at com.android.server.pm.PackageManagerService.freeStorage(PackageManagerService.java:2920)

源码分析:我们来分析一下PMS的freeStorage的代码流程,代码如下

//frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java 
    public void freeStorage(String volumeUuid, long bytes, @StorageManager.AllocateFlags int flags) throws IOException {
        final StorageManager storage = mInjector.getSystemService(StorageManager.class);
        //获取当前目录,通常是/data目录
        final File file = storage.findPathForUuid(volumeUuid);
        //如果/data目录可用空间大于bytes就不需要进行释放
        if (file.getUsableSpace() >= bytes) return;
        if (mEnableFreeCacheV2) {
            final boolean internalVolume =  Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, volumeUuid);
            final boolean aggressive = (flags & StorageManager.FLAG_ALLOCATE_AGGRESSIVE) != 0;
            // 1. Pre-flight to determine if we have any chance to succeed
            // 2. Consider preloaded data (after 1w honeymoon, unless aggressive)
            if (internalVolume && (aggressive || SystemProperties.getBoolean("persist.sys.preloads.file_cache_expired", false))) {
                //删除预加载的文件缓存
                deletePreloadsFileCache();
                //如果/data目录可用空间大于bytes就直接退出不用再进行释放了
                if (file.getUsableSpace() >= bytes) return;
            }
            // 3. Consider parsed APK data (aggressive only)
            if (internalVolume && aggressive) {
                //删除指定cache目录
                FileUtils.deleteContents(mCacheDir);
                //如果/data目录可用空间大于bytes就直接退出不用再进行释放了
                if (file.getUsableSpace() >= bytes) return;
            }
            // 4. Consider cached app data (above quotas)
            synchronized (mInstallLock) {
                try {
                    //删除cached app data
                    mInstaller.freeCache(volumeUuid, bytes, Installer.FLAG_FREE_CACHE_V2);
                } catch (InstallerException ignored) {
                }
            }
            //如果/data目录可用空间大于bytes就直接退出不用再进行释放了
            if (file.getUsableSpace() >= bytes) return;
            Computer computer = snapshotComputer();
            // 5. Consider shared libraries with refcount=0 and age>min cache period
            if (internalVolume && mSharedLibraries.pruneUnusedStaticSharedLibraries(computer, bytes, android.provider.Settings.Global.getLong(mContext.getContentResolver(), Global.UNUSED_STATIC_SHARED_LIB_MIN_CACHE_PERIOD, FREE_STORAGE_UNUSED_STATIC_SHARED_LIB_MIN_CACHE_PERIOD))) {
                return;
            }
            // 6. Consider dexopt output (aggressive only)
            // TODO: Implement
            // 7. Consider installed instant apps unused longer than min cache period
            if (internalVolume) {
                if (mInstantAppRegistry.pruneInstalledInstantApps(computer, bytes, android.provider.Settings.Global.getLong( mContext.getContentResolver(), Global.INSTALLED_INSTANT_APP_MIN_CACHE_PERIOD, InstantAppRegistry.DEFAULT_INSTALLED_INSTANT_APP_MIN_CACHE_PERIOD))) {
                    return;
                }
            }
            // 8. Consider cached app data (below quotas)
            synchronized (mInstallLock) {
                try {
                    mInstaller.freeCache(volumeUuid, bytes, Installer.FLAG_FREE_CACHE_V2 | Installer.FLAG_FREE_CACHE_V2_DEFY_QUOTA);
                } catch (InstallerException ignored) {
                }
            }
            if (file.getUsableSpace() >= bytes) return;
            // 9. Consider DropBox entries
            // TODO: Implement
            // 10. Consider instant meta-data (uninstalled apps) older that min cache period
            if (internalVolume) {
                if (mInstantAppRegistry.pruneUninstalledInstantApps(computer, bytes, android.provider.Settings.Global.getLong( mContext.getContentResolver(), Global.UNINSTALLED_INSTANT_APP_MIN_CACHE_PERIOD, InstantAppRegistry.DEFAULT_UNINSTALLED_INSTANT_APP_MIN_CACHE_PERIOD))) {
                    return;
                }
            }
            // 11. Free storage service cache
            StorageManagerInternal smInternal = mInjector.getLocalService(StorageManagerInternal.class);
            long freeBytesRequired = bytes - file.getUsableSpace();
            if (freeBytesRequired > 0) {
                smInternal.freeCache(volumeUuid, freeBytesRequired);
            }
            // 12. Clear temp install session files
            mInstallerService.freeStageDirs(volumeUuid);
        } else {
            synchronized (mInstallLock) {
                try {
                    //其实通常删除应用缓存的接口就是mInstaller.freeCache
                    mInstaller.freeCache(volumeUuid, bytes, 0);
                } catch (InstallerException ignored) {
                }
            }
        }
        //如果/data目录可用空间大于bytes就直接退出不用再进行释放了
        if (file.getUsableSpace() >= bytes) return;
        //如果做了上述释放应用缓存的动作,还是不满足可用空间大于bytes,就直接抛IOException异常
        throw new IOException("Failed to free " + bytes + " on storage device at " + file);
    }

问题总结:如上日志在data目录可用空间不足,且通过pms进行了缓存的释放,但是还达不到标,此时就会打印这种异常,但是此时系统并还没有挂掉

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐