Linux 目录流

文件系统编程概述

三种类型的函数对比

ISO C 标准库函数

  • 这些函数是由 ISO(国际标准化组织)提供的,具有最好的通用性和跨平台性。在所有遵循 ISO 标准的现代 C 编译平台上,这些函数的行为都是一致的。
  • 这些函数提供了一些通用的功能实现,例如:printfscanffopenmalloc 等。

  • 它们在 man 手册的第 3 部分(3 号手册)中有所描述。

POSIX 标准库函数

  • POSIX-C 标准旨在为不同的类 Unix 操作系统提供一致的编程接口,以实现应用程序的跨平台移植。
  • 这些函数同样是库函数,也在 man 手册的第 3 部分中描述。

  • 典型的 POSIX 标准库函数,如 opendirclosedir,用于在 Linux 环境下操作目录文件。

POSIX 标准系统调用函数

  • 这些是遵循 POSIX 标准的系统调用接口,它们在所有类 Unix 平台上较为通用。

  • 系统调用函数是 Linux 系统内核和应用程序交互的接口。它们直接与内核交互,因此其通用性和跨平台性相对较差,因为它们的实现依赖于内核的架构和细节。

函数的返回值和形参

在 C 语言中,函数返回值常用于指示函数是否成功执行。如果函数返回一个特殊的值,这通常表示发生了错误。

返回值类型与错误标志

  • 函数返回值类型为 void 时,表示函数不返回任何值,通常不需要进行错误处理。
  • 当函数返回 int 类型时,错误标志通常是 -1
  • 对于返回 size_t 类型的函数,错误标志通常是 0,因为 size_t 是一个无符号整数类型。
  • 返回 ssize_t 类型的函数,错误标志同样是 -1ssize_t 是有符号整数类型。
  • 如果函数返回指针类型,错误标志通常是 NULL,表示未找到或无法分配内存。

指针返回值

  • 指针类型的返回值表示函数可能在堆上分配了内存或获取了资源。这通常意味着需要手动管理这些资源,如释放内存或关闭资源,除非文档中明确指出不需要这样做。

在查看函数的参数列表时,指针类型的参数尤其重要。需要检查指针是否被 const 修饰:

  1. const 修饰的指针参数表示函数不会修改它指向的内存区域。
  2. 未被 const 修饰的指针参数表示函数可能会修改它指向的内存区域。

目录流相关函数

chmod 改变文件权限

chmod 是一个在 Unix 和类 Unix 系统中广泛使用的系统调用,用于改变文件或目录的权限。这个函数定义在 POSIX 标准中,并且通常在 C 语言的 sys/stat.h 头文件中声明。

函数原型 int chmod(const char *pathname, mode_t mode); 包含两个参数:

  1. pathname:一个指向以 null 结尾的字符串的指针,这个字符串包含了要改变权限的文件或目录的路径。
  2. mode:一个新的文件模式,它定义了文件或目录的权限。这个模式是一个位掩码,通常使用宏来表示不同的权限设置,例如 S_IRUSR(用户读权限)、S_IWUSR(用户写权限)、S_IXUSR(用户执行权限)等。

函数的返回值:

  • 如果函数成功执行,返回 0。
  • 如果函数执行失败,返回 -1,并设置全局变量 errno 以指示错误类型。

mode_t 是一个在 <sys/types.h> 中定义的类型,通常是一个无符号整数类型,用于表示文件的模式。

下面是一个示例:

int main(int argc, char *argv[]){
    ARGS_CHECK(argc, 3);
    mode_t mode;
    sscanf(argv[2], "%o", &mode );  // %o表示八进制无符号数

    int ret = chmod(argv[1], mode);
    ERROR_CHECK(ret, -1, "chmod");
    return 0;
}

需要注意的是:设置权限时需要传参权限的 “数字表示法”,而且设定的权限就是文件的最终权限,掩码只会影响新建文件,不会影响 chmod 函数。

getcwd 获取当前工作目录的完整路径

getcwd 是一个 C 语言标准库函数,用于获取当前工作目录的完整路径。这个函数定义在 POSIX 标准中,并且通常在 C 语言的 unistd.h 头文件中声明。

函数原型 char *getcwd(char *buf, size_t size); 包含两个参数:

  1. buf:一个指向字符数组的指针,用于存储当前工作目录的路径。如果这个参数是 NULL,函数会分配一个足够大的缓冲区来存储路径,并且返回指向这个缓冲区的指针。注意,如果 buf 不是 NULL,调用者需要确保它有足够的空间来存储完整的路径。
  2. size:缓冲区的大小,即 buf 可以存储的字符数,包括最后的空字符('\0')。

函数的返回值:

  • 如果函数成功执行,返回指向 buf 的指针,其中包含了当前工作目录的路径。
  • 如果函数执行失败,返回 NULL,并设置全局变量 errno 以指示错误类型。

使用 getcwd 函数时,需要特别注意缓冲区溢出的问题。如果 size 参数指定的大小不足以存储完整的路径,函数将失败,并且 errno 将被设置为 ERANGE

下面是一个示例:

