Vince Jiang

CSCI3150 / 课程伴读 · 本站更新

CSCI3150 L03 User-level Programming via System Calls (Memory)

课程:CSCI3150 Introduction to Operating Systems · 2022未核 本讲:L03 User-level Programming via System Calls (Memory) · 原件 L03-系统调用-内存-2022未核.pptx(本地课件) · PDF页码副本(本地课件) p.1–17 定位:分清指针与所指对象,逐种解释内存错误,并能计算字符串和数组实际需要的分配空间。 承接:#L01-03 系统调用与库接口

p.1 标题页。

分配对象与取得地址

系统接口与库接口的分工(p.2–4)【延伸 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.4 Several system calls

User programming should not directly call them but use memory API (library functions).

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

#include <unistd.h>

int brk(void *addr)
void *sbrk(intptr_t increment);

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

#include <sys/mman.h>

void *mmap(void *ptr, size_t length, int port, int flags, int fd, off_t offset)

p.2 再次定位到用户级系统调用层,p.3 的接口图强调内存也是 OS 提供的服务。p.4 列 brk、sbrk 和 mmap,并紧接着要求普通用户程序使用内存库接口。

p.4 User programming should not directly call them but use memory API (library functions).

补充:这是分配器与操作系统的分工:库可以从较大的区域中切出许多小对象,因此一次 malloc 不必对应一次系统调用。brk/sbrk 涉及进程数据区边界,mmap 建立映射;理解本讲时先掌握对象的大小、地址和生命周期,不把所有内存 API 都叫成系统调用。

读懂 p.4 的参数:brk(addr) 中的 addr 是希望设置的数据区边界地址,sbrk(increment) 的参数是边界增量;mmap 列出起始地址提示、映射字节数、保护方式、标志、文件描述符和文件偏移。课件把保护参数拼成 port,常见接口名为 prot;参数名本身不改变 C 函数类型。本讲列出这些接口,是要说明堆分配器背后需要 OS 管理地址空间;它没有要求每次 malloc 都直接进行一次 brk,也没有提供可不加检查就运行的完整分配器实现。

malloc的字节数与sizeof的对象(p.5–6)【新】

p.5 Allocate a memory region on the heap.

Argument

size_t size : size of the memory block(in bytes)

size_t is an unsigned integer type.

Return

Success : a void type pointer to the memory block allocated by malloc

Fail : a null pointer

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

#include <stdlib.h>

void* malloc(size_t size)

p.6 Routines and macros are utilized for size in malloc instead typing in a number directly.

Two types of results of sizeof with variables

The actual size of ‘x’ is known at run-time.

The actual size of ‘x’ is known at compile-time.

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

int *x = malloc(10 * sizeof(int));
printf(“%d\n”, sizeof(x));

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

int x[10];
printf(“%d\n”, sizeof(x));

p.5 的 malloc 参数是字节数,成功返回可用于该区域的指针,失败返回空指针。声明 int *x 只产生一个指针变量;malloc 成功后才获得另一块可放整数的对象存储。它们是两个对象,不能混为一谈。

p.6 比较 int *x=malloc(10*sizeof(int)) 与 int x[10]。前者的 sizeof(x) 问的是指针本身,课件输出4;后者问整个数组,输出40。算一遍:按本页 int=4字节,数组10个元素合计10×4=40字节;指针值所指对象有40字节,指针自身仍只占本例的4字节。64位环境指针可能为8字节,这不会自动把10个4字节整数变成80字节。

已核对原图。讲义以“run-time / compile-time”描述这两例,容易让人误会 sizeof 能查回 malloc 的分配长度;补充:普通指针表达式的 sizeof 由指针类型决定,不追踪其目标分配尺寸。本例关键区别是指针类型与数组类型。打印 size_t 的通用写法使用 %zu,不要把课件 %d 当所有平台均正确的格式。

