Linux学习记录——십유 基础IO(1)

文章目录

  • 文件描述符
    • 如何理解“一切皆文件”
    • 如何理解缓冲区
    • 模拟实现FILE


学习C语言的时候,有文件操作的知识,但那只是学到了操作函数,然后直接用即可,至于原理如何,并不清楚。这篇会逐渐写出来文件的通用知识。

当文件没被打开的时候,文件是存储在磁盘中的;当操作者使用函数打开文件后,文件会被加载进内存里。文件是由内容+属性构成的,所以文件的什么会被加载进内存?不管是什么,属性是一定要有的。加载进内存这个过程,是操作系统做的,让他这样做的是操作者引起的进程。进程令系统加载文件,那么内存中一定不会只有一个文件,多个文件在内存中的时候,操作系统对它们会进行管理,管理的方式就是先描述再组织,如同系统管理内存数据一样,对于文件,系统也会用结构体struct file来存储它们的各项数据,比如属性,以及还有指向下一个文件的指针和其他指针,所以对多个文件的管理就变成了链表的管理。这里也体现出了Linux里一切皆文件的思想。

文件描述符

在C语言中有一系列文件操作函数,Linux也一样。

Linux学习记录——십유 基础IO(1)_第1张图片

和fopen有所不同,返回数据的类型并不是FILE*而int。如果执行无误那就返回文件描述符,错误则返回-1.描述符先不管,先看函数

用man查看,会发现有很多参数,函数先不管,先想一个问题,操作系统如何了解操作者传给它的标志位,也就是函数参数这些?传多个标志位的时候,我们会多设几个位置,但是如果标志位太多这样做也不行,而系统对它的处理就是把这个标志位当做位图结构,一个int的参数有32个比特位,每个比特位就表示一个传过来的参数,也就是一个标志位。

写一段相对应的代码

  1 #include <stdio.h>                                                                                                                                                                                                             
  2 
  3 #define one 0x1
  4 #define two 0x2
  5 #define three 0x4
  6 #define four 0x8
  7 #define five 0x10
  8 
  9 void Print(int flags)
 10 {
 11     if(flags & one) printf("hello 1\n");
 12     if(flags & two) printf("hello 2\n");
 13     if(flags & three) printf("hello 3\n");
 14     if(flags & four) printf("hello 4\n");
 15     if(flags & five) printf("hello 5\n");
 16 }
 17 
 18 int main()
 19 {
 20     printf("------------------------\n");
 21     Print(one);
 22     Print(two);
 23     Print(three);
 24     Print(four | five);
 25     Print(one | four | five);
 26     Print(two | three | four | five);
 27     return 0;
 28 }

Linux学习记录——십유 基础IO(1)_第2张图片

现在回到myfile.c,写文件相关的代码

 1: myfile.c ? ?                                                                                                                                                                                                       ?? buffers 
  1 #include <stdio.h>
  2 #include <errno.h>
  3 #include <string.h>
  4 #include <stdlib.h>
  5 #include <unistd.h>
  6 #include <sys/types.h>
  7 #include <sys/stat.h>
  8 #include <fcntl.h>
  9 
 10 #define LOG "log.txt"
 11 
 12 int main()
 13 {
 14     int fd = open(LOG, O_WRONLY | O_CREAT);                                                                                                                                                                                    
 15     if(fd == -1)
 16     {
 17       printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
 18     }
 19     else printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
 20     close(fd);
 21     return 0;
 22 }

两个参数的意思就是只读,如果读不了那就创建。

在这里插入图片描述

但是创建出来的文件的权限不够好,可以添加第三个参数,表示文件权限应该是多少。

Linux学习记录——십유 基础IO(1)_第3张图片

还可以使用umask改一下权限编码。然后往文件里写东西

Linux学习记录——십유 基础IO(1)_第4张图片

往缓冲区里写,然后write读取。正常地运行,但是如下图

Linux学习记录——십유 基础IO(1)_第5张图片

在这里插入图片描述

Linux学习记录——십유 基础IO(1)_第6张图片

还有一个问题,O_CREAT会延续之前的内容,不会清空文件而继续写。

int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);

Linux学习记录——십유 基础IO(1)_第7张图片

Linux学习记录——십유 基础IO(1)_第8张图片

所以当进行文件写入时,底层会传递O_等选项。C语言还有a用来追加,Linux也有O_APPEND用来追加,但必须先写文件才能追加。

int fd = open(LOG, O_WRONLY| O_CREAT | O_APPEND, 0666); 

读取文件O_RDONLY,int fd = open(LOG, O_RDONLY);

Linux学习记录——십유 基础IO(1)_第9张图片

Linux学习记录——십유 基础IO(1)_第10张图片
所以像fopen,fclose,open,close这些,就是库函数和系统调用,库函数封装了系统调用,fopen里就封装了系统的open。

