Vince Jiang

CSCI3150 / 课程伴读 · 本站更新

CSCI3150 L04 User-level Programming via System Calls (File & Directory)

课程:CSCI3150 Introduction to Operating Systems · 2022未核 本讲:L04 User-level Programming via System Calls (File & Directory) · 原件 L04-系统调用-文件与目录-2022未核.pptx(本地课件) · PDF页码副本(本地课件) p.1–48 定位:能沿FDT、打开文件状态和inode追踪文件操作,解释重定向、管道EOF、持久化与两种链接。 承接:#L01-08 持久化;#L02-06 exec替换映像

p.1 标题页。

文件名字、打开状态与字节偏移

从持久化需求到文件和目录(p.2–4)【延伸 L01 p.46–48】

p.3 OS provides services via System Call (typically a few hundred) to run process, access memory/devices/files, etc.

p.3 The Design Of The Unix Operating System (Maurice Bach, 1986)

p.4 Keep a data intact even if there is a power loss.

Hard disk drive

Solid-state storage device

Two key abstractions in the virtualization of storage

File

Directory

p.2 的课程图将本讲定位在系统调用,p.3 的接口图把请求交给内核文件系统与设备层。p.4 提出即使掉电也要保持数据的需求,并引出两个抽象:文件与目录。它们把程序从“知道磁盘哪个物理位置”提升到“通过名字读取一组字节”。

p.4 Two key abstractions in the virtualization of storage

文件负责承载内容,目录负责命名和组织;持久化是目标,文件与目录是实现这个目标时程序使用的接口抽象。

目录保存名字到inode的映射(p.5)【新】

p.5 File – A container to contain data of a file (a linear array of bytes)

Each file has low-level name (inode number)

Directory – Implement directory tree (directory hierarchy)

Like a file, it also has a low-level name (inode number).

It contains a list of (file name, inode number) pairs.

Each entry in a directory refers to either files or other directories.

inode number 是文件系统内部的对象编号,文件名则是人使用的名字。目录项把名字连接到inode,因此目录树是查找对象的一种路径组织。

表(p.5):dirA示例中的目录项

inode number file name
123 .
247 ..
220 dirB
230 My1.dat
230 My2.dat

图(p.5):目录树与目录项 元素:根 / 下有 dirC、dirA;dirA 下有 dirB、My1.dat、My2.dat;dirB 内另有 My1.dat。表中两个名字映射到230。 看什么:相同短文件名可以出现在不同目录;不同名字也可以指向同一inode。名字与对象不是一一对应。

open返回的是描述符,不是inode(p.6–7)【新】

p.6 Use open() system call with O_CREAT flag.

O_CREAT : create file.

O_WRONLY : only write to that file while opened.

O_TRUNC : make the file size zero (remove any existing content).

open() system call returns file descriptor.

File descriptor is an integer, and is used to access files.

read(file descriptor, buffer pointer, the number of bytes to read from)

Return the number of bytes it read

write(file descriptor, buffer pointer, the number of bytes to write to)

Return the number of bytes it write

课件程序与接口(p.6,原页片段)

int myfd = open(“/home/ann/my.dat”, O_CREAT | O_WRONLY | O_TRUNC);

p.7 A file descriptor specifies the index into file descriptor table (FDT) of the process.

Entries of FDT contain pointers to entries in system file table (SFT).

When a file is opened, an entry is created in both EDT and SFT.

SFT entry contains information about whether a file is open for read or write, protection, and lock, the file offset, where the next data is read from or written to in the file, etc.

Several entries in SFT may point to the same physical file

p.7 myfd (the file descriptor for the newly-created file) is 3

When a process is created, file descriptor 0, 1, 2, are opened by default for standard input/ output/ error.

p.6 用O_CREAT创建不存在的文件,O_WRONLY选择写方式,O_TRUNC把已有内容截为0字节。补充:实际以O_CREAT创建时须提供权限参数;本页省略参数的示意式不能直接当完整调用。read/write返回实际传输字节数,不保证每次等于请求长度。

p.7 图中 fd 是本进程FDT的下标,FDT项指向系统打开文件表SFT,SFT再指向inode。偏移和打开方式属于打开状态;两个独立open可以指向同一inode,却保持不同偏移。图中0、1、2已占用,新open得到3;这是本例条件,不是open永远返回3。