补充:大小算对之后,还要把合法地址范围算对。 沿本页十个 int 的例子,在 int 为 4 字节的条件下,分配区可用字节偏移是 0–39。x+9 前进九个 int,即 36 字节,解引用得到第十个元素;x+10 指向数组之后的位置,可用于某些边界比较,但不能读取第十一个元素。sizeof(x) 问指针类型,sizeof(*x) 问一个元素类型,10*sizeof(*x) 才表达本次想分配的对象总量。表达式里的星号在这里用于给 sizeof 确定类型,并不是要求先读出一个未初始化的 int。

还应区别“分配失败”和“分配成功后程序越界”:前者没有得到可用区域,应在任何解引用之前走失败分支;后者已经得到区域,却把权限扩张到了申请范围之外。看到崩溃只说 malloc 出错会漏掉这两个完全不同的因果链。

free结束哪一个对象的生命周期(p.7)【新】

p.7 Free a memory region allocated by a call to malloc.

Argument

void *ptr : a pointer to a memory block allocated with malloc

Return

none

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

#include <stdlib.h>

void free(void* ptr)

free 接收分配所得区域的起始指针,没有返回值。释放结束的是堆对象的使用权,不是把指针变量本身从栈上抹掉。补充:释放后旧指针变量仍可能保留相同数字,但不再授权解引用;把一个别名设为NULL也不能自动清除其他别名。成功分配、合法访问、恰好一次释放,应围绕同一对象配对追踪。

六类常见内存错误

未分配目标与补足终止字节(p.8–10)

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

char *src = “hello”; //character string constant
char *dst;        //unallocated
strcpy(dst, src);    //segfault and die

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

strcpy(dst, src);

p.9 Common Error: Forgetting To Allocate Memory(Cont.)

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

char *src = “hello”;    //character string constant
char *dst (char *)malloc(strlen(src) + 1 ); // allocated
strcpy(dst, src);   //work properly

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

char *src = “hello”; //character string constant
char *dst (char *)malloc(strlen(src)); // too small
strcpy(dst, src);    //work properly

p.8 的 dst 没有指向已分配的可写存储,就交给 strcpy。strcpy 不替调用者申请空间,它只沿着地址复制,因而“有一个指针变量”不意味着“有一块可写缓冲区”。图中 src 指向 hello,dst 指向未分配区域。

p.9 在堆上申请目标,再复制,使源和目标成为两个不同存储对象。原图中的声明缺少赋值等号;补充:可编译的表达式需要写成 char *dst = malloc(strlen(src)+1);,并检查返回值。本说明只纠正用于理解的语法,不执行原课件代码。

p.10 少申请了末尾终止字节。算一遍:hello 的可见字符5个,strlen返回5;strcpy复制 h、e、l、l、o、终止字节,共6字节。若只申请5字节,合法下标0–4,终止字节却写到下标5,越界1字节。课件称“Incorrect code, but work properly”,意思是一次运行可能看似成功;通行C语义仍是未定义行为,不能把不崩溃当成正确。

图(p.8–10):字符串复制的三个版本 元素:src指向字符串常量;dst先无合法目标,随后指向6字节区域,再对比错误的5字节区域;p.10 标出被遗漏的终止字节。 看什么:目标是否存在和目标是否足够大是两道独立检查。

已分配不等于已初始化(p.11)

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

int *x = (int *)malloc(sizeof(int)); // allocated
printf(“*x = %d\n”, *x); // uninitialized memory access

p.11 Encounter an uninitialized read

本页 malloc 一个 int 后立即打印 *x,没有先给这个整数赋值。malloc 成功只保证取得对象存储,不保证旧内容已经清零。图中特意把以前用过的值留在新分配块里。补充:不应给这个错误程序预测一个固定数;正确的教学动作是先写 *x=0 再读,或在适合的场景使用清零分配。程序第一次运行碰巧读到0,也不能推成接口保证。

泄漏:不用了但未归还(p.12)【新】

p.12 A program runs out of memory and eventually dies.

图中分配块逐渐被标为 unused,新的分配继续累积。内存泄漏的问题是逻辑上不再需要的对象仍占用分配器管理的空间;如果还丢失了所有可用指针,就无法正常定位该对象去释放。它和越界不同:程序可能每一次读写都在边界内,却因长期不归还而耗尽资源。

