奇特析拓熵衍中

\documentclass[UTF8,11pt]{ctexart} % 使用 CTeX 中文支持
\usepackage[
    paperwidth=8.5in,
    paperheight=11in,
    margin=10pt            % 去除所有页边距
]{geometry}               % 控制页面布局
\usepackage{multicol}     % 分栏支持
\usepackage{lipsum}       % 示例文本(可删除)
\usepackage{xcolor}
\usepackage{tabularx}
% 可选:调整栏间距(默认10pt)
\setlength{\columnsep}{1cm} 
\begin{document}
\pagestyle{empty} % 移除页眉页脚

% 自动分栏环境(内容填满第一栏后自动进入第二栏)
\begin{multicols}{2}   
\section*{虚拟内存(Virtual Memory)}
每个进程都有其专有的虚拟内存空间,往往是 32 位或 48 位地址,它们需要被分配至物理内存,分配的最小单位是\textcolor{red}{页(page)},一般为 4kB,但也可以大至数 MB 乃至 GB。存在未分配的 page。\textcolor{red}{页表(page table)}将虚拟内存地址翻译为物理内存地址,每一个 virtual page 分配一个 \textcolor{red}{PTE (Page Table Entry)},每个 PTE 包括:\textbf{Valid Bit} 标志是否被占据,\textbf{Access Rights} 即 RWX(特别地,W 和 X 权限一般而言不可共存),\textbf{Physical Page Number} 又称 \textbf{Frame Number} 映到物理内存的地址。一个虚拟地址的前若干位会定位到对应的 PTE,后 12 位(因为 page size 是 4kB)是 offset,在映到物理内存后 offset 仍然保持。\textcolor{green}{因此,Physical Page Number 只包括除 offset 以外的部分,而 Virtual Page Number 同样如此。}

VM 可以用于内存管理、内存保护,还可以在 \textbf{多个进程间分享数据(指向同一个物理内存)},同时也是一种 caching。其由 OS 管理,被硬件所使用(从 PT 中查找并将虚拟地址翻译为物理地址)。

虚拟内存有三种状态:unallocated,allocated but unmapped(此时存储内容会自动被交换到外存,如磁盘和 SSD),allocated and mapped(此时指向了物理内存即 DRAM)。DRAM 就像是磁盘/SSD 的一种由软件管理的 cache,每个 page 是一个 cacheline,而这个 cache 是 fully associative 的,因为 miss 就要从外存中调数据,代价过大;另一方面,由操作系统管理的页表总是知道数据存储位置:\textbf{page hit} 的场合是在 DRAM 上,否则作为一种称作 \textbf{page fault} 的 exception,此时在磁盘上某处。Page Fault 由硬件检测,但是需要软件介入以 \textbf{demand paging},把外存中的东西 \textbf{page in}(\textcolor{green}{这不需要 CPU 干涉:其由另一硬件 DMA 执行})。因为这很慢,所以会挂起出现 page fault 的进程,直到 paged in 后再唤起。paged in 的 victim 可以使用各种与 cache 相同的规则。

在访问一段虚拟内存时,首先先检查 Valid Bit,如果其为 1 的话即表示现存于内存中;然后硬件会检查权限,如果权限不对则返回 permission fault,否则访问具体物理内存。否则其为 0,则返回 page fault,然后视情况而定:如果是程序问题(访问了未分配内存)则 RE 并终止进程,否则 demand paging。

由 locality,一个程序只会访问一段临近的页即 working set:如果其在内存中存得下则几乎不会 page fault,否则出现了 \textcolor{blue}{thrashing},即系统比起真正执行任务,更多时间反倒在处理 page fault,此时 IO rate 高(要频繁与磁盘交互)但是 CPU 利用率低(要等待 page in),唯一解决方案是开大内存。存储工作集的东西是 \textcolor{red}{Transaction Look-aside Buffer(TLB)},其中的每一项的内容包括:valid bit、dirty bit 等(视具体实现而定),以及必不可少的:virtual page number 当作 tag,和 PTE 中的数据。如果在 TLB 中 miss 了,就要到完整的 PTB 中找,此乃 \textbf{page table walk},软件硬件均可处理。同时,为解决 page table 大小过大的问题,使用 radix tree 存储所有目前 mapped 的 PTE。x86-64 结构使用了 5 层,每层 9 位(512 叉)radix tree 存储 PTE。具体而言,多层 PTE 的结构是,高层的 PTE 指向低层 PTE。

\textbf{Page Sharing} 是在不同进程间分享 page 的过程,即两个进程的虚拟内存指向同一处物理内存。\textbf{Copy-on-Writing(CoW)} 策略是一种处理复制的策略,即复制后的数据仅仅是虚拟内存指向原始物理内存,并在 PTE 中将这个 page 标记为 write-protected。然后,在原本/副本被写入时,报 permission fault 并作真正意义上的 copy。在 \texttt{fork} 新进程时,直接 CoW 复制整个 PTE 即可。而当使用 \texttt{execve} 等指令执行其它程序时,要将整个旧 page table 清空、使用可执行文件备份 program 和 statically initialized data、使用下文中提到的匿名文件备份栈。

虚拟内存还可以指向内存中的某个文件的存储位置以实现读写。可以指向三种文件:\textcolor{red}{普通文件 (regular file)},字面意思,在 memory mapping 时仅初始化 PTE,然后等到首次 page fault 时再 demand paging;\textcolor{red}{交换文件 (swapping file)},仅在物理内存不足时将某些内存文件手动交换到外存中以释放内存,由操作系统控制不需要手动操作;\textcolor{red}{匿名文件 (anonymous file)},存储程序运行过程中开的堆栈等信息,不显式地对应文件,因此只在首次写入时才真正分配内存,除此之外就默认返回 0。\texttt{mmap} 和 \texttt{mumap} 可以手动实现 memory mapping。

