Skip to content

如何写一个微内核

导言

有时常关注我博客的朋友知道,去年我自己开发过一个很简单的微内核 Totoro,目前已经暂时停止更新。从开发到能真正运行花了一两个月的时间,当然都是我在业余时间做的啦。还记得当时那种兴奋的心情,可能跟 Linux 的最初开发者 Linus 差不多,有兴趣可以看这篇文章 Linux – a free unix-386 kernel

当然了,水平毕竟不是一个层次的。不过今天,我想把如何开发这样一个简单的内核给记录下来,一来是让有兴趣的后来者可以很快的摸到门路,二来是做一个阶段性的总结,再来就是看看自己还有没有兴致继续把它做得更好,毕竟现在漏洞百出,也不可能给别人拿去用。总的来说,出来做了三年的软件开发,没做过什么高端的有难度的东西,这样一个作品让我自己还是比较开心的!

关于单线程

从单片机(又称单板机)玩过来的同学应该知道,我们刚开始写代码的时候,几乎都是裸奔在处理器上的。写个流水灯,点亮数码管,高端点的来个串口打印。这对刚接触的同学其实就有很多东西可以学了。但是人也总不会一直停步不前,写了太多的单线程代码的菜鸟开始思考,多线程到底多了什么了呀?说到这其实我很想笑,假如你去问一问那些入门语言是 Java,Python 的同学,他们一定会说,多线程有什么了不起啊!对呀,对这些这些高级语言来说,多线程?开玩笑,随便调个接口,要几十个线程,甚至几百个,分分钟给你造好了。在这些人眼里,这个东西,没什么了不起。但对于出身即低端又比较吝啬的嵌入式的同学们来说,这个多线程词汇,却魔咒般的充满了太多的神秘感。那么多线程到底指的是什么?我们不去讨论太复杂的东西,不去讨论进程空间,虚拟内存等等高端神秘的词汇。我们只关注上下文环境。这个对于容易满足的嵌入式开发者来说,已经很知足了。时常感慨,是不是嵌入式开发做久了,人也会对自己变吝啬。

上下文环境

上下文(Context)环境,指的是这个线程运行时的一些状态,比如局部变量的值,寄存器里保存的值,比如,PC 指针指向那里,代表程序即将运行到那里,SP 指针,记录了调用信息,LR 记录了回去的路(返回地址)等等。那么要实现这个多线程我们需要做些什么?

保存上下文。我们在做线程调度的时候,将当前线程的上下文环境保存在那个线程申请的“栈”内存里(其实就是一个大数组)。然后通过一系列的底层操作,将要被调度的线程唤出来。其实也就是把之前保存的上下文从内存里读到寄存器上啦,只不过这个过程需要一些技巧,我们会在代码解析里面讲到。其实多线程就是这样的啦,那么我的这篇文章是不是该完结了?不尽然,我们要做到微内核的基本功能,那还得有调度器呀。

傻逼式调度器

我们要实现这样一个调度器。对于平级线程,也就是优先级一样的应用线程,我们会去分片(Time-sharing)执行它们。对于高优先级应用线程,我们会义无反顾的一直执行它,直到它愿意自己挂起。我觉得这是最自然的调度器,因为优先级既然高,那么也就得优先执行。当然,也是很傻逼的调度器啦。我们现在的目标是,保持一切尽量的简单。这样才有兴趣继续干下去嘛。这样约定好了以后,我们开辟了两个队列,一个用于保存处于准备态的线程,一个用于保存挂起的线程。而当前运行的进程会由一个 current 任务结构体指针来标记。所以一个任务执行完之后会有两种状态,一个是挂起,一个是再次处于准备状态。这样我们就需要一个链表接口模块。

基础数据结构

我们用单链表结构来实现这两个队列。

include/totoro/taskq.h

struct tasklist {
	struct ttr_tcb *tcb;
	struct tasklist *next;
};

实现单链表的几个接口:

static struct tasklist *migrate(struct ttr_tcb *tcb,
                        struct tasklist **src,
                        struct tasklist **dst)

这个接口做任务迁移,将挂起任务迁移到准备态或者反过来。注意到,我们采用了二级指针来管理这个单链表,这样可以省掉很多不必要的条件判断。

static void insert(struct tasklist *node, struct tasklist **list);

这个接口用于插入任务之用。

extern ttr_err_t task_enqueue(struct ttr_tcb *tcb);

这个接口用于注册任务之用。

extern struct ttr_tcb *task_next(void);

这个接口用于提取下一个要运行的任务。

extern void task_init(void);

猜猜这是干嘛的。

 

任务结构

include/totoro/taskq.h

