3.文件的属性信息 - wolai 笔记
众所周知,Linux是一个基于文件的操作系统,因此作为文件本身也就有很多属性,如果想要查看某一个文件的属性有两种方式:命令函数。虽然有两种方式但是它们对应的名字是相同的,叫做stat。另外使用file命令也可以查看文件的一些属性信息。

1.file命令

该命令用来识别文件类型,也可用来辨别一些文件的编码格式。它是通过查看文件的头部信息来获取文件类型,而不是像Windows通过扩展名来确定文件类型的。
命令语法如下:
# 参数在命令中的位置没有限制
$ file 文件名 [参数] 
file 命令的参数是可选项, 可以不加, 常用的参数如下表:
参数功能
-b只显示文件类型和文件编码, 不显示文件名
-i显示文件的 MIME 类型
-F设置输出字符串的分隔符
-L查看软连接文件自身文件属性

1.1 查看文件类型和编码格式

使用不带任何选项的 file 命令,即可查看指定文件的类型和文件编码信息。
# 空文件
$ file 11.txt 
11.txt: empty

# 源文件, 编码格式为: ASCII
$ file b.cpp
b.cpp: C source, ASCII text

# 源文件, 编码格式为: UTF-8 
robin@OS:~$ file test.cpp 
test.cpp: C source, UTF-8 Unicode (with BOM) text, with CRLF line terminators

# 可执行程序, Linux中的可执行程序为 ELF 格式
robin@OS:~$ file a.out 
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=5317ae9fba592bf583c4f680d8cc48a8b58c96a5, not stripped

1.2 只显示文件格式以及编码

使用-b参数,可以使 file 命令的输出不出现文件名,只显示文件格式以及编码。
# 空文件
$ file 11.txt -b
empty

# 源文件, 编码格式为: ASCII
$ file b.cpp -b
C source, ASCII text

# 源文件, 编码格式为: UTF-8 
robin@OS:~$ file test.cpp  -b
C source, UTF-8 Unicode (with BOM) text, with CRLF line terminators

# 可执行程序, Linux中的可执行程序为 ELF 格式
robin@OS:~$ file a.out  -b
ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=5317ae9fba592bf583c4f680d8cc48a8b58c96a5, not stripped

1.3 显示文件的 MIME 类型

file命令添加-i参数,可以输出文件对应的 MIME 类型的字符串。
MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型。是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式。
# charset 为该文件的字符编码

# 源文件, MIME类型: text/x-c, 字符编码: utf-8
$ file occi.cpp -i
occi.cpp: text/x-c; charset=utf-8

# 压缩文件, MIME类型: application/gzip, 字符编码: binary
$ file fcgi.tar.gz -i
fcgi.tar.gz: application/gzip; charset=binary

# 文本文件, MIME类型: text/plain, 字符编码: utf-8
$ file english.txt -i
english.txt: text/plain; charset=utf-8

# html文件, MIME类型: text/html, 字符编码: us-ascii
$ file demo.html -i
demo.html: text/html; charset=us-ascii

1.4 设置输出分隔符

file 命令中,文件名和后边的属性信息默认使用冒号(:)分隔,我们可以通过 -F 参数修改分隔符,分隔符可以是单字符也可以是一个字符串,如果分隔符是字符串需要将这个参数值写到引号中(单/双引号都可以)。
# 默认格式输出
$ file english.txt 
english.txt: UTF-8 Unicode text, with very long lines, with CRLF line terminators

# 修改分隔符为字符串 “==>"
$ file english.txt -F "==>"
english.txt==> UTF-8 Unicode text, with very long lines, with CRLF line terminators

$ file english.txt -F '==>'
english.txt==> UTF-8 Unicode text, with very long lines, with CRLF line terminators

# 修改分隔符为单字符 '='
$ file english.txt -F = 
english.txt= UTF-8 Unicode text, with very long lines, with CRLF line terminators

1.5 查看软连接文件

软连接文件是一个特殊格式的文件, 查看这种格式的文件可以得到两种结果: 第一种是软连接文件本身的属性信息, 另一种是链接文件指向的那个文件的属性信息。直接通过 file 查看文件属性得到的是链接文件指向的文件的信息,如果添加参数 -L得到的链接文件自身的属性信息。
# 使用 ls 查看链接文件属性信息
$ ll link.lnk 
lrwxrwxrwx 1 root root 24 Jan 25 17:27 link.lnk -> /root/luffy/onepiece.txt

