Linux调用stat获取文件分区系统信息
2022-09-21 14:56:34 阿炯

本文从系统与程序视角对文件及文件系统的信息获取上进行了分析说明,挂载点与设备,文件系统或设备编号的不同表现方式,关联关系等。

Linux的statvfs与stat的一些释义:
stat, fstat, lstat, fstatat - get file status
statfs, fstatfs - get filesystem statistics
statvfs, fstatvfs - get filesystem statistics

缩写:DOP(DISK-OR-PARTITION)

C语言下可通过以下sysfs函数以编程方式获取:

#include <sys/statvfs.h>

function:
int statvfs(const char *path, struct statvfs *buf);

structure:
struct statvfs {
    unsigned long  f_bsize;    /* file system block size */
    unsigned long  f_frsize;   /* fragment size */
    fsblkcnt_t     f_blocks;   /* size of fs in f_frsize units */
    fsblkcnt_t     f_bfree;    /* # free blocks */
    fsblkcnt_t     f_bavail;   /* # free blocks for unprivileged users */
    fsfilcnt_t     f_files;    /* # inodes */
    fsfilcnt_t     f_ffree;    /* # free inodes */
    fsfilcnt_t     f_favail;   /* # free inodes for unprivileged users */
    unsigned long  f_fsid;     /* file system ID */
    unsigned long  f_flag;     /* mount flags */
    unsigned long  f_namemax;  /* maximum filename length */
};

本文也参考了stat.2-3、inode.7的这些参考手册

https://man7.org/linux/man-pages/man2/stat.2.html

https://man7.org/linux/man-pages/man2/statfs.2.html
https://man7.org/linux/man-pages/man3/statvfs.3.html
https://man7.org/linux/man-pages/man7/inode.7.html
https://man7.org/linux/man-pages/man3/major.3.html
https://man7.org/linux/man-pages/man3/minor.3.html

在Perl中的CPAN上Filesys::Statvfs模块最后更新是在2006年,因此与sys/statvfs.h有一些差异,前者少了fsid这个项;但较新的IO::AIO与C语言中的输出字段是相同的了。

后面选用IO::AIO:
my($bsize, $frsize, $blocks, $bfree, $bavail, $files, $ffree, $favail, $flag, $namemax) = statvfs("/tmp");

aio_statvfs "/", sub {
   my $f = $_[0] or die "statvfs: $!";
   use Data::Dumper;
   say Dumper $f;
};
 
# result:
{
'bfree' => 144674665,
'bavail' => 144674665,
'namemax' => 255,
'blocks' => 487589771,
'fsid' => 2051,
'bsize' => 4096,
'flag' => 4096,
'frsize' => 4096,
'ffree' => 195004966,
'files' => 195131136,
'favail' => 195004966
};

对其它字段的理解没有问题,但对fsid这个做如何的理解呢,它又是如何得来的。

f_fsid字段

Solaris、Irix和POSIX兼容Unix(如Linux)有一个系统调用statvfs(2),它返回一个包含在结构statvfs中定义的无符号长整形f_fsid。Linux、SunOS、HP-UX、4.4BSD有一个系统调用statfs(),它返回一个包含fsid_t f_fsid的statfs结构体(struct)。FreeBSD也是如此,只是它使用了头文件。

一般的想法是,f_fsid包含一些随机内容,因此用(f_fsid,ino)来唯一地确定一个文件,而某些操作系统使用设备号或设备号与文件系统类型相结合的(变体)。一些操作系统限制只向超级用户提供f_fsid字段(对于非特权用户该字段为零),因为NFS导出时,该字段在文件系统的文件句柄中来使用,提供该字段可能也是一个安全问题。

在某些操作系统中,fsid可以用作sysfs(2)系统调用的第二个参数。


struct stat {
    dev_t     st_dev;         /* ID of device containing file */
    ino_t     st_ino;         /* Inode number */
    mode_t    st_mode;        /* File type and mode */
    nlink_t   st_nlink;       /* Number of hard links */
    uid_t     st_uid;         /* User ID of owner */
    gid_t     st_gid;         /* Group ID of owner */
    dev_t     st_rdev;        /* Device ID (if special file) */
    off_t     st_size;        /* Total size, in bytes */
    blksize_t st_blksize;     /* Block size for filesystem I/O */
    blkcnt_t  st_blocks;      /* Number of 512B blocks allocated */

    /* Since Linux 2.6, the kernel supports nanosecond
       precision for the following timestamp fields.
       For the details before Linux 2.6, see NOTES. */