int main(void){
    // 方式1:直接用栈数组作为getcwd返回值数组
    char path[1024] = {0};
    char *p = getcwd(path,sizeof(path));
    ERROR_CHECK(p,NULL,"getcwd");
    printf("cwd = %s\n", path);

    // 方式2:动态分配一个堆数组
    // char *path = (char *)malloc(1024);   
    // char *p = getcwd(path,1024);
    // free(path);  
}

chdir 改变当前目录

chdir 是一个在 Unix 和类 Unix 系统中广泛使用的系统调用,用于改变当前工作目录。这个函数定义在 POSIX 标准中,并且通常在 C 语言的 unistd.h 头文件中声明。

函数原型 int chdir(const char *path); 包含一个参数:path:一个指向以 null 结尾的字符串的指针,这个字符串包含了要切换到的目录的路径。

函数的返回值:

  • 如果函数成功执行,返回 0。
  • 如果函数执行失败,返回 -1,并设置全局变量 errno 以指示错误类型。

使用 chdir 函数可以改变程序的当前工作目录,这会影响后续的文件操作,如打开文件、创建文件等。这些操作默认都是在当前工作目录下进行的。

下面是一个示例:

int main(int argc, char *argv[]) {
  ARGS_CHECK(argc, 2);  // 命令行参数必须2个,第二个是要切换的目标目录

  // 先获取当前工作目录,然后打印
  char buf[1024] = {0};
  char *ret = getcwd(buf, sizeof(buf));
  ERROR_CHECK(ret, NULL, "getcwd");
  printf("一开始的工作目录是: ");
  puts(buf);

  // 改变当前工作目录
  int ret2 = chdir(argv[1]);
  ERROR_CHECK(ret2, -1, "chdir");

  // 再次打印当前工作目录
  char *ret3 = getcwd(buf, sizeof(buf));
  ERROR_CHECK(ret3, NULL, "getcwd");
  printf("chdir后的工作目录是: ");
  puts(buf);

  return 0;
}

每个进程都有自己的当前工作目录,这是进程解析相对路径的起点。

当父进程创建子进程时,子进程会继承父进程的当前工作目录。这意味着子进程在创建时,其工作目录与父进程相同。

Shell 与子进程

  • Shell(如 bash)是一个交互式命令行界面,它允许用户执行命令。当在 Shell 中执行一个可执行程序时,会创建一个新的子进程。
  • 子进程在创建时继承了 Shell 的当前工作目录,但子进程可以独立地改变其工作目录,这不会影响父进程(Shell)的工作目录。

chdir 函数与 cd 命令

  • chdir 函数是一个系统调用,用于改变调用进程的当前工作目录。它与 Shell 中的 cd 命令不同,因为 cd 命令实际上会改变 Shell 进程的当前工作目录,而不仅仅是当前执行的程序。
  • 子进程中使用 chdir 函数修改工作目录的操作是局部的,只影响该子进程,而不影响父进程。

202403100032762.png

cd 指令的原理cd 并不是一个独立的外部程序,而是 Shell(如 bash)的内置命令。这意味着 cd 作为 Shell 的一部分,直接修改当前 Shell 进程的工作目录。

为什么 which cd 找不到可执行程序: 由于 cd 是 Shell 的内置命令,它不存在于文件系统中作为独立的可执行文件。因此,使用 which 命令查找 cd 会失败,因为 which 仅用于查找外部可执行文件的路径。

cd 指令的作用cd 命令允许用户更改当前 Shell 会话的工作目录。这对于导航到不同目录以执行命令或访问文件非常有用。

mkdir 创建目录

mkdir 是一个在 Unix 和类 Unix 系统中广泛使用的系统调用,用于创建一个新的目录。这个函数定义在 POSIX 标准中,并且在 C 语言的 sys/stat.h 头文件中声明。

函数原型 int mkdir(const char *pathname, mode_t mode); 包含两个参数:

  1. pathname:一个指向以 null 结尾的字符串的指针,这个字符串包含了要创建的目录的路径。
  2. mode:新目录的权限模式。这个模式是一个位掩码,定义了目录的权限。这个参数与 chmod 函数中使用的模式相同,通常使用宏来表示不同的权限设置,例如 S_IRWXU(所有者读、写、执行权限)、S_IRWXG(组读、写、执行权限)等。

函数的返回值:

  • 如果函数成功执行,返回 0。
  • 如果函数执行失败,返回 -1,并设置全局变量 errno 以指示错误类型。

mode_t 是一个在 <sys/types.h> 中定义的类型,通常是一个无符号整数类型,用于表示文件的模式。

int main(int argc, char* argv[]) {
    ARGS_CHECK(argc, 3);    // 需要三个命令行参数,允许传入一个三位八进制数表示权限
    mode_t mode;
    sscanf(argv[2], "%o", &mode);       // 将第三个命令行参数字符串转换成八进制无符号整数
    int ret = mkdir(argv[1], mode);
    ERROR_CHECK(ret, -1, "mkdir");
    return 0;
}

umask 的作用umask 是一个系统设置,它定义了新创建的文件和目录的默认权限。umask 的值通常是一个三位的八进制数,每一位分别对应文件的拥有者、组用户和其他用户的权限。

umask 的影响

  • 当创建新目录时,如果设置的权限为 777,而 umask 设置为 002,那么最终目录的权限将是 775(即 777 – 002)。
  • 这种计算方式同样适用于文件权限的设置。

