Linux系统编程-文件操作和目录操作(黑马笔记)

张开发
2026/6/10 9:09:52 15 分钟阅读
Linux系统编程-文件操作和目录操作(黑马笔记)
1.系统调用和库函数的区别系统调用操作系统内核提供给应用程序的底层接口用来访问内核功能比如操作文件、进程、硬件。库函数C 标准库或其他库提供的函数运行在用户态是对系统调用或其他逻辑的封装。总结来说系统调用是操作系统内核提供的底层函数是应用程序访问硬件、文件、进程的唯一入口但接口原始、难用、不安全。所以库函数比如 C 标准库 IO 函数就是在系统调用外面包了一层统一标准接口跨平台加参数检查加用户层缓冲区 buffer加错误处理、封装逻辑让系统调用变得更易用、更高效、更安全 具体区别对照表维度系统调用System Call库函数Library Function调用方式触发软中断syscall指令[会切换到内核态]普通函数调用call指令[不会切换到内核态]执行权限需要切换到内核态ring 0用户态即可ring 3执行速度慢用户↔内核切换开销快无上下文切换功能范围直接与硬件、资源交互如文件、进程、网络可以是系统调用的封装也可以纯计算如字符串处理例子open,read,write,fork,execvefopen,fread,printf,malloc,strlen来源Linux 内核提供libc如 glibc或其他库提供可移植性依赖操作系统Linux 特有标准库函数跨平台如 ANSI C2.open和close函数open#include fcntl.h #include sys/stat.h #include unistd.h int open(const char *pathname, int flags, mode_t mode);核心参数参数名类型详细解析pathnameconst char*要打开 / 创建的文件路径支持绝对路径 / 相对路径如/tmp/test.txtflagsint打开文件的模式标志可通过 基础读写权限必选其一O_RDONLY只读模式 O_WRONLY只写模式 O_RDWR可读可写模式写入行为控制可选叠加O_CREAT文件不存在则创建需配合 mode 参数 O_APPEND所有写操作追加到文件末尾O_TRUNC文件存在且可写时清空原有内容modemode_t仅当 flags 包含 O_CREAT 时生效指定新建文件的初始权限八进制数▌示例0644 → 对应权限 rw-r--r--所有者读写其他只读▌最终权限计算mode ~umask二进制位运算去掉 umask 禁止的权限位才是文件实际权限▌umask 作用系统默认屏蔽危险权限如默认 002 会屏蔽其他用户的写权限flags// 1. 追加写日志最常用 int fd1 open(app.log, O_WRONLY | O_CREAT | O_APPEND, 0644); // 2. 覆盖写入文件 int fd2 open(output.txt, O_WRONLY | O_CREAT | O_TRUNC, 0644); // 3. 只读打开 int fd3 open(config.json, O_RDONLY);mode八进制要0来开头在使用了O_CREAT后必须加上的参数规定创建的文件的所属权限。常见的0644本质上是110 010 010 自己可以读写 同组用户和其他用户只能读。 最后一个位表示是否可以被执行返回值结果返回值补充说明成功fd非负整数文件描述符后续读写 / 关闭文件需使用该 fd失败-1同时设置全局变量errno如 ENOENT文件不存在EEXISTO_EXCLO_CREAT 时文件已存在closeclose 函数int close(int fd);demo#include unistd.h #include fcntl.h #include stdio.h #include errno.h #include string.h int main(int argc, char *argv[]) { int fd; /* 1. 权限读写2. 不存在则创建3. 若已存在则清空 */ fd open(./dict.cp, O_RDWR | O_CREAT | O_TRUNC, 0644); /* rw-r--r-- */ /* 4. 必须检查返回值 */ if (fd -1) { //stderr标准输入流 默认是终端 //stderr是标准错误输出流 打印后面两个参数 strerror是将对应的错误码转化为错误信息 fprintf(stderr, open failed: %s\n, strerror(errno)); return 1; } printf(fd %d\n, fd); /* 5. 关闭同样检查错误虽然极少失败但养成习惯 */ if (close(fd) -1) { //错误处理函数 perror(close); return 1; } return 0; }other文件描述符整个虚拟内存分为用户区0-3G和内核区(3G-4G)。用户区 正常分区的内存地址是从低位-高位。 但是栈的内存地址是从高位到低位。内核区PCB是一个进程控制块每个创建好的进程都会给其在内核里面申请一块PCB里面存储一个进程运行所需要的信息。 其中就有一个fd数组里面存储了这个进程打开的所有文件并且每个文件都有一个对应的fd。在这个进程中可以通过这个fd访问这个文件。不过值得注意的是因为是指针其他进程也可以通过不同的fd访问相同的文件。预读入和缓输出文件 I/O 的缓存机制用户态的buffer设计方式预读入缓输出为了优化linux读写文件的性能提出的一种缓存策略。预读入 缓输出1.如果既没有用户态bufferC 标准库缓冲也没有内核态buffer页缓存那么如果执行下面代码写字符到文件中。int fd open(file.txt, O_WRONLY|O_CREAT); for (int i 0; i 1000; i) { write(fd, A, 1); // 每次只写 1 字节 } close(fd);此时这1000个write会产生1000次从用户态到内核态的切换消耗1000次。并且内核也会对磁盘进行1000次IO那么就会大大拖累读写的效率。2.如果在此基础上添加了内核态buffer页缓存这里用户虽然产生了1000次用户态到内核态的切换。但是全都写入到页缓存里了然后异步刷盘到了磁盘里。 减少了磁盘IO的开销全自动的3.用户态多一个bufferfputc和fread的区别是一个是写入一个字符一个是写入一个字符串FILE *fp fopen(file.txt, w); for (int i 0; i 1000; i) { fputc(A, fp); // 写到用户层 buffer 一次只写一个 } fclose(fp); // 一次性 flush 到内核这个方式下1000次都是写入到了用户态的buffer然后只有一次read和刷盘是最有效率的 也就是“缓输出” 本质就是不着急一个一个来而是用一个buffer攒着然后一次性运输存储的数据。#include stdio.h #include fcntl.h #include unistd.h int main() { char c; int i; // // 版本1无用户缓冲只靠内核自动预读 // 每次读1字节 → 1000次用户-内核切换 // 内核会预读但切换开销极大 // int fd open(file.txt, O_RDONLY); for (i 0; i 1000; i) { read(fd, c, 1); // 每次1字节无用户缓冲 // 内核自动预读到页缓存 // 但每次都切内核非常慢 } close(fd); // // 版本2加用户层缓冲FILE* fgetc // 一次性 read 4KB 到用户 buffer // 只触发 1 次系统调用 // FILE *fp fopen(file.txt, r); for (i 0; i 1000; i) { c fgetc(fp); // 先从【用户缓冲】取 // 缓冲区空了才调用 read // 减少99%的内核切换 } fclose(fp); // // 整体两层缓冲总结最精简版 // // 磁盘 → 内核页缓存预读减少磁盘IO // ↑ // 内核 read 一次读一大块 // ↑ // 用户层缓冲fgetc内部减少切换 // ↑ // 你的程序 // return 0; }ftml函数int fcntl(int fd, int cmd, ... /* arg */ );参数fd文件描述符。cmd命令指定要执行的操作。arg命令的参数具体取决于cmd的值。返回值成功时返回执行命令后的结果这可能因命令不同而有所不同。失败时返回 -1并设置errno以指示错误。常见的命令F_DUPFD复制文件描述符。这个命令会创建一个新的文件描述符它与原始文件描述符引用同一个文件。F_GETFD获取文件描述符的标志。这些标志控制文件描述符的行为例如是否应该在执行exec函数时关闭文件描述符。F_SETFD设置文件描述符的标志。F_GETFL获取文件状态标志。这些标志控制文件的打开模式例如是否为只读或只写。设置fd从阻塞到非阻塞。阻塞就是文件没有数据那么代码就卡在这一行不动非阻塞就是会继续执行然后会返回错误码#include unistd.h #include fcntl.h #include stdio.h int set_nonblock(int fd, int on) { int flags fcntl(fd, F_GETFL); if (flags -1) return -1; if (on) flags | O_NONBLOCK; else flags ~O_NONBLOCK; //设置flag成功返回1否则返回0 return fcntl(fd, F_SETFL, flags); } int main() { int fd open(fifo, O_RDWR); if (fd -1) { perror(open); return 1; } set_nonblock(fd, 1); /* 设为非阻塞 */ /* ... 读写 ... */ set_nonblock(fd, 0); /* 恢复阻塞 */ close(fd); return 0; }2.4使用fcntl函数设置文件描述符#include stdio.h #include fcntl.h #include unistd.h #include errno.h #include string.h int main() { // 随便打开一个文件也可以是 socket、pipe 等 int fd open(test.txt, O_RDONLY); if (fd -1) { perror(open failed); return 1; } // // 获取当前文件状态标志 // int flags fcntl(fd, F_GETFL, 0); if (flags -1) { perror(fcntl F_GETFL failed); close(fd); return 1; } // // 添加非阻塞标志 O_NONBLOCK // if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) -1) { perror(fcntl F_SETFL O_NONBLOCK failed); close(fd); return 1; } printf(设置非阻塞成功\n); close(fd); return 0; }3.read和write函数read 函数原型ssize_t read(int fd, void *buf, size_t count);参数fd 文件描述符buf 存放读入数据的缓冲区count 缓冲区大小最多读多少字节返回值成功实际读到的字节数可能 count失败-1并设置 errno-1并且errnoBAGIN或EWOULDBLOCK说明不是read失败而是被阻塞设备文件或者网络文件write 函数原型ssize_t write(int fd, const void *buf, size_t count);参数fd 文件描述符buf 待写入数据的缓冲区count 要写入的字节数返回值成功实际写入的字节数可能 count失败-1并设置 errnodemo实现copy效果#include stdio.h #include unistd.h #include fcntl.h #include errno.h int main(int argc, char *argv[]) int fd1 open(argv[1], O_RDONLY); if (fd1 -1) { perror(open src); return 1; } //以读写打开有就截断为空没有就是create0664是创建的权限 int fd2 open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0664); if (fd2 -1) { perror(open dst); close(fd1); return 1; } char buf[1024]; /* 64K 缓冲更快 */ ssize_t n; while ((n read(fd1, buf, sizeof(buf))) 0) { if (write(fd2, buf, n) ! n) { perror(write); break; } } if (n -1) perror(read); close(fd1); close(fd2); return 0; }4.lseek函数lseek 函数控制光标的偏移off_t lseek(int fd, off_t offset, int whence);参数fd 文件描述符offset 偏移量字节数whence 偏移基准SEEK_SET 文件开头SEEK_CUR 当前位置SEEK_END 文件末尾返回值成功返回新的偏移量相对于文件起始位置的字节数失败-1并设置 errnodemo1. 文件的“读”和“写”使用同一偏移位置。#include stdlib.h #include unistd.h #include string.h #include fcntl.h int main(void) { int fd, n; char msg[] Its a test for lseek\n; char ch; fd open(lseek.txt, O_RDWR | O_CREAT, 0644); if (fd 0) { perror(open lseek.txt error); exit(1); } write(fd, msg, strlen(msg)); /* 使用 fd 对打开的文件进行写操作文件读写位置位于文件结尾处 */ lseek(fd, 0, SEEK_SET); /* 修改文件读写指针位置位于文件开头。注释该行会怎样呢 */ while ((n read(fd, ch, 1))) { if (n 0) { perror(read error); exit(1); } /* 此处缺少输出或处理逻辑可自行补充 */ } close(fd); return 0; }demo2. 使用 lseek 获取文件大小。#include stdio.h #include stdlib.h #include string.h #include unistd.h #include fcntl.h int main(int argc, char *argv[]) { int fd open(argv[1], O_RDWR); if (fd -1) { perror(open error); exit(1); } int length lseek(fd, 0, SEEK_END); printf(file size: %d\n, length); close(fd); return 0; }demo3. 使用 lseek 拓展文件大小要想使文件大小真正拓展必须引起一次 IO 操作如 write 一个字节#include stdio.h #include fcntl.h #include stdlib.h #include string.h #include unistd.h int main(int argc, char *argv[]) { int fd open(argv[1], O_RDWR); if (fd -1) { perror(open error); exit(1); } int length lseek(fd, 110, SEEK_END); printf(file size:%d\n, length); //引起真正的io操作否则只是简单的偏移。 write(fd, a, 1); close(fd); return 0; }5.inode和detry总结一句话是inode本身是文件的一种数据结构他记录了许多文件的属性。dentry是目录项一个目录中有许多dentrydentry记录了文件名-inode的映射。inode索引节点inode是文件系统中的一个数据结构用于存储文件的元数据metadata但不包括文件名。元数据包括文件的权限、所有者、大小、创建时间、修改时间、访问时间、文件类型如普通文件、目录、符号链接等、以及指向文件数据块的指针等。每个文件在文件系统中都有一个唯一的 inode 号通过这个 inode 号可以找到文件的 inode进而访问文件的元数据和数据块。dentry目录项dentry是目录中的一个条目它将文件名与 inode 号关联起来。目录际上是一个特殊的文件其中包含了多个 dentry每个 dentry 包含一个文件名和一个指向 inode 的指针。· 当你访问一个文件时系统首先在目录中查找对应的 dentry通过 dentry 中的 inode 号找到文件的 inode然后通过 inode 访问文件数据。6.lstat和stat函数其实就是用文件名获得inode通过inode结构体获取文件的一些属性。stat/lstat 函数int stat(const char *path, struct stat *buf);参数path文件路径buf传出参数存放文件属性。返回值成功0失败-1 errno获取文件大小buf.st_size获取文件类型buf.st_mode获取文件权限buf.st_mode符号穿透stat会。lstat不会。demo#include sys/stat.h #include pthread.h int main(int argc, char *argv[]) { struct stat sb; int ret stat(argv[1], sb); if (ret -1) { perror(stat error); exit(1); } if (S_ISREG(sb.st_mode)) { printf(Its a regular\n); } else if (S_ISDIR(sb.st_mode)) { printf(Its a dir\n); } else if (S_ISFIFO(sb.st_mode)) { printf(Its a pipe\n); } else if (S_ISLNK(sb.st_mode)) { printf(its a sym link\n); } return 0; }文件权限位7.link和unlink函数link函数硬链接和软连接的区别主要是在硬链接是不同的dentry但还是相同的inode相当于给文件取了一个别名我的理解而软连接是新创建了一个文件用于链接可以类比windows中的快捷方式。#include stdio.h #include stdlib.h #include string.h #include unistd.h #include pthread.h int main(int argc, char *argv[]) { link(argv[1], argv[2]); unlink(argv[1]); return 0; }unlink函数调用unlink后并没有直接删除而是系统择机删除#include unistd.h #include fcntl.h #include stdlib.h #include string.h #include stdio.h int main(void) { int fd; char *p test of unlink\n; char *p2 after write something.\n; fd open(temp.txt, O_RDWR | O_CREAT | O_TRUNC, 0644); if (fd 0){ perror(open temp error); exit(1); } int ret write(fd, p, strlen(p)); if (ret -1) { perror(----write error); } printf(hi! Im printf\n); ret write(fd, p2, strlen(p2)); if (ret -1) { perror(----write error); } printf(Enter anykey continue\n); getchar(); close(fd); int unlink_ret unlink(temp.txt); if(unlink_ret 0){ perror(unlink error); exit(1); } return 0; }7.opendir和closedir以及readdir()7.1文件夹的权限概念7.2opendir根据传入的目录名打开一个目录库函数DIR *opendir(const char *name); 成功返回指向该目录结构体指针失败返回 NULL参数支持相对路径、绝对路径两种方式DIR是目录结构体目录里有许多目录项dentry’例如打开当前目录① getcwd(), opendir() ② opendir(.);7.3closedir()关闭打开的目录。int closedir(DIR *dirp); 成功0失败-1 设置 errno 为相应值。7.4readdir函数struct dirent *readdir(DIR *dirp);参数dirp一个指向DIR结构的指针该结构由opendir函数返回表示一个已经打开的目录。返回值成功时readdir返回一个指向struct dirent结构的指针该结构包含了目录中下一个条目的信息。如果到达了目录流的末尾返回NULL。如果发生错误也返回NULL并设置errno以指示错误类型。返回值struct dirent内容#include dirent.h struct dirent { long d_ino; // 文件的 inode 编号 off_t d_off; // 文件在目录中的偏移量 unsigned short d_reclen; // 结构体的长度 unsigned char d_type; // 文件类型 char d_name[256]; // 文件或目录的名称 };demo实现递归遍历目录#include stdio.h #include stdlib.h #include string.h #include unistd.h #include pthread.h #include sys/types.h #include sys/stat.h #include dirent.h void isFile(char *path); void isDir(char *path){ DIR *dir opendir(path); char buf[256]; if(dir NULL){ perror(opendir error); exit(1); } struct dirent *entry; while((entry readdir(dir)) ! NULL){ { //递归调用需要拼接路径 默认目录里都带有.和..代表当前文件夹和上一级文件夹 if(strcmp(entry-d_name, .) 0 || strcmp(entry-d_name, ..) 0){ continue; // 跳过当前目录和上级目录 } //path是目录d_name是文件名 sprintf(buf, %s/%s, path, entry-d_name); isFile(buf); } } } void isFile(char *path){ struct stat buf; int ret stat(path,buf); if(ret-1){ perror(stat error); exit(1); } if(S_ISDIR(buf.st_mode)){ //目录处理逻辑 isDir(path); } //文件处理逻辑 printf(%s \t %ld\n,path,buf.st_size); } int main(int argc, char *argv[]) { if(argc1){ isFile(.); }else{ //遍历多参数 for(int i1;iargc;i){ isFile(argv[i]); } } return 0; }8.dup和dup2重定向函数这里的重定向其实就是将fd文件描述符重定向掉其他文件比如fd1 本来指向hello.c文件我们可以通过重定向将fd1 指向world.c文件。 这样可以实现相同的fd来操作不同的文件。8.dup和dup2重定向函数对oldfd所指的文件创建一个新的fd就可以通过newfd来操作老资源了dup 函数dup函数用于复制一个现有的文件描述符并返回一个新的文件描述符该描述符与原始文件描述符引用相同的文件或资源。函数原型int dup(int oldfd);参数oldfd要复制的原始文件描述符。返回值成功时返回一个新的文件描述符。失败时返回 -1并设置errno以指示错误。dup2函数dup2函数不仅复制文件描述符还可以指定新文件描述符的值。如果指定的新文件描述符已经打开它会被关闭并替换为新的文件描述符。int dup2(int oldfd, int newfd);参数oldfd要复制的原始文件描述符。newfd指定的新文件描述符的值。newfd和oldfd指向同一块位置。 以后对newfd的访问其实全部都是oldid返回值成功时返回新的文件描述符通常与newfd相同。失败时返回 -1并设置errno以指示错误。demo#include stdlib.h #include string.h #include fcntl.h #include unistd.h #include pthread.h int main(int argc, char *argv[]) { int fd1 open(argv[1], O_RDWR); // 012 --- 3 int fd2 open(argv[2], O_RDWR); // 012 --- 3 //如果无异常fd2和fdret相同 int fdret dup2(fd1, fd2); printf(fdret %d\n, fdret); //向fd2写入数据但其实此时已经是像fd1写入数据了 int ret write(fd2, 1234567, 7); printf(ret %d\n, ret); //此时可以用新的操作旧的了。这里新的是标准输入流如果对标准输入流进行读操作实际上是对fd1进行读操作 dup2(fd1, STDOUT_FILENO); printf(-----------------------------886\n); return 0; }

更多文章