继续学习文件相关内容:理解Linux下一切皆文件,缓冲区的概念,最后简易实现一下C语言文件流相关操作,加深对缓冲区及对系统接口的封装的理解。
一切皆文件
如何理解LInux下一切皆文件? 首先,在windows中是文件的东西,它们在linux中也是文件;其次一些在windows中不是文件的东西,比如进程、磁盘、显示器、键盘这样硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们获得信息;甚至管道,也是文件;将来我们要学习网络编程中的socket(套接字)这样的东西,使用的接口跟文件接口也是⼀致的。
这样做最明显的好处是,开发者仅需要使用⼀套 API 和开发工具,即可调取 Linux 系统中绝⼤部分的资源。举个简单的例子,Linux 中几乎所有读(读文件,读系统状态,读PIPE)的操作都可以用read
函数来进行;几乎所有更改(更改文件,更改系统参数,写 PIPE)的操作都可以用write
函数来进行。
操作系统管理软硬件资源,对于软资源的文件化理解都还可以接受,那么硬件资源是如何文件化的呢?怎么做到把硬件也一文件的视角看待呢?
在冯诺依曼体系中,外设必须把资源加载到内存中才能被执行。这些外设有很多,如显示器,键盘,网卡等;它们有各自不同的特性,但是他们都必须共有一个特性——支持IO操作。但是不同的设备的IO方式可能会有所不同,但对于操作系统而言,我只需要提供一个指向硬件对应方法的方式即可,具体的方法由硬件厂商自己提供,由此一来,OS就能对硬件进行读写操作。
再以内核的视角看:当打开⼀个文件时,操作系统为了管理所打开的件,都会为这个文件创建⼀个file
结构体,由这个file
就找到对应文件。
那么对于这个文件而言,是如何读取键盘输入的内容的呢? 在文件对象file
中,还有一个f_op
的指针指向file_operations
结构体,该结构体中包含了大量的函数指针,这些函数指针就是OS提供的指向硬件所提供的读写方法,所以,当该文件进行读写时,就能通过这些函数指针访问硬件资源了。
这些函数指针,VFS(虚拟文件系统)定义了统一的文件操作接口,使得应用程序可以通过统一的接口来与不同的设备进行交互。file_operation
就是把系统调用和驱动程序关联起来的关键数据结构,这个结构的每⼀个成员都对应着⼀个系统调用。读取 file_operation
中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。
上图中的外设,每个设备都可以有自己的read
、write
,但⼀定是对应着不同的操作方法!!但通过struct file 下 file_operation 中的各种函数回调,让我们开发者只用file便可调取 Linux 系统中绝大部分的资源!!这便是“Linux下⼀切皆文件”的核心理解
缓冲区
什么是缓冲区
缓冲区是内存空间的⼀部分。也就是说,在内存空间中预留了⼀定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区
为什么要引入缓冲区机制
读写文件时,如果不会开辟对文件操作的缓冲区,直接通过系统调用对磁盘进行操作(读、写等),那么每次对文件进行⼀次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行⼀次系统调用,执行以次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗⼀定的CPU时间,频繁的磁盘访问对程序的执行效率造成很大的影响。
为了减少使用系统调用的次数,提高效率,我们就可以采用缓冲机制。比如我们从磁盘里取信息,可以在磁盘文件进行操作时,可以⼀次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不需要再使用系统调用了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大文快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。
又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的CPU可以处理别的事情。可以看出,缓冲区就是⼀块内存区,它用在输入输出设备和CPU之间,用来缓存数据。它使得低速的输入输出设备和高速的CPU能够协调⼯作,避免低速的输⼊输出设备占用CPU,解放出CPU,使其能够高效率⼯作。
就如同你到菜鸟驿站取快递,菜鸟驿站承担的就是缓冲区的责任,不用客户频繁去取快递,节省了客户的时间。
缓冲类型
标准I/O提供了3种类型的缓冲区。
- 全缓冲区:这种缓冲方式要求填满整个缓冲区后才进行I/O系统调调操作。对于磁盘文件的操作通常使用全缓冲的方式访问。
- 行缓冲区:在行缓冲情况下,当在输入和输出中遇到换行符时,标准I/O库函数将会执行系统调用操作。当所操作的流涉及⼀个终端时(例如标准输⼊和标准输出),使用行缓冲方式。因为标准I/O库每行的缓冲区长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行I/O系统调用操作,默认行缓冲区的大小为1024。
- 无缓冲区:无缓冲区是指标准I/O库不对字符进行缓存,直接调用系统调用。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。
除了上述列举的默认刷新方式,下列特殊情况也会引发缓冲区的刷新:
- 缓冲区满时;
- 执行fflush语句强制刷新;
来看以下例子感受三种缓冲方式
int main()
{
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return 0;
}
printf("hello world: %d\n", fd);
close(fd);
return 0;
}
我们本来想使用重定向思维,让本应该打印在显示器上的内容写到“log.txt”文件中,但我们发现,程序运行结束后,文件中并没有被写入内容
这是由于我们将1号描述符重定向到磁盘文件后,缓冲区的刷新方式成为了全缓冲。而我们写入的内容并没有填满整个缓冲区,导致并不会将缓冲区的内容刷新到磁盘文件中。怎么办呢?可以使用fflush
强制刷新下缓冲区。
其中,标准出错流stderr
通常是不带缓冲区的,如果我们使用标准出错流stderr
演示上述代码,理论上不需要fflush也能将内容刷新到磁盘文件。
int main()
{
close(2);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return 0;
}
perror("hello world");
close(fd);
return 0;
}
perror
对应使用的就是stderr
流
可以看到确实不需要使用fflush
就能将内容输出到文件中,所以证实stderr流
是不带缓冲区的,采用的是无缓冲的刷新方式。
perror
用于打印错误信息,此时没有错误,所以会有个success
所以这个缓冲区在哪?
FILE
FILE是C语言封装的一个结构体,因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd
访问的。所以C库当中的FILE结构体内部,必定封装了fd。
如以下示例:
使用C语言的方式以只写的方式打开一个文件,再使用FILE
的_fileno
成员向系统调用接口write
来向对应文件写入。
//FILE的封装
int main()
{
FILE* fp=fopen("log.txt","w");
if(!fp)
{
perror("fopen failed\n");
return -1;
}
const char*msg="hello fopen\n";
write(fp->_fileno,msg,strlen(msg));
printf("fp->_fileno:%d\n",fp->_fileno);
fclose(fp);
}
不出意料,打开的文件的fd为3,而且FILE
的_fileno
指向的就是我们所打开的文件(fd=3),内容也最终通过系统调用write
写到了文件中。所以FILE
只是C语言文件操作封装的一个结构体,其内部必定包含了系统调用所打开文件按的fd
,也就是FILE
中的_fileno
成员
在C语言的角度,进行IO操作都是通过三个流stdin
,stdout
,stderr
来完成的。所以,这个缓冲区实际上就在封装的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
};
通过以下demo来理解用户层缓冲区;使用三种方式向屏幕输出,其中printf
,fwrite
为库函数,write
为系统调用。
int main()
{
const char *msg0 = "hello printf\n";
const char *msg1 = "hello fwrite\n";
const char *msg2 = "hello write\n";
//库函数:输出到屏幕
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
//系统调用:输出到屏幕
write(1, msg2, strlen(msg2));
fork();
return 0;
}
结果也符合我们的认知。
如果将该内容重定向输入到文件中呢?./file >log.txt
此时会发现,我们发现printf
和fwrite
(库函数)都输出了2次,而write
只输出了⼀次(系统调用)且输出的顺序有所不同,write
本应该是最后一个写入的,但是这里却成了第一个。
为什么同一份代码,当重定向之后却是不同的结果?这时候就需要结合fork
和缓冲区刷新方式来看待这个问题了;
⼀般C库函数写如文件时是全缓冲的,而写入显示器是行缓冲。printf fwrite
库函数会自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。而我们放在缓冲区中的数据,就不会被立即刷新,直到缓冲区写满或者进程退出时会统⼀刷新写入文件当中。而fork的时候,父子进程会共享同一份代码和数据,当父子进程缓冲区准备刷新的时候,会发生写时拷贝,至此父子进程就会有独立拥有一份同样的数据。所以当父子进程结束后会各自将自己的缓冲区内容刷新到文件中,所以使用库函数的printf
fwrite
打印的语句会有两份;而write
作为系统调用没有用户层缓冲区,直接会把内容写道内核中输出到文件,这也是为什么打印的write语句最先看到并且只有一份的原因
- write有自己对应的内核级缓冲区,此时我们认为内容刷新到内核中就可以刷新到文件即可。
简易模拟C语言文件流
使用
test.c
#include"mystdio.h"
int main()
{
MY_FILE*pf=MY_fopen("log.txt","w");
if(!pf)
{
perror("MY_fopen failed\n");
return -1;
}
int cnt=5;
const char*str="hello world\n";
while(cnt--)
{
MY_fwrite(str,strlen(str),1,pf);
}
MY_fclose(pf);
return 0;
}
// int main()
// {
// MY_FILE*pf=MY_fopen("log.txt","r");
// if(!pf)
// {
// perror("MY_fopen failed\n");
// return -1;
// }
// char buffer[64];
// size_t sz=MY_fread(buffer,sizeof(buffer),1,pf);
// printf("%s",buffer);
// MY_fclose(pf);
// return 0;
// }
头文件
stdio.h
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#define SIZE 1024
#define FLUSH_NOW 0//立即刷新
#define FLUSH_LINE 1//行刷新
#define FLUSH_ALL 2//满了再刷
struct IO_FILE//简易设计的文件类;模仿FILE
{
int file_no;//文件描述符fd
int flag;//刷新方式
char buffer[SIZE];//模拟的缓冲区
int size;//下标
int capacity;//缓冲区大小
};
typedef struct IO_FILE MY_FILE;//模拟FILE
MY_FILE* MY_fopen(const char *pathname, const char *mode);//打开文件
int MY_fclose(MY_FILE *stream);//关闭文件
size_t MY_fread(void *ptr, size_t size, size_t nmemb, MY_FILE *stream);//读取文件
int MY_fflush(MY_FILE *stream);//将缓冲区内容刷新到内核缓冲区中
size_t MY_fwrite(const void *ptr, size_t size, size_t nmemb,MY_FILE *stream);//写入文件
模拟实现
stdio.c
#include"mystdio.h"
MY_FILE* MY_fopen(const char *pathname, const char *mode)
{
assert(pathname && mode);
//打开方式
int fd=-1;
if(strcmp(mode,"r")==0)
{
fd=open(pathname,O_RDONLY);
}
else if(strcmp(mode,"w")==0)
{
fd=open(pathname,O_WRONLY|O_CREAT|O_TRUNC,0666);
}
else if(strcmp(mode,"a")==0)
{
fd=open(pathname,O_WRONLY|O_CREAT|O_APPEND,0666);
}
if(fd<0)
{
perror("open failed\n");
return NULL;
}
MY_FILE*pf=(MY_FILE*)malloc(sizeof(MY_FILE));
//初始化FILE
pf->file_no=fd;
pf->capacity=SIZE;
pf->size=0;
pf->flag=FLUSH_LINE;//默认行刷新
memset(pf->buffer,0,SIZE);
return pf;
}
int MY_fflush(MY_FILE *stream)
{
assert(stream);
if(stream->size>0)
{
write(stream->file_no,stream->buffer,stream->size);//'\0'注意
stream->size=0;
}
}
size_t MY_fwrite(const void *ptr, size_t size, size_t nmemb,MY_FILE *stream)
{
// 写入先判断缓冲区空间是否足够
if ((stream->size+size+1) >= SIZE) MY_fflush(stream);
strcpy(stream->buffer+stream->size,ptr);//连"\0"也拷
stream->size+=size;
//检查是否刷新
if(stream->flag==FLUSH_LINE&&stream->size>0&&stream->buffer[stream->size-1]=='\n')
{
MY_fflush(stream);
}
return size;
}
size_t MY_fread(void *ptr, size_t size, size_t nmemb, MY_FILE *stream)
{
ssize_t sz=1;
size=0;
while(sz)
{
sz=read(stream->file_no,stream->buffer,SIZE);
size+=sz;
}
stream->buffer[size] = '\0';
strcpy(ptr, stream->buffer);
return size;
}
int MY_fclose(MY_FILE *stream)
{
//关闭前需要把缓冲数据刷新到内核缓冲区中。
assert(stream);
MY_fflush(stream);
int rfd=close(stream->file_no);
free(stream);//释放
if(rfd!=0)//失败
{
return -1;
}
return rfd;
}
- 简易实现,禁不起测试
通过以上简易实现C语言文件流操作,帮助我们更好理解函数调用与系统调用的关系;认识缓冲区:用户层及内核层。