Linux系统编程(三):stat函数和ls -l命令的实现

在该系列的上一篇中,介绍了文件概念和相关的诸多函数,但实例仅有两三个。本篇文章不再专门介绍函数,而是先承接上文,记录一下通过stat()函数来实现ls -l命令。

1 ls -l命令的作用

ls -l命令用于查看当前或指定目录下文件的详细信息,使用效果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ ls -l
total 124
-rwxrwxrwx 1 choupin choupin 15768 Feb 15 22:19 cp01
-rw-rw-rw- 1 choupin choupin 987 Feb 15 22:19 cp01.c
-rw-r--r-- 1 choupin choupin 987 Feb 23 18:49 cp02.c
-rwxrwxrwx 1 choupin choupin 11136 Feb 21 14:05 fork_loop
-rw-rw-rw- 1 choupin choupin 353 Feb 21 14:05 fork_loop.c
-rwxrwxrwx 1 choupin choupin 11720 Feb 18 14:03 ls01
-rw-rw-rw- 1 choupin choupin 483 Feb 18 14:03 ls01.c
-rwxrwxrwx 1 choupin choupin 18256 Feb 21 14:05 ls_l
-rw-rw-rw- 1 choupin choupin 2681 Feb 19 17:05 ls_l.c
-rwxrwxrwx 1 choupin choupin 15808 Feb 17 17:56 lseek
-rw-rw-rw- 1 choupin choupin 824 Feb 17 17:55 lseek.c
-rw-rw-rw- 1 choupin choupin 164 Feb 20 17:23 makefile
drwxrwxrwx 1 choupin choupin 512 Feb 18 13:56 testls
-rw-r--r-- 1 choupin choupin 229 Feb 17 17:55 testlseek
-rw-r--r-- 1 choupin choupin 229 Feb 17 17:56 testlseek.txt

其中每行包含7个字段,分别

  • 模式mode:第一个字符表示文件类型,后面表示文件访问权限
  • 链接数links:即文件被引用的次数
  • 文件所有者owner:用户名
  • 组group:文件所有者所在的组
  • 大小size:以字节bytes为单位
  • 最后修改时间last-modified:较新文件会列出月日时分,较老文件只列出月日年
  • 文件名name:最长为256个字节

2 ls -l 如何工作

上篇文章中,我们已经实现过不带选项的ls命令,两者的区别就在于一个只简单地输出文件名,而另一个则需要输出文件的详细信息。

那么文件信息如何获取呢?答案是使用之前已经介绍过的stat()函数,关于该函数的介绍见上篇文章

所以其原理就是:

  1. 打开当前或给定目录
  2. 循环读取目录中的文件,获取文件名
  3. 通过文件名来调用stat()函数来获取文件的详细信息
  4. 按照指定格式来输出文件信息。

第1步和第2步已经在之前ls的实现中有过练习,显然这里的主要工作就是通过stat()获取文件信息和按照指定格式来输出信息。

3 获取文件信息

stat()的返回值是一个struct stat,这里需要分析一下如何从这个结构体中获取我们要在ls -l命令中打印的信息,结构体信息摘录于下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <unistd.h>
// 一个用来存放文件状态的结构体
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 */

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
};

3.1 获取mode信息

需要从成员st_mode中获取,其类型mode_t在我的系统中定义就是unsigned int。但在命令ls -l的输出应该是类似-rw-rw-r--的10个字符的字符串。这之间如何转换呢?

其实这些信息是以位图的方式放进了这个整数类型的16位中。

其中前4位用作文件类型,最多可标识16种类型,目前已有7种文件类型。

接下来3位是文件特殊属性,分别是set-user-ID,set-group-ID和sticky位,1表示具有该属性,0表示没有。

最后9位表示许可权限,分为3组,分别对应文件所有者、同组用户、其他用户的读写执行权限。同样有的话为1,没有的话为0。

既然是位图,那么必然可以通过掩码来取得其信息。如关于文件类型,在<sys/stat.h>中定义了掩码S_IFMT来取得文件类型信息,用法如下

