Linux之文件基础IO详解

Posted 小赵小赵福星高照~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux之文件基础IO详解相关的知识,希望对你有一定的参考价值。

基础IO

文章目录


首先我们先理解以下文件的几个基础知识:

  1. 文件的宏观理解:文件是在硬盘上存储的,对文件的所有的操作,都是对外设的输入输出,简称IO
  2. 磁盘上的空文件,占不占用磁盘空间呢?

我们看到新建文件的属性上写的大小和占用空间是0字节,但是它依旧占用磁盘空间,因为文件 = 内容+属性(元数据),这里只是说的内容的大小和占用空间,而文件的属性也是占用空间的,所以一个空文件依旧占用磁盘空间,所以我们所学的文件操作,本质上不是对属性的操作就是对内容的操作

  1. 代码变成进程的过程:文件操作代码->.exe文件->加载到内存中->进程

对文件的操作,本质都是进程对文件的操作,C语言提供的库函数是在用户层,不能直接访问硬件,库函数去调用系统调用接口去访问硬件

下面我们来看一下C语言中对文件的操作:

include<stdio.h>
int main()

    FILE *fp = fopen("log.txt","w");
    if(NULL = fp)
    
        perror("fopen error!\\n");
      	return 1;
    
    char c = 'A';
    for(;c <= 'Z';c++)
    
        fputc(c,fp);
    
    fclose(fp);
    return 0;

上面的代码执行后。就会在log.txt这个文件中写入A-Z,26个英文字母:

那么站在系统的角度上,进程是怎么打开文件的呢?我们下面会慢慢进行讲解,fputc是一个字符字符写,我们看一个一次写很多的函数fwrite,这个函数也可以给文件里写:

#include<stdio.h>
#include<string.h>
int main()

    FILE* fp = fopen("./log.txt","a");
    if(NULL = fp)
    
        perror("fopen");
        return 1;
    
    const char *msg = "hello world";
    fwrite(msg,strlen(msg),1,fp);//这里strlen不要+1,不需要将\\0写入,这本质是C的规定,并不是文件的规定
    
    fclose(fp);
    return 0;

w方式写:写入,每次写入都是重新写入,意味着之前的文件内容会被清空!

a方式写:append,追加写,本质也是写入,不清空原始文件,在文件的最后写入

看到我们打开文件时./log.txt指明了在当前路径下找log.txt,什么是当前路径?每个进程都有一个内置的属性cwd,当前路径就是进程的当前路径。

stdin & stdout &stderror

任何C程序,默认打开的三个"文件":

stdin:标准输入,键盘文件

stdout:标准输出,显示器文件

stderr:标准错误,显示器文件

我们可以看到stdin、stdout、stderr都是FILE*类型的,我们知道键盘,显示器都是硬件,那么这些硬件怎么和文件有关联呢?所有的外设硬件输入输出,本质对应的核心操作无外乎是read和write,他们都有对应的读方法和写方法,不同的硬件,对应的读写方式肯定是不一样的

每个硬件(磁盘、键盘、显示器、网卡,显卡)都有自己的read和write,而每个文件的信息,属性,操作存储在结构体file当中,在struct file结构体中有文件的属性,文件的操作,键盘、磁盘、显示器、网卡显卡的各自的struct file有指向各自硬件的读写方法的指针:

这就是Linux下一切皆文件的原理,中间封装了软件层,通过函数指针去调用方法,在软件层以同样的方式来看待文件,都有一样的文件操作。

我们可以通过曾经的C语言接口,直接对stdin,stdout,stderr进行读写:

#include<stdio.h>
#include<string.h>
int main()

    const char *msg = "hello world!\\n";
    fwrite(msg,strlen(msg),1,stdout);
    return 0;

#include<stdio.h>
#include<string.h>
int main()

    char buff[11] = 0;
    fread(buff,10,1,stdin);
    printf("%s",buff);
    return 0;

那么为什么所有语言都要提供默认打开的标准输入,标准输出,标准错误这些窗口呢?

计算机发明出来,如何交互?语言。那么语言也是需要进行交互的,scanf->键盘,printf->显示器,perror->显示器,如果没有打开,不可以直接调用这些接口,默认打开可以便于语言进行上手使用,都有输入输出的需求,所以所有语言都有默认打开的标准输入,标准输出,标准错误

站在系统角度如何理解文件?

接下来我们理解两个东西:

1.文件和进程的关系

2.系统调用

下面我们来看系统中的文件IO:

系统文件I/O

操作文件,除了上面的C接口(当然,C++等语言也有),我们还可以采用系统接口来进行文件访问,先来直接以代码的形式,实现和上面一模一样的功能:

对应于上面的函数,系统接口是open,close,read,write,所有的语言都是去使用系统调用接口去操作文件的。

open

第一个参数是你要打开的文件名,第二个参数是打开方式:其中有只读,只写,还有读写:

pathname: 要打开或创建的目标文件

flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。

参数:

O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写

比如:

