_syscallX(type,name,type1,arg1,type2,arg2,...)
一个系统调用分为两个部分,一部分是给用户进程的接口函数,属于用户空间。另一部分是与之对应的内核具体实现,属于内核空间,此部分完成的是功能需求,即我们一直所说的系统调用子功能处理函数。为了区分,内核空间的函数名要在用户空间的函数名前面加 “ sys_” 。所以当我们实现时候,是要在用户空间通过 int 0x80,产生中断进入中断入口程序,然后再次进入真正的中断处理函数中。所以我们要编写中断向量号 0x80 的入口程序,即在 idt 中断描述符表中写好中断程序的选择子和偏移地址。
make_idt_desc(&idt[0x80], IDT_DESC_ATTR_DPL3, syscall_handler);
注意:这个中断描述符的DPL=3,这样用户进程的int 中断才能进去,之前的时钟中断、键盘中断等的中断描述符的DPL都是0特权级的,其实用3特权级就可以了。
/* 三个参数的系统调用 */
#define _syscall3(NUMBER, ARG1, ARG2, ARG3) ({ \
int retval; \
asm volatile ( \
"int $0x80" \
: "=a" (retval) \
: "a" (NUMBER), "b" (ARG1), "c" (ARG2), "d" (ARG3) \
: "memory" \
); \
retval; \
})
CPU首先进入中断入口程序,然后作为主调函数:压入参数push,call 调用函数的地址,add esp xxx。因为系统调用参数的不一样,有时候是3个参数,有时候是一个,但是我们调用函数时候都统一在栈中压入3个参数。在调用中断处理函数时候,eax中储存的是子功能号,因此我们利用这个特性去调用子功能号函数。事先我们将这些子功能号函数都写好,然后将地址都分别填入 syscall_table[] 数组中,然后 call [syscall_table+eax*4]即可。
extern syscall_table
section .text
global syscall_handler
syscall_handler:
;1 保存上下文环境
push 0 ; 压入0, 使栈中格式统一
push ds
push es
push fs
push gs
pushad
push 0x80 ; 此位置压入0x80也是为了保持统一的栈格式
;2 为系统调用子功能传入参数
push edx ; 系统调用中第3个参数
push ecx ; 系统调用中第2个参数
push ebx ; 系统调用中第1个参数
;3 调用子功能处理函数
call [syscall_table + eax*4] ; 编译器会在栈中根据C函数声明匹配正确数量的参数
add esp, 12 ; 跨过上面的三个参数
;4 将call调用后的返回值存入待当前内核栈中eax的位置
mov [esp + 8*4], eax
jmp intr_exit ; intr_exit返回,恢复上下文
uint32_t getpid()
{
return _syscall0(SYS_GETPID)
}
#define va_start(v,l)
#define va_end(v)
#define ca_arg(v,l)
va_start(ap,v):ap是指向可变参数(压入栈中)的指针变量,v是可变参数(压入栈中)的第一个参数,比如printf()来说,v是指 format 。
va_arg(ap,t):t是可变参数类型,函数功能是指针 ap 指向栈中下一个参数的地址并返回其值。
va_end(ap):将指针变量ap 置为 null。
printf()函数是“格式化” “输出” 函数,但是真正起到“格式化”作用的是 vsprintf 函数,真正起 “输出”作用的是 write 系统调用。
man 2 write:
size_t write(int fd,const void *bf,size_t count); //将buf中的 count 个字符写到文件描述符 fd 指向的文件中。
因为write() 是系统调用,所以我们按照三部曲编写好write。
int printf(const char* farmat,...); //format是格式化字符串,里面包含 “%类型字符”。
int vsprintf(char* str,const char* format,va_list ap);
#define va_start(ap, v) ap = (va_list)&v // ap指向第一个固定参数v:v 是format字符串的指针:char*,所以ap是二级指针,要转换为一级指针:char*
#define va_arg(ap, t) *((t*)(ap += 4)) // ap指向下一个参数并返回其值
#define va_end(ap) ap = NULL // 清除ap
static void itoa(uint32_t value, char** buf_ptr_addr, uint8_t base) {
uint32_t m = value % base; // 求模,最先掉下来的是最低位
uint32_t i = value / base; // 取整
if (i) { // 如果倍数不为0则递归调用。
itoa(i, buf_ptr_addr, base);
}
if (m < 10) { // 如果余数是0~9
*((*buf_ptr_addr)++) = m + '0'; // 将数字0~9转换为字符'0'~'9'
} else { // 否则余数是A~F
*((*buf_ptr_addr)++) = m - 10 + 'A'; // 将数字A~F转换为字符'A'~'F'
}
}
/* 将参数ap按照格式format输出到字符串str,并返回替换后str长度 */
uint32_t vsprintf(char* str, const char* format, va_list ap) {
char* buf_ptr = str;
const char* index_ptr = format;
char index_char = *index_ptr;
int32_t arg_int;
while(index_char) {
if (index_char != '%') {
*(buf_ptr++) = index_char;
index_char = *(++index_ptr);
continue;
}
index_char = *(++index_ptr); // 得到%后面的字符
switch(index_char) {
case 'x':
arg_int = va_arg(ap, int);
itoa(arg_int, &buf_ptr, 16);
index_char = *(++index_ptr); // 跳过格式字符并更新index_char
break;
}
}
return strlen(str);
}
arena: 是由一大块内存(一般都是一页)被划分成很多”小内存块“的内存仓库。arena是个提供内存分配的数据结构,分为两部分:一部分是元信息,12字节,用来描述自己内存池中空闲内存块数量、内存块描述符指针。另一部分就是内存池区域,这里面无数的小内存块。
//arena元信息
struct arena {
struct mem_block_desc* desc; // 此arena关联的mem_block_desc
uint32_t cnt;
bool large; //large为ture时,cnt表示的是页框数。否则cnt表示空闲mem_block数量
};
//内存块:里面只有一个链表元素,虽然每一个内存规格的sizof(struck mem_block)=8,比如:64 byte 的内存块,我们把一页内存划分很多64 byte 的内存块后,虽然这个结构的大小为8,只占了64字节的前小部分,但是我们使用索引来定位每个小内存块的起始地址,得到起始地址即可。
struct mem_block {
struct list_elem free_elem;
};
内存块描述符:分别为每一种规格的内存块建立一个内存块描述符,即 mem_block_desc,记录了内存块规格大小,以及位于所有同类arena中空闲内存块链表。内存块规格有多少种,内存块描述符就有多少种,因此各种内存块描述符的区别就是 block_size 不同,free_list中指向的内存规格不同。
/* 内存块描述符 */
struct mem_block_desc {
uint32_t block_size; // 内存块大小
uint32_t blocks_per_arena; // 本arena中可容纳此mem_block的数量.
struct list free_list; // 目前可用的mem_block链表
};
//内存块描述符:arena中的内存块数量:(4KB-元信息)/内存块大小
//我们要定义内核内存块描述符和用户内存块描述符,首先定义内核内存块描述符,供内核线程程序申请
struct mem_block_desc k_block_descs[DESC_CNT];
void block_desc_init(struct mem_block_desc* desc_array)
{
uint16_t desc_idx, block_size = 16;
/* 初始化每个mem_block_desc描述符 */
for (desc_idx = 0; desc_idx < DESC_CNT; desc_idx++)
{
desc_array[desc_idx].block_size = block_size; //从16字节开始
/* 初始化arena中的内存块数量 */
desc_array[desc_idx].blocks_per_arena = (PG_SIZE - sizeof(struct arena)) / block_size;
list_init(&desc_array[desc_idx].free_list);
block_size *= 2; // 更新为下一个规格内存块
}
}
用户的用户内存块描述符要在线程或者进程的 task_struct 的内容里面添加 u_block_decs[DESC_CNT] ,然后初始化,以便供用户代码申请。
/* 返回arena中第idx个内存块的地址 */
static struct mem_block* arena2block(struct arena* a, uint32_t idx)
{
return (struct mem_block*)((uint32_t)a + sizeof(struct arena) + idx * (a->desc->block_size));
}
static struct arena* block2arena(struct mem_block* b)
{
return (struct arena*)((uint32_t)b & 0xfffff000);
}
void* sys_malloc(uint32_t size)
{
enum pool_flags PF;
struct pool* mem_pool;
uint32_t pool_size;
struct mem_block_desc* descs;
struct task_struct* cur_thread = running_thread();
/* 判断用哪个内存池,然后确定用哪个内存块描述符表和从哪个内存池申请内存*/
if (cur_thread->pgdir == NULL)
{ // 若为内核线程,用内核内存块描述符表和在内核内存池中申请内存
PF = PF_KERNEL;
pool_size = kernel_pool.pool_size;
mem_pool = &kernel_pool;
descs = k_block_descs;
}
else
{ // 用户进程,则用的用户内存块描述表和在用户内存池中申请内存
PF = PF_USER;
pool_size = user_pool.pool_size;
mem_pool = &user_pool;
descs = cur_thread->u_block_desc;
}
/* 若申请的内存不在内存池容量范围内则直接返回NULL */
if (!(size > 0 && size < pool_size))
{
return NULL;
}
struct arena* a;
struct mem_block* b;
lock_acquire(&mem_pool->lock);
/* 超过最大内存块1024, 就分配页框 */
if (size > 1024)
{
uint32_t page_cnt = DIV_ROUND_UP(size + sizeof(struct arena), PG_SIZE); // 向上取整需要的页框数
a = malloc_page(PF, page_cnt);
if (a != NULL)
{
memset(a, 0, page_cnt * PG_SIZE); // 将分配的内存清0
/* 对于分配的大块页框,将desc置为NULL, cnt置为页框数,large置为true */
a->desc = NULL;
a->cnt = page_cnt;
a->large = true;
lock_release(&mem_pool->lock);
return (void*)(a + 1); // 跨过arena大小,把剩下的内存返回
}
else
{
lock_release(&mem_pool->lock);
return NULL;
}
}
else
{ // 若申请的内存小于等于1024,可在各种规格的mem_block_desc中去适配
uint8_t desc_idx;
/* 从内存块描述符中匹配合适的内存块规格 */
for (desc_idx = 0; desc_idx < DESC_CNT; desc_idx++)
{
if (size <= descs[desc_idx].block_size)
{ // 从小往大后,找到后退出
break;
}
}
/* 若mem_block_desc的free_list中已经没有可用的mem_block,
* 就创建新的arena提供mem_block */
if (list_empty(&descs[desc_idx].free_list))
{
a = malloc_page(PF, 1); // 分配1页框做为arena
if (a == NULL)
{
lock_release(&mem_pool->lock);
return NULL;
}
memset(a, 0, PG_SIZE);
/* 对于分配的小块内存,将desc置为相应内存块描述符,
* cnt置为此arena可用的内存块数,large置为false */
a->desc = &descs[desc_idx];
a->large = false;
a->cnt = descs[desc_idx].blocks_per_arena;
uint32_t block_idx;
enum intr_status old_status = intr_disable();
/* 开始将arena拆分成内存块,并添加到内存块描述符的free_list中 */
for (block_idx = 0; block_idx < descs[desc_idx].blocks_per_arena; block_idx++)
{
b = arena2block(a, block_idx);
ASSERT(!elem_find(&a->desc->free_list, &b->free_elem));
list_append(&a->desc->free_list, &b->free_elem);
}
intr_set_status(old_status);
}
/* 开始分配内存块 */
b = elem2entry(struct mem_block, free_elem, list_pop(&(descs[desc_idx].free_list)));
memset(b, 0, descs[desc_idx].block_size);
a = block2arena(b); // 获取内存块b所在的arena
a->cnt--; // 将此arena中的空闲内存块数减1
lock_release(&mem_pool->lock);
return (void*)b;
}
}
首先我们实现清理整个页表,然后在其基础上实现释放内存块。
我们分配内存操作:
1,在虚拟地址池中分配虚拟地址
2,在物理内存池中分配物理地址
3,填好页表完成映射
那么释放内存就是反操作;
1,在物理地址池中释放物理页地址,4K为单位的。物理地址可以不连续,需要一页一页的释放
2,在将物理地址释放后就可以释放页表了,在pte中将 p 位置0
3,在虚拟地址池中释放虚拟地址,虚拟地址是连续,可以连续释放
注意:我们只清零pte,不清零pde。一个页表是4K,一个页表项是4B。清零一个PTE是清理了4K,清零一个PDE是清理了4K。当我们要清理一个PDE时,我们还要循环检测每一个PTE的 P 位,所以为了不麻烦,我们就不清理这个。
//释放物理地址:清理物理地址就是把该物理地址对应的位图置0即可。物理地址是单独的
/* 将物理地址pg_phy_addr回收到物理内存池 */
void pfree(uint32_t pg_phy_addr)
{
struct pool* mem_pool;
uint32_t bit_idx = 0;
if (pg_phy_addr >= user_pool.phy_addr_start)
{ // 用户物理内存池
mem_pool = &user_pool;
bit_idx = (pg_phy_addr - user_pool.phy_addr_start) / PG_SIZE;
}
else
{ // 内核物理内存池
mem_pool = &kernel_pool;
bit_idx = (pg_phy_addr - kernel_pool.phy_addr_start) / PG_SIZE;
}
bitmap_set(&mem_pool->pool_bitmap, bit_idx, 0); // 将位图中该位清0
}
//置零PTE:找到该虚拟地址的PTE指针,然后将p为置0,然后更新tlb缓存,即把页表中该虚拟地址的pte重新写入tlb。
static void page_table_pte_remove(uint32_t vaddr)
{
uint32_t* pte = pte_ptr(vaddr);
*pte &= ~PG_P_1; // 将页表项pte的P位置0
asm volatile ("invlpg %0"::"m" (vaddr) : "memory"); //更新tlb
}
//释放虚拟地址:找到相应虚拟地址的位图,然后将其置0即可,虚拟地址是连续的。
static void vaddr_remove(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt)
{
uint32_t bit_idx_start = 0, vaddr = (uint32_t)_vaddr, cnt = 0;
if (pf == PF_KERNEL)
{ // 内核虚拟内存池
bit_idx_start = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE;
while (cnt < pg_cnt)
{
bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 0);
}
}
else
{ // 用户虚拟内存池
struct task_struct* cur_thread = running_thread();
bit_idx_start = (vaddr - cur_thread->userprog_vaddr.vaddr_start) / PG_SIZE;
while (cnt < pg_cnt)
{
bitmap_set(&cur_thread->userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 0);
}
}
}
void sys_free(void* ptr) //对于小内存来说,ptr一定内存块的起始地址
{
ASSERT(ptr != NULL);
if (ptr != NULL)
{
enum pool_flags PF;
struct pool* mem_pool;
/* 判断是线程还是进程 */
if (running_thread()->pgdir == NULL)
{
ASSERT((uint32_t)ptr >= K_HEAP_START);
PF = PF_KERNEL;
mem_pool = &kernel_pool;
}
else
{
PF = PF_USER;
mem_pool = &user_pool;
}
lock_acquire(&mem_pool->lock);
struct mem_block* b = ptr;
struct arena* a = block2arena(b); // 把mem_block转换成arena,获取元信息
ASSERT(a->large == 0 || a->large == 1);
if (a->desc == NULL && a->large == true)
{ // 大于1024的内存
mfree_page(PF, a, a->cnt);
}
else
{ // 小于等于1024的内存块
/* 先将内存块回收到free_list */
list_append(&a->desc->free_list, &b->free_elem);
/* 再判断此arena中的内存块是否都是空闲,如果是就释放arena */
if (++a->cnt == a->desc->blocks_per_arena)
{
uint32_t block_idx;
for (block_idx = 0; block_idx < a->desc->blocks_per_arena; block_idx++)
{
struct mem_block* b = arena2block(a, block_idx);
ASSERT(elem_find(&a->desc->free_list, &b->free_elem));
list_remove(&b->free_elem);
}
mfree_page(PF, a, 1);
}
}
lock_release(&mem_pool->lock);
}
}