# 使用file直接查看链接文件信息: 得到的是链接文件指向的那个文件的名字
$ file link.lnk 
link.lnk: symbolic link to `/root/luffy/onepiece.txt'

# 使用 file 查看链接文件自身属性信息, 添加参数 -L
$ file link.lnk -L
link.lnk: UTF-8 Unicode text

2.stat命令

stat命令显示文件或目录的详细属性信息包括文件系统状态,比ls命令输出的信息更详细。语法格式如下:
# 参数在命令中的位置没有限制
$ stat [参数] 文件或者目录名
关于这个命令的可选参数如下表:
参数功能
-f不显示文件本身的信息,显示文件所在文件系统的信息
-L查看软链接文件关联的文件的属性信息
-c查看文件某个单个的属性信息
-t简洁模式,只显示摘要信息, 不显示属性描述

2.1 显示所有属性

$ stat english.txt 
  File: 'english.txt'
  Size: 129567          Blocks: 256        IO Block: 4096   regular file
Device: 801h/2049d      Inode: 526273      Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1001/   robin)   Gid: ( 1001/   robin)
Access: 2021-01-31 00:00:36.791490304 +0800
Modify: 2021-01-31 00:00:36.791490304 +0800
Change: 2021-01-31 00:00:36.791490304 +0800
 Birth: -
在输出的信息中我们可以看到很多属性,
  • File: 文件名
  • Size: 文件大小, 单位是字节
  • Blocks: 文件使用的数据块总数
  • IO Block:IO块大小
  • regular file:文件的实际类型,文件类型不同,该关键字也会变化
  • Device:设备编号
  • Inode:Inode号,操作系统用inode编号来识别不同的文件,找到文件数据所在的block,读出数据。
  • Links:硬链接计数
  • Access:文件所有者+所属组用户+其他人对文件的访问权限
  • Uid: 文件所有者名字和所有者ID
  • Gid:文件所有数组名字已经组ID
  • Access Time:表示文件的访问时间。当文件内容被访问时,这个时间被更新
  • Modify Time:表示文件内容的修改时间,当文件的数据内容被修改时,这个时间被更新
  • Change Time:表示文件的状态时间,当文件的状态被修改时,这个时间被更新,例如:文件的硬链接链接数,大小,权限,Blocks数等。
  • Birth: 文件生成的日期

2.2 只显示系统信息

stat 命令添加 -f参数将只显示文件在文件系统中的相关属性信息, 文件自身属性不显示
$ stat luffy/ -f
  File: "luffy/"
    ID: 47d795d8889d00d3 Namelen: 255     Type: ext2/ext3
Block size: 4096       Fundamental block size: 4096
Blocks: Total: 10288179   Free: 8991208    Available: 8546752
Inodes: Total: 2621440    Free: 2515927

2.3 软连接文件

使用 stat 查看软链接类型的文件, 默认显示的是这个软链接文件的属性信息, 添加参数 -L就可以查看这个软连接文件关联的文件的属性信息了。
# 查看软件文件属性 -> 使用 ls -l
ls -l link.lnk 
lrwxrwxrwx 1 root root 24 Jan 25 17:27 link.lnk -> /root/luffy/onepiece.txt

# 使用 stat 查看软连接文件属性信息
$ stat link.lnk 
  File: ‘link.lnk’ -> ‘/root/luffy/onepiece.txt’
  Size: 24              Blocks: 0          IO Block: 4096   symbolic link
Device: fd01h/64769d    Inode: 393832      Links: 1
Access: (0777/lrwxrwxrwx)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2021-01-30 23:46:29.922760178 +0800
Modify: 2021-01-25 17:27:12.057386837 +0800
Change: 2021-01-25 17:27:12.057386837 +0800
 Birth: -

# 使用 stat 查看软连接文件关联的文件的属性信息
$ stat link.lnk -L
  File: ‘link.lnk’
  Size: 3700              Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d    Inode: 660353      Links: 2
Access: (0444/-r--r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2021-01-30 23:46:53.696723182 +0800
Modify: 2021-01-25 17:54:47.000000000 +0800
Change: 2021-01-26 11:57:00.587658977 +0800
 Birth: -

2.4 简洁输出

使用 stat 进行简洁信息输出的可读性不是太好, 所有的属性描述都别忽略了, 如果只想得到属性值, 可以给该命令添加-t参数:
$ stat luffy/ -t
luffy/ 4096 8 41fd 1001 1001 801 662325 8 0 0 1611659086 1580893020 1580893020 0 4096

2.5 单个属性输出

如果每次只想通过 stat 命令得到某一个文件属性, 可以给名添加 -c参数。 不同的文件属性分别对应一些定义好的特殊符号,想得到哪个属性值,将其指定到参数 -c后边即可。属性对应的字符如下表:
格式化字符功能
%a文件的八进制访问权限(#0是输出标准)
%A人类可读形式的文件访问权限(rwx
%b已分配的块数量
%B报告的每个块的大小(以字节为单位)
%CSELinux 安全上下文字符串
%d设备编号 (十进制)
%D设备编号 (十六进制)
%F文件类型
%g文件所属组组ID
%G文件所属组名字
%h用连接计数
%iinode编号
%m挂载点
%n文件名
%N用引号括起来的文件名,并且会显示软连接文件引用的文件路径
%o最佳I/O传输大小提示
%s文件总大小, 单位为字节
%t十六进制的主要设备类型,用于字符/块设备特殊文件
%T十六进制的次要设备类型,用于字符/块设备特殊文件
%u文件所有者ID
%U文件所有者名字
%w文件生成的日期 ,人类可识别的时间字符串 – 获取不到信息不显示
%W文件生成的日期 ,自纪元以来的秒数 (参考 %X )– 获取不到信息不显示
%x最后访问文件的时间, 人类可识别的时间字符串
%X最后访问文件的时间, 自纪元以来的秒数(从1970.1.1开始到最后一次文件访问的总秒数)
%y最后修改文件内容的时间, 人类可识别的时间字符串
%Y最后修改文件内容的时间, 自纪元以来的秒数(参考 %X )
%z最后修改文件状态的时间, 人类可识别的时间字符串
%Z最后修改文件状态的时间, 自纪元以来的秒数(参考 %X )
仔细阅读上表可以知道:文件的每一个属性都有一个或者多个与之对应的格式化字符,这样就可以精确定位所需要的属性信息了,下面举了几个例子,可以作为参考:
$ stat occi.cpp -c %a
644

$ stat occi.cpp -c %A           
-rw-r--r--

# 使用 ls -l 验证权限
$ ll occi.cpp 
-rw-r--r-- 1 robin robin 1406 Jan 31 00:00 occi.cpp    # 0664

$ stat link.lnk -c %N
'link.lnk' -> '/home/robin/english.txt'

$ stat link.lnk -c %y
2021-01-31 10:48:52.317846411 +0800

3.stat/lstat函数

stat/lstat 函数的功能和 stat 命令的功能是一样的, 只不过是应用场景不同。这两个函数的区别在于处理软链接文件的方式上:
  • lstat(): 得到的是软连接文件本身的属性信息
  • stat(): 得到的是软链接文件关联的文件的属性信息
函数原型如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *pathname, struct stat *buf);
int lstat(const char *pathname, struct stat *buf);
  • 参数:
    • pathname: 文件名, 要获取这个文件的属性信息
    • buf: 传出参数, 文件的信息被写入到了这块内存中
  • 返回值: 函数调用成功返回 0,调用失败返回 -1
这个函数的第二个参数是一个结构体类型, 这个结构体相对复杂, 通过这个结构体可以存储得到的文件的所有属性信息, 结构体原型如下:
struct stat {
    dev_t          st_dev;          // 文件的设备编号
    ino_t           st_ino;          // inode节点
    mode_t      st_mode;          // 文件的类型和存取的权限, 16位整形数  -> 常用
    nlink_t        st_nlink;       // 连到该文件的硬连接数目,刚建立的文件值为1
    uid_t           st_uid;         // 用户ID
    gid_t           st_gid;         // 组ID
    dev_t          st_rdev;        // (设备类型)若此文件为设备文件,则为其设备编号
    off_t            st_size;        // 文件字节数(文件大小)   --> 常用
    blksize_t     st_blksize;     // 块大小(文件系统的I/O 缓冲区大小)
    blkcnt_t      st_blocks;      // block的块数
    time_t         st_atime;       // 最后一次访问时间
    time_t         st_mtime;       // 最后一次修改时间(文件内容)
    time_t         st_ctime;       // 最后一次改变时间(指属性)
};

3.1 获取文件大小

下面调用 stat() 函数, 以代码的方式演示一下如何得到某个文件的大小:
#include <sys/stat.h>

int main()
{
    // 1. 定义结构体, 存储文件信息
    struct stat myst;
    // 2. 获取文件属性 english.txt
    int ret = stat("./english.txt", &myst);
    if(ret == -1)
    {
        perror("stat");
        return -1;
    }

    printf("文件大小: %d\n", (int)myst.st_size);

    return 0;
}

3.2 获取文件类型

文件的类型信息存储在 struct stat 结构体的st_mode成员中, 它是一个 mode_t类型, 本质上是一个16位的整数。Linux API中为我们提供了相关的宏函数,通过对应的宏函数可以直接判断出文件是不是某种类型,这些信息都可以通过 man 文档(man 2 stat)查询到。
相关的宏函数原型如下:
// 类型是存储在结构体的这个成员中: mode_t  st_mode;  
// 这些宏函数中的m 对应的就是结构体成员  st_mode
// 宏函数返回值: 是对应的类型返回-> 1, 不是对应类型返回0

S_ISREG(m)  is it a regular file?  
  - 普通文件
S_ISDIR(m)  directory?
  - 目录
S_ISCHR(m)  character device?
  - 字符设备
S_ISBLK(m)  block device?
  - 块设备
S_ISFIFO(m) FIFO (named pipe)?
  - 管道
S_ISLNK(m)  symbolic link?  (Not in POSIX.1-1996.)
  - 软连接
S_ISSOCK(m) socket?  (Not in POSIX.1-1996.)
  - 本地套接字文件
在程序中通过宏函数判断文件类型, 实例代码如下:
int main()
{
    // 1. 定义结构体, 存储文件信息
    struct stat myst;
    // 2. 获取文件属性 english.txt
    int ret = stat("./hello", &myst);
    if(ret == -1)
    {
        perror("stat");
        return -1;
    }

    printf("文件大小: %d\n", (int)myst.st_size);

    // 判断文件类型
    if(S_ISREG(myst.st_mode))
    {
        printf("这个文件是一个普通文件...\n");
    }

    if(S_ISDIR(myst.st_mode))
    {
        printf("这个文件是一个目录...\n");
    }
    if(S_ISLNK(myst.st_mode))
    {
        printf("这个文件是一个软连接文件...\n");
    }

    return 0;
}

3.2 获取文件权限

用户对文件的操作权限也存储在 struct stat 结构体的st_mode成员中, 在这个16位的整数中不同用户的权限存储位置如下图,如果想知道有没有相关权限可以通过按位与(&)操作将这个标志位值取出判断即可。
Linux 中为我们提供了用于不同用户不同权限判定使用的宏,具体信息如下:
关于变量 st_mode: 
- st_mode -- 16位整数
  ○ 0-2 bit -- 其他人权限
    - S_IROTH    00004  读权限   100
    - S_IWOTH    00002  写权限   010
    - S_IXOTH    00001  执行权限  001
    - S_IRWXO    00007  掩码, 过滤 st_mode中除其他人权限以外的信息
  ○ 3-5 bit -- 所属组权限
    - S_IRGRP    00040  读权限
    - S_IWGRP    00020  写权限
    - S_IXGRP    00010  执行权限
    - S_IRWXG    00070  掩码, 过滤 st_mode中除所属组权限以外的信息
  ○ 6-8 bit -- 文件所有者权限
    - S_IRUSR    00400    读权限
    - S_IWUSR    00200    写权限
    - S_IXUSR    00100    执行权限
    - S_IRWXU    00700    掩码, 过滤 st_mode中除文件所有者权限以外的信息
  ○ 12-15 bit -- 文件类型
    - S_IFSOCK   0140000 套接字
    - S_IFLNK    0120000 符号链接(软链接)
    - S_IFREG    0100000 普通文件
    - S_IFBLK    0060000 块设备
    - S_IFDIR    0040000 目录
    - S_IFCHR    0020000 字符设备
    - S_IFIFO    0010000 管道
    - S_IFMT     0170000 掩码,过滤 st_mode中除文件类型以外的信息
      
#      
    1111 1111 1111 1011   # st_mode
    0000 0000 0000 0100   # S_IROTH
## &
    0000 0000 0000 0000   # 没有任何权限
    
通过仔细阅读上边提供的宏信息, 我们可以知道处理使用它们得到用户对文件的操作权限, 还可以用于判断文件的类型(判断文件类型的第二种方式),具体操作方式可以参考如下代码:
#include <sys/stat.h>

int main()
{
    // 1. 定义结构体, 存储文件信息
    struct stat myst;
    // 2. 获取文件属性 english.txt
    int ret = stat("./hello", &myst);
    if(ret == -1)
    {
        perror("stat");
        return -1;
    }

    printf("文件大小: %d\n", (int)myst.st_size);

    // 判断文件类型
    if(S_ISREG(myst.st_mode))
    {
        printf("这个文件是一个普通文件...\n");
    }

    if(S_ISDIR(myst.st_mode))
    {
        printf("这个文件是一个目录...\n");
    }
    if(S_ISLNK(myst.st_mode))
    {
        printf("这个文件是一个软连接文件...\n");
    }

    // 文件所有者对文件的操作权限
    printf("文件所有者对文件的操作权限: ");
    if(myst.st_mode & S_IRUSR)
    {
        printf("r");
    }
    if(myst.st_mode & S_IWUSR)
    {
        printf("w");
    }
    if(myst.st_mode & S_IXUSR)
    {
        printf("x");
    }
    printf("\n");
    return 0;
}

4.练习

掌握了如何通过 stat / lstat 函数获取文件相关属性之后, 我们就可以使用这两个函数来模拟执行命令 ls -l 的效果,具体代码实现如下:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <time.h>
#include <pwd.h>
#include <grp.h>


int main(int argc, char* argv[])
{
    if(argc < 2)
    {
        printf("./a.out filename\n");
        exit(1);
    }

    struct stat st;
    int ret = stat(argv[1], &st);
    if(ret == -1)
    {
        perror("stat");
        exit(1);
    }

    // 存储文件类型和访问权限
    char perms[11] = {0};
    // 判断文件类型
    switch(st.st_mode & S_IFMT)
    {
        case S_IFLNK:
            perms[0] = 'l';
            break;
        case S_IFDIR:
            perms[0] = 'd';
            break;
        case S_IFREG:
            perms[0] = '-';
            break;
        case S_IFBLK:
            perms[0] = 'b';
            break;
        case S_IFCHR:
            perms[0] = 'c';
            break;
        case S_IFSOCK:
            perms[0] = 's';
            break;
        case S_IFIFO:
            perms[0] = 'p';
            break;
        default:
            perms[0] = '?';
            break;
    }
    // 判断文件的访问权限
    // 文件所有者
    perms[1] = (st.st_mode & S_IRUSR) ? 'r' : '-';
    perms[2] = (st.st_mode & S_IWUSR) ? 'w' : '-';
    perms[3] = (st.st_mode & S_IXUSR) ? 'x' : '-';
    // 文件所属组
    perms[4] = (st.st_mode & S_IRGRP) ? 'r' : '-';
    perms[5] = (st.st_mode & S_IWGRP) ? 'w' : '-';
    perms[6] = (st.st_mode & S_IXGRP) ? 'x' : '-';
    // 其他人
    perms[7] = (st.st_mode & S_IROTH) ? 'r' : '-';
    perms[8] = (st.st_mode & S_IWOTH) ? 'w' : '-';
    perms[9] = (st.st_mode & S_IXOTH) ? 'x' : '-';

    // 硬链接计数
    int linkNum = st.st_nlink;
    // 文件所有者
    char* fileUser = getpwuid(st.st_uid)->pw_name;
    // 文件所属组
    char* fileGrp = getgrgid(st.st_gid)->gr_name;
    // 文件大小
    int fileSize = (int)st.st_size;
    // 修改时间
    char* time = ctime(&st.st_mtime);
    char mtime[512] = {0};
    strncpy(mtime, time, strlen(time)-1);

    char buf[1024];
    sprintf(buf, "%s  %d  %s  %s  %d  %s  %s", 
            perms, linkNum, fileUser, fileGrp, fileSize, mtime, argv[1]);

    printf("%s\n", buf);

    return 0;
}

Comment