另一方面,\texttt{malloc(size)} 可以申请一个指向长度为 \texttt{size} 的内存的指针,并使用 \texttt{free(p)} 释放。\texttt{C++} 中使用 \texttt{new, delete}。但是问题是,按照来的顺序顺次占用物理内存,在多次 \texttt{malloc} 与 \texttt{free} 后就会出现碎片化的内存,此时总内存足够,但是不存在单个连续空间。更好的内存管理方式是将内存以倍增的尺度划分为各个尺度的连续段,例如长度为 $1,2,4,8,\dots$ 的内存段都存在,每次找到满足 $m\geq n$ 的内存段并占用,如果找不到就开大堆大小。这是 \textcolor{red}{外部碎片化 (external fragmentation)},同理有 \textcolor{red}{内部碎片化 (internal fragmentation)},实际使用的内存小于分配的内存,发生原因包括 padding for alignment、Allocator policy e.g., segregated lists、Metadata store, e.g., size, status 等(最常见的:最后一个 page 中剩下的零碎)。解决方案是可以把空出来的部分扔到一个 free list 里面,在将来提供给较小的需求。一个好的分配器应该在快的同时,最小化内外的碎片化,提高内存的利用率。

以上是 \textcolor{red}{显式内存分配},即使用 \texttt{malloc} 与 \texttt{free} 执行的 allocation。不过,更多的语言会选择 \textcolor{red}{隐式内存管理},这就涉及到一个叫做 \textcolor{red}{垃圾清理 (garbage collection)} 的任务,自动清理那些不再被需要的内存。但是什么内存是不需要的呢?如果没有指向它的指针的话。但是这样做需要让编程语言满足 \textcolor{blue}{``pointer is a pointer''} 的原则,它包括禁止指针与整型间的互相转化、强制指针指向 object 的开头、存在指针与整型间的明确区分方式等。然后就存在两种垃圾清理的方法:\textcolor{blue}{Mark-and-Sweep},拓扑地标记所有可达的内存,然后释放它们;\textcolor{blue}{Reference Counting},每个 object 维护指向其的指针数量,并在其归零时删除,优势是快,劣势是无法处理成环。

\section*{输入输出策略(I/O Strategies)}

输入输出的两个挑战:设备多样,需要统一的标准;有些设备很慢,还可能有难以预料的行为(随时随地操纵鼠标)。不过,它们总是可以被一些属性所刻画:行为(输入/输出/通信)、交互者(人/机器)、传输速率(MBps)等。如键盘等提供逐位的信息,而磁盘则可以成块读取(\textbf{数据粒度 Data Granularity});网络等提供序列化的信息,而磁盘等可以随机访问(\textbf{访问策略 Access Pattern});传输效率更是可以差很多数量级。此外,有一些 IO 操作是 \textbf{Blocking interface} 的,此时进程会挂起直到相应操作执行完毕;\textbf{Non-blocking interface} 的,此时进程多少做一点,并回复成功传输的字节数,但同时意味着可能做一半自己掐掉,比要求的总字节数要少;\textbf{Asynchronous interface} 的,立刻返回,在后台执行相应操作,并在执行完毕后通知用户。

IO 设备一般都通过 \textcolor{red}{IO bridge} 与 CPU 相连:CPU 间则使用 \textcolor{red}{Processor-Processor Bus}、CPU 与 Memory 间使用 \textcolor{red}{Processor-Memory Bus}。访问 IO 设备的方法之一是 memory-mapped IO,将未分配的 \textbf{物理内存地址} 分配给 IO 设备,被称作 \textcolor{red}{IO 地址}。对这些地址的读写实际上就是在与 IO 设备交互。因此,对 IO 地址的访问,要么使用 \texttt{read/write} 等系统指令,让系统自动翻译为与 IO 设备的相应互动,要么也可以用 \texttt{mmap} 等指令将虚拟内存与 IO 地址绑定。这就需要统一的接口。如 SSD、磁盘等支持随机访问的 \textbf{块设备(Block Device)} 一般都支持 \texttt{open()}, \texttt{close()}, \texttt{read()}, \texttt{write()}, \texttt{seek()} 等指令,而与之对应的 \textbf{流设备(Strean Device)} 则使用 \texttt{get()}, \texttt{put()} 或其它接口。

IO 设备会在某些场合需要通知系统与程序,例如异步任务完成、网络中传来数据、读磁盘时出现错误、用户按了键盘。但是问题在于,这些时间是非频繁的,并且发生时间不可预料。由此产生两种策略:\textcolor{red}{Pooling},IO 设备将信息放到一个 \textbf{status register} 中,OS 周期性查看。优势在于易于实现、可以由处理器完全控制,劣势在于周期不好确定,太慢则产生延迟,太快则浪费 CPU 算力。以及,\textcolor{red}{Interrupts},就如同出现 exception 一般告知处理器。不同的中断有不同的优先级:网络的 interrupt 优先级很高,因为数据流太快,如果不及时接收就会丢很多包,并且 buffer 也容易爆满,而用户的 interrupt 优先级就很低,因为人对延迟不敏感。好处是 CPU 可以在等 IO 完成前作其它运算,坏处是频繁的 context switch 引起额外开销,且实现复杂。如果想避免浪费 CPU 时间,则 interrupts 更好;如果想减少额外开销,则 polling 更好。polling 更适合常见、周期性的事件,这个设备应有一个稳定的传输速率并且需要时常的关注;罕见、不可预知、且可以自行启动(如磁盘读取等必须要 CPU 手动调用)的则更适合 interrupts。

以上的所有操作都需要 CPU 自己与 IO 互动,即所谓 \textcolor{red}{Programmed I/O};如果想节省 CPU 的干预,那就要使用 \textcolor{red}{直接内存访问 (Direct Memory Access, DMA)}。DMA 自己就是一个 IO 设备,可以有 memory-mapped interface。需要从处理器中接受 data transfer descriptor,包括 source、destination 和 length,且可以使用队列存储多个 descriptor 并依次处理。处理器使用任务性质(R/W)、来源或目标的内存地址、要传输的字节数,在数据准备好后自动开始传输,在出现 error 或完成时通知处理器,因为全搞完才通知,所以使用 interrupting;DMA 只在大规模传输时有效果,因为它的 setup 需要额外的成本。

