阅读 1791

android 筑基 - 操作系统

本文编写过程有些长,经过反复折磨,可能看着有些凌乱,像系统学习的朋友,请先食用过推荐的视频课程之后再来看文章,文章内容也就是个总结性东西,看着和课堂笔记差不多,字数多了实在太卡了,我拆成多篇文章了~

前言

操作系统我觉得是软件领域最难学的部分了,绝对没有之一,太多的概念和知识点,要理清楚期中脉络,相互关系,然后串起来形成一个技能树,这也一个及其困难的事。更令人绝望的是绝大部分学习资料都不合格,我找了很久,有半年吧,终于找到了O看的资料,看完这些能帮你把整个操作系统学明白,所以轻珍惜这次的资料推荐还

有一点,操作系统虽然学完了你感觉没什么大用,能直接帮助你的很少,但是请不要因此请示对于其的学习,操作系统的内容是贯穿我们整个职业生涯的,所有的代码,软件,都是运行在操作系统上的,职业生涯中肯定会是不是碰到涉及操作系统的时候,这个时候你不会,相关的点你就吸收不好,干脆看不懂,很耽误你事。你要是遇到一点学一点,你会崩溃的,扯出泥巴带着跟,跟还连着别的苗,那学习效果懒得1B

android 面试一般估计不会问到这些,但是不要忽视操作系统的内容,操作系统里的很多内容其实是我们精进技术必须了解的基础知识点,操作系统不熟后面你看好多深入C层的技术会根天书一样,尤其是腾讯的 MMKV 了,看得懂吗,能理解吗,即便对着你讲你也会懵逼的吧

学习资料

主力资料:

  • B站:Y4NGY 老师的 Linux 课程,这位老师是南京那边学校的
  • 《Linux内核设计与实现》,容易上手

辅助资料:

资料食用教程:

  1. 小白们直接看 Y4NGY 的课程,最好的方式了,没有其它
  2. 辅助资料[1][2] 适合对 linux 有基础像快速过一遍的,时间加起来也就15分钟左右
  3. Y4NGY 老师的课程在内核调度这里要是打架看不懂没明白的,请食用 辅助资料[3],2者印证下来就没问题了

简单介绍下 linux

操作系统简单可以分成3部分:封装、抽象硬件的内核给用户程序提供服务的外层UI层

Linux 不是一个完整的操作系统,Linux 仅仅是一个开源的内核罢了,有好多基于 Linux 内核开发的操作系统:ubuntu/centos/opensuse/redhat这些

怎么理解,这些所谓的系统都是在 Linux 内核的基础上,添加了自己的一层UI界面和一些服务,还有软件源(类似于应用市场)罢了,包括 android 都是这样

Linux 的学习是很困难的一件事

  1. 入门 -> 推荐看《鸟哥的私房菜》这本书,先明白 linux 的几个组成部分,linux 的安装,基本的 Linux 指令,要学习的东西很多的shell 命令
  2. 深入 -> 推荐看《unix高级环境编程》,熟悉 posxi api,能编写 Linux 系统代码
  3. 然后就是学习 mysql/redis 等等基于在 Linux 环境上跑的软件了

所以说学习 Linux 是一件耗时很长的事

操作系统是个什么东西

操作系统:封装一切对硬件的操作、交互,在软件开发层面屏蔽硬件操作,只需关心代码逻辑即可。按马老师的说法,操作系统这东西就是一种特殊的软件,对上服务我们的程序,对下管理硬件

操作系统是60年代开始出现的,进化到现在也是经历了许许多多的。在没有操作系统的时代,我们写程序可不会像现在一样简单,只要专注于业务就行了

所有的操作我们都要自己去做关联,我们要自己去和硬件打交道,我们要自己控制内存如何存储数据,控制内存地址的变迁,往显卡写入数据,需要知道显卡的端口号,往显示器输出图像,需要知道显示器什么制式的,用打印机打印需要知道打印机是什么牌子的,每一家不同牌子的打印机支持的机器指令都是不同的

这样写程序本身是一件及其费时费力的事,而且写出来的程序只能在这个型号上的机器跑,换个硬件这个程序就不能用了,程序没有一点移植性可言,放到现在这是不可想象的,但是当年就是这样

后来人们发现这样不行啊,硬件一更新程序就要重新写,这样太费事了,根本不是程序应该有的样子。于是聪明的人想到应该把那些和硬件有关的操作都统一起来,屏蔽掉这些繁琐的和硬件之间操作,使用统一协议规范硬件之间的交互、指令,在程序开发时不用再考虑和硬件之间的操作,把这些交给上面我们封装好的和硬件交互的代码,所以操作系统诞生了

我这个解释估计不是很好,但是操作系统虽然是个复杂的哦东西,但是本质上不复杂,就是把所有根硬件的操作都封装起来,最后就变成了操作系统这个庞然大物。最合适的理解其实应该是早期的 DOS 系统

可能第一次接触的朋友还是不怎么明白,简单的说从啊做系统分2层:一层是封装硬件操作的内核;一层是给应用进程提供功能的外层,大家这么理解就行了,下面就说到内核了。要是还不理解,那么就记住操作系统是硬件的一层抽象

按照我新找到的学习资料来说,操作系统扮演的是一个 interface 接口的角色,软硬件之间的接口分3层:

  • 硬件 —> 硬件之间的接口: 典型的 USB 接口就是,使用总线相联,硬件提供中断命令和驱动给操作系统实现硬件的响应、使用、调度
  • 硬件 -> 软件之间的接口:
  • 软件 -> 软件之间的接口:

操作系统向下提供硬件->软件的接口,以实现软件操作硬件的可能性;向上提供软件->软件的接口,已实现用户程序对硬件操作的可能性和安全、权限管理

操作系统内核

一般操作系统都有这么一个内核,内核里面管理硬件,在内核周边运行着一些服务,来管理应用程序

操作系统分2层:内核态、用户态,这个内核指的就是操作系统内核了,内核的东西就是上面说的封装的那些对硬件的操作。这些和硬件的操作很多,除了内核的核心之外,基于封装思想还有5大功能模块:

  • 内存管理模块
  • cpu调度模块
  • 其他硬件设备管理模块
  • 文件系统管理模块
  • 进程调度模块

操作系统内核就是一个程序,而这5大功能又可以看成5大程序,当然操作系统内核还是有自己的核心的,一些基础的、杂七杂八的内容还是放在核心中的,核心中是操作系统最为核心的东西,核心和5大功能程序共同组成和操作系统内核,5个功能模块可以看成单独分离出来的核心的小弟,受核心管理,核心是老大,带着5个小弟,这个社团叫操作系统内核

