Linux 系统调用是通过中断门来实现的,通过软中断指令 int 来主动发送中断信号。但由于系统调用的功能不止一个两个,所以不可能一个系统调用对应一个中断门描述符,即不可能一个系统调用对应一个中断向量。
因此 Linux 使用了一个中断向量号 0x80
,通过子功能号的形式去调用不同的功能。
查看系统调用帮助信息:
$ man syscall
函数 syscall 并不是由操作系统提供的,它由运行库 glibc 提供的,因此实际上 syscall 是库函数。
查看直接调用帮助信息:
$ man _syscall
Linux 中的系统调用是用寄存器来传递参数,这些参数从左到右的顺序依次存入不同的通用寄存器(除了 ESP)。
eax
:保存子功能号ebx
:保存第 1 个参数ecx
:保存第 2 个参数edx
:保存第 3 个参数esi
:保存第 4 个参数edi
:保存第 5 个参数还有个传入 6 个参数的,略过了。
lib/user/syscall.h:
#ifndef __LIB_USER_SYSCALL_H
#define __LIB_USER_SYSCALL_H#include "stdint.h"enum SYSCALL_NR {SYS_GETPID,SYS_WRITE,SYS_MALLOC,SYS_FREE
};uint32_t getpid(void);
uint32_t write(char* str);
void* malloc(uint32_t size);
void free(void* ptr);#endif
lib/user/syscall.c:
#include "syscall.h"// 无参数的系统调用
#define _syscall0(NUMBER) ({ \int retval; \asm volatile ("int $0x80" : "=a"(retval) : "a"(NUMBER) : "memory"); \retval; \
})// 一个参数的系统调用
#define _syscall1(NUMBER, ARG1) ({ \int retval; \asm volatile ("int $0x80" : "=a"(retval) : "a"(NUMBER), "b"(ARG1) : "memory"); \retval; \
})// 两个参数的系统调用
#define _syscall2(NUMBER, ARG1, ARG2) ({ \int retval; \asm volatile ("int $0x80" : "=a"(retval) : "a"(NUMBER), "b"(ARG1), "c"(ARG2) : "memory"); \retval; \
})// 三个参数的系统调用
#define _syscall3(NUMBER, ARG1, ARG2, ARG3) ({ \int retval; \asm volatile ("int $0x80" : "=a"(retval) : "a"(NUMBER), "b"(ARG1), "c"(ARG2), "d"(ARG3) : "memory"); \retval; \
})// 返回当前任务的 PID
uint32_t getpid() {return _syscall0(SYS_GETPID);
}// 打印字符串
uint32_t write(char* str) {return _syscall1(SYS_WRITE, str);
}// 申请 size 字节大小的内存,并返回结果
void* malloc(uint32_t size) {return (void*) _syscall1(SYS_MALLOC, size);
}// 释放 ptr 指向的内存
void free(void* ptr) {_syscall1(SYS_FREE, ptr);
}
kernel/interrupt.c:
// 初始化中断描述符表
static void idt_desc_init(void) {int i, lastindex = IDT_DESC_CNT - 1;for(i = 0; i < IDT_DESC_CNT; i++) make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);// 单独处理系统调用,系统调用对应的中断门 DPL 为 3// 注:此描述符的 DPL 必须为 3,若为 0,则在 3 特权级环境下执行 int 指令会出现 GP 异常make_idt_desc(&idt[lastindex], IDT_DESC_ATTR_DPL3, syscall_handler);put_str(" idt_desc_init done\n");
}
kernel/kernel.S:
;;;;;;;;;;;;;;;; 0x80号中断 ;;;;;;;;;;;;;;;;
[bits 32]
extern syscall_table
section .text
global syscall_handler
syscall_handler:
; 保护上下文环境 BEGINpush 0 ; 压入 0 假装一下中断错误码,使其格式统一push dspush espush fspush gspushadpush 0x80; 压入参数push edx ; 第三个参数push ecx ; 第二个参数push ebx ; 第一个参数; 调用子功能处理函数call [syscall_table + eax * 4] ; 编译器会在栈中传入的参数去找到正确的重载函数add esp, 12 ; 跨过上面传入的三个参数; 将 call 调用后的返回值存入到 intr_exit 准备要恢复现场的对应 EAX 位置
; 这是为了避免 intr_exit 后 EAX 的值被旧的 EAX 值所还原、覆盖mov [esp + 8 * 4], eaxjmp intr_exit ; 执行 intr_exit 恢复上下文环境
userprog/syscall-init.h:
#ifndef __USERPROG_SYSCALLINIT_H
#define __USERPROG_SYSCALLINIT_H
#include "stdint.h"
void syscall_init(void);
uint32_t sys_getpid(void);
#endif
userprog/syscall-init.c:
#define syscall_nr 32
typedef void* syscall;
syscall syscall_table[syscall_nr]; // 系统调用表// 返回当前任务的 PID
uint32_t sys_getpid(void) {return running_thread() -> pid;
}// 打印字符串(未实现文件系统前的版本)
uint32_t sys_write(char* str) {console_put_str(str);return strlen(str);
}// 初始化系统调用
void syscall_init(void) {put_str("syscall_init start.\n");syscall_table[SYS_GETPID] = sys_getpid;syscall_table[SYS_WRITE] = sys_write;syscall_table[SYS_MALLOC] = sys_malloc;syscall_table[SYS_FREE] = sys_free;put_str("syscall_init done.\n");
}
上面说了堆用于动态内存分配,而函数用的是栈空间,因此编译器必须要要提前知道函数所需要占用的内存大小,但可变参数的参数却可以有很多个,看上去很动态的样子。
可变参数本质上是静态的,这得益于编译器采用 C 调用约定来处理函数的传参方式。
C 调用约定:由调用者把参数从右向左的顺序压入栈中,并且由调用者清理堆栈中的参数。
既然参数由调用者压入,那调用者自然知道有几个参数,以及所占用多少空间,因此采用 C 调用约定,无论有多少参数,调用者都可以确定个数以及所占用空间大小,从而完好的回收栈空间。
也就是说,传入的参数的个数是编译器在编译阶段就以及确定下来的了。
查看 stdarg.h:
man stdarg
va_start、va_end、va_arg
这三个表示可变参数。
查看 stdarg.h 文件:
...
#define va_start(v,l) __builtin_va_start(v,l)
#define va_end(v) __builtin_va_end(v)
#define va_arg(v,l) __builtin_va_arg(v,l)
...
我的路径:/usr/lib/gcc/x86_64-linux-gnu/9/include/stdarg.h
__builtin_va_*
表示内建符号,内建 builtins 表示这个是 gcc 内部实现的功能。
参数:
ap(argument pointer)
:是个指针变量,它指向 format 在栈中的地址。三个宏:
#define va_start(ap, v) ap = (va_list)&v // 把ap指向第一个固定参数v
#define va_arg(ap, t) *((t*)(ap += 4)) // ap指向下一个参数并返回其值
#define va_end(ap) ap = NULL // 清除a
printf 输出靠内部的 write 系统调用,其内容的格式化又靠 vsprintf 函数,因此 printf 只是个壳子。
lib/user/syscall.h:
enum SYSCALL_NR {SYS_WRITE,
};uint32_t write(char* str);
lib/user/syscall.c:
// 打印字符串
uint32_t write(char* str) {return _syscall1(SYS_WRITE, str);
}
userprog/syscall-init.c:
// 初始化系统调用
void syscall_init(void) {syscall_table[SYS_WRITE] = sys_write;
}
lib/stdio.c:
#define va_start(ap, v) ap = (va_list)&v // 把ap指向第一个固定参数v
#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) itoa(i, buf_ptr_addr, base); // 若为0表示已经除到头了、转换完了if(m < 10) *((*buf_ptr_addr)++) = m + '0'; // 若取模得到的余数 < 10,则转换为 '0'~'9' 字符else *((*buf_ptr_addr)++) = m - 10 + 'A'; // 若取模得到的余数 >= 10,则转换为 '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; // 指向 format 字符串某个字符int32_t arg_int;char* arg_str;while(index_char) {if(index_char != '%') { // 处理当前字符不是 % 的情况*(buf_ptr++) = index_char; // 先把字符输出到 str,然后++index_char = *(++index_ptr);// 得到 format 下一个字符,赋值给 index_charcontinue;}index_char = *(++index_ptr); // 得到 % 后的字符,它表示数据类型switch(index_char) {case 's':arg_str = va_arg(ap, char*);strcpy(buf_ptr, arg_str);buf_ptr += strlen(arg_str);index_char = *(++index_ptr);break;case 'c':*(buf_ptr++) = va_arg(ap, char);index_char = *(++index_ptr);break;case 'd':arg_int = va_arg(ap, int);if(arg_int < 0) { // 若为负数,将其转为正数后,在前面添加一个负号'-'arg_int = 0 - arg_int;*buf_ptr++ = '-';}itoa(arg_int, &buf_ptr, 10);index_char = *(++index_ptr);break;case 'x':arg_int = va_arg(ap, int);itoa(arg_int, &buf_ptr, 16);index_char = *(++index_ptr); // 指向 format 的下一个字符break;}}return strlen(str);
}/* 同printf不同的地方就是字符串不是写到终端,而是写到buf中 */
uint32_t sprintf(char* buf, const char* format, ...) {va_list args;uint32_t retval;va_start(args, format);retval = vsprintf(buf, format, args);va_end(args);return retval;
}// 格式化输出字符串 format
uint32_t printf(const char* format, ...) {va_list args;va_start(args, format); // 将 args 指向 formatchar buf[1024] = {0}; // 用于存储拼接后的字符串vsprintf(buf, format, args);va_end(args);return write(buf);
}
lib/stdio.h:
#ifndef __LIB_STDIO_H
#define __LIB_STDIO_H
#include "stdint.h"
typedef char* va_list;
uint32_t printf(const char* str, ...);
uint32_t vsprintf(char* str, const char* format, va_list ap);
#endif
arena 是很多开源项目中都会用到的内存管理概念,将内存划分为多个大内存块,每个大内存块之间互不干涉,可以分别管理,这个大内存块称为 arena。
而大内存块,即 arena 中包含两部分,其中一部分用来划分小内存块,这个小内存块我们称为 mem_block。
本书中一个大内存块为 4KB 粒度,即为一页,当然,大内存块也可以是一页或多页,根据大内存块的容量平均划分小内存块的大小。
arena 是个提供内存分配的数据结构,它分为两部分,一部分是元信息,用来描述自己内存池中空闲内存块的数量,该部分占用 12 字节,另一部分就是内存池区域,里面有很多被划分好的小内存块。每个内存块命名为 mem_block,它们是内存分配粒度更细的资源。
图示:
当一个 arena 中的小内存块,即 mem_block 都分配完了后,若还需要申请空间,则此时需要再次创建一个同一规格的 arena,若同一规格的 arena 越来也多,为了跟踪这些 arena,也就是为了跟踪这些 arena 中的 mem_block,我们需要为每个 arena 的元信息中建立一个内存块描述符,即 mem_block_desc,在其中记录内存块规格大小,以及所有同一规格 arena 中的空闲块(是个链表)。
本书支持了 7 种规格,分别是:16、32、64、128、256、512、1024 字节。超过 1024 字节的直接分配一页空间。
arena 中 mem_block 数量的计算方式:(内存粒度 - 12) / 规格。
例如:(4096 - 12) / 128
arena 是个内存仓库,并不直接对外提供内存分配,只有内存块描述符才对外提供内存块,内存块描述符将同类 arena 中的空闲块汇聚到一起,作为某个同一规格内存块的分配入口。
kernel/memory.c:
// 内存仓库 arena 的元信息
struct arena {struct mem_block_desc* desc; // 此 arena 锁关联的 mem_block_descuint32_t cnt; // 若 large = true,则 cnt 表示页宽数// 若 large = false,则 cnt 表示空闲 mem_block 的数量bool large; // 标识该 arena 到底是管理 1024 字节内的内存(true),如何超过 1024 字节的内存(false)
};struct mem_block_desc k_block_descs[DESC_CNT]; // 内核内存块描述符数组
kernel/memory.h:
// 内存块
struct mem_block {struct list_elem free_elem;
};// 内存块描述符
struct mem_block_desc {uint32_t block_size; // 内存块大小uint32_t blocks_per_arena; // 该 arena 中可容纳多少 block_size 大小的块struct list free_list; // 目前可用的 mem_block 链表
};#define DESC_CNT 7 // 内存块描述符的个数
kernel/menory.c:
// 为 malloc 做准备
void block_desc_init(struct mem_block_desc* desc_array) {uint16_t desc_idx, block_size = 16;for(desc_idx = 0; desc_idx < DESC_CNT; desc_idx++) {desc_array[desc_idx].block_size = block_size;// 初始化 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; // 更新为下一个规格的内存块}}// 内存管理部分的初始化入口
void mem_init() {put_str("mem_init start\n");// 0xb00 是 loader.S 中定义的 mem_bytes_total 存储总内存容量uint32_t mem_bytes_total = (*(uint32_t*)(0xb00)); mem_pool_init(mem_bytes_total); // 初始化内存池block_desc_init(k_block_descs);put_str("mem_init done\n");
}
thread/thread.h:
// 进程或线程的 PCB,即程序控制块
struct task_struct {...struct mem_block_desc u_block_desc[DESC_CNT]; // 用户进程内存块的描述符...
};
userprog/process.c:
void process_execute(void* filename, char* name) {...thread -> pgdir = create_page_dir();block_desc_init(thread -> u_block_desc);...
}
kernel/memory.c:
// 返回 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);
}// 返回内存块 b 所在的 arena 地址
static struct arena* block2arena(struct mem_block* b) {return (struct arena*)((uint32_t)b & 0xFFFFF000);
}// 在堆中申请 size 字节内存
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;}// 若申请的内存不在内存池容量范围内,则直接返回 NULLif(!(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) {lock_release(&mem_pool -> lock);return NULL;}memset(a, 0, page_cnt * PG_SIZE);// 对于分配的大块页框,要初始化如下:a -> desc = NULL; // 没有关联的内存块描述符a -> cnt = page_cnt; // 页框的数量a -> large = true; // 该 arena 表示为页框lock_release(&mem_pool -> lock);return (void*) (a + 1); // 跨过 arena 大小,把剩下的内存将其首地址返回} else {uint8_t desc_idx;// 从内存块描述符中匹配合适的内存块规格for(desc_idx = 0; desc_idx < DESC_CNT; desc_idx++) {if(size <= descs[desc_idx].block_size) break;}// 判断该规格的空闲块是否还有剩余,若无,则进行生产if(list_empty(&descs[desc_idx].free_list)) {a = malloc_page(PF, 1);if(a == NULL) {lock_release(&mem_pool -> lock);return NULL;}memset(a, 0, PG_SIZE);a -> desc = &descs[desc_idx];a -> cnt = descs[desc_idx].blocks_per_arena;a -> large = false;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 所在的 arenaa -> cnt--; // 将此 arena 中的空闲内存块减一lock_release(&mem_pool -> lock);return (void*) b;}
}
内存的使用情况是通过位图来管理的,因此,无论内存的分配或释放,本质上都是在设置相关位图中的相应位。
分配内存:
在虚拟地址内存池中寻找可用虚拟地址,相关函数为 vaddr_get。
操作的位图:kernel_vaddr.vaddr_bitmap 或 pcb->userprog_vaddr.vaddr_bitmap。
在物理内存池中寻找可分配的物理地址,相关函数为 palloc。
操作的位图:kernel_pool->pool_bitmap 或 user_pool->pool_bitmap。
在页表中完成虚拟地址到物理地址的映射,相关函数为 page_table_add。
以上步骤封装到了 malloc_page 中。
释放内存:
// 将物理地址 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);
}// 去掉页表中虚拟地址 vaddr 的映射,只去掉 vaddr 对应的 PTE
static void page_table_pte_remove(uint32_t vaddr) {uint32_t* pte = pte_ptr(vaddr);*pte &= ~PG_P_1; // 将页表项 PTE 的 P 位置为 0asm volatile("invlpg %0" : : "m"(vaddr) : "memory"); // 更新 TLB
}// 在虚拟地址内存池中释放以 _vaddr 起始的连续 pg_cnt 个虚拟页地址
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; // 计算 vaddr 在虚拟内存池中的偏移量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);}
}
// 释放以虚拟地址 vaddr 为起始的 cnt 个物理页信息
void mfree_page(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt) {uint32_t pg_phy_addr;uint32_t vaddr = (int32_t) _vaddr, page_cnt = 0;ASSERT(pg_cnt >= 1 && vaddr % PG_SIZE == 0);pg_phy_addr = addr_v2p(vaddr);// 确保待释放的物理内存 存在低端 1M + 1k 大小的页目录 + 1k 大小的页表地址范围外ASSERT((pg_phy_addr % PG_SIZE) == 0 && pg_phy_addr >= 0x102000);// 判断 pg_phy_addr 属于哪个区域if(pg_phy_addr >= user_pool.phy_addr_start) { // 用户物理内存池vaddr -= PG_SIZE;while(page_cnt < pg_cnt) {vaddr += PG_SIZE;pg_phy_addr = addr_v2p(vaddr);// 确保物理地址属于用户内存池ASSERT((pg_phy_addr % PG_SIZE) == 0 && pg_phy_addr >= user_pool.phy_addr_start);// 先将对应的物理页框规划到内存池pfree(pg_phy_addr);// 再从页表中清除此虚拟地址所在页表项 PTEpage_table_pte_remove(vaddr);//pg_cnt++;page_cnt++;}// 清空虚拟地址位图中相应的位vaddr_remove(pf, _vaddr, pg_cnt);} else { // 内核物理内存池vaddr -= PG_SIZE;while(page_cnt < pg_cnt) {vaddr += PG_SIZE;pg_phy_addr = addr_v2p(vaddr);// 确保待释放的物理内存只属于内核物理内存池ASSERT((pg_phy_addr % PG_SIZE) == 0 && pg_phy_addr >= kernel_pool.phy_addr_start && pg_phy_addr < user_pool.phy_addr_start);// 线将对应的物理页框归还到内存池pfree(pg_phy_addr);// 再从页表中清除此虚拟地址所在的页表项 PTEpage_table_pte_remove(vaddr);page_cnt++;}// 清空虚拟地址位图中的相应位vaddr_remove(pf, _vaddr, pg_cnt);}
}
// 回收内存 ptr
void sys_free(void* 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; // 转为 mem_block 内存块类型指针struct arena* a = block2arena(b); // mem_block 转换到 arenaASSERT(a -> large == 0 || a -> large == 1);if(a -> desc == NULL && a -> large == true) // 要释放的内存大于 1024 字节mfree_page(PF, a, a -> cnt);else { // 要释放的内存小于或等于 1024 字节// 先将内存块回收到 free_listlist_append(&a -> desc -> free_list, &b -> free_elem);// 再判断此 arena 中的内存块是否都是空闲块,若是,则释放 arenaif(++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); // 释放该 arena}}lock_release(&mem_pool -> lock);}
}
lib/user/syscall.h:
enum SYSCALL_NR {SYS_GETPID,SYS_WRITE,SYS_MALLOC,SYS_FREE
};uint32_t getpid(void);
uint32_t write(char* str);
void* malloc(uint32_t size);
void free(void* ptr);
lib/user/syscall.c:
// 申请 size 字节大小的内存,并返回结果
void* malloc(uint32_t size) {return (void*) _syscall1(SYS_MALLOC, size);
}// 释放 ptr 指向的内存
void free(void* ptr) {_syscall1(SYS_FREE, ptr);
}
userprog/syscall-init.c:
// 初始化系统调用
void syscall_init(void) {put_str("syscall_init start.\n");syscall_table[SYS_GETPID] = sys_getpid;syscall_table[SYS_WRITE] = sys_write;syscall_table[SYS_MALLOC] = sys_malloc;syscall_table[SYS_FREE] = sys_free;put_str("syscall_init done.\n");
}