【2021/7/19更新】【梳理】简明操作系统原理 第十三章 文件和目录(docx)

配套教材:
Operating Systems: Three Easy Pieces Remzi H. Arpaci-Dusseau Andrea C. Arpaci-Dusseau Peter Reiher
参考书目:
1、计算机操作系统(第4版) 汤小丹 梁红兵 哲凤屏 汤子瀛 编著 西安电子科技大学出版社

在线阅读:
http://pages.cs.wisc.edu/~remzi/OSTEP/
University of Wisconsin Madison 教授 Remzi Arpaci-Dusseau 认为课本应该是免费的
————————————————————————————————————————
这是专业必修课《操作系统原理》的复习指引。
需要掌握的概念在文档中以蓝色标识,并用可读性更好的字体显示 Linux 命令和代码。代码部分语法高亮。
文档下载地址:
链接:https://pan.baidu.com/s/1hMSwtYGLZg3tC_cXQm74BQ
提取码:0000

十三 文件和目录

文件(file)的本质,是一个一维的字节数组,可以进行读写。每个文件都有一个低级名称(low-level name),通常是一个数字,用户一般不会注意到这个名称。由于历史原因,低级名称常被叫做索引节点号(inode number),这个将在未来的章节中学习。现在只需知道:每个文件都有一个索引节点号。
其实很多操作系统对文件的具体结构都不知道太多(比如这个文件是图片、文档还是C / C++代码)。操作系统对文件的责任,主要是存取数据到磁盘上,确保当你需要原先存储的数据时,能够将其正确读出来。
目录(directory)也是文件的一种,也称为目录文件(directory file)。目录包含了一系列有序对,有序对的两个元素分别是用户可读的名称(即文件名)和索引节点号。

可以用目录树(directory tree)来表示目录间的关系:目录中的目录在树上能够清晰地表示出来。

UNIX系统的目录树总是从根目录“/”开始,“/”符号也是目录树上相邻目录的分隔符。我们可以看到,从任意一个目录对应的节点向下走一步,就定位到了该目录的一个子目录或该目录包含的文件。设字符串的内容一开始为空,从目录树的根节点一路走下来,每向下走到一个非根节点就将该字符串先添加一个“/”,再添加走到的节点对应的目录名或文件名,直到找到所需的目录或文件。最后,这个字符串含有的内容就叫做找到的文件或目录的绝对路径名(absolute pathname),或绝对路径(absolute path)。

扩展名(extension)标示了一个文件的类型。但是标注扩展名并不是强制的。在UNIX中,访问磁盘、USB存储设备和光驱乃至许多其它设备都可以通过目录树。

运行下面的代码:

#include <fcntl.h>
int main() {
int fd = open(“foo”, O_CREAT);
return 0;
}
open()系统调用配合宏定义O_CREAT,在当前目录下创建一个文件。当第二个参数增加O_WRONLY和O_TRUNC时,分别使得该文件只能被写入,或删除已有内容(即截断)。传入多个这种参数时,用或运算符“|”连接。
open()返回的是文件描述符(file descriptor)。它是一个int型数据,为每个进程私有。文件描述符在UNIX系统中用于访问文件。一个文件打开后,可以用open()返回的文件描述符来对文件进行操作(前提是你有相应的权限)。文件描述符可以看作一个文件的指针。
UNIX系统内核中存在专门的结构来追踪每个进程已打开的文件。系统范围的已打开文件表(open file table)的每一项对应一个进程的这种结构。

在终端上依次输入:
echo hello>foo
sudo cat foo
sudo vim foo
可以看到,不但终端出现了输入的内容,文件中也出现了我们输入的内容。(提示输入管理员密码时,请输入超级用户的密码)


strace是一个很实用的工具。它可以追踪一个程序的系统调用,查看函数调用时传递的参数(实参)和返回值。在终端输入strace -h获得更多帮助。

