【Linux】一切皆文件的理解 && 缓冲区 && 简易设计libc库

文章目录

理解linux下"一切皆文件"

首先,在windows中是文件的东西,它们在linux中也是文件;其次⼀些在windows中不是文件的东西,比如进程磁盘显示器键盘这样硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们获得信息;甚至管道,也是文件;将来我们要学习网络编程中的socket(套接字)这样的东西,使用的接⼝跟文件接⼝也是⼀致的。这样做最明显的好处是,开发者仅需要使用⼀套 API 和开发工具,即可调取 Linux 系统中绝大部分的资源 。举个简单的例子,Linux 中几乎所有读(读文件,读系统状态,读PIPE)的操作都可以⽤read函数来进行;几乎所有更改(更改⽂件,更改系统参数,写 PIPE)的操作都可以⽤ write函数来进行。

之前我们讲过,当打开⼀个文件时,操作系统为了管理所打开的文件,都会为这个文件创建⼀个struct file结构体。

以下是该结构体的部分内容:

c 复制代码
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其余都是函数指针

c 复制代码
struct file_operations {
    struct module *owner;               
    //指向拥有该模块的指针; 
    loff_t (*llseek) (struct file *, loff_t, int);   
    //llseek ⽅法⽤作改变⽂件中的当前读/写位置, 并且新位置作为(正的)返回值.  
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);  
    //⽤来从设备中获取数据. 在这个位置的⼀个空指针导致 read 系统调⽤以 -EINVAL("Invalid argument") 失败. ⼀个⾮负返回值代表了成功读取的字节数( 返回值是⼀个 "signed size" 类型, 常常是⽬标平台本地的整数类型). 
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    //发送数据给设备. 如果 NULL, -EINVAL 返回给调⽤ write 系统调⽤的程序. 如果⾮负, 
返回值代表成功写的字节数. 
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, 
loff_t);
    //初始化⼀个异步读 -- 可能在函数返回前不结束的读操作. 
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned 
long, loff_t);
    //初始化设备上的⼀个异步写. 
    int (*readdir) (struct file *, void *, filldir_t);
    //对于设备⽂件这个成员应当为 NULL; 它⽤来读取⽬录, 并且仅对**⽂件系统**有⽤. 
    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 (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, 
int);
    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设备驱动程序的⼯作。

用一张图总结一下:

虚拟文件系统(VFS)

struct file 结构体:进程打开文件时通过 struct file 访问,其内部包含属性、内核级缓冲区和函数指针。函数指针指向底层设备的读写方法,屏蔽了底层硬件差异。上层通过文件描述符访问文件,通过struct file 下 file_operation 中的各种函数回调,便可调取Linux系统中绝大部分的资源,让进程认为一切都是文件,即实现了 "一切皆文件"。

缓冲区:

什么是缓冲区

缓冲区是内存空间的⼀部分。也就是说,在内存空间预留了⼀定的存储空间,这些存储空间⽤来缓

冲输⼊或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输⼊设备还是输出设

备,分为输入缓冲区和输出缓冲区。

为什么要引入缓冲区机制

读写⽂件时,如果不会开辟对⽂件操作的缓冲区,直接通过系统调⽤对磁盘进⾏操作(读、写等),那么

每次对⽂件进⾏⼀次读写操作时,都需要使⽤读写系统调⽤来处理此操作,即需要执⾏⼀次系统调

⽤,执⾏⼀次系统调⽤将涉及到CPU状态的切换,即从⽤⼾空间切换到内核空间,实现进程上下⽂的

切换,这将损耗⼀定的CPU时间,频繁的磁盘访问对程序的执⾏效率造成很⼤的影响。

简单来说引入缓冲区机制为了减少磁盘的读写次数,大大提高计算机的运行速度。

缓冲区原理图:

缓冲类型

标准I/O提供了3种类型的缓冲区:

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

语言级缓冲区刷新策略:

1.强制刷新:fflush(FILE *stream);

2.刷新条件满足:全缓冲:用于文件

行缓冲(行刷新):用于显示器

3.进程退出:main 函数返回时进程并未结束,可能还有一些清理工作,如关闭文件描述符和 Stdin、Stdout 等,此时会进行刷新。

内核级缓冲区刷新策略:

