1.基础IO
(1)文件操作
在C语言中,用来进行文件操作的函数有很多,比如:
所谓文件操作,简而言之就是通过语言层面向系统层面进行函数调用,命令操作系统为在磁盘上为其创建文件,那么这些函数的使用如下:
返回值为FILE*,是一个结构体指针,此结构体保存的就是刚刚进行完文件操作后针对文件属性的一个结构体指针。
而以fopen函数为例,可以看到其中一个名为mode的参数,参数是const char*,也就是不可更改的字符串类型,那么在这里代表了要对文件进行什么操作,比如"r"为读(read),"w"为写(write),并且是覆盖式地写,并不会保存原来的数据,"a"为在保存原来数据的基础上进行追加(append)。
那么除了fopen,fclose,对文件进行写操作后,还要考虑如何将其读出来,所以针对文件读取也有很多函数,比如:
可以发现这些函数基本上都有FILE* stream这个参数,那么上文提到过,FILE*是一个关于存储文件属性的结构体,而stream,学过C++的朋友不难理解,stream就是“流”的意思,也就是说向这些函数中传入FILE* 类型的流参数,而这个参数所指向的就是刚刚的文件,并且可以从这些文件中读取字符,字符串等。
(2)stdin、stdout、stderr
了解过C语言的文件函数后,我们又要提出一个疑问,我们在进行上述对文件的写入操作后,数据确实被写入了文件中,然后通过fgets函数将字符串获取到了buffer中,最后打印到屏幕上,那么文件中的数据和屏幕上的数据,有什么分别?屏幕这个外设为例,以及其他外设,是否也和文件一样具有某种相同属性呢?
在编写C语言程序时候会默认打开三个IO流,stdin、stdout、stderr,分别代表标准输入,标准输出和标准错误,C++也有相同的流,那么即cin、cout、cerr,那么其中标准输入是通过键盘向“文件”中写入数据,而标准输出和标准错误都是通过屏幕将数据打印出。
可以看到,这三个标准流的类型都是我们刚刚看到过的FILE*,通过刚刚的介绍相信不难理解,这三个流,肯定是和文件有关系的,所以我们可以更加坚信刚刚的理论,我们所使用的键盘、屏幕、鼠标等等外设,都可以和文件放到一起考虑!
所以可以得出结论:操作系统中,一切皆文件!
那么,我们回过头来看一开始的文件函数,既然fputs是向文件中放数据,那么如果屏幕是文件的话,可以直接将fputs的第二个参数改为stdout吗?
我们可以看到,将fputs函数的第二函数,也就是输出目标文件从刚刚的fp改为stdout和stderr,数据成功地被打印到了屏幕上,所以我们的猜测是正确的,一切皆文件!使用文件操作,把目标从文件换为外设,数据依然可以正常被打印。
stdout和stderr都是在屏幕上打印,所以字符串都被打印出来了,但是可以注意到使用'>'重定向到log.txt时却只有stdout可以,为什么?因为'>'重定向全名叫做输出重定向,也就是只把stdout的内容重定向到文件中,这里的输出重定向并不包括错误重定向。
所以我们调用的一切函数,都会从语言层面跨越到操作系统层面,最终都是访问硬件:显示器、键盘、文件(磁盘),所以OS就是硬件的管理者。
(3)文件的系统调用接口
那么之于Linux操作系统,和C语言不同的是,它也有自己独有的系统调用接口,使用这些接口,可以进行系统编程,直接对系统进行操作。
与C语言中 fopen对标的就是open函数,同为打开文件的函数,并且由于Linux底层就是C语言完成的,所以在参数方面可以发现和fopen差不多,第一个也是文件的路径,第二个是对文件的操作,第三个是文件的权限,返回值:成功>0的数,<0则表示失败。
如果open不写mode_t的参数的话,系统默认为新创建的文件设置的权限是乱的。
那么其中的O_WRONLY | O_CREATE又是什么意思呢?wronly代表的就是write only,也就是只写,后面的create代表如果这个文件在打开时没有被创建,则创建之,而这两个关键字在底层可以理解为定义好的宏,将这些关键字的二进制分别设置不同的位置,并且对它们进行按位或,然后在OS内部再将传入的flags与每个标志位进行按位与,就能得到它们哪个位置设置了1,就能知道要如何操作该文件了。
那么针对返回值,我们可以发现系统调用的接口返回值都是int,大于0肯定是成功,那么这个返回值的作用难道只是简单的代表文件创建成功与否吗?并不是,在Linux中,新创建或者打开的文件,都会有一个专门的fd标识了其唯一的身份,而open、close等函数的返回值,就是通过该fd打开、关闭某个文件。
那么我们不妨做个实验,只是随机打开一个文件,看一下fd会是多少:
我们知道,Linux的底层使用C语言写的,而一看到数字,我们就会非常敏感的将其和数据结合起来,上文中提到了每个C程序都会默认打开三个标准流,那么这三个标准流,就是隐藏起来的0 1 2,所以我们也就知道为什么新创建的文件下标会从3开始了。
文件描述符fd的分配规则即:未使用过的,从小到大的使用。 所以如果我们继续创建几个文件呢?fd会如何变化?
那么我们知道,所有的文件操作,其实本质的上都是一个进程对某个文件执行响应的操作,要想操作文件,首要的就是先打开文件,了解电脑简单运行原理的朋友肯定清楚,当一个进程被打开时,首先就需要将其数据从磁盘拿到内存,cpu再从内存中对其进行处理,那么其中的“数据”是什么呢?我们刚刚提过,操作系统中一切皆文件,也就是说,系统层面无论硬件软件其属性,底层都有着相似的结构,所以这里的数据也就可以理解成:该文件的相关属性。那么就又有一个疑问了,我们平时如果创建了一个空文件,即使里面没有数据,那么该文件就一定是空的吗?并不是,因为操作系统在创建该文件时会给它在磁盘中申请空间储存其相关的文件属性,这个属性就包括了文件创建的时间,文件的权限,文件的描述符等等。
所以我们可以理解,系统运行程序,本质就是对文件的操作——将文件打开并将其数据和属性加载到内存,那么既然如此的话,操作系统中是不是就有着大量的文件呢?是的,操作系统和文件的关系,就是1:n的关系,操作系统打开大量的文件并不是稀奇事,那么如何管理就是一大关键问题了,那么如何管理呢:先描述,再组织。
先描述,也就是先将待管理的文件针对其属性创建响应的数据结构struct file,此结构体中就包括了打开的文件的相关属性,和链接属性,描述完毕,再按照此结构体进行管理。
当然Linux针对系统函数的读写操作也有相对应的函数,分别名为write和read,其使用方法和C语言的接口非常类似,无非就是从哪里读,都多少,读到哪里,向哪里写,写什么内容,写多少数据。
2.一切皆文件
刚刚我们首次提到了一切皆文件的概念,即我们所熟悉的外设,磁盘本质上都和类似.txt文本文件有着相似的结构,其操作在操作系统内核看来一般无二, 那么,虽然一切皆文件这个概念很大一统,但是我们必须知道,比如键盘、显示器、磁盘、显卡等外设,它们即使是文件,但是和我们常见的文本文件还是有着本质的区别的。
很类似C++中的多态,我们知道在多态中,子类在继承父类后,重写了父类的方法,那么在使用父类的指针调用这两个方法时,传入的是子类,完成的就是子类的操作,反之则是父类的。
3.重定向
我们刚刚提到了输出重定向,即向文件输入的数据变为向屏幕上打印,在有了一切皆文件这个概念后我们就不难理解了,屏幕和我们创建的log.txt本质上都是文件,都有其相对应的文件描述符。
那么重定向就只有输出重定向吗?既然重定向是将本该写入某个文件的数据该为写入另一个文件,那么可以不写入屏幕吗?换言之,可以将输出重定向改为文件和文件之间的重定向吗?
我们知道,stdout代表了标准输出,其文件描述符默认设置为1,如果我们将这个文件描述符使用系统调用接口close关闭掉,同样是文件,结果是肯定是会成功的,那么关闭后,使用open函数打开一个文件,我们知道,系统会为该文件分配新的文件描述符,而分配的规则就是从小到大找未被使用过的,也就是刚刚关闭的1,之后再执行printf函数,该数据将会向哪里打印呢?
我们可以发现,本该打印到屏幕的数据被打印进了log.txt文件,而log.txt的文件描述符我们刚刚关闭的1,所以这更加加深了我们对一切皆文件的理解,即使'>'操作名为输出重定向,但是其本质就是文件和文件之间的重定向,因为一切皆文件。
除了输出重定向,在Linux中还有其他的重定向方式,本质都是文件和文件之间对于字符串的操作。
那么在Linux的系统接口中,针对重定向也有专门的函数:
通过介绍不难发现,此接口的功能就是针对老的文件创建一份其拷贝作为新文件,并且以后的数据将重定向到此,第二个参数依旧是对于该文件的操作。
那么我们可以针对上述的操作,使用系统接口dup来实现输出重定向:
使用write函数也可以完成相同的功能:
但是此时我们发现当查看重定向后的log.txt时,只有标准输出打印出来了,为什么标准错误没有打印呢?标准输出和标准错误难道不是同样是向屏幕打印吗?为什么一起重定向后产生了不同的结果呢?其本质还是'>'只是输出重定向,并不会把标准错误的信息重定向到文件中,那么如果我们想实现此操作,只需加一条指令:
此操作就是将标准输出和标准错误两个文件同时向一个log.txt文件中进行重定向,在逻辑层面可以理解为此指令将stdout、stderr之前的执行流进行了更改,同时更改进了log.txt。
4.缓冲区
缓冲区——在我们的理解层面,是一块区域,作用是做某些缓冲的,为了不使某些操作太拥挤,目的就是为了给我们想实现的某些操作更多的安全性。
那么之于操作系统,也有其相应的缓冲区,那么这个缓冲区究竟是干嘛的?首先我们先看一段代码:
我们知道,在关闭了1号文件描述符后,open函数打开的文件将会自动使用1号描述符,从而模拟了重定向操作,而1号描述符恰好就是stdout,所以此操作也叫做输出重定向,所以我们将此进程连续执行三次后,由于文件的操作加上了追加操作,所以三次操作后重定向后的文件log.txt里面有三条消息,这并不难理解,那么再来看一段代码:
当加上了close(fd)后,为什么本该重定向进log.txt的数据没有了呢?为什么去掉close(fd)后就有呢?为了解决此疑问,正式引出缓冲区的概念:
当我们调用了C语言接口后,比如一条简单的printf函数,在用户层使用了此函数,那么接下来,这个函数会向下调用,通过系统调用接口调用操作系统的接口,将待打印的数据从C语言缓冲区写入系统缓冲区,因为一切硬件都是由操作系统管理,所以屏幕也不例外,所以最后由操作系统来调用系统接口向屏幕上打印数据。而在这个过程中,是一定需要fd的!也就是文件描述符。
而上述的重定向操作,系统的标准输出流分别指向了不同的文件,而不同文件,针对数据有着不同的刷新策略,从哪里刷新,就是从缓冲区中,采取哪种刷新策略,代表了不同种类的文件。
所以,log.txt中没有数据的原因是;先关闭了close(1),又创建了fd打开了log.txt进行了输出重定向,现在stdot指向的是log.txt,将数据全部写入log.txt后,由于显示器是行缓冲,文件是全缓冲,而缓冲区又没满,所以数据一直在log.txt中没有被刷新到显示器而这时如果关闭了fd,就关闭了1,相当于文件刷新到显示器的桥梁就断了,所以数据一直在内核缓冲区中,如果想看到数据,可以在close(fd)前加上fflush(fd),此函数是刷新缓冲区,数据会立即刷新到显示器上。
所以在语言层面,会有专门的缓冲区,在调用print函数时,数据会先写入语言缓冲区,并且调用语言的print函数时在底层系统会自动调用系统接口,所以数据会从语言的缓冲区写入内核缓冲区,最终打印到显示器。
所以这也是为什么每次在写完打印函数后都要加一个'\n'字符,就是因为在语言层面,无论是C还是C++,都有自己的缓冲区,而它们的刷新策略都是行刷新,这也就是为什么'\n'叫做换行符。
5.文件系统
我们之前讲到,创建一个文件时,无论是否向文件中写入了数据,在磁盘上都会为这个文件开辟空间,在底层都会有其对应的结构体对其进行管理,所以我们可以理解为文件 = 文件属性 + 文件内容。
而文件名在系统层是没有意义的,因为操作系统区分文件的方法并不是靠文件名,而是通过inode编号,一个文件一个inode。
在计算机中,磁盘是计算机的硬件机械设备,其作用就是用来保存各式各样的文件,那么首先将磁盘分区,再将文件系统写入,也就是通常所说的格式化,这样一来,磁盘就被线性式地划分好了,而每个分区,就用来存储不同的文件。
每个磁盘都可以理解为分成了Block group 0~Block group n,以一个Block group为例,对文件来说最重要的就是Inode Bitmap,Inode Table,Data Blocks。
Data Blocks用来存文件的内容,用一个个小空间存。
Inode Table用来存储文件的属性,也是类似于Data Blocks的划分,只不过空间更小,用来存一个个结构体。
Inode Bitmap用来存inode Table中哪些用过了哪些没用过,这样存文件就能节省时间,不用遍历。
因为Linux内核是由C语言编写,所以这些属性,在底层中,用结构体来存储再合适不过了,所以每个文件在底层,操作系统都会为其分配结构体,struct file,其中存储对应的属性值。
以这样的方式管理好一块分区后,再以同样的方式去管理其他分区,由小到大管理好整个磁盘。
而目录也是文件,在操作系统中,一切皆文件,所谓目录就是路径,在底层可以理解为二叉树的形式,目录中存放的就是文件名和inode编号的映射关系。
6.软硬链接
我们在Linux中,使用 [ln -s 源文件名 目标文件名] 的方式,可以将某个文件指定成另一个文件的软连接。
除了两个文件指定软链接外,还可以指定一个文件为另一个文件夹下某个文件的软链接,例如我们某个文件夹下有个可执行程序,使用软链接就可以迅速找到它,相当于windows中的快捷方式。
而软链接有自己独立的inode,有inode就说明软链接在底层也是文件, 也有自己的属性,自己的数据块,可能保存的就是链接所指向的文件所在路径和文件名。
而与之相对的硬链接,和软链接就有本质的区别,它不再是一个文件,在底层中并未对其分配inode,本质并不是一个独立的文件,而是一个文件名和inode编号的映射关系,因为没有独立的inode,即也没有属性,数据块等。
所以创建硬链接的本质是在特定的目录下填写一对文件名和inode的映射关系。
我们可以发现,软链接对应的两个文件的inode是不同的,因为是两个文件,而硬链接是相同的,说明硬链接在链接后,还是相同的文件,可以理解为起别名。
file.txt刚创建出来时,只有一个硬链接,代表file.txt和inode只有一个映射关系,创建file_hard文件并且和file.txt产生硬链接后,就有了两组映射关系,其中struct inode中可能也有一个int ref,用来记录指向文件的数目,如果多来一个指向,ref++,也就是引用计数。
而这里的. 和..代表上级目录和上上级目录,为什么.的硬链接也是2呢?我们上面说过,目录也是文件,也有其对应的inode,所以它有着和文件相同的属性,这里的硬链接,除了它本身之外,我们也可以通过它打开的文件找到它,以它路径下被打开的文件的角度来看,也有一条硬链接,以bin目录和其目录下的log.txt为例,bin的inode,和log.txt中的上级目录的inode是相同的,所以是两个硬链接数。