文章目录
💐专栏导读💐文章导读🐧C语言中的文件操作🐦fopen函数🐦写文件🐦读文件 🐧系统文件I/O🐦open系统调用 🐧文件描述符🐦fd 0、1、2去哪了🐦文件描述符的作用 🐧文件描述符的分配规则🐧重定向🐦dup2系统调用
💐专栏导读
🌸作者简介:花想云 ,在读本科生一枚,C/C++领域新星创作者,新星计划导师,阿里云专家博主,CSDN内容合伙人…致力于 C/C++、Linux 学习。
🌸专栏简介:本文收录于 Linux从入门到精通,本专栏主要内容为本专栏主要内容为Linux的系统性学习,专为小白打造的文章专栏。
🌸相关专栏推荐:C语言初阶系列、C语言进阶系列 、C++系列、数据结构与算法。
💐文章导读
本章我们将深入学习Linux文件系统,深入理解文件操作的底层原理。首先我们将回忆在C语言阶段曾经学习过的文件操作相关函数,其次我们将认识何为文件描述符——OS管理进程对应文件的关键。最后,我们需要理解曾经只是了解过的重定向操作究竟在底层做了什么~
🐧C语言中的文件操作
C语言大家可能都学习过,但是C语言中的文件操作却不经常被使用。我们先来回顾一下C语言中的文件操作。
🐦fopen函数
*FILE fopen (const char *path, const char *mode);
参数path
:指定打开的文件所在路径;
参数mode
:指定文件打开的方式,包含如下方式:
🐦写文件
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
int fprintf(FILE *stream, const char *format, …);
int sprintf(char *str, const char *format, …);
int snprintf(char *str, size_t size, const char *format, …);
🔔示例代码
#include <stdio.h>#define LOG "log.txt"int main(){ // 以 ' w' (只写)的方式打开文件 FILE* fp = fopen(LOG,"w"); if(fp == NULL) { perror("fopen"); return 1; } // 对文件进行操作 const char* msg = "Hello World!"; int cnt = 5; while(cnt) { fprintf(fp,"%s: %d\n",msg,cnt); cnt--; } // 关闭文件 fclose(fp); return 0;}
🐦读文件
size_t fread (void *ptr, size_t size, size_t nmemb, FILE *stream);
int fscanf (FILE *stream, const char *format, …);
int sscanf (const char *str, const char *format, …);
🔔示例代码
#include <stdio.h>#define LOG "log.txt"int main(){ // 打开文件 FILE* fp = fopen(LOG,"r"); if(fp == NULL) { perror("fopen"); return 1; } // 对文件进行操作 const char* msg = "Hello World!"; char str[1024]; while(fscanf(fp,"%s ",str) != EOF) { fprintf(stdout,"%s ",str); } // 关闭文件 fclose(fp); return 0;}
上面我们只是简单回忆一下C语言的文件操作,若想更加详细的了解文件操作,请参考该文章:
🐧系统文件I/O
文件操作并不是只存在于C语言的,Java,python等编程语言中同样存在文件操作,只不过方法不同而已。其实我们可以尝试在系统层面统一看待文件操作。
操作系统提供了许多系统调用接口,而我们在语言层面看到的例如fopen
,fwrite
等还有其他语言相关函数都是对操作系统提供的文件相关的系统调用接口做了不同程度的封装而已。
接下来,我们就认识一个十分重要的系统调用接口——open
。
🐦open系统调用
open
是操作系统为语言层提供的系统调用接口,其作用是打开文件。可以根据参数来指定文件打开的方式、文件以何种权限被打开等等。在fopen
以及各类语言的文件操作中,底层必定使用了open
系统调用接口。
函数形式
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
函数参数
pathname
:指定打开的文件所在路径;flags
:指定以何种形式打开文件; O_CREAT:若要打开的文件不存在,则创建之;O_WRONLY:只对文件进行写入;O_TRUNC:打开文件的时候会将文件原本的内容清空;更多标志位请参考man手册; mode
:指定文件被创建时的权限; 返回值
成功返回大于等于0的值
;失败返回-1
; 🔔示例代码
#include <stdio.h>#include <errno.h>#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#define LOG "log.txt"int main(){umask(0); // 将系统默认的掩码置0 int fd = open(LOG, O_CREAT | O_WRONLY | O_TRUNC, 0666); if(fd == -1) { printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno)); } const char* msg = "aaabbb "; int cnt = 5; while(cnt--) { write(fd, msg, strlen(msg)); } return 0;}
🔔注意
打开文件时,我们指定文件的权限是666
,但是实际查看该文件的权限是,却并不是我们想要的结果,如图所示。
原因其实是,该log.txt
文件是预先创建好的。并不是程序运行时创建的,而open
的mode
参数,仅当要打开的文件不存在时,需要自动创建才会生效。
int fd = open(LOG, O_CREAT | O_WRONLY | O_TRUNC, 0666);
🐧文件描述符
上一小节中,我们知道在open
函数打开一个文件时,如果成功,会返回一个大于等于0的数字;失败时,则返回-1。
其实所谓的大于等于0的数字,就是文件描述符
。我们可以创建多个文件,并将这些文件描述符打印出来。
🔔示例代码
#include <stdio.h>#include <errno.h>#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>int main(){ umask(0); // 将系统默认的掩码置0 int fd1 = open("log1.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); int fd2 = open("log2.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); int fd3 = open("log3.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); int fd4 = open("log4.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); // 跳过检查的步骤,默认打开文件肯定会成功 printf("fd1: %d\nfd2: %d\nfd3: %d\nfd4: %d\n",fd1,fd2,fd3,fd4); return 0;}
可见,文件描述符就是从0开始的小整数,且文件描述符是随着打开文件的操作递增的。那么现在有两个问题:
为什么文件描述符看起来好像是从3开始的,有什么特殊含义吗?文件描述符有什么作用?我们首先来解决第一个疑问。
🐦fd 0、1、2去哪了
早在C语言阶段,我们就知道,一个程序运行时,有3个文件流是默认被打开的。它们就是:
标准输入流
(一般对应的物理设备是键盘);标准输出流
(一般对应的物理设备是显示器);标准错误流
(一般对应的物理设备是显示器); 而此刻我们也应该能猜到,0、1、2这三个文件描述符其实是被这三个文件所占据。这也印证了Linux下一切皆文件
,向显示器打印其实就是向文件中写入。
所以输入输出还可以采用如下方式:
🔔示例代码
#include <stdio.h>#include <errno.h>#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>int main(){ char buf[1024]; ssize_t s = read(0, buf, sizeof(buf)); // 0为标准输入流的文件描述符 if (s > 0) { buf[s] = 0; write(1, buf, strlen(buf));// 1为标准输出流的文件描述符 write(2, buf, strlen(buf));// 2为标准错误流的文件描述符 } return 0;}
🐦文件描述符的作用
我们知道文件其实是文件内容加属性,属性包含文件的大小、文件创建的时间、上一次修改的时间、文件的权限等。OS在管理文件时,需要在内存中创建相应的数据结构来描述文件,这就是file结构体
,表示一个已经打开的文件对象。所以OS在管理文件时,只要找到了该文件所对应的struct_file
对象,就能找到该文件的内容和属性。
运行起来的程序我们把它叫做进程,当一个进程执行open
调用,必须先让进程与要操作的文件关联起来。每个进程都有一个指针 *file
,该指针指向一张表files_struct
。这张表中最重要的部分就是一个指针数组。数组的每个元素都是一个指向打开文件的指针
。
所以,本质上,文件描述符就是该数组的下标
,所以,只要拿到了文件描述符,就能找到对应的文件!
🐧文件描述符的分配规则
上面我们通过测试,发现创建一个新文件,该文件的文件描述符是从3开始依次递增的。那么当我们把前面打开的文件关掉之后,再创建一个新的文件,会对该文件的文件描述符有影响吗?
🔔示例代码
#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#define LOG "log.txt"int main(){ umask(0); // 将系统默认的掩码置0 close(0); // 关闭文件描述符0所对应的文件,也就是标准输入 int fd = open(LOG, O_CREAT | O_WRONLY | O_TRUNC, 0666); // 省略检查的步骤... printf("%d\n",fd); return 0;}
如图所示,我们可以得出结论_
文件描述符的分配规则
:在files_struct
数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。 🐧重定向
之前我们可能见过这样的操作——
echo "hello linux" > log.txt
’>
‘意为重定向,这条指令的含义是,将本应该在屏幕上打印的内容,输出重定向到log.txt
中。
那么这样的重定向操作是如何完成的呢?
🐦dup2系统调用
int dup(int oldfd);
int dup2(int oldfd, int newfd);
dup2系统调用的作用是,将newfd
文件描述符所对应的文件进行关闭,然后make一个新的fd——oldfd
(注意这里有点奇怪,新创建的fd反而叫做oldfd)。在files_struct
数组中,将下标为oldfd
所在的内容复制到newfd
中。
有点看不懂?没关系,来看看这幅图——
int fd = open(LOG, O_CREAT | O_WRONLY | O_TRUNC, 0666); dup2(fd,1);
再来看一组测试代码~
🔔示例代码
#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <unistd.h>#define LOG "log.txt"int main(){ umask(0); // 将系统默认的掩码置0 int fd = open(LOG, O_CREAT | O_WRONLY | O_TRUNC, 0666); // 省略检查的步骤... dup2(fd,1); printf("hello world!\n"); // 本来应该向屏幕上打印,现在转为向LOG中打印 printf("hello world!\n"); printf("hello world!\n"); printf("%d\n",fd); return 0;}
经过测试,我们得出结论——
所谓重定向,其本质就是更改进程中对应的文件描述符表的指向内容!
本章的内容到这里就结束了!如果觉得对你有所帮助的话,欢迎三连~