严格抠字眼的话我这里应该不怎么对,但是大家要是不熟悉,之前没研究过的话,这么理解是最好的,对于做 Android 的来说,我么拿过来学习操作系统的部分不是为了开发操作系统的,就是为了夯实下基础的知识点,能理解就行了

宏内核、微内核

什么是宏内核,看字面意思,宏是大的意思,5大功能模块和核心必须安装在一起共同组成系统内核,这个就是宏内核,当然这样的内核会很大,会占用很多系统资源。PC,手机都是宏内核,win 启动后大家看看吃多少内存走就知道这种宏内核设计非常耗费系统资源了

什么是微内核,微就是微小,可以把系统内核做的很小,目的是减少资源消耗,微内核只有核心和进程管理2个组件。其他功能组件可以安装在系统内核之外的地方,这样系统内核运行时还是会去寻找相应的功能组件,有点像分布式系统

形象理解下:

  • 宏内核 - 内核这团大哥和小弟必须坐在一起办公
  • 微内核 - 社团本部有大哥和进程调度这个管钱的小弟就行了,其他人可以外派出去,也可以在本部呆着

2者的优缺点:

  • 微内核占用资源非常少,内核体积也很小,有的只有不到10M大小,非常适合物联网设备,物联网设备硬件有好有坏,有的需要Z合格功能,有的不需要,做成微内核这种分布式系统,需要的功能组件就填进来,不需要就不放进
  • 宏内核的优点是性能好,因为所有功能组件都安装在系统中,调用相关功能的时候可以直接运行。而微内核必须去寻找这个相关的功能程序,然后再把结果交换给内核,内核再通知用户,性能上没有优势

对于微内核来说,除了进程调度这个模块必须在打在内核中,其他的模块你想用就挂到内核中,甚至可以做成分布式的,挂在别的设备、芯片上

华为退出的鸿蒙就是微内核的,智能家居可以看成微内核应用,一屋子的设备,冰箱、电视、空调、扫地机器人、洗碗机,这些有一个总的控制器,这个总得控制器可以看成内核核心,其他设备可以看成不同的功能组件,用到哪个去找哪个就行,整体系统可以随时扩容或者瘦身

你说一张 SIM 卡能有多大性能,宏内核系统跑得起来吗,也只能是微内核这种系统啦,这种小微设备还有很多,恰恰这些小微设备就是物联网的基础

微内核的特性必然在物联网时代中大红大紫,微内核系统本身又和硬件尤其是芯片紧密相关,说不定物联网的时代华为麒麟+鸿蒙会占半壁江山也不说不定,现在头部公司都在大力推进、后进公司也在布局这方面

  • PC时代:inter + window
  • 移动时代:ARM + Android/IOS

我很期待物联网 IOT 时代是:麒麟+鸿蒙的,很期待

外核

外核也是一种核心,只不过是应用在科研领域,市面上的商业项目是没有的。起特点是可以根据具体场景,生成最适合这个场景运行的系统内核

像阿里正在研发的 JVM,内存分配不再是根据对象为基础分区,分代来了,针对高并发这种场景,每一个 request 进来,JVM 都会给这个 request 分配一块内存,request 结束时回收这块内存。不用再去遍历对象树,不用再去判断对象是不是死了,是不是要升级,最简单的就是性能最好的

还有阿里研发的多租户,这个不详说了

这个了解就行,顶尖大学里可能能看见这东西

VMM 虚拟化

VMM可以看成是一个虚拟层,VMM 又专用的应用场景:资源极端富裕。像有的公司只是泡泡一般的简单程序,但是服务器配置贼高,128个CPU,每个CPU8个核,内存几T,你说这样资源浪费不浪费。所以就又了 VMM 这个东西,可以在同一套硬件资源上运行多个操作系统,VMM 就是介于硬件核操作系统之间的虚拟核心

这个了解即可

从安全层面理解系统内核

这个是重点

早在 DOS 系统时代,一个程序想干什么就干什么,想控制哪个硬件就控制哪个硬件,想访问哪块内存就访问哪块内存,这个时代也是病毒天堂的时代,计算机是极度不安全的

为了系统安全,为了系统稳定运行

  • 硬件操作权限上,操作系统分成2层:内核态,用户态
  • 内存地址上,操作系统分成2块:内核空间,用户空间

用户程序是不能访问内核空间的,但是内核可以访问用户程序内存空间

目前,在硬件层面就可以实现对指令分级,inter CPU 上吧机器指令分成4个级别:ring0,ring1,ring2,ring3,Linux 系统只使用了 ring0,ring3 这2个权限级别,具体解释就是:

用户程序程序只能使用 ring3 级别的指令,而内核态程序就能使用 ring0 级别的指令

用户程序想用网卡读取数据,那么首先向系统内核申请 ring0 指令使用授权,操作系统内核使用 ring0 指令读到数据后,再使用 ring3 指令把数据交给用户程序。对于硬件来说是指令在 ring0/ring3 之间不断切换

再比如用户程序计算2+3,用户程序使用 ring3 指令生成2核3,然后向内核申请 add 计算这样的 ring0 指令的使用权限,系统内核使用 ring0 指定 add 之后把数据写回内存,用户程序用 ring3 指令就能读取到结果了

ring0 可以访问所有的内存,ring3 只能访问属于自己程序的那块内存

系统内核的功能都是通过内核函数对外暴露出来的,Linux 系统内核指令不多,就200多个,像 java 中 socket 操作,实际上是调系统内核的操作。创建线程,用户程序是干不了的,只能去找内核做操作,内核操作完了再通知你。JVM 什么级别,站在操作系统的角度,你JVM就是一个普通程序

操作系统层级结构

我们继续深化理解操作系统的层级结构,上面说了操作系统简单的就分2层:内核和外层服务,不直观,不好理解,虽然上面我们看了系统内核,但是这里我们还是要结合图示进一步看看

  • 蓝色是整个操作系统的范围
  • 上部分是操作系统外层,包括GUI用户界面,batch批处理,这个就是命令行界面了,command line 这个是命令行,这些都是针对平通用户而言的,这个就是 linux 里面的 shell 命令了
  • 中间的是用户接口了,这一层就针对的是广大程序员开发工程师了,这一层级提供了大量 API 可以用来调用系统调用、绘制界面、操作硬件设备,这些 API 的综合使用可以构建出优秀的用户程序,注意这些系统 API 还是C/C++ 的,很多语言 JDK 会对这一层做一层本语言的包装方便开发者使用,比如 java JDK 中的 GUI 开发 API,里面就是调用操作系统的系统 API,这一层级也是也就是我们常说的 native API
  • 再下一层是 system calls,这个是系统调用,系统 API 内部使用的就是系统调用。操作系统对硬件操作封装出来的方法就是系统调用,但是这些调用不能简简单单的全部对外开放直接使用,而是以系统 API 的方式提供对外使用
  • 最下面一层就是就是系统内核了,这里大家再感觉一下

