读 xv6 必备的 C 语言和指针基础 / C And Pointers For xv6
1. 为什么读 xv6 会被 C 指针卡住
xv6 的代码量不算大。
但它对 C 语言的使用非常内核化。
这里说的“内核化”不是语法更高级。
恰恰相反,xv6 使用的 C 很朴素。
难点在于:每个指针背后都对应一个真实的内核对象、生命周期和同步规则。
你看到的不是普通练习题里的:
int *p;而是:
struct proc *p;
struct file *f;
struct inode *ip;
pagetable_t pagetable;
uint64 dstva;
char *src;这些变量每一个都连接着操作系统概念。
struct proc *p 可能指向进程表里的一个进程槽位。
struct file *f 可能指向全局打开文件表中的对象。
struct inode *ip 可能指向 inode cache 中的 inode。
pagetable_t 代表页表。
uint64 dstva 可能只是一个用户虚拟地址数值,不是内核可以直接访问的指针。
如果把这些东西都粗暴理解成“地址”,很快就会迷路。
这篇文章只解决一个目标:
让你读 xv6 时,能看懂 C 代码表达的对象关系。
总路线是:
C syntax
|
v
object and memory
|
v
pointer relation
|
v
xv6 structure fields
|
v
kernel ownership and lifetime
|
v
safe reading of xv6 source这里不会完整讲 C 语言。
我们只讲读 xv6 最需要的部分。
1.1 本文基础词小注释
读 C 指针时,先别急着觉得自己必须“完全懂指针”。
先把下面几个词分清。
对象
|
+-- object
+-- 内存里一块有意义的数据
+-- 例如一个 struct proc 对象
地址
|
+-- address
+-- 内存位置的编号
+-- 指针变量里保存的通常就是地址
指针
|
+-- pointer
+-- 保存地址的变量
+-- struct proc *p 表示 p 指向一个 struct proc
字段
|
+-- struct 里的成员
+-- p->pid 表示 p 指向的进程对象里的 pid 字段
生命周期
|
+-- 一个对象从创建、使用到释放的过程
+-- 内核指针是否安全,和生命周期强相关最基础的一张图:
pointer variable p
|
| stores address
v
real object in memory2. 先分清对象、地址和值
C 语言里最容易混的三个词是:
- 对象。
- 地址。
- 值。
对象是内存中一块有类型含义的数据。
地址是这块数据所在位置的编号。
值是变量当前保存的内容。
看一个简单例子:
int x = 10;
int *p = &x;可以画成:
memory
+---------+ +---------+
| x = 10 | <----- | p = &x |
+---------+ +---------+
address A address Bx 是一个 int 对象。
&x 是 x 的地址。
p 是一个指针变量。
p 自己也是对象。
p 的值是 x 的地址。
这句话非常重要:
指针变量本身也占内存。
它保存的是另一个对象的地址。
在 xv6 里,这种关系到处都是。
例如:
struct proc *p;p 是一个指针变量。
它的值应该是某个 struct proc 对象的地址。
这个 struct proc 对象通常位于全局数组 proc[NPROC] 的某个槽位中。
p
|
v
+-------------------+
| struct proc slot |
| pid |
| state |
| pagetable |
| trapframe |
+-------------------+所以读 xv6 时,不要只问“这个指针是什么”。
要问:
这个指针变量在哪里?
它的值是什么地址?
这个地址指向什么类型的对象?
这个对象是谁创建的?
这个对象什么时候销毁?3. . 和 -> 的区别
读 xv6 时会频繁看到:
p->state
p->pid
p->trapframe
cpus[cpuid()].proc先讲 .。
如果你有一个结构体对象:
struct point {
int x;
int y;
};
struct point a;
a.x = 1;a.x 表示访问对象 a 的字段 x。
再看 ->。
如果你有一个指向结构体的指针:
struct point *p = &a;
p->x = 2;p->x 等价于:
(*p).x先用 *p 找到 p 指向的结构体对象。
再用 .x 访问字段。
图如下:
p
|
v
+----------------+
| struct point a |
| x |
| y |
+----------------+所以:
a.x
|
+-- a is object
p->x
|
+-- p is pointer
+-- p points to objectxv6 里常见写法:
struct proc *p = myproc();
p->killed = 1;这表示:
myproc() 返回当前进程的 struct proc 指针。
然后通过这个指针修改当前进程的 killed 字段。
如果你读成“p 里面有 killed”,就会有点模糊。
更准确地说:
p 指向的那个 struct proc 对象里面有 killed 字段。
4. 结构体:内核对象的形状
结构体是 xv6 最重要的 C 语言机制。
因为操作系统需要管理很多对象。
例如:
- 进程。
- CPU。
- 文件。
- inode。
- buffer。
- lock。
- pipe。
C 没有类。
xv6 用 struct 描述这些对象。
例如进程:
struct proc
|
+-- identity
+-- state
+-- memory
+-- scheduling context
+-- opened files
+-- parent relation文件:
struct file
|
+-- type
+-- reference count
+-- readable/writable
+-- inode pointer
+-- pipe pointer
+-- offsetbuffer:
struct buf
|
+-- valid flag
+-- disk flag
+-- device id
+-- block number
+-- sleeplock
+-- data block结构体定义不是创建对象。
这是 C 初学者常犯的错误。
struct proc {
int pid;
};这只是定义一种类型。
它告诉编译器:struct proc 这种对象里面有一个 pid 字段。
真正创建对象要看变量定义。
例如:
struct proc proc[NPROC];这才创建了一个数组。
数组里有 NPROC 个 struct proc 对象。
图如下:
proc
|
v
+---------+---------+---------+---------+-----+
| proc[0] | proc[1] | proc[2] | proc[3] | ... |
+---------+---------+---------+---------+-----+如果有:
struct proc *p = &proc[3];那就是:
p
|
v
proc[3]读 xv6 时,要不断区分:
type definition
|
+-- describes shape
object definition
|
+-- allocates storage
pointer variable
|
+-- stores address of object5. 嵌入结构体和指针字段
看一个简化版 struct proc。
struct proc {
struct spinlock lock;
enum procstate state;
pagetable_t pagetable;
struct trapframe *trapframe;
struct context context;
struct file *ofile[NOFILE];
};这里有几种不同字段。
第一种:嵌入结构体。
struct spinlock lock;
struct context context;表示 struct proc 对象内部直接包含这些对象。
+--------------------------------+
| struct proc |
| |
| +----------------------------+ |
| | struct spinlock lock | |
| +----------------------------+ |
| |
| +----------------------------+ |
| | struct context context | |
| +----------------------------+ |
+--------------------------------+第二种:指针字段。
struct trapframe *trapframe;表示 struct proc 内部只保存一个地址。
真正的 struct trapframe 对象在别处。
+-----------------------------+
| struct proc |
| trapframe ------------------+----+
+-----------------------------+ |
v
+------------------+
| struct trapframe |
+------------------+第三种:数组里的指针。
struct file *ofile[NOFILE];这不是“一个指向数组的指针”。
它是一个数组。
数组里的每个元素都是 struct file *。
proc.ofile
|
v
+-------+-------+-------+-------+-----+
| fd 0 | fd 1 | fd 2 | fd 3 | ... |
+---+---+---+---+---+---+---+---+-----+
| | |
v v v
file file file第四种:类型别名。
pagetable_t pagetable;pagetable_t 在 xv6 中是一个类型别名。
它通常代表页表地址。
虽然写起来不像 *,但本质上和地址有关。
所以不要只看字段表面。
要追踪类型定义。
field syntax
|
v
field type
|
v
actual meaning in xv66. 数组:xv6 为什么喜欢固定大小表
xv6 经常使用固定大小数组。
例如:
proc[NPROC]
ftable.file[NFILE]
bcache.buf[NBUF]
itable.inode[NINODE]这是一种很教学化的设计。
真实系统会更复杂。
但固定数组让你更容易看清对象生命周期。
以进程表为例:
proc[NPROC]
+----------+----------+----------+----------+----------+
| UNUSED | RUNNABLE | SLEEPING | RUNNING | ZOMBIE |
+----------+----------+----------+----------+----------+
proc[0] proc[1] proc[2] proc[3] proc[4]UNUSED 表示这个槽位空闲。
allocproc() 会遍历数组,找一个 UNUSED 槽位。
找到后把它变成可使用状态。
所以新进程不是从天上掉下来。
它来自进程表中的一个槽位。
文件表也类似。
global file table
+----------+----------+----------+----------+
| ref = 0 | ref = 2 | ref = 1 | ref = 0 |
+----------+----------+----------+----------+
file[0] file[1] file[2] file[3]引用计数 ref 为 0,表示槽位空闲。
filealloc() 会找一个空闲文件对象。
进程的 fd 表再指向它。
process A ofile[3] ----+
v
global file[1] ref = 2
^
process B ofile[4] ----+这里能看出指针的重要性。
两个进程可以通过不同 fd 指向同一个 struct file。
这就是为什么 filedup() 和 fileclose() 要维护引用计数。
7. 头文件:声明、类型和跨文件协作
xv6 的头文件很多。
初学者容易觉得头文件只是“放重复代码”。
这不对。
头文件的核心作用是提供声明。
C 编译器按文件编译。
当 proc.c 调用 kalloc() 时,编译器需要先知道 kalloc() 的函数原型。
这个声明通常来自 defs.h。
proc.c
|
| includes defs.h
v
knows kalloc declaration
|
v
can call function implemented in kalloc.c头文件大致分几类。
第一类:基础类型。
types.h
|
+-- uint
+-- ushort
+-- uchar
+-- uint64第二类:参数常量。
param.h
|
+-- NPROC
+-- NOFILE
+-- NFILE
+-- NBUF第三类:结构体定义。
proc.h
file.h
fs.h
buf.h
spinlock.h
sleeplock.h第四类:函数声明。
defs.h第五类:硬件和地址常量。
riscv.h
memlayout.h
virtio.h读源码时,如果看到一个类型不懂,不要猜。
用搜索找到它定义在哪里。
推荐路径:
unknown type or function
|
v
rg "name"
|
v
find header definition
|
v
find implementation
|
v
return to original code8. typedef:名字变短,但含义不能丢
typedef 可以给类型起别名。
这让代码更短。
但也可能隐藏复杂性。
xv6 中常见:
typedef uint64 pte_t;
typedef uint64 *pagetable_t;这意味着:
pte_t 是页表项类型。
pagetable_t 是指向 uint64 的指针。
也就是说:
pagetable_t pagetable;表面上没有 *。
但它实际是指针类型。
可以画成:
pagetable
|
v
+-------+-------+-------+-------+
| PTE 0 | PTE 1 | PTE 2 | ... |
+-------+-------+-------+-------+这就是为什么读内核 C 代码不能只看变量名。
一定要追类型定义。
遇到 typedef 时问:
alias name
|
v
original type
|
v
is it a value, pointer, array, or function pointer?pagetable_t 就是典型例子。
它看起来像普通值。
实际代表页表根地址。
这会影响你理解 walk()、mappages()、uvmcopy()。
9. 指针和所有权:谁负责释放对象
普通 C 教程经常讲:
malloc()
free()xv6 内核里没有完整 libc。
它有自己的对象管理方式。
物理页由 kalloc.c 管。
进程槽位由 proc.c 管。
打开文件对象由 file.c 管。
inode 由 fs.c 和 inode cache 管。
buffer 由 bio.c 管。
所以看到一个指针时,不只问它指向哪里。
还要问谁拥有它。
pointer
|
+-- points to object
|
+-- object has owner or manager
|
+-- manager decides allocation and release以 trapframe 为例。
进程结构体里有:
struct trapframe *trapframe;这个对象通常由进程分配阶段准备。
进程释放时也要释放。
所以 trapframe 的生命周期和进程强相关。
以 struct file *ofile[NOFILE] 为例。
进程持有文件指针。
但 struct file 对象属于全局文件表。
进程退出时要关闭这些文件。
关闭不一定立刻销毁底层 inode。
因为别的进程可能还持有引用。
proc
|
+-- ofile[fd] ----> struct file
|
+-- ref count
+-- inode pointer引用计数可以理解成:
number of active references
|
v
when ref reaches 0, object can be reused or released读 xv6 时,引用计数是理解 file、inode、buffer 的关键。
10. 用户地址不是普通内核指针
这是读 xv6 必须早早建立的概念。
用户程序传给内核的地址,不能随便当作内核指针用。
例如:
read(fd, buf, n);这里的 buf 是用户程序里的地址。
对用户程序来说,它是一个 char *。
但系统调用进入内核后,这个地址只是一个用户虚拟地址。
内核要把数据写到这个用户地址,需要通过进程页表检查映射。
所以 xv6 使用:
copyin
copyout
copyinstr关系图:
user virtual address
|
| belongs to process pagetable
v
copyin/copyout
|
| walk page table
| validate mapped pages
v
kernel buffer不要把下面两种东西混为一谈。
char *p
|
+-- may be a kernel pointer
+-- can be dereferenced only if valid in current address space
uint64 user_va
|
+-- numerical user virtual address
+-- must be translated/checked through user pagetable这也是为什么很多 xv6 函数参数用 uint64 表示用户地址。
它提醒你:这只是一个地址数值。
不是内核可以直接访问的 C 指针。
11. 函数指针和系统调用表
xv6 的系统调用分发会用到函数指针表。
函数指针听起来吓人。
但模型很简单。
普通指针指向数据。
函数指针指向代码入口。
data pointer
|
v
data object
function pointer
|
v
function code系统调用表大致可以理解为:
syscall number
|
v
array index
|
v
function pointer
|
v
sys_* implementation例如:
SYS_fork ----> sys_fork
SYS_exit ----> sys_exit
SYS_read ----> sys_read
SYS_write ----> sys_write当用户程序执行系统调用时:
a7 register contains syscall number
|
v
syscall.c reads a7
|
v
looks up syscalls[num]
|
v
calls selected function这就是函数指针的用途。
它让内核可以根据编号动态选择函数。
初学时不需要深入 C 声明语法的所有怪异角落。
只要先知道:
array of function pointers
|
v
dispatch table这就够读系统调用路径。
12. 指针数组和数组指针不要混
xv6 常见的是指针数组。
例如:
struct file *ofile[NOFILE];读法是:
ofile 是一个数组。
数组有 NOFILE 个元素。
每个元素是 struct file *。
图:
ofile
|
v
+-------------+-------------+-------------+
| file ptr 0 | file ptr 1 | file ptr 2 |
+------+------+------+------+------+------+
| | |
v v v
file obj file obj null这和下面这种东西不一样:
struct file (*p)[NOFILE];这个才是指向数组的指针。
xv6 初学阶段通常不需要纠结这种复杂声明。
但要记住 struct file *ofile[NOFILE] 的本质。
它不是一个文件。
也不是一个指针。
它是“很多文件指针槽位”。
每个进程都有自己的 ofile[]。
这就是 fd 表。
process A
|
+-- fd 0 -> console file
+-- fd 1 -> console file
+-- fd 2 -> console file
+-- fd 3 -> some inode file
process B
|
+-- fd 0 -> console file
+-- fd 1 -> pipe file不同进程的 fd 数字可以相同。
但它们位于不同进程的 ofile[] 中。
所以 fd 是进程局部的。
struct file 是内核全局对象。
13. void *、char * 和内存拷贝
C 里 char * 经常用来表示字节地址。
内核里很多数据复制、清零、移动都绕不开它。
例如:
memset
memmove
copyin
copyoutchar * 的特点是按字节移动。
如果一个函数需要处理任意类型的内存区域,经常会转成字节视角。
typed object view
|
+-- struct proc
+-- struct file
+-- struct inode
byte view
|
+-- char *
+-- copy n bytes
+-- clear n bytes例如清空一块结构体内存:
memset(p, 0, sizeof(*p));这句话要仔细读。
p 是指针。
*p 是 p 指向的对象。
sizeof(*p) 是这个对象的大小。
所以这句意思是:
把 p 指向的整个对象清零。
不是把指针变量 p 自己清零。
图:
p
|
v
+-------------------+
| object bytes |
| all set to zero |
+-------------------+这类写法在内核源码里很常见。
初学者要慢慢习惯。
14. sizeof:看对象大小,不要凭感觉
sizeof 是读 C 内核代码必须熟悉的操作符。
常见写法:
sizeof(struct proc)
sizeof(*p)
sizeof(p)
sizeof(buf)这些不是一回事。
假设:
struct proc *p;那么:
sizeof(p)
|
+-- size of pointer variable
+-- usually 8 bytes on xv6-riscv
sizeof(*p)
|
+-- size of struct proc object
+-- much larger than pointer这点非常重要。
如果你要清空 p 指向的对象,用:
memset(p, 0, sizeof(*p));如果误写成:
memset(p, 0, sizeof(p));那只会清空指针大小的几个字节。
这在真实 C 程序里是常见 bug。
xv6 代码相对干净。
读它时正好可以建立正确直觉。
15. 链接关系:一个对象怎么指向另一个对象
操作系统不是一堆孤立结构体。
它是一张对象图。
以进程为中心:
struct proc
|
+-- parent ------------> struct proc
|
+-- pagetable ---------> page table pages
|
+-- trapframe ---------> struct trapframe
|
+-- ofile[fd] ---------> struct file
| |
| +-- inode ----> struct inode
| |
| +-- pipe -----> struct pipe
|
+-- cwd ---------------> struct inode这张图比单独背字段有用。
因为 xv6 的很多函数就是在维护这张图。
fork() 会复制或增加引用:
parent proc
|
+-- user memory copied
+-- trapframe copied
+-- open files filedup
+-- cwd idup
|
v
child procexit() 会释放或关闭引用:
exiting proc
|
+-- close open files
+-- release cwd
+-- reparent children
+-- become ZOMBIE
+-- wait for parent to reapexec() 会替换用户地址空间:
same proc
|
+-- old user pagetable removed
+-- new ELF loaded
+-- new user stack built
+-- pid remains
+-- open files usually remain所以学进程,绝不能只看 pid。
进程是对象关系的中心。
16. 生命周期:创建、使用、释放
每个内核对象都有生命周期。
读源码时可以用统一模板。
object lifecycle
|
+-- where allocated?
+-- how initialized?
+-- who can reference it?
+-- what lock protects it?
+-- when state changes?
+-- when released?进程生命周期:
UNUSED
|
v
USED
|
v
RUNNABLE
|
v
RUNNING
|
+--> SLEEPING
| |
| v
| RUNNABLE
|
v
ZOMBIE
|
v
UNUSED文件对象生命周期:
ref = 0
|
v
filealloc sets ref = 1
|
v
filedup increases ref
|
v
fileclose decreases ref
|
v
ref reaches 0
|
v
slot reusablebuffer 生命周期:
not cached
|
v
bread finds or allocates buf
|
v
locked buffer
|
v
read/write data
|
v
brelse releases lock
|
v
buffer remains cached如果你不知道对象生命周期,就很难理解锁。
因为锁通常保护生命周期中的关键状态。
17. 宏和常量:不要跳过大写名字
xv6 里有很多大写名字。
例如:
NPROC
NOFILE
NFILE
NBUF
PGSIZE
MAXPATH
KERNBASE
PHYSTOP
TRAMPOLINE
TRAPFRAME它们通常来自头文件。
大写名字多半是宏或常量。
不要跳过。
这些常量经常决定数组大小、地址边界和系统规模。
例如:
NPROC
|
+-- maximum number of processes
NOFILE
|
+-- maximum open files per process
NFILE
|
+-- maximum open file objects in global file table
PGSIZE
|
+-- page size
TRAMPOLINE
|
+-- special virtual address for trampoline code读源码时遇到宏,用搜索查定义。
尤其是内存管理。
地址常量决定你看到的地址关系。
如果跳过它们,页表代码会变成魔法。
18. 锁字段:结构体里的同步边界
很多 xv6 结构体都有锁。
例如:
struct proc {
struct spinlock lock;
...
};锁不是装饰。
锁表示这个对象会被并发访问。
它保护某些字段的正确关系。
例如进程状态:
proc.lock
|
+-- protects state
+-- protects chan
+-- protects killed
+-- protects xstate
+-- coordinates sleep/wakeup/scheduler理解锁时,不要只问:
“这里为什么加锁?”
要问:
What invariant is protected?
Who else may access this field?
Could another CPU change it now?
Could an interrupt handler observe it?
Could sleep/wakeup lose a transition?比如 sleep(chan, lock) 这类函数,难点不是 C 语法。
难点是并发时序。
process decides to sleep
|
v
must not miss wakeup
|
v
lock coordinates condition check and state change所以看到锁字段时,要把它当成对象关系的一部分。
19. 常见 C 阅读误区
误区一:看到 * 就只想“取值”。
在声明里,* 表示这是指针类型。
在表达式里,* 表示解引用。
struct proc *p; // declaration
(*p).pid; // dereference expression误区二:看到数组名就认为它总是指针。
数组名在很多表达式中会退化为指针。
但数组对象本身和指针变量不是同一个东西。
ofile 是数组。
ofile[0] 是数组元素。
ofile[0] 的类型是 struct file *。
误区三:认为结构体定义会分配内存。
结构体定义只描述类型。
变量定义才分配对象。
误区四:把用户虚拟地址当内核指针。
这是内核学习中的危险误区。
用户地址要通过页表检查和复制。
误区五:只看字段名,不查类型。
xv6 字段名通常简短。
只有查类型,才知道它是对象、指针、数组还是地址值。
误区六:认为 typedef 后就不是指针。
pagetable_t 表面没有星号。
但它的底层定义可能就是指针。
误区七:忽略引用计数。
引用计数决定对象能不能释放。
如果只看指针,不看 ref,文件系统对象关系会很混乱。
20. 读 xv6 指针的固定流程
以后读每个结构体字段,都按这个流程。
field
|
v
read its type
|
v
is it embedded object?
|
+-- yes: object lives inside parent
|
+-- no
|
v
is it pointer?
|
+-- yes: find pointed object type and owner
|
+-- no
|
v
is it scalar state/value?
|
+-- yes: find who writes it and what invariant it affects对函数参数也一样。
function parameter
|
v
is it input value?
|
v
is it output pointer?
|
v
is it kernel pointer?
|
v
is it user virtual address?例如:
int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len);逐个看:
pagetable 是页表。
dstva 是目标用户虚拟地址。
src 是内核源缓冲区指针。
len 是复制字节数。
这比只看函数名有效得多。
21. 用 struct proc 做一次完整练习
现在用 struct proc 练习。
目标不是背字段。
目标是建立对象图。
简化模型:
struct proc
|
+-- lock
+-- state
+-- pid
+-- parent
+-- pagetable
+-- trapframe
+-- context
+-- ofile[]
+-- cwd字段分类:
embedded object
|
+-- lock
+-- context
scalar value
|
+-- state
+-- pid
pointer to same kind of object
|
+-- parent
memory address / pointer-like
|
+-- pagetable
+-- trapframe
array of pointers
|
+-- ofile[]
pointer to filesystem object
|
+-- cwd对象图:
proc p
|
+-- parent -----------> proc parent
|
+-- pagetable --------> page table
|
+-- trapframe --------> trapframe page
|
+-- ofile[0] ---------> file object
|
+-- ofile[1] ---------> file object
|
+-- cwd --------------> inode object生命周期图:
allocproc
|
v
initialize fields
|
v
make RUNNABLE
|
v
run / sleep / wakeup
|
v
exit
|
v
wait frees resources如果你能把这三张图画出来,proc.h 就不再吓人。
后面读 proc.c 会顺很多。
22. 用 struct file 做第二次练习
文件对象是第二个适合练习的结构。
简化模型:
struct file
|
+-- type
+-- ref
+-- readable
+-- writable
+-- pipe
+-- ip
+-- off分类:
state/value
|
+-- type
+-- ref
+-- readable
+-- writable
+-- off
pointer
|
+-- pipe
+-- ip对象关系:
proc.ofile[fd]
|
v
struct file
|
+-- if type == FD_PIPE
| |
| v
| struct pipe
|
+-- if type == FD_INODE
|
v
struct inode这里要注意:
同一个结构体可以根据 type 字段表示不同种类的文件对象。
如果 type 是 FD_PIPE,就看 pipe 字段。
如果 type 是 FD_INODE,就看 ip 字段。
这是一种 C 里常见的手动多态。
没有 C++ 的虚函数。
但通过 type 字段选择不同逻辑。
type tag
|
v
choose valid fields
|
v
execute matching operation23. 用页表参数做第三次练习
现在看虚拟内存函数的参数。
比如:
int copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len);逐个解释。
pagetable 是用户进程页表。
dst 是内核目的缓冲区。
srcva 是用户虚拟地址。
len 是字节数。
图:
user address space
|
| srcva
v
+------------------+
| user memory |
+------------------+
|
| copyin walks pagetable
v
+------------------+
| kernel dst |
+------------------+对应 copyout():
int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len);图:
kernel src
|
| copyout walks pagetable
v
user dstva不要被 char *dst 和 uint64 srcva 搞混。
一个是内核指针。
一个是用户虚拟地址数值。
读函数参数时必须看命名。
va 通常表示 virtual address。
pa 通常表示 physical address。
dst 和 src 要结合函数语义判断是内核还是用户。
24. 小实验:观察指针地址和对象复用
可以做一个小实验。
在 kernel/proc.c 的进程分配路径中打印。
目标是观察:
struct proc *p的地址。pid。state。- 槽位是否被复用。
示意输出可以像这样:
allocproc: p=0x... pid=3 state=USED
fork: parent=2 child=3
exit: pid=3 state=ZOMBIE
freeproc: p=0x... pid=3 state=UNUSED观察重点不是具体地址数值。
重点是:
同一个 struct proc 槽位可能被不同生命周期的进程复用。
这能帮助你理解:
process identity
|
+-- pid changes across allocation
|
+-- struct proc slot may be reused再做一个文件实验。
在 filealloc() 和 fileclose() 打印 struct file *f 和 ref。
观察:
open increases active file object
dup increases ref
close decreases ref
ref reaches 0 then file slot reusable这些实验比背概念更能建立指针直觉。
25. 本篇总结
读 xv6 需要的 C 语言重点不是花哨语法。
而是对象关系。
你要牢牢记住:
struct defines object shape
|
v
global arrays store many kernel objects
|
v
pointers connect objects
|
v
locks protect shared object state
|
v
lifetimes decide when pointers remain valid对 xv6 来说,最重要的 C 阅读能力是:
- 区分对象和值。
- 区分对象和指针。
- 区分嵌入字段和指针字段。
- 区分数组和指针数组。
- 追踪
typedef背后的真实类型。 - 明白用户虚拟地址不是普通内核指针。
- 用所有权和生命周期理解指针。
- 用对象图理解结构体字段。
下一篇进入:
03-riscv-basics-for-xv6.md因为 C 对象关系解决的是“源码怎么表达”。
RISC-V 基础解决的是“CPU 怎么进入内核、怎么切换地址空间、怎么保存寄存器”。
两者合起来,才真正能读懂 xv6 的 trap、proc 和 scheduler。