Androidでは、基本的にシステムもアプリも永続的データは/data
パーティションに書き込む。
そのため、システムの最低限の動作が継続できるように、アプリがデータを書きすぎないための仕組みがいくつか入っている。
本記事では、それらの仕組みについて紹介する。
なお、ソースコードは下記のバージョンを参照している。
- Android: android-10.0.0_r5
- Linux kernel: 5.3.7
- e2fsprogs: 1.45.4
1. ストレージの空き容量を確認するAPI
File#getTotalSpace()
, File#getFreeSpace()
, File#getUsableSpace()
いずれも最終的にはJNIでstatvfs(3)
を、すなわちstatfs(2)
を呼び出す。
File#getTotalSpace()
はstatfs.f_blocks * statfs.f_bsize
に相当し、パーティションの全サイズを返す。
df /data
したときの1K-blocks
と一致する。
File#getFreeSpace()
はstatfs.f_bfree * statfs.f_bsize
に相当し、実際の空き領域を返す。
rootユーザでdf /data
したときのAvailable
と一致する。
File#getUsableSpace()
はstatfs.f_bavail * statfs.f_bsize
に相当し、非特権ユーザが使用可能な空き領域を返す。
一般ユーザでdf /data
したときのAvailable
と一致する。
public long getTotalSpace() {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("getFileSystemAttributes"));
sm.checkRead(path);
}
if (isInvalid()) {
return 0L;
}
return fs.getSpace(this, FileSystem.SPACE_TOTAL);
}
...
public long getFreeSpace() {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("getFileSystemAttributes"));
sm.checkRead(path);
}
if (isInvalid()) {
return 0L;
}
return fs.getSpace(this, FileSystem.SPACE_FREE);
}
...
public long getUsableSpace() {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("getFileSystemAttributes"));
sm.checkRead(path);
}
if (isInvalid()) {
return 0L;
}
return fs.getSpace(this, FileSystem.SPACE_USABLE);
}
public long getSpace(File f, int t) {
BlockGuard.getThreadPolicy().onReadFromDisk();
BlockGuard.getVmPolicy().onPathAccess(f.getPath());
return getSpace0(f, t);
}
#define statvfs64 statvfs
...
JNIEXPORT jlong JNICALL
Java_java_io_UnixFileSystem_getSpace0(JNIEnv *env, jobject this,
jobject file, jint t)
{
jlong rv = 0L;
WITH_FIELD_PLATFORM_STRING(env, file, ids.path, path) {
struct statvfs64 fsstat;
memset(&fsstat, 0, sizeof(fsstat));
if (statvfs64(path, &fsstat) == 0) {
switch(t) {
case java_io_FileSystem_SPACE_TOTAL:
rv = jlong_mul(long_to_jlong(fsstat.f_frsize),
long_to_jlong(fsstat.f_blocks));
break;
case java_io_FileSystem_SPACE_FREE:
rv = jlong_mul(long_to_jlong(fsstat.f_frsize),
long_to_jlong(fsstat.f_bfree));
break;
case java_io_FileSystem_SPACE_USABLE:
rv = jlong_mul(long_to_jlong(fsstat.f_frsize),
long_to_jlong(fsstat.f_bavail));
break;
default:
assert(0);
}
}
} END_PLATFORM_STRING(env, path);
return rv;
}
statvfs(3)
は、Androidの独自libcであるbionicで実装されており、statfs(2)
を呼び出す。
int statvfs(const char* path, struct statvfs* result) {
struct statfs tmp;
int rc = statfs(path, &tmp);
if (rc != 0) {
return rc;
}
__statfs_to_statvfs(tmp, result);
return 0;
}
__strong_alias(statvfs64, statvfs);
man statfs(2)
によると、f_bfree
はすべてのfree block、f_bavail
は非特権ユーザにとってのfree block。
f_bfree
とf_bavail
の差分の詳細については後述する。
fsblkcnt_t f_bfree; /* Free blocks in filesystem */
fsblkcnt_t f_bavail; /* Free blocks available to
unprivileged user */
StorageManager#getAllocatableBytes()
Android 8.0から導入された、アプリが確保可能な最大サイズを返すAPI。
大きなファイルを書き込む場合、事前に空き領域を確認するならこのAPIがもっとも適切な値を返す。
計算式は(File#getUsableSpace() + cacheClearable) - StorageManager#getStorageLowBytes()
。
File#getUsableSpace()
に削除可能なキャッシュのサイズを加え、システムとして残して置かなければならないサイズまでの余裕を求めている。
削除可能なキャッシュのサイズは、StorageStatsManager#getCacheBytes() - StorageManager#getStorageCacheBytes
で求めている。
注意点として、SystemServiceへのBinder呼び出しが発生するので、完了までに数秒かかる可能性がある。
そのためUIスレッドから呼び出すべきではなく、30秒に1回以上呼び出すべきではない。
public long getAllocatableBytes(String volumeUuid, int flags, String callingPackage) {
flags = adjustAllocateFlags(flags, Binder.getCallingUid(), callingPackage);
final StorageManager storage = mContext.getSystemService(StorageManager.class);
final StorageStatsManager stats = mContext.getSystemService(StorageStatsManager.class);
final long token = Binder.clearCallingIdentity();
try {
// In general, apps can allocate as much space as they want, except
// we never let them eat into either the minimum cache space or into
// the low disk warning space. To avoid user confusion, this logic
// should be kept in sync with getFreeBytes().
final File path = storage.findPathForUuid(volumeUuid);
final long usable = path.getUsableSpace();
final long lowReserved = storage.getStorageLowBytes(path);
final long fullReserved = storage.getStorageFullBytes(path);
if (stats.isQuotaSupported(volumeUuid)) {
final long cacheTotal = stats.getCacheBytes(volumeUuid);
final long cacheReserved = storage.getStorageCacheBytes(path, flags);
final long cacheClearable = Math.max(0, cacheTotal - cacheReserved);
if ((flags & StorageManager.FLAG_ALLOCATE_AGGRESSIVE) != 0) {
return Math.max(0, (usable + cacheClearable) - fullReserved);
} else {
return Math.max(0, (usable + cacheClearable) - lowReserved);
}
} else {
// When we don't have fast quota information, we ignore cached
// data and only consider unused bytes.
if ((flags & StorageManager.FLAG_ALLOCATE_AGGRESSIVE) != 0) {
return Math.max(0, usable - fullReserved);
} else {
return Math.max(0, usable - lowReserved);
}
}
} catch (IOException e) {
throw new ParcelableException(e);
} finally {
Binder.restoreCallingIdentity(token);
}
}
StorageManager#getAllocatableBytes()
を呼び出した結果、必要なサイズの空きがない場合は、
Intent.ACTION_MANAGE_STORAGE
をEXTRA_UUID
とEXTRA_REQUESTED_BYTES
を付与して投げればユーザにディスクの解放を促すことができる。
下記のようにDeletionHelperActivity
が立ち上がる。
File#getFreeSpace()
とFile#getUsableSpace
の差分はなに?
File#getFreeSpace()
とFile#getUsableSpace
の差分は、
ext4ファイルシステムの場合、特権ユーザしか確保できない領域のサイズと、extent用の予約領域からなる。
下記はext4のstatfs(2)
の実装である。
static int ext4_statfs(struct dentry *dentry, struct kstatfs *buf)
{
struct super_block *sb = dentry->d_sb;
struct ext4_sb_info *sbi = EXT4_SB(sb);
struct ext4_super_block *es = sbi->s_es;
ext4_fsblk_t overhead = 0, resv_blocks;
u64 fsid;
s64 bfree;
resv_blocks = EXT4_C2B(sbi, atomic64_read(&sbi->s_resv_clusters));
if (!test_opt(sb, MINIX_DF))
overhead = sbi->s_overhead;
buf->f_type = EXT4_SUPER_MAGIC;
buf->f_bsize = sb->s_blocksize;
buf->f_blocks = ext4_blocks_count(es) - EXT4_C2B(sbi, overhead);
bfree = percpu_counter_sum_positive(&sbi->s_freeclusters_counter) -
percpu_counter_sum_positive(&sbi->s_dirtyclusters_counter);
/* prevent underflow in case that few free space is available */
buf->f_bfree = EXT4_C2B(sbi, max_t(s64, bfree, 0));
buf->f_bavail = buf->f_bfree -
(ext4_r_blocks_count(es) + resv_blocks);
if (buf->f_bfree < (ext4_r_blocks_count(es) + resv_blocks))
buf->f_bavail = 0;
buf->f_files = le32_to_cpu(es->s_inodes_count);
buf->f_ffree = percpu_counter_sum_positive(&sbi->s_freeinodes_counter);
buf->f_namelen = EXT4_NAME_LEN;
fsid = le64_to_cpup((void *)es->s_uuid) ^
le64_to_cpup((void *)es->s_uuid + sizeof(u64));
buf->f_fsid.val[0] = fsid & 0xFFFFFFFFUL;
buf->f_fsid.val[1] = (fsid >> 32) & 0xFFFFFFFFUL;
#ifdef CONFIG_QUOTA
if (ext4_test_inode_flag(dentry->d_inode, EXT4_INODE_PROJINHERIT) &&
sb_has_quota_limits_enabled(sb, PRJQUOTA))
ext4_statfs_project(sb, EXT4_I(dentry->d_inode)->i_projid, buf);
#endif
return 0;
}
特権ユーザしか確保できない領域のブロック数は、スーパーブロックのs_r_blocks_count_lo
に記録されている。
ext4_r_blocks_count(es)
のサイズはtune2fs -l
で確認できる。
"Reserved block count" * "Block size"
が特権ユーザ用に予約されているサイズとなる。
$ adb shell 'tune2fs -l $(mount | grep " on /data " | cut -d" " -f1)' | grep -e "Reserved block count:" -e "Block size:"
Reserved block count: 4096
Block size: 4096
extent用の予約領域は、全体の2%か4096クラスタの小さい方のサイズが割り当てられる。
ちなみに、extentはext4で導入されたファイルのデータ位置の管理方式の一つで、連続領域をまとめて表現することで効率を上げている。
なぜextent方式を使うときだけ予備領域が必要になるかはよくわからなかった。
(おわかりの方がいらっしゃいましたら、ぜひ教えてください)
実際の設定値は/sys/fs/ext4/{block_device_name}/reserved_clusters
からクラスタ数が確認できる。
クラスタのサイズは、tune2fs -l
のFragment size
で確認できる。
static void ext4_set_resv_clusters(struct super_block *sb)
{
ext4_fsblk_t resv_clusters;
struct ext4_sb_info *sbi = EXT4_SB(sb);
/*
* There's no need to reserve anything when we aren't using extents.
* The space estimates are exact, there are no unwritten extents,
* hole punching doesn't need new metadata... This is needed especially
* to keep ext2/3 backward compatibility.
*/
if (!ext4_has_feature_extents(sb))
return;
/*
* By default we reserve 2% or 4096 clusters, whichever is smaller.
* This should cover the situations where we can not afford to run
* out of space like for example punch hole, or converting
* unwritten extents in delalloc path. In most cases such
* allocation would require 1, or 2 blocks, higher numbers are
* very rare.
*/
resv_clusters = (ext4_blocks_count(sbi->s_es) >>
sbi->s_cluster_bits);
do_div(resv_clusters, 50);
resv_clusters = min_t(ext4_fsblk_t, resv_clusters, 4096);
atomic64_set(&sbi->s_resv_clusters, resv_clusters);
}
$ adb shell 'cat /sys/fs/ext4/$(basename $(mount | grep " on /data " | cut -d" " -f1))/reserved_clusters'
4096
$ adb shell 'tune2fs -l $(mount | grep " on /data " | cut -d" " -f1)' | grep -e "Fragment size:"
Fragment size: 4096
void list_super2(struct ext2_super_block * sb, FILE *f)
{
...
if (ext2fs_has_feature_bigalloc(sb))
fprintf(f, "Cluster size: %u\n",
EXT2_CLUSTER_SIZE(sb));
else
fprintf(f, "Fragment size: %u\n",
EXT2_CLUSTER_SIZE(sb));
...
2. ストレージの空き容量をモニターするSystemService DeviceStorageMonitorService
DEFAULT_CHECK_INTERVAL
(1分)ごとに、File#getUsableSpace()
を確認し、
下記3段階の閾値と比較して下回ったときに処理を実行する。
-
StorageManager#getStorageLowBytes() * 1.5
-
PackageManagerService#freeStorage()
を呼び出しキャッシュを解放する。
-
-
StorageManager#getStorageLowBytes()
- Notification Centerに容量不足を通知する。この通知をtapすると、
DeletionHelperActivity
に遷移する。 -
Intent.ACTION_DEVICE_STORAGE_LOW
をBroadcastする。
- Notification Centerに容量不足を通知する。この通知をtapすると、
-
StorageManager#getStorageLowBytes()
-
Intent.ACTION_DEVICE_STORAGE_FULL
をBroadcastする。
-
なお、dumpsys devicestoragemonitor force-low -f
で、擬似的にLEVEL_LOW
にすることができる。
また、dumpsys devicestoragemonitor reset -f
で、元に戻すことができる。
(force-full
も欲しかった。。。)
@WorkerThread
private void check() {
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()) {
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.
if (file.getUsableSpace() < (lowBytes * 3) / 2) {
final PackageManagerService pms = (PackageManagerService) ServiceManager
.getService("package");
try {
pms.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) && !isBootImageOnDisk()
&& 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)) {
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CHECK),
DEFAULT_CHECK_INTERVAL);
}
}
ストレージがLEVEL_LOW
になったときに表示される通知は以下のとおり。
StorageManager.getStorageLowBytes()
AndroidのストレージがLEVEL_LOW
だとする閾値を求めるAPI。
デフォルトでは、ストレージサイズの5%か、500MiBの小さい方になる。
閾値となる割合もサイズも、Settingsで変更可能。
割合はsys_storage_threshold_percentage
、サイズはsys_storage_threshold_max_bytes
。
private static final int DEFAULT_THRESHOLD_PERCENTAGE = 5;
private static final long DEFAULT_THRESHOLD_MAX_BYTES = DataUnit.MEBIBYTES.toBytes(500);
...
@UnsupportedAppUsage
public long getStorageLowBytes(File path) {
final long lowPercent = Settings.Global.getInt(mResolver,
Settings.Global.SYS_STORAGE_THRESHOLD_PERCENTAGE, DEFAULT_THRESHOLD_PERCENTAGE);
final long lowBytes = (path.getTotalSpace() * lowPercent) / 100;
final long maxLowBytes = Settings.Global.getLong(mResolver,
Settings.Global.SYS_STORAGE_THRESHOLD_MAX_BYTES, DEFAULT_THRESHOLD_MAX_BYTES);
return Math.min(lowBytes, maxLowBytes);
}
StorageManager.getStorageFullBytes
AndroidのストレージがLEVEL_FULL
だとする閾値を求めるAPI。
デフォルトでは、1MiBになる。
Settings sys_storage_full_threshold_bytes
で変更可能。
private static final long DEFAULT_FULL_THRESHOLD_BYTES = DataUnit.MEBIBYTES.toBytes(1);
...
@UnsupportedAppUsage
public long getStorageFullBytes(File path) {
return Settings.Global.getLong(mResolver, Settings.Global.SYS_STORAGE_FULL_THRESHOLD_BYTES,
DEFAULT_FULL_THRESHOLD_BYTES);
}
3. アプリケーションが暴走して書き過ぎてしまうのを防止する
Android8.1まではquotactl(2)
で上限が設定されていた。
installd
によってアプリのインストール時に設定され、アプリによらず全てストレージサイズの9割になっていた。
quota
はuidごとに設定されるので、各アプリの使用サイズがストレージサイズの9割を超えたら、EDQUOT
で書き込みが失敗するようになる。
ファイルシステムレベルでの書き込み禁止なので漏れの可能性が少なく、悪さをしたアプリ以外は正常動作を継続することができる。
ただしこの方式では、例えば2つアプリが5割ずつ書き込んだ場合はエラーにならず、
システムの動作に影響を及ぼしてしまう可能性があった。
static int prepare_app_quota(const std::unique_ptr<std::string>& uuid, const std::string& device,
uid_t uid) {
if (device.empty()) return 0;
struct dqblk dq;
if (quotactl(QCMD(Q_GETQUOTA, USRQUOTA), device.c_str(), uid,
reinterpret_cast<char*>(&dq)) != 0) {
PLOG(WARNING) << "Failed to find quota for " << uid;
return -1;
}
#if APPLY_HARD_QUOTAS
if ((dq.dqb_bhardlimit == 0) || (dq.dqb_ihardlimit == 0)) {
auto path = create_data_path(uuid ? uuid->c_str() : nullptr);
struct statvfs stat;
if (statvfs(path.c_str(), &stat) != 0) {
PLOG(WARNING) << "Failed to statvfs " << path;
return -1;
}
dq.dqb_valid = QIF_LIMITS;
dq.dqb_bhardlimit =
(((static_cast<uint64_t>(stat.f_blocks) * stat.f_frsize) / 10) * 9) / QIF_DQBLKSIZE;
dq.dqb_ihardlimit = (stat.f_files / 2);
if (quotactl(QCMD(Q_SETQUOTA, USRQUOTA), device.c_str(), uid,
reinterpret_cast<char*>(&dq)) != 0) {
PLOG(WARNING) << "Failed to set hard quota for " << uid;
return -1;
} else {
LOG(DEBUG) << "Applied hard quotas for " << uid;
return 0;
}
} else {
// Hard quota already set; assume it's reasonable
return 0;
}
#else
// Hard quotas disabled
return 0;
#endif
}
そこで、Android9以降では、FileSystemのReserved blockを活用するようになっている。
reserved_disk
グループを用意し、特別にReserved blockへの書き込みを許可することで、
たとえアプリにディスクが書き潰されたとしても、最低限のシステムが動作できるようにしている。
$ adb shell 'tune2fs -l $(mount | grep " on /data " | cut -d" " -f1)' | grep "Reserved blocks"
Reserved blocks uid: 0 (user root)
Reserved blocks gid: 1065 (group reserved_disk)
例えば、zygote
やvold
はinitスクリプトでreserved_disk
グループに追加されている。
といってもこれらはrootユーザで動作しているので、もともとReserved blockへ書き込み可能だが。
おそらく今後のメンテナンスのことを考えてグループに追加しているのだろう。
root以外のユーザでreserved_disk
グループに属しているユーザは今回の調査では見つけられなかった。
service vold /system/bin/vold \
--blkid_context=u:r:blkid:s0 --blkid_untrusted_context=u:r:blkid_untrusted:s0 \
--fsck_context=u:r:fsck:s0 --fsck_untrusted_context=u:r:fsck_untrusted:s0
class core
ioprio be 2
writepid /dev/cpuset/foreground/tasks
shutdown critical
group root reserved_disk
なお、Reserved blockの数は、tune2fs -m
or tune2fs -r
で設定できる。
試したエミュレータの環境では、/dataパーティションの5%がReserved blockとなっていた。
4. 十分な空き領域があるストレージにしかアプリのインストールをしないPackageHelper#resolveInstallVolume
PackageHelper#resolveInstallVolume()
でインストール先volumeを探すときに、
StorageManager#getAllocatableBytes()
とparams.sizeBytes
を比較する。
private int createSessionInternal(SessionParams params, String installerPackageName, int userId)
public static String resolveInstallVolume(Context context, SessionParams params,
TestableInterface testInterface) throws IOException {
final StorageManager storageManager = testInterface.getStorageManager(context);
final boolean forceAllowOnExternal = testInterface.getForceAllowOnExternalSetting(context);
final boolean allow3rdPartyOnInternal =
testInterface.getAllow3rdPartyOnInternalConfig(context);
// TODO: handle existing apps installed in ASEC; currently assumes
// they'll end up back on internal storage
ApplicationInfo existingInfo = testInterface.getExistingAppInfo(context,
params.appPackageName);
// Figure out best candidate volume, and also if we fit on internal
final ArraySet<String> allCandidates = new ArraySet<>();
boolean fitsOnInternal = false;
VolumeInfo bestCandidate = null;
long bestCandidateAvailBytes = Long.MIN_VALUE;
for (VolumeInfo vol : storageManager.getVolumes()) {
if (vol.type == VolumeInfo.TYPE_PRIVATE && vol.isMountedWritable()) {
final boolean isInternalStorage = ID_PRIVATE_INTERNAL.equals(vol.id);
final UUID target = storageManager.getUuidForPath(new File(vol.path));
final long availBytes = storageManager.getAllocatableBytes(target,
translateAllocateFlags(params.installFlags));
if (isInternalStorage) {
fitsOnInternal = (params.sizeBytes <= availBytes);
}
if (!isInternalStorage || allow3rdPartyOnInternal) {
if (availBytes >= params.sizeBytes) {
allCandidates.add(vol.fsUuid);
}
if (availBytes >= bestCandidateAvailBytes) {
bestCandidate = vol;
bestCandidateAvailBytes = availBytes;
}
}
}
}
(おまけ) ストレージへの書き込み負荷試験を行うアプリStorageEater
今回の振る舞いを実験するために、各APIでストレージの状況を表示しつつ、FileOutputStream
でひたすらデータを書く負荷アプリを作った。
takeoverjp/StorageEater: Android storage stress test application.
注意
ストレージフルを発生させるため、端末がまともに動かなくなる可能性があります。
復旧手段がない or 消えたら困るデータがストレージに保存されている場合は、動かさないでください。
まとめ
永続データを保存するのは、ほぼすべてのアプリが必要とする機能だからか、特別なPermissionは必要ない。
だからといって適当に使うと、特にレガシーなAndroidでは取り返しのつかない不具合を引き起こす可能性がある。
アプリを開発する場合は外部ストレージやCacheなど他の選択肢を考慮した上で、どうしても内部ストレージでないといけないときのみきちんとStorageManager#getAllocatableBytes()
を確認した上で保存しなければならない。
一方で、プラットフォームを開発する場合は、無遠慮に書き込んでくるアプリのことも踏まえ、最低限のシステムの動作(せめてアプリのデータ削除までたどり着けること)を保証しなければならない。
参考
- File#getFreeSpace | Android Developers
- File#getUsableSpace | Android Developers
- File#getTotalSpace | Android Developers
- StorageManager | Android Developers
- tune2fs(8) - Linux manual page
- statvfs(3) - Linux manual page
- statfs(2) - Linux manual page
- quotactl(2) - Linux manual page
- Ext4
- E2fsprogs: Ext2 Filesystem Utilities