c 从dat文件读取信息并存储到数组中_持久化(4):文件和目录

在看文章前可以先看下这个,

吴海波:专栏的序。

先有个大概的认识会对阅读有所帮助。

这一章是对文件系统的简单介绍,主要是对一些概念和基本操作的解释。

对于存储设备的抽象是关键,因为我们希望我们建立的系统是能够尽可能通用的。存储设备有2个重要抽象:文件和目录。文件可以简单理解为一个可以读取和写入的字节数组,并且每个文件都有一个底层名字:inode number。对于OS来说,文件中的内容它是不知道的,文件系统只是负责存储数据到对应的存储器中。

目录和文件一样,也会有一个底层名字,不过内容的形式不太一样:它包含一个键值对(用户看到的名字,底层的名字)的链表。举个例子,我们有一个文件,底层的名字是“10”,对应用户看到的名字是“foo”。那么目录中就会有一个条目(“foo”, “10”)来进行映射。目录中的每个条目都会指向一个文件或者目录。通过将目录放在其他目录中,用户可以构建任意目录树(或目录层次结构),在该目录树下存储所有文件和目录。

目录层次结构从根目录开始(在基于UNIX的系统中,根目录简称为/)并使用某种分隔符命名后续子目录,直到达到所需的文件或目录。例如,如果用户在根目录/中创建了一个目录foo,然后在目录foo中创建了一个文件bar.txt,我们就可以通过它的绝对路径名来引用该文件,在这种情况下它将是/foo/bar.txt。有关更复杂的目录树,请参见图39.1; 示例中的有效目录是/,/ foo,/ bar,/ bar / bar,/ bar / foo,有效文件是/foo/bar.txt和/bar/foo/bar.txt。目录和文件可以具有相同的名称,只要它们位于文件系统树中的不同位置(例如,图中有两个名称都叫bar.txt)。

你可能还注意到,此示例中的文件名通常包含两部分:bar和txt,以句点分隔。第一部分是任意名称,而文件名的第二部分通常用于指示文件的类型,例如,它是C代码(例如.c)还是图像(例如.jpg) ,或音乐文件(例如.mp3)。但是,这通常只是一种惯例,不是强制要这么做。因此,我们可以看到文件系统提供了一个方便的机制:通过名字就可以访问我们感兴趣的文件的便捷方式。名称在系统中很重要,因为访问任何资源的第一步是能够命名它。因此,在UNIX系统中,文件系统提供了一种统一的方式来访问磁盘,U盘,CD-ROM,以及许多其他设备上的文件。实际上还有很多其他东西,都位于目录树下。

ef53519b1f86910e2871a8ecdc1eb90f.png

下面来介绍下文件系统提供的接口。

1.创建文件

这可以通过open系统调用来完成; 通过调用open()并传递O_CREAT标志,程序可以创建一个新文件。下面是示例代码,用于在当前工作目录中创建名为“foo”的文件。

int fd = open("foo", O_CREAT|O_WRONLY|O_TRUNC, S_IRUSR|S_IWUSR);

open()需要许多不同的标志。在此示例中,第二个参数(OCREAT)表示如果文件不存在就创建,O_WRONLY确保文件只能写入O_TRUNC表示如果文件已存在,则将其截断为零字节大小从而删除任何现有内容。第三个参数指定权限,在这种情况下,文件可由文件所有者读取和写入。

open()的一个重要方面是它返回的内容:文件描述符。文件描述符只是一个整数,每个进程私有,并在UNIX系统中用于访问文件; 因此,一旦打开文件,就可以使用文件描述符来读取或写入文件,假设你有权这样做。

通过这种方式,文件描述符使你能够执行某些操作。另一种思考文件描述符的方法就是将文件描述符想象为指向文件类型对象的指针; 一旦你有这样一个对象,你可以调用其他“方法”来访问文件,比如read()和write()。如上所述,文件描述符由操作系统基于每个进程管理。 这意味着在UNIX系统上的proc结构体中会保留对应的数据。