系统API、系统call关系

有必要再强调一下系统 API 和系统调用的关系,这2个概念必须理解清晰才行:

  • 系统调用提供了访问和使用操作系统提供的服务的接口,这一层级的实现是操作系统级别的
  • 系统API是指名了参数和返回值的一组函数,应用app开发人员通过API间接访问系统调用

系统调用也是函数,只不过是操作系统级别的函数,可以理解为系统内核中的函数,这些方法因为安全和权限考虑不直接对外提供访问服务,而是通过经过考虑的、再次封装过的、可以对外提供访问服务的系统API来间接调用。系统API这一层的方法就不在内核中了,而是在内核外部

比如标准函数库里提供的 API:printf,可以显示器输出字符串,这个函数内部就是使用了系统调用 wirte,是一个从用户态到内核态,再从内核态切换回用户态的过程

还有几张有意思的图来说这个问题:

系统调用

理解到图中的这些内容就行了,更深入的有需求再去看,一般 app 层开发是用不到了

80中断

早期不是每一个系统东段都有对应的中断编号的,而是用统一用一个中断编号:0x80,80中断就是这么来的,用 0x80 代表系统调用,软中断

0x80 这个中断在中断向量表里保存了系统调用派发程序的入口,去系统调用表里根据调用编号找到处理函数入口

后来为了优化系统调用的性能,改为通过特殊指令触发系统调用,X86的 sysenterAMD64的 syscall,有个专用寄存器保存派发入口,不用再去中断向量表里查了

理解虚拟地址空间

常常被2个概念搞的头大~

  • 虚拟地址: 这是说内存寻址的
  • 虚拟内存: 这来源于 WIN 系统,说的是用硬盘来扩展内存的大小,把一部分硬盘当内存使

这2个概念不要乱,我们经常说的其实是虚拟地址这个东西,这个概念很多人都说,但是能讲清除的甚少啊,推荐大家看看B站佩雨小姐姐的这2个视频:

简单易懂,我算是看这个视频真正理解了虚拟地址

首先大家必须明确地址是干啥的,物理内存中每一个内存位都有在矩阵中,有自己的坐标,我们通过这个坐标来找到数据,坐标就是地址

物理地址就是内存位真实的物理存储位置

虚拟内存是操作系统内核为了对进程地址空间进行管理(process address space management)而精心设计的一个逻辑意义上的内存空间概念。我们程序中的指针其实都是这个虚拟内存空间中的地址。

早期我们直接使用物理内存地址

早期,那时程序都很小,我们都是直接把程序本身全部加载在内存中的。比如1个程序在硬盘中是2M大小,我们运行这个程序会把2M的代码全局一次性加载进内存,此时我们适用物理内存地址来访问内存

就行这样->

后面我们发现了其中严重的问题:

  • 进程不隔离带来的安全问题: 典型的 DOS 系统,病毒可以随意干什么,病毒进程可以随意修改其他进程的数据。因为进程间内存是不隔离的,为什么不隔离呢,因为大家用的都是真实的物理内存地址,我们可以访问其他进程的地址上的数据
  • 使用效率低: 要是运行的程序需要的内存大小超过了物理内存大小呢,系统会把部分内存数据写入硬盘,把硬盘当做内存的次级缓存,把节省出来的内存分配给需要的进程,这样会造成内存隔离,进程用的都是内存碎片,内存碎片会带来性能问题

后来产诞生了虚拟内存

正是因为上面直接使用真实物理内存地址带来的种种问题,我们不得不给真实内存地址上套一层,使用一个相互之间不通用的别名来代替真实内存地址,这个虚假的内存地址别名就是虚拟地址了

我就不画图了,大家脑补下,就好像我们给电报加密,进程A使用自己加密方式去使用内存地址,病毒进程即便拿到进程A的内存地址也无法定位到真实的内存地址

虚拟内存使用:分段、分页 技术进一步优化内存使用,具体由操作系统和CPU硬件中的MMU单元来管理

在计算机系统中,映射的工作是由硬件和软件共同来完成的。承担这个任务的硬件部分叫做存储管理单元MMU,软件部分就是操作系统的内存管理模块了

1. 分段技术

为了避免内存碎片的诞生,我们直接给进程分配一段连续的真实内存,然后使用虚拟内存映射到真实物理内存上

通过分段技术,实现了进程间内存隔离,进程之间不能访问其他进程的内存了,因为每个进程虚拟地址都是独一份,单独维护的,单独和物理内存映射的,其他进程拿到也没用,没有虚拟内存映射关系你是找不到真实物理地址的

2. 分页技术

分段技术远远不够,内存对于电脑来说永远是不够的,我们不能说进程你要多少内存就给你多少物理内存,一个电脑上同时运行的进程数有好几十,这点内存怎么够分,即便几T都不一定够分,所以我们使用了分页这个技术,实现按需分配

进程A你要60M内存,OK 虚拟内存层面给你,但是物理内存先不给你,等你运行时,需要一点内存我就给你分配一点内存,做到按需分配,这样就能实现内存的高效应用了

分页中的页指的是内存管理单元把内存安页这个基本的单位分配,一页是4K大小,虚拟内存中的页叫页,物理内存中的页叫页框,记录页于页框之间映射关系的叫页表

页的分配原则是按需分配,进程A告诉操作系统我需要60M内存,那么操作系统就先给了进程A60M虚拟内存,但是没给物理内存。在进程运行时计算真的需要1M内存,此时才分配1M物理内存给进程A,实现进程A虚拟内存于这1M物理内存的映射,等不够用了再分配物理内存,但是总量不能超过进程A启动时申请的60M虚拟内存这个阀值

3. 页于页框的映射关系

页的地址由:页码+偏移量组成

  • 页码 - 虚拟内存中页的位置,其实就是排序数,虚拟内存划分的最小单位就是页,假如说虚拟内存分10000个页,那么0x23这个页码数就是说第0x23个页
  • 偏移量 - 数据位于该页中的位置,一页是4K的大小,可以装好多对象了,内存又是顺序分配,所以这个偏移量就是数据在这个页中内存位置的首地址,页和页框中的偏移量其实都是一样的,大家想啊都是4K大小,在这4K中的位置能有区别嘛

4. 页表

每个进程都有自己独立的虚拟地址空间,这些地址空间需要通过页表映射到不同的物理地址

页表记录页于页框的映射关系,页和页框地址的偏移量,页表核心的就是记录页码和页框码了,看下图就是这个意思

最终CPU处理虚拟内存到物理内存就是下图:

5. 数据共享