open("log.txt",O_RDONLY|O_CREAT,0664)//只读打开,若文件不存在,则创建它。设置创建的文件权限为664
open("log.txt",O_WRONLY|O_APPEND)//只写打开,追加写
open("log.txt",O_RDWR|O_APPEND)//读写打开,追加写

返回值

成功:新打开的文件描述符
失败:-1

open函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。

close

关闭文件的参数是所要关闭文件的文件描述符,文件描述符的相关细节下面讲解

read

返回值:返回读的字节数

参数:

第一个参数是文件描述符,将文件描述符指向的文件内容读到buf当中,第二个参数就是保存数据的存储单元,第三个参数是读的个数

read的使用:

#include<stdio.h>
#include<string.h>
#include<unistd.h>
int main()

    int fd = open("log.txt",O_RDONLY);
    if(fd < 0)
    
        perror("open");
        return 1;
    
    char buffer[1024];
    ssize_t s = read(fd,buffer,sizeof(buffer)-1);//返回读的个数
    if(s > 0)
    
        buffer[s] = '\\0';
        printf("%s",buffer);
    
    close(fd);
    return 0;

write

write是写到一个文件当中,第一个参数是文件描述符,第二个是需要写的内容,第三个参数是写的字节数

返回值:

如果写成功,返回写的字节数,如果失败,返回-1

系统接口write的使用

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()

    int fd = open("log.txt",O_WRONLY|O_APPEND);
    if(fd < 0)
    
        perror("open");
        return 1;
    
        
    const char *msg = "hello!\\n";
    write(fd,msg,strlen(msg));
    write(fd,msg,strlen(msg));
    write(fd,msg,strlen(msg));
    write(fd,msg,strlen(msg));
    close(fd);
    return 0;

为什么所有语言要封装read,open等接口呢?

兼容自身语法特征,系统调用使用成本较高,而且不具备可移植性,read、open等接口只有在Linux平台下才能使用,在windows平台不能使用,并且方便二次开发

open函数返回值

文件描述符fd

#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
int main()

    int fd1 = open("log1.txt",O_WRONLY|O_CREAT);
    int fd2= open("log2.txt",O_WRONLY|O_CREAT);
    int fd3 = open("log3.txt",O_WRONLY|O_CREAT);
    int fd4 = open("log4.txt",O_WRONLY);
    int fd5 = open("log5.txt",O_WRONLY);
    
    printf("%d\\n",fd1);
    printf("%d\\n",fd2);
    printf("%d\\n",fd3);
    printf("%d\\n",fd4);
    printf("%d\\n",fd5);
    
    return 0;

3,4,5这些都是文件描述符,fd4和fd5是因为不存在这两个文件打开失败,所以返回-1,在OS层面文件描述符就是一个整数,这看起来就是数组的下标,数组不应该在0开始吗?为什么从3开始呢?是因为0,1,2,已经被占用了,他们分别对应:0:标准输入,1:标准输出,2:标准错误,所谓的默认打开文件,标准输入,标准输出,标准错误,其实是有底层系统支持的。默认一个进程在运行的时候,就打开了0,1,2

0,1,2,3,4…其实本质是数组下标,他们是文件描述符

所有的文件,如果要被使用,首先必须被打开,一个进程可不可以打开多个文件呢?答案是可以的,那么当大量文件加载到内存当中时,操作系统一定要把打开的文件管理起来,怎么管理呢?先描述再组织,在Linux内核当中有一个struct file结构体,里面描述了文件的属性信息,文件的操作方法,文件的缓冲与存储位置等等,一个文件有一个struct file,所有文件的struct file用链表组织起来,文件是由进程打开的,一个进程能够打开多个文件,一个进程有一个PCB,那么进程的PCB就要和文件的struct file关联起来,在Linux内核当中是用一个结构体指针关联起来的,它指向struct files_struct,这个结构体里面有个struct file* fd_array[],这个数组存放结构体指针,这些指针指向struct file结构体,所以方法可以访问,就可以对磁盘进行读写了:

对所有的文件进行操作,统一使用一套接口,各自的硬件都有自己的一套读写方法:

对于进程来讲,对所有的文件进行操作,统一使用一套接口(一组函数指针),本质上利用了多态原理

我们看一看linux内核中的task_struct中有struct files_struct *files,这个指针指向files_struct结构体:

我们再来看看files_struct这个结构体,我们可以看到里面确实有一个数组,数组的元素是指向struct file结构体的指针:

我们可以看到struct file结构体中有文件操作:

再转到文件操作里面,可以发现里面有各个读写方法的函数指针:

下面我们来看文件描述符的分配规则:

文件描述符的分配规则

#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>

int main()

	int fd = open("log.txt",O_WRONLY | O_CREAT);
    if(fd < 0)
    
        perror("open error!\\n");
        return 0;
    
    printf("%d\\n",fd);
    close(fd);
    return 0;

当我们打开一个文件时,将文件的描述符输出,输出发现是fd是3,那么当我们关闭0时:

#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
int main()

    close(0);
	int fd = open("log.txt",O_WRONLY | O_CREAT);//我们在log.txt文件中写了aaaaaaaaaa
    if(fd < 0)
    
        perror("open error!\\n");
        return 0;
    
    printf("%d\\n",fd);
    close(fd);
    return 0;

发现文件描述符结果是: fd是0

当我们关闭2时:

#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
int main()

    close(2);
	int fd = open("log.txt",O_WRONLY | O_CREAT);//我们在log.txt文件中写了aaaaaaaaaa
    if(fd < 0)
    
        perror("open error!\\n");
        return 0;
    
    printf("%d\\n",fd);
    close(fd);
    return 0;

发现文件描述符结果是: fd是2

所以得出结论:

文件描述符的分配规则:在files_struct的fd_array数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。对进程来讲,默认的文件描述符从3开始,可以从最小的没有被分配的fd给进程

重定向

那我们如果关闭1呢?看代码:

#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
int main()

    close(1);
    int fd = open("log.txt",O_WRONLY | O_CREAT);
    if(fd < 0)
    
        perror("open error!\\n");
        return 0;
    
    printf("%d\\n",fd);
    close(fd);
    return 0;

我们发现并没有打印,因为1号下标已经不是显示器了,1号下标而是新打开的文件,按道理应该会写在这个打开的文件当中,但是我们查看这个文件发现也没有内容:

为了解决这个疑问,我们想一下这个问题:C语言中的fopen和系统接口open是怎么进行耦合的呢?

我们C语言中,打开文件这样写:

FILE *fp = fopen("log.txt","w");

那么C语言中FILE的是什么呢?它是一个结构体—struct _IO_FILE,在这个结构体里面有一个_fileno,这个_fileno其实就是文件描述符,有了这个_fileno我们就可以和read进行耦合了,比如我们要读文件,系统接口这样调用:

FILE *fp = fopen("log.txt","w");
read(fp->_fileno)

通过fp->_fileno就拿到了文件描述符,这样就可以使用read函数了。所以C语言中的库函数和系统调用接口是通过FILE结构体里面的_fileno进行耦合的

我们来验证一下存在_fileno:

#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
int main()

    printf("%d\\n",stdin->_fileno); 
    printf("%d\\n",stdout->_fileno);
    printf("%d\\n",stderr->_fileno);
    FILE* fp = fopen("log.txt","r");
    printf("%d\\n",fp->_fileno);
    return 0;

可以看到确实是有的,而且他打印出来确实是文件描述符

那么有一个问题:close(1),我们在关闭标准输出文件时,FILE* stdout还存在吗?

close(1)//FILE* stdout还存在吗?存在

答案是存在的,close仅仅是将文件描述符对应的结构体指针指向的struct file释放或者设置为失效了,并没有将FILE清理

在FILE结构体内部包含:

  1. 底层对应的文件描述符下标fileno

  2. 应用层C语言提供的缓冲区数据

在前面的那个文件中没有写入内容这个问题本质上是因为数据在缓冲区中没有刷新,因为新打开的文件是普通文件,它是缓冲区刷新策略是全刷新策略,所以数据在缓冲区中没有刷新,我们这里fflush刷新一下就可以了:

#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
int main()

    close(1);
	int fd = open("log.txt",O_WRONLY | O_CREAT);//我们在log.txt文件中写了aaaaaaaaaa
    if(fd < 0)
    
        perror("open error!\\n");
        return 0;
    
    printf("%d\\n",fd);
    fflush(stdout);//刷新标准输出文件的缓冲区
    close(fd);
    return 0;

printf后面有\\n为什么内容没有刷新到文件里面呢?因为文件变了,显示器文件变成普通文件了,刷新策略变了。显示器是行刷新,普通文件是全刷新

用fprintf函数向显示器文件中打印:

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()

    fprintf(stdout,"heelo world!\\n");
    return 0;

用fprintf函数打印解释重定向:

#include<stdio.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,0644);
    if(fd < 0)
    
        perror("open");
        return 1;
    
    fprintf(stdout,"heelo world!: %d\\n",fd);
    fflush(stdout);
    close(fd);
    return 0;

这里可以看到的是我们将1关闭,close仅仅是将文件描述符对应的结构体指针指向的struct file释放或者设置为失效了,并没有将FILE清理,我们用fprintf向显示器文件打印东西,其实是重定向到了新打开的文件log.txt中:

int main()

    //C接口
    printf("hello printf\\n");
    fprintf(stdout,"hello fprintf\\n");
    fputs("hello fputs\\n",stdout);
    
    //system call接口
    const char *msg = "hello write\\n";
    write(Linux的IO性能监控工具iostat详解

Linux的IO性能监控工具iostat详解

Linux的IO性能监控工具iostat详解

Linux之磁盘与文件系统管理

Linu之linux系统基础优化和基本命令

[OS-Linux]详解Linux的基础IO ------- 文件描述符fd