    struct timespec st_atim;  /* Time of last access */
    struct timespec st_mtim;  /* Time of last modification */
    struct timespec st_ctim;  /* Time of last status change */

#define st_atime st_atim.tv_sec      /* Backward compatibility */
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};

st_dev字段描述此文件所在的设备(major(3)和minor(3)中的宏可能有助于分解此字段中的设备ID)。设备的inode位于stat.st_dev,关联了stx_dev_minor和statx.stx_deve_major。每个inode(以及关联的文件)驻留在设备上的文件系统中。该设备(ID)由其主ID(标识设备的常规类)和次ID(标识常规类中的特定实例)的组合标识。

有一点可以在同一文件系统中证明:
statvfs.fsid=stat.st_dev

statvfs.fsid与设备的major:minor有什么关系吗?

fsid在运行时的虚拟目录(/proc,/sys)下没有找到,但major:minor存放于/sys/block/DOP/dev文件中。

major(3)中对Device ID是有说明的。

A device ID consists of two parts: a major ID, identifying the class of the device, and a minor ID, identifying a specific instance of a device in that class. A device ID is represented using the type dev_t.
设备ID由两部分组成:一个主ID标识设备的类别,另一个次ID标识该类别中设备的特定实例。设备ID使用dev_t类型表示。

Given major and minor device IDs, makedev() combines these to produce a device ID, returned as the function result. This device ID can be given to mknod(2), for example.
给定主设备ID和次设备ID,makedev()将它们组合起来生成一个设备ID,作为函数结果返回。可以将此设备ID指定给mknod(2)。

The major() and minor() functions perform the converse task: given a device ID, they return, respectively, the major and minor components. These macros can be useful to, for example,decompose the device IDs in the structure returned by stat(2).
major()和minor()函数执行相反的任务:给定设备ID,它们分别返回主要和次要组件。这些宏可以用于解析stat(2)返回的结构中的设备ID。

Device number vs major/minor number in stat output

Unix/Linux系统中一切旨文件已经是共识,这些文件类型主要分为7类:
Regular file (-)  
Directory (d)
Symbolic Link (l)
Character Device (c)
Block Device (b)
Socket files (s)
Named Pipe (p)

常用的存储设备将会被认为是块设备文件,使用'ls -l'查看时其输出的首字母为'b'为其类型的典型标识。
brw-rw---- 1 root disk      8,   0 8月  15 22:37 sda

计算机中操作系统管理这些设备的主要方式是为其分类,然后就是为其编号,以数字的方式来管理这些信息;从其本身来说计算机善长处理数字,其二是从数字的结构中体现出设备编号的一些细节(这在下文中会有讲到)。

Linux内核是如何按不同类型分配主、次要设备在其官方手册(admin-guide/devices)中有述。

主、次设备号(Major and Minor number)

Linux中的每个设备都表示为一个文件,通常称为设备文件,位于/dev目录下。在Linux终端的/dev目录中执行'ls -l'命令,可以看到系统中所有可用设备的列表。


系统通过打开各自的设备文件访问和使用这些设备。Linux中的每个设备文件都有两个与之关联的唯一数字编号,这两个数字是主要数字(major number)和次要数字(minor number)。

Character and block devices are represented in Linux Kernel as pair of <major number>:<minor number>.

主要编号是指定特定驱动程序的唯一编号。每个设备驱动程序都有其唯一的主要编号,这有助于内核识别驱动程序。当用户空间中的任何应用程序打开任何设备文件时,内核将检查与该设备文件相关的主要编号并确定哪个驱动程序负责处理此请求。之后对该特定设备的任何后续调用都传递给同一驱动程序。

一个设备驱动程序可以处理多个设备或控制器的请求。例如,一个USB驱动程序处理PC中的多个USB端口,或者同一个硬盘驱动程序可以管理多个硬盘。因此,每当对此类设备的任何请求到达内核时,内核必须区分设备或控制器。为了区分它们,linux系统中的每个设备都提供了一个唯一的编号,称为次要设备编号。例如,系统中的COM端口1具有与COM端口2不同的次要数字。可以说次要数字表示具体唯一设备,而主要数字指定驱动程序。但还需要注意的一点是,两个或多个相同类型的设备可以具有相同的主编号,但次要编号肯定不同。

主编号多用于表示大类的设备类型,次编号为其设备下的子设备。

Major Number: Each character and block driver registered with the kernel has a major number. It's used to identify the driver.  Kernel uses major number at open call to forward execution to the appropriate driver.
向内核注册的每个字符和块驱动程序都有一个主数字,用于识别驱动程序。内核在开始调用时使用主号码将操作转发到适当的驱动程序。

