目录
1.C语言文件回顾
2.系统文件I/O
2.1 系统接口介绍
2.2 文件描述符fd
2.3 重定向
2.4 理解缓冲区
2.5 理解文件系统
1.C语言文件回顾
在学习系统文件的操作之前,还记得C语言是如何进行对文件的操作的吗?下面看C语言接口:
FILE *fopen(const char *path, const char *mode);
fopen()的功能为打开文件,该文件的名称是path指向的字符串,并将流与之关联。参数:mode:
返回值:成功,则返回一个FILE指针,失败返回NULL
读文件:函数fread()从stream指向的流中读取nmemb个数据元素,每个元素size字节长,并将它们存储在ptr给定的位置
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
写文件:函数fwrite()将从ptr给定的位置获得的nmemb个数据元素(每个大小字节长)写入stream所指向的流。size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
返回值:成功后,fread()和fwrite()返回读取或写入的items数。此数字等于仅当size为1时传输的字节数。如果发生错误,或者到达文件末尾,则返回值为短项目计数(或零)。
- C默认会打开三个输入输出流,分别是stdin, stdout, stderr
- 仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针
2.系统文件I/O
2.1 系统接口介绍
#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: 要打开或创建的目标文件flag:要打开或创建的目标文件返回值:成功返回一个文件描述符 失败返回-1O_RDONLY: 只读打开O_WRONLY: 只写打开O_RDWR : 读,写打开这三个常量,必须指定一个且只能指定一个O_CREAT : 若文件不存在,则创建它。需要使用mode 选项,来指明新文件的访问权限O_APPEND: 追加写
O_TRUNC:覆盖式的写文件
mode:设置文件的权限,否则文件权限为乱码 权限:mode&~umsk
int close(int fd); 关闭一个文件描述符fd返回值:成功返回0,失败返回-1
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count); 从文件描述符fd中读取最多count字节的数据到buf中
返回值:成功返回读取到的字节数,如果该数字少于申请的字节数,不存在错误;失败返回-1。
ssize_t write(int fd, const void *buf, size_t count);
描述:从指向的缓冲区buf向文件描述符fd引用的文件写入最多count个字节。
返回值:成功返回写入的字节数,如果count为零并且fd引用的是一个常规文件,可能会检测到错误,write()可能会返回失败状态。如果没有检测到错误,将返回0,而不会造成任何其他影响。失败返回-1。
2.2 文件描述符fd
文件描述符就是一个小整数
- Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2
- 0,1,2对应的物理设备一般是:键盘,显示器,显示器。这三个文件描述符也是可以用close关闭的
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
char buf[1024];
ssize_t s = read(0, buf, sizeof(buf));
if(s > 0){
buf[s] = 0;
write(1, buf, strlen(buf));
write(2, buf, strlen(buf));
}
return 0;
}
上述代码是从文件描述符0中也就是键盘读入数据,再分别写到文件描述符1,2中。
那么我们如何理解文件呢?
被打开的文件要被OS管理,采用先描述再组织的方式,OS为文件创建对应的内核数据结构标识文件struct file{},每个文件对应一个file,链式结构存储;这个结构体中包含文件的大部分属性。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件 描述符,就可以找到对应的文件。一般我们自己打开的文件描述符是从3开始的。
2.3 重定向
若我们关闭文件描述符1,再代开一个新的文件时,文件描述符1会被分配给新的文件,那么此时,本应该输出到屏幕上的内容,被输出到我们新打开的文件中,这种现象就叫做输出重定向。
重定向的本质:上层用的fd不变,在内核中更改fd对应的structural file*的地址
#include <unistd.h>
int dup2(int oldfd, int newfd);
使newfd成为oldfd的副本,必要时可以先关闭newfd
返回值:成功返回新的文件描述符,失败返回-1.
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#define FILENAME "log.txt"
int main()
{
int fd = open("out.txt", O_CREAT | O_TRUNC | O_RDWR, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
close(1);
dup2(fd, 1);
while(1)
{
char buf[1024];
ssize_t s = read(0, buf, sizeof(buf)-1);
if(s < 0)
{
perror("read");
break;
}
else{
printf("%s", buf);
fflush(stdout);
}
}
return 0;
}
上述这段代码是,向打开一个文件,并关闭标准输出文件描述符1。将其重定向到fd中,循环的从键盘读入数据,本该输出到屏幕的数据,经过重定向后,被输出到文件描述符1所链接的文件out.txt中。结果如下:
常见的重定向有:>, >>, <
> : 输入重定向 >> : 追加重定向 < : 输出重定向
执行程序替换时,并不会影响曾经进程打开的重定向文件,只是将代码和数据替换,不会影响内核数据结构。
2.4 理解缓冲区
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。所以C库当中的FILE结构体内部,必定封装了fd.
缓冲区:本质就是一块内存 意义:节省进程进行数据IO的时间
拷贝:fwrite、write,可以理解为将数据从进程拷贝到"缓冲区"或者外设中。
缓冲区刷新策略的问题:
- 立即刷新 -- 无缓冲
- 行刷新 -- 行缓冲 显示器
- 缓冲区满 -- 全缓冲 磁盘文件
- 特殊情况:用户强制刷新 -- fflush(); 进程退出,一般都会刷新缓冲区
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}
结果如下:
我们将结果从定向到文件file.txt中时,会发现:库函数都输出了两次,而write只输出了一次,为什么呢?结果肯定是与fork()有关的!
出现上述现象的原因:
- 没有重定向">"前,看到三条消息,是因为stdout默认使用行刷新。由于每一条数据的结尾都为"\n",在fork()之前,三条C函数已经将数据刷新到显示器(外设)上,FILE结构体内部已经不存在对应的数据了。
- 进行重定向">"后,写入文件不再是显示器,而是普通文件,采用的刷新策略为全缓冲(缓冲区满/进程退出/手动刷新时才会进行缓冲区刷新),三条C显示函数虽然带了"\n",但是缓冲区并未满,数据不会进行刷新。执行fork时,stdout属于父进程,创建子进程后,紧接着就是进程退出,无论谁先退出,都会刷新缓冲区。刷新缓冲区就是修改,会发生写时拷贝,复制一份一摸一样的缓冲区给子进程,父子进程退出时都会刷新缓冲区,出现两份打印数据。
- write没有打印两次是因为:上面的过程与write无关,write没有FILE“,而是fd,没有C语言提供的缓冲区。
2.5 理解文件系统
磁盘上的大量文件,也是需要被静态管理起来的,方便我们随时打开。在堆磁盘文件进行管理时,要将其分区再进一步分组,每个分组有自己的BlockGroup。如下图:
- Super Block:保存整个文件系统的信息。记录的信息主要有:bolck 和 inode的总量, 未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的 时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个 文件系统结构就被破坏了。
- inode table:保存了分组内部所有的可用(已使用+未使用)的inode,存放文件属性 如 文件大 小,所有者,最近修改时间等。每个bit表示一个inode是否空闲可用。
- data block:保存分组内部所以普文件的数据块
- inode Bitmap:inode对应的位图结构,位图中比特位的位置和当前文件对应的inode的位置是 一一对应的。
- block Bitmap:数据块对应的数据结构,位图中比特位的位置和当前dataBlcok对应的数据块 位置是一一对应的。
- GDT:块组描述表:对应分组的宏观的属性信息
可以通过命令 ls -i 文件名 来查看文件的inode编号。
inode中存在一个block[]数组,元素为data block的编号。通过这种映射关系来通过inode编号找到所对应的文件内容数据。
创建一个新文件主要有一下4个操作:
- 1. 存储属性--内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。
- 2. 存储数据--该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。
- 3. 记录分配情况--文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
- 4. 添加文件名到目录
目录文件也是像上面这种方式设计的。
删除文件则只需要设置inode bitmap即可。