----------------------------
无论是打开还是写入文件,都是用户层在访问硬件,通过系统调用来访问,他们不能越过操作系统去访问。无论哪个语言,文件操作的时候都是调用OS的open,write等函数,只不过各个语言封装形式有所不同。

---------------------------

刚才这个程序,如果正常运行,fd会得到返回值,但是为什么是3?

任何一个进程在启动的时候都会默认打开3个文件,标准输入,标准输出,标准错误,它们本质上都是文件,而它们在语言层面的表现就是像C语言中stdin,stdout,stderr,C++中cin,cout, cerr一样,不过C++里那是一个类。

  1 #include <iostream>
  2 #include <cstdio>
  3 
  4 int main()
  5 {
  6     //Linux里一切皆文件,向显示器打印本质就是向文件写入                        
  7     //C                              
  8     printf("printf->stdin\n");           
  9     fprintf(stdout,  "printf->stdout\n");
 10     fprintf(stderr, "printf->stderr\n");
 11                                      
 12     //C++                                  
 13     std::cout << "cout->cout" << std::endl;
 14     std::cerr << "cerr->cerr" << std::endl;
 15                                      
 16 }

所有打印的语句都会出现在显示器上。三个标准文件是这样的

标准输入—设备文件–>键盘文件
标准输出—设备文件–>显示器文件
标准错误—设备文件–>显示器文件

所以3之前的012对应的就是标准输入输出错误,所以我们得到的就是3.这几个数字,也就是文件描述符,也就是open返回值,是数组的下标。为什么会出现数组?

在系统底层,用户通过进程命令操作系统打开文件,会建立一个进程的pcb,在内存中也会建立文件的struct file,只要找到file,就能找到文件的内容和属性。一个进程可以打开多个文件,进程会再建立一个files_struct结构体,里面有一个struct file类型的指针数组,每一个元素都是struct file的指针,用来找到这个进程打开的文件,进程pcb里又有一个struct files_struct* 类型的指针指向这个结构体,所以进程就可以找到所有自己打开的文件了,数组下标也就能够解释了。

当用write往文件写入内容时,这个进程会找到用户给的文件描述符所对应的文件,每个文件会链接一个缓冲区,write函数把要写的内容拷贝到这个缓冲区,至于缓冲区什么时候刷新到文件里,由系统决定。所以这些IO类write等函数,本质上是在用户空间和内核空间进行数据的来回拷贝。

如何理解“一切皆文件”

一个进程pcb通过一个结构体,结构体里有数组,通过下标访问每个文件的结构体struct file。file有文件的内容+属性,还有缓冲区。不止这些,像键盘,显示器,网卡,显卡等这些外设,它们都有自己的IO函数,比如读写函数,而Linux下每个文件结构体里都有一些函数指针,来指向这些外设自己的IO函数。在上层,系统有read,write等函数,前面写到过,它们的本质是向缓冲区里拷贝数据,然后函数指针调用底层不同设备的方法,那么就可以把数据放到设备里。这就是一切皆文件。底层无论有什么差异,最终都变成一个公式,函数指针调用函数去操作数据,把缓冲区的数据放到设备里。

继续深入

FILE是什么?为什么C语言中文件操作函数的类型是FILE

FILE是一个结构体,是由C语言的库提供的。

在这里插入图片描述

底层有系统创建的文件结构体,再往上一层就是系统调用接口,比如read,write等接口,再往上就是语言库,C语言里有FILE相关的结构体,里面有在库中文件的所有属性。C语言想用fopen函数,就需要FILE结构体,想要调用open函数就得需要fd。所以FILE结构体必定封装了fd,这样才能用系统调用接口。

同理,其他语言的文件库也必须有文件描述符。

回到描述符的问题,虽然现在已经知道这是数组下标了,但还是有问题的。打开多个文件,依次打印3456789后,如果把3这个文件给去掉,再添加一遍,那么还会是3吗?还是10?

写一些代码

 15     int fd1 = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
 16     int fd2 = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
 17     int fd3 = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
 18     int fd4 = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
 19 
 20     printf("%d\n", fd1);
 21     printf("%d\n", fd2);
 22     printf("%d\n", fd3);
 23     printf("%d\n", fd4);

Linux学习记录——십유 基础IO(1)_第11张图片

在这之前加上这一句fclose(stdin);

Linux学习记录——십유 基础IO(1)_第12张图片

0就出来了。再关闭stderr就会这样

Linux学习记录——십유 基础IO(1)_第13张图片

进程中,文件描述符的分配规则:在文件描述符表中,最小的,没有被使用的数组元素,分配给新文件。

现在针对1做一些操作

Linux学习记录——십유 基础IO(1)_第14张图片

关闭标准输出

