Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

dfコマンドはどこからファイルシステムの統計を取得するのか

CPUやメモリの統計は/proc以下のファイルを見れば調べられますが、ファイルシステムの容量などはどうやって取得しているんだろうと気になったでdf(1)のコードを眺めてみました。

ライブラリの動作検証で用意したコードはこちら。

Linuxの場合

Linuxでは、ファイルシステムの情報はstatvfs(3)で取得できるようです。これはstatfs(2)システムコールのラッパーという扱いですが、基本的にはstatvfs(3)を使うように推奨されます。statvfs(3)は以下のように使います。

#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <inttypes.h>
#include <errno.h>
#include <sys/statvfs.h>

char *argv0;

void fstat(char *);
uintmax_t bused(struct statvfs *);
uintmax_t bsize(struct statvfs *, uintmax_t);

int
main(int argc, char **argv)
{
    int i;

    argv0 = argv[0];
    for(i = 1; i < argc; i++)
        fstat(argv[i]);
    return 0;
}

void
fstat(char *path)
{
    struct statvfs fs;

    if(statvfs(path, &fs) < 0){
        fprintf(stderr, "%s: statvfs: %s\n", argv0, strerror(errno));
        exit(1);
    }
    printf("path %s\n", path);
    printf("block size %lu\n", fs.f_bsize);
    printf("fragment size %lu\n", fs.f_frsize);
    printf("fragments in a block %lu\n", fs.f_bsize/fs.f_frsize);

    printf("used blocks %" PRIuMAX "\n", bsize(&fs, bused(&fs)));
    printf("avail blocks %" PRIuMAX "\n", bsize(&fs, fs.f_bavail));
    printf("total blocks %" PRIuMAX "\n", bsize(&fs, fs.f_blocks));
    printf("used inodes %u\n", fs.f_files - fs.f_ffree);
    printf("free inodes %u\n", fs.f_ffree);
    printf("flags");
    if(fs.f_flag&ST_RDONLY)
        printf(" readonly");
    printf("\n");
    printf("namelen %lu\n", fs.f_namemax);
    printf("\n");
}

uintmax_t
bused(struct statvfs *f)
{
    return f->f_blocks - f->f_bfree;
}

uintmax_t
bsize(struct statvfs *f, uintmax_t n)
{
    return n * f->f_frsize / 1024;
}

実行した結果、df(1)の出力と同じ値になっているので正しそうですね。

$ cc fs_linux.c
$ ./a.out /
path /
block size 4096
fragment size 4096
fragments in a block 1
used blocks 15635224
avail blocks 42476028
total blocks 61252420
used inodes 561671
free inodes 3345913
flags
namelen 255

$ df -P /
Filesystem     1024-blocks      Used Available Capacity Mounted on
overlay           61252420  15635224  42476028      27% /

$ df -i /
Filesystem         Inodes   IUsed      IFree IUse% Mounted on
overlay           3907584  561671    3345913   15% /

マウントされたファイルシステムのリストは、getmntent(3)を使うと取得できます。使い方はこんな雰囲気。

FILE *r;
struct mntent *p;

r = setmntent("/etc/mtab", "r");
while(p = getmntent(r))
    printf("%s\n", p->mnt_dir);
endmntent(r);

setmntent(3)の第1引数にファイルパスを渡すようになっていて、上の例でもそうですがこのファイルパスには度々/etc/mtabが使われます。手元の環境では、/etc/mtab/proc/self/mountinfoへのシンボリックリンクとなっているようでした*1getfsent(3)という似た名前の関数もあるけど、こちらは非推奨のようです。

macOSの場合

macOSでは、getmntent(3)が存在しないので、上記のコードがコンパイルできません。また、statvfs(3)は存在しますが、f_ffreeの値がdf(1)の結果と異なっています。

% ./a.out /    
path /
block size 1048576
fragment size 4096
fragments in a block 256
used blocks 22031496
avail blocks 670026888
total blocks 976490576
used inodes 488433
free inodes 586997151     <- この値だけおかしい
flags readonly
namelen 255

% df /         
Filesystem   512-blocks     Used Available Capacity iused      ifree %iused  Mounted on
/dev/disk1s5  976490576 22031496 670024808     4%  488433 4881964447    0%   /

macOSdf(1)ソースコードを読むと、statvfs(3)ではなくstatfs(2)が使われていて、こちらを使えばdf(1)の出力と同じ値を取得できました。ただし、statfs.f_bsizestatvfs.f_frsize相当の値で、statfs.f_iosizestatvfs.f_bsize相当となっていて、f_bsizeの意味が異なっているところは難しいですね。

struct statfs fs;

if(statfs(path, &fs) < 0){
    fprintf(stderr, "%s: statfs: %s\n", argv0, strerror(errno));
    exit(1);
}
printf("mounted on %s\n", fs.f_mntonname);
printf("fstype %s\n", fs.f_fstypename);
printf("block size %u\n", fs.f_iosize);
printf("fragment size %u\n", fs.f_bsize);
printf("fragments in a block %u\n", fs.f_iosize/fs.f_bsize);
...

これでdfと同じ値を読むことができました。

% ./a.out /
mounted on /
fstype apfs
block size 1048576
fragment size 4096
fragments in a block 256
used blocks 22031496
avail blocks 670031712
total blocks 976490576
used inodes 488433
free inodes 4881964447
flags readonly

% df /
Filesystem   512-blocks     Used Available Capacity iused      ifree %iused  Mounted on
/dev/disk1s5  976490576 22031496 670031712     4%  488433 4881964447    0%   /

最後にmacOSでマウントしているファイルシステムの列挙は、getmntinfo(3)で取れます。

struct statfs *f, *p, *e;
int n;

n = getmntinfo(&f, MNT_NOWAIT);
if(n < 0){
    fprintf(stderr, "%s: getmntinfo: %s\n", argv0, strerror(errno));
    return;
}
e = f+n;
for(p = f; p < e; p++){
    printf("mounted on %s\n", p->f_mntonname);
    ...
}
free(f); /* mallocされるのでfreeが必要 */

getmntinfo(3)の代わりに、もっと低レベルなgetfsstat(2)を使っても実現できます。

/* 最初の引数にNULLを渡すとマウントされたファイルシステムの数を調べられる */
n = getfsstat(NULL, 0, MNT_NOWAIT);
if(n < 0){
    fprintf(stderr, "%s: getfsstat: %s\n", argv0, strerror(errno));
    return;
}
f = malloc(sizeof(*f)*n);
if(f == NULL){
    fprintf(stderr, "%s: malloc: %s\n", argv0, strerror(errno));
    return;
}
if(getfsstat(f, sizeof(*f)*n, MNT_NOWAIT) < 0){
    fprintf(stderr, "%s: getfsstat: %s\n", argv0, strerror(errno));
    return;
}

他OSの場合

手元にないので実験していませんが、coreutilsのコードを読む限りは大変そうな気配がありました。

*1:手元とは言ったけど他もだいたい同じかな