打开文件后,使用系统调用lseek()可以重新定位文件中的偏移。在一个文件打开后,操作系统会追踪这个文件读写到什么位置。一次读取或写入n个字节后,这个偏移就增加n,下一次读写总是从这个偏移指向的位置进行。使用lseek可以将偏移定位到自己想要的位置。不过,lseek并不进行磁盘寻道,而只是修改操作系统追踪的偏移变量的值。只有在偏移指定的位置发起一次读写操作时,才会引发磁盘寻道(如果需要)。
lseek的参数如下:

__off_t lseek (int __fd, __off_t __offset, int __whence)
第一个参数是文件描述符,第二个参数是偏移,第三个参数有三种:
当设为SEEK_SET时,偏移设为第二个参数本身;当设为SEEK_CUR时,偏移设为当前的偏移加上第二个参数;当设为SEEK_END时,偏移设为文件的长度加上第二个参数。

系统调用和分别用于读写文件:

ssize_t read (int __fd, void *__buf, size_t __nbytes)
ssize_t write (int __fd, const void *__buf, size_t __n)
各个参数的作用已经非常明显了,这里不再赘述。

已打开文件表中,每一项都有一个成员:引用计数(reference count)。每多一个子进程使用同一文件,该计数就加1。

系统调用dup()(以及其变体dup2()、dup3())用于创建一个指向同一个文件的新的文件描述符。dup()在编写UNIX shell或者在执行输出重定向等操作时很有用。

调用write()后,数据不会被立刻写入到文件,而是会在缓冲区停留一段时间(如,几秒);但有时候要求立刻将还在缓冲区的待写入数据写入相应文件,这时候就要用到系统调用fsync()。待写入数据全部写入完毕后,fsync()才返回。
但是,fsync不会写入对指向文件的目录项的修改。也就是说:如果新创建了一个文件,要是确保下次能正确读出的话,就需要把所在目录也fsync一下。这一点常被忽略,从而引发了许多bug。

mv命令可以重命名或移动文件。这个操作是原子的。如果计算机在重命名期间断电或崩溃了,那么被操作的文件要么被修改为新名称,要么仍然保持旧名称。这一点对要求原子更新操作的应用程序很有用。该命令调用系统调用rename()来完成重命名。但实际上rename()的执行过程是:将新文件写入指定位置,同时删除原文件。

除了访问文件以外,我们希望文件系统还能为每个文件记录一些有用的信息。这些信息称为元数据(metadata)。查看元数据可以使用系统调用stat()或fstat()。在命令行中也可以使用命令stat来查看元数据。元数据一般会保存于inode(或类似的数据结构)中。
头文件<stat.h>中定义了stat和stat64结构体。篇幅所限,这里不予列出其定义。
不同的文件包括的元数据不一定是相同的。例如,文档文件的元数据包含作者、标题、日期、关键字等;音乐文件的元数据包含标题、艺术家、专辑、流派、年代等。
我们在命令行中查看刚才创建的新文件的信息。输入:
stat foo
结果:

索引节点中,保存了一些文件信息;所有的索引节点都保存在硬盘上。当然,有一些会被读入内存,以加快访问速率。

目录是不能直接写入的,因为目录的格式被认为是文件系统元数据的一部分。文件系统认为自己要对目录数据的完整性负责。如果要修改一个目录文件的内容,只能间接通过创建文件、创建目录等方式。
创建目录使用系统调用mkdir()。终端亦可直接使用mkdir命令。	
创建目录后,虽然这个目录包含了少量内容,但一般还是认为它是空的。用“.”代表当前目录,“..”代表上级目录。使用ls -a可以将当前目录下的文件连同目录本身及其上级目录一起列出。使用“ls -al”还可以将它们的常见信息也一并列写。