DMA 除了使用虚拟地址外,也可以使用物理地址。此时要解决几个问题:1.在物理内存中的页可能会被 swapped out。其解决方案是 \textcolor{blue}{memory pinning},阻止 DMA 所在区域被换出去。2.连续的虚拟内存不一定在物理上页连续。解决方案是将众多单页需求串联并在结束时仅发送单个 interrupt,或者干脆使用虚拟内存。3.DMA 所需数据的最新版本可能压根不在内存里而在 cache 里,在从 IO 设备中读入时,要把 cache 中存储的数据打上过时的标签;写入 IO 设备时,则可能需要从 cache 中获取最新的数据。解决方案是在 IO 前 flushes 整个 cache 或是强制 writeback,可以选择性实施或者对整个 cache 实行,开销可能很大;或者可以把内存 route 到 cache 上,需要在 cache 中搜寻该内存的 copy,可能会引发负优化。

\section*{输入输出设备(I/O Devices)}

\textcolor{red}{磁盘 (Magnetic Hard Disk)} 是 long-term、non-volatile 的存储方案,规模大且便宜,但是读取较慢。Disk 由 Platter 组成,Platter 有两个 surface,每个 surface 有多个 track,track 被分割成多个 sector,一般是 512B 每 sector。传输时,会将多个 sector(常常是多层间作为一个 cylinder 读取)以一个整体作为 block 发送。\textcolor{blue}{磁盘访问时间 (Disk Access Time)} 由以下部分构成:1.\textcolor{blue}{controller overheads}。2.\textcolor{blue}{queuing time} 如果还有其它请求。3.\textcolor{blue}{seek time} 将读取头移动到目标轨道,一般是数毫秒,较慢。4.\textcolor{blue}{rotational latency} 等待目标 sector 旋转到读取头,平均会是旋转半圈的时间。5.\textcolor{blue}{data transfer time} 是整个 sector 通过读取头的时间。小规模的数据读取主要由 seek time 和 rotational latency 决定,可以使用 \textcolor{blue}{disk scheduling} 提高数据的 locality,或者搭配 \textcolor{blue}{caching} 使用。Scheduling 的策略即处理请求的顺序,如 FIFO 策略对请求是公平的(不会 starving)但是慢,Short Time Seek First (SSTF) 选择离读取头最近的任务,提高效率但引起 starving,SCAN 会让读取头在盘面上来回扫,不会引起 starvation、效率高,但是会偏好居中的数据,C-SCAN 则只在来时读取,回程不读取数据,是公平的。

\textcolor{red}{Solid State Drive(SSD)} 则不牵涉到物理移动操作,效率更高。每页 4kB,32 至 128 页构成一个 block,读取以页为单位进行,延迟约 20μs,写入则很慢,只能写入被清空的页 (200μs),而页以 block 为单位整体清空 (1.5ms)。SSD 不支持就地修改,因此修改必须建立备份、把虚拟地址指向备份处、然后标记原数据过时。这些操作由 \textcolor{red}{Flash Translation Layer(FTL)} 管理,它支持自逻辑地址至物理地址的映射,维护 \textcolor{blue}{Block Info Table} 存储块信息(有无被清空)、维护 \textcolor{blue}{Sequence Number} 即块中最后一个被写过的页以 write pages in a block sequentially、同时作 \textcolor{blue}{磨损均衡 (Wear Leveling)} 以平衡各块间的磨损,避免某些块成为高频磨损的 hotspot。

\textcolor{red}{GPU} 也是一种 IO 设备,使用方式是将 CPU 中数据复制到 GPU、GPU 计算、然后将数据拷回来,二者之间的联系通过 \textcolor{blue}{IO bus}。因为 CPU 和 GPU 的数据存储是分离的,所以数据传输的消耗是显著的,因此可以将 GPU 的计算与 bus 的传输 overlap。GPU 可以作为加速器实现大部分的运算,或是与 CPU 合作实现 \textcolor{blue}{heterogeneous computing},共同完成一个目标,需要好的工作量分配技巧。

\section*{网络(Networking)}

网络中的 \textbf{节点 (nodes)} 间存在 \textbf{连接 (links)}。为了避免连成完全图,使用 \textcolor{red}{多路复用(multiplexing)} 技术,有一些 interior nodes 作为 \textcolor{red}{交换器(switch)} 工作。源节点发送附有地址的数据包——单一信息可能需要被拆分为多个包,而每个包会独立地发送至目标,交换器则在发送过程中使用包上地址进行寻址。

\textcolor{red}{MAC 地址} 是局域网中用于寻址的地址,一般写作 12 位 16 进制数,形如 \texttt{XX:XX:XX:XX:XX:XX},前 6 位是厂商识别码,后 6 位是网卡制造商为每台设备分配的序列号。MAC 地址唯一分配给每台设备终生不变。\textcolor{red}{IP 地址} 则适用于互联网、广域网中通信,且随设备地点不同而变化。使用 IP 而非 MAC 在于前者具有更强的 scalability。\textcolor{red}{Network Interface Card (NIC)} 就是网卡,在物理意义上把设备连入网络。一个设备可以拥有多个网卡,以应对有线无线等多种连接方式。\textcolor{red}{端口(ports)} 是计算机标记某个进程或服务的一种接口,每个端口都有唯一的 \textcolor{red}{端口号(port number)},由程序或操作系统分配,是 16 位整数。在网络上,一个 endpoint 被使用 \texttt{(IP address, port number)} 二元组标记。常见应用与端口号的对应关系如下:

\begin{minipage}{0.9\columnwidth}
    \centering
    \begin{tabularx}{\columnwidth}{|X|X|}
        \hline
        Wake-on-LAN:9 &  FTP data:20 \\
        \hline
        FTP control:21 & SSH:22 \\
        \hline
        Telnet:23 & DNS:53 \\
        \hline
        HTTP:80 & SNMP:161 \\
        \hline
    \end{tabularx}
\end{minipage}