Minor Number: A single driver can control various hardware, or support multiple instances of the same hardware, minor number is used to distinguish between the various hardware it controls. Minor number is used by only the driver mentioned by the major number.
单个驱动程序可以控制各种硬件或支持同一硬件的多个实例,次数字用于区分它控制的各种硬件。次编号仅由主编号所提及的驾驶员使用。

从上图中可以看到一个主要编号为4的驱动程序正在处理三个次要编号为3、4和5的设备(device0、device1、device2)。

主要和次要编号的内部表示

Kernel use dev_t type which is defined in </linux/types.h> to hold major and minor number.size of dev_t is 32-bit in which 12 bits are used for major number and 20 bits are used for minor number.
内核使用</linux/types.h>中定义的dev_t类型中定义了主要和次要设备数字。dev_ t的为32位正整数,其中前12位用于主要数字,后20位用于次要数字(这点很重要)。


dev_t is a 32-bit unsigned integer. Where 12 upper most bits are used to store the major number and the remaining lower most 20 bits are used to store the minor number.

注意:不要直接提取主次编号。

Linux kernel also provide below two macros which can be used in our driver to find major number and minor number.
Linux内核还提供了以下两个宏,可以在驱动程序中使用它们来查找设备的主要和次要数字编号。

MAJOR(dev_t dev)
MINOR(dev_t dev)

这两个宏定义在</linux/kdev_t.h>中。


内核虚拟文件亦有此类记录
/proc/devices

上文提及过,设备有除了编号数字外还有自己的名称,下面是来自IBM的总结

Device names, device nodes, and major/minor numbers
Linux内核将字符和块设备表示为编号数对<major>:<minor>。

一些主要编号是为特定设备驱动程序保留的。当Linux启动时,其他主要编号会动态分配给设备驱动程序。例如,主编号94始终是DASD设备的主编号,而通道连接磁带设备的设备驱动程序没有固定的主编号。多个设备驱动程序也可以共享一个主要数字。请参阅/proc/devices以了解如何在运行的Linux实例上分配主要编号。

设备驱动程序使用次要编号<minor>来区分单个物理或逻辑设备。例如,DASD设备驱动程序为每个DASD分配四个次要数字:一个分配给整个DASD,另三个分配给最多三个分区。 设备驱动程序根据特定于设备驱动程序的命名方案为其设备分配设备名称。每个设备名称与一个次要编号关联。

图:次要号码和设备名称


用户态的应用程序通过设备节点(也称为设备文件)访问字符和块设备。创建设备节点时,将会把主编号和次编号进行关联。

图:设备节点


此图显示了一个形式为/dev/<device_name>的设备节点,指向内核中的主:次编号。

RHEL 7.5使用udev来创建设备节点。总是有一个设备节点与内核使用的设备名称相匹配,并且可以通过特殊的udev规则创建其他节点。有关更多详细信息,请参阅udev手册页。

很显然,这些编号(主、次编号组合)在同一主机上是惟一的,部分的编号是系统保留使用用途;块的主编号1分配给RAM磁盘,字符设备的主编号为1分配给一组内核设备,如/dev/zero和/dev/null。有两种方式来分配这些编号:
1.Static
2.Dynamic

主编号230-254、384-511保留用于动态分配,说明页中了明确提及了为LARGE MAJORS跟IpV4的分配规则有点像。

上面讲了部分相关的知识点,接下来结合实际的情况分析说明。

先用lsblk与stat指令查看主次设备编号与

root@yklin:/home/freeoa# lsblk
NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sda      8:0    0   20G  0 disk
├─sda1   8:1    0 19.6G  0 part /
└─sda2   8:2    0  452M  0 part [SWAP]
sdb      8:16   0   64G  0 disk /home

root@yklin:/home/freeoa# stat /home/
  文件:/home/
  大小:124           块:0          IO 块:4096   目录
设备:810h/2064d    Inode:96          硬链接:11
权限:(0755/drwxr-xr-x)  Uid:( 1000/     freeoa)   Gid:(  100/   users)

通过stat的输出,在设备这一项中有:810h/2064d,这里已经暗示了该文件的ID,不过是以两种数字格式予以了表示:
十六进制(hex):810
十进制(decimal):2064

通过Perl中的IO::AIO模块下的函数aio_statvfs来取得挂载点的信息如下:
MountPiont:/home
$VAR1 = {
'namemax' => 255,
'files' => 30021472,
'fsid' => 2064,
'blocks' => 16769024,
'flag' => 4096,
'frsize' => 4096,
'bavail' => 3738964,
'bfree' => 3738964,
'bsize' => 4096,
'ffree' => 29916932,
'favail' => 29916932
};

