大纲
目录和文件
获取文件属性
stat函数
int stat(const char *pathname, struct stat *statbuf); //通过文件名
int fstat(int fd, struct stat *statbuf); //通过文件描述符
int lstat(const char *pathname, struct stat *statbuf); //在符号链接文件上处理不同
函数从pathname/fd这个文件中获取信息,存入statbuf结构体中
stat结构体内容如下:
struct stat {
dev_t st_dev; //包含当前设备的文件的ID
ino_t st_ino; //inode
mode_t st_mode; //权限信息rwx
nlink_t st_nlink; //硬链接数
uid_t st_uid; //userID
gid_t st_gid; //groupID
dev_t st_rdev; //设备的ID
off_t st_size; //大小(单位字节)
blksize_t st_blksize; //一个blocksize有多大,一般是512B
blkcnt_t st_blocks; //当前这个文件占了多少个512字节的块
};
注意这里的size并不指文件的大小,只是一个属性,比如写一个10G的空洞文件,size是10G,但是占的空间只有几M。实际大小由blksize和blocks决定。
返回值
三个函数都是成功返回0,失败返回-1
文件类型
stat结构体中的st_mode是个16位的位图,其中有文件类型的信息、文件访问权限以及特殊权限位,文件类型有dcb-lsp这七种,有七个宏用来看传入的st_mode是什么类型的文件:
S_ISREG(m) is it a regular file?
S_ISDIR(m) directory?
S_ISCHR(m) character device?
S_ISBLK(m) block device?
S_ISFIFO(m) FIFO (named pipe)?
S_ISLNK(m) symbolic link?
S_ISSOCK(m) socket?
还有一个方法是让st_mode和另一个16位全1的位图相与,就可以得到st_mode实际是什么文件
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;
}
文件访问权限
每个文件有9个访问权限位,可将它们分成3类
S_IRUSR 用户读
S_IWUSR 用户写
S_IXUSR 用户执行
S_IRGRP 组读
S_IWGRP 组写
S_IXGRP 组执行
S_IROTH 其他读
S_IWOTH 其他写
S_IXOTH 其他执行
普通文件的访问权限
- 普通文件的读权限决定了能否打开该文件进行读操作,与open函数的O_RDONLY和O_RDWR标识相关;
- 普通文件的写权限决定了能否打开该文件进行写操作,与open函数的O_WRONLY和O_RDWR标识相关;
- 对一个文件指定open函数的O_TRUNC标志,表示文件必须具有写权限;
- 普通文件的执行权限意味着我们可以通过运行该文件开始一个进程,可用6个exec函数中的任一个来执行该文件。
目录文件的访问权限
- 对目录的读权限和执行权限的意义是不同的,读权限允许我们读目录,获得在该目录中的所有文件名的列表;
- 如果一个目录是我们要访问文件的路径名的一部分,则对该目录的执行权限制使我们通过这个目录
- 如果想在目录下创建文件,则目录必须同时具有写权限和执行权限
- 如果想删除目录下的文件,这目录也必须同时具有写权限和执行权限,反而文件本身则不需要有这些权限。
内核访问权限测试
进程每次打开、创建或删除一个文件时内核就会进行文件访问权限测试,测试顺序如下:
- 进程的effective user id为0,则允许访问
- 进程的effective user id等于文件的所有者id,并且所有者的
适当的访问权限位
被设置才允许访问,否则拒绝访问。适当的访问权限位
指若进程为读(写、执行)而打开文件,则用户读(写、执行)位应为1 - 若进程的有效组ID或附加组ID之一等于文件的组ID,并且文件的组适当的访问权限位被设置,则允许访问
- 若文件的其他权限位被适当设置,则允许访问
umask
创建文件后的默认属性是0666 & ~umask的值
举例
0666: 000 110 110 110
~umask(0022): 111 111 101 101
0666 & ~umask:000 110 100 100
即默认属性是rw-r–r--
umask的作用是防止产生权限过松的文件
文件权限更改:chmod、fchmod
命令的使用看chmod
在编程过程中需要临时修改一个文件的权限,可以使用函数的chmod。但是为了改变一个文件的权限位,进程的effective user id必须等于文件的所有者ID,或者该进程必须具有超级用户权限。
int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
//使用样例
struct stat statbuf;
stat("foo", &statbuf);
chmod(filename, (stat.st_mode & ~S_IXGRP) | S_ISGID);
chmod(fd, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
粘着位(t位)
原本用于给二进制执行文件在内存中保留其使用痕迹,现在cache的出现使得用处已经逐渐不大。现在常用于给目录设置t位
文件系统
- 自举块:也称为引导块,分区中文件系统自身引导程序存放的地方。超级块:超级块在每个文件系统的根上,超级块描述和维护文件系统的状态
- i节点包含了文件的所有信息,包含文件的权限位信息,文件类型文件长度等。只有两项重要数据存放在目录项中:文件名和i节点编号
上图中两个目录项指向同一个i节点,这就是马上要讲的硬链接。每个i节点都有一个链接计数,值是指向该i节点的目录项数。只有当链接计数减少为0才可以删除该文件。这就是为什么解除对一个文件的链接并不总是意味着释放该文件占用的磁盘块的原因。这也是为什么删除一个目录项的函数被称为unlink而不是delete的原因。
目录项中的i节点编号指向同一个文件系统中的相应i节点,一个目录项不能指向另一个文件系统的i节点。这就是为什么ln指令不能跨越文件系统的原因。
在不更换文件系统的情况下为一个文件重命名时,该文件的实际内容并未移动,只需构造一个指向现有i节点的新目录项,并删除老目录项。
然后说明目录文件的链接计数字段,如果我们创建了一个目录文件testdir:
编号2549的类型字段表示它是个目录,链接计数是2。任何一个叶目录(不包含其他目录的目录)的链接计数总是2,原因看图上i节点数组,2549有两个指针指向,一个由自己指向,另一个是自己里面的.指向。现在可以想想1267节点目录的链接计数是多少,答案是至少有3个,图上有一个没标出,就是1267自己指向自己,另外两个一个是2549目录的…指向,另一个是1267目录的.指向。所以可以看到父目录中的每一个子目录都使该父目录的链接计数加1
硬链接和符号链接
硬链接:用一个不同的名字关联源文件的inode,类似两个指针指向同一块内存。如果把源文件删了,另一个还能继续使用,只有链接计数归0才会真正删除文件。建立硬链接有限制:不能给分区建立,不能给目录建立
符号链接:相当于windows中的快捷方式,本身是独立的文件,拥有自己的inode,只不过文件内容是源文件的路径。如果删除源文件会导致符号链接失效。引入符号链接是为了避开硬链接的两个限制:
- 硬链接通常要求链接和文件位于同一文件系统
- 只有超级用户才能创建指向目录的硬链接
也就是说任何用户都可以让符号链接指向目录。符号链接一般用于将一个文件或整个目录结构移到系统中的另一个位置。当使用以名字引用文件的函数时要了解这个函数能不能处理符号链接,也就是能不能跟随符号链接到它所链接的文件上去
比如之前讲过的stat函数,其中有个lstat:
int lstat(const char *pathname, struct stat *statbuf);
如果给lstat函数传入的是符号链接文件,则获取的是符号链接文件的属性。而给stat传入符号链接文件,得到的是所指向的目标文件的属性。与之类似的还有lchown、readlink、remove、rename和unlink
创建硬链接使用link函数实现的:
int link(const char *oldpath, const char *newpath);
//创建newpath链接指向oldpath
还有个函数unlink:
int unlink(const char *pathname);
这个命令从文件系统中中删除一个名字,如果没有进程处于打开这个文件的状态,并且这个名字是指向这个文件的最后一个链接,则删除这个文件,释放这个文件占用的空间。如果有进程打开这个文件,则要等到进程关闭这个文件之后才删除。如果这个名字指向一个符号链接,则删除这个符号链接。
小技巧:可以在open/creat一个文件后立刻unlink,因为文件处于打开状态不会被删除,在进程关闭该文件或终止时删除该文件(当然肯定要看链接数是否为0)。
目录的创建和销毁:mkdir、rmdir(只能删除空目录)
更改当前工作路径
cd是用chdir函数进行实现
int chdir(const char *path);
int fchdir(int fd);
传入想切换去的路径
还有个函数用来获取当前工作路径
char *getcwd(char *buf, size_t size);
分析目录/读取目录内容
glob
用于Linux文件系统中路径名称的模式匹配,即查找文件系统中指定模式的路径。
int glob(const char *pattern, int flags,
int (*errfunc) (const char *epath, int eerrno),
glob_t *pglob);
pattern是个通配符,描述一个文件路径的模式。
flag是或一些符号信息,有以下几个常用的:
- GLOB_NOSORT
按照pattern找出来的文件默认按照字典序排序。添加这个flag可以让他不排序 - GLOB_NOCHECK
如果按照pattern什么都没找到,就返回pattern本身 - GLOB_APPEND
将匹配到的内容追加到pglob后
第三个参数是一个函数指针,如果glob函数解析出错,将会把出错的路径和eerrno都传入这个函数。
第四个参数是一个结构体,内容如下:
typedef struct {
size_t gl_pathc; //匹配到的路径数,类似argc
char **gl_pathv; //匹配到的路径,类似argv
size_t gl_offs; /* Slots to reserve in gl_pathv. */
} glob_t;
返回值
成功返回0,否则返回:
- GLOB_NOSPACE
内存不足 - GLOB_ABORTED
读取出错 - GLOB_NOMATCH
没找到匹配
使用样例
#define PAT "/etc/.*"
#if 0
static int errfunc(const char *epath, int eerrno)
{
puts(epath);
fprintf(stderr, "ERROR MSG: %s\n", strerror(eerrno));
return 0;
}
#endif
int main(int argc, char *argv[])
{
glob_t globres;
int err = glob(PAT, GLOB_NOSORT, NULL, &globres);
if (err)
{
printf("ERROR CODE: %d\n", err);
exit(1);
}
for (int i = 0; i < globres.gl_pathc; i++)
puts(globres.gl_pathv[i]);
globfree(&globres); //别忘了释放
exit(0);
}
使用目录相关函数
像操作文件函数一样使用目录的函数
opendir、closedir
通过目录名打开/关闭一个目录流
DIR *opendir(const char *name);
DIR *fdopendir(int fd); //open可以打开目录,也会得到一个描述符
//成功则返回目录流指针,否则返回空
int closedir(DIR *dirp);
readdir
用于读取一个目录
struct dirent *readdir(DIR *dirp);
返回一个结构体指针,存有目录的基本属性,该指针内容如下:
struct dirent {
ino_t d_ino; /* Inode number */
off_t d_off; /* Not an offset; see below */
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 */
};
小练习
利用glob、stat以及一些C字符串函数实现一个简易的du,仅用于计算一个目录及其内部文件总的大小。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <glob.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#define PATHSIZE 1024
static long long mydu(const char *path);
static int pretendLoop(const char *path);
int main(int argc, char *argv[])
{
if (argc < 2)
{
fprintf(stderr, "Usage: %s\n", argv[0]);
exit(1);
}
printf("%lld\n", mydu(argv[1]) / 2); //blocks / 2就是文件大小
exit(0);
}
static long long mydu(const char *path)
{
struct stat statres;
if (lstat(path, &statres) == -1)
{
perror("lstat()");
exit(1);
}
if (!S_ISDIR(statres.st_mode))
return statres.st_blocks;
glob_t globres;
char nextPath[PATHSIZE];
strcpy(nextPath, path);
strcat(nextPath, "/*");
//先利用glob将这个目录下的除.和..以外的文件放入globres中
int err = glob(nextPath, 0, NULL, &globres);
if (err != 0 && err != GLOB_NOMATCH)
{
printf("ERROR CODE1: %d\n", err);
exit(1);
}
strcpy(nextPath, path);
strcat(nextPath, "/.*");
//再将这个目录下.开头的文件(隐藏)放入globres中
err = glob(nextPath, GLOB_APPEND, NULL, &globres);
//注意用GLOB_APPEND附加在globres后
if (err != 0 && err != GLOB_NOMATCH)
{
printf("ERROR CODE2: %d\n", err);
exit(1);
}
long long sum = statres.st_blocks;
for (size_t i = 0; i < globres.gl_pathc; i++)
if (pretendLoop(globres.gl_pathv[i]))
sum += mydu(globres.gl_pathv[i]);
return statres.st_blocks;
}
static int pretendLoop(const char *path)
{
const char *rch = strrchr(path, '/');
if (rch == NULL)
exit(1);
//遇到.和..则不进行递归,否则会陷入无限递归
if (strcmp(rch + 1, ".") == 0 || strcmp(rch + 1, "..") == 0)
return 0;
return 1;
}
再利用目录相关函数重新实现一遍
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <dirent.h>
#define PATHSIZE 1024
static long long mydu(const char *path);
int main(int argc, char *argv[])
{
if (argc < 2)
{
fprintf(stderr, "Usage: %s <filename>", argv[0]);
exit(1);
}
printf("%lld\n", mydu(argv[1]) / 2);
exit(0);
}
static long long mydu(const char *path)
{
struct stat statres;
if (lstat(path, &statres) == -1)
{
perror("lstat()");
exit(1);
}
if (!S_ISDIR(statres.st_mode))
return statres.st_blocks;
char nextPath[PATHSIZE];
long long sum = statres.st_blocks;
strcpy(nextPath, path);
strcat(nextPath, "/");
DIR *dir = opendir(path);
if (dir == NULL)
{
perror("opendir()");
exit(1);
}
struct dirent *pdr = NULL;
while ((pdr = readdir(dir)) != NULL)
{
if (strcmp(pdr->d_name, ".") != 0 && strcmp(pdr->d_name, "..") != 0)
{
char tmpPath[PATHSIZE];
strcpy(tmpPath, nextPath);
strcat(tmpPath, pdr->d_name);
sum += mydu(tmpPath);
}
}
closedir(dir);
return sum;
}