Linux系统调用六、stat函数与 struct stat 文件信息结构体深度刨析
🎈1. inode节点与硬链接
通过上图可以看到,硬链接和源文件引用的是同一个inode节点,并且在inode节点中有一条硬链接计数信息,每当inode被引用一次,这个硬链接计数就会加1,我们可以通过ls命令来查看inode节点信息。我们先建立一个文件以及该文件的硬链接,通过ll命令可以查看文件信息(实际上这些信息就是存在inode节点中的信息)。
可以看到,建立一个硬链接之后,硬链接计数增加了1个。通过ls命令的-i选项可以查看文件的inode节点编号。
硬链接文件和源文件的inode节点编号一样,说明它们引用的是同一个inode节点。
在上图中的目录项中,有一条信息是类型,如果当前是目录的话,可以继续进入下一级目录。简单举个例子,比如说我们使用vi打开当前目录可以得到下面的内容。
这里面的三个条目是当前文件夹下的文件,我们可以通过tree命令查看一下当前文件夹./的目录结构
当我们把光标停在某一文件所在行按回车键就可以查看该文件内容,如果这个文件是目录,就会进入该目录并显示目录下的条目。
比如,进入file.txt文件
进入目录111
🎈2. stat函数与 struct stat 结构体
- 包含头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
- 函数原型
int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);
-
函数功能
Get file status - These functions return information about a file.
-
函数参数
-
path:指定文件
-
buf:buf是一个传出参数,也就是一级指针做输出,我们应该先定义一个结构体变量,并把该变量取地址&传给形参。
struct stat { dev_t st_dev; /* ID of device containing file */ ino_t st_ino; /* inode number */ mode_t st_mode; /* protection 实际上就是权限位 */ 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; /* blocksize for file system I/O 块大小 */ blkcnt_t st_blocks; /* number of 512B blocks allocated 块的个数 */ time_t st_atime; /* time of last access 最后访问时间 */ time_t st_mtime; /* time of last modification 最后修改时间 */ time_t st_ctime; /* time of last status change 最后状态改变时间 */ };
-
对于结构体struct stat中的 mode_t st_mode 进行简要介绍(下面并没有全部列出,只列出了部分),首先是判断文件类型的两种方法。第一种是掩码的方式。
S_IFMT 0170000 bit mask for the file type bit fields S_IFSOCK 0140000 socket S_IFLNK 0120000 symbolic link 符号链接 S_IFREG 0100000 regular file 普通文件 S_IFBLK 0060000 block device 块设备 S_IFDIR 0040000 directory 目录 S_IFCHR 0020000 character device 字符设备 S_IFIFO 0010000 FIFO 管道
这个实际上就是我们ll命令查看文件时,第一列所显示的文件类型,在上面列出的内容中,中间一列数字用来标识文件类型,从S_IFSOCK到S_IFIFO就是我们所熟知的7种文件类型,S_IFMT是文件类型的掩码。通过和S_IFMT的与运算,就可以把标识文件类型的位保留下来(也就是前两个数017,第一个0表示8进制,后面17共四位 1 111 用来标识文件类型,那么S_IFMT用二进制表示就是1111后面加12个0,通过与与运算就把后面12位置为0而保留代表文件类型的四个bit,再把相与的结果与下面的S_IFSOCK到S_IFIFO进行对比来判断文件类型),进而判断文件类型。比如,现在有一个0041100,把它和S_IFMT做与运算结果为
0041100 & 0170000 = 0040000 (S_IFMT) (S_IFDIR)
通过结果对比可以得出这是一个目录文件。这是通过掩码的方式来判断文件类型。
另外一种判断文件类型的方法是使用它为我们提供的宏来判断,7种文件类型判断相关的宏如下所示,这里的m是指stat结构体中的st_mode。
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.)
最后就是用户、组、其他用户的权限位(4位掩码,第一个0表示8进制)
S_IRWXU 00700 mask for file owner permissions S_IRUSR 00400 owner has read permission S_IWUSR 00200 owner has write permission S_IXUSR 00100 owner has execute permission S_IRWXG 00070 mask for group permissions S_IRGRP 00040 group has read permission S_IWGRP 00020 group has write permission S_IXGRP 00010 group has execute permission S_IRWXO 00007 mask for permissions for others (not in group) S_IROTH 00004 others have read permission S_IWOTH 00002 others have write permission S_IXOTH 00001 others have execute permission
-
st_mode 各位含义示意图
-
结构体struct stat中的时间time_t也是一个结构体,它的原型如下
struct timespec { _kernel_time_t tv_sec; /*seconds 当前时间到1970.1.1 0:0:0的秒数*/ long tv_nsec; /*nanoseconds 纳秒*/ }
-
-
-
函数返回值
成功返回0,失败返回-1并设置errno 。On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
🎈3. stat函数实例分析及stat命令
下面通过一个实例来演示一下stat函数的使用方法。测试函数如下
/************************************************************
>File Name : getstat.c
>Author : QQ
>Company : QQ
>Create Time: 2022年05月14日 星期六 18时37分17秒
************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(int argc, char* argv[])
{
if(argc < 2)
{
printf("not found filename\n");
return -1;
}
struct stat m_status;
stat(argv[1], &m_status);
return 0;
}
我们可以借助gdb调试器来查看结构体内容,并且借助gdb强大的功能可以很优美的打印出结构体内容。
我们使用set来设置一下,优美的打印结构体
该文件的硬链接数为st_nlink=2,我们确实对该文件创建了硬链接
我们可以看到这里面有一个st_mode=33188,这个就是我们在介绍stat结构体时重点介绍的权限参数,但是这里是十进制显示的,我们可以把它的八进制形式在gdb中打印出来
我们通过这个8进制数和文件类型掩码、权限掩码分别相与就可以得到这个文件的文件类型以及各用户的权限。
S_IFMT & 0100644 = 0100000 ===> S_IFREG 0100000
可以看到相与之后的值对应于S_IFREG,也就是说这是个普通文件。
继续往下看 st_uid=0,st_gid=0,这个对应的是用户及组ID,这里要说明的是,我当前使用的是root用户,相应用户的ID可以在 /etc/passwd 中查看,我们可以使用vim编辑器打开文件查看
vim /etc/passwd
在第一行就可以看到root,后面两个0就对应结构体中的 st_uid=0和st_gid=0 。
继续往下看st_size = 11表示文件大小,可以通过ll命令来验证一下
再后面是块大小st_blksize = 4096和块的个数st_blocks = 8,那么每个块的大小是512,实际上这两个内容就是记录有多少个512大小的块。
再后面就是三个时间结构体的信息,最近访问时间、最近更改时间、最近状态改动时间。
- Access 最近访问时间:是指最近的一次访问(读/写等),比如使用cat、touch等命令访问了该文件(访问但是没有修改),那么最近访问时间就会更新;
- Modify 最近更改时间:是指最近一次文件内容的更改时间;
- Change 最近状态改动时间:是指最近一次文件属性的更改时间,文件属性的更改包括文件大小、硬链接计数、文件权限等等的修改,并且一般Modify发生改变的时候,Change也会随之变化,因为文件内容的修改一般都会涉及到文件大小等的变化;
如果我们只是用cat查看一下文件,那么只有Access时间会更新;如果我们只是修改文件的权限,比如增加可执行权限,那么只有Change时间更新;如果我们使用重定向往文件中写入内容,那么Modify和Change时间会更新,而Access时间不会更新,因为在重定向的过程中,并没有访问文件。
实际上,上面介绍的这些内容,直接通过stat命令就可以查看
🎈4. 实现 ls -l filename命令
我们可以通过stat函数来实现 ls -l 命令的功能,下面我们实现查看指定文件的 ls -l 命令,即
ls -l filename
实现代码如下
/************************************************************
>File Name : mls.c
>Author : QQ
>Company : QQ
>Create Time: 2022年05月15日 星期日 16时29分01秒
>实现目标:-rw-r--r--. 2 root root 11 5月 14 15:02 file.txt
************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>
#include <pwd.h>
#include <grp.h>
int main(int argc, char* argv[])
{
if(argc < 2)
{
printf("not found filename\n");
return -1;
}
/*通过stat函数获取文件信息*/
struct stat fstatus;
stat(argv[1], &fstatus); /*穿透*/
/*lstat(argv[1], &fstatus); 非穿透 */
/*解析文件信息,st_mode st_uid st_gid time*/
/*-rw-r--r--.*/
char stmode[11];
memset(stmode, '-', sizeof(stmode));
/*使用宏来判断文件属性*/
if(S_ISREG(fstatus.st_mode)) /* regular file */
{
stmode[0] = '-';
}
if(S_ISDIR(fstatus.st_mode))
{
stmode[0] = 'd';
}
if(S_ISCHR(fstatus.st_mode))
{
stmode[0] = 'c';
}
if(S_ISBLK(fstatus.st_mode))
{
stmode[0] = 'b';
}
if(S_ISFIFO(fstatus.st_mode))
{
stmode[0] = 'p';
}
if(S_ISLNK(fstatus.st_mode))
{
stmode[0] = 'l';
}
if(S_ISSOCK(fstatus.st_mode))
{
stmode[0] = 's';
}
/*解析权限位*/
if(fstatus.st_mode & S_IRUSR)
{/*为真表示拥有该权限,否则无权限,因为整块内存已初始化为 - 所以不需要else分支*/
stmode[1] = 'r';
}
if(fstatus.st_mode & S_IWUSR)
{
stmode[2] = 'w';
}
if(fstatus.st_mode & S_IXUSR)
{
stmode[3] = 'x';
}
if(fstatus.st_mode & S_IRGRP)
{
stmode[4] = 'r';
}
if(fstatus.st_mode & S_IWGRP)
{
stmode[5] = 'w';
}
if(fstatus.st_mode & S_IXGRP)
{
stmode[6] = 'x';
}
if(fstatus.st_mode & S_IROTH)
{
stmode[7] = 'r';
}
if(fstatus.st_mode & S_IWOTH)
{
stmode[8] = 'w';
}
if(fstatus.st_mode & S_IXOTH)
{
stmode[9] = 'x';
}
stmode[10] = '\0';
/*获取时间 localtime() 函数(非系统调用)
*原型:struct tm *localtime(const time_t *timep);
*参数:time_t类型,struct stat中time_t st_atime,这里应该是文件访问时间
*返回:struct tm {
int tm_sec; seconds (0-60 )
int tm_min; minutes (0-59)
int tm_hour; hours (0-23)
int tm_mday; day of the month (1-31)
int tm_mon; month (0-11)
int tm_year; year (-1900) 如果要求实际年份,应加上1990
int tm_wday; day of the week sunday=0
int tm_yday; day in the year
int tm_isdst; daylight saving time
};
*/
struct tm* filetime = localtime(&fstatus.st_atim.tv_sec);
char timebuf[20] = {0};
sprintf(timebuf, "%d月 %d %02d:%02d", \
filetime->tm_mon + 1, \
filetime->tm_mday, \
filetime->tm_hour, \
filetime->tm_min);
/*打印格式 -rw-r--r--. 2 root root 11 5月 14 15:02 file.txt*/
printf("%s %ld %s %s %ld %s %s\n", \
stmode, \
fstatus.st_nlink, \
getpwuid(fstatus.st_uid)->pw_name, \
getgrgid(fstatus.st_gid)->gr_name, \
fstatus.st_size, \
timebuf, \
argv[1]);
/* 两个函数(非系统调用)
struct passwd *getpwuid(uid_t uid); 根据uid获取用户信息
struct passwd {
char *pw_name; username
char *pw_passwd; user password
uid_t pw_uid; user ID
gid_t pw_gid; group ID
char *pw_gecos; real name
char *pw_dir; home directory
char *pw_shell; shell program
};
struct group *getgrgid(gid_t gid); 根据gid获取组信息
struct group {
char *gr_name; group name
char *gr_passwd; group password
gid_t gr_gid; group ID
char **gr_mem; group members
};
*/
return 0;
}
测试一下效果
🎈5. 穿透与非穿透
上面介绍了stat函数并通过stat函数实现了 ls -l 命令的功能。我们上面演示了使用自己实现的 ./mls 查看文件信息,假如说使用 ./mls 查看一个链接文件是什么效果呢,下面演示一下。
通过对比我们可以看到,符号链接(软链接)file.txt.soft的实际大小是8,但是我们自己实现的 ./mls 命令显示的大小是11。实际上,原因是这样的,我们在实现 ./mls 命令的时候是基于stat函数来获取文件信息的,stat函数有一个特性就是在获取链接文件信息的时候会进行穿透,去追溯符号链接的源文件,也就是说我们通过上面的命令 ./mls file.txt.soft 获取到的大小实际上是源文件file.txt的大小,我们可以验证一下。
那么我们自己如何实现获取符号链接的实际大小呢,这就用到了非穿透函数lstat,只要把上面代码实现中的函数调用stat替换为lstat就可以了,下面测试一下。
lstat(argv[1], &fstatus);