其输出的fsid就是stat输出列中的设备十进制号,在不同的编码中有不定的注释:
/* file system ID */
/* ID of device containing file */

现在来按上面的描述来计算文件数据单的关系。

32-bit unsigned integer的长度是32位,4个字节的长度;前12位为主设备编号,后20位为次设备编号。

注意:这里的位是二进制位(bit)!一定不能与位置位相混。

上面的lsblk的'/home'所在设备为/dev/sdb的'MAJ:MIN'输出为'8:16',按上面的算法看是否能得到2064的文件系统编号(fsid)。

主编号8的二进制位:00001000
次编号16的二进制位:00010000

现将它们'串行结合'(字串拼接)起来的二进制为:0000100000010000,转为十进制为2064,十六进制为810。

因受限于个人能力水平,上述的'算法'没有能找到其它已有的代码佐证,但经过笔者验证在多台centos与debian上验证通过。用于在从内核虚拟文件系统中取得的主、次编号与statvfs中的文件系统ID(fsid)建立关系。

#Perl下将major与minor的ID转换十进制的fsid并返回
sub majinid{
    my ($major,$minor)=@_;
    my ($rtn);#return no
    warn 'Major and Minor ID Mustbe Give!' unless(defined($major) && defined($minor));
    die 'Major or Minor ID Exceed!' unless((($major>0) && ($major<256)) && (($minor>=0) && ($minor<256)));
    $rtn=sprintf("%08b",$major).sprintf("%08b",$minor);
    return oct("0b$rtn");
}

虽然内核目前已经支持超过256与512之间主设备编号,但这里依然采用了256以下的主设备编号。在《Perl取得文件详细信息》中对stat有比较详细的讲解,不过是针对文件级的stat,不是针对文件系统;从其手册也容易得知,返回的第1个字段代表了文件所在设备的设备编号(fsid)。

/proc/partitions与/proc/diskstats这两个文件的关联中其中major,minor,name。

可见major:minor是这些统计联系的最好纽带,次是name。

这里多说一下交换分区的概念,由于它没有挂载点,但在上述两个文件中的均有其记录,可在/proc/swap中找到Type为partition的行,第一列为其全路径的设备名称,即可将其信息进行关联。但话又说回来,对交换分区的IO监控意义不大,可跳过之。

目前的监控系统中只取的硬盘KiB大小:(/sys/block/DOP/size)/2
/sys/block/DOP/stat中记录了该盘或分区的IO统计情况,同样在/proc/diskstats中也包含了这些信息,只不过多了三个字段:设备的主、次号、(DISK)名称。

/sys/block从linux2.6.26版本开始已经移到了/sys/class/block,代表着系统中当前被发现的所有块设备(物理盘及分区、设备的映射(Device Mapper))。

即可以从/proc/partitions中取的硬盘与分区的大小,从/proc/diskstats中取的本地硬盘与分区的IO统计;不必从/sys/block/DOP/目录下分别进行相关的盘与分区的统计。因此就可以比较容易取得分区的IO状态统计,这在分区位于多个硬盘(LVM)时就很有用了。


Linux(64bits)最多支持挂载多少块存储

假设:
1.以普通的SATA主板为例,其上有足够的的SATA接口;
2.不考虑阵列或扩展卡的情况。

事实上,Linux的类SCSI子系统(包括SATA和USB驱动器)能直接挂载的驱动器数量是有限制的。这是因为设备文件由主/次设备号对进行了标记,并且为SCSI子系统分配的方案具有此隐式的限制。内核(kernel)的devices.txt文档中有一些说明。

主设备号的分配号可能为:8、65到71、128到135,通常会为其分配了16个分块,次设备号限制为256之下(范围为0..255)。每个磁盘都有16个连续的次设备号,其中第一个表示整个磁盘,接下来的15个表示分区。某些情况下的命名方案(/dev/sdbz7)是由用户层而不是Linux内核选择的。在大多数情况下,这些是由udev、eudev或mdev管理的(尽管过去它们是手动创建的)。是由系统管理员设定的设备命名策略。

Oracle VM VirtualBox 6.1手册(第5章 Virtual Storage,p90)中提供了以下类别的虚拟存储及插槽数量限制:
1.4 slots attached to the traditional IDE controller, which are always present. One of these is typically a virtual CD/DVD drive.
2.30 slots attached to the SATA controller, if enabled and supported by the guest OS.
3.15 slots attached to the SCSI controller, if enabled and supported by the guest OS.
4.Up to 255 slots attached to the SAS controller, if enabled and supported by the guest OS.
5.Eight slots attached to the virtual USB controller, if enabled and supported by the guest OS.
6.Up to 255 slots attached to the NVMe controller, if enabled and supported by the guest OS.
7.Up to 256 slots attached to the virtio-scsi controller, if enabled and supported by the guest OS.