使用Linux时,一定要当心一些存在危险性的命令。例如:
rm *
会删除当前目录下的全部文件。
rm命令不会删除不为空的目录。但是如果要求递归删除(-r)、强制删除(-f),命令的危险性就比较大了。例如:
sudo rm -rf /*
将会删除整个计算机的全部文件和目录。
关于错误使用rm -rf的后果,大家可以在网络上查找相关的资料。

下面的代码展示了目录的常见系统调用的用法:

#include <dirent.h>
#include <unistd.h>
#include
#include
int main() {
DIR* dp = opendir(".");
assert(dp);
dirent* d;
while (d = readdir(dp)) {
printf("%lu %s\n", d->d_ino, d->d_name);
}
closedir(dp);
return 0;
}
结构体dirent和dirent64的定义如下:
struct dirent {
#ifndef __USE_FILE_OFFSET64
__ino_t d_ino;
__off_t d_off;
#else
__ino64_t d_ino;
__off64_t d_off;
#endif
unsigned short int d_reclen;
unsigned char d_type;
char d_name[256]; /* We must not include limits.h! */
};

#ifdef __USE_LARGEFILE64
struct dirent64 {
__ino64_t d_ino;
__off64_t d_off;
unsigned short int d_reclen;
unsigned char d_type;
char d_name[256]; /* We must not include limits.h! */
};
#endif
d_name、d_ino、d_off、d_reclen、d_type分别代表:
文件名、索引节点号、离下一个dirent的偏移、文件名长度、文件类型。
其实目录并不存储太多信息,而是提供一个到索引节点的映射。一个程序如需获得更多信息,就需要调用stat()。在使用ls命令的时候,-l参数就调用了这个系统调用。

rmdir系统调用(终端下为rmdir命令)用于删除目录。当然,如果目录非空,会拒绝删除。rm既可以删除文件也可以删除目录。

系统调用link()需要两个参数:旧路径和新路径。当你将新文件名链接到旧文件名时,就创建了另一个可以访问这个文件的方式。这样创建的链接称为硬链接(hard link)。
link()在创建链接的目标目录创建一个新文件名,然后将其指向与原文件相同的索引节点号。被链接的文件不会被复制,不过现在可以通过两个文件名访问同一个文件了。对于文件系统来说,这两个文件名没有任何区别。
在命令行下执行:
ln foo foo2
ls -i foo foo2
可以看到foo和foo2的索引节点号都是一样的:

创建硬链接后,相应的索引节点号中的引用计数(reference count,有时也称链接计数,link count)会增加1。rm也可以移除硬链接,移除以后引用计数减去1。但是,除非引用计数降低到0,原文件不会删除。unlink也可以移除链接,但不能通过unlink删除目录。当删除文件时,rm和unlink的效果是完全一样的。



符号链接(symbolic link)也称软链接(soft link)。硬链接的使用有限制:
1、不能为目录创建硬链接。
(1)硬链接到父目录或祖先目录时,允许沿着环路不断地访问子文件夹,导致死循环。
(2)硬链接到非父目录与非祖先目录,但该目录下又有到父目录与祖先目录的硬链接。此时同样也会形成环路,允许沿着这样的环路不断访问子文件夹,陷入死循环。
2、也不可以硬链接到位于其它磁盘分区的文件。因为索引节点号只在同一个文件系统的实例中有效,在其它文件系统的实例中无效。而软链接不存在这些限制。
使用ln命令时,加上-s,就可以创建软链接。
但是软链接和硬链接不同。通过stat命令就可以看出来。在终端下输入:
touch test
ls
ln -s test test2
stat test
ls
stat test2
其中,touch命令用于修改文件或者目录的时间属性,包括存取时间和更改时间。若文件不存在,系统会建立一个新文件。
结果:

硬链接是普通文件(regular file),但软链接与硬链接的文件类型是不同的。我们可以看到,空文件test的大小是0字节,但软链接test2的大小却是4字节。因为软链接保存了链接到的文件的路径。如果被链接的文件的路径(包括名称)很长,那么软链接的大小还会更大。
现在,我们将被链接到的文件test移除,然后再访问它的软链接test2。在终端下输入:
rm test

可以看到,原文件移除后,软链接就失效了。这称为悬挂引用(dangling reference)。UNIX中的软链接相当于Windows系统中的快捷方式。

