抱佛脚一时爽,一直抱佛脚一直爽!这篇文章总结常见的计算机网络面试问题~因为是抱佛脚,所以结构上没有什么逻辑...
参考链接:Waking-Up CycNotes 牛客网
并发 vs 并行
并发
宏观上在一段时间内能同时运行多个程序
并行
同一时刻能运行多个指令
虚拟化
- 把一个物理实体转换为多个逻辑实体
- 多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占用处理器,每次只执行一小个时间片并快速切换
- 虚拟内存使用了空分复用技术,它将物理内存抽象为地址空间,每个进程都有各自的地址空间
系统调用
作用
如果一个进程在用户态需要使用内核态的功能,就进行系统调用从而陷入内核,由操作系统代为完成
内容
进程控制 fork(); exit(); wait();
进程通信 pipe(); shmget(); mmap();
文件操作 open(); read(); write();
设备操作 ioctl(); read(); write();
信息维护 getpid(); alarm(); sleep();
安全 chmod(); umask(); chown();
进程 vs 线程
- 进程是资源分配的基本单位,线程不拥有资源,线程可以访问隶属进程的资源,除了栈之外,其他资源都是多个线程共享的;线程是CPU使用、独立调度的基本单位
- 创建或撤销进程开销远大于创建或撤销线程时的开销;创建开销:分配和建立进程控制块表项、建立资源表格并分配资源、加载程序并建立地址空间
- 进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小
- 线程间可以通过直接读写同一进程中的数据段进行通信(不过需要同步和互斥机制来保证数据的一致性),但是进程通信需要借助 IPC
- 进程间不会互相影响;但一个线程挂掉将导致整个进程挂掉,因为只有进程有自己的 address space,而这个 space 中经过合法申请的部分叫做 process space。Process space 之外的地址都是非法地址。当一个线程向非法地址读取或者写入,无法确认这个操作是否会影响同一进程中的其它线程,所以只能是整个进程一起崩溃
进程 | 线程 | |
---|---|---|
状态 | 创建态、就绪态、运行态、阻塞态、终止态 | 新创建、可运行、被阻塞(请求锁)、等待(wait)、计时等待(sleep)、被终止 |
资源 | PCB、地址空间、页表、文件描述符表、(程序计数器、通用寄存器、环境变量) | 线程ID、栈、程序计数器、通用寄存器、错误返回码、信号掩码/信号屏蔽字 |
切换 | 模式切换(用户态到内核态); 保存处理机上下文到进程的私有堆栈,包括程序计数器和其他寄存器; 更新PCB信息; 把进程的PCB移入相应的队列; 选择另一个进程执行,并更新其PCB; 更新内存管理的数据结构; 恢复处理机上下文 |
模式切换(用户态到内核态) 保存当前线程id、线程状态、栈、寄存器状态 (SP寄存器:堆栈指针,指向栈顶; PC寄存器:程序计数器,存储下一条要执行的指令; EAX寄存器:累加寄存器,用于加法和乘法的缺省寄存器) |
通信 | 无名管道、命名管道、共享内存、信号量、信号、消息队列、socket | 临界区、互斥锁、信号量、信号 |
同一进程的线程共享的资源
进程代码段、数据段、BSS段、堆
进程打开的文件描述符
进程的当前目录
信号处理器/信号处理函数:对收到的信号的处理方式
进程ID与进程组ID
协程
- 由程序员自己写程序来管理的轻量级线程叫做『用户空间线程』(协程),具有对内核来说不可见的特性
- 一个线程可以拥有多个协程
- 协程的目的就是当出现长时间的I/O操作时,通过让出目前的协程调度,执行下一个任务的方式,来消除ContextSwitch上的开销
- 由于在同一个线程上,因此可以避免竞争关系而使用锁
- 当出现IO阻塞的时候,由协程的调度器进行调度,通过将数据流立刻yield掉(主动让出),并且记录当前栈上的数据,阻塞完后立刻再通过线程恢复栈,并把阻塞的结果放到这个线程上去跑
- 协程的暂停完全由程序控制,发生在用户态上;而线程的阻塞状态是由操作系统内核来进行切换,发生在内核态上
- 协程拥有自己的寄存器上下文和栈
- 切换开销:修改寄存器的值即可,不需要从用户态切到内核态
进程间通信方式
管道
- 无名管道
- 用于父子、兄弟进程间通信
- 半双工(单向交替传输),读端和写端确定
- 写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据
- 只存在于内存、不属于任何文件系统的文件
- 命名管道
- 允许无亲缘关系进程间的通信
- 有路径名与之相关联,以一种特殊设备文件形式存在于文件系统中
IPC
- 消息队列
- 消息的链表,在内核空间中
- 一个消息队列由一个标识符(即队列ID)来标记
- 有写权限的进程可以写消息,有读权限的进程可以读消息
- 进程终止时,消息队列及其内容并不会删除,从而实现异步读写
- 不一定要以FIFO的顺序读,可以按消息类型读
- 信号量
- 一般结合共享内存使用,规定最多同时有多少进程可以访问一个资源
- 信号量基于操作系统的PV操作,是原子操作
- 共享内存
- 两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存
- 需要与同步机制配合,比如信号量
- 是最快的IPC,因为进程是直接对内存进行读取
- 当进程脱离共享存储区后,计数器减一,挂架成功时,计数器加一,只有当计数器变为零时,才能被删除。当进程终止时,它所附加的共享存储区都会自动脱离
- 信号
- 信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件
- 栗子:kill 函数发送杀死pid指向的进程的信号,raise 发送杀死自己的信号;ctrl+c发送终止信号
套接字 socket
用于不同机器间的进程通信
线程间通信方式
临界区
通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问
互斥锁
只有拥有锁的线程才可以访问公共资源
vs 临界区:互斥量是可以命名的,可以用于不同进程之间的同步,而临界区只能用于同一进程中线程的同步;创建互斥量需要的资源更多,因此临界区的优势是速度快,节省资源
信号量
- 限制同一时刻访问一个资源的最大线程数目
- sem_wait系统调用:以原子操作的方式将信号量-1,若信号量值为0,则sem_wait被阻塞,直到信号量变为非0值
- sem_post系统调用:以原子操作的方式将信号量+1,若信号量>0,其他正调用sem_wait的线程将被唤醒
信号
- wait/notify
PCB 进程控制块
含义
描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作
内容
进程标识信息、进程现场信息(寄存器的值、指令计数器、各种指针等)、进程控制信息(状态、优先级、阻塞原因、程序和数据地址、资源清单、同步和通信机制等)
PCB表
- OS把所有PCB组合成一张表,每一行是类似Pid+该进程PCB的位置的格式
- 相同状态(执行、阻塞、就绪等)的PCB通过指针连接,组成链表
- PCB表的大小限制了操作系统中可同时存在的进程个数,称为系统的并发度
fork vs vfork
fork
- 作用:是一个系统调用,用于创建一个和当前进程映像一样的进程
- 返回值:子进程中,成功的fork返回0;父进程中,成功的fork返回子进程的pid、错误的fork返回负值
- 为什么会返回两次:由于在复制时复制了父进程的堆栈段,所以两个进程都停留在fork函数中,等待返回
- 无法确定fork之后是子进程先运行还是父进程先运行
- 过程
- 子进程是父进程的副本,它将获得父进程用户级虚拟地址空间、打开文件描述符集合的拷贝
- 使用exec()载入二进制映像给子进程,替换父进程的映像,此时两个进程可以执行不同的程序了
vfork
- vfork创建的子进程与父进程共享数据段
- 由vfork创建的子进程将先于父进程运行,在它调用exec或exit之后父进程才可能被调度运行
孤儿进程 vs 僵尸进程
孤儿进程
- 一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程
- 孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作,所以不会对系统造成危害
僵尸进程
- 一个子进程的进程描述符(pid、终止状态、资源利用信息
)在子进程退出时不会释放(即挪出PCB表),只有当父进程通过 wait() 或 waitpid() 获取了子进程信息后才会释放 - 如果子进程退出,而父进程并没有调用 wait() 或 waitpid(),那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程
- 僵尸进程通过 ps 命令显示出来的状态为 Z(zombie)
- 系统所能使用的进程号是有限的,如果产生大量僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程
- 要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时僵尸进程就会变成孤儿进程,从而被 init 进程所收养,这样 init 进程就会释放所有的僵尸进程所占有的资源,从而结束僵尸进程
- 每个子进程在结束时都要经过僵尸进程的阶段
为什么需要线程
- 创建子进程时每次都要把父进程的数据都copy一份,造成资源空间的冗余浪费
- 子进程和父进程的数据交互比较麻烦:不同的进程位于不同的地址空间,必须通过共享内存或者通信机制
- 系统在进行进程的调度时还涉及到资源的分配与状态转换等一系列动作,开销大
CPU调度算法
批处理系统
没有太多用户操作,追求吞吐量和周转时间:
- 先来先服务 FCFS
- 短作业优先
- 最短剩余时间优先
交互式系统
追求响应速度:
- 时间片轮转:将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程
- 优先级调度
- 多级反馈机制:设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,..;进程在第一个队列没执行完,就会被移到下一个队列;只有上一个队列没有进程在排队,才能调度当前队列上的进程
磁盘调度算法
- 先来先服务
- 最短寻道时间优先:优先调度与当前磁头所在磁道距离最近的磁道
- 电梯算法:总是按一个方向来进行磁盘调度,直到该方向上没有未完成的磁盘请求,然后改变方向
缓存淘汰策略
- FIFO
- LRU
- LFU:最不经常使用
内存管理
虚拟内存
-
作用
- 物理内存扩充成更大的逻辑内存,将磁盘空间也看作是内存空间的一部分,从而让程序获得更多的可用内存
- 进程看到的是自己独自占有了当前系统的4G内存,不会互相干扰(但所有进程实际上共享同一物理内存)
- 公平内存分配:每个进程都有同样大小的虚拟内存空间
- 实现数据共享:当不同的进程使用同一份代码时,物理内存中可以只存一份代码,不同进程把虚拟内存映射过去即可
- 程序需要分配连续的内存空间时,只需要在虚拟内存分配连续空间,而不需要实际物理内存的连续空间,从而可以利用碎片
-
管理
- 由内存管理单元管理地址空间和物理内存的映射,其中的页表存储着页(程序地址空间)和页框(物理内存空间)的映射表
-
从虚拟(逻辑)地址到物理地址的过程
-
从虚拟地址到线性地址
虚拟地址=段标识符A+段内偏移量B
段式管理单元在段描述符表中,找到A对应的段的起始位置C
线性地址=C+B -
从线性地址到物理地址
线性地址=页目录D+页表E+页偏移F
到页目录的第D项中,找到页表的地址
到页表的第E项中,找到页的起始位置G
物理地址=G+F
-
缺页中断
- 含义:每个程序拥有自己的地址空间,每个地址映射到物理内存中的一页,访问到该页时,若该页不在物理内存中,由硬件发生缺页中断从而将该页调入内存中,并重新执行失败的指令
- 处理步骤
- 保护CPU现场
- 分析中断原因
- 转入缺页中断处理程序进行处理
- 恢复CPU现场,继续执行产生中断的命令(注意:其他中断一般是执行下一条指令)
页面置换算法
- LRU:把最近最久未使用的页换出
- NRU:最近未使用
- FIFO:先进先出
分页 vs 分段
- 分页主要用于实现虚拟内存,从而获得更大的地址空间
- 分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护
- 页的大小不可变,段的大小可以动态改变
颠簸现象
- 定义
- 指频繁的页调度行为
- 进程发生缺页中断时必须置换某一页。然而,其他所有的页都在使用,它置换一个页,但又立刻再次需要这个页。因此会不断产生缺页中断,导致整个系统的效率急剧下降
- 解决方法
- 修改页面置换算法
- 降低同时运行的程序的数量
- 终止该进程或增加物理内存容量
Linux
命令
touch:建立新文件
more:一页一页查看文件内容
cut: xxx | cut -d ' ' -f 1 // 以空格为分隔符,取出第1个
awk
wc
wc -l filename // 统计文件行数
grep -o "hello" 文件名 | wc -l // 统计
netstat -anp | grep port // 查看占用端口的进程
free -m
top
df -h 查看磁盘占用
sef ff=unix
tail -f filename
du -sh * 查看文件大小
ldd 二进制文件名
ps -ef | grep xx | grep -v grep | awk '{print 2}' | awk -F '&' '{print $1}' | sort | uniq -c | sort -nk 1
目录
建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名
文件读取
对于 Ext2 文件系统,当要读取一个文件的内容时,先在 inode (记录文件的属性)中查找文件内容所在的所有 block(记录文件的内容),然后把所有 block 的内容读出来
而对于 FAT 文件系统,它没有 inode,每个 block 中存储着下一个 block 的编号
软链接和硬链接
- 硬链接
- 链接和原文件inode 值相同,它指向了物理硬盘的一个区块,文件系统会维护一个引用计数,只要有文件指向这个区块,它就不会从硬盘上消失
- 原文件被删除,硬链接内容不受影响
- 软链接
- 链接是普通文件,有自己独立的inode
- 软链接的inode所指向的内容实际上是保存了一个绝对路径,当用户访问这个文件时,系统会自动将其替换成其所指的文件路径
- 原文件被删除,软链接指向空
四种锁
- 互斥锁
- 任何时刻只有至多一个线程访问加锁对象
- 获取锁失败线程进入睡眠,锁释放时被唤醒
- 读写锁
- 唤醒时优先考虑写者
- 自旋锁
- 只有至多一个线程访问加锁对象
- 获取锁失败线程不睡眠,而是不断尝试获取锁,直到锁被释放,节省线程状态切换的开销
- 若加锁时间很长,会影响CPU效率
- RCU
- 在修改数据时,先读取数据,然后生成副本、修改副本,修改后在把老数据改为新数据
虚拟文件系统 VFS
- Linux遵循一切皆文件的原则,会将机器中所有硬件设备全部通过驱动、各位文件系统及VFS虚拟为文件
- VFS是一个可以让open()、read()、write()等系统调用不用关心底层的存储介质和文件系统类型就可以工作的粘合层
- 一个Linux机器上可以挂载多种文件系统,但统一呈现为一个统一一致的操作接口
- 调用操作系统的输出操作实质就是对VFS调用输出操作,VFS会根据调用类型转换为调用不同的文件系统的输出操作,如文件IO对应ext3或ext4等文件系统IO
- 通常像ext3、ext4这样的文件系统也有自身的缓存机制,以匹配高速的程序处理过程和缓慢的硬盘读写过程
文件IO整体过程
- 程序层:用户态
- 编辑完文件进行保存时,调用C++标准库中的输出流isotream进行输出,将文件信息写入输出流的缓冲区中
- 缓冲区是作为IO中介的内存块,用于协调输入输出两端的速度
- 当缓冲区满或程序主动调用了清空缓冲的操作时,就会通过系统调用调用操作系统的写操作,将指令和数据发送给操作系统进行输出操作
- OS层:内核态
- 调用操作系统的输出操作实质就是对VFS调用输出操作
- VFS会根据调用类型转换为调用不同的文件系统的输出操作,如文件IO对应ext3或ext4等文件系统IO
- 通常像ext3、ext4这样的文件系统也有自身的缓存机制,以匹配高速的程序处理过程和缓慢的硬盘读写过程
- 当文件信息写入文件系统的缓冲区后,内核有pdflush线程在不停的检测脏页,判断是否要写回到磁盘中
- 把需要写回的页提交到IO队列——即IO调度队列。由IO调度队列调度策略调度何时写回。当然也可以由程序主动调用强制刷新操作来完成缓冲区写入硬盘的操作
- 从IO队列出来后,就到了驱动层,驱动层通过DMA,将数据写入磁盘cache。至于磁盘cache时候写入磁盘介质,那是磁盘控制器的事情
内核态 vs 用户态
区别
- 特权级别不同,用户态的程序不能直接访问OS内核的数据结构和程序,没有占用CPU的能力,CPU资源可以被其它程序获取;内核态可以访问内存所有数据以及外围设备,也可以进行程序的切换
- 内核空间主要操作访问CPU资源、I/O资源、内存资源等硬件资源
- 用户空间是上层应用程序的固定活动空间
转换方式
系统调用、异常、中断
(控制寄存器中的一个模式位被设置时进入内核态)
为什么要分用户态和内核态
- 安全性:防止用户程序恶意或者不小心破坏系统/内存/硬件资源
- 封装性:用户程序不需要实现更加底层的代码
- 利于调度:如果多个用户程序都在等待键盘输入,这时就需要进行调度;统一交给操作系统调度更加方便
内核线程 vs 用户线程
- 一般一个处理核心对应一个内核线程,比如单核处理器对应一个内核线程,双核处理器对应两个内核线程,四核处理器对应四个内核线程
- 现在的电脑一般是双核四线程、四核八线程,是采用超线程技术将一个物理处理核心模拟成两个逻辑处理核心,对应两个内核线程,所以在操作系统中看到的CPU数量是实际物理CPU数量的两倍
- 程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Lightweight Process,LWP),轻量级进程就是我们通常意义上所讲的线程,也被叫做用户线程
- 模型:在现在流行的操作系统中,大都采用多对多的模型
- 一对一模型:
- 一个用户线程就唯一地对应一个内核线程
- 一个线程因某种原因阻塞时其他线程的执行不受影响
- 许多操作系统限制了内核线程的数量,因此一对一模型会使用户线程的数量受到限制
- 许多操作系统内核线程调度时,上下文切换的开销较大,导致用户线程的执行效率下降
- 多对一模型:
- 将多个用户线程映射到一个内核线程上
- 线程之间的切换由用户态的代码来进行
- 如果其中一个用户线程阻塞,那么其它所有线程都将无法执行,因为此时内核线程也随之阻塞了
- 多对多模型:
- 将多个用户线程映射到多个内核线程上
- 一对一模型:
page cache
缓存了部分磁盘文件。要从磁盘中读取文件时,先去page cache查找
文件描述符
- 是非负整数
- 进程初始会有三个默认已分配的descriptor,0-standard input,1-standard output,2-error output
- 每一个进程有自己的文件描述符表,文件描述符是该表的索引,每一行是{文件描述符,系统文件描述符表的某一行}的形式;在系统的文件描述符表中,每一行是{文件位置,引用计数<即指向该表项的描述符表项数>,inode表的某一行}的形式;inode表中,每一行是{文件大小,文件类型...}的形式
局部性原理
时间上:最近被访问的页在不久的将来还会被访问
空间上:内存中被访问的页周围的页也很可能被访问
I/O
定义
在主存和外部设备(如磁盘、终端、网络)之间拷贝数据的过程
Unix I/O
所有外部设备都被模型化为文件,使得所有输入输出都能以一种统一且一致的方式来执行:
- 打开文件:“打开文件”的系统调用-->内核返回文件描述符-->内核记住该文件的所有信息(在inode表中),应用程序只需记住文件描述符
- 改变当前的文件位置(即从文件的哪一行开始访问):对每个打开的文件,内核记录着一个文件位置
- 读写文件
- 关闭文件:“关闭文件”的系统调用-->内核释放文件打开时创建的数据结构,并将对应的文件描述符恢复到可用的描述符池中
wait vs sleep
- sleep()方法不会释放锁,wait()方法释放对象锁。
- sleep()方法可以在任何地方使用,wait()方法则只能在同步方法或同步块中使用。
- sleep()使线程进入阻塞状态(线程睡眠),wait()方法使线程进入等待队列(线程挂起),也就是阻塞类别不同
多线程如何避免死锁
规定加锁顺序
当多个线程需要相同的一些锁,确保所有的线程都是按照相同的顺序获得锁。这种方式需要你事先知道所有可能会用到的锁并对锁进行排序
规定加锁时限
线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试
死锁检测
每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生
若发生了死锁,则选一种方法解锁:①释放所有锁,回退,并且等待一段随机的时间后重试;②给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁
死锁产生的条件
只要下述条件之一不满足,就不会发生死锁
互斥条件
任意时刻一个资源只能给一个进程使用
不可剥夺条件
进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺
请求和保持条件
进程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源
循环等待条件
在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路
线程池
含义
事先创建若干个可执行的线程放入池中,需要时从池中自取,而不用自己创建线程,使用完毕不是销毁而是放回池中
作用
- 减少创建和销毁线程的开销
- 设置线程数的上限,避免线程过多
reactor模型
主线程只负责监听文件描述上是否有事件发生,有则将该事件通知给工作线程,除此之外,主线程不做任何实质性工作,读写数据、处理请求等均在工作线程中完成
内存中数组和链表的遍历那个快
CPU缓存会把一片连续的内存空间读入,因为数组结构是连续的内存地址,所以数组全部或者部分元素被连续存在CPU缓存里面,平均读取 每个元素的时间只要3个CPU时钟时间。而链表的节点分散在堆空间里面,这时候CPU缓存帮不上忙,只能是去读取内存,平均读取时间需要100个CPU时钟周期
多级缓存
CPU 寄存器 – immediate access (0-1个CPU时钟周期)
CPU L1 缓存 – fast access (3个CPU时钟周期)
CPU L2 缓存 – slightly slower access (10个CPU时钟周期)
CPU L3 缓存
内存 (RAM) – slow access (100个CPU时钟周期)
硬盘 (file system) – very slow (10,000,000个CPU时钟周期)
远程网络服务器磁盘