这是xv6内核的proc结构体的相关部分:

struct proc {
...
struct file *ofile[NOFILE]; // Open files
...
};

在proc结构体中用一个简单的数组(最多有NOFILE个打开文件)跟踪基于每个进程打开的文件。数组的每个条目实际上就是对应打开文件的指针,它将用于跟踪有关被访问的文件的信息; 我们将在下面进一步讨论。

2.读取和写入文件

一旦我们有了一些文件,我们可能想读或写它们。让我们从读取现有文件开始。

prompt> echo hello > foo
prompt> cat foo
hello
prompt>

在这段代码片段中,我们将程序echo的输出重定向到文件foo,输出就是单词“hello”。然后我们使用cat来查看文件的内容。 但是cat程序如何访问文件foo?我们将使用一个非常有用的工具来跟踪程序运行期间调用的系统调用。在Linux上,该工具称为strace; 其他系统也有类似的工具(参见Mac上的dtruss,或者某些旧的UNIX变体上的truss)。strace的作用是跟踪程序运行时发出的每个系统调用,并打印到屏幕上供查看。下面是一个使用strace来确定cat正在做什么的示例(为了便于阅读,删除了一些调用):

prompt> strace cat foo
...
open("foo", O_RDONLY|O_LARGEFILE) = 3
read(3, "hellon", 4096)                            = 6
write(1, "hellon", 6)                              = 6
hello
read(3, "", 4096)                                   = 0
close(3)                                            = 0
...
prompt>

cat做的第一件事是打开文件进行读取。以下是我们需要注意的点; 首先,该文件仅为读取(不写入)而打开,如O_RDONLY标志所示; 第二,使用64位偏移量(O LARGEFILE); 第三,对open()的调用成功并返回一个文件描述符,其值为3。为什么第一次调用open()会返回3,而不是0或者是1? 实际上每个正在运行的进程已经打开了三个文件,标准输入(进程可以读取以接收输入),标准输出(进程可以写入以便将信息转储到屏幕上)和标准错误( 进程可以写错误消息)。它们分别由文件描述符0,1和2表示。因此,当第一次打开一个文件时(如上图所示),它几乎肯定是文件描述符3。

打开成功后,cat使用read()系统调用重复读取文件中的字节。read()的第一个参数是文件描述符,从而告诉文件系统要读取哪个文件; 一个进程当然可以同时打开多个文件,因此描述符使操作系统能够知道读取所引用的特定文件。第二个参数指向一个缓冲区,其中将放置read()的结果; 在上面的系统调用跟踪中,strace显示读取结果(“hello”)。第三个参数是缓冲区的大小,在本例中为4 KB。对read()的调用也成功返回,这里返回它读取的字节数(6,其中包括5个字母的"hello"和一个行结束标记)。

此时,会看到另一个有趣的事情:对write()系统调用进行了单独调用,对应的文件描述符是1。正如我们上面提到的,这个描述符被称为标准输出,用于程序cat中将单词"hello"打印在屏幕上。但是它直接调用write()吗? 也许(如果它是高度优化的),但如果没有,cat可能会做的是调用库例程printf(); 在内部,printf()会先将内容格式化,然后才将结果写入标准输出以打印到屏幕上。

然后cat程序尝试从文件中读取更多内容,但由于文件中没有剩余字节,read()返回0,这样程序就知道它已经读取了整个文件。因此,系统调用close()表示文件foo已经读取完成了,这个调用传入相应的文件描述符。写入文件是通过一组类似的步骤完成的。首先,打开一个文件,然后调用write()系统调用,大文件可能重复调用,最后再调用close()关闭。

3.上面介绍的对文件的读和写都是顺序的, 其实有些时候也可以是非顺序的,比如我想从文件的某个指定的位置进行操作。lseek提供了这种功能:

off_t lseek(int fildes, off_t offset, int whence);