图(p.7):FDT→SFT→inode 元素:FDT第3项指向一个SFT项,多个SFT项可指向同一inode;0/1/2标为标准输入、输出、错误。 看什么:必须区分描述符编号、打开文件状态与文件对象三层。

偏移怎样自动或手工变化(p.8–9)【新】

p.8 An open file has a current offset.

Determine where the next read or write will begin reading from or writing to within the file.

Update the current offset

Implicitly: A read or write of N bytes takes place, N is added to the current offset.

Explicitly: lseek()

p.9 Reading And Writing, But Not Sequentially (Cont.)

p.9 fd : File descriptor

offset : Position the file offset to a particular location within the file

whence : Determine how the seek is performed

课件程序与接口(p.9,原页片段)

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

p.9 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.

p.8 的current offset决定下一次读写起点。一次实际传输N字节后偏移增加N,不是增加请求参数中未必完成的数量。p.9 的SEEK_SET从文件开头算,SEEK_CUR从当前偏移算,SEEK_END从当前文件长度算;lseek只改定位状态,不执行读写。

算一遍(补充;数字是我为演示取的,讲义没给):文件长100字节,当前偏移20。lseek(fd,5,SEEK_SET)得到5;接着实际读8字节后为13;lseek(fd,−3,SEEK_CUR)后为10;lseek(fd,−10,SEEK_END)后为90。最后一步的基准是100,而不是上一步10。

补充:用同一文件区分三种“相同”。 设文件内容为 ABCDEF,两个独立 open 得到 a、b,随后 c=dup(a)。这里假设全部成功且初始偏移为 0,是解释用小例子。

顺序操作 读到的字节 a/c 共用偏移 b 的独立偏移
read(a, …, 2) 实际返回 2 AB 2 0
read(c, …, 1) 实际返回 1 C 3 0
read(b, …, 1) 实际返回 1 A 3 1
lseek(c,0,SEEK_SET) 没有读取 0 1

a、b、c 都通向同一个文件对象,所以看到同一组文件内容;只有 a/c 指向同一个打开状态,所以共享偏移。这种表比“fd 是文件”更准确:fd 只是当前进程的整数索引,关闭 a 不会自动使 c 失效,重新 open 也不等同于 dup。

重定向改变的是描述符关系

dup选择哪个编号(p.10–12)【新】

p.10 To access a file, a process uses a file descriptor, which is an index into the process file descriptor table, which in turn points to an entry in the system file table.

Redirection means that the process modifies its file descriptor table entry so that it points to a different entry in the system file table.

Consider the command cat, which reads from a file and echoes to standard output. The following command redirects standard output to my.file

ls -l > my.file

课件程序与接口(p.11,原页片段)

int dup(int fd);

DESCRIPTION
dup() is a “smart” function that can duplicate the file descriptor, “fd”, to the lowest-numbered unused file descriptor in the file descriptor table.

RETURN VALUE:
On success, return the new file descriptor.
On error, -1 is returned, and errno is set to indicate the error.

p.12 dup() is a “smart” function that can duplicate the file descriptor, “fd”, to the lowest-numbered unused file descriptor in the file descriptor table.

课件程序与接口(p.12,原页片段)

int dup(int fd)

课件程序与接口(p.12,原页片段)

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void)
{
        int fd;

        fd = open("my.file", O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR| S_IWUSR );
        close(1);
        dup(fd);
        close(fd);

        char *cmd = "ls";
        char *argv[3];
        argv[0] = "ls";    argv[1] = "-l";     argv[2] = NULL;

        execvp(cmd, argv);
}

p.10 的重定向让标准输出编号1指向另一个打开对象,因此命令无需知道它在写终端还是文件。p.11 的dup复制fd引用,选择最小未用编号;不是把整个文件复制一份。

p.12 的流程是open文件、close(1)、dup(fd)、close(fd)、execvp。按0/1/2原来都打开、open取得3计算:关闭1后,最小空槽为1;dup(3)把1连到同一打开对象;关闭3仍保留1;exec后ls写1就写文件。补充:一般代码可用dup2指定目标,并须检查错误;这个例子成立依赖1确实是最小空槽。