鉴于存储控制器的选择如此之多,可能不知道该如何选择。通常应该避免使用IDE,除非它是客户机支持的唯一控制器。无论使用SATA、SCSI还是SAS,都不会产生任何实际的差异。各种控制器仅由Oracle VM VirtualBox提供,以与现有硬件和其他虚拟机监控程序兼容。上面的数字是该控制器接口能直接连接的设备数量,不含使用了扩展卡的情况。

虚拟层的实现不能突破物理层的限制,来看看SATA物理层的限制:
《SATA.Storage.Technology》(ISBN 978-0-9770878-1-5),p261:Device Port Numbers

一个端口复用器最多可以实现15个SATA设备连接。这些设备端口必须从设备端口0开始编号,并按顺序分配,直到最大端口数。因为SATA是点对点实现,所以每个设备接口都执行与HBA端口相关的多数相同功能。例如,每个端口复用器都有SATA特定寄存器,用于指定功能、控制链路以及收集和报告状态和错误。

Linux stat 命令参考

stat 指令的输出有如下的字段:
(占用存储空间)大小与类型、设备类型、INode、用户ID、组ID、文件链接数、访问或修改变更的日期时间。
file size and type, device type, inode number, UID, GID, number of links and access/modification dates of the file.

File : This shows the name of the file.
Size : Size of the file in bytes.
Block : Number of blocks allocated to the file.
IO Block : This is the byte size of every block.
Device :  The device number in hexadecimal or decimal format.
Inode : This is the inode number of the file.
Links : Number of hard links associated with the file.
Access : File permissions either in symbolic or numeric format.
Uid :  User ID & name of the owner.
Gid :  Group ID & name of the owner.
Context : SeLinux security context.
File type : Shows what type the file is (Whether a regular file, symbolic link etc).
Access : Shows the last time the file was accessed.
Modify : Shows the last time the contents of the file were changed.
Change : Shows the last time a file's metadata e.g permissions & ownership was changed.

使用'-f'可以查看挂载点的文件系统类型信息
stat -f /home/
文件:"/home/"
ID:81000000000 文件名长度:255:类型:xfs
块大小:4096:  基本块大小:4096
块:总计:16769024   空闲:5202590    可用:5202590
Inodes: 总计:33554432   空闲:33439862

可以取的部分文件系统的块大小、文件系统类型等信息,块和INode大小。

可以通过传入不同的参数来格式化其输出

stat -t freeoa.txt
freeoa.pl 8030 16 81a0 1000 1000 810 101830236 1 0 0 1669452017 1669452002 1669452002 1669452002 4096

使用'-L'参数对链接文件的处理(symbolic links)

使用'--printf'或'--format'来对输出进行格式化,格式化输出的选项

%a:Displays the access rights in octal format.
%A:Displays the access rights in a human readable format.
%b:This is the number of blocks allocated (see %B).
%B:the size in bytes of each block reported by %b.
%C:Shows the SELinux security context string.
%d:Displays the device number in a decimal format.
%D:The device number in hexadecimal format.
%f:Displays the raw mode in hexadecimal.
%F:Displays the file type.
%g:Prints the group ID of owner.
%G:Prints the group name of owner.
%h:Displays the number of hard links.
%i:Prints out the inode number.
%m:Prints the mount point.
%n:Displays the file name of the file.
%N:Shows quoted file name with dereference if symbolic link.
%o:Prints the optimal I/O transfer size hint.
%s:total size, in bytes.
%t:major device type in hex, for character/block device special files.
%T:minor device type in hex, for character/block device special files.
%u:Shows the user ID of owner.
%U:Prints the username of owner.
%w:Reveals the time of file birth, human-readable; – if unknown.
%W:Prints the time of file birth, seconds since Epoch; 0 if unknown.
%x:The time of last access, human-readable.
%X:The time of last access, seconds since Epoch.
%y:Displays the last time of last modification, human-readable.
%Y:Prints the time of last modification, seconds since Epoch.
%z:This is the time of last change, human-readable.
%Z:The time of last change, seconds since Epoch.

输出文件的inode编号
stat --printf='%i\n' freeoa.net.pl
101830236

权限与用户id
stat --printf='%a:%u\n' freeoa.net.pl
640:1000

stat --format='%a:%F' freeoa.net.pl
640:普通文件


本文的编写参考了诸多互联网资料,感谢各位原作者。