大家想想要是能在2个进程中,要是都是指向相同的物理内存上,是不是就能实现跨进程内存共享啦,内存共享是进程间通信的一种方式

6. SWAP

swap 这个概念一直都不好理解,简单来说就是实现虚拟内存->硬盘的映射,下面是我找到的比较明白的解释

虚拟内存通过缺页中断为进程分配物理内存,内存总是有限的,如果所有的物理内存都被占用了怎么办呢?

Linux 提出 SWAP 的概念,Linux 中可以使用 SWAP 分区,在分配物理内存,但可用内存不足时,将暂时不用的内存数据先放到磁盘上,让有需要的进程先使用,等进程再需要使用这些数据时,再将这些数据加载到内存中,通过这种”交换”技术,Linux 可以让进程使用更多的内存

另一个物理内存管理要处理的事情就是页面的换出。每个进程都有自己的虚拟地址空间,虚拟地址空间都非常大,而不可能有这么多的物理内存。所以对于一些长时间不使用的页面,将其换出到磁盘,等到要使用的时候,将其换入到内存中,以此提高物理内存的使用率

当然,也存在这样的情况:在请页成功之后,内存中已没有空闲物理页框了。这是,系统必须启动所谓地“交换”机制,即调用相应的内核操作函数,在物理页框中寻找一个当前不再使用或者近期可能不会用到的页面所占据的页框。找到后,就把其中的页移出,以装载新的页面。对移出页面根据两种情况来处理:如果该页未被修改过,则删除它;如果该页曾经被修改过,则系统必须将该页写回辅存

为了公平地选择将要从系统中抛弃的页面,Linux系统使用最近最少使用(LRU)页面的衰老算法。这种策略根据系统中每个页面被访问的频率,为物理页框中的页面设置了一个叫做年龄的属性。页面被访问的次数越多,则页面的年龄最小;相反,则越大。而年龄较大的页面就是待换出页面的最佳候选者

最后要注意 Swap 和 mmap 的区别:Swap 是操作系统自动的内存到文件的映射,mmap 是用户主动的内存到文件的映射,后面会详说 mmap

Swap:表示非mmap内存(也叫anonymous memory,比如malloc动态分配出来的内存)由于物理内存不足被swap到交换空间的大小

7. PTBR、TLB

大家想啊,页表本身也是存储在内存中的,为了访问一个内存中的数据,要经历2次内存访问:1-> MMU 访问内存中进程的页表,获取对一个的物理内存地址,2-> 通过物理地址访问变量

减少 CPU 访问内存的次数是系统优化的重点,这里自然就有优化的点,要是 MMU 能直接计算出进程虚拟内存对用的物理地址,那就能减少一次访问内存的操作了,所以 CPU 结构中专门有一个寄存器会存储处于运行状态的进程、进程的物理内存首地址,这个寄存器就是:PTBR

后来物理内存分块分配,光有 PTBR 寄存器也不好使了,物理内存都是按块分配,不再是连续分配了,这时候就要在 CPU 中缓存进程页表了,于是又诞生了一个寄存器:TLB,该寄存器会保存进程部分页表,为了提高 TLB 的命中还有其他一些算法,这里就不说了,知道这个东西就行了,线程切换,进程切换,TLB 缓存也会跟着失效

进程

进程大家熟悉这个东西,很多人觉得知道是什么个东西就行了, 我知道它的特性啊,这些就够了呀,但是我还是要说请大家仔仔细细的把进程的所有学习一遍,这回解释很多模糊的地方,及其有学习意义

进程的概念

程序、进程、线程,这2个概念,面试的时候总是爱问,除了应付面试之外,我们其实也是应该能把这3个概念说清楚的

  • 程序: 程序就是一个可执行文件,是存储再硬盘上的一列列指令,就像 win 系统里的 .exe 文件,这是一个可执行的安装文件,解压缩我们可以看到好多好多的代码,只是这些代码现在都是静态的,都还没有运行起来
  • 进程: 当一个可执行文件被加载进内存,程序就变成了进程。进程就是已经被加载进内存的一系列相互关联的课执行指令。进程把第一条指令加载进内存,然后按顺序一条条的执行指令。进程本质就是程序计数器+运行时数据程序
  • 线程: CPU 执行的任务就是一个个线程,线程中有栈帧,栈帧就是一个个将要运行的方法和临时数据

面试时这样回答:进程是资源分配、保护和调度的基本单位线程是CPU调度的基本单位

并发并行

在继续深入进程之前,先把并发并行这俩概念搞清楚,很有必要

  • 并行: 多个进程在多个CPU核心中一起执行,执行时机是固定的,是一起执行的,相互之间没有资源的抢占和冲突
  • 并发: 多个进程在多个CPU核心中执行,可以是同时执行,也可以是你前我后,我后你前,执行时机是顺机的,相互之间有资源的抢占和冲突,比如对于CPU时间片的抢夺

进程的内存结构

得益于上面我们已经说多了虚拟内存的部分,大家知道了用户态和内核态,所有操作硬件的指令都必须要在内核态中执行,这里我们就好说多了

1. 虚拟地址空间结构

不管物理内存有多少,linux 系统都会给每一个进程分配4G的虚拟内存也叫逻辑内存

  • 0-3G的低位内存分配给用户态
  • 3-4G的高维内存分配给内核态

用户态的3G内存就是进程自己的,别的进程访问不了,但是这1G的内核态内存都会同一映射到物理内存中的内核内存部分

物理内存中,一般有1/4,最少1G的起始内存是分配给操作系统内核专门使用的,用户进程是没法映射到内核所属的物理内存的,但是操作系统内核却可以访问全部的物理内存地址,进程间通信就是通过内核映射的物理内存做中转的

从3G-4G空间为内核空间,存放内核代码和数据,只有内核态进程能够直接访问,用户态进程不能直接访问,只能通过系统调用和中断进入内核空间,而这时就要进行的指令权限切换

操作系统中的所有进程中的这1G内核内存映射到的都是同一段物理内存,也就是所有进程共享内核所属的物理内存,这点必须明确

最后我们总体的看一下这4G逻辑内存的结构:

2. 用户态内存结构

内核态内存先不说,我们来详细看看属于进程自己的这3G用户态内存,结构如图:

  • text: 这是代码段内存,保存就是每个程序指令的首地址
  • data、bss: 统称数据段,保存已初始化、未初始化的全局和静态变量
  • heap: 堆内存,存放的是运行时分配的内存,比如用 malloc 函数申请的内存块就是保存在堆中
  • stack: C函数运行使用的内存
  • shared libs 这是共享内存部分,共享函数库,mmap 内存到文件的映射用的就是这块,位于堆和栈内存的中间

注意:

  • heap 堆分配内存是从下往上分配,从低地址开始
  • stack 方向是反过来的

