• Linux 基础IO


    先来段代码回顾C文件接口

    写文件

    #include<stdio.h
    int main()
    {
    	FILE * fp=fopen("./log.txt","w");//"a",appand 追加写入
    	if(NULL==fp)	{
    		perror("OPEN");		return 1;
    	}
    	int cnt=10;
    	const char* str="ahah\n";
    	while(cnt--)
    	{
    		fputs(str,fp);
    	}
      fclose(fp);		
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    读文件

    #include<stdio.h
    int main()
    {
    	FILE * fp=fopen("./log.txt","r");
    	if(NULL==fp)
    	{
    		perror("OPEN");
    		return 1;
    	}
    	int cnt=10;
    	char buff[128];
    	while(fgets(buff,sizeof(buff),fp))
    	{
    		printf("%s\n",buff);
    	}
     fclose(fp);
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    输出信息到显示器,你有哪些方法

    #include<stdio.h
    int main()
    {
    	const char * msg="hello fwrite\n";
     fwrite(msg,strlen(msg),1,stdout);
    
     printf("hello printf\n");
     fprintf(stdout,"hello fprintf\n");
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    所有的文件操作,表现上都是进程执行对应的函数!进程对文件的操作,对文件的操作就要先打开文件,打开文件的本质就是加载文件相关的属性,加载到内存!

    操作系统中存在大量的进程,进程对文件的比例是 1:n,那么系统中就存在可能更多的,打开文件!打开文件是加载到内存中,os必须对文件进行管理,先描述后组织,
    struct file{

    ​ //包含了打开文件的相关属性

    ​ // 打开文件之间的链接属性

    }

    默认打开的三个流:stdin & stdout & stderr

    extern FILE *stdin;
    extern FILE *stdout;
    extern FILE *stderr;
    
    • 1
    • 2
    • 3
    • C默认会打开三个输入输出流,分别是stdin, stdout, stderr
    • 仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针

    C语言对系统接口进行封装,那么就会有一套属于C语言的IO,但是最终都是访问硬件(显示器,硬盘,文件(磁盘),os是硬件的管理者,所有的语言上对“文件”的操作,都必须贯穿os!一切皆文件

    所以,几乎所有的语言层的输入输出函数,在底层一定需要使用os提供的系统调用!为了更好的使用文件操作,我们需要学习文件的系统调用接口。

    下面是c语言提供好的函数接口,本质就是对系统接口进行封装

    文件操作函数功能
    fopen打开文件
    fclose关闭文件
    fputc写入一个字符
    fgetc读取一个字符
    fputs写入一个字符串
    fgets读取一个字符串
    fprintf格式化写入数据
    fscanf格式化读取数据
    fwrite向二进制文件写入数据
    fread从二进制文件读取数据
    fseek设置文件指针的位置
    ftell计算当前文件指针相对于起始位置的偏移量
    rewind设置文件指针到文件的起始位置
    ferror判断文件操作过程中是否发生错误
    feof判断文件指针是否读取到文件末尾

    系统接口

    open

    ​ 在认识返回值之前,先来认识一下两个概念: 系统调用 和 库函数

    上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
    而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口

    可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。

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

    参数的介绍:

    1、pathname参数: 文件路径

    2、flags参数: 传递标志位

    int有32个bit,一个bit,代表一个标志,若将一个比特位作为一个标志位,则理论上flags可以传递32种不同的标志位。

    O_WRONLY O_RDONLY O_CREAT等都是只有一个比特位是1的数据,而且不重复

    所以我们传入多个标志,只需要给每个相邻的标志按位或即可

    注意:实际上传入flags的每一个选项在系统当中都是以宏的方式进行定义的。

    例如:

    #define O_RDONLY         00
    #define O_WRONLY         01
    #define O_RDWR           02
    #define O_CREAT        0100
    
    • 1
    • 2
    • 3
    • 4

    因为每个标志都是独立的,所以open内部就可以通过&按位与来进行区分选项,

    例如:

    int open(arg1, arg2, arg3){
    	if (arg2&O_RDONLY){
    		//设置了O_RDONLY选项
    	}
    	if (arg2&O_WRONLY){
    		//设置了O_WRONLY选项
    	}
    	if (arg2&O_RDWR){
    		//设置了O_RDWR选项
    	}
    	if (arg2&O_CREAT){
    		//设置了O_CREAT选项
    	}
    	//...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    参数选项含义
    O_RDONLY以只读的方式打开文件
    O_WRONLY以只写的方式打开文件
    O_APPEND以追加的方式打开文件
    O_RDWR以读写的方式打开文件
    O_CREAT当目标文件不存在时,创建文件

    3、mode_t mode参数: 打开权限

    例如 将mode设置为:0666,则创建出来的文件权限为: -rw-rw-rw-

    但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。

    注意: 当不需要创建文件时,open的第三个参数可以不必设置

    4、返回值:
    成功:新打开的文件描述符
    失败:-1

    如下面显示:

    int main()
    {
    //FILE* fd=fopen("./log.txt","w");等于下面
    int fd=open("./log.txt",O_WRONLY|O_CREAT,0644);//创建失败返回-1;
      if(fd<0){
      	printf("open error\n");
    }
      close(fd);
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    close

    系统接口中使用close函数关闭文件,close函数的函数原型如下:

    int close(int fd);
    
    • 1

    使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。

    write

    系统接口中使用write函数向文件写入信息,write函数的函数原型如下:

    头文件#include <unistd.h>
    ssize_t write(int fd, const void *buf, size_t count);
    
    函数说明:write()会把参数buf所指的内存写入count个字节到参数fd所指的文件内。
    
    返回值:如果顺利write()会返回实际写入的字节数(len)。当有错误发生时则返回-1,错误代码存入errno中。
    
    三个参数:
    	第一个参数 文件描述符fd
    	第二个参数 无类型的指针buf,可以存放要写的内容
    	第三个参数 写多少字节数
    	strlen()用来读取长度
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    #include <stdio.h
    #include <string.h
    #include <unistd.h
    #include <sys/types.h6
    #include <sys/stat.h
    #include <fcntl.h
    int main()
    {
    	int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
    	if (fd < 0){
    		perror("open");
    		return 1;
    	}
    	const char* msg = "hello syscall\n";
    	for (int i = 0; i < 5; i++){
    		write(fd, msg, strlen(msg));// ‘\0’不需要被写入,字符串以'\0'为结尾属于c语言的标记规则。
    	}
    	close(fd);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    read

    系统接口中使用read函数从文件读取信息,read函数的函数原型如下:

    头文件 :#include <unistd.h>
    ssize_t read(int fd, void *buf, size_t count);
    read函数的三个参数:
    (1)fd:文件描述符
    (2)buf:指定读入数据的数据缓冲区
    (3)count:指定读入的字节数
    返回值:
    成功:返回读取的字节数
    出错:返回-1并设置errno
    如果在调read之前已到达文件末尾,则这次read返回0
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    我们可以使用read函数,从文件描述符为fd的文件读取count字节的数据到buf位置当中。

    #include <stdio.h
    #include <string.h
    #include <unistd.h
    #include <sys/types.h6
    #include <sys/stat.h
    #include <fcntl.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-1] = 0;// '\n' 也会被读取,所以在最后一个字符抹去'\n'
            printf("%s\n", buffer);
        }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    文件描述符fd

    通过对open函数的学习,我们知道了文件描述符就是一个小整数

    在这里插入图片描述

    当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针files_struct*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件指针,找到文件指针,就可以对文件结构体进行操作,对文件结构体的操作就是对磁盘文件操作 。通过上面分析,C语言FILE结构体,里面一定会包含对应的文件描述符。

    补充:那么文件是什么时候关闭的,准确的来说file文件对象什么时候被移除,这里会涉及到引用计数器;

    int main()
    {
    int fd1=open("./log.txt",O_WRONLY|O_CREAT,0644);//创建失败返回-1;
    int fd2=open("./log.txt",O_WRONLY|O_CREAT,0644);//创建失败返回-1;
    int fd3=open("./log.txt",O_WRONLY|O_CREAT,0644);//创建失败返回-1;
    int fd4=open("./log.txt",O_WRONLY|O_CREAT,0644);//创建失败返回-1;
    	if(fd1<0){
    		printf("open error\n");
    }
    		printf("%d\n",fd1);
    		printf("%d\n",fd2);
    		printf("%d\n",fd3);
    		printf("%d\n",fd4);
    return 0;
    }
    //打印
    3
    4
    5
    6
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    其中:0 1 2下标存放标准输入,标准输出和标准错误文件指针。0123456 文件描述符本质上是一个指针数组的下标。printf() 是C语言的库函数,对open()进行封装,printf()想要访问文件必须有文件描述符,printf()默认向stdout输入数据,而stdout指向的是一个struct FILE类型的结构体,该结构体当中有一个存储文件描述符的变量,而stdout指向的FILE结构体中存储的文件描述符就是1,因此printf实际上就是向文件描述符为1的文件输出数据。类似scanf()函数也是如此,stdin 指向的结构体,该结构体当中有一个变量存储文件描述符0;

    理解一切皆文件

    驱动层用来传递和获取硬件数据,每个硬件的读取方式都不一样,所以每个硬件都会一一对应属于自己的一套交互方法。

    软件的虚拟层VFS虚拟文件系统,操作系统一切皆文件,上层要打开键盘,往键盘写入,操作系统就需要维护一个file结构体。C语言层面上实现多态,让以后file指向某种设备时,调用同一个方法实现不同的方式。我们可以在file结构体里定义一批 int(*read)(),int(*write)()的函数指针,在创建file时,我们给这批函数指针指向某种设备对应驱动层函数。在struct_file的上层看来,所有的文件,读就调用read(),写就调用write(),根本不关心你到底是什么文件。所有在上层就有统一的视角,一切皆文件。

    在这里插入图片描述小结:当我们调用文件操作函数时,我们整个的过程是这样的,通过参数fd找到进程里对应的文件指针,找到对应系统维护的文件结构体file,再调用write,read系列的对应方法。

    磁盘文件vs内存文件

    类似于程序与进程的关系,当程序被运行时,先创建进程PCB,mm_struct,页表等系统级的结构体,加载相关的数据和代码到内存中,通过页表对虚拟内存与物理内存物理产生映射关系;

    磁盘文件与内存文件,也是一样的,打开的文件都会有一个对象,用来保存相关的文件属性,文件对象间的链接关系类似于双链表,在要读取文件等操作时,才会把文件数据加载到物理内存中

    文件描述符的分配规则

    通过一下实验进行证明:

    通过上面的一下样例,0,1,2文件描述符对应标准输入输出错误,现在我们看看关闭文件 0,然后再打开一个文件,我们都知道如果不关闭0那么新打开的文件的文件描述符为3。

    int main()
    {
    	close(0);
       int fd1=open("./log.txt",O_WRONLY|O_CREAT,0644);//创建失败返回-1;
    	if(fd1<0){
    		printf("open error\n");
    }
    		printf("%d\n",fd1);
    return 0;
    }
    //打印
    0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    close(0) ,file*fd_array[0]里的指针不指向可用文件,也就是说array[0]没有被使用,指向新打开的./log.txt文件对象的指针存放到array[]里,通过文件描述符的分配规则把指针存放到相应的下标里,并且返回该文件描述符。

    文件描述符的分配规则:在files_struct里的file* fd_array[]数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符

    重定向

    重定向原理

    重定向原理:基于文件描述符的分配规则;

    #include <stdio.h
    #include <unistd.h
    #include <sys/types.h
    #include <sys/stat.h
    #include <fcntl.h
    int main()
    {
    	close(1);
    	int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
    	if (fd < 0){
    		perror("open failed");
    		return 1;
    	}
    	printf("hello\n");
    	fflush(stdout);
    	close(fd);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    原本我们的printf要把"hello\n" 输出到显示器上的,现在却输出到log.txt的文件里;

    其实就是把文件描述符1 里的内容不指向任何一个struct file,然后打开文件时,通过文件描述符分配规则进行分配;

    现在文件描述符1里就放了新打开的文件指针

    在这里插入图片描述
    追加重定向也是类似

    在上述上,打开文件时按照追加的形式打开即可;

    #include <stdio.h
    #include <unistd.h
    #include <sys/types.h
    #include <sys/stat.h
    #include <fcntl.h
    int main()
    {
    	close(1);
    	int fd = open("log.txt", O_WRONLY|O_APPEND|O_CREAT, 0666);
    	if (fd < 0){
    		perror("open failed");
    		return 1;
    	}
    	printf("hello\n");
    	fflush(stdout);
    	close(fd);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    需要注意的是:

    1.printf() 是C语言的库函数,对open()进行封装,printf()想要访问文件必须有文件描述符,printf()默认向stdout输入数据,而stdout指向的是一个struct FILE类型的结构体,该结构体当中有一个存储文件描述符的变量,而stdout指向的FILE结构体中存储的文件描述符就是1,因此printf实际上就是向文件描述符为1的文件输出数据。类似scanf()函数也是如此,stdin 指向的结构体,该结构体当中有一个变量存储文件描述符0;

    2.上述重定向的代码都是对标准输出进行重定向,如果想对stderr重定向,只需要close(2),然后打开重定向后的文件。

    dup2

    我们想要把输出到显示器,重定向到log.txt文件里,那么我们只需要把file * fd_array[3] 的内容复制到file * fd_array[1]里即可;

    操作系统给我们提供了dup2接口,我们可以使用它完成重定向。

    int dup2(int oldfd, int newfd);
    **函数功能**: dup2会将fd_array[oldfd]的内容拷贝到fd_array[newfd]当中,
    如果有必要的话我们需要先使用关闭文件描述符为newfd的文件。
    
    **函数返回值**: dup2如果调用成功,返回newfd,否则返回-1。
    
    使用dup2时,我们需要注意以下两点:
    
    1、 如果oldfd不是有效的文件描述符,则dup2调用失败,
    并且此时文件描述符为newfd的文件没有被关闭。
    
    2、如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,
    则dup2不做任何操作,并返回newfd。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    #include <stdio.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|O_CREAT, 0666);
    	if (fd < 0){
    		perror("open failed");
    		return 1;
    	}
    		close(1);
    		dup2(fd,1);
    	printf("hello dup2\n");
    	fflush(stdout);
    	close(fd);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    上述代码用途为输出重定向,open打开文件log.txt返回fd指针,关闭fd 1(标准输出),把 fd_array[fd] 复制到fd_array[1]里;printf是C库当中的IO函数,一般往 stdout 中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1
    下标所表示内容,已经变成了myfile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向 。

    dup系列详解

    linux 关闭打开的文件描述符,关闭它们后,如何重新打开stdout和stdin文件描述符?

    详解

    FILE

    上述我们知道了write,open,read这些都是系统接口,在C语言里fprintf,等函数对这些系统接口进行封装,其中在C语言上有stdout,stdin,stderr流,其实就是一个FILE*的指针,调用printf时其实就是向stdout指向的 struct _IO_FILE 这个结构体里写入,这个结构体包含了fd,用户级缓存区;printf把数据写入到c缓冲区里,printf就完成任务了,然后C语言的缓存区按照刷新规则刷新到操作系统的缓冲区里这里的刷新也需要调用系统接口,也需要fd,然后再按照操作系统的刷新机制进行对硬件的写入;下面来详细介绍

    缓冲区的作用如果你了解可以不点进来

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

    FILE结构体如下:

    typedef struct _IO_FILE FILE;// 在/usr/include/stdio.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
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    我们看到的 int _fileno成员变量;就是封装后的文件描述符fd;

    首先来段代码研究一下:

    #include <stdio.h
    #include <string.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(msg0), 1, stdout);
    	write(1, msg2, strlen(msg2));
    	fork();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    运行结果:

    hello printf hello fwrite hello write

    如果输出重定向到一个文件时(./hellofile):

    文件内容:

    hello write hello printf hello fwrite hello printf hello fwrite

    我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)

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

    c语言缓存区刷新机制:

    1. 不缓冲。
    2. 行缓冲。(常见的对显示器进行刷新数据)
    3. 全缓冲。(常见的对磁盘文件写入数据)

    ​ 行缓存按行刷新

    ​ 全缓冲按满了就刷新,或者进程退出的时候,会刷新FILE内部的数据到os缓冲区

    该缓冲区由C语言提供,该缓冲区的位置在FILE结构体中,也就是说,这里的缓冲区是由C语言提供,在FILE结构体当中进行维护的,FILE结构体当中不仅保存了对应文件的文件描述符还保存了用户缓冲区的相关信息。

    在来看几个例子:

    a、没有输出重定向

    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(msg0), 1, stdout);
    	write(1, msg2, strlen(msg2));
    	return 0;
    }
    // 向屏幕输出 
    /*
    hello printf
    hello fwrite
    hello write
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    b、输出重定向到 log.txt

    int main()
    {
     int fd = open("log.txt", O_WRONLY|O_APPEND|O_CREAT, 0666);
    	const char *msg0="hello printf\n";
    	const char *msg1="hello fwrite\n";
    	const char *msg2="hello write\n";
        close(1);
    	dup2(fd,1);// 输出重定向
    	printf("%s", msg0);
    	fwrite(msg1, strlen(msg0), 1, stdout);
    	write(1, msg2, strlen(msg2));
    	close(1);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    运行结果:
    log.txt文件内容:hello write
    分析:
    输出重定向以后,输入到显示器的内容都要输出到log.txt里,首先printf fwrite会把内容放到FILE的缓冲区里,输出到硬盘文件按照满刷新规则,在close(1)关闭以后,进程退出做刷新缓冲区操作,这时候要向文件描述符1写入,但是fd1已经没使用了,所以最后c语言的接口没有输出到log.txt文件里,write()为系统接口不会按照c规则刷新,也就是在close(1)关闭前已经输入到文件去了。
    解决方法:
    调用flush(stdout);
    缓冲区相关的代码:

    //缓冲区相关
    /* 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. */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    补充:

    操作系统也有缓存区

    当我们刷新用户缓冲区(例如c语言缓冲区)时,并不是立刻将数据刷新到磁盘或显示器上,而是先将数据缓存到操作系统的缓存区里,然后操作系统的缓存区等待操作系统自制的刷新机制进行刷新。这一方面我们没必要过多的了解,只需知道操作系统也是有缓冲区的;

    理解文件系统

    内存文件是如何管理的上述已经介绍了,下面我们来理解一下文件系统是如何管理磁盘文件的。

    磁盘的概念

    把缓冲区数据刷新到磁盘实际就是os把数据写入到盘片上

    什么是磁盘?

    磁盘是一种永久性存储介质,在计算机中,磁盘几乎是唯一的机械设备。与磁盘相对应的就是内存,内存是掉电易失存储介质,目前所有的普通文件都是在磁盘中存储的。

    盘片—扇区: 盘片被分成许多扇形的区域

    磁道:盘片以盘片中心为圆心,不同半径的同心圆。

    柱面:硬盘中,不同盘片相同半径的磁道所组成的圆柱。

    每个磁盘有两个面,每个面都有一个磁头。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t41PQnoe-1650585781350)(C:\Users\原永康\Desktop\图片\CT-20220421114944.png)]

    磁盘查询方案:

    磁盘的读写时是如何查询读写位置的?

    盘面(磁头)---- 磁道 ---- 扇区

    1. 确定读写信息在磁盘的哪个盘面。
    2. 确定读写信息在磁盘的哪个柱面。
    3. 确定读写信息在磁盘的哪个扇区。

    注意:磁盘写入的基本单位是:扇区------512字节

    磁盘线性存储

    理解文件系统,我们必须将磁盘盘片想象成一个线性的存储介质,例如磁带,当磁道卷起来时就像磁盘一样是圆形的。

    站在os角度,我们认为磁盘是线性结构的。如图:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aNpD4dvj-1650585781351)(C:\Users\原永康\AppData\Roaming\Typora\typora-user-images\image-20220421201502000.png)]

    LBA索引相当于虚拟地址与物理内存的关系,LBA是站在os角度认识的,最后LBA要转换成磁盘能读懂,磁盘查询步骤–确定盘面,柱面,扇区。

    磁盘分区与格式化

    磁盘分区

    os系统为了更好的管理磁盘,与是就对磁盘进行分区管理。

    计算机为了更好的管理磁盘,于是对磁盘进行了分区。磁盘分区就是使用分区编辑器在磁盘上划分几个逻辑部分,盘片一旦划分成数个分区,不同的目录与文件就可以存储进不同的分区,分区越多,就可以将文件的性质区分得越细,按照更为细分的性质,存储在不同的地方以管理文件,例如在Windows下磁盘一般被分为C盘和D盘两个区域。
    在Linux操作系统中,我们也可以通过以下命令查看我们磁盘的分区信息:

    ls /dev/vda* -l
    
    • 1

    磁盘格式化

    什么是格式?

    当磁盘完成分区后,我们还需要对磁盘进行格式化。格式化的本质就写入文件系统。写入文件系统为“高级格式化”,相当于建好了房屋,并且我可以通过房屋的建设图纸去找到对应的房间,对于磁盘,按照EXT的方式去初始化磁盘也就有了存储结构,操作系统按照EXT的方式去找到存储位置来存储数据。

    其中,写入的管理信息是什么是由文件系统决定的,不同的文件系统格式化时写入的管理信息是不同的,常见的文件系统有EXT2、EXT3、XFS、NTFS等。

    EXT2文件系统的存储方案


    上述了解磁盘的基本概念,主要目的是为了把磁盘想象成线性的结构,为了更好的管理,我们给磁盘进行分区细分化,然后再给每个分区格式化—配上管理系统。接下来我们要学习 一个没有被打开的文件是如何是磁盘是存储的。


    os为了更好的管理磁盘,对磁盘进行分区,如果os把一个分区管理好了,实际上其他分区也可以按照同样的方法进行管理。如果你想让不同的分区按不同的文件系统进行管理,是可以的,因为现在所有的操作系统都支持多文件系统。

    而对于每一个分区,分区的头部会有一个启动块(Boot Block),对于其他区域,EXT2文件系统会根据系统分区的大小将其划分为一个个的块组(Block Group)。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2FPgGNDg-1650585781352)(C:\Users\原永康\AppData\Roaming\Typora\typora-user-images\image-20220421212432022.png)]

    其次,每个块组都包含一个相同的结构,这个结构都由超级块(Super Block)、块组描述符表(Group Descriptor Table)、块位图(Block Bitmap)、inode位图(inode Bitmap)、inode表(inode Table)以及数据表(Data Block)组成。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hQZkg7Nr-1650585781352)(C:\Users\原永康\AppData\Roaming\Typora\typora-user-images\image-20220421214023501.png)]

    1. Super Block: 存放文件系统本身的结构信息。记录的信息主要有:Data Block和inode的总量、未使用的Data Block和inode的数量、一个Data Block和inode的大小、最近一次挂载的时间、最近一次写入数据的时间、最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。

    2. Group Descriptor Table: 块组描述符表,描述该分区当中块组的属性信息。

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

    4. inode Bitmap: inode位图当中记录着每个inode是否空闲可用。

    5. inode Table: 存放文件属性,即每个文件的inode。

    6. Data Blocks: 存放文件内容。
      注意:

    7. 其他块组当中可能会存在冗余的Super Block,当某一Super Block被破坏后可以通过其他Super Block进行恢复。

    8. 磁盘分区并格式化后,每个分区的inode个数就确定了。

    重点学习后4个

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ko4TpnLV-1650585781354)(C:\Users\原永康\AppData\Roaming\Typora\typora-user-images\image-20220421220003421.png)]

    初识文件inode

    如果一个文件没有被打开

    文件=文件内容+文件属性;文件放在磁盘上

    文件内容就是文件当中存储的数据,文件属性就是文件的一些基本信息,例如文件名,文件大小创建时间等信息都是文件属性,文件属性又称为元信息。

    我们使用ls -l的时候看到的除了看到文件名,还看到了文件元数据 。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JTbJ6k2b-1650585781349)(C:\Users\原永康\AppData\Roaming\Typora\typora-user-images\image-20220421191451140.png)]

    每行包含7列 :

    • 模式
    • 硬链接数
    • 文件所有者
    • 大小
    • 最后修改时间
    • 文件名

    ls -l读取存储在磁盘上的文件信息,然后显示出来

    在这里插入图片描述

    在文件系统里,文件的元信息和内容是分离的,保存元信息由一个inode的结构保存,在文件系统里存在大量的inode结构,所以我们需要给每个inode进行编号,即inode 号。在文件系统里几乎所有的文件都有一个唯一标识的inode。

    stat命令

    其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息

      File: ‘abc’
      Size: 9         	Blocks: 8          IO Block: 4096   regular file
    Device: fd01h/64769d	Inode: 787165      Links: 1
    Access: (0664/-rw-rw-r--)  Uid: ( 1003/     BBQ)   Gid: ( 1003/     BBQ)
    Access: 2022-04-21 18:50:53.336108679 +0800
    Modify: 2022-04-21 18:50:51.406039972 +0800
    Change: 2022-04-21 18:50:51.409040079 +0800
     Birth: -
    我们可以把inode看成一个结构体,结构体里大概存放了
    
    ```cpp
    struct inode{
     //文件的所有属性
     // 数据 int inode_number;
      int blocks[32];//数据块列表
       int ref ;// 硬链接数量
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    如何理解位图?

    假设二进制的位图:0000 1010

    从左往右比特位的位置含义: inode编号

    比特位的内容含义:特定inode“是否" 被占用

    例如编号为1的位置为0,0代表没有被占用;

    注意:上面理解位图,inode结构,都是方便大家理解。

    如何理解创建一个文件?

    将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过touch一个新文件来看看如何工作 。

    touch abc
    ls -i abc
    
    • 1
    • 2

    为了说明问题,我们将指令输出简化:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p9852ApS-1650585781354)(C:\Users\原永康\AppData\Roaming\Typora\typora-user-images\image-20220421222541300.png)]

    创建一个新文件主要有一下4个操作:

    1. 存储属性

      内核通过inode Bitmap先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。

    2. 存储数据

      该文件需要存储在三个磁盘块,内核通过iBlock Bitmap找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据

      复制到300,下一块复制到500,以此类推。

    3. 记录分配情况

      文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。

    4. 将该文件的文件名和新的inode编号添加到目录文件的数据块中,建立映射关系。

    如何理解删除一个文件?

    ​ 1.将文件对应的inode在inode位图当中置位无效。

    ​ 2.数据块也同样如此,将该文件申请过的数据块在Block Bitmap当中置位无效。

    所以,删除一个文件,不是把文件的内容删掉。如果我们误删一个文件,是可以恢复的。如果进行其他操作例如开辟一个文件,那么置为无效的文件可能被别人使用了,并修改了文件内容。所以有可能不能恢复。

    如何理解目录?

    1. 在Linux一切皆文件,目录也是一个文件。
    2. 目录的inode存放目录的属性。
    3. 目录的内容存放该目录下的文件名以及文件名映射的inode编号。

    理解硬链接

    我们看到,真正找到磁盘上文件的并不是文件名,而是inode。 其实在linux中可以让多个文件名对应于同一个inode。

    ln abc def
    
    • 1
    • abc和def的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode 263466 的硬连接数为2。
    • 我们在删除文件时干了两件事情:1.在目录中将对应的记录删除,2.将硬连接数-1,如果为0,则将对应的磁盘释放。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZADKodRE-1650585781355)(C:\Users\原永康\AppData\Roaming\Typora\typora-user-images\image-20220422000115482.png)]

    硬链接本质是根本就不是一个独立的文件,而是一个文件名和inode编号的映射关系,因为自己没有独立的inode。

    创建硬链接,本质是在特定的目录下,填写一对文件名和inode 的映射关系。

    理解软链接

    硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件,在shell中的做法

    ln -s abc abcs
    
    • 1

    软链接是有自己独立的inode的!软链接是一个独立文件!!!有自己的inode属性,也有自己的数据块(保存的是指向文件的所在路径+文件名)

    A,C,M三个时间

    下面解释一下文件的三个时间 :

    Access:最后访问的时间 (较新的linux会在一定间隔的时间内进行更新,也就是说Access时间不会被立即更新,原因该时间高频被访问会影响性能)

    Modify :文件内容最后修改时间

    Change : 属性最后修改时间

    注意:通常文件内容被修改后,不仅Modify被修改了,Change也会被修改,例如文件属性的大小被修改了。

    touch file
    
    • 1

    touch 指令更新已存在文件的三个时间。

    三个时间的应用场景

    makefile与gcc会根据时间问题,来判定源文件和可执行程序谁更新,从而指导系统那些源文件需要被重写编译。

  • 相关阅读:
    八大排序(三)堆排序,计数排序,归并排序
    分类预测 | MATLAB实现WOA-CNN-BiGRU鲸鱼算法优化卷积双向门控循环单元数据分类预测
    CVE-2022-42475-FortiGate-SSLVPN HeapOverflow 学习记录
    标记肽Suc-AAPI-pNA、72682-77-0
    视频监控/安防监控/AI视频分析/边缘计算/TSINGSEE青犀AI算法智慧仓储解决方案
    Linux知识点 -- HTTPS协议
    Kubernetes(K8s)从入门到精通系列之十八:使用 Operator Lifecycle Manager(OLM) 安装operator
    力扣203 - 移除链表元素【LeetCode转VS调试技巧教学】
    搜维尔科技:Varjo-探讨汽车工业使用虚拟现实/XR的可能性
    BatchNormalization和Layer Normalization解析
  • 原文地址:https://blog.csdn.net/weixin_58004346/article/details/126257485