课程学习总结报告

作业要求

请您根据本课程所学内容总结梳理出一个精简的Linux系统概念模型,最大程度统摄整顿本课程及相关的知识信息,模型应该是逻辑上可以运转的、自洽的,并举例某一两个具体例子(比如读写文件、分配内存、使用I/O驱动某个硬件等)纳入模型中验证模型。

谈谈您对课程的心得体会,改进建议等。


 作为一种经典的操作系统,对Linux的学习可以让我们更好的理解操作系统的运行机制。

操作系统从本质上来说,是对硬件的一种抽象,让我们可以将与硬件管理有关的部分托管给操作系统,从而更专注于编程或上层应用程序的使用。

课程学习总结报告_第1张图片

而对于硬件的抽象,又可以分为多个部分:

  • CPU:进程的创建,调度,执行和销毁
  • 内存:缓存区管理,虚拟内存,地址映射
  • 硬盘:文件读写,访问权限
  • 外设:设备I/O中断

这些抽象就是操作系统所要实现的功能。要实现这些功能,基于两种非常关键的技术:特权级;中断。

下面结合本课程中学习到的关于Linux系统的知识来进行具体的分析。

特权级

在Linux操作系统中体现为用户态和内核态的转换。

两者的区别在于,内核态具有更高的特权,可以访问一些跟资源管理相关的特殊命令,因为如果任何人都可以直接去操作硬件,那将会极大地增加系统的不稳定性。比如,我们在日常的编程中,我们如果使用sudo去执行一些命令,经常会出现一些意想不到的情况,不得不重装系统...由此可见实现特权的分离是非常重要的。

另一方面,一个进程拥有内核堆栈和用户堆栈两种数据存储的结构,也可以从数据的层面去对两种状态进行分离,避免不同状态下的数据相互污染。

内核可以访问到任何的物理地址,这是它特权实现的原因。而用户进程只能使用逻辑地址,不知道最后会被映射到什么物理内存上执行,也是一种保证安全的机制,无法通过“推测”的方式去越界访问。

中断

广义上的中断可以进一步分为:异步的硬件中断,通常由硬件随机产生;同步的异常,通常来自于程序内部,可能是程序执行出现了除0,缺页等错误需要调用相应的程序进行处理,或者程序内部代码要获取某些资源,如读写文件,此时就需要陷入内核态去执行相关的系统调用。

中断的重要性在于,cpu的速度是远远大于其他硬件的,如果中途要与其他硬件进行同步,那么cpu强大的处理能力也就失去了意义。中断机制的存在,使得cpu可以与其他硬件并行处理,当其他硬件处理完毕之后,再发消息来通知cpu继续处理。

实现特权级别的翻转是中断的前提。一般在用户态执行的进程,需要进行中断或异常处理时,都必须翻转到内核态才能去执行那些特殊的处理代码。

中断的具体流程:

假定内核已经初始化,CPU在保护模式下运行

当cpu执行完一条指令之后,会检查当前是否有待处理的中断或者异常。如果有:首先确定与中断或异常相关的中断向量i,读idtr寄存器获得IDT表的第i项;从gdtr寄存器获得GDT的基地址,并读取相应的cs; 验证特权级别,防止低特权级用户访问特殊命令;在栈中保存eflags,cs和eip,如果是异常,把硬件出错码保存在栈中;装载cs和eip寄存器

课程学习总结报告_第2张图片

 

 

 中断或者异常处理完毕后,执行iret指令:用保存在栈中的值装载cs,eip和eflags寄存器;检查是否需要返回用户态,如果需要则继续装载ss和esp寄存器;检查ds,es,fs和gs端寄存器内容

Linux运作模型

 接下来,从一个整体的流程来分析Linux操作系统的运转过程。

在启动计算机之后,CS:IP被强制指向默认的物理地址,即开始执行BIOS程序,此时处于实模式。在这一阶段,中断向量表和中断服务程序进行初始化。接着BIOS利用初始化好的其中一个中断处理程序,将操作系统代码加载到内存中,此时转变为保护模式。

操作系统开始真正运行,接下来需要进行一系列的初始化。

