Linux编程实践1---ls命令实现

前言

学习了两周时间的Linux后,对Linux的一些基本命令有了大致的了解,经过两周时间的期末复习后,暑期留校学习小组内接到的第一个任务–实现Linux命令:ls

项目分析

ls是Linux中最常见的命令之一,功能:列出目录内容。

在Linux文件系统中所有的设备都是文件,从硬件设备到程序等等都是文件,包括目录也是文件,而目录这个文件的文件内容就是该目录下的所有文件。

从ls的功能着手,那么ls的命令显然要经过以下阶段:
打开当前目录 —> 获取目录内容 —> (按-option要求的格式)输出目录内容 —> 关闭目录
初学者面临的一些问题就是如何打开目录并执行一系列的操作最后关闭目录,就跟刚开始学c中文件的处理一样,函数库中也提供了一些系统调用函数供我们实现以上功能。

系统调用函数

  1. getcwd() 获取当前目录

#include
char *getcwd(char *buf, size_t size);

getcwd()会将当前的工作目录绝对路径复制到参数buf所指的内存空间,参数size为buf的空间大小。在调用此函数时,buf所指的内存空间要足够大,若工作目录绝对路径的字符串长度超过参数size大小,则返回值为NULL,errno的值为ERANGE。倘若参数buf为NULL,getcwd()会根据参数size的大小自动分配内存(使用malloc),如果size也为0,则getcwd()会根据工作目录绝对路径的字符串长度来决定配置的内存大小。进程可以在使用完此字符串后利用free()来释放空间。
执行成功则将结果复制到参数buf所指向的内存空间,返回自动分配的字符串指针,失败返回NULL.

  1. chdir() 设置工作目录

#include
int chdir(const char *path);
int fchdir(int fd);这里是引用

chdir()将工作目录改为由path指定的目录,fchdir()将工作目录改为由文件描述符fd指定的目录。
执行成功返回0,错误返回-1

  1. 获取目录信息

(1) opendir()

#include
#include
DIR *opendir(const char *name);

opendir()打开参数name指定的目录,并返回DIR*形态的目录流,类似于文件描述符。成功返回目录流,失败返回NULL

(2) readdir()

#include

#include

struct dirent *readdir(DIR *dir);

readdir用来从dir所指向的目录读取出目录的信息,返回一个struct dirent结构的指针。

struct dirent

{

long d_ino;//此目录i节点编号

off_t d_off;//目录文件开头至此目录进入点的位移

unsigned short d_reclen;//d_name的长度

char d_name[NAME_MAX+1];//以NULL结尾的文件名

}

(3) closedir()

#include

#include

int *closedir(DIR *dir);

关闭目录,成功返回0,失败返回-1

  1. stat 获取文件属性

#include

#include

#include

int stat(const char *filename,struct stat *buf);

int fstat(fd, struct stat *buf);

int lstat(const char *filename, struct stat *buf);

fstat与stat的区别是fstat通过文件描述符指定文件;lstat与stat的区别在于,对于符号连接文件,lstat返回的是符号链接文件本身的状态信息,而stat返回的是符号链接指向的文件状态信息。

成功返回0,错误范围-1

struct stat {

mode_t st_mode; //文件对应的模式,文件,目录等

ino_t st_ino; //inode节点号

dev_t st_dev; //设备号码

dev_t st_rdev; //特殊设备号码

nlink_t st_nlink; //文件的连接数

uid_t st_uid; //文件所有者

gid_t st_gid; //文件所有者对应的组

off_t st_size; //普通文件,对应的文件字节数

time_t st_atime; //文件最后被访问的时间

time_t st_mtime; //文件内容最后被修改的时间

time_t st_ctime; //文件状态改变时间

blksize_t st_blksize; //文件内容对应的块大小

blkcnt_t st_blocks; //文件内容对应的块数量

};

知道了上面了几个系统调用函数,对一个目录的基本操作可以概括为
opendir()打开当前工作目录,返回的DIR* 类型指针作为参数传递给readdir()通过循环获取当前目录中所有文件,每次循环由readdir()返回的dirent* 类型指针,可将其成员d_name(文件名)作为参数传递给lstat(),从而获取文件各个属性存储到buf所指向的stat类型中。

ls -l命令实现剖析

下面举例说明ls -l 命令的实现
先参考一下ls -l命令的效果图
Linux编程实践1---ls命令实现_第1张图片
再看一下官方给出的ls -l命令描述
Linux编程实践1---ls命令实现_第2张图片

在-l命令中,需要输出的数据依次从左往右为
文件类型&权限、 硬链接数、 所有者名(usr)、 组名(group)、 大小(byte)、 时间信息(默认为mtime)、 文件名称
还有第一行显示的total即“总块数”

  • 文件类型&权限
    在struct stat类型中,成员st_mode代表了文件类型和权限。而在stat结构体中,st_mode 是mode_t 类型,而非我们想要的是char* 类型的字符串来显示文件类型和权限,所以需要一个转换步骤。下面例子中我们将st_mode转换为char mode[11];

