0
点赞
收藏
分享

微信扫一扫

linux之基础IO(万字长文详解)

基础IO

重谈文件

首先我们要知道

  1. 空文件也是占据磁盘空间的!
  2. 文件 = 内容 + 属性!
  3. 文件操作 = 对内容的操作+ 对属性的操作 or是对内容和属性的操作
  4. 找到一个文件 必须使用文件路径 +文件名!——因为这样具有唯一性!
  5. 如果没有指明对应的文件路径!默认是在当前路径进行文件访问——当前路径就是进程当前的工作路径!
  6. 当我们把fopen,fclose,fread等接口写完后,形成二进制可执行程序,但是没有运行,文件对应的操作有没有被执行?——没有!对文件的操作本质是==进程对文件的操作!==
  7. 一个文件要被访问首先要先被打开!——那么是要被谁打开的呢?——==是被用户进程+OS打开的!用户进程去调用系统接口!操作系统帮用户进行实际上的打开!==但是不是磁盘上的所有文件会被打开!——所以文件又可以分为被打开的文件和没有被打开的文件!

==所以文件操作的本质!:是进程和被打开的文件的关系!==

没有被打开的文件是由文件系统进行管理的!

重谈文件操作

我们学习到来的各种语言都有属于自己的文件接口!——C/C++/java.....

但是每种语言的接口都是不一样的!

open

image-20230106152429799

image-20230106152932544

这就是linux下面的系统调用接口!用于专门打开或者创建一个文件!

返回值是一个==新的文件描述符==或者出错就返回-1

int open(const char* pathname,int flags,mode_t mode);
int open(const char* pathname,int flags);

pathname就是设置文件路径!

mode就是权限

close

image-20230106161315578

image-20230106161353406

用来关闭一个文件描述符!

关闭成功返回0 失败返回 -1!

open与close的使用

#include <stdio.h>    
#include <unistd.h>    
#include<sys/stat.h>    
#include <sys/types.h>    
#include<fcntl.h>    
#define FILE_NAME "log.txt"    
int main()    
{    
       int fd = open(FILE_NAME,O_WRONLY);                                                                               
       if(fd < 0)    
       {    
           perror("open:");    
           return 1;    
       }                   
       close(fd); 
       return 0;
}

image-20230106161918936

怎么回事呢?为什么文件不存在呢?

umask

image-20230106163613059

image-20230106163739817

我们一般都是使用的是系统给我们自带的umask值!

因为我们的程序本质就是父shell的子进程!所以可以继承系统的umask!

但是我们也可以在编程的使用自己去设置umask!

这个系统调用是一直会成功地!所以返回值是上一次的umask的值!

#include <stdio.h>    
#include <unistd.h>    
#include<sys/stat.h>    
#include <sys/types.h>    
#include<fcntl.h>    
#define FILE_NAME "log.txt"    
int main()    
{   
       umask(0);
       int fd = open(FILE_NAME,O_WRONLY);                                                                               
       if(fd < 0)    
       {    
           perror("open:");    
           return 1;    
       }                   
       close(fd); 
       return 0;
}

image-20230106164005984

==这样子我们就得到了一个666权限的文件!==

这个umask改变的是子进程的umask但是不影响原来的umask的值!

write

image-20230106164147179

image-20230106164651877

write是一个系统接口!

作用是将buf里面的数据按count个写入到fd指向的文件中!

read

image-20230107104241569

image-20230107105215020

read是一个系统调用接口!

这个函数是用来读取一个文件描述符的!

关于read的返回值正常情况下

lseek

image-20230107110930610

image-20230107111739837

lseek的用法比较复杂

==它支持多种写入指令==——whence参数就是要输入的指令!

SEEK_SET 将偏移量的位置设置到offset!

SEEK_CUR 将偏移量的位置设置到 现在的位置加上offset的值!

SEEK_END 将偏移量的位置设置到 文件大小加上offset的值!

同时后面还增加了的SEEK_DATA和SEEK_HOLE这两个指令!读者有兴趣的可以自行去查阅

文件描述符

==我们上面说过文件操作的本质就是进程和被打开文件的关系!==

==而且毋庸置疑进程是可以打开多个文件的!那么系统中一定存在大量**被打开文件!**那么被打开的文件是一定要被操作系统管理起来的!==

那么如何管理这些被打开的文件呢?——操作系统就要先描述再组织!

所以操作系统必须就要为文件创建对应的内核数据结构用来标识文件!

==然后操作系统就可以通过特定的数据结构来对这个结构体进行管理!从而管理全部的被打开文件!==