第一个参数是操作文件的描述符。第二个参数是偏移量,第三个参数whence决定了seek操作是如何完成的,具体的意义可以参照man手册:

If whence is SEEK_SET, the offset is set to offset bytes.
If whence is SEEK_CUR, the offset is set to its current
location plus offset bytes.
If whence is SEEK_END, the offset is set to the size of
the file plus offset bytes.

从该描述中可以看出,对于每个进程打开的文件,操作系统会跟踪该文件“当前”偏移量,该偏移量确定下一个读取或写入的位置。因此,打开文件这个抽象概念中的一部分是它具有当前偏移这个属性,有两种方式可以更新这个值。第一种是当发生N个字节的读或写,N被加到当前的偏移量上; 因此,每次读取或写入都会隐式更新偏移量。第二个是lseek显式,它改变了上面指定的偏移量。这个偏移量会保存在file结构体中,被前面说的proc引用。这是xv6中简化的file结构体。

struct file {
int ref;
char readable;
char writable;
struct inode *ip;
uint off;
};

根据上面文件的结构体,操作系统可以使用它来确定打开的文件是可读还是可写(或两者),它引用的底层文件(由struct inode指针ip指向)和当前偏移量。还有一个引用计数(ref)值,我们将在下面进一步讨论。这些文件结构的总和代表系统中当前所有打开的文件; 它们有时被称为打开文件表(open file table)。在xv6内核中,用如下的结构体表示:

struct {
struct spinlock lock;
struct file file[NFILE];
} ftable;

让我们用几个例子来说明一点。首先,让我们跟踪一个打开了一个文件(大小为300字节)的进程,并通过重复调用read()来读取它,每次读取100个字节。trace中有几个值得关注的点。首先,可以看到当打开文件时当前偏移量初始化为零。接下来,可以看到每次read()后的递增; 这使得进程可以轻松地继续调用read()来获取文件的下一个块。最后,可以看到read()返回零,从而向进程指示它已完整读取该文件。

b792045b12b6f5e664a7701270292aab.png

然后,让我们跟踪一个打开同一个文件两次并向每个文件发出读取的进程。在此示例中,分配了两个文件描述符(3和4),每个文件描述符引用open file table中的不同条目(在此示例中,分别对应条目10和11,如图的标题中所示; OFT代表打开文件表)。可以看到当前偏移量是如何独立更新的。

271ca318688351e3fb11852b27f6ae4a.png

在最后一个示例中,进程使用lseek()在读取之前重新定位当前偏移量; 在这种情况下,只需要一个打开的文件表条目(与第一个示例一样)。这里,lseek()调用首先将当前偏移量设置为200.随后的read()读取接下来的50个字节,并相应地更新当前偏移量。

465ea749cb260690eec72eec7d7644e7.png

4.共享文件列表中的条目:fork()和dup()

在许多情况下(如上面所示的示例),文件描述符到打开文件表(open file table OFT)中的条目的映射是一对一映射。例如,当进程运行时,它可能决定打开一个文件,读取它然后关闭;该文件将在打开的文件表中具有唯一条目。即使某个其他进程同时读取同一个文件,每个进程都会在打开的文件表中有自己的条目。在这种情况下,文件的每个逻辑读取或写入都是独立的,并且每个文件在访问给定文件时都有自己的当前偏移量。但是,某些情况是共享打开文件表中的条目。当父进程使用fork()创建子进程就是其中的一种场景。图39.2显示的代码片段中,父进程在其中创建一个子进程,然后等待它完成。子进程通过调用lseek()调整当前偏移量然后退出。最后,父进程在等待子进程完成之后,检查当前的偏移并打印出其值。

d97f7a4ae4dec8400d5113959b3e55d3.png

这个程序的输出如下:

prompt> ./fork-seek
child: offset 10
parent: offset 10
prompt>

327c71b71ab8e72b242faa8e6511e81b.png