/* definition for Task(Thread) Control Block */
struct ttr_tcb
{
	/* stack pointer */
	volatile void *sp;
	/* current state of the thread */
	uint8_t stat;
	/* priority of the thread */
	uint8_t prior;
	/* thread's stack size */
	uint32_t stack_size;
	/* thread's stack */
	void *stack;
	/* thread function */
	void (*ttr_thread)(void);
};

看到这里,聪明的同学一定能看到了,我都帮你们注释好了每个成员的作用,因此也不赘述,大家如果看了代码应该不难发现,大部分关键的地方都 加上了注释。

 

任务优先级

include/totoro/taskq.h

#define TASK_HIGHEST_PRIORITY  ( 0 )
#define TASK_LOWEST_PRIORITY   (255)
#define TASK_DEFAULT_PRIORITY  ( 1 )

我们提供给应用程序一个默认优先级,这样用户就不需要做太多纠结了。

 

任务状态

include/totoro/taskq.h

#define TASK_SUSPENDED  ( 0 )
#define TASK_READY      ( 1 )
#define TASK_RUNNING    ( 2 )

 

任务内存申请

kernel/taskq.c

#define MALLOC_TASK_NODE(node, _tcb) \
	((node = (struct tasklist *)malloc(sizeof(struct tasklist))), \
	(!!node ? (node->tcb = _tcb, TTR_ERR_OK) : TTR_ERR_MEM))

#define DELETE_TASK_NODE(node) \
	if (node) { \
		free(node); \
		node->tcb = NULL; \
		node = NULL; \
	}

对的,我们实现了两个漂亮的宏来做任务结构体内存申请和释放,史上最小的 MM。

 

线程切换

是不是很紧张,终于来到破解魔咒的关键点。COME WITH ME!

arch/cortex-m0/gcc/port.S

    cpsid     i
    mrs       r0, psp
    subs      r0, #32
    stmia     r0!, {r4 - r11}
    subs      r0, #32

    ldr       r2, =ttr_running_task         
    ldr       r1, [r2]
    str       r0, [r1]

    ldr       r1, =ttr_running_task
    ldr       r2, =ttr_next_task
    ldr       r3, [r2]
    str       r3, [r1]

    ldr       r2, =ttr_next_task                
    ldr       r1, [r2]
    ldr       r0, [r1]

    ldmia     r0!, {r4 - r11}
    msr       psp, r0

    ldr       r0, =0xfffffffd
    cpsie     i
    bx        r0

这份代码是从 GCC 分支拷贝下来的,没有一行注释,大家可以去看 ARM 分支里面的同一个文件,写全了注释,我这个人很懒,不喜欢写太多注释。

1,关全局中断,保存程序栈指针,调整栈指针,保存当前线程的寄存器值;

2,保存当前运行线程的任务指针;

3,加载即将运行线程的任务指针;

4,加载即将运行线程的上下文环境;

5,恢复程序栈指针;

6,开全局中断,返回调用处;

 

驱动接口

drivers/clock, drivers/gpio, drivers/serial

Totoro 提供了 Clock, GPIO, Serial 底层驱动的框架与实现。参见代码。

 

信号量

等等,一个内核最基本的东西是什么?很明显,它需要线程切换,肯定也要一个线程同步机制啦。我们根据 POSIX 规范的接口实现了这样一个模块。

include/totoro/sem.h

extern int sem_signal(sem_t *sem);
extern int sem_wait(sem_t *sem, uint32_t timeout);

 

互斥锁

include/totoro/simplelock.h

#define simplelock_get()
#define simplelock_put()

 

Softirq

include/totoro/softirq.h

kernel/softirq.c

当然了,为了提供 Bottom Half 机制,用于提供用户定时任务,耗时任务,中断延迟处理我们加入了软中断模块。目前是用一个高优先级的内核线程实现,用一个简单的超时机制实现用户任务之间的均衡处理。

 

3 Test Cases

test/ab.c

test/sem.c

test/sirq.c

大家看名字就知道干嘛用的。我们也不再贴代码了,要玩就自己去看了。好的,整个微内核玩下来就是这么个流程。说简单也很简单,但要一步一步自 己做起来,还是需要花时间,耐心。我们现在只注重优雅的实现,简单,漂亮。

 

感悟

终于来到了个人时间了,大家有这样的感觉吗,看自己以前写的代码,竟然感觉很美妙。特别是用 plain text 模式浏览的时候,我仿佛在查阅某 上世纪大神写出来的神作。呵呵,开个玩笑。好了,这篇文章差不多就这样了,未来会怎样,我暂时也没什么想法。

Post a Comment

Your email is never published nor shared. Required fields are marked *