==那么被打开文件又如何和进程联系起来的呢?==——答案就是文件描述符

我们上面可以看到上面使用的write/open/read/close的时候都会提到一个叫文件描述符的东西!我们从open的返回值可以看出来这是一个int类型的整数。

#include <stdio.h>                                                      
#include <unistd.h>    
#include <string.h>    
#include<sys/stat.h>    
#include <sys/types.h>    
#include<fcntl.h>     
#define FILE_NAME(number) "log.txt"#number     

int main()    
{    
    umask(0);    
    int fd0 = open(FILE_NAME(1),O_WRONLY | O_CREAT| O_APPEND,0666);    
    int fd1 = open(FILE_NAME(2),O_WRONLY | O_CREAT| O_APPEND,0666);    
    int fd2 = open(FILE_NAME(3),O_WRONLY | O_CREAT| O_APPEND,0666);    
    int fd3 = open(FILE_NAME(4),O_WRONLY | O_CREAT| O_APPEND,0666);    
    int fd4 = open(FILE_NAME(5),O_WRONLY | O_CREAT| O_APPEND,0666);    

    printf("fd0:%d\n",fd0);    
    printf("fd1:%d\n",fd1);    
    printf("fd2:%d\n",fd2);    
    printf("fd3:%d\n",fd3);    
    printf("fd4:%d\n",fd4);    

    close(fd0);    
    close(fd1);    
    close(fd2);    
    close(fd3);    
    close(fd4);
    return 0;
}

image-20230107115118956

==为什么是从3开始呢?——0,1, 2是什么呢?==

==那为什么是 0 1 2 3 4 .......==为什么是一串连续的数字呢?——文件描述符的本质是什么呢?

image-20230108222119593

==这样子每次打开新的文件的时候,就会从这个数组里面去找没有使用的文件描述符,因为 0 1 2都被使用了,那么就只能用3了!那么就首先加载进内存,然后把对象构建好,然后把这个新的文件对象的地址填入这个数组里面!然后将这个文件描述符通过系统返回!我们就得到了一个整形数字!==

==操作系统就是通过进程的这个文件描述符表来找到文件!然后对文件进行操作了!==

==所以文件描述符的本质就是数组的下标!这个数组就是文件描述符表!==

文件描述符的分配规则

#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
       close(0);//我们可以先试试看把0关闭
       close(2);
       umask(0);
       int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
       if(fd <0)
       {
           perror("opne");    
           return 1;    
       }
       printf("open:fd %d\n",fd);    
       close(fd);    
       return 0;                                                     
} 

image-20230109171309366

==fd的分配规则——就是扫描文件描述符表!从小到大!按照顺循序去寻找最小的且乜有被占用的fd!==

==我们把0 关闭,那么最小的没有被使用的fd就是 0了所以自然就被分配到0了!==

关于stdout 上面我们都是把stdin和stderr关闭如果我们关闭stdout会发生什么?

close(1);//

image-20230109172729873

答案是**什么都不会打印!**为什么没有显示出来呢?

首先此时的标准输出流被关闭!1 的文件描述符空间!我们又同时打开了新的文件是的 1文件描述符分配给了 这个新打开的文件!==而printf函数本质就是向stdout里面进行打印的!printf是怎么和stdout这个文件进行联系起来的呢?答案是1 文件描述符!因为1文件描述符默认就是stdout!==但是此时1虽然没有变,这个文件描述符里面的内容改变了!

==所以其实我们是将printf要给stdin的内容实际写入到了我们新打开的文件中去!==

image-20230109174112085

没有呀?这是怎么回事呢?——原因是因为向普通文件和向显示器的刷新策略是不一样的!

但是实际数据一直留在缓冲区里面!所以我们只要刷新一下就可以了!

printf("open:fd %d\n",fd);    
fprintf(stdout,"open:fd %d\n",fd);
fflush(stdout);//在关闭之前刷新缓冲区
close(fd);    

image-20230109174822239

此时数据就被成功写入到了log.txt里面!而且我们也可以看出来stdout底层本质就是 1 号文件描述符!

==这种特性我们称为重定向!==——虽然改变文件描述符里面的地址!但是不改变文件描述符就是重定向!——本质就是上层fd不变!在内核中修改fd中对应的struct file* 的地址!

重定向又分为 输出重定向(覆盖写),输出重定向(追加写),输入重定向!

dup系列接口

image-20230109233218184

image-20230109233313418