图39.3显示了各个数据之间的关系。我们在这里讲解之前提到的引用计数值的含义。共享文件表条目时,对应的引用计数是会记录下数量的; 只有当两个进程都关闭文件(或退出)时才会删除该条目。在父和子之间共享打开的文件表条目有的场景是有用的。例如,如果你创建了许多协作处理任务的进程,则可以在不进行任何额外协调的情况下写入相同的文件。有关调用fork()时进程共享内容的更多信息,可以查看man pages。

另一个有趣的是也许更有用的共享案例发生在dup()系统调用(及其非常相似的兄弟,dup2(),dup3())。dup()调用允许进程创建一个新的文件描述符,该描述符引用与现有描述符相同的底层打开文件。图39.4显示了一个小代码片段,显示了如何使用dup()。

int main(int argc, char *argv[]) {
int fd = open("README", O_RDONLY);
assert(fd >= 0);
int fd2 = dup(fd);
// now fd and fd2 can be used interchangeably
return 0;
}

在编写UNIX shell并执行输出重定向等操作时,dup()调用(特别是dup2())非常有用。 花一些时间思考为什么! 而现在,你在想:为什么他们在做shell项目的时候没告诉我们? 哦,即使在这样一本令人难以置信的关于操作系统的的书,你也无法以正确的顺序获得所有内容。(作者对这本书的自豪感跃然纸上,不过确实名副其实)。

5.立刻写入的fsync()方法

在大多数调用write()的时候,os都是先将数据写到缓存,然后过段时间再写到磁盘中,这种方式会加快I/O操作,但是在某些环境中确实会带来问题,比如数据库系统,所以os提供了fsync()方法来供调用,调用后会立刻将数据写入硬盘而不是缓冲区。

大多数情况下,当程序调用write()时,它只是告诉文件系统:在将来的某个时刻,将此数据写入存储设备。出于性能原因,文件系统将在内存中缓冲这些写入一段时间(比如5秒或30);在稍后的时间点,才将数据实际写入存储设备。从应用程序的角度来看,写入似乎很快完成,并且仅在极少数情况下(例如,在write()调用之后但在写入磁盘之前机器崩溃)数据将丢失。但是,某些应用程序就是必须要这种立即写入的保证。例如,在数据库管理系统(DBMS)中,开发正确的恢复协议需要能够强制立刻写入磁盘的能力。为了支持这些类型的应用程序,大多数文件系统提供了一些额外控制的API。在UNIX世界中提供给应用程序的接口称为fsync(int fd)。当进程为特定文件描述符调用fsync()时,文件系统将脏数据(还没有写入磁盘)强制立刻写入磁盘。所有这些写入完成后,fsync()例程返回。这是一个如何使用fsync()的简单示例。代码打开文件foo,向其写入一个数据块,然后调用fsync()以确保写入立即被强制写入到磁盘。一旦fsync()返回,应用程序就可以安全地继续运行,因为知道数据已被持久化(如果fsync()正确实现)。

下面的代码给出了示例:

int 

有趣的是,这个代码并不能保证你所期望的一切; 在某些情况下,你还需要对包含文件foo的目录执行fsync()操作。添加此步骤不仅可以确保文件本身位于磁盘上,还可以确保文件(如果是新创建的)也是目录的一部分。这类细节经常被忽视,导致许多应用程序级错误。

6.给文件改名字。

一旦我们有了一个文件,有时候需要为文件修改为一个不同的名称。在命令行中是用mv命令; 在这个例子中,文件foo被重命名为bar:

prompt> mv foo bar

使用strace,我们可以看到mv使用系统调用 rename(char * old,char * new),该函数有两个入参:文件的原始名称(old)和新名称(new)。rename()调用是原子调用; 如果系统在重命名期间崩溃,则文件将被命名为旧名称或新名称,不会出现奇怪的中间状态。因此,rename()对文件状态进行原子更新对某些类型的应用程序至关重要。更具体的说,想象一下,你正在使用文件编辑器(例如,emacs),并在文件的中间插入一行。例如,文件的名称是foo.txt。