补充:目标描述符号相等时为什么是重要边界。 重定向的目标是让编号 1 指向指定的打开状态,而不是“无论如何先关 1 再复制”。如果程序原来的 1 已经关闭,open 可能直接返回 1;此时再 close(1) 会关掉刚打开的目标。dup2(fd,1) 在 fd 本来就等于 1 且有效时保留它;普通的 close 加 dup 序列必须另外考虑这个边界。因此也不能在 dup2 后无条件 close(fd):若 fd 就是 1,会把刚完成的标准输出再次关闭。

这不是要求修改课件的理想化执行图,而是补清它暗含的前提:图中打开目标所得编号不同于标准输出,并且中间没有另一个操作抢占空槽。Linux 接口还明确规定 dup2 的关闭目标与复制整体完成,可避免把两个普通调用之间的间隙当成原子操作。接口核验:dup(2)

重定向之前(p.13)

p.13 fd = open(“my.file”, O_CREAT, S_IRUSR| S_IWUSR); /* Create the file – my.file */

close(1); /* Close 1 (Standard output)*/

dup(fd); /* Duplicate fd */

execvp(cmd, argv); /* Execute the command ls -l */

图(p.13):重定向的当前状态 元素:FDT的0/1/2/3、SFT、终端inode与my.file的inode;红色标注标出本步语句。 看什么:FDT的0、1、2经各自打开状态连到终端。此时标准输出仍是1→终端,目标文件尚未加入。

打开目标文件(p.14)

p.14 fd = open(“my.file”, O_CREAT, S_IRUSR| S_IWUSR); /* Create the file – my.file */

close(1); /* Close 1 (Standard output)*/

dup(fd); /* Duplicate fd */

execvp(cmd, argv); /* Execute the command ls -l */

图(p.14):重定向的当前状态 元素:FDT的0/1/2/3、SFT、终端inode与my.file的inode;红色标注标出本步语句。 看什么:open后出现fd=3,3指向my.file的打开状态;0、1、2未改。只是打开文件不会自动改变stdout。原图的简化open行只写O_CREAT;应结合p.12带O_WRONLY的完整示例理解可写打开方式。

腾出标准输出槽(p.15)

图(p.15):重定向的当前状态 元素:FDT的0/1/2/3、SFT、终端inode与my.file的inode;红色标注标出本步语句。 看什么:close(1)移除1的引用,而3→my.file仍在。删除一个描述符关系不等于删除文件。

复制到最小空槽(p.16)

图(p.16):重定向的当前状态 元素:FDT的0/1/2/3、SFT、终端inode与my.file的inode;红色标注标出本步语句。 看什么:dup(fd)找到空的1,让1和3指向同一SFT项。两者共享这一次打开的偏移;这与重新open同一个名字不同。

在旧映像中完成接线(p.17)

p.17 I/O redirection (continue; before execvp(…) )

图(p.17):重定向的当前状态 元素:FDT的0/1/2/3、SFT、终端inode与my.file的inode;红色标注标出本步语句。 看什么:a.out仍在运行,描述符1已经连到my.file。需要先接好线再exec,否则成功exec后不会回到旧代码补做重定向。

替换程序映像(p.18)

p.18 I/O redirection (continue; executing execvp(…) )

p.18 fd = open(“my.file”, O_CREAT, S_IRUSR| S_IWUSR); /* Create the file – my.file */

close(1); /* Close 1 (Standard output)*/

dup(fd); /* Duplicate fd */

execvp(cmd, argv); /* Execute the command ls –l */

图(p.18):重定向的当前状态 元素:FDT的0/1/2/3、SFT、终端inode与my.file的inode;红色标注标出本步语句。 看什么:调用execvp时替换的是进程运行的代码与数据映像,本例打开描述符关系得以保留。补充:设置了close-on-exec标志的描述符例外,本图没有该设置。

新程序沿旧描述符写出(p.19)

p.19 Note that stdout of Process

for “ls -l” has been redirected to my.file

图(p.19):重定向的当前状态 元素:FDT的0/1/2/3、SFT、终端inode与my.file的inode;红色标注标出本步语句。 看什么:进程体已经成为ls -l,stdout还是编号1,但落点是my.file。程序名称变了,编号1的含义由之前接好的引用关系决定。

用管道把一个命令的输出变成另一个的输入

管道两端与讲义问题(p.20)【新】

p.20 Communication between parent/child processes via pipe

p.20 System call pipe() returns two file descriptors by which we can access the input/output of a pipe (an I/O mechanism)

课件程序与接口(p.20,原页片段)

