腾讯一面:内存满了,会发生什么?
704
2022-05-28
@TOC
零、前言
本章主要讲解学习Linux基础IO流的知识
一、C语言文件IO
1、C库函数介绍
具体详解博文: 文件操作超详解CSDN博客
打关文件fopen/fclose:
FILE * fopen(const char* filename, const char* mode); int fclose (FILE* stream );
文件打开方式:
读写函数fread/fwrite:
size_t fread( void *buffer, size_t size, size_t count, FILE *stream ); size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
格式化读写fscanf/fprintf:
int fscanf( FILE *stream, const char *format [, argument ]... ); int fprintf( FILE *stream, const char *format [, argument ]...);
示例1:输出使用
#include
示例2:文件读写
#include
结果:
2、stdin/stdout/stderr
文件原型:
extern FILE *stdin; extern FILE *stdout; extern FILE *stderr;
概念:
任何C程序运行都会默认打开三个输入输出流,分别是:stdin, stdout, stderr
分三个文件流分别对应键盘文件,显示器文件,显示器文件
为什么这里的文件流和外设关联上了:
对于所有外设硬件来说,其本质对应的操作不外乎是读操作和写操作,对于不同外设也就有不同的读写方式
OS要管理硬件设备无非是先描述再组织,由此将属性以及读写操作构成一个结构体,而文件其本身也是属性加读写操作,这样就由文件结构体同一管理文件(包括外设)
在C语言中虽然没有多态,但是结构体中可以储存函数指针,初始化结构体时,将属性写入的同时也将对应的读写函数给写入;对于外设来说,通过对应的文件结构体使用函数指针调用对应的读写函数,也就将数据刷新到对于设备上/从设备上读取数据
由此将普通文件和硬件设备管理组织好,所以对于Linux来说:一切皆文件
为什么C语言默认打开这三个输入输出流:
不仅仅是C语言会默认打开这三个输入输出流文件,几乎是任何语言都会这样,而这就不仅仅是语言层面上的功能了,也是由操作系统所支持的
对于任何语言来说,都有输入输出的需求,而不打开这三个输入出输出流文件,则无法使用这些接口
二、系统文件IO
1、系统调用介绍
操作文件,除了上述C接口(当然C++也有接口,其他语言也有),还可以使用系统接口
open接口:
#include
参数解释:
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags
mode_t:如果没有对应文件需要进行创建的话,就需要指定创建文件的八进制访问权限值
注:这里的参数选项是依靠不同的比特位来标识对应的功能设定,所以这里的异或操作就是将对应比特位置为1,同时函数也是通过对每个比特位进行与操作检查是否该比特位置为了1
原型示例:
#define O_RDONLY 00 #define O_WRONLY 01 #define O_RDWR 02 #define O_CREAT 0100
注:open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open
其他接口:
int close(int fd); //使用close函数时传入需要关闭文件的文件描述符fd即可,若关闭文件成功则返回0,若关闭文件失败则返回-1 ssize_t write(int fd, const void *buf, size_t count); //使用write函数,将buf位置开始向后count字节的数据写入文件描述符为fd的文件当中 //如果数据写入成功,实际写入数据的字节个数被返回;如果数据写入失败,-1被返回 ssize_t read(int fd, void *buf, size_t count); //使用read函数,从文件描述符为fd的文件读取count字节的数据到buf位置当中 //如果数据读取成功,实际读取数据的字节个数被返回;如果数据读取失败,-1被返回
示例:文件读写
#include
结果:
2、系统调用和库函数
概念:
对于上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc);而 open close read write lseek 都属于系统提供的接口,称之为系统调用接口
对于系统调用来说,接近底层,使用成本较高,并且不具备可移植性,只在本系统下可以,其他系统不行
对于库函数来说,是在系统暴露的接口上的一个二次开发(最终调用系统调用),在兼容自己语法的特性的同时,具有可移植性(自动根据平台选择自己底层对应的接口)
即可以认为库函数是对系统调用的封装,减低人工学习成本,方便二次开发
示图:
三、文件描述符
1、open返回值
文件描述符fd:
文件描述符就是一个小整数
0 & 1 & 2:
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2
示例1:
#include
结果:
示例2:
#include
结果:
注:从示例中可见,文件描述符就是从0开始的小整数:默认打开0,1,2,再打开则是从后递增
分析:
当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件
于是就有了file结构体,表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。
每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针
所以本质上,文件描述符就是该数组的下标。只要拿着文件描述符,就可以通过PCB到file_struct的指针数组找到对应的文件结构体地址
示图:
2、fd分配规则
文件描述符分配规则:
在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符
示例1:
#include
结果:输出3
示例2:
#include
结果:关闭0输出0,关闭2输出2
四、重定向
1、概念及演示
Linux 中标准的输入设备默认指的是键盘,标准的输出设备默认指的是显示器
输入/输出重定向:
输入重定向:指的是重新指定设备来代替键盘作为新的输入设备
输出重定向:指的是重新指定设备来代替显示器作为新的输出设备
注:通常是用文件或命令的执行结果来代替键盘作为新的输入设备,而新的输出设备通常指的就是文件
常用重定向:
示例:
#include
结果:
注:本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中fd=1,这种现象叫做输出重定向
重定向本质:
从上述示例来看,输出重定向是将进程中的文件指针数组中的标准输出stdout文件给关闭(并非真正关闭,而是将指针数组对应下标的内容置空),再将新打开文件分配到标准输出文件对应的下标上,再输出时,系统不知道文件已经替换,依旧输出到stdout文件对应的数组下标为1的文件上,但是此时文件已经被替换了
示图:
2、dup2系统调用
函数原型:
#include
示例:
#include
结果:
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、缓冲区和刷新策略
示例:
#include
结果:
解释:
这里明明将输出结果重定向到文件myfile中,但是myfile文件并没有内容,与上面示例的区别是在文件关闭之前并没有将结果给强制刷新
对于文件结构体来说,里面除了读写方法外,还存在着缓冲区,再正式刷新到磁盘上对应的文件之前,数据先是由文件缓冲区保存着
对于标准输出的刷新策略是行缓冲,当遇到\n时触发刷新机制,对于普通文件来说则是全缓冲,当缓冲满时就进行刷新,而强制刷新以及进程结束刷新对两者都有效
这里输出重定向之后指针数组对应的原标准输出文件的替换成了普通文件,数据写到对应文件缓冲区里,同时对应刷新策略也改变成全缓冲,关闭文件之前没有强制刷新,则数据也就没写到对应磁盘上的文件里
刷新策略:
无缓冲:无缓冲的意思是说,直接对数据进行操作,无需经过数据缓冲区缓存,系统调用接口采用此方式
行缓存:缓冲区的数据每满一行即对数据进行操作,而通常情况下向屏幕打印数据就是行缓存方式
全缓冲:缓冲区的数据满时才对数据进行操作,通常向文件中写数据使用的就是全缓冲方式
五、文件及文件系统
1、FILE
概念:
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的,所以C库当中的FILE结构体内部,必定封装了fd
示例:
#include
运行出结果:
hello printf hello fwrite hello write
输出重定向结果: ./hello > file
hello write hello printf hello fwrite hello printf hello fwrite
区别:这里 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结构体:
//在/usr/include/stdio.h typedef struct _IO_FILE FILE; //在/usr/include/libio.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 di rectly. */ 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 };
2、文件系统
命令 ls -l 查看文件信息:
[root@localhost linux]# ls -l 总用量 12 -rwxr-xr-x. 1 root root 7438 "9月 13 14:56" a.out -rw-r--r--. 1 root root 654 "9月 13 14:56" test.c
每行包含7列:模式;硬链接数;文件所有者;组;大小;最后修改时间 ;文件名
命令 stat 查看文件信息:
[root@localhost linux]# stat test.c File: "test.c" Size: 654 Blocks: 8 IO Block: 4096 普通文件 Device: 802h/2050d Inode: 263715 Links: 1 Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root) Access: 2017-09-13 14:56:57.059012947 +0800 Modify: 2017-09-13 14:56:40.067012944 +0800 Change: 2017-09-13 14:56:40.069012948 +0800
注意:
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命令将配置文件更新
Linux
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。