为了支持我们更好的重定向!linux操作系统为我们提供了,dup系列的系统调用来帮组我们!——如果我们不使用dup系列的接口!我们重定向很麻烦!要先关闭特定的接口!然后再打开!

==我们最经常使用的就是dup2接口!==所以我们下面也就是重点结束dup2接口!

==dup2接口作用就是进行文件描述符内容互相进行拷贝!——不是拷贝数组的下标!是拷贝文件描述符表里面存储的文件结构体地址!==

image-20230109234054730

image-20230109233551037

int dup2(int oldfd,int newfd);

==那么我们该如何使用呢?==

int main()
{
       umask(0);
       int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
       if(fd <0)
       {
           perror("opne");    
           return 1;    
       }
       dup2(fd,1);
       printf("open:fd %d\n",fd);
       fprintf(stdout,"open:fd %d\n",fd);
       fflush(stdout);
       close(fd);    
       return 0;                                                     
} 

image-20230109234758483

==这个函数的作用是将oldfd里面的内容拷贝到newfd里面去!那么我们要让1号文件标识符指向我们打开的文件!意思就是说要改变1号文件标识符里面的内容!让其指向我们打开的文件!==

==所以oldfd 就是 我们的fd ,newfd就是 1号文件标识符!==

image-20230109235931215 ==这样我们就实现了输出重定向(覆盖写)==

int main()    
{    
       umask(0);    
       int fd = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);    
       if(fd <0)    
       {    
           perror("opne");    
           return 1;    
       }    
       dup2(fd,1);    
       printf("open:fd %d\n",fd);    
       fprintf(stdout,"open:fd %d\n",fd);    
       const char* buff = "helle world\n";                      
       write(1,buff,strlen(buff));//我们甚至可以使用write 
       fflush(stdout);
       close(fd);    
       return 0;    
}  

只要把文件的打开方式改成追加写,不就成功完成了==追加写重定向!==

image-20230110001425456

#include <stdio.h>    
#include <sys/stat.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <fcntl.h>    
#include <string.h>    
int main()    
{    
       umask(0);    
       int fd = open("log.txt",O_RDONLY);    
       if(fd <0)    
       {    
           perror("opne");    
           return 1;    
       }    
       dup2(fd,0);//输入重定向                                            
       char line[64];                       
       while(1)                             
       {                                    
           printf("> ");                    
           if(fgets(line,sizeof(line),stdin)== NULL)    
           {                                
               break;                       
           }//正常情况下应该是从键盘读取的!    
           //如果我们想要它从文件里面读取?    
           printf("%s",line);               
       }
       return 0;
}

image-20230110002750607

==这样就完成了输入重定向!-==

dup接口的使用要请看自制命令行解释器!

进一步的认识文件

在linux下面一切皆文件——我们该理解这句话呢?

==现在我们可以知道!电脑外设(硬件)任何的数据处理都要先把数据加载到内存里面!然后再将内存的数据刷新到内核当中——这就叫做IO==

每个不同的硬件,都有不同的读取方法!内核里面对于每个不同的硬件也都有对应的结构体!——结构体里面包含了该设备的属性信息!

==这些硬件也有各自对应的读取方法!一般都是在驱动层里面!!==

==那么对于有着不同的读取方法的硬件linux要该怎么对其进行统一的管理!体现一切皆文件的思想呢?==

linux将统一的属性抽象出来,然后在这个内核结构体里面有着大量的读写文件函数的指针!需要那种读写方式就指向需要调用的函数——例如我们打开键盘那么就让其指向键盘的读方法和写方法,用这种方式来对不同的硬件进行统一!

image-20230111162606474

==通过这中虚拟的文件系统,就可以摒弃底层文件的差别!统一使用文件接口的方式来操作!==

image-20230111170202939

==文件打开本质就是引用计数的增加,文件关闭本质就是引用计数的减少!我们其实上层是没有资格去觉得文件的打开和关闭的都是由操作系统来决定文件是否打开或者关闭!而操作系统判断的标准就是引用计数==——所以我们打开文件不一定是真的打开!关闭文件也不一定是真的关闭!

==我们现在能够直观的看到内核里面的的IO操作==

缓冲区

在正式认识缓冲区之前我们先看下面的代码

#include <sys/stat.h>    
#include <sys/types.h>    
#include <fcntl.h>    
#include <string.h>    
#include <unistd.h>
int main()    
{    
    //C    
    printf("hello printf\n");    
    fprintf(stdout,"helle fprintf\n");    
    fputs("hellp fputs\n",stdout);    
    //system call     
    const char* msg = "hello write\n";    
    write(1,msg,strlen(msg));    
    fork();
    return 0;                
}    

