面试之操作系统

linux常用命令

  • ls,cd,mkdir,rmdir,rm,mv,cp,touch,cat,kill…
  • more和less都用于分页读取文件(cat不能分页),more是一次读取整个文件然后分页显示,而less是根据显示需要读取内容,故less在读取大型文件(如日志文件)时效率更高。
  • >指令是重定向,将左侧输出覆盖写到右侧文件。>>指令是追加,将左侧输出追加写到右侧文件。
  • head和tail也用于查看文件内容,head查看文件内容前n行(默认10),tail查看文件内容后n行(默认10)。指定参数-f实现对文件内容实时追踪,tail -f xxx可用于实时追踪日志文件的更新。
  • du(disk usage)查看指定目录磁盘占用情况,df(disk free)查看磁盘整体占用情况,free查看内存整体占用情况。

Shell编程

脚本文件的第一行**#!/bin/bash**表示使用/bin/bash来解释执行该脚本,单独的#表示单行注释。

变量的定义与访问

变量的定义和赋值通过“变量名=值”实现,=前后不能有空格,如果值是字符串可以加双引号或单引号,访问变量直接通过“$变量名”访问。变量默认都是字符串类型,数学计算通过**$((xxx))或者$[xxx]实现,且乘法 ‘*’ 需要加’ ’ 转义。字符串的拼接示例:"hello, "$name**。

通过脚本文件名执行脚本实质上是新开了子bash进程去执行脚本,故存在变量作用域的问题。全局变量指所有父子bash都能访问的变量,局部变量指仅当前bash能访问的变量。环境变量指的是系统提前创建好的全局变量,如$HOME,$PATH。可以通过“export 变量名”命令将局部变量提升为全局变量,但在子bash中修改全局变量是不会对父bash生效的(可理解为副本)。

参数的接收

  • 通过**$1…$9获取第一到第九个输入参数,第十个及之后的参数使用${10}**获取。
  • **$#**用于获取输入参数的个数。
  • **$*$@**用于获取所有参数,但前者将所有参数视为一个整体,后者把每个参数分开对待(可用于循环)。
  • **$?**用于获取上一条命令的执行结果,如0表示正常执行,或其他错误代码。

条件判断

test命令后跟条件参数,简写为**[ xxx ]**,注意前后必须各带一个空格。例如判断变量a是否等于hello:[ $a = hello ],这里的=左右必须各带一个空格(赋值的时候不带)。如果是数组的比较,则需使用\eq(equal),\neq(not equal),\lt(less than),\le(less equal),\gt(greater than),\ge(greater equal)。通过&&连接两个命令,当前一个命令执行成功后再执行后一个命令。通过 || 连接两个命令,当前一个命令执行失败后再执行后一个命令,即常见语言中的逻辑短路(通过分号连接多个命令可以实现依次执行)。

[ -e filname ] #判断filename是否存在
[ -d filname ] #判断filename是否为目录文件
[ -f filname ] #判断filename是否为普通文件
[ -L filname ] #判断filename是否为符号链接

流程控制

  • if判断。其中,elif和else可省略。
if [ 条件判断 ] 
then
	程序执行
elif [ 条件判断 ]
then
	程序执行
else
	程序执行
fi

条件判断的组合使用 [ 条件判断 ] && [ 条件判断 ] 或者 [ 条件判断 ] || [ 条件判断 ]。放在[]内则必须使用**[ 条件判断 -a 条件判断 ](-a表示and)或者[ 条件判断 -o 条件判断 ]**(-o表示or)。

  • for循环。
sum=0
for ((i=1; i <= 10; i++))
do
	sum=$[$sum + $i]
done
#------------------------
#使用{a..b}可以生成a到b步距为1的序列
for i in {1..100}
do
	sum=$[$sum + $i]
done

命令替换

通过$(xxx)执行系统调用(或者函数调用)并获取结果。

正则匹配

