编写 ls -l
- 1.1 问题 1:ls -l 能做些什么
- 1.2 问题 2:ls -l 是如何 工作的
- 1.3 用 stat 得到文件信息
- 1.4 stat 提供的其他信息
- 1.5 如何实现
- 1.6 将模式字段转换成字符
- 1.7 将用户/组 ID 转换成字符串
- 1.8 编写 ls1.c
- 留给大家的问题
作者:高玉涵
时间:2022.3.27 16:23
博客:blog.csdn.net/cg_i
环境:Linux ubuntu 4.15.0-163-generic #171-Ubuntu SMP Fri Nov 5 11:53:11 UTC 2021 i686 i686 i686 GNU/Linux
ls 要做两件事情 ,一是列出目录的内容,二是显示 文件的详细信息,这实际上是两件不同的工作,目录包含文件名,文件信息则需要从另外的途径获得,接下来从 3 个问题入手,来解决文件信息显示的问题。
1.1 问题 1:ls -l 能做些什么
先来看 ls -l 的输出 :
gao@ubuntu:~/c$ ls -al
总用量 80
drwxrwxr-x 3 gao gao 4096 3月 27 16:34 .
drwxr-xr-x 8 gao gao 4096 3月 27 16:01 ..
-rw-r--r-- 1 gao gao 7548 3月 27 14:53 copy.of.cp1
-rwxrwxr-x 1 gao gao 7548 3月 27 14:53 cp1
-rw-rw-r-- 1 gao gao 1037 3月 27 14:52 cp1.c
drwxrwxr-x 2 gao gao 4096 3月 27 16:34 dir
-rwxrwxr-x 1 gao gao 7392 3月 27 16:11 ls1
-rw-rw-r-- 1 gao gao 754 3月 27 16:11 ls1.c
-rwxrwxr-x 1 gao gao 7488 3月 17 22:12 who1
-rw-r--r-- 1 gao gao 1285 3月 27 15:05 who1.c
-rwxrwxr-x 1 gao gao 7544 3月 27 15:16 who2
-rw-rw-r-- 1 gao gao 1890 3月 27 15:16 who2.c
-rwxrwxr-x 1 gao gao 7416 3月 17 22:03 who_macos
-rw-r--r-- 1 gao gao 390 3月 17 22:03 who_macos.c
gao@ubuntu:~/c$
每行都包含 7 个字段。
模式(mode) | 每行的第一个字符表示 文件类型。“-”代表普通文件,“d“代表目录,等等。 接下来的 9 个字符表示 文件访问权限,分为读权限、写权限和执行权限,又分别针对 3 种对象:用户、同组用户和其它用户,所以一共需要 9 位来表示。从前面 ls -l 的输出中可以看出,所有文件和目录对所有用户都是可读的,只有文件的所有者才能对文件进行修改,所有用户都有 tail 的执行权限。 |
---|---|
链接数(links) | 链接数指的是该文件被引用的次数。 |
文件所有者(owner) | 指出文件所有者的用户名。 |
组(group) | 指文件所有者所在的组。 |
大小(size) | 第 5 列显示文件的大小。在前面的 ls -l 的输出中,所有的目录大小相等,都是 4096 字节(因系统而异),因为目录所占空间的分配是以块(block)为单位,每个块 512 字节,所以目录的大小径常是相等的。如果是一般文件,size 列显示了文件中数据 的实际 字节数。 |
最后修改时间(last-modified) | 对于较老的文件,只能列出月、日和年。 |
文件名(name) | 文件名。 |
1.2 问题 2:ls -l 是如何 工作的
如何得到文件的信息呢?在键盘上输入:
$man -k file|grep -i information
在有些系统上可以得到有用的参考信息,有些却不可以。因为这些系统使用的术语是文件状态(file status)而不是文件信息(file information)或者文件属性(file properties)来代表文件的各种信息。提取文件状态的系统调用是 stat。
1.3 用 stat 得到文件信息
stat 的工作方式。
stat(name, ptr)
将 name 所指定的文件信息读入一个结构中。
磁盘上的文件有很多属性,如文件大小、文件所有者的 ID 等。如果需要得到文件属性,进程可以定义一个结构 struct stat,然后调用 stat,告诉内核把文件属性放到这个结构中。
stat | |
---|---|
目标 | 得到文件的属性。 |
头文件 | #include <sys/stat.h> |
函数原型 | int result = stat(char *fname, struct stat *bufp) |
参数 | fname 文件名 bufp 指向 buffer 的指针 |
返回值 | -1 遇到错误 0 成功返回 |
stat 把文件 fname 的信息复制到指针 bufp 所指的结构中。下面的代码展示了如何用 stat 来得到文件的大小:
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>int main()
{struct stat infobuf;if( stat( "/etc/passwd", &infobuf ) == -1 )perror( "/etc/passwd" );elseprintf( "The size of /etc/passwd is %d\n", infobuf.st_size );
}
stat 把文件的信息复制到结构 infobuf 中,程序从成员变量 st_size 中读到文件大小。
1.4 stat 提供的其他信息
stat 的联机帮助和头文件 /usr/include/sys/stat.h 描述了 struct stat 的成员变量:
st_mode | 文件类型和许可权限 |
---|---|
st_uid | 用户所有者的 ID |
st_gid | 所属组的 ID |
st_size | 所占的字节数 |
st_nlink | 文件链接数 |
st_mtime | 文件最后修改时间 |
st_atime | 文件最后访问时间 |
st_ctime | 文件属性最后改变的时间 |
stat 结构中其他未被 ls -l 用到的成员变量未在这里列出。下面的例子 fileinfo.c 得到以上这些属性并显示出来。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>void show_stat_info( char *fname, struct stat *buf );int main(int ac, char *av[])
{struct stat info;if( ac>1 )if( stat( av[1], &info) != -1 ){show_stat_info( av[1], &info );return EXIT_SUCCESS;}elseperror( av[1] );return EXIT_FAILURE;
}void show_stat_info( char *fname, struct stat *buf )
{printf( " mode:%o\n", buf->st_mode);printf( " links:%d\n", buf->st_nlink);printf( " user:%d\n", buf->st_uid);printf( " group:%d\n", buf->st_gid);printf( " size:%ld\n", buf->st_size);printf( " modtime:%ld\n", buf->st_mtime);printf( " name:%s\n", fname);
}
编译并运行 fileinfo,并把它跟 ls -l 作对比:
gao@ubuntu:~/c$ gcc fileinfo.c -o fileinfo
gao@ubuntu:~/c$ ./fileinfo fileinfo.cmode:100664links:1user:1000group:1000size:680modtime:1648371939name:fileinfo.cgao@ubuntu:~/c$ ls -l fileinfo.c
-rw-rw-r-- 1 gao gao 680 3月 27 17:05 fileinfo.c
1.5 如何实现
链接数(links)、文件大小(size)的显示都没有问题,最后修改时间(modtime)是 time_t 类型的,可以用 ctime 将其转化成字符串,也没有问题。
fileinfo 将模式(mode)字段以数字形式输出,然而需要的是如下的形式:
-rw-rw-r--
结构中的用户所有者(user)和组(group)字段都是数值,而显示出来的应该是用户名和组名,为了完善 ls -l,必须进一步处理模式、用户名和组的显示。
1.6 将模式字段转换成字符
文件类型和许可权限是如何存储在 st_mode 中呢?又如何将它们转成 10 个字符的串?八进制 100664 又与 “-rw-rw-r–"有什么关系呢?对这 3 个问题的回答就构成了本节的内容。
st_mode 是一个 16 进制的二进制数,文件类型和权限被编码在这个数中,如图 1.6.1 所示。
其中前 4 位用作文件类型,最多可以标识 16 种类型,目前已经使用了其中的 7 个。
接下来的 3 位是文件的特殊属性,1 代表具有某个属性,0 代表没有,这 3 位分别是 set-user-ID 位、set-group-ID 位和 sticky 位,它们的含义以后介绍。
最后的 9 位是许可权限,分为 3 组,对应 3 种用户,它们是文件所有者、同组用户和其他用户。其他用户指与用户不在同一个组的人。每组 3 位,分别是读、写和执行的权限。相应的地方如果是 1,就说明该用户拥有对应的权限,0 代表没有。
- 字段的编码
把多种信息编码到一个整数不同字段中是一种常用的技术,如:
编码的例子 | |
---|---|
617-495-4204 | 电话号码(区号-局号-线号) |
027-93-111 | 社会保障号 |
128.103.33.100 | IP 地址 |
- 如何读取被编码的值
怎么来读取被编码的值呢?比如怎么知道 212-222-4444 所对应的区号是 212?很简单,一种方法是将号码的值前 3 位同 212 比较,另一种方法是将暂时不需要的地方置 0,这里把电话号码的后 7 位置 0,然后同 212-000-0000 比较。
为了比较,把不需要的地方置 0,这种技术称为掩码(masking),就如同带上面具把其他部位都遮起来,就只留下眼睛在外面。这里用一系统掩码来把 st_mode 的值转化成 ls -l 要显示 的字符串。
子域编码(subfield coding)是系统编程中一种重要且常用的技术,以下四方面详细介绍子域编码与掩码。
(1) 掩码的概念
掩码会将不需要的字段置 0,需要的字段和值不发生改变。
(2) 整数是 bit 组成的序列
整数在计算机中是以 bit 序列的形式存在的,图 1.6.2 显示了如何以二进制的 0 和 1 的串来表示十进制的215。想一下 00011010 表示十进制的几?
(3) 掩码技术
与 0 作位与(&)操作可以将相应的 bit 置为 0,图 1.6.3 是八进制的 1000664 通过位与操作把一些 bit 置为 0。注意,数字中的某些 1 是如何被置为 0 的。
(4) 使用八进制数
直接处理二进制数是很枯燥乏味的。如同处理一长串十进制数时人们常将它们三位一组分开(如 23,234,456,022)一样,一种简化的方法是将二进制数每三位分为一组来操作,这就是八进制数(0 至 7)。
如可以把二进制的 1000000110110100 分为 1,000,000,110,110,100,从而得到八进制的 100664,这样更容易理解。
- 使用掩码来解码得到文件类型
文件类型在模式字段的第一个字节的前四位,可以通过掩码来将其他的部分置 0,从而得到类型的值。
在<sys/stat.h>中有以下定义:
#define S_IFMT 00170000
#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
#define S_ISUID 0004000
#define S_ISGID 0002000
#define S_ISVTX 0001000
S_IFMT 是一个掩码,它的值是 0170000,可以用来过滤出前四位表示 的文件类型。S_IFREG 代表普通文件,值是 0100000,S_IFDIR 代表目录文件,值是 0040000 。下面的代码:
if( (info.st_mode & 0170000) == 0040000 )printf( "this is a directory" );
通过掩码把其他无关的部分置 0,再与表示目录代码比较,从而判断这是否是一个目录。
更简单的方法是用<sys/stat.h>中的宏来代替上述代码:
#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK)
#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
#define S_ISCHR(m) (((m) & S_IFMT) == S_IFCHR)
#define S_ISBLK(m) (((m) & S_IFMT) == S_IFBLK)
#define S_ISFIFO(m) (((m) & S_IFMT) == S_IFIFO)
#define S_ISSOCK(m) (((m) & S_IFMT) == S_IFSOCK)
使用宏的话就可以这样写代码:
if( S_ISDIR( info.st_mode ))printf( "this is a directory" );
- 解码得到许可权限
模式字段的最低 9 位是许可权限,它标识了文件所有者、组用户、其他用户的读、写、执行权限。ls 将这些位转换为短横和字母的串。在<sys/stat.h>中每一位都有相应的掩码,下面的代码给出了如何使用的例子:
void mode_to_letters(int mode, char str[] )
{strcpy( str, "----------" ); /* default = no perms */if( S_ISDIR(mode) ) str[0] = 'd'; /* directory? */if( S_ISCHR(mode) ) str[0] = 'c'; /* char devices */if( S_ISBLK(mode) ) str[0] = 'b'; /* block device */if( mode & S_IRUSR ) str[1] = 'r'; /* 3 bits for user */if( mode & S_IWUSR ) str[2] = 'w';if( mode & S_IXUSR ) str[3] = 'x';if( mode & S_IRGRP ) str[4] = 'r'; /* 3 bits for group */if( mode & S_IWGRP ) str[5] = 'w';if( mode & S_IXGRP ) str[6] = 'x';if( mode & S_IROTH ) str[7] = 'r'; /* 3 bits for other */if( mode & S_IWGRP ) str[5] = 'w';if( mode & S_IXGRP ) str[6] = 'x';
}
- 解码并编写 ls
到此为止,已经可以正确处理文件大小、链接数、文件名、模式、最后修改时间。最后还有一个要解决的问题是文件所有者(user)和组(group)的表示。
1.7 将用户/组 ID 转换成字符串
在 struct stat 中,文件所有者和组是以 ID 的形式存在的,然而 ls 要求输出用户名和组名,如何根据 ID 找到用户名和组名呢?
可以试着在联机帮助中查找关键字 username、uid、group,看看有什么结果。不同的系统中得到的结果很不同。下面是一些说明:
(1) /etc/passwd 包含用户列表
回想一下登录过程,输入用户名和密码,经过验证后登录成功,出现提示符。系统怎么知道用户名和密码是否正确的?
这就涉及到 /etc/passwd 这个文件,它包含了系统中所有的用户信息,下面是一个例子:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
这是个纯文本文件,每一行代表一个用户,用冒号“:”分成不同的字段,第一个字段是用户名,第二个字段是密码,第三个字段是用户 ID,第四个字段是所属的组,接下来的是用户全名、主目录、用户使用 shell 程序的路径。所有的用户对这个文件都有读权限,关于这个文件的详细信息,参见联机帮助。
似乎使用这个文件就可以解决用户 ID 和用户名的关联问题,只需搜索用户 ID,然后就可以得到相应的用户名。然而实际应用中并不是这样做的,搜索文件是一件很繁琐的工作,而且对于很多网络计算系统,这种方法是不起作用的。
(3) 通过 getpwuid 来得到完整的用户列表
可以通过库函数 getpwuid 来访问用户信息,如果用户信息保存在 /etc/passwd 中,那么 getpwuid 会查找 /etc/passwd 的内容,如果用户信息在 NIS 中,getpwuid 会从 NIS 中获取信息,所以用 getpwuid 使程序有很好的可移植性。
getpwuid 需要 UID(user ID)作为参数,返回一个指向 struct passwd 的指针,这个结构定义在 /usr/include/pwd.h 中:
/* The passwd structure. */
struct passwd
{char *pw_name; /* Username. */char *pw_passwd; /* 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. */
};
这正是 ls -l 的输出中需要的:
char *uid_to_name( uid_t uid )
{return getpwuid( uid )->pw_name;
}
这段代码很简单,但还不够健壮,如果 uid 不是一个合法的用户 ID,那 getpwuid 返回空指针 NULL,这时 getpwuid(uid)->pw_name 就没有意义,这种情况会发生吗?常用的 ls 命令有一种处理这种情况的办法。
(4) UID 没有对应的用户名
假设在一台 Unix 主机上有一个账号,用户名是 pat,用户 ID 是 2000,创建了一个文件,这个文件的 st_uid 的值就是 2000 。
标准的 ls 如果遇到这种情况,会打印出 UID 。
当新加入一个用户时,新用户有可能与一个已被删除的用户相同的 UID,这时,老用户所留下来的文件会被用户所拥有,新用户对这些文件有所有的权限。
最后一个问题是组 ID 如何处理?什么是组?什么是组 ID?
(5) /etc/group 是组的列表
对一台公司里的主机而言,可能要将用户分为不同的组,如销售人员一组、行政人员一组等。要是在学校里,可能有老师组和学生组。Unix 提供了进行组管理的手段,文件 /etc/group 是一个保存所有的组信息的文本文件:
root:x:0:
daemon:x:1:
bin:x:2:
sys:x:3:
adm:x:4:syslog,gao
tty:x:5:
disk:x:6:
lp:x:7:
cdrom:x:24:gao
sudo:x:27:gao
dip:x:30:gao
plugdev:x:46:gao
gao:x:1000:
第一字段是组名,第二个是组密码,这个字段极少用到,第三个是组 ID(GID),第四个是组中的成员列表。
(6) 用户可以同时属于多个组
passwd 文件中有每个用户所属的组,实际上那里列出的用户的主组(primary group)。用户还可以是其他组的成员,要将用户添加到组中,只要把它的用户名添加到 /etc/group 中这个组所在行的最后一个字段即可。在刚才的例子中,用户 gao 同时属于 adm、gao、cdrom、sudo 等多个组 。这个列表在处理组访问权限时会被用到。例如一个文件属于 dip 组,且组成员有这个文件的写权限,所以用户 gao 就可以修改这个文件。
(7) 通过 getgrgid 来访问组列表
在网络计算系统中,组信息也被保存在 NIS 中。Unix 系统提供 getgrgid 函数屏蔽掉实现的差异。用这个函数,用户可以得到组名而不用操心实现的细节。getgrgid 的用户手册对这个函数及相关函数做了详细解释。在 ls -l 中,可以这样得到组名:
char *gid_to_name( gid_t gid)
{return getgrgid(gid)->gr_name;
}
1.8 编写 ls1.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <pwd.h>
#include <grp.h>
#include <time.h>
#include <string.h>void do_ls( char[] );
void dostat( char * );
void show_file_info( char *, struct stat * );
void mode_to_letters( int, char[] );
char *uid_to_name( uid_t );
char *gid_to_name( gid_t ); int main(int ac, char *av[])
{if( ac == 1 )do_ls( "." );elsewhile( --ac ){printf( "%s:\n", *++av);do_ls( *av );}}/** list files in directory called dirname*/
void do_ls( char dirname[] )
{DIR *dir_ptr; /* the directory */struct dirent *direntp; /* each entry */if( (dir_ptr = opendir( dirname )) == NULL )fprintf( stderr, "ls: cannot open %s\n", dirname );else{while( (direntp = readdir( dir_ptr )) != NULL )dostat( direntp->d_name );closedir( dir_ptr );}
}void dostat( char *filename )
{struct stat info;if( stat( filename, &info) == -1 )perror( filename );elseshow_file_info( filename, &info );
}void show_file_info( char *filename, struct stat *info_p )
{/* char *uid_to_name(), *ctime(), *gid_to_name(), *filemode();void mode_to_letters(); */char modestr[11];mode_to_letters( info_p->st_mode, modestr );printf( "%s", modestr );printf( "%4d ", (int)info_p->st_nlink );printf( "%-8s ", uid_to_name(info_p->st_uid) );printf( "%-8s ", gid_to_name(info_p->st_gid) );printf( "%8ld ", (long)info_p->st_size );printf( "%.12s ", 4 + ctime( &info_p->st_mtime) );printf( "%s\n", filename);
}void mode_to_letters(int mode, char str[] )
{strcpy( str, "----------" ); /* default = no perms */if( S_ISDIR(mode) ) str[0] = 'd'; /* directory? */if( S_ISCHR(mode) ) str[0] = 'c'; /* char devices */if( S_ISBLK(mode) ) str[0] = 'b'; /* block device */if( mode & S_IRUSR ) str[1] = 'r'; /* 3 bits for user */if( mode & S_IWUSR ) str[2] = 'w';if( mode & S_IXUSR ) str[3] = 'x';if( mode & S_IRGRP ) str[4] = 'r'; /* 3 bits for group */if( mode & S_IWGRP ) str[5] = 'w';if( mode & S_IXGRP ) str[6] = 'x';if( mode & S_IROTH ) str[7] = 'r'; /* 3 bits for other */if( mode & S_IWGRP ) str[5] = 'w';if( mode & S_IXGRP ) str[6] = 'x';
}char *uid_to_name( uid_t uid )
{/* struct passwd *getpwuid(), *pw_ptr;*/struct passwd *pw_ptr;static char numstr[10];if( (pw_ptr = getpwuid( uid )) == NULL ){sprintf( numstr, "%d", uid );return numstr;}elsereturn pw_ptr->pw_name;
}char *gid_to_name( gid_t gid )
{struct group *getgrgid(), *grp_ptr;static char numstr[10];if( (grp_ptr = getgrgid(gid)) == NULL ){sprintf( numstr, "%d", gid );return numstr;} elsereturn grp_ptr->gr_name;}
将 ls1 的输出与标准的 ls 对比:
gao@ubuntu:~/c$ ./ls1
-rw-r--r-- 1 gao gao 390 Mar 17 22:03 who_macos.c
drwxrwxr-- 3 gao gao 4096 Mar 27 18:53 .
-rw-r--r-- 1 gao gao 7548 Mar 27 14:53 copy.of.cp1
-rw-r--r-- 1 gao gao 1285 Mar 27 15:05 who1.c
-rw-rw-r-- 1 gao gao 244 Mar 27 16:51 filesize.c
-rwxrwxr-- 1 gao gao 7544 Mar 27 15:16 who2
-rwxrwxr-- 1 gao gao 12052 Mar 27 18:53 ls2
drwxrwxr-- 2 gao gao 4096 Mar 27 16:34 dir
-rw-rw-r-- 1 gao gao 1037 Mar 27 14:52 cp1.c
-rw-rw-r-- 1 gao gao 2698 Mar 27 18:53 ls2.c
-rwxrwxr-- 1 gao gao 7408 Mar 27 17:05 fileinfo
-rw-rw-r-- 1 gao gao 1890 Mar 27 15:16 who2.c
-rwxrwxr-- 1 gao gao 7376 Mar 27 16:51 filesize
-rwxrwxr-- 1 gao gao 7392 Mar 27 16:11 ls1
-rw-rw-r-- 1 gao gao 754 Mar 27 16:11 ls1.c
-rwxrwxr-- 1 gao gao 7488 Mar 17 22:12 who1
-rwxrwxr-- 1 gao gao 7548 Mar 27 14:53 cp1
drwxr-xr-- 8 gao gao 4096 Mar 27 16:01 ..
-rw-rw-r-- 1 gao gao 680 Mar 27 17:05 fileinfo.c
-rwxrwxr-- 1 gao gao 7416 Mar 17 22:03 who_macos
留给大家的问题
ls1 的输出看起来已经很不错了,模式字段、用户名和组名的处理已经完成。但还有些工作要做。标准的 ls 会显示记录总数,ls1 不会,而且 ls1 还没将结果按文件名排序,也不支持选项 -a 。它还假设参数是目录。
ls1 还有一个致命问题,不能显示指定目录的信息,你可以试一试,在命令行输入: ls1 /tmp 。这些留给大家来修正吧。(_)/