目录
1.初识文件
2.文件操作
2.1.文件操作接口
2.2.文件描述符fd
3.缓冲区
3.1.简要介绍
3.2.重定向
3.2.1.输出重定向
3.2.2.追加重定向
3.2.3.输入重定向
3.2.4.调用接口实现重定向
3.2.5.文件重定向的作用
3.3.用户缓冲区与内核缓冲区
3.4.缓冲区和重定向
我们在C语言学习时,初步接触过几个C语言的文件接口,也就是我们的文件学习仅仅停留在调用这几个接口的水平上,接下来我们就从文件的本质开始学习,直到理解操作系统的文件管理和文件系统!那么我们开始吧
1.初识文件
在计算机操作系统中,文件是有创建者所定义的、具有文件名的一组相关元素的集合,并且在实际的操作系统中,文件也是跟进程一样,拥有自己的内容和属性,内容的本质上就是数据,属性的本质也是数据。那么就有我们对文件的一系列操作,实际上就是对文件的相关数据进行操作。
比如:我们在Windows修改文件内容时,不仅文件的内容发生改变,文件的属性,例如:修改时间、大小等属性也可能发生改变。
文件 = 内容 + 属性
对文件的最基本的访问就是打开文件,那什么是打开文件呢?
在操作系统中,打开文件的本质就是创建进程来访问文件。文件打开前就是一个存储在外存(磁盘)的数据集合,当我们访问时,需要将文件加载到内存中。没有被打开的文件依旧存留在外存里,这里我们不作考虑。
那么加载(打开)文件一定会涉及到访问磁盘硬件,也就是需要操作系统来实现将外存上的文件通过进程来搬运到内存中,再结合我们之前学习的进程管理模块,操作系统也会进行“文件管理”,况且实际场景下,一个进程可以打开(访问)多个文件,那么多个进程的情况下就需要我们将打开的文件进行管理,也就需要将进程和对应文件联系起来 。也就回到了“先描述再组织”!
2.文件操作
2.1.文件操作接口
C语言文件接口
打开文件
// 具体使用 FILE* fp = fopen("文件名", "打开方式");
- 当文件名不存在时,会在当前目录下,创建一个以该文件名命名的文件
- 打开方式:为 “r”时表示只读;"w"时表示先清空文件在写入;"a"表示追加写入
系统调用接口
打开文件
int fd = open("文件名", "文件标志位"); int fd = open("文件名", "文件标志位", "文件权限");
这里我们讲一下文件标记位,我们知道一个int的整型是32位(1字节8个比特位),标志位一般为2个字节,那么就可以传入多个标志位给一个int类型,在通过一定的运算找到我们想要的操作。这里用一个demo来展示一下,32位省略写为4位
#include<stdio.h> #define Print1 1 // 0001 #define Print2 (1<<1) // 0010 #define Print3 (1<<2) // 0100 #define Print4 (1<<3) // 1000 void show(int flags) { if(flags & Print1 != 0) printf("hello 1\n"); if(flags & Print2 != 0) printf("hello 2\n"); if(flags & Print3 != 0) printf("hello 3\n"); if(flags & Print4 != 0) printf("hello 4\n"); } int main() { show(Print1); printf("\n"); show(Print1 | Print2); printf("\n"); show(Print1 | Print2 | Print3); printf("\n"); show(Print4); }
具体在Linux中我们通过宏替换来实现这些标记位操作
- O_RDONLY 只读文件
- O_WRONLY 只写文件
- O_CREAT 文件不存在时,创建它
- O_TRUNC 截断这个文件,清空文件内容
- O_APPEND 在文件后追加
所以最终我们可以通过 或 操作来同时进行不同的选择。
例如:以只读的形式,打开log.txt文件,并且赋予0664权限,当文件不存在时自动创建。
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
写入文件
write("文件描述符", "传入的字符串", "字符串长度");
值得注意的是,这里wire只完成写入的工作,对于覆盖写入、追加写入、清空写入这三种情况不受wire的影响,需要通过open函数的标记位来实现!
当我们在研究系统调用接口时,我们发现C语言的接口也可以实现这些功能,并且C语言的接口调用更加得心应手。这是为什么呢?这里我们就要讲一下系统和语言的关系了!
我们知道操作系统是打开文件的那个角色,C语言并不能直接地访问文件,然而C语言也可以实现系统调用的接口,注定了语言级别这些上层的接口封装了底层的这些系统级别的接口!
2.2.文件描述符fd
当我们在辨析语言级别访问文件和系统级别访问文件时,我们发现open函数成功调用,最后用一个int fd 来接收,这个整型称为文件描述符。讲文件标识符之前我们需要知道,文件在操作系统中的表现。
我们在引子中提到:文件 = 内容 + 属性,并且进程运行的期间可能会涉及多个文件,需要被操作系统管理!也就是需要被描述成类似进程控制块(PCB)这样的数据结构。
因为一个进程能够实现多个文件,那么对应一个进程就需要一个容器来存放这些文件,我们之前在进程管理中并不陌生用地址来实现映射关系最后进行管理。在Linux中具体实现如图所示:
那么这样子就实现了初步的文件管理,但是究竟什么是文件描述符呢?我们看一下这个图,发现在这个存放新文件的指针数组对应的下标从3开始对应着新文件,也就是这个下标就是文件标识符!
更加清晰的解释:文件描述符就是从0开始的小整数,也就是进程管理文件的指针数组的下标。其中0,1,2分别对应着标准输入,标准输出,标准错误
当我们打开文件时,操作系统在内存中要创建相应的数据结构来 描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进 程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数 组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件 描述符,就可以找到对应的文件
接下来我们重点讲一下0,1,2
进程在创建时会默认打开3个文件,分别是标准输入、标准输出、标准错误。
我们在C语言中也有标准输入,标准输出,标准错误,我们知道在系统中这三个是文件,在C语言中类型也恰好为FILE* 也对应着文件!
FILE本质上是 typedef 后的struct FILE,也就是一个结构体对象。另外在C语言的FILE结构体中,内置着一个整型int _fileno也就是文件描述符
#include<stdio.h> int main() { printf("stdin->fd: %d\n", stdin->_fileno); printf("stdout->fd: %d\n", stdout->_fileno); printf("stderr->fd: %d\n", stderr->_fileno); FILE* newFile = fopen("log.txt", "w"); printf("newFile->fd: %d\n", newFile->_fileno); }
这里我们验证了0,1,2和新文件的从3开始,那么如果我们在创建新文件之前就关闭了stdin,stdout,stderr这三个文件,最终我们能看到什么结果呢?这里就不做演示了!
答:对于文件描述符的分配规则,在这个file_struct这个数组中找到没有被使用的最小的一个下标,作为新的文件描述符
另外,标准输入 对应着键盘这个硬件,标准输出 和 标准错误 对应着显示器。我们知道他们在操作系统中对应着文件,而文件往往对应着读写这两个功能,对于不同的硬件,读写功能的权限也不一定一样。也就是对于硬件的读写方法我们也需要进行对应,具体操作如图:
最终我们实现了将硬件抽象成文件!这也就是Linux一切皆文件的体现
3.缓冲区
3.1.简要介绍
对于一个描述文件的结构体,不仅存放着关于文件的所有内容和属性,还拥有着一块属于自己的缓冲区。当我们在进行对文件的读取时,需要将文件数据加载到内存。如果需要进行写入时也是需要加载数据到内存中。而这这些数据一般存放在文件定义的这一块文件缓冲区中!
那么用户在应用层进行数据读写的本质就是:在内存进行数据的读写后,再进行内存与外存之间的拷贝,也就是 将内核缓冲区的数据进行来回的拷贝!!!
3.2.重定向
I/O重定向功能可以改变输出内容发送的目的地,也可以改变输入内容的来源地。通常来说,输出内容显示在屏幕上,输入内容来自于键盘。但是使用I/O重定向功能可以改变这一惯例。
......相信大家并没有看懂,接下来我用几个小demo来体会一下重定向功能。
3.2.1.输出重定向
代码如下:我们的逻辑是关闭stdout文件,然后再进行打印,这时我们大概能猜出显示器没有打印结果,因为我们关闭了printf对应的显示器文件!
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> int main() { close(1); int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); printf("#############\n"); printf("fd: %d\n", fd); printf("stdout->fd: %d\n", stdout->_fileno); fflush(stdout); close(fd); }
如图是在Linux下的运行结果
我们发现当我们关闭1后,原本printf打印到的位置是显示器,而显示器打印出结果的本质是对显示器进行写入,因为我们这里关闭了1,并且新的文件的fd也随之设置为1,最终这些打印的内容就全部被写入进了log.txt文件中,这个就是输出重定向。
3.2.2.追加重定向
那么当我们将打开文件方式由截断,改为追加时,也就是稍微修改一下这个demo
这时我们通过不断的调用这个进程,我们会发现会不断地往log.txt文件中打印一样的内容,那么这个时候我们应该能猜到这个追加重定向可以用来打印日志了吧!!!
当我们把这几个demo中的fflush函数关闭时,就不存在这种情况了,而fflush函数是刷新当前缓冲区,那么我们也可以大胆猜测:重定向与缓冲区有关
3.2.3.输入重定向
我们将代码修改一下后,实现关闭键盘后对stdin进行只读
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> int main() { close(0); int fd = open("log.txt", O_CREAT | O_RDONLY, 0666); char buffer[10000]; fread(buffer, 1, sizeof(buffer), stdin); printf("%s\n", buffer); close(fd); }
最后我们发现,在Linux的命令行中打印出来的内容是log.txt文件的内容,并且我们没有输入的功能,这也符合了我们提前关闭键盘的现象。
值得讨论的是:当我们关闭stdin后对应的fd = 0转移到了log.txt中(这是底层的实现,操作系统不关心),然而操作系统只通过fd来访问文件,所以最后fread进入系统调用时,就读取到了log.txt并写入进buffer中!
3.2.4.调用接口实现重定向
我们发现在上面的demo中我们需要不断的关闭文件,来实现重定向下标文件的替换,而C语言库提供了一个函数方便我们使用
int fd = open("文件名", "方式");
dup2(fd, "需要替换的文件下标"); // 替换stdin 就为 dup2(fd,0)
重定向:上层fd不变,底层fd指向的内容在改变,本质就是修改特性文件的下标内容,也就是实现文件描述符的数组内容的拷贝。
那重定向有什么作用呢?
3.2.5.文件重定向的作用
文件重定向是一种操作系统提供的功能,它可以将一个程序的输入或输出从标准设备(通常是键盘和显示器)重定向到文件中,或者从文件中读取输入或输出。文件重定向的作用主要有以下几个方面:
输入重定向:通过将输入从键盘改为从文件中读取,可以方便地批量处理大量输入数据。例如,可以使用"<"符号将文件作为输入重定向到程序中,程序将从文件中读取数据而不是等待用户输入。
输出重定向:通过将输出从显示器改为写入到文件中,可以将程序的输出结果保存到文件中,方便后续的处理和查看。例如,可以使用">"符号将程序的输出结果重定向到文件中,而不是在显示器上显示。
错误重定向:除了标准输入和标准输出之外,还有一个标准错误输出。错误重定向可以将程序的错误信息输出到文件中,以便后续的排查和分析。例如,可以使用"2>"符号将程序的错误信息重定向到文件中。
通过文件重定向,我们可以更加灵活地处理输入输出,方便地进行批量处理、结果保存和错误排查等操作。
在实际开发场景中,我们借助文件重定向功能可以辅助我们查看程序进行的信息,比如打印日志、错误信息在不同的文件中
下面我们再用一个demo,这里的printf和perror的区别就是打印正常信息,错误信息的接口
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> int main() { fprintf(stdout, "print log\n"); fprintf(stderr, "print error\n"); printf("正常输出信息\n"); perror("错误输出信息\n"); }
接下来我们在Linux输入指令
./a.out 1> log.txt 2> log.txt.error
这样子我们就可以把对应的stdout和printf的正常输入信息重定向写入到log.txt中,将错误信息重定向写入到log.txt.error中。
重定向可以帮助我们将我们在程序运行时的信息从平时输出显示屏到输出到文件中,便于实际开发时定位查询!同理我们也可以通过文件中的数据来实现写入,而不是从键盘中写入,这样子效率也增加了!
3.3.用户缓冲区与内核缓冲区
在3.1.中的简要介绍中提及,每一个进程维护的文件结构体中都会有一个文件缓冲区来实现内存和外存之间的数据拷贝!这一块缓冲区一般是通过malloc实现的,也就是缓冲区本质上就是由用户或者系统定义并设置的一块内存!
如图为:缓冲区的大致形式
我们之前在学习操作系统时,见过这样一个场景 :
printf("hello linux"); sleep(3);
当程序进入sleep时,我们发现并没有打印出hello Linux这一句话,这时为什么呢?因为printf中并没有'\n' 而 换行符不仅可以实现换行,也是缓冲区行刷新的标志,也就是sleep时,数据存留在缓冲区中。
操作系统定义缓冲区的目的:为了效率,提高资源的利用率
我们平时要送东西给身边的朋友,一般是找时间亲自送过去。那么如果我们要送东西给远在他乡的朋友呢?如果亲自送过去会不会有一点太麻烦?回归到现实,我们一般是通过快递站点来送给朋友,这样是不是减少了麻烦,提高了你(使用者)的效率。
从缓冲区的层面上来看,缓冲区时进行数据在内外存进行拷贝的场所,一般来说我们进行拷贝有两种方式
- 我们在缓冲区完成了数据的增删查改后再进行内外存拷贝
- 不专门设置缓冲区,我们修改一部分,就进行内外存拷贝一部分,一收到就发送
在大部分情况下我们都选择第一种方式,这样子可以减少内外存拷贝的次数减少资源的调度。这也很合理,没有快递公司,送东西是有一件就送一件的,一般都是收取够了快递后再发送......
缓冲区的刷新方式
- 无缓冲(立即刷新)
- 行缓冲(行刷新)
- 全缓冲(缓冲区满了再刷新)
一般来说,进程退出时,一般会强制刷新缓冲区!也就是sleep后进程退出时,hello Linux也会打印出来。
所以综合上面的内容,我们就能够很好的理解:操作系统会自己创建一份内核缓冲区用来接收从用户缓冲区(语言级别缓冲区)传来的数据,并且内核缓冲区也是用来进行内外存拷贝的场所。
我们来看一下C语言库中的FILE结构体,来看看里面缓冲区结构
在/usr/include/stdio.h对FILE的声明
typedef struct _IO_FILE FILE;
在/usr/include/libio.h中对FILE的详细定义
struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ #define _IO_file_flags _flags //缓冲区相关 /* The following pointers correspond to the C++ streambuf protocol. */ /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; //封装的文件描述符 #if 0 int _blksize; #else int _flags2; #endif _IO_off_t _old_offset; /* This used to be _offset but it's too small. */ #define __HAVE_COLUMN /* temporary */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; /* char* _save_gptr; char* _save_egptr; */ _IO_lock_t *_lock; #ifdef _IO_USE_OLD_IO_FILE };
3.4.缓冲区和重定向
我们通过上面的讲述明白了,对于我们实际输入一段信息,借助C语言接口是直接写入进C语言级别缓冲区中,当我们再进行重定向时,这时我们的数据就保留在C语言缓冲区中(通过fflush函数刷新缓冲区),而写入操作系统时,操作系统只会以文件标识符fd为判断,这时我们通过修改fd,然后刷新缓冲区,将本来需要写入进stdin、stdout、stderr的内容,转而写入我们想要写入的磁盘文件中。
这里缓冲区扮演的角色就是,C语言写入操作系统前,先通过重定向“骗过”操作系统,也正是有了用户级缓冲区和系统级缓冲区,我们才能够实现重定向功能。