command | grep pattern
#------------------
^a #匹配以a开头的行
a$ #匹配以a结尾的行
.  #匹配一个任意字符
a* #匹配任意多个a
$^ #匹配空行

文本处理

  • cut命令。-d 指定分隔符,-f 指定取哪些列。例 grep xxx | cut -d " " -f 1,3,4。
  • awk命令。-F 指定分隔符(默认按空格分割),-v 指定后方代码块传入参数。内置参数FILENAME指定文件名,NR指定行号,NF指定每一行的列数。
#统计passwd文件中每行(按:分割)的行号,每行的列数
cat passwd | awk -F ":" '{print FILENAME NR NF}'
#统计ifconfig中空行的行号
ifconfig | awk '/^$/ {print NR}'
#统计ifconfig中所有ip。代码含义:先匹配netmask所在行,然后按默认空格分割,输入第二个参数(即分割后的第二个串)
ifconfig | awk '/netmask/{print $2}'

CPU飙升100%如何排查

  1. 定位进程获取pid,使用top命令。
  2. 定位线程获取tid,使用top -H -p pid命令。
  3. 打印线程堆栈信息,使用jstack tid > a.txt。

硬链接与软链接

索引节点(index node,inode)

机械硬盘的最小存储单位是扇区(0.5KB),但linux的文件最小存取单位是块(最常见为4KB),文件的数据都存储在块中。除了存储文件,还需存储每个文件相关的元信息,如创建时间、文件大小等,linux将存储文件元信息的数据结构称为索引节点inode。linux存储所有inode的地方称为inode table,它也在硬盘中,具体大小由文件系统在对硬盘格式化时规定,默认大小约占整个硬盘的1/100。文件系统在初始化硬盘时将其分为三部分:超级块(super block)存放文件系统本身的信息如块大小以及硬盘空闲块等信息;索引节点表(inode table)存放文件的属性信息;索引节点(inode)存放文件的数据。

inode和文件的关系

indoe负责存放文件的元信息,主要有文件的字节数、文件拥有者的User ID、文件的Group ID、文件的读写和执行权限、文件的时间戳(创建、最近修改、最近打开)、链接数、文件数据块的位置。每个文件对应有一个inode结点和编号,但硬盘存储inode的区域大小有限,故inode数量有上限,可能出现硬盘有剩余空间但inode已分配完从而无法创建文件的异常现象。

目录文件

linux一切皆文件,目录也是如此,便于描述这里称为目录文件。目录文件中存放的是一个个目录项,每个目录项包含两个内容:文件名和inode编号,故inode中没有存储文件名。创建目录时会默认有两个目录项:”.“和”…“,前者指向自己,后者指向父目录。当使用ls命令时是列出目录下所有文件名,如果加-l参数显示详细信息则需要根据文件的inode编号读取对应inode节点的内容。所以如果用户没有目录文件的执行权限的话,只能获取目录下文件的文件名而不能获取详细信息。为了便于内核快速访问目录从而得到文件的inode编号,linux在内存中缓存了最近访问过的目录树结构,其中每一个结点称为dentry(directory entry),每个dentry就对应某个目录文件中的目录项 ,也即每个dentry中会存放某个文件的innode编号。

硬链接

linux下一个文件对应一个inode,但一个inode可以对应多个文件名(即一个文件可以有多个文件名,类似java多个引用指向同一个对象)。通过任意一个文件名修改文件内容都是实质性的内存修改,但删除一个文件名并不会删除文件,除非不再有文件名指向该文件,这种情况称为硬链接(可以将硬链接理解为引用)。当使用ln命令给文件A创建硬链接B时,本质是在某个目录文件(可以是当前目录也可以是其他目录)中添加一项目录项,文件名是B但inode编号和文件A对应的inode编号一致,同时修改inode中链接数+1。当rm删除文件时本质是在其所在目录文件中删除对应目录项,同时同时修改inode中链接数-1,当链接数为0时由系统回收inode编号和实际存储空间。

