从一个ELF程序的加载窥探操作系统内核-(3)

从一个ELF程序的加载窥探操作系统内核-(3)

操作系统加载一个ELF程序看似一个EASY的动作,其实下面隐藏了很多很多OS内核的关键实现,让我们一起来解密其中的流程

作者是一个micro kernel的开发者,在设计动态链接器的时候,在此留下一些笔记,重点参考了以下资料文献

  • 《程序员的自我修养》
  • 《深入理解计算机系统》
  • 《现代操作系统-原理与实现》
  • 《深入理解LINUX内核》
  • 《设计模式/JAVA》
进程和线程究竟有何区别

让我们先回到MCU的世界:

  • 由于MCU没有MMU单元,所以在RTOS中没有进程的概念,只有任务的概念,任务调度是基于任务来进行的
  • 也没有内核态用户态之分,RTOS下操作系统和用户程序都处于一种状态(在Armv7-M下为线程模式),所以用户可以修改内核的数据,而且一旦应用程序崩溃,OS内核也会崩溃,因为他们共享了堆栈空间
  • 所有的内存读写都处于实地址模式下,也就是直接操作的是物理地址空间

现在我们有个需求我们需要把内核和应用程序分开,这样用户就不能随意修改我的内核了,在MMU没有出现之前这是没有办法实现的,当然还得要CPU支持两种模式

  • 有了MMU,我们可以把内核和app分别编译到不同的地址空间去,这个由链接脚本指定就可以
  • Armv7-A中支持7种模式,我们使用其中的User和SVC这两种来对应用户态和内核态
  • 在页表的映射中,我们可以指定内核只能在内核态才能资源访问,这样在用户态就无权对内核地址空间做修改了
    if (iskernel) {
             /*
             * 内核页表
             * 内核代码段:只有内核态有RO权限
             * 内核数据段:只有内核态有RW权限
             */
            ap_ro = MAP_AP_KRO_UNA;
            ap_rw = MAP_AP_KRW_UNA;
        } else {
            /*
             * 用户页表
             * 用户代码段:用户和内核态都只有RO权限
             * 用户数据段:用户和内核态都拥有RW权限
             */
            ap_ro = MAP_AP_KRO_URO;
            ap_rw = MAP_AP_KRW_URW;
        }
    
重中之重:页表

无论是内核态用户态的划分,还是多进程的实现,关键就在于页表的映射

  • 当内核启动后,启动第一个用户任务,这个任务的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为单独进程
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值