int fd[2];

int pipe(int fd[2]);
   return: 0 success; -1 error

题面(L04-系统调用-文件与目录-2022未核.pdf · 管道问题 · p.20)

p.20 Question: How to implement “ls –l | wc -l”?

pipe(fd)在数组中写入两个描述符:fd[0]读、fd[1]写;调用本身成功返回0。数组中端点值与系统调用返回值不能混淆。

图(p.20):单向字节流 元素:fd[1]的箭头进入pipe,pipe的箭头离开并到fd[0]。 看什么:把ls的stdout连接写端,把wc的stdin连接读端。后面的图按fork、分支和exec逐步建立这一关系。

先建立管道(p.21)

课件程序与接口(p.21,原页片段)

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>

main()
{
        int fd[2];
        int ret;
        char *cmd;
        char *argv[3];

        pipe(fd);
        if ( (ret=fork()) > 0 ){
                /* Parent process*/
                close(1);
                dup(fd[1]);   close(fd[0]);  close(fd[1]);

                cmd = "ls";  argv[0] = "ls";  argv[1] = "-l";  argv[2] = NULL;
                execvp(cmd, argv);
        }else if (ret == 0 ){
                /* Child process*/
                close(0);
                dup( fd[0]);  close( fd[0]);  close ( fd[1]);

                cmd = "wc";   argv[0] = "wc";  argv[1] = "-l";   argv[2] = NULL;
                execvp(cmd, argv);
        } else{
                /* Error in fork()*/
                printf("Error occurs when executing fork().\n");
                exit(-1);
        }
}

图(p.21):ls与wc的执行流及管道引用 元素:源码的父子分支、fork/exec位置,以及随页逐渐加入的两条执行线、pipe与标准描述符。 看什么:源码在fork前调用pipe,所以一个父进程先持有读写两端。图右侧尚只显示a.out一条执行线;没有fork就没有第二个并发命令。

执行到fork(p.22)

图(p.22):ls与wc的执行流及管道引用 元素:源码的父子分支、fork/exec位置,以及随页逐渐加入的两条执行线、pipe与标准描述符。 看什么:父进程来到fork位置。由于管道已存在,随后子进程才能继承对同一个管道对象的引用;若两边各新建管道,则不是同一通道。

父子分支分工(p.23)

图(p.23):ls与wc的执行流及管道引用 元素:源码的父子分支、fork/exec位置,以及随页逐渐加入的两条执行线、pipe与标准描述符。 看什么:fork后两条执行线并存。父返回正数,选择ls分支;子返回0,选择wc分支。这个例子把原父进程也换成命令,不是保留交互shell的完整设计。

两个分支到达exec(p.24)

图(p.24):ls与wc的执行流及管道引用 元素:源码的父子分支、fork/exec位置,以及随页逐渐加入的两条执行线、pipe与标准描述符。 看什么:父分支先把1复制为写端,子分支先把0复制为读端,并关闭各自多余端点,再分别exec。先后调度可以变化,但两边各自的局部语句顺序不能颠倒。

两个进程成为不同命令(p.25)

图(p.25):ls与wc的执行流及管道引用 元素:源码的父子分支、fork/exec位置,以及随页逐渐加入的两条执行线、pipe与标准描述符。 看什么:父进程体为ls,子进程体为wc。exec没有再增加进程数,成功fork后总数仍为2;程序映像不同,管道对象仍共同连接两者。

实际的数据方向(p.26)

图(p.26):ls与wc的执行流及管道引用 元素:源码的父子分支、fork/exec位置,以及随页逐渐加入的两条执行线、pipe与标准描述符。 看什么:图中ls→写端→pipe→读端→wc。wc -l计算读到的换行数,不直接读取ls的内部数据结构;由字节流隔离两个程序的实现。

把生产者的1连到写端(p.27)

图(p.27):ls与wc的执行流及管道引用 元素:源码的父子分支、fork/exec位置,以及随页逐渐加入的两条执行线、pipe与标准描述符。 看什么:父进程先close(1),dup(fd[1])占用1。补充算例:若pipe端点为3、4,dup(4)返回1;随后close(3)、close(4),仍有1持有写端。数字3/4是演示取的。

把消费者的0连到读端(p.28)

