CSCI3150 L02 User-level Programming via System Calls (Process)
课程:CSCI3150 Introduction to Operating Systems · 2026T1 本讲:L02 User-level Programming via System Calls (Process) · 原件 L02-系统调用-进程-2026T1.pptx(本地课件) · PDF页码副本(本地课件) p.1–22 定位:能逐分支追踪fork、wait与exec,算清进程数和先后关系,并辨别孤儿、僵尸及后台进程。 承接:#L01-03 系统调用接口
p.1 标题页。
一个程序怎样成为进程
从系统调用进入进程管理(p.2–3)【延伸 L01 p.5–7】
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.2):本讲所在的系统调用层 元素:红框位于 System Calls (User-level Programming),上层依次连接文件系统、调度、内存与并发。 看什么:先掌握进程接口的可见行为,后面才讨论操作系统如何实现它。
p.3 的 Unix 图把 system call interface 与 process control subsystem 相连,其中涉及调度、进程间通信及内存管理。进程请求并不只是“运行一个函数”:内核要维护可以独立调度、占用资源、被识别和回收的执行对象。这里承接 #L01-03,下面聚焦创建、等待、换程序与结束。
静态程序与动态实例(p.4–5)【新】
p.4 A process is an instance of a program in execution (running program)
process is dynamic while program is static.
A process is the basic unit for competing the resources and CPU scheduling.
What happens when a program become a process?
The program is read into memory,
A unique process ID is assigned,
Appropriate information is added to the OS kernel structure,
Necessary resources (e.g. registers) to run the program code is allocated, and
Have a thread of execution – a stream of instructions and a program counter.
p.5 A process has
a unique ID,
a thread of control, and
some private data
A hierarchy of processes by parent-child links
The process that executes the request for creation of a process is called the parent of that process, and the created process is called the child.
A child process inherits all the properties of its parent when it is created
p.4 的进程(process)是程序的一次执行实例。磁盘上的同一份程序可以启动多次,得到多个不同进程;每次启动要有地址空间、进程标识、内核管理信息和执行流。执行流中的 PC 表示运行到了哪条指令,所以仅有程序文件不足以描述正在运行的状态。
p.5 用父子关系组织进程:发出创建请求的是父进程,创建出来的是子进程。子进程继承创建时的许多属性,但课件“All the properties”应结合后续 fork 语义读:子进程有自己的 PID,fork 返回值也不同。补充:继承不表示从此所有属性共同变化;私有内存中同一个变量的两份副本随后可以不同。
PID、PPID、UID与观察工具(p.6–7)
p.6 How can we identify processes from one to another?
Each process is given an unique ID number, and is called the process ID, or PID.
The system call, getpid(), prints the PID of the calling process.
UID: Each user has a unique user ID
Each process is associated with a particular user called the owner of the process
The owner has certain privileges with respect to the process.
Use getpid, getppid and getuid to obtain the ID of the child, the parent, and the owner.
课件程序与接口(p.6,原页片段)
#include <stdio.h> // printf()
#include <unistd.h> // getpid()
int main(void) {
printf("My PID is %d\n", getpid() );
return 0;
}
p.7 ps is the short for process status.
“ps” lists your current processes
“ps -a ” lists more processes, including ones being run by other users and at other terminals (but not include the shells)
“ps -l ” prints longer, more information lines, including UID, PID, PPID, process status, etc.
p.6 的 PID 标识进程,PPID 标识父进程,UID 标识用户身份。getpid() 返回调用者的 PID,getppid() 返回调用者的父 PID,getuid() 返回用户 ID;打印由 printf 完成,不能把 getpid 当打印函数。课件把连续运行示例列成 1234、1235、1237,恰好也说明 PID 不需要连续。
p.7 用 ps 观察进程。默认视图、-a 与 -l 展示范围和字段不同。读输出时先找列名,再比较 PID、PPID、UID 与状态;用户相同不代表父进程相同。补充:ps 是某一时刻的快照,进程可能已在下一瞬间结束。
fork:从一个执行流变成两个
分叉后从哪里继续(p.8–10)【新】
p.8 To create a process, we use the system call fork().
课件程序与接口(p.9,原页片段)
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <unistd.h>
4
5 int main(void)
6 {
7 int ret_from_fork;
8
9 printf("Before: my pid is %d\n", getpid());
10
11 ret_from_fork = fork();
12
13 sleep(1);
14
15 printf("After: my pid is %d, return value from fork():%d\n",
16 getpid(), ret_from_fork);
17
18 return 0;
19 }
p.9 Simple Example(fork.c): The code that creates a child process
p.10 The value returned by fork() allows the parent and the child to execute different part of the code.
The fork returns 0 to the child and returns the child’s PID to the parent:
课件程序与接口(p.10,原页片段)
1 int main(void) {
2 int result;
3 printf("before fork ...\n");
4 result = fork();
5 printf("result = %d.\n", result);
6
7 if(result == 0) {
8 printf("I'm the child.\n");
9 printf("My PID is %d\n", getpid());
10 }
11 else {
12 printf("I'm the parent.\n");
13 printf("My PID is %d\n", getpid());
14 }
15
printf("program terminated.\n");
return 0;
17 }
p.8 图里原执行流继续向右,子执行流从 fork 位置分出;子进程从 fork 返回之后继续,而不是从 main 开头重跑。p.9 的 Before 在 fork 前执行一次;后面的 After 在两个进程中各执行一次,前提是创建成功、终端输出按示例处理。sleep 让两个进程都等待一下,却没有建立谁先输出的严格顺序。
p.10 用返回值分支:子进程拿到 0,父进程拿到子 PID。补充:真实代码还要先处理返回 −1 的失败分支;不应像简化示例那样把所有非零都直接当成功的父分支。假设父 PID=200、子 PID=201(数字是我为演示取的,讲义没给),父进程中 getpid=200、fork 返回201;子进程中 getpid=201、fork 返回0。0 是分支标签,不是子进程的真实 PID。
考试角度:(C 级 通识)常见考法是分别填父子进程的 getpid 与 fork 返回值,或判断哪些输出的先后顺序没有保证。
补充:把“复制”拆成三个问题。 先有一个父进程 P,局部变量 x=7,并已打开一个普通文件。fork 成功后,P 和新子进程 C 都从下一条语句继续,但不是两个线程在修改同一个局部变量。下表是教材通识示例,数值不是课件给定值。
| 动作 | P 的私有 x | C 的私有 x | 内核中的打开文件偏移 |
|---|---|---|---|
| fork 刚返回 | 7 | 7 | 0 |
| C 执行 x=9 | 7 | 9 | 0 |
| C 经继承的 fd 实际读 2 字节 | 7 | 9 | 2 |
| P 随后经该 fd 实际读 1 字节 | 7 | 9 | 3 |
第一列变量的分离来自私有地址空间。最后一列共享来自另一条关系:fork 复制了描述符表中的引用,两份表项仍指向同一次打开的内核对象;它没有重新 open 文件。这里明确假设 C 的 read 先完成,P 再 read;没有同步时不能擅自给它们排顺序。后续 L04 会把这三层画成 FDT→打开文件状态→inode。写时复制是实现私有内存的一种优化,不改变这里“C 改 x 不等于 P 改 x”的可见语义。
还要单独处理 C 库缓冲:printf 已调用不等于字节已送入内核。fork 前尚未刷出的用户态输出缓冲也可能随私有内存复制,两个进程正常退出时各自刷新,造成一段输出出现两次。因此 p.9 的 Before 一次指的是通常终端行缓冲条件下的示例;把输出重定向到文件后,不能仅凭源码位置预测实际出现次数。分析时先问“打印函数执行几次”,再问“缓冲何时真正写出”。
三次连续 fork 与链、扇形(p.11)
课件程序与接口(p.11,原页片段)
How many?
fork();
fork();
fork();
题面(L02-系统调用-进程-2026T1.pdf · How many? · p.11,图片视觉核对)
p.11 How many?
fork();
fork();
fork();
图(p.11):三种创建形状 元素:左侧为 1→2→3→4 的链;中间为 1 分别指向 2、3、4 的扇形;右侧是上述三次连续调用。 看什么:链和扇形需要控制哪些进程继续创建;无条件连续调用让所有现存进程都继续 fork。
算一遍:假定每次调用成功,开始1个;第一次后2个;这2个都执行第二次,所以4个;这4个再各创建1个,所以8个。新建子进程数是1+2+4=7,含原进程总数是8。不能因为源码只写三行就回答“3个子进程”。完整按每轮存活执行流计数见附录 A1。
链要求每轮只有最新的子进程继续创建;扇形要求只有原父进程继续创建。两种形状都有4个节点,但父子关系不同,因此后续谁能 wait 谁也不同。
A1 三次无条件 fork 的逐轮计数(p.11)
用 P 表示原进程,并假定所有调用成功。第一次 P 产生 A,当前集合为 P、A。第二次 P 产生 B,A 产生 C,集合为 P、A、B、C。第三次这四者分别产生 D、E、F、G,集合为 P、A、B、C、D、E、F、G。每一轮新创建者也从本轮 fork 后继续,因此只参加后面的调用,不能倒回去参加同一行 fork。
| 完成哪一轮 | 调用本轮前的进程数 | 本轮新增 | 本轮后总数 |
|---|---|---|---|
| 第一轮 | 1 | 1 | 2 |
| 第二轮 | 2 | 2 | 4 |
| 第三轮 | 4 | 4 | 8 |
回看关系可见并非四级直链,也并非原父进程生七个孩子;原父直接生了A、B、D三个,A又生C、E,B生F,C生G。
等待、换程序与结束
wait在等待什么(p.12)【新】
课件程序与接口(p.12,原页片段)
After fork, both parent and child proceed independently. If a parent wants to wait until the child finishes, it executes wait or waitpid.
pid_t wait(int *stat);
In general, the wait system call
Cause the caller process to pause until a child terminates or stops or until the caller receives a signal.
However, it returns right away
if the process has no children or
if a child has already terminated or stopped but has not yet been waited for
Return value: If wait returns because a child terminated, the return value is positive and is the PID of that child; otherwise, wait returns -1 and sets errno.
What happen if the parent terminates first without waiting for its children? (the children are still running) - Orphan
The stat is a pointer to an integer variable that stores the exit status of the child.
p.12 The stat is a pointer to an integer variable that stores the exit status of the child.
fork 后父子各自推进。父进程想确认子进程已结束并取得终止状态时调用 wait。返回值是被回收的子 PID;指针参数所指向的整数接收状态。这是两个不同输出通道:不能把 wait 的返回值当作 exit 的状态码。
若孩子已经结束且尚未被回收,wait 可立即返回;没有可等待子进程时返回失败,而不是永远睡眠。课件把停止和信号也放进概述;补充:实际停止事件是否被报告取决于 wait/waitpid 选项与跟踪状态,应区分本讲结束回收的主线和接口的完整条件。读取 status 应用退出状态宏,不能假设整数原样等于 exit 参数。
补充:沿三个时刻观察“结束”与“回收”。 时刻一,孩子仍在运行:父亲的阻塞式等待尚不能取得终止结果。时刻二,孩子已经结束:它不再执行用户指令,但内核保留可供父亲读取的终止记录。时刻三,父亲成功等待:返回的 PID 指认这次取走的是哪个孩子的记录,状态参数描述该孩子怎样结束。僵尸状态对应中间这一段,不能把它画成仍在消耗 CPU 的死循环。
父亲先调用 wait,还是孩子先结束,只改变父亲是否需要等待,不改变“终止记录能被随后收取”的目的。多个孩子存在时,一次成功 wait 只回收一个,不能把一次调用画成同时回收整棵进程树。检查程序时先数成功创建了几个直接孩子,再数成功回收了几个;fork 失败时并没有新增孩子,不能把失败也加入待回收计数。
五节点链为什么倒着结束(p.13)
课件程序与接口(p.13,原页片段)
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/wait.h>
4 #include <unistd.h>
5 #include <errno.h>
6
7 int main(void)
8 {
9 int i;
10 int n=5;
11 pid_t childpid;
12 int status;
13 pid_t waitreturn;
14
15 for (i = 1; i < n; ++i)
16 if ( (childpid = fork()) >0 )
17 break;
18
19 if((waitreturn = wait(&status))<0)
20 printf("wait(): Return <0 - %d.\n", waitreturn);
21
22
23 printf("My PID:%d, Parent PID: %d\n", getpid(), getppid());
24
25 return 0;
26 }
题面(L02-系统调用-进程-2026T1.pdf · Example for wait() · p.13;以下为参与进程关系的完整代码段)
p.13 int n=5;
for (i = 1; i < n; ++i) if ( (childpid = fork()) >0 ) break;
if((waitreturn = wait(&status))<0) printf("wait(): Return <0 - %d.\n", waitreturn);
printf("My PID:%d, Parent PID: %d\n", getpid(), getppid());
每轮父进程得到正 PID 就 break,因此只有新子进程进入下一轮。i=1、2、3、4 各创建一个孩子,总数5。链末节点从循环退出时没有孩子,wait 失败后打印并结束;上一节点等待它结束,回收后打印,再结束。这个关系向链头传播,所以成功路径上的 PID 行沿链从末端返回到根。它不是“操作系统总先调度孩子”,而是每一级 wait 建立了结束依赖。fork 失败时不再适用上述成功路径结论。
把 p.13 的循环变量也列出来。 令 P0 是原进程,P1–P4 是沿链创建的孩子;这些是解释用标签,不是真实 PID。下表假定四次 fork 全成功。
| 本轮 i | 谁调用 fork | 父分支做什么 | 子分支接着做什么 |
|---|---|---|---|
| 1 | P0 | 返回正值,break,去 wait(P1 的终止记录) | P1 得到 0,走循环增量,i 变为 2 |
| 2 | P1 | 返回正值,break,去等 P2 | P2 的 i 变为 3 |
| 3 | P2 | 返回正值,break,去等 P3 | P3 的 i 变为 4 |
| 4 | P3 | 返回正值,break,去等 P4 | P4 的 i 变为 5,i<n 不成立 |
课件代码实际调用的是 wait(&status),表中的“等 P1”等文字是在说明该进程唯一的直接孩子,并不是源码传了一个 PID 参数。P4 没有孩子,wait 返回 −1;其余每个节点各有一个孩子。P4 打印并退出后,P3 的 wait 才能成功,随后 P2、P1、P0 依次完成。这也解释了为什么 for 循环的 break 只终止当前进程自己的循环,不会命令另一进程停止。
若 fork 返回 −1,条件 >0 为假,原程序会继续下一轮;它没有完整处理失败。因此“五个进程、倒序打印”是明确以创建全部成功为前提的推演,不能用于断言资源不足时也有相同输出。
exec替换当前进程的程序(p.14–16)【新】
p.14 The fork system call creates a copy of the calling process. However, many applications require the child process to execute code different from the parent’s.
The exec() family of system calls provides a facility for overlaying the calling process with a new executable module
exec loads a new executable into the process image, copies arguments into the process, and calls main(argc, argv).
If successful, exec never returns; the calling process is completely overlaid by the new program and is started from its beginning
The traditional way to use the fork-exec combination is to have the child execute the new program while the parent continues to execute the original code
课件程序与接口(p.15,原页片段)
1 #include <sys/types.h>
2 #include <sys/wait.h>
3 #include <unistd.h>
4 #include <stdio.h>
5 #include <stdlib.h>
6
7 int main(void)
8 {
9 pid_t childpid;
10 int status;
11
12 char *cmd = "ls";
13 char *argv[2];
14 argv[0] = "ls";
15 argv[1] = NULL;
16
17 if ((childpid = fork()) == -1){
18 printf("Error: fork()\n");
19 return -1;
20 } else if (childpid == 0) {
21 /*child code*/
22 if (execvp(cmd, argv) < 0) {
23 printf("Error: execvp()\n");
24 return -1;
25 }
26 }else if (childpid != wait(&status)){
27 /* parent code */
28 printf("Error: wait()");
29 return -1;
30 }
31
32 return 0;
33 }
p.16 Example #1: run the command ls (Use whereis to find where a command locates)
课件程序与接口(p.16,原页片段)
char *cmd = "ls";
char *argv[2];
argv[0] = "ls";
argv[1] = NULL;
execvp(cmd, argv);
课件程序与接口(p.16,原页片段)
int execvp(const char *file, char *const argv[]);
Description:
The exec() family of functions replaces the current process image with a new process image.
The execvp() function provides an array of pointers to null-terminated strings that represent the argument list available to the new program.
The first argument, by convention, should point to the filename associated with the file being executed (The search path is the path specified in the environment by PATH variable).
The array of pointers must be terminated by a NULL pointer.
p.14 的exec改变当前进程运行的程序映像。fork 负责多出一个进程;exec 负责让某个现有进程运行另一份程序。成功后旧程序的下一行不再执行;返回到旧代码通常意味着失败。因此 shell 可以先 fork,让孩子 exec 命令,而父 shell 继续存在。
p.15 的父进程等待,子进程调用 execvp 运行 ls。先检查 fork 是否 −1,子分支执行新命令;若 execvp 返回负数,才进入错误处理。p.16 的 argv 是指向字符串的指针数组,argv[0]按惯例写程序名,最后一项必须是空指针。数组终止空指针与每个字符串内部的终止字节是两层不同的结束标志。
算一遍:按 p.16 的 ls 例子,argv 有两个槽:第0槽指向字符串 ls,第1槽为NULL,所以传给新程序的参数个数为1,而不是2。若要传 -l(补充),需要第0槽 ls、第1槽 -l、第2槽NULL,共3槽、2个参数。
补充:沿成功和失败两条路径走完。 以父进程启动 ls 为例,父进程先保存 fork 返回的子 PID;子进程准备 argv 后 exec。成功时,子 PID 没变,但旧栈、旧代码的执行上下文已被新映像取代,所以旧代码里的“exec 下一行”没有机会打印成功消息。父进程的 wait 等的是这个 PID 最终终止,不是等待 exec 这一次接口调用结束。
若命令不存在,exec 返回错误:此时孩子仍运行旧程序。应报告错误并终止孩子,不能让它掉回交互 shell 的读命令循环,否则一次错误会额外产生一个 shell。教材常用 _exit 结束这种失败分支,以免再次刷新从父进程复制来的 stdio 缓冲;这不是说任何场景都应把 exit 换成 _exit。
父侧的 waitpid(child, &status, 0) 只回收指定孩子;返回该 PID 后,先用 WIFEXITED(status) 判断是否正常退出,成立才读 WEXITSTATUS(status)。若被信号终止则看 WIFSIGNALED 与 WTERMSIG。等待被信号打断时还须检查错误是否为 EINTR,不能把每次返回 −1 都说成“孩子没有了”。不检查终止类别就直接打印 status,会把编码后的状态误当程序返回码。
自测:孩子 exec 失败后为什么不能简单 return 到 shell 主循环?先说出两个进程各自还在执行什么。
第一层:fork 已创建孩子,exec 失败不会撤销 fork。父 shell 仍存在,孩子仍保留旧 shell 程序映像。
第二层:若孩子回到主循环,它会和父进程一起读终端,出现两个命令解释器。正确错误路径应让这个孩子报告失败并退出,再由父侧回收。先关闭孩子独占的临时描述符,再结束;不要让清理错误反过来掩盖最初的 exec 错误。
exec的字母后缀(p.17)
p.17 Six variations of the exec system call, which can be distinguished by
the way command-line arguments and environment are passed, and
whether a pathname has to be given for the executable.
The execl calls (execl, execlp, and execle)
pass the command-line arguments as a list of pointers
The execv calls (execv, execvp, execve)
pass the command-line arguments in an argument array
In all calls, in addition to the pathname, name of the executable must also be provided, either as a separate parameter or as the first element of the argument array.
本页区分参数列表形式 l 与数组形式 v,也区分是否给出执行文件路径及怎样传递环境。补充:p 后缀表示按 PATH 搜索;e 变体显式传环境数组。先决定“参数已放在数组里还是逐项列出”,再决定“是否需要搜索 PATH”,就能理解 execvp 的 v 与 p,不必把六个名字当毫无关系的函数死记。
僵尸和孤儿为什么不是一回事(p.18)【新】
p.18 Upon termination of a process, the OS de-allocates the resources held by the process, updates the appropriate statistics, and notifies other processes:
Canceling pending timers and signals,
Releasing virtual memory spaces,
Releasing locks, closing open files
Notifying the parent in response to a wait system call
What happened if the parent of the terminating process is not currently executing a wait()? – Zombie
A zombie process is not an orphan process – an orphan process is a process that is still executing, but whose parent has died.
All orphan processes are adopted by init (process ID 1)
A process can terminate either normally or abnormally.
进程结束后,内核回收大部分资源并保留终止信息,供父进程查询。僵尸关注“孩子已结束、状态尚未被父进程回收”;孤儿关注“孩子仍在运行、原父进程已经结束”。一个是回收阶段问题,一个是亲缘关系变化。
课件用 init、PID1 概括孤儿收养。补充:现代 Linux 还可能由子收割者接管,故把“所有孤儿必定直接给全系统 PID1”当成普适实现规则过强;本讲先按课件简化模型理解。父先结束不会自动把仍运行的孩子变成僵尸。
正常结束、异常结束与退出清理(p.19–20)
p.19 A normal termination occurs if there was
a return from main,
an implicit return from main,
a call to the _exit system call.
a call to the C function exit, or
exit calls user-defined exit handlers and may provide additional cleanup before it invokes the _exit system call.
exit takes a single, integer argument, called exit status, which will be made available to the parent process (which may be waiting). By convention, a zero means success, some non-zero value means something has gone wrong.
p.20 A process can terminate abnormally by
calling abort, causing the SIGABRT signal to be sent to the calling process, or
processing a signal that causes termination.
A code dump may be produced.
User-installed exit handlers will not be called upon abnormal termination.
p.19 把 main 返回、exit 与底层 _exit 联系起来。exit 可以执行用户注册的退出处理与额外清理,再结束进程;_exit 走更直接的系统级终止路径。成功通常用0,失败用非零。补充:数字只是约定,父进程仍需要根据正常退出还是信号终止来解释状态。
p.20 说明 abort 或致命信号可导致异常终止,正常注册的退出处理并不因此必然执行。课件写“A code dump may be produced”,通常术语为 core dump;这里按其意图理解为可能保存用于调试的进程转储,不把“可能”解释为每次都会产生文件。
前台、后台与这一讲的接口组合(p.21–22)
p.21 The shell is a command interpreter which
prompts for commands,
reads the commands from standard input, forks children to execute the commands, and waits for the children to finish.
A user can terminate execution of a command by ctrl-c.
Most shells interpret a command line ending with & as one that should be executed by a background process
When a shell creates a background process, it does not wait for the process to complete before issuing a prompt and accepting additional commands
ctrl-c does not terminate a background process.
p.22 System calls related to process
Creation: fork()
Wait: wait()
Change: exec()
Terminate: _exit()
Next: User-level programming using system calls (process, memory, file)
Chapter 14 (Memory) , Chapter 39 (Files)
p.21 中 shell 读命令、fork、让子进程执行命令;前台情况等待结束,后台 & 则允许 shell 很快再次提示输入。后台仍是进程,不是“没有父亲”或“更高权限”。课件说 Ctrl-C 不终止后台进程;补充:常见终端行为是向前台进程组发信号,这解释了为何后台组通常不受这次按键影响,而不是后台进程不能收到信号。
p.22 的四个动词可作为读代码的顺序:fork 创建、wait 回收、exec 换程序、_exit 结束。看见一段 shell 风格程序时,先画父子关系,再标哪些分支换程序,最后画等待依赖,输出顺序才有根据。
本讲核心考点
- fork一次调用在父子两边返回不同值(p.8–10,C 级通识考法)。
- 三次无条件fork成功后总数8,新建7(p.11,C 级通识考法)。
- wait返回PID,状态通过指针参数取得(p.12–13,C 级通识考法)。
- exec成功替换映像而不回旧程序(p.14–17,C 级通识考法)。
- 僵尸已结束待回收,孤儿原父已结束(p.18,C 级通识考法)。
附录
必要演算已移至对应页正文,保留原锚点供旧链接使用。
来源与证据
-
原件:L02-系统调用-进程-2026T1.pptx(本地课件);PDF副本(本地课件) p.1–22。本文页码均为 PDF 页号。
-
教材:讲义 p.22 指向后续 Chapter 14(Memory)、Chapter 39(Files)。
-
考试证据:读了 课程大纲-2026T1.pdf、教学计划-2026T1.pdf、T08-期中参考-2025T2-题目-解答.pdf、T07-多级反馈队列-2025T2.pdf(p.13–15内嵌历史试题);未识别用途的 PDF:无
-
2026-10-13 内容补全:保留原页顺序与版本,补入对应页的程序、接口、字段、条件和演算;原程序片段保留课件行号、拼写与省略处,不能据此视为经过编译的完整软件。本文已有运行记录属于其标注日期,本次未重新运行课程容器或提交作业。