在这里插入图片描述

没有输出,并且这时候LOG,也就是log.txt的内容却是you can see me!。本来要打印到显示器的内容打印到了文件了,也就是发生了重定向。在语言层上,printf是向标准输出打印,而这时候标准输出关掉了,取而代之的是log.txt,所以就发生了重定向。而这个过程上层并不知道。

所以重定向的原理就是在上层无法感知的情况下,在OS内部,更改进程对应的文件描述符表中特定下标的指向。

除了输入和输出重定向,还有追加重定向。只要O_TRUNC改成O_APPEND就可。

像stdout,cout都是向1号描述符对应的文件打印,而cerr则是2;输出重定向只改的是1号对应的指向,2号不影响。

现在写个代码,把常规信息打印到log.normal,异常消息打印到log.error中。

Linux学习记录——십유 基础IO(1)_第15张图片

  1 #include <stdio.h>
  2 #include <errno.h>
  3 #include <string.h>
  4 #include <stdlib.h>
  5 #include <unistd.h>
  6 #include <sys/types.h>
  7 #include <sys/stat.h>
  8 #include <fcntl.h>
  9 
 10 #define LOG "log.txt"
 11 #define LOGN "lognormal.txt"
 12 #define LOGE "logerror.txt"
 13 
 14 int main()
 15 {
 16     close(1);
 17     open(LOGN, O_WRONLY | O_CREAT | O_APPEND, 0666);
 18     close(2);
 19     open(LOGE, O_WRONLY | O_CREAT | O_APPEND, 0666);
 20     printf("hello printf->stdout\n");
 21     printf("hello printf->stdout\n");
 22     fprintf(stdout, "fprintf->stdout\n");
 23     fprintf(stdout, "fprintf->stdout\n");
 24     fprintf(stderr, "fprintf->stderr\n");
 25     fprintf(stderr, "fprintf->stderr\n");

要是想都放到一个文件里,./a.out > log.txt >2&1

但是这样不方便。有dup2函数可以重定向,两个参数为oldfd,newfd,newfd会是oldfd的拷贝,所以最终只有oldfd这个数值,比如要把1重定向到fd上,那么oldfd就是fd,1就是newfd。

Linux学习记录——십유 基础IO(1)_第16张图片

如何理解缓冲区

Linux学习记录——십유 基础IO(1)_第17张图片

在这里插入图片描述

如果在最后加上fork()。

Linux学习记录——십유 基础IO(1)_第18张图片

Linux学习记录——십유 基础IO(1)_第19张图片

为什么会出现三行?去掉fork就是正常的两行,所以一定是fork影响了这里的结果。

在操作系统里,有进程pcb,文件结构体,指向标准输入输出等的指针,而在这上层,有C语言库。当我们自己的程序调用fprintf后,会通过struct FILE调用系统调用接口,C库会封装fd,也还有给我们准备好的缓冲区。当我们写入数据,会先把数据放到C的缓冲区里,然后函数返回,暂时还没有把数据放到系统内部的缓冲区。至于数据如何刷新给系统,C库有自己的策略,按照自己的策略去调用write给对应的文件缓冲区里。

C库常有的策略有:

1、无缓冲(也就是直接调用write把数据放到系统中)
2、行缓冲(遇到\n就把\n这之前的数据放到系统中)
3、全缓冲(全部写完后在放到系统中)

显示器采用行缓冲。普通文件采用全缓冲。

缓冲区能够节省调用者的时间,让它有更多的时间去做别的事,因为系统调用是有时间代价的,这样交给C库缓冲区后,进程可以继续忙自己的事。而C库会把所有要缓冲的数据都输入进来后,在送给系统,节省时间。

那么C的缓冲区在哪里?缓冲区在FILE结构体里,比较复杂。在操作者使用fopen等函数时,FILE结构体就会在库中建立好。

在上面的代码中,write是系统调用,所以和C的缓冲区无关,它直接去找系统。像显示器打印时,两条语句都是带\n的,所以在fork之前,fprintf的内容就已经通过行缓冲打印出来了,那么fork就无用;而当重定向到文件里后,就是全缓冲策略了,fprintf的内容会在C的缓冲区里,当创建子进程后,刚才的就是父进程写入的数据,而子进程又会写一遍,当刷新时,就出来了两次fprintf的内容。

模拟实现FILE

链接: main.c

链接: my_stdio.c

链接: mystdio.h

当使用文件函数时,系统会先把数据放到缓冲区里,系统调用接口会根据缓冲区刷新策略用fd把数据放到系统内部的文件缓冲区里,最后根据策略把数据从缓冲区刷新到磁盘。

用户也可以强制刷新,把内核的文件缓冲区的数据刷新出来。fsync接口。

结束。

你可能感兴趣的:(Linux学习,linux,学习,运维)