图(p.28):ls与wc的执行流及管道引用 元素:源码的父子分支、fork/exec位置,以及随页逐渐加入的两条执行线、pipe与标准描述符。 看什么:子进程close(0),dup(fd[0])占用0,再关多余3、4。演示端点3/4下,最终父仅通过1写,子仅通过0读;双方遗留的无用写端都已关闭,后续才能正确识别EOF。

阻塞、EOF与SIGPIPE的条件(p.29)

p.29 More about Pipe (https://man7.org/linux/man-pages/man7/pipe.7.html)

p.29 Pipes provide a unidirectional interprocess communication channel. A pipe has a read end and a write end. Data written to the write end of a pipe can be read from the read end of the pipe.

If a process attempts to read from an empty pipe, then read() will block until data is available.

The communication channel provided by a pipe is a byte stream: there is no concept of message boundaries.

If all file descriptors referring to the write end of a pipe have been closed, then an attempt to read() from the pipe will see end-of-file (read() will return 0). If all file descriptors referring to the read end of a pipe have been closed, then a write() will cause a SIGPIPE signal to be generated for the calling process. If the calling process is ignoring this signal, then write() fails with the error EPIPE.

空管道有写端仍打开时,阻塞式read等待数据;所有写端引用关闭且缓冲数据读完后,read才返回0表示EOF。因此“某个写进程结束了”不足以证明EOF,如果另一个进程还持有写端,读者仍可能等待。

另一方向上,所有读端关闭后继续写,会产生SIGPIPE;忽略信号时write失败并给出EPIPE。它不是“写者收到EOF”,EOF是读操作的返回语义。算一遍(补充;数字是我为演示取的,讲义没给):两次write分别写3和2字节,读取者一次最多请求5字节,可能一口读到5,也可能分几次;管道没有保留“第一个消息/第二个消息”的边界。

补充:管道读操作要区分三个结果。 数据区里仍有字节时,read 可以返回正数;数据区空但至少一个写端引用仍开着时,阻塞模式的 read 等待后续数据;数据区空而所有写端都关闭时才返回 0,表示 EOF。它不是在数据流里发现一个叫“EOF”的特殊字符。

检查卡死时,列出每个进程的全部描述符,尤其是 fork 继承、dup 新增却没关闭的写端。若消费者自己还留着管道写端,即使生产者已退出,内核仍无法判定再无写者。另一个方向也要检查:生产者写满管道后会等待读者;若父进程先等待生产者结束、尚未启动消费者,就可能双方都无法推进。缓冲容量只会推迟这一错误暴露,不能修复依赖关系。

自测:unlink 一个名字后,已打开 fd 还读得到文件,是否说明 unlink 失败?

第一层:unlink 修改目录名字映射,不是关闭现存 fd。

第二层:没有剩余硬链接时,打开引用仍可以维持对象生命周期。进程读到旧对象不证明名字仍在目录中;重新用被删的路径 open 则可能失败。观察“路径是否存在”和“旧 fd 是否可读”是在问两件事。

文件内容的提交与名字的修改

write返回与fsync完成(p.30–31)【新】

p.30 The file system will buffer writes in memory for some time (for performance reasons), e.g. 5 or 30 seconds.

At that later point in time, the write(s) will actually be issued to the storage device.

Writes seem to complete quickly; but data can be lost (e.g., machine crashes).

However, some applications require persistence guarantee, e.g. DBMS requires force writes to disk from time to time (your bank transactions).

课件程序与接口(p.30,原页片段)

int fsync(int fd)

force all dirty (i.e., not yet written) data written to disk
fsync() returns once all of theses writes are complete.

p.31 An Example of fsync().

In some cases, this code needs to fsync() the directory that contains the file foo.

课件程序与接口(p.31,原页片段)

int fd = open("foo", O_CREAT | O_WRONLY | O_TRUNC);
assert (fd > -1)
int rc = write(fd, buffer, size);
assert (rc == size);
rc = fsync(fd);
assert (rc == 0);

课件程序与接口(p.31,原页片段)

#include <unistd.h>

int fsync(int fd)

 On success, these system calls return zero.  On error, -1 is returned, and errno is set appropriately.

p.30 用写缓冲解释为何write很快返回却不等于掉电后内容安全。缓存中的脏数据还没写到存储设备。fsync请求完成相应同步写入,成功返回0;p.31 示例同时检查write实际长度与fsync结果。

p.31 还指出有时需要同步包含foo的目录,因为文件内容和“目录中出现这个名字”是两份不同状态。补充:不能把例子里的5或30秒当作所有系统固定刷盘周期,也不能在未检查失败时宣称持久化已经完成。

临时文件与原子改名(p.32)

课件程序与接口(p.32,原页片段)

prompt> mv foo bar  // mv uses the system call rename()

课件程序与接口(p.32,原页片段)

int fd = open("foo.txt.tmp", O_WRONLY|O_CREAT|O_TRUNC, S_IRWXU);
char buffer[20]= "hello";
write(fd, buffer, sizeof(buffer)); // write out new version of file
fsync(fd);
close(fd);
rename("foo.txt.tmp", "foo.txt");

p.32 How to update a file atomically:

课件先写foo.txt.tmp,fsync并close,再rename为foo.txt。目的在于让名字切换发生在新版本准备好以后,而不是在旧文件上逐块覆盖时让读者看见半新半旧。算一遍:示例 char buffer[20]="hello" 的数组总长20;write使用sizeof(buffer),因而写20字节,其中hello是5字节,剩下15字节为零初始化部分,而不是只写5个字符。

补充:原子名字替换与断电持久化是不同保证;完整崩溃安全流程还需考虑目录同步、错误处理和文件系统条件。本页的rename教学模型不授权把跨文件系统移动称为一次原子rename。

补充:把 p.32 的更新分成可观察的三个时刻。 用“旧对象”“新对象”区分文件内容,不把名字误当对象本身。

时刻 foo.txt 这个名字指向什么 foo.txt.tmp 指向什么 读者通过 foo.txt 能看到什么
准备写临时文件 旧对象 正在写的新对象 旧版本
临时文件写完并同步,尚未 rename 旧对象 完整新对象 仍是旧版本
rename 成功 新对象 该临时名字已移走 随后重新打开得到新版本

这里假定目标原来存在,改名在支持该语义的同一文件系统内成功,且没有其他写者干扰。若读者在 rename 前已经 open 了旧目标,它持有的是旧对象的打开引用,不会自动转到新对象;同一个路径在不同打开时刻得到不同对象,正是名字和引用分离的结果。原子替换保证名字切换的整体性,不等于强行更新所有已有 fd,也不等于旧对象的所有其他硬链接一起改指向。

stat字段与文件实际占用(p.33–34)

课件程序与接口(p.33,原页片段)

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 */
};