结合代码来看看:

int a = 100;

void f(int b,int c){
    int* p = malloc(100);
}

void g(int d){
    f(d,d+1);
}

int main(){
    static int e = 10;
}
复制代码
  • a 是全局变量,保存在 data 里
  • p 是malloc函数分配的内存块,保存在 heap 里
  • d+1 是函数运行时产生的,保存在 stack 里
  • e 是静态变量,保存在 data 里

大家可以对比 java 的内存模型看看,其实很像的,java 就是用自己的方式跑的C

3. PCB 进程控制块

进程都有自己的状态的,这部分也是有专门的内存块来保存的,这块内存就叫做:PCB,结构如下:

PCB 进程控制块很重要的,要理解的,后面马上就用到,PCB+用户态内存合成进程的上下文

再加一点,PCB 在 Linux 系统用是 task_struct 这个属性,看代码的时候要能反应过来,task_struct 描述的是进程的数据结构

  • mm: 描述进程的内存资源
  • fs: 描述文件系统资源,就是本进程的代码在磁盘哪里
  • filel: 进程运行过程中打开了哪些文件
  • signal: 信号处理函数

PID 的数量,进程的个数不是无限支持的,32位系统中最多支持 32768个进程,文件在:cat/proc/sys/kernel/pif_max

进程的状态

进程状态有2种说法:5状态、7状态,这里先说5的,理解了之后再说7的,7的就是在5上细分出来的

1. 进程5状态:

大家千万别和线程的装唉搞混了,虽然看着很像啊

  • 一个新的进程刚刚创建时,就是 new 的状态,此时需要等待系统分配资源
  • 系统分配完资源,此时进程就进入了 ready 就绪状态,此时进程等待分配 CPU 资源。
  • 进程运行后就是 running
  • 进程自然结束就是 terminated 结束状态
  • 需要细说的自然就是 waiting 等待状态了,此时进程在等待某些事件(中断)的结束。一个 running 状态的进程在没有抢到新的 CPU 时间片之后就会进入 ready 就绪状态,等待系统重新调度。running 状态的进程在发出中断信号之后会进入 waiting 的等待状态,等该中断执行完成后会回到 ready 就读状态等待系统重新调度

2. 进程7状态

  • 图中内有 new 和 ternimated 状态
  • Linux 中 running 和 ready 统称 running,但是我们还是要知道其实是2个的,在代码上看成一个罢了
  • 暂停状态: 就是字面意思,该进程被暂停了,而不是 warting 去了,暂停状态下只有我们再次唤醒进程才能继续运行
  • 僵尸状态: 进程死了,但是还留有一具尸体,只有父进程主动使用 wart方法回收尸体,进程尸体才会消失,否则进程尸体一直就在,kill 9 也杀不没。僵尸状态的进程所有资源都释放了,只有进程的PCB task_struct 还留存,其目的是告诉父进程子进程死亡的原因。创建进程时传进去的 state,父进程可以通过这个参数拿到子进程死亡的原因。再说一次僵尸状态资源都已经释放了,是系统主动释放的,绝对不会存在内存泄露的问题哈
  • 深度随眠: 一般系统调用都是深度睡眠,只有进程在 warting 的那个中断信号完事了,进程才能重回 ready 去排队执行
  • 浅度睡眠: 任何信号都能唤醒 warting 的进程,一般驱动程序都用的是浅睡眠

睡眠可以看成一种阻塞,结合后面进程调度的内容,不同状态,睡眠的进程都有自己的 warting 队列

看图,说的就是僵尸状态 state 的使用 ->

3. 进程状态码

  • R: TASK_RUNNING,可执行状态
  • S: TASK_INTERRUPTIBLE,浅睡眠状态
  • D: TASK_UNINTERRUPTIBLE,深度睡眠状态
  • T: TASK_STOPPED or TASK_TRACED,暂停状态
  • Z: TASK_DEAD - EXIT_ZOMBIE,僵尸状态
  • X: TASK_DEAD - EXIT_DEAD,退出状态,即将被销毁

4. 子进程被杀死后,我们说进程会变成僵尸进程,但后等待父进程回收,怎么理解呢?

进程被杀死后其实4G用户空间中,高位的内核态部分并没有被回收,依然残留有一些关键信息,比如 PCB 依然还存在没有被回收

父进程可以拿到进程的残留的 PCB,可以知道子进程的死因等一系列信息,父进程回收子进程是指父进程把子进程高位内核态地址全部回收,此时 PCB 会被销毁

进程切换的概念

什么叫进程切换,就是进程是去了 CPU,这里大家先不考虑同一个进程内多线程的状况,这个后面到线程时再说。造成线程切换的唯一原因就是中断了

中断就是一个信号,每个中断源都有编号,内核在接受到中断后,会看看中断号,就能找到对应需要执行的任务,根据不同的中断源来选择handle处理。内核中有一个中断向量表,存的就是中断的对应任务,外部硬件的中断都是依赖驱动程序注册到系统内核中的,要不系统怎么知道你这个硬件要干啥啊

中断是用户态和内核态之间切换的唯一原因

中断是指程序执行过程中,当发生一个事件时,会立即终止在CPU上执行的进程,然后马上执行这个事件对应的任务,该任务结束后再恢复这个进程继续执行程序

中断类型

  • 内部中断: 来自 CPU 外部的中断,这个又叫:硬件中断,注意单次是:interrupt,中断来源:
    • 键盘,IO 设备等外部硬件,这些硬件在操作时都会产生一个硬件中断信号
      • 外部中断都是异步中断,因为这些中断谁也不知道会执行多久,我也需要等待结果,所以没需要等着
  • 外部中断: 来自 CPU 内部的中断,注意使用的单词时:Exception,系统异常其实都是一个个事件,这和 error 是不同的,是系统在运行过程中自己发出的事件,中断来源:
    • 硬件异常:掉电,奇偶校验错误
      • 程序异常:非法操作,地址越界,断点,
      • CPU 时间片到期
      • 更高优先级的进程操作
  • 软件中断: 由软件程序发出来的中断,前面2个都是硬件设备发出的中断信号,但是软件同样也有这样的需求
    • 软件中断只有一个中断号:0x80,这就是常说的80中断,具体解释后面有

内部中断也叫等待资源,外部中断也叫等待信号,有的地方说进程队列中等资源、等信号啥的大家要能反应过来,也许这样说不怎么正确,但是请这么理解。大家想啊,CPU 以外的硬件不就是系统的硬件资源嘛,这样想具理解了

进程调度

大家回忆上上面说的系统内核,内核中的一个功能模块就是进程管理,所以对进程的任何变化都必须在内核态中执行,也就是说操作进程的指令都是 ring0 级别的

