文件和目录操作的系统函数
本节简要介绍一下文件和目录操作常用的系统函数,常用的文件操作命令如ls
、cp
、mv
等也是基于这些函数实现的。本节的侧重点在于讲解这些函数的工作原理,而不是如何使用它们,理解了实现原理之后再看这些函数的用法就很简单了,请读者自己查阅Man Page了解其用法。
stat(2)
函数读取文件的inode,然后把inode中的各种文件属性填入一个struct stat
结构体传出给调用者。stat(1)
命令是基于stat
函数实现的。stat
需要根据传入的文件路径找到inode,假设一个路径是/opt/file
,则查找的顺序是:
-
读出inode表中第2项,也就是根目录的inode,从中找出根目录数据块的位置
-
从根目录的数据块中找出文件名为
opt
的记录,从记录中读出它的inode号 -
读出
opt
目录的inode,从中找出它的数据块的位置 -
从
opt
目录的数据块中找出文件名为file
的记录,从记录中读出它的inode号 -
读出
file
文件的inode
还有另外两个类似stat
的函数:fstat(2)
函数传入一个已打开的文件描述符,传出inode信息,lstat(2)
函数也是传入路径传出inode信息,但是和stat
函数有一点不同,当文件是一个符号链接时,stat(2)
函数传出的是它所指向的目标文件的inode,而lstat
函数传出的就是符号链接文件本身的inode。
access(2)
函数检查执行当前进程的用户是否有权限访问某个文件,传入文件路径和要执行的访问操作(读/写/执行),access
函数取出文件inode中的st_mode
字段,比较一下访问权限,然后返回0表示允许访问,返回-1表示错误或不允许访问。
chmod(2)
和fchmod(2)
函数改变文件的访问权限,也就是修改inode中的st_mode
字段。这两个函数的区别类似于stat
/fstat
。chmod(1)
命令是基于chmod
函数实现的。
chown(2)
/fchown(2)
/lchown(2)
改变文件的所有者和组,也就是修改inode中的User
和Group
字段,只有超级用户才能正确调用这几个函数,这几个函数之间的区别类似于stat
/fstat
/lstat
。chown(1)
命令是基于chown
函数实现的。
utime(2)
函数改变文件的访问时间和修改时间,也就是修改inode中的atime
和mtime
字段。touch(1)
命令是基于utime
函数实现的。
truncate(2)
和ftruncate(2)
函数把文件截断到某个长度,如果新的长度比原来的长度短,则后面的数据被截掉了,如果新的长度比原来的长度长,则后面多出来的部分用0填充,这需要修改inode中的Blocks
索引项以及块位图中相应的bit。这两个函数的区别类似于stat
/fstat
。
link(2)
函数创建硬链接,其原理是在目录的数据块中添加一条新记录,其中的inode号字段和原文件相同。symlink(2)
函数创建一个符号链接,这需要创建一个新的inode,其中st_mode
字段的文件类型是符号链接,原文件的路径保存在inode中或者分配一个数据块来保存。ln(1)
命令是基于link
和symlink
函数实现的。
unlink(2)
函数删除一个链接。如果是符号链接则释放这个符号链接的inode和数据块,清除inode位图和块位图中相应的位。如果是硬链接则从目录的数据块中清除一条文件名记录,如果当前文件的硬链接数已经是1了还要删除它,就同时释放它的inode和数据块,清除inode位图和块位图中相应的位,这样就真的删除文件了。unlink(1)
命令和rm(1)
命令是基于unlink
函数实现的。
rename(2)
函数改变文件名,需要修改目录数据块中的文件名记录,如果原文件名和新文件名不在一个目录下则需要从原目录数据块中清除一条记录然后添加到新目录的数据块中。mv(1)
命令是基于rename
函数实现的,因此在同一分区的不同目录中移动文件并不需要复制和删除文件的inode和数据块,只需要一个改名操作,即使要移动整个目录,这个目录下有很多子目录和文件也要随着一起移动,移动操作也只是对顶级目录的改名操作,很快就能完成。但是,如果在不同的分区之间移动文件就必须复制和删除inode和数据块,如果要移动整个目录,所有子目录和文件都要复制删除,这就很慢了。
readlink(2)
函数读取一个符号链接所指向的目标路径,其原理是从符号链接的inode或数据块中读出保存的数据,这就是目标路径。
mkdir(2)
函数创建新的目录,要做的操作是在它的父目录数据块中添加一条记录,然后分配新的inode和数据块,inode的st_mode
字段的文件类型是目录,在数据块中填两个记录,分别是.
和..
,由于..
表示父目录,因此父目录的硬链接数要加1。mkdir(1)
命令是基于mkdir
函数实现的。
rmdir(2)
函数删除一个目录,这个目录必须是空的(只包含.
和..
)才能删除,要做的操作是释放它的inode和数据块,清除inode位图和块位图中相应的位,清除父目录数据块中的记录,父目录的硬链接数要减1。rmdir(1)
命令是基于rmdir
函数实现的。
opendir(3)
/readdir(3)
/closedir(3)
用于遍历目录数据块中的记录。opendir
打开一个目录,返回一个DIR *
指针代表这个目录,它是一个类似FILE *
指针的句柄,closedir
用于关闭这个句柄,把DIR *
指针传给readdir
读取目录数据块中的记录,每次返回一个指向struct dirent
的指针,反复读就可以遍历所有记录,所有记录遍历完之后readdir
返回NULL
。结构体struct dirent
的定义如下:
struct dirent { ino_t d_ino; /* inode number */ off_t d_off; /* offset to the next dirent */ unsigned short d_reclen; /* length of this record */ unsigned char d_type; /* type of file */ char d_name[256]; /* filename */ };
这些字段和图 29.6 “根目录的数据块”基本一致。这里的文件名d_name
被库函数处理过,已经在结尾加了'\0',而图 29.6 “根目录的数据块”中的文件名字段不保证是以'\0'结尾的,需要根据前面的文件名长度字段确定文件名到哪里结束。
下面这个例子出自[K&R],作用是递归地打印出一个目录下的所有子目录和文件,类似ls -R
。
例 29.1. 递归列出目录中的文件列表
#include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <dirent.h> #include <stdio.h> #include <string.h> #define MAX_PATH 1024 /* dirwalk: apply fcn to all files in dir */ void dirwalk(char *dir, void (*fcn)(char *)) { char name[MAX_PATH]; struct dirent *dp; DIR *dfd; if ((dfd = opendir(dir)) == NULL) { fprintf(stderr, "dirwalk: can't open %s\n", dir); return; } while ((dp = readdir(dfd)) != NULL) { if (strcmp(dp->d_name, ".") == 0 || strcmp(dp->d_name, "..") == 0) continue; /* skip self and parent */ if (strlen(dir)+strlen(dp->d_name)+2 > sizeof(name)) fprintf(stderr, "dirwalk: name %s %s too long\n", dir, dp->d_name); else { sprintf(name, "%s/%s", dir, dp->d_name); (*fcn)(name); } } closedir(dfd); } /* fsize: print the size and name of file "name" */ void fsize(char *name) { struct stat stbuf; if (stat(name, &stbuf) == -1) { fprintf(stderr, "fsize: can't access %s\n", name); return; } if ((stbuf.st_mode & S_IFMT) == S_IFDIR) dirwalk(name, fsize); printf("%8ld %s\n", stbuf.st_size, name); } int main(int argc, char **argv) { if (argc == 1) /* default: current directory */ fsize("."); else while (--argc > 0) fsize(*++argv); return 0; }