由于操作系统刷新策略比用户层复杂的多,具体何时刷新、采用何种策略由操作系统自主决定,但也可通过控制层接口让其立即刷新。

但是我们可以认为只要将数据交给操作系统,就相当于交给了硬件。

数据流动的本质:

数据交给系统,交给硬件---本质全是拷贝!

计算机数据流动本质:一切皆拷贝!!!

C语言层面的缓冲区在哪里?

C语言中的fprintf, int fprintf(FILE *stream, const char format, ...);就是将格式化数据写到文件里。为什么呢?大家有没有发现fgets、fgetc...中都有FILE stream参数,其实FILE 是 C 语言提供的结构体,内部封装了文件描述符缓冲区。所以fprintf是先将格式化数据先写到stream指向结构体中的缓冲区中,需要时使用 write 系统调用,从 FILE * 里拿文件描述符(fd)把数据拷贝到操作系统内核级缓冲区。
所以C语言层面的缓冲区在哪里:FILE结构体内

  • FILE 结构体(包括它指向的 buf 缓冲区)都在你程序的用户态内存空间中(堆 / 数据段,取决于编译器实现),属于进程私有,操作系统内核无法直接访问,只有 C 标准库能操作。
  • 缓冲区通过 FILE 结构体管理,每个打开的文件(包括标准流)对应一个 FILE,其内部的 buf 指针指向缓冲区的具体内存地址;
  • C 标准库负责缓冲区的创建、数据写入 / 读取、刷新(到内核缓冲区),进程退出 /fclose()/ 强制刷新(fflush())时才会把缓冲区数据传递给内核级缓冲区。
    FILE是typedef出来的,在内核中可以看看FILE结构体:

研究一下这段代码:

c 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>  
#include <unistd.h>
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(msg1), 1, stdout);  // 库函数,用户级缓冲区
    write(1, msg2, strlen(msg2));          // 系统调用,无缓冲区,数据直接写入内核缓冲区。
    fork();  // 复制进程,包括未刷新的用户级缓冲区
    return 0;
}

运行一下:

重定向输出到文件呢?

输出重定向到文件后,C 标准库的缓冲区策略变为全缓冲(不是行刷新),因此hello printf和hello fwrite并没有立即写入文件,而是暂存在父进程的用户级缓冲区中。所以printf/fwrite 的用户级缓冲区未刷新就执行 fork,子进程完整复制父进程的内存空间状态,退出时父子进程各自刷新导致重复。而write是系统调用,直接把hello write写入内核缓冲区,无用户级缓冲,所以只会输出 1 次。

简单设计一下libc库:

mystdio.h:

c 复制代码
  1 #pragma once
  2 #define SIZE 1024
  3 
  4 #define NONE_FLUSH (1<<0)
  5 #define LINE_FLUSH (1<<1)
  6 #define FULL_FLUSH (1<<2)
  7 typedef struct MY_IO_FILE
  8 {
  9     int flag;//刷新方式
 10     int fileno;//文件描述符
 11     char outbuffer[SIZE];//缓冲区大小
 12     int size;//缓冲区有效内容长度
 13     int cap;//缓冲区容量
 14 
 15 }MYFILE;
 16 
 17 //设计接口
 18 MYFILE* Myfopen(const char* path,const char* mode);
 19 void Myfclose(MYFILE* stream);
 20 void Myfflush(MYFILE* stream);
 21 int Myfwrite(const void* ptr,int num,MYFILE* stream);        

mystdio.c:

c 复制代码
  1 #include"mystdio.h"
  2 #include<string.h>
  3 #include <sys/types.h>
  4 #include <sys/stat.h>
  5 #include <fcntl.h>
  6 #include<unistd.h>
  7 #include<stdlib.h>
  8 MYFILE* BuyFile(int fd)
  9 {
 10     MYFILE* mf=(MYFILE*)malloc(sizeof(MYFILE));
 11     if(mf==NULL)
 12     {
 13         //申请空间失败,返回并关闭文件描述符
 14         close(fd);
 15         return NULL;
 16     }
 17 
 18     mf->flag=LINE_FLUSH;//设置默认刷新方式
 19     mf->fileno=fd;
 20     mf->cap=SIZE;
 21     mf->size=0;
 22     //初始化内存
 23     memset(mf->outbuffer,0,mf->cap);
 24     return mf;
 25 }
 26 MYFILE* Myfopen(const char* path,const char* mode)
 27 {
 28     int fd=-1;
 29     if(strcmp(mode,"r")==0) 
 30     {
 31         fd=open(path,O_RDONLY);                                                                                                                                        
 32     }
 33     else if(strcmp(mode,"w")==0)
 34     {
 35         fd=open(path,O_CREAT | O_WRONLY | O_TRUNC,0666);
 36     }
 37     else if(strcmp(mode,"a")==0)
 38     {
 39         fd=open(path,O_CREAT | O_WRONLY | O_APPEND,0666);
 40     }
 41     if(fd<0)
 42     {
 43         return NULL;
 44     }
 45     return BuyFile(fd);
 46 }
 47 void Myfclose(MYFILE* stream)
 48 {
 49     if(stream->size>0)
 50     {
 51         Myfflush(stream);
 52     }
 53     close(stream->fileno);
 54     free(stream);
 55 }
 56 //强制刷新到内核缓冲区和外设
 57 void Myfflush(MYFILE* stream)
 58 {                                                                                                                                                                      
 59     if(stream->size>0)
 60     {
 61         //刷新到内核缓冲区
 62         write(stream->fileno,stream->outbuffer,stream->size);
 63         //立即刷新到外设
 64         fsync(stream->fileno);
 65         //刷新到外设后,缓冲区容量要清零
 66         stream->size=0;
 67     }
 68 }
 69 int Myfwrite(const void* ptr,int num,MYFILE* stream)
 70 {
 71     //向语言级缓冲区写入数据,本质是拷贝
 72     memcpy(stream->outbuffer+stream->size,ptr,num);
 73     stream->size+=num;
 74 
 75     //检查是否要刷新
 76     if((stream->flag & LINE_FLUSH) && stream->outbuffer[stream->size-1]=='\n')
 77     {
 78         Myfflush(stream);
 79     }
 80     return num;
 81 }                                   

usercode.c:

c 复制代码
  1 #include"mystdio.h"
  2 #include<stdio.h>
  3 #include<unistd.h>
  4 #include<string.h>
  5 int main()
  6 {
  7     MYFILE* fp=Myfopen("log.txt","a");
  8     if(fp==NULL)
  9     {
 10         perror("open fail");
 11         return 1;
 12     }
 13     //写入文件信息
 14     int cnt=10;
 15     char* buf="hello linux!!";
 16     while(cnt--)
 17     {
 18         Myfwrite(buf,strlen(buf),fp);                                                                                                                                  
 19         printf("%s\n",fp->outbuffer);
 20         sleep(1);
 21     }
 22 
 23     //关闭文件
 24     Myfclose(fp);
 25     return 0;
 26 }

当要写入的字符串buf,不带'\n'时,Myfwrite写入到语言级缓冲区的内容不会立即刷新到内核缓冲区,因为不满足行刷新的条件,最后进程退出才会将语言级缓冲区的数据写入到内核缓冲区中,然后再由内核刷新到外设。

相关推荐
AL3172 小时前
模拟实现NetDevOps全生命周期自动化网络运维
运维·docker·centos·ensp·netmiko
乾元2 小时前
实战案例:解析某次真实的“AI vs. AI”攻防演练
运维·人工智能·安全·web安全·机器学习·架构
懂营养的程序员2 小时前
DevOps 是如何诞生的?从“左右互搏”到“左右开弓”的故事
运维·devops
不念霉运2 小时前
中国DevOps平台选型指南:云原生时代的技术决策方法论
运维·云原生·devops
测试人社区—03922 小时前
UI测试在DevOps流水线中的卡点设计:质量保障的智能防线
运维·驱动开发·测试工具·ui·ar·vr·devops
Alaaaaaaan2 小时前
[DevOps]使用github-action工具部署docker容器(实现提交代码一键推送部署到服务器)
服务器·前端·docker·容器·github
快来吃饭�2 小时前
Ubuntu-20.04 gem5 构建并实现一个简单的配置脚本
linux·ubuntu·系统架构
daemon.qiang2 小时前
mplayer使能支持vaapi
linux·centos
70asunflower2 小时前
Docker exec 命令完全解析
linux·ubuntu·docker