书籍推荐《Linux内核的设计与实现 第三版》
以下所有内容均以 Linux 为例
内核是OS的核心,它管理着系统的各种资源。
宏内核:kernel + 一些高级的虚拟接口(控制硬件)
简单的说,宏内核相当于一个是一个中央集权控制中心,把内存管理,文件管理等功能全部管理。PC上用的比较多,比如常见的windows、Linux。
微内核:提供操作系统核心功能的内核的精简版本,它设计成在很小的内存空间内增加移植性,提供模块化设计,以使用户安装不同的接口。
比如DOS、华为的鸿蒙。如嵌入式系统一样,可针对不同需求组装进来不同的模块。
存在理论实验中。为应用定制操作系统。
类似淘宝的多租户JVM,比如可以专门为浏览器定制一个OS。
一般的操作系统对执行权限进行分级(Linux分为0~3),分别为用用户态(ring 3)和内核态(ring 0)。大多数时间各类程序都是执行在用户态下。
内核态相当于一个介于硬件与应用之间的层,内核有ring 0的权限,可以执行任何cpu指令,也可以引用任何内存地址,包括外围设备, 例如硬盘, 网卡,权限等级最高。
用户态则权利有限,例如在内存分配中,有一部分内存是仅为内核态使用的,用户态code则不允许访问那些内存地址,每个进程只允许访问自己申请到的内存。而且不允许访问外围设备。另外在执行cpu指令的时候也可以被高优先级抢占。
为了保障OS的安全,用户态相较于内核态有较低的执行权限,很多操作是不被操作系统允许的。对于系统的关键访问,需要经过kernel的同意,以保证系统健壮性。
一个程序的执行过程,要么处于用户态,要么处于内核态。
进程是OS分配资源的基本单位。Linux中也称为task。
资源:独立的地址空间。里面存放着PCB、全局变量、数据段等信息。
PCB(进程描述符):PCB是维护进程信息的数据结构。每一个进程都跟着一个PCB。PCB的大小不固定,因为每个进程的信息不一样。
linux通过系统函数 fork() 来创建进程,通过exec() 来运行进程。从进程A中fork进程B时,A被称之为B的父进程
线程是执行调度的基本单位。
在linux中,线程就是一个普通的进程,它和其它进程共享资源(内存空间、全局数据等)。
和GC一样,线程也有后台线程,在OS中叫内核线程。它会在内核启动后做一些后台操作(如计时, 定期清理某些垃圾)。内核线程只在内核空间运行。
在java中,调用thread对象的start方法时,会调用native的start0方法,该方法将JVM中的一个线程对应上OS中的一个线程。这是一个重量级的线程,需要先从用户态切换到内核态,向内核申请资源,然后由内核态再切换到用户态。由于这种操作太重了,所以引入了fiber。
纤程是线程中的线程。它是用户态的线程,切换和调度时不需要经过OS。
优势:
基于以上优点,Fiber比较适合具有很多很短的计算任务的场景。
支持Fiber的语言有Go、Scala、Kotlin等。Java目前(14)不支持,需要利用Quaser库(不成熟)。Go语言最大的优势就是Go内置 Fiber,更适合并发编程。
父进程产生子进程后,会维护子进程的一个PCB结构,子进程退出,由父进程释放,如果父进程没有释放,那么子进程成为一个僵尸进程。
僵尸进程只占PCB,对系统影响不大。可通过 ps-ef | grep defult
来查看
子进程结束之前,父进程已经退出。孤儿进程会成为init进程的孩子,由init进程(1号进程)维护。
进程的调度由内核进程调度器负责。它决定该哪个进程运行,何时开始,运行多长时间。
Linux内核中每个进程都有专属的调度方案并且可以自定义。
Linux默认调度策略 :
实时进程犹如急诊病人,当有多个急诊病人时,按FIFO的方式排队,如果有相同优先级的急诊病人,则在此基础上按RR方式执行。当急诊病人(实时进程)处理完毕或主动放弃治疗(主动让出)后,普通病人(普通进程)才会按CFS算法得到诊断的机会。
中断是硬件跟操作系统内核打交道的一种机制。
在office软件中按下键盘上的一个键会发生什么?
首先,在键盘上按下一个键时,会触发一个电信号,这个信号会通过总线发送到中断控制器来处理,中断控制器检测键盘按下这个中断是否激活,如果是则将该信号发送给CPU,CPU接受到该信号后就会立即停止自己正在做的事,然后通知kernel,kernel再根据中断向量表查询出该中断的类型和要调用的中断处理函数(里面已经写好的一堆处理程序,如处理键盘,处理打印机等等),随后kernel调用相应的函数进行处理,最后再由office(应用程序)处理。
中断又分硬中断和软中断:
硬中断处理程序要确保它能快速地完成任务,这样程序执行时才不会等待较长时间,称为上半部。
软中断处理硬中断未完成的工作,是一种推后执行的机制,属于下半部。
硬中断中,有一张中断向量表来记录每一种中断信号的中断处理函数的关系。比如:
0x80H是所有软中断的信号,这个号通常对应的一堆的中断处理函数。比如 read(),write() 等等。
当一个应用程序想要读取网卡上的数据时,必须先经过内核来进行系统调用,此时它就会发出0x80信号来通知kernel。向ax寄存器中填入调用号(比如read函数是1号,write函数是2号,exit函数是-1号等),参数通过寄存器bx、cx、dx、si、di传入内核,返回值通过ax返回。
;hello.asm
;write(int fd, const void *buffer, size_t nbytes)
;fd 文件描述符 file descriptor , 比如fd=0表示标准输入,fd=1表示标准输出,fd=2表示标准错误输出
;buffer 文件内容
;nbytes 输出长度
section data
msg db "Hello", 0xA
len equ $ - msg
section .text
global _start
_start:
mov edx, len
mov ecx, msg
mov ebx, 1 ;文件描述符1 std_out
mov eax, 4 ;write函数系统调用号 4
int 0x80
mov ebx, 0
mov eax, 1 ;exit函数系统调用号
int 0x80
系统调用函数write接受3个参数,文件描述符、文件内容,输出长度,它们分别存放在ebx、ecx、edx中,随后向eax寄存器中填入调用号4,表示调用系统函数write,最后触发软中断。这样应用程序就会中断通知kernel它要进行系统调用write。再调用结束后再触发系统调用exit,告诉kernel系统调用结束。
常见的,比如java程序要读网络,首先在jvm层会调用一个read相关的函数,它会调用c库中的read相关函数,这时就会触发软中断,程序由用户态进入内核态,在内核空间执行系统调用处理程序,获取内容后,程序又从内核态恢复到了用户态。
从OS角度来说,一个程序在进行IO操作时,如果在用户态时被阻塞,需要等待,则该操作是BIO,如果用户态时不被阻塞,则该操作是NIO。
早期,内存容量有限,程序员必须将写好的代码分段,然后一段一段地转入到内存进行读取。为了解决这个问题,提出了虚拟内存的概念,并引入内存映射机制,将程序员从大量烦琐的管理工作中解放了出来。
早期的DOS,同一时间只能有一个进程在运行;目前的OS都引入了虚拟内存,可以将多个进程转入内存。由于内存容量有限,这就产生了两个主要问题:
为了解决内存不够用的问题,引入了分页机制。即将内存分成多份固定大小的页帧(通常为4K),把程序(硬盘上)也分成4K大小的块,在启动程序时,实际上就是将程序进行切分,然后和内存中的地址进行映射,并将内存映射信息保存到页表中。当程序在执行过程中,用到程序中的哪一块,就去加载哪一块。在加载的过程中,如果内存已经满了,通过LRU算法,将内存中最不常用的一块放到硬盘中的swap分区, 并把最新的一块加载到内存。
常用的缓存算法:LRU、LFU、FIFO
虚拟内存解决了多个进程相互打扰的问题。
虚拟内存的优势:
由于进程工作在虚拟内存,程序中用到的空间地址不再是直接的物理地址,而是虚拟的地址,这样,A进程永远不可能访问到B进程的空间。每一个进程都觉得自己独享了整个物理内存和CPU。
而实际的内存地址,通过MMU(内存管理单元,硬件),完成内存映射。类似汇编的偏移寻址,即偏移量 + 段的基地址 = 线性地址。