操作系统
内存
地址空间
物理地址空间
硬件支持的地址空间,由硬件维护分配。
逻辑地址空间
例如进程的地址空间,指的是进程可寻址的虚拟内存组成。
物理地址空间和逻辑地址空间之间建立映射是由操作系统完成的。
例如:CPU需要执行一条指令,提供逻辑地址向MMU缓存中获取对应硬件地址,如果有则返回地址并向内存获取内容,如果没有就会到内存中的页表找,找到物理地址后内存通过总线传给CPU。
操作系统维护内存访问的安全
为了保证程序之间访问内存互不干扰保证安全,操作系统会保存每个程序允许访问的逻辑地址空间表,并在每次寻址时判断是否越界。
内存分配
内存碎片
内存碎片指的是无法被操作系统或者程序使用的内存空间段
外碎片
指的是分配单元之间无法使用的内存块
内碎片
在分配单元中程序无法使用的内存块
连续分配内存
分配给程序的物理内存必须连续
存在内碎片和外碎片
内存分配后动态修改困难(前后可能都被人用了)
内存利用率低
首次适配
在内存中寻找到的第一个可分配的内存块。简单,容易产生外碎片
最优适配
在内存中寻找到内存块大小与需求大小最接近的内存块。简单,当大部分是小尺寸需求时非常有效。但是同样会产生很多外碎片
最差适配
在内存中寻找到内存块大小与需求大小差别最大的内存块。它会拆分大内存块,可能会导致大分区无法被分配
优化连续内存分配
压缩式碎片整理
思路:将程序已占用的内存块移动至一段让他们连续,使得外碎片得以合并。
缺点:整理的时机?程序运行时改变其内存地址是不安全的,其次是整理的频率,过高会导致系统效率低下。
交换式碎片整理
思路:将暂时无用的进程的内存块暂时的存储到磁盘上,当需要用到时再拷贝回内存。
缺点:磁盘读写的速度是瓶颈。
非连续内存分配
程序所使用的物理地址空间是非连续的
更好的利用内存和管理内存
允许进程间共享内存
缺点在于需要建立虚拟地址和物理地址之间的转换
分段机制
段表示访问方式和存储数据等属性相同的一段地址空间(存储同样类型的东西,数据段,代码段,堆栈等)
段对应的是一个连续的内存块。若干个段组成进程的逻辑地址空间
段访问:逻辑地址由二元组(段号,段内偏移)组成
CPU寻址时通过段表获得物理地址的起始地址,并计算访问的长度是否合法,合法则计算终止地址,从而访问内存。
缺点: 分段管理会产生内存碎片问题
分页机制
将逻辑地址划分成和物理地址页帧大小相同的页面,基于页表实现映射。
程序使用的物理页可以不连续。
解决内存空间碎片问题
页访问:逻辑地址由(多级页号,偏移量)
优点:
程序使用的物理页可以不连续
减少外碎片的产生
缺点:
页表的空间占用大,一次寻址需要多次读取
存在内碎片
解决读取页表速度问题
在MMU当中有TLB做缓存,它的特点是具备快速访问(K/V结构),缓存近期访问频繁的虚拟地址的物理地址,如果TLB未命中,则访问页表获取并更新至TLB。
解决分页占用空间大问题
使用多级页表,比起一级页表,多级页表可以按程序实际使用的虚拟地址空间载入,从而避免加载整个寻址空间的页表。多级页表的结构中,逻辑地址包含了各个页表的地址值,逐级寻找到底层页表存储对应的物理地址
就绪(Ready): 进程已经分配到除了CPU以外的所有必要资源,等待获得处理机执行
运行(Running): 进程获取到处理机,正在执行的状态
阻塞(Blocking): 进程由于等待某个事件而无法继续执行时,便进入阻塞,等待目标事件完成。如:IO,等待信号等。
进程挂起和阻塞的区别
进程挂起时不占用内存空间,被存储在了磁盘上,而普通的阻塞仍然在内存中。
进程的内存分配
内核虚拟内存:
存放在内核空间中的信息,PCB,内核栈,内核代码和数据
内核栈:
每个进程在内核空间中都有其对应的内核栈,在内核态运行时需要使用对应的内核栈。
用户栈:
用户态下运行使用的栈结构,参考JVM栈,存放栈帧,函数运行时定义的局部变量,中断时存放运行环境。
共享内存映射区域:
专门存放对共享区域的内存映射,存放的是共享数据的指针。
运行时堆:
由程序中申请和释放。C库中使用malloc申请free释放。具体步骤为:
程序malloc申请内存,触发系统调用进入内核态。
内核检查访问的虚拟地址是否合法
在虚拟内存堆中为其分配内存。在首次读写时触发缺页中断,进入缺页中断的处理。
.bss
未初始化全局变量区,存放未初始化的全局变量
int a; static int b; // 都是未初始化的全局变量
.data
已初始化的全局变量区
static int a = 1;
.text
可执行文件,存放程序编译后的二进制机器码和常量。
进程的创建
在Linux中,所有进程都是PID=1的init进程的后代,所以系统中的每一个进程必有一个父进程,可以想象进程实际上也是树形结构。
那么进程的创建实际上就是创建子进程。
写时拷贝
可以推迟拷贝,甚至免除拷贝的技术。内核在拷贝时,以共享的方式代替拷贝,直到真正发生写操作时才对修改的页面进行拷贝,在其之前两个进程都是以只读的方式共享。
创建过程
fork() ,拷贝父进程的PCB创建子进程,接着修改子进程的私有数据(Pid,进程状态等)。此时子进程的代码段、数据段、堆栈都是指向父进程的物理空间(注意拷贝的是虚拟地址的指针,没有拷贝物理内存),依据写时拷贝策略还只是共享。如果父子进程要对共享的其中页面写操作,这是内核才会复制一个物理页面给修改的进程使用,同时修改原虚拟地址的指向(写时复制)。此时父子进程运行起来是完全一样的。
fork()完毕后通常会调度子进程先执行,因为需要执行exec()操作加载新的代码段,并清空数据和堆栈(只是清除和父进程的共享指针而已)。如果先走父进程可能会有大量写时拷贝。
exec(),一般fork()后都要执行exec()加载子进程要执行的程序。execv()是系统调用,它会加载指定路径的程序文件,将子进程中的代码段、数据段、堆栈等信息全部替换。
[参考文章] https://blog.csdn.net/qq_38410730/article/details/81193118
子进程继承了父进程什么
在fork()后子进程拷贝了父进程的PCB,继承了用户号、用户组号、当前的工作目录、打开的文件描述符、堆栈、代码段数据段等。在exec()后会发生变化。
独有的:进程号PID、不同的父进程号、独立的地址空间。
用户态和内核态
概念
内核空间:是OS内核操作的一块受保护的受保护的空间,里面存放系统内核的代码函数和数据等,用户进程无法直接访问。
用户空间:指的是用户可以操作和访问的空间,通常是内存中除了内核空间外的空间。
用户态:当一个进程在用户空间执行时成为用户态。
内核态: 当进程在内核空间执行时,成为内核态。通俗的说就是运行系统代码。
用户态和内核态切换的触发
通常有三种方式
系统调用
这是用户态进程主动要求切换到内核态的一种方式,程序通过调用内核提供的API(getpid()获取进程号,fork() 创建子进程,sleep()进程睡眠等),进入内核态运行。
异常
当运行用户态下的程序时,发生了一些没有预知的异常,这时会切换到内核中处理这些异常的相关代码中运行。例如:缺页中断。
外围设备的中断
当外围操作完成用户请求的操作后,会向CPU发出中断信号,这时CPU会转而执行内核中的中断信号对应的处理程序,如果先前CPU在执行用户态程序则会切换到内核态。例如:磁盘读写完成后,切换到中断处理程序执行后序操作。
用户态和内核态的切换过程
有点类似于进程上下文切换
从进程描述符PCB中提取内核栈信息
内核栈保存当前用户态进程的执行上下文信息
内核态执行程序
获取用户态上下文信息继续执行。
为什么需要用户态和内核态?
和系统调用存在的意义类似,进程之间要实现隔离,内存要保证安全操作,并且保证操作系统本身的安全,一切敏感的操作需要由内核定义的程序执行,防止恶意程序随意的进行操作。其次是对其他功能实现的保证,例如进程的访问权限需要由操作系统保证,那么在操作时就要由操作系统完成;虚拟内存机制需要防止进程访问进程外的地址空间等等。
僵尸进程
僵尸进程指的是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程
线程
线程是轻量级的“进程”,发明线程的本意是为了降低原先多个进程协同工作时共享资源的开销。
进程和线程的区别
进程是操作系统资源分配的基本单位,线程是任务调度执行的基本单位。
线程在创建时不会分配内存空间,线程只能使用进程提供的资源。
进程主要负责资源管理,线程负责执行
线程的创建
在linux中,从内核的角度来说线程是一种特殊的进程,Linux把所有的线程当做进程来实现,在创建线程的系统调用中多传递了一些参数来指明需要共享的资源,最终创建出来的线程(子进程)和父进程之间共享代码段、数据段信息,线程中有独立的堆栈、程序计数器。
用户线程和守护线程
他们本质上没有区别,只不过是在生命周期上有区别,用户线程执行到结束就死亡了,而守护线程需要等待所有用户线程死亡后才会死亡。
调度算法
先来先服务FCFS
机制:顾名思义,选择任务队列中最先到达的任务进行处理,严格按照到达的顺序执行。它是非抢占式的算法,若一个长作业先到达系统会导致后面许多短作业等待相当长时间。
特点: 算法简单,对短作业不利
短作业优先SJF
机制: 从任务队列中选择预估执行时间最短的作业分配处理机执行。非抢占
特点: 对长作业不利,一有短作业就会被提前执行,可能导致长作业饥饿现象。
最高响应比优先
机制: 通过计算响应比(等待时间+要求服务时间)/ 要求服务时间,选择响应比最高的执行。对先来先服务和短作业优先的一种平衡。同时考虑等待时间和执行时间的算法。
轮询/时间片
机制: 按照作业的到达顺序排成一个队列,按照先来先服务,但是每个作业只能执行一个单位的时间片,然后储存上下文让出CPU给下一个作业执行。
特点: 时间片的大小难以确定,如果太小进程频繁切换,上下文开销大。
多级反馈队列调度
机制: 通过动态调整进程优先级和时间片的大小,具有了前几种算法的优点。系统中存在多个就绪队列,并为各个队列设定递增优先级,优先级越高的队列时间片越长。
如图:任务到达后先到达1级队列,如果完成则出队列,如果未完成进入2级,以此类推。
特点: 短作业在1级队列中周转时间较短,并不受长作业影响,而长作业在后期的队列中又可以得到较多的执行时间。
进程间通信IPC
概念
进程间通信指的是不同进程之间传播或交换信息。
信号
概念: 进程间的软件中断和处理机制。
特点:
通信高效,但是传送的信号类型单一,只适合用于通知,而不是传数据
例子: 进程运行时,接收到另一个进程发出的信号,中断当前运行的程序转而运行定义好的信号处理程序,处理完再回到原程序,这个实现要求OS对程序堆栈做更改。
管道
概念:管道,通常指的是无名管道,是一种古老的IPC形式
特点:
它是半双工的,数据采用字节流的形式,只能单向流动,具有固定的读端和写端
内置同步互斥机制,多个进程一起读管道,只有一个进程能读取到完整的数据,管道为空读阻塞,管道满了,写阻塞
生命周期:所有引用该管道的进程都销毁之后,管道才会真正的被释放,释放的是内存中的区域。
举例:常见的shell命令 ls | more 中间的|就是管道,将ls的信息流传入管道由命令more读取
匿名管道
它不属于其他任何文件系统,它只是内存中的一块固定大小的buffer,属于间接通信
它只能用于具有亲缘关系的进程之间的通信(父子或者兄弟进程间),因为不同进程的内存区域无法相互访问
命名管道
命名管道实现了任何进程之间的通信
命名管道FIFO存在于文件系统中,进而实现任意进程之间的通信
消息队列
概念:本质是内核中维护的消息链表,由消息队列标识符标识。
特点:
消息队列是独立于发送与接收进程的,只要有读写权限的进程都可以对其操作。
共享内存
概念: 将同一个物理内存区域映射到多个进程的内存地址空间实现的进程通信机制。
特点:
快速且方便的共享数据
需要程序员实现额外的同步机制来协调数据的访问
Socket
概念: 通过socket通信机制可以对本机或者其他机器上的进程进行通信
同步
一些概念
临界区: 访问和操作共享数据的代码
硬件实现
屏蔽中断:
禁止系统的中断来防止执行临界代码的进程被打断,它简单并且高效,但是代价高,并且用在用户进程上有安全性问题,恶意进程不会被打断也会一直执行,同时不适用于多处理器,其他处理器执行并不需要中断。
自旋锁
使用操作系统提供的原子操作函数,在没有获取到锁时忙等,较为消耗资源,实现简单。适用于临界区执行时间短的轻量级加锁,总体忙等时间较短的情况。
信号量
是一种睡眠锁,线程获取不到锁时会进入睡眠等待。当有位置可以执行时会唤醒等待队列的线程。用于执行时间较长的同步,否则频繁的上下文切换和等待队列的维护成本会比实际执行时间更长。
信号量的PV操作会散落在各个线程中,维护困难,容易出现死锁
互斥量
其实就是简单版本的信号量,它的值只有1,所以只能有一个线程持有锁,它的实现相比信号量会更轻量级。
管程
管程为一个同步结构,具有互斥访问的特性,能根据某些条件来阻塞线程。
它是一种实现同步的思想,在各种高级语言中都有实现,如Java中的synchronize和JUC包中的锁。
死锁
概念
死锁指的是两个或多个线程之间相互等待资源而无法继续运行的情况,如果没有外力将会永远僵持。
产生死锁的必要条件
互斥条件,只能有一个线程占有
保持条件,线程因获取不到资源阻塞时不释放已经占有的资源
不可剥夺条件,线程的资源只能由自己释放,无法被其他线程剥夺
环路条件,必然是循环等待
几种死锁的处理策略
以下的策略逐级放宽
预防死锁
其实就是破坏产生死锁的必要条件
资源一次性分配(可能会导致有的线程饥饿)
不保持资源,当获取不到所有资源时,释放已经占有的资源(额外开销大,频繁获取释放)
有序分配法,给资源赋予编号,按照编号顺序获取资源,释放则相反。(操作系统编号耗费性能)
避免死锁
预防死锁对系统性能影响较大,在避免死锁的策略中允许动态申请资源,因而在进行资源分配前操作系统会计算资源分配的安全性,若此次分配会导致系统进入不安全状态,则进程等待。
例如经典的“银行家算法”就是计算分配资源是否安全的算法
首先明确,系统资源量有限,不得超出
在进程首次申请资源时要测试进程对资源的最大需求量,如果此时满足则直接分配,否则推迟分配。
当进程在运行时再次申请资源时,先测试本次申请的资源是否超出系统资源的剩余,超出则推迟分配,符合就直接分配。
死锁检测
在系统运行中定时检测死锁
操作系统建立进程和已占用资源进程的等待图,如果图中出现有环,则说明出现了死锁
死锁修复
检测到死锁后,根据一定的规则杀死进程,如根据优先级、运行时间、占有资源的数量等。
鸵鸟策略
遇到死锁当做什么都没发生,啥也不干
文件系统
总体
Linux采用的是虚拟文件系统,向上提供了访问文件系统的系统调用接口;向下则在内核中实现了对不同文件操作的实现。
Linux中的文件类型如下
文件类型 描述 示例 普通文件 常见的普通文件 程序代码、图片、音频、视频 目录文件 用于表示目录 各种目录/root、/homt 链接文件 用于不同目录下的文件共享,类似快捷方式 创建一个存在的文件的链接时,这个链接指向已存在的文件 设备文件 访问硬件设备 键盘、磁盘、光驱、打印机等 命名管道 特殊类型的文件,可以用于进程间通讯
在进程控制块PCB中存放了线程已经打开的文件描述符,它实际是一个表,指向了系统中存放已经打开的文件表,可以快速根据索引访问对应文件。
Linux系统限制总体打开的描述符数量、也限制了某个用户下进程最大打开文件描述符数量。可以通过配置更改。
针对IO操作还有缓冲区的概念,操作系统将读取的内容放入缓冲区提供给应用程序,同理写操作也是在缓冲区中,并定时写回IO设备,也可以手动刷新
概述
Linux采用的是虚拟文件系统,向上提供了访问文件系统的系统调用接口,向下则实现了对不同文件系统(ext,NTFS,Fat,ISO9660等)操作的具体实现;它对用户隐藏了不同文件系统的差异。
文件系统的描述
卷控制块/超级块
每个文件系统(分区)都会有一个卷控制块
它存储在分区的前几个扇区中,在OS挂载这个分区时就会被加载到内存当中。
卷控制块中存放这个分区下的磁盘块数量、块大小、占用和空余的大小等等。
文件控制块
每个文件都有对应的文件控制块
它存储在卷控制块之后的几个扇区中,在文件被访问时读进内存
它包含了很多属性,如文件的大小、在磁盘的位置、创建时间、文件名、文件的拥有者等等信息。
目录控制块
每个目录项一个控制块,
存在文件控制块之后的扇区中,在遍历文件路径时进入内存。
对应存放的就是目录的信息,想目录名、子目录、父目录、目录下的文件等等。
缓冲区
缓存了最近访问的页面,可以减少IO次数。这个又有点像页式内存管理,满了以后又有对应的置换算法选择替换。
读/写过程
在读取文件时就是先通过卷控制块获得文件控制块信息再通过文件控制块获得对应的目录控制块,最后从目录控制块中获取到具体的扇区位置和偏移量读取。
接着就是磁盘的旋转寻道读取数据,放入操作系统的缓冲区中,CPU再从缓存区获取数据。
写操作也是写到缓冲区中,OS会定时刷新脏数据到硬盘中,也可以手动要求刷新。
[参考] https://www.ibm.com/developerworks/cn/linux/l-linux-filesystem/
常用命令
显示目录和文件
ls 查看当前目录下所有文件
du 显示目录或者文件大小和具体路径
修改目录,文件权限
chmod改变文件或目录的权限
chown改变文件属性的命令
创建删除目录
mkdir创建目录
rm删除-r递归删,-f强制删
创建文件
touch创建一个新文件
vi/vim创建并编辑文件,或编辑已有文件
mv 重命名或移动
cp复制
显示文件内容
cat显示文件全部内容
more 按分页形式显示文件
Tail 显示文件后几行内容-f会自动刷新
杂
解压命令,不同格式不一样tar
free查看内存情况
ps 查看进程
Kill 杀死进程
clear清屏
ping
netstat查看网络状况
等等