int fd = open("foo.txt.tmp", O_WRONLY|O_CREAT|O_TRUNC,
S_IRUSR|S_IWUSR);
write(fd, buffer, size); // write out new version of file
fsync(fd);
close(fd);
rename("foo.txt.tmp", "foo.txt");

编辑器在这个例子中做的很简单:用临时名称(foo.txt.tmp)写出新版本的文件,用fsync()强制它到磁盘,然后,当应用程序确定新文件元数据和内容位于磁盘上,将临时文件重命名为原始文件的名称。最后一步以原子方式将新文件交换到位,同时删除旧版本的文件,从而实现原子文件更新。

7.获取文件的相关信息。

除了文件访问之外,我们希望文件系统能够保存有关它所存储的每个文件的所有信息。我们通常称这些数据为文件元数据。要查看某个文件的元数据,我们可以使用stat()或fstat()系统调用。

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 blocks allocated */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last status change */
};

你可以看到有关于文件的大量信息,包括其大小(以字节为单位),其底层的名称(即inode number),一些所有权信息以及有关何时访问或修改文件的一些信息等等。你可以使用stat命令来获取这些信息。

prompt> echo hello > file
prompt> stat file
File: ‘file’
Size: 6 Blocks: 8 IO Block: 4096 regular file
Device: 811h/2065d Inode: 67158084 Links: 1
Access: (0640/-rw-r-----) Uid: (30686/ remzi) Gid: (30686/ remzi)
Access: 2011-05-03 15:50:20.157594748 -0500
Modify: 2011-05-03 15:50:20.157594748 -0500
Change: 2011-05-03 15:50:20.157594748 -0500

事实上,每个文件系统通常将这种类型的信息保存在一个名为inode1的结构中。当我们谈论文件系统实现时,我们将学习更多关于inode的知识。现在,你应该将inode视为由文件系统保留的持久数据结构,其中包含我们在上面看到的信息。所有inode都驻留在磁盘上; 通常将一部分活跃使用的缓存在内存中以加快访问速度。

8.删除文件

现在我们知道如何创建文件并按顺序访问它们。但是如何删除文件? 在命令行中只需输入rm。但是用什么系统调用来删除文件? 让我们再次使用我们的老朋友strace来一探究竟。

prompt> strace rm foo
...
unlink("foo") = 0
...

我们从跟踪的输出中删除了一堆无关的内容,只留下了一个名为unlink()的系统调用。如你所见,unlink()只获取要删除的文件的名称,并在成功时返回零。但这引出了一个很大的难题:为什么这个系统调用名为 unlink 而不是remove或者delete。要解决这个问题,我们不仅仅要理解文件,还要理解下面要介绍的目录的概念。

9.新建目录

除了文件之外,还有一组与目录相关的系统调用,可以用来新建,读取和删除目录。请注意,你永远不能直接写入目录; 因为目录被视为文件系统元数据,所以只能通过创建文件,目录或其他类型的数据来间接更新目录。在这种情况下,文件系统确保目录的内容始终符合预期。要创建目录,可以使用单个系统调用mkdir()。同名的mkdir命令可用于创建这样的目录。

prompt> strace mkdir foo
...
mkdir("foo", 0777) = 0
...
prompt>

当一个目录被创建后,虽然包括少量的信息,但是我们认为它是空的。特别的,一个空的目录包含2个条目:一个引用自己的条目,一个指向父目录的条目。通常用.和..来表示。你可以通过命令行的ls命令看到。

prompt> ls -a
./ ../
prompt> ls -al
total 8
drwxr-x--- 2 remzi remzi 6 Apr 30 16:17 ./
drwxr-x--- 26 remzi remzi 4096 Apr 30 16:17 ../

10.读取目录信息

