Linux编程入门(6)-读取目录与文件的属性(ls 进阶)

前言

上一篇文章学习了如何读取目录内容,并通过编程简单实现了 ls 指令。上篇文章链接为

Linux编程入门(5)-读取目录(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经典书籍。
在这里插入图片描述

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zsky_01

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值