算一遍(补充;数字是我为演示取的,讲义没给):每轮申请1024字节且不释放,1000轮后遗留1024000字节,约1000KiB。是否立刻失败取决于资源限制,不能把这个演示数当作本页给定的崩溃阈值。

悬空指针与重复释放(p.13–14)【新】

p.13 Freeing memory before it is finished using

A program accesses to memory with an invalid pointer

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

int *x = (int *)malloc(sizeof(int)); // allocated
free(x); // free memory
free(x); // free repeatedly

p.13 链式图中,a 指向2KB位置的节点,它的后继为3KB,后继再指向4KB。b也指向3KB。free(b)结束中间节点的生命周期,a所指节点中的后继仍残留3KB,于是沿链访问会经过已释放对象;原来通往4KB节点的链接也不能再作为合法访问路径。

图(p.13):释放链中节点后的关系 元素:2KB、3KB、4KB三个节点;b指向3KB;释放后3KB变为freed,2KB的后继指针成为悬空引用,4KB被标unreachable。 看什么:释放对象不会自动修复其他指针和链结构。

p.14 先malloc,随后连续free(x)两次。第一次后x保存的地址已无有效分配可释放,第二次违反生命周期约定。补充:free(NULL)合法并不能挽救这里的错误,因为第一次free不会自动把x改成NULL。若还有其他别名,仅把x清零也不能使那些别名重新有效。

考试角度:(C 级 通识)常见考法是分类一段代码属于泄漏、悬空访问、越界还是重复释放,并指出第一个破坏对象生命周期的动作。

补充:用所有权表找第一个非法动作。 所有权不是 C 编译器替你强制的语法,而是“谁负责最终归还这一块存储”的设计约定。下面只演示生命周期,不是课件原程序。

操作 堆对象是否活着 p、q 的关系 可执行什么
p=malloc(8),成功 是 p 指向新对象 在 8 字节内初始化和访问
q=p 是 两个别名指向同一块 没有多出第二块内存
free(p) 否 两指针数字可能仍保留 不能再用任一别名访问旧对象
p=NULL 否 只清除了 p q 没有自动变 NULL

如果最后执行 free(q),它不是“清理另一个指针”,而是对同一分配再次释放。若只执行 q=NULL,则没有重复释放,但也并非 free 的替代:只有在对象已经释放或还有可靠拥有者时,这样丢掉引用才不会造成泄漏。

调试时把“在哪里崩溃”与“第一次破坏约定在哪里”分开。越界写可能先损坏相邻对象,直到后面的 free 才暴露。支持时可在自己的练习程序使用 AddressSanitizer 查越界/释放后使用;工具报出的失败路径是证据,没有报告不等于证明全部输入安全。未初始化读取还需要适合该错误类型的检查,不能假设一个工具覆盖所有六类错误。

把 p.13 的链图修复动作按先后写清(补充)

原图的关系是 a → 2KB 节点 → 3KB 节点 → 4KB 节点,同时 b → 3KB 节点。要删除中间节点,首先需要保住通往后继的合法引用,然后让前驱绕过它,最后才能释放它。下面用 next 表示图中的后继箭头,作为图解伪代码;不是讲义提供的完整 C 结构体程序。

victim = a.next        // 指向仍活着的 3KB 节点
successor = victim.next  // 在释放前读出通往 4KB 的箭头
a.next = successor    // 2KB 现在直接连到 4KB
free(victim)          // 此后不再从 victim 读 next
b = NULL             // 清除这里已知的另一份别名

若先 free 再读 b.next,错误已经发生在读取后继那一步;内存暂时还保留原字节也不使访问合法。若只把 b 设为 NULL 而没有 free,对象仍占空间;若只 free 而不改 a.next,链上仍留着悬空边。三个动作分别维护链的连通性、对象生命周期、已知别名,不能互相替代。还存在其他别名时,也必须按其使用约定处理;这段演示没有自动发现所有指针的能力。

另外两种分配请求

calloc、realloc与接口选择(p.15–17)

p.15 Allocate memory on the heap and set with zero before returning.

Argument

size_t num : number of blocks to allocate

size_t size : size of each block(in bytes)