st_mode本身就是一个16位的二进制,前四位是文件的类型,紧接着三位是文件的特殊权限,最后的九位就是ls -l列出来的九个权限。如何把st_mode转换成对应的权限就是权限处理这块的关键了。linux本身提供了很多测试宏来测试文件的类型。

测试文件类型(文件信息在/usr/include/linux/stat.h)
Linux编程实践1---ls命令实现_第3张图片

#define S_IFMT 00170000 / * These bits determine file type. * /

/* File types. * /
#define S_IFSOCK 0140000 / * Socket. * / 在我们想要转换到的char* 类型的权限属性中第一个属性(文件类型)为 [s]
#define S_IFLNK 0120000 / * Symbolic link. * / 第一个属性为 [l]
#define S_IFREG 0100000 / * Regular file. * / 第一个属性为[-]
#define S_IFBLK 0060000 / * Block device. * / 第一个属性为[b]
#define S_IFDIR 0040000 / * Directory. * / 第一个属性为[d]
#define S_IFCHR 0020000 / * Character device. * / 第一个属性为[c]
#define S_IFIFO 0010000 / * FIFO. * / 第一个属性为[p]

// For example
if(S_IFREG & st_mode){
	/* regular file*/
}
// Or
if(S_ISREG(st_mode)){
	/* regular file*/
	mode[0] = '-';
} 
if(S_ISDIR(st_mode)){
	/*Directory*/
	mode[0] = 'd';
}

测试文件权限
Linux编程实践1---ls命令实现_第4张图片

上图第一部分为usr权限,第二部分为group权限,第三部分为other权限
每部分第一个代表可读可写可执行,即rwx;第二行代表可读,即r–;第三行代表可写,即-w-;第四行代表可执行,即–x

// For example
if(S_IRUSR & st_mode){
	/* usr can read file*/
	mode[1] = 'r';
}
if(S_IRGRP & st_mode){
	/* group can read file*/
	mode[4] = 'r';
}
if(S_IROTH & st_mode){
	/* other can read file*/
	mode[7] = 'r';
}

上面只列举了几个例子,完整实现还需自行添加内容。

  • 硬链接数
    硬链接数在结构体stat中的成员st_nlink给出,直接添加即可

  • 所有者名与用户组名
    在stat结构体中,st_uid与st_gid都是数字化的(uid_t 类型与gid_t类型,相当于int),而我们想要的是字符串类型的名称,这里又需要一步处理,先认识几个相关函数。

(1)getpwuid()

#include “pwd.h”
struct passwd *getpwuid(__uid_t __uid);

getpwuid函数接受一个__uid_t(uid_t) 类型的参数,若成功则返回一个passwd结构体类型的指针,失败返回NULL。

Linux编程实践1---ls命令实现_第5张图片

(2)getgrgid()

#include “grp.h”
struct group *getgrgid(__gid_t __gid);

getgrgid接受一个__gid_t(gid_t)类型的参数, 若成功则返回一个group结构体类型的指针,失败返回NULL。

Linux编程实践1---ls命令实现_第6张图片

故此调用getpwuid函数只需将数字化的所有者ID: st_uid作为参数传递给getpwuid,return 该所有者对应的passwd结构体,该结构体中有我们想要的 pw_name就是所有者名。用户组名的获取也一样。下面看示例

struct passwd *puid = NULL ;
puid = getpwuid(st_uid);
char *usr_name = puid->pw_name;

struct group *pgid = NULL;
pgid = getgrgid(st_gid);
char *group_name = pgid->gr_name;
  • 大小
    文件大小直接在stat结构体成员st_size给出

  • 时间信息
    由于默认是mtime,故这里只讨论mtime,在stat结构体中给出的st_mtime是一个time_t类型,是一个时间戳。
    而这里我们需要将时间戳转换为我们通常看到的日期格式。

#include “time.h”
char *ctime(const time_t *timer);

ctime()函数接受一个时间戳参数,返回一个表示当地时间的字符串

返回的字符串格式如下: Www Mmm dd hh:mm:ss yyyy 其中,Www 表示星期几,Mmm 是以字母表示的月份,dd
表示一月中的第几天,hh:mm:ss 表示时间,yyyy 表示年份。

// For example
char *time = ctime(&st_time);

