xv6源码剖析 004
今天晚上我们来看看xv6的内存配置器和用户进程的初始化,分别是umalloc.c
和init.c
这两个文件。
ps:2024/4/11,昨晚着急睡觉,所以有些地方疏忽了,不好意思不好意思
umalloc.c
我们先来看看配置维护的数据结构
typedef long Align;
union header {
struct {
union header *ptr;
uint size;
} s;
Align x;
};
xv6的核心配置器是一个联合体,这个联合体可以被当成long
使用也可以被当成一个结构体来使用,当成结构体来使用的时候就是一个链表,
在umalloc.c
中定义的全局静态变量
typedef union header Header;
static Header base;
static Header *freep;
可以看到有两个联合体一个是base
,一个是free
。我们继续往下看,现在还看不出什么门道(没什么内核开发的经验)。我们知道,在c语言中,POSIX提供了两个分配内存的接口,分别是malloc
和free
。在这个源文件中大佬也写了这两个接口,但是在malloc
中是调用了另一功能函数morecore(uint nu)
。
看了一下,好像并没有什么用,那我们直接忽略这个
free(void *ap)
static Header *morecore(uint nu)
void *malloc(uint nbytes)
我们先来看看
free(void *ap)
这段代码真的很坑人,我开始带入我自己的内存池模型,掉大坑了,下面我通过画图的方式来讲解一下
void
free(void *ap)
{
Header *bp, *p;
bp = (Header*)ap - 1;
/// 1.
for(p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr)
if(p >= p->s.ptr && (bp > p || bp < p->s.ptr))
break;
/// 2.
if(bp + bp->s.size == p->s.ptr){
bp->s.size += p->s.ptr->s.size;
bp->s.ptr = p->s.ptr->s.ptr;
} else
bp->s.ptr = p->s.ptr;
/// 3.
if(p + p->s.size == bp){
p->s.size += bp->s.size;
p->s.ptr = bp->s.ptr;
} else
p->s.ptr = bp;
freep = p;
}
我们将这个函数的过程分为三个部分
第一部分:确定释放的内存在内存池中的位置
for(p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr)
if(p >= p->s.ptr && (bp > p || bp < p->s.ptr))
break;
知道确定了释放的内存块在内存池具体的块之后,也就是当bp > p && bp < p->s.str
成立时,退出循环进入下一个步骤
第二步:设置空闲块的属性,我们在看一个图
如果条件bp + bp->s.size == p->s.ptr
成立,那么就会将两个块合成一个大块,因为在内存池中的内存块不一定是连续的,如果不成立的话就将这个块作为一个单独的块加入内存池中。
/// 2.
if(bp + bp->s.size == p->s.ptr){
bp->s.size += p->s.ptr->s.size;
bp->s.ptr = p->s.ptr->s.ptr;
} else
bp->s.ptr = p->s.ptr;
如下图所示
下面给大家展示一下,执行完步骤二后的内存池中的情况
最后一步就是:设置p的值了,也就是freep的值
相信通过上面的分析,这一步也很简单了,核心思路也很简单,如果连续就合并,如果不连续就修改一下指针。
if(p + p->s.size == bp){
p->s.size += bp->s.size;
p->s.ptr = bp->s.ptr;
} else
p->s.ptr = bp;
freep = p;
ok,我们再来看看另一函数
morecore(uint nu)
static Header* morecore(uint nu)
{
char *p;
Header *hp;
if(nu < 4096) // 1.
nu = 4096;
p = sbrk(nu * sizeof(Header)); // 2.系统调用sbrk
if(p == (char*)-1) // 3.
return 0;
hp = (Header*)p; // 4.
hp->s.size = nu;
free((void*)(hp + 1));
return freep;
}
在morecore
我们可以将函数看成四个主要的步骤:
-
检查申请的内存的大小是否满足一个page的大小(在xv6中一个页固定大小为4096,当然我们实现内存池的时候当然可以根据我们自己的需要将内存划分为多种不同的大小的块,就是将xv6的这个配置器看成我们内存池的一个子集)。为什么要分配固定分配4096个字节的数据呢?这主要是为了避免内存碎片(这个是错的,大家看完了就知道了) 申请4096的大小的空间是因为xv6内核的页就是固定的4096个字节,哈哈哈,(这是我猜的)在原来的libc中的cmalloc中,经过长时间的malloc和free,在堆区中会长生大量的内存碎片,看下图。可以看到其实在下面的内存中我们是有足够的空间来容纳两个新的数据块的,但是,因为这几个空闲内存块并不连续,导致了malloc返回一个空指针,在实际情况中我们可能也会发现,我们的内存利用率并不高,但是内存分配总是失败,有可能就是这个原因。
- 我们再来说一下内存对齐(结构体对齐)算了时间不是很充足,兄弟们自己了解一下吧
-
第二步就是进行系统调用
sbrk()
向操作系统申请内存,我们需要注意的是这个系统调用是为进程申请空间。后面我们还会接触到这个系统调用的 -
然后就是判空了
-
最后分配成功,我们为这个新的内存块设置属性,并将这个内存块添加到空闲列表中(
freep
)
最后一个函数 malloc(uint nbytes)
我先说明一下下面三个变量的作用吧
prevp
:跟踪迭代器,以便修改内存池的块的信息
p
:迭代器
nunits
:将用户给定的申请的内存大小进行对齐,以便找出一个合适的内存块
不难看出,xv6的内存池使用的是优先匹配原则:一旦遇到合适的块就给出去
void* malloc(uint nbytes)
{
Header *p, *prevp;
uint nunits;
/// 1.
/// sizeof(Header) = 8
nunits = (nbytes + sizeof(Header) - 1)/sizeof(Header) + 1;
/// 2.
if((prevp = freep) == 0){
base.s.ptr = freep = prevp = &base;
base.s.size = 0;
}
/// 3.
for(p = prevp->s.ptr; ; prevp = p, p = p->s.ptr){
if(p->s.size >= nunits){
if(p->s.size == nunits)
prevp->s.ptr = p->s.ptr;
else {
p->s.size -= nunits;
p += p->s.size;
p->s.size = nunits;
}
freep = prevp;
return (void*)(p + 1);
}
if(p == freep)
if((p = morecore(nunits)) == 0)
return 0;
}
}
-
这一步有点类似单位转换,顺便进行对齐
-
这一步也很简单,检查freep是否被初始化了,
base就类似我们所说的虚拟头节点
-
这一步也很简单,找到第一个合适的块,如果块的大小刚刚好,直接全部给,否则就进行切割,
如果确实没有内存的话就执行morecore先内核申请一块。
init.c
ok,还有点时间我们光速过一下init.c
直接上代码,so easy,明天看看应该就能够进入内核篇了,内核才有意思哈哈哈哈。
int
main(void)
{
int pid, wpid;
/// 1.
打开终端,终端不是我们说的shell
if(open("console", O_RDWR) < 0){
mknod("console", CONSOLE, 0);
open("console", O_RDWR);
}
/// 将描述符进行拷贝,
/// 这时候,我们的fd 0 1 2
/// 分别为 stdin stdout stderr
dup(0); // stdout
dup(0); // stderr
/// 运行shell
/// 这时候才有我们看到的“终端”
for(;;){
printf("init: starting sh\n");
pid = fork();
if(pid < 0){
printf("init: fork failed\n");
exit(1);
}
if(pid == 0){
exec("sh", argv);
printf("init: exec sh failed\n");
exit(1);
}
/// 作为shell的父进程等待shell结束
for(;;){
// this call to wait() returns if the shell exits,
// or if a parentless process exits.
wpid = wait((int *) 0);
if(wpid == pid){
// the shell exited; restart it.
break;
} else if(wpid < 0){
printf("init: wait returned an error\n");
exit(1);
} else {
// it was a parentless process; do nothing.
}
}
}
}