查看和设置 umask

  • 使用 umask 命令可以查看当前的文件掩码值。
  • 使用 umask n 命令可以设置一个新的文件掩码值,其中 n 是一个八进制数。

umask 的重要性

  • umask 的存在是为了提高系统的安全性,防止新创建的文件和目录具有过于宽松的权限设置。
  • 较低的 umask 值会导致更开放的权限设置,而较高的 umask 值会导致更严格的权限设置。

rmdir 删除空目录

rmdir 是一个 POSIX 标准定义的系统调用,用于删除一个空目录。这个函数通常在 C 语言的 unistd.h 头文件中声明。

函数原型 int rmdir(const char *pathname); 包含一个参数:

  1. pathname:一个指向以 null 结尾的字符串的指针,这个字符串指定了要删除的目录的路径。

函数的返回值:

  • 如果函数成功执行,返回 0。
  • 如果函数执行失败,返回 -1,并设置全局变量 errno 以指示错误类型。

使用 rmdir 函数时,需要注意以下几点:

  • 只有当目录是空的,即不包含任何文件和子目录时,rmdir 才能成功删除该目录。
  • 如果目录不是空的,rmdir 调用将失败,并且 errno 将被设置为 ENOTEMPTY
  • 如果指定的路径不存在,或者调用者没有足够的权限删除该目录,rmdir 也会失败。

一个示例如下:

int main(int argc, char* argv[]) {
    ARGS_CHECK(argc, 2);
    int ret = rmdir(argv[1]);   // 注意:rmdir 只能删除空目录
    ERROR_CHECK(ret, -1, "rmdir");
    return 0;
}

opendir 打开目录流

pendir 是一个 POSIX 标准定义的函数,用于打开一个目录流,以便可以读取其中的内容。这个函数通常在 C 语言的 dirent.h 头文件中声明。

函数原型 DIR *opendir(const char *name); 包含一个参数:name:一个指向以 null 结尾的字符串的指针,这个字符串指定了要打开的目录的路径。

函数的返回值:

  • 如果函数成功执行,返回一个指向新分配的 DIR 结构的指针,该结构表示目录流。
  • 如果函数执行失败,返回 NULL,并设置全局变量 errno 以指示错误类型。

使用 opendir 打开目录流后,可以使用 readdir 函数来读取目录中的条目,使用 rewinddir 函数来重置目录流的位置,以及使用 closedir 函数来关闭目录流并释放相关资源。

closedir 关闭目录流

closedir 是一个 POSIX 标准定义的函数,用于关闭之前使用 opendir 函数打开的目录流,并释放与之关联的所有资源。这个函数通常在 C 语言的 dirent.h 头文件中声明。

函数原型 int closedir(DIR *dirp); 包含一个参数 dirp:一个指向 DIR 结构的指针,该结构是通过 opendir 函数返回的,表示要关闭的目录流。

函数的返回值:

  • 如果函数成功执行,返回 0。
  • 如果函数执行失败,返回 -1,并设置全局变量 errno 以指示错误类型。

使用 closedir 函数时,通常在完成对目录内容的遍历之后调用,以确保及时释放系统资源。如果 closedir 调用失败,可以通过检查 errno 来确定错误原因,常见的错误可能包括传入的 dirp 指针无效或者内存分配失败等。

readdir 读目录流

readdir 是一个 POSIX 标准定义的函数,用于从之前使用 opendir 打开的目录流中读取下一个目录项。这个函数通常在 C 语言的 dirent.h 头文件中声明。

函数原型 struct dirent *readdir(DIR *dirp); 包含一个参数:

  1. dirp:一个指向 DIR 结构的指针,该结构是通过 opendir 函数返回的,表示要读取的目录流。

函数的返回值:

  • 如果函数成功执行,返回一个指向 struct dirent 的指针,该结构包含了目录中下一个条目的信息。
  • 如果已经到达目录的末尾,返回 NULL
  • 如果函数执行失败,也返回 NULL,并设置全局变量 errno 以指示错误类型。

struct dirent 结构通常包含以下成员:

  • d_ino:一个整数,表示条目的 inode 号(在某些系统上可能不可用)。
  • d_off:一个长整数,表示目录流中的偏移量,可以用于 seekdirtelldir 函数。
  • d_reclen:一个短整数,表示 struct dirent 结构的长度。
  • d_type: 类型(type)。这是一个指示目录项类型的标志,例如,是否是文件、目录、符号链接等。这个字段不是 POSIX 标准的一部分,但在某些系统(如 GNU/Linux)中可用。
  • d_name:一个字符数组,包含了条目的名称。
// dirent 是 directory entry 的简写,就是目录项的意思
struct dirent {
    ino_t          d_ino;           // 此目录项的 inode 编号,目录项中会存储文件的 inode 编号。一般是一个 64 位无符号整数(64 位平台)
    off_t          d_off;           // 到下一个目录项的偏移量。可以视为指向下一个目录项的指针(近似可以看成链表),一般是一个 64 位有符号整数
    unsigned short d_reclen;        // 此目录项的实际大小长度,以字节为单位(注意不是目录项所表示文件的大小,也不是目录项结构体的大小)
    unsigned char  d_type;          // 目录项所表示文件的类型,用不同的整数来表示不同的文件类型
    char           d_name[256];     // 目录项所表示文件的名字,该字段一般决定了目录项的实际大小。也就是说文件名越长,目录项就越大
};