p.34 To see stat information, you can use the command line tool stat.

File system keeps this type of information in an inode structure.

课件程序与接口(p.34,原页片段)

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/ root) 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

p.33 的stat结构描述对象而非返回文件内容:st_ino是inode号、st_mode含类型与权限、st_nlink是硬链接数、st_size是逻辑字节长度、st_blocks是已分配块计数。三个时间字段分别涉及访问、修改内容和状态变化;不能把ctime望文生义当作creation time。

p.34 的echo hello输出带换行,所以Size为6而不是5。算一遍:本例Blocks=8,以该stat接口使用的512字节计数单位解释(补充),分配占用8×512=4096字节;IO Block=4096是I/O提示尺寸,不能直接把Blocks=8再乘4096。文件逻辑长6字节与分配了4096字节并不矛盾。

目录与链接

unlink提出的问题与目录创建(p.35–36)

课件程序与接口(p.35,原页片段)

prompt> strace rm foo
…
unlink(“foo”)       = 0 // return 0 upon success
…
prompt>

p.35 Why it calls unlink() not “remove or delete”?

We can get the answer later.

p.36 mkdir(): Make a directory

When a directory is created, it is empty.

Empty directory have two entries: . (itself), .. (parent)

课件程序与接口(p.36,原页片段)

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 ../

课件程序与接口(p.36,原页片段)

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

p.35 的问题先留住“名字与对象分离”这条线索:unlink取消一个名字连接,不保证立即销毁文件对象;p.39–43会用硬链接算清。p.36 的mkdir建立目录,逻辑上的空目录仍有.指自身、..指父目录。空指没有其他用户目录项,不是目录文件没有任何内容。

补充:创建权限还受进程umask影响,因此调用参数0777与ls显示的最终权限可能不同。

读取目录与删除空目录(p.37–38)

p.37 A sample code to read directory entries (like ls).

课件程序与接口(p.37,原页片段)

