Linux —— 文件操作

目录

1.内核提供的文件系统调用

1.1open和close

1.2标记位

1.3write和read

2.文件描述

2.1文件描述符

 2.2文件描述符分配规则

3.重定向

3.1最“挫”的重定向

3.2使用系统调用

3.3重定向原理

3.4让我们的"shell"支持重定向操作

4.一切皆文件

5.缓冲区

5.1缓冲区的本质

5.2缓冲区的刷新策略

5.3缓冲区的位置

5.4缓冲区与写时拷贝

5.5模拟实现"缓冲区"

1.内核提供的文件系统调用

1.1open和close

通过[man]指令浏览其描述,这里截取片段。 

Linux —— 文件操作_第1张图片

第一个open是文件存在的情况下打开文件,第一个参数为文件名,若不指定文件路径,则默认为父进程的工作路径。第二个参数为标记位,int类型的每个比特位的0 和 1代表了不同的标记。Linux提供了多种标记。

第二个open是文件不存在的情况下打开文件,第三个参数为文件创建的初始权限。

close即关闭文件。

 下面给出代码实例以供参考:

umask(0);       //将umask置0
int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);     //相当于C语言的fopen的"w",只写、自动创建、自动覆盖
//文件的权限受umask的影响
close(fd);

下面列举一些常用的标记:

  • O_WRONLY        ->只写
  • O_CREAT           ->创建
  • O_TRUNC          ->覆盖
  • O_RDONLY        ->只读
  • O_APPEND        ->追加

需要多个标记组合在一起时,使用 '|'(按位或运算符) 连接即可。

1.2标记位

int类型有4个字节,32个比特位,每一个比特位的0 和 1代表了不同的标记。例如有:......0001与......0010就是两个不同的标记。我们使用代码实例来演示:

#include 
#include 
#include 
#include 
#include 
#include 

#define ONE (1<<0)
#define TOW (1<<1)      
#define THREE (1<<2)        
#define FOUR (1<<3)

void select(int flags)
{
    if(flags & ONE) printf("one\n");
    if(flags & TOW) printf("tow\n");
    if(flags & THREE) printf("three\n");
    if(flags & FOUR) printf("four\n");
}

int main()
{
    select(ONE);        
    select(ONE | TOW);
    select(ONE | TOW | THREE);
    select(ONE | TOW | THREE | FOUR);
    return 0;
}

Linux —— 文件操作_第2张图片

 也就是说,Linux提供的标记实质上是一个宏,每一个宏代表了不同的信号。

1.3write和read

write是一个系统调用,其声明为([man]指令查看):

需要注意的是,write()是从文件的起始位置开始写的,如果在open()中没有O_TRUNC或者O_APPEND标记,那么为将文件以前的内容的一部分覆盖掉。也就是说,要想像C语言一样自动清空文件的内的数据必须加上O_TRUNC标记 。

下面给出write的实际使用案例以供参考:

#include 
#include 
#include 
#include 
#include 
#include 
int main()
{
    umask(0);       //将umask置0
    int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);     //相当于C语言的fopen的"w",只写、自动创建、自动覆盖
    if(fd < 0) return 1;
        
    char* msg = "hello Linux\n";
    write(fd,msg,strlen(msg));      //像fd描述的文件写入msg指向的字符串
    close(fd);
    return 0;
}

编译运行,查看生成的"log.txt"文件:

read也是一个系统调用,其声明为([man]指令查看):

Linux —— 文件操作_第3张图片

这里以上面write生成的文件给出read的实际使用案例以供参考:

#include 
#include 
#include 
#include 
#include 
#include 
int main()
{
    int fd = open("log.txt",O_RDONLY);

    char buffer[64];
    read(fd,buffer,sizeof(buffer)-1);       //留一个位置补'\0'
    buffer[strlen(buffer)]=0;       //文件的字符串不以'\0'结尾
    printf("%s",buffer);

    close(fd);
    return 0;
}

2.文件描述

2.1文件描述符

进程可以打开多个文件,所以操作系统会有大量的文件。操作系统为了管理被打开的文件,使用了"先描述,再组织"的方法将被打开的文件描述为一个struct file的内核结构体,其包含了文件的大部分属性,多个内核结构体之前以特定的数据结构组织起来。

这些struct file结构体并不是文件描述符,而是操作系统为了管理被打开的文件而创建的。事实上,文件操作研究的是进程和被打开文件的关系,也就是说文件是被进程打开的。那么进程和struct file结构体中间还有一层结构体,名为struct files_struct(文件描述符表),在task_struct中有一个struct files_struct* files指针指向文件描述符表。文件描述表有一个专门用来存储struct file结构体的地址的指针数组(struct file* fd_array[]),这个数组的下标即为文件描述符