其中文件类型 d_type 的可选值如下(使用宏常量定义的整数):

DT_BLK      // 块设备文件,对应整数值 6
DT_CHR      // 字符设备文件,对应整数值 2
DT_DIR      // 目录文件,对应整数值 4
DT_FIFO     // 有名管道文件,对应整数值 1
DT_LNK      // 符号链接文件,对应整数值 10
DT_REG      // 普通文件,对应整数值 8
DT_SOCK     // 套接字文件,对应整数值 12
DT_UNKNOWN  // 未知类型文件,对应整数值 0

seekdirtelldir

telldirseekdir 是两个与目录流操作相关的 POSIX 标准函数,它们允许你在目录流中获取当前位置和设置新位置。

telldir 函数原型:

long telldir(DIR *dirp);

参数:dirp:指向 DIR 结构的指针,该结构是通过 opendir 函数返回的,表示要查询的目录流。

功能:telldir 函数返回一个值,表示当前目录流的位置。这个位置是一个偏移量,可以用于 seekdir 函数来重新定位目录流。

返回值:

  • 返回一个 long 类型的值,表示当前目录流中的偏移量。
  • 如果出现错误,返回 (-1L) 并设置 errno 以指示错误。

seekdir 函数原型:

void seekdir(DIR *dirp, long loc);

参数:

  • dirp:指向 DIR 结构的指针,该结构是通过 opendir 函数返回的,表示要设置位置的目录流。
  • loc:要设置的目录流中的偏移量。

功能:seekdir 函数将目录流的位置设置为由 loc 指定的偏移量。这个偏移量应该由先前的 telldir 调用返回。

返回值:

  • 此函数不返回任何值(void 类型)。
  • 如果出现错误,会设置 errno 以指示错误。

这两个函数通常一起使用,以实现目录流的定位和重定位。例如,可以先使用 telldir 保存当前位置,然后使用 seekdir 跳转到目录中的特定位置,最后再次使用 telldir 来获取新位置。

int main(int argc, char *argv[]) {
    ARGS_CHECK(argc, 2);
    DIR *dirp = opendir(argv[1]);
    ERROR_CHECK(dirp, NULL, "opendir");

    struct dirent *dp;  // 指向目录项结构体的指针
    // 需求: 记录 dir2 目录项,然后在 while 循环的下面重新打印这个目录项
    long location;
    while ((dp = readdir(dirp)) != NULL) {
        // 每一次循环就处理一个目录项
        printf(
            "inode num = %lu"
            "offset = %ld"
            "reclen = %hu"
            "type = %u"
            "file name = %s\n",
            dp->d_ino, dp->d_off, dp->d_reclen, dp->d_type, dp->d_name);
        if (strcmp("..", dp->d_name) == 0) {
            // 记录 dir2 目录项的位置
            location = telldir(dirp);
        }
    }  // 当 while 循环结束时,dp 就是空指针

    printf("----------------------------\n");
    seekdir(dirp, location);
    dp = readdir(dirp);
    printf(
        "inode num = %lu"
        "offset = %ld"
        "reclen = %hu"
        "type = %u"
        "file name = %s\n",
        dp->d_ino, dp->d_off, dp->d_reclen, dp->d_type, dp->d_name);

    // 不要忘记关闭流
    int ret = closedir(dirp);
    ERROR_CHECK(ret, -1, "closedir");
    return 0;
}

rewinddir 目录流重置

rewinddir 是一个 POSIX 标准定义的函数,用于将目录流的位置重置到开始的位置,即目录的第一个条目。这个函数通常在 C 语言的 dirent.h 头文件中声明。

函数原型:

void rewinddir(DIR *dirp);

参数:dirp:指向 DIR 结构的指针,该结构是通过 opendir 函数返回的,表示要重置的目录流。

功能:rewinddir 函数将目录流中的当前位置重置到目录的第一个条目。这允许程序重新遍历目录中的所有条目。

返回值:

  • 此函数不返回任何值(void 类型)。
  • 如果出现错误,会设置 errno 以指示错误。

使用 rewinddir 可以方便地重新访问目录中的条目,而不需要关闭并重新打开目录流。这在需要多次遍历同一目录的情况下非常有用。

stat 获取元数据

stat 函数是一个广泛使用的 POSIX 标准函数,用于获取文件或目录的元数据(如大小、权限、时间戳等)。这个函数在 C 语言的 sys/stat.h 头文件中声明。

函数原型:

int stat(const char *path, struct stat *buf);

参数:

  1. path:一个指向以 null 结尾的字符串的指针,指定了要获取信息的文件或目录的路径。
  2. buf:一个指向 struct stat 结构的指针,该结构用于接收关于 path 指定的文件或目录的信息。

