ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Linux基础IO:从文件描述符到重定向的底层逻辑

2026/8/27 9:15:23 拓冰建站 浏览量
Linux基础IO:从文件描述符到重定向的底层逻辑 前言本章通过六部分带你去梳理基础IO的内容。一、重新认识文件--从c接口出发1、文件属性内容2、进程打开文件后会记录当前工作路径(cwd)。这里可以通过实验来看int main(){FILE *fp fopen(myfile, w);if(!fp){printf(fopen error!\n);}w hile(1);fclose(fp);return 0;}可以通过ls -l /proc/PID 来查看此时PID是你运行程序的PID。此时cwd就是当前工作路径3、语言层接口(fopen,fwrite,fread)是系统封装函数方便用户接入。FILE *fopen(const char *restrict pathname, const char *restrict mode);这个接口是打开一个文件的有了就打开没有就直接创建mode是类型看你选择是读还是写。size_t fread(void ptr[restrict .size * .nmemb], size_t size, size_t nmemb,FILE *restrict stream);这个接口是读的从你的文件里读取内容到内存里。size_t fwrite(const void ptr[restrict .size * .nmemb],size_t size, size_t nmemb, FILE *restrict stream);这个接口是做写入的将数据写入到这个文件里。在这里不做详细介绍可以取Linux系统中man相关函数去进行详细的了解。//////////////////////////////////////////////////////////////////////////////////////////////////int main(){FILE* fpfopen(myfile,w);char buf[1024];const char* msghello world;if(!fp){printf(fopen is error\n);}int count5;while(count--){fwrite(msg,strlen(msg),1,fp);}fclose(fp);fpfopen(myfile,r);size_t i;while((i(fread(buf,1,sizeof(buf)-1,fp)))0){buf[i]\0;printf(%s\n,buf);}fclose(fp);return 0;}上边是一段代码可以练习读写文件的操作同时完成一个简易的cat小程序。二、跨过语言层--使用系统调用我们上文讲述的fwritefopen等函数其实底层调用的就是这里的openwrite等的系统调用。1、系统调用int open(const char * pathname,int flags,mode_t mode);这个pathname就是你想要打开的文件。flags是可以传入多个参数选项下边列举几个O_RDONLY:只读打开O_WRONLY:只写打开O_RDWR:读写打开O_CREAT:若文件不存在就创建O_APPEND:追加写返回值:-1就是失败了如果成功就会返回文件描述符(fd).2、ssize_t write(int fd, const void buf[.count], size_t count);fd就是上文提到的文件描述符也就是open如果成功打开的返回值fd是进程内部的资源编号writeread等系统调用只认fd也就是说通过fd可以找到你这个文件。buf这个就是一个数组在这里返回也就是返回的是buf的指针。这里buf其实就是后文要讲的用户缓冲区提前了解一下就是系统调用是有代价的如果频繁的调用系统调用的话会导致效率低下所以在这里采用了缓冲区的思想。count就是第二个参数的大小。返回值0的情况是成功写入了多少字节数0的情况在write极少出现只需要记住0即可-1出错3、ssize_t read(int fd, void buf[.count], size_t count);这个与上文的write类似就不再多说了着重说一下返回值。返回值0的情况就是读到了多少字节数0的情况是读到了文件末尾-1就是出错了代码练习:实现向文件写入5次然后读取并打印到显示器上int main(){int fdopen(myfile,O_WRONLY|O_CREAT,0644);int count5;const char* msghello world\n;while(count--){write(fd,msg,strlen(msg));}close(fd);fdopen(myfile,O_RDONLY);size_t i;char buf[1024];while((iread(fd,buf,sizeof(buf)))0){buf[i]\0;printf(%s,buf);}close(fd);return 0;}该部分思考1、open 的返回值是什么答失败了返回-1成功了返回文件描述符fd。2、为什么系统调用要传递mode(如0644)答因为文件创建需要权限。3、write的第三个参数是期望写入的字节数第二个参数是缓冲区首地址返回值实际写入的字节数如果说返回比期望少该怎么办答循环重试。(解析出现这个情况就是说部分写入部分没有写入发现写入信号中断就会进行循环重试移动缓冲区首地址变相的相当于又有了一块地方可以写入此时就可以继续写直到写完为止。)三、文件描述符的秘密--0、1、2图片1、实验并观察现象1、先close(0),再open打印fd观察现象int main(){close(0);int fdopen(myfile,O_CREAT|O_RDONLY);printf(fd的值:%d\n,fd);return 0;}这里可以加一个部分就是可以回顾之前的知识就是你可以在open那里的代码中第二个参数把O_CREAT去掉此时就会返回-1也就是打开失败为什么呢可以想一想。2、再试试close(2)惯出现象。{close(2);int fdopen(myfile,O_CREAT|O_RDONLY);printf(fd的值:%d\n,fd);return 0;}3、指令:ls -l /proc/PID/fd复制新的ssh渠道去观察现象(对了记得把close注释掉要不然观察不全)。会发现0123.后续还会讲此时点一点这个数字其实就是文件描述符fd。坑点注意在这里有一个点会非常容易出错就是open失败的场景不妨看一下这个代码以及图示int main(){close(2);pid_t idgetpid();printf(PID的值是:%d\n,id);int fdopen(myfile,O_CREAT|O_WRONLY|O_TRUNC,0644);if(fd0){printf(error\n);}while(1){sleep(1);}close(fd);return 0;}乍一看没有错误对吧看文件创建失败返回-1为什么呢注意如果按照本文代码一点一点的敲的话会发现open的话之前会创建一个权限是只读的权限此时如果你修改了权限但是之前的文件没有删除就会open失败之前的文件是只读的你如果要写入就会直接失败没有权限同时可以看得到一个结论就是O_CREAT只管新文件创建。删除后恢复正常。2、文件描述符(fd)的分配规则同样观察现象代码和上边的代码一样改改就行这里就不放了就是那个坑点的代码。1、如果说close(2)的情况下可以观察到close(2)之后就少了一个2-/dev/pts/0反而说原来的/home/.../myfile却被2指了。2、再观察一组现象把close(2)改为close(0)可以观察到/home/..../myfile又到了0。也就是说我close哪个就去哪个这也就是fd分配的规则就是找当前未被占用的最小下标3、剩余知识1、Linux的三个标准流stdin(0),stdout(1),stderr(2),分别为标准输入标准输出标准错误。2、每一个进程都会有一个file_struct里边有一个指针数组fd_array[],下标就是文件描述符fd指向打开的struct_file。该部分思考1、为什么fd从3开始答0、1、2都被标准流占用自己的文件依据分配规则占用最小下标也就是3.2、为什么操作系统无法用一个整数直接表示复杂文件信息答fd只是一个索引真正的文件信息都在struct_file由内核维护。四、重定向的本质--dup2与shell增强1、给出代码观察实验现象引出重定向int main(){close(1);int fdopen(myfile,O_WRONLY|O_CREAT,0644);if(fd-1){perror(open fail);}printf(hello);fflush(stdout);close(fd);return 0;}可以观察到当关闭stdout时运行hello可执行程序却没有打印而这个值却放在了myfile里。修改代码并再次观察实验现象。int main(){pid_t idgetpid();printf(PID:%d\n,id);close(1);int fdopen(myfile,O_WRONLY|O_CREAT,0644);if(fd-1){perror(open fail);}printf(hello\n);fflush(stdout);while(1){sleep(1);}close(fd);return 0;}根据fd分配规则我们创建的文件自然会在fd1的位置出现。fd1的为标准输出通过这个索引可以指向对应的文件标准情况下就是显示器所以说没有close(1)的时候能在显示器上看到。当close掉1之后我们创建的文件就会被1索引也就是说输出从显示器变到了我们创建的文件此时计算机并不知道这些依然写所以数据就被写入到了我们的文件之中显示器不显示而cat我们的文件可以发现数据在我们的文件之中。这也就是重定向。2、dup2格式int dup2(int oldfd, int newfd);部分代码int fdopen(myfile,O_WRONLY|O_CREAT,0644);dup2(fd,1);printf(hello\n);fflush(stdout);可以观察到dup2第一个参数是旧的fd第二个参数是新的fd。效果和上边的代码一样但是这里没有close(1)也就是说让newfd指向oldfd的文件表会自动关闭newfd打开的文件。自然就不用close了。这也就是dup2的一个大好处。3、知识点详解1、重定向的底层本质就是修改fd也就是修改指向让某个fd指向另外一个文件。2、dup2(oldfd,newfd)newfd会复制oldfd的副本即使newfd已经打开会被覆盖。该部分思考1、重定向的本质是改变了什么答改变了进程的fd数组项使得原本指向终端的fd指向了别的文件。2、为什么重定向必须在fork后的子进程中做答因为shell是父进程如果说修改shell的文件描述符则会修改I/O环境后续的进程都会受影响而创建子进程时会复制父进程的文件描述符也就是所谓的副本此时修改文件描述符不会影响父进程。五、一切皆文件--内核视角1、内核部分在这里可以发现file是一个文件对象其中最重要的就是有一个f_op指针指向struc file_operations(函数操作表)接下来看看这个函数操作表。这是一部分也就是说当用户调用read(fd,buf,n)的时候就会通过fd找到这个struct file此时再通过调用f_op-read去完成相关操作。下边看一张图片去理解部分内容。通过这一张图片可以发现当调用read时会去调用不同硬件的read和write怎么理解呢指令:ls -l /dev。这个指令可以打印硬件设备。所以可以看到各个硬件会被抽象成统一设备文件此时各个硬件实现自己的readwrite函数当open设备的时候内核把struct_file 里的f_op指针指向该驱动自己的file_operations实例。这体现了一个点就是多态。该部分思考1、为什么read系统调用既能读取磁盘文件也能读键盘答因为read通过fd找到对应的struct file再通过f_op-read回调到不同的驱动函数。2、Linux下一切接文件的好处是什么答统一的API简化开发方便管理。六、缓冲区之谜--为什么重定向之后printf出现两次1、缓冲区类型全缓冲区这种缓冲方式要求填满整个缓冲区之后才进行I/O系统调用操作一般情况下对于磁盘文件的操作通常使用全缓冲的方式访问。行缓冲区在行缓冲情况下当在输入和输出中遇到换行符是标准I/O库函数会执行系统调用操作。无缓冲区:指的是标准I/O库不对字符进行缓存直接调用系统调用。2、代码实验(简单认识行缓冲和全缓冲)int main(){close(1);int fdopen(myfile,O_CREAT|O_WRONLY|O_TRUNC,0664);if(fd0){perror(open fail);}printf(hello world%d:,fd);close(fd);return 0;}此时可以发现文件内是没有信息的可以cat看看默认是全缓冲此时没有存满用户缓冲区是不会去调用系统函数的然后close就关掉了如果想要写入就加一个fflush强制刷新即可此时就是行缓冲。int main(){close(1);int fdopen(myfile,O_CREAT|O_WRONLY|O_TRUNC,0664);if(fd0){perror(open fail);}printf(hello world%d:\n,fd);close(fd);return 0;}3、实验(认识系统调用无缓冲区和库函数调用有缓冲区)int main(){const char* msg0hello printf\n;const char *msg1hello fwrite\n;const char* msg2hello write\n;printf(%s,msg0);fwrite(msg1,strlen(msg1),1,stdout);write(1,msg2,strlen(msg2));fork();return 0;}运行可执行程序可以发现这种情况正常打印是因为终端是行缓冲遇到换行符会立即刷新所以会直接在终端上看见。但是当重定向到文件后cat文件可以看到一个现象。可以看到系统调用的write是只打印了一次而使用库函数的两个分别打印了两次。系统调用是无用户缓冲也就是说会直接打印在终端上而重定向到普通文件时数据的缓冲方式就变成了全缓冲也就是说没有存满是不可能刷新的此时fork创建子进程子进程会继承父进程的数据也就是说此时有了两份数据。进程退出之前会强制刷新所以就看到了两份数据。同时如果说在fork之前刷新用户缓冲区那么将数据刷洗到内核缓冲区此时用户缓冲区数据清零子进程继承不到所以重定向只会去打印一份。(代码不重复了)该部分思考1、用户缓冲区是哪个模块提供的答C标准库提供的2、为什么fork会使缓冲区的内容出现两份答因为firk发生了写时拷贝子进程会拿到父进程缓冲区的副本退出时都会刷新。后记后续会更新文件系统相关内容觉得写的不错的可以点点赞关注一下。