软链接

有一种特殊的文件是文件A存文件B的路径,系统读取文件A时会自动导向文件B,此时文件A称为文件B的软链接或者符号链接。所以文件A依赖于文件B存在,如果文件B被删除,打开文件A会提示No such file or directory。软链接通过ln -s命令创建,但它的创建和删除并不会影响inode中的链接数。

中断

概念

中断指程序在执行过程中,遇到急需处理的事件时,暂时中止CPU上现行程序的运行, 转去执行相应的中断处理程序,待处理完成后再返回原程序被中断处或调度其他程序执行的过程。中断是多任务处理器的基础,也是操作系统从用户态变为内核态的唯一手段。

中断向量及中断向量表

不同的中断信号对应的有一个符号表示(0-255),这种符号表示称为中断向量。对每种中断信号,操作系统都有对应的中断处理程序,中断向量和中断处理程序间的映射关系存在中断向量表中。

中断的分类

  • 内中断,与CPU当前正执行的指令有关。内中断又分为:1. 异常,也称软件中断,指程序运行逻辑错误,如整数除0;2. 硬件中断,如缺页;3. 系统调用,也称陷阱,是操作系统内核提供给用户进程的调用API,如读写文件、新建进程等。
  • 外中断,与CPU当前正执行的指令无关。如IO操作完成发出的中断、外设发出的中断等。

中断处理

CPU在执行完每条指令后都会去检查是否有外中断信号,一旦CPU检测到中断信号,就会保护被中断进程的运行环境(也称为处理器现场),包括程序状态字、程序计数器、各种通用寄存器等,然后根据中断向量查询中断向量表并跳转执行相应的中断处理程序,执行结束后返回原进程并恢复运行环境继续运行(也可能进程调度到其他进程)。

内存管理

进程的地址空间

为了实现处理器在多进程间的快速切换,操作系统会把每个进程对应的内存(程序指令、堆、栈)保留在物理内存中,避免了每次切换进程都需要从外存读取。由于多个进程同时存在于内存中,出于安全考虑,操作系统为每个进程分配了一段内存空间称为进程的地址空间。连续分配下堆与栈之间是一段空闲区,堆由低到高扩展,栈由高到低扩展,可能会造成空闲区的浪费和内存碎片问题。主要有以下部分:命令行参数环境变量、栈区、空闲区、堆区、未初始化全局变量、已初始化全局变量、字符常量代码区。

虚拟内存产生背景与基本思想

  • 背景:很多情况下现有空闲内存大小无法满足进程的内存需求。

  • 历史解决方案:1. 覆盖技术,程序员主动将程序分块,依次装入内存后执行(一次一块)。2. 交换技术,操作系统将暂时不会执行的进程拷贝到外存,进而获得空闲空间装入新进程。

    这两种方案为进程分配的地址空间都是连续的,会产生内存碎片问题,此时的解决方案是内存紧缩技术。

  • 解决思路:为了减少内存碎片问题,实现进程在物理内存中的非连续分配,这就需要将进程的逻辑地址空间与物理地址空间相隔离,并建立映射关系。进程的逻辑地址空间可能很大(32位机器下每个进程的逻辑地址空间可以达到2^32byte=4GB),但实际装入内存的只有一部分,剩下的在硬盘上,需要时再读取。

