目录
在文件系统中,目录的存储方式类似文件。目录与普通文件区别:
- 在其i-node条目中,会将目录标记为一种不同的文件
- 目录是经特殊组织的文件。本质为一个表格,包含文件名和i-node编号
在大多数远程Linux系统中,文件名长度可达255个字符
i-node表的编号始于1, 若目录条目的i-node字段值为0,则表明该条目 尚未使用。i-node字段 1用来记录文件系统的坏块。 文件系统根目录(/)总是存储在i-node条目2中
- 以/etc/passwd为例
硬链接
所有的原生Linux和UNIX文件系统均支持硬链接,然而,许多非UNIX文件系统(比如,微软的VFAT)则不支持。(微软的NTFS文件系统支持硬链接)
root@root-PC:~$ echo -e -n "It is good to collect things, \n" > abc
root@root-PC:~$ ls -li abc
1605455 -rw-r--r-- 2 root root 31 4月 24 13:59 abc
root@root-PC:~$ ls abc xyz
abc xyz
root@root-PC:~$ cat xyz
It is good to collect things,
root@root-PC:~$ echo ' but it is better to go on walks.' >> xyz
root@root-PC:~$ cat abc
It is good to collect things,
but it is better to go on walks.
root@root-PC:~$ ls -li abc xyz
1605455(inode条目) -rw-r--r-- 2(链接计数) root root 65 4月 24 13:59 abc
1605455(inode条目) -rw-r--r-- 2(链接计数) root root 65 4月 24 13:59 xyz
# 1605455为inode条目
# 2 为链接计数, 由于它们指向了相同的inode所以, abc,xyz链接计数均为2
root@root-PC:~$ ls -li xyz
1605455 -rw-r--r-- 1 root root 65 4月 24 13:59 xyz
# 删除第一个文件名, 链接计数降1
- 硬链接特点
- 仅当i-node的链接计数降为0时(移除了文件的所有名字时),才释放文件的i-node记录和数据块
- 同一文件的所有名字(链接)地位平等——没有一个名字会优于其他。在移除与文件相关的某个名称后,物理内存继续存在,但只能通过另一个文件名来访问内容
在Linux系统中,借助于readdir()对Linux特有/proc/PID/fd目录内容(内含符号链接指向进程当前打开的每个文件描述符)的扫描,可以获知一个进程当前打开了哪些文件。此外,已经移植到多个UNIX系统中的lsof(1)和fuser(1)的工具也精通此道
- 硬链接的限制
- 因为目录条目(硬链接)对文件的指代采用了i-node编号,而i-node编号的唯一性仅在一个文件系统之内才能得到保障,所以硬链接必须与其指代的文件驻留在同一文件系统中
- 不能为目录创建硬链接,从而避免出现令诸多系统程序陷于混乱的链接环路
早期的 UNIX 实现一度曾允许超级用户为目录创建硬链接。这在当时是必要的,因为
这些实现并未提供 mkdir()系统调用。相反,当时会使用 mknode()调用创建一个目录,然后
为.和…创建链接([Vahalia, 1996])
。虽然这一特性已是昨日黄花,但一些现代 UNIX 实现出于
向后兼容的目的仍对其加以保留。
使用绑定挂载(bind mount)可以获得与为目录创建硬链接相似的效果。
符号(软)链接
符号链接,是一种特殊的文件类型,其数据是另一文件的名称
- 创建软连接
$ ln -s /etc/apt/sources.list a.list
$ ls -l
lrwxrwxrwx 1 root root 21 4月 24 14:58 a.list -> /etc/apt/sources.list
$ ls -F # 在符号链接的末尾显示@
a.list@
符号链接的内容既可以是绝对路径,也可以是相对路径。解释相对符号链接时以链接本
身的位置作为参照点。
符号链接的地位不如硬链接。尤其是,文件的链接计数中并未将符号链接计算在内。因此,如果移除了符号链接所指向的文件名,符号链接本身还将继续存在,尽管无法再对其进行解引用(下溯)操作,也将此类链接称之为悬空链接。更有甚者,还可以为并不存在的文件名创建一个符号链接。
如图: 硬链接与符号链接的展现
某些 UNIX 文件系统的优化举措,不但没有在正文中提及,而且也未见诸于图 18-2。如
果构成符号链接内容的字符串总长度很小,足以放入 i-node 中通常用于存放数据指针的位
置,那么就会将字符串直接存储在那里。这省去对一个磁盘块的分配,也加速了对符号链
接信息的访问,因为获取信息时仅涉及到文件的 i-node。例如,ext2、ext3 及 ext4 采用这一技术,将 i-node 中通常用于存放数据块指针的 60 个字节转而用于存放长度合适的符号字符串。
实践证明,这一优化措施卓有成效。
- 系统调用对符号链接的解释
参见Linux-UNIIX系统编程 18.2
- 符号链接的文件权限和所有权
大部分操作会无视符号链接的所有权和权限(创建符号链接时会为其赋予所有权限)。是否允许操作反而是由符号链接所指代文件的所有权和权限来决定。仅当在带有粘性权限位(Linux-UNIIX系统编程 15.4.5 节)的目录中对符号链接进行移除或改名操作时,才会考虑符号链接自身的所有权。
- 创建和移除(硬链接): link 和 unlink
#include <unistd.h>
int link(const char* oldpath, const char *newpath)
Returns 0 on success, or -1 on error
int unlink(const char *pathname)
Returns 0 on success, or -1 on error
1. unlink()系统调用移除一个链接(删除一个文件名),且如果此链接是指向文件的最后一个链接,那么还将移除文件本身。
2. unlink()不能移除一个目录。
3. unlink()系统调用不会对符号链接进行解引用操作,若 pathname 为符号链接,则移除链接本身,而非链接指向的名称。
- 仅当关闭所有文件描述符时,方可删除一个已打开的文件
内核除了为每个 i-node 维护链接计数之外,还对文件的打开文件描述计数。当移除指向文件的最后一个链接时,如果仍有进程持有指代该文件的打开文件描述符,那么在关闭所有此类描述符之前,系统实际上将不会删除该文件。这一特性的妙用在于允许取消对文件的链接,而无需担心是否有其他进程已将其打开。
- unlink 创建临时文件
int main() {
int fd = open("a.txt", O_CREAT | O_RDWR, 0664);
if (-1 == fd) {
perror("open");
exit(1);
}
// 删除临时文件
int ret = unlink("a.txt");
// write
write(fd, "hello", 5);
// 重置文件指针, 移动到文件开始处
lseek(fd, 0, SEEK_SET);
// read
char buf[24] = {
0};
int len = read(fd, buf, sizeof(buf));
write(1, buf, len);
close(fd);
return 0;
}
- 更改文件名: rename
int main()
{
int fd, res, flags = O_CREAT | O_RDWR;
fd = open("a.txt", flags, 0644);
write(fd, "Hello World!\n", 13);
close(fd);
res = rename("a.txt", "b.txt");
if (-1 == res) {
perror("rename");
exit(1);
}
return 0;
}
- 使用符号链接: symlink()和readlink()
创建和查看符号链接
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <limits.h> // defined PATH_MAX
int main()
{
int res;
char buf[PATH_MAX];
res = symlink("b.txt", "b_link.txt");
if (-1 == res) {
perror("symlink");
}
res = readlink("b_link.txt", buf, PATH_MAX);
if (-1 == res) {
perror("readlink");
exit(1);
}
printf("%s\n", buf);
return 0;
}
创建和移除目录: mkdir()和rmdir()
#include <sys/stat.h>
int mkdir(const char *pathname, mode_t mode);
Return 0 on success, or -1 on error
int rmdir(const char *pathname);
Return 0 on success, or -1 on error
移除一个文件或目录:remove()
#include <stdio.h>
int remove(const char *pathname);
Return 0 on success, or -1 on error
如果 pathname 是一文件,那么 remove()去调用 unlink();如果 pathname 为一目录,那么
remove()去调用 rmdir()。
与 unlink()、rmdir()一样,remove()不对符号链接进行解引用操作。若 pathname 是一符号
链接,则 remove()会移除链接本身,而非链接所指向的文件。
读目录: opendir() 和 readdir()等函数
- opendir
opendir()函数打开一个目录,并返回指向该目录的句柄,供后续调用
#include <dirent.h>
DIR *opendir(const char *dirpath)
Returns direcotry stream handle, or NULL on error
opendir()函数打开由 dirpath 指定的目录,并返回指向 DIR 类型结构的指针。该结构即所谓目录流(directory stream),亦即调用者传递给下述其他函数的句柄。一旦从 opendir()返回,则将目录流指向目录列表的首条记录。
- fdopendir()
除了要创建的目录流所针对的目录由打开文件描述符指代之外,fdopendir()与 opendir()并无不同。
#include <dirent.h>
DIR *fdopendir(int fd)
Returns direcotry stream handle, or NULL on error
提供 fdopendir()函数,意在帮助应用程序免受 Linux-UNIX系统编程18.11 节所述各种竞态条件的困扰。
- readdir
readdir()函数从一个目录流中读取连续的条目。
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
Returns pointer to a statically allocated structure describing
next directory entry, or NULL on end-of-directory or error 每调用 readdir()一次,就会从 dirp 所指代的目录流中读取下一目录条目,并返回一枚指针,指向经静态分配而得的 dirent 类型结构,内含与该条目相关的如下信息:
struct dirent {
ino_t d_ino; /* File i-node number */
char d_name[]; /* Null-terminated name of file */
}
/*
出于对程序可移植性的考虑,上述定义略去了 Linux dirent 结构中的各种非标准字段。
这其中最令人感兴趣的当属 d_type,它同时获得了 BSD 流派的支持,但并未在其他 UNIX 系统中实现。该属性值用于标识命名于 d_name 之中文件的类型,诸如 DT_REG(普通文件)、DT_DIR(目录)、DT_LNK(符号链接)或 DT_FIFO(FIFO)。(这些名称类似于表 15-1所列诸宏。)利用该属性值可省去为确定文件类型而对 lstat ()的调用。注意,写作本书时,该属性仅获得 Btrfs、ext2、ext3 以及 ext4 的全面支持。
*/
readdir()返回时并未对文件名进行排序,而是按照文件在目录中出现的天然次序(这取决于文件系统向目录添加文件时所遵循的次序,及其在删除文件后对目录列表中空隙的填补方式)。(命令 ls–f 对文件列表的排列与调用 readdir()时一样,均未做排序处理。)
- scandir()
使用 scandir(3)函数可以获得经过排序处理的文件列表,且排列规则可由程序员定义,具体细节请参考手册页。尽管该函数未获 SUSv3 接纳,但得到了大多数 UNIX 实现的支持。SUSv4 也对 scandir()作了定义。
- rewinddir()
rewinddir()函数可将目录流回移到起点,以便对 readdir()的下一次调用将从目录的第一个文件开始。
#include <dirent.h>
void rewinddir(DIR *dirp)
- closedir()
rewinddir()函数可将目录流回移到起点,以便对 readdir()的下一次调用将从目录的第一个文件开始。
#include <dirent.h>
int closedir(DIR *dirp)
Returns 0 on success, or -1 on error
- telldir()和seekdir()
SUSv3 还定义了两个高级函数:telldir()和 seekdir(),允许随机访问目录流。有关这些函数的深入信息请参考手册页。
- 目录流与文件描述符
有一个目录流,就有一个文件描述符与之关联。dirfd()函数返回与 dirp 目录流相关联的文件描述符。
#include <dirent.h>
int dirf(DIR *dirp);
Returns file descriptor on success, or -1 on error
例如,将 dirfd()返回的文件描述符传递给 fchdir()(参见 18.10 节),就可以把进程的当前工作目录改成相应目录。此外,还可以将其传递给 18.11 节所述各函数的 dirfd 参数。dirfd()函数还见诸于 BSD 系统,但在其他实现中则鲜有踪迹。该函数未获 SUSv3 接纳,但 SUSv4 则对其做了规范。这里值得一提的是,opendir()会为与目录流相关联的文件描述符自动设置 close-on-exec 标志(FD_CLOEXEC),以确保当执行 exec()时自动关闭该文件描述符。(SUSv3 要求这一行为。)close-on-exec 标志将在 27.4 节加以描述。
- 示例程序: 扫描目录
#include <stdio.h>
#include <dirent.h>
#include <string.h>
static void list_files(const char *dirpath)
{
DIR *dirp;
struct dirent *dp;
int errno;
dirp = opendir(dirpath);
if (dirp == NULL) {
perror("opendir");
return;
}
/* For each entry in this directory, print directory + filename */
for (;;) {
errno = 0; /* To distinguish error from end-of-direcotry */
dp = readdir(dirp);
if (dp == NULL)
break;
if (strcmp(dp->d_name, ".") == 0 || strcmp(dp->d_name, "..") == 0) {
continue; /* Skip . and .. */
}
printf("%s\n", dp->d_name);
}
}
int main(int argc, char **argv)
{
if (argc < 2)
list_files("./");
else
list_files(argv[1]);
return 0;
}
- readdir_r()函数
readdir_r()函数是 readdir()的变体。二者之间语义上的关键差异在于前者是可重入的,而后者不是。这是因为 readdir_r()对文件条目的返回利用的是由调用者分配的 entry 参数,而 readdir()则是将信息置于静态分配的结构并返回其指针。21.1.2 节和 31.1 节讨论了可重入性(reentrancy)
文件树遍历: nftw()
#include <ftw.h>
int nftw(const char *dirpath,
int (*fn) (const char *fpath, const struct stat *sb,
int typeflag, struct FTW *ftwbuf),
int nopenfd, int flags);
nftw()函数遍历由 dirpath 指定的目录树,并为目录树中的每个文件调用一次由程序员定义的 func 函数。(递归调用)
- 示例程序
#define _XOPEN_SOURCE 600
#include <stdio.h>
#include <stdlib.h>
#include <ftw.h>
#include <unistd.h>
static int dirTree(const char *fpath, const struct stat *sbuf,
int typeflags, struct FTW *ftwbuf)
{
switch (sbuf->st_mode & S_IFMT) {
/* Print file type */
case S_IFREG: printf("- "); break;
case S_IFDIR: printf("d "); break;
case S_IFCHR: printf("c "); break;
case S_IFBLK: printf("b "); break;
case S_IFLNK: printf("l "); break;
case S_IFIFO: printf("p "); break;
case S_IFSOCK: printf("s "); break;
default: printf("?"); break; /* Should never happen (on linux) */
}
printf("%s ",
(typeflags == FTW_D) ? "D " : (typeflags == FTW_DNR) ? "DNR" :
(typeflags == FTW_DP) ? "DP " : (typeflags == FTW_F) ? "F " :
(typeflags == FTW_SL) ? "SL " : (typeflags == FTW_SL) ? "SLN" :
(typeflags == FTW_NS) ? "NS " : " ");
if(typeflags != FTW_NS)
printf("%7ld ", (long ) sbuf->st_ino);
else
printf(" ");
printf(" %*s", 4 * ftwbuf->level, ""); /* Indent suitable */
printf(" %s\n", &fpath[ftwbuf->base]); /* Print basename */
return 0; /* Tell nftw() to continue */
}
int main(int argc, char **argv)
{
int opt, flags = 0;
printf("argv[1] = %s\n", argv[1]);
while ( (opt = getopt(argc, argv, "dmp")) != -1 ) {
switch(opt) {
case 'd' : flags |= FTW_DEPTH; break;
case 'm' : flags |= FTW_MOUNT; break;
case 'p' : flags |= FTW_PHYS; break;
default: printf("Unknow\n"); break;
}
}
if (argc > optind + 1) {
perror("argc > optind + 1");
exit(EXIT_FAILURE);
}
if ( argc < 2) {
nftw("./", dirTree, 5, flags);
} else {
if (nftw(argv[1], dirTree, 5, flags) == -1 ) {
perror("nftw");
exit(EXIT_FAILURE);
}
}
exit(EXIT_SUCCESS);
}
获取&修改进程的当前工作目录
- getcwd()
获取进程工作目录 #include <unistd.h>
- chdir() & fchdir()
chdir()系统调用将调用进程的当前工作目录改变为由 pathname 指定的相对或绝对路径名(如属于符号链接,还会对其解除引用)。
fchdir()系统调用与 chdir()作用相同,只是在指定目录时使用了文件描述符,而该描述符是之前调用 open()打开相应目录时获得的。
/* 将进程当前工作目录修改,之后还原 */
#include <unistd.h>
int main()
{
/* example */
int fd;
fd = open("./", O_RDONLY); /* Remember where we are */
chdir(savapath); /* Go somewhere else */
fchdir(fd); /* Return to original directory */
close(fd);
/* example */
char oldpath[PATH_MAX];
getcwd(oldpath, PATH_MAX];
chdir(newpath);
chdir(oldpath);
}
针对目录文件描述符的相关操作
始于版本2.6.16,Linux内核提供了一系列新的系统调用,在执行与传统调用相似任务的同时,还提供了一些附加功能,对某些应用程序非常有用
改变进程的根目录: chroot()
每个进程都有一个根目录,该目录是解释绝对路径(即那些以/开始的目录)时的起点。默认情况下,这是文件系统的真实根目录。(新进程从其父进程处继承根目录。)有些场合需要改变一个进程的根目录,而特权级(CAP_SYS_CHROOT)进程通过 chroot()系统调用能够做到这一点。
解析路径名: realpath()
realpath()库函数对 pathname(以空字符结尾的字符串)中的所有符号链接一一解除引用,并解析其中所有对/.和/…的引用,从而生成一个以空字符结尾的字符串,内含相应的绝对路径名。
#include <stdlib.h>
- 代码示例
#include <stdio.h>
#include <sys/stat.h>
#include <limits.h>
#include <unistd.h>
#include <stdlib.h>
#define BUF_SIZE PATH_MAX
int main(int argc, char *argv[])
{
struct stat statbuf;
char buf[BUF_SIZE];
ssize_t numBytes;
if (argc != 2) {
printf("Too few prameters\n");
return 1;
}
if (lstat(argv[1], &statbuf) == -1) {
perror("lstat");
return 1;
}
if (!S_ISLNK(statbuf.st_mode)){
printf("%s is not a symbolic link\n", argv[1]);
return 1;
}
numBytes = readlink(argv[1], buf, BUF_SIZE - 10);
if (numBytes == -1) {
perror("readlink");
return 1;
}
buf[numBytes] = '\0'; /* Add terminating null byte */
printf("readlink: %s --> %s\n", argv[1], buf);
if (realpath(argv[1], buf) == NULL) {
perror("realpath");
return 1;
}
printf("realpath: %s --> %s\n", argv[1], buf);
return 0;
}
解析路径名字符串: dirname()和basename()
dirname()和 basename()函数将一个路径名字符串分解成目录和文件名两部分。
#include <libgen.h>
char *dirname(char *pathname);
char *basename(char *pathname);
Both return a pointer to a null-terminated(and possibly statically allocated)string
#include <stdio.h>
#include <libgen.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
char *t1, *t2;
int j;
for (j = 1; j < argc; j++) {
t1 = strdup(argv[j]);
if (t1 == NULL) {
printf("t1 is nulll\n");
return 1;
}
t2 = strdup(argv[j]) ;
if (t2 == NULL) {
printf("t2 is null\n");
return 1;
}
printf("%s ==> %s + %s \n", argv[1], basename(argv[1]), basename(argv[1]));
free(t1);
free(t2);
}
return 0;
}
- 注意
dirname()和 basename()均可修改 pathname 所指向的字符串。因此,如果希望保留原有的路径名字符串,那么就必须向 dirname()和 basename()传递该字符串的副本
总结
i-node 中并不包含文件的名称。相反,对文件的命名利用的是目录条目,而目录则是列
出文件名和 i-node 编号之间对应关系的一个表格。也将这些目录条目称作(硬)链接。一个
文件可以有多个链接,这些链接之间的地位是平等的。可使用 link()和 unlink()来创建和移除
链接,对文件的重命名则使用系统调用 rename()。