Memory allocator

实验目标:优化内存分配器的并行性能,通过将单全局锁+单空闲链表的设计改为每CPU锁+每CPU空闲链表,减少多核环境下的锁竞争。

当前的瓶颈:

1
2
3
4
struct {
struct spinlock lock;
struct run *freelist;
} kmem;

所有CPU共享同一个空闲链表和一个锁, 任何kalloc/kfree操作都需要竞争这个全局锁

解决方案:每个CPU操作自己的链表和锁

打开kalloc.c,先修改数据结构

1
2
3
4
struct {
struct spinlock lock;
struct run *freelist;
} kmem[NCPU];

初始化每个CPU锁

1
2
3
4
5
6
7
8
9
void
kinit()
{
// 为每个CPU初始化锁
for(int i = 0; i < NCPU; i++) {
initlock(&kmem[i].lock, "kmem");
}
freerange(end, (void*)PHYSTOP);
}

修改kalloc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void *
kalloc(void)
{
struct run *r;
int id;

// 安全获取当前CPU ID
push_off();
id = cpuid();
pop_off();

// 先尝试从当前CPU分配
acquire(&kmem[id].lock);
r = kmem[id].freelist;
if(r) {
kmem[id].freelist = r->next;
release(&kmem[id].lock);

if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}
release(&kmem[id].lock);

// 当前CPU链表空,从其他CPU偷取
for(int i = 0; i < NCPU; i++) {
if(i == id) continue; // 跳过自己

acquire(&kmem[i].lock);
r = kmem[i].freelist;
if(r) {
kmem[i].freelist = r->next;
release(&kmem[i].lock);

if(r)
memset((char*)r, 5, PGSIZE);
return (void*)r;
}
release(&kmem[i].lock);
}

return 0;
}

修改kfree

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void
kfree(void *pa)
{
struct run *r;

if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");

// Fill with junk to catch dangling refs.
memset(pa, 1, PGSIZE);

r = (struct run*)pa;

// 安全获取当前CPU ID
push_off();
int id = cpuid();
pop_off();

// 使用当前CPU的锁,而不是全局锁
acquire(&kmem[id].lock);
r->next = kmem[id].freelist;
kmem[id].freelist = r;
release(&kmem[id].lock);
}

测试通过

Read-write lock

本实验要实现读写自旋锁,以提高多核下的读取并发性能。主要要实现读模式:允许多个读者同时持有锁,同时禁止写者进入;写模式:只允许一个写者持有锁,禁止其他读者和写者进入。

实验思路:在读写锁结构体中引入一把标准的自旋锁(lk)来严格保护 readers(读者数)、writer(写锁持有位)和 pending_writers(写者排队数)这三个状态变量;所有读写操作在检查或修改这些状态前必须先持有 lk。同时根据题目采用写者优先,写者在尝试获取锁前先增加 pending_writers,读者一旦发现有等待的写者便主动避让。

下面开始写代码:

首先在spinlock.h里修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifdef LAB_LOCK
// Reader-writer lock.
struct rwspinlock {
// 自旋锁,用来保护下面的 3 个计数器
struct spinlock lk;

int readers; // 读者数量
int writer; // 写者是否持有 (0 或 1)
int pending_writers; // 写者排队计数 (实现写者优先)

// 调试信息
char *name;
struct cpu *cpu;
};
#endif

然后修改spinlock.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static void
read_acquire_inner(struct rwspinlock *rwlk)
{
while(1) {
acquire(&rwlk->lk); // 1. 关门 (持有锁)

// 2. 检查:是否有写者持有?是否有写者排队?
if (rwlk->writer == 0 && rwlk->pending_writers == 0) {
rwlk->readers++; // 进场
release(&rwlk->lk); // 开门 (放锁)
return;
}

// 3. 条件不满足,开门,并在外面自旋重试
release(&rwlk->lk);
}
}

static void
read_release_inner(struct rwspinlock *rwlk)
{
acquire(&rwlk->lk); // 关门
rwlk->readers--; // 离场
release(&rwlk->lk); // 开门
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static void
write_acquire_inner(struct rwspinlock *rwlk)
{
// 1. 举手排队 (必须加锁修改)
acquire(&rwlk->lk);
rwlk->pending_writers++;
release(&rwlk->lk);

while(1) {
acquire(&rwlk->lk); // 关门

// 2. 检查:必须完全空闲 (无读 且 无写)
if (rwlk->readers == 0 && rwlk->writer == 0) {
rwlk->writer = 1; // 抢占
rwlk->pending_writers--; // 取消排队

rwlk->cpu = mycpu();
release(&rwlk->lk); // 开门
return;
}

release(&rwlk->lk); // 条件不满足,开门重试
}
}

static void
write_release_inner(struct rwspinlock *rwlk)
{
acquire(&rwlk->lk); // 关门

rwlk->cpu = 0;
rwlk->writer = 0; // 释放写状态

release(&rwlk->lk); // 开门
}

1
2
3
4
5
6
7
8
9
10
11
void
initrwlock(struct rwspinlock *rwlk)
{
rwlk->name = "rwlock";
// 必须初始化内部的锁
initlock(&rwlk->lk, "rwlk_inner");
rwlk->readers = 0;
rwlk->writer = 0;
rwlk->pending_writers = 0;
rwlk->cpu = 0;
}

本以为修改完成,但是结果却是这样

1
2
3
4
5
rwspinlock_test(0): 0
rwspinlock_test(2): -1
rwspinlock_test(1): 0
rwspinlock_test(3): -1
rwlktest: 2/4 CPUs succeeded

找了很久的原因,直到我把delay函数里的10000改为100000,才能通过

1
2
3
4
5
6
7
8
9
static uint
delay()
{
static uint v;
for (int i = 0; i < 100000; i++) {
__atomic_fetch_add(&v, 1, __ATOMIC_RELAXED);
}
return __atomic_load_n(&v, __ATOMIC_RELAXED);
}

ai的解释:原有的 delay 时间过短(仅微秒级),无法覆盖宿主机几十毫秒的调度间隙,导致“读者”在“写者”被冻结期间误判为无人排队而抢先进入。增加 delay 的本质是用软件层面的等待时间换取物理层面的调度容错,强迫读者空转更久,从而保证即使写者被宿主机暂时挂起,也能在读者结束等待前“复活”并完成排队登记,消除了因双重调度延迟导致的逻辑误判。

测试通过

1
2
3
4
5
rwspinlock_test(0): 0
rwspinlock_test(1): 0
rwspinlock_test(3): 0
rwspinlock_test(2): 0
rwlktest: 4/4 CPUs succeeded