struct stat 结构包含了以下一些常见的成员(具体成员可能因系统而异):

  • st_dev:设备编号
  • st_ino:inode 号(文件系统的唯一标识符)
  • st_mode:文件类型和权限
  • st_nlink:硬链接数量
  • st_uidst_gid:文件所有者的用户 ID 和组 ID
  • st_size:文件大小(字节)
  • st_atime, st_mtime, st_ctime:分别是文件的最后访问时间、最后修改时间和元数据最后改变时间的时间戳

功能:

  • stat 函数尝试获取 path 指定的文件或目录的元数据,并将结果存储在 buf 指向的 struct stat 结构中。

返回值:

  • 如果函数成功执行,返回 0。
  • 如果函数执行失败,返回 -1,并设置全局变量 errno 以指示错误类型。

目录流获取的文件名:在使用目录流读取目录项时,我们可以获得每个文件的文件名。但这并不一定是文件的完整路径名。

  1. 当当前工作目录与要打印目录信息的目录相同(例如,都是 /dir),此时从目录流获取的文件名可以直接作为 stat 函数的参数,因为它们是相对于当前工作目录的相对路径名。

    例如,如果 /dir 目录下有三个文件:file1file2file3,那么它们的相对路径名分别是 file1file2file3。在这种情况下,文件名可以直接作为 stat 函数的参数,因为它们已经是相对于当前工作目录的路径。

  2. 如果当前工作目录与要打印目录信息的目录不同(例如,当前工作目录是 /dir,但要打印的是另一个目录 /subdir 的信息),则从目录流获取的文件名不是完整的路径名。

  • 例如,如果 /subdir 目录下有三个文件:file1file2file3,而当前工作目录是 /dir,则这些文件的绝对路径应该是 /subdir/file1/subdir/file2/subdir/file3。如果使用 /dir 作为工作目录,直接使用文件名作为 stat 函数的参数将无法正确找到这些文件。

从目录流获取的文件名是否可以直接作为 stat 函数的参数,取决于当前工作目录是否与要查询的目录相同。如果不同,需要将文件名与目录路径拼接成完整的路径名。

int main(int argc, char *argv[]) {
    ARGS_CHECK(argc, 2);
    DIR *dirp = opendir(argv[1]);
    ERROR_CHECK(dirp, NULL, "opendir");

    // 解决方法 1:切换工作目录为要打印信息的目录
    // int ret = chdir(argv[1]);
    // ERROR_CHECK(ret, -1, "chdir");

    struct dirent *dp;
    while ((dp = readdir(dirp)) != NULL) {
        struct stat stat_buf;
        // 循环处理目录项,把文件名直接作为路径名传参给 stat 函数
        // 这里需要配合解决办法 1
        // int ret = stat(path, &stat_buf);
        // ERROR_CHECK(ret, -1, "stat");

        // 解决办法 2:拼接文件的绝对路径然后再传参给 stat 函数
        char path[1024] = {0};
        // strcpy(path, argv[1]);
        // strcat(path, "/");
        // strcat(path, dp->d_name);
        sprintf(path, "%s%s%s", argv[1], "/", dp->d_name);

        int ret = stat(path, &stat_buf);
        ERROR_CHECK(ret, -1, "stat");
        // 获取 stat 结构体后打印文件的详细信息
        printf("%o %lu %u %u %lu %ld %s\n", stat_buf.st_mode, stat_buf.st_nlink,
               stat_buf.st_uid, stat_buf.st_gid, stat_buf.st_size,
               // stat_buf.st_mtim.tv_sec,
               stat_buf.st_mtime, dp->d_name);
    }
    closedir(dirp);
    return 0;
}

关于 struct stat 结构体:

struct stat 是一个在 POSIX 兼容的操作系统中定义的数据结构,用于存储文件或目录的元数据。当你调用像 statlstat 这样的函数时,它们会将关于文件的信息填充到一个 struct stat 结构体实例中。这个结构体定义在 sys/stat.h 头文件中。

struct stat 结构体通常包含以下成员(成员可能根据不同的系统和编译环境有所不同):

struct stat {
    mode_t    st_mode;          // 包含文件的类型以及权限信息
    nlink_t   st_nlink;         // 文件的硬链接数量 
    uid_t     st_uid;           // 文件所有者的用户ID
    gid_t     st_gid;           // 文件所有者组的组ID
    off_t     st_size;          // 文件的实际大小, 以字节为单位

    struct timespec st_mtim;  /* 包含文件最后修改时间的结构体对象 */
};

这些字段的类型都是使用别名来定义的,在 64 位 Linux 操作系统上,这些别名的类型一般是:

  1. mode_t:一般是一个 32 位无符号整数。
  2. nlink_t:一般是一个 64 位无符号整数。
  3. uid_tuid_t:一般是一个 32 位无符号整数。
  4. off_t:一般是一个 64 位无符号整数。

其中的 timespec 结构体定义如下:

struct timespec {
    time_t tv_sec;  // 秒,表示时间戳中的秒部分
    long tv_nsec;   // 纳秒,表示时间戳中的微秒部分,用于更精确地表示时间
};
  • tv_sec:表示时间戳的秒部分,通常与历元(Epoch)的时间差有关。
  • tv_nsec:表示时间戳的纳秒部分,即一秒内的微秒数,可以提供额外的时间精度。

