ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux系统编程(3):目录操作

Linux系统编程(3):目录操作 一、Linux 的 7 种文件类型在正式学习目录操作之前先回顾 Linux 中的7 种文件类型记忆口诀b c d - l s p类型标识含义宏常量b块设备文件block deviceS_IFBLKc字符设备文件character deviceS_IFCHRd目录文件directoryS_IFDIR-普通文件regular fileS_IFREGl符号链接/软链接symbolic linkS_IFLNKs套接字文件socketS_IFSOCKp管道文件named pipe / FIFOS_IFIFO通过struct stat中的st_mode字段与S_IFMT掩码做按位与运算即可判断文件类型。二、目录操作函数1. opendir — 打开目录#include sys/types.h #include dirent.h DIR *opendir(const char *name);功能打开一个目录返回关联的目录流指针。返回值成功返回DIR *失败返回NULL同时errno会被设置。2. readdir — 读取目录#include dirent.h struct dirent *readdir(DIR *dirp);功能每次调用读取目录中的一个条目。返回值成功返回struct dirent *指针到达目录末尾或失败时返回NULL。struct dirent结构体中的重要字段字段含义d_inoinode 编号d_type文件类型如DT_REG、DT_DIR等d_name[256]文件名以 null 结尾d_type常见取值包括DT_BLK、DT_CHR、DT_DIR、DT_FIFO、DT_LNK、DT_REG、DT_SOCK、DT_UNKNOWN。3. closedir — 关闭目录int closedir(DIR *dirp);功能关闭目录流。返回值成功返回0失败返回-1同时errno会被设置。注意不要多次释放同一个目录流指针否则会导致程序异常。4. chdir — 切换工作目录int chdir(const char *path);功能改变当前程序的工作路径类似 shell 中的cd命令。返回值成功返回0失败返回-1。5. getcwd — 获取当前工作目录char *getcwd(char *buf, size_t size);功能获取当前的工作路径类似 shell 中的pwd命令。参数buf为保存路径的缓冲区首地址size为缓冲区长度。返回值成功返回包含路径的字符串首地址失败返回NULL。6. mkdir — 创建目录int mkdir(const char *pathname, mode_t mode);功能创建一个目录。参数pathname为路径mode为权限实际权限为mode ~umaskumask 通常为0002。返回值成功返回0失败返回-1。7. rmdir — 删除空目录int rmdir(const char *pathname);功能删除一个空目录。返回值成功返回0失败返回-1。注意只能删除空目录如果目录非空则删除失败。三、代码示例示例 1getcwd 与 chdir 基本用法#include stdio.h #include unistd.h int main(int argc, const char *argv[]) { char buf[1024] {0}; getcwd(buf, sizeof(buf)); printf(buf %s\n, buf); chdir(/home/linux); getcwd(buf, sizeof(buf)); printf(buf %s\n, buf); return 0; }先获取当前工作目录并打印然后切换到/home/linux再次获取并打印验证chdir生效。示例 2mkdir 创建目录#include stdio.h #include sys/stat.h #include sys/types.h int main(int argc, const char *argv[]) { mkdir(argv[1], 0777); return 0; }通过命令行参数指定目录名权限设为0777实际会受 umask 影响。示例 3rmdir 删除空目录#include stdio.h #include unistd.h int main(int argc, const char *argv[]) { if (rmdir(argv[1]) 0) { perror(rmdir fail); return -1; } return 0; }删除命令行参数指定的空目录失败时打印错误信息。示例 4综合操作 — chdir getcwd mkdir open#include stdio.h #include unistd.h #include sys/stat.h #include sys/types.h #include fcntl.h int main(int argc, const char *argv[]) { char buf[1024] {0}; getcwd(buf, sizeof(buf)); printf(before cd %s\n, buf); chdir(/home/linux); getcwd(buf, sizeof(buf)); printf(after cd %s\n, buf); mkdir(dir123, 0777); chdir(dir123); open(123.txt, O_WRONLY | O_CREAT, 0666); return 0; }先获取当前路径切换到/home/linux创建dir123目录进入该目录并创建一个123.txt文件。示例 5opendir readdir 遍历目录#include stdio.h #include dirent.h int main(int argc, const char *argv[]) { if (argc ! 2) { printf(Usage: %s dir name\n, argv[0]); return -1; } DIR *dp opendir(argv[1]); if (dp NULL) { perror(opendir fail); return -1; } struct dirent *pdir NULL; while (1) { pdir readdir(dp); if (pdir NULL) break; printf(%s , pdir-d_name); } putchar(\n); return 0; }循环读取目录中的每个条目并打印文件名。注意这里会包含.和..。示例 6统计目录下普通文件和目录的个数#include stdio.h #include dirent.h int main(int argc, const char *argv[]) { if (argc ! 2) { printf(Usage: %s dir name\n, argv[0]); return -1; } DIR *dp opendir(argv[1]); if (dp NULL) { perror(opendir fail); return -1; } int cnt_r 0; int cnt_d 0; struct dirent *pdir NULL; while (1) { pdir readdir(dp); if (pdir NULL) break; if (pdir-d_type DT_REG) cnt_r; else if (pdir-d_type DT_DIR) cnt_d; } putchar(\n); closedir(dp); printf(cnt_r %d cnt_d %d\n, cnt_r, cnt_d); return 0; }利用d_type字段区分普通文件DT_REG和目录DT_DIR并分别计数。示例 7隐藏文件过滤与注意 closedir 不可多次调用#include stdio.h #include dirent.h int main(int argc, const char *argv[]) { if (argc ! 2) { printf(Usage: %s dir name\n, argv[0]); return -1; } DIR *dp opendir(argv[1]); if (dp NULL) { perror(opendir fail); return -1; } struct dirent *pdir NULL; while (1) { pdir readdir(dp); if (pdir NULL) break; if (pdir-d_name[0] ! .) printf(%s , pdir-d_name); } putchar(\n); closedir(dp); closedir(dp); // ⚠️ 注意这里多次调用 closedir 是错误写法 return 0; }通过判断文件名首字符是否为.来过滤隐藏文件。代码末尾两次调用closedir(dp)是一个错误示例实际开发中应避免因为多次释放同一个目录流指针会导致未定义行为。示例 8fdopen — 将文件描述符转为 FILE 指针#include stdio.h #include fcntl.h int main(int argc, const char *argv[]) { int fd open(1.txt, O_RDONLY); if (fd 0) { perror(open fail); return -1; } printf(fd %d\n, fd); FILE *fp fdopen(fd, r); if (fp NULL) { perror(fdopen fail); return -1; } char buf[100]; while (1) { char *ret fgets(buf, sizeof(buf), fp); if (ret NULL) { if (feof(fp)) { perror(fgets feof); break; } else if (ferror(fp)) { perror(fgets fail); break; } } printf(buf %s\n, buf); } return 0; }先用底层的open系统调用打开文件获得文件描述符fd再通过fdopen将其转换为标准 I/O 的FILE *指针然后就可以使用fgets等标准库函数读取文件内容。通过feof和ferror判断读取结束的原因。示例 9lseek 实现文件空洞拷贝#include stdio.h #include unistd.h #include fcntl.h int main(int argc, const char *argv[]) { if (argc ! 3) { printf(Usage: %s src dest\n, argv[0]); return -1; } int fd_s open(argv[1], O_RDONLY); int fd_d open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0666); if (fd_s 0 || fd_d 0) { perror(open fail); return -1; } off_t len lseek(fd_s, 0, SEEK_END); printf(len %ld\n, len); lseek(fd_d, len - 1, SEEK_SET); write(fd_d, , 1); close(fd_s); close(fd_d); return 0; }这个示例展示了lseek的用法先用lseek(fd_s, 0, SEEK_END)获取源文件大小然后在目标文件中用lseek定位到相同偏移位置写入一个字节。这会创建一个带有文件空洞的稀疏文件。四、stat 函数与文件属性stat 函数原型#include sys/types.h #include sys/stat.h #include unistd.h int stat(const char *path, struct stat *buf);功能获取文件的属性信息。参数path为文件路径buf为存放属性的结构体指针。返回值成功返回0失败返回-1。struct stat 结构体struct stat { dev_t st_dev; /* 文件所在设备 ID */ ino_t st_ino; /* inode 编号 */ mode_t st_mode; /* 文件类型与权限 */ nlink_t st_nlink; /* 硬链接数 */ uid_t st_uid; /* 所有者用户 ID */ gid_t st_gid; /* 所有者组 ID */ dev_t st_rdev; /* 特殊文件设备 ID */ off_t st_size; /* 文件大小字节 */ blksize_t st_blksize; /* 文件系统 I/O 块大小 */ blkcnt_t st_blocks; /* 已分配的 512B 块数 */ time_t st_atime; /* 最后访问时间 */ time_t st_mtime; /* 最后修改时间 */ time_t st_ctime; /* 最后状态变更时间 */ };完整 stat 示例代码#include stdio.h #include sys/types.h #include sys/stat.h #include unistd.h #include time.h int main(int argc, const char *argv[]) { if (argc ! 2) { printf(Usage: %s filename\n, argv[0]); return -1; } struct stat st; if (stat(argv[1], st) 0) { perror(stat fail); return -1; } printf(File: %s\n, argv[1]); printf(Size: %ld \tBlocks: %ld \tIO Block: %ld\t, st.st_size, st.st_blocks, st.st_blksize); // 打印文件类型 switch (st.st_mode S_IFMT) { case S_IFSOCK: printf(socket file!\n); break; case S_IFLNK: printf(symbolic file!\n); break; case S_IFREG: printf(reg file!\n); break; case S_IFBLK: printf(block file!\n); break; case S_IFDIR: printf(dir file!\n); break; case S_IFCHR: printf(char file!\n); break; case S_IFIFO: printf(pipe file!\n); break; } printf(Device: %ld \t, st.st_dev); printf(Inode: %ld \t, st.st_ino); printf(Links: %ld\n, st.st_nlink); // 打印权限字符串类似 ls -l 的格式 printf(Access: (%#o/, st.st_mode (S_IRWXU | S_IRWXG | S_IRWXO)); switch (st.st_mode S_IFMT) { case S_IFSOCK: putchar(s); break; case S_IFLNK: putchar(l); break; case S_IFREG: putchar(-); break; case S_IFBLK: putchar(b); break; case S_IFDIR: putchar(d); break; case S_IFCHR: putchar(c); break; case S_IFIFO: putchar(p); break; } st.st_mode S_IRUSR ? putchar(r) : putchar(-); st.st_mode S_IWUSR ? putchar(w) : putchar(-); st.st_mode S_IXUSR ? putchar(x) : putchar(-); st.st_mode S_IRGRP ? putchar(r) : putchar(-); st.st_mode S_IWGRP ? putchar(w) : putchar(-); st.st_mode S_IXGRP ? putchar(x) : putchar(-); st.st_mode S_IROTH ? putchar(r) : putchar(-); st.st_mode S_IWOTH ? putchar(w) : putchar(-); st.st_mode S_IXOTH ? putchar(x) : putchar(-); putchar()); printf(\tUid: (%d), st.st_uid); printf(\tGid: (%d)\n, st.st_gid); // 打印时间 struct tm *ptm localtime(st.st_atime); printf(Access: %04d-%02d-%02d %02d:%02d:%02d\n, ptm-tm_year 1900, ptm-tm_mon 1, ptm-tm_mday, ptm-tm_hour, ptm-tm_min, ptm-tm_sec); ptm localtime(st.st_mtime); printf(Modify: %04d-%02d-%02d %02d:%02d:%02d\n, ptm-tm_year 1900, ptm-tm_mon 1, ptm-tm_mday, ptm-tm_hour, ptm-tm_min, ptm-tm_sec); ptm localtime(st.st_ctime); printf(Change: %04d-%02d-%02d %02d:%02d:%02d\n, ptm-tm_year 1900, ptm-tm_mon 1, ptm-tm_mday, ptm-tm_hour, ptm-tm_min, ptm-tm_sec); return 0; }这段代码功能比较完整打印了文件的大小、类型、设备号、inode 号、硬链接数、权限八进制 字符串形式、UID/GID 以及三种时间戳。五、实战手写简易 ls 命令结合目录操作和stat函数可以实现一个类似ls -l的功能#include stdio.h #include sys/types.h #include sys/stat.h #include unistd.h #include pwd.h #include grp.h #include time.h #include dirent.h // 打印单个文件的详细信息 int do_ls_file(const char *filename) { struct stat st; if (stat(filename, st) 0) { perror(stat fail); return -1; } // 1. 文件类型 switch (st.st_mode S_IFMT) { case S_IFSOCK: putchar(s); break; case S_IFLNK: putchar(l); break; case S_IFREG: putchar(-); break; case S_IFBLK: putchar(b); break; case S_IFDIR: putchar(d); break; case S_IFCHR: putchar(c); break; case S_IFIFO: putchar(p); break; } // 2. 权限 st.st_mode S_IRUSR ? putchar(r) : putchar(-); st.st_mode S_IWUSR ? putchar(w) : putchar(-); st.st_mode S_IXUSR ? putchar(x) : putchar(-); st.st_mode S_IRGRP ? putchar(r) : putchar(-); st.st_mode S_IWGRP ? putchar(w) : putchar(-); st.st_mode S_IXGRP ? putchar(x) : putchar(-); st.st_mode S_IROTH ? putchar(r) : putchar(-); st.st_mode S_IWOTH ? putchar(w) : putchar(-); st.st_mode S_IXOTH ? putchar(x) : putchar(-); // 3. 硬链接数 printf( %-3ld , st.st_nlink); // 4. 用户名和组名 printf( %-10s , getpwuid(st.st_uid)-pw_name); printf( %-10s , getgrgid(st.st_gid)-gr_name); // 5. 文件大小 printf( %-10ld , st.st_size); // 6. 最后修改时间 struct tm *ptm localtime(st.st_mtime); printf(%02d月 %02d %02d:%02d , ptm-tm_mon 1, ptm-tm_mday, ptm-tm_hour, ptm-tm_min); // 7. 文件名 printf( %s \n, filename); return 0; } // 遍历目录中所有文件 int do_ls_dir(const char *dirname) { if (dirname NULL) return -1; if (chdir(dirname) 0) { perror(chdir fail); return -1; } DIR *dp opendir(dirname); if (dp NULL) { perror(opendir fail); return -1; } while (1) { struct dirent *pdir readdir(dp); if (pdir NULL) break; do_ls_file(pdir-d_name); } closedir(dp); return 0; } int main(int argc, const char *argv[]) { if (argc ! 2) { printf(Usage: %s dir or filename\n, argv[0]); return -1; } struct stat st; if (stat(argv[1], st) 0) { perror(stat fail); return -1; } // 判断是目录还是文件 if (st.st_mode S_IFDIR) do_ls_dir(argv[1]); else do_ls_file(argv[1]); return 0; }核心逻辑1.用stat判断参数是目录还是普通文件。2.如果是文件直接调用do_ls_file打印详细信息。3.如果是目录调用do_ls_dir内部用opendirreaddir遍历逐个调用do_ls_file。4.do_ls_file中用getpwuid和getgrgid将 UID/GID 转换为可读的用户名和组名。六、文件构成与软链接、硬链接Linux 文件的三要素Linux 系统中一个文件由以下三部分组成目录项struct dirent包含文件名和 inode 节点号。inode 表包含文件的元信息如 inode 号、文件类型、文件大小等。数据块文件实际内容存放的地方。当我们访问一个文件时Linux 系统的工作流程1.根据文件名找到目录项中的inode 号。2.通过 inode 号进入inode 表查找到该文件的元信息。3.通过 inode 表中保存的数据区指针读取相应数据区的内容。硬链接Hard Link本质给文件取别名。特点不占用额外硬盘空间不能对目录操作不能跨文件系统如 ext4 和 FAT/NTFS 之间不能互相创建硬链接与源文件共用同一个 inode 号修改任意一个另一个同时被修改删除一个另一个不受影响只有当文件的打开次数为 0且硬链接数为 0 时文件才会被真正删除命令ln 源文件名 硬链接文件名创建后可以发现硬链接文件和源文件的inode 号相同说明它们指向同一个数据块。软链接Symbolic Link / 符号链接本质类似于 Windows 的快捷方式记录的是目标文件的路径。特点占用少量磁盘空间存储的是路径字符串可以对目录操作可以跨文件系统拥有自己独立的 inode 号与源文件不同删除源文件后软链接将不可用因为存储的路径已经失效命令ln -s 源文件 软链接文件硬链接与软链接对比对比项硬链接软链接inode 号与源文件相同与源文件不同占用空间不额外占用占用少量空间存路径对目录操作不可以可以跨文件系统不可以可以删除源文件不受影响仍可访问链接失效本质文件的别名快捷方式存路径练习建议1.新建一个hello.c文件分别对它做软链接和硬链接操作观察 inode 号的变化。2.在用户主目录/home/linux下创建一个指向/mnt/hgfs/share的软链接shareln -s /mnt/hgfs/share /home/linux/share七、总结本文覆盖了以下核心知识点知识点关键 API / 概念目录打开/读取/关闭opendir、readdir、closedir工作目录操作getcwd获取、chdir切换目录创建与删除mkdir、rmdir文件属性获取stat、struct stat文件描述符与流转换fdopen文件定位lseek文件类型判断st_mode S_IFMT权限位操作S_IRUSR、S_IWUSR等硬链接与软链接ln、ln -s掌握这些 API 和原理就具备了在 Linux 下进行基本文件系统操作和简单ls命令实现的能力。建议结合代码动手实践加深理解。
返回列表