Linux 系统 - 基础 IO
@TOC
零、前言
本章主要讲解学习 Linux 基础 IO 流的知识
一、C 语言文件 IO
1、C 库函数介绍
具体详解博文: 文件操作超详解CSDN博客
打关文件fopen/fclose:
文件打开方式:
读写函数 fread/fwrite:
格式化读写 fscanf/fprintf:
示例 1:输出使用
示例 2:文件读写
结果:
2、stdin/stdout/stderr
文件原型:
概念:
任何 C 程序运行都会默认打开三个输入输出流,分别是:stdin, stdout, stderr
分三个文件流分别对应键盘文件,显示器文件,显示器文件
为什么这里的文件流和外设关联上了:
对于所有外设硬件来说,其本质对应的操作不外乎是读操作和写操作,对于不同外设也就有不同的读写方式
OS 要管理硬件设备无非是先描述再组织,由此将属性以及读写操作构成一个结构体,而文件其本身也是属性加读写操作,这样就由文件结构体同一管理文件(包括外设)
在 C 语言中虽然没有多态,但是结构体中可以储存函数指针,初始化结构体时,将属性写入的同时也将对应的读写函数给写入;对于外设来说,通过对应的文件结构体使用函数指针调用对应的读写函数,也就将数据刷新到对于设备上/从设备上读取数据
由此将普通文件和硬件设备管理组织好,所以对于 Linux 来说:一切皆文件
为什么 C 语言默认打开这三个输入输出流:
不仅仅是 C 语言会默认打开这三个输入输出流文件,几乎是任何语言都会这样,而这就不仅仅是语言层面上的功能了,也是由操作系统所支持的
对于任何语言来说,都有输入输出的需求,而不打开这三个输入出输出流文件,则无法使用这些接口
二、系统文件 IO
1、系统调用介绍
操作文件,除了上述 C 接口(当然 C++也有接口,其他语言也有),还可以使用系统接口
open 接口:
参数解释:
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成 flags
mode_t:如果没有对应文件需要进行创建的话,就需要指定创建文件的八进制访问权限值
注:这里的参数选项是依靠不同的比特位来标识对应的功能设定,所以这里的异或操作就是将对应比特位置为 1,同时函数也是通过对每个比特位进行与操作检查是否该比特位置为了 1
原型示例:
注:open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要 open 创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的 open
其他接口:
示例:文件读写
结果:
2、系统调用和库函数
概念:
对于上面的 fopen fclose fread fwrite 都是 C 标准库当中的函数,我们称之为库函数(libc);而 open close read write lseek 都属于系统提供的接口,称之为系统调用接口
对于系统调用来说,接近底层,使用成本较高,并且不具备可移植性,只在本系统下可以,其他系统不行
对于库函数来说,是在系统暴露的接口上的一个二次开发(最终调用系统调用),在兼容自己语法的特性的同时,具有可移植性(自动根据平台选择自己底层对应的接口)
即可以认为库函数是对系统调用的封装,减低人工学习成本,方便二次开发
示图:
三、文件描述符
1、open 返回值
文件描述符 fd:
文件描述符就是一个小整数
0 & 1 & 2:
Linux 进程默认情况下会有 3 个缺省打开的文件描述符,分别是标准输入 0, 标准输出 1, 标准错误 2
示例 1:
结果:
示例 2:
结果:
注:从示例中可见,文件描述符就是从 0 开始的小整数:默认打开 0,1,2,再打开则是从后递增
分析:
当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件
于是就有了 file 结构体,表示一个已经打开的文件对象。而进程执行 open 系统调用,所以必须让进程和文件关联起来。
每个进程都有一个指针*files, 指向一张表 files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针
所以本质上,文件描述符就是该数组的下标。只要拿着文件描述符,就可以通过 PCB 到 file_struct 的指针数组找到对应的文件结构体地址
示图:
2、fd 分配规则
文件描述符分配规则:
在 files_struct 数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符
示例 1:
结果:输出 3
示例 2:
结果:关闭 0 输出 0,关闭 2 输出 2
四、重定向
1、概念及演示
Linux 中标准的输入设备默认指的是键盘,标准的输出设备默认指的是显示器
输入/输出重定向:
输入重定向:指的是重新指定设备来代替键盘作为新的输入设备
输出重定向:指的是重新指定设备来代替显示器作为新的输出设备
注:通常是用文件或命令的执行结果来代替键盘作为新的输入设备,而新的输出设备通常指的就是文件
常用重定向:
示例:
结果:
注:本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中 fd=1,这种现象叫做输出重定向
重定向本质:
从上述示例来看,输出重定向是将进程中的文件指针数组中的标准输出 stdout 文件给关闭(并非真正关闭,而是将指针数组对应下标的内容置空),再将新打开文件分配到标准输出文件对应的下标上,再输出时,系统不知道文件已经替换,依旧输出到 stdout 文件对应的数组下标为 1 的文件上,但是此时文件已经被替换了
示图:
2、dup2 系统调用
函数原型:
示例:
结果:
3、重定向的原理
注:重定向与程序替换是可以同时进行,重定向改变的是进程 PCB 中的文件指针数组中的文件地址信息,而程序替换则是触发写时拷贝将进程地址空间的代码和数据进行替换,这之间没有影响
输出重定向示例:命令 cat test.c > myfile
系统创建子进程 exec 替换程序执行 cat test.c 命令之前,先将标准输出文件关闭,并打开 myfile 文件(如果不存在则创建,对应的 open 选项则是 O_WRONLY|O_CREAT)
追加重定向示例:命令 cat test.c >> myfile
这里大致和输出重定向一样,只不过 open 的选项改为 O_APPEND|O_CREAT
输入重定向示例:命令 mycmd > test.c
系统创建子进程 exec 替换程序执行 test.c 命令之前,先将标准输入文件关闭,并打开 mycmd 文件(对应的 open 选项则是 O_RDONLY)
4、缓冲区和刷新策略
示例:
结果:
解释:
这里明明将输出结果重定向到文件 myfile 中,但是 myfile 文件并没有内容,与上面示例的区别是在文件关闭之前并没有将结果给强制刷新
对于文件结构体来说,里面除了读写方法外,还存在着缓冲区,再正式刷新到磁盘上对应的文件之前,数据先是由文件缓冲区保存着
对于标准输出的刷新策略是行缓冲,当遇到\n 时触发刷新机制,对于普通文件来说则是全缓冲,当缓冲满时就进行刷新,而强制刷新以及进程结束刷新对两者都有效
这里输出重定向之后指针数组对应的原标准输出文件的替换成了普通文件,数据写到对应文件缓冲区里,同时对应刷新策略也改变成全缓冲,关闭文件之前没有强制刷新,则数据也就没写到对应磁盘上的文件里
刷新策略:
无缓冲:无缓冲的意思是说,直接对数据进行操作,无需经过数据缓冲区缓存,系统调用接口采用此方式
行缓存:缓冲区的数据每满一行即对数据进行操作,而通常情况下向屏幕打印数据就是行缓存方式
全缓冲:缓冲区的数据满时才对数据进行操作,通常向文件中写数据使用的就是全缓冲方式
五、文件及文件系统
1、FILE
概念:
因为 IO 相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过 fd 访问的,所以 C 库当中的 FILE 结构体内部,必定封装了 fd
示例:
运行出结果:
输出重定向结果: ./hello > file
区别:这里 printf 和 fwrite (库函数)都输出了 2 次,而 write 只输出了一次(系统调用),而这与就和 fork 有关
解释:
printf fwrite 库函数是 C 语言上的函数,这些库函数在实现输出时必定通过调用 C 语言的文件 IO 函数实现。C 语言文件 IO 函数的返回类型是 FILE*,这里的 FILE 是 C 语言上的文件结构体,其中为了实现语言与系统层面的相连,FILE 结构体里也存在着_fileno(对应 fd)以及用户层面的缓冲区,所以库函数输出数据是先输出到 FILE 文件结构体里的缓冲区
如果是直接运行,即没有发生输出重定向时,向显示屏文件的刷新机制是行缓冲(遇到\n 则刷新),即立即将缓冲数据给刷新,fork 之后没有什么作用
当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲(普通文件是全缓冲的,缓冲满则刷新),即 FILE 中缓冲区存有数据,当 fork 之后,子进程会与父进程代码共享,数据各有一份(刷新就是写入,发生写时拷贝),程序结束退出时强制刷新数据,所以库函数调用的都输出了两次
write 为系统接口无缓冲机制,就直接将数据刷新
注意:
OS 内核区实际上也是有缓冲区的,当我们刷新用户缓冲区的数据时,并不是直接将用户缓冲区的数据刷新到磁盘或是显示器上,而是先将数据刷新到操作系统缓冲区,然后再由操作系统将数据刷新到磁盘或是显示器上
操作系统也有自己的刷新机制,这样的分用户层面和内核层面的缓冲区,便于用户层面与内核层面进行解耦
FILE 结构体:
2、文件系统
命令 ls -l 查看文件信息:
每行包含 7 列:模式;硬链接数;文件所有者;组;大小;最后修改时间 ;文件名
命令 stat 查看文件信息:
注意:
Access 最近访问文件时间(不会立即刷新,访问是一个比较频繁的行为,立即刷新则会减缓效率)
Modify:最近修改文件的时间(主要是文件的内容,立即更新)
Change:最近修改文件属性的时间(修改文件内容可能会造成文件属性的修改,立即更新)
如何读取文件信息:
通过读取存储在磁盘上的文件信息,然后显示出来
示图:
文件系统概念:
对于文件操作来说,我们操作的都是在内存打开的文件,而大多数文件都是未打开的文件并且储存在磁盘上,而对于磁盘上的文件 OS 也需要进行管理,由此就需要文件系统
示图:
确定磁盘的读写文件:
确定读写信息在磁盘的哪个盘面/柱面/扇区,但是这样的方式并不便于移植,由此我们将磁盘抽象成数组,数组的下标是单调递增不重复的数字,可以直接确定要读写的文件
分区管理:
磁盘分区就是使用分区编辑器在磁盘上划分几个逻辑部分,盘片一旦划分成数个分区,不同的目录与文件就可以存储进不同的分区,分区越多,就可以将文件的性质区分得越细,按照更为细分的性质,存储在不同的地方以管理文件
磁盘是典型的块设备,硬盘分区被划分为一个个 block,一个 block 的大小是由格式化的时候确定的,并且不可以更改
如何进行管理:
示图:
说明:
Boot Block:该区域磁盘文件的驱动文件,如果驱动损坏,那么则无法进行读取对应区域的文件信息及数据
Block Group:ext2 文件系统会根据分区的大小划分为数个 Block Group,而每个 Block Group 都有着相同的结构组成
Super Block:存放文件系统本身的结构信息,记录的信息主要有:bolck 和 inode 的总量,未使用的 block 和 inode 的数量,一个 block 和 inode 的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block 的信息被破坏,可以说整个文件系统结构就被破坏了
Group Descriptor Table:块组描述符,描述块组属性信息,整体 group 的空间使用信息,以及其他信息
Block Bitmap:Block Bitmap 中记录着 Data Block 中哪个数据块已经被占用,哪个数据块没有被占用
inode Bitmap: inode 位图当中记录着每个 inode 是否空闲可用
inode Table:存放文件属性,即每个文件的 inode,每个文件对应一个 inode,而 inode 才是标识文件的唯一方式
Data Blocks:存放 inode 对应的文件数据
注:其他块组当中可能会存在冗余的 Super Block,当某一 Super Block 被破坏后可以通过其他 Super Block 进行恢复;磁盘分区并格式化后,每个分区的 inode 个数就确定了
如何理解创建一个文件:
通过遍历 inode 位图的方式,找到一个空闲的 inode,在 inode 表当中找到对应的 inode,并将文件的属性信息填充进 inode 结构中,并将该文件的文件名和 inode 的映射关系添加到目录文件的数据块中,如果写入内容,需要通过 Block Bitmap 找到闲置的数据块,将数据写入数据块,并将映射关系写到 inode 结构中
如何理解对文件写入信息:
通过目录文件中的数据块找到文件名及其 inode 的映射,再找到对应的 inode 结构,再通过 inode 结构找到存储该文件内容的数据块,并将数据写入数据块;若不存在数据块或申请的数据块已被写满,则通过遍历块位图的方式找到一个空闲的块号,并在数据区当中找到对应的空闲块,再将数据写入数据块,最后还需要建立数据块和 inode 结构的对应关系
如何理解删除一个文件:
将该文件对应的 inode 在 inode 位图当中置为无效,将该文件申请过的数据块在块位图当中置为无效,并不真正将文件对应的信息删除,而只是将其 inode 号和数据块号置为了无效,所以当我们删除文件后短时间内是可以恢复的,如果再次创建文件及数据,可能将对应的数据块给覆盖,原来的数据也就没有了
如何理解目录:
目录也是文件,有自己的属性信息,目录的 inode 结构当中存储的就是目录的属性信息,比如目录的大小、目录的拥有者等;目录也有自己的内容,目录的数据块当中存储的就是该目录下的文件名以及对应文件的 inode 指针
注: 每个文件的文件名并没有存储在自己的 inode 结构当中,而是存储在该文件所处目录文件的文件内容当中。计算机只关注文件的 inode 号,只有用户才关注,用户需要看到文件名,所以将文件名和文件的 inode 指针存储在其目录文件的文件内容当中后,目录通过文件名和文件的 inode 指针即可将文件名和文件内容及其属性连接起来
3、软硬链接
软链接概念:
软链接又叫做符号链接,软链接文件相对于源文件来说是一个独立的文件,该文件有自己的 inode 号,但是该文件只包含了源文件的路径名,所以软链接文件的大小要比源文件小得多。软链接就类似于 Windows 操作系统当中的快捷方式
但是软链接文件只是其源文件的一个标记,当删除了源文件后,链接文件不能独立存在,虽然仍保留文件名,但却不能执行或是查看软链接的内容了
命令 ln -s 创建软连接:
硬链接概念:
硬链接文件就是源文件的一个别名,一个文件有几个文件名,该文件的硬链接数就是几
当硬链接的源文件被删除后,硬链接文件仍能正常执行,只是文件的链接数减少了一个
使用命令 ln 创建硬连接:
注:硬链接文件的 inode 号与源文件的 inode 号是相同的,并且硬链接文件的大小与源文件的大小也是相同的,特别注意的是,当创建了一个硬链接文件后,该硬链接文件和源文件的硬链接数都变成了 2
为什么创建的目录的硬链接数是 2:
创建一个普通文件,该普通文件的硬链接数是 1,因为此时该文件只有一个文件名。而目录创建后,该目录下默认会有两个隐含文件.和..,它们分别代表当前目录和上级目录,因此这里创建的目录有两个名字,一个是 dir 另一个就是该目录下的.,所以刚创建的目录硬链接数是 2
示图:
注:通过命令我们也可以看到 dir 和该目录下的.的 inode 号是一样的,也就可以说明它们代表的实际上是同一个文件
软硬链接的区别:
软链接是一个独立的文件,有独立的 inode,而硬链接没有独立的 inode
软链接相当于快捷方式,硬链接本质没有创建文件,只是建立了一个文件名和已有的 inode 的映射关系,并写入当前目录
六、动静态库
概念:
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码
一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间,缺点是一旦库缺失,所以依赖的程序都不可运行
静态库链接方式生成的可执行程序体积比较大,因为他会将库里面的代码拷贝至可执行程序,缺点是程序的体积比较大,浪费系统空间资源,但是如果库缺失不影响程序运行
示例:
注:编译时默认是动态编译,加上-static 选项则是静态编译
库文件名称和引入库的名称:
如:libc.so -> c 库,去掉前缀 lib,去掉后缀.so,.a
1、制作使用静态库
示例:
注意:
制作静态库指令:ar -rc
ar 是 gnu 归档工具;rc 表示(replace and create)
查看静态库中的目录列表: ar -tv libmymath.a
t:列出静态库中的文件;v:verbose 详细信息
指定链接静态库:gcc main.c -L. -lmymath
-L 指定库路径;-l 指定库名
注:测试目标文件生成后,静态库删掉,程序照样可以运行
库搜索路径:
从左到右搜索-L 指定的目录
由环境变量指定的目录 (LIBRARY_PATH)
由系统指定的目录(/usr/lib;/usr/local/lib)
2、制作使用动态库
示例:
注意:
生成动态库选项:
shared: 表示生成共享库格式;fPIC:产生位置无关码(position independent code)
动态库是文件,先从磁盘加载到内存上的共享区,并与进程的程序地址空间建立映射关系,由此映射的位置不能影响到进程就需要 fPIC
编译选项:
-I:指定头文件搜索路径;-L:指定库文件搜索路径;-l:指明需要链接库文件路径下的哪一个库
运行动态库方法:
拷贝动态库.so 文件到系统共享库路径下, 一般指/usr/lib
添加库路径到 LD_LIBRARY_PATH
ldconfig 配置/etc/ld.so.conf.d/,ldconfig 更新
具体操作:首先将库文件所在目录的路径存入一个以.conf 为后缀的文件当中;然后将该.conf 文件拷贝到/etc/ld.so.conf.d/目录下;使用 ldconfig 命令将配置文件更新
版权声明: 本文为 InfoQ 作者【可口也可樂】的原创文章。
原文链接:【http://xie.infoq.cn/article/0d9e3d872ffaa3c0f9fdb29bc】。文章转载请联系作者。
评论