由于宏 #define st_mtime spec.tv_sec 的存在,可以直接使用 statbuf.st_mtime 来访问 statbuf.st_mtim.tv_sec 的值。

statbuf.st_mtim.tv_secstatbuf.st_mtime 是等价的,都表示文件的最后修改时间的秒部分。

注意:

  1. 一个是 st_mtim,这是一个结构体类型,它是 stat 结构体的成员。
  2. 一个是宏定义 st_time,它是一个 long int 类型的表示文件最后修改时间的时间戳秒数。

getpwuid 获取用户信息

getpwuid 是一个在 POSIX 标准中定义的函数,用于根据用户 ID(uid)获取用户的信息。这个函数通常在 C 语言的 pwd.h 头文件中声明。

函数原型:

struct passwd *getpwuid(uid_t uid);

参数:

  • uid:用户 ID,是一个无符号整数,表示要获取信息的用户的唯一标识符。

功能:

  • getpwuid 函数在系统用户数据库中查找与给定 uid 相对应的用户记录,并返回一个指向 struct passwd 结构的指针,该结构包含了用户的信息。

返回值:

  • 如果找到对应的用户记录,函数返回一个指向 struct passwd 结构的指针。
  • 如果没有找到对应的用户记录或发生错误,函数返回 NULL 并设置全局变量 errno 以指示错误类型。

struct passwd 结构通常包含以下成员:

  • pw_name:一个指针,指向一个字符串,表示用户的登录名。
  • pw_passwd:一个指针,指向一个字符串,表示用户的密码。出于安全考虑,通常密码以加密形式存储。
  • pw_uid:用户的用户 ID。
  • pw_gid:用户的组 ID。
  • pw_gecos:一个指针,指向描述用户信息的字符串,如全名、工作电话、家庭电话等。
  • pw_dir:一个指针,指向用户主目录的路径。
  • pw_shell:一个指针,指向用户默认登录 shell 的路径。
struct passwd {
    char *pw_name;   // 用户名
    char *pw_passwd; // 密码(通常是加密后的密码,但在现代系统中通常是 'x' 或 '*',真正的密码存储在安全文件中)
    uid_t pw_uid;    // 用户ID
    gid_t pw_gid;    // 组ID
    char *pw_gecos;  // GECOS 字段,包含其他用户信息
    char *pw_dir;    // 用户主目录
    char *pw_shell;  // 用户登录 shell
};

getgrgid 获取组信息

getgrgid 是一个在 POSIX 标准中定义的函数,用于根据组 ID(gid)获取组的信息。这个函数通常在 C 语言的 grp.h 头文件中声明。

函数原型:

struct group *getgrgid(gid_t gid);

参数:gid:组 ID,是一个无符号整数,表示要获取信息的组的唯一标识符。

功能:getgrgid 函数在系统组数据库中查找与给定 gid 相对应的组记录,并返回一个指向 struct group 结构的指针,该结构包含了组的信息。

返回值:

  • 如果找到对应的组记录,函数返回一个指向 struct group 结构的指针。
  • 如果没有找到对应的组记录或发生错误,函数返回 NULL 并设置全局变量 errno 以指示错误类型。

struct group 结构通常包含以下成员:

  • gr_name:一个指针,指向一个字符串,表示组的名称。
  • gr_passwd:一个指针,指向一个字符串,表示组的密码。通常这个字段不使用或包含一个占位符。
  • gr_gid:组的组 ID。
  • gr_mem:一个指向字符串数组的指针,数组中包含了属于该组的所有用户的登录名列表。
struct group {
    char *gr_name;   // 组名
    char *gr_passwd; // 组密码(在现代系统中通常是 'x' 或 '*',真正的密码存储在安全文件中)
    gid_t gr_gid;    // 组ID
    char **gr_mem;   // 包含组成员用户名的字符串数组
};

localtime 时间函数

localtime 是 C 标准库中的一个函数,用于将自纪元(Epoch,即 1970 年 1 月 1 日 00:00:00 UTC)以来的秒数(time_t 类型)转换为一个更易读的结构化时间表示,即 struct tm 类型。这个函数定义在 time.h 头文件中。

函数原型:

struct tm *localtime(const time_t *timer);

参数:timer:一个指向 time_t 类型的指针,包含了要转换的时间值,表示自纪元以来的秒数。

功能:localtime 函数将 timer 指向的时间戳转换为当地时间(考虑了时区和夏令时设置)的 struct tm 格式。

返回值:

  • 函数返回一个指向 struct tm 结构的指针,该结构包含了转换后的时间信息。
  • 如果转换失败,返回 NULL 并设置全局变量 errno 以指示错误类型。

struct tm 结构通常包含以下成员:

  • tm_sec:秒(0-59)
  • tm_min:分钟(0-59)
  • tm_hour:小时(0-23)
  • tm_mday:一个月中的第几天(1-31)
  • tm_mon:年份中第几个月,从 0(1 月)开始计数
  • tm_year:自 1900 年以来的年数(例如,2023 表示为 123)
  • tm_wday:一周中的第几天,从 0(通常表示星期日)开始计数
  • tm_yday:一年中的第几天,从 0(1 月 1 日)开始计数
  • tm_isdst:夏令时标志,非零表示夏令时,零表示标准时间
