从一个ELF程序的加载窥探操作系统内核-(3)
操作系统加载一个ELF程序看似一个EASY的动作,其实下面隐藏了很多很多OS内核的关键实现,让我们一起来解密其中的流程
作者是一个micro kernel的开发者,在设计动态链接器的时候,在此留下一些笔记,重点参考了以下资料文献
- 《程序员的自我修养》
- 《深入理解计算机系统》
- 《现代操作系统-原理与实现》
- 《深入理解LINUX内核》
- 《设计模式/JAVA》
进程和线程究竟有何区别
让我们先回到MCU的世界:
- 由于MCU没有MMU单元,所以在RTOS中没有进程的概念,只有任务的概念,任务调度是基于任务来进行的
- 也没有内核态用户态之分,RTOS下操作系统和用户程序都处于一种状态(在Armv7-M下为线程模式),所以用户可以修改内核的数据,而且一旦应用程序崩溃,OS内核也会崩溃,因为他们共享了堆栈空间
- 所有的内存读写都处于实地址模式下,也就是直接操作的是物理地址空间
现在我们有个需求我们需要把内核和应用程序分开,这样用户就不能随意修改我的内核了,在MMU没有出现之前这是没有办法实现的,当然还得要CPU支持两种模式
重中之重:页表
无论是内核态用户态的划分,还是多进程的实现,关键就在于页表的映射
- 当内核启动后,启动第一个用户任务,这个任务的tcb里包含了pagetable的数据结构,在2级映射中,一级页表其实就是一个4096的数组,二级页表是动态分配的。
- 在1号进程中,对地址空间进行了映射,首先内核的代码和数据,需要进行映射到1号进程的页表中,有小伙伴要问了,1号不是用户进程吗?为什么要映射内核的代码和数据给他呢?
- 其实这里有一个认识误区:我们区分的是用户态和内核态,不是用户页表与内核页表,对没有内核线程的OS来说,除了idle任务外,其他的任务都是用户任务,我们的任务调度实际上是在用户任务中进行
- 如果你不把内核资源映射给1号进程,会发生什么?当中断调度发生,此刻会陷入内核态,但是页表还是1号,此刻进入内核态执行的代码全是在内核地址空间,此刻是不是就有问题?所以必须要把内核的代码和数据映射给用户进程的页表
- 我们只需要设置用户页表里必须进入内核态才能访问访问内核的AP权限就可以了,这才是实现隔离用户空间和内核空间的关键
- 在linux经典的VMA空间布局中,每个进程的高端内存都是内核地址空间的映射,就是由此而来
理解了这个后,多进程其实就很简单了,多进程就是每个任务有不同的页表,虽然看起来大家都是从同一个虚拟地址(linux下是从0x08040000)开始,但是由于页表不同,最后对应的物理地址空间是不一样的。
线程:轻量级进程
首先,线程是依赖于进程的,一个进程下的多个线程共享了同一个页表而已
进程模型:通常我们称这样为一个进程
int main()
{
printf("hallo world");
return 0
}
线程模型:通常我们要先建立一个进程,然后再在这个进程上创建线程
void *hallo(void *pthread)
{
while(1)
{
printf("hallo thread");
sleep(1);
}
}
int main()
{
printf("hallo world");
pthread_create(hallo_thread);
return 0
}
在线程模型中,多个线程的TCB中的页表是相同的,都指向进程的页表,这就是轻量级进程的由来,地址空间的创建是消耗很大的
总结一下线程和进程的区别:
- 进程间的资源是隔离的,页表不同
- 线程间的资源是共享的,页表相同,可以理解成线程就是MCU下RTOS的任务模型
从上图可以看到
- 对于调度器来说,不管你是进程还是线程,对我来说都是以任务为单元来进行调度的,task1,task2,task3明显就是一个进程内的,他们的页表是相同的,task1是在fork+exec创建的进程,task2,task3是通过pthread_create创建的线程
- 从资源管理的角度来说,task1,task2,task3统称为一个进程,task4,task5为单独进程
|