页式存储

  • 页式存储将进程的逻辑地址空间和物理内存划分为大小一样的页和页框,逻辑地址空间中的一页可以装入物理内存中任意一个页框,从而实现物理内存的非连续分配。既然是任意分配,那么还需要给每个进程维护一个页表来保存每个页与页框的映射关系。同时页表也是存储在内存中,故操作系统会维护一个请求表来记录每个进程对应的页表存在哪,包括页表起始地址、页表大小、分配状态等。为了便于快速分配空闲空间,操作系统还会有一个物理页框表来记录哪些页框是空闲的,这里可以是位示图(01表示是否空闲)或空闲链表。
  • 页式存储逻辑地址转物理地址的具体流程。页式存储中指令的逻辑地址会包含页号和页内地址(偏移地址)两部分,CPU中的内存管理单元(MMU)拿到逻辑地址后会根据页号在页表中找到物理页框号(页表地址一般会存在CPU寄存器中),然后加上页内地址得到实际物理地址。
  • 页式存储中一个进程中访问内存中的一个操作数会两次访问内存。第一次访问进程页表得到物理页框号,再加上页内地址得到物理地址;第二次完成对真正需要的数据的读取。这种访问方式时间消耗严重,故将进程页表的部分内容放入CPU内部高速缓存中,称为快表,每次要访问内存时先访问快表,未命中再去访问内存中的页表。
  • 分页管理机制。操作系统在创建进程时只会将进程的一小部分内容真正装入内存,其余部分需要访问时,由处理器产生一个缺页异常,由缺页中断服务程序去加载所需的内容。此时如果内存没有空闲空间分配则会掉用页面置换算法淘汰部分内存页,如FIFO先进先出算法、LRU最近最久未使用算法等。

段式存储

按程序内部的关系将逻辑地址空间分为几个段,每个段定义了一组有完整逻辑意义的信息(如一个方法),操作系统以段为单位分配连续内存,多个段之间在物理上可以不连续,通过地址映射机制把段式虚拟地址转换为实际内存物理地址,转换方式与页式存储类似。段式存储的逻辑地址包含段号和段内地址(偏移地址)两部分。

段页式存储

段页式即段式存储与页式存储相结合,先分段,再分页。段页式存储的逻辑地址包含段号、段内页号和页内地址(偏移地址)三部分。

文件读取

寄存器->一级缓存->二级缓存->三级缓存->内存->硬盘,每一级保存下面一级的部分内容作为缓存(因为级别约高访问速度越快)。缓存的原理是局部性原理:

  • 时间局部性:正在访问的数据近期可能会被再次访问。
  • 空间局部性:正在访问的数据地址附近的数据近期可能会被访问。

页缓存Page Cache

为了缓解CPU对内存和硬盘的读写速度的巨大差异,linux使用Page Cache作为保存在内存中的缓冲区,所有文件IO(包括网络文件)都是直接和页缓存交互。

  • 读文件。用户进程发起系统调用read(),CPU切换到内核态执行操作系统提供的对应API,根据文件路径在目录项中检索到对应的inode,然后访问inode指向的address_space(硬盘文件与页缓存之间的桥梁),最后根据address_space进一步访问页缓存。如果页缓存命中则直接返回,否则引起缺页异常由中断处理程序将硬盘上文件的指定部分装入页缓存然后返回。(页缓存处于内核空间,故返回时还需将页缓存copy到用户空间缓存)
  • 写文件。一直到访问页缓存之前的步骤与读文件一样,此时如果页缓存命中则直接写入,否则引起缺页异常由中断处理程序装入页缓存后再写入。页缓存中的页如果被写入则变为脏页,此时可以主动调用同步方法写回硬盘或者由操作系统专门的线程去周期性写回硬盘。

预缓存

  • 读写文件时将文件内容缓存在内存中考虑的是时间局部性原理。
  • 读写文件时将文件地址附近的内容一起缓存进内存中考虑的是空间局部性原理,此时称为预缓存。

mmap