struct tm {
    int tm_sec;    /* 秒 – 取值区间为[0,59] */
    int tm_min;    /* 分 - 取值区间为[0,59] */
    int tm_hour;   /* 时 - 取值区间为[0,23] */
    int tm_mday;   /* 一个月中的日期 - 取值区间为[1,31] */
    int tm_mon;    /* 月份(从一月开始,0代表一月)- 取值区间为[0,11] */
    int tm_year;   /* 年份,其值从1900开始 */
    int tm_wday;   /* 星期 – 取值区间为[0,6],星期日为0 */
    int tm_yday;   /* 从每年的1月1日开始的天数 – 取值区间为[0,365],1月1日为0 */
    int tm_isdst;  /* 夏令时标识符,夏令时时为正,不是夏令时时为0,不了解情况时为负 */
};

目录流和文件流

202403101159558.png

目录流(DIR *)和文件流(如 FILE *)是文件 I/O 操作中的两种不同类型,它们在操作流程和使用方式上有一些相似之处,但也有明显的区别。

相似点:

  1. 操作流程:无论是文件流还是目录流,操作流程都是相同的,即先打开流,然后进行操作,最后关闭流。
  2. 操作位置指示:打开目录文件后,会得到一个目录流指针 DIR *,类似于文件流的 FILE * 指针,它指示了操作的位置。
  3. 库函数封装:文件流和目录流都是库函数,它们封装了系统调用,提供了相似的使用方式。

不同点:

  1. 写操作:最明显的不同是目录流不允许写操作,只能进行读操作。系统内核没有提供写目录流的功能。
  2. 库函数标准:文件流通常遵循 ISO C 标准库函数,而目录流则遵循 POSIX 标准库函数。
文件流 目录流
fopen opendir
fclose closedir
fread readdir
fwrite ×
ftell telldir
fseek seekdir
rewind rewinddir

实现 ll 指令

简易版

使用 opendirclosedirreaddir 实现一个简易版指令

#include <stdc.h>

int main(int argc, char *argv[]) {
    ARGS_CHECK(argc, 2);
    DIR *dirp = opendir(argv[1]);
    // 打开目录流需要进行错误处理
    ERROR_CHECK(dirp, NULL, "opendir");
    struct dirent *pdirent;
    // 循环读目录项并打印目录信息,循环结束的条件是返回值为 NULL
    while ((pdirent = readdir(dirp)) != NULL) {
        printf(
            "inode num = %lu, reclen = %hu, type = %u, name = %s\n",
            pdirent->d_ino,  // 64 位平台这个 inode 编号一般是一个 64 位无符号整数
            pdirent->d_reclen,  // 无符号短整型
            pdirent->d_type,    // 无符号整型
            pdirent->d_name);   // 以字符串类型打印
    }
    // 及时释放资源,关闭目录流
    closedir(dirp);
    return 0;
}

输出结果如下:

优化

#include <stdc.h>

int main(int argc, char *argv[]) {
  ARGS_CHECK(argc, 2);
  DIR *dirp = opendir(argv[1]);
  ERROR_CHECK(dirp, NULL, "opendir");

  // 切换工作目录为参数传入的目录
  int ret = chdir(argv[1]);
  ERROR_CHECK(ret, -1, "chdir");

  // dirent 结构体指针,用于存放目录项信息
  struct dirent *pdirent;
  // 读取目录中的每个目录项
  while ((pdirent = readdir(dirp)) != NULL) {
    // 拼接文件路径,如果上面没有 chdir 切换目录,这里就需要做绝对路径的拼接操作
    // char path[1024] = {0};
    // sprintf(path, "%s%s%s", argv[1], "/", pdirent->d_name);
    // 或者也可以用 strcpy 以及 strcat 函数进行复制拼接获取最终目录
    // strcpy(path, argv[1]);
    // strcat(path, "/");
    // strcat(path, pdirent->d_name);

    // 获取文件相关的信息
    struct stat statbuf;
    int ret = stat(
        pdirent->d_name,
        &statbuf);  // 由于切换了工作目录,所以文件名就是路径名。如果没有切换,那么这里需要传参 path
    ERROR_CHECK(ret, -1, "stat");

    // 输出文件相关的信息
    printf(
        "%o %lu %u %u %lu %lu %s\n",
        statbuf.st_mode,  // 以八进制无符号输出,可以看到权限的数字表示法
        statbuf.st_nlink,  // 无符号长整型输出
        statbuf.st_uid,    // 无符号 int 输出
        statbuf.st_gid,    // 无符号 int 输出
        statbuf.st_size,   // 无符号长整型输出
        statbuf.st_mtim
            .tv_sec,  // 时间戳打印秒数,以无符号长整型打印。这里还可以写 statbuf.st_mtime
        pdirent->d_name);  // 字符串打印
  }
  // 关闭目录流
  closedir(dirp);
  return 0;
}

再优化

获取文件权限信息st_mode 成员的最后 9 位二进制数表示文件的权限。以下是如何根据 st_mode 判断文件权限的方法:

  • 文件权限 777 的二进制表示为 111 111 111
  • 文件权限 775 的二进制表示为 111 110 101
  • 文件权限 664 的二进制表示为 110 110 100

