基础IO+文件系统(从软件到硬件再到软件)

一、回顾C文件接口

C语言文件操作

1.1 C语言如何写文件?

在这里插入图片描述

1.2 C语言如何读文件?

在这里插入图片描述

1.3 C语言打开文件的方式

在这里插入图片描述
在这里插入图片描述
C默认会打开三个输入输出流,分别是stdin, stdout, stderr
仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针。

二、系统文件IO

操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问,先来直接以代码的形式,实现和上面一模一样的代码:
在这里插入图片描述
在这里插入图片描述

2.1 接口介绍

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
返回值:
成功:新打开的文件描述符
失败:-1

mode_t理解:直接 man 手册查看,比什么都清楚。
open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。

2.2 open的返回值

在认识返回值之前,先来认识一下两个概念: 系统调用 和 库函数。
fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口

在这里插入图片描述

通过以上这张图,系统调用和库函数之间的关系就一目了然了,
可以认为,所有的f*系列的函数都是对系统调用接口进行了封装,方便二次开发,因为系统调用接口用起来的难度很大,所以库函数做了封装使用起来就变得更简单一些。

2.3 文件描述符(重中之重)

2.3.1 Linux进程默认打开的输入输出流(012)

Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
0,1,2对应的物理设备一般是:键盘,显示器,显示器。
所以输入输出还可以采用如下方式:
在这里插入图片描述
在这里插入图片描述
文件描述符的含义: 我们现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了struct file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。所以每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件,进行访问了。

2.3.2 文件描述符的分配规则

在files_struct结构体中维护的struct file* fd_array数组当中,找到当前没有被使用的最小的一个下标,作为新打开文件的文件描述符。
在这里插入图片描述

2.4 重定向(重中之重)

在这里插入图片描述
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <

那么重定向的本质是什么???
在这里插入图片描述

2.4.1 使用 dup2 系统调用

在这里插入图片描述
使用场景:
在这里插入图片描述
printf是C库当中的IO函数,一般往 stdout 中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1下标所表示内容,已经变成了myfile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。那追加和输入重定向是如何完成呢?追加重定向的原理和输出重定向的原理是一样的,仅仅是open打开文件时的方式不同,追加重定向是不清空文件,输出重定向是先清空文件再输出。

追加重定向:
在这里插入图片描述
追加重定向和输出重定向的代码在其它的地方没有一点区别。

输入重定向:
在这里插入图片描述

2.5 FILE

因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。所以C库当中的FILE结构体内部,必定封装了fd。
在这里插入图片描述

我们发现 printf 和 fwrite (库函数)都输出了2次,而 write(系统调用) 只输出了一次。为什么呢?肯定和fork有关!

