第二篇: 文件子系统
普天之下,莫非王土;率土之滨,莫非王臣. UNIX之中,莫非文件.
四、文件系统结构
磁盘在使用前, 需要分区和格式化. 格式化操作将在磁盘分区中创建文件系统, 它们将确定文件的存储方式和索引方法, 确定磁盘空间分配和回收算法.
UNIX文件系统的存储由<目录-i节点-数据块>三级构成,其中目录存储了文件的层次结构, 数据块存储了文件的具体信息, i节点是连接文件层次结构与其数据内容的桥梁.
UNIX文件系统将磁盘空间划分为一系列大小相同的块, 划分为引导块、超级块、i节点区和数据区四个部分.
文件系统通过i节点对文件进行控制和管理. 其中, 每个文件对应一个i节点, 每个i节点具有唯一的节点号, 记录了文件的属性和内容在磁盘上的存储位置. 但文件名并不记录在i节点里, 而是存储在目录文件中.
磁盘文件如何存储?
文件系统通过目录记载文件名及其对应的i节点编号, 通过i节点记录文件的信息和内容. 事实上, i节点直接记录的只是文件的属性, 文件的具体内容存储在数据区的数据块中, i节点中仅保留了一个<磁盘地址表>来记录文件内容存储的位置.
<磁盘文件表>由13个块号组成, 每个块号占用4个字节, 代表了数据区中的一个数据块编号.UNIX文件系统采用三级索引结构存储文件,它把<磁盘地址表>分为直接索引地址, 一级索引地址, 二级索引地址和三级索引地址等四个部分. 其中前10项为直接索引地址, 直接指向文件数据所在磁盘快的块号. 第11/12/13项分别为一级/二级/三级索引地址. 一级间接索引的含义在于其存储的并非文件数据所在磁盘块的块号, 而是先指向一个<磁盘块号表>然后再指向具体磁盘块的块号. 同理, 二级/三级间接索引则是先间接指向了两次<磁盘块号表>才指向具体磁盘块的块号.
如果文件系统的数据块大小为1kB, 每个<磁盘块号表>能够记录256个数据项. 那么, 直接索引能管辖10个数据块, 而一级索引能管辖1*256个数据块, 二级索引能管辖1*256*256(65536)个数据块, 三级索引能管辖1*256*256*256(16777216)个数据块.
例题: 大小为56000K的文件,占用多少索引块空间?
答: 因为(10+256) < 56000 < (10+256+65536), 故该文件具有二级间接索引. (56000-10-256)/256=217.7, 则文件需要二级间接索引块为218个,所以总索引块需要1(一级间接索引块)+1(二级间接索引块)+218=220.
磁盘文件读取示例(仿ls命令)
通过stat结构中st_mode判断文件类型
int GetFileType(mode_t st_mode, char *resp){ if(resp == NULL) return 0; if(S_ISDIR(st_mode)) resp[0] = 'd'; // 使用宏定义判断 else if(S_ISCHR(st_mode)) resp[0] = 'c'; else if(S_ISBLK(st_mode)) resp[0] = 'b'; else if(S_ISREG(st_mode)) resp[0] = '-'; else if(S_ISFIFO(st_mode)) resp[0] = 'p'; else if(S_ISLNK(st_mode)) resp[0] = 'l'; else resp[0] = ' ';
return 1; } |
同样,通过st_mode判断文件访问权限
int GetFileMode(mode_t st_mode, char *resp){ if(resp == NULL) return 0; memset(resp, '-', 9); if(st_mode & S_IRUSR) resp[0] = 'r'; // 使用各种宏定义与st_mode做与处理判断 if(st_mode & S_IWUSR) resp[1] = 'w'; if(st_mode & S_IXUSR) resp[2] = 'x'; if(st_mode & S_IRGRP) resp[3] = 'r'; if(st_mode & S_IWGRP) resp[4] = 'w'; if(st_mode & S_IXGRP) resp[5] = 'x'; if(st_mode & S_IROTH) resp[6] = 'r'; if(st_mode & S_IWOTH) resp[7] = 'w'; if(st_mode & S_IXOTH) resp[8] = 'x';
return 9; } |
处理文件其他属性如下
int GetFileOtherAttr(struct stat info, char *resp){ struct tm *mtime;
if(resp == NULL) return 0; mtime = localtime(&info.st_mtime); // 按ls命令显示顺序处理其他属性 return(sprintf(resp, " %3d %6d %6d %11d %04d%02d%02d", info.st_nlink, info.st_uid, / info.st_gid, info.st_size,mtime->tm_year+1900, mtime->tm_mon+1, mtime->tm_mday)); } |
设计类似于UNIX命令<ls -l>的程序lsl, 主程序如下
[bill@billstone Unix_study]$ cat lsl.c #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <time.h>
int GetFileType(mode_t st_mode, char *resp); int GetFileMode(mode_t st_mode, char *resp); int GetFileOtherAttr(struct stat info, char *resp);
int main(int argc, char **argv) { struct stat info; char buf[100], *p = buf;
if(argc != 2){ printf("Usage: lsl filename/n"); return; } memset(buf, 0, sizeof(buf)); if(lstat(argv[1], &info) == 0){ p += GetFileType(info.st_mode, p); p += GetFileMode(info.st_mode, p); p += GetFileOtherAttr(info, p); printf("%s %s/n", buf, argv[1]); } else printf("Open file failed!/n");
return 0; } |
运行结果如下:
[bill@billstone Unix_study]$ make lsl cc lsl.c -o lsl [bill@billstone Unix_study]$ ./lsl Usage: lsl filename [bill@billstone Unix_study]$ ./lsl /etc/passwd -rw-r--r-- 1 0 0 1639 20090328 /etc/passwd [bill@billstone Unix_study]$ ls -l /etc/passwd -rw-r--r-- 1 root root 1639 3月 28 16:38 /etc/passwd |
五 标准文件编程库
在UNIX的应用中, 读写文件是最常见的任务. 标准文件编程库就是操作文件最简单的工具.
标准编程函数库对文件流的输入输出操作非常灵活, 我们既可以采用所见即所得的方式, 以无格式方式读写文件, 又可以对输入输出数据进行转化, 以有格式方式读写文件.
文件的无格式读写
无格式读写分三类: 按字符读写, 按行读写和按块读写.
字符读写函数族:
#include <stdio.h> int getc(FILE *stream); int fgetc(FILE *stream); int putc(int c, FILE *stream); int fputc(int c, FILE *stream); |
函数fgetc的功能类似于getc, 不同的是, 它的执行速度远低于getc.
行读写函数族:
#include <stdio.h> char *gets(char *s); char *fgets(char *s, int n, FILE *stream); int puts(const char *s); int fputs(const char *s, FILE *stream); |
函数fgets中加入了放溢出控制, 应该优先选用. 注意函数fputs把字符串s(不包括结束符'/0')写入文件流stream中, 但不在输出换行符'/n'; 而函数puts则自动输出换行符.
块读写函数族:
#include <stdio.h> size_t fread(void *ptr, size_t size, size_t nitems, FILE *stream); size_t fwrite(const void *ptr, size_t size, size_t nitems, FILE *stream); |
函数fread和fwrite都不返回实际读写的字符个数, 而返回的是实际读写的数据项数.块读写函数常用于保存和恢复内存数据.
文件的格式化读写
文件格式化读写时能够自动转换的数据格式有: 数据类型、精度、宽度、进制和标志等,而其一般格式为
% [标志] [宽度] [.精度] 类型 |
格式化输出函数族
#include <stdio.h> int printf(const char *format, /* [arg,] */ . . .); int fprintf(FILE *stream, const char *format, /* [arg,] */ . . .); int sprintf(char *s, const char *format, /* [arg,] */ . . .); |
在做字符串处理时应该善用sprintf函数.
格式化输入函数族
#include <stdio.h> int scanf(const char format, /* [pointer,] */ . . .); int fscanf(FILE *stream, const char format, /* [pointer,] */ . . .); int sscanf(const char *s, const char format, /* [pointer,] */ . . .); |
二进制读写与文本读写
记得刚开始学习C语言的文件操作时, 这是一个最让我疑惑的问题. 我们都知道在调用fopen函数时需要指定操作类型, 比如说文本写'r'和二进制写'rb'.
那么它们究竟有何区别呢? 这要牵涉到两种存储方式: 以字符为单位存储的文本文件和以二进制数据为单位存储的二进制文件. 举个例子: 我们通常阅读的Readme.txt文件就是文本文件, 该类型文件存储的是一个一个的字符, 这些字符往往是可以打印的; 而我们的可执行程序(比如a.out)则是二进制文件, 该文件是不可读的,需要解析才能识别.
那么在调用fopen函数时该如何选择呢? 如果你是先写入再从另外的地方读出, 那么两种方式都可以; 只要按写入时的方式读取就可以了. 但是, 比起文本方式, 二进制方式在保存信息时有着优势:
a) 加快了程序的执行速度, 提高了软件的执行效率. 内存中存储的都是二进制信息, 直接以二进制方式与文件交互, 可以免除二进制格式与文本格式之间的信息转换过程.
b) 节省了存储空间. 一般来讲, 二进制信息比文件信息占用更少的空间, 比如8位的整型数采用文本方式存储至少需要8字节, 而采用二进制存储只需一个整型即4个字节.
编写变长参数函数
文件的格式化输入输出函数都支持变长参数. 定义时, 变长参数列表通过省略号'...'表示, 因此函数定义格式为:
type 函数名(参数1, 参数2, 参数n, . . .);
UNIX的变长参数通过va_list对象实现, 定义在文件'stdarg.h'中, 变长参数的应用模板如下所示:
#include <stdarg.h>
function(parmN, ...){ va_list pvar; ............................. va_start(pvar, parmN); while() { .................. f = va_arg(pvar, type); .................. } va_end(pvar); } |
va_list数据类型变量pvar访问变长参数列表中的参数. 宏va_start初始化变长参数列表, 根据parmN判断参数列表的起始位置. va_arg获取变长列表中参数的值, type指示参数的类型, 也使宏va_arg返回数值的类型. 宏va_arg执行完毕后自动更新对象pvar, 将其指向下一个参数. va_end关闭对变长参数的访问.
下面给出一个实例mysum, 计算输入参数的和并返回
[bill@billstone Unix_study]$ cat mysum.c #include <stdarg.h>
int mysum(int i, ...){ // 参数列表中, 第一个参数指示累加数的个数 int r = 0, j = 0; va_list pvar;
va_start(pvar, i); for(j=0;j<i;j++){ r += va_arg(pvar, int); } va_end(pvar);
return(r); }
int main() { printf("sum(1,4) = %d/n", mysum(1,4)); printf("sum(2,4,8) = %d/n", mysum(2,4,8));
return 0; } [bill@billstone Unix_study]$ make mysum cc mysum.c -o mysum [bill@billstone Unix_study]$ ./mysum sum(1,4) = 4 sum(2,4,8) = 12 [bill@billstone Unix_study]$ |