1
2
3
4
5
6
7
8
9
10
switch (sb.st_mode & S_IFMT) {
case S_IFBLK: printf("block device\n"); break;
case S_IFCHR: printf("character device\n"); break;
case S_IFDIR: printf("directory\n"); break;
case S_IFIFO: printf("FIFO/pipe\n"); break;
case S_IFLNK: printf("symlink\n"); break;
case S_IFREG: printf("regular file\n"); break;
case S_IFSOCK: printf("socket\n"); break;
default: printf("unknown?\n"); break;
}

而掩码S_IFMT以及表示相应文件类型的S_IDBLK等实际上是一些八进制位数的宏。

除了直接使用掩码来判断,更简单的方法是使用定义好的比较宏

1
2
3
4
5
6
7
#define S_ISDIR(m)	(((m)&(0170000)) == (0040000))
#define S_ISCHR(m) (((m)&(0170000)) == (0020000))
#define S_ISBLK(m) (((m)&(0170000)) == (0060000))
#define S_ISREG(m) (((m)&(0170000)) == (0100000))
// 判断一个文件是否为目录
if(S_ISDIR(info.st_mode))
printf("this is a directory\n");

而许可权限的信息同样可以通过相应的掩码来获取。

在获取之后这些信息之后,即可将其转换为对应的字符串类似drwxrwxr--

3.2 获取链接数、文件大小和时间信息

链接信息即保存在成员st_nlinks里,直接打印即可。

文件大小信息即保存在成员st_size里,同样直接打印即可。

时间信息即保存在成员st_mtime里,需要注意的是这里需要使用ctime()函数来将其转换成要求的格式来打印。

1
2
3
#include <time.h>
// 返回的是类似"Wed Jun 30 21:49:08 1993\n"一个字符串
char *ctime(const time_t *timep);

3.3 获取所有者和组名信息

首先需要知道Linux系统的用户信息保存在/etc/passwd中,但这个文件又没有包括所有的用户,在一些网络计算机系统中,所有主机通过NIS来进行身份验证,本地只保存所有用户的一个自己以备离线操作。

这里可以通过getwuid()来获取完整的用户列表,该调用会自动选择从/etc/passwd还是NIS中获取信息,提高了程序的可移植性。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <pwd.h>
// 接受用户的uid作为参数,返回指针指向的结构体中包含了用户信息
struct passwd *getpwuid(uid_t 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; /* user information */
char *pw_dir; /* home directory */
char *pw_shell; /* shell program */
};

所以可以调用getpwuid()来获取用户信息,打印用户名称。另外还有一种可能是,文件所有者已经搬走了,账号被删除,但这个文件还在,此时该函数将返回NULL。所以这里还需要检查返回值,而不能直接读取,否则可能出现段错误。

类似的使用getgrgid()来获取组列表,用法大同小异

1
2
3
4
5
6
7
8
9
10
#include <grp.h>
struct group *getgrgid(gid_t gid);

struct group {
char *gr_name; /* group name */
char *gr_passwd; /* group password */
gid_t gr_gid; /* group ID */
char **gr_mem; /* NULL-terminated array of pointers
to names of group members */
};

该函数也是有可能返回NULL的。

4 实现代码

ls-l.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#include <dirent.h>
#include <grp.h>
#include <pwd.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

// 用于完成ls -l功能的函数
void do_ls(char[]);
// 获取文件信息并打印的函数
void dostat(char*, char*);
// 将st_mode转换为字符串的函数
void mode_to_letters(int, char[]);
// 获取用户名的函数
char* uid_to_name(uid_t);
// 获取用户组名的函数
char* gid_to_name(gid_t);

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;
struct dirent* direntp;

if ((dir_ptr = opendir(dirname)) == NULL)
fprintf(stderr, "ls:cannot open %s\n", dirname);
else {
while ((direntp = readdir(dir_ptr)) != NULL) {
char *dname = direntp->d_name;
// 这里过滤一下“."和".."
if (strcmp(dname, ".") && strcmp(dname, "..")) {
char path[256];
// 如果这里不做拼接的话,将无法获取非当前目录下的文件信息
sprintf(path, "%s%s", dirname, dname);
dostat(path, dname);
}
}
closedir(dir_ptr);
}
}