image-20230111172145434

==这是怎么回事呢?==——为什么重定向后反倒多出来了几条信息

而且我们可以观察到C接口的函数是被重复打印了!系统接口只打印了一次

当我们把fork注释掉

image-20230111172510446

==又正常了!那么中间是什么导致了上面现象的发生?==

按理来说fork是最后执行的!fork后面已经没有代码了应该不会做任何事情,但是为什么会多打印出来了?

缓冲区在哪呢?

image-20230112102753132

==上面的这个例子我们可以看出来——这个现象的发生一定和缓冲区有关!==——我们首先可以知道缓冲区一定不会再内核里面!为什么?因为如果缓冲区在内核里面那么write这个函数也应该会打印两次!——write是属于系统接口!不是语言级别的接口

==我们刚刚谈论的所有缓冲区的概念都是指用户级语言层面给我们提供的缓冲区!==

我们在使用fprintf/fputs/fwrite 都要用到stdin /stdout/stderr ——这些都是FILE* 类型的!——FILE是一个结构体!——里面含有fd(文件标识符)不仅如此==缓冲区其实就包含在这里结构体里面!==

所以当我们使用c语言接口强制刷新的时候 要传入一个文件指针 ffush(文件指针),或者关闭文件的时候要传入一个文件指针 close(文件指针)——这是为什么呢?因为里面存在一个缓冲区!

我们就可以正式的看一下FILE这个结构体了!

typedef struct _IO_FILE FILE;// 在/usr/include/stdio.h
//在/usr/include/libio.h
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
};

==所以我们在C语言上进行的文件操作其实最后都会被写入FILE结构体的缓冲区里面!然后因为FIEL里面封装了fd 所以C语言会自动帮我们在合适的时候将数据刷新到外设里面!==

那我们就可以解释上面为什么会出现打印不同的问题了!

#include <sys/stat.h>    
#include <sys/types.h>    
#include <fcntl.h>    
#include <string.h>    
#include <unistd.h>
int main()    
{    
 	//C    
 	printf("hello printf\n");    
 	fprintf(stdout,"helle fprintf\n");    
 	fputs("hellp fputs\n",stdout);    
 	//system call     
 	const char* msg = "hello write\n";    
     write(1,msg,strlen(msg));    
 	fork();
	return 0;                
}    

在代码结束前创建子进程,如果我们没有进行 > ,看到了四条消息

==stdout(显示器)默认使用的是行刷新!因为每条打印后面都带 \n 所以在进程fork()之前,三条c函数已经将数据进行打印输出到显示器(外设),在FIEL内部即进程的内部已经不存在对应的数据了!==

如果我们进行了 > ,写入文件不在是显示器,而是普通文件!

==采用的刷新策略就是全缓冲!==所以之前的3条c语言的显示函数,==虽然带了\n但是不足以将stdout的缓冲区写满!那么此时数据就没有被刷新!==

==所以后面执行fork的时候,stdout属于父进程,创建子进程的时候,紧接着就是进程退出!父子进程谁先退出!谁就先进行缓冲区刷新(进程出现修改)!==

==此时发生了修改!所以发生了写实拷贝!先退出的进程将数据刷新写入到文件里面,随后的退出的进程因为发生了写实拷贝所以又拷贝了一份数据!然后退出的时候又将数据再写入了一次!==

==write为什么没有呢?因为上面的过程都和write无关!write没有使用FILE,而是使用fd,所以没有C提供的缓冲区区!==

封装缓冲区

我们可以自己手动封装一下缓冲区做一个简易的demo用来更好的理解什么叫缓冲区和缓冲区的刷新策略!

//mystdio.h
#pragma once
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#define  SIZE 1024
#define  SYNC_NOW 1
#define  SYNC_LINE 2
#define  SYNC_FULL 4

typedef struct _FILE
{
    int flags;//刷新方式
    int fileno;
    int cap;//buffer的总容量!
    int size;//buff的当前使用量!
    char buffer[SIZE];
}FILE_;

