欢迎来到DragonOS的文档!
DragonOS简介
DragonOS龙操作系统(以下简称“DragonOS”)是一个基于x86-64体系结构开发的,基于GPLv2协议开放源代码的64位操作系统
你可能对DragonOS中已经实现了哪些功能感兴趣,您可以转到这里:功能特性
DragonOS的功能
规范
启动引导:Multiboot2
接口:posix 2008
内核层
内存管理
页分配器
slab分配器
VMA
MMIO地址空间自动分配
多核
多核引导
ipi框架
进程管理
进程创建
进程回收
内核线程
fork
exec
进程睡眠(支持高精度睡眠)
kthread机制
同步原语
mutex互斥量
semaphore信号量
atomic原子变量
spinlock自旋锁
wait_queue等待队列
调度
CFS调度器
单核调度
IPC
匿名pipe管道
文件系统
VFS
fat32
devfs
rootfs
异常及中断处理
APIC
softirq 软中断
内核栈traceback
内核数据结构
普通二叉树
kfifo缓冲区
循环链表
IDR
内核实用库
LZ4压缩库(1.9.3)
字符串操作库
ELF可执行文件支持
printk
基础数学库
屏幕管理器
textui框架
CRC函数库
系统调用
测试框架
ktest
驱动程序
ACPI 高级电源配置模块
IDE硬盘
AHCI硬盘
PCI
XHCI(usb3.0)
ps/2 键盘
ps/2 鼠标
HPET高精度定时器
RTC时钟
local apic定时器
UART串口
VBE显示
用户层
LibC
基础系统调用
基础标准库函数
部分数学函数
shell命令行程序
基于字符串匹配的解析
基本的几个命令
驱动程序
ps/2键盘用户态驱动
构建DragonOS
1.写在前面
无论您采用后文中的何种方式来编译DragonOS,您必须先按照本小节中的步骤,初始化您的开发环境。
开始之前,您需要一台运行Linux或MacOS的计算机,并且处理器架构为X86-64.
对于Linux发行版,建议使用Ubuntu22、Debian、Arch Linux这样的,仓库软件版本较新的发行版,这能为您减少很多麻烦。
使用一键初始化脚本进行安装
我们提供了一键初始化脚本,可以一键安装,只需要在控制台运行以下命令:
cd tools
bash bootstrap.sh
备注
一键配置脚本目前只支持以下系统:
Ubuntu/Debian/Deepin/UOS 等基于Debian的衍生版本
欢迎您为其他的系统完善构建脚本!
如果一键初始化脚本能够正常运行,并输出最终的“祝贺”界面,那么恭喜你,可以直接跳到这里进行阅读!
依赖清单(手动安装)
如果自动安装脚本不能支持您的操作系统,那么您需要手动安装依赖程序。以下是依赖项的清单:
在以下依赖项中,除了docker-ce
和Rust及其工具链
以外,其他的都能通过系统自带的包管理器进行安装。关于docker以及rust的安装,请看后文。
docker-ce
llvm-dev
libclang-dev
clang
gcc-multilib
qemu qemu-system qemu-kvm
build-essential
fdisk
lsb-release
git
Rust以及其工具链
请留意,若您的Linux系统是在虚拟机中运行的,还请您在您的VMware/Virtual Box虚拟机的处理器设置选项卡中,开启Intel VT-x或AMD-V选项,否则,DragonOS将无法运行。
备注
在某些Linux发行版的软件仓库中构建的Qemu可能由于版本过低而不兼容DragonOS,如果遇到这种问题,请卸载Qemu,并采用编译安装的方式重新安装Qemu
在该地址下载Qemu源代码: https://download.qemu.org/
解压后进入源代码目录,然后执行下列命令:
# 安装编译依赖项
sudo apt install -y autoconf automake autotools-dev curl libmpc-dev libmpfr-dev libgmp-dev \
gawk build-essential bison flex texinfo gperf libtool patchutils bc \
zlib1g-dev libexpat-dev pkg-config libglib2.0-dev libpixman-1-dev libsdl2-dev \
git tmux python3 python3-pip ninja-build
./configure --enable-kvm
make -j 8
sudo make install
# 编译安装完成
请注意,编译安装的QEMU,将通过VNC模式进行链接,因此,您还需要在您的计算机上安装VNC viewer以连接至QEMU虚拟机。
安装Docker
您可以在docker官网下载安装docker-ce.
安装Rust
警告
【常见误区】:如果您打算采用docker进行编译,尽管docker镜像中已经安装了Rust编译环境,但是,为了能够在VSCode中使用Rust-Analyzer进行代码提示,以及make clean
命令能正常运行,您的客户机上仍然需要安装rust环境。
您可以在控制台输入以下命令,安装rust。
# 这两行用于换源,加速Rust的安装过程
export RUSTUP_DIST_SERVER=https://mirrors.ustc.edu.cn/rust-static
export RUSTUP_UPDATE_ROOT=https://mirrors.ustc.edu.cn/rust-static/rustup
# 安装Rust
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly
# 把Rustup加到环境变量
echo "export PATH=\"\$HOME/.cargo/bin:\$PATH\"" >> ~/.bashrc
source ~/.cargo/env
source "$HOME/.cargo/env"
# 更换cargo的索引源
touch ~/.cargo/config
echo -e "[source.crates-io] \n \
registry = \"https://github.com/rust-lang/crates.io-index\" \n \
\n \
replace-with = 'dragonos-gitee' \n \
[source.dragonos-gitee] \n \
registry = \"https://gitee.com/DragonOS/crates.io-index.git\" \n \
" > ~/.cargo/config
# 安装DragonOS所需的工具链
cargo install cargo-binutils
rustup toolchain install nightly
rustup default nightly
rustup component add rust-src
rustup component add llvm-tools-preview
rustup target add x86_64-unknown-none
# Rust安装完成
解决KVM权限问题
在部分计算机上,可能由于权限问题而无法启动虚拟机,我们可以通过把当前用户加到kvm用户组的方式,解决该问题:
# 解决kvm权限问题
USR=$USER
sudo adduser $USR kvm
sudo chown $USR /dev/kvm
下载DragonOS的源代码
假设您的计算机上已经安装了git,您可以通过以下命令,获得DragonOS的最新的源代码:
git clone https://github.com/fslongjin/DragonOS
cd DragonOS
至此,公共依赖项已经安装完成,您可以根据自己的需要,阅读后续章节
2.从Docker构建(推荐)
为减轻配置环境的负担,DragonOS发布了一个Docker编译环境,便于开发者运行DragonOS。我们强烈建议您采用这种方式来运行DragonOS。
本节假设以下操作均在Linux下进行。
获取DragonOS编译镜像
当您成功安装了docker之后,您可以通过以下命令,下载DragonOS的编译镜像:
docker pull dragonos/dragonos-dev:v1.1.0-beta3
安装qemu虚拟机
在本节中,我们建议您采用命令行安装qemu:
sudo apt install -y qemu qemu-system qemu-kvm
创建磁盘镜像
首先,您需要使用tools文件夹下的create_hdd_image.sh,创建一块虚拟磁盘镜像。您需要在tools文件夹下运行此命令。
bash create_hdd_image.sh
运行DragonOS
如果不出意外的话,这将是运行DragonOS的最后一步。您只需要在DragonOS的根目录下方,执行以下命令,即可运行DragonOS。
make run-docker
稍等片刻,DragonOS将会被运行。
在qemu虚拟机被启动后,我们需要在控制台输入字母c
,然后回车。这样,虚拟机就会开始执行。
备注
首次编译时,由于需要下载Rust相关的索引(几百MB大小),因此需要一定的时间,请耐心等候!
关于编译命令的用法,请见:编译命令讲解
3.在本机中直接编译
若您追求快速的编译速度,以及完整的开发调试支持,且愿意花费半个小时来配置开发环境的话,该小节的内容能帮助到您。
软件依赖
您需要通过以下命令,获取您本机安装的Grub的版本:
grub-install --version
如果显示的版本号为2.06及以上,且您已经按照第一小节中的内容,安装相关的依赖,那么,恭喜您,您可以直接在本机编译DragonOS!
否则,您需要编译安装Grub-2.06。它的源代码可以通过https://ftp.gnu.org/gnu/grub/grub-2.06.tar.gz获得。
grub 2.06 (不必使用sudo权限进行install)
创建磁盘镜像
首先,您需要使用sudo
权限运行tools/create_hdd_image.sh
,为DragonOS创建一块磁盘镜像文件。该脚本会自动完成创建磁盘镜像的工作,并将其移动到bin/
目录下。
编译、运行DragonOS
安装编译及运行环境
进入DragonOS文件夹
输入
make run
即可编译并写入磁盘镜像,并运行
在qemu虚拟机被启动后,我们需要在控制台输入字母c
,然后回车。这样,虚拟机就会开始执行。
备注
首次编译时,由于需要下载Rust相关的索引(几百MB大小),因此需要一定的时间,请耐心等候!
关于编译命令的用法,请见:编译命令讲解
4.编译命令讲解
本地编译,不运行:
make all -j 您的CPU核心数
本地编译,并写入磁盘镜像,不运行:
make build
本地编译,写入磁盘镜像,并在QEMU中运行:
make run
Docker编译,并写入磁盘镜像,:
make docker
Docker编译,写入磁盘镜像,并在QEMU中运行:
make run-docker
不编译,直接从已有的磁盘镜像启动:
make qemu
引导加载
DragonOS采用GRUB2作为其引导加载程序,支持Multiboot2协议引导。目前仅支持GRUB2.06版本。
引导加载程序
原理
目前,DragonOS仅支持Legacy BIOS进行启动引导。
在head.S
的头部包含了Multiboot2引导头,里面标志了一些Multiboot2相关的特定信息,以及一些配置命令。
在DragonOS的启动初期,会存储由GRUB2传来的magic number以及multiboot2_boot_info_addr。当系统进入Start_Kernel
函数之后,将会把这两个信息保存到multiboot2驱动程序之中。信息的具体含义请参照Multiboot2 Specification进行理解,该部分难度不大,相信读者经过思考能理解其中的原理。
未来发展方向
增加对UEFI启动的支持
参考资料
Multiboot2支持模块
Multiboot2支持模块提供对Multiboot2协议的支持。位于kernel/driver/multiboot2
文件夹中。
根据Multiboot2协议,操作系统能够从BootLoader处获得一些信息,比如基本的硬件信息以及ACPI表的起始地址等。
数据结构
kernel/driver/multiboot2/multiboot2.h
中按照Multiboot2协议的规定,定义了大部分的数据结构,具体细节可查看该文件: DragonOS/multiboot2.h at master · fslongjin/DragonOS · GitHub
迭代器
由于Multiboot2的信息存储在自multiboot2_boot_info_addr
开始的一段连续的内存空间之中,且不同类型的header的长度不同,因此设计了一迭代器multiboot2_iter
。
函数原型
void multiboot2_iter(bool (*_fun)(const struct iter_data_t *, void *, unsigned int *),
void *data, unsigned int *count)
_fun
指定的handler。当某个header的tag与该handler所处理的tag相同时,handler将处理该header,并返回true。
其第一个参数为tag类型,第二个参数为返回的数据的指针,第三个值为计数(某些没有用到该值的地方,该值可以为空)
data
传递给_fun
的第二个参数,_fun
函数将填充该指针所指向的内存区域,从而返回数据。
count
当返回的data为一个列表时,通过该值来指示列表中有多少项。
迭代工作函数
在模块中,按照我们需要获取不同类型的tag的需要,定义了一些迭代器工作函数。
核心API文档
这里是DragonOS的核心api文档。
DragonOS内核核心API
循环链表管理函数
循环链表是内核的重要的数据结构之一。包含在kernel/common/list.h
中。
void list_init(struct List *list)
描述
初始化一个List结构体,使其prev和next指针指向自身
参数
list
要被初始化的List结构体
void list_add(struct List *entry, struct List *node)
描述
将node插入到entry的后方
参数
entry
已存在于循环链表中的一个结点
node
待插入的结点
void list_append(struct List *entry, struct List *node)
描述
将node插入到entry的前方
参数
entry
已存在于循环链表中的一个结点
node
待插入的结点
void list_del(struct List *entry)
描述
从链表中删除结点entry
参数
entry
待删除的结点
list_del_init(struct List *entry)
描述
从链表中删除结点entry,并将这个entry使用list_init()进行重新初始化。
参数
entry
待删除的结点
bool list_empty(struct List *entry)
描述
判断链表是否为空
参数
entry
链表中的一个结点
struct List *list_prev(struct List *entry)
描述
获取entry的前一个结点
参数
entry
链表中的一个结点
struct List *list_next(struct List *entry)
描述
获取entry的后一个结点
参数
entry
链表中的一个结点
void list_replace(struct List *old, struct List *new)
描述
将链表中的old结点替换成new结点
参数
old
要被换下来的结点
new
要被换入链表的新的结点
list_entry(ptr, type, member)
描述
该宏能通过ptr指向的List获取到List所处的结构体的地址
参数
ptr
指向List结构体的指针
type
要被换入链表的新的结点
member
List结构体在上述的“包裹list结构体的结构体”中的变量名
list_first_entry(ptr, type, member)
描述
获取链表中的第一个元素。请注意,该宏要求链表非空,否则会出错。
参数
与list_entry()相同
list_first_entry_or_null(ptr, type, member)
描述
获取链表中的第一个元素。若链表为空,则返回NULL。
参数
与list_entry()相同
list_last_entry(ptr, type, member)
描述
获取链表中的最后一个元素。请注意,该宏要求链表非空,否则会出错。
参数
与list_entry()相同
list_last_entry_or_full(ptr, type, member)
描述
获取链表中的最后一个元素。若链表为空,则返回NULL。
参数
与list_entry()相同
list_next_entry(pos, member)
描述
获取链表中的下一个元素
参数
pos
指向当前的外层结构体的指针
member
链表结构体在外层结构体内的变量名
list_prev_entry(pos, member)
描述
获取链表中的上一个元素
参数
list_for_each(ptr, head)
描述
遍历整个链表(从前往后)
参数
ptr
指向List结构体的指针
head
指向链表头结点的指针(struct List*)
list_for_each_prev(ptr, head)
描述
遍历整个链表(从后往前)
参数
list_for_each_safe(ptr, n, head)
描述
从前往后遍历整个链表(支持删除当前链表结点)
该宏通过暂存中间变量,防止在迭代链表的过程中,由于删除了当前ptr所指向的链表结点从而造成错误.
参数
ptr
指向List结构体的指针
n
用于存储临时值的List类型的指针
head
指向链表头结点的指针(struct List*)
list_for_each_prev_safe(ptr, n, head)
描述
从后往前遍历整个链表.(支持删除当前链表结点)
该宏通过暂存中间变量,防止在迭代链表的过程中,由于删除了当前ptr所指向的链表结点从而造成错误.
参数
list_for_each_entry(pos, head, member)
描述
从头开始迭代给定类型的链表
参数
pos
指向特定类型的结构体的指针
head
指向链表头结点的指针(struct List*)
member
struct List在pos的结构体中的成员变量名
list_for_each_entry_reverse(pos, head, member)
描述
逆序迭代给定类型的链表
参数
list_for_each_entry_safe(pos, n, head, member)
描述
从头开始迭代给定类型的链表(支持删除当前链表结点)
参数
pos
指向特定类型的结构体的指针
n
用于存储临时值的,和pos相同类型的指针
head
指向链表头结点的指针(struct List*)
member
struct List在pos的结构体中的成员变量名
list_prepare_entry(pos, head, member)
描述
为list_for_each_entry_continue()准备一个’pos’结构体
参数
pos
指向特定类型的结构体的,用作迭代起点的指针
head
指向要开始迭代的struct List结构体的指针
member
struct List在pos的结构体中的成员变量名
list_for_each_entry_continue(pos, head, member)
描述
从指定的位置的【下一个元素开始】,继续迭代给定的链表
参数
pos
指向特定类型的结构体的指针。该指针用作迭代的指针。
head
指向要开始迭代的struct List结构体的指针
member
struct List在pos的结构体中的成员变量名
list_for_each_entry_continue_reverse(pos, head, member)
描述
从指定的位置的【上一个元素开始】,【逆序】迭代给定的链表
参数
list_for_each_entry_from(pos, head, member)
描述
从指定的位置开始,继续迭代给定的链表
参数
list_for_each_entry_safe_continue(pos, n, head, member)
描述
从指定的位置的【下一个元素开始】,继续迭代给定的链表.(支持删除当前链表结点)
参数
pos
指向特定类型的结构体的指针。该指针用作迭代的指针。
n
用于存储临时值的,和pos相同类型的指针
head
指向要开始迭代的struct List结构体的指针
member
struct List在pos的结构体中的成员变量名
list_for_each_entry_safe_continue_reverse(pos, n, head, member)
描述
从指定的位置的【上一个元素开始】,【逆序】迭代给定的链表。(支持删除当前链表结点)
参数
list_for_each_entry_safe_from(pos, n, head, member)
描述
从指定的位置开始,继续迭代给定的链表.(支持删除当前链表结点)
参数
基础C函数库
内核编程与应用层编程不同,你将无法使用LibC中的函数来进行编程。为此,内核实现了一些常用的C语言函数,并尽量使其与标准C库中的函数行为相近。值得注意的是,这些函数的行为可能与标准C库函数不同,请在使用时仔细阅读以下文档,这将会为你带来帮助。
字符串操作
int strlen(const char *s)
测量并返回字符串长度。
src
源字符串
long strnlen(const char *src, unsigned long maxlen)
测量并返回字符串长度。当字符串长度大于maxlen时,返回maxlen
src
源字符串
maxlen
最大长度
long strnlen_user(const char *src, unsigned long maxlen)
测量并返回字符串长度。当字符串长度大于maxlen时,返回maxlen。
该函数会进行地址空间校验,要求src字符串必须来自用户空间。当源字符串来自内核空间时,将返回0.
src
源字符串,地址位于用户空间
maxlen
最大长度
char *strncpy(char *dst, const char *src, long count)
拷贝长度为count个字节的字符串,返回dst字符串
src
源字符串
dst
目标字符串
count
要拷贝的源字符串的长度
char *strcpy(char *dst, const char *src)
拷贝源字符串,返回dst字符串
src
源字符串
dst
目标字符串
long strncpy_from_user(char *dst, const char *src, unsigned long size)
从用户空间拷贝长度为count个字节的字符串到内核空间,返回拷贝的字符串的大小
该函数会对字符串的地址空间进行校验,防止出现地址空间越界的问题。
src
源字符串
dst
目标字符串
size
要拷贝的源字符串的长度
int strcmp(char *FirstPart, char *SecondPart)
比较两个字符串的大小。
返回值
情况 |
返回值 |
---|---|
FirstPart == SecondPart |
0 |
FirstPart > SecondPart |
1 |
FirstPart < SecondPart |
-1 |
FirstPart
第一个字符串
SecondPart
第二个字符串
printk(const char* fmt, ...)
该宏能够在控制台上以黑底白字格式化输出字符串.
fmt
源格式字符串
…
可变参数
printk_color(unsigned int FRcolor, unsigned int BKcolor, const char* fmt, ...)
在控制台上以指定前景色和背景色格式化输出字符串.
FRcolor
前景色
BKcolor
背景色
fmt
源格式字符串
…
可变参数
int vsprintf(char *buf, const char *fmt, va_list args)
按照fmt格式化字符串,并将结果输出到buf中,返回写入buf的字符数量。
buf
输出缓冲区
fmt
源格式字符串
args
可变参数列表
int sprintk(char *buf, const char *fmt, ...)
按照fmt格式化字符串,并将结果输出到buf中,返回写入buf的字符数量。
buf
输出缓冲区
fmt
源格式字符串
…
可变参数
内存操作
void *memcpy(void *dst, const void *src, uint64_t size)
将内存从src处拷贝到dst处。
dst
指向目标地址的指针
src
指向源地址的指针
size
待拷贝的数据大小
void *memmove(void *dst, const void *src, uint64_t size)
与memcpy()
类似,但是在源数据区域与目标数据区域之间存在重合时,该函数能防止数据被错误的覆盖。
dst
指向目标地址的指针
src
指向源地址的指针
size
待拷贝的数据大小
CRC函数
函数列表
uint8_t crc7(uint8_t crc, const uint8_t *buffer, size_t len)
uint8_t crc8(uint8_t crc, const uint8_t *buffer, size_t len)
uint16_t crc16(uint16_t crc, uint8_t const *buffer, size_t len)
uint32_t crc32(uint32_t crc, uint8_t const *buffer, size_t len)
uint64_t crc64(uint64_t crc, uint8_t const *buffer, size_t len)
描述
用于计算循环冗余校验码
参数说明
crc
传入的CRC初始值
buffer
待处理的数据缓冲区
len
缓冲区大小(字节)
原子变量
简介
DragonOS实现了原子变量,类型为atomic_t. 原子变量是基于具体体系结构的原子操作指令实现的。具体实现在kernel/common/atomic.h
中。
API
请注意,以下API均为原子操作。
inline void atomic_add(atomic_t *ato, long val)
描述
原子变量增加指定值
参数
ato
原子变量对象
val
变量要增加的值
inline void atomic_sub(atomic_t *ato, long val)
描述
原子变量减去指定值
参数
ato
原子变量对象
val
变量要被减去的值
void atomic_inc(atomic_t *ato)
描述
原子变量自增1
参数
ato
原子变量对象
void atomic_dec(atomic_t *ato)
描述
原子变量自减1
参数
ato
原子变量对象
inline void atomic_set_mask(atomic_t *ato, long mask)
描述
将原子变量的值与mask变量进行or运算
参数
ato
原子变量对象
mask
与原子变量进行or运算的变量
inline void atomic_clear_mask(atomic_t *ato, long mask)
描述
将原子变量的值与mask变量进行and运算
参数
ato
原子变量对象
mask
与原子变量进行and运算的变量
内核数据结构
内核中实现了常用的几种数据结构,这里是他们的api文档。
kfifo先进先出缓冲区
kfifo先进先出缓冲区定义于common/kfifo.h
中。您可以使用它,创建指定大小的fifo缓冲区(最大大小为4GB)
kfifo_alloc
int kfifo_alloc(struct kfifo_t *fifo, uint32_t size, uint64_t reserved)
描述
通过动态方式初始化kfifo缓冲队列。fifo缓冲区的buffer将由该函数进行申请。
参数
fifo
kfifo队列结构体的指针
size
缓冲区大小(单位:bytes)
reserved
当前字段保留,请将其置为0
返回值
当返回值为0时,表示正常初始化成功,否则返回对应的errno
kfifo_init
void kfifo_init(struct kfifo_t *fifo, void *buffer, uint32_t size)
描述
使用指定的缓冲区来初始化kfifo缓冲队列
参数
fifo
kfifo队列结构体的指针
buffer
缓冲区基地址指针
size
缓冲区大小(单位:bytes)
kfifo_free_alloc
void kfifo_free_alloc(struct kfifo_t* fifo)
描述
释放通过kfifo_alloc创建的fifo缓冲区. 请勿通过该函数释放其他方式创建的kfifo缓冲区。
参数
fifo
kfifo队列结构体的指针
kfifo_in
uint32_t kfifo_in(struct kfifo_t *fifo, const void *from, uint32_t size)
描述
向kfifo缓冲区推入指定大小的数据。当队列中空间不足时,则不推入数据。
参数
fifo
kfifo队列结构体的指针
from
源数据基地址指针
size
数据大小(单位:bytes)
返回值
返回成功被推入的数据的大小。
kfifo_out
uint32_t kfifo_out(struct kfifo_t *fifo, void *to, uint32_t size)
描述
从kfifo缓冲区取出数据,并从队列中删除数据。当队列中数据量不足时,则不取出。
参数
fifo
kfifo队列结构体的指针
to
目标缓冲区基地址指针
size
数据大小(单位:bytes)
返回值
返回成功被取出的数据的大小。
kfifo_out_peek
uint32_t kfifo_out_peek(struct kfifo_t *fifo, void *to, uint32_t size)
描述
从kfifo缓冲区取出数据,但是不从队列中删除数据。当队列中数据量不足时,则不取出。
参数
fifo
kfifo队列结构体的指针
to
目标缓冲区基地址指针
size
数据大小(单位:bytes)
返回值
返回成功被取出的数据的大小。
kfifo_reset
kfifo_reset(fifo)
描述
忽略kfifo队列中的所有内容,并把输入和输出偏移量都归零
参数
fifo
kfifo队列结构体的指针
kfifo_reset_out
kfifo_reset_out(fifo)
描述
忽略kfifo队列中的所有内容,并将输入偏移量赋值给输出偏移量
参数
fifo
kfifo队列结构体的指针
kfifo_total_size
kfifo_total_size(fifo)
描述
获取kfifo缓冲区的最大大小
参数
fifo
kfifo队列结构体的指针
返回值
缓冲区最大大小
kfifo_size
kfifo_size(fifo)
描述
获取kfifo缓冲区当前已使用的大小
参数
fifo
kfifo队列结构体的指针
返回值
缓冲区当前已使用的大小
kfifo_empty
kfifo_empty(fifo)
描述
判断kfifo缓冲区当前是否为空
参数
fifo
kfifo队列结构体的指针
返回值
情况 |
返回值 |
---|---|
空 |
1 |
非空 |
0 |
kfifo_full
kfifo_full(fifo)
描述
判断kfifo缓冲区当前是否为满
参数
fifo
kfifo队列结构体的指针
返回值
情况 |
返回值 |
---|---|
满 |
1 |
不满 |
0 |
ID Allocation
ida的主要作用是分配+管理id. 它能分配一个最小的, 未被分配出去的id. 当您需要管理某个数据结构时, 可能需要使用id来区分不同的目标. 这个时候, ida将会是很好的选择. 因为ida的十分高效, 运行常数相对数组更小, 而且提供了基本管理id需要用到的功能, 值得您试一试.
IDA定义于idr.h
文件中. 您通过DECLARE_IDA(my_ida)
来创建一个ida对象, 或者struct ida my_ida; ida_init(&my_ida);
来初始化一个ida.
ida_init
void ida_init(struct ida *ida_p)
描述
通初始化IDA, 你需要保证调用函数之前, ida的free_list为空, 否则会导致内存泄漏.
参数
ida_p
指向ida的指针
返回值
无返回值
ida_preload
int ida_preload(struct ida *ida_p, gfp_t gfp_mask)
描述
为ida预分配空间.您可以不自行调用, 因为当ida需要空间的时候, 内部会自行使用kmalloc
函数获取空间. 当然, 设计这个函数的目的是为了让您有更多的选择. 当您提前调用这个函数, 可以避免之后在开辟空间上的时间开销.
参数
ida_p
指向ida的指针
gfp_mask
保留参数, 目前尚未使用.
返回值
如果分配成功,将返回0; 否则返回负数错误码, 有可能是内存空间不够.
ida_alloc
int ida_alloc(struct ida *ida_p, int *p_id)
描述
获取一个空闲ID. 您需要注意, 返回值是成功/错误码.
参数
ida_p
指向ida的指针
p_id
您需要传入一个int变量的指针, 如果成功分配ID, ID将会存储在该指针所指向的地址.
返回值
如果分配成功,将返回0; 否则返回负数错误码, 有可能是内存空间不够.
ida_count
bool ida_count(struct ida *ida_p, int id)
描述
查询一个ID是否被分配.
参数
ida_p
指向ida的指针
id
您查询该ID是否被分配.
返回值
如果分配,将返回true; 否则返回false.
ida_remove
void ida_remove(struct ida *ida_p, int id)
描述
删除一个已经分配的ID. 如果该ID不存在, 该函数不会产生异常错误, 因为在检测到该ID不存在的时候, 函数将会自动退出.
参数
ida_p
指向ida的指针
id
您要删除的id.
返回值
无返回值.
ida_destroy
void ida_destroy(struct ida *ida_p)
描述
释放一个IDA所有的空间, 同时删除ida的所有已经分配的id.(所以您不用担心删除id之后, ida还会占用大量空间.)
参数
ida_p
指向ida的指针
返回值
无返回值
ida_empty
void ida_empty(struct ida *ida_p)
描述
查询一个ida是否为空
参数
ida_p
指向ida的指针
返回值
ida为空则返回true,否则返回false。
IDR
idr是一个基于radix-tree的ID-pointer的数据结构. 该数据结构提供了建id与数据指针绑定的功能, 它的主要功能有以下4个:
获取一个ID, 并且将该ID与一个指针绑定
删除一个已分配的ID
根据ID查找对应的指针
根据ID使用新的ptr替换旧的ptr
您可以使用DECLARE_idr(my_idr)
来创建一个idr。或者您也可以使用struct idr my_idr; idr_init(my_idr);
这两句话创建一个idr。 至于什么是radix-tree,您可以把他简单理解为一个向上生长的多叉树,在实现中,我们选取了64叉树。
idr_init
void idr_init(struct idr *idp)
描述
通初始化IDR, 你需要保证调用函数之前, idr的free_list为空, 否则会导致内存泄漏.
参数
idp
指向idr的指针
返回值
无返回值
idr_preload
int idr_preload(struct idr *idp, gfp_t gfp_mask)
描述
为idr预分配空间.您可以不自行调用, 因为当idr需要空间的时候, 内部会自行使用kmalloc
函数获取空间. 当然, 设计这个函数的目的是为了让您有更多的选择. 当您提前调用这个函数, 可以避免之后在开辟空间上的时间开销.
参数
idp
指向idr的指针
gfp_mask
保留参数, 目前尚未使用.
返回值
如果分配成功,将返回0; 否则返回负数错误码, 有可能是内存空间不够.
idr_alloc
int idr_alloc(struct idr *idp, void *ptr, int *id)
描述
获取一个空闲ID. 您需要注意, 返回值是成功/错误码.
调用这个函数,需要您保证ptr是非空的,即: ptr != NULL
, 否则将会影响 idr_find/idr_find_next/idr_find_next_getid/...
等函数的使用。(具体请看这三个函数的说明,当然,只会影响到您的使用体验,并不会影响到idr内部函数的决策和逻辑)
参数
idp
指向ida的指针
ptr
指向数据的指针
id
您需要传入一个int变量的指针, 如果成功分配ID, ID将会存储在该指针所指向的地址.
返回值
如果分配成功,将返回0; 否则返回负数错误码, 有可能是内存空间不够.
idr_remove
void* idr_remove(struct idr *idp, int id)
描述
删除一个id, 但是不释放对应的ptr指向的空间, 同时返回这个被删除id所对应的ptr。 如果该ID不存在, 该函数不会产生异常错误, 因为在检测到该ID不存在的时候, 函数将会自动退出,并返回NULL。
参数
idp
指向idr的指针
id
您要删除的id.
返回值
如果删除成功,就返回被删除id所对应的ptr;否则返回NULL。注意:如果这个id本来就和NULL绑定,那么也会返回NULL
idr_remove_all
void idr_remove_all(struct idr *idp)
描述
删除idr的所有已经分配的id.(所以您不用担心删除id之后, idr还会占用大量空间。)
但是你需要注意的是,调用这个函数是不会释放数据指针指向的空间的。 所以您调用该函数之前, 确保IDR内部的数据指针被保存。否则当IDR删除所有ID之后, 将会造成内存泄漏。
参数
idp
指向idr的指针
返回值
无返回值
idr_destroy
void idr_destroy(struct idr *idp)
描述
释放一个IDR所有的空间, 同时删除idr的所有已经分配的id.(所以您不用担心删除id之后, ida还会占用大量空间.) - 和idr_remove_all
的区别是, 释放掉所有的空间(包括free_list的预分配空间)。
参数
idp
指向idr的指针
返回值
无返回值
idr_find
void *idr_find(struct idr *idp, int id)
描述
查询一个ID所绑定的数据指针
参数
idp
指向idr的指针
id
您查询该ID的数据指针
返回值
如果分配,将返回该ID对应的数据指针; 否则返回NULL.(注意, 返回NULL不一定代表这ID不存在,有可能该ID就是与空指针绑定。)
当然,我们也提供了idr_count
函数来判断id是否被分配,具体请查看idr_count介绍。
idr_find_next
void *idr_find_next(struct idr *idp, int start_id)
描述
传进一个start_id,返回满足 “id大于start_id的最小id” 所对应的数据指针。
参数
idp
指向idr的指针
start_id
您提供的ID限制
返回值
如果分配,将返回该ID对应的数据指针; 否则返回NULL.(注意, 返回NULL不一定代表这ID不存在,有可能该ID就是与空指针绑定。)
当然,我们也提供了idr_count
函数来判断id是否被分配,具体请查看idr_count介绍。
idr_find_next_getid
void *idr_find_next_getid(struct idr *idp, int start_id, int *nextid)
描述
传进一个start_id,返回满足 “id大于start_id的最小id” 所对应的数据指针。同时,你获取到这个满足条件的最小id, 即参数中的 *nextid。
参数
idp
指向idr的指针
start_id
您提供的ID限制
返回值
如果分配,将返回该ID对应的数据指针; 否则返回NULL.(注意, 返回NULL不一定代表这ID不存在,有可能该ID就是与空指针绑定。)
当然,我们也提供了idr_count
函数来判断id是否被分配,具体请查看idr_count介绍。
idr_replace
int idr_replace(struct idr *idp, void *ptr, int id)
描述
传进一个ptr,使用该ptr替换掉id所对应的Old_ptr。
参数
idp
指向idr的指针
ptr
您要替换原来的old_ptr的新指针
id
您要替换的指针所对应的id
返回值
0代表成功,否则就是错误码 - 代表错误。
idr_replace_get_old
int idr_replace_get_old(struct idr *idp, void *ptr, int id, void **oldptr)
描述
传进一个ptr,使用该ptr替换掉id所对应的Old_ptr,同时你可以获取到old_ptr。
参数
idp
指向idr的指针
ptr
您要替换原来的old_ptr的新指针
id
您要替换的指针所对应的id
old_ptr
您需要传进该(void**)指针,old_ptr将会存放在该指针所指向的地址。
返回值
0代表成功,否则就是错误码 - 代表错误。
idr_empty
void idr_empty(struct idr *idp)
描述
查询一个idr是否为空
参数
idp
指向idr的指针
返回值
idr为空则返回true,否则返回false。
idr_count
bool idr_count(struct idr *idp, int id)
描述
查询一个ID是否被分配.
参数
ida_p
指向idr的指针
id
您查询该ID是否被分配.
返回值
如果分配,将返回true; 否则返回false.
内存管理
这里快速讲解了如何在DragonOS中分配、使用内存。以便您能快速的了解这个模块。
详细的内存管理模块的文档请参见:内存管理文档
内存分配指南
DragonOS提供了一些用于内存分配的api。您可以使用kmalloc来分配小的内存块,也可以使用alloc_pages分配连续的2MB大小的内存页面。
选择合适的内存分配器
在内核中,最直接、最简单的分配内存的方式就是,使用kmalloc()
函数进行分配。并且,出于安全起见,除非内存在分配后一定会被覆盖,且您能确保内存中的脏数据一定不会对程序造成影响,在其余情况下,我们建议使用kzalloc()
进行内存分配,它将会在kmalloc()
的基础上,把申请到的内存进行清零。
您可以通过kmalloc()
函数分配得到32bytes到1MBytes之间的内存对象。并且,这些内存对象具有以下的性质:
内存起始地址及大小按照2次幂对齐。(比如,申请的是80bytes的内存空间,那么获得的内存对象大小为128bytes且内存地址按照128bytes对齐)
对于需要大量连续内存的分配,可以使用alloc_pages()
向页面分配器申请连续的内存页。
当内存空间不再被使用时,那么必须释放他们。若您使用的是kmalloc()
分配的内存,那么您需要使用kfree()
释放它。若是使用alloc_pages()
分配的内存,则需要使用free_pages()
来释放它们。
内存管理API
SLAB内存池
SLAB内存池提供小内存对象的分配功能。
void *kmalloc(unsigned long size, gfp_t gfp)
获取小块的内存。
kmalloc用于获取那些小于2M内存页大小的内存对象。可分配的内存对象大小为32bytes~1MBytes. 且分配的内存块大小、起始地址按照2的n次幂进行对齐。(比如,申请的是80bytes的内存空间,那么获得的内存对象大小为128bytes且内存地址按照128bytes对齐)
size
内存对象的大小
gfp
标志位
void *kzalloc(unsigned long size, gfp_t gfp)
获取小块的内存,并将其清零。其余功能与kmalloc相同。
size
内存对象的大小
gfp
标志位
unsigned long kfree(void *address)
释放从slab分配的内存。
该函数用于释放通过kmalloc申请的内存。如果address
为NULL,则函数被调用后,无事发生。
请不要通过这个函数释放那些不是从kmalloc()
或kzalloc()
申请的内存,否则将会导致系统崩溃。
address
指向内存对象的起始地址的指针
物理页管理
DragonOS支持对物理页的直接操作
struct Page *alloc_pages(unsigned int zone_select, int num, ul flags)
从物理页管理单元中申请一段连续的物理页
zone_select
要申请的物理页所位于的内存区域
可选值:
ZONE_DMA
DMA映射专用区域ZONE_NORMAL
正常的物理内存区域,已在页表高地址处映射ZONE_UNMAPPED_IN_PGT
尚未在页表中映射的区域
num
要申请的连续物理页的数目,该值应当小于64
flags
分配的页面要被设置成的属性
可选值:
PAGE_PGT_MAPPED
页面在页表中已被映射PAGE_KERNEL_INIT
内核初始化所占用的页PAGE_DEVICE
设备MMIO映射的内存PAGE_KERNEL
内核层页PAGE_SHARED
共享页
成功申请则返回指向起始页面的Page结构体的指针
当ZONE错误或内存不足时,返回NULL
void free_pages(struct Page *page, int number)
从物理页管理单元中释放一段连续的物理页。
page
要释放的第一个物理页的Page结构体
number
要释放的连续内存页的数量。该值应小于64
页表管理
int mm_map_phys_addr(ul virt_addr_start, ul phys_addr_start, ul length, ul flags, bool use4k)
将一段物理地址映射到当前页表的指定虚拟地址处
virt_addr_start
虚拟地址的起始地址
phys_addr_start
物理地址的起始地址
length
要映射的地址空间的长度
flags
页表项的属性
use4k
使用4级页表,将地址区域映射为若干4K页
int mm_map_proc_page_table(ul proc_page_table_addr, bool is_phys, ul virt_addr_start, ul phys_addr_start, ul length, ul flags, bool user, bool flush, bool use4k)
将一段物理地址映射到指定页表的指定虚拟地址处
proc_page_table_addr
指定的顶层页表的起始地址
is_phys
该顶层页表地址是否为物理地址
virt_addr_start
虚拟地址的起始地址
phys_addr_start
物理地址的起始地址
length
要映射的地址空间的长度
flags
页表项的属性
user
页面是否为用户态可访问
flush
完成映射后,是否刷新TLB
use4k
使用4级页表,将地址区域映射为若干4K页
映射成功:0
映射失败:-EFAULT
void mm_unmap_proc_table(ul proc_page_table_addr, bool is_phys, ul virt_addr_start, ul length)
取消给定页表中的指定地址空间的页表项映射。
proc_page_table_addr
指定的顶层页表的基地址
is_phys
该顶层页表地址是否为物理地址
virt_addr_start
虚拟地址的起始地址
length
要取消映射的地址空间的长度
mm_unmap_addr(virt_addr, length)
该宏定义用于取消当前进程的页表中的指定地址空间的页表项映射。
virt_addr
虚拟地址的起始地址
length
要取消映射的地址空间的长度
内存信息获取
struct mm_stat_t mm_stat()
获取计算机目前的内存空间使用情况
无
返回值是一个mm_mstat_t
结构体,该结构体定义于mm/mm.h
中。其中包含了以下信息(单位均为字节):
参数名 |
解释 |
---|---|
total |
计算机的总内存数量大小 |
used |
已使用的内存大小 |
free |
空闲物理页所占的内存大小 |
shared |
共享的内存大小 |
cache_used |
位于slab缓冲区中的已使用的内存大小 |
cache_free |
位于slab缓冲区中的空闲的内存大小 |
available |
系统总空闲内存大小(包括kmalloc缓冲区) |
锁
这里是DragonOS的锁变量的说明文档。
锁的类型及其规则
简介
DragonOS内核实现了一些锁,大致可以分为两类:
休眠锁
自旋锁
锁的类型
休眠锁
休眠锁只能在可抢占的上下文之中被获取。
在DragonOS之中,实现了以下的休眠锁:
semaphore
mutex_t
自旋锁
spinlock_t
进程在获取自旋锁后,将改变pcb中的锁变量持有计数,从而隐式地禁止了抢占。为了获得更多灵活的操作,spinlock还提供了以下的方法:
后缀 |
说明 |
---|---|
_irq() |
在加锁时关闭中断/在放锁时开启中断 |
_irqsave()/_irqrestore() |
在加锁时保存中断状态,并关中断/在放锁时恢复中断状态 |
当您同时需要使用自旋锁以及引用计数时,一个好的方法是:使用lockref
. 这是一种额外的加速技术,能额外提供“无锁修改引用计数”的功能。详情请见:lockref
详细介绍
semaphore信号量
semaphore信号量是基于计数实现的。
当可用资源不足时,尝试对semaphore执行down操作的进程将会被休眠,直到资源可用。
mutex互斥量
mutex是一种轻量级的同步原语,只有0和1两种状态。
当mutex被占用时,尝试对mutex进行加锁操作的进程将会被休眠,直到资源可用。
特性
同一时间只有1个任务可以持有mutex
不允许递归地加锁、解锁
只允许通过mutex的api来操作mutex
在硬中断、软中断中不能使用mutex
数据结构
mutex定义在common/mutex.h
中。其数据类型如下所示:
typedef struct
{
atomic_t count; // 锁计数。1->已解锁。 0->已上锁,且有可能存在等待者
spinlock_t wait_lock; // mutex操作锁,用于对mutex的list的操作进行加锁
struct List wait_list; // Mutex的等待队列
} mutex_t;
API
void mutex_init(mutex_t *lock)
初始化一个mutex对象。
void mutex_lock(mutex_t *lock)
对一个mutex对象加锁。若mutex当前被其他进程持有,则当前进程进入休眠状态。
void mutex_unlock(mutex_t *lock)
对一个mutex对象解锁。若mutex的等待队列中有其他的进程,则唤醒下一个进程。
void mutex_trylock(mutex_t *lock)
尝试对一个mutex对象加锁。若mutex当前被其他进程持有,则返回0.否则,加锁成功,返回1.
void mutex_is_locked(mutex_t *lock)
判断mutex是否已被加锁。若给定的mutex已处于上锁状态,则返回1,否则返回0。
lockref
lockref是将自旋锁与引用计数变量融合在连续、对齐的8字节内的一种技术。
lockref结构
struct lockref
{
union
{
#ifdef __LOCKREF_ENABLE_CMPXCHG__
aligned_u64 lock_count; // 通过该变量的声明,使得整个lockref的地址按照8字节对齐
#endif
struct
{
spinlock_t lock;
int count;
};
};
};
特性描述
由于在高负载的情况下,系统会频繁的执行“锁定-改变引用变量-解锁”的操作,这期间很可能出现spinlock和引用计数跨缓存行的情况,这将会大大降低性能。lockref通过强制对齐,尽可能的降低缓存行的占用数量,使得性能得到提升。
并且,在x64体系结构下,还通过cmpxchg()指令,实现了无锁快速路径。不需要对自旋锁加锁即可更改引用计数的值,进一步提升性能。当快速路径不存在(对于未支持的体系结构)或者尝试超时后,将会退化成“锁定-改变引用变量-解锁”的操作。此时由于lockref强制对齐,只涉及到1个缓存行,因此性能比原先的spinlock+ref_count的模式要高。
关于cmpxchg_loop
在改变引用计数时,cmpxchg先确保没有别的线程持有锁,然后改变引用计数,同时通过lock cmpxchg
指令验证在更改发生时,没有其他线程持有锁,并且当前的目标lockref的值与old变量中存储的一致,从而将新值存储到目标lockref。这种无锁操作能极大的提升性能。如果不符合上述条件,在多次尝试后,将退化成传统的加锁方式来更改引用计数。
参考资料
进程管理模块
kthread 内核线程
内核线程模块定义在common/kthread.h
中,提供对内核线程的及支持功能。内核线程作为内核的“分身”,能够提升系统的并行化程度以及故障容错能力。
原理
每个内核线程都运行在内核态,执行其特定的任务。
内核线程的创建是通过调用kthread_create()
或者kthread_run()
宏,向kthreadd
守护线程发送创建任务来实现的。也就是说,内核线程的创建,最终是由kthreadd
来完成。
当内核线程被创建后,虽然会加入调度队列,但是当其被第一次调度,执行引导程序kthread()
后,将进入休眠状态。直到其他模块使用process_wakeup()
,它才会真正开始运行。
当内核其他模块想要停止一个内核线程的时候,可以调用kthread_stop()
函数。该函数将会置位内核线程的worker_private
中的KTHREAD_SHOULD_STOP
标志位,并等待内核线程的退出,然后获得返回值并清理内核线程的pcb。
内核线程应当经常检查KTHREAD_SHOULD_STOP
标志位,以确定其是否要退出。当检测到该标志位被置位时,内核线程应当完成数据清理工作,并调用kthread_exit()
或直接返回一个返回码,以退出内核线程。
创建内核线程
kthread_create()
原型
kthread_create(thread_fn, data, name_fmt, arg...)
简介
在当前NUMA结点上创建一个内核线程(DragonOS目前暂不支持NUMA,因此node可忽略。)
请注意,该宏会创建一个内核线程,并将其设置为停止状态.
参数
thread_fn
该内核线程要执行的函数
data
传递给 thread_fn 的参数数据
name_fmt
printf-style format string for the thread name
arg
name_fmt的参数
返回值
创建好的内核线程的pcb
kthread_run()
原型
kthread_run(thread_fn, data, name_fmt, ...)
简介
创建内核线程并加入调度队列。
该宏定义是kthread_create()
的简单封装,提供创建了内核线程后,立即运行的功能。
停止内核线程
kthread_stop()
原型
int kthread_stop(struct process_control_block * pcb)
简介
当外部模块希望停止一个内核线程时,调用该函数,向kthread发送停止消息,请求其结束。并等待其退出,返回内核线程的退出返回值。
参数
pcb
内核线程的pcb
返回值
内核线程的退出返回码。
kthread_should_stop()
原型
bool kthread_should_stop(void)
简介
内核线程可以调用该函数得知是否有其他进程请求结束当前内核线程。
返回值
一个bool变量
值 |
解释 |
---|---|
true |
有其他进程请求结束该内核线程 |
false |
该内核线程没有收到停止消息 |
kthread_exit()
原型
void kthread_exit(long result)
简介
让当前内核线程退出,并返回result参数给kthread_stop()函数。
参数
result
内核线程的退出返回码
PCB 进程控制块
PCB的全称为process control block, 它是每个进程/线程的核心控制结构。定义于kernel/src/process/proc-types.h
中。
PCB详解
Todo:
与PCB的管理相关的API
根据pid寻找pcb
process_find_pcb_by_pid
该API提供了根据pid寻找pcb的功能,定义在kernel/src/process/process.h
中。
当找到目标的pcb时,返回对应的pcb,否则返回NULL。
参数
pid 进程id
返回值
struct process_control_block 目标pcb
DragonOS调度
这里是DragonOS中,与进程调度相关的说明文档。
与“等待”相关的api
如果几个进程需要等待某个事件发生,才能被运行,那么就需要一种“等待”的机制,以实现进程同步。
一. wait_queue等待队列
wait_queue是一种进程同步机制,中文名为“等待队列”。它可以将当前进程挂起,并在时机成熟时,由另一个进程唤醒他们。
当您需要等待一个事件完成时,使用wait_queue机制能减少进程同步的开销。相比于滥用自旋锁以及信号量,或者是循环使用usleep(1000)这样的函数来完成同步,wait_queue是一个高效的解决方案。
警告
wait_queue.h
中的等待队列的实现并没有把队列头独立出来,同时没有考虑为等待队列加锁。所以在后来的开发中加入了wait_queue_head_t
的队列头实现,实质上就是链表+自旋锁。它与wait_queue.h
中的队列wait_queue_node_t
是兼容的,当你使用struct wait_queue_head
作为队列头时,你同样可以使用等待队列添加节点的函数。
简单用法
等待队列的使用方法主要分为以下几部分:
创建并初始化一个等待队列
使用
wait_queue_sleep_on_
系列的函数,将当前进程挂起。晚挂起的进程将排在队列的尾部。通过
wait_queue_wakeup()
函数,依次唤醒在等待队列上的进程,将其加入调度队列
要使用wait_queue,您需要#include<common/wait_queue.h>
,并创建一个wait_queue_node_t
类型的变量,作为等待队列的头部。这个结构体只包含两个成员变量:
typedef struct
{
struct List wait_list;
struct process_control_block *pcb;
} wait_queue_node_t;
对于等待队列,这里有一个好的命名方法:
wait_queue_node_t wq_keyboard_interrupt_received;
这样的命名方式能增加代码的可读性,更容易让人明白这里到底在等待什么。
初始化等待队列
函数wait_queue_init(wait_queue_node_t *wait_queue, struct process_control_block *pcb)
提供了初始化wait_queue结点的功能。
当您初始化队列头部时,您仅需要将wait_queue首部的结点指针传入,第二个参数请设置为NULL
将结点插入等待队列
您可以使用以下函数,将当前进程挂起,并插入到指定的等待队列。这些函数大体功能相同,只是在一些细节上有所不同。
函数名 |
解释 |
---|---|
wait_queue_sleep_on() |
将当前进程挂起,并设置挂起状态为PROC_UNINTERRUPTIBLE |
wait_queue_sleep_on_unlock() |
将当前进程挂起,并设置挂起状态为PROC_UNINTERRUPTIBLE。待当前进程被插入等待队列后,解锁给定的自旋锁 |
wait_queue_sleep_on_interriptible() |
将当前进程挂起,并设置挂起状态为PROC_INTERRUPTIBLE |
从等待队列唤醒一个进程
您可以使用void wait_queue_wakeup(wait_queue_node_t * wait_queue_head, int64_t state);
函数,从指定的等待队列中,唤醒第一个挂起时的状态与指定的state
相同的进程。
当没有符合条件的进程时,将不会唤醒任何进程,仿佛无事发生。
二. wait_queue_head等待队列头
数据结构定义如下:
typedef struct
{
struct List wait_list;
spinlock_t lock; // 队列需要有一个自旋锁,虽然目前内部并没有使用,但是以后可能会用.
} wait_queue_head_t;
等待队列头的使用逻辑与等待队列实际是一样的,因为他同样也是等待队列的节点(仅仅多了一把锁)。且wait_queue_head的函数基本上与wait_queue一致,只不过多了***_with_node_***的字符串。
同时,wait_queue.h文件中提供了很多的宏,可以方便您的工作。
提供的宏
宏 |
解释 |
---|---|
DECLARE_WAIT_ON_STACK(name, pcb) |
在栈上声明一个wait_queue节点,同时把pcb所代表的进程与该节点绑定 |
DECLARE_WAIT_ON_STACK_SELF(name) |
传在栈上声明一个wait_queue节点,同时当前进程(即自身进程)与该节点绑定 |
DECLARE_WAIT_ALLOC(name, pcb) |
使用 |
DECLARE_WAIT_ALLOC_SELF(name) |
使用 |
创建等待队列头
您可以直接调用宏
DECLARE_WAIT_QUEUE_HEAD(m_wait_queue_head); // 在栈上声明一个队列头变量
也可以手动声明
struct wait_queue_head_t m_wait_queue_head = {0};
wait_queue_head_init(&m_wait_queue_head);
将结点插入等待队列
函数名 |
解释 |
---|---|
wait_queue_sleep_with_node(wait_queue_head_t *head, wait_queue_node_t *wait_node) |
传入一个等待队列节点,并设置该节点的挂起状态为PROC_UNINTERRUPTIBLE |
wait_queue_sleep_with_node_unlock(wait_queue_head_t *q, wait_queue_node_t *wait, void *lock) |
传入一个等待队列节点,将该节点的pcb指向的进程挂起,并设置挂起状态为PROC_UNINTERRUPTIBLE。待当前进程被插入等待队列后,解锁给定的自旋锁 |
wait_queue_sleep_with_node_interriptible(wait_queue_head_t *q, wait_queue_node_t *wait) |
传入一个等待队列节点,将该节点的pcb指向的进程挂起,并设置挂起状态为PROC_INTERRUPTIBLE |
从等待队列唤醒一个进程
在wait_queue.h
中的wait_queue_wakeup
函数直接kfree掉了wait_node节点。对于在栈上的wait_node,您可以选择wait_queue_wakeup_on_stack(wait_queue_head_t *q, int64_t state)
来唤醒队列里面的队列头节点。
三. completion完成量
简单用法
完成量的使用方法主要分为以下几部分:
声明一个完成量(可以在栈中/使用kmalloc/使用数组)
使用wait_for_completion等待事件完成
使用complete唤醒等待的进程
等待操作
void wait_fun() {
DECLARE_COMPLETION_ON_STACK(comp); // 声明一个completion
// .... do somethind here
// 大部分情况是你使用kthread_run()创建了另一个线程
// 你需要把comp变量传给这个线程, 然后当前线程就会等待他的完成
if (!try_wait_for_completion(&comp)) // 进入等待
wait_for_completion(&comp);
}
完成操作
void kthread_fun(struct completion *comp) {
// ...... 做一些事 .......
// 这里你确定你完成了目标事件
complete(&comp);
// 或者你使用complete_all
complete_all(&comp);
}
更多用法
kernel/sched/completion.c文件夹中,你可以看到 __test 开头的几个函数,他们是completion模块的测试代码,基本覆盖了completion的大部分函数.你可以在这里查询函数使用方法.
初始化完成量
函数completion_init(struct completion *x)
提供了初始化completion的功能。当你使用DECLARE_COMPLETION_ON_STACK
来创建(在栈上创建)的时候,会自动初始化.
关于完成量的wait系列函数
函数名 |
解释 |
---|---|
wait_for_completion(struct completion *x) |
将当前进程挂起,并设置挂起状态为PROC_UNINTERRUPTIBLE。 |
wait_for_completion_timeout(struct completion *x, long timeout) |
将当前进程挂起,并设置挂起状态为PROC_UNINTERRUPTIBLE。当等待timeout时间(jiffies时间片)之后,自动唤醒进程。 |
wait_for_completion_interruptible(struct completion *x) |
将当前进程挂起,并设置挂起状态为PROC_INTERRUPTIBLE。 |
wait_for_completion_interruptible_timeout(struct completion *x, long timeout) |
将当前进程挂起,并设置挂起状态为PROC_INTERRUPTIBLE。当等待timeout时间(jiffies时间片)之后,自动唤醒进程。 |
wait_for_multicompletion(struct completion x[], int n) |
将当前进程挂起,并设置挂起状态为PROC_UNINTERRUPTIBLE。(等待数组里面的completion的完成) |
关于完成量的complete系列函数
函数名 |
解释 |
---|---|
complete(struct completion *x) |
表明一个事件被完成,从等待队列中唤醒一个进程 |
complete_all(struct completion *x) |
表明与该completion有关的事件被标记为永久完成,并唤醒等待队列中的所有进程 |
其他用于查询信息的函数
函数名 |
解释 |
---|---|
completion_done(struct completion *x) |
查询completion的done变量是不是大于0,如果大于0,返回true;否则返回false。在等待前加上这个函数有可能加速?(暂未经过实验测试,有待证明) |
try_wait_for_completion(struct completion *x) |
查询completion的done变量是不是大于0,如果大于0,返回true(同时令done-=1);否则返回false。在等待前加上这个函数有可能加速?(该函数和 |
内存管理文档
这里讲解了内存管理模块的一些设计及实现原理。
如果你正在寻找使用内存管理模块的方法,请转到:内存管理API文档
MMIO
MMIO是“内存映射IO”的缩写,它被广泛应用于与硬件设备的交互之中。
地址空间管理
DragonOS中实现了MMIO地址空间的管理机制,本节将介绍它们。
为什么需要MMIO地址空间自动分配?
由于计算机上的很多设备都需要MMIO的地址空间,而每台计算机上所连接的各种设备的对MMIO地址空间的需求是不一样的。如果我们为每个类型的设备都手动指定一个MMIO地址,会使得虚拟地址空间被大大浪费,也会增加系统的复杂性。并且,我们在将来还需要为不同的虚拟内存区域做异常处理函数。因此,我们需要一套能够自动分配MMIO地址空间的机制。
这套机制提供了什么功能?
为驱动程序分配4K到1GB的MMIO虚拟地址空间
对于这些虚拟地址空间,添加到VMA中进行统一管理
可以批量释放这些地址空间
这套机制是如何实现的?
这套机制本质上是使用了伙伴系统来对MMIO虚拟地址空间进行维护。在mm/mm.h
中指定了MMIO的虚拟地址空间范围,这个范围是0xffffa10000000000
开始的1TB的空间。也就是说,这个伙伴系统为MMIO维护了这1TB的虚拟地址空间。
地址空间分配过程
初始化MMIO-mapping模块,在mmio的伙伴系统中创建512个1GB的
__mmio_buddy_addr_region
驱动程序使用
mmio_create
请求分配地址空间。mmio_create
对申请的地址空间大小按照2的n次幂进行对齐,然后从buddy中申请内存地址空间创建VMA,并将VMA标记为
VM_IO|VM_DONTCOPY
。MMIO的vma只绑定在initial_mm
下,且不会被拷贝。分配完成
一旦MMIO地址空间分配完成,它就像普通的vma一样,可以使用mmap系列函数进行操作。
MMIO的映射过程
在得到了虚拟地址空间之后,当我们尝试往这块地址空间内映射内存时,我们可以调用mm_map
函数,对这块区域进行映射。
该函数会对MMIO的VMA的映射做出特殊处理。即:创建Page
结构体以及对应的anon_vma
. 然后会将对应的物理地址,填写到页表之中。
MMIO虚拟地址空间的释放
当设备被卸载时,驱动程序可以调用mmio_release
函数对指定的mmio地址空间进行释放。
释放的过程中,mmio_release
将执行以下流程:
取消mmio区域在页表中的映射。
将释放MMIO区域的VMA
将地址空间归还给mmio的伙伴系统。
文件系统
DragonOS的文件系统模块由VFS(虚拟文件系统)及具体的文件系统组成。
VFS虚拟文件系统
在DragonOS中,VFS作为适配器,遮住了具体文件系统之间的差异,对外提供统一的文件操作接口抽象。
DragonOS虚拟文件系统概述
简介
DragonOS的虚拟文件系统是内核中的一层适配器,为用户程序(或者是系统程序)提供了通用的文件系统接口。同时对内核中的不同文件系统提供了统一的抽象。各种具体的文件系统可以挂载到VFS的框架之中。
与VFS相关的系统调用有open(), read(), write(), create()等。
dentry对象
dentry的全称为directory entry,是VFS中对于目录项的一种抽象数据结构。当读取具体文件系统时,将会由创建dentry对象。dentry对象中包含了指向inode的指针。
dentry对象为真实文件系统上的目录结构建立了缓存,一旦内存中存在对应路径的dentry对象,我们就能直接获取其中的信息,而不需要进行费时的磁盘操作。请注意,dentry只是为提高文件系统性能而创建的一个缓存,它并不会被写入到磁盘之中。
inode对象
inode的全称叫做index node,即索引节点。一般来说,每个dentry都应当包含指向其inode的指针。inode是VFS提供的对文件对象的抽象。inode中的信息是从具体文件系统中读取而来,也可以被刷回具体的文件系统之中。并且,一个inode也可以被多个dentry所引用。
要查找某个路径下的inode,我们需要调用父目录的inode的lookup()方法。请注意,该方法与具体文件系统有关,需要在具体文件系统之中实现。
文件描述符对象
当一个进程试图通过VFS打开某个文件时,我们需要为这个进程创建文件描述符对象。每个文件对象都会绑定文件的dentry和文件操作方法结构体,还有文件对象的私有信息。
文件描述符对象中还包含了诸如权限控制、当前访问位置信息等内容,以便VFS对文件进行操作。
我们对文件进行操作都会使用到文件描述符,具体来说,就是要调用文件描述符之中的file_ops所包含的各种方法。
注册文件系统到VFS
如果需要注册或取消注册某个具体文件系统到VFS之中,则需要以下两个接口:
#include<filesystem/VFS/VFS.h>
uint64_t vfs_register_filesystem(struct vfs_filesystem_type_t *fs);
uint64_t vfs_unregister_filesystem(struct vfs_filesystem_type_t *fs);
这里需要通过struct vfs_filesystem_type_t
来描述具体的文件系统。
struct vfs_filesystem_type_t
这个数据结构描述了具体文件系统的一些信息。当我们挂载具体文件系统的时候,将会调用它的read_superblock方法,以确定要被挂载的文件系统的具体信息。
该数据结构的定义在kernel/filesystem/VFS/VFS.h
中,结构如下:
struct vfs_filesystem_type_t
{
char *name;
int fs_flags;
// 解析文件系统引导扇区的函数,为文件系统创建超级块结构。其中DPTE为磁盘分区表entry(MBR、GPT不同)
struct vfs_superblock_t *(*read_superblock)(void *DPTE, uint8_t DPT_type, void *buf, int8_t ahci_ctrl_num, int8_t ahci_port_num, int8_t part_num);
struct vfs_filesystem_type_t *next;
};
name
文件系统名称字符串
fs_flags
文件系统的一些标志位。目前,DragonOS尚未实现相关功能。
read_superblock
当新的文件系统实例将要被挂载时,将会调用此方法,以读取具体的实例的信息。
next
指向链表中下一个struct vfs_filesystem_type_t
的指针。
超级块(superblock)对象
一个超级块对象代表了一个被挂载到VFS中的具体文件系统。
struct vfs_superblock_t
该数据结构为超级块结构体。
该数据结构定义在kernel/filesystem/VFS/VFS.h
中,结构如下:
struct vfs_superblock_t
{
struct vfs_dir_entry_t *root;
struct vfs_super_block_operations_t *sb_ops;
void *private_sb_info;
};
root
该具体文件系统的根目录的dentry
sb_ops
该超级块对象的操作方法。
private_sb_info
超级块的私有信息。包含了具体文件系统的私有的、全局性的信息。
struct vfs_super_block_operations_t
该数据结构为超级块的操作接口。VFS通过这些接口来操作具体的文件系统的超级块。
该数据结构定义在kernel/filesystem/VFS/VFS.h
中,结构如下:
struct vfs_super_block_operations_t
{
void (*write_superblock)(struct vfs_superblock_t *sb);
void (*put_superblock)(struct vfs_superblock_t *sb);
void (*write_inode)(struct vfs_index_node_t *inode); // 将inode信息写入磁盘
};
write_superblock
将superblock中的信息写入磁盘
put_superblock
释放超级块
write_inode
将inode的信息写入磁盘
索引结点(inode)对象
每个inode对象代表了具体的文件系统之中的一个对象(目录项)。
struct vfs_index_node_t
该数据结构为inode对象的数据结构,与文件系统中的具体的文件结点对象具有一对一映射的关系。
该数据结构定义在kernel/filesystem/VFS/VFS.h
中,结构如下:
struct vfs_index_node_t
{
uint64_t file_size; // 文件大小
uint64_t blocks; // 占用的扇区数
uint64_t attribute;
struct vfs_superblock_t *sb;
struct vfs_file_operations_t *file_ops;
struct vfs_inode_operations_t *inode_ops;
void *private_inode_info;
};
file_size
文件的大小。若为文件夹,则该值为文件夹内所有文件的大小总和(估计值)。
blocks
文件占用的磁盘块数(扇区数)
attribute
inode的属性。可选值如下:
VFS_IF_FILE
VFS_IF_DIR
VFS_IF_DEVICE
sb
指向文件系统超级块的指针
file_ops
当前文件的操作接口
inode_ops
当前inode的操作接口
private_inode_info
与具体文件系统相关的inode信息。该部分由具体文件系统实现,包含该inode在具体文件系统之中的特定格式信息。
struct vfs_inode_operations_t
该接口为inode的操作方法接口,由具体文件系统实现。并与具体文件系统之中的inode相互绑定。
该接口定义于kernel/filesystem/VFS/VFS.h
中,结构如下:
struct vfs_inode_operations_t
{
long (*create)(struct vfs_index_node_t *parent_inode, struct vfs_dir_entry_t *dest_dEntry, int mode);
struct vfs_dir_entry_t *(*lookup)(struct vfs_index_node_t *parent_inode, struct vfs_dir_entry_t *dest_dEntry);
long (*mkdir)(struct vfs_index_node_t *inode, struct vfs_dir_entry_t *dEntry, int mode);
long (*rmdir)(struct vfs_index_node_t *inode, struct vfs_dir_entry_t *dEntry);
long (*rename)(struct vfs_index_node_t *old_inode, struct vfs_dir_entry_t *old_dEntry, struct vfs_index_node_t *new_inode, struct vfs_dir_entry_t *new_dEntry);
long (*getAttr)(struct vfs_dir_entry_t *dEntry, uint64_t *attr);
long (*setAttr)(struct vfs_dir_entry_t *dEntry, uint64_t *attr);
};
create
在父节点下,创建一个新的inode,并绑定到dest_dEntry上。
该函数的应当被sys_open()
系统调用在使用了O_CREAT
选项打开文件时调用,从而创建一个新的文件。请注意,传递给create()函数的dest_dEntry
参数不应包含一个inode,也就是说,inode对象应当被具体文件系统所创建。
lookup
当VFS需要在父目录中查找一个inode的时候,将会调用lookup方法。被查找的目录项的名称将会通过dest_dEntry传给lookup方法。
若lookup方法找到对应的目录项,将填充完善dest_dEntry对象。否则,返回NULL。
mkdir
该函数被mkdir()系统调用所调用,用于在inode下创建子目录,并将子目录的inode绑定到dEntry对象之中。
rmdir
该函数被rmdir()系统调用所调用,用于删除给定inode下的子目录项。
rename
该函数被rename系统调用(尚未实现)所调用,用于将给定的目录项重命名。
getAttr
用来获取目录项的属性。
setAttr
用来设置目录项的属性
VFS API文档
FAT32文件系统
简介
FAT32文件系统是一种相对简单的文件系统。
FAT32文件系统实现在kernel/filesystem/fat32/
中。
相关数据结构
struct fat32_BootSector_t
fat32启动扇区结构体
struct fat32_BootSector_t
{
uint8_t BS_jmpBoot[3]; // 跳转指令
uint8_t BS_OEMName[8]; // 生产厂商名
uint16_t BPB_BytesPerSec; // 每扇区字节数
uint8_t BPB_SecPerClus; // 每簇扇区数
uint16_t BPB_RsvdSecCnt; // 保留扇区数
uint8_t BPB_NumFATs; // FAT表数量
uint16_t BPB_RootEntCnt; // 根目录文件数最大值
uint16_t BPB_TotSec16; // 16位扇区总数
uint8_t BPB_Media; // 介质描述符
uint16_t BPB_FATSz16; // FAT12/16每FAT扇区数
uint16_t BPB_SecPerTrk; // 每磁道扇区数
uint16_t BPB_NumHeads; // 磁头数
uint32_t BPB_HiddSec; // 隐藏扇区数
uint32_t BPB_TotSec32; // 32位扇区总数
uint32_t BPB_FATSz32; // FAT32每FAT扇区数
uint16_t BPB_ExtFlags; // 扩展标志
uint16_t BPB_FSVer; // 文件系统版本号
uint32_t BPB_RootClus; // 根目录起始簇号
uint16_t BPB_FSInfo; // FS info结构体的扇区号
uint16_t BPB_BkBootSec; // 引导扇区的备份扇区号
uint8_t BPB_Reserved0[12];
uint8_t BS_DrvNum; // int0x13的驱动器号
uint8_t BS_Reserved1;
uint8_t BS_BootSig; // 扩展引导标记
uint32_t BS_VolID; // 卷序列号
uint8_t BS_VolLab[11]; // 卷标
uint8_t BS_FilSysType[8]; // 文件系统类型
uint8_t BootCode[420]; // 引导代码、数据
uint16_t BS_TrailSig; // 结束标志0xAA55
} __attribute__((packed));
struct fat32_FSInfo_t
该扇区存储了FAT32文件系统的一些参考信息。
struct fat32_FSInfo_t
{
uint32_t FSI_LeadSig;
uint8_t FSI_Reserved1[480];
uint32_t FSI_StrucSig;
uint32_t FSI_Free_Count;
uint32_t FSI_Nxt_Free;
uint8_t FSI_Reserved2[12];
uint32_t FSI_TrailSig;
} __attribute__((packed));
FSI_LeadSig
FS info扇区标志符 数值为0x41615252
FSI_Reserved1
保留使用,全部置为0
FSI_StrucSig
FS_Info扇区的另一个标志符,数值为0x61417272
FSI_Free_Count
上一次记录的空闲簇数量,这是一个参考值
FSI_Nxt_Free
空闲簇的起始搜索位置,这是为驱动程序提供的参考值.
FSI_Reserved2 保留使用,全部置为0
FSI_TrailSig
FS_Info扇区结束标志,数值为0xaa550000
struct fat32_Directory_t
短目录项结构体。
struct fat32_Directory_t
{
unsigned char DIR_Name[11];
unsigned char DIR_Attr;
unsigned char DIR_NTRes;
unsigned char DIR_CrtTimeTenth;
unsigned short DIR_CrtTime;
unsigned short DIR_CrtDate;
unsigned short DIR_LastAccDate;
unsigned short DIR_FstClusHI;
unsigned short DIR_WrtTime;
unsigned short DIR_WrtDate;
unsigned short DIR_FstClusLO;
unsigned int DIR_FileSize;
} __attribute__((packed));
DIR_Name
目录项名称。前8bytes为基础名,后3bytes为扩展名
DIRAttr
目录项属性。可选值有如下:
ATTR_READ_ONLY
ATTR_HIDDEN
ATTR_SYSTEM
ATTR_VOLUME_ID
ATTR_DIRECTORY
ATTR_ARCHIVE
ATTR_LONG_NAME
DIR_NTRes
该项为Windows下特有的表示区域,通过该项的值,表示基础名和扩展名的大小写情况。该项的值为EXT|BASE
组合而成,其中,具有以下定义:
BASE:LowerCase(8),UpperCase(0) EXT:LowerCase(16),UpperCase(0)
DIR_CrtTimeTenth
文件创建的毫秒级时间戳
DIR_CrtTime
文件创建时间
DIR_CrtDate
文件创建日期
DIR_LastAccDate
文件的最后访问日期
DIR_FstClusHI
文件起始簇号(高16bit)
DIR_WrtTime
最后写入时间
DIR_WrtDate
最后写入日期
DIR_FstClusLO
文件起始簇号(低16bit)
DIR_FileSize 文件大小
struct fat32_partition_info_t
该数据结构为FAT32分区的信息结构体,并不实际存在于物理磁盘上。这个结构体在挂载文件系统时被创建,作为文件系统的超级块的私有信息的一部分。
struct fat32_inode_info_t
该结构体是VFS的inode结构体的私有信息部分的具体实现。
已知问题
对目录项名称的检查没有按照标准严格实现
当磁盘可用簇数量发生改变时,未更新FS_Info扇区
未填写目录项的时间字段
TODO
完全实现VFS定义的文件接口
性能优化
参考资料
rootFS 根文件系统
rootFS是DragonOS开启后挂载的第一个文件系统,它是一个基于内存的伪文件系统。rootfs的功能主要是在具体的磁盘文件系统被挂载之前,为其他的伪文件系统提供挂载点,使得系统能被正确的初始化。
rootfs的初始化将与VFS一同初始化。rootfs将为系统的各项文件系统的挂载创建dentry,使得其他的文件系统如devfs
等,能在磁盘文件系统被挂载之前被正确的初始化。
当磁盘根文件系统被挂载后,将调用rootfs_umount()
函数。该函数将会把原本挂载在rootfs上的各种伪文件系统迁移到磁盘根文件系统上。当迁移完成后,将会释放rootfs所占用的资源。
devFS 设备文件系统
devfs是一种基于内存的伪文件系统,设备可注册到devfs中。对上层模块及应用程序而言,每个设备都是可操作的文件。
原理
由于每个设备被抽象为文件,因此对于驱动程序而言,只需实现文件的操作接口,上层的应用程序以及其他系统组件即可操作文件操作接口来控制硬件设备。
目录结构
按照设备的主类型的不同,将多种设备放置在devfs的不同文件夹下.请注意,同一设备可以出现在不同的文件夹下。
char
字符设备block
块设备usb
usb设备stdio等设备放置在devfs的根目录下
设备注册
驱动程序可使用devfs_register_device()
函数将设备注册到devfs之中。
设备卸载
【尚未实现】
内核调试模块
这里是DragonOS的内核调试模块文档。
内核栈traceback
简介
内核栈traceback的功能位于kernel/debug/traceback/
文件夹中。为内核态提供traceback的功能,打印调用栈到屏幕上。
API
void traceback(struct pt_regs * regs)
作用
该接口定义于kernel/debug/traceback/traceback.h
中,将会对给定内核栈进行traceback,并打印跟踪结果到屏幕上。
参数
要开始追踪的第一层内核栈栈帧(也就是栈的底端)
实现原理
当内核第一次链接之后,将会通过Makefile中的命令,运行kernel/debug/kallsyms
程序,提取内核文件的符号表,然后生成kernel/debug/kallsyms.S
。该文件的rodata段中存储了text段的函数的符号表。接着,该文件将被编译为kallsyms.o
。最后,Makefile中再次调用ld
命令进行链接,将kallsyms.o链接至内核文件。
当调用traceback
函数时,其将遍历该符号表,找到对应的符号并输出。
未来发展方向
增加写入到日志文件的功能
内核测试
本章节将介绍如何测试内核,包括手动测试以及自动测试。
我们需要尽可能的对内核进行完善的测试,以便我们能够更好的保证内核的稳定性,且减少其他模块的debug难度。
设置完善的测试用例能帮助我们尽可能的检测到问题,防止我们在写新的模块的时候,被已有的模块的一些藏得很深的bug“背刺一刀”。
由于您难以借助GDB等工具进行调试,因此在内核中进行手动测试比应用程序测试要困难一些。
对于一些模块,我们可以使用编写代码进行单元测试,并输出异常信息。遗憾的是,并非所有模块都可以进行单元测试。比如我们常见的内存管理、进程管理等模块都不能进行单元测试。
内核测试框架
DragonOS提供了一个测试框架,旨在对内核的一些模块进行自动化测试。内核测试框架位于ktest/
下。
我们可以使用这个测试框架,按照规范编写测试代码,然后在合适的地方使用ktest_start()
创建一个全新的内核线程并发起测试。
使用方法
创建自动测试程序
假如您要对kfifo模块进行自动测试,您可以在ktest/
下,创建一个名为test-kfifo.c
的测试文件,并编写Makefile。
在test-kfifo.c
中,包含ktest_utils.h
和ktest.h
这两个头文件。
您需要像下面这样,在test-kfifo.c
中,创建一个测试用例函数表,并把测试用例函数填写到其中:
static ktest_case_table kt_kfifo_func_table[] = {
ktest_kfifo_case0_1,
};
然后创建一个函数,作为kfifo测试的主函数。请注意,您需要将它的声明添加到ktest.h
中。
uint64_t ktest_test_kfifo(uint64_t arg)
{
kTEST("Testing kfifo...");
for (int i = 0; i < sizeof(kt_kfifo_func_table) / sizeof(ktest_case_table); ++i)
{
kTEST("Testing case %d", i);
kt_kfifo_func_table[i](i, 0);
}
kTEST("kfifo Test done.");
return 0;
}
编写测试用例
您可以创建一个或多个测试用例,命名为:ktest_kfifo_case_xxxxx
. 在这个例子中,我创建了一个测试用例,命名为:ktest_kfifo_case0_1
.如下所示:
static long ktest_kfifo_case0_1(uint64_t arg0, uint64_t arg1)
这里最多允许我们传递两个参数到测试函数里面。
那么,我们该如何编写测试用例呢?
我们主要是需要设置一些情节,以便能测试到目标组件的每个情况。为了检验模块的行为是否符合预期,我们需要使用assert(condition)
宏函数,对目标condition
进行校验。若condition
为1,则表明测试通过。否则,将会输出一行assert failed信息到屏幕上。
发起测试
我们可以在pid≥1的内核线程中发起测试。由于DragonOS目前尚不完善,您可以在process/process.c
中的initial_kernel_thread()
函数内,发起内核自动测试。具体的代码如下:
ktest_start(ktest_test_kfifo, 0);
这样就发起了一个内核测试,它会创建一个新的内核线程进行自动测试,您不必担心第一个内核线程会被阻塞。
API文档
ktest_start
pid_t ktest_start(uint64_t (*func)(uint64_t arg), uint64_t arg)
描述
开启一个新的内核线程以进行测试
参数
func
测试函数. 新的测试线程将会执行该函数,以进行测试。
arg
传递给测试函数的参数
返回值
测试线程的pid
assert
#define assert(condition)
描述
判定condition是否为1,若不为1,则输出一行错误日志信息:
[ kTEST FAILED ] Ktest Assertion Failed, file:%s, Line:%d
kTEST
#define kTEST(...)
描述
格式化输出一行以[ kTEST ] file:%s, Line:%d
开头的日志信息。
ktest_case_table
typedef long (*ktest_case_table)(uint64_t arg0, uint64_t arg1)
描述
ktest用例函数的类型定义。
处理器架构
该部分文档提供了和处理器架构相关的一些编程实现细节的描述。
LibC文档
简介
LibC是连接用户程序和操作系统的纽带,LibC为应用程序提供了一系列标准库函数。应用程序可以通过DragonOS的LibC,快速地与操作系统进行交互。 DragonOS的LibC主要依照POSIX 2008规范实现,与Linux下的glibC具有相似之处。
API文档
ctype.h
函数列表(这里只列出已实现的函数):
``int isprint(int c)`` : 传入一个字符,判断是否可以被输出
``int islower(int c)`` : 传入一个字符,判断是否是小写字母
``int isupper(int c)`` : 传入一个字符,判断是否是大写字母
``int isalpha(int c)`` : 传入一个字符,判断是否是字母
``int isdigit(int c)`` : 传入一个字符,判断是否是数字
``int toupper(int c)`` : 传入一个小写字母字符,返回这个字母的大写形式
``int tolower(int c)`` : 传入一个大写字母字符,返回这个字母的小写形式
``int isspace(int c)`` : 传入一个字符,判断是否是空白字符
宏定义:
### 暂无用处
``#define _U 01``
``#define _L 02``
``#define _N 04``
``#define _S 010``
``#define _P 020``
``#define _C 040``
``#define _X 0100``
``#define _B 0200``
dirent.h
简介
与文件夹有关的头文件。
结构体列表:
``struct DIR`` :
变量列表:
``int fd`` : 文件夹id(不推荐修改)
``int buf_pos`` : 文件夹缓冲区指针的位置
``int buf_len`` : 文件夹缓冲区的大小(默认为256)
``struct dirent`` :
变量列表:
``ino_t(see libc/sys/types.h) ino`` : 文件序列号(不推荐修改)
``off_t d_off`` : dir偏移量(不推荐修改)
``unsigned short d_reclen`` : 文件夹中的记录数
``unsigned char d_type`` : 目标的类型(有可能是文件,文件夹,磁盘)
``char d_name[]`` : 目标的名字
函数列表(这里只列出已实现的函数):
``DIR opendir(const char *path)``
传入文件夹的路径,返回文件夹结构体
``int closedir(DIR *dirp)``
传入文件夹结构体,关闭文件夹,释放内存
若失败,返回-1
``dirent readdir(DIR *dir)``
传入文件夹结构体,读入文件夹里的内容,并打包为dirent结构体返回
宏定义:
文件夹类型:
``#define VFS_IF_FILE (1UL << 0)``
``#define VFS_IF_DIR (1UL << 1)``
``#define VFS_IF_DEVICE (1UL << 2)``
缓冲区长度的默认值
``#define DIR_BUF_SIZE 256``
errno.h
简介:
共享错误号码
属性:
``extern int errno`` : 通用错误代码
宏定义(复制自代码,了解即可):
#define E2BIG 1 /* 参数列表过长,或者在输出buffer中缺少空间 或者参数比系统内建的最大值要大 Argument list too long.*/
#define EACCES 2 /* 访问被拒绝 Permission denied*/
#define EADDRINUSE 3 /* 地址正在被使用 Address in use.*/
#define EADDRNOTAVAIL 4 /* 地址不可用 Address not available.*/
#define EAFNOSUPPORT 5 /* 地址family不支持 Address family not supported.*/
#define EAGAIN 6 /* 资源不可用,请重试。 Resource unavailable, try again (may be the same value as [EWOULDBLOCK]).*/
#define EALREADY 7 /* 连接已经在处理 Connection already in progress.*/
#define EBADF 8 /* 错误的文件描述符 Bad file descriptor.*/
#define EBADMSG 9 /* 错误的消息 Bad message.*/
#define EBUSY 10 /* 设备或资源忙 Device or resource busy.*/
#define ECANCELED 11 /* 操作被取消 Operation canceled.*/
#define ECHILD 12 /* 没有子进程 No child processes.*/
#define ECONNABORTED 13 /* 连接已断开 Connection aborted.*/
#define ECONNREFUSED 14 /* 连接被拒绝 Connection refused.*/
#define ECONNRESET 15 /* 连接被重置 Connection reset.*/
#define EDEADLK 16 /* 资源死锁将要发生 Resource deadlock would occur.*/
#define EDESTADDRREQ 17 /* 需要目标地址 Destination address required.*/
#define EDOM 18 /* 数学参数超出作用域 Mathematics argument out of domain of function.*/
#define EDQUOT 19 /* 保留使用 Reserved*/
#define EEXIST 20 /* 文件已存在 File exists.*/
#define EFAULT 21 /* 错误的地址 Bad address*/
#define EFBIG 22 /* 文件太大 File too large.*/
#define EHOSTUNREACH 23 /* 主机不可达 Host is unreachable.*/
#define EIDRM 24 /* 标志符被移除 Identifier removed.*/
#define EILSEQ 25 /* 不合法的字符序列 Illegal byte sequence.*/
#define EINPROGRESS 26 /* 操作正在处理 Operation in progress.*/
#define EINTR 27 /* 被中断的函数 Interrupted function.*/
#define EINVAL 28 /* 不可用的参数 Invalid argument.*/
#define EIO 29 /* I/O错误 I/O error.*/
#define EISCONN 30 /* 套接字已连接 Socket is connected.*/
#define EISDIR 31 /* 是一个目录 Is a directory*/
#define ELOOP 32 /* 符号链接级别过多 Too many levels of symbolic links.*/
#define EMFILE 33 /* 文件描述符的值过大 File descriptor value too large.*/
#define EMLINK 34 /* 链接数过多 Too many links.*/
#define EMSGSIZE 35 /* 消息过大 Message too large.*/
#define EMULTIHOP 36 /* 保留使用 Reserved.*/
#define ENAMETOOLONG 37 /* 文件名过长 Filename too long.*/
#define ENETDOWN 38 /* 网络已关闭 Network is down.*/
#define ENETRESET 39 /* 网络连接已断开 Connection aborted by network.*/
#define ENETUNREACH 40 /* 网络不可达 Network unreachable.*/
#define ENFILE 41 /* 系统中打开的文件过多 Too many files open in system.*/
#define ENOBUFS 42 /* 缓冲区空间不足 No buffer space available.*/
#define ENODATA 43 /* 队列头没有可读取的消息 No message is available on the STREAM head read queue.*/
#define ENODEV 44 /* 没有指定的设备 No such device.*/
#define ENOENT 45 /* 没有指定的文件或目录 No such file or directory.*/
#define ENOEXEC 46 /* 可执行文件格式错误 Executable file format error.*/
#define ENOLCK 47 /* 没有可用的锁 No locks available.*/
#define ENOLINK 48 /* 保留 Reserved.*/
#define ENOMEM 49 /* 没有足够的空间 Not enough space.*/
#define ENOMSG 50 /* 没有期待类型的消息 No message of the desired type.*/
#define ENOPROTOOPT 51 /* 协议不可用 Protocol not available.*/
#define ENOSPC 52 /* 设备上没有空间 No space left on device.*/
#define ENOSR 53 /* 没有STREAM资源 No STREAM resources.*/
#define ENOSTR 54 /* 不是STREAM Not a STREAM*/
#define ENOSYS 55 /* 功能不支持 Function not supported.*/
#define ENOTCONN 56 /* 套接字未连接 The socket is not connected.*/
#define ENOTDIR 57 /* 不是目录 Not a directory.*/
#define ENOTEMPTY 58 /* 目录非空 Directory not empty.*/
#define ENOTRECOVERABLE 59 /* 状态不可覆盖 State not recoverable.*/
#define ENOTSOCK 60 /* 不是一个套接字 Not a socket.*/
#define ENOTSUP 61 /* 不被支持 Not supported (may be the same value as [EOPNOTSUPP]).*/
#define ENOTTY 62 /* 不正确的I/O控制操作 Inappropriate I/O control operation.*/
#define ENXIO 63 /* 没有这样的设备或地址 No such device or address.*/
#define EOPNOTSUPP 64 /* 套接字不支持该操作 Operation not supported on socket (may be the same value as [ENOTSUP]).*/
#define EOVERFLOW 65 /* 数值过大,产生溢出 Value too large to be stored in data type.*/
#define EOWNERDEAD 66 /* 之前的拥有者挂了 Previous owner died.*/
#define EPERM 67 /* 操作不被允许 Operation not permitted.*/
#define EPIPE 68 /* 断开的管道 Broken pipe.*/
#define EPROTO 69 /* 协议错误 Protocol error.*/
#define EPROTONOSUPPORT 70 /* 协议不被支持 Protocol not supported.*/
#define EPROTOTYPE 71 /* 对于套接字而言,错误的协议 Protocol wrong type for socket.*/
#define ERANGE 72 /* 结果过大 Result too large.*/
#define EROFS 73 /* 只读的文件系统 Read-only file system.*/
#define ESPIPE 74 /* 错误的寻道 Invalid seek.*/
#define ESRCH 75 /* 没有这样的进程 No such process.*/
#define ESTALE 76 /* 保留 Reserved.*/
#define ETIME 77 /* 流式ioctl()超时 Stream ioctl() timeout*/
#define ETIMEDOUT 78 /* 连接超时 Connection timed out.*/
#define ETXTBSY 79 /* 文本文件忙 Text file busy.*/
#define EWOULDBLOCK 80 /* 操作将被禁止 Operation would block (may be the same value as [EAGAIN]).*/
#define EXDEV 81 /* 跨设备连接 Cross-device link.*/
fcntl.h
简介
文件操作
函数列表:
``int open(const char * path,int options, ...)``
传入文件路径,和文件类型(详细请看下面的宏定义),将文件打开并返回文件id。
宏定义(粘贴自代码,了解即可):
#define O_RDONLY 00000000 // Open Read-only
#define O_WRONLY 00000001 // Open Write-only
#define O_RDWR 00000002 // Open read/write
#define O_ACCMODE 00000003 // Mask for file access modes
#define O_CREAT 00000100 // Create file if it does not exist
#define O_EXCL 00000200 // Fail if file already exists
#define O_NOCTTY 00000400 // Do not assign controlling terminal
#define O_TRUNC 00001000 // 文件存在且是普通文件,并以O_RDWR或O_WRONLY打开,则它会被清空
#define O_APPEND 00002000 // 文件指针会被移动到文件末尾
#define O_NONBLOCK 00004000 // 非阻塞式IO模式
#define O_EXEC 00010000 // 以仅执行的方式打开(非目录文件)
#define O_SEARCH 00020000 // Open the directory for search only
#define O_DIRECTORY 00040000 // 打开的必须是一个目录
#define O_NOFOLLOW 00100000 // Do not follow symbolic links
math.h
简介:
数学库
函数列表:
``double fabs(double x)`` : 返回 x 的绝对值
``float fabsf(float x)`` : 返回 x 的绝对值
``long double fabsl(long double x)``: 返回 x 的绝对值
``double round(double x)`` 四舍五入 x
``float roundf(float x)`` 四舍五入 x
``long double roundl(long double x)`` 四舍五入 x
``int64_t pow(int64_t x,int y)`` 返回 x 的 y 次方
stdio.h
简介:
向标准输入输出里操作
函数列表:
``int64_t put_string(char *str, uint64_t front_color, uint64_t bg_color)``
输出字符串(带有前景色,背景色)
``int printf(const char *fmt, ...)``
就是正常的 ``printf``
``int sprintf(char *buf,const char *fmt,...)```
就是正常的 ``sprintf``
``int vsprintf(char *buf,const char *fmt,va_list args)``
格式化,不建议调用,请用 printf 或 sprintf 替代。
宏定义
### 字体颜色的宏定义
``#define COLOR_WHITE 0x00ffffff //白``
``#define COLOR_BLACK 0x00000000 //黑``
``#define COLOR_RED 0x00ff0000 //红``
``#define COLOR_ORANGE 0x00ff8000 //橙``
``#define COLOR_YELLOW 0x00ffff00 //黄``
``#define COLOR_GREEN 0x0000ff00 //绿``
``#define COLOR_BLUE 0x000000ff //蓝``
``#define COLOR_INDIGO 0x0000ffff //靛``
``#define COLOR_PURPLE 0x008000ff //紫``
### 无需使用
``#define SEEK_SET 0 /* Seek relative to start-of-file */``
``#define SEEK_CUR 1 /* Seek relative to current position */``
``#define SEEK_END 2 /* Seek relative to end-of-file */``
``#define SEEK_MAX 3``
printf.h
不建议引用,需要 printf
函数请引用 stdio.h
stddef.h
简介:
定义了关于指针的常用类型
定义:
``typedef __PTDIFF_TYPE__ ptrdiff_t`` : 两个指针相减的结果类型
``NULL ((void *) 0)`` : 空指针
stdlib.h
简介:
一些常用函数
函数列表:
``void *malloc(ssize_t size)`` : 普通的 ``malloc``
``void free(void *ptr)`` : 释放内存
``int abs(int x)`` : x 的绝对值
``long labs(long x)`` : x 的绝对值
``long long llabs(long long x)`` : x 的绝对值
``int atoi(const char *str)`` 字符串转数字
``void exit(int status)`` : 普通的 ``exit``
string.h
简介:
字符串操作
函数列表:
``size_t strlen(const char *s)`` : 返回字符串长度
``int strcmp(const char *a,const char *b)`` 比较字符串的字典序
``char* strncpy(char *dst,const char *src,size_t count)``
拷贝制定字节数的字符串
dst: 目标地址
src: 原字符串
count: 字节数
``char* strcpy(char *dst,const char *src)`` : 复制整个字符串
``char* strcat(char *dest,const char* src)`` : 拼接两个字符串
time.h
简介:
时间相关
时刻以纳秒为单位
结构体:
``struct timespec`` : 时间戳
### 变量列表:
``long int tv_sec`` : 秒
``long int tv_nsec`` : 纳秒
宏定义:
``#define CLOCKS_PER_SEC 1000000`` 每一秒有1000000个时刻(纳秒)
函数列表:
``int nanosleep(const struct timespec *rdtp,struct timespec *rmtp)``
休眠指定时间
rdtp : 指定休眠的时间
rmtp : 返回剩余时间
``clock_t clock()`` : 获得当前系统时间
unistd.h
简介:
也是一些常用函数
函数列表:
``int close(int fd)`` : 关闭文件
``ssize_t read(int fd,void *buf,size_t count)`` : 从文件读取
传入文件id,缓冲区,以及字节数
返回成功读取的字节数
``ssize_t write(int fd,void const *buf,size_t count)`` : 写入文件
传入文件id,缓冲区,字节数
返回成功写入的字节数
``off_t lseek(int fd,off_t offset,int whence)`` : 调整文件访问位置
传入文件id,偏移量,调整模式
返回结束后的文件访问位置
``pid_t fork(void)`` : fork 当前进程
``pid_t vfork(void)`` : fork 当前进程,与父进程共享 VM,flags,fd
``uint64_t brk(uint64_t end_brk)`` : 将堆内存调整为end_brk
若end_brk 为 -1,返回堆区域的起始地址
若end_brk 为 -2,返回堆区域的结束地址
否则调整堆区的结束地址域,并返回错误码
``void *sbrk(int64_t increment)`` :
将堆内存空间加上offset(注意,该系统调用只应在普通进程中调用,而不能是内核线程)
increment : 偏移量
``int64_t chdir(char *dest_path)``
切换工作目录(传入目录路径)
``int execv(const char* path,char * const argv[])`` : 执行文件
path : 路径
argv : 执行参数列表
``extern int usleep(useconds_t usec)`` : 睡眠usec微秒
这里是所有libc头文件的集合,在代码里可以这样引用:
#include<libc/src/xxx.h>
设计文档
[内容待完善]
参与开发
DragonOS社区欢迎您的加入!学习技术本身固然重要,但是以下这些文档将会帮助您了解DragonOS社区需要什么。
阅读这些文档将会帮助您参与到开发之中,并且能让您的代码能更快合并到主线。
开发流程介绍
作为一名想要参与开发的新人,您可能迫切想要了解如何参与开发,仔细阅读这篇文章将能帮助您了解整个开发流程,以及一些注意事项。
注:本文参考了Linux文档中的一些思想、内容,非常感谢Linux社区的工作者们的经验!
1.概要
对于新人而言,参与开发的过程主要包括以下几步:
运行DragonOS:按照文档:构建DragonOS中的教程,编译DragonOS,并成功运行。在运行过程中,如果有任何的疑问,欢迎您在交流群或者BBS上进行反馈!
联系Maintainer:您可以通过邮箱longjin@RinGoTek.cn或者QQ
184829088
与龙进取得联系,或者是对应的模块的开发者进行联系(目前您可以通过发行日志上的邮箱与他们建立联系,在将来我们将编写一份“Maintainers List”以便于您能快速找到要联系的人)。 为了节省您的时间,请简要说明:如何称呼您
您目前掌握的技术能力
您希望为DragonOS贡献什么功能,或者进行某个方面的开发,亦或者是想要按照社区的需要来参与新功能开发及bug的修复。
如果您是来自高校/科研单位/企业/组织的代表,希望与社区开展合作研究、开发。那么,除使用QQ交流之外,还请麻烦您通过您的教师邮箱/学生邮箱/企业邮箱向contact@DragonOS.org发送一封相关内容的邮件!这么做的目的是为了确认您是来自您的单位,而不是网络上其他人员冒充您的身份。
加入工作群:在进一步了解,确认您愿意参与开发后,我们将邀请您加入工作群。
开始开发:正式开始代码开发工作。
备注
一些小功能的改进以及Bug修复并不一定需要提前与社区建立正式的联系,对于这些patch,您可以直接开发,然后在Github上进行Pull Request. 这也是可以的。
但是,如果您愿意的话,与Maintainer联系会是一个更好的选择。
2.开发过程是如何运作的?
如今的DragonOS由于正处于开发的早期阶段,开发者数量不超过50人,因此,现在DragonOS的开发过程是通过比较松散的方式组织起来的。
2.1.版本发布周期
自从2022年11月6日,DragonOS发布第一个版本以来,版本发布就被定义为15~21天发布一个更新版本。由于开发人员数量仍然较少,因此,目前这个时间是21天。我们将版本号定义为x.y.z
的格式,每21天发布一个z
版本. 在积累了2~3个月后,当DragonOS引入了足够的新功能,则发布一个y
版本。请注意,我们仍未定义x
版本的发行周期。当前,x
版本号仍然是0
。
创建没有BUG的、具有尽可能少BUG的代码,是每个开发者的目标之一。我们希望在每个y
版本发布时,能够修复已知的问题。然而,由于在操作系统中,影响问题的变量太多了,以至于我们只能尽全力去减少BUG,我们无法保证y
版本不存在bug.
2.2.每个补丁的生命周期
当您向DragonOS的仓库发起一次PR,那么这次PR就是一个补丁。我们会对您的补丁进行Review,以确保每个补丁都实现了一个希望在主线中进行的更改。并且,Maintainer或者感兴趣的小伙伴会对您的补丁提出修改意见。当时机合适时,您的代码将被合入主线。
如果您的补丁的规模比较小,那么,它将会比较快的被合入主线。如果补丁的规模较大,或者存在一些争议,那么我们需要对其进行进一步的讨论及修改、审查,直到它符合要求。
每个Patch都会经历这么一个过程(这只是一个概述,详细的描述请看后文):
设计:在这个阶段,我们将明确,这个补丁将要实现什么功能,或者是解决什么问题,以及我们将要采用什么样的方式去完成它。通常来说,这项工作是开发者自己完成的。但是,我们建议您,在设计了这个补丁之后,能够把您的设计方案公开,和大家讨论这项工作。 闭门造车容易出错,在与大家沟通的过程中,则能及早的发现设计上的错误,从而节省很多的时间。
代码编写:经过了设计阶段,您已经能够明白自己要实现的到底是一个什么样的东西。您在这个阶段进行代码编写、调试。
代码审查:当完成代码编写后,您可以通过Github发起一个PR,然后您的补丁进入了代码审查阶段。在这一阶段,开发者们,或者是Maintainer会和您进行沟通交流,对您的代码进行评论,以及对发现的问题提出修改建议。
合并主线:如果一切顺利,那么代码将会被合并入主线。若该补丁在合并主线后,被发现具有较大的问题,那么它可能会被回退。重新进入前面的阶段,进行修改。
长期维护:虽然说,代码被合并之后,原来的开发人员可能会在很久一段时间后,不再理会这些代码,但是这种行为可能会给其他开发者们留下不好的印象。其实,当补丁被合并入主线后,其他开发人员会尝试继续维护这些代码,这样能够很大程度的减轻您的维护负担。但是,如果想要这些代码能够长期的被保留下来,持续的发光发热,那么离不开原始开发人员的支持(不然的话,后来的人可能难以理解、维护这些代码),这是一件很有意义的事情。
对于没有参与过开源项目的同学来说,他们可能会想当然的,简单的把上述流程简化成合并主线这一个步骤,这样是不可取的。因为这样会导致很多问题,包括但不限于“写了代码但是效果很差”、“写的东西由于无法满足项目的需求,而不能被合并”。
2.3.开发工具
从上面的描述可以看出,为了让开发人员们能高效地进行协作,那么必须使用版本控制工具来管理这个过程。目前,DragonOS使用Git来进行源代码管理。它是一个非常优秀的工具,这是不必多说的。对于每个开发者而言,Git的使用是一项必备的技能;哪怕您只是想学习DragonOS的源代码,您也需要git来获取、同步最新的代码。虽然Git的使用,对于新手来说,有些困难,但是经过一些时间的学习后,还是可以掌握的。
git的官方网站为https://git-scm.com/
2.4.沟通交流
DragonOS的主要开发工作是通过飞书以及bbs进行的。对于正准备参与的开发者而言,您可以加入我们的交流讨论QQ群,具体的群号可以在 与社区建立联系 一文中找到。
何时使用即时通讯软件? 我们在飞书上创建了工作群,为提高即时沟通的效率,这个群仅供那些真正有意愿、且正在进行或准备进行(能够保证会进行)代码开发的开发者们加入。
何时使用BBS? 对于一些正式的、需要大家广泛参与,或者是能够帮助尚未参与开发的同学了解当前的开发进度的主题,请您在https://bbs.DragonOS.org上,使用类似于写信件一样的,正式的语言,完整地描述清楚您想表达的内容。这样有助于更多的人快速明白您要表达的是什么,也能提高整体的沟通效率。并且,bbs能够长期保存以往的帖子,这样后来者能更方便的了解“当初开发的时候,人们究竟是怎么想的”。
关于交流讨论会 除由于法定节假日放假,或特殊情况以外,我们每周末都会召开线上交流讨论会,同步介绍每周的进展。社区成员可以在这里分享自己的方案设计或是一些操作系统相关的知识(分享的话,需要提前跟会议主持人提出,以便妥善安排)。
如何提问? 下面这些建议能够帮助您与他人开展高效率的对话:
对于具有主题性的问题,在bbs上发帖进行讨论。 这样能够让讨论更具有目标性。当谈论到新的主题的时候,请开一个新的帖子,并在原来的帖子中,添加对特定的子问题所在的主题的链接。
请礼貌的交流。 文明的语言能够减少不必要的冲突。技术意见上的冲突是思维的碰撞,但是如果涉及到了不文明的语言,或者在非技术层面,对他人进行攻击,这将破坏和谐讨论的氛围,这是我们反对的。如果有人试图激怒你,请忽略他的消息,别理他就好了。
在提问之前,请确保您已经搜索了bbs以及互联网上的解决方案,并描述清楚您的问题的上下文情景、您的思考以及网络上的解决方案。 一些开发人员会对“明显没有进行认真思考”的问题,表现出不耐烦的态度(因为未经思考的问题会浪费他们大量的时间)。
当别人向您提问时,请您耐心听他人的问题。如果您认为对方的问题过于简单或是未经思考,还请您为对方指个路,告诉对方,在哪些地方,使用什么样的方式搜索,能够得到对解决问题有帮助的资料。有时候,新手需要的是一个指路人,他会非常感谢您的!
2.5.如何入门开发?
DragonOS原采用C语言进行开发,目前正在用Rust重构原有代码、开发新的模块,也就是说,除非您要进行对C语言代码的BUG修复,否则,其余的开发工作,我们都建议您通过Rust完成。因为,它能从语言层面解决那些让我们头疼的内存安全问题。从长期来看,能够提升开发效率以及软件质量。
如何开发第一个补丁,是一个非常常见的问题。可以理解的是,个人开发者面对这样一个项目,常常会不知道从哪个地方开始入手。这是一件很正常的事情,因此我们建议您通过上文提到的方式,与社区建立联系,了解目前社区正在做什么,以及需要什么。
对于一个新的参与者来说,我们建议您从这样一个步骤开始:
阅读文档,编译、运行DragonOS,并且尝试使用它目前已有的功能。
然后,您可以通过查看DragonOS的GitHub仓库的project面板,看看目前仍有哪些待解决的问题。可以肯定的是,永远不会缺少待解决的问题,您在解决这些问题的过程中,能够获得一些宝贵的经验。
3.早期设计
对于软件开发而言,写代码永远不是第一步。在编写代码之前,进行一些必要的设计(提出架构、技术方案),是项目成功的基础工作。在新的补丁开发的早期,花费一些时间进行计划和沟通,往往能够在接下来的阶段节省更多的时间。
3.1.定义我们要解决的问题
与大多数的工程项目一样,在DragonOS中进行开发,首先需要清晰的描述要解决的问题。只有精准的定义了问题,才能找到正确的解决方案。有时候,我们能很轻易的定义问题,比如“编写串口驱动程序,使得它能把屏幕上打印的字符,输出到串口”。
但是,有时候,我们容易将真正的问题与某个解决方案相混淆,并且还没意识到这一点。
在2022年10月时,我发现,在真机调试的时候,需要频繁的拔插硬盘(先连接到电脑,待数据拷贝完毕后,再连接到测试机)。我对这一过程非常的不满,因为很浪费时间。我的直觉想法是:“有没有一种设备,能够一头连接电脑,另一头连接测试机的SATA接口。从测试机的角度看来,这是一块硬盘;测试机对这块磁盘的操作,都转换为了对我的电脑上面的一个磁盘镜像文件的操作。”我的想法就是:“购买/定制一款设备,能够实现上面的这个功能,那就能解决频繁拔插硬盘的烦恼了!”然后我为了寻找这样的设备,询问了一些硬件公司,他们的开价都在2万元左右。
我在上面的这个过程中,就犯了一个错误:将真正的问题与某个解决方案相混淆了。真正的问题是:“解决需要频繁的拔插硬盘”,但是,在我的思考的过程中,不知不觉间,将问题转换成了“如何实现一款具有硬盘模拟功能的设备”。后面这个问题,只是某个解决方案下,需要解决的问题,并不是我们要解决的根本问题。
对于要解决的根本问题,事实上有更好的解决方案:“制作一个类似于开关一样的转换器,当数据从电脑拷贝到磁盘后,把开关拨向另一边,使得电路与测试机接通”。这个方案的成本估摸着就十几二十块钱。
上面的这个故事,告诉我们的是,在早期设计阶段,我们应当关注的是问题本身——而不是特定的解决方案。
我们需要关注系统的稳定性、长期的可维护性,解决方案必须考虑到这两点。由于系统比较复杂,因此,请您在开始编码之前,与社区的小伙伴讨论您的设计方案,以便您的方案能充分地,从全局的角度,考虑到系统的稳定性、可维护性。
因此,在开发的早期,我们应当对以下三个问题,拥有一个答案:
要解决的本质问题是什么?
这个问题会影响哪些方面/哪些用户?提出的解决方案应当解决哪些用例、场景?
DragonOS目前在解决该问题的方面,具有哪些不足/问题?
只有考虑清楚了上面三个问题,讨论的解决方案才是有意义的。这是一个架构设计的过程,需要进行仔细的思考。尽管我们目前提倡敏捷开发,但是前期的架构设计仍然是非常重要的。
3.2.早期讨论
在实施开发之前,与社区的成员们进行讨论是非常有意义的。这能够通过多种方式节省您的时间,并减少许多不必要的麻烦:
DragonOS可能以您不知道、不理解的方式,已经解决了相关的问题。DragonOS里面的一些特性、功能细节不是很明显,他们不一定会被写进文档。也许这些细节只是在某个不起眼的注释里面提到了,因此您很难注意到这些。这种细节可能只有一些开发人员知道。因此,与社区沟通能够避免您进行重复的工作。
您提出的解决方案中,可能会有一些东西,由于一些原因(比如方案中的一些设计会在将来造成问题、方案的架构设计具有明显缺陷),无法合入主线。
其他的开发人员可能已经考虑过这个问题;他们可能有更好的解决方案,或者是更好的想法。并且,他们可能愿意帮助你一起完善你的解决方案。
Linux文档中提到:闭门造车式的设计和开发,所产生的代码总会有问题,这些问题只有在发布到社区里面的时候才会暴露出来。因此,我们必须吸取前人之鉴,通过与社区开发人员进行早期讨论,从而避免大量的痛苦和额外的工作。
3.3.在何时发布帖子?
如果可能的话,在开发的早期阶段发布您的计划、设计,会是一个不错的选择。发帖的时候,您可以描述您正在解决的问题,以及已经制定的一些计划。包括但不限于:如何将设计付诸实施。您在社区内发布帖子,不仅能够帮助您获得一些有用的建议,也能帮助整个DragonOS社区提供有用的信息,使得社区沟通更高效。
在这个阶段,可能您发布的帖子并不会引来很多评论,这并不一定意味着您做的不好,或者大家对您所做的工作不感兴趣。当然,也不能就此认为您的设计、想法不存在问题。可能只是因为大家比较忙,看了您的帖子之后,了解到了您的工作,但是大家并不一定有时间进行回复。(但是事实上您发布的信息对他人来说是有用的)
在这种情况下,请不要气馁,您最好的做法就是,继续您的工作,并且不时地在您的帖子下分享您的工作,这样能够让社区的成员们随时了解到您的最新进展。
3.4.获得您所在的组织的支持
如果您对DragonOS的开发工作,是在您的公司内完成的。那么,很显然,在您把计划、代码发布到社区论坛之前,您必须取得您的经理或上级的许可。
同时,请注意,根据我们的授权许可,基于DragonOS操作系统的内核、官方开源的用户库而开发的代码,或者为DragonOS操作系统本身而开发的代码,根据开源授权许可,必须同样以GPLv2协议进行授权发布。如果您所在的组织,违背了GPLv2协议中的要求,以除GPLv2以外的协议开放源代码,或者是进行“闭源使用”,那么DragonOS社区对您的公司/组织所授予的使用DragonOS源代码的授权,将会被自动撤销。这将会面临一系列的法律问题。因此,在这个问题上,公司的管理人员、法律人员如果能越早地就公司要在DragonOS中开发的软件项目达成一致,将能促进您的公司在该项目上的进展。
如果您的公司的项目/或者是您研究的项目根据您所在组织的保密规定,不能将其进行过早的披露,那也没有问题。只要您的公司能够确保这部分代码,在其编译而成的二进制产品被发布之时,按照GPLv2协议进行开源,并向开源社区贡献这部分代码即可。
4.如何正确的编写代码
5.发起Pull Request
6.后期跟进
7.另外的一些话题
8.更多信息
9.结语
代码风格
这份文档将会简要的介绍DragonOS的代码风格。每个人的代码风格都各不相同,这是一件非常正常的事情。但是,对于一个开源项目的可维护性而言,我们希望制定一些代码规范,以便包括您在内的每个开发者都能在看代码的时候更加舒服。一个充斥着各种不同代码风格的项目,是难以维护的。
我们在这里提出一些建议,希望您能够尽量遵循这些建议。这些建议与Linux的代码规范相似,但又略有不同。
0. 代码格式化工具
在提出下面的建议之前,我们建议您在开发的时候使用Visual Studio Code的C/C++ Extension Pack
插件作为代码格式化工具。这些插件能为您提供较好自动格式化功能,使得您的代码的基本格式符合DragonOS的要求。
当您在编码时,经常性的按下Ctrl+shift+I
或您设置的代码格式化快捷键,能帮助您始终保持良好的代码格式。
1. 缩进
一个制表符的宽度等于4个空格。代码的缩进是按照制表符宽度(在多数编辑器上为4个字符)进行缩进的。
这样能够使得您的代码变得更加容易阅读,也能更好的看出代码的控制结构。这样能避免很多不必要的麻烦!
举个例子:在switch语句中,将switch和case放置在同一缩进级别。并且将每个case的代码往右推进一个tab。这样能让代码可读性变得更好。
switch (cmd)
{
case AHCI_CMD_READ_DMA_EXT:
pack->blk_pak.end_handler = NULL;
pack->blk_pak.cmd = AHCI_CMD_READ_DMA_EXT;
break;
case AHCI_CMD_WRITE_DMA_EXT:
pack->blk_pak.end_handler = NULL;
pack->blk_pak.cmd = AHCI_CMD_WRITE_DMA_EXT;
break;
default:
pack->blk_pak.end_handler = NULL;
pack->blk_pak.cmd = cmd;
break;
}
2. 分行
我们建议,每行不要超过120个字符。如果超过了,除非有必要的理由,否则应当将其分为两行。
在分行时,我们需要从被分出来的第二行开始,比第一行的起始部分向右进行一个缩进,以表明这是一个子行。使用代码格式化的快捷键能让你快速完成这件事。
对于一些日志字符串而言,为了能方便的检索到他们,我们不建议对其进行分行。
对于代码的分行,请不要试图通过以下的方式将几个语句放置在同一行中,这样对于代码可读性没有任何好处:
// 错误示范(1)
if(a) return 1;
// 错误示范(2)
if(b)
do_a(),do_b();
3. 大括号和空格
3.1 大括号
大括号的放置位置的选择是因人而异的,主要是习惯原因,而不是技术原因。我们推荐将开始括号和结束括号都放置在一个新的行首。如下所示:
while(i<10)
{
++i;
}
这种规定适用于所有的代码块。
这么选择的原因是,在一些编辑器上,这样放置括号,编辑器上将会出现辅助的半透明竖线,且竖线两端均为括号。这样能帮助开发者更好的定位代码块的层次关系。
下面通过一些例子来演示:
在下面这个代码块中,我们需要注意的是,else if
语句需要另起一行,而不是跟在上一个}
后方。这是因为我们规定{
必须在每行的起始位置,并且还要保持缩进级别的缘故。
if (*fmt == '*')
{
++fmt;
}
else if (is_digit(*fmt))
{
field_width = skip_and_atoi(&fmt);
}
当循环中有多个简单的语句的时候,需要使用大括号。
while (condition)
{
if (test)
do_something();
}
当语句只有1个简单的子句时,我们不必使用大括号。
if(a)
return 1;
3.2 空格
对于大部分关键字,我们需要在其后添加空格,以提高代码的可读性。
请您在所有这些关键字后面输入一个空格:
if, switch, case, for, do, while
关键字sizeof、typeof、alignof、__atrribute__的后面则不需要添加空格,因为使用他们的时候,就像是使用函数一样。
对于指针类型的变量,*
号要贴近变量名而不是贴近类型名。如下所示:
char *a;
void *func(char* s, int **p);
在大多数二元和三元运算符周围(在每一侧)使用一个空格,如下所示:
= + - < > * / % | & ^ <= >= == != ? :
这些一元运算符后方没有空格
& * + - ~ ! sizeof typeof alignof __attribute__ defined
特殊的例子,以下运算符的前后都不需要空格:
++ -- . ->
4. 命名
DragonOS中的命名规范不使用诸如TempValue
这样的驼峰命名法的函数名,而是使用tmp
这样言简意赅的命名。
注意,这里指的是我们在整个项目内都不希望使用驼峰命名法。并不意味着程序员可以随便的使用一些难以理解的缩写来作为变量名。
对于全局变量或者全局可见的函数、结构体而言,我们需要遵循以下的命名规定:
名称需要易于理解,且不具有歧义。如:对于一个计算文件夹大小的函数而言,我们建议使用
count_folder_size()
来命名,而不是cntfs()
这样令其他人头大的命名。全局的,非static的名称,除非有特别的必要,命名时需要遵循以下格式:
模块名缩写前缀_函数/变量名
。这样的命名能便于别人区分这个名称位于哪个模块内,也减少了由于命名冲突所导致的麻烦。不需要让其他代码文件可见的全局名称,必须添加
static
修饰符。
对于函数内的局部变量而言,命名规范则是需要言简意赅。过长的名称在局部变量中没有太大的意义。
【文档未完成,待继续完善】
与社区建立联系
社区公共邮箱:contact@DragonOS.org
DragonOS负责人: longjin
工作邮箱: longjin@RinGoTek.cn
开发交流QQ群: 115763565
DragonOS官网: https://DragonOS.org
发行日志
这里是DragonOS的发行日志,会记录DragonOS的每一个版本的更新内容。
V0.1.2
贡献者名单
DragonOS V0.1.2版本由以下小伙伴贡献代码:
Gou Ngai sujintao@DragonOS.org
赞助者名单
感谢以下同学的赞赏,我们将不断努力!
David Wen
TerryLeeSCUT
悟
slientbard
其中,非常感谢Seele.Clover给予DragonOS项目人民币500元的赞助与支持!我们对于每一笔赞助款项,将仔细登记,并确保其能被妥善的使用。
更新内容-内核
删除rust_helloworld文件 (#113)
Signal: 允许用户注册信号处理函数,能够进入自定义的handler。(#112)
支持kill命令
允许用户自定义信号处理函数
新增2个系统调用:
SYS_SIGACTION
,SYS_RT_SIGRETURN
libc增加
signal()
,sigaction()
函数。暂时只支持旧版的sighandler,即:只有1个参数的
void handler(int signum)
类型的信号处理函数。对于另一种信号处理函数void handler(int signum, siginfo_t *info, void* data)
,尚不支持传递第三个参数。
在内核代码中加入自定义的stdint.h文件 (#109)
调整编译grub的脚本的部分 (#108)
新增32、64位uefi启动 (#105)(#101)
使用编译安装的grub-2.06,解决客户机上grub版本不对导致的编译无法运行的问题。
增加了timekeeping模块 (#106)
bugfix: 修复rtc时钟对BCD码进行转换的时候,忘了处理day字段的问题 (#104)
new: 开发过程文档(完成了一半)
bootstrap.sh解决下载rust慢的问题
更新“构建系统”文档
procfs->status增加显示preempt和虚拟运行时间 (#100)
ffz函数:获取u64中的第一个值为0的bit (#100)
解决由于编译器优化导致local_irq_restore无法获取到正确的rflags的值的问题
使用Rust重构串口驱动 (#99)
更新内容-用户环境
about app: 显示当前构建的git commit sha1以及构建时间(#114)
shell: 修复shell的exec命令对绝对路径的拼接错误问题(#114)
shell: exec命令增加”&”后台运行选项 (#100)
new: 测试signal用的app
将libc目录进行调整,加入cargo作为rust的包管理器
源码、发布版镜像下载
您可以通过以下方式获得源代码:
通过Git获取
您可以访问https://github.com/fslongjin/DragonOS/releases下载发布版的代码,以及编译好的,可运行的磁盘镜像。
我们在gitee上也有镜像仓库可供下载:https://gitee.com/DragonOS/DragonOS
通过DragonOS软件镜像站获取
为解决国内访问GitHub慢、不稳定的问题,同时为了方便开发者们下载DragonOS的每个版本的代码,我们特意搭建了镜像站,您可以通过以下地址访问镜像站:
您可以通过镜像站获取到DragonOS的代码压缩包,以及编译好的可运行的磁盘镜像。
开放源代码声明
备注
为促进DragonOS项目的健康发展,DragonOS以GPLv2开源协议进行发布。所有能获得到DragonOS源代码以及相应的软件制品(包括但不限于二进制副本、文档)的人,都能享有我们通过GPLv2协议授予您的权利,同时您也需要遵守协议中规定的义务。
这是一个相当严格的,保护开源软件健康发展,不被侵占的协议。
对于大部分的善意的人们而言,您不会违反我们的开源协议。
我们鼓励DragonOS的自由传播、推广,但是请确保所有行为没有侵犯他人的合法权益,也没有违反GPLv2协议。
请特别注意,对于违反开源协议的,尤其是商业闭源使用以及任何剽窃、学术不端行为将会受到严肃的追责。(这是最容易违反我们的开源协议的场景)。
并且,请注意,按照GPLv2协议的要求,基于DragonOS修改或二次开发的软件,必须同样采用GPLv2协议开源,并标明其基于DragonOS进行了修改。亦需保证这些修改版本的用户能方便的获取到DragonOS的原始版本。
您必须使得DragonOS的开发者们,能够以同样的方式,从公开渠道获取到您二次开发的版本的源代码,否则您将违反GPLv2协议。
关于协议详细内容,还敬请您请阅读项目根目录下的LICENSE文件。请注意,按照GPLv2协议的要求,只有英文原版才具有法律效力。任何翻译版本都仅供参考。
开源软件使用情况
DragonOS在开发的过程中,参考了一些开源项目的设计,或者引入了他们的部分代码,亦或是受到了他们的启发。现将他们列在下面。我们对这些开源项目的贡献者们致以最衷心的感谢!
格式:<项目名> - <链接> - <开源协议>
Linux - https://git.kernel.org/ - GPLv2
skiftOS - https://github.com/skiftOS/skift - MIT
FYSOS - https://github.com/fysnet/FYSOS - FYSOS’ License
LemonOS - https://github.com/LemonOSProject/LemonOS.git - BSD 2-Clause License
LZ4 - https://github.com/lz4/lz4 - BSD 2-Clause license
SerenityOS - https://github.com/SerenityOS/serenity.git - BSD 2-Clause license
MINE - 《一个64位操作系统的设计与实现》田宇; 人民邮电出版社
chcore - 《现代操作系统:设计与实现》陈海波,夏虞斌; 机械工业出版社
SimpleKernel - https://github.com/Simple-XX/SimpleKernel - MIT
当前版本的所有提交记录
commit 7a818da88a1c7a1760de7671141b0ce1ca4e3dde
Author: login <longjin@ringotek.cn>
Date: Sat Dec 17 17:49:12 2022 +0800
Patch about auto gen version string (#114)
* new: about app中,显示当前构建的git commit sha1以及构建时间
* bugfix: 修复shell的exec命令对绝对路径的拼接错误问题
commit 83a7aaa46bbc411c43d4fc099c6c8884efbe4771
Author: login <longjin@ringotek.cn>
Date: Sat Dec 17 16:31:50 2022 +0800
删除rust_helloworld文件 (#113)
commit 6efd4740336205c9bfdd8b164e667cee2f38781e
Author: login <longjin@ringotek.cn>
Date: Sat Dec 17 16:27:50 2022 +0800
允许用户自定义信号处理函数 (#112)
* new: 用户注册信号处理函数,能够进入自定义的handler
* 修复忘了传信号的数字给用户的处理函数的bug
* new:sigreturn
* 删除注释
commit 0e0c187484281768391e131495f0655e40d70cf7
Author: login <longjin@ringotek.cn>
Date: Fri Dec 16 16:20:09 2022 +0800
在内核代码中加入自定义的stdint.h文件 (#109)
commit d02e6ea4112ad520aa4090ff73cdf592e14c0a82
Author: login <longjin@ringotek.cn>
Date: Wed Dec 14 20:01:55 2022 +0800
调整编译grub的脚本的部分 (#108)
1、bugfix: 修复编译grub的脚本的部分错误
2、将grub下载源替换为tuna
3、优化写入磁盘镜像的脚本
4、将bios文件夹改名为legacy
commit 38b341b8aa671f75ac26d05059aa2e9a09e653b7
Author: YJwu2023 <119829947+YJwu2023@users.noreply.github.com>
Date: Wed Dec 14 16:58:49 2022 +0800
新增32位uefi启动 (#105)
* 新增32位uefi启动
* 修复小bug
* 增加grub本地编译安装
* 增加本地grub编译安装脚本
* 修正小错误
* 修复空文件夹不上传的bug
commit 01876902fbf6ed43992cc7d153bd8c505cb5224b
Author: Gou Ngai <94795048+AlbertSanoe@users.noreply.github.com>
Date: Wed Dec 14 15:13:54 2022 +0800
增加了timekeeping模块 (#106)
* 增加了timekeeping模块
* 格式化文档和细节更改
Co-authored-by: longjin <longjin@RinGoTek.cn>
commit 728aca308917a7d4d0ba10fe8174e9408d77a9a6
Author: login <longjin@ringotek.cn>
Date: Sun Dec 11 22:59:47 2022 +0800
bugfix: 修复rtc时钟对BCD码进行转换的时候,忘了处理day字段的问题 (#104)
commit 237e95c6ddce72d72ae7fedfeca412fab82b3622
Author: wwc-15172310230 <78997674+wwc-15172310230@users.noreply.github.com>
Date: Sun Dec 11 22:22:10 2022 +0800
调整user下libs的libc目录结构 (#103)
* 调整user下libs的libc目录结构
* 修正.gitignore文件的问题
* 修复无法编译的问题
Co-authored-by: longjin <longjin@RinGoTek.cn>
commit 2291ffdece1dc5a703602f79f74df8a4854d215b
Author: login <longjin@ringotek.cn>
Date: Sun Dec 11 20:09:58 2022 +0800
文档更新 (#102)
* new: 开发过程文档(完成了一半)
* bootstrap.sh解决下载rust慢的问题
* 更新“构建系统”文档
commit 7f439c5ddbd2ecffc112149d16983975f523052c
Author: YJwu2023 <119829947+YJwu2023@users.noreply.github.com>
Date: Fri Dec 9 16:08:54 2022 +0800
增加uefi启动 (#101)
* 增加uefi启动
* 修改脚本
* uefi修改
* 删除错误的注释
* 修正写入磁盘镜像的脚本
* 修改X86_64为x86_64
Co-authored-by: longjin <longjin@RinGoTek.cn>
commit 1a2eaa402f05f82aaeebe1e03824534a0a425d4d
Author: login <longjin@ringotek.cn>
Date: Thu Dec 8 22:59:51 2022 +0800
signal的处理(kill命令)以及一些其他的改进 (#100)
* 将entry.S中冗余的ret_from_syscall代码删除,改为jmp Restore_all
* new: 增加判断pt_regs是否来自用户态的函数
* new: rust的cli和sti封装
* 将原有的判断pt_regs是否来自用户态的代码,统一改为调用user_mode函数
* ffz函数:获取u64中的第一个值为0的bit
* spinlock增加 spinlock irq spin_unlock_irq
* 临时解决显示刷新线程迟迟不运行的问题
* 更改ffi_convert的生命周期标签
* new: 测试signal用的app
* 解决由于编译器优化导致local_irq_restore无法获取到正确的rflags的值的问题
* new: exec命令增加"&"后台运行选项
* procfs->status增加显示preempt和虚拟运行时间
* 更改引用计数的FFIBind2Rust trait中的生命周期标签
* new: signal处理(kill)
* 更正在review中发现的一些细节问题
commit f8b55f6d3fcbf152a1cb6d6fc722bf1607418b28
Author: TingHuang <92705854+TingSHub@users.noreply.github.com>
Date: Tue Dec 6 22:15:03 2022 +0800
Patch uart (#99)
* 添加UART驱动相关文件
* 添加驱动核心文件,将rust编写的驱动代码加入Package中
* 添加glib.h文件生成rust代码,添加uart驱动代码
* 添加串口发送及接收相关代码
* 添加字符串发送函数,未实现具体功能
* 为调用uart驱动的代码添加rust接口
* 添加字符串发送函数,修改C语言调用接口
* 添加rust串口驱动
* 添加uart.h头文件,将串口端口类型改为enum
* 添加注释,规范代码
commit 036acc52ce9d0fb9e7d92768ff74939a29c07f32
Author: login <longjin@ringotek.cn>
Date: Tue Nov 29 21:46:13 2022 +0800
将entry.S中冗余的ret_from_syscall代码删除,改为jmp Restore_all (#98)
* 将entry.S中冗余的ret_from_syscall代码删除,改为jmp Restore_all
V0.1.1
贡献者名单
DragonOS V0.1.1版本由以下小伙伴贡献代码:
赞助者名单
感谢以下同学的赞赏,我们将不断努力!
David Wen
TerryLeeSCUT
悟
slientbard
其中,非常感谢David Wen给予DragonOS项目人民币1000元的赞助与支持!我们对于每一笔赞助款项,将仔细登记,并确保其能被妥善的使用。
更新内容-内核
新增rust ffi (#77)
port kmalloc and printk to rust
rust下的kdebug kinfo kwarn kBUG kerror宏
bugfix: 修复进程pcb被回收时,未将其从链表中删除的问题
目录结构优化:移动asm.h和cmpxchg.h
signal的发送
procfs:查看进程的status
解决第一次编译时磁盘镜像权限错误的问题
将fork相关代码移动到fork.c
更新内容-用户环境
shell:增加kill命令,可向目标进程发送信号。但由于仍未完善signal机制,因此目标进程暂时不能响应这个信号。
源码、发布版镜像下载
您可以通过以下方式获得源代码:
通过Git获取
您可以访问https://github.com/fslongjin/DragonOS/releases下载发布版的代码,以及编译好的,可运行的磁盘镜像。
我们在gitee上也有镜像仓库可供下载:https://gitee.com/DragonOS/DragonOS
通过DragonOS软件镜像站获取
为解决国内访问GitHub慢、不稳定的问题,同时为了方便开发者们下载DragonOS的每个版本的代码,我们特意搭建了镜像站,您可以通过以下地址访问镜像站:
您可以通过镜像站获取到DragonOS的代码压缩包,以及编译好的可运行的磁盘镜像。
开放源代码声明
备注
为促进DragonOS项目的健康发展,DragonOS以GPLv2开源协议进行发布。所有能获得到DragonOS源代码以及相应的软件制品(包括但不限于二进制副本、文档)的人,都能享有我们通过GPLv2协议授予您的权利,同时您也需要遵守协议中规定的义务。
这是一个相当严格的,保护开源软件健康发展,不被侵占的协议。
对于大部分的善意的人们而言,您不会违反我们的开源协议。
我们鼓励DragonOS的自由传播、推广,但是请确保所有行为没有侵犯他人的合法权益,也没有违反GPLv2协议。
请特别注意,对于违反开源协议的,尤其是商业闭源使用以及任何剽窃、学术不端行为将会受到严肃的追责。(这是最容易违反我们的开源协议的场景)。
并且,请注意,按照GPLv2协议的要求,基于DragonOS修改或二次开发的软件,必须同样采用GPLv2协议开源,并标明其基于DragonOS进行了修改。亦需保证这些修改版本的用户能方便的获取到DragonOS的原始版本。
您必须使得DragonOS的开发者们,能够以同样的方式,从公开渠道获取到您二次开发的版本的源代码,否则您将违反GPLv2协议。
关于协议详细内容,还敬请您请阅读项目根目录下的LICENSE文件。请注意,按照GPLv2协议的要求,只有英文原版才具有法律效力。任何翻译版本都仅供参考。
开源软件使用情况
DragonOS在开发的过程中,参考了一些开源项目的设计,或者引入了他们的部分代码,亦或是受到了他们的启发。现将他们列在下面。我们对这些开源项目的贡献者们致以最衷心的感谢!
格式:<项目名> - <链接> - <开源协议>
Linux - https://git.kernel.org/ - GPLv2
skiftOS - https://github.com/skiftOS/skift - MIT
FYSOS - https://github.com/fysnet/FYSOS - FYSOS’ License
LemonOS - https://github.com/LemonOSProject/LemonOS.git - BSD 2-Clause License
LZ4 - https://github.com/lz4/lz4 - BSD 2-Clause license
SerenityOS - https://github.com/SerenityOS/serenity.git - BSD 2-Clause license
MINE - 《一个64位操作系统的设计与实现》田宇; 人民邮电出版社
chcore - 《现代操作系统:设计与实现》陈海波,夏虞斌; 机械工业出版社
SimpleKernel - https://github.com/Simple-XX/SimpleKernel - MIT
当前版本的所有提交记录
commit d65ade9c5909076747bd00966a398fe27fbd290d
Author: DaJiYuQia <88259094+DaJiYuQia@users.noreply.github.com>
Date: Sun Nov 27 14:21:31 2022 +0800
Patch procf (#95)
* debug color problem
Co-authored-by: longjin <longjin@RinGoTek.cn>
commit 6cb769c423b09e88fea1763210200a716477be0a
Author: login <longjin@ringotek.cn>
Date: Sun Nov 27 14:17:36 2022 +0800
将include目录下的rust代码转移到他们应当属于的模块中 (#96)
* 将include目录下的rust代码转移到他们应当属于的模块下。
commit 27a97abd2474b03ad09b562e5ed11e1fdae8eb32
Author: DaJiYuQia <88259094+DaJiYuQia@users.noreply.github.com>
Date: Sat Nov 26 17:34:00 2022 +0800
Patch procf (#90)
* 1234
* 123
* 合并master
* procfs
* 1
* procfs展示进程基本信息
* modified code
* 恢复权限
* 恢复权限
#恢复权限
* modify permission
* 删除run.sh
* 解决第一次编译时磁盘镜像权限错误的问题
* 恢复.vscode/c_cpp_properties.json
* 删除process.c中错误的do_fork
* remake procfs
* 修改一些变量名
* 修改类型
* modified
* data_puts缓冲区溢出后return
Co-authored-by: longjin <longjin@RinGoTek.cn>
commit ad23fcddf893d7f92d2bf3efdb66e969416d2852
Author: login <longjin@ringotek.cn>
Date: Wed Nov 23 21:34:35 2022 +0800
bugfix: 修复进程退出时未释放signal和sighand && 增加赞赏者名单:David Wen (#93)
* bugfix: 修复进程退出时未释放signal和sighand的bug
* 增加赞赏者名单:David Wen
commit 0274cd6eeec01885232e7418a501857cb76da69e
Author: login <longjin@ringotek.cn>
Date: Wed Nov 23 20:43:18 2022 +0800
修正drop signal结构体的box对象的的问题 (#92)
* fix: exit signal and exit sighand
commit c8025a88798dc57ecc5d7f20ad69de695445638f
Author: login <longjin@ringotek.cn>
Date: Wed Nov 23 20:18:22 2022 +0800
new:在fork时拷贝signal和sighand (#91)
* refcount初始化
* new: 实现copy_sighand
del: 删除sighand_struct的wqh, 待将来有需要时,替换成rust版本的
* new: 拷贝signal
bugfix: 解决拷贝sighand时的uaf问题
commit 66f67c6a95b8aad85cfd2146a86e5e3e6a3568e7
Author: login <longjin@ringotek.cn>
Date: Wed Nov 23 11:38:20 2022 +0800
signal的发送(暂时父子进程之间共享信号及相应的结构体) (#89)
* 解决由于spinlock.h中包含preempt_enable()带来的循环include问题
* new: 初步实现signal的数据结构
* new:signal相关数据结构
* fix: 解决bindings.rs报一堆警告的问题
* new: rust下的kdebug kinfo kwarn kBUG kerror宏
* 移动asm.h和cmpxchg.h
* new: signal的发送(暂时只支持父子进程共享信号及处理函数)
commit 3d729e2069e01ee07525ff83167566dac5322a40
Author: login <longjin@ringotek.cn>
Date: Fri Nov 18 17:59:33 2022 +0800
bugfix: 修复进程pcb被回收时,未将其从链表中删除的问题 (#87)
* bugfix: 修复进程pcb被回收时,未将其从链表中删除的问题
new: pcb相关api文档
* 将文档加入目录
commit 0bfe94f46be9bdde1ade81a20e803aa2aafd2964
Author: login <longjin@ringotek.cn>
Date: Fri Nov 18 16:32:15 2022 +0800
new: rust下的kdebug kinfo kwarn kBUG kerror宏 (#86)
* new: rust下的kdebug kinfo kwarn kBUG kerror宏
commit c6174797dcf3427f38bfa0f4bd3e039c319f7c5b
Author: login <longjin@ringotek.cn>
Date: Thu Nov 17 20:29:29 2022 +0800
fix: 解决bindings.rs报了很多警告的问题 (#85)
* fix: 解决bindings.rs报一堆警告的问题
commit cffd7144fbed84f9775e89d7b99602c6ccc5a510
Author: login <longjin@ringotek.cn>
Date: Wed Nov 16 15:18:03 2022 +0800
signal相关数据结构&代码结构优化 (#84)
* 解决由于spinlock.h中包含preempt_enable()带来的循环include问题
* new: 初步实现signal的数据结构
commit fb6c29d01d4cf92368efec08c01e419c2a941f7d
Author: login <longjin@ringotek.cn>
Date: Sun Nov 13 16:43:58 2022 +0800
port kmalloc and printk to rust (#83)
* 暂时移除cbindgen
* 将lib文件夹更名为libs文件夹(解决rust的冲突)
* 实现了全局的allocator
* 实现了printk宏
* new: 完善了printk的颜色
commit 82d2e446a401e7eee57a847f48a6d162931170c3
Author: login <longjin@ringotek.cn>
Date: Sat Nov 12 15:25:54 2022 +0800
new: 暂时移除cbindgen (#82)
commit 2aaf7808efe44ecfaadd51ae4f8892e667108578
Author: login <longjin@ringotek.cn>
Date: Fri Nov 11 22:21:44 2022 +0800
在内核中引入cbindgen,生成rust-C的FFI (#81)
* 解决codeql失败问题
* new: 为内核引入cbindgen
commit 2813126e3190c9b3c1a836a647b259a7adbe0cf3
Author: login <longjin@ringotek.cn>
Date: Fri Nov 11 15:35:37 2022 +0800
新增rust ffi (#77)
* 引入cargo
* 取消对Cargo.lock的跟踪
* 解决vscode报错问题
* new: rust的代码能够调用c语言的printk_color
* 1、将原本run.sh的工作拆解,变为几个不同的make命令
2、在docker镜像中编译rust
* 更改workflow
* update workflow
* new: 解决workflow无法通过编译的问题
commit 5e023cf7911333eb05bfe65704dce4b01fa4d0a7
Author: login <longjin@ringotek.cn>
Date: Fri Nov 11 15:21:45 2022 +0800
Update makefile.yml
commit e44795008f7e34d2068cf28dcedbcb91f5ccd66b
Author: login <longjin@ringotek.cn>
Date: Fri Nov 11 15:18:13 2022 +0800
Update makefile.yml (#80)
commit ec5fb84b61c313824cc2199ab64e3af4b7e5f895
Author: login <longjin@ringotek.cn>
Date: Fri Nov 11 15:08:09 2022 +0800
Update makefile.yml
commit 6d9dff5f1ff347ea780a0249e54eef356cdcaaea
Author: login <longjin@ringotek.cn>
Date: Fri Nov 11 15:07:48 2022 +0800
Revert "Update makefile.yml (#78)" (#79)
This reverts commit badc7d238f2341e844a90be3e357e5dd77a447fc.
commit badc7d238f2341e844a90be3e357e5dd77a447fc
Author: login <longjin@ringotek.cn>
Date: Fri Nov 11 15:05:52 2022 +0800
Update makefile.yml (#78)
V0.1.0
前言
DragonOS从2022年1月15日开始开发,到如今已经经历了将近300天。在这么多个日夜里,已经数不清到底花了多少时间在DragonOS的开发之中, 我基本上把所有的空闲时间都给了DragonOS,保守估计总工时已经在1000小时以上。能够发布第一个版本,我感到十分有成就感。
在2022年7月以来,陆陆续续的,有来自6所高校或企业的小伙伴/大佬加入了DragonOS的开发。我当时非常的欣喜,我想,也许在大家的一同努力下,我们能创造出 一个真正具有实用性的操作系统呢!我们累计召开了14次交流讨论会。我相信,在大家的共同努力下,将来,我们一定能创造出真正独立自主的、开放的、面向服务器领域应用的开源操作系统,并在生产环境中得到应用。
尽管DragonOS目前只是一个玩具水平的操作系统,只是“比本科生毕业设计难度略高的”操作系统。但是,请不要小看它,它的内在的架构设计,瞄准了Linux5.18及以后的发行版, 虽尚未能达到Linux的水平,但我们正努力追赶。得益于Linux的相关资料,DragonOS在架构设计之时,学习了Linux的很多设计思想,相关组件都尽量考虑了可扩展性与可移植性。
千里之行,始于足下。DragonOS V0.1.0版本的发布,是一个全新的开始。希望在未来的十年里,我们能与众多伙伴们一同努力,在2032年,将DragonOS建设成为具有实用意义的,能够在服务器领域取得广泛应用的开源操作系统!
百舸争流,奋楫者先;中流击水,勇进者胜。 我相信,在接下来的时间里,在社区开发者们的不断努力下,我们的目标,终将成为现实!
特别鸣谢
在DragonOS V0.1.0版本的发布之际,我想对我的老师、前辈以及学校表示衷心的感谢!
佛山市南海区大沥镇中心小学 姚志城老师: 您是带领我接触计算机,学会编程的领路人。十年前,与您交谈时,您说过:“我们国家目前还没有自主的、成熟的操作系统”。这句话,为我的梦想埋下了种子。您培养了我对计算机的热爱,因此我选择了软件工程这个专业。感谢当年您的教导,师恩难忘!
佛山市南海区石门实验学校: 在石实就读的三年里,非常感谢石实的“扬长教育”理念,在老师们的培养下,让我充分发挥了自己的个性和特长,也取得了不错的成绩。在石实的三年里,我学会了C++、Java以及简单的算法,也自己开发了几个安卓app,积累了将近6千行的代码量。
佛山市南海区石门中学:“任重道远,毋忘奋斗”是石中的校训,我想,这句校训,也应当成为我们每个新时代青年人的座右铭。在石门中学的三年,家国情怀教育对我产生了很大的影响。我想,我们作为新时代的青年,更应当肩负起时代的重任,奋勇拼搏,为祖国的发展,为民族的自强,为人类的未来,努力奋斗!
华南理工大学:“博学慎思,明辨笃行”,在华工,我得到了进一步的学习与发展。开拓了自己的视野,学会了跟很多人打交道。并且,在软件学院,我遇到了一群认真负责的老师。非常感谢学院对我的支持,支持我们成立项目组。我相信,在学院的支持下,能让DragonOS取得更好的发展,走的更远!
华南理工大学软件学院 王国华老师:王老师是我《操作系统》课程的老师,在她的指导下,我对操作系统的原理有了更深的理解,并参加了“泛珠三角+大学生计算机作品赛“,在2022年6月的广东省选拔赛中,DragonOS取得了一等奖、最佳创新奖的好成绩。
华南理工大学软件学院 汤峰老师: 汤老师是我们在校内的项目组的指导老师。在她的悉心指导下,我们将不断前行,保持我们前进的方向,持续构建开源社区。我由衷地感谢汤老师的指导!
Yaotian Feng: 在Bilibili上认识了这位非常厉害的老哥,解答了我的很多问题,很多次在我毫无头绪的debug了几天之后,几句话点醒了我,让我找到解决问题的路径。并且,他也跟我分享了容易踩坑的地方,让我在将要踩坑的时候能够有个心理预期,不至于那么难受哈哈哈哈。
贡献者名单
DragonOS V0.1.0版本的发布,离不开以下小伙伴们的共同努力:
zzy666-hw zzy666@mail.ustc.edu.cn
kkkkkong kongweichao@DragonOS.org
houmkh jiaying.hou@qq.com
wang904 1234366@qq.com
Liric Mechan i@liric.cn
Mustang handsomepd@qq.com
Eugene caima12138@foxmail.com
zhujikuan 1335289286@qq.com
Alloc Alice 1548742234@qq.com
赞助者名单
感谢以下同学的赞赏,我们将不断努力!
TerryLeeSCUT
悟
slientbard
内核
遵循的一些标准规范
启动引导:Multiboot2
系统接口:posix 2008
硬件架构
目前支持在x86-64架构的处理器上运行
Bootloader
使用Grub 2.06作为bootloader
内存管理
实现了基于bitmap的页分配器
实现了slab分配器,用来分配小块的、具有对齐要求的内存
抽象出VMA(虚拟内存区域)
实现VMA反向映射机制
实现MMIO地址空间自动映射机制
多核
支持多核引导。也就是说,在DragonOS启动后,将会启动AP处理器。但是,为了简化其他内核模块的实现,目前AP处理器上,暂时没有任务在运行。
粗略实现了IPI(处理器核间通信)框架
进程管理
支持进程的创建、回收
内核线程
kthread机制
用户态、内核态进程/线程的fork/vfork(注意,用户态的fork和内核态的有一定的区别,内核态的fork更复杂)
exec 让进程去执行一个新的可执行文件
进程的定时睡眠(sleep)(支持spin/rdtsc高精度睡眠、支持上下文切换方式的睡眠)
同步原语
spinlock 自旋锁
mutex 互斥量
atomic 原子变量
wait_queue 等待队列
semaphore 信号量
调度相关
CFS调度器
单核调度(暂时不支持多核负载均衡)
completion “完成”机制,让一个进程能等待某个任务的完成。
IPC进程间通信
匿名管道
文件系统
VFS虚拟文件系统的基本功能
FAT32文件系统(尚不支持删除文件夹)
devfs设备文件系统。目前只将键盘文件注册到其中。
rootfs根文件系统,在真实的磁盘文件系统被挂载前,为其他的伪文件系统提供支持。
挂载点抽象。目前实现了文件系统的挂载,使用类似于栈的方式管理所有的挂载点。(将来需要优化这部分)
异常及中断处理
处理器异常的捕获
对APIC的支持
softirq软中断机制
能够对内核栈进行traceback
内核数据结构
普通的二叉树
kfifo先进先出缓冲区
循环链表
IDR 映射数据结构
IDA ID分配数据组件
屏幕显示
VESA VBE显示芯片驱动
实现了屏幕管理器,支持多个显示框架注册到屏幕管理器中。
实现了TextUI文本界面框架,能够渲染文本到屏幕上。并且预留了上下滚动翻页、多显示窗口的支持。
printk
内核实用库
字符串操作库
ELF可执行文件支持组件
基础数学库
CRC函数库
软件移植
移植了LZ4压缩库(V1.9.3),为将来实现页面压缩机制打下基础。
内核测试
ktest单元测试框架
支持使用串口(COM1)输出屏幕内容到文件之中。
驱动程序支持
IDE硬盘
AHCI硬盘(SATA Native)
ACPI 高级电源配置模块
PCI总线驱动
XHCI主机控制器驱动(usb3.0)
ps/2键盘
ps/2鼠标
HPET高精度定时器
RTC时钟
local APIC定时器
UART串口(支持RS-232)
VBE显示
虚拟tty设备
系统调用
DragonOS目前一共有22个有效的系统调用。
SYS_PUT_STRING 往屏幕上打印字符
SYS_OPEN 打开文件
SYS_CLOSE 关闭文件
SYS_READ 读取文件
SYS_WRITE 写入文件
SYS_LSEEK 调整文件指针
SYS_FORK fork系统调用
SYS_VFORK vfork系统调用
SYS_BRK 调整堆大小为指定值
SYS_SBRK 调整堆大小为相对值
SYS_REBOOT 重启 (将来sysfs完善后,将删除这个系统调用,请勿过度依赖这个系统调用)
SYS_CHDIR 切换进程的工作目录
SYS_GET_DENTS 获取目录中的目录项的元数据
SYS_EXECVE 让当前进程执行新的程序文件
SYS_WAIT4 等待进程退出
SYS_EXIT 退出当前进程
SYS_MKDIR 创建文件夹
SYS_NANOSLEEP 纳秒级睡眠(最长1秒)在小于500ns时,能够进行高精度睡眠
SYS_CLOCK 获取当前cpu时间
SYS_PIPE 创建管道
SYS_MSTAT 获取系统当前的内存状态信息
SYS_UNLINK_AT 删除文件夹或删除文件链接
Rust支持
实现了一个简单的rust语言的hello world,计划在接下来的版本中,逐步转向使用rust进行开发。
用户环境
LibC
LibC是应用程序与操作系统交互的纽带。DragonOS的LibC实现了一些简单的功能。
malloc堆内存分配器
基础数学库
简单的几个与文件相关的函数
pipe
fork/vfork
clock
sleep
printf
Shell命令行程序
基于简单的字符串匹配的解析(不是通过编译课程学的的那一套东西做的,因此比较简单,粗暴)
支持的命令:ls,cd,mkdir,exec,about,rmdir,rm,cat,touch,reboot
用户态驱动程序
用户态键盘驱动程序
源码、发布版镜像下载
您可以通过以下方式获得源代码:
通过Git获取
您可以访问https://github.com/fslongjin/DragonOS/releases下载发布版的代码,以及编译好的,可运行的磁盘镜像。
我们在gitee上也有镜像仓库可供下载:https://gitee.com/DragonOS/DragonOS
通过DragonOS软件镜像站获取
为解决国内访问GitHub慢、不稳定的问题,同时为了方便开发者们下载DragonOS的每个版本的代码,我们特意搭建了镜像站,您可以通过以下地址访问镜像站:
您可以通过镜像站获取到DragonOS的代码压缩包,以及编译好的可运行的磁盘镜像。
开放源代码声明
备注
为促进DragonOS项目的健康发展,DragonOS以GPLv2开源协议进行发布。所有能获得到DragonOS源代码以及相应的软件制品(包括但不限于二进制副本、文档)的人,都能享有我们通过GPLv2协议授予您的权利,同时您也需要遵守协议中规定的义务。
这是一个相当严格的,保护开源软件健康发展,不被侵占的协议。
对于大部分的善意的人们而言,您不会违反我们的开源协议。
我们鼓励DragonOS的自由传播、推广,但是请确保所有行为没有侵犯他人的合法权益,也没有违反GPLv2协议。
请特别注意,对于违反开源协议的,尤其是商业闭源使用以及任何剽窃、学术不端行为将会受到严肃的追责。(这是最容易违反我们的开源协议的场景)。
并且,请注意,按照GPLv2协议的要求,基于DragonOS修改或二次开发的软件,必须同样采用GPLv2协议开源,并标明其基于DragonOS进行了修改。亦需保证这些修改版本的用户能方便的获取到DragonOS的原始版本。
您必须使得DragonOS的开发者们,能够以同样的方式,从公开渠道获取到您二次开发的版本的源代码,否则您将违反GPLv2协议。
关于协议详细内容,还敬请您请阅读项目根目录下的LICENSE文件。请注意,按照GPLv2协议的要求,只有英文原版才具有法律效力。任何翻译版本都仅供参考。
开源软件使用情况
DragonOS在开发的过程中,参考了一些开源项目的设计,或者引入了他们的部分代码,亦或是受到了他们的启发。现将他们列在下面。我们对这些开源项目的贡献者们致以最衷心的感谢!
格式:<项目名> - <链接> - <开源协议>
Linux - https://git.kernel.org/ - GPLv2
skiftOS - https://github.com/skiftOS/skift - MIT
FYSOS - https://github.com/fysnet/FYSOS - FYSOS’ License
LemonOS - https://github.com/LemonOSProject/LemonOS.git - BSD 2-Clause License
LZ4 - https://github.com/lz4/lz4 - BSD 2-Clause license
SerenityOS - https://github.com/SerenityOS/serenity.git - BSD 2-Clause license
MINE - 《一个64位操作系统的设计与实现》田宇; 人民邮电出版社
chcore - 《现代操作系统:设计与实现》陈海波,夏虞斌; 机械工业出版社
SimpleKernel - https://github.com/Simple-XX/SimpleKernel - MIT