void dostat(char* filename, char* name) {
struct stat info;
if (stat(filename, &info) == -1)
perror(filename);
else {
char modestr[11];
struct stat* info_ptr = &info;
mode_to_letters(info_ptr->st_mode, modestr);

printf("%s", modestr);
printf("%4d ", (int)info_ptr->st_nlink);
printf("%-8s", uid_to_name(info_ptr->st_uid));
printf("%-8s ", gid_to_name(info_ptr->st_gid));
printf("%8ld ", (long)info_ptr->st_size);
printf("%.12s ", 4 + ctime(&info_ptr->st_ctime));
printf("%s\n", name);
}
}

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 (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;
static char uname[10];
if ((pw_ptr = getpwuid(uid)) == NULL) {
// 若没有该用户则打印uid
sprintf(uname, "%d", uid);
return uname;
} else
return pw_ptr->pw_name;
}

char* gid_to_name(gid_t gid) {
struct group* grp_ptr;
static char gname[10];
if ((grp_ptr = getgrgid(gid)) == NULL) {
// 若没有该组则打印gid
sprintf(gname, "%d", gid);
return gname;
} else
return grp_ptr->gr_name;
}

将其编译并进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ gcc ls_l.c -o ls_l
$ ./ls_l /
/:
drwxr-xr-x 1 root root 512 Jan 11 20:23 bin
drwxr-xr-x 1 root root 512 Dec 10 10:47 boot
drwxr-xr-x 1 root root 512 Feb 23 13:13 dev
drwxr-xr-x 1 root root 512 Feb 23 13:13 etc
drwxr-xr-x 1 root root 512 Dec 10 10:49 home
-rwxr-xr-x 1 root root 591344 Jan 1 08:00 init
drwxr-xr-x 1 root root 512 Dec 20 15:22 lib
drwxr-xr-x 1 root root 512 Dec 10 10:47 lib64
drwxr-xr-x 1 root root 512 Dec 10 10:47 media
drwxr-xr-x 1 root root 512 Feb 23 13:13 mnt
drwxr-xr-x 1 root root 512 Dec 10 10:47 opt
dr-xr-xr-x 45 root root 0 Feb 23 13:13 proc
drwx------ 1 root root 512 Feb 11 10:08 root
drwxr-xr-x 1 root root 512 Feb 23 13:13 run
drwxr-xr-x 1 root root 512 Dec 19 20:12 sbin
drwxr-xr-x 1 root root 512 Dec 10 10:47 snap
drwxr-xr-x 1 root root 512 Dec 10 10:47 srv
dr-xr-xr-x 12 root root 0 Feb 23 13:13 sys
drwxrwxrwx 1 root root 512 Feb 23 22:36 tmp
drwxr-xr-x 1 root root 512 Dec 10 10:47 usr
drwxr-xr-x 1 root root 512 Dec 10 10:47 var

实现完成。

5 结束语

本篇文章主要通过了实现ls -l来练习stat()相关函数以及struct stat的使用,熟悉了如何获取文件详细信息,并将其转换成我们需要的格式。在文件mode信息获取中,使用到的关于掩码的宏较多,通过这次练习即使记不住也有个大概印象,下次再需使用时也可以通过查询相关帮助信息来完成编写。

本篇文章主要内容依然总结自《Unix\Linux编程实践教程》一书,下一篇依然会是编程练习,记录who命令的实现。

-------- 本文结束 感谢阅读 --------
给我加块红烧肉吧
  • 本文标题: Linux系统编程(三):stat函数和ls -l命令的实现
  • 本文作者: Chou Bin
  • 创建时间: 2020年02月23日 - 23时02分
  • 修改时间: 2020年02月23日 - 23时02分
  • 本文链接: http://yoursite.com/2020/02/23/apue-hm03/
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!