【Linux学习笔记】理解一切皆文件实现原理和文件缓冲区

【Linux学习笔记】理解一切皆文件实现原理和文件缓冲区

🔥个人主页大白的编程日记

🔥专栏Linux学习笔记


前言

哈喽,各位小伙伴大家好!上期我们讲了重定向 今天我们讲的是理解一切皆文件实现原理和文件缓冲区。话不多说,我们进入正题!向大厂冲锋!

4.理解"---切皆文件"

首先,在windows中是文件的东西,它们在linux中也是文件;其次一些在windows中不是文件的东西,比如进程、磁盘、显示器、键盘这样硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们获得信息;甚至管道,也是文件;将来我们要学习网络编程中的socket(套接字)这样的东西,使用的接口跟文件接口也是一致的。

这样做最明显的好处是,开发者仅需要使用一套API和开发工具,即可调取Linux系统中绝大部分的资源。举个简单的例子,Linux中几乎所有读(读文件,读系统状态,读PIPE)的操作都可以用read函数来进行;几乎所有更改(更改文件,更改系统参数,写PIPE)的操作都可以用write函数来进行。

之前我们讲过,当打开一个文件时,操作系统为了管理所打开的文件,都会为这个文件创建一个file线构体,该结构体定义在/usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fs.h下,以下展示了该结构部分我们关系的内容

cpp 复制代码
struct file {
    ...
    struct inode    *f_inode;    /* cached value */
    
    const struct file_operations    *f_op;
    
    ...
    atomic_long_t    f_count;    // 表示打开文件的引用计数,如果有多个文件指针指向它,就会增加f_count的值。
    unsigned int    f_flags;    // 表示打开文件的权限
    fmode_t         f_mode;     // 设置对文件的访问模式,例如:只读,只写等。所有的标志在头文件<fcntl.h>中定义
   loff_t          f_pos;       // 表示当前读写文件的位置
    ...
} __attribute__((aligned(4)));  /* lest something weird decides that 2 is OK */

值得关注的是struct file中的f_op指针指向了一个file_operations结构体,这个结构 体中的成员除了struct module*owner其余都是函数指针。该结构和struct file都在fs.h下。

cpp 复制代码
struct file_operations {
    struct module *owner;
    loff_t (*lseek)(struct file *, loff_t, int);
    ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
    int (*readdir)(struct file *, void *, filldir_t);
    unsigned int (*poll)(struct file *, struct poll_table_struct *);
    int (*ioctl)(struct inode *, struct file *, unsigned int, unsigned long);
    long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
    long (*compat_ioctl)(struct file *, unsigned int, unsigned long);
    int (*mmap)(struct file *, struct vm_area_struct *);
    // mmap 用来请求将设备内存映射到进程的地址空间。如果这个方法是 NULL,mmap 系统调用返回 -ENODEV。
    int (*open)(struct inode *, struct file *);
    // 打开一个文件
    int (*flush)(struct file *, fl_owner_t id);
    // flush 操作在进程关闭它的设备文件描述符的拷贝时调用;
    int (*release)(struct inode *, struct file *);
    // 在文件结构被释放时引用这个操作。如同 open,release 可以为 NULL。
    int (*fsync)(struct file *, struct dentry *, int datasync);
    // 用户调用来刷新任何挂着的数据。
    int (*aio_fsync)(struct kiocb *, int datasync);
    int (*fasync)(int, struct file *, int);
    int (*lock)(struct file *, int, struct file_lock *);
    // lock 方法用来实现文件加锁;加锁对常规文件是必不可少的特性,但是设备驱动几乎从不实现它。
    ssize_t (*readpage)(struct file *, struct page *, unsigned long, unsigned long);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock)(struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **);
};

file_operation就是把系统调用和驱动程序关联起来的关键数据结构,这个结构的每一个成员都对应着一个系统调用。读取file_operation中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。

介绍完相关代码,一张图总结:

上图中的外设,每个设备都可以有自己的read、write,但一定是对应着不同的操作方法!!但通过struct filefile_operation中的各种函数回调,让我们开发者只用file便可调取 Linux系统中绝大部分的资源!!这便是"linux下一切皆文件"的核心理解。

5.缓冲区

5-1什么是缓冲区

缓冲区是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。

5-2为什么要引入缓冲区机制