内核进程管理模块使用队列来管理进程,不同状态的进程分别在不同的队列中排队,每个中断源都有自己专属的进程排队队列,比如进程A和进程B都因为要操作IO设备而触发了IO中断信号,在IO处理完之前,A和B都淂在IO中断队列里排队,看下图,比较形象了

具体A和B用户态到内核态的切换过程大家再去上面系统调用哪里看看,每个系统调用都会发出对应的中断信号的 Ψ( ̄∀ ̄)Ψ

大家不好奇队列里存的时什么吗,不卖关子,队列里存的是进程的 PCB,通过 PCB 就能代表一个进程了,就能找到一个进程的位置,内存数据,所以没必要把进程所有数据都装进来

进程在排队结束后不一定会按照进入的顺序再获取 CPU 资源执行自己的任务,具体的要看操作系统采用哪种进程调度策略,抢占式大家都熟悉吧,完事了有资格的进程去抢CPU时间片

PCB内存的作用再看下图:

进程切换过程

再次重申一遍,中断是用户态和内核态之间切换的唯一原因。先不考虑多线程的问题,这个之后说

进程的切换是进程丢失 CPU 再获取 CPU 的过程,也是从用户态切换到内核态,再从内核态切换回用户态的过程,这个过程值得仔细看看

还记得上面说的进程上下文吗,回忆一下,还有 PCB 这里就用到了

切换过程:

  • 保存被中断的进程的上下文信息
  • 修改被中断进程的状态
  • 把被中断的进程加入对应的中断队列
  • 执行中断任务
  • 中断任务执行完,调度一个新的进程并恢复它的上下文

啊,又是上下文切换,线程切换有上下文切换,进程切换也有上下文切换,进程的上下文就是 PCB+用户态内存。进程保存在主内存中,获得CPU执行任务要把相应的方法和数据写入CPU缓存中的,当中断信号来了,不管是主动的还是被动的,都淂让出CPU给别的进程使用,这时候我们要保存进程当前执行的位置、线程,以实现之后抢到CPU再回到现在的点继续执行任务

注意这5个过程都是在内核态中执行的,进程上下文的保存和切换都是在内核态由内核代码执行的。操作系统由一个 load PSW 指令就是专门恢复进程现场的,重新加载进程上下文

这个过程不是一瞬间就完成的,也是耗时的,进程要是老是切来切去的,一样会浪费大量性能,所以减少进程的切换也是一个优化性能的重点

fork() 函数

fork 函数是创建子进程的,这里强调这个函数是因为对于后面学习非常有意思

PCB 里有2个参数 ->

  • PID: 进程ID
  • PPID: 父进程ID

PID = fork(); fork 函数是有返回值的,返回的是子进程的PID ->

  • -1: 子进程创建失败
  • 0: 创建出来的子进程还没有子进程,所以这个数是0
  • 非0: 这个就是子进程的PID了

通过这个方法我们一般可以确定进程的父子关系

fork 函数的特点:新创建一个空白进程出来,然后分配资源,把父进程的所有数据完完全全的拷贝一份放到子进程内存中,父进程 fork() 之后的代码,子进程一样会在这里开始继续开始运行

看到 fork 这里大家惊讶不惊讶,会把进程的内存打包复制一份给子进程,做 android 的朋友们注意来,android 是大量用到 fork 了的,明白 fork 对于理解 android 很重要的

main{
    int* a = 1;
    fork();
    printf("AA")
}
复制代码

就这段代码,fork 之后生成的子进程会继续执行 fork() 之后的代码,结合到这里就是 AA 打印了2次

wait() 函数

main{   
    int* a = 1;
    int* PID = fork();
    wait(PID);
    printf("AA")
}
复制代码

上面代码加上一个 wait() 函数,wait 和 java 里的一样,其实应该说 java 的 wait 就是用的 C 的 wait,加上 wait 之后父进程会等待子进程执行完成后再执行自己,这个就是一个深度睡眠了,父进程在 wait 这个调度队列里一直等着,等着 wait 这个特定的中断完成再把自己调度回 ready 队列

孤儿进程问题

父进程里面启动了一个子进程,然后这2个进程并发执行,系统回倾向于先执行父进程,当然也可以设置倾向于执行子进程,这样就有一个问题,要是在子进程执行完之前,父进程先结束了,那么子进程的 PPID 就有问题了