既然我们已经创建了一个目录,我们也就会希望读取一个目录的信息。让我们编写自己的小工具,如ls,看看它是如何完成的。我们改为使用一组新的调用,而不是像打开一个文件一样打开一个目录。下面是一个打印目录内容的示例程序。该程序使用三个调用,opendir(),readdir()和closedir()来完成工作,你可以看到实现起来有多简单; 我们只使用一个简单的循环一次读取一个目录条目,并打印出目录中每个文件的名称和inode号。

int main(int argc, char *argv[]) {
DIR *dp = opendir(".");
assert(dp != NULL);
struct dirent *d;
while ((d = readdir(dp)) != NULL) {
printf("%lu %sn", (unsigned long) d->d_ino, d->d_name);
}
closedir(dp);
return 0;
}

下面的struct dirent数据结构,显示每个目录条目中可用的信息:

struct dirent {
char d_name[256]; /* filename */
ino_t d_ino; /* inode number */
off_t d_off; /* offset to the next dirent */
unsigned short d_reclen; /* length of this record */
unsigned char d_type; /* type of file */
};

因为目录包含的信息很少(基本上只是将名称映射到inode编号,以及一些其他细节),程序可能希望在每个文件上调用stat()以获取有关每个文件的更多信息,例如其长度或 其他详细信息。

11.删除目录:rmdir()

最后,你可以通过调用rmdir()(由同名命令rmdir使用)来删除目录。但是,与删除文件不同,删除目录很危险,因为你可以使用单个命令删除大量数据。因此,rmdir()要求在删除之前目录为空(即,只有“.”和“..”条目)。如果你尝试删除非空目录,对rmdir()的调用将失败

12.硬连接。前面我们提到删除一个文件的时候,调用的是unlink(),为了理解这个调用,我们从添加一个条目到文件系统的link()调用说起。link()系统调用接受2个参数,一个老的路径名和一个新的,当你对老的文件名link一个新的名字的时候,实际上是在新建一个新的方式去指向同样的文件。ln命令经常用来实现这个功能。

prompt> echo hello > file
prompt> cat file
hello
prompt> ln file file2
prompt> cat file2
hello
prompt> ls -i file file2
67158084 file
67158084 file2
prompt>

上面的file和file2实际上指向的同一个文件,因为他们的inode number都是67158084 。实际上在创建文件的时候是做了2件事:首先我们创建一个数据结构来描述这个文件的信息,包括硬盘中的块在哪,文件的大小等等。然后连接用户给这个文件起的名字,将这个数据记录在目录中。link()实际上就是在目录中再新增一个条目指向这个文件,所以在删除的时候实际上就是从目录中删除这个连接就可以了。

到现在为止,我们才可以理解unlink()这个调用。创建文件时,你实际上做了两件事。首先,创建一个结构(inode),它将跟踪几乎所有有关该文件的信息,包括其大小,其块在磁盘上的位置,等等。然后你将用户可读的名称链接到该文件,并将该链接放入目录中。创建文件的硬链接到文件系统后,原始文件名(file)和新创建的文件名(file2)之间没有区别; 实际上,它们都只是指向文件底层元数据的链接,可以在inode编号67158084中找到。因此,要从文件系统中删除文件,我们调用unlink()。

prompt> rm file
removed ‘file’
prompt> cat file2
hello

这样做的原因是因为当文件系统取消链接文件时,它会检查inode编号中的引用计数。此引用计数(有时称为链接计数)允许文件系统跟踪已将多少个不同的文件名链接到此特定inode。当调用unlink()时,它会删除用户可读名称(正在删除的文件)与给定的inode编号之间的连接,并减少引用计数; 只有当引用计数达到零时,文件系统才会释放inode和相关的数据块,从而真正地删除文件。你当然可以使用stat()查看文件的引用计数。让我们看看当我们创建和删除文件的硬链接时会发生什么。在这个例子中,我们将创建三个指向同一文件的链接,然后删除它们。

