目录
1、获取文件属性
2、文件访问权限
3、umask
4、文件权限管理
4.1 chmod
4.2 fchmod
5、粘住位
6、文件系统举例
6.1 FAT系统
6.2 UFS系统
6.3 补充
7、链接
7.1 硬链接
7.2 符号链接
7.3 相关函数
7.3.1 link
7.3.2 unlink
7.3.3 remove
7.3.4 rename
8、utime
9、目录的创建和销毁
9.1 mkdir
9.2 rmdir
10、工作路径相关
10.1 chdir && fchdir
10.2 getcwd
11、分析目录
11.1 glob
11.2 opendir
11.3 closedir
11.4 readdir
11.5 rewinddir
11.6 seekdir
11.7 telldir
-
目标:代码实现类似 ls 命令的功能
1、获取文件属性
我们希望实现类似 ls 命令的功能,首先要对 LINUX 下的 ls 命令有一定了解
- ls:显示当前路径下的内容
- ls -a:连同隐藏文件一起显示
- ls -i:连同文件的 inode 一起显示
- ls -l:显示文件详细属性(user name 和 group name)
- ls -n:显示文件详细属性(user id 和 group id)
获取文件属性的函数:stat (其实 stat 也被封装成了一个同名 UNIX 命令,能够显示文件属性)
man 2 stat
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *statbuf);
int fstat(int fd, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf); // lstat() is identical to stat(), except that if pathname is a symbolic link, then it returns information about the link itself, not the file that it refers to.
功能:获取文件状态属性
- pathname — 待获取属性的文件
- fd — 表征待获取属性的文件
- 调用成功返回 0;失败返回 -1,并将错误信息转化为某个值设置给全局变量 errno
- statbuf — 指向一块结构体,调用函数会将有关文件的一些属性信息存入这块结构体中
有哪些信息会填入这块结构体呢?struct stat 类型的说明如下:
struct stat {
dev_t st_dev; /* 文件所在的设备编号 */
ino_t st_ino; /* 索引结点编号 */
mode_t st_mode; /* 文件类型和权限*/
nlink_t st_nlink; /* 硬链接数 */
uid_t st_uid; /* 用户ID */
gid_t st_gid; /* 组ID */
dev_t st_rdev; /* 设备类型(若此文件为设备文件,则为设备编号 */
off_t st_size; /* 文件大小,以字节为单位 */
blksize_t st_blksize; /* 文件系统的I/O块大小 */
blkcnt_t st_blocks; /* 块数 */
time_t st_atime; /* 访问(读)时间 */
time_t st_mtime; /* 更改文件数据时间 */
time_t st_ctime; /* 更改文件亚数据时间 */
};
通过这个结构体,可以获取很多和文件有关的信息
代码示例:打印出文件的大小
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
// 注意返回值是off_t类型
static off_t flen(const char *fname) { // const表示不更改文件名
struct stat statres; // 声明一个stat类型的结构体statres
if(stat(fname, &statres) < 0) {
perror("stat()");
exit(1);
}
// 返回st_size成员
return statres.st_size;
}
int main(int argc, char **argv) {
if(argc < 2) {
fprintf(stderr, "Usage...\n");
exit(1);
}
printf("total size: %lld\n", (long long)flen(argv[1]));
exit(0);
}
off_t 类型用于指示文件的偏移量,通常就是 long 类型,其默认为一个 32 位的整数,在 gcc 编译中会被编译为 long int 类型,在 64 位的 Linux 系统中则会被编译为 long long int,这是一个 64 位的整数,其定义在 unistd.h 头文件中可以查看
注意:在描述文件属性的 stat 结构体中,有以下三个描述文件大小的成员
struct stat {
off_t st_size; /* 文件大小,以字节为单位*/
blksize_t st_blksize; /*文件系统的I/O块大小*/
blkcnt_t st_blocks; /* 块数 */
};
其中,块大小一般为4096字节,即4KB(一个块为连续8个扇区,每个扇区为512B);块数为该文件的占用的块数
注意:st_size ≠ st_blksize * st_blocks;或者说,st_size 是文件的逻辑大小(可以看成文件的一个属性),而 st_blksize * st_blocks 是文件实际的物理大小
代码示例:构建一个 st_size ≠ st_blksize * st_blocks 的文件
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char **argv) {
int fd;
if(argc < 2) {
fprintf(stderr, "Usage...");
exit(1);
}
if((fd = open(argv[1], O_WRONLY|O_CREAT|O_TRUNC, 0600)) < 0) {
perror("open");
exit(1);
}
// 先让指针从文件开头向后移动5G-1个字节
lseek(fd, 5LL * 1024LL * 1024LL * 1024LL - 1LL, SEEK_SET);
// 如果上述代码写成 5*1024*1024*1024-1,则编译器可能默认在做int之间的计算,那么计算结果也会被裁切为int,会整型溢出
// 在最后写入一个空字符
write(fd, "", 1);
close(fd);
exit(0);
}
通过该程序创建文件 file,再 stat file
可以看出:文件大小为5368709120B(字节),但是占用的块数却为8,即实际占用的物理大小为4KB * 8 = 32KB(注意这在 UNIX 环境下和 Windows 环境下是不一样的,Windows 系统下,文件大小就等于文件实际占用的物理大小)
这样的文件我们称之为空洞文件
定义:从偏移文件头部 5368709120 字节处开始写入数据,也就意味着 1~5368709119 字节之间出现了一个空洞,因为这部分空间并没有写入任何数据,所以形成了空洞,这部分区域就被称为文件空洞,那么相应的该文件也被称为空洞文件。文件空洞部分实际上并不会占用任何物理空间,直到在某个时刻对空洞部分进行写入数据时才会为它分配对应的物理空间,但是空洞文件形成时,逻辑上该文件的大小(即 st_size)是包含了空洞部分的大小的
拷贝文件的空洞部分时,不会发生 read/write 系统调用,而会直接 lseek 定位到空洞部分的下一位置,继续读取/写入数据
空洞文件的作用
空洞文件对多线程共同操作文件是很有用的,因为我们在创建一个很大文件的时候,我们就把一个文件分成很多的段,然后采用多线程的方式,让每个线程负责写入其中的某一段的数据。这样的话比我们用单个线程写入是快很多的
- 在使用迅雷下载文件时,还未下载完成,就发现该文件已经占据了全部文件大小的空间,这也是空洞文件;下载时如果没有空洞文件,多线程下载时文件就只能从一个地方写入,这就不能发挥多线程的作用了;如果有了空洞文件,可以从不同的地址同时写入,就达到了多线程的优势
- 在创建虚拟机时,你给虚拟机分配了 100G 的磁盘空间,但其实系统安装完成之后,开始也不过只用了 3、4G 的磁盘空间,如果一开始就把 100G 分配出去,资源是很大的浪费
2、文件访问权限
注意:在描述文件属性的 stat 结构体中,有这样一个成员
struct stat {
mode_t st_mode; /* 文件类型和权限*/
};
st_mode是一个16位的位图,用于表示文件类型,文件访问权限以及特殊权限位
它的类型为mode_t,其实就是普通的unsigned int,但是只是用了低16位
那么每一位的含义是什么?
通过查看手册 man 7 inode 可以查看详情
首先,高4位表示文件类型
低9位表示文件的权限
中间3位表示设置用户 ID 位(set-user-ID),设置组 ID 位(set-group-ID),sticky 位
具体什么含义呢?怎么看的呢?下面详解
man 手册中显示的值只是将位图转化为八进制表示罢了
3、umask
umask 存在的意义:防止创建出权限过松的文件
权限公式:mode & ~umask
LINUX 默认的 umask 值:八进制数 0022 (注意第一个0只是说明这个数是八进制,是个符号)
则:
LINUX 默认创建文件的实际权限:0666 & ~umask = 0666 & ~0022 = 0644
LINUX 默认创建文件夹的实际权限:0777 & ~umask = 0777 & ~0022 = 0755
使用 open 系统调用创建文件的实际权限:mode & ~umask
查看和更改系统的 umask 值(一个八进制数,macOS 下把八进制数开头的那个表示其为八进制数的 0 省略了)
上面介绍的 umask 是个命令。这个命令是由一个同名函数封装出来的
查看手册:man 2 umask
#include <sys/stat.h>
mode_t umask(mode_t cmask);
The umask() routine sets the process's file mode creation mask to cmask and returns the previous value of the mask.
4、文件权限管理
4.1 chmod
先看看 chmod 命令
chmod [对谁操作(ugoa)][操作符 (+-=)][赋予的权限(rwxs或数字)] 文件名1 文件名2...
chmod [八进制mode] 文件名1 文件名2...
如果想在一个进程中临时修改某个文件的权限,可以用 chmod 函数
#include <sys/types.h>
#include <sys/stat.h>
int chmod(const char *path, mode_t mode);
- path — 指定文件
- mode — 指定权限(八进制数)
注意,用 chmod 设置的时候,不用考虑 umask
4.2 fchmod
#include <sys/types.h>
#include <sys/stat.h>
int fchmod(int fd, mode_t mode);
- fd — 指定文件为 fd 所关联的文件
- mode — 指定权限
5、粘住位
如果一个可执行程序文件的这一位被设置了,那么在该程序第一次被执行并结束时,其程序正文部分的一个副本仍被保存在交换区,(程序的正文部分是机器指令部分)。这使得下次执行该程序时能较快地将其装入内存中。现今较新的 UNIX 系统大多数都配置有虚拟存储系统以及快速文件系统,所以不再需要使用这种技术。
现今的系统扩展了粘住位的使用范围,允许针对目录设置粘住位。如果对一个目录设置了粘住位,则只有对该目录具有写权限的用户在满足下列之一的情况下,才能删除或更名该目录下的文件或目录:
- 拥有此文件
- 拥有此目录
- 是超级用户
6、文件系统举例
文件系统:用于文件或数据的储存和管理
6.1 FAT系统
本质:静态存储的单链表,即用数组实现的单链表
假设每个结构体中的 data 部分储存特定量的数据,那么如果有一个大文件,单个结构体中的 data 不足以存放这个大文件的所有数据,那么就需要多个结构体来存放这个大文件的数据。
显然,多个结构体之间是按照单链表逻辑进行连接的,访问大文件的数据需要涉及到遍历单链表,故 FAT 系统储存大文件性能不好
注意这只是个超级超级超级简化版本的示意图,了解即可
6.2 UFS系统
UFS 系统的几个组成部分了解一下即可
inode 和数据块示意图如下
上图建议从左上角逆时针看。。。
还有 i 位图和块位图
UFS 和 FAT 不同,UFS 不善于管理小文件
如果是一个小文件,UFS 为文件分配一个 inode 结构体,结构体里的数据块指针数组里面至少会包含一个块指针指向 4KB 大小的块,可是要是这个文件不足 4KB,就会造成数据块空间的浪费。
因此,如果小文件很多,是有可能出现 inode 结构体数组满了,而块还没满的情况。查找结构体中的数据需要遍历很多结构体,比较慢。
6.3 补充
刚刚说的几乎所有和文件有关的信息都在inode结构体中,但不包含文件名
文件的文件名统一存放在目录文件中
某个目录的目录文件存在于该目录下,目录文件的目录项记录了目录中文件的 inode 及其对应的文件名
当用 vim 打开一个目录时,其实就查看到了目录文件的一部分内容
7、链接
7.1 硬链接
创建硬链接的命令
ln src dest # 为src创建名为dest的硬链接
命令示例
如图,stat 查看文件 testFile 的信息,发现链接数为 1,经过创建硬链接后,硬链接和源文件的链接数都变成了 2,但是硬链接和源文件的 inode 号是一样的
因此,硬链接实际上是在当前目录下的目录文件多写了一行,有另外一个名字关联到了和源文件相同的 inode(相当于两个指针指向了同一空间)
即使删除源文件,硬链接文件也能正常用。硬链接本质上就是一个普通的文件,对应着一条目录项
7.2 符号链接
创建符号链接的命令
ln -s src dest # 为src创建名为dest的符号链接
特别像 Windows 下的快捷方式!
命令示例
如图,用 stat 查看符号链接和源文件的信息,关注几个点:
- 符号链接的 inode 与源文件的 inode 不一样
- 符号链接的硬链接数与源文件的硬链接数都是 1
- 源文件的 size 更大,而符号链接的 size 刚好就是源文件名的字节长度
- 符号链接占用磁盘块为 0(UNIX将符号链接文件的属性内容放在了 inode 结构体中,就不占块了)
删除源文件后,符号链接就无法使用了
文件A与文件B的号码虽然不一样,但是文件A的内容是文件B的路径,A就是B的符号链接文件。读取文件A时,系统会自动访问导向文件B的文件名,然后再根据B的inode去访问存储在块中的数据。
而这意味着,文件A依赖于文件B而存在,若删除了文件B,打开文件A就会报错。这就是符号链接与硬链接最大的不同:文件A指向文件B的文件名,而不是inode号码,文件B的inode链接数不会发生变化
硬链接与目录项是同义词,且建立硬连接有限制:不能给分区建立,不能给目录建立
符号链接优点:可跨分区,可以给目录建立
7.3 相关函数
根据过往经验,UNIX 中的命令很多都是由函数封装得到的,刚刚我们用的 ln 命令自然也不例外
后续介绍函数时,需要脑海中想想这些函数调用后,对目录文件进行了怎样的更改
7.3.1 link
man 2 link
#include <unistd.h>
int link(const char *oldpath, const char *newpath);
功能:为已存在的文件创建一个硬链接
- oldpath — 源文件
- newpath — 硬链接文件名
当创建硬链接后,可以说硬链接和源文件的地位是平等的
7.3.2 unlink
man 2 unlink
#include <unistd.h>
int unlink(const char *pathname);
功能:从文件系统删除一个文件名
unlink() deletes a name from the filesystem.
If that name was the last link to a file and no processes have the file open, the file is deleted and the space it was using is made available for reuse.
If the name was the last link to a file but any processes still have the file open, the file will remain in existence until the last file descriptor referring to it is closed.
If the name referred to a symbolic link, the link is removed.
If the name referred to a socket, FIFO, or device, the name for it is removed but processes which have the object open may continue to use it.
通过上述描述,有这么一个创建匿名文件的方式:
- 先调用 open 打开一个文件,获取一个文件描述符
- 立马执行 unlink,从文件系统删除该文件名
此时,该文件所占数据空间其实没有直接被释放,因为还有个打开的文件描述符表征该文件。直到调用 close 关闭该描述符时,数据空间才被真正释放
7.3.3 remove
man 3 remove
#include <stdio.h>
int remove(const char *pathname);
本质上是对 unlink 等函数进行了封装,这个函数是命令 rm 的实现
remove() deletes a name from the filesystem. It calls unlink(2) for files, and rmdir(2) for directories.
7.3.4 rename
man 2 rename
#include <stdio.h>
int rename(const char *oldpath, const char *newpath);
功能:改变文件的名称或者位置
该函数是命令 mv 的实现
8、utime
#include <utime.h>
int utime(const char *path, const struct utimbuf *times);
功能:用于更改文件的最后访问时间和最后修改时间
UNIX 下的三个时间属性:
- mtime(modify time):最后一次修改文件数据内容或目录内容的时间
- ctime(change time) :最后一次改变文件或目录亚数据(即:属性)的时间
- atime(access time):最后一次访问文件或目录的时间
9、目录的创建和销毁
9.1 mkdir
man 2 mkdir
#include <sys/stat.h>
int mkdir(const char *path, mode_t mode);
注意:创建出目录的实际权限同时受 mode 与 umask 约束
9.2 rmdir
man 2 rmdir
#include <unistd.h>
int rmdir(const char *path);
注意:只能删除空目录
10、工作路径相关
10.1 chdir && fchdir
man 2 chdir
#include <unistd.h>
int chdir(const char *path);
int fchdir(int fd);
功能:修改当前进程的工作路径
- path — 指定切换到某个路径
- fd — 表征一个已经被成功打开的路径
- 该函数是命令 cd 的实现,即该函数封装出了命令 cd
10.2 getcwd
man 2 getcwd
#include <unistd.h>
char * getcwd(char *buf, size_t size);
功能:获取当前工作路径
- buf — 指向一片空间,用于储存当前进程工作的绝对路径
- size — buf 所指空间的长度
- 返回值也是 buf
- 该函数封装出了命令 pwd
11、分析目录
11.1 glob
man 3 glob
#include <glob.h>
int glob(
const char *pattern,
int flags,
int (*errfunc) (const char *epath, int eerrno),
glob_t *pglob
);
void globfree(glob_t *pglob); // 释放pglob的成员所指向的堆内存
功能:解析模式/通配符(所谓解析,就是将字符串解析成名字)
- pattern — 通配符,要解析的 pattern,如 "/*" 表示解析出根文件下的所有文件名(不包括隐藏文件)
- flags — flags 参数可以设置解析的特殊要求,如无特殊要求置为 0
- errfunc — 函数指针,glob 函数执行出错会执行的函数,出错的路径会回填到 epath 中,出错的原因回填到 eerrno 中。如不关注错误可设置为NULL
- pglob — 解析出来的结果放在这个参数里,是一个结构体指针
- 返回值 — 成功返回 0,错误返回非 0
其中,glob_t 结构体内容如下:
typedef struct {
// pathc与pathv类似于main函数参数argc与argv
size_t gl_pathc; //匹配到的数量
char **gl_pathv; //匹配到的元素放在这里
size_t gl_offs;
} glob_t;
代码示例:解析路径
使用如下:
UNIX 还提供了一组对目录操作的函数,能够操作目录,和对文件操作的函数基于 FILE 结构体类似,对目录的操作基于名为 DIR 的结构体
11.2 opendir
man 3 oprndir
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
DIR *fdopendir(int fd);
功能:获取目录流
- name — 获取 name 所表征目录的目录流
- fd — 获取已经打开的 fd 所表征目录的目录流
- 成功返回指向目录流 DIR 的指针;失败返回 NULL 并设置 errno
11.3 closedir
man 3 closedir
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);
功能:关闭目录流
opendir 的逆操作,通过 opendir 获取的目录流应该通过 closedir 关闭
11.4 readdir
man 3 readdir
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
功能:从目录流读取下一条目录项
The readdir() function returns a pointer to a dirent structure representing the next directory entry in the directory stream pointed to by dirp. It returns NULL on reaching the end of the directory stream or if an error occurred.
将读取到的目录项内容填充到一个结构体,并返回指向其的指针
返回的结构体指针代表了读取到的目录项,结构体内容如下:
struct dirent {
ino_t d_ino; /* Inode number */
off_t d_off; /* 现代操作系统中不对其内容做任何假设 */
unsigned short d_reclen; /* Length of this record */
unsigned char d_type; /* Type of file; not supported by all filesystem types */
char d_name[256]; /* Null-terminated filename */
};
11.5 rewinddir
man 3 rewinddir
#include <sys/types.h>
#include <dirent.h>
void rewinddir(DIR *dirp);
功能:重置目录流
The rewinddir() function resets the position of the directory stream dirp to the beginning of the directory.
调用该函数后,接下来若再通过 readdir 读取目录流将重新从第一条目录项开始读取
11.6 seekdir
man 3 seekdir
#include <dirent.h>
void seekdir(DIR *dirp, long loc);
功能:设置下一次 readdir 从目录流中读取目录项的起始位置
11.7 telldir
man 3 telldir
#include <dirent.h>
long telldir(DIR *dirp);
功能:返回 readdir 当前读取到目录流的哪个位置
国庆快乐!🎉🎉