Linux 里面进程都是父子关系的,进程不能没有爹,你爹要是挂了,系统回再给你动态的找个爹 ┗|`O′|┛ 嗷~~ ,这样 PPID 也会跟着变

  • 当有 subreaper 进程时,系统把把离你最近的 subreaper 进程当成你新的爹
  • 当没有 subreaper 进程时,系统会把你托管给系统最初的进程 SYSTEM,这个进程的 PID 是 1

copy on wirte

copy on wirte 说的就是 fork 子进程的事,我们说了 fork 出来的子线程把父进程的所有数据的都拷贝饿了一份,进程调度队列中存的是什么,是 PCB 啊,PCB 就可以表示一个进程,fork 的过程就是复制了 P1 的 PCB 给 P2,此时 P1 和 P2 的 PCB 是完全一样啊,看 PCB 图:

系统会复制 P1 DE PCB 给 P2,mm 段表示内存,那此时 P1 P2 的内存地址都是一样的,那么操作都是相同的数据了,copy on wirte 发挥作用的就在这里

子进程创建出来后,使用的还是父进程的页表,不过系统会把父进程页表改成 RD-ONLY (只读)的,当子进程修改数据时,系统看到 RD-ONLY 会触发缺页中断,把新的页分配给子进程,把父进程的数据 copy 一份给子进程。虽然父子进程之间的虚拟内存页表地址都一样,但是指向的却是不同的物理地址,新的内存页就有写权限了

fork 内部使用的是 clone(),clone 这个函数,这个函数非常灵活,可以选择把 PCB 的哪些数据段复制给 P2,哪些数据端共享

总结下:P1 把 task_struct 对拷一份给 P2,一开始是一样的,但是只要 P2 改了就变了,P2 是在自己的那份上改的,内存最难对拷,只要 P2 修改资源了,就重新分配内存页给 P2,那P2就是自己的一份了,谁先写谁得到新的物理内存地址,父进程先写,那父进程就获得新的物理内存地址

vfork() 函数

vfork() 函数使用 clone() 函数,PCB 其他的数据段都复制一份,mm 内存段则共享,这样一来子进程操作的就是父进程的数据了

fork 的点到这利就差不多了,知道怎么回事就行,又不是做 Linux 开发的,没必要深究~

还有一点,没有 MMU 单元的 CPU,没法使用 fork() 函数

进程中主次线程的问题

main{
    ......
}
复制代码

Linux 进程中默认都会有一个主线程,这个线程不用大家自己去new,系统在创建进程时一块iu创建出来了,这个线程就是主线程,看见main函数就代表主线程了。

再创建的其他线程都叫子线程,有个特点一定要知道:一旦进程中的主线程结束了,不管这个进程还有多少子线程,这些子线程是不是还在执行任务,这些子线程都会跟着你一块结束。也就是说进程中主线程的结束代表这进程的死亡,所以进程的主线程一般都是设计成循环遍历的,空闲时会阻塞

android MainThread 里的 main 函数熟悉不熟悉,Linux 这块你没学过,你能理解到精髓码,你能真的看得懂吗,多半都是猜吧,猜就不免会有疑惑、顾虑,这就不叫学明白,大家要清楚这点

线程

什么是线程

线程大家耳熟能详了吧,不过多说概念了,Linux 中线程是基本的调度单位,面试说这个就行了

每个进程都有自己的主线程,在进程创建时系统默认就会把主线程创建出来,再创建的线程都是子线程,对于进程来说,线程就是进程内的多个执行流

线程共享进程数据、资源,PCB 进程控制块可以找到进程所有的资源,PCB 就可以代表一个进程,对于线程来说既然我们要共享继承的资源,我们怎么做最简单,把 PCB 复制一份就行了,寄存器的值就存线程自己的就行了。复制出来的 PCB 可以作为线程的控制单元,改名叫:TCB

线程私有的资源也就是线程栈和寄存器临时数值了,线程栈在用户内存中,寄存器临时数值在内核空间中,PCB 也是在内存中,也就是说复制一份 PCB 出来,生成 TCB,TCB 的 registers 重置一下就行了,PCB 和 TCB 在 Linux 中都是 task_sturct 结构体

代码上复制 PCB 最方便的方式就是 lone() 函数了,clone() 函数必会复制一份 PCB 出来,Linux 创建进程的函数 pthread_create() 用的就是 clone() 函数

TCB 中的数据基本都是复制了一份 PCB,但是注意啊,mm 是和 PCB 共享的,也就是用的是同一份数据,而没有选择复制,TCB 之间 mm 也会是用的同一份,看图:

线程创建模式

其实我们所说的线程指的是 用户线程,也就是这个线程是运行在用户空间中的,但是我们要是需要访问硬件资源怎么办,必须要切换到系统内核也就是内核空间中啊,系统提供了对应的 内核线程 这个东西,下面我们说的线程创建模式就是用户线程和内核线程的相互关系

1. M:1 多对一模型

多个用户线程对应一个内核线程: 蛋疼,要是哪个用户线程耗时太长,那别的用户线程就别想执行了,这显然是不行的,所以也没有操作系统哟尼姑这种线程模式

2. M:M 多对多模型

多个用户线程对应一个内核线程,几个对几个就不固定了: 这个问题就是实现起来比较复杂,目前也没有操作系统用这个线程模型

3. 1:1 一对一模型

有1个用户线程就有1个内核线程: 性能和复杂度的中和,目前基本用的都是这个线程模型

线程库

Thread Library 是为程序眼创建、管理用户线程服务的,不同操作系统有不同的线程库

  • POSIX Pthreads: 这是 linux 的线程库 API,可以创建出用户线程和内核线程
  • Windows Threads: win 平台的
  • java Threads: java 因为要跨平台嘛,所以具体要看目标的操作系统了

PID/TID

  • PID: 进程的ID,其实就是线程所属进程的 PCB 号
  • TID: 线程自己的 TCD ID 号

getpid() 函数获取 PID,gettid() 函数获取 TID

内核调度器

内核调度的是什么,就是CPU啊,内核调度器决定哪个任务执行,哪个任务排队,不管是单核心,还是多核心都是依靠内核调度器来调度计算任务的

内核调度的是什么

还记得面试时我们对于线程的回答吗:线程是系统调度的基本单元,这里展开一下

对于内核调度器来说,没有什么进程线程的概念,只有 task_sturct,也就是 PCB、TCB 这东西,内核调度器遇到 task_sturct 就可以去调度

TCB 我们知道它代表资源,那怎么理解 PCB,它可是代表的进程啊,最小调度单元不是线程嘛,干 PCB 什么事。大家还记得不,Linux 进程一创建,系统会马上创建给进程创建出一个默认的线程出来,这个线程就是主线程,主线程有 TCB 吗,没有,PCB 就是 Linux 进程主线程的 TCB,也许这么解释不是很正确,但是我觉得这么理解就行了

所以上面讲的进程切换的东西在这里都适用~

内核调度带来的性能损失

涉及到的几个方面:

  • cpu 指令切换
  • 上下文切换
  • 进程切换
  • cache miss

CPU性能损失 -> Linux 采用了2种机器指令权限范围,ring0,ring3 内核态可以使用,ring3 用户态可以使用。CPU 有自己的指令缓存寄存器、高速缓存的,平时都会缓存你这个操作级别对应的及其指令的,任何用户态和内核态的切换都会造成指令寄存器和缓存的无效,要重新加载,这里有一点性能损失

上下文切换性能损失 -> 用户线程运行在用户空间,内核线程运行内核空间,线程的切换必要要在内核中进行,这样不光用户线程切换带来性能损失,内核线程切换一样会带来性能损失。线程有自己数据,在内核调度器中就是 PCB、TCB,这些数据是要加载到 CPU 缓存中的,CPU 一切换任务,这些数据都要离开 CPU 缓存,把新线程的数据加载进来

进程切换 -> 你 CPU 前后切换的线程要是分属不同进程,那会还会造成进程切换,上下文切换的范围更大,CPU 缓存内进程的页目录要切换,TLB 缓存会失效

cache miss性能损失 -> cache miss 是什么,是缓存命中无效啊。为了减少 CPU 等待内存读取数据的等待时间,有个缓存命中的技术,会有把相关的数据都加载进来,有时候一整页 4K 的数据都会加载进来,L3、L2、L1 都会有,你一切换线程,这些为了缓存命中加载家来的数据都无效了,这叫缓存命中无效或丢失,还得重新加载一次

你的线程要是没事切来切去,这些性能损失也不小了,一般会占 CPU 时间片的 1% 甚至更高

内核调度原则

这里我先说一个参数和2种任务类型 ->

  • 相应时间: 从提交任务到第一次相应的时间
  • CPU 密集型任务: 像学科计算这种需要占用很多 CPU 时间的任务
  • IO 密集型任务: 像访问资源这种不怎么需要占用 CPU 时间,但是需要大量等待访问资源时间的任务

这3个东西是放在一起说的,比如像鼠标操作,他是不占 CPU 时间的,需要的就是操作系统及时反应我们按键盘就行了,要是响应时间太长了,那就是卡顿了,像有大量用户交互的系统,响应时间是最重要的指标

那磁盘操作这种 IO 任务,也需要及时响应,及时去放访问资源就行了,然后我等着呗,这个过程中可以释放对 CPU 资源的占用

要是大家都排队执行的话,一个 IO 型任务长时间占用 CPU 但是不用,这对 cpu 型任何是没法接收的。要是按照谁快谁来这样排队的话,那 IO 任务永远没有执行的机会了

所以 ARM 平台针对这2种类型的任务,专门推出了 big.LITTLE 型 CPU 架构,说白了就是大小核设计,大核计算能强,小核功耗小。系统会把 CPU 型任务度放到大核中执行,系统会把 IO 型任务放到小核中执行

这种针对任务类型设计的 CPU 架构需要系统架构同步的去这样设计,效果就是实现用 4个核心的功耗实现7个核心的计算能力,这样带来的成本压缩、功耗下降对于移动平台来说至关重要

android RXjava,kotlin 的协程都设计有 cpu密集型任务线程池和 IO密集型线程池,也是为了响应硬件上的设计思路,所以大家看看国外程序眼的眼界多高,代码都可以迎合硬件架构思路做优化,这个点太 NB 了,我太佩服了

内核调度策略

1. FCFS 先来先服务

说白了就是排队,排在前面的先执行,排在后面的后执行,只有前面的执行完了,后面的才能执行,不能插队 问题也很明显,前面的执行太慢,后面的任务响应时间就没法预测了,对于鼠标键盘来说, 这种策略是不能接受的

2. RR 时间片轮转

这个大家就熟悉了,限定每次 CPU 执行的时间,大家还是按照顺序排队,CPU 时间用完了就换下一个,然后自己到队尾接着排队 好处是公平了,但是问题时对于键盘鼠标还是不能接受,我键盘鼠标要是长时间连着操作呢,不能一会一卡吧。一般时间片选择在 10ms-100ms 之间

3. SJF 最短作业优先

这个就是预判谁的任务执行时间最短,谁就执行,然后比较下一个 思路还可以,但是问题是这个时间执行时间怎么预测啊,不可预知的东西太多了,现在也没有成熟的算法,所以这个目前也是没人用,但这是目前研究的一个方向,现在的成果是:记录线程之前平均运行时长来作为参考

4. PRIORITY 优先级调度

优先级高的先执行呗~

5. SCS/PCS

SCS/PCS 是简写,是2种线程调度模式

  • SCS: PTHREAD_SCOPE_SYSTEM,所有的可以公平的去竞争所有 CPU
  • PCS: PTHREAD_SCOPE_PROCESS,进程先去竞争 CPU,然后该进程内部的线程再去竞争

linux 使用的 SCS 模式,Thread.scope 参数表示的就是这个,原因很简单,Linux 采用 1:1 线程模型啊,每个用户线程都有自己对应的内核线程,内核线程可以去抢 CPU 的呀

Linux 调度策略

Linux 中 把优先级分成 [0-139],数字越小,优先级越高,下面说的任务和线程是一回事

Thread 里有个 Scheduling prlicy 参数,这个就是线程策略,Linux 系统根据线程要求响应的不同分成2大的调度策略:

  • Real-time Schduling: 实时调度策略,一般内核线程都是这种调度策略,内部使用 FIFO+优先级的思路,每个优先级都有一个队列,优先级高的队列先执行,相同优先级的按照顺序排队运行。若是有个高优先级的来了,就得让给这个后来的优先级高的线程

    • SCHED_FIFO 默认是这个
      • SCHED_RR
  • Normal Schduling 一般任务,用户线程都是这个级别的,使用 RR+优先级的思路,不过就不是优先级高的运行完了才能等到优先级低的,而是可以同时枪,区别是优先级高的运行时间长了,就把你优先级调低,优先级低的一定时间轮不到你,就把你的优先级往上调,这个幅度一般是 +-5。这样做的目的就是为了大家都能轮到执行,不会说你优先级低就等到最后

    • SCHED_OTHER 默认是这个
      • SCHED_IDLE
      • SCHED_BATCH

0-99 对应是 Real-time Schduling 实时线程,100-139 对应的 Normal Schduling 普通线程,Linux 早期,100是-20,139是19,0是-139,具体看你的 Linux 版本

对于100-139的普通线程来说,优先级高的线程对优先级低的线程不具有绝对的优势,100的线程比110的线程,就是执行时间更长,在从 wrating 到 ready 的时候,100的线程能抢到时间片

普通线程的优先级还有 Nice 这个参数,用来动态调节优先级的,nice 的取值范围:[-20,19],nice 越大优先级越小,nice 算是一个惩罚机制,你运行的时间太长了,把你 nice 值调大,你优先级就降低了,留出机会给其他线程

普通线程也是有 IO型任务的,IO型任务响应一定要快,Linux 系统本身就会照顾 IO 型的任务,所以就诞生了 nice 这个值,没有 nice,怎么实现照顾 IO型任务,尽量让 IO 型任务得到及时响应呀

RT 补丁包

后来 Linux 出了一个 RT 补丁包,可以设置实时线程一段时间内占用 CPU 时间的最大值,比如 1000 个时间片,通过 RT 可以设置实时任务最多占900个,剩下的留给普通任务。大家想啊,这个设计也是合理的,内核任务你要是跑起来没完没了,后面的普通任务,用户程序的任务怎么执行,不能一直都卡在那里吧,要不用户体验就糟糕死了

CFS

RT 补丁包了对于普通线程还添加了一个调度算法:CFS 完全公平调度策略,CFS 会计算出一个虚拟时间,谁的虚拟时间小,谁执行,采用红黑树的数据结构

计算公式:累计运行时间/权重

权重和优先级转换:

在最求虚拟时间相等的前提下,权重越小,虚拟时间最大,想要虚拟时间小,就得累计运行时间长,也就是得到 CPU 时间片的机会更多

cgroup

cgroup 可以让我们给线程划分群组,可以让该群组运行在某个核心上,或者某几个核心上,或者该群组的线程优先级更高,获得 CPU 的机会更大

android 系统上分了2个群组:

  • apps: 前台 app
  • bg_non_interactive: 背景非交互的,app 不再前台了都是这个
  • apps: cpu.share = 1024
  • bg_non_interactive: cpu.share = 52

数越大权重越高,获取 cpu 的机会越大,在前前台运行的 app 能够更大的抢到 CPU

查询步奏:

  • root@XXXX:/proc/6566 # ps | grep -i "video" adb shell进入已经root的Android设备终端,获得进程的pid
  • adb shell cat proc/6566/cgroup

结果:

  • cpu:/(前台进程)
  • cpu:/bg_non_interactive(后台非交互进程)