首先在保护模式下初始化操作系统自身的中断服务程序,这是第一次初始化,用汇编代码实现。

课程学习总结报告_第3张图片

 

 主要是建立全局描述符表GDT,GDTR寄存器存GDT基地址,中断描述符表IDT,IDTR寄存器存IDT基地址。

在操作系统的其他汇编语言程序执行完毕后,操作系统跳转到main函数开始执行更多的初始化。

初始化的核心函数是start_kernel()

中断服务程序挂接

可以看做是中断向量表在操作系统中进行进一步的初始化,这里是用c语言实现的。主要是将中断、异常处理的服务程序与IDT进行挂接

课程学习总结报告_第4张图片

一些外部设备如显示器,键盘的挂接,就是在IRQ中挂接的

时间设置

中断跟时间是密不可分的,如进程调度的时间片轮转就必须靠准备的时间来产生中断信号。

首先,根据实时时钟RTC,操作系统获得了初始化的时间。

课程学习总结报告_第5张图片

 

 然后根据CLK频率,对于PIT, TSC等其他的计时器进行初始化。

初始化进程

start_kernel执通过init_task 生成0号进程,接下来就可以用fork产生其他进程了。

pid = kernel_thread(kernel_init, NULL, CLONE_FS);
...
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); 

第一个pid是在创建1号进程kernel_init, 后续主要用于管理用户进程

第二个pid是在创建2号进程kthreadd , 后续主要用于管理内核进程

这两个进程的创建都是调用的 kernel_thread 方法,用do_fork来实现。初始化了0,1和2号进程之后,接下来新的进程都可以统一用 _do_fork 来进行创建了。

根设备挂载

我们之前在实验中接触到了根文件系统的制作,制作好的文件系统也要作为加载操作镜像的一个参数

1 find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
2 qemu-system-x86_64 -kernel arch/x86/boot/bzImage -initrd rootfs.cpio.gz

在boot阶段setup程序加载了根文件系统,此时rootfs是一个虚拟的文件系统,需要在start_kernel()中用真正的文件系统进行替换。

课程学习总结报告_第6张图片

有了进程,中断向量,定时系统以及文件和外部设备,我们的操作系统就可以开始正常运行了。

当有新的任务需要执行时,首先通过系统调用进入内核态用do_fork创建一个进程,新进程放入到就绪队列里面等待执行。

当其他进程阻塞,或者时间片到达时,此时要进行进程的调度,通过系统调用进入内核态执行schedule进行进程切换。如果,中途与外设有交互,外设发送中断信号IRQ, cpu对信号进行响应并读取中断向量。保存现场,进入内核态,读取中断向量入口地址,执行处理程序,返回进程继续执行。

下面以一个驱动程序的注册为例,分析具体过程。

在Linux中字符设备和块设备都被映射到Linux文件系统的文件和目录,通过文件系统的系统调用接口open() write() read() close() 等,像文件一样去进行操作。

课程学习总结报告_第7张图片

 

 注册一个设备驱动就是把它与对应的设备文件连接起来,使得对设备文件发出的系统调用可以由内核转化为相应的设备驱动程序对应的函数,按照设备驱动程序模型,分配一个新的device_driver描述符,对应到设备文件上。

注册主要发生在内核静态编译时的内核初始化阶段或者在装入模块时动态注册。

注册完成后字符设备与对应的驱动程序完成绑定,当这一类型的设备需要I/O操作是,Linux系统调用会访问到设备的file_operations中重写的read() write() 等文件操作方法,实现对特定设备的各自不同的实现。

可以看出,通过这种注册方式,使得对上可以使用统一的系统调用接口,对下又可以兼容不同的硬件设备,更有利于维护和拓展。同时,如果把各种驱动都写进内核将会导致内核代码过于臃肿,通过这种模块化设计和按序加载,也使得内核的结构更为轻量。

课程总结

在本次课程中不仅学习到了底层的原理,了解了很多源代码,还通过实验有了更直观的认识。将整个Linux的运作流程串联起来,形成了整体的知识体系,进一步加深了理解。

虽然有很多细节的东西还是不太明白,但还是有非常多的收获。

 

你可能感兴趣的:(课程学习总结报告)