需要注意的是 标准ls -l输出中,日期格式为 Mmm dd hh:mm,所以在ctime函数基础上还应再采取一些措施得到标准输出中的日期格式。(对于6个月以上的文件或超出未来一小时的文件,时间信息中的时分将被年份所取代)

  • 文件名称
    还记得在调用readdir()函数读取目录内容会return 一个dirent结构体的指针类型吗,目录内容即是目录中所存放的文件,所以返回的该结构体指针中的d_name即是这里的文件名称

  • total
    最后来说一下第一行显示的total,先看一下官方给出的total解释
    Linux编程实践1---ls命令实现_第7张图片
    total显示的即是目录下全部文件所占的磁盘空间,但是 total != Sum(st_size),如果total表示的全部文件所占的磁盘空间,那为什么又不等于全部文件大小的和呢?

在Linux文件系统中,每个文件都是以(block)为单位进行存储的。
一个块的大小一般为4096bytes,在Linux中可以使用命令getconf PAGESIZE查看当前文件系统中一个块的大小
需要注意的是:
由于文件以块为单位进行存储,也就是以4096bytes为一个单位进行存储,假设某个文件不够4096bytes,该文件依旧占用一个单位,也就是一个块,即4096bytes.
举个栗子:
某文件实际大小为1089bytes,该文件占用块数为1089/4096 + 1 = 1块 = 1 * 4096bytes,该文件所占磁盘空间为4096bytes = 4K
某文件实际大小为5023bytes,该文件占用块数为5023/4096 + 1 = 2块 = 2 * 4096bytes,该文件所占磁盘空间为4096bytes* 2 = 8192bytes = 8K

通过文件中实际数据计算一下(第一列显示的是该文件所占磁盘空间大小,以K为单位,第6列属性是文件的实际大小,以bytes为单位)
Linux编程实践1---ls命令实现_第8张图片
截取了一部分文件信息,只选取红线框中的两行进行计算,其他可以自行练习用

1.wchar.h
文件实际大小为31111bytes,显然31111/1024所得到的结果并不是32K而大约是30K。
根据上述计算方法:

文件所占磁盘空间 = {文件实际大小/4096 + (文件实际大小%4096)>0? 1 : 0 } 块 * 4096bytes / 1024bytes = m K

31111 / 4096 = 7 —> 31111%4096 > 0 —> 7 +1 = 8块
8块 * 4096bytes = 32768bytes (一个块占4096bytes,8个块即占32768bytes)
30768bytes / 1024bytes = 32K (最后将单位进行转化,1024bytes = 1K)

这样计算即可得到wchar.h实际所占用磁盘空间的大小为32K

2.wctype.h
这里直接写计算式
文件所占磁盘空间 = 5548/4086 + 1 = 2块 = 2 * 4K = 8K

若文件系统中一个块所占字节数为4096bytes,则 1块 = 4096bytes = 4K

知道了单个文件所占用磁盘空间的大小后,total即是将每个文件占用的磁盘空间大小求和。
但是这里不必这么麻烦,在stat结构体中成员st_blocks直接为我们提供了单个文件所占用的磁盘空间

st_blocks
但是在这里提供的st_blocks是以512bytes作为单位,所以在获取到st_blocks后执行st_blocks/=2才是以1024bytes为1K的占用磁盘容量。

任务总结

代码就不粘了,ls -l的实现基本上就到这里。
可以说ls -l的实现是实现ls命令的核心,会了-l,其他的都是些小case。
在这次任务中,还有一些值得注意的错误提示在这里粘出来,也是一点点的小总结和小成长吧。
这次对ls命令的实现实现了 5个基本命令以及它们的二级组合
这5 个基本命令分别是 -a -l -s -F -R

值得一提的是在-R的实现中遇到的一些BUG。
由于-R是要递归输出遇到的所有子目录及其内容,在实现过程中有以下两点需要及其注意,特别是第二点

1.由于递归会调用自己,所以工作目录的切换要及时,有两个步骤
(1) 进入子目录时将当前工作目录设置为子目录(否则当前工作目录仍然会停留在上一级目录,会访问不到子目录下的文件)
(2) 退出子目录(子目录执行完毕)后将当前工作目录设置为子目录的上层目录(否则子目录退出后到达的上层目录将无法再继续访问剩下的文件)
(3) 用到的两个函数
chdir()设置当前工作目录, getcwd()获取当前工作目录,函数具体细节查看本文前面

2 每次对目录的访问除了上一条以外,还需要opendir()以及readdir();
最最重要的是目录访问结束后一定一定要调用closedir()关闭目录,否则会提示 “段错误” “too many open files”…

最后一条让我觉得收获不浅的是 perror函数的使用,它可以打印具体错误信息到屏幕上,这一操作省了很多排错的力气,牛批!
#include “stdio.h”
void perror(const char *str);
该函数把一个描述性错误消息输出到标准错误 stderr。首先输出字符串 str,后跟一个冒号,然后是一个空格,再输出错误信息。
str – 这是 C 字符串,包含了一个自定义消息,将显示在原本的错误消息之前。

就到这里,byebye~

你可能感兴趣的:(Linux编程)