prompt> echo hello > file
prompt> stat file
... Inode: 67158084 Links: 1 ...
prompt> ln file file2
prompt> stat file
... Inode: 67158084 Links: 2 ...
prompt> stat file2
... Inode: 67158084 Links: 2 ...
prompt> ln file2 file3
prompt> stat file
... Inode: 67158084 Links: 3 ...
prompt> rm file
prompt> stat file2
... Inode: 67158084 Links: 2 ...
prompt> rm file2
prompt> stat file3
... Inode: 67158084 Links: 1 ...
prompt> rm file3

13.符号连接。上面介绍的硬连接有其局限性,首先是不能指向一个目录(防止循环引用),另外就是不能指向不同的磁盘分区(因为不同的文件系统不共用inode number),为了解决这些限制,新的link方式产生了。

prompt> echo hello > file
prompt> ln -s file file2
prompt> cat file2
hello

prompt> stat file
... regular file ...
prompt> stat file2
... symbolic link ...

prompt> ls -al
drwxr-x--- 2 remzi remzi 29 May 3 19:10 ./
drwxr-x--- 27 remzi remzi 4096 May 3 15:14 ../
-rw-r----- 1 remzi remzi 6 May 3 19:10 file
lrwxrwxrwx 1 remzi remzi 4 May 3 19:10 file2 -> file

如上所见,创建软链接和硬连接看起来大致相同,现在可以通过文件名file以及符号链接名称file2访问原始文件。然而,除了这种表面相似性之外,符号链接实际上与硬链接完全不同。第一个区别是符号链接实际上是一个不同类型的文件本身。我们已经讨论过常规文件和目录; 符号链接是文件系统中的第三种类型。

运行ls也揭示了这一事实。如果仔细查看ls输出的第一个字符,可以看到最左列中的第一个字符是 - ,对于常规文件,d表示目录,l表示软链接。你还可以看到符号链接的大小(本例中为4个字节),以及链接指向的内容(名为file的文件)。file2为4个字节的原因是因为连接的文件的名称大小就是4字节。因为我们链接到一个名为file的文件,所以我们的链接文件file2很小(4个字节)。如果我们链接到更长的文件名,我们的链接文件会更大:最后,由于创建符号链接的方式,可能会留下了所谓的悬空引用的可能性:正如你在本示例中所看到的,完全不同于硬链接,删除名为file的原始文件会导致链接指向不再存在的文件名。

f36de1b21190c02c5737b397036a757e.png

14.权限标记位和访问控制列表

进程的抽象提供了两个重要的虚拟化对象:CPU和内存。这些给每个进程一个假象,它有自己的私有CPU和自己的私有内存; 实际上,OS使用各种技术以安全可靠的方式在竞争实体之间共享有限的物理资源。文件系统也提供了磁盘的虚拟视图,将其从一堆原始块转换为更加用户友好的文件和目录,如本章所述。但是,文件系统的抽象与CPU和内存的抽象明显不同,因为文件通常在不同的用户和进程之间共享,而不是(总是)私有的。因此,在文件系统中通常存在更全面的机制来用于实现各种程度的共享。

这种机制的第一种形式是经典的UNIX权限位。要查看文件foo.txt的权限,只需键入:

prompt> ls -l foo.txt
-rw-r--r-- 1 remzi wheel 0 Aug 24 16:29 foo.txt

我们只关注这个输出的第一部分,即-rw-r--r--。这里的第一个字符显示文件的类型: - 表示常规文件,d表示目录,l表示符号链接,依此类推;这(大部分)与权限无关,所以我们暂时忽略它。我们对权限位感兴趣,这些位由接下来的九个字符(rw-r--r--)表示。对于每个常规文件,目录和其他实体,这些位确定谁可以访问它以及如何访问它。权限由三个分组组成:文件的所有者可以对其执行的操作,组中的某个人可以对该文件执行的操作,以及任何人(有时称为其他人)可以执行的操作。所有者,组成员或其他人可以拥有的能力包括读取文件,编写或执行文件的能力。在上面的示例中,ls输出的前三个字符表示文件是所有者可读写的(rw-),有组权限的成员以及系统中的任何其他人只有读取权限。文件的所有者可以随时更改这些权限,例如使用chmod命令(to change the file mode)。