与内存不同,每个进程都具有自己单独的内存空间,但文件常常为大量程序所共享。因此,我们有必要使用新的机制来划定文件的访问权限。
通过ls -l可以查看文件的权限位(permission bits):

前面的“-rw-rw-r--”就是权限位的内容。第一位为“-”,代表其为普通文件(d为目录,l为软链接),与权限无关。剩下的9位分成3组:owner、group和other。每组的三位要么为“-”,要么为r(读)、w(写)或x(执行)。在本例中,后面的两个andy,第一个是所有者,第二个是组名,也就是group指代的名称。
通过命令chmod可以改变权限。输入:
chmod 600 demo.cpp
结果:

600是八进制数,转换成二进制是110 000 000,即rw-------。
如果一个可执行文件的执行位没有置位,那么试图执行该文件时将因为权限不足而被拒绝。
对于目录,执行位的意义有一点不同。关于权限位的作用,请大家多进行上机实验。

mount命令可以将其它文件系统挂载(mount)到指定的目录下。例如:Android手机中,TF卡被挂载的位置为/storage/emulated/0。被挂载的位置称为挂载点(mount point)。通过挂载点可以访问被挂载的文件系统中的文件。
挂载的意义是:使不同的文件系统可以通过一个统一的目录树进行访问。该命令还可以查看系统中已经挂在的文件系统及挂载点。

TOCTTOU(Time Of Check To Time Of Use)可以被用于攻击。举例:
设有一个邮件服务运行在root下。这个服务接收到了一封新邮件。经检查发现,这封邮件确实是属于收件人的普通文件,并不是重定向到邮件服务不应该更新的其它文件的链接,没有问题。所以,邮件服务决定将这封邮件正常投递至收件箱(即向对应的目录新增这个文件)。
但就在这时,攻击者(接收到该邮件的用户)设法获得了下一个时间片,CPU转而执行攻击者发出的rename()系统调用(或建立一个链接)。因为之前的检查已经允许了接下来的访问,所以该系统调用得以进行。这个调用将新邮件指向了存放用户名及密码的文件夹(假设收件箱和保存密码的位置都已被攻击者确定)。如果邮件内容是一个新的具有最高权限的用户名及其密码,那么攻击者就获得了最高权限。他将可以通过这个权限用户执行特权级操作。
防范TOCTTOU攻击目前并没有普适的办法。常见的措施有:向open()传递一个参数O_NOFOLLOW,一旦尝试打开一个符号链接则立即拒绝。一些更激进的策略还有:使用事务型文件系统(transactional file system)、尽量减少拥有最高权限的程序数量。但事务型文件系统应用并不广泛,所以比较稳妥的办法大概也只有后者。
获得CPU时间片的方法主要有:文件系统迷惑(file system mazes),以及算法复杂度攻击(algorithmic complexity attacks)等。文件系统迷惑,是强制应用程序读取一个不在操作系统缓冲中的目录入口,于是操作系统可能将被攻击的应用程序置于睡眠状态(随后从磁盘中读取)。而算法复杂度攻击是强制应用程序浪费掉操作系统为它分配的CPU资源(使得攻击目标的优先级往下掉)。例如:攻击者可以创建一大堆hash值相同的文件,从而导致内核hash表的链非常长,然后让用户去索引相关的文件,耗费其资源(在拒绝服务的漏洞中也有这种攻击)。

Linux中,文件类型只有以下这几种:
1. -,普通文件。
2. d,目录文件,d是directory的简写。
3. l,软链接文件,亦称符号链接文件。s是soft或者symbolic的简写。
4. b,块文件,是设备文件的一种。b是block的简写。
5. c,字符文件,也是设备文件的一种。c是character的文件。
I / O设备统一被视为文件可以方便管理。只是对这些文件的操作由设备驱动程序来完成。
硬链接是普通文件。FLAC是哪种文件?普通文件。纯文本是哪种文件?普通文件。M2TS是哪种文件?普通文件。……
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值