CSCI3150 L01 Course Overview: Introduction to OS
课程:CSCI3150 Introduction to Operating Systems · 2026T1 本讲:L01 Course Overview: Introduction to OS · 原件 L01-课程概览-2026T1.pptx(本地课件) · PDF页码副本(本地课件) p.1–50 定位:从程序装入和CPU执行建立操作系统模型,逐步追踪两线程怎样把应得的52写成51,再连接到持久化。
p.1 标题页。
从机器到操作系统
本讲的位置与程序的装入(p.2–3)【新】
p.3 Loading:
Takes on-disk program
and reads it into the address space of process
p.3 Fetches an instruction from the memory.
Decode: which instruction this is
Execute: e.g arithmetic, logic, memory, control,
Repeat the above
图(p.2):自底向上的课程组织 元素:底层 Course Overview 被红框选中,其上依次为 System Calls、Persistence、Virtualization、Concurrency。 看什么:本讲先建立后续各章共同使用的机器与执行模型。
图(p.3):磁盘上的程序变为内存中的进程 元素:Disk 内含 code 与 static data,红色虚线指向 Memory 中的进程地址空间;地址空间包含 code、static data、heap、stack;CPU 经总线访问内存。 看什么:程序是存放的代码与数据;执行它时,系统建立运行时状态,CPU 才能从内存取指。
p.2 把文件系统放在持久化部分,把 CPU 与内存管理放在虚拟化部分,把线程与锁放在并发部分。p.3 则提供共同起点:磁盘不会自己执行程序。装入使指令和初始数据进入进程可访问的地址空间,随后程序才可能申请堆对象、形成函数调用栈。补充:堆通常存放动态分配对象,栈保存调用过程中的局部状态;这不是说整个虚拟空间必须一次性装满物理内存。
CPU 的取指、译码、执行(p.4)【新】
p.4 Fetch an instruction from memory.
Decode: Figure out which instruction this is
Execute: i.e., arithmetic/logic operations (e.g. add/and), memory, condition, branch, etc.
Repeat the above steps
图(p.4):简化 CPU 与内存 元素:控制单元含 PC、IR、PSR;通用寄存器中有 eax;ALU 接收操作数并给出结果;总线连接 CPU 与内存。 看什么:PC 给出要取哪条指令,IR 暂存取到的指令,ALU 执行算术逻辑,寄存器保存执行状态。
PC 是程序计数器,值是一个指令地址,不是已经执行的指令数。IR 是指令寄存器,装的是指令本身;PSR 保存处理器状态。取指读“怎么做”,数据访存读“对什么做”,两者都访问内存但含义不同。课件把运行写成循环:取指→译码→执行→再取指。
算一遍:本讲 p.22 的指令地址依次是 100、105、108,执行完后一条后 PC 为 113。它们相差 5、3、5,说明该例指令长度不同;不能机械地把 PC 每次加 1。PC=105 时取到加法指令,执行结果写寄存器,之后才指向 108 的存储指令。
操作系统管理什么,怎样提供服务(p.5–7)【新】
p.5 OS’s major functions
Manage Resources (virtualization)
CPU, Memory, Disk, etc.
Provide Services (system call)
Process (running program): Easily run programs
Memory: Share memory among processes
IO devices: Enable processes to interact with or operate/utilize devices
p.6 The OS manages resources such as CPU, memory and disk.
many programs to run (processes) Sharing the CPU
many processes to concurrently access their own instructions and data Sharing memory
many processes to access devices Sharing disks
etc.
How to achieve this – Virtualization
OS takes a physical resource and transforms it into a virtual form of itself.
Physical resource: Processor, Memory, Disk …
The virtual form is more general, powerful and easy-to-use.
We can refer to the OS as a “virtual” machine.
p.7 OS provides services (to utilize resources) via System Call (typically a few hundred) to run process, access memory/devices, etc.
p.7 The Design Of The Unix Operating System (Maurice Bach, 1986)
p.5 把 OS 的职责分为两面:管理 CPU、内存、磁盘等有限资源;向进程提供启动程序、管理内存、访问设备等服务。p.6 的虚拟化(virtualization)就是把有限物理资源组织成程序更容易使用的视图。程序看到自己像是在连续运行、拥有自己的地址空间,并不表示机器真的为每个程序新增了一套硬件。
p.7 的系统调用(system call)是请求这些服务的接口。它把“我要读取文件”与“设备如何完成读取”分开。图中 user programs、libraries 位于用户侧,system call interface 以下连接 file system 与 process control subsystem,底部才是 hardware control 和 hardware。
图(p.7):Unix 系统调用接口 元素:用户程序可经库调用进入 system call interface;文件系统连接 buffer cache、设备驱动;进程控制关联调度、进程间通信和内存管理;虚线区分用户、内核与硬件层。 看什么:库是程序可使用的一层接口,但真正管理受保护资源的请求进入内核。图不是说所有库函数都必须陷入内核。
补充:进入内核与换一个进程不是同一动作。 当当前程序执行一次系统调用时,CPU 可以仍在为这个进程做事,只是进入受保护的内核代码;若请求立即完成,就回到该程序继续执行。若请求必须等设备,内核才可能把它置为等待状态,选择另一个就绪进程。因而“系统调用发生了”不能直接推出“进程一定切换了”。反过来,程序没有主动调用任何阻塞接口,也可能因计时器中断而被调度器切走。
把这段过程接回 p.4 的 PC:用户代码有下一条指令位置,内核服务有自己的执行位置;恢复用户执行时必须回到正确位置,并交还规定的返回值。程序不是从 main 重新开始。这里先建立执行连续性,后面读 fork、缺页恢复与线程切换时,才能分清哪些地方继续原执行流、哪些地方开始另一条执行流。
虚拟化的两个可见效果
一个 CPU 上的四个程序(p.8–10)【新】
p.8 The system has a very large number of virtual CPUs.
Turn a single CPU into a seemingly infinite number of CPUs.
Allow many processes to seemingly run at the same time Virtualizing the CPU
课件程序与接口(p.9,原页片段)
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <sys/time.h>
4 #include <assert.h>
5 #include <unistd.h>
6
7 int main(int argc, char *argv[])
8 {
9 if (argc != 2) {
10 fprintf(stderr, "usage: cpu <string>\n");
11 exit(1);
12 }
13
14 char *str = argv[1];
15 int i=0;
16
17 while (i<100) {
18 sleep(1);
19 i++;
20 printf("%s\n", str);
21 }
22 return 0;
23 }
24
p.9 Simple Example(cpu.c): Code that loops and prints
课件程序与接口(p.10,原页片段)
prompt> gcc -o cpu cpu.c -Wall
prompt> ./cpu A
A
A
A
ˆC
prompt>
p.8 的“seemingly”是关键:多个程序在一段时间内都取得进展,并不等于单个 CPU 同一瞬间执行四条指令。p.9 的 cpu.c 接受一个字符串参数,令 i=0,每轮先 sleep(1)、再递增 i、再打印字符串;i<100 时继续。p.10 先编译成 cpu,再执行 ./cpu A,终端看见多行 A;Ctrl-C 可以提前打断。
算一遍:沿 p.9 代码,第一次检查 0<100 成立,休眠后 i=1,打印第 1 次;第 100 轮后 i=100,再检查失败。因此正常结束打印 100 次。课件写的是每轮请求休眠一秒,不能由此推出整个程序严格只耗时 100 秒;打印与调度也需要时间(补充)。
后台运行与地址是什么(p.11–12)
课件程序与接口(p.11,原页片段)
prompt> ./cpu A & ./cpu B & ./cpu C & ./cpu D &
[1] 7353
[2] 7354
[3] 7355
[4] 7356
A
B
D
C
A
B
D
C
A
C
B
D
...
p.11 Even though we have only one CPU, the four processes seem to be running at the same time!
p.12 The physical memory is an array of bytes.
A program keeps all of its data structures in memory.
Read memory (load):
Specify an address to be able to access the data
Write memory (store):
Specify the data to be written to the given address
p.11 在四条命令后使用 &,终端因而不用等前一条结束再启动下一条。输出出现 A、B、D、C 等交错,表明四个执行流都在取得进展;输出的某一次顺序不能当成每次都成立的调度规则。补充:这段程序大量休眠,一个进程等待期间 CPU 可以服务另一个,因此它也不是证明“四倍 CPU 算力”的基准测试。
p.12 把物理内存看成按字节编号的数组。load 根据地址取内容,store 同时需要地址与要写的值。地址回答“哪个位置”,值回答“那里是什么”;把两者混为一谈,后面看到相同指针数值就会误以为一定在共享数据。
动态内存与两份互不干扰的计数(p.13–15)
课件程序与接口(p.13,原页片段)
1 #include <unistd.h>
2 #include <stdio.h>
3 #include <stdlib.h>
4 #include <assert.h>
5 #define MAX 100000000
6 int
7 main(int argc, char *argv[])
8 {
9 int *p = malloc(MAX*sizeof(int)); // a1: allocate memory
10 assert(p != NULL);
11 printf("(%d) Address Range: %p - %p\n",getpid(),
p, (p+MAX-1));
*p = 0; // a3: put zero into the first slot of the memory
13 int i=0;
14 while (i<10) {
15 sleep(1);
16 *p = *p + 1; i=i+1;
17 printf("(%d) p: %d\n", getpid(), *p);
18 }
19 return 0;
}
p.14 The output of the program mem.c
The newly allocated memory range is at Address Range: 0x7fca105ba010 - 0x7fca2833240c
It updates the value and prints out the results.
课件程序与接口(p.14,原页片段)
prompt> ./mem
(25187) Address Range: Address Range: 0x7fca105ba010 - 0x7fca2833240c
(25187) p: 1
(25187) p: 2
(25187) p: 3
(25187) p: 4
(25187) p: 5
(25187) p: 6
(25187) p: 7
(25187) p: 8
(25187) p: 9
(25187) p: 10
p.15 Running mem.c multiple times
The memory ranges from two processes are overlapped – Each process has its own private memory.
Each running program has allocated memory at its own address space.
Each seems to be updating the value independently.
课件程序与接口(p.15,原页片段)
prompt> ./mem & ./mem &
(25274) Address Range: 0x7fb44a50d010 - 0x7fb46228540c
(25275) Address Range: 0x7fb44a50d010 - 0x7fb46228540c
(25274) p: 1
(25275) p: 1
(25274) p: 2
(25275) p: 2
(25274) p: 3
(25275) p: 3
(25274) p: 4
(25275) p: 4
...
p.13 的 malloc(MAX*sizeof(int)) 申请 MAX 个整数的空间,成功后 p 是首元素指针;p+MAX-1 指向最后一个整数的起始位置。代码只修改 *p,并没有遍历整个数组。p.14 的一个进程从 0 开始,每轮加一,依次打印 1 到 10。
算一遍:MAX=100000000。若按课件地址差所对应的 4 字节整数计算,空间为 100000000×4=400000000 字节;最后元素起点与首地址相差 99999999×4=399999996 字节。最后一个元素还占 4 字节,所以不能用“两地址之差”代替整个分配长度。
p.15 两个 PID 打印相同的虚拟地址区间,却分别输出 1、2、3、4。这说明数值相同的地址可以属于不同进程的地址空间。PID 区分进程;p 是进程内部使用的地址;两者要一起看,不能跨进程只比较指针数字。
补充:同一指针数字为什么能指向不同东西。 可以把虚拟地址想成“房间内第 16 个抽屉”,进程身份像房间号。P1 的地址 16 与 P2 的地址 16,缺少房间号时看似相同,实际需要分别查各自映射。这个比喻只帮助理解命名,真实硬件并不在每次访存时拼接 PID 文本。映射由当前地址空间的配置决定。
把 p.15 的观察分成三步:先确认是两个 PID;再确认两个进程各自修改 *p;最后看其中一个递增是否改变另一个的值。相同打印地址只支持“虚拟布局可能相同”,不能单独证明隔离,也不能单独证明共享。要证明共享,必须有共同映射的依据,再观察双方对同一对象的读写。显式共享内存、同一进程的两个线程与普通 fork 后私有对象是三种不同情况。
私有地址空间与共享物理内存(p.16)【新】
p.16 Each process accesses its own private virtual address space.
The OS maps address space onto the physical memory.
A memory reference within one running program does not affect the address space of other processes.
Physical memory is a shared resource, managed by the OS.
程序通常在自己的虚拟地址空间里访问内存。操作系统管理映射,把访问送到正确的物理位置,并维持进程之间的隔离。物理内存作为硬件是共享资源,而“每个进程有私有地址空间”描述的是程序看到的访问规则,两句话并不冲突。
补充:这页描述默认私有内存模型。显式共享内存是另外配置的映射,不能拿它反过来否定本页;具体转换机制将在 L09–L12 展开。
考试角度:(C 级 通识)常见考法是给两个 PID 与相同指针值,判断是否足以证明它们操作同一物理对象;仅凭指针值不足以证明。
并发为什么会丢失更新
两个线程怎样运行同一个 worker(p.17–19)【新】
p.17 The OS is juggling many things at once, first running one process, then another, and so forth.
Modern multi-threaded programs also exhibit the concurrency problem.
课件程序与接口(p.18,原页片段)
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <pthread.h>
4
5 volatile int counter = 0;
6 int loops;
7
8 void *worker(void *arg) {
9 int i;
10 for (i = 0; i < loops; i++) {
11 counter++;
12 }
13 return NULL;
}
p.19 The main program creates two threads.
Thread: a function running within the same memory space. Each thread starts running in a routine called worker().
worker(): increments a counter
课件程序与接口(p.19,原页片段)
16 int
17 main(int argc, char *argv[])
18 {
19 if (argc != 2) {
20 fprintf(stderr, "usage: threads <value>\n");
21 exit(1);
22 }
23 loops = atoi(argv[1]);
24 pthread_t p1, p2;
25 printf("Initial value : %d\n", counter);
26
27 pthread_create(&p1, NULL, worker, NULL);
28 pthread_create(&p2, NULL, worker, NULL);
29 pthread_join(p1, NULL);
30 pthread_join(p2, NULL);
31 printf("Final value : %d\n", counter);
32 return 0;
33 }
p.17 把问题从多个进程推进到同一个程序中的多个线程。p.18 的共享变量 counter 初值为 0;每个 worker 做 loops 次 counter++。p.19 的主线程创建 p1 与 p2,再分别 pthread_join,最后打印 counter。
这里要分清两种约束:join 让主线程等 worker 结束,因此最后打印发生在两个 worker 结束以后;它没有限制两个 worker 在循环内部怎样交错。补充:volatile 也不把读、加、写合成不可分割的操作。语言层面的数据竞争还涉及 C 的内存模型,本讲先用三条机器指令的教学模型展示丢失更新。
从正确的期望值到错误的实测值(p.20–21)
p.20 loops determines how many times each of the two workers will increment the shared counter in a loop.
loops: 1000.
loops: 1000000000
.
课件程序与接口(p.20,原页片段)
prompt> gcc -o thread thread.c -Wall -pthread
prompt> ./thread 1000
Initial value : 0
Final value : 2000
课件程序与接口(p.20,原页片段)
prompt> ./thread 1000000000
Initial value : 0
Final value : 1997974414 // huh??
prompt> ./thread 1000000000
Initial value : 0
Final value : 1997940107 // what the??
p.21 Increment a shared counter take three instructions.
Load the value of the counter from the memory into a register.
Increment it
Store it back into the memory
These three instructions do not execute atomically. The problem of concurrency happens.
p.20 在 loops=1000 时给出 2000,但在 loops=1000000000 时给出 1997974414 与 1997940107。前一次“恰好正确”没有证明程序安全,只说明那次运行没有显露出错误结果。p.21 将自增拆为 load、increment、store;原子性要求其他执行流看不到把整体拆开的中间状态,这三条指令却可以被切换打断。
算一遍:大循环的理想值为 2×1000000000=2000000000。第一份输出少了 2000000000−1997974414=2025586;第二份少了 2059893。差额展示丢失的净更新量,不能据此恢复唯一的线程切换时序。
初值 50 为什么只变成 51(p.22)
题面(L01-课程概览-2026T1.pdf · 两线程自增示例 · p.22)
p.22 Example with two threads
p.22 counter = counter + 1 (initial value: 50)
p.22 We expect the result is 52. However,
表(p.22):讲义所示交错,PC 与 eax 为当前运行线程的状态
| 动作 | PC | eax | counter |
|---|---|---|---|
| T1 初始 | 100 | 0 | 50 |
| T1 读内存 | 105 | 50 | 50 |
| T1 加一 | 108 | 51 | 50 |
| 保存 T1、恢复 T2 | 100 | 0 | 50 |
| T2 读内存 | 105 | 50 | 50 |
| T2 加一 | 108 | 51 | 50 |
| T2 写回 | 113 | 51 | 51 |
| 保存 T2、恢复 T1 | 108 | 51 | 51 |
| T1 写回 | 113 | 51 | 51 |
算一遍:T1 把 50 读入自己的寄存器,算出 51,还没写内存就切换;T2 仍从内存读出 50,也算出 51 并写回;恢复 T1 时恢复的是它保存的寄存器 51,所以它再次写 51。两次加法都执行了,但两次都基于旧值 50,第二次存储覆盖的是相同结果,最终只净增 1。下列各页逐步拆开表中的取指与执行阶段。
补充:先认清这三条汇编指令的读法。 本例采用源操作数在左、目的操作数在右的写法。%eax 是寄存器名;$0x1 中的 $ 表示立即数,0x1 就是十进制 1;没有 $ 的 0x8049a1c 在这里表示要访问的内存位置。因此三行应读成:
| 原指令 | 读哪里 | 写哪里 | 此步是否修改共享 counter |
|---|---|---|---|
mov 0x8049a1c, %eax |
counter 所在的内存 | 当前线程的 eax | 否,只取得旧值 |
add $0x1, %eax |
eax 和常数 1 | 当前线程的 eax | 否,只计算中间值 |
mov %eax, 0x8049a1c |
当前线程的 eax | counter 所在的内存 | 是,覆盖原内存值 |
mov 是复制,不是把源位置清空。图里还有两类“内存格”:一类存共享 counter,另一类存暂停线程的 PC、eax 现场。它们即使都画在 Memory 框内,也不是同一个变量。切换时复制保存格只负责记住线程做到哪里,不会顺带把 eax 中算出的 51 提交给 counter。带着这个区分逐页读 p.23–45,就能判断每个箭头究竟是取指、读数据、保存现场还是写共享变量。
初始状态(p.23)
图(p.23):CPU、共享 counter 与线程保存区 元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。 看什么:PC=100,eax=0,内存 counter=50。PC 指向读内存指令;eax 尚未载入 counter。图中还保存了 T2 的起始状态 100、0,说明线程停下时的 PC 和寄存器可以放在内存里。
T1 取读指令(p.24)
p.24 Step 1: Fetch instruction from the memory (PC->Address=100)
图(p.24):CPU、共享 counter 与线程保存区
元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。
看什么:CPU 根据 PC=100 把 mov 0x8049a1c, %eax 装入 IR。此时只取了指令,eax 仍为 0,counter 仍为 50;不能把取指阶段误算成数据已经读完。
T1 执行读取(p.25)
图(p.25):CPU、共享 counter 与线程保存区 元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。 看什么:译码确认数据源是地址 0x8049a1c,执行后 eax=50,内存仍为 50。箭头从内存数据格指向寄存器,说明是 load。
T1 前进到加法(p.26)
p.26 Step 4: Increase PC (pointed to the next instruction in the memory)
图(p.26):CPU、共享 counter 与线程保存区 元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。 看什么:PC 更新为 105,eax 保留 50。保存程序进度靠 PC,保存计算中间值靠寄存器,二者必须同时考虑。
T1 取加法指令(p.27)
p.27 Step 1: Fetch instruction from the memory (PC->Address=105)
图(p.27):CPU、共享 counter 与线程保存区
元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。
看什么:PC=105 把 add $0x1, %eax 取入 IR。立即数 1 是指令的操作数,尚未执行时 eax 仍为 50。
T1 只在寄存器里加一(p.28)
图(p.28):CPU、共享 counter 与线程保存区 元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。 看什么:ALU 算 50+1=51,写入 eax。counter 仍为 50,因为本条指令的目的地是寄存器,没有发生 store。
T1 即将写回(p.29)
p.29 Step 3: Increase PC (pointed to the next instruction in the memory)
图(p.29):CPU、共享 counter 与线程保存区 元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。 看什么:PC 前进到 108,即写回指令;eax=51,counter=50。危险窗口就是新值只存在寄存器里,其他线程仍能读到旧内存值。
保存 T1 的状态(p.30)
图(p.30):CPU、共享 counter 与线程保存区 元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。 看什么:中断发生,图把 PC=108、eax=51 保存到 T1 的状态区。保存执行现场不等于执行尚未运行的写回指令,因此共享 counter 仍为 50。
恢复 T2 的状态(p.31)
图(p.31):CPU、共享 counter 与线程保存区 元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。 看什么:从 T2 状态区取回 PC=100、eax=0。此后处理器为 T2 执行。T1 的 108、51 留在内存的保存区,不再是 CPU 当前寄存器值。
T2 取读指令(p.32)
图(p.32):CPU、共享 counter 与线程保存区 元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。 看什么:PC=100 再次取得 load 指令;执行主体与 T1 相同,但此刻属于 T2 的控制流。取指本身不修改 counter。
T2 读到旧的 50(p.33)
图(p.33):CPU、共享 counter 与线程保存区 元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。 看什么:内存尚未被 T1 写过,所以 T2 load 后 eax=50。它不会自动取得 T1 保存区中的 51;保存区是执行现场,不是共享 counter。
T2 前进到 105(p.34)
图(p.34):CPU、共享 counter 与线程保存区 元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。 看什么:PC=105,eax=50,counter=50。现在 T2 正处于 T1 曾经过的加法前状态。
T2 取得加法(p.35)
图(p.35):CPU、共享 counter 与线程保存区 元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。 看什么:IR 接收加一指令,当前寄存器仍为 50;必须到下一页的执行阶段才变成 51。
T2 算出相同结果(p.36)
图(p.36):CPU、共享 counter 与线程保存区 元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。 看什么:eax 从 50 加一成为 51,counter 仍为 50。两个线程各算出一个 51,并不会自动把两个结果加成 52。
T2 准备写回(p.37)
图(p.37):CPU、共享 counter 与线程保存区 元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。 看什么:PC=108,指向 store。到这里 T2 的本地结果已计算完成,但共享位置还没变化。
T2 取存储指令(p.38)
p.38 Step 1: Fetch instruction from the memory (PC->Address=108)
图(p.38):CPU、共享 counter 与线程保存区
元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。
看什么:IR 装入 mov %eax, 0x8049a1c。与前面的 load 相比源、目的颠倒;这次将寄存器值送回内存。
T2 写出第一个 51(p.39)
图(p.39):CPU、共享 counter 与线程保存区 元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。 看什么:执行 store,counter 由 50 变为 51,eax 仍为 51。共享内存到这一步才第一次改变。
T2 完成这次自增(p.40)
图(p.40):CPU、共享 counter 与线程保存区 元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。 看什么:PC=113,表示越过三条自增指令。其自增作为一段程序已结束,但 T1 还有一条旧结果的 store 等待执行。
保存 T2(p.41)
图(p.41):CPU、共享 counter 与线程保存区 元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。 看什么:操作系统保存 T2 的 PC=113、eax=51。counter 保持 51;保存现场本身不增加 counter。
恢复 T1(p.42)
图(p.42):CPU、共享 counter 与线程保存区 元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。 看什么:PC 恢复为 108,eax 恢复为 51。CPU 回到的是 T1 被打断的位置,不会从 100 重新 load,因此不会重新读取内存中的最新 51 再加一。
T1 取尚未执行的 store(p.43)
图(p.43):CPU、共享 counter 与线程保存区 元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。 看什么:PC=108 取回写内存指令。页中当前 CPU 的 PC=108、eax=51,与共享 counter=51 是本次结果的关键状态;保存区的非活动标签不参与这条 store 的计算。
T1 覆盖为同一个 51(p.44)
图(p.44):CPU、共享 counter 与线程保存区 元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。 看什么:把 eax=51 写入已为 51 的 counter,内存值不变。这就是丢失更新:没有丢掉线程,也没有跳过加法,丢的是两个逻辑加一应当累积的效果。
最终 PC 与最终值(p.45)
图(p.45):CPU、共享 counter 与线程保存区 元素:PC、IR、eax、三条指令地址 100/105/108、counter 地址 0x8049a1c,以及 T1/T2 的 PC 与 eax 保存格;红色数值及箭头标记本步变化。 看什么:PC=113,counter=51。理想串行执行应先写 51,第二个线程再读 51、算 52、写 52;实际交错中第二次 load 太早,所以最终差 1。
数据为什么能够跨越一次运行
持久化与文件接口(p.46–48)【新】
p.46 Devices such as DRAM store values in a volatile manner.
Hardware and software are needed to store data persistently.
Hardware: I/O device such as a hard drive, solid-state drives(SSDs)
Software:
File system manages storage devices (hard drives, SSDs, etc.).
File system is responsible for storing files that users create.
Regular files
Directory files
Special files (e.g. devices – keyboards, monitors, etc.)
etc.
p.47 Create a file (myfile) that contains the string “hello world” (fs.c)
课件程序与接口(p.47,原页片段)
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <assert.h>
4 #include <fcntl.h>
5 #include <sys/types.h>
6
7 int
8 main(int argc, char *argv[])
9 {
10 int fd = open("myfile", O_WRONLY | O_CREAT | O_TRUNC, S_IRWXU);
11 assert(fd > -1);
12 int rc = write(fd, "hello world\n", 13);
13 assert(rc == 13);
14 close(fd);
15 return 0;
16 }
p.47 open(), write(), and close() system calls are routed to the part of OS called the file system, which handles the requests
p.48 What OS does in order to write data to the disk?
Store file meta data in the disk (file name, creation time, etc.)
Figure out where on the disk this new data will reside
Issue I/O requests to the underlying storage device
File system handles system crashes during write.
Journaling or copy-on-write
Carefully order writes to disk
p.46 说明 DRAM 中的数据不能作为断电后仍保存的承诺。持久化需要硬盘、SSD 等设备,也需要文件系统组织文件、目录与设备相关对象。硬件回答“在哪里存”,软件回答“哪个文件的哪些字节放在哪里”。
p.47 的程序先 open 获取文件描述符 fd,以写入、创建和截断标志打开 myfile;再 write;最后 close。描述符是操作系统提供的引用编号,不是磁盘块地址。算一遍:本页字符串 hello world\n 含 5+1+5+1=12 个可见字符及换行字节,末尾还有一个 C 字符串终止字节,因此代码的写入长度 13 会把该终止字节也写进文件。若只要求通常的文本行,长度应是 12;这是对页上代码行为的逐字节解释,不把 13 默改成 12。
p.48 把写文件拆为保存元数据、定位存储位置、发出 I/O 请求。发生崩溃时,数据和元数据可能只完成一部分,文件系统因而还要安排写入顺序,或采用日志、写时复制来维持可恢复状态。补充:一次 write 返回成功与设备已经抗断电持久化不是同一个条件,本页未展开同步持久化接口。
目标之间的关系与本讲收束(p.49–50)
p.49 Build up abstraction
Make the system convenient and easy to use.
Provide high performance
Minimize the overhead of the OS.
OS must strive to provide virtualization without excessive overhead.
Protection between applications
Isolation: Bad behavior of one does not harm others and the OS itself.
High degree of reliability
It is desirable for OS to run non-stop.
Other issues: Energy-efficiency, Security, Mobility
p.50 Computer architecture (Von Neumann): CPU, Memory, and IO devices
OS main functions
Manage resources via virtualization
CPU, Memory, IO devices (e.g. storage (e.g. hard drives), keyboard, etc.)
Provide services via system calls
Process (run programs), memory management, file, etc.
Design goals
Abstraction, high performance, protection, reliability etc.
Related Chapter: Chapter 2 (2.1 – 2.5)
Next: User-level programming using system calls (process, memory, file)
Chapter 5 (Process) , Chapter 14 (Memory) , Chapter 39 (Files)
Self study: Lab tutorial
p.49 列出抽象、性能、保护、可靠性以及能耗、安全、移动性。抽象让程序少处理硬件细节;性能要求抽象的成本可控;保护防止程序破坏其他进程与 OS;可靠性要求系统持续正确运行。它们是设计评价维度,某个接口方便并不自动说明它开销小或隔离强。
p.50 把硬件、虚拟化和系统调用重新连起来:硬件执行指令,OS 管理资源,程序通过接口使用服务。下一讲不从调度算法开始,而是先学习用户程序怎样创建与控制进程,这与 p.2 的自底向上路线一致。
从丢失更新反推正确保护范围(补充,教材通识)
p.22 的错误并不是 CPU 算错了 50+1,而是两个执行流都把同一个旧值当作起点。修复时需要保护整个“读旧值→计算新值→写回”事务。若只给最后一条 store 上锁,T1、T2 仍可能先各自在寄存器里算出 51;它们轮流存储 51 依旧只增加一次。若锁只存在于 T1 的代码而 T2 完全不遵守,同样无法形成互斥。
画时序时每行只改变被执行操作会影响的状态:load 改寄存器,不改 counter;算术改寄存器,不改另一线程的寄存器;store 才改共享内存。上下文切换保存/恢复的是各线程的 PC 和寄存器,不能把 T2 的临时计算结果自动送给 T1。这个规则也解释为什么 join 只能保证最后观察发生在工作结束以后,不能修复工作期间已经覆盖掉的数据。
自测:若每次运行都得到正确总数,是否证明没有数据竞争?
先区分观测与保证。一次正确输出只记录这次调度的一个结果;可能恰好没有发生会暴露丢失更新的交错。
进一步说,真实 C 程序对普通共享对象发生未同步数据竞争还涉及未定义行为,三指令模型只用于解释一种失败方式,并不是枚举其所有合法输出的语言保证。证明应指出所有共享访问使用的同步关系,而不是增加循环次数后“暂时没有复现”。
本讲核心考点
- 程序与进程、PC与IR的区别(p.3–4,C 级通识考法)。
- 系统调用提供服务,虚拟化组织有限资源(p.5–8,B 级大纲学习目标)。
- 相同虚拟地址不证明共享物理对象(p.12–16,C 级通识考法)。
- load/add/store交错导致50变51,join不使自增互斥(p.18–45,C 级通识考法)。
- 持久化同时涉及数据、元数据与I/O(p.46–48,B 级大纲主题)。
附录
本份没有需要移至附录的长推导;计算过程已在对应页正文写全。
来源与证据
-
原件:L01-课程概览-2026T1.pptx(本地课件);PDF副本(本地课件) p.1–50。本文页码均为 PDF 页号。
-
教材:OSTEP Chapter 2,2.1–2.5;讲义 p.50 指向 Chapter 5、14、39。
-
考试证据:读了 课程大纲-2026T1.pdf、教学计划-2026T1.pdf、T08-期中参考-2025T2-题目-解答.pdf、T07-多级反馈队列-2025T2.pdf(p.13–15内嵌历史试题);未识别用途的 PDF:无
-
2026-10-13 内容补全:保留原页顺序与版本,补入对应页的程序、接口、字段、条件和演算;原程序片段保留课件行号、拼写与省略处,不能据此视为经过编译的完整软件。本文已有运行记录属于其标注日期,本次未重新运行课程容器或提交作业。