一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
printf 和 fwrite 等库函数会自带缓冲区(之前的进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。所以我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后,但是进程退出之后,会统一刷新,写入文件当中。
但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,即子进程也会刷新缓冲区,所以printf和fwite会产生两份数据。write 没有变化,说明系统调用接口没有所谓的缓冲区。

综上: printf fwrite 等库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们这的讨论范围之内。
那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C库函数,所以该缓冲区由C标准库提供。

在/usr/include/libio.h中有FILE结构体的定义:
在这里插入图片描述

三、Linux下的ext2文件系统

在这里插入图片描述
Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的。

Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。例如学校管理学院一样。

1、超级块(Super Block):存放文件系统本身的结构信息,是描述整个分区的所有基本信息的。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了,但是一般在多个Block Group中都保存着Super Block,所以某个Super Block信息被破环,整个文件系统也不会立即被破坏了。

2、GDT,Group Descriptor Table:块组描述符,描述块组属性信息,例如该块组中有多少个inode被使用了,剩余多少个,有多少个data block被使用了,剩余多少个,inode编号是从多少开始的,该分组一共多大等关于该块组的所有基本信息。

3、块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。

4、inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。

5、inode table:存放文件属性的结构体数组,数组中的每一个元素是一个inode结构体,结构体存文件的信息, 如 文件大小,所有者,最近修改时间等。

6、Data blocks :存放着以块为单位的内存块,用于保存文件内容,一个块等于8个扇区。

在这里插入图片描述

四、软硬链接

4.1 硬链接

我们看到,真正找到磁盘上文件的并不是文件名,而是inode。 其实在linux中可以让多个文件名对应于同一个inode。 在这里插入图片描述
test.txt和hard_link的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode 657073的硬连接数为2。
我们在删除文件时干了两件事情:1.在目录中将对应的记录删除,2.将硬连接数-1,如果为0,则在对应的磁盘释放。

硬链接不是一个独立的文件,因为它没有独立的inode,建立硬链接只是在该目录下增加了一个文件名与inode的映射关系而已。所以硬链接数起始就是指有多少个文件名指向同一个inode的意思。

4.2 软链接

在这里插入图片描述
软链接是一个独立的文件,因为它有独立的inode,软链接文件中的内容是指向的文件的路径,在这里可以认为soft_link中保存的内容是file.txt的路径信息。

五、shell命令行解释器进化版(添加重定向)

5.1 makefile

在这里插入图片描述

5.2 myshell.cc

#include <iostream>
using namespace std;
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>

int quit=0;
//最近一次进程的退出码
int lastcode=0;
//命令行参数的最大长度
#define LINE_SIZE 1024
//命令行字符数组的长度
#define ARGC_SIZE 32
//输入的命令行
char CommandLine[LINE_SIZE];
//当前路径
char pwd[LINE_SIZE];
//字符串指针数组
char* argv[ARGC_SIZE];
//命令的分隔符
#define SPLIT " "

//自定义环境变量表
char myenv[LINE_SIZE];

#define NONE -1
#define IN_RDIR 0
#define OUT_RDIR 1
#define APPEND_RDIR 2


char* rdirfilename=NULL;
int rdir=NONE;

//把整体输入的命令分离成一个一个的命令
int splitstring(char* cline)
{
    int i=0;

    argv[i]=strtok(cline,SPLIT);
    if(argv[i]==NULL)
    {
        return 0;
    }
    i++;
    //最后一次会把NULL赋值给argv[i]
    while(argv[i++]=strtok(NULL,SPLIT));
    //for(argv[i]=strtok(cline,SPLIT);strtok(NULL,SPLIT);i++)
    //{}

    return i-1;
}

void check_rdir(char* cmd)
{
    char* pos=cmd;
    while(*pos)
    {
        if(*pos=='>')
        {
            if(*(pos+1)=='>')
            {
                //追加重定向
                (*pos)='\0';
                pos++;
                (*pos)='\0';
                pos++;
                while(isspace(*pos))
                {
                    pos++;
                }
                rdirfilename=pos;
                rdir=APPEND_RDIR;
                break;
            }
            else
            {
                //输出重定向
                (*pos)='\0';
                pos++;
                while(isspace(*pos))
                {
                    pos++;
                }
                rdirfilename=pos;
                rdir=OUT_RDIR;
                break;

            }
        }
        else if(*pos=='<')
        {
            //输入重定向
            (*pos)='\0';
            pos++;
            while(isspace(*pos))
            {
                pos++;
            }
            rdirfilename=pos;
            rdir=IN_RDIR;
            break;
        }
        pos++;
    }
}

//获取当前路径
void getpwd()
{
    getcwd(pwd,sizeof(pwd));
}

//输入命令行
void interact(char* cline,int sz)
{
    getpwd();
    printf("[%s@%s %s]$ ",getenv("USER"),getenv("HOSTNAME"),pwd);
    char* s=fgets(cline,sz,stdin);
    assert(s);
    (void)s;
    cline[strlen(cline)-1]='\0';

    //cout<<strlen(cline)<<endl;
    check_rdir(cline);

    //cout<<cline<<endl;
}

//内建命令
//需要父进程自己执行
int BuildCommand(int _argc,char* _argv[])
{
    if(_argc==2&&strcmp(_argv[0],"cd")==0)
    {
        chdir(_argv[1]);
        //更改路径后需要重新刷新
        getpwd();
        //修改环境变量表中的PWD
        sprintf(getenv("PWD"),"%s",pwd);
        lastcode=0;
        return 1;
    }
    if(_argc==2&&strcmp(_argv[0],"export")==0)
    {
        //这里必须要自己开一个数组存储导出的环境变量
        //不然等到下一次运行别的命令的时候该导出的环境变量会被覆盖
        //详情可以看后面的图片分析
        strcpy(myenv,(char*)_argv[1]);
        //到处环境变量
        putenv(myenv);
        //设置退出码
        lastcode=0;
        return 1;
    }
    if(strcmp(_argv[0],"ls")==0)
    {
        //"--color"是使我们的不同文件有不同的配色
        _argv[_argc++]=(char*)"--color";
        //指令要以NULL结尾
        _argv[_argc]=NULL;
        return 0;
    }
    if(_argc==2&&strcmp(_argv[0],"echo")==0)
    {
        if(strcmp(_argv[1],"$?")==0)
        {
            cout<<lastcode<<endl;
            lastcode=0;
            return 1;
        }
        else if(*_argv[1]=='$')
        {
            char* p=(char*)_argv[1]+1;
            //获取环境变量
            char* val=getenv(p);
            if(val!=NULL)
            {
                //打印环境变量
                printf("%s\n",val);
            }
            lastcode=0;
            return 1;
        }
        else
        {
            return 0;
        }
    }
    return 0;
}

void NormalExcute()
{
    //普通命令
    //创建子进程进行程序替换执行
    pid_t id=fork();
    if(id<0)
    {
        perror("fork fail");
        lastcode=1;
        exit(1);
    }
    if(id==0)
    {
        //输入重定向
        if(rdir==IN_RDIR)
        {
            int fd=open(rdirfilename,O_RDONLY);
            if(fd<0)
            {
                perror("open fail");
                lastcode=5;
                exit(5);
            }
            dup2(fd,0);
        }
        else if(rdir==OUT_RDIR)//输出重定向
        {
            int fd=open(rdirfilename,O_WRONLY|O_CREAT|O_TRUNC,0666);
            if(fd<0)
            {
                perror("open fail");
                lastcode=6;
                exit(6);
            }
            dup2(fd,1);
        }
        else if(rdir==APPEND_RDIR)//输出重定向
        {
            int fd=open(rdirfilename,O_WRONLY|O_CREAT|O_APPEND,0666);
            if(fd<0)
            {
                perror("open fail");
                lastcode=7;
                exit(7);
            }
            dup2(fd,1);
        }

        //子进程
        execvp(argv[0],argv);
        lastcode=2;
        exit(2);
    }
    else
    {
        //父进程等待
        int status=0;
        pid_t childpid=waitpid(-1,&status,0);
        if(childpid<0)
        {
            perror("wait fail");
            lastcode=3;
            exit(3);
        }
        else if(childpid==id)
        {
            cout<<"father wait success!"<<endl;
            if(WIFEXITED(status))
            {
                cout<<"子进程正常退出;"<<"退出码:"<<WEXITSTATUS(status)<<endl;
                lastcode=WEXITSTATUS(status);
            }
            else
            {
                cout<<"子进程异常退出;"<<"退出信号:"<<(status&0x7F)<<endl;
            }
        }
    }
}

int main()
{
    while(!quit)
    {
        rdirfilename=NULL;
        rdir=NONE;

        //交互问题,获取命令行
        interact(CommandLine,sizeof(CommandLine));
        //分割字符串
        int argc=splitstring(CommandLine);
        if(argc<=0)
        {
            continue;
        }
        
        //判断是否为内建命令
        //不能用sizeof(argv),否则会越界
        int ret=BuildCommand(argc,argv);
       
        if(!ret)
        {
            //普通命令
            NormalExcute();
        }
    }
    return 0;
}

六、自主实现一个Mystdio.h库

6.1 main.c

在这里插入图片描述

6.2 makefile

在这里插入图片描述

6.2 Mystdio.h

#pragma once

#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <stdlib.h>

#define SIZE 1024
#define Permission 0666
#define FLUSH_NOW 1<<0
#define FLUSH_LINE 1<<1
#define FLUSH_ALL 1<<2

typedef struct IO_file
{
    int _fileno;
    //char _inbuffer[SIZE];
    char _outbuffer[SIZE];
    int out_pos;
    int flag;

}_FILE;

_FILE* _fopen(const char* file,const char* s);
int _fwrite(const char* msg,size_t size,size_t n,_FILE* fp);
void _fclose(_FILE* fp);

6.3 Mystdio.c

#include "Mystdio.h"


//"w" , "r" , "a"
_FILE* _fopen(const char* file,const char* s)
{
    int fd=-1;
    if(strcmp(s,"w")==0)
    {
        fd=open(file,O_WRONLY|O_CREAT|O_TRUNC,Permission); 
    }
    else if(strcmp(s,"a")==0)
    {
        fd=open(file,O_WRONLY|O_CREAT|O_APPEND,Permission);
    }
    else if(strcmp(s,"r")==0)
    {
        fd=open(file,O_RDONLY);
    }

    if(fd==-1)
    {
        return NULL;
    }

    _FILE* fp=(_FILE*)malloc(sizeof(_FILE));
    if(fp==NULL)
    {
        return NULL;
    }
    fp->_fileno=fd;
    memset(fp->_outbuffer,0,sizeof(fp->_outbuffer));
    fp->out_pos=0;
    fp->flag=FLUSH_LINE;
    return fp;

}
int _fwrite(const char* msg,size_t size,size_t n,_FILE* fp)
{
    //立即刷新缓冲区
    if(fp->flag&FLUSH_NOW)
    {
        for(size_t i=0;i<n;i++)
        {
            size_t m=write(fp->_fileno,msg,size);
            if(m!=size)
            {
                return i;
            }
        }
    
    }
    else if(fp->flag&FLUSH_LINE)  //行刷新
    {
        if(msg[strlen(msg)-1]=='\n')
        {
            for(size_t i=0;i<n;i++)
            {
                size_t m=write(fp->_fileno,msg,size);
                if(m!=size)
                {
                    return i;
                }
            }
            
        }
        else
        {
            for(size_t i=0;i<n;i++)
            {
                memcpy(fp->_outbuffer+fp->out_pos,msg,strlen(msg));
                fp->out_pos+=strlen(msg);
            }
        }
    }
    else if(fp->flag&FLUSH_ALL) //全缓冲
    {
         for(size_t i=0;i<n;i++)
         {
             memcpy(fp->_outbuffer+fp->out_pos,msg,strlen(msg));
             fp->out_pos+=strlen(msg);
         }
    }
    return n;

}

void _fflush(_FILE* fp)
{
    if(fp->out_pos>0)
    {
        write(fp->_fileno,fp->_outbuffer,fp->out_pos);
    }
}

void _fclose(_FILE* fp)
{
    if(fp==NULL)
    {
        return;
    }
    _fflush(fp);
    close(fp->_fileno);
    fp->out_pos=0;
    free(fp);
    fp=NULL;
}

七、基础IO+文件系统(从软件到硬件再到软件整体脉络一览图)

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
你学会了吗?如果感觉到有所收获,那就点点小心心点点关注呗!后期还会持续更新Linux操作系统的相关知识哦!我们下期见!

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值