读写文件时,如果不会开辟对文件操作的缓冲区,直接通过系统调用对磁盘进行操作(读、写等),那么每次对文件进行一次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行一次系统调用,执行一次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗一定的CPU时间,频繁的磁盘访问对程序的执行效率造成很大的影响。
为了减少使用系统调用的次数,提高效率,我们就可以采用缓冲机制。比如我们从磁盘里取信息,可以在磁盘文件进行操作时,可以一次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不需要再使用系统调用了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。

又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的CPU可以处理别的事情。可以看出,缓冲区就是一块内存区,它用在输入输出设备和CPU之间,用来缓存数据。它使得低速的输入输出设备和高速的CPU能够协调工作,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作。

5-3缓冲类型

标准I/0提供了3种类型的缓冲区。

  • 全缓冲区:这种缓冲方式要求填满整个缓冲区后才进行I/O系统调用操作。对于磁盘文件的操作通常使用全缓冲的方式访问。
  • 行缓冲区:在行缓冲情况下,当在输入和输出中遇到换行符时,标准|/O库函数将会执行系统调用操作。当所操作的流涉及一个终端时(例如标准输入和标准输出),使用行缓冲方式。因为标准I/O库每行的缓冲区长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行I/0系统调用操作,默认行缓冲区的大小为1024。
  • 无缓冲区:无缓冲区是指标准I/O库不对字符进行缓存,直接调用系统调用。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。

除了上述列举的默认刷新方式,下列特殊情况也会引发缓冲区的刷新:

  1. 缓冲区满时;
  2. 执行flush语句;
    示例如下:
cpp 复制代码
#include <stdio.h>
 #include <string.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_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"文件中,但我们发现,程序运行结束后,文件中并没有被写入内容:

bash 复制代码
[hyb@VM-8-12-centos buffer]$ ./myfile
[hyb@VM-8-12-centos buffer]$ ls
log.txt  makefile  myfile  myfile.c
[hyb@VM-8-12-centos buffer]$ cat log.txt
[hyb@VM-8-12-centos buffer]$

这是由于我们将1号描述符重定向到磁盘文件后,缓冲区的刷新方式成为了全缓冲。而我们写入的内容并没有填满整个缓冲区,导致并不会将缓冲区的内容刷新到磁盘文件中。怎么办呢?可以使用fflush强制刷新下缓冲区。

cpp 复制代码
 #include <stdio.h>
 #include <string.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_WRONLY | O_CREAT | O_TRUNC, 0666);
	 if (fd < 0) {
	 perror("open");
	 return 0;
	 }
	 printf("hello world: %d\n", fd);
	 fflush(stdout);
	 close(fd);
	 return 0;
 }

还有一种解决方法,刚好可以验证一下stderr是不带缓冲区的,代码如下:

cpp 复制代码
 #include <stdio.h>
 #include <string.h>
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <fcntl.h>
 #include <unistd.h>
 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;
 }

这种方式便可以将2号文件描述符重定向至文件,由于stderr没有缓冲区,"helloworld"不用fflash就可以写入文件:

bash 复制代码
 [hyb@VM-8-12-centos buffer]$ ./myfile 
[hyb@VM-8-12-centos buffer]$ cat log.txt 
hello world: Success

自定义glic

现在我们可以自己封装出一个fopen等库函数

头文件

定义宏表示文件打开方式权限位

定义IO_FILE结构体 包含文件描述符 文件打开方式

文件缓冲区字符数组 缓冲区长度 刷新方式

声明库函数

cpp 复制代码
#pragma once

#include <stdio.h>

#define MAX 1024
#define NONE_FLUSH (1<<0)
#define LINE_FLUSH (1<<1)
#define FULL_FLUSH (1<<2)

typedef struct IO_FILE
{
    int fileno;
    int flag;
    char outbuffer[MAX];
    int bufferlen;
    int flush_method;
}MyFile;


MyFile *MyFopen(const char *path, const char *mode);
void MyFclose(MyFile *);
int MyFwrite(MyFile *, void *str, int len);
void MyFFlush(MyFile *);

BuyFile函数

申请IO_FILE结构体空间

初始化结构体内容 meset初始化缓冲区内容为0

返回结构体指针

cpp 复制代码
static MyFile* BuyFile(int fd, int flag)
{
	MyFile* ret = (MyFile*)malloc(sizeof(MyFile));
	ret->fileno = fd;
	ret->flag = flag;
	ret->bufferlen = 0;
	ret->flush_method = LINE_FLUSH;
	memset(ret->outbuffer, 0, sizeof(ret->outbuffer));
	return ret;
}

