前言
上一篇文章学习了如何读取目录内容,并通过编程简单实现了 ls 指令。上篇文章链接为
ls 指令如果有选项 -l ,会显示文件的详细信息(如文件大小、文件所有者、修改时间等)。那如何获取这些信息呢?下面,让我们来一起分析、学习。
学习目标
通过分析指令 ls -l ,来学习 Linux 目录和文件属性相关的知识。
代码实验环境
操作系统:Ubuntu 18.04 LTS
编译器 gcc 版本:gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
分析 ls -l
ls -l 要做两件事:一是列出目录的内容,二是显示文件的详细信息。列出文件内容,前边已经分析过,从目录中读取即可。但是文件信息不包含在目录中,需要从另外的途径获得。
我们先看看标准 ls -l 的输出内容:
$ ls -l
total 2097260
drwxr-xr-x 2 root root 4096 9月 26 16:19 bin
drwxr-xr-x 3 root root 4096 11月 10 09:11 boot
drwxrwxr-x 2 root root 4096 4月 18 2021 cdrom
drwxr-xr-x 18 root root 4180 11月 17 08:58 dev
drwxr-xr-x 147 root root 12288 11月 17 08:59 etc
drwxr-xr-x 3 root root 4096 6月 22 14:43 home
lrwxrwxrwx 1 root root 34 11月 9 10:21 initrd.img -> boot/initrd.img-4.15.0-162-generic
lrwxrwxrwx 1 root root 34 11月 9 10:21 initrd.img.old -> boot/initrd.img-4.15.0-161-generic
drwxr-xr-x 22 root root 4096 4月 18 2021 lib
drwxr-xr-x 2 root root 4096 4月 18 2021 lib32
drwxr-xr-x 2 root root 4096 4月 18 2021 lib64
drwx------ 2 root root 16384 4月 18 2021 lost+found
drwxr-xr-x 4 root root 4096 4月 21 2021 media
drwxr-xr-x 2 root root 4096 4月 27 2018 mnt
drwxr-xr-x 3 root root 4096 10月 29 13:08 opt
dr-xr-xr-x 242 root root 0 11月 17 08:58 proc
drwx------ 7 root root 4096 5月 21 09:39 root
drwxr-xr-x 31 root root 1000 11月 17 09:03 run
drwxr-xr-x 2 root root 12288 11月 17 08:59 sbin
drwxr-xr-x 15 root root 4096 9月 29 08:53 snap
drwxr-xr-x 2 root root 4096 4月 27 2018 srv
-rw------- 1 root root 2147483648 4月 18 2021 swapfile
dr-xr-xr-x 13 root root 0 11月 17 10:16 sys
drwxrwxrwt 13 root root 4096 11月 17 20:09 tmp
drwxr-xr-x 15 root root 4096 4月 18 2021 usr
drwxr-xr-x 15 root root 4096 8月 13 17:37 var
lrwxrwxrwx 1 root root 31 11月 9 10:21 vmlinuz -> boot/vmlinuz-4.15.0-162-generic
lrwxrwxrwx 1 root root 31 11月 9 10:21 vmlinuz.old -> boot/vmlinuz-4.15.0-161-generic
每一行都包含 7 个字段:
-
模式(mode)
第一个字符表示文件类型。“-” 表述普通文件;“d” 表示目录。其他类型后续分析。
接下来的 9 个字符表示文件的访问权限。三种访问权限:读、写、执行。分别针对 3 种对象:文件所有者、同组用户、其他用户。因此,用需要 9 未位来表示。
-
链接数(links)
链接数指的是该文件被引用次数。这方面内容后续会进行学习。
-
文件所有者(owner)
指出文件所有者的用户名。
-
组(group)
指的是文件所有者所在的组。
-
大小(size)
上面的显示信息种,所有的目录大小相等,都是 4096 个字节。因为目录所占空间的分配是以块为单位的,每个块 512 个字节,因此目录的大小经常是相等的。
如果是一般文件,则显示文件中数据的实际字节数。
-
最后修改时间
文件最后的修改时间。如果是较新的文件,会列出 月、日和时刻。
-
文件名
获取文件信息
系统调用函数 stat() 可以获取文件的详细信息。其函数原型如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *statbuf);
参数 pathname,为文件名。
参数 statbuf,指向存储文件信息缓冲区的指针。
如果要读取文件属性信息,需要定义一个结构 struct stat,然后调用 stat(),告诉内核把文件属性存放到这个结构中。即 stat() 把 pathname 的属性信息复制到指针 statbuf 所指的结构中。
函数调用成功,返回 0 。调用遇到错误,则返回 -1 。
结构体 struct stat 类型定义如下:
struct stat
{
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* i节点号 */
mode_t st_mode; /* 文件类型和许可权限 */
nlink_t st_nlink; /* 文件链接数 */
uid_t st_uid; /* 文件所有者用户ID */
gid_t st_gid; /* 所属组的ID */
dev_t st_rdev; /* Device ID (if special file) */
off_t st_size; /* 文件所占的字节数 */
blksize_t st_blksize; /* Block size for filesystem I/O */
blkcnt_t st_blocks; /* Number of 512B blocks allocated */
struct timespec st_atim; /* 文件最后修改时间 */
struct timespec st_mtim; /* 文件最后访问时间 */
struct timespec st_ctim; /* 文件属性最后修改时间 */
/* 向后兼容 */
#define st_atime st_atim.tv_sec /* 文件最后修改时间 */
#define st_mtime st_mtim.tv_sec /* 文件最后访问时间 */
#define st_ctime st_ctim.tv_sec /* 文件属性最后修改时间 */
};
根据结构体信息字段可知,属性信息字段均是数字形式的。其中链接数和文件大小直接显示没有问题。最后修改时间、文件模式、文件用户、组等信息需要显示为字符串形式。
最后修改时间可以通过 ctime()
函数转化成字符串,然后打印显示。
模式、用户名和组如何转化为字符串,需要进一步分析处理。
模式字段转换为字符
文件类型和许可权限存储在 st_mode 中,这是一个 16 位的二进制数,文件类型和权限被编码在这个数中,如下图所示
前 4 位用来表示文件类型,最多可以标识 16 种类型,目前已经使用了 7 个。
接下来的 3 位是文件特殊属性,1 代表具有某个属性,0 代表没有。这三位分别是 set-user-ID位、set-group-ID位、sticky位。
最后 9 位 是许可权限,分为 3 组,对应 3 种用户,分别是文件所有者、同组用户和其他用户。其他用户指的是与用户不再同一个组中的用户。每组 3 位,分别是读、写、执行的权限。如果对应位是 1,说明该用户拥有对应的权限,0 代表没有。
解码得到文件类型
使用掩码技术来解析文件类型。文件类型在模式字段的前 4 位,可以通过将其他部分置为 0 ,从而得到类型值。
为了比较,把不需要的地方置 0 ,需要的字段值不发生变化,这种技术称为掩码。与 0 做位与(&)操作可以将相应的 bit 置为 0 。
在 <sys/stat.h> 头文件中有以下定义:
#define S_IFMT 0170000 /* 文件类型掩码 */
#define S_IFSOCK 0140000 /* 套接字 */
#define S_IFLNK 0120000 /* 符号链接 */
#define S_IFREG 0100000 /* 普通文件 */
#define S_IFBLK 0060000 /* 块设备 */
#define S_IFDIR 0040000 /* 目录 */
#define S_IFCHR 0020000 /* 字符设备 */
#define S_IFIFO 0010000 /* 管道或FIFO */
S_IFMT 是一个掩码,值为 0170000(八进制数),可以用来过滤出 st_mode 前 4 位表示的文件类型。用模式字段 st_mode 与 S_IFMT 进行位与(&)运算,将结果与一系列常量进行比较,可以确定文件类型。示例代码:
if((statbuf.st_mode & S_IFMT) == S_IFREG)
{
printf("regular file\n");
/* 处理普通文件 */
}
有没有简单的方法进行文件类型判断呢?当然有。Linux 提供了标准宏来确定文件类型,这些宏的参数都是 stat 结构中的 st_mode 成员:
测试宏 | 文件类型 |
---|---|
S_ISREG(m) | 普通文件 |
S_ISDIR(m) | 目录文件 |
S_ISCHR(m) | 字符特殊文件(字符设备) |
S_ISBLK(m) | 块特殊文件(块设备) |
S_ISFIFO(m) | 管道或FIFO |
S_ISLNK(m) | 符号链接 |
S_ISSOCK(m) | 套接字 |
使用这些宏,可以这样写代码:
if(S_ISREG(statbuf.st_mode))
{
printf("regular file\n");
/* 处理普通文件 */
}
解码得到许可权限
模式字段的最低 9 位是许可权限,标识了文件所有者、组用户、其他用户的读、写、执行权限。ls -l 将这些位转换为短横线和字母组成的字符串。
同样的,Linux 在头文件 <sys/stat.h> 定义了权限位常量:
#define S_IRWXU 00700 /* 文件所有者的权限掩码 */
#define S_IRUSR 00400 /* 文件所有者具有读权限 */
#define S_IWUSR 00200 /* 文件所有者具有写权限 */
#define S_IXUSR 00100 /* 文件所有者具有执行权限 */
#define S_IRWXG 00070 /* 用户组的权限掩码 */
#define S_IRGRP 00040 /* 组用户具有读权限 */
#define S_IWGRP 00020 /* 组用户具有写权限 */
#define S_IXGRP 00010 /* 组用户具有执行权限 */
#define S_IRWXO 00007 /* 其他用户的权限掩码 */
#define S_IROTH 00004 /* 其他用户具有读权限 */
#define S_IWOTH 00002 /* 其他用户具有写权限 */
#define S_IXOTH 00001 /* 其他用户具有执行权限 */
通过与模式字段 st_mode 进行位与(&)运算,可以判断特定权限位是否开启。
编程实现模式字段的转换
void mode_to_letters(int mode, char str[])
{
/* 默认值 */
strcpy(str, "----------");
/* 目录 */
if(S_ISDIR(mode)) str[0] = 'd';
/* 字符设备 */
if(S_ISCHR(mode)) str[0] = 'c';
/* 块设备 */
if(S_ISBLK(mode)) str[0] = 'b';
/* 符号链接 */
if(S_ISLNK(mode)) str[0] = 'l';
/* 文件所有者权限 */
if(mode & S_IRUSR) str[1] = 'r';
if(mode & S_IWUSR) str[2] = 'w';
if(mode & S_IXUSR) str[3] = 'x';
/* 用户组权限 */
if(mode & S_IRGRP) str[4] = 'r';
if(mode & S_IWGRP) str[5] = 'w';
if(mode & S_IXGRP) str[6] = 'x';
/* 其他用户权限 */
if(mode & S_IROTH) str[7] = 'r';
if(mode & S_IWOTH) str[8] = 'w';
if(mode & S_IXOTH) str[9] = 'x';
}
用户和组ID转换成字符串
在结构 struct stat 中,文件所有者和组是以 ID 形式存在的,然而 ls -l 要求输出用户名和组名,如何根据 ID 找到用户名和组名呢?
包含用户列表的文件 /etc/passwd
这个文件包含了系统中所有用户信息。针对系统中每个账号,系统密码文件 /etc/passwd 会有专列一行进行描述。每行都包含 7 个字段,字段之间用冒号 “:” 隔开,如下所示:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
user:x:1000:1000:user,,,:/home/user:/bin/bash
这 7 个字段依次为:用户名(登录名)、经过加密的密码、用户 ID(UID)、组ID(GID)、用户描述信息、主目录、用户使用的 shell 程序。所有用户对这个文件都具有读权限。
注意,在单机系统中,所有用户信息都存储在 /etc/passwd 文件中。在网络计算机系统中,如果使用了 NIS (网络信息系统)或 LDAP(轻型目录访问协议)进行用户身份验证,那么部分密码信息可能会由远端系统保存。所有需要用户信息的程序也从 NIS 上获取。
shadow 密码文件 /etc/shadow
此文件的设计理念是,用户所有非敏感信息存放与 “人人可读” 的密码文件中,而经过加密处理的密码则有 shadow 密码文件单独维护,仅供具有特权的程序读取。
shadow 密码文件包含有登录名(用户名)、经过加密的密码,以及其他若干与安全相关的字段。
注意,要是系统启用了 shadow 密码(这是常规做法),系统将不会解析 /etc/passwd 文件中的密码字段,这个密码字段会包含字母 “x”(也可以是其他非空字符串)。这就解析了上述 /etc/passwd 文件中密码字段为什么是 “x”。
组文件 /etc/group
此文件是一个保存所有的组信息的文本文件。系统中每个组在文件 /etc/group 中都对应着一条记录。每条记录包含 4 个字段,之间用冒号 “:” 分隔,如下所示:
root:x:0:
daemon:x:1:
user:x:1000:
tech:x:1002:avr,rbl,alc
组信息的 4 个字段分别为:组名、组密码、组 ID(GID)、组中成员列表。其中,组成员列表各个用户名以都好分隔。
好了,用户名和用户组的信息存储位置分析清楚了。那我们可以读取 /etc/passwd 和 /etc/group 这两个文件,搜索对应的用户 ID 和组 ID,就可以得到对应的字符串名字了。实际上是这样吗?答案是否定的。
Linux 提供了专门获取这些信息的函数调用。
获取用户和组的信息
系统函数 getpwuid() 可以获取用户信息记录。如果用户信息存在 /etc/passwd中,那么 getpwuid() 会查找 /etc/passwd 的内容;如果用户信息在 NIS 中,getpwuid() 会从 NIS 中获取信息。其函数原型如下:
#include <sys/types.h>
#include <pwd.h>
struct passwd *getpwuid(uid_t uid);
参数 uid,为用户 ID。
调用成功,则返回一个指向 struct passwd 的指针;调用失败,则返回NULL。
结构体 struct passwd 包含了与密码记录相对应的信息:
struct passwd
{
char *pw_name; /* 用户名 */
char *pw_passwd; /* 用户密码 */
uid_t pw_uid; /* 用户 ID */
gid_t pw_gid; /* group ID */
char *pw_gecos; /* 用户信息 */
char *pw_dir; /* 主目录 */
char *pw_shell; /* shell 程序 */
};
系统函数 getgrgid() 可以获取组信息记录。同样的,在网络计算机系统中,如果组信息也被保存在 NIS 中,此函数也可从 NIS 中获取组信息。其函数原型如下:
#include <sys/types.h>
#include <grp.h>
struct group *getgrgid(gid_t gid);
参数 gid,为组 ID。
调用成功,则返回一个指向 struct group 的指针;调用失败,则返回NULL。
结构体 struct group 定义:
struct group
{
char *gr_name; /* 组名 */
char *gr_passwd; /* 组密码 */
gid_t gr_gid; /* 组 ID */
char **gr_mem; /* 组用户成员列表指针 */
};
编写代码实现 ls -l
编写代码 ls2.c 如下:
#include<stdio.h>
#include<sys/types.h>
#include<dirent.h>
#include<sys/stat.h>
#include<pwd.h>
#include<grp.h>
#include<time.h>
#include<string.h>
void do_ls(char dirname[]);
void do_stat(char *dirname, char *filename);
void show_file_info(char *filename, struct stat *info_p);
void mode_to_letters(int mode, char str[]);
char *uid_to_name(uid_t uid);
char *gid_to_name(gid_t gid);
int main(int argc, char *argv[])
{
if(argc == 1)
{
do_ls(".");
}
else
{
while(--argc)
{
printf("%s:\n", *++argv);
do_ls(*argv);
}
}
return 0;
}
void do_ls(char dirname[])
{
DIR *dir_ptr = NULL;
struct dirent *direntp = NULL;
if((dir_ptr = opendir(dirname)) == NULL)
{
fprintf(stderr, "ls2: cannot open %s\n", dirname);
}
else
{
while((direntp = readdir(dir_ptr)) != NULL)
{
do_stat(dirname, direntp->d_name);
}
closedir(dir_ptr);
}
}
void do_stat(char *dirname, char *filename)
{
struct stat info;
char pathname[256];
sprintf(pathname, "%s//%s", dirname, filename);
if(stat(pathname, &info) == -1)
{
perror(filename);
}
else
{
show_file_info(filename, &info);
}
}
void show_file_info(char *filename, struct stat *info_p)
{
char modestr[11];
mode_to_letters(info_p->st_mode, modestr);
printf("%-12s", modestr);
printf("%-8d", (int)info_p->st_nlink);
printf("%-8s", uid_to_name(info_p->st_uid));
printf("%-8s", gid_to_name(info_p->st_gid));
printf("%-16ld", (long)info_p->st_size);
printf("%-16.12s", 4 + ctime(&info_p->st_mtime));
printf("%s\n", filename);
}
void mode_to_letters(int mode, char str[])
{
strcpy(str, "----------");
if(S_ISDIR(mode)) str[0] = 'd';
if(S_ISCHR(mode)) str[0] = 'c';
if(S_ISBLK(mode)) str[0] = 'b';
if(S_ISLNK(mode)) str[0] = 'l';
if(mode & S_IRUSR) str[1] = 'r';
if(mode & S_IWUSR) str[2] = 'w';
if(mode & S_IXUSR) str[3] = 'x';
if(mode & S_IRGRP) str[4] = 'r';
if(mode & S_IWGRP) str[5] = 'w';
if(mode & S_IXGRP) str[6] = 'x';
if(mode & S_IROTH) str[7] = 'r';
if(mode & S_IWOTH) str[8] = 'w';
if(mode & S_IXOTH) str[9] = 'x';
}
char *uid_to_name(uid_t uid)
{
struct passwd *pw_ptr = NULL;
static char numstr[10];
if((pw_ptr = getpwuid(uid)) == NULL)
{
sprintf(numstr, "%d", uid);
return numstr;
}
else
{
return pw_ptr->pw_name;
}
}
char *gid_to_name(gid_t gid)
{
struct group *grp_ptr = NULL;
static char numstr[10];
if((grp_ptr = getgrgid(gid)) == NULL)
{
sprintf(numstr, "%d", gid);
}
else
{
return grp_ptr->gr_name;
}
}
编译运行
$ gcc ls2.c -o ls2
./ls2 /
/:
drwxrwxr-x 2 root root 4096 Apr 18 15:05 cdrom
drwxr-xr-x 18 root root 4180 Nov 19 08:58 dev
drwxr-xr-x 2 root root 12288 Nov 19 08:59 sbin
drwxr-xr-x 25 root root 4096 Nov 9 10:21 .
drwxrwxrwx 13 root root 4096 Nov 19 13:56 tmp
dr-xr-xr-x 13 root root 0 Nov 19 08:57 sys
dr-xr-xr-x 242 root root 0 Nov 19 08:57 proc
drwxr-xr-x 22 root root 4096 Apr 18 23:24 lib
-rw------- 1 root root 8453792 Oct 18 18:37 vmlinuz
drwx------ 7 root root 4096 May 21 09:39 root
drwxr-xr-x 147 root root 12288 Nov 19 08:59 etc
drwx------ 2 root root 16384 Apr 18 15:03 lost+found
drwxr-xr-x 2 root root 4096 Apr 18 20:21 lib64
-rw-r--r-- 1 root root 42709349 Oct 29 13:09 initrd.img.old
drwxr-xr-x 15 root root 4096 Aug 13 17:37 var
drwxr-xr-x 2 root root 4096 Apr 18 23:25 lib32
drwxr-xr-x 3 root root 4096 Jun 22 14:43 home
drwxr-xr-x 2 root root 4096 Apr 27 02:18 mnt
drwxr-xr-x 31 root root 1000 Nov 19 09:03 run
drwxr-xr-x 3 root root 4096 Oct 29 13:08 opt
drwxr-xr-x 3 root root 4096 Nov 10 09:11 boot
drwxr-xr-x 15 root root 4096 Apr 18 23:25 usr
drwxr-xr-x 15 root root 4096 Sep 29 08:53 snap
drwxr-xr-x 25 root root 4096 Nov 9 10:21 ..
-rw------- 1 root root 2147483648 Apr 18 15:03 swapfile
drwxr-xr-x 4 root root 4096 Apr 21 16:56 media
drwxr-xr-x 2 root root 4096 Apr 27 02:18 srv
drwxr-xr-x 2 root root 4096 Sep 26 16:19 bin
-rw-r--r-- 1 root root 42715926 Nov 9 10:21 initrd.img
-rw------- 1 root root 8453792 Oct 15 21:19 vmlinuz.old
ls2 的输出看起来接近标准 ls -l 的输出了。模式字段、用户名和组名的处理已经完成。当然,跟标准 ls 相比,还差好多功能没实现。比如,记录总数、按文件名排序、不支持 -a 选项 等等。
不过,我们的学习目的达到了,这些待完善的功能,如果有兴趣,可以自己尝试去实现。
小结
通过分析 ls -l 的实现过程,学习了 Linux 目录和文件很多相关的知识:
-
目录与文件
目录是特殊的文件,其内容是文件和子目录的名字。
-
用户与组
每个用户都有用户名和用户 ID,系统通过 UID 区分不同用户的文件。每个用户都属于至少一个组,每个组都有组名和组 ID。
-
文件属性
每个文件都有很多属性,程序可以通过系统调用来得到文件的属性信息。
-
许可权限
文件的许可权限决定了哪种用户可以进行哪些操作。
学习的系统函数有:
- stat()
- getpwuid()
- getgrgid()
- 文件类型判断的宏
至此,我们详细分析了目录和文件属性信息的部分内容,下篇继续学习一下剩余没讲解的属性信息,这样就齐活了。
OK,精彩继续!
————————————————————————————————
关注公众号【一起学习嵌入式】,后台回复 “ linux ” 获取Linux经典书籍。