在进程(C语言程序)被加载到内存时,会默认生成打开三个文件(这些文件是C语言使用的,系统是是使用文件描述符的),即:

  • stdin    ->标准输入
  • stdout    ->标准输出
  • stderr    ->标准错误

这三个文件的struct file结构体的地址依次按顺序存储在struct file* fd_array数组对应的下标0、1、2位置。所以我们用户的进程第一次创建的文件的描述符为3。

Linux —— 文件操作_第4张图片

下面给出进程与文件的假象模型图:

Linux —— 文件操作_第5张图片

 2.2文件描述符分配规则

 在struct file* fd_array[]数组中,按下标从小到大的顺序,寻找最小、且没有被占用下标作为文件描述符。

下面给出一段代码供加深理解:

#include 
#include 
#include 
#include 
#include 
#include 
int main()
{
    close(0);
    close(2);       //关闭掉标准输入和标准错误文件,即清空数组的占用
    int fd = open("log.txt",O_RDONLY);
    printf("%d\n",fd);
    close(fd);
    return 0;
}

3.重定向

3.1最“挫”的重定向

观察下面这段代码以及现象:

#include 
#include 
#include 
#include 
#include 
#include 
int main()
{
    close(1);       //将fd=1的数组位置清空
    int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);     //文件打开时就占用fd=1的位置
    printf("hello world\n");
    fflush(stdout);
    close(fd);
    return 0;
}

以上是最原始的重定向操作。其原因在于:printf()函数是默认向fd=1对应的文件(标准输出)输出的,但是进行close(1)操作后,fd=1位置的内容就清空了,随后log.txt文件的struct file结构体地址占用了fd=1的位置,所以printf就向log.txt文件输出了。 

3.2使用系统调用

dup类接口是系统提供给我们的接口,其中dup2最为常用。我们通过[man]指令查询:

Linux —— 文件操作_第6张图片

下面给出实际使用案例以供参考:

#include 
#include 
#include 
#include 
#include 
#include 
int main()
{
    int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
    dup2(fd,1);     //重定向
    printf("hello world");
    fflush(stdout);
    close(fd);
    return 0;
}

3.3重定向原理

上层看到的文件描述符是不会变的,例如printf规定了向标准输出输出,即向fd=1对应的文件输出,那么printf找的是文件描述符而不是对应的文件,所以fd的内容无论怎么变,上层找的还是fd。

Linux —— 文件操作_第7张图片

子进程重定向不会影响父进程,因为进程之间相互独立。即子在进程在创建出来的时候,就拷贝了一份文件描述符表。但是文件不属于进程,是不会拷贝的。

3.4让我们的"shell"支持重定向操作

在上次模拟实现命令行解释器的基础上,再进行升级,以支持重定向操作。

#include 
#include 
#include 
#include 
#include       
#include 
#include 
#include 

#include 
#include 

#define NUM 1024
#define NON_REDIR 0     //无重定向
#define INPUT_REDIR 1       //输入重定向 '<'
#define OUT_REDIR 2        //输出重定向 '>'
#define APPEND_REDIR 3      //追加重定向 '>>'



char command[NUM];      //c99数组
char* myargv[64];       //存储指令参数
char* file_name=NULL;
int redir_type=0;

void command_check(char* command)
{
    assert(command);

    //首、尾指针
    char* begin = command;
    char* end = command+strlen(command);
    
    while(begin < end)
    {
        if(*begin == '>')
        {
            *begin=0;
            begin++;
            if(*begin == '>')
            {
                redir_type=APPEND_REDIR;
                begin++;
            }
            else redir_type=OUT_REDIR;
            while(*begin == ' ') begin++;
            file_name=begin;
        }
        else if(*begin == '<')
        {
            *begin=0;       //置0
            redir_type=INPUT_REDIR;
            begin++;
            while(*begin == ' ') begin++;
            file_name = begin;
        }
        else ++begin;
    }

}
int main()
{
    while(1)
    {

        redir_type=NON_REDIR;
        file_name=NULL;

        char buffer[1024]={0};
        getcwd(buffer,sizeof(buffer)-1);        //获取shell的工作路径
        buffer[strlen(buffer)]=0;
        printf("[用户名@主机名 %s]",buffer);
        fflush(stdout);     //刷新缓冲区

        char* s = fgets(command,sizeof(command),stdin);     //输入指令
        command[strlen(command)-1]=0;       //清除 \n 
        

        command_check(command);     //检查指令是否有重定向操作
        myargv[0] = strtok(command," ");
        int i = 1;
        while(myargv[i++] = strtok(NULL," "));      //切割空格

        if(myargv[0] != NULL &&  strcmp(myargv[0],"cd") == 0)
        {
           if(myargv[1] != NULL) chdir(myargv[1]);      //cd命令移动shell的工作路径
           continue;
        }

        pid_t id = fork();
        if(id == 0)
        {

            switch(redir_type)      //使用switch语句
            {
                case NON_REDIR:     //不作处理
                    break;
                case INPUT_REDIR:       //输入重定向
                    {
                        int fd = open(file_name,O_RDONLY);
                        if(fd < 0)
                        {
                            perror("open:");
                            exit(1);
                        }
                        dup2(fd,0);
                    }
                    break;

                case OUT_REDIR:
                case APPEND_REDIR:
                    {
                        umask(0);
                       int flag = O_WRONLY | O_CREAT;
                       if(redir_type == APPEND_REDIR) flag |= O_APPEND;
                       else flag |= O_TRUNC;
                       int fd = open(file_name,flag,0666);
                       if(fd < 0)
                       {
                           perror("open:");
                           exit(1);
                       }
                       dup2(fd,1);
                    }
                    break;
                default:
                    break;
            }
           execvp(myargv[0],myargv);        //进程替换
           exit(1);
        }
        waitpid(id,NULL,0);
    }
    return 0;
}

