目录
C语言文件IO
C语言文件IO接口汇总
C语言的操作函数接口
文件的打开形式
若想要打开一个二进制文件,模式字符串中必须包含“b”字符。这个附加的“b”字符可以附加在字符串的末尾(从而形成以下复合模式:“rb”、“wb”、“ab”、“rb+”、“wb+”、“ab+”)
对文件进行写入操作示例:
#include <stdio.h>
int main()
{
FILE* fp = fopen("log.txt","w");
//若文件打开失败,返回空指针
if(fp == NULL)
{
perror("fopen");
return -1;
}
int i = 3;
while(i--)
{
//获取一个字符串
fputs("hello word\n",fp);
}
//关闭文件
fclose(fp);
return 0;
}
程序运行后会对指定文件进行写入,若文件不存在则会在当前路径下生成对应文件
对文件进行读取操作示例:
#include <stdio.h>
int main()
{
FILE* fp = fopen("log.txt","r");
//若文件不存在则打开失败,返回空指针
if(fp == NULL)
{
perror("fopen");
return -1;
}
char buffer[64];
for(int i = 0 ;i < 3 ;i++)
{
//获取一个字符串
fgets(buffer,sizeof(buffer),fp);
printf("%s",buffer);
}
//关闭文件
fclose(fp);
return 0;
}
程序运行后将刚才对文件输入的数据读取并答应出来,若文件不存在则打开失败
注意: 当前路径不是指可执行程序所处的路径,而是指该可执行程序运行成为进程时我们所处的路径
默认打开的输入输出流
C默认会打开三个输入输出流
stdin(标准输入流), stdout(标准输出流), stderr(标准错误流)
标准输入流对应的设备是键盘,而标准输出流和标准错误流对应的设备是显示器
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针
当我们的C程序被运行起来时,操作系统就会默认使用C语言的相关接口将这三个输入输出流打开,之后我们才能调用类似于scanf和printf之类的函数向键盘和显示器进行相应的输入输出操作
系统文件I/O
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问
接口介绍
open
系统接口中使用open函数打开文件,open函数的函数原型如下
int open(const char *pathname, int flags, mode_t mode);
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
以上这三个常量,必须指定一个且只能指定一个
打开文件时,可以传入多个参数选项,当有多个选项传入时,将这些选项用“或”运算符隔开。
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
mode:表示创建文件的默认权限。
返回值:
成功:新打开的文件描述符
失败:-1
我们可以尝试一次打开多个文件
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
umask(0);
int f1 = open("log1.txt",O_RDONLY | O_CREAT ,666);
int f2 = open("log2.txt",O_RDONLY | O_CREAT ,666);
int f3 = open("log3.txt",O_RDONLY | O_CREAT ,666);
int f4 = open("log4.txt",O_RDONLY | O_CREAT ,666);
printf("f1 : %d\n",f1);
printf("f2 : %d\n",f2);
printf("f3 : %d\n",f3);
printf("f4 : %d\n",f4);
return 0;
}
程序运行后可以看到,打开文件的文件描述符是从3开始连续且递增的
当打开一个不存在的文件时
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
int f1 = open("log5.txt",O_RDONLY);
printf("f1 : %d\n",f1);
return 0;
}
运行程序后可以看到,打开文件失败时获取到的文件描述符是-1。
open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件 的默认权限,否则,使用两个参数的open。
write read close 类比上述的C文件相关接口
文件描述符fd
通过对open函数的学习,我们知道了文件描述符就是一个小整数
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0,标准输出1,标准错误2
0,1,2对应的物理设备一般是:键盘,显示器,显示器
而键盘和显示器都属于硬件,属于硬件就意味着操作系统能够识别到,当某一进程创建时,操作系统就会根据键盘、显示器、显示器形成各自的struct file,将这3个struct file连入文件双链表当中,并将这3个struct file的地址分别填入fd_array数组下标为0、1、2的位置,至此就默认打开了标准输入流、标准输出流和标准错误流。
文件描述符就是从0开始的小整数,当我们打开文件时,操作系统在内存中要创建相应的数据结构来对目标文件进行描述,于是就有了file结构体。表示一个已经打开的文件对象,因此,操作系统务必要对这些已经打开的文件进行管理,操作系统会为每个已经打开的文件创建各自的struct file结构体,然后将这些结构体以双链表的形式连接起来,之后操作系统对文件的管理也就变成了对这张双链表的增删查改等操作。所以,本质上,文件描述符就是该数组的下标,所以拿着文件描述符就可以找到对应文件
文件描述符的分配规则
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
umask(0);
int f1 = open("log1.txt",O_RDONLY | O_CREAT ,666);
int f2 = open("log2.txt",O_RDONLY | O_CREAT ,666);
int f3 = open("log3.txt",O_RDONLY | O_CREAT ,666);
int f4 = open("log4.txt",O_RDONLY | O_CREAT ,666);
printf("f1 : %d\n",f1);
printf("f2 : %d\n",f2);
printf("f3 : %d\n",f3);
printf("f4 : %d\n",f4);
return 0;
}
可以看到这五个文件获取到的文件描述符都是从3开始连续递增的,这很好理解,因为文件描述符本质就是数组的下标,而当进程创建时就默认打开了标准输入流、标准输出流和标准错误流,也就是说数组当中下标为0、1、2的位置已经被占用了,所以只能从3开始进行分配
若我们在文件打开前先行关闭0、1、2的其中一个文件会怎么样呢
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
close(0);
int fd = open("log.txt", O_RDONLY);
if(fd < 0){
perror("open");
return -1;
}
printf("fd : %d\n", fd);
close(fd);
return 0;
}
发现是结果是: fd: 0 可见
文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的 最小的一个下标,作为新的文件描述符
重定向
重定向原理
先看代码,若果在文件打开前先关闭1时
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
close(1);
int fd = open("log.txt",O_WRONLY);
if(fd < 0)
{
perror("open");
return -1;
}
printf("hello\n");
printf("hello\n");
printf("hello\n");
printf("hello\n");
printf("hello\n");
printf("hello\n");
fflush(stdout);
close(fd);
return 0;
}
可以看到程序运行后并没有向显示器打印数据,而是将数据都写入到了对应文件里,那是因为我们在打开文件前先关闭了1,当打开文件时按照文件描述符分配规则,fd就为1;
printf函数是默认向stdout输出数据的,而stdout指向的是一个struct FILE类型的结构体,该结构体当中有一个存储文件描述符的变量,而stdout指向的FILE结构体中存储的文件描述符就是1,因此printf实际上就是向文件描述符为1的文件输出数据
dup2
函数原型如下:
#include <unistd.h>
int dup2(int oldfd, int newfd);
函数功能介绍: dup2会将fd_array[oldfd]的内容拷贝到fd_array[newfd]当中,如果有必要的话我们需要先使用关闭文件描述符为newfd的文件。
函数返回值: dup2如果调用成功,返回newfd,否则返回-1。
使用dup2时,我们需要注意以下两点:
- 如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭。
- 如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并返回newfd。
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("log.txt",O_WRONLY);
if(fd < 0)
{
perror("open");
return -1;
}
close(1);
dup(fd,1);
printf("hello\n");
printf("hello\n");
printf("hello\n");
printf("hello\n");
printf("hello\n");
printf("hello\n");
fflush(stdout);
close(fd);
return 0;
}
该代码运行结果与上一个代码一样,没有向显示器打印数据,而是将数据都写入到了对应文件里
理解文件系统
我们使用ls -l的时候看到的除了看到文件名,还看到了文件元数据
每行包含7列:
模式
硬链接数
文件所有者
组
大小
最后修改时间
文件名
磁盘的概念
磁盘上计算机中唯一的一个机械设备,同时它还是一个外设
对磁盘进行读写操作时,一般有以下几个步骤:
- 确定读写信息在磁盘的哪个盘面。
- 由半径确定读写信息在磁盘的哪个磁道。
- 再根据扇区的编号确定读写信息在磁道的哪个扇区。
通过以上三个步骤,最终确定信息在磁盘的读写位置。
磁盘分区
磁盘通常被称为块设备,一般以扇区为单位,一个扇区的大小通常为512字节。我们若以大小为512G的磁盘为例,该磁盘就可被分为十亿多个扇区
操作系统要对磁盘做管理,就将磁盘看做是一个大数组 ,对磁盘的管理,变成了对数组的管理(先描述,再组织)
inode
为了能解释清楚inode我们先简单了解一下文件系统
Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的
Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相 同的结构组成。政府管理各区的例子
超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量, 未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的 时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了
注意:因为Super Block至关重要,所以其他块组当中可能会存在冗余的Super Block,当某一Super Block被破坏后可以通过其他Super Block进行恢复
GDT,Group Descriptor Table:块组描述符,描述块组属性信息
块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没 有被占用 inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
i节点表:存放文件属性 如 文件大小,所有者,最近修改时间等
数据区:存放文件内容
在命令行输入ls -i,即可查看目录下文件的inode编号
文件创建及读写的原理
创建:
- 通过遍历inode位图的方式,找到一个空闲的inode。
- 在inode表当中找到对应的inode,并将文件的属性信息填充进inode结构中。
- 将该文件的文件名和inode指针添加到目录文件的数据块中。
读写:
1.通过文件的inode编号找到对应的inode结构。
2.通过inode结构找到存储该文件内容的数据块,并将数据写入数据块。
3.若不存在数据块或申请的数据块已被写满,则通过遍历块位图的方式找到一个空闲的块号,并在数据区当中找到对应的空闲块,再将数据写入数据块,最后还需要建立数据块和inode结构的对应关系
同理,需要删除文件时,只需要找到文件对应的inode位图和数据块号将其置为无效即可,当然,这种操作并不会真正的删除掉所有信息,在短时间内是可以恢复的,至于为什么时短时间内呢,这是因为该文件对应的inode号和数据块号已经被置为了无效,因此后续创建其他文件或是对其他文件进行写入操作申请inode号和数据块号时,可能会将该置为无效了的inode号和数据块号分配出去,此时删除文件的数据就会被覆盖,也就无法恢复文件了
软硬链接
软链接
我们可以通过以下命令创建一个文件的软连接
ln -s mytest mytest-s
通过ls -i -l
命令我们可以看到,软链接文件的inode号与源文件的inode号是不同的,并且软链接文件的大小比源文件的大小要小得多
软链接是通过名字引用另外一个文件,又叫做符号链接,软链接文件相对于源文件来说是一个独立的文件,该文件有自己的inode号,但是该文件只包含了源文件的路径名,所以软链接文件的大小要比源文件小得多。软链接就类似于Windows操作系统当中的快捷方式
注意: 软链接文件只是其源文件的一个标记,当删除了源文件后,链接文件不能独立存在,虽然仍保留文件名,但却不能执行或是查看软链接的内容了,就像在Windows下删除了源文件后快捷方式也就失效了
硬链接
硬链接没有独立的inode,而是建立了新的文件名和老的inode的映射关系,而硬链接数,本质是一种引用计数,表达的是有多少文件名指向我
我们可以通过以下命令创建一个文件的硬连接
ln mytest mytest-h
可以看到文件的硬链接数有1变为了2
与软连接不同的是,当硬链接的源文件被删除后,硬链接文件仍能正常执行,只是文件的链接数减少了一个,因为此时该文件的文件名少了一个
软硬链接的区别
- 软链接是一个独立的文件,有独立的inode,而硬链接没有独立的inode。
- 软链接相当于快捷方式,硬链接本质没有创建文件,只是建立了一个文件名和已有的inode的映射关系,并写入当前目录。