判断权限: 通过将 st_mode 与特定的位掩码进行按位与操作,可以判断文件的权限:

  • 0400(可读)进行按位与操作,结果非 0 表示拥有者具有读权限。
  • 0200(可写)进行按位与操作,结果非 0 表示拥有者具有写权限。
  • 0100(可执行)进行按位与操作,结果非 0 表示拥有者具有执行权限。
// 设置文件类型和权限字符串
void set_type_mode(mode_t mode, char *tm_str){
    // 处理第一个字符,即文件类型 
    switch (mode & S_IFMT) {
        case S_IFBLK:   tm_str[0] = 'b';        break;
        case S_IFCHR:   tm_str[0] = 'c';        break;
        case S_IFDIR:   tm_str[0] = 'd';        break;
        case S_IFIFO:   tm_str[0] = 'p';        break;
        case S_IFLNK:   tm_str[0] = 'l';        break;
        case S_IFREG:   tm_str[0] = '-';        break;
        case S_IFSOCK:  tm_str[0] = 's';        break;
        default:        tm_str[0] = '?';        break;
    }

    // 处理后续九个字符,即文件的权限信息
    // 设置拥有者的权限信息
    tm_str[1] = (mode & 0400) ? 'r' : '-';
    tm_str[2] = (mode & 0200) ? 'w' : '-';
    tm_str[3] = (mode & 0100) ? 'x' : '-';
    // 设置拥有者组的权限                          
    tm_str[4] = (mode & 0040) ? 'r' : '-';
    tm_str[5] = (mode & 0020) ? 'w' : '-';
    tm_str[6] = (mode & 0010) ? 'x' : '-';
    // 设置其他人的权限
    tm_str[7] = (mode & 0004) ? 'r' : '-';
    tm_str[8] = (mode & 0002) ? 'w' : '-';
    tm_str[9] = (mode & 0001) ? 'x' : '-';
    tm_str[10] = '\0'; // 确保字符串以 null 结尾
}

// 获取格式化的时间字符串
void set_time(time_t mtime, char *time_str){
    // 由于tm结构体中存储的是月份的整数值,我们需要的是月份字符串,所以用一个字符串数组来存储月份字符串
    const char month_arr[][10] = {
        "1月", "2月", "3月", "4月", "5月", "6月",
        "7月", "8月", "9月", "10月", "11月", "12月"
    };  // tm结构体当中的月份范围是[0, 11],刚好可以适配这个数组

    // 调用localtime函数,获取tm结构体指针
    struct tm* st_tm = localtime(&mtime);
    // 构建时间字符串,格式为:月份 天数 时:分
    sprintf(time_str, "%s %2d %02d:%02d",
            month_arr[st_tm->tm_mon],
            st_tm->tm_mday,
            st_tm->tm_hour,
            st_tm->tm_min);
}

/* Usage: ./07_myls pathname 或 ./07_myls */
int main(int argc, char* argv[]) {
    char* dir_name; // 存储目录名的指针
    if(argc == 1) {
        dir_name = ".";  // 如果命令行参数没有提供要打印的目录,就打印当前工作目录
    }
    else if (argc == 2){
        dir_name = argv[1]; // 否则使用提供的命令行参数作为待打印的目标
    }else {
        fprintf(stderr, "args num error!\n");
        exit(1);
    }

    DIR* dirp = opendir(dir_name); // 打开指定的目录
    ERROR_CHECK(dirp, NULL, "opendir");

    // 改变工作目录到指定目录
    int ret = chdir(dir_name);
    ERROR_CHECK(ret, -1, "chdir");

    struct dirent* pdirent;
    // 遍历目录项
    while ((pdirent = readdir(dirp)) != NULL) {
        struct stat stat_buf;
        // 获取目录项的详细信息
        int ret = stat(pdirent->d_name, &stat_buf);
        ERROR_CHECK(ret, -1, "stat");

        char mode_str[1024] = { 0 }; // 保存文件类型和权限信息
        set_type_mode(stat_buf.st_mode, mode_str); // 设置类型和权限

        char time_str[1024] = { 0 }; // 保存格式化后的时间信息
        /*
            localtime需要传入时间戳描述
            所以这里传参时可以写stat_buf.st_mtime(宏定义,更简写)
            也可以写stat_buf.st_mtim.tv_sec(宏定义的原版代码)
            但不能直接写stat_buf.st_mtim
            原因上面已经讲过了!
        */
        set_time(stat_buf.st_mtime, time_str); // 获取时间字符串

        printf("%s %2lu %s %s %6lu %s %s\n", 
               mode_str,                            // 文件类型与权限
               stat_buf.st_nlink,                   // 硬链接数,不足2个字符的在前面补空格
               getpwuid(stat_buf.st_uid)->pw_name,  // 拥有者名
               getgrgid(stat_buf.st_gid)->gr_name,  // 拥有者组名
               stat_buf.st_size,                    // 文件大小,使用%4lu表示最少输出4个字符,若不足4个字符则在前面补空格
               time_str,                            // 最后修改时间字符串
               pdirent->d_name);                    // 文件名
    }

    // 关闭目录
    closedir(dirp);
    return 0;
}

202403140057673.png

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