mmap是一种内存映射文件的方法,先在进程逻辑地址空间中开辟一段虚拟地址与文件的硬盘地址相映射,等到第一次读写数据时才会真正把硬盘中的数据拷贝到内存中(内核空间的页缓存),使得一个或多个用户进程可以共享这一块页缓存。此外,mmap也可以用于实现零拷贝:

  • 传统的网络传输文件需要4次上下文切换+4次拷贝:1. 用户态切换到内核态,DMA将硬盘数据拷贝到内核缓存,CPU将内核缓存拷贝回用户缓存,切换回用户态(2次切换+2次拷贝)。 2. 用户态切换到内核态,CPU将用户缓存拷贝到内核的Socket缓存,DMA将Socket缓存拷贝到网卡,切换回用户态(2次切换+2次拷贝)。其中4次拷贝仅包含2次CPU拷贝。
  • 而mmap的实现方式需要4次上下文切换+3次拷贝:1. 用户态切换到内核态,DMA将硬盘数据拷贝到内核缓存,切换回用户态(2次切换+1次拷贝)。2. 用户态切换到内核态,CPU将内核缓存拷贝到Socket缓存,DMA将Socket缓存拷贝到网卡,切换回用户态(2次切换+2次拷贝)。其中3次拷贝仅包含1次CPU拷贝,从而减少了一次CPU拷贝的时间,提高了效率。

sendFile(2.1版本)

2.1版本的sendFile在传统网络传输文件的基础上进行更改,需要2次上下文切换+3次拷贝:用户态切换到内核态,DMA将硬盘数据拷贝到内核缓存,CPU将内核缓存拷贝到Socket缓存,DMA将Socket缓存拷贝到网卡,切换回用户态。其中3次拷贝仅包含1次CPU拷贝,并减少了2次上下文切换,提高了效率。

sendFile(2.4版本)

2.4版本的sendFile在2.1版本基础上进行更改,需要2次上下文切换+2次拷贝:用户态切换到内核态,DMA将硬盘数据拷贝到内核缓存,CPU将内核缓存的文件描述符(内核缓存的地址和偏移量)拷贝到Socket缓存,DMA根据Socket缓存中的文件描述符直接将内核缓存的数据拷贝到网卡,切换回用户态。其中,因为文件描述符的大小很小相对于文件大小可以忽略不计,故sendFile也称为无CPU参与的“零拷贝”,但是sendFile的使用仅限于将文件的内核缓存拷贝到Socket缓存的场景。

进程调度算法

  • 先来先服务,根据进程达到时间进行调度。
  • 短作业优先,根据进程服务所需时间进行调度。
  • 优先级调度,根据进程设置的优先级进行调度。
  • 时间片轮询(抢占式),轮询进程分配时间片进行调度。
  • 多级反馈队列(抢占式),包含多个优先级不同的队列,每个进程刚来时进入优先级最高的队列,CPU按先来先服务 + 时间片轮询的方式调度优先级更高的队列,一旦某个进程时间片用完仍未执行完则降到下一级队列,当且仅当所有优先级高于 i 的队列中没有进程时,才会调度优先级为 i 的队列中的进程。

进程间通信(Inter-Process Comunication,IPC)

操作系统为每个进程都分配了独立的的用户地址空间,进程与进程之间交换数据只能借助内核完成,在内核中开辟一段缓冲区,通过读写该缓冲区实现进程通信的方式称为进程间通信。实现进程间通信的方式有:

  • 无名管道。本质是两个进程使用同一块固定大小的内核缓存,进程以先进先出的方式进行读写,属于半双工通信,如果进程出现读空或写满则会阻塞。无名管道仅适用于父子或兄弟进程间通信,且仅存在于内存中,用完即销毁。
  • 命名管道。本质与无名管道类似,但在硬盘空间中有对应的文件inode(没有实际数据),故可以用于任意两个进程间通信。本质是两个进程同时打开一个文件,一个读一个写,但该文件的内容不会写入硬盘。
  • 信号量。本质是无符号型整数,通过操作系统的原子操作(PV操作)实现对临界资源的访问控制,常用作锁机制。
  • 消息队列。本质在内核中由消息组成的链表,消息有数据类型,进程可根据数据类型访问特定消息。
  • 共享内存(mmap)。本质是建立进程逻辑地址空间与内核物理内存空间的映射,故不同进程间可以共享内核空间的一段页缓存,直接操作即可。共享内存是最快的IPC方式,原因是不用执行用户空间与内核空间的缓存互相拷贝。
  • 套接字(Socket)。基于TCP/IP协议的网络通信,双方先建立连接后进行全双工通信。