文件描述符表是属于进程的并且是一个内核数据结构,进程替换是不会影响它的,进程替换只是替换数据段和代码段,是不影响内核数据结构的。

对于文件的关闭,在进程退出时自动关闭。其本质还是在于struct file里面的计数器,这个计数器记录有多少个进程正在引用这个struct file,当进程退出时,计数器就会减1,当计数器为0才文件才销毁。

4.一切皆文件

不止是磁盘上的可执行文件被打开才是文件,硬件也是文件。键盘、鼠标、显示器、内存、硬盘等等都是文件。

操作系统能够使用驱动来管理硬件,那么在驱动上,就一定有硬件与操作系统的IO交互方法。那么在内核中,都有唯一的一份struct file内核结构体对应硬件,以便操作系统管理。所以在Linux的视角来看,一切皆文件。

那么在上层想要与硬件互动时,也是通过struct file结构体实现的,其原因在于此结构体有硬件与操作系统IO交互方法的函数指针,通过这些函数指针去调用不同的交互方式。

发现了吗?即使磁盘上的各种可执行文件或者是硬件非常杂乱,但是在操作系统下总是能有序的抽象化成一个struct file结构体来进行管理。也就是说,我们通过统一的struct file结构体(其中描述了文件的共有属性)来操作不同的文件。用官方的话说(参考Linux内核设计与实现原理):我们可以直接使用open()、read()和write()这样的系统调用而无需考虑具体文件系统和实际物理介质。这样的行为就构成了内核的子系统——虚拟文件系统(VFS)。

Linux —— 文件操作_第8张图片

5.缓冲区

5.1缓冲区的本质

缓冲区就是内存上的一段,专门用来做缓存工作(将进程的数据拷贝到缓冲区)。

需要缓冲区的原因在于:将数据直接写入外设的效率是非常低的(外设的处理速度相对于cpu的处理速度慢了几千几万倍),当大量进程和大量数据时,进程更多的时间花费在与外设的交互上。

将数据先交给缓冲区,与外设的交互由缓冲区去完成,那么进程就不再被外设“耽搁”了。那么节省进程进行IO的时间就是缓冲区的本质。

5.2缓冲区的刷新策略

缓冲区的刷新有三个策略和两个例外。

三个策略:

  • 无缓冲:当数据写入时,立即刷新缓冲区。
  • 行缓冲:数据每写入一行,缓冲区刷新一次。
  • 全缓冲:数据一直写入直至缓冲区满,此时再刷新。

两个例外:

  • 用户强制刷新(例如fflush)就会立即刷新缓冲区。
  • 进程退出时,一般都要进程缓冲区的刷新。

5.3缓冲区的位置

调用C语言库函数exit时会刷新缓冲区;调用系统调用_exit时不会刷新缓冲区(上一篇博客讲到的)。可以证明,缓冲区的位置不在内核当中。

在使用C语言编程时,缓冲区的位置在FILE结构体中(由C语言提供)。FILE结构体里面封装了缓冲区字段、文件描述符字段等等,由此可见,在上层使用C语言编程时,调用的都是底层的接口。也因此可以得出一个结论:使用系统调用与外设进行IO交互数据不会写入缓冲区(因为系统调用可以直接使用操作系统提供的fd,而不需要使用C语言的FILE结构体)。

5.4缓冲区与写时拷贝

给出一段代码,观察向不同的外设写入数据时产生的不同效果:

#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    const char* msg_write="write\n";
    const char* msg_fprintf="fprintf\n";
    const char* msg_fwrite="fwrite\n";
    printf("printf\n");
    fwrite(msg_fwrite,sizeof(char),strlen(msg_fwrite),stdout);
    fprintf(stdout,"%s",msg_fprintf);
    fputs("fputs\n",stdout);
    write(1,msg_write,strlen(msg_write));       //这五个函数都是向标准输出输出
    
    fork();     //创建一个子进程
    return 0;
}

Linux —— 文件操作_第9张图片

其原因在于:正常运行程序时,是向标准输出输出的,缓冲区采用的是行刷新策略(write除外),所以当创建子进程之前,父进程的缓冲区数据已经被刷到标准输出了,创建子进程后,子进程的缓冲区与父进程共享,即子进程的缓冲区没有数据,进程退出时即使刷新缓冲区也不会触发写时拷贝。 

当输出重定向到文件时,缓冲区采用的是全缓冲策略(write除外),所以创建子进程后父进程的缓冲区依然有数据(wtire的数据不经过缓冲区直接输出到了文件上),子进程共享父进程的缓冲区,而这两个进程任意一个进程退出时,会触发缓冲区的刷新,此时就会触发写时拷贝,也就看到了上面的现象。

5.5模拟实现“缓冲区”

为了加深理解,这里展示一个超级、无敌、极限、超纯净阉割版的缓冲区模拟实现。

//头文件

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 



#define SIZE 1024

#define ALL 0       //全缓冲策略
#define LINE 1      //行刷新策略(暂时只实现一个吧)
typedef struct _FILE
{
    int flags;      //缓冲区刷新策略
    int fileno;     //文件描述符
    char cush[SIZE];        //缓冲区
    int size;       //有效数据
    int cap;        //最大容量
}_FILE;

_FILE* _fopen(const char* filename,const char* mode);       //模拟fopen
void _fclose(_FILE* fp);     //模拟fclose
void _fflush(_FILE* fp);        //模拟fflush
void _fwrite(const char* msg,int size,int len,_FILE* fp);     //模拟fwrite
//源文件

#include "_stdio.h"


_FILE* _fopen(const char* filename,const char* mode)       //模拟fopen
{
    int flags=0;
    if(strcmp(mode,"w") == 0)
    {
        flags |= (O_WRONLY | O_CREAT | O_TRUNC);
    }
    else if(strcmp(mode,"r") == 0)
    {
        flags |= O_RDONLY;
    }
    else if(strcmp(mode,"a") == 0)
    {
        flags |= (O_WRONLY | O_CREAT | O_APPEND);
    }
    //上面在确认文件打开方式

    int fd=0;
    if(flags & O_RDONLY) fd=open(filename,flags);
    else fd=open(filename,flags,0666);
    if(fd < 0)
    {
        const char* msg=strerror(errno);
        write(1,msg,strlen(msg));
        return NULL;    //创建文件失败
    }
    //上面在创建文件


    _FILE* fp = (_FILE*)malloc(sizeof(_FILE));
    fp->flags=LINE;     //默认行刷新
    fp->fileno=fd;
    fp->size=0;
    fp->cap=SIZE;
    memset(fp->cush,0,SIZE);
    //上面在初始化_FILE结构体

    return fp; 
}


void _fclose(_FILE* fp)     //模拟fclose
{
   _fflush(fp);
   close(fp->fileno);
}



void _fflush(_FILE* fp)        //模拟fflush
{
    if(fp->size > 0)
    {
        write(fp->fileno,fp->cush,fp->size);
        fp->size=0;
    }
}


void _fwrite(const char* msg,int size,int len,_FILE* fp)     //模拟fwrite
{
    //size没什么乱用,对齐fwrite而已
    
    memcpy(fp->cush+fp->size,msg,len);
    fp->size+=len;

    //行刷新
    if(fp->cush[fp->size-1] == '\n')
    {
        write(fp->fileno,fp->cush,fp->size);
        fp->size=0;
    }
    else if(fp->size == fp->cap)
    {
        write(fp->fileno,fp->cush,fp->size);
        fp->size=0;
    }

}
//主函数

#include "_stdio.h"

#include 
int main()
{
    _FILE* fp = _fopen("log.txt","w");

    const char* msg="hello Linux\n";

    int cnt = 10;
    while(cnt)
    {
        _fwrite(msg,sizeof(char),strlen(msg),fp);
        printf("count:%d\n",cnt--);
        sleep(1);
    }
    _fclose(fp);
    return 0;
}

 读者自行将上面的代码copy走,将主函数的msg字符串的'\n'删除掉或保留,在Linux终端下观察文件的数据情况。

 

你可能感兴趣的:(linux,运维,服务器)