Linux编程系列——目录与链接

目录

在文件系统中,目录的存储方式类似文件。目录与普通文件区别:

  1. 在其i-node条目中,会将目录标记为一种不同的文件
  2. 目录是经特殊组织的文件。本质为一个表格,包含文件名和i-node编号
    在大多数远程Linux系统中,文件名长度可达255个字符
    i-node表的编号始于1, 若目录条目的i-node字段值为0,则表明该条目 尚未使用。i-node字段 1用来记录文件系统的坏块。 文件系统根目录(/)总是存储在i-node条目2中
  • 以/etc/passwd为例
    以/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
  • 硬链接特点
  1. 仅当i-node的链接计数降为0时(移除了文件的所有名字时),才释放文件的i-node记录和数据块
  2. 同一文件的所有名字(链接)地位平等——没有一个名字会优于其他。在移除与文件相关的某个名称后,物理内存继续存在,但只能通过另一个文件名来访问内容

在Linux系统中,借助于readdir()对Linux特有/proc/PID/fd目录内容(内含符号链接指向进程当前打开的每个文件描述符)的扫描,可以获知一个进程当前打开了哪些文件。此外,已经移植到多个UNIX系统中的lsof(1)和fuser(1)的工具也精通此道

  • 硬链接的限制
  1. 因为目录条目(硬链接)对文件的指代采用了i-node编号,而i-node编号的唯一性仅在一个文件系统之内才能得到保障,所以硬链接必须与其指代的文件驻留在同一文件系统中
  2. 不能为目录创建硬链接,从而避免出现令诸多系统程序陷于混乱的链接环路

早期的 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()。

猜你喜欢

转载自blog.csdn.net/gripex/article/details/103457641