有关c语言的IO函数
fopen、fread、fwrite、f等等
我们先来看一个简单的例子:打开一个文件
我们可以清晰的看到,有一个exe 指 可执行文件所处的路径,cwd 指进程运行时的路径
这个小小的例子就是打开一个用来写入的文件,要注意操作完要记着关闭文件,接下来我们尝试操作文件
fputs 将字符写入流中
fgets,将流中的内容写到缓冲区中,其中第一个参数为缓冲区,第二个参数则是我们预期输入缓冲区的大小
至此我们完成了大致的c语言文件操作,这里有几个细节
FILE* 是我们打开文件时的返回值,这里的文件类型可以是普通文本文件、二进制文件、设备文件等
还需要注意的是 ,进程运行时,会自动打开三个流文件,分别叫做标准输入、标准输出、标准错误,他们都在头文件中被声明,分别对应的设备为键盘、显示器、显示器。
因此,我们就可以通过fgets和fputs 来模拟printf和scanf
第三点:a和w分别为追加写入和覆盖写入,也就是从文末开始写 与 从文件开头开始写
以上就是大致c语言相关的io操作 ,当然还有fseek、ftell、frewind等等函数,我们这里不深入探究。
我们上文中所使用的C语言IO函数,也就是c语言的库函数,它通过对系统调用接口的封装来让程序员轻易使用,接下来我们就聊聊更加底层的系统调用接口。
我们先来看看如何打开文件,叫做 open函数
它的参数有三个,第一个为我们所要打开的文件路径和文件名,这个与fopen相同
第二个参数为标志位参数,可以有O_RDONLY , O_WRONLY , O_RDWR 分别对应了只读 、只写、 可读可写。
除此之外,还有其他的标志位,比如 O_CREAT 我们可以发现 我们的系统调用接口与fopen有一些不同,fopen在文件不存在的情况下会自动创建,而open不可以,你需要写标志位O_CREAT 来增加这一功能。否则会打开失败,返回-1
第三个参数为我们创建文件时的权限值,所以如果文件已经存在 ,就不需要第三个参数了。
在此,我们来对第二个参数的标志位进行深入的了解:
我们可以认为有32个不同的标志位,因为我们在传参时,理论上可以传32bit位,标志位的本质就是一位为1,其余31位全部为0,来分别标志不同的状态。这也就解释了我们在进行标志时用 或 (|)这个运算符来处理。
这样比特位传递参数的方式就给我们许多的选线。
最后一个参数也有一些小细节,我们一定要输入四位的权限 ,第一位我们默认为0就好了,否则我们仅仅输入 666这样的码是无法完成权限的赋值的。
其中在赋值权限时,还要注意 umask这样的掩码,我们可以在打开文件前将掩码赋为0。
我们就可以轻易的赋值权限了。
最后我们来谈谈open的返回值,让我们意想不到的是它为int ,而我们fopen的返回值为FILE * 。
open的返回值:成功 返回file descriptor – 文件描述符 ,失败则返回 -1
我们可以打开多个文件,发现他们的fd值是从3开始的连续整数。
有两个问题:1.为什么没有0、1、2
2.为什么它是连续的,很容易让人想到数组
我们回答第一个问题,我们还记得之前说过进程运行时会自动打开3个流,其中0就是 stdin的 fd, 1就是stdout 的 fd, 2 就是 stderr 的 fd。所以我们在打开第四个文件时,其返回值fd会为3 。我们可以想到,这三个流也是被打开的文件,所有文件都会在打开时有一个fd值。我们所谓的文件描述符就是数组下标,之后打开的文件会依次被赋予fd值。当然这是大致的了解。
我们现在就来看看文件描述符的底层逻辑:
我们的文件可以分为两种:1.磁盘文件 2.内存文件
磁盘文件:
就是我们保存在磁盘中的文件,这个文件会在进程调用它的时候进行加载到内存的操作,类似于我们的程序替换这样的加载器。这个文件的组成为:内容+属性,属性比如创建时间、文件大小、所属组、修改日期等等这样的信息 , 也称作 元信息 ,而内容就是我们打开所看到的内容。
内存文件:
当我们进程需要一个打开或是操作一个文件的时候,我们操作系统会为这个文件生成一个struct file 这样的文件描述结构体,其中大多包含了文件的元信息。一个个这样的结构体会形成双链表,让我们管理。在这之前,在进程创建的时候,还会生成一个struct files_struct 这样的结构体,这个结构体被pcb内的指针所指向,用来管理文件。其中这个结构体中有一个部分为一个指针数组,struct file* fd array[32] ,这里的每一个指针就会指向我们文件的struct file。而这里的数组下标就是我们所知道的文件描述符。其中这个文件描述符所在的数组是可以扩展的。所以我们每打开一个文件,都会有文件描述符指针指向它,并且它的指向是有特性的:也就是依次从小到大指。
我们都知道,一个系统中会有无数已经被打开的文件,所以我们通过这样的方式对文件进行管理。
当然这些结构体都处在内存中。
如果我们需要对一个文件进行操作,不是一下将磁盘中的全部文件拷贝到我们的内存中,而是通过缓冲区,延后式的加载到内存中让我们操作。
比如我们打开一个log.txt文件,并被赋予了3号fd , 我们现在read(3,xx,yy),此时执行到read这一步时才会通过我们的文件描述信息struct file 将文件加载到内存中。
此时如果我们close (1)这个stdout文件,那么我们在打开文件时,log.txt会被分配到 1号fd。
以上就是我们文件描述符的底层实现,它的分配规则就是从最小但是没有被使用的fd开始分配。
重定向的本质:就是修改文件描述符fd下标所对应的 struct file * 的内容。
比如原本 1 号fd 默认打开 stdout ,而现在让其指向了 log .txt ,以至于我们想要printf打印的内容 ,写入到了log.txt中。这就叫做重定向
我们在正式开始重定向的原理之前,我们需要明白一个道理,C语言的文件操作和系统调用的文件操作 ,究竟谁优谁劣
C库函数是一个fp指针,指向一个FILE对象。而这个FILE 对象中 就存在了 fd文件描述符 以及 缓冲区信息
我们经常使用的stdout 就是一个fp指针 ,指向了一个FILE对象,这个FILE对象中 存储了fd =1 这样的信息,然后进程对这个fd进行检索,在file_struct中 找到这个文件。也就是说:
C库函数是无法对fd进行直接操作的,而系统调用可以。
所以我们推荐使用C库函数等语言级别的函数来对文件进行操作,原因如下:
其中,这也就解释了,当我们关闭一个文件,而printf为什么不会依旧打印在stdout“显示器”中,因为stdout,它只认识FILE* ,当我们关闭文件导致 fd =1 指向其它文件,但是stdout所指向的结构体中依旧指向 fd = 1 的文件,因为C语言无法直接改变fd ,所以此时fd = 1 指向了log.txt ,printf 也就会往这个文件中打印信息了。
在这份代码中,我们混用了系统调用和c库,这是完全没有问题的
现象:本应该打印到显示其中的内容,打印到了文件中 --这就叫做输出重定向
本质:由于我们的关闭操作,fd = 1 这个 指针,被我们新打开的文件占用了。由于stdout这个 FILE * 没有鉴别能力,致使其指向的结构体依旧指向fd = 1 这个指针。致使printf打印到了文件中。
此时我们需要了解几个问题:
请问fopen究竟做了什么?!
答案:
输出重定向:以写的方式打开文件,写到哪? 写到stdout中,我们在底层修改stdout所指的文件。达到输出重定向。
输入重定向:
输入重定向: 这里我们通过读的方式打开文件,从哪读?键盘磁盘,还是文件?这种改变就叫做输入重定向。
追加重定向:
追加重定向是输出重定向的一种,我们只需要在打开文件时添加上 追加标识符 O_APPEND ,其余和输出重定向完全一样
在这里,我们写了三个不同的输出,其中fputs 是 没有格式化输出的概念的,你给他写什么都会原模原样的打印
我们需要在了解一个概念:
我们键盘显示器都叫做字符设备,输入的还是显式的都是字符。而printf的功能就是将其它类型的数据,转换为字符,并打印到显示器。我们所写的%d 之类的,就是告诉编译器 这个数据是整数,你把整数转换成字符在输出吧。 —这就叫做格式化输出
格式化输入也是相同的道理,而我们转换的依据 ,就是ascll 码表。
我们先来看一段简单的代码:
当我们不带\n时,现象:休眠3s后才显示我们打印的字符串。
而当我们带上\n后,就是直接显示出来。
结论:不论什么情况,都是按代码步骤,先打印后sleep,但是打印的数据去了哪里,我们并没有看到。这就是缓冲区。
缓冲区有三种:无缓冲、行缓冲、全缓冲
行缓冲:一般用于显示器率刷新数据时的策略:遇到**\n** 或是 一行缓冲区满了 或是 进程结束了,才会刷新出数据,让数据从缓冲区中流出来。
全缓冲:只有当缓冲区文件写满了才会刷新,将数据流入磁盘中。
一个很简单的道理:全缓冲的效率是最高的,一次积攒很多,一起送达目的地,少去了很多来回路上的时间。但是这样虽然高效,我们在看显示器时,无法等待太久,所以采取了折中的方法,行缓冲,其效率和可用性是一种平衡
缓冲区究竟在哪里呢?缓冲区是谁提供的呢?接下来我们就来研究这两个问题。
依旧是一份简单的代码
操作:用C库和系统调用写入数据,写入到显示器和文件中,并通过fork 创建进程,使子进程和父进程有相同的代码和数据
现象:当正常打印到显示器中时,且不论有没有fork创建进程,两份代码都是正常打印我们想要的字符。而当我们重定向到文件中同时fork子进程,我们发现 只有系统调用所打印的字符串打印了一遍,C库打印的字符串都打印了两遍
分析:显示器 --行缓冲 文件-- 全缓冲 重定向改变了缓冲方式 ;c接口打印了两次 ,系统接口打印了一次。
解释:
向显示器打印,由于我们带了 \n 所以fork 前,这些字符都被打印并且被刷新出来。
此时重定向到文件中,缓冲区变为了全缓冲,也就意味着,我们这C语言的两行字符串在fork前,都没有刷新,全部被打印在了全缓冲区中。缓冲区一直没满且进程也没退出。
此时我们创建子进程,我们需要知道 ,缓冲区是在内存中,所以当我们创建子进程时,子进程会与父进程有相同的数据和代码。所以此时父子进程会有相同的缓冲区,由于进程之间的独立性,当子进程结束时,需要刷新缓冲区,也就是改变内存中的数据,就要发生写时拷贝。子进程会开辟新的空间并把之前的数据拷贝一份。随着进程结束,缓冲区刷新出一份字符串,之后父进程也刷新一份字符串。所以打印出了两份字符串。
大家一定很疑惑,为什么全缓冲区中有两行代码,而不是三行呢?
因为write是没有缓冲区的,只有两个C库函数存在缓冲区,所以write只打印了一份。
我们这里谈到的缓冲区 指 : 用户级缓冲区
结论:
为什么被打印两次:1、全缓冲区机制 2、写时拷贝 (OS)机制
至此,我们解释了为什么会出现上面代码的情况,而为什么系统调用没有缓冲区呢?C为什么有?
这个原因我们之后再谈,但是我们现在可以解决的是,缓冲区是C自带的,而不是系统提供的,因为C是对系统调用的封装,系统都没有缓冲区,你C自创的。
什么是流:数据流入缓冲区 在流出 缓冲区。缓冲区 相当于一个河床。
所以我们现在理解了,为什么FILE 对象,是由 fd 和用户缓冲 组成的了。
我们来看看FILE结构体吧!
刷新的本质:用户级别无法与硬件直接交互,更别提将数据直接刷新到硬件上。冯诺依曼体系决定了,用户层之下,一定有操作系统层,来对下,管理硬件。所以我们的刷新,就是将我们的用户级数据刷新到 kernel级缓冲区中,这就是操作系统的缓冲区,再由操作系统刷新到硬件中。
我又有问题了,为什么我们再写代码的时候,必须要加fflush 才能刷新出数据。
解释:当我们写入数据到文件中,全部写入了用户层缓冲区中,还没有流入系统级别缓冲区,此时进程还没有结束,我们在代码的最后就用close关闭了文件,那系统级缓冲区没了。之后随着进程的结束,用户缓冲区终于准备自动刷新了,可是此时系统文件已经被close了,数据就全部消失了。
所以面对这样的,写入无法刷新的情况,我们通常采用这样方法:
这里需要清楚的是close关闭的是系统文件,而不是用户层文件,也就是说,close清空了fd的指向。而fclose 清空了FILE *的内容。fclose 清空fd和用户缓冲区 ,然后数据流入系统缓冲区,fclose还会调用close 对系统文件进行关闭。
至此,我们再来系统了解一下重定向:
文件描述符本质–数组的下标
重定向:用户层不变,也就是FILE* 层不变,改变的是系统层file* 的指向。语言级别感受不到底层的变换。这也就是重定向的根本。
stdout 和stderr 虽然都是显示器,但是两个是不同的fd,也就是说同一份文件,被同一个进程,打开了两次,fd分别对应了1和2
为什么说,linux下一切皆文件
我们进程控制硬件时 , 磁盘有read读取,write 写入,显示器没有read读出,只有write写入,网卡有读出和写入,键盘只有读出数据给进程,而没有写入。那么这么多读写操作,每一个都不同。那我们如何管理呢?我们只写了一份代码 ,一个read和write 就可以控制这么多硬件。本质在于 我们所使用的read 和 write 都是函数指针,指向了底层不同的硬件的read和write。我们只需要管理这个函数指针,就可以完成对所有硬件的操作。而这中函数指针,存在于struct_file 中,也就是元信息中,每一个文件打开后,都有对应的函数指针,指向底层硬件的接口。
所有一切皆文件,指一切皆 struct file { } ;
我们上文中写了很多重定向,都是用close关闭这样的函数实现的,这也太麻烦了。dup来了。
dup就是通过对fd指针的拷贝,实现重定向。也可以理解为,拷贝之后,有两个fd都指向同一个文件。
我们重点讲dup2,
先来读一段英文: dup2 makes newfd be the copy of oldfd 也就是说newfd是oldfd的拷贝
比如我们想要实现输出重定向,只需要 dup2 (fd ,1),此时 fd 和 1 都指向我们打开的文件。
这就是dup函数的基本使用了。
文件系统是什么?我们在通过ls -l这样的文件查看命令,查看各种文件时,其中的1或者是其它数字代表硬链接,他究竟是什么意思?我们现在就来看看,在了解软硬连接前,我们先来看一个新概念 – inode
inode是一个码,我们通过 ls -i 可以对其进行查看。
我们之前再讲进程控制的时候,将文件分为两类:磁盘文件和内存文件。今天我们就主要谈谈磁盘文件
我们可以看到我们查看文件的第一列中,就是inode码,下一列是指文件的类型,之后分别是文件权限、硬链接数、拥有者、所属组、文件大小、日期、文件名。 —我们如今把他们都称为一个文件的元信息
一个文件由 文件的内容 和 文件的属性(元信息) 所组成,我们的linux将属性和内容进行了分离存储,inode就是用来保存元信息的变量,它一般占了128 或是 256字节。inode就是任何一个文件的属性集合,每一个文件都有对应的inode编号,彼此都不相同。我们的linux中 一定会存在大量的文件,为了更好的了解inode这样的文件属性信息,我们现在来讲讲操作系统是如何对硬盘进行分区的,是如何管理如此多的文件的。
小tips: 我们通常使用的cat 打印的就是文件的内容,而ls 打印的就是文件的元信息,也就是文件的属性。
我们知道文件存储在磁盘中,而如今我们更多的使用ssd这样的电子设备,今天我们来复古一波,讲讲机械设备 — 磁盘,我们简单粗暴的认为磁盘是计算机中唯一的机械设备 。
既然是机械设备,不是电子设备,那必定有深入人心的缺点,效率低下
我们可以看到,磁盘的几个主要部件,扇区、磁道、柱面。
一般的机械硬盘会将文件信息等数据保存在盘面中,而一个硬盘会有很多个盘,每一个盘也都有两个面,而我们所给的是一个俯视图,如果把它竖起来,它将是一个圆柱,而信息在磁盘中是一圈一圈存储的,每一个盘面中都有一个圈,其所组成的就是我们的柱面,这个圈,不单单是一条线,而是一个区域,宽度的不同才产生了不同的信息,我们称之为磁道。而磁道又会被分为好几个区域,叫扇区。
说了这么多磁盘,那必须要来说说冯诺依曼了
磁盘在冯诺依曼结构中,既充当输入设备,也充当输出设备
read ,将数据从磁盘中读入到内存中 --输入设备
write , 将数据从内存中写入到磁盘中 – 输出设备
磁盘是一个经典的外设,效率相比于其它的结构 很低 。
磁盘是一个永久性存储介质,当然也存在掉电易失存储介质–内存。
磁盘是一个块设备,访问的基本单位位扇区(512字节)
大概说了一下磁盘的结构,如果不懂也没关系。
我们不是在讲linux吗?
我们如果把磁盘抽象一下,将其圆形结构展开,也就是把我们的磁道展开,再来看看吧!
我们将磁盘展开后,为了便于管理,我们对磁盘会进行分区,就像将中国分为这么多省份一样,当然每一个分区分多少,分多少个区都是由你或操作系统定的。
分区结束之后,当然就是要对各个分区进行格式化了。
格式化:将管理信息写入各个分区,此处的管理信息是由文件系统所决定的。
而我们linux 所采用的就是EXT2这套文件系统。
文件管理:分区格式化- 相当于一套管理机制 ,格式化就是对所有的分区实施这套机制。
我们可以看到,我们磁盘的最开始一个部分是BOOT Block,这是系统启动的文件,而之后就是我们一个一个分区。
我们现在来看看格式化后各个分区究竟是如何!
我们先来看看inode Table,这个字段存储的是我们文件的inode码,以及所使用的Data blocks区域。
Data blocks字段就是我们存储文件内容的区域,一般我们将一个Data blocks分为很多块,每一块都有相应的编号,而inode Table 中 ,如果该文件使用了某一个block块,就会对其进行标记。
所以我们就可以理解上述 inode Table所存储的inode 编号以及占用的block数组了。
接下来介绍一下inode Bitmap,我们上文提到,inode编号是独一无二的,我们通过比特位标志的方式对其进行标识。
每当有一个文件被创建,就会对其分配新的inode编号,同时将inode Bitmap中的一个位置位1.
其中比特位的位置代表某一个编号,而0或1,代表该编号是否被使用。
而最后的两个,SuperBlock 以及Group Descriptor Table 都是存了inode 谁被使用了,还剩下多少 ,inodetable哪些被占用了,哪些没有了,Date blocks哪些block被占用了,哪些没有。这样的信息,我们不再详谈。
讲了那么多,那我们来提一个问题:
请描述一下,创建一个文件的过程,以及写入1kb数据的过程,再描述一下,删除一个文件是做什么?
至此,我们回答完了问题的一部分。
我们上文提到,有一个文件属性为 硬链接
当然也会存在软链接,我们就先来看看软链接究竟是什么?
软连接:
头脑清晰,先上代码:
ln -s 目标文件名 原文件
此时,我们生成了一份mytest.s 这样的文件,它是mytest的软连接。其实软连接的本质就是:快捷方式
软链接是一个独立的文件,文件内保存的内容是所链接的文件的路径。
其实创建一个软连接,就是创建一个新文件,保存路径。
硬链接:
命令:ln 不带参数 原文件名 目标文件名
我们可以看到,当我们创建了一个文件的硬链接后,它的硬链接数就变为了2.
硬链接的本质:同一个文件的别名,我们从文件的大小就能看出来,这两个文件名,指向的是同一份文件
原始文件与硬链接文件的 inode 是相同的 – 取别名
我们知道,我们touch一个普通的文件,它的硬链接数为1,而一个目录的硬链接数为2.
这是为什么呢?因为我们还记得 相对路径操作符 . 和 … 分别为当前目录和上级目录。也就是说dir这个文件有一个 文件名为 . 的别名,所以硬链接数为2.
而我们可以进入dir目录,此时我们的 … 和上级目录是同名的文件。
硬链接的作用:通过相对路径符进行目录的跳转
此时我们可以回答一下删除一个文件究竟是在做什么了。
两件事:1.在目录中删除对应的文件记录 2.将硬链接数 -1 ,如果硬链接数为0 ,则释放该文件磁盘中的空间。