链接: Untyped
Untyped
这篇主要是针对seL4物理内存管理的介绍。
物理内存
在seL4系统中,除了内核占用的一小部分静态内存之外,其他的所有的物理内存都是用户一级管理的。seL4在启动时创建的对象能力,以及seL4管理的其余物理资源,都会在启动时传递给根任务。
未分类内存以及能力
除了用于创建根任务的对象,对于其他的所有可用物理内存的能力都被传递给了根任务作为untyped内存 。未类型化内存是一块连续的指定大小的物理内存。Untyped capabilities是针对未类型化内存的能力。未类型化能力可以被retyped成为内核对象以及针对这些对象的能力,或者进一步成为通常来讲更小的未类型化能力。
未类型化能力有一个布尔类型的属性即device,该属性指明这块内存是不是内核可写的:也就是说这块内存可能不是由RAM支持的而是一些其他的设备,或者说在RAM区域中这块内存是无法被内核寻址的。Device untyped能力只能重新类型化为帧对象(即物理内存帧,可映射到虚拟内存中),并且内核不能对其进行写操作。
初始状态
提供给根任务的seL4_BootInfo描述了所有的未类型化能力,包括他们的大小,如果他们是未类型化设备(device),还会提供未类型化设备的地址。下面的代码展示了如何输出由seL4_BootInfo提供的初始的未类型化的能力。
printf(" CSlot \tPaddr \tSize\tType\n");
for (seL4_CPtr slot = info->untyped.start; slot != info->untyped.end; slot++) {
seL4_UntypedDesc *desc = &info->untypedList[slot - info->untyped.start];
printf("%8p\t%16p\t2^%d\t%s\n", (void *) slot, (void *) desc->paddr, desc->sizeBits, desc->isDevice ? "device untyped" : "untyped");
}
重新类型化
未类型化的能力只有一个函数调用:seL4_Untyped_Retype,也即是从一个未类型化的能力创建一个新的能力(以及潜在的对象)。特别的是,由重新类型化调用创建的新能力提供了对原始内存功能内存范围子集的访问,或者作为一个更小的未类型化的能力(前面提到过),或者是作为一个特定的类型指向一个新的对象。由重新类型化一个未类型化能力创建的新能力被认为是那个未类型化能力的孩子。
未类型化的能力以一种贪心的策略从调用的未类型化能力中增量的重新类型化,在seL4系统中,为了获得有效的内存利用,理解这一点很重要。每个未类型化的能力都维持了一个水印,水印之前的地址不可用(已重新类型化),而水印之后的地址仍然空闲(尚未重新类型化)。在重新类型化的操作中,水印首先移动到正在创建的对象的对齐的位置,然后到对象大小的末尾。
例如,如果我们先创建了一个4KiB的对象,然后又创建了一个16KiB的对象(KiB是210B,而KB指的是103B),有12KiB空间若位于4KiB对象的末尾以及16KiB对象的开头则会因对其而被浪费掉。这块内存将在这两个孩子都被撤销之后才能被回收。
应该按照尺寸从大到小给对象进行分配,最大的最先分配,这样做可以避免浪费内存。
error = seL4_Untyped_Retype(parent_untyped, // the untyped capability to retype
seL4_UntypedObject, // type
untyped_size_bits, //size
seL4_CapInitThreadCNode, // root
0, // node_index
0, // node_depth
child_untyped, // node_offset
1 // num_caps
);
上面的代码片段展现了将未类型化能力重新类型化为一个更小的4KiB未类型能力的案例。尽管名称未seL4_UntypedObject,但是没有创建任何对象,新的无类型能力只是指向了parent_untyped的未类型化内存的子集。但是处于此操作的目的,将这一块内存视为对象是比较方便的。当我们在下面讨论重新类型化调用的每个参数时,我们会引用这个代码片段。 回想一下,第一个参数时正在被调用进行重新类型化的未类型化能力。
type
在seL4中的每个对象都有一个特定的类型,并且所有的类型常量都可以在libsel4中找到。有一些类型时特定于体系结构的,而其他类型则是跨体系结构通用的。在上面改的例子中,我们创建了一个新的未类型化的能力(也就是创建的这个能力可以被进一步的重新类型化)。其他的所有对象类型都会创建一个内核对象以及操作该对象的能力,所创建的对象(及能力)的类型决定了该能力上可以执行的调用操作。例如,假如我们重新类型化了一个线程控制块(TCB——seL4_TCBObject),那么我们就可以对新的TCB对象的能力执行TCB调用操作,包括seL4_TCB_SetPriority。
Size
size参数决定了新对象的大小。参数的含义取决于正在被请求创建的对象类型(Type)。
- seL4中的大多数对象大小都是固定的,因此创建这类对象的时候,内核会忽略该参数
- 对于seL4_UntypedObject以及seL4_SchedContextObject这两个对象允许一个可变的大小,这是在size_bits中指定的(更多信息见下文)
- seL4_CapTableObject这个对象的大小也是可变的,type参数会指定能力插槽的数量。类似于size_bits一样的机制,但是指定的是插槽的数量,而不是字节数的大小。
通常来讲,在seL4中,如果以位(bit)为单位,他们是两个的幂。“Bits”并不是指该对象占据的位数,而是值需要描述其内容所需的位宽度。位大小位n的对象大小为2n个字节。同样的,通常来说在seL4中,对象和内存区域对齐于他们的大小,例如一个n-bit的对象对齐于2n字节,或者,同样的,对象的地址将0作为其n个底部位。
对于重新类型化而言,记住size_bits参数意味着对象大小为2size_bits且对于seL4_CapTableObject而言,其意味着插槽数量为2size_bits就足够了(计算CapTableObject对象的大小在(一)中已经讲过)。
Root,node_index&node_depth
root、node_index以及node_depth参数用于指定将新能力放置在哪个CNode中去。取决于depth参数,使用的插槽是通过调用或者直接寻址进行定址(一中已经讲过)。
在上面的案例中,node_depth被设置为0,着意味着使用的是调用寻址:使用当前线程的CSpace根以seL4_WordBits为参数进行隐含查找root。所以上面的案例指定了根任务的CNode。而node_index参数在这种情况下是被忽略的。
如果node_depth的值不为0,那么将对当前线程CSpace根作为根进行直接寻址。然后node_index参数被用于定位CNode能力以放置新的能力。简单点来说就是node_index给出地址,然后node_depth给出解析深度,root作为根。主要是面向多级CSpaces设计的,在这篇课程中并不涉及。
Node_offset
node_offset用于指定开始创建新能力的CSlot,就是在前面参数指定的CNode中。在上面的代码案例中,初始CNode的第一个空槽会被选中。前面三个参数用于选择CNode,这个参数用于选择空槽。
Num_caps
重新类型化能够被用于一次创建多个能力以及对象——能力的数量使用这个参数指定。注意,该值有两个限制:
- 未分类对象必须足够大以能适应所有的重新类型化内存。(num_caps * (1u << size_bits))
- CNode必须有足够的连续的空槽以放置新的能力
上面个几个东西看的人有点迷糊,再总结一下:
- root:指定了根CNode能力,表示从哪个CNode开始查找要放置新能力的槽位。
- node_index 和 node_depth:用于在这个根CNode中进行层次结构的寻址。这两个参数结合起来指定了从根CNode到目标CNode的路径,用于找到要放置新能力的具体CNode。
- node_offset:指定在目标CNode中的具体槽位位置,即将新能力放置在哪个槽位。
总结:root、node_index、node_depth是用于寻址到CNode的,而node_offset在目标CNode中指定了最终放置新能力的槽位。
实操
创建一个未类型化能力
先拉取好教程:
./init --tut untyped
cd untyped_build
ninja
./simulate
当你运行这个tutorials的时候,你将会看到类似于下面的输出:列出了所有在启动时提供给根任务的所有未类型化的能力:
Booting all finished, dropped to user space
CSlot Paddr Size Type
0x12d 0x100000 2^20 device untyped
0x12e 0x200000 2^21 device untyped
0x12f 0x400000 2^22 device untyped
0x130 0xb2e000 2^13 device untyped
可以看到我的执行结果如下:
在输出的最后有一个错误:
出现这个错误的原因是因为我们想要创建一个大小为0的未类型化对象。
实操:计算出孩子未类型化对象所需要的大小,以便该未类型化子级可用于创建objects数组中列出的所有对象。需要牢记的是对象的大小使用bit_size进行标识的,这是两个玩意的幂。
首先代码中看到两行这玩意,在API中进行查找也没找到,seL4_Word通常用于表示无符号整数,具体来说它通常是一个固定大小的整数类型,这里就很奇怪,怎么一个Object能定义为一个整数(应该是在seL4的某个.h文件里面定义的),后来仔细想了一下,发现这其实是一种设计,将一种类型用一个整数来标识,在对一块untyped空间进行retype的时候可以使用整数值快速的指定想要重新类型化为什么对象。而这里定义这个数组也是为了告诉读者,你一会需要retype这三种对象,而下面的sizes数组保持与上面的objects数组对齐,好让读者去计算每类对象所占空间的大小,对于程序而言,这两行代码没什么实际意义。
不妨将这三个Bits输出出来,看一下各自是多大。
printf("TCB: %zu,EndPoint: %zu,Notification: %zu\n",seL4_TCBBits, seL4_EndpointBits, seL4_NotificationBits);
可以看到最大为11,因此size写12即可,而且还有很多空间用不到,完全足够。
seL4_Word untyped_size_bits = 12;//主要是将这里改成12
seL4_CPtr parent_untyped = 0;
seL4_CPtr child_untyped = info->empty.start;
// First, find an untyped big enough to fit all of our objects
// for循环查找未类型化对象中能够放得下Bits为12的,且是非device,然后在其内进行重新类型化
for (int i = 0; i < (info->untyped.end - info->untyped.start); i++) {
if (info->untypedList[i].sizeBits >= untyped_size_bits && !info->untypedList[i].isDevice) {
parent_untyped = info->untyped.start + i;
break;
}
}
// create an untyped big enough to retype all of the above objects from
error = seL4_Untyped_Retype(parent_untyped, // the untyped capability to retype
seL4_UntypedObject, // type
untyped_size_bits, //size
seL4_CapInitThreadCNode, // root
0, // node_index
0, // node_depth
child_untyped, // node_offset
1 // num_caps
);
ZF_LOGF_IF(error != seL4_NoError, "Failed to retype");
此时可以看到输出已经变了,没有了Failed to retype,而是如下
需要做下一步的练习。
创建一个TCB对象
优先级检查失败是因为child_tcb是一个空插槽。看一下代码
seL4_CPtr child_untyped = info->empty.start;
error = seL4_Untyped_Retype(parent_untyped, // the untyped capability to retype
seL4_UntypedObject, // type
untyped_size_bits, //size
seL4_CapInitThreadCNode, // root
0, // node_index
0, // node_depth
child_untyped, // node_offset
1 // num_caps
);
seL4_CPtr child_tcb = child_untyped + 1;
error = seL4_TCB_SetPriority(child_tcb, seL4_CapInitThreadTCB, 10);
child_untyped原本是第一个空槽,在进行了retype之后,该槽用于放置新的能力,而child_untyped+1此时应该是一个空槽。而在最后一行代码中对该空槽设置了优先级,所以会出现Failed to set priority的错误。因此这步需要做的是从child_untyped中创建一个TCB对象,然后将能力插入到child_tcb这个空槽里面。考虑到上面理论部分,重新类型化的时候直接寻址一般是用于多级CNode的,这里的CSpace只是由一个CNode组成的。因此应使用调用寻址,也就是node_index和node_depth都为0,下面是实现代码。
error = seL4_Untyped_Retype(child_untyped,
seL4_TCBObject,
seL4_TCBBits,
seL4_CapInitThreadCNode,
0,
0,
child_tcb,
1
);
正确创建之后可以看到输出的错误信息已经来到了下一步,如下所示
创建一个端点对象
现在的错误是因为一个非法的端点能力,看一下代码
// use the slot after child_tcb for the new endpoint cap:
seL4_CPtr child_ep = child_tcb + 1;
/* TODO create an endpoint in CSlot child_ep */
// identify the type of child_ep
uint32_t cap_id = seL4_DebugCapIdentify(child_ep);
ZF_LOGF_IF(cap_id == 0, "Endpoint cap is null cap");
可以看到child_ep指向了下一个空槽,因此在此处我们要创建一个端点对象,类似于创建TCB对象,代码如下:
error = seL4_Untyped_Retype(child_untyped,
seL4_EndpointObject,
seL4_EndpointBits,
seL4_CapInitThreadCNode,
0,
0,
child_ep,
1
);
此时输出的错误信息来到了下一步:
创建一个信号量对象
下一部分代码尝试使用了一个不存在的信号量对象,因此我们需要从child_untyped中重新类型化出一个信号量对象,然后将新的能力放置在child_ntfn插槽里面,类似于上面的直接上代码。
error = seL4_Untyped_Retype(child_untyped,
seL4_NotificationObject,
seL4_NotificationBits,
seL4_CapInitThreadCNode,
0,
0,
child_ntfn,
1
);
此时输出的错误信息来到了下一步:
删除对象
可以看到输出的错误信息是未能创建端点,因为最后一部分是尝试创建足够多的端点对象将child_untyped来消耗完整个未类型化的对象。失败的原因是因为该未类型化对象已经被先前的对象完全分配掉了(存疑)。看一眼代码:
// TODO revoke the child untyped
// allocate the whole child_untyped as endpoints
// Remember the sizes are exponents, so this computes 2^untyped_size_bits / 2^seL4_EndpointBits:
// 这里作减法,除法->幂相减,再取指
seL4_Word num_eps = BIT(untyped_size_bits - seL4_EndpointBits);
error = seL4_Untyped_Retype(child_untyped, seL4_EndpointObject, 0, seL4_CapInitThreadCNode, 0, 0, child_t>
ZF_LOGF_IF(error != seL4_NoError, "Failed to create endpoints.");
printf("Success\n");
因此需要将之前创建的对象都删除掉,回收之前已经分配出去的untyped空间。使用(一)中提到的seL4_CNode_Delete(),这个方法,将使用retype方法创建的对象删除掉,只需要删除掉在seL4_CapInitThreadCNode中插入的新能力。代码如下:
// TODO revoke the child untyped
error = seL4_CNode_Delete(seL4_CapInitThreadCNode, child_ntfn, 64);
ZF_LOGF_IF(error, "Failed to delete notification object.");
error = seL4_CNode_Delete(seL4_CapInitThreadCNode, child_ep, 64);
ZF_LOGF_IF(error, "Failed to delete Endpoint object.");
error = seL4_CNode_Delete(seL4_CapInitThreadCNode, child_tcb, 64);
ZF_LOGF_IF(error, "Failed to delete TCB object.");
运行之后结果如下:
进一步实操
把课程过一遍再说吧