prompt> chmod 600 foo.txt

对于目录,执行位的行为略有不同。具体地说,它使用户(或组或每个人)能够做诸如将目录(即cd)改变到给定目录中的权限,并且与可写位一起才能在其中创建文件。了解更多信息的最佳方式就是你可以自己多尝试! 。一些文件系统,包括称为AFS的分布式文件系统(在后面的章节中讨论),包括更复杂的控件来控制权限。例如,AFS以每个目录的访问控制列表(ACL)的形式来判断权限。访问控制列表是一种更通用,更强大的方式,可以准确表示谁可以访问给定资源。

例如,以下是作者的AFS帐户中私人目录的访问控制,如fs listacl命令所示:

prompt> fs listacl private
Access list for private is
Normal rights:
system:administrators rlidwka
remzi rlidwka

该列表显示系统管理员和用户remzi都可以在此目录中查找,插入,删除和管理文件,以及读取,写入和锁定这些文件。

要允许某人(在这种情况下,另一位作者)访问此目录,用户remzi只需键入以下命令即可。

prompt> fs setacl private/ andrea rl

15.制作和安装文件系统

我们现在已经浏览了访问文件,目录和某些特殊类型链接的基本接口。但是还有一个我们应该讨论的主题:如何将许多底层文件系统组装成完整的目录树。此任务是通过首先创建文件系统,然后安装它们以使其内容可访问来完成的。为了创建文件系统,大多数文件系统都提供了一个工具,通常称为mkfs(发音为“make fs”)。流程如下:对给定的设备和一种类型的文件系统(例如,ext3),将文件系统写入设备中,并且将它作为根目录。一旦创建了这样的文件系统,就需要在统一文件系统树中能够访问它。这个任务是通过mount程序实现的(它调用系统调用mount()完成实际工作)。挂载的作用非常简单,就是将现有目录作为目标挂载点,将新文件系统粘贴到目录树上。这里的一个例子可以帮助你理解。想象一下,我们有一个未安装的ext3文件系统,存储在设备分区/ dev / sda1中,它具有以下内容:一个根目录,其中包含两个子目录a和b,每个子目录依次包含一个名为foo的文件。假设我们希望在挂载点/home/users上安装此文件系统。我们将输入以下内容:

prompt> mount -t ext3 /dev/sda1 /home/users

如果成功,则mount将使这个新文件系统可用。但是,请注意现在如何访问新文件系统。 要查看根目录的内容,我们将使用这样的ls:

prompt> ls /home/users/
a b

如上所示,路径名/ home / users / 现在指的是新安装目录的根目录。 同样,我们可以使用路径名/ home / users / a和/ home / users / b访问目录a和b。 最后,可以通过/ home / users / a / foo和/ home / users / b / foo访问名为foo的文件。 因此mount的美妙之处在于:mount将所有文件系统统一到一棵树中,而不是拥有多个独立的文件系统,这样最终使命名统一且方便。

要查看系统上安装的文件系统以及挂载在哪,只需运行mount命令就可以了。 你会看到这样的事情:

/dev/sda1 on / type ext3 (rw)
proc on /proc type proc (rw)
sysfs on /sys type sysfs (rw)
/dev/sda5 on /tmp type ext3 (rw)
/dev/sda7 on /var/vice/cache type ext3 (rw)
tmpfs on /dev/shm type tmpfs (rw)
AFS on /afs type afs (rw)

这个疯狂的混合显示了大量不同的文件系统,包括ext3(基于标准磁盘的文件系统),proc文件系统(用于访问当前进程信息的文件系统),tmpfs(仅用于临时文件的文件系统) )和AFS(分布式文件系统),它们都粘在一起,集成到这台电脑的文件系统树上。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值