目录
1.2 块组描述符表(GDT,Group Descriptor Table)
5 link/unlink/symlink/readlink函数
一、ext2文件系统
ext2文件系统中,一个分区由一个引导块(Boot Block)和多个块组(Block Group)组成。其中Boot Block大小固定为1k,用来存储磁盘分区信息和启动信息,任何文件系统都不能使用启动块。启动块之后才是ext2文件系统的开始,ext2文件系统将整个分区划成若干个同样大小的块组(Block Group),每个块组都由超级块、块组描述符表、块位图、inode位图、inode表、数据块组成。
1 ext2文件系统块组
1.1 超级块(Super Block)
描述整个分区的文件系统信息,例如块大小、文件系统版本号、上次mount的时间等等。超级块在每个块组的开头都有一份拷贝。
1.2 块组描述符表(GDT,Group Descriptor Table)
由很多块组描述符组成,整个分区分成多少个块组就对应有多少个块组描述符。每个块组描述符(Group Descriptor)存储一个块组的描述信息,例如在这个块组中从哪里开始是inode表,从哪里开始是数据块,空闲的inode和数据块还有多少个等等。和超级块类似,块组描述符表在每个块组的开头也都有一份拷贝,这些信息是非常重要的,一旦超级块意外损坏就会丢失整个分区的数据,一旦块组描述符意外损坏就会丢失整个块组的数据,因此它们都有多份拷贝。通常内核只用到第0个块组中的拷贝,当执行e2fsck检查文件系统一致性时,第0个块组中的超级块和块组描述符表就会拷贝到其它块组,这样当第0个块组的开头意外损坏时就可以用其它拷贝来恢复,从而减少损失。
1.3 块位图(Block Bitmap)
一个块组中的块是这样利用的:数据块存储所有文件的数据,比如某个分区的块大小是1024字节,某个文件是2049字节,那么就需要三个数据块来存,即使第三个块只存了一个字节也需要占用一个整块;超级块、块组描述符表、块位图、inode位图、inode表这几部分存储该块组的描述信息。那么如何知道哪些块已经用来存储文件数据或其它描述信息,哪些块仍然空闲可用呢?块位图就是用来描述整个块组中哪些块已用哪些块空闲的,它本身占一个块,其中的每个bit代表本块组中的一个块,这个bit为1表示该块已用,这个bit为0表示该块空闲可用。
为什么用df命令统计整个磁盘的已用空间非常快呢?因为只需要查看每个块组的块位图即可,而不需要搜遍整个分区。相反,用du命令查看一个较大目录的已用空间就非常慢,因为不可避免地要搜遍整个目录的所有文件。
与此相联系的另一个问题是:在格式化一个分区时究竟会划出多少个块组呢?主要的限制在于块位图本身必须只占一个块。用mke2fs格式化时默认块大小是1024字节,可以用-b参数指定块大小,现在设块大小指定为b字节,那么一个块可以有8b个bit,这样大小的一个块位图就可以表示8b个块的占用情况,因此一个块组最多可以有8b个块,如果整个分区有s个块,那么就可以有s/(8b)个块组。格式化时可以用-g参数指定一个块组有多少个块,但是通常不需要手动指定,mke2fs工具会计算出最优的数值。
1.4 inode位图(inode Bitmap)
和块位图类似,本身占一个块,其中每个bit表示一个inode是否空闲可用。
1.5 inode表(inode Table)
我们知道,一个文件除了数据需要存储之外,一些描述信息也需要存储,例如文件类型(常规、目录、符号链接等),权限,文件大小,创建/修改/访问时间等,也就是ls -l命令看到的那些信息,这些信息存在inode中而不是数据块中。每个文件都有一个inode,一个块组中的所有inode组成了inode表。
inode表占多少个块在格式化时就要决定并写入块组描述符中,mke2fs格式化工具的默认策略是一个块组有多少个8KB就分配多少个inode。由于数据块占了整个块组的绝大部分,也可以近似认为数据块有多少个8KB就分配多少个inode,换句话说,如果平均每个文件的大小是8KB,当分区存满的时候inode表会得到比较充分的利用,数据块也不浪费。如果这个分区存的都是很大的文件(比如电影),则数据块用完的时候inode会有一些浪费,如果这个分区存的都是很小的文件(比如源代码),则有可能数据块还没用完inode就已经用完了,数据块可能有很大的浪费。如果用户在格式化时能够对这个分区以后要存储的文件大小做一个预测,也可以用mke2fs的-i参数手动指定每多少个字节分配一个inode。
1.6 数据块(Data Block)
根据不同的文件类型有以下几种情况对于常规文件,文件的数据存储在数据块中。对于目录,该目录下的所有文件名和目录名存储在数据块中,注意文件名保存在它所在目录的数据块中,除文件名之外,ls -l命令看到的其它信息都保存在该文件的inode中。注意这个概念:目录也是一种文件,是一种特殊类型的文件。对于符号链接,如果目标路径名较短则直接保存在inode中以便更快地查找,如果目标路径名较长则分配一个数据块来保存。设备文件、FIFO和socket等特殊文件没有数据块,设备文件的主设备号和次设备号保存在inode中。
2 目录中记录项文件类型
编号 | 文件类型 |
0 | Unknown |
1 | Regular file |
2 | Directory |
3 | Character device |
4 | Block device |
5 | Named pipe |
6 | Socket |
7 | Symbolic link |
3 数据块寻址
在inode表中每一个Inode都存有索引项(Block)来找到文件的数据块存在的位置,如下图所示。
从上图可以看出,索引项Blocks[13]指向两级的间接寻址块,最多可表示(b/4)^2+b/4+12个数据块,对于1K的块大小最大可表示64.26MB的文件。索引项Blocks[14]指向三级的间接寻址块,最多可表示(b/4)^3+(b/4)^2+b/4+12个数据块,对于1K的块大小最大可表示16.06GB的文件。
可见,这种寻址方式对于访问不超过12个数据块的小文件是非常快的,访问文件中的任意数据只需要两次读盘操作,一次读inode(也就是读索引项)一次读数据块。而访问大文件中的数据则需要最多五次读盘操作:inode、一级间接寻址块、二级间接寻址块、三级间接寻址块、数据块。实际上,磁盘中的inode和数据块往往已经被内核缓存了,读大文件的效率也不会太低。
二、文件操作
1 stat/lstat函数
int stat(const char *pathname, struct stat *buf);
int lstat(const char *pathname, struct stat *buf);
参数:
pathname-指定要查看的文件名
buf-一个结构体,其定义如下,它包含了文件的信息,如所在的物理位置,inode号,权限,大小等等。
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* protection */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for filesystem I/O */
blkcnt_t st_blocks; /* number of 512B blocks allocated *//* Since Linux 2.6, the kernel supports nanosecond
precision for the following timestamp fields.
For the details before Linux 2.6, see NOTES. */struct timespec st_atim; /* time of last access */
struct timespec st_mtim; /* time of last modification */
struct timespec st_ctim; /* time of last status change */#define st_atime st_atim.tv_sec /* Backward compatibility */
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};
需要重点介绍的域st_mode,它是一个位图,并用八进制表示,现有的linux基本只使用了低16位,我们对这十六位进行介绍,如下图所示。低地址三位表示其他用户权限,中地址三位表示组用户权限,高地址三位为当前用户权限,最高四位表示文件类型(可以表示2^4共16种文件类型),中间三位代表文件的特殊属性,分别为设置用户ID位setUID、设置1组ID位setGID和粘着位sticky。
1.1 文件类型
在linux操作系统中12到15位只7种类型,S_IFMT为文件类型掩码(12-15位全部填充1,0-11位全部填充0),用来过滤低位不相关标志,如下表所示:
S_IFMT | 0170000 | bit mask for the file type bit field |
S_IFSOCK | 0140000 | socket |
S_IFLNK | 0120000 | symbolic link |
S_IFREG | 0100000 | regular file |
S_IFBLK | 0060000 | block device |
S_IFDIR | 0040000 | directory |
S_IFCHR | 0020000 | character device |
S_IFIFO | 0010000 | FIFO |
利用S_IFMT&st_mode可以获得文件类型,也可以使用如下宏定义:
#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK)
#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
#define S_ISCHR(m) (((m) & S_IFMT) == S_IFCHR)
#define S_ISBLK(m) (((m) & S_IFMT) == S_IFBLK)
#define S_ISFIFO(m) (((m) & S_IFMT) == S_IFIFO)
#define S_ISSOCK(m) (((m) & S_IFMT) == S_IFSOCK)
代码示例(获取文件大小,文件类型):
#include <stdlib.h>//perror
#include <stdio.h>
#include <sys/stat.h>//stat & struct stat
int main(int argc,char * argv[])
{
struct stat sbuf;
int ret = 0;
ret = stat(argv[1],&sbuf);
if(ret==-1)
{
perror("stat error");
exit(1);
}
printf("file %s size = %ld bytes",argv[1],sbuf.st_size);//获取文件大小
//获取文件类型
mode_t m = sbuf.st_mode;
if(S_ISDIR(m))
{
printf("%s is directory",argv[1]);
}
else if(S_ISREG(m))
{
printf("%s is regular file",argv[1]);
}
else if(S_ISBLK(m))
{
printf("%s is block device",argv[1]);
}
else if(S_ISCHR(m))
{
printf("%s is character device",argv[1]);
}
else if(S_ISLNK(m))
{
printf("%s is symbolic link file",argv[1]);
}
else if(S_ISFIFO(m))
{
printf("%s is FIFO",argv[1]);
}
else if(S_ISSOCK(m))
{
printf("%s is socket",argv[1]);
}
return 0;
}
需要注意的是,stat会穿透软连接,而lstat不会。例如,我们使用以上代码执行./stat symlinkfile查看一个软连接文件symlinkfile,会显示原始文件的类型,而不是软链接。能穿透符号连接的常见命令有cd、vim、ls等,而ll不会穿透符号连接,而是直接显示符号连接的信息。不穿透符号连接的命令还有readlink等。
1.2 文件权限
黏着位sticky在st_mode第9位
2 access函数
int access(const char * pathname, int mode);
描述:
测试pathname对应的文件是否包含访问权限,mode规定了具体的权限,可提供的值有如下几项。
- R_OK 是否有读权限
- W_OK 是否有写权限
- X_OK 是否有执行权限
- F_OK 测试一个文件是否存在
返回值:
0,返回成功
-1,返回失败
代码示例:
#include <stdlib.h>//perror
#include <stdio.h>
#include <sys/stat.h>//access
#include <unistd.h>//R_OK
int main(int argc,char * argv[])
{
struct stat sbuf;
int ret = 0;
if(access(argv[1],R_OK))
{
perror("access error");
exit(1);
}
printf("read permision ok\n");
return 0;
}
3 chmod/fchmod函数
int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
描述:
与命令chmod用法一致,mode指定要修改的文件权限值。
4 truncate/ftruncate函数
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
描述:
用来扩展文件大小(也可以使用lseek函数实现),但是可以不需要打开文件
5 link/unlink/symlink/readlink函数
1)int link(const char *oldpath, const char *newpath);
描述:
给文件oldpath创建一个硬链接newpath
返回值:
成功返回0
失败返回-1
2)int unlink(const char *pathname);
描述:
删除硬链接pathname
返回值:
成功返回0
失败返回-1
3)int symlink(const char *oldpath, const char *newpath);
描述:
给文件oldpath创建一个符号链接(软连接)newpath
返回值:
成功返回0
失败返回-1
4)ssize_t readlink(const char * path, char * buf, size_t bufsiz);
描述:
读取符号链接文件本身内容(不穿透),得到连接所指向的文件名存入到buf开始的位置
返回值:
成功返回实际读到的字节数
失败返回-1,同时这是对应的errno
隠式回收
当进程结束运行时,所有该进程打开的文件会被关闭,申请的内存空间会被释放。系统的这一特性称之为隐式回收系统资源
6 rename函数
int renam(const char * oldpath,const char * newpath)
描述:
重命名oldpath为newpath
返回值:
成功返回0
失败返回-1,并设置相应的errno
三、目录操作
1 getcwd/chdir函数
char * getcwd(char * buf,size_t size);
描述:
获取当前进程工作目录(类似命令pwd)
返回值:
当前进程工作目录
int chdir(const char * paht);
描述:
更改当前进程工作目录到path
返回值:
成功返回0
失败返回-1
2 opendir/readdir/closedir函数
DIR * opendir(const char * name);
描述:
打开name目录
返回值:
目录流指针,错误返回NULL,并且设置对应的errno
int closedir(DIR * dirp);
描述:
关闭已经打开的目录dirp
成功返回0
失败返回-1,并设置对应的errno
代码示例:
#include <unistd.h> //opendir/closedir
#include <stdlib.h> //perror
#include <stdio.h>
#include <dirent.h> //DIR
int main(int argc,char * argv[])
{
DIR * dir;
dir = opendir(argv[1]);
if(dir==NULL)
{
perror("opendir error");
exit(1);
}
closedir(dir);
return 0;
}
3 readdir函数
struct dirent *readdir(DIR *dirp);
描述:
每调用一次,读取打开的目录dirp中的一个目录项
返回值:
目录项,其详细结构如下所示,读到末尾返回NULL
struct dirent {
ino_t d_ino; /* inode number */
off_t d_off; /* not an offset; see NOTES */
unsigned short d_reclen; /* length of this record */
unsigned char d_type; /* type of file; not supported
by all filesystem types */
char d_name[256]; /* filename */
};
代码示例:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <dirent.h> //DIR
int main(int argc,char * argv[])
{
DIR * dir;
struct dirent * sdp;
dir = opendir(argv[1]);
if(dir==NULL)
{
perror("opendir error");
exit(1);
}
while(sdp=readdir(dir))
{
printf("%s\n",sdp->d_name);
}
closedir(dir);
return 0;
}
4 rewinddir函数
void rewinddir(DIR * dirp);
描述:
回卷目录读写位置到dirp开始位置
5 telldir、seekdir
long telldir(DIR * dirp);
描述:
获取目录读写位置
返回值:
成功返回与dirp相关的当前读写位置。
失败返回-1,并置errno为对应值
void seekdir(DIR * dirp, long loc);
描述:
设置dirp的目录读写位置为loc
四、虚拟文件系统
1 dup/dup2
int dup(int fildes);
int dup2(int fildes, int fildes2);
描述:
重定向文件
代码示例:
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc,char *argv[])
{
int fd = open(argv[1],O_RDONLY|O_CREAT,0664);
if(fd==-1)
{
perror("open error");
exit(1);
}
printf("fd=%d\n",fd);
int save_fd = dup(STDOUT_FILENO);
dup2(fd,STDOUT_FILENO);
close(fd);
char msg[] = "hello dup2\n";
write(STDOUT_FILENO,msg,strlen(msg));
dup2(save_fd,STDOUT_FILENO);
write(STDOUT_FILENO,msg,strlen(msg));
close(save_fd);
return 0;
}
程序的逻辑如下图所示,1)程序开始打开一个文件,并分配fd=3;2)然后,利用dup函数,将分配描述符save_fd=4,使其指向标准输出STDOUT_FILENO(1);3)使用dup2函数使STDOUT_FILENO重定向到打开的文件,这时,save_fd还保存着终端tty的副本;4)关闭fd之后,somefile只有STDOUT_FILENO可以访问,此时,往STDOUT_FILENO中写入数据,那么数据就重定向写入到somefile中了;5)此时继续调用dup2,将STDOUT_FILENO重定向到save_fd也就是指向tty终端设备,同时断开了somefile的连接;6)最后调用close函数关闭save_fd,恢复原样。