FILE_* fopen_(const char* pathname,const char* mode);
void fclose_(FILE_* fp);
void fwrite_(const void* ptr,int num,FILE_*fp);
void fflush_(FILE_* fp);
#include "mystdio.h"
// 打开文件!
FILE_* fopen_(const char* pathname,const char* mode)
{    int flags= 0;
    int defaultMode = 0666;

    // 比较打开方式!
    if(strcmp(mode,"r")==0)
    {
        flags |= O_RDONLY;
    }
    else if(strcmp(mode,"w")==0)
    {
        flags |= (O_WRONLY|O_CREAT|O_TRUNC);
    }
    else if(strcmp(mode,"a")==0)
    {
        flags |= (O_WRONLY|O_APPEND |O_CREAT);
    }
    else 
    {
        //todo
    }
    int fd = 0;
    // 正式打开文件!
    if(flags & O_RDONLY) fd = open(pathname,flags);
    else fd = open(pathname,flags,defaultMode);
    if(fd < 0)
    {
        const char* err = strerror(errno);
        write(2,err,strlen(err));
        return NULL;
    }
    // 创建结构体!
    FILE_* fp = (FILE_*)malloc(sizeof(FILE_));
    assert(fp);
    // 初始化结构体!
    fp->flags = SYNC_LINE;//默认设置为行刷新! 
    fp->fileno = fd;
    fp -> cap =SIZE;
    fp->size = 0;
    //初始化缓冲区!
    memset(fp->buffer,0,SIZE);
    return fp;//这就是为什么打开一个就会返回一个fp指针!

}
//刷新
void fflush_(FILE_* fp)
{
       //要fflush首先就是要把数据写入操作系统里面!
       //因为我们的数据是在缓冲区里面的!
       if(fp->size >0) write(fp->fileno,fp->buffer,fp->size);
       fsync(fp->fileno);//强制要求操作系统进行外设刷新!
       //TODO 
}
// 关闭文件!
void fclose_(FILE_* fp)
{
       //关闭首先就要刷新缓冲区!
       fflush_(fp);
       close(fp->fileno);
} 
//对文件进行写
void fwrite_(const void* ptr,int num,FILE_*fp)
{
       //写入其实不是讲数据写入操作系统里面!
       //这是刷新干的事情!
       //写入其实是将数据写入缓冲区里面!
       memcpy(fp->buffer + fp->size, ptr, num);
       fp->size +=num;
       // 之所以加size是因为曾经还有放在缓冲区里面的但是没有被刷新出去的!
       //这里我们暂时不考虑缓冲区溢出的问题!

       //写入完毕要判断是否刷新!
       if(fp->flags & SYNC_NOW)
       {
           fflush_(fp);
           fp->size =0;//清空缓冲区!
       }
       else if(fp->flags & SYNC_LINE)
       {
           //如果有\n就把\n之前的所有数据都刷新!
           if(fp->buffer[fp->size-1] == '\n') // 这种方式比较粗暴!如果遇到aaaa\naaa就不好使了!遇到这种其实可以使用循环!
           {
               fflush_(fp);
               fp->size = 0;
           }

       }
       else if(fp->flags & SYNC_FULL)
       {
           if(fp->size == fp->cap)
           {
               fflush_(fp);
               fp->size =0;
           }
       }
    
}

int main()
{
     FILE_ *fp = fopen_("./log.txt", "w");
     if (fp == NULL)
     {
           return 1;
     }
     int cnt =0;
     const char *test = "hello world\n"; 
     fwrite_(test, strlen(test), fp);
     const char *msg = "hello world";  
     while (1)
     {
           fwrite_(msg, strlen(msg), fp);
           sleep(1);
           cnt++;
           printf("%d\n",cnt);
           if(cnt == 10) break;
     }
     //测试行缓冲
     fclose_(fp);
}

image-20230113202736435

缓冲区与操作系统的关系

image-20230113205630970

==我们刚刚所说的行缓冲/全缓冲/无缓冲全都是我们自己封装的FILE里面的用户级缓冲区的刷新策略!==

==而内核缓冲区的刷新策略是由操作系统来决定的!==——这个刷新策略是更加复杂的!

==这个写入外设的过程对于用户层面毫无关系!==——所以一共经过了3次拷贝!——从内存到用户级缓冲区——从用户级缓冲区到内核缓冲区——从内核缓冲区到磁盘!——所以如果操作系统一旦出现了宕机就很可能会导致数据丢失!

==所以如果我们害怕数据丢失!想要在进入内核缓冲区后就立刻刷新要怎么办呢?==——我们可以使用==fsync函数==告知操作系统!立刻刷新!

fsync函数

image-20230113205137971image-20230113205230808

==强制同步内核里面的存储到存储设备当中!==

==一旦成功返回0,失败返回-1==

void fflush_(FILE_* fp)
{
   if(fp->size >0) write(fp->fileno,fp->buffer,fp->size);
   fsync(fp->fileno);//强制要求操作系统进行外设刷新!
}

==所这次才是真正的刷新!==

举报

相关推荐

0 条评论