2026-10-25 · 每周学习总结(视频转ai总结)
# 整体评价
这是一份**以自己写的简易内核代码(trap/中断、PCB任务块)为载体,复盘C语言基础**的分享稿,思路非常好:不是孤立背语法,而是拿着工程代码去理解C,非常贴合你做内核、操作系统开发的路线。
优点:
1. 知道区分全局/局部、自动/静态局部变量、函数返回值、头文件声明、结构体、指针、链表,能联系到内核里的 `trap_handler`、PCB;
2. 自己主动画图理解指针(箭头模型),抓准了指针“存地址、间接访问”的核心;
3. 意识到很多地方自己还模糊、留了后续学习空间,不是强行装懂。
下面分两块:**精简版整理稿(可以直接用来做周末分享PPT/讲稿) + 逐条纠错 + 知识点思辨补充**。
> 说明:原文里大量口误拼写:`onside the long` = `unsigned long`;`trop` = `trap`;`Stick` = `static`;`Alt` 是你自己的命名习惯标记;`chop` 大概率也是 `trap` 口误。下面统一修正术语。
---
## 一、整理后的通顺总结(可直接拿去讲)
这个周末我复盘这周学的C语言。之前发现C基础薄弱,这周抽时间补,拿我写的内核trap相关代码做例子,从 `trap.c / trap.h` 的中断处理函数切入,梳理C基础。
1. **函数结构:返回类型、函数名、形参列表**
以 `unsigned long trap_handler(unsigned long frame_sp)` 举例:
- `unsigned long`:返回值类型;括号里 `unsigned long frame_sp` 是形参;
- 类型要匹配:返回值类型和最后return出来的值类型要一致;无返回就写 `void`;
- `unsigned long` 是无符号长整型,它占用字节由平台ABI决定(RISC-V下一般是8字节),用来存地址、寄存器值,内核里大量用。
2. **变量分类:全局变量、局部变量(自动auto / static静态局部)**
- 全局变量:定义在函数外面,默认初始值为0,作用域整个文件,习惯命名加g_区分;
- 自动局部变量(默认,auto关键字省略):定义在函数内部,**没有默认初值,是随机垃圾值**,必须手动初始化;函数调用时在栈上分配,函数退出自动销毁;
- static静态局部变量:同样定义在函数内部,但不在栈上、放在全局数据区;**只在第一次进入函数初始化一次,之后多次调用函数,值会保留,不会随函数退出销毁;默认初始值0**。
> 我自己的习惯:自动局部变量加标记区分,方便读代码。
3. **函数声明、头文件、跨文件调用**
函数要在`.h`头文件里声明,`.c`里实现;别的文件include头文件之后,才能调用这个函数,编译器提前知道函数签名(返回类型、参数类型),保证调用类型匹配。
比如 `trap_init` 是void无返回函数,在内核里做中断初始化,里面会操作指针、把入口地址绑定到全局的trap入口变量。
4. **数组、结构体(内核PCB核心)**
- 数组:同类型变量连续存放,先写类型,数组名,方括号长度;本次暂时没有深度用到;
- 结构体 `struct`:把一堆不同类型的成员打包在一起,非常适合描述内核控制块,典型就是PCB任务控制块 `struct task`:里面存task id、状态、程序计数器pc、栈指针sp、上下文寄存器等。
- 我们可以定义多个结构体实例task_a、task_b,分别代表不同任务,统一管理。
5. **指针(本次难点)**
指针本身也是变量,专门存**内存地址**,指针必须带类型:`int *p` 代表p是一个指向int变量的指针。
- `&x`:取地址符,拿到变量x的地址;`p = &x`:让指针p指向x;
- `*p`:解引用,顺着p里面存的地址,去读写那个地址上的值;
> 形象理解:p里面放的是盒子x的门牌号;`p`是门牌号;`*p`是打开门牌号对应的盒子,拿里面的值。
6. **结构体访问符号 . 和 ->**
- 结构体**实体(不是指针)**:用 `.` 访问成员,如 `task.id`
- 结构体**指针**:用 `->` 访问成员,如 `ptask->id`
7. **链表**
链表就是结构体 + 结构体指针:结构体里放数据 + 一个同结构体类型的next指针,用指针把一个个节点串起来,内核调度队列大量用链表。
---
## 二、原文里错误/不准确的点(重点,按顺序)
> 标注:❌错误 / ⚠️理解有偏差
1. > `unsigned long` 其实是 unsigned long int
✅这句本身语法上对,C里`unsigned long`等价`unsigned long int`;
⚠️但是:**不是所有平台字节都不一样**。RISC-V 64位平台 `unsigned long` 固定8字节;32位RISC-V是4字节;它是由**架构+ABI**规定,不是随便变化。内核开发必须清楚当前目标平台,不能模糊理解。
2. > 静态static局部变量:不管怎么调用,它的值都跟初始化的值一样,不会变化
❌ 这里是核心错误!
static局部变量**只初始化一次**,不是值固定不变!
```c
void test(){
static int cnt = 0;
cnt++;
printf("%d", cnt);
}
```
第一次调用输出1,第二次2,第三次3。
👉 关键点:初始化语句 `static int cnt=0` 只跑1次;代码里依然可以修改它的值;只是函数退出,变量不会销毁、值保留。
你原话理解成“值不能改”,是典型误区。
3. > 自动局部变量我们也喜欢在前面加Alt
⚠️ 这是你自己自定义标记,C没有alt关键字,只是个人命名习惯,没问题,但要分清:C关键字是`auto`,auto局部变量就是默认局部变量,平时都省略不写auto。
4. > `p = x` (你想写指针赋值)
❌ 正确写法:`p = &x`;
`p = x` 是把x里面的数值直接赋值给p(指针变量),类型不匹配,编译报错。
`&` 是取地址运算符,这个符号不能丢,这是指针入门最容易写错的地方。
5. > `S = *p`,S就拿到x的值;printf输出p会输出10
❌
- p存的是x的**内存地址**,printf直接打印p,出来的是一串地址数字,不是x的值;
- `*p` 才代表x里面存放的数据(也就是10);
```c
int x = 10;
int *p = &x;
int s;
s = *p; // s = 10 ✅
printf("%p", p); // 打印x的地址
printf("%d", *p); // 打印10
```
6. > 指针就是一个箭头,用来把不同的值连接起来
⚠️ 比喻可以这么记,但严谨定义:指针变量存**内存地址**;箭头只是你理解的模型,指针本身是内存里一个存地址的数值,不是什么抽象连线。
7. > 链表:struct里面放next形的指针
⚠️ 严谨写法:
```c
struct node {
int data;
struct node *next; // 指向同类型节点的指针
};
```
必须是 `struct node *next`,不能随便写next类型。
8. > 结构体指针用大于符号、普通变量用点
❌ 口误:结构体指针访问成员运算符是 `->`,不是大于号 `>`。
- 结构体实例:`.`
- 结构体指针:`->`
9. > 全局变量默认0,static局部变量默认0,auto局部无初值
✅ 这一条你说对了,这个点很多人搞混,你抓对了!
---
## 三、知识点思辨板块(给你自己加深理解,分享的时候也可以加上)
### 1. 为什么内核里疯狂用 unsigned long?
内核到处操作寄存器、虚拟地址、物理地址,地址一定是非负数;signed long有符号位,最高位会被解释成负数,地址运算很容易溢出出错。所以地址、帧指针sp、寄存器值统一用无符号类型。
### 2. 为什么区分auto / static局部变量?什么时候在内核里用static?
- auto局部变量放栈:栈空间很小,函数退出自动回收,适合临时变量;中断、trap handler里大部分临时变量都是auto;
- static局部放数据段:生命周期和整个程序一样。内核里尽量少用static局部,容易并发竞态;一般用来放一次性初始化、只读的东西。
### 3. 头文件 .h 和实现 .c 的意义(你找trap_handler找不到的时候就是这个问题)
函数声明放.h,给别的文件看(告诉编译器长什么样);实现放.c;
你在main.c里include `trap.h`,编译器知道 `trap_handler` 签名;链接阶段才去trap.c找函数实体。
> 找不到函数两种情况:①头文件没写声明;②链接脚本没把对应.o编进去;③名字拼写错误(trap / chop)
### 4. 指针和数组的关系(后面你写链表、缓冲区会碰到)
数组名在很多场景下会退化成首元素指针;但数组不是指针:数组自带长度信息,分配方式不一样。
### 5. PCB结构体为什么是操作系统的核心?
内核要同时跑多个任务,每个任务的寄存器、sp、id、状态打包进struct task,就是PCB;调度器就是在操作一堆PCB节点的链表。
## 四、工具运用(Git/仓库角度,贴合你内核项目)
1. 写这类学习复盘,推荐在仓库里新建 `notes/c_catchup.md`,每次学完commit,把口播稿、错误点、代码示例一起存;
2. 遇到函数找不到的时候:用 `grep -r "trap_handler" .` 在仓库全局搜索符号,快速定位声明和定义;
3. 区分报错:编译报错=类型/声明不对;链接报错=找到声明、找不到函数实现。
---
## 五、留给你后续要补的清单(你自己也意识到模糊的地方)
1. 不同架构下 `unsigned long` / uint64_t 的区别,stdint.h固定宽度类型(内核推荐用uint64_t,比unsigned long更稳定)
2. 指针运算(p++不是简单+1,是+sizeof(type))
3. 结构体内存对齐
4. 链表的增删改查、内核list_head(Linux风格链表,后面你大概率会接触)
5. 函数指针(trap入口、中断向量表本质就是函数指针,刚好贴合你trap代码)
疑问 1:unsigned long,它的字节长度,是不是随计算机系统不一样?我这块不清楚
原话:unsigned long 根据你的计算机系统,好像有不同的字节常数,这个我不是很清楚。
✅ 结论:是的,它是平台 ABI 相关,不是固定字节
RISC-V64 / x86_64:unsigned long = 8字节(64bit)
RISC-V32 /x86 32 位:unsigned long = 4字节(32bit)
但不是 “随便变”,是架构 + ABI 规定死的,同一个平台下永远不变。
内核开发推荐:如果想要固定不变的宽度,用 <stdint.h> 里的 uint64_t、uint32_t;unsigned long 适合存放地址(地址宽度和它天然对齐)。
疑问 2:局部变量没初始化,如果直接使用,会发生什么?(你说:这个很蠢点,我得往下思考)
原话:局部变量我好像没进行初始化,我不知道这个点,我想一下。
✅ 结论:
auto 局部变量放在栈上,不会清零,里面是栈上残留的垃圾值(随机值)。
行为:C 标准里这属于未定义行为,程序时而正常、时而崩溃、算出奇怪数字;
内核里非常危险:中断 handler、trap 里面乱读未初始化局部变量,会随机炸内核。
对比记忆:全局 /static 变量在数据段,加载时会默认清零。
疑问 3:trap_init 里面 trap.entry、addr,哪个是全局、哪个是局部?这里没搞懂
原话:trap.entry 肯定是全局变量,这个 addr 肯定也是一个全局变量?对,不是,这我没搞懂。
简单判断规则:
看定义位置,不是看它被谁赋值:
在函数外面定义 → 全局;
在函数{}内部定义 → 局部。
举例子:
c
运行
unsigned long trap_entry; // 在函数外面:全局变量
void trap_init(unsigned long addr) { // addr是形参,本质是局部变量
trap_entry = addr;
}
👉 trap_entry:全局;addr:函数形参(属于局部 auto 变量)
你这里是把局部 addr 的值拷贝给全局 trap_entry,不是说赋值给谁,谁就和它同生命周期。
疑问 4:指针这块:printf直接打印 p,会不会输出 10?这里明显感觉自己错了
原话:printf 输出 p 等于?它 p 本来就一个地址,怎么能输出 10 呢,这个地方需要再理解。
c
运行
int x = 10;
int *p = &x;
p:指针变量,存的是 x 的内存地址,打印 p(% p)出来是一串地址数字,不是 10
*p:解引用,去 p 存的地址上拿数据,*p == 10,printf("%d", *p) 才会打印 10
一句话区分:
不带*:操作地址本身
带*:顺着地址,操作地址里面存的数据
疑问 5:结构体指针访问成员,到底是 -> 还是大于号?(口误混淆)
原话:如果里面是指针的话就是通过大于符号,如果是简单的变量的话就是点
✅ 纠正:不是大于 >!是箭头运算符 ->
结构体实体(普通变量):.
c
运行
struct task t;
t.id = 1;
结构体指针:->
c
运行
struct task *pt = &t;
pt->id = 2;
等价写法:(*pt).id = 2,-> 就是这个的简写。
疑问 6:函数调用、头文件、找不到 trap_handler 的疑惑:为什么.h 里声明了,在很多文件里翻找不到实现?
原话:在.h 里面看到接口声明,但是到处找找不到函数实现。
两个核心原因:
声明和实现分离:.h 只告诉编译器 “这个函数长这样”,代码实现在另一个.c;
搜索的时候名字写错(你一直口误 chop /trap);
工程查找命令(内核项目非常好用)
bash
运行
grep -r "trap_handler" .
找到 unsigned long trap_handler(...); → 这是声明(h 文件)
找到 unsigned long trap_handler(...) { ... } → 这是实现(c 文件)
疑问 7:链表结构体里面那个 next 指针为什么必须是 struct node *next?有点扯不清楚
原话:结构体里面的 next 指针,这个感觉有点扯不清楚。
c
运行
struct node {
int data;
struct node *next;
};
链表节点要串下一个同类型节点,所以指针类型必须是自己这个结构体;
难点:写的时候struct node还没定义完,但 C 允许不完整类型指针,所以可以写。
作用:每个节点存下一个节点的地址,把零散节点串成一条链,内核任务队列就是这么干的。