Linux操作系统
进程
新进程的创建时通过fork一个父进程实现的。就像我们写代码,从头写太麻烦,复制一个,改吧改吧。
0号进程:系统创建的第一个进程。这是唯一一个没有通过 fork 或者 kernel_thread 产生的进程。是内核进程。但0号进程不是一个实实在在可以看到的进程
1号进程:它将运行一个用户进程。是所有用户态进程的始祖
2号进程:管理所有内核态的进程,是后面所有内核态进程的始祖
用户进程和用户进程必须进行权限分割。内核态 用户态的概念
用户进程要访问核心资源,只能通过系统调用。
当一个用户态的程序运行到一半,要访问一个核心资源,例如访问网卡发一个网络包,就需要暂停当前的运行,调用系统调用,接下来就轮到内核中的代码运行了。
暂停的那一刻,要把当时 CPU 的寄存器的值全部暂存到一个地方
这个过程就是这样的:用户态 - 系统调用 - 保存寄存器 - 内核态执行系统调用 - 恢复寄存器 - 返回用户态,然后接着运行。

1 | $ ps -ef |
PID 1 的进程就是我们的 init 进程 systemd,PID 2 的进程是内核线程 kthreadd
其中用户态的不带中括号,内核态的带中括号。
所有带中括号的内核态的进程,祖先都是 2 号进程。而用户态的进程,祖先都是 1 号进程。
每个进程都有自己独立的虚拟内存空间
进程上下文切换
上下文切换主要干两件事情,一是切换进程空间,也即虚拟内存;二是切换寄存器和 CPU 上下文。
抢占的时机
真正的抢占还需要时机,一定要规划几个时机,这个时机分为用户态和内核态。
用户态的抢占时机
线程
对于任何一个进程来讲,即便我们没有主动去创建线程,进程也是默认有一个主线程的。
线程是负责执行二进制指令的。进程要比线程管的宽多了,除了执行指令之外,内存、文件系统等等都要它来管。
所以,进程相当于一个项目,而线程就是为了完成项目需求,而建立的一个个开发任务。
可不可以多进程代替多线程?
一个进程之内的线程共享内存。
进程之间内存相互隔离,进程间通信问题。
每个线程有私有的数据有两种,一是执行方法的的栈空间,
1 | $ ulimit -a |
二是线程私有数据。Thread Specific Data。这个存储时一个key,但各线程可根据自己的需要往 key 中填入不同的值,这就相当于提供了一个同名而不同值的全局变量。而等到线程退出的时候,就会调用析构函数释放 自己的value。
并发数据保护
互斥锁 Mutex
task
在 Linux 里面,无论是进程,还是线程,到了内核里面,我们统一都叫任务(Task),由一个统一的结构task_struct进行管理

系统调用
glibc,对系统调用进行了封装。用户进程一般通过glibc进行系统调用
一旦进行系统调用,那么用户进程中断,陷入(trap)内核态
用户态函数栈

栈基地址存的是前一个栈帧的地址,从里面拉取局部变量
内核态函数栈
内核态和用户态

调度
进程数目远远超过 CPU 的数目,因而就需要进行进程的调度,有效地分配 CPU 的时间,既要保证进程的最快响应,也要保证进程之间的公平。这也是一个非常复杂的、需要平衡的事情。
调度策略
进程大概可以分成两种:实时进程,普通进程
优先级,优先级其实就是一个数值,对于实时进程,优先级的范围是 0~99;对于普通进程,优先级的范围是 100~139。数值越小,优先级越高。从这里可以看出,所有的实时进程都比普通进程优先级要高。
普通进程使用的调度策略是 fair_sched_class 完全公平调度算法
在 Linux 里面,实现了一个基于 CFS 的调度算法。CFS 全称 Completely Fair Scheduling,叫完全公平调度
首先,你需要记录下进程的运行时间。CPU 会提供一个时钟,过一段时间就触发一个时钟中断。就像咱们的表滴答一下,这个我们叫 Tick。CFS 会为每一个进程安排一个虚拟运行时间 vruntime。如果一个进程在运行,随着时间的增长,也就是一个个 tick 的到来,进程的 vruntime 将不断增大。没有得到执行的进程 vruntime 不变。
显然,那些 vruntime 少的,原来受到了不公平的对待,需要给它补上,所以会优先运行这样的进程。
CFS 需要一个数据结构来对 vruntime 进行排序,找出最小的那个。–》能够平衡查询和更新速度的是树,在这里使用的是红黑树。

vruntime 最小的在树的左侧,vruntime 最多的在树的右侧。 CFS 调度策略会选择红黑树最左边的叶子节点作为下一个将获得 cpu 的任务。
内存管理
虚拟地址
虚拟空间一切二,一部分用来放内核的东西,称为内核空间,一部分用来放进程的东西,称为用户空间。
分段

其实 Linux 倾向于另外一种从虚拟地址到物理地址的转换方式,称为分页(Paging)。
对于物理内存,操作系统把它分成一块一块大小相同的页,这样更方便管理,例如有的内存页面长时间不用了,可以暂时写到硬盘上,称为换出。一旦需要的时候,再加载进来,叫作换入。这样可以扩大可用物理内存的大小,提高物理内存的利用率。
这个换入和换出都是以页为单位的。页面的大小一般为 4KB。为了能够定位和访问每个页,需要有个页表,保存每个页的起始地址,再加上在页内的偏移量,组成线性地址,就能对于内存中的每个位置进行访问了。
虚拟地址分为两部分,页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址。这个基地址与页内偏移的组合就形成了物理内存地址。


虚拟内存区域可以映射到物理内存,也可以映射到文件,映射到物理内存的时候称为匿名映射,anon_vma 中,anoy 就是 anonymous,匿名的意思,映射到文件就需要有 vm_file 指定被映射的文件。

每个 CPU 都有自己的本地内存,CPU 访问本地内存不用过总线,因而速度要快很多,每个 CPU 和内存在一起,称为一个 NUMA 节点。但是,在本地内存不足的情况下,每个 CPU 都可以去另外的 NUMA 节点申请内存,这个时候访问延时就会比较长。
每一个节点分成一个个区域 zone,zone内又分为多部份
有一块是DMA(Direct Memory Access,直接内存存取)的内存。DMA 是这样一种机制:要把外设的数据读入内存或把内存的数据传送到外设,原来都要通过 CPU 控制完成,但是这会占用 CPU,影响 CPU 处理其他事情,所以有了 DMA 模式。CPU 只需向 DMA 控制器下达指令,让 DMA 控制器来处理数据的传送,数据传送完毕再把信息反馈给 CPU,这样就可以解放 CPU。
内存映射mmap
其实内存映射不仅仅是物理内存和虚拟内存之间的映射,还包括将文件中的内容映射到虚拟内存空间。这个时候,访问内存空间就能够访问到文件里面的数据。
如果我们要申请小块内存,就用 brk。如果申请一大块内存,就要用 mmap。对于堆的申请来讲,mmap 是映射内存空间到物理内存。
另外,如果一个进程想映射一个文件到自己的虚拟内存空间,也要通过 mmap 系统调用。这个时候 mmap 是映射内存空间到物理内存再到文件。