文件和文件目录
open
, read
, write
, close
, seek
) ./dev/sda2
)、终端 (/dev/tty2
)、打印机等都表现为 /dev
目录下的特殊文件. 对这些文件进行读写操作就相当于与对应的硬件设备交互./boot/vmlinuz...
) 和内核运行时的状态、参数 (/proc
目录下的各种”文件”) 也都以文件形式存在.open
, read
, write
, close
) 来处理普通文件、设备、管道等, 大大简化了编程模型.(这是UNIX/Linux系统中常见的文件类型, 可以通过 ls -l
命令的第一个字符看到)
-
普通文件 (Regular File): 最常见的文件类型, 包含用户数据. 可以是文本文件 (ASCII/UTF-8) 、二进制文件 (可执行程序、图片、音视频) . 内容由应用程序解释.d
目录文件 (Directory File): 特殊的文件, 其内容是该目录下包含的文件和子目录的列表 (目录项) . 它提供了文件系统的层级结构.c
字符特殊文件 (Character Special File): 用于与字符设备 (一次传输一个字符, 无缓冲) 进行交互的文件. 例如:/dev/tty1
)/dev/ttyS0
)/dev/null
):写入它的数据被丢弃, 读取它得到文件结束符./dev/random
, /dev/urandom
)b
块特殊文件 (Block Special File): 用于与块设备 (一次传输固定大小的数据块, 有缓冲) 进行交互的文件. 例如:/dev/sda
, /dev/nvme0n1
)/dev/sda1
)/dev/sr0
)p
管道文件 (Pipe File / FIFO): 用于实现进程间通信. 一个进程写入管道的数据可以被另一个进程读取, 遵循”先进先出” (FIFO) 原则.s
套接字 (Socket): 用于网络通信或本地进程间通信. 网络通信的端点.l
符号链接文件 (Symbolic Link File): 包含指向另一个文件或目录的路径名. 访问符号链接时, 系统会沿着它指向的路径去访问目标. 类似于Windows快捷方式.seek
操作定位到任意字节位置进行随机访问.seek
) , 允许程序显式地移动读写指针到指定位置.0
: 表示该块未使用 (空闲) .下一个块号
: 指向文件逻辑上的下一块.文件结束标记
(如-1, FFF, FFFFFFFF): 表示这是文件的最后一个块.坏块标记
: 表示该块已损坏, 不可用.树形目录结构 (Tree-structured Directory):
/
in Unix/Linux, C:\
in Windows) . 示例: /home/user/documents/report.txt
/home/user
, 则相对路径 documents/report.txt
指向与上面绝对路径相同的文件.cd
) 改变当前目录.目录操作 (常用系统调用或命令):
mkdir
/ CreateDirectory
: 创建一个新的空目录.rmdir
/ RemoveDirectory
: 删除一个空目录.opendir
: 打开一个目录以准备读取其内容. 返回一个目录流指针.readdir
: 读取目录流中的下一个目录项.closedir
: 关闭先前打开的目录流.rename
: 重命名文件或目录, 或将其移动到同一文件系统下的另一个目录.link
: 创建一个指向现有文件的新链接 (硬链接) .unlink
/ remove
: 删除一个文件名 (目录项) . 如果是硬链接, 减少链接计数;如果是最后一个链接, 则删除文件.ls
/ dir
: 列出目录内容.示例:解析文件名 /User_B/Draw/ABC
(在提供的示例图中)
User_B
: 在根目录文件中搜索名为 “User_B” 的目录项.User_B
: 找到该项, 获取它指向的 “User_B” 目录文件的位置 (比如起始块号或inode号) . 读取 “User_B” 目录文件.Draw
: 在 “User_B” 目录文件中搜索名为 “Draw” 的目录项.Draw
: 找到该项, 获取 “Draw” 目录文件的位置并读取.ABC
: 在 “Draw” 目录文件中搜索名为 “ABC” 的文件项.(目录文件内部如何存储和组织目录项)
(不同文件系统布局不同, 但通常包含以下部分)
$LogFile
(日志), $Volume
(卷信息), $AttrDef
(属性定义), $\
(根目录), $Bitmap
(卷空间位图), $Boot
(引导文件副本), $BadClus
(坏簇文件) 等. 这些文件的内容可以分布在数据区.目的: 为了提高文件访问效率和管理并发访问, 操作系统在内存中维护一系列与文件系统相关的数据结构.
主要结构:
文件打开流程关联: 当进程 open()
一个文件时:
open()
返回 fd 给用户进程.文件读写流程关联: 当进程 read(fd, ...)
或 write(fd, ...)
时:
i
位代表磁盘上的第 i
块.1
表示块空闲, 0
表示块已分配 (或者反过来, 约定一致即可) .k
个块时, 在位图中搜索 k
个连续的 1
(对于连续分配) 或任意 k
个 1
(对于非连续分配). 找到后, 将对应的位设置为 0
, 并返回块号.i
时, 将位图中第 i
位设置为 1
.s.n
.s.n > 0
, 直接从内存列表 s.free
中取出一个块号 (如 s.free[s.n]
) , 将 s.n
减 1, 返回该块号. (无磁盘 I/O)s.n == 0
(内存中的组已用完) :next_group_leader
(可能存在 s.free[0]
中).next_group_leader == 0
, 表示磁盘空间已满, 分配失败.next_group_leader
这个磁盘块的内容 (包含下一组的数量和块号列表) 到内存的 s.n
和 s.free
中. (一次磁盘读)next_group_leader
这个块本身作为本次分配的结果返回.block_num
)s.n == 100
).block_num
加入内存列表 s.free
, 并将 s.n
加 1. (无磁盘 I/O)s.n
和 s.free
列表 (包含100个块号) 写入到要回收的 block_num
这个磁盘块中, 使其成为新的组长块. (一次磁盘写)s.n
置为 1.s.free[1]
(或 s.free[0]
) 设置为 block_num
(现在只知道这一个新的组长块是空闲的).f = open(name, flag); ... read(f, ...); write(f,...); seek(f,...); ... close(f);
create(文件路径名, 访问权限)
(可能作为 open
的一个标志 O_CREAT)open
标志决定是报错还是覆盖 (截断) .open
时创建) , 或返回成功状态.fd = open(文件路径名, 打开方式)
打开方式
(读/写/读写) 和用户的身份 (UID/GID), 与从 FCB/i-node 中获取的文件权限进行比较. 如果权限不足, 打开失败, 返回错误.fd
.fd
: 将文件描述符返回给调用进程.close(fd)
fd
在当前进程的打开文件表中定位到对应的表项. 如果 fd
无效, 返回错误.close
操作完成, 直接返回.bytes_read = read(fd, buffer, length)
fd
定位到进程打开文件表项, 再找到系统打开文件表项, 获取 i-node/FCB 信息和当前进程的读写指针.length
字节, 但不能超过文件末尾. 计算出需要读取的逻辑块号范围和块内偏移.buffer
.buffer
.bytes_read
).bytes_read
(可能小于 length
, 如果遇到文件尾或错误) .bytes_written = write(fd, buffer, length)
length
字节. 计算涉及的逻辑块号.buffer
拷贝到内核管理的缓存页. 这些缓存页被标记为”脏页 (Dirty Page)”.sync
命令、close
文件且引用计数为0) 发生时, 才批量地将脏页数据写回磁盘. 这可以提高性能 (合并写操作、利用磁盘调度) . 也可以配置为写穿 (Write Through) 模式, 即每次写都立即同步到磁盘, 可靠性更高但性能较低.bytes_written
).bytes_written
(通常等于 length
, 除非磁盘空间不足或发生错误) .new_offset = lseek(fd, offset, whence)
fd
定位到对应的进程打开文件表项.whence
参数指定 offset
的基准:SEEK_SET
: offset
是相对于文件开头的绝对位置.SEEK_CUR
: offset
是相对于当前读写指针的相对位移.SEEK_END
: offset
是相对于文件末尾的相对位移 (通常 offset
为负或0) .whence
, offset
和当前文件大小, 计算出新的绝对偏移量 new_offset
. 需要检查新位置是否有效 (如不能为负, 有时不允许超出文件尾) .new_offset
.new_offset
, 或返回 -1 表示错误.rename(old_path, new_path)
mv oldname newname
)old_path
父目录中的 oldname
目录项.newname
.newname
是否已存在 (可能需要先删除或报错) .mv old_path /new_dir/newname
)old_path
的目录项, 获取其指向的 FCB/i-node 号./new_dir
是否存在且有写权限./new_dir/newname
是否已存在./new_dir
目录文件中创建一个新的目录项, 文件名为 newname
, 指向与 old_path
相同的 FCB/i-node 号.old_path
的父目录文件中删除原来的目录项.rename
系统调用通常不支持跨文件系统移动. 需要应用程序先复制文件, 再删除源文件.cp source_path destination_path
open(source_path, O_RDONLY)
获取源文件描述符 fd_src
.open(destination_path, O_WRONLY | O_CREAT | O_TRUNC)
获取目标文件描述符 fd_dest
. (O_TRUNC
表示如果目标文件已存在则清空) .while ((bytes_read = read(fd_src, buffer, BUFFER_SIZE)) > 0)
: 从源文件读取一块数据到内存缓冲区 buffer
.write(fd_dest, buffer, bytes_read)
: 将缓冲区的数据写入目标文件.close(fd_src); close(fd_dest);
ln target link_name
):target
文件的 i-node.link_name
.link_name
目录项中的 i-node 号设置为与 target
相同的 i-node 号.rm link_name
或 rm target
):/home/A/data.txt
. 用户 B 执行 ln /home/A/data.txt /home/B/shared_data.txt
. 此时, 两个路径指向同一个 inode, 链接计数为 2. 用户 A 删除 /home/A/data.txt
后, 文件依然存在, 用户 B 仍可通过 /home/B/shared_data.txt
访问, 链接计数变为 1. 只有当用户 B 也删除 /home/B/shared_data.txt
后, 文件才会被真正删除.ln -s target link_name
): 创建一个类型为 ‘l’ 的新文件 link_name
, 并将字符串 target
(可以是绝对路径或相对路径) 写入该文件的内容区域.link_name
时, 它会识别出这是一个软链接文件, 然后读取其内容 (即路径字符串 target
) , 并重新开始从该路径 target
去查找目标文件. 这个过程称为符号链接解析 (Dereferencing).mount
命令, 提供两个主要参数:/dev/sdb1
) 、卷标、UUID 或网络路径 (如 server:/share
)./mnt/usb
, /media/cdrom
).ls /mnt/usb
) 实际上访问的是被挂载文件系统的根目录. 挂载点目录下原来的内容会被临时隐藏, 直到卸载.unmount
命令, 提供设备名或挂载点路径./usr/ast/mbox
(结合课件图)/
) 的 i-node 号 (假设为 2) .LONGFI~1.TXT
) .$STANDARD_INFORMATION
: 包含时间戳、权限标志等标准信息.$FILE_NAME
: 文件名 (可以有多个, 如长文件名、DOS 短文件名、硬链接名) .$DATA
: 文件的数据内容 (默认的、未命名的流) . 一个文件可以有多个数据流 (备用数据流 Alternate Data Streams) .$SECURITY_DESCRIPTOR
: 文件的访问控制列表 (ACL).$INDEX_ROOT
, $INDEX_ALLOCATION
, $BITMAP
: 用于实现目录 (索引) 的属性.$INDEX_ROOT
属性中 (常驻) , 并按文件名排序.$INDEX_ROOT
只存储部分信息和 B+ 树的根节点. 实际的目录项存储在 MFT 外部的索引缓冲区 (通常 4KB 大小的簇) 中, 这些缓冲区通过 B+ 树结构组织和链接. MFT 记录中的 $INDEX_ALLOCATION
属性 (非常驻) 存储指向这些索引缓冲区的映射信息, $BITMAP
属性跟踪索引缓冲区的分配情况. 这使得大目录的查找、插入、删除效率很高.chkdsk
) 的需要.open
, read
, write
, stat
等) , VFS 层会根据操作的文件路径判断其属于哪个实际的文件系统 (通过挂载点信息) , 然后调用该具体文件系统驱动提供的相应实现函数.inode
(代表文件元数据) 和 dentry
(Directory Entry Cache, 代表路径名组件) 等 VFS 层抽象对象, 以及 file
(代表打开的文件实例) 和 superblock
(代表挂载的文件系统实例) 对象, 这些对象都包含指向具体文件系统实现的操作函数指针表.练习1 (Exercise 1)
题目 (Problem Statement):
A
->B
->C
, D
->E
, F
->G
, etc.)。图中省略号表示有若干内容未显示。 ROOT (内存中 In Memory)
|
+---+---+
| | |
A B C ...
|
+--+--+
| |
D E ...
|
+--+--+
| |
F G ...
|
+--+--+
| |
H I ...
|
+--+--+
| |
J K ...
plaintext/A/D/G/I/K
中的某一块,最少要启动磁盘几次?最多要启动磁盘几次?/A/D/G/I/K
的第55块,最少启动硬盘几次?最多几次?/A/D/G/I/K
的第5555块,最少启动硬盘几次?最多几次?解答 (Solution):
分析共同部分:路径查找 (Path Traversal)
/A/D/G/I/K
A
, D
, G
, I
来找到文件 K
的目录项 (其中包含 K
的 FCB 地址)。60 / 10 = 6
个磁盘块。/A/D/G/I
的磁盘访问次数:A
(Root 在内存): 读目录 A
找 D
,最少 1 次,最多 6 次。D
(在 A
中找到入口): 读目录 D
找 G
,最少 1 次,最多 6 次。G
(在 D
中找到入口): 读目录 G
找 I
,最少 1 次,最多 6 次。I
(在 G
中找到入口): 读目录 I
找 K
的 FCB 地址,最少 1 次,最多 6 次。分析文件访问部分 (File Access - K):
K
的目录项后,需要读取 K
的 FCB。综合计算各问题:
Min_Total = Path_min + FCB_read + Data_read = 4 + 1 + 1 = 6
次。Max_Total = Path_max + FCB_read + Index_reads + Data_read = 24 + 1 + 3 + 1 = 29
次。 (3 次索引块读取 + 1 次数据块读取)Min_Total = Path_min + FCB_read + Data_reads = 4 + 1 + 55 = 60
次。Max_Total = Path_max + FCB_read + Data_reads = 24 + 1 + 55 = 80
次。Min_Total = Path_min + FCB_read + Data_read = 4 + 1 + 1 = 6
次。Max_Total = Path_max + FCB_read + Data_read = 24 + 1 + 1 = 26
次。总结:
练习2 (Exercise 2)
题目 (Problem Statement):
mkdir /A
mkdir /A/B
create /A/B/File1
(占用 4 块/簇)mkdir /C
mkdir /D
mkdir /C/E
create /C/E/File2
(占用 16 块/簇)mkdir /C/E/F
create /C/E/F/File3
(占用 8 块/簇)create /C/E/F/File4
(占用 2 块/簇)解答 (Solution):
磁盘和块信息:
1. 简化 UNIX 文件系统布局描述:
2. FAT16 文件系统布局描述:
.
和 ..
两个特殊项) .-t <type>
: 指定要创建的文件系统类型 (如 ext4
, vfat
, ntfs
, xfs
).-F <fat_type>
(for mkfs.vfat): 指定 FAT 类型 (如 12, 16, 32).-b <block_size>
: 指定块大小.-L <label>
: 设置卷标.-N <number_of_inodes>
(for mkfs.ext*): 指定 i-node 数量.