IP 地址太难记了,因此使用更容易记忆的字符串即 \textcolor{red}{域名 (Domain Name)},如 \texttt{iiis.tsinghua.edu.cn}。一个域名可以对应多个 IP 地址,以便于使用最近的服务器,或是避免攻击。将域名定向至 IP 的工作由 \textcolor{red}{Domain Name System(DNS)} 进行。一般流程如下:首先先在 \textbf{本地服务器 (Local DNS Server)} 寻找域名的 IP 地址,找到直接返回,否则向 \textbf{根服务器 (Root DNS Server)} 即 \texttt{.cn} 服务器请求,后者提供 \texttt{.edu.cn} 服务器地址,向其请求后,得到 \texttt{tsinghua.edu.cn} 服务器地址,向其请求可得最终 IP 地址。

在局域网中,如何获取某个 IP 地址的 MAC?使用 \textcolor{red}{Address Resolution Protocol(ARP)}。其流程为:首先检查本地是否有存储。如果没有的话,在局域网中广播形如「谁拥有这个 IP 地址?告诉我你的 MAC 地址」的消息,所有设备都会收到,但只有该地址的所有者会响应。局域网中的所有设备通过 physical wire 连接,通过 \textbf{集线器 (hubs)} 连接各根线。如果在线路上出现 congestion,则 detect 并 retransmit。除了被集线器连接外,还可以被交换器连接,使用 \textcolor{blue}{learning switch} 策略,记住每个 MAC 对应的端口,比广播策略要高效得多。但是缺点是,所有设备都要记住其它人的地址,因此缺少 scalability。

\textcolor{red}{广域网 (Wide Area Network, WAN)} 覆盖多个 \textcolor{blue}{数据层网络 (Datalink Layer Network)} 即 LAN,数据层网络之间通过 \textcolor{red}{路由器 (router)} 连接。路由器会将所有 incoming link 中收到的数据包根据其目标 IP 地址 \textcolor{blue}{forward} 到某个 outgoing link,这需要维护 forwarding table,将 IP 地址与 output link 相对应。但是,如果你发给了错误的服务器,虽然它可能有能力给你重定向到正确的服务器,但是供应商没有义务好心地为你提供这种服务。

衡量信道的种种参数:\textcolor{blue}{latency},首个 bit 到达目标的时刻;\textcolor{blue}{capacity/bandwidth}:通讯速率;\textcolor{blue}{jitter},latency 的方差;\textcolor{blue}{loss/reliability},丢包率;是否会出现 \textcolor{blue}{packet reordering}。数据可能会丢失,但问题是一方面无法得知丢包的原因,是连接问题还是 congestion;一方面也无法区分过久的 delay 和真正的 lost。数据也可能会被污染,解决方案是使用 checksum。数据还可能会 reorder。网络也可能会 overload,可以使用 buffering 以减少 jittering——但是如果 buffer 溢出了那就丢包了,解决方案是确保网络不会拥塞,或是动态调整发送率直到不出现过载。

\textcolor{red}{Stop \& Wait Protocol} 可以用于设计 \textcolor{red}{Polite Networks},达到 reliable 的传输。接收方在收到数据后,会传回 \texttt{ACK} 指令表示接受完毕,此后发送方再发送下一份数据。倘若在一个合理的 \textcolor{blue}{timeout} 后发送方没有收到 \texttt{ACK},则重发送。

发送的延迟包括以下组成部分:\textcolor{blue}{propagation delay} 与 link 的长度成正比,\textcolor{blue}{transmission delay} 正比于包大小和 link speed 的倒数,\textcolor{blue}{processing delay} 与路由器的速度有关,此外还有 \textcolor{blue}{queuing delay},与负载和队列大小有关。例如,10kb 的 packet 在 link speed 为 100Mbps 的网络上,transmit delay 为 1ms。假设传输距离为 5000km,则 propagation delay 为 (5000km)/(2×10\textsuperscript{8}m/s)=25ms,于是 stop\&wait 模式下,忽略 processing 和 queuing,耗时为 1ms+25ms,但是还要考虑 \texttt{ACK} 传输,因为过小没有 transmission,只有 25ms 的 propagation,总计 51ms。

效率不是很高。但是,可以发送多个包再要求 \texttt{ACK}。于是有 \textcolor{red}{TCP 协议}:从一个很小的发送率开始,迅速扩大发送率,直到发现 overflow。此后直接折半发送率,然后重复上述流程。此乃 \textcolor{blue}{Additive Increment Multiplicative Decrement (AIMD)} 策略。然而,在会丢包的环境下,TCP 不是好选择,因为只要丢了一个包就要全体重传,发送率根本上不来。

\section*{文件系统(File Systems)}

对磁盘的访问 block 为单位进行,而 SSD 则是 page。Block 是一种逻辑地址,与 sector 对应的物理地址相对应。假设每个 block 都有一个 \textcolor{red}{Logical Block Address(LBA)},编号从 0 到 block 的最大数目,可能与物理地址不同(为了磨损管理),而 controller 会从 LBA 翻译为物理地址,阻止了操作系统访问磁盘内部信息。

\textcolor{red}{文件系统 (File System)} 则是进一步将对 block 的访问转为对文件和文件夹的访问的操作系统部件。从用户的角度,它看起来是连续的字节,但是从操作系统的角度,它是可能不连续的 block。文件系统由两部分组成:文件集合,存储各种文件;路径结构,将文件组织起来,支持通过名称访问文件。它支持 \textcolor{blue}{disk management}(对于文件检索储存位置;在新建文件时分配空白地址)、\textcolor{blue}{naming}(通过名称检索)、\textcolor{blue}{protection}(隔离数据)、\textcolor{blue}{reliability/durability}(让数据在可能的错误下保持一致)。

\textcolor{red}{文件 (File)} 必须是 durable 的,在创建它的进程乃至系统 terminate 后,仍然 persist。它是命名的,通过名称进行检索。一旦它被命名,就与进程、用户乃至设备无关。从用户的角度,文件是最小的 logical secondary storage 单位。Unix,Linux 等系统提供 \texttt{/proc} 文件夹,可以使用文件系统接口去检查系统状态,但是系统状态并没有被真正存储于其中,而是在调用时系统动态检测并返回。此时,读取系统状态就是读取文件,修改系统状态就是修改文件。例如,\texttt{/proc/cpuinfo} 查看 CPU 状态,\texttt{/proc/567} 查看进程 567 状态。

