九、文件
1、文件系统的物理结构
1)硬盘的物理结构:驱动臂、盘片、主轴、磁头、控制器
2)磁表面存储器的读写原理
硬盘片的表面覆盖着薄薄的磁性涂层,涂层中含有无数微小的磁性颗粒,谓之磁畴。相邻的若干磁畴组成一个磁性存储元,以其剩磁的极性表示二进制数字0和1。为磁头的写线圈中施加脉冲电流,可把一位二进制数组转换为磁性存储元的剩磁极性。利用磁电变换,通过磁头的读线圈,可将磁性存储元的剩磁极性转换为相应的电信号,表示二进制数。
3)磁道和扇区
磁盘旋转,磁头固定,每个磁头都会在盘片表面画出一个圆形轨迹。改变磁头位置,可以形成若干大小不等的同心圆,这些同心圆就叫做磁道(Track)。每张盘片的每个表面上都有成千上万个磁道。一个磁道,以512字节为单位,分成若干个区域,其中的每个区域就叫做一个扇区(Sector)。扇区是文件存储的基本单位。
4)柱面、柱面组、分区和磁盘驱动器
硬盘中,不同盘片相同半径的磁道所组成的圆柱称为柱面(Cylinder)。整个硬盘的柱面数与每张盘片的磁道数相等。
硬盘上的每个字节需要通过以下参数定位:
磁头号:确定哪个盘面
柱面号:确定哪个磁道 > 磁盘I/O
扇区号:确定哪个区域 /
偏移量:确定扇区内的位置
若干个连续的柱面构成一个柱面组
若干个连续的柱面组构成一个分区
每个分区都建有独立的文件系统
若干分区构成一个磁盘驱动器
2、文件系统的逻辑结构
磁盘驱动器:| 分区 | 分区 | 分区 |
分区:| 引导块 | 超级块 | 柱面组 | 柱面组 | 柱面组 |
柱面组:
| 引导块 | 柱面组 | i节点映 | 块位图 | i节点表 | 数据块集 |
| 副 本 | 信 息 | 射 表 | | | |
i节点号:431479
i节点
文件元数据
100 | 200 | 300
根据目录文件中记录的i节点编号检索i节点映射表,获得i节点下标,用该下标查i节点表,获得i节点,i节点中包含了数据块索引表,利用数据块索引从数据块集中读取数据块,即获得文件数据。
直接块:存储文件实际数据内容
间接块:存储下级文件数据块索引表
100
-----
xxx
200
-----
xxx
300
----
400 | 500 | 600
3、文件分类
使用: ls -l 查看的第一位表示的就是文件的内容
普通文件(-):可执行程序、文本、图片、音频、视频、网页
目录文件(d):该目录中每个硬链接名和i节点号的对应表
符号链接文件(l):存放目标文件的路径 ln -s 软连接命令
管道文件§:有名管道,进程间通信
套接字文件(s):进程间通信
块设备文件(b):按块寻址,顺序或随机读写
字符设备文件©:按字节寻址,只能以字节为单位顺序读写
4、文件的打开与关闭
打开:在系统内核中建立一套数据结构,用于访问文件
进程表项
…
文件描述符表
|文件描述符标志 | 文件表项指针 | 0
|文件描述符标志 | 文件表项指针 | 1
|文件描述符标志 | 文件表项指针 | 2
… | ^
±-------------------------------------+ |
|
| 文件描述符
v
文件表项
文件状态标志
文件读写位置
v节点指针
… |
±----+
|
v
v节点
i节点内容
…
关闭:释放打开文件过程中建立的数据结构
FILE* fp = fopen(“reame.txt”, “r”);
fread(fp, …);
#include <fcntl.h>
打开已有的文件或创建新文件
int open(const char* pathname, int flags,mode_t mode);
成功返回文件描述符,失败返回-1。
pathname - 文件路径
flags - 状态标志,可取以下值:
O_RDONLY - 只读 \
O_WRONLY - 只写 > 只选其一
O_RDWR - 读写 /
O_APPEND - 追加
O_CREAT - 创建,不存在即创建,已存在即打开,除非与以下两个标志之一合用,由此标志mode参数才有效。
O_EXCL - 排它,已存在即失败
O_TRUNC - 清空,已存在即清空,同时有O_WRONLY或O_RDWR
O_SYNC - 写同步,在数据被写到磁盘之前写操作不会完成,读操作本来就是同步的,此标志对读操作没有意义
O_ASYNC - 异步,在文件可读写时产生一个SIGIO信号,在对信号的处理过程中读写I/O就绪的文件,只能用于终端设备或网络套接字,而不能用于磁盘文件
O_NONBLOCK - 非阻塞,读操作不会因为无数据可读而阻塞,写操作也不会因为缓冲区满而阻塞,相反会返回失败,并设置特定的errno
mode - 权限模式,三位八进制:0XXX
__/ | _ / |
拥有者用户 同组用户 其它用户
4: 可读
2: 可写
1: 可执行
所创建文件的实际权限除了跟mode参数有关,还受权限掩码的影响。
mode=0666
umask=0002
权限=mode&~umask=0664
创建新文件
int creat(const char* pathname, mode_t mode);
flags: O_WRONLY | O_CREAT | O_TRUNC
打开已有文件
int open(const char* pathname, int flags);
关闭文件
int close(int fd);
成功返回0,失败返回-1。
fd - 文件描述符
代码:open.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main(void) {
int fd1 = open("open.txt", O_RDWR | O_CREAT |
O_TRUNC, 0666);
if (fd1 ```== ```-1) {
perror("open");
return -1;
}
printf("fd1 = %d\n", fd1);
int fd2 = open("open.txt", O_RDONLY);
if (fd2 == -1) {
perror("open");
return -1;
}
printf("fd2 = %d\n", fd2);
close(fd2);
close(fd1);
return 0;
}
作为文件描述符表项在文件描述符表中的下标,合法的文件描述符一定是大于或等于0的整数。每次产生新的文件描述符表项,系统总是从下标0开始在文件描述符表中寻找最小的未使用项。每关闭一个文件描述符,无论被其索引的文件表项和v节点是否被删除,与之对应的文件描述符表项一定会被标记为未使用,并在后续操作中为新的文件描述符所占用。系统内核缺省为每个进程打开三个文件描述符:
#include <unistd.h>
#define STDIN_FILENO 0 // 标准输入,即键盘
#define STDOUT_FILENO 1 // 标准输出,终端屏幕,有缓冲
#define STDERR_FILENO 2 // 标准错误,终端屏幕,无缓冲
UC C C++
标准输入 0 stdin cin
标准输出 1 stdout cout
标准错误 2 stderr cerr
插播:文件重定向
./可执行文件 0<i.txt 1>o.txt 2>e.txt
将标准输入重定向到i.txt 将标准输出重定向到o.txt 将标准错误重定向到e.txt文件。scanf (标准输入) printf(标准输出) perror(标准错误)
文件重定向 代码实现 redir.cpp
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
void redir(void)
{
close(STDIN_FILENO);//关闭标准输入
creat("i.txt",0664);
close(STDOUT_FILENO);//关闭标准输出
creat("o.txt",0664);
close(STDERR_FILENO);//关闭标准错误输出
creat("e.txt",0664);
}
void resume(void) {
close(STDIN_FILENO);
stdin = fopen("/dev/tty", "r");//键盘
close(STDOUT_FILENO);
stdout = fopen("/dev/tty", "w");//显示器
close(STDERR_FILENO);
stderr = fopen("/dev/tty", "w");//显示器
setbuf(stderr, NULL);//标准错误不带缓冲
}
void doio(void) {
int x, y;
// 读标准输入
scanf("%d%d", &x, &y);
// 写标准输出
printf("%d+%d=%d\n", x, y, x + y);
fflush(stdout);//刷新缓冲区
malloc(0xFFFFFFFF);
// 写标准错误
perror("malloc");
}
int main(void) {
redir();
doio();
//恢复
resume();
doio();
return 0;
}
数据类型 int FILE* iostream
文件描述符是用户程序和系统内核关于文件的唯一联系方式。
5、文件的读取和写入
向指定文件写入字节流
ssize_t write(int fd, const void* buf, size_t count);
成功返回实际写入的字节数(0表示未写入),失败返回-1。
fd - 文件描述符
buf - 内存缓冲区
count - 期望写入的字节数
ssize_t read(int fd, void* buf, size_t count);
成功返回实际读取的字节数(0表示读到文件尾),失败返回-1。
fd - 文件描述符
buf - 内存缓冲区
count - 期望读取的字节数
代码:write.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main(void) {
int fd = open("write.txt", O_WRONLY |
O_CREAT | O_TRUNC, 0644);
if (fd ```== ```-1) {
perror("open");
return -1;
}
const char* text = "Hello, World!";
printf("写入内容:%s\n", text);
size_t towrite = strlen(text) * sizeof(
text[0]);
ssize_t written = write(fd, text,
towrite);
if (written == -1) {
perror("write");
return -1;
}
printf("期望写入%u字节,实际写入%d字节。\n",
towrite, written);
close(fd);
return 0;
}
代码:read.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main(void) {
int fd = open("read.txt", O_RDONLY);
if (fd```== ```-1) {
perror("open");
return -1;
}
char text[256];
size_t toread = sizeof(text) - sizeof(
text[0]);
ssize_t readed = read(fd, text, toread);
if (readed == -1) {
perror("read");
return -1;
}
printf("期望读取%u字节,实际读取%d字节。\n",
toread, readed);
text[readed / sizeof(text[0])] = '\0';
printf("读取内容:%s\n", text);
close(fd);
return 0;
}
基于系统调用的文件读写本来就是面向二进制字节流的,因此对二进制读写而言,无需做任何额外的工作。如果要求文件中内容必须是可阅读(人)的,那么就必须通过格式化和文本解析处理二进制形式的数据和文本字符串之间的转换。
代码:binary.c
以二进制方式进行读写
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main(void) {
int fd = open("binary.dat", O_WRONLY |
O_CREAT | O_TRUNC, 0644);//binary.dat二进制文件
if (fd ```== ```-1) {
perror("open");
return -1;
}
char name[256] = "张飞";
if (write(fd, name, sizeof(name)) ```== ```-1) {
perror("write");
return -1;
}
unsigned int age = 38;
if (write(fd, &age, sizeof(age)) ```== ```-1) {
perror("write");
return -1;
}
double salary = 20000;
if (write(fd, &salary,
sizeof(salary)) ```==```-1) {
perror("write");
return -1;
}
struct Employee {
char name[256];
unsigned int age;
double salary;
} employee = {
"赵云", 25, 10000};
if (write(fd, &employee, sizeof(
employee)) ```==```-1) {
perror("write");
return -1;
}
close(fd);
//写完之后关闭,然后开始从文件中读
if ((fd = open("binary.dat",
O_RDONLY)) ```==```-1) {
perror("open");
return -1;
}
if (read(fd, name, sizeof(name)) ```==```-1) {
perror("read");
return -1;
}
printf("姓名:%s\n", name);
if (read(fd, &age, sizeof(age)) ```==```-1) {
perror("read");
return -1;
}
printf("年龄:%u\n", age);
if (read(fd, &salary, sizeof(
salary)) ```==```-1) {
perror("read");
return -1;
}
printf("工资:%g\n", salary);
if (read(fd, &employee, sizeof(
employee)) == -1) {
perror("read");
return -1;
}
printf("员工:%s, %u, %g\n", employee.name,
employee.age, employee.salary);
close(fd);
return 0;
}
代码:text.c
转换为人可阅读的文件
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main(void) {
int fd = open("text.txt", O_WRONLY |
O_CREAT | O_TRUNC, 0644);
if (fd ```== ```- 1) {
perror("open");
return -1;
}
char name[256] = "张飞";
unsigned int age = 38;
double salary = 20000;
char buf[1024];
//写之前将其转换为字符
sprintf(buf, "%s %u %.2lf\n", name, age,
salary);
if (write(fd, buf, strlen(buf) * sizeof(
buf[0])) ```== ```-1) {
perror("write");
return -1;
}
struct Employee {
char name[256];
unsigned int age;
double salary;
} employee = {
"赵云", 25, 10000};
sprintf(buf, "%s %u %.2lf\n", employee.name,
employee.age, employee.salary);
if (write(fd, buf, strlen(buf) * sizeof(
buf[0]))```==```-1) {
perror("write");
return -1;
}
close(fd);
//写完开始读
if ((fd = open("text.txt",
O_RDONLY)) ```==```-1) {
perror("open");
return -1;
}
memset(buf, 0, sizeof(buf));
if (read(fd, buf, sizeof(buf) - sizeof(
buf[0])) ```==```-1) {
perror("read");
return -1;
}
sscanf(buf, "%s%u%lf%s%u%lf", name, &age,
&salary, employee.name, &employee.age,
&employee.salary);
printf("姓名:%s\n", name);
printf("年龄:%u\n", age);
printf("工资: %g\n", salary);
printf("员工:%s, %u, %g\n", employee.name,
employee.age, employee.salary);
close(fd);
return 0;
}
插播sprintf():
C 库函数 int sprintf(char *str, const char *format, …) 发送格式化输出到 str 所指向的字符串。
sscanf():
C 库函数 int sscanf(const char *str, const char *format, …) 从字符串读取格式化输入。
6、顺序与随机读写
ABCdef
^ ^ ^
0 3 6
每个打开的文件都有一个与其相关的文件读写位置保存在文件表项中,用以记录从文件头开始计算的字节偏移。文件读写位置通常是一个非负的整数,用off_t类型表示,在32位系统上被定义为long int,而在64位系统上则被定义为long long int。打开一个文件时,除非指定了O_APPEND标志,否则文件读写位置一律被设为0,即文件首字节的位置。每一次读写操作都从当前的文件读写位置开始,并根据所读写的字节数,同步增加文件读写位置,为下一次读写做好准备。因为文件读写位置是保存在文件表项而不是v节点中的,因此通过多次打开同一个文件得到多个文件描述符,各自拥有各自的文件读写位置。
人为调整文件读写位置
off_t lseek(int fd, off_t offset, int whence);
成功返回调整后的文件读写位置,失败返回-1。
fd - 文件描述符
offset - 文件读写位置相对于whence参数的偏移量
whence
SEEK_SET - 从文件开始
SEEK_CUR - 从当前位置开始
SEEK_END - 从文件尾开始
lseek函数仅仅是修改文件表项中的文件读写位置,并不引发实际的I/O操作,速度很快。
lseek(fd, 10, SEEK_SET);
lseek(fd, -10, SEEK_END);
lseek(fd, 0, SEEK_CUR); // 返回当前读写位置
lseek(fd, 0, SEEK_END); // 返回文件总字节数
lseek(fd, -10, SEEK_SET); // 错误
lseek(fd, 10, SEEK_END); // 允许,空洞部分补0
代码:seek.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main(void) {
int fd = open("seek.txt", O_RDWR | O_CREAT |
O_TRUNC, 0644);
if (fd ```==```-1) {
perror("open");
return -1;
}
const char* text = "Hello, World!";
if (write(fd, text, strlen(text) * sizeof(
text[0])) ```==```-1) {
perror("write");
return -1;
}
if (lseek(fd, -6, SEEK_CUR)```==```-1) {
perror("lseek");
return -1;
}
off_t pos = lseek(fd, 0, SEEK_CUR);
if (pos ```==```-1) {
perror("lseek");
return -1;
}
printf("当前读写位置:%ld\n", pos)