#include <sys/stat.h>
#include <assert.h>
#include <stdio.h>
#include <dirent.h>

int main(int argc, char *argv[]) {
    DIR *dp = opendir(".");             // open current directory
    assert(dp != NULL);
    struct dirent *d;
    while ((d = readdir(dp)) != NULL)   // read one directory entry
    {
        // print outthe name and inode number of each file
        printf("%d %s\n", (int) d->d_ino, d->d_name);
    }
    closedir(dp);                       // close current directory
    return 0;
}

课件程序与接口(p.37,原页片段)

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

p.37 The information available within struct dirent

p.38 rmdir(): Delete a directory.

Require that the directory be empty

i.e. Only has “.” and “..” entries.

If you call rmdir()to a non-empty directory, it will fail.

p.37 先opendir取得目录流,循环readdir逐项取得dirent,打印d_ino与d_name,最后closedir。目录流指针不是普通文件描述符整数;目录项提供的是名字与inode等目录层信息,完整元数据可另用stat获取。

p.38 的rmdir要求没有.、..以外的项;普通非空目录删除会失败。补充:readdir返回NULL还可能表示错误,应结合errno判别;不同文件系统可能不提供可靠d_type,所以需要类型时不能无条件只依赖这一个字段。

硬链接增加名字,不复制内容(p.39–41)【新】

p.39 link(old pathname, new one)

Link a new file name to an old one

Create another way to refer to the same file

The command-line link program : ln

课件程序与接口(p.39,原页片段)

prompt> echo hello > file
prompt> cat file
hello
prompt> ln file file2  // create a hard link, link file to file2
prompt> cat file2
hello

p.40 The way link works:

Create another name in the directory.

Refer it to the same inode number of the original file.

The file is not copied in any way.

Then, we now just have two human names (file and file2) that both refer to the same file.

p.41 The result of link()

Two files have same inode number, but two file name (file, file2).

There is no difference between file and file2.

Both just link to the underlying metadata about the file.

课件程序与接口(p.41,原页片段)

prompt> ls -i file file2
67158084 file  /* inode value is 67158084 */
67158084 file2 /* inode value is 67158084 */
prompt>

p.39 的ln file file2创建另一个名字,p.40说明它指向同一inode,p.41用两个相同的67158084验证这一点。因此没有一个在语义上更“原始”的名字;通过任一名字修改同一对象,另一名字看到的是同一内容。补充:跨文件系统仅比较inode数字不能判同一对象,还需要文件系统/设备身份。

算一遍:示例起初1个名字,link后2个名字、1个inode、1份数据。它不是2个inode各持1份内容,也不是文件大小因此翻倍。

链接计数与最后一次unlink(p.42–43)

p.42 Thus, to remove a file, we call unlink().

reference count

Track how many different file names have been linked to this inode.

When unlink() is called, the reference count decrements.

If the reference count reaches zero, the files system free the inode and related data blocks.  truly “delete” the file

课件程序与接口(p.42,原页片段)

prompt> rm file
removed ‘file’
prompt> cat file2       // Still access the file
hello

p.43 The result of unlink()

stat() shows the reference count of a file.

课件程序与接口(p.43,原页片段)

prompt> echo hello > file           /* create file*/
prompt> stat file
... Inode: 67158084 Links: 1 ...    /* Link count is 1 */
prompt> ln file file2               /* hard link file2 */
prompt> stat file
... Inode: 67158084 Links: 2 ...    /* Link count is 2 */
prompt> stat file2
... Inode: 67158084 Links: 2 ...    /* Link count is 2 */
prompt> ln file2 file3              /* hard link file3 */
prompt> stat file
... Inode: 67158084 Links: 3 ...    /* Link count is 3 */
prompt> rm file                     /* remove file */
prompt> stat file2
... Inode: 67158084 Links: 2 ...    /* Link count is 2 */
prompt> rm file2                    /* remove file2 */
prompt> stat file3
... Inode: 67158084 Links: 1 ...    /* Link count is 1 */
prompt> rm file3

p.42 删除file后file2仍可读,回答了p.35:取消一个名字并不一定删除底层对象。p.43逐次增删名称:创建file后为1;ln file file2后2;ln file2 file3后3;rm file后2;rm file2后1;rm file3后0。

