Linux基础IO
Posted 北川_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux基础IO相关的知识,希望对你有一定的参考价值。
目录
系统文件IO
open
open接口的作用是打开文件。
头文件及参数:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
第一个参数pathname: 要打开或创建的目标文件。
第二个参数flags:代表打开文件的方式,打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
这些选项本质上都叫做标志位,一个整型32位,系统用宏定义的比特位不重叠的二进制序列,用特定一个比特位来表示一种标志。
这也就是为什么当传入两个标志位是要用‘|’按位或。
第三个参数:如果一个文件不存在需要创建它,需要对文件通过mode参数设置它的权限。
open函数打开文件成功返回文件描述符,失败返回-1。
open使用举例:
int fd = open("log.txt", O_WRONLY|O_CREAT, 0644);
write
write函数向文件写入数据。
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
第一个参数是想要写入的文件描述符,第二个参数是想要写入的数据,第三个参数表示想要写入多少个数据。
如果数据写入成功,返回实际写入数据的字节个数,失败返回-1。
read
read从一个文件描述符中读取数据。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
从文件描述符为fd的文件读取count字节的数据到buf中。
读取成功返回实际读取数据的字节个数,失败返回-1。
close
close用来关闭一个文件描述符。
#include <unistd.h>
int close(int fd);
参数传入想要关闭文件的文件描述符。
关闭文件成功返回0,失败返回-1。
文件描述符fd
open函数打开文件成功返回文件描述符,失败返回-1。
下面用open函数打开5个文件,输出它们的文件描述符。
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
int fd1 = open("log1.txt", O_WRONLY|O_APPEND|O_CREAT, 0644);
int fd2 = open("log2.txt", O_WRONLY|O_APPEND|O_CREAT, 0644);
int fd3 = open("log3.txt", O_WRONLY|O_APPEND|O_CREAT, 0644);
int fd4 = open("log4.txt", O_WRONLY|O_APPEND|O_CREAT, 0644);
int fd5 = open("log5.txt", O_WRONLY|O_APPEND|O_CREAT, 0644);
printf("fd1: %d\\n", fd1);
printf("fd2: %d\\n", fd2);
printf("fd3: %d\\n", fd3);
printf("fd4: %d\\n", fd4);
printf("fd5: %d\\n", fd5);
return 0;
结果:
任何C程序,都默认会打开三个“文件”,分别叫做标准输入(stdin),标准输出(stdout),标准错误(stderr),都是FILE* 类型。
在进程控制块tast_struct中有一个files struct* 指针,
该指针指向一个struct files_struct结构体,
files_struct里面有一个指针数组fd_array,数组里里保存着一个个struct file的地址。
struct file里面包括目标文件的基本操作和部分属性。
其中struct file_operations指针指向结构体file_operations,里面包含各种文件操作的函数指针。
所以打开文件的过程,其实就是在fd_array数组中找一个最小的未被使用的位置,把新文件的地址填进去,然后把这个位置的下标返回给进程。用户层看到的fd,本质是系统中维护进程和文件对应关系的数组的下标!
而刚刚打印结果从3开始是因为文件描述符0,1,2是每个进程默认打开的,分别对应的标准输入流(键盘文件),标准输出流(显示器文件),标准错误流(显示器文件)。
系统中分配文件描述符的规则:最小的没有被使用的进行分配。
FILE
在使用c语言打开文件的时候,
FILE* fp = fopen(pathname, "r/w/a");
其中FILE*叫做文件指针,而FILE是一个结构体,
typedef struct _IO_FILE FILE;
这个结构体中封装了文件描述符int _fileno; // 封装的文件描述符
换言之,c语言中用FILE*对文件进行操作,但最终在c的接口里面实际底层IO函数一定都用这个文件描述符调用read或write对文件进行相关的读写操作,所以上层语言和下层系统耦合的时候,用的是结构体里套的一个整型数字。
这个结构体中除了有文件描述符外还有另一个重要的属性就是缓冲区。
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 directly. */
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
;
当我们打印一句话时:
printf("hello world\\n");
它在底层一定会往stdout里面打印,stdout本身有一个文件描述符fd,还有一段缓冲区。当打印helloworld时,这个数据并没有真正向显示器文件中写入,而是直接将数据暂存到了c语言的缓冲区遇到了‘\\n’,或者fflush(stdout)才会通过fd把数据通过进程找到文件描述符,找到对应的文件,将数据写到内核缓冲区然后刷到磁盘。
FILE结构体内部包含两个重要的东西:
1.底层对应的文件描述符下标
2.应用层c语言提供的缓冲区数据
重定向
有一个.c文件ctl_file.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
close(1);
int fd = open("log.txt", O_CREAT|O_WRONLY, 0644);
if(fd < 0)
perror("open");
return 1;
printf("hello world! : %d\\n", fd);
fflush(stdout);
close(fd);
return 0;
把文件描述符为1的显示器文件关闭,打开一个新文件,然后打印一句helloworld。
需要注意的是系统中分配文件描述符的规则是最小的没有被使用的进行分配,所以此时的fd为1。
结果:
结果发现运行ctl_file结果并没有出现要打印的helloworld,而是写到了另一个文件中。
把文件描述符为1的显示器文件关掉了,重新打开一个文件,本来printf应该往显示器上打印,最终发现往文件中打印了,这种策略就叫重定向的原理。
所谓的重定向,本质其实就是把1关闭,本来1指向的是显示器文件,把1关掉再打开个文件,所以1号下标对应的就是普通文件了。在进行这一番操作时,上层看到的stdout里面的fileno永远认为是1,这是每个进程默认的,所以并不是把1改了,而是改了1下标所指向的内容,本来它指向的是显示器文件,现在指向了普通文件,而上层打印依旧会往1里面打印,而此时1号下标指向新文件了,这就叫做输出重定向!。
使用 dup2 系统调用
#include<unistd.h>
int dup2(int oldfd, int newfd);
让newfd成为oldfd的拷贝,这里拷贝的是fd_array数组里面的内容。最终两个数组下标对应的内容都应该和oldfd一致。
dup2举例:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
int fd = open("log.txt", O_CREAT|O_WRONLY, 0644);
if(fd < 0)
perror("open");
return 1;
dup2(fd, 1);
const char* msg = "hello dup2\\n";
for(int i = 0; i < 10; ++i)
write(1, msg, strlen(msg));
close(fd);
return 0;
文件系统
磁盘
磁盘(disk)是指利用磁记录技术存储数据的存储器。磁盘是计算机主要的存储介质,可以存储大量的二进制数据,并且断电后也能保持数据不丢失。早期计算机使用的磁盘是软磁盘(Floppy Disk,简称软盘),如今常用的磁盘是硬磁盘(Hard disk,简称硬盘)。
磁盘和内存交互
内存在OS的角度,使用的时候是有基本单位的,一般是4KB,操作系统把内存想象成一个大数组,数组内每个元素的空间大小就是4KB,这是站在操作系统的角度。而实际上当我们访问内存时,基本单位是1字节。
磁盘存储也有基本单位,叫做扇区。大部分磁盘扇区大小是512字节,也有一些先进的磁盘它的扇区大小是4KB。
内存和磁盘之间是要有交互的,这种交互称为output和input也就是IO。而一般内存和磁盘交互的时候,并不是硬件级别的交互,实际上要通过文件系统完成,也就是通过OS完成。
除了要考虑内存和磁盘的基本单位大小外,还需要考虑IO的基本单位大小。在IO交互的时候,内存和外设之间在文件系统上交互的基本单位一般也是4KB(有些操作系统可能是1KB)。
所以一次想从磁盘上把数据读到内存,如果是文件系统完成的,一次要读8个扇区(扇区大小512字节),读到内存里,然后把内存一页填充满。这就是把数据从磁盘加载到内存的过程。
磁盘存储的基本单位是扇区,同时磁盘读取的最小单元也就是扇区。所以操作系统往磁盘上写数据,最好的方式是,交互的单位是4KB,所以当上层应用层有数据写到内存里,如果把4KB的内容写完了,就可以向所对应的磁盘当中进行8个扇区的刷新。
这就是磁盘与内存的基本交互过程。
磁盘的分区与格式化
操作系统想要管理磁盘,磁盘的圆形结构物理上是圆形的,很难去管理。而且一旦硬件结构变了,那操作系统曾经设计的读写磁盘的逻辑就会发生变化,这样操作系统和磁盘之间属于一种强耦合的关系。所以需要在OS上对磁盘做一层抽象,将磁盘空间抽象成线性结构。
把磁盘看作一个大数组,这样只需要让操作系统知道想要访问磁盘的哪一个下标,其中这种地址称为LBA(Logical block addressing)逻辑块寻址,也就抽象出来的新的逻辑地址。而把空间抽象成这个样子,往对应下标写入时,最终还是要往磁盘上写。操作系统想要往2号地址处直接写数据,最终二号下标也能够对应磁盘当中的某个盘面上某个柱面的某个扇区上,而其中维护这种映射关系,由磁盘驱动来完成。
所以操作系统对磁盘的管理就简化成对这个大数组的管理。
假设磁盘容量400GB,即便是这样它也太大了,不好管理,所以操作系统把磁盘划分成几个分区。
这样操作系统只要管理好一个分区,其他的区域按照同样的方法管理就可以了。 这就是分区的过程,也解释了为什么我们的电脑中会出现C盘、D盘、E盘这样的概念。
这样做仅仅是把空间进行划分了,而要管理好每个分区,还必须要写入相关的管理数据。对每个分区的管理就叫做格式化,所谓的格式化在计算机里面就是写入文件系统,把文件系统的信息直接写入到磁盘的分区当中。文件系统里面核心包括两个东西,一个是数据,一个是方法。也就是说要把数据和对应的方法写道磁盘上。文件系统有自身的属性数据,比如文件系统的类型,一些各种各样的特征,方法对应着操控文件或者其他文件属性的方法。把这些内容写入文件系统后,这样就完成了对文件系统的写入过程。
不同的分区可以用不同的文件系统。
Linux支持多种文件系统:Ext2、Ext3、fs、usb-fs、sysfs、proc
Ext2文件系统的存储方案
即便是把磁盘分区后,每个分区仍然很大。所以在一个分区内,还要对它进行划分。把它分成一个一个的块组。
每个块组都有相同的组成结构,超级块(Super Block)、块组描述符(Group Descriptor Table)、块位图(Block Bitmap)、inode位图(inode Bitmap)、数据区(Data blocks)。
- Super Block: 存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
- Group Descriptor Table: 块组描述符,描述块组属性信息。
- Block Bitmap: Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
- inode Bitmap: 每个bit表示一个inode是否空闲可用。
- inode Table: 存放文件的属性。
- Data blocks: 存放文件内容。
inode
文件=内容+属性,Ext2和Ext3系列文件系统,linux中它们两个保存文件的内容和属性采用分离存储,属性放在inode Table中,而内容放在Data blocks中。其中每个文件都对应一个inode节点。这个文件所有的属性都会写到inode上,包括文件的大小、权限,文件的拥有者所属组等。
在磁盘格式化的时候,写入文件系统之后,一个组可以放多少个inode是一定的。
当我们创建一个空文件时,这个空文件也占空间,并不是它的数据占空间,而是这个文件的属性信息在占空间。需要注意的时inode这个属性集合里面没有文件名。
所有的文件的数据在Data blocks里面。Data blocks相当于一个数据块集合,以4K为单位其中会包括很多很多的数据块,哪些数据块属于哪个文件,最终它的关系维护是由Data blocks数据块和inode产生关联。
以上图为例,
inode里面包含了各种各样小的块,这些块大小是一样的,它里面保存了每个文件的属性信息。
Data blocks里面也划分了若干的数据块,这些数据块每个里面都可以保存若干的数据。
假设在Datablocks里面每个数据块都是有编号的,1,2,3…100,inode本身也是有编号的。
现在要创建一个文件,要申请一个inode,所以申请一号。申请1号inode后这个inode中包括了各种各样的这个文件的属性,而这些属性里面可以理解成它包含了一个数组block b[32] (假如说32个),假设现在需要两个数据块2和3,所以就把这个数组的0下标赋值2,1下标赋值3,此时相当于inode信息指向2号数据块和3号数据块,换句话说一个文件要在磁盘上被查找,我们只需要知道文件的inode号就可以了。所以真正标识文件的不是文件名,而是文件的inode编号。
在Linux中查看文件inode编号:
ls -ali
橙色框框里面为文件的inode编号。
但是我们是如何知道inode编号在哪里呢?
首先程序员是如何定位一个文件呢?
通过路径定位的,比如说通过绝对路径或相对路径,但是不管是绝对还是相对,最终一定要有目录。
那么如何理解目录呢?
目录也一定是文件,所以目录也有自己独立的inode,同样也要有自己的数据块,既然它有自己的inode,所以它也能找到属于自己的数据块,但是目录的数据块它存在的意义是什么呢?
它数据块里面保存的就是文件名和inode的映射关系
所以上图中的有关文件的信息是怎样读到的?
找到当前路径的目录,然后访问目录的数据块,找到文件名和inode的映射关系,然后找到文件的inode,然后再去找到inode对应的属性信息,这样在linux中我们ll的时候就看到了文件的属性信息。
怎么知道inode Table里面有多少个inode已经被使用了,以及哪些已经被使用,哪些没有被使用?
使用位图。
所以在块组中有Block Bitmap标识了是数据块的使用情况,对应的是块位图。inode Bitmap对应的就是inode的使用情况。
实际上当创建一个文件时,并不是一个个遍历的去找哪个inode有没有被使用。只需要在系统启动时,将Block Bitmap和inode Bitmap预加载到系统当中,当要创建文件的时候,就申请一个inode,只需要将inode位图中的一个比特位由0置为1,假如文件数据要8KB空间,那么就申请两个数据块,把Block Bitmap中的两个0置为1,此时这两个数据块也就被占用了。
通过这样的位图的方案,可以快速的完成inode本身的申请和释放,同时也可以快速的确认当前磁盘的使用情况。
总结
1.基本上,一个文件一个inode(包括目录)。
2.inode是一个文件的所有的属性集合。这个集合里面没有文件名,属性也是数据,也要占据空间。
3.真正标识文件的不是文件名,而是文件的inode编号。
4.inode是可以和特定的数据块产生关联的。
5.Linux下属性和内容是分离的,属性inode保存着,内容data blocks保存着。
touch一个空文件,ext*文件系统做了什么工作?
1.首先在inode Bitmap中申请没有被使用的位图,凡是比特位为0的都可以使用。这样有了inode,然后把这个空文件的属性信息写到inode中。
2.把对应申请号的inode编号和文件名,找到当前目录,在目录文件里面更新一条映射关系,叫做文件名和inode的映射关系。
所以这也就是为什么大多数OS在同一个目录下不允许存在同名文件,因为第一不方便人查看,第二因为文件名本身是个索引,是key值。
如何理解删除文件?
删除文件的时候不需要清空该文件占据的所有的空间数据。只需要把inode位图和所占的data位图置为零,然后在所在目录下把曾经的inode编号和文件名的映射关系去掉即可。
软硬链接
软连接
可以用ln -s给文件建立软连接。
可以看到黄色区域两个编号不同,软连接文件就是一个普通的正常文件,有自己独立的inode编号。可以把软连接理解成它就是一个普通文件,有独立的inode,它的数据块里面保存的是它所指向文件的路径。类似于Windows下的快捷方式。
硬链接
使用ln对文件建立硬链接。
可以发现硬链接和它链接的文件的inode都是一样的,硬链接没有自己独立的inode,所以理论上严格意义来讲它不是一个文件,或者只是依附其他文件的文件。
另外还可以发现硬链接成功后file.c的硬链接数变为2,并且file.c和hard_link它们两个的属性完全一样。所以硬链接文件本质是原文件的别名。
软硬链接的区别
1.软连接是一个独立的文件,有独立的inode,而硬链接没有。
2.软连接相当于快捷方式,硬链接本质没有创建文件,只是建立了一个文件名和已有的inode的映射关系并写入当前目录。
文件的三个时间
在Linux中可以使用stat+文件名查看文件信息。
其中包含文件的三个时间信息:
Access: 文件最后被访问的时间
Modify: 文件内容最后的修改时间
Change: 文件属性最后的修改时间
当修改文件内容时,文件的大小一般也会改变,一般情况下Modify的改变会带动Change一起改变,但修改文件属性一般不会影响到文件内容,所以一般情况下Change的改变不会带动Modify改变。
可以使用touch+文件名对文件信息进行更新。当一个文件存在时使用touch命令,此时touch命令的作用变为更新文件信息。
以上是关于Linux基础IO的主要内容,如果未能解决你的问题,请参考以下文章