MyFopen函数

定义文件描述符fd和打开方式

根据打开方式参数mode分流 设置对应的打开方式

调用open系统调用打开path文件 BuyFile申请IO_FILE结构体

申请缓冲区 返回BuyFile的指针

cpp 复制代码
MyFile* MyFopen(const char* path, const char* mode)
{
	int fd = -1;
	int flag = 0;
	if (strcmp(mode, "w") == 0)
	{
		flag = O_CREAT | O_WRONLY | O_TRUNC;
		fd = open(path, flag, 0666);
	}
	else if (strcmp(mode, "a") == 0)
	{
		flag = O_CREAT | O_WRONLY | O_APPEND;
		fd = open(path, flag, 0666);
	}
	else if (strcmp(mode, "r") == 0)
	{
		flag = O_RDONLY;
		fd = open(path, flag);
	}
	else
	{

	}
	if (fd < 0)
	{
		return NULL;
	}
	return BuyFile(fd, flag);
}

MyFclose函数

判断文件描述符合法性 调用MyFFlush刷新文件缓冲区

调用close关闭文件 free释放IO_FILE结构体

cpp 复制代码
void MyFclose(MyFile* file)
{
	if (file->fileno 《》 0)
	{
		return;
	}
	MyFFlush(file);
	MyFclose(file);
	free(file);
}

MyFwrite函数

memcpy拷贝内容到缓冲区 更新缓冲区长度

判断如果刷新方式为行刷新并且最后缓冲区最后一个字符为\n

调用MyFFlush刷新缓冲区

cpp 复制代码
int MyFwrite(MyFile* file, void* str, int len)
{
	memcpy(file->outbuffer + file->bufferlen, str, len);
	file->bufferlen += len;
	printf("%d->\n", file->bufferlen);
	if ((file->flush_method & LINE_FLUSH) && file->outbuffer[file->bufferlen - 1] == '\n')
	{
		MyFFlush(file);
	}
	return 0;
}

MyFFlush函数

如果缓冲区为空不刷新

调用write刷新到文件缓冲区

文件缓冲区是否刷新到文件有操作系统决定

如果一定要刷新到文件中 可以使用fsync强制刷新 然后缓冲区长度为0;

cpp 复制代码
void MyFFlush(MyFile* file)
{
	if (file->bufferlen <= 0)
	{
		return;
	}
	int n = write(file->fileno, file->outbuffer, file->bufferlen);
	//强制刷新
	fsync(file->fileno);
	file->bufferlen = 0;
}
  • 测试一

所以这里我们不断向文件缓冲区写入不刷新 此时缓冲区的内容越来越多 当循环结束Flose后 语言缓冲区的内容统一刷新到内核文件缓冲区

此时文件的内容突然增多了 因为我们fsync强制刷新了 否则刷新到内核文件缓冲区不一定刷新到文件!


  • 测试二

这里我们写一次刷新一次 所以缓冲区的内容永远只有一条消息 但是文件的内容写入一条增多一条消息!


后言

这就是理解一切皆文件实现原理和文件缓冲区。大家自己好好消化!今天就分享到这! 感谢各位的耐心垂阅!咱们下期见!拜拜~

相关推荐
饕餮争锋8 分钟前
org.slf4j.MDC介绍-笔记
java·开发语言·笔记
weixin_4481199417 分钟前
Datawhale 5月llm-universe 第1次笔记
笔记
说码解字27 分钟前
ExoPlayer 如何实现音画同步
开发语言·学习·音视频
chennalC#c.h.JA Ptho28 分钟前
lubuntu 系统详解
linux·经验分享·笔记·系统架构·系统安全
冼紫菜29 分钟前
解决 CentOS 7 镜像源无法访问的问题
linux·运维·服务器·centos
几道之旅31 分钟前
分别在windows和linux上使用curl,有啥区别?
linux·运维·windows
季柳东32 分钟前
在虚拟机Ubuntu18.04中安装NS2教程及应用
linux·运维·ubuntu
冼紫菜35 分钟前
如何在 CentOS 7 虚拟机上配置静态 IP 地址并保持重启后 SSH 连接
linux·开发语言·centos·ssh
海尔辛1 小时前
学习黑客BitLocker与TPM详解
stm32·单片机·学习
oioihoii1 小时前
C++23 views::slide (P2442R1) 深入解析
linux·算法·c++23