Android SystemServer 系列专题【篇七:DeviceStorageMonitorService】
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进行了缓存的释放,但是还达不到标,此时就会打印这种异常,但是此时系统并还没有挂掉
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)