\textcolor{red}{文件描述符 (File Descriptor)},在 Linux 下称 \textcolor{red}{索引节点 (inode)} 或 \textcolor{red}{file control block},和文件一同被存储在磁盘上,存储文件的 \textcolor{red}{元数据 (metadata)},包括文件大小、位置、时间戳、权限等。

在对文件进行 \textcolor{red}{\texttt{open}} 时,通过名称去检索一个文件。首先会将名称翻译为 \textcolor{red}{file number}(也称 \textcolor{red}{i-number}),用以定位文件描述符在磁盘上的位置,然后将其从磁盘 copy 到内核中,并返回一个整数的 \textcolor{red}{file handle} 用以访问之。\textbf{为什么不直接访问指向其的指针?为了保护描述符,只提供其有限的接口。}然后之后的 \textcolor{red}{\texttt{read/write/seek/sync}} 等操作即每次使用 file handle 去定位 file descriptor,再用其定位文件所在位置。

为了在有多个进程同时访问同一个文件时也能在 \textcolor{red}{\texttt{close}} 时退出文件,使用一个两级的 \textcolor{red}{open-file tables}:一个是 \textcolor{red}{per-process open-file table},关于每个进程独立,存储其打开的文件表;另一个是 \textcolor{red}{system-wide open-file table},存储系统中所有被打开的文件表。如果第一个进程已经打开文件了,则它会在系统表中有储存,那么第二个进程就不需要再进行 copy 描述符之类的操作,可以直接指向系统表中的相应项即可。而当某个文件在所有进程中被 close,系统就会自动将其真正地 close,从系统表中移除。

文件可能有多种访问方式,如最常见的 \textcolor{blue}{逐行 (sequential)} 访问;但是也有 \textcolor{blue}{随机 (random)} 访问,使用 \texttt{seek} 的功能,在 demand paging 的时候有用;还有不常见的 \textcolor{blue}{键值 (keyed/indexed)} 访问,在全文搜索时有用,但是大部分操作系统不支持这种操作,而是通过建立 database 实现这一点。

如何为文件存储分配磁盘空间?回忆起,文件存储必须以 block 为单位存储,那么最后一个 block 可能会有部分未使用空间,即产生 \textcolor{blue}{internal fragmentation}。此外,大部分文件都是小文件,但是大部分磁盘空间却被大文件占据,且一个文件的大小随时可能不可预料地增长。

\textcolor{blue}{Contiguous}:倍增地动态分配连续内存,易于实现,但是大文件难以存储,并且难以增加文件大小。\textcolor{blue}{Linked}:可以链表式存储文件,但是就不支持随机访问了。Windows 就采取了这种策略,但是没有在 block 内存储指针,而是维护了一一对应的 \textcolor{red}{File Allocation Table (FAT)} 表格,file descriptor 只需要存储起始位即可。\textcolor{blue}{Indexed}:独立开一片空间,用一个 block 存储指向该文件所存储的各 block 的指针,这样就可以更好地支持随机访问了。然而,因为要在一个 block 中存得下,指针数目往往是预定义的,因此最大文件大小不能超过其对应阈值。Unix 使用 \textcolor{blue}{Multi-Level Index} 的方法,block 大小 4kB,指针大小 4B,每个文件描述符存储 14 个指针:12 个指针指向 \textcolor{red}{direct blocks},用于快速处理不超过 48kB 的小文件;1 个指针指向 \textcolor{red}{indirect block},存储 1024 个指针(不需要时这个 block 不被分配);最后 1 个指针指向一个 \textcolor{red}{double-indirect block},一个多层的结构。使用间接存储时,最大文件 4MB,而二级间接时可达 4GB。

分配 free block 的策略,早期系统会维护一个 free blocks 的 \textbf{链表},而 Unix 会使用 \textbf{bitmap} 也称 \textcolor{red}{free map} 维护所有 block 是否被占据,allocation 时寻找离文件上一个 block 最近的空 block。问题是磁盘接近满了的时候会很慢,解决方案是预留 10\% 的容量,在 90\% 满的时候即报告磁盘已满。\textcolor{green}{\textbf{Bitmap 与 SSD 还是 disk 无关,因此在外存将满之时效率都会下降。同理,碎片化现象同样也是无法避免的。}}

\textcolor{red}{目录 (Directory)} 系统被用于将名称映到 file number。早期策略是把整个磁盘当作一个目录,或是为每个用户分开开一个目录,但是重名现象很严重。所以现在使用 \textcolor{red}{层次目录结构 (Hierarchical Directory Structure)},使用 \textcolor{red}{\texttt{/}} 分层。一个目录就像一个普通文件,其中的数据是 \textcolor{blue}{(名称,指针)} 的无序表,每对数据指向一个子文件或子目录,指针指向其 descriptor。称作 \textcolor{red}{root} 的特殊目录没有名字,有特殊的 \texttt{i-number=2},在磁盘中位置 \textbf{固定}。特别地,\texttt{i-number=0} 是空 block,\texttt{=1} 是损坏 block。访问地址 \texttt{/A/B/C} 时,要读 \texttt{root} 的 inode 后,读取数据从中寻找 \texttt{A} 的信息,再读取 \texttt{A} 的 inode 后,读取数据……直到最后读取到 \texttt{C} 的 inode,共须 7 次读取。

\textcolor{red}{Hard link} 是为两个不同的路径赋以相同的 i-node 的方式,这样同一个文件就可以使用两种不同的路径访问。例如,\texttt{/some/path} 下可以有 \texttt{<name,i-num>} 的对,另一个 \texttt{/another/path} 下可以有 \texttt{<name',i-num>},此时 \texttt{/some/path/name} 和 \texttt{/another/path/name'} 就是同一个文件了。这样,文件系统就是一个 DAG 了。而一个文件可以被删去,仅当所有指向其的路径都不存在了。另一种策略是 \textcolor{red}{Soft link (Symbolic link)} 类似超链接,\texttt{/another/path} 下存储的是 \texttt{<name',i-num'>} 对,而 \texttt{i-num'} 对应的 \texttt{i-node'} 中存储着 \texttt{/some/path/name} 的路径。当删去 \texttt{name} 后,\texttt{name'} 的超链接仍然存在,但是已经不再有效了。

\textcolor{red}{工作目录 (Working Director)} 因进程而异,其对应的 inode 存储在 \textcolor{blue}{Process Control Block (PCB)} 中。以 \texttt{/} 开头的路径是绝对路径,不以其开头的是相对路径。

\textcolor{red}{Buffer Cache} 使用部分内存储存外存信息,可以存 inodes, data blocks for 
directories and files, indirect blocks, free bitmaps 等众多数据。其功能和虚拟内存有一定重合,因此为了避免有数据被 cache 两遍,很多操作系统现在将二者统一了。如果 Cache 里的东西被改写了如何?可以 \textcolor{blue}{同步修改 (Synchronous Writes)},也可以 \textcolor{blue}{延迟修改 (Delayed writes)},前者更安全但也更慢,后者在宕机后会损失数据。

为了避免意外或有意的 abuse,需要多种保护方法:\textcolor{red}{认证 (Authentication)},检查用户身份;\textcolor{red}{授权 (Authorization)},依据身份不同提供不同的权限,二者合并起来是 \textcolor{red}{Access Enforment}。认证一般使用密码实现,问题是系统必须在某处存储所有密码用于比对。解决方案是存储 hash 结果并比对 hash 值,但是如果预处理出来所有 hash 的结果呢?为了避免预处理出来一张表吃遍所有 system 和 user,将密码后面拼接一些可能很长的 \textcolor{blue}{salt},把密文扩充得很长。在认证后,所有指令都被打上了 user ID。至于授权,可以被 \textcolor{red}{access matrix} 描述,每个用户可以取得某些权限,但是这个矩阵很大且很稀疏,所以使用 \textcolor{red}{Access Control List (ACL)} 描述:对于每个 object,存储哪些 principal 被允许执行哪些 operation。为了压缩,将用户分组。好处是信息临近 object,坏处是除非全体遍历,不知道每个用户被赋以哪些权限。另一种是 \textcolor{red}{Capability},从 principal 的角度存储对于每个 object 的 permission。

\section*{数据库 (Databases)}

一个 \textcolor{red}{数据模型(Data Model)} 是一堆对象以及其间关系。其中的一个 \textcolor{red}{模式(Schema)} 是数据库对象的一个集合。\textcolor{red}{关系数据模型(Relational Data Model)} 是最常见的数据模型,包含很多二维数组的 \textcolor{blue}{关系(Relation)},每个关系都有附加的 \textcolor{blue}{模式}。数据库中不使用指针,而是使用 identifier。例如,可以有学生类 \texttt{Students(sid:string, ...)}、课程类 \texttt{Courses(cid:string, ...)}、以及将二者联系起来的选课关系类 \texttt{Enrolled(sid:string,cid:string,...)}。\texttt{sid} 与 \texttt{cid} 充当了 identifier,但是它们检索起来会比较麻烦。于是,将数据库排序以便于二分,或是将其排成树形结构,便成了刚需。因此,\textcolor{red}{数据库管理系统(DBMS)} 就需要支持 \textcolor{red}{数据定义语言(DDL)}:定义关系和模式;\textcolor{red}{数据操作语言(DML)}:使用询问对数据检索、分析与修改;并在这些过程中维护数据库的一致性。数据操作语言仅需要暴露一个很简单的需求接口,例如\texttt{SELECT sid, name, gpa}; \texttt{FROM Students S}; \texttt{WHERE S.gpa > 3} 并给 DBMS 留出充分的内部优化空间。通过调整指令的执行顺序,有可能可以提升其效率。

为保证一致性,使用原子操作 \textcolor{red}{事务(transactions)} 确保它总是从一个一致态到另一个一致态。用户可以自行定义 \textcolor{red}{完整性条件 (integrity constraints, IC)} 来限制之。事务有 \textcolor{blue}{ACID} 原则:\textcolor{blue}{原子性(atomicity)},整个事物是原子的;\textcolor{blue}{一致性(consistency)},事务保证状态的一致性;\textcolor{blue}{隔离性(isolation)},所有事务彼此独立,不会产生共时性问题;\textcolor{blue}{持久性(durability)},一旦写入就算崩溃也不会受到影响。

但是,就连在常规文件系统中写一个文件都是非原子的,所以会使用特定的操作顺序:先分配内存、写入内存,再写 inode,再更新 freebitmap(否则上述东西仍然是 free 的,不会出现僵尸内存),再更新目录的相关信息如 last modify time 等。自崩溃中恢复,则需要搜索 inode 表并清空所有不在任何文件夹中的文件(或是置入 \texttt{lost\&found} 文件夹中),比较 freebitmap 和 inode tree,搜索目录寻找未 commit 的 access time 等,所需时间正比于磁盘大小。效果很不好,因此解决方案是先修改 \textcolor{red}{log},在开始修改时在 log 中写入 start,然后把所有对原始数据的修改写在 log 里面,结束修改时在 log 中写入 commit,这之后将 log 的修改复制回磁盘并清空 log。倘若 log 写一半挂了,则忽略写的东西;倘若复制到一半挂了,则相当于啥都没复制,再次复制。代价是很昂贵(两次写入)。

事务可以被重排序以希望最大化共时性,但是前提是保证 \textcolor{red}{Serial Schedule},即某个事务要么不执行要么一次执行完;存在很多 \textcolor{red}{Equivalent Schedules},执行它们的效果相同;而我们希望使用 \textcolor{red}{Serializable Schedule},它虽然不是 serial 的(以便于并行),但是与某个 serial 的等价,因此是我们喜欢的。为了判定是否可串行化,定义两个操作是 \textcolor{red}{冲突(conflict)} 的,如果它们针对同一个数据,且至少有一个是写入。两个排序是 \textcolor{red}{Conflict Equivalent} 的,如果两者中所有冲突对的顺序都相同。某个排序是 \textcolor{red}{Conflict Serializable} 如果与一个串行排序是冲突等价的。显然,冲突等价是可串行的子集,但是因为判定两个「执行效果」是否等价是困难(准确的说,NP-完全)的,所以我们只使用冲突等价性来串行化。冲突等价性可以在所有冲突对间连边(先来的指向后到的,边上标注冲突的数据名,一条边上可以标注多个冲突的数据)并建立 \textcolor{blue}{dependency graph},则冲突可串行当且仅当其是 DAG。

数据库中也有 lock,可以对整个数据库、某个 table 或是其中的某行进行,有 \textcolor{blue}{shared lock} 衡量共时事务能否同步在数据上操作,或 \textcolor{blue}{exclusive lock} 衡量排他性。每个事务在读入之前必须获得 S/X 的 lock 之一,在写入前必须获得 X lock。但是注意到,一个锁一旦被 acquire 就必须一直持有直到全部执行完毕(不然其可能被其它事务抢走),因此所有事务都存在一个锁数目增加的阶段和一个减少的阶段,一旦任何锁被释放,就不能在获得新锁了。这种策略能正确执行,当且仅当它是冲突可串行的,不然就会死锁。

\section*{网络层级 (Network Layering)}

应用有许多,传输途径(如同轴电缆、光纤或电波)也有许多。为了避免它们之间两两都要手动实现适配,在中间建立 \textbf{中继层(Intermediate Layer)}。每一层只依赖于其下面一层提供的服务,同时为其上面一层提供服务,不存在跨层信息。

每一层有如下性质:其提供的 \textcolor{blue}{服务(Service)};其 \textcolor{blue}{接口(Service Interface)},告诉上层应该如何与其交互;一层除了对上层负责以外,还可能需要与其它设备的同层之间进行交互,这需要 \textcolor{blue}{协议(Protocol)} 加以规定。

\textcolor{blue}{Open Systems Interconnection (OSI)} 模型具有七层:\textcolor{red}{应用(application)}, \textcolor{red}{表示(presentation)}, \textcolor{red}{会话(session)}, \textcolor{red}{传输(transport)}, \textcolor{red}{网络(network)}, \textcolor{red}{数据链路(datalink)}, \textcolor{red}{物理(physical)};\textcolor{blue}{Internet Protocol (IP)} 模型则没有表示和会话层,它们的功能被应用层实现了。

\textcolor{red}{1.物理层}提供的\textbf{服务}是在被物理线路连接的两层间传输数据,\textbf{接口}规定如何收发数据,\textbf{协议}是如何编码数据;\textcolor{red}{2.数据链路层}提供的\textbf{服务}是在终端间使用物理连线或无线连接交换被称为\textcolor{blue}{帧(frame)}的 atomic 信息,\textbf{接口}是与其它终端间收发帧,协议包括寻址的\textcolor{blue}{ARP}和地址的\textcolor{blue}{MAC}。\textcolor{red}{3.网络层}的\textbf{服务}是对某个具体的 IP 地址发送\textcolor{blue}{包(packet)},\textbf{接口}是收发包,协议是全局唯一的网络地址,以及 packet forwarding。\textcolor{red}{4.传输层}的\textbf{服务}是\textcolor{blue}{进程}间端到端的通信,以及作\textcolor{blue}{多路复用(demultiplexing)},让多个进程可以同时通信,\textbf{接口}是收发信息,\textbf{协议}是端口号、可能的对可靠性、流量控制、分包和分帧等服务的支持等,例如强调了可靠性、有序性的 \textcolor{blue}{TCP},和速率更快的 \textcolor{blue}{UDP} 都是传输层的协议。第五层和第六层是操作系统的一部分而非网络的一部分,它们的功能是被 \textcolor{red}{7.应用层}实现的。

整个结构是为,应用层有一个数据;在下放到传输层后前面加了一个传输头(Trans.Hdr.);下放到网络层又加了一个网络头(Net.Hdr.);下放到数据链路层又加了一个帧头(Frame Hdr.);下放到物理层……没有加,直接变成 01 串了。所有设备都实现了下三层,而上两层一一般只在 host 有(而路由器等没有),但也有 smart 的设备会实现上两层。路由器会解包到网络层获知目标 IP,然后再打包回物理层将其发送出去。

层级结构中,上层和下层都有多种实现方式,只有网络层仅有 IP 一种协议(虽然有 v4 和 v6 两种),此乃\textcolor{red}{沙漏模型(Hourglass Model)}。它允许任何网络进行\textcolor{red}{协作(interoperate)},允许在 IP 协议上构建的应用无缝衔接一切网络,允许 IP 以上和以下的东西同步更新。然而,IP 本身的更新因此是困难的。层级也有其弊端:它会对表现有一定影响,同时层层叠加的 header 有时会比真实数据还要长,同时高层可能会重复实现纠错等底层任务影响效率,同时不同层可能还需要如时间戳等重复的信息。

就算下层保证了完美传输,如应用层等处还是有必要处理未正确传输等 issue,因为开发应用过程中仍然会出现例如填错端口等问题。

\section*{分布式系统(Distributed System)}

因为很难无限提升单核的能力,因此多核确有必要,于是分布式系统登场。它们没有共享的内存,交流必须依靠收发信息。但是,仍然希望分布式系统如同单核系统一般。分布式系统层是一个中间层,在所有机器上共享,高于系统层但低于应用层。应用也可以在多个机器间共享。但是,大部分分布式系统都有以下毛病:\textcolor{red}{Worst Availability},只有所有成员设备都开机才能运算;\textcolor{red}{Worst Reliability},任何设备宕机都会丢失数据;\textcolor{red}{Worst Security},所有人都可以入侵系统。而该系统的目标是达到资源共享,同时对外隐藏资源的某些细节,最好还有开源的接口和协议,最后还要有好的 scalability。

\textcolor{red}{跨进程通信(Inter Process Communication, IPC)} 可以使用 \texttt{fork()},signal,文件系统,pipe,共享内存或 networking 等,但是通信一般而言都使用着 \textcolor{red}{raw bits},有点太底层了。可以以较高层的方式进行通讯,例如提供 \textcolor{red}{data type},即数据大小和每一段数据的意思,是一种协议。在分布式系统的场合,更好的方式是使用\textcolor{red}{远程过程调用(Remote Procedure Call, RPC)},它允许远程调用进程,如同在调用本地进程一般。这需要 \textcolor{red}{参数编组(Marshalling)} 技术:将本地参数转成可以跨语言、跨平台的格式(如 \texttt{json} 等)。使用 \textcolor{red}{存根(Stub)} 机制进行参数编组:客户端存根对发送的参数进行编组,然后对收到的值尽心解编组,而服务器端则相反。存根可以使用 \textcolor{red}{Interface Definition Language(IDL)} 生成。RPC 有一个特色是,出锅的话,挂的不是本地机器而是服务器。此外,本地调过程的消耗远小于本机 RPC,又远小于跨网络 RPC。

\textcolor{red}{分布式文件系统} 中,所有读写都要上传到服务器。优点是服务器端可以将全局的信息分享给多个用户,缺点是很慢。可以通过 caching 一定程度上提升效率。\textcolor{red}{Network File System (NFS)} Caching 使用 \textcolor{blue}{Write Through Caching} 的形式,即所有的修改都要同步到服务器的磁盘才算修改完成可以报告给客户端:缺点是失去了 caching 的某些优势、执行写入操作的时间会很久、同时需要报告写入完成的额外机制。NFS 实现的方法是用户周期性地检索服务器,因此会出现服务器端已经修改完成,但是某些客户端还在使用旧版本的场合。当多个客户同时修改同一个文件时,会出现未知结果:得到任一版本,或是两版本的叠加态。好处是简单且易迁移,坏处是会出现不一致,且 scalability 弱(所有客户都要周期性调查),对网络延迟高度敏感。\textcolor{red}{Andrew File System(AFS)} 则是另一种策略,支持大规模的部署,假设客户端都存在存储,且 write/write 与 write/read sharing 是罕见的(代码一般是单人编辑),同时对本地磁盘的读写远快于对网络端的读写。AFS 将每一个管理域放到形为 \texttt{/afs/cellname(e.g. andrew.cmu.edu)} 的 \textcolor{blue}{cell} 里面,每个 cell 再被分成通常仅在一台服务器中存储的 \textcolor{blue}{volume},客户端则使用 \textcolor{blue}{protection server} 进行 authentication,使用 \textcolor{blue}{volume location server} 把 volume 映到服务器。AFS 的 caching 策略更加激进,会将整个文件记入磁盘而非仅在内存中,会进行 \textcolor{blue}{prefetching} 操作,即在打开文件时就直接存到本地然后剩下的编辑在本地进行,为了处理偶发的共享写入,需要 \textcolor{red}{回调失效(callback)},用户告诉服务器其拥有一份文件,然后服务器在文件被修改时告诉用户其已经失效。

本地文件系统具有强的 \textcolor{red}{一致性(Consistency)}:因为所有的读写都是原子操作,所有的写入都会立刻对所有后续读取生效,这被称作 \textcolor{blue}{UNIX Semantics} 或 \textcolor{blue}{One Copy Semantics}。但是 NFS 和 AFS 都不保证:前者会在用户端的周期性检索后才得到同步,后者则是所谓的 \textcolor{blue}{Session Semantics}:只有在一个文件被关闭、所有修改被确认后,才会同步到其它进程。此外还有不可变 \textcolor{blue}{immutable} 的文件,以及通过 \textcolor{blue}{transaction} 保证的一致性。AFS 的 SS 式修改只对关闭后新的访问生效,旧的访问仍然是不可见的,且多个 \texttt{close()} 操作中只有最后一个会胜出。实现是将修改过的数据储存在本地,只在关闭后传输到服务器,并对所有其它用户 ccb。\textbf{注意:只有写入操作的 \texttt{close()} 才会同步,仅读的不会。}AFS 使用的操作叫做 \textcolor{blue}{Write Back Caching},仅在本地 cache 溢出或者被 \texttt{close()} 时才回传,因此若用户端崩溃则会损失信息。

\textcolor{green}{在文件大小很大时,AFS 不是一个好选择,特别是修改会在整个文件上随机进行时(此时就不能对文件进行 caching 了)。}

服务器如果崩溃,存在于内存中的数据不会被保存。因此,如果服务器在检索后崩溃,建立在该基础上的读取操作就无法进行。此外,可能会发生重复删除的现象。至于客户端崩溃了,本地 cache 的修改数据会丢失。因此,NFS 要求 \textcolor{red}{无状态(stateless)},即所有请求都携带完整参数,而不能先 \texttt{open()} 再从对应地址中读取再 \texttt{close()}。此外,命令需要有 \textcolor{red}{幂等性(Idempotent)},即重复执行多次与仅执行一次等效。在服务器崩溃后,NFS 既可以让操作挂起直到服务器恢复,也可以直接报错。AFS 则在服务器崩溃后,会损失所有 ccb 的状态,因此需要询问每个用户存储的文件。用户端的崩溃则需要检查所有缓存文件,以避免错过了某些 ccb。此外,还可以使用 \textcolor{red}{Optimistic Replica Control} 策略:在正常模式即 \textcolor{blue}{Hoarding} 状态下,会贮藏在断线时所需的数据。断线后即进入 \textcolor{blue}{Emulating} 模式,此时对不在客户端有备份的数据的访问被拒绝,所有的修改都会被写入 log,同时 log 中的过时数据会被定期清除。最后,当重新连接后,即进入 \textcolor{blue}{Reintegration} 模式,把所有的 log 同步到服务器。
\end{multicols}

\end{document}
posted @ 2025-06-08 23:57  Troverld  阅读(41)  评论(2)    收藏  举报