补充:课件以链接计数归零概括最终释放;若仍有打开引用,真实Unix文件通常还要等这些引用关闭才释放数据。链接数描述命名引用,不等于当前打开描述符数。进程可以在文件名被删后继续通过已打开fd访问它。

软链接存路径而不是给目标再添一个名字(p.44–46)【新】

p.44 Symbolic link is more useful than Hard link.

Hard Link cannot be created for a directory.

Hard Link cannot create to a file to other partition.

Because inode numbers are only unique within a file system.

Create a symbolic link: ln -s

课件程序与接口(p.44,原页片段)

prompt> echo hello > file
prompt> ln –s file file2  /* option –s : create a symbolic link, */
prompt> cat file2
hello

p.45 What is different between Symbolic link and Hard Link?

Symbolic links are a third type the file system knows about.

The size of symbolic link (file2) is 4 bytes.

A symbolic link holds the pathname of the linked-to file as the data of the link file.

课件程序与接口(p.45,原页片段)

prompt> stat file
 ... regular file ...
prompt> stat file2
 ... symbolic link ...  // Actually a file itself of a different type

课件程序与接口(p.45,原页片段)

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

p.46 If we link to a longer pathname, our link file would be bigger.

课件程序与接口(p.46,原页片段)

prompt> echo hello > alongerfilename
prompt> ln -s alongerfilename file3
prompt> ls -al alongerfilename file3
-rw-r----- 1 remzi remzi  6 May 3 19:17 alongerfilename
lrwxrwxrwx 1 remzi remzi 15 May 3 19:17 file3 -> alongerfilename

p.44指出普通硬链接不能跨文件系统,也通常不允许链接目录;软链接用路径解决这些限制。p.45说明软链接是独立类型的文件,其数据是目标路径。路径解析时先看到链接,再沿所存路径寻找对象,因而多了一层解析。

算一遍:链接内容为file时长度4字节,不是目标文件的6字节;p.46改为alongerfilename,路径字符数15,所以链接长度15,目标文件仍为6字节。读软链接本身的属性与跟随它去读目标属性是两种查询,应检查工具或API是否跟随链接。

悬空软链接与接口总览(p.47–48)

p.47 Dangling reference

When remove an original file, symbolic link points nothing.

课件程序与接口(p.47,原页片段)

prompt> echo hello > file
prompt> ln -s file file2
prompt> cat file2
hello
prompt> rm file     // remove the original file
prompt> cat file2
cat: file2: No such file or directory

p.47删除目标file后,软链接file2本身还在,但解析其存储路径失败,cat报告No such file or directory。它没有增加目标硬链接计数,因此不能像file2硬链接那样保住目标对象。

p.48的接口可按作用分组:open/read/write/lseek处理打开和字节位置;dup/pipe改变输入输出关系;fsync处理同步;rename/unlink/link处理名字;stat取元数据;目录流接口读取目录项。这样读接口序列,才能知道每行修改的是字节、偏移、描述符、名字还是持久化状态。

本讲核心考点

  • 文件名、inode、打开状态、fd是不同层次(p.5–7,C 级通识考法)。
  • dup共享打开状态;lseek改变该状态中的偏移(p.8–19,C 级通识考法)。
  • 管道0读1写;EOF取决于所有写端引用关闭(p.20–29,C 级通识考法)。
  • fsync与rename分别处理同步和名字切换(p.30–32,C 级通识考法)。
  • 硬链接共享inode,软链接存目标路径(p.39–47,C 级通识考法)。

附录

本份没有需要移至附录的长推导;计算过程已在对应页正文写全。

来源与证据

  • 原件:L04-系统调用-文件与目录-2022未核.pptx(本地课件);PDF副本(本地课件) p.1–48。本文页码均为 PDF 页号。

  • 教材:讲义 p.48 指向 OSTEP Chapter 40(下一讲)。

  • 考试证据:读了 课程大纲-2026T1.pdf、教学计划-2026T1.pdf、T08-期中参考-2025T2-题目-解答.pdf、T07-多级反馈队列-2025T2.pdf(p.13–15内嵌历史试题);未识别用途的 PDF:无

  • 2026-10-13 内容补全:保留原页顺序与版本,补入对应页的程序、接口、字段、条件和演算;原程序片段保留课件行号、拼写与省略处,不能据此视为经过编译的完整软件。本文已有运行记录属于其标注日期,本次未重新运行课程容器或提交作业。