Return

Success : a void type pointer to the memory block allocated by calloc

Fail : a null pointer

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

#include <stdlib.h>

void *calloc(size_t num, size_t size)

p.16 Change the size of memory block.

A pointer returned by realloc may be either the same as ptr or a new.

Argument

void *ptr: Pointer to memory block allocated with malloc, calloc or realloc

size_t size: New size for the memory block(in bytes)

Return

Success: Void type pointer to the memory block

Fail : Null pointer

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

#include <stdlib.h>

void *realloc(void *ptr, size_t size)

p.17 Functions related to memory allocation

Allocation: malloc()

Free: free()

Allocation & Initialization: calloc()

Re-allocation: realloc()

Next: User-level programming using system calls (file & Directory)

Chapter 39 (Files)

p.15 的calloc把元素数和元素大小分成两个参数,并将获得的存储清零。算一遍(补充;数字是我为演示取的,讲义没给):calloc(3,4)请求12字节,成功后这12个字节均为0;malloc(12)请求同样字节数,却没有同样的清零保证。

p.16 的realloc改变已分配对象大小,返回地址可能不变,也可能迁移。补充:通常保留旧对象与新对象长度较小者范围内的内容;失败时应保留旧指针,以免把唯一引用覆盖为空。先用临时指针接收返回值,成功才更新拥有者;不能在失败后仍假设扩容成功,也不能在成功移动后继续使用旧别名。零长度参数有特殊语义,不由这页的简单失败模型外推。

p.17 将四种职责收束为分配malloc、释放free、分配并清零calloc、调整尺寸realloc。选接口时先问需要多少字节、是否需要初始化、谁拥有并负责释放;这些问题比“背出函数签名”更能防止错误。

把两组参数展开来看:calloc(num, size) 中 num 是元素个数,size 是每个元素的字节数,返回的是一个连续分配区的起始地址;realloc(ptr, size) 的 ptr 必须对应适用的既有分配,size 表示新的总字节数,不是额外增加多少。两者都返回 void *,失败返回空指针。free 无返回值,不能写成“检查 free 返回 0 再判断成功”。这四个函数的声明均由 <stdlib.h> 提供。

扩容时为什么先保留旧指针(补充,教材通识)

设 p 指向 8 字节,现要扩成 16 字节(自取演示数字)。先写 tmp = realloc(p, 16):若失败返回 NULL,旧 8 字节仍由 p 拥有,可以继续使用或释放;若成功则通过 tmp 使用新对象,再令 p=tmp。反过来直接 p = realloc(p,16),一旦失败,唯一的旧地址就被 NULL 覆盖,旧对象仍占空间而程序找不到它。

成功的另一分支同样要检查:若对象移动,旧 q 别名不能继续用;扩出来的后 8 字节也不能自动当成已经初始化。即使返回的地址数值与旧地址相同,也应按 realloc 的新生命周期重新建立可用引用。容量计算本身还要避免乘法溢出:n 个元素各 s 字节,先确认 n 不大于可表示最大大小除以 s,再求 n×s;“malloc 成功”不能补救先前算错的请求尺寸。

自测:malloc(6) 成功后可否立刻把这 6 字节当 C 字符串打印?

不可以。容量足够只说明可写空间存在;还需要在合法范围写入有效字符并放置终止零字节。否则字符串函数会继续查找终止符,可能越过这 6 字节。分配、初始化、使用和释放是四件事。

本讲核心考点

  • sizeof指针与sizeof数组问的是不同对象(p.6,C 级通识考法)。
  • 字符串分配须计入终止字节,hello需要6字节(p.8–10,C 级通识考法)。
  • 分配、初始化、合法使用、释放是不同动作(p.5–14,C 级通识考法)。
  • calloc清零,realloc成功后地址可能变化(p.15–16,C 级通识考法)。

附录

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

来源与证据

  • 原件:L03-系统调用-内存-2022未核.pptx(本地课件);PDF副本(本地课件) p.1–17。本文页码均为 PDF 页号。

  • 教材:讲末指向下一主题 OSTEP Chapter 39;本讲原件未给当前章节号。

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

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