操作系统-文件系统概要
文件系统的功能规划
对于运行的进程来说,内存就像一个纸箱子,仅仅是一个暂存数据的地方,而且空间有限。如果我们想要进程结束之后,数据依然能够保存下来,就不能只存在内存里,而是应该保存在外部存储中。就像图书馆这种地方,不仅空间大,而且能够永久保存,在操作系统中我们称之为文件系统。
我们最常用的外部存储就是硬盘,数据是以文件的形式保存在硬盘上的。为了管理这些文件,我们在规划文件系统的时候,需要考虑以下几点:
第一点,文件系统要有严格的组织形式,使得文件能够以块为单位进行存储。这就像在图书馆里,我们会设置一排排书架,然后再把书架分成一个个小格子,有的项目存放的资料非常多,一个格子放不下,就需要多个格子来存放。
第二点,文件系统中也要有索引区,方便查找一个文件分成的多个块都放在了什么位置。这就好比,图书馆的书太多了,为了方便查找,我们需要专门设置一排书架,这里面会写清楚整个档案库有哪些资料,资料在哪个架子的哪个格子上。这样找资料时候就不要跑遍整个档案库,在这个书架上找到后,直奔目标书架就可以了。
第三点,如果文件系统中有的文件是热点文件,近期经常被读取和写入,文件系统应该有缓存层。这就相当于图书馆里面的热门图书区,这里面的书都是畅销书或者是常常被借还的图书。因为借还的次数比较多,那就没必要每次有人还了之后,还放回遥远的货架,我们可以专门开辟一个区域,放着这些借还频次高的图书。这样借还的效率就会提高。
第四点,文件应该用文件夹的形式组织起来,方便管理和查询。这就像在图书馆里面,你可以给这些资料分门别类,比如分成计算机类、文学类、历史类等等。这样你也容易管理,项目组借阅的时候只要在某个类别中去找就可以了。
在文件系统中,每个文件都有一个名字,这样我们访问一个文件,希望通过它的名字就可以找到。文件名就是一个普通的文本。当然文件名会经常冲突,不同用户取相同的名字的情况还是会经常出现的。
要想把很多的文件有序地组织起来,我们就需要把他们成为目录或者文件夹。这样,一个文件夹里可以包含文件夹,也可以包含文件,这样就形成了一种树形结构。而我们可以将不同的用户放在不同的用户目录下,就可以一定程度上避免了命名的冲突问题。
如图所示,不同的用户的文件放在不同的目录下,虽然很多文件都叫“文件 1”,只要在不同的目录下,就不会有问题。
第五点,Linux内核要在自己的内存里面维护一套数据结构,来保存哪些文件被哪些进程打开和使用。这就好比,图书馆里会有个图书管理系统,记录哪些书被借阅,被谁借阅了,借阅了多久,什么时候归还。
文件系统相关系统调用
如何使用系统调用操作文件呢?我们先来看一个完整的例子:
int main(int argc, char *argv[])
{
int fd = -1;
int ret = 1;
int buffer = 1024;
int num = 0;
if((fd=open("./test", O_RDWR|O_CREAT|O_TRUNC))==-1)
{
printf("Open Error\n");
exit(1);
}
ret = write(fd, &buffer, sizeof(int));
if( ret < 0)
{
printf("write Error\n");
exit(1);
}
printf("write %d byte(s)\n",ret);
lseek(fd, 0L, SEEK_SET);
ret= read(fd, &num, sizeof(int));
if(ret==-1)
{
printf("read Error\n");
exit(1);
}
printf("read %d byte(s),the number is %d\n", ret, num);
close(fd);
return 0;
}
当使用系统调用open打开一个文件时,操作系统会创建一些数据结构来表示这个被打开的文件。为了能够找到这些数据结构,在进程中,我们会为这个打开的文件分配一个文件描述符fd(File Descriptor)。
文件描述符,就是用来区分一个进程打开的多个文件的。它的作用域就是当前进程,出了当前进程这个文件描述符就没有意义了。open返回的fd必须记录好,我们对这个文件的所有操作都要靠这个fd,包括最后关闭文件。
在Open函数中,有一些参数:
O_CREAT表示当文件不存在,创建一个新文件。
O_RDWR表示以读写方式打开。
O_TRUNC表示打开文件后,将文件的长度截断为0.
接下来,write用于写入数据。第一个参数就是文件描述符,第二个参数表示要写入的数据存放位置,第三个参数表示希望写入的字节数,返回值表示成功写入到文件的字节数。
最终,close将关闭一个文件。
对于命令行来讲,通过ls可以得到文件的属性,使用代码怎么办呢?
我们有下面三个函数,可以返回与打开的文件描述符相关的文件状态信息。这个信息将会写到类型为struct stat的buf结构中。
int stat(const char *pathname, struct stat *statbuf);
int fstat(int fd, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* Inode number */
mode_t st_mode; /* File type and mode */
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; /* Block size for filesystem I/O */
blkcnt_t st_blocks; /* Number of 512B blocks allocated */
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 */
};
函数stat和lstat返回的是通过文件名查到的状态信息。这两个方法区别在于,stat没有处理符号链接(软链接)的能力。如果一个文件是符号链接,stat会直接返回它所指向的文件的属性,而lstat返回的就是这个符号链接的内容,fstat则是通过文件描述符获取文件对应的属性。
接下来我们来看,如何使用系统调用列出一个文件夹下面的文件以及文件的属性。
int main(int argc, char *argv[])
{
struct stat sb;
DIR *dirp;
struct dirent *direntp;
char filename[128];
if ((dirp = opendir("/root")) == NULL) {
printf("Open Directory Error%s\n");
exit(1);
}
while ((direntp = readdir(dirp)) != NULL){
sprintf(filename, "/root/%s", direntp->d_name);
if (lstat(filename, &sb) == -1)
{
printf("lstat Error%s\n");
exit(1);
}
printf("name : %s, mode : %d, size : %d, user id : %d\n", direntp->d_name, sb.st_mode, sb.st_size, sb.st_uid);
}
closedir(dirp);
return 0
}
opendir函数打开一个目录名所对应的DIR目录流。并返回执行DIR目录流的指针。流定位在DIR目录流的第一个条目。
readdir函数从DIR目录流中读取一个项目,返回的是一个指针,指向dirent结构体,且流会自动指向下一个目录条目。如果已经流到最后一个条目,则返回NULL。
closedir()关闭参数dir所指的目录流。
总结
通过本章内容,我们对于文件系统的主要功能有了一个总体的映像,我们通过下面的这张图梳理一下。
在文件系统上,需要维护文件严格的格式,可以通过mkfs.ext4命令来格式化为严格的格式。
每一个硬盘上保存的文件都要有一个索引,来维护这个文件上的数据块都保存在哪里。
为了能够更快的读取文件,内存里会分配一块空间作为缓存,让一些数据块放在缓存里面。
在内核中,要有一整套的数据结构来表示打开的文件。
在用户态,每个打开的文件都有一个文件描述符,可以通过各种文件相关的系统调用,操作这个文件描述符。