死锁的条件和解决方案

四个必要条件

  • 资源互斥,即一个资源只能被一个进程占有。
  • 占有并等待,即进程占有一部分资源同时在等待其他资源的释放。
  • 不可剥夺,即一个进程不能强制获取其他进程占有的资源。
  • 循环等待,即多个进程对资源的需求形成一个循环链。

死锁预防

  • 破坏占有并等待条件,有两种方法:1. 进程开始执行时就获取所有需要的资源,但会导致严重的资源浪费; 2. 进程执行之初获取部分资源,运行中释放不再需要的资源。
  • 破坏不可剥夺条件,当进程抢夺资源失败时,释放其拥有的所有资源,一段时间后再全部重新申请,但会导致CPU吞吐量低,因为进程释放资源后之前的所有工作都白做了。
  • 破坏循环等待条件,给每个资源设置编号,禁止进程申请比其占有资源编号更小的资源,但也会导致资源浪费,因为即使编号小的资源空闲时也不会申请成功。

死锁避免

操作系统在进行资源分配之前预先计算资源分配的安全性,若此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法,最后会得到一个安全的进程推进序列。

银行家算法

维护四个主要的数据结构:系统当前可用资源一维向量Available;每个进程需要的最大各类资源数矩阵Max;每个进程已拥有的各类资源数矩阵Allocation;每个进程还需要的各类资源数矩阵Need。显然Need + Allocation = Max,算法每次试探性的将当前可用资源Available分配给某个进程(该进程在Need中对应的向量应该小于等于Available),若该进程能开始执行则将其Allocation中的资源数加到Available中,寻找下一个进程再试探性分配,最后得到一个安全的进程推进序列。(感觉银行家算法本质就是一个DFS+回溯…)

网络IO

阻塞IO

首先分析用户线程调用accpet()、read()等方法后经过的步骤:

  1. 网卡将数据写到内核缓存;
  2. 内核将内核缓存的数据写到用户缓存;
  3. 用户线程解除阻塞状态,读取数据。

其中,网卡与内核缓存的读写、内核缓存与用户缓存的读写(由用户线程执行系统调用read或write触发)均由内核完成。期间,如果没有网络数据传输或者第1步未完成,亦或者第2步未完成,则用户线程会一直阻塞。

非阻塞IO

与阻塞IO不同的地方在于,用户线程仅会在执行第2步时阻塞,当没有网络数据传输或者第1步未完成时,用户线程并不会挂起等待(即阻塞),而是不断轮询判断用户缓存数据是否就绪,这个判断的步骤仍然是执行recv()系统调用。

io多路复用

一个线程监测多个io操作。

select、poll和epoll

三者都属于io多路复用相关的方法,用于解决用户线程使用非阻塞io轮询造成的效率低的现象(需要遍历每个连接依次执行系统调用recv方法判断是否就绪)。

  • 当用户线程调用select()方法后就会被阻塞,一次性将所有socket连接交给内核去轮询,当某个或多个连接有事件发生时,解除阻塞并返回发生事件的连接数目,用户线程再循环遍历找到发生事件的连接并处理。select()最多支持1024个连接。
  • poll()方法与select()没有本质区别,但没有最大连接数限制。
  • epoll()方法不同于前两种方法使用轮询机制查找发生事件的连接,而是通过事件驱动的方式实现,在每个socket连接上注册回调函数,当网卡接收到数据时会回调该函数,将其加入就绪链表中并返回,此时用户线程只需遍历就绪链表中的连接即可。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值