《面试必备知识》——操作系统71问

本期分享的是与操作系统相关的面试题,希望对大家有帮助。
整理不易,希望大家能够点赞、收藏、转发一下!

需要PDF版本的请在下面评论,看到后会回复~

  • 目录
  • 请善用CTRL+F来查找

1. 操作系统是什么?

  • 管理计算机硬件和软件资源的程序

2. 内核与外壳分别是什么?

  • 内核就是能操作硬件的程序
    • 内核管理系统的进程、内存、设备驱动程序、文件、网络等,决定着系统的性能和稳定性
  • 外壳就是围绕内核的应用程序

3. 系统调用是什么?有哪几类?

  • 根据进程访问资源的特点,可以将进程在系统上的运行分为两个级别:
    • 用户态:用户态下的进程可以直接读取用户程序的数据
    • 内核态:内核态下的进程几乎可以访问计算机的任何资源,不受限制
  • 我们运行的程序基本都是运行在用户态,如果需要调用操作系统提供的内核态级别的功能的时候,就需要使用系统调用,通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成
  • 系统调用按照功能分类:

4. 进程状态有哪几种?

5. 线程有哪些同步方式?

6. 进程有哪些同步方式?

  • 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
    • 当多个线程访问一个独占性共享资源时,可以使用临界区对象。拥有临界区的线程可以访问被保护起来的资源或代码段,其他线程若想访问,则被挂起,直到拥有临界区的线程放弃临界区为止,以此达到用原子方式操 作共享资源的目的。
    • 优点:保证在某一时刻只有一个线程能访问数据的简便办法。
    • 缺点:虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。
  • 互斥量:为协调共同对一个共享资源的单独访问而设计的。互斥量跟临界区很相似,比临界区复杂,互斥对象只有一个,只有拥有互斥对象的线程才具有访问资源的权限
    • 优点:使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。
    • 缺点:
      • 互斥量是可以命名的,也就是说它可以跨越进程使用,所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。
      • 通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号量对象可以说是一种资源计数器。
  • 信号量:为控制一个具有有限数量用户资源而设计。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。互斥量是信号量的一种特殊情况,当信号量的最大资源数=1就是互斥量了。
    • 优点:适用于对Socket(套接字)程序中线程的同步。
    • 缺点:
      • 信号量机制必须有公共内存,不能用于分布式操作系统,这是它最大的弱点;
      • 信号量机制功能强大,但使用时对信号量的操作分散, 而且难以控制,读写和维护都很困难,加重了程序员的编码负担;
      • 核心操作P-V分散在各用户程序的代码中,不易控制和管理,一旦错误,后果严重,且不易发现和纠正。
  • 事件: 用来通知线程有一些事件已发生,从而启动后继任务的开始。
    • 优点:事件对象通过通知操作的方式来保持线程的同步,并且可以实现不同进程中的线程同步操作。

7. 进程调度算法有哪些?

8. 操作系统的内存管理主要做什么?

  • 负责内存的分配和回收(malloc:申请内存,free:释放内存)
  • 将逻辑地址转化为相应的物理地址

9. 内存管理机制和内存管理方式有哪些?

  • 连续分配管理:为用户分配一个连续的内存空间
    • 块式管理
      • 将内存分成几个固定大小的块,每个块只包含一个进程。如果程序需要的内存较少,那么分配的这块内存很大一部分几乎被浪费了。在块中没有被使用到的空间即为碎片
  • 离散分配管理:允许一个程序使用的内存分布在离散或者不相邻的内存中
    • 页式管理
      • 将内存分为大小相等且固定的一页一页的形式,页较小,相比于块式管理划分得更细,提高内存利用率,减少碎片。页式管理通过页表对应逻辑地址和物理地址
      • 逻辑地址划分为固定大小的页,同样,物理地址划分为同样大小的页框,页与页框之间通过页表对应起来
      • 没有外碎片(页的大小是固定的),但是会产生内碎片(一个页可能会填不满)
    • 段式管理
      • 将程序的地址空间划分为若干段,如代码段、数据段、堆栈段等
      • 每一段具有实际意义
      • 段式管理通过段表对应逻辑地址和物理地址
      • 没有内碎片,但是会存在外碎片
    • 段页式管理
      • 先将主存分成若干段,每个段又分成若干页,段页式管理中段与段之间以及段的内部都是离散的。
      • 集合了段式管理和页式管理的优点,提高内存的利用效率

10. 什么是分页?

  • 把内存空间划分为大小相等且固定的块,作为主存的基本单位。因为程序数据存储在不同的页面中,而页面又离散的分布在内存中,因此需要一个页表来记录映射关系,以实现从页号到物理块号的映射。

  • 访问分页系统中内存数据需要两次的内存访问 (一次是从内存中访问页表,从中找到指定的物理块号,加上页内偏移得到实际物理地址;第二次就是根据第一次得到的物理地址访问内存取出数据)。

img

11. 什么是分段?

  • 分页是为了提高内存利用率,而分段是为了满足程序员在编写代码的时候的一些逻辑需求(比如数据共享,数据保护,动态链接等)。

  • 分段内存管理当中,地址是二维的,一维是段号,二维是段内地址;其中每个段的长度是不一样的,而且每个段内部都是从0开始编址的。由于分段管理中,每个段内部是连续内存分配,但是段和段之间是离散分配的,因此也存在一个逻辑地址到物理地址的映射关系,相应的就是段表机制。

img

  • 由于分段管理中,每个段内部是连续内存分配,但是段和段之间是离散分配的,因此也存在一个逻辑地址到物理地址的映射关系,相应的就是段表机制。段表中的每一个表项记录了该段在内存中的起始地址和该段的长度。段表可以放在内存中也可以放在寄存器中。
  • 访问内存的时候根据段号和段表项的长度计算当前访问段在段表中的位置,然后访问段表,得到该段的物理地址,根据该物理地址以及段内偏移量就可以得到需要访问的内存。由于也是两次内存访问,所以分段管理中同样引入了联想寄存器。

12. 什么是段页式管理?

  • 页式存储管理能有效地提高内存利用率,而分段存储管理能反映程序的逻辑结构并有利于段的共享
  • 段页式管理就是将程序分为多个逻辑段,在每个段里面又进行分页,即将分段和分页组合起来使用。
  • 为了实现地址变换,系统为每个进程建立一张段表,而每个分段有一张页表(在一个进程中,段表只有一个,而页表可能有多个)
  • 在进行地址变换时,首先通过段表查到页表起始地址,然后通过页表找到页帧号,最后形成物理地址。如图所示,进行一次访问实际需要至少三次访问主存,这里同样可以使用快表以加快查找速度,其关键字由段号、页号组成,值是对应的页帧号和保护码。
  • 三次内存访问:
    1. 访问内存中的段表查到页表的起始地址
    2. 访问内存中的页表找到页帧号,形成物理地址
    3. 得到物理地址后,再一次访问内存,存取指令或者数据

13. 内存碎片是什么?

  • 内部碎片

    • 一个进程分配到了一个固定大小的内存块
    • 但是进程实际所需的空间小于分区块
    • 此时分区块剩余的空间无法被系统利用,称为内部碎片
  • 外部碎片

    • 空闲内存分为小区块被不同进程使用
    • 当某个进程需要大空间的时候,此时虽然空闲空间足够大,但是由于剩余空间被划分为大大小小的区块,没有一个足够大让程序可以使用
  • 解决方法

  • 外碎片

    • 紧凑技术
    • 让操作系统不时对进程进行移动和整理,比较费时
  • 内碎片

    • 内存交换
    • 先将已使用的部分交换到硬盘,然后再重新加载到内存,不过再次加载时应该放到一个新的区域上,这样子之前空出来的部分就可以使用了

14. 快表和多级页表是什么?

  • 分页内存管理中有两点比较重要:

    • 虚拟地址到物理地址的转换要快
    • 解决虚拟地址空间大,页表也会很大的问题
  • 快表

    • 用来缓存页表的一部分或者全部内容,是一种特殊的高速缓冲存储器
    • 使用快表后的地址转换流程:
      • 根据虚拟地址中的页号查找快表
      • 如果该页在快表中,直接从快表中读取相应的物理地址
      • 如果该页不在快表中,就访问内存的页表,从页表中得到物理地址,同时将该页表中的该映射表项添加到快表中
      • 当快表填满后,又要登记新的页的时候,就按照一定的淘汰策略淘汰掉快表中的一个页
  • 多级页表

    • https://www.polarxiong.com/archives/%E5%A4%9A%E7%BA%A7%E9%A1%B5%E8%A1%A8%E5%A6%82%E4%BD%95%E8%8A%82%E7%BA%A6%E5%86%85%E5%AD%98.html
    • 避免把全部页表都放在内存中
    • 第一级的页表覆盖了全部的虚拟地址空间,但是由于采用了分级,因此第一级的页表项较少,占用内存少
      • 第一级页表只记录了下一级页表的物理地址,因此第一级页表占用的内存空间较少
      • 根据一级页表得到的物理地址,找到下一级页表所在的页,然后加载进来,这样子就能够避免将不需要的页加载到内存中
    • 后面的几级页表可以在用到的时候再进行创建
    • 而且后面的几级页表可以先存放在磁盘上,根据程序运行的局部性原理,虚拟内存映射存在局部性,因此后面几级的页表可以在需要时再调入内存,进一步减少内存的占用
  • 为了提高内存的利用率,提出了多级页表,用时间换空间;为了补偿损失的时间,提出了快表

15. 快表的工作流程?

  • CPU给出逻辑地址,由某个硬件(MMU)算得页号、页内偏移量,将页号与快表中的所有页号进行比较
  • 如果找到匹配的页号,说明要访问的页表项在快表中有副本,则直接从中取出该页对应的内存块号,再将内存块号与页内偏移量拼接形成物理地址,最后,访问该物理地址对应的内存单元。因此,若快表命中,则访问某个逻辑地址仅需一次访存即可。
  • 如果没有找到匹配的页号,则需要访问内存中的页表,找到对应页表项,得到页面存放的内存块号,再将内存块号与页内偏移量拼接形成物理地址,最后,访问该物理地址对应的内存单元。因此,若快表未命中,则访问某个逻辑地址需要两次访存(注意:在找到页表项后,应同时将其存入快表,以便后面可能的再次访问。但若快表已满,则必须按照指定的算法对旧的页表项进行替换)
地址变换过程 访问一个逻辑地址的访存次数
基本地址变换机构 ①算页号、页内偏移量 ②检查页号合法性 ③查页表,找到页面存放的内存块号 ④根据内存块号与页内偏移量得到物理地址 ⑤访问目标内存单元 两次访存
具有快表的地址变换机构 ①算页号、页内偏移量 ②检查页号合法性 ③查快表。若命中,即可知道页面存放的内存块号,可直接进行⑤;若未命中则进行④ ④查页表,找到页面存放的内存块号,并且将页表项复制到快表中 ⑤根据内存块号与页内偏移量得到物理地址 ⑥访问目标内存单元 快表命中,只需一次访存 快表未命中,需要两次访存

16. 分页机制和分段机制的共同点和区别?

  • 共同点:
    • 都是为了提高内存利用率,减少内存碎片
    • 两者都是离散内存分配。但是每个页和每个段内部是连续的
  • 不同点:
    • 页的大小是固定的,由操作系统决定;段的大小不固定,取决于当前运行的程序
    • 分页是为了满足操作系统内存管理的需要,没有具体的含义,而段是逻辑信息单位,在程序中体现为代码段,数据段,能够更好地满足用户的需求

17. 分页系统是什么?

  • 分页是将磁盘或者硬盘分为大小固定的块,称为页
  • 将内存分为同样大小的块,称为页框
    • 程序执行的时候,将磁盘的页装入页框中
  • 页和页框由两部分组成,分别是页号和偏移量
  • 通过操作系统维护的页表来维护页框和页之间的关系
    • 页表中,页号为索引,页框号为表的内容

18. 分页和分段的区别是什么?

  • 分页对程序员是透明的,但是分段需要程序员显式划分每个段。
  • 分页的地址空间是一维地址空间,分段是二维的
    • 分页的地址结构是:页号+位移量
      • 页号定位页项,得到对应块号,块是事先划分好的,操作系统可以根据地址结构直接访问到,因此地址空间是一维的
    • 分段的地址结构:段号+位移量
      • 根据段号在段表中找到对应的段项
      • 段项包含了基址,是程序员设定的,操作系统并不知道
      • 因此操作系统要结合地址结构和基址才能访问具体的内存
  • 页的大小不可变,段的大小可以动态改变
  • ==分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护==
  • 信息共享:段是信息的逻辑单位,便于存储保护和信息的共享,页的保护和共享受到限制;

19. 逻辑(虚拟)地址与物理地址的含义?

  • 在编程中指针指向的内存地址就是逻辑地址,逻辑地址由操作系统决定
  • 物理地址是指真实内存单元的地址

20. CPU寻址?为什么需要虚拟地址空间?

  • CPU寻址使用的是虚拟寻址的方式,将虚拟地址翻译成物理地址,以此访问到真实的物理内存

  • 一般完成该工作是CPU中的内存管理单元(MMU)的硬件

  • 如果没有虚拟地址空间的话,程序将直接操作真实的物理内存,此时:

    • 用户可以访问和修改任意内存,很容易有意或无意间破坏操作系统,造成操作系统崩溃
    • 同时运行多个程序将会非常困难,因为多个程序之间有可能会相互覆盖对方内存地址上的数据
  • 通过使用虚拟地址空间:

    • 程序可以使用一系列连续的虚拟地址空间来访问物理地址中不相邻的内存空间
    • 程序可以使用虚拟地址来访问大于可用物理内存的内存缓冲区间。当物理内存不足时,可以将物理内存页保存到磁盘中。数据和代码页可以根据需要在物理内存和磁盘间移动
    • 不同进程间的虚拟地址彼此隔离。因此不同的进程无法干涉彼此正在使用的物理内存

21. 什么是虚拟内存?

  • 虚拟内存是计算机系统提供的一种内存管理技术

  • ==它给每个进程提供了一连续的虚拟地址空间,使得每一个进程具有独占主存的错觉,通过将内存扩展到硬盘空间,让程序可以拥有超过系统物理内存大小的可用内存空间==

  • 它定义了一个连续的虚拟地址空间,并且把内存扩展到了硬盘空间

  • 虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存,也就是说一个程序不需要全部调入内存就可以运行,这使得有限的内存运行大程序成为可能

    • 对于没有映射到物理内存的页,可以在使用的时候将其装入物理内存
  • 虚拟内存是计算机系统提供的一种内存管理技术,它给每个进程提供了一个一致的、私有的、连续的地址空间,把内存扩展到了硬盘空间,让程序可以拥有超过物理内存大小的内存空间

22. 逻辑地址与物理地址的转换过程?

  • 内存管理单元(MMU)管理逻辑地址和物理地址的转换,其中页表存储页(逻辑地址空间)和页框(物理内存空间)的映射表
  • 一个虚拟地址分为两个部分,一个部分存储页面号,一部分存储偏移量,两个部分结合,就可以在页表中得到页对应的页框地址

23. 局部性原理了解吗?

  • 在某个较短的时间内,程序执行局限于某一小部分,程序访问的存储空间也局限于某个区域
  • 两个方面:
    • 时间局部性:某条指令被执行后,不久后可能会再次执行;某个数据被访问后,不久后可能会再次被访问。该局部性的典型原因是程序中存在大量的循环操作。
    • 空间局部性:程序访问了某个存储单元后,不久后其附近的存储单元也将被访问到。因为程序指令往往是顺序存放、顺序执行的,数据一般也是聚簇存储的。
  • 时间局部性通过将近来使用的指令和数据存储在高速缓存中,并且使用高速缓存的层次结构来实现。空间局部性则使用较大的高速缓存并通过预取机制集成到高速缓存中实现。
  • 利用局部性原理来实现高速缓存

24. 虚拟存储器是什么?

25. 虚拟内存的技术实现方式?

  • 虚拟内存中,允许将一个作业分多次调入内存
  • 虚拟内存的实现需要基于离散分配的内存管理方式的基础上
  • 三种
    • 请求分页存储管理
      • 在分页管理基础上,添加了请求调页功能和页面置换功能
      • 在程序运行时,只装入当前要执行的部分页,如果在后续运行过程中发现访问的页不存在于内存中的话,则由处理器通知操作系统按照对应的页面置换算法将相应的页面置换到主存中,将暂时不用的页置换到外存中
    • 请求分段存储管理
      • 在分段管理基础上,添加了请求调段功能和分段置换功能
      • 过程类似上述
    • 请求段页存储管理
  • 请求分页和分页的主要区别在于是否在一开始就将所有的地址空间都装入内存
    • 由于请求分页无需一次性装载,因此该方法可以提供虚拟内存

26. 页面置换算法有哪些?

  • 缺页中断
    • 要访问的页不在主存中,需要操作系统将页面调入内存中
  • 页面淘汰算法:

  • FIFO算法改进
    • 给每个页面添加一个R位
    • 在弹出页面的时候,先检查页面的R位是否为0,为0说明当前页面又老有没有用到
    • 否则,则将R为置为1,然后放到链表的尾部
  • LRU算法
    • 考虑程序访问的时间局部性,性能较好
    • 最近最常使用的页面在接下来的时间被使用到的概率较大
算法规则 优缺点
OPT 优先淘汰最长时间内不会被访问的页面 缺页率最小,性能最好;但无法实现
FIFO 优先淘汰最先进入内存的页面 实现简单;但性能很差,可能出现Belady异常
LRU 优先淘汰最近最久没访问的页面 性能很好;但需要硬件支持,算法开销大
CLOCK (NRU) 循环扫描各页面 第一轮淘汰访问位=0的,并将扫描过的页面访问位改为1。若第-轮没选中,则进行第二轮扫描。 实现简单,算法开销小;但未考虑页面是否被修改过。
改进型CLOCK (改进型NRU) 若用(访问位,修改位)的形式表述,则 第一轮:淘汰(0,0) 第二轮:淘汰(O,1),并将扫描过的页面访问位都置为0 第三轮:淘汰(O, 0) 第四轮:淘汰(0, 1) 算法开销较小,性能也不错
  • 在Clock算法的基础上添加一个修改位,替换时根据访问位和修改位综合判断。优先替换访问位和修改位都是0的页面,其次是访问位为0修改位为1的页面。

  • 最优算法在当前页面中置换最后要访问的页面。不幸的是,没有办法来判定哪个页面是最后一个要访问的,因此实际上该算法不能使用。然而,它可以作为衡量其他算法的标准。
  • NRU 算法根据 R 位和 M 位的状态将页面分为四类。从编号最小的类别中随机选择一个页面。NRU 算法易于实现,但是性能不是很好。存在更好的算法
  • FIFO 会跟踪页面加载进入内存中的顺序,并把页面放入一个链表中。有可能删除存在时间最长但是还在使用的页面,因此这个算法也不是一个很好的选择。
  • 第二次机会算法是对 FIFO 的一个修改,它会在删除页面之前检查这个页面是否仍在使用。如果页面正在使用,就会进行保留。这个改进大大提高了性能。
  • 时钟 算法是第二次机会算法的另外一种实现形式,时钟算法和第二次算法的性能差不多,但是会花费更少的时间来执行算法
    • 页面设置一个访问位,并将页面链接为一个环形队列,页面被访问的时候访问位设置为1。页面置换的时候,如果当前指针所指页面访问为为0,那么置换,否则将其置为0,循环直到遇到一个访问为位0的页面。
  • LRU 算法是一个非常优秀的算法,但是没有特殊的硬件(TLB)很难实现。如果没有硬件,就不能使用 LRU 算法
  • NFU 算法是一种近似于 LRU 的算法,它的性能不是非常好。
  • 老化 算法是一种更接近 LRU 算法的实现,并且可以更好的实现,因此是一个很好的选择
  • 最后两种算法都使用了工作集算法。工作集算法提供了合理的性能开销,但是它的实现比较复杂。WSClock 是另外一种变体,它不仅能够提供良好的性能,而且可以高效地实现。

最好的算法是老化算法和WSClock算法。他们分别是基于 LRU 和工作集算法。他们都具有良好的性能并且能够被有效的实现。还存在其他一些好的算法,但实际上这两个可能是最重要的。- 最优算法在当前页面中置换最后要访问的页面。不幸的是,没有办法来判定哪个页面是最后一个要访问的,因此实际上该算法不能使用。然而,它可以作为衡量其他算法的标准。

27. 僵尸进程是什么?如何解决?

  • 子进程运行结束了而父进程还没有,而且父进程未对子进程进行回收,就会产生僵尸进程

  • 原因:

    • 子进程在完成工作后,会给父进程发送SIGCHILD信号,等待父进程进行处理
    • 如果父进程没有妥善处理,就会产生僵尸进程
  • 目的:

    • 维护子进程的信息,让父进程在以后的某个时间获取
    • 信息包括了进程ID,进程的终止状态,以及该进程使用的CPU时间
    • 父进程调用wait或waitpid时就可以得到这些信息
  • 解决方法:

    • 父进程调用wait方法

    • // 参数保存子进程退出通知码,返回 -1 表示没有子进程或者错误。否则返回子进程的进程 id 号。 pid_t wait(int *status);   // 例子 pid = wait(NULL); // 忽略子进程通知码
  • 如果僵尸进程一直不处理,就会导致系统的资源被耗尽(如果父进程不消亡的话)

    • 如果父进程先消亡了,那么就有init进程来继承它们,并对这些进程进行清理操作

28. 如何避免僵尸进程?

  • 通过signal(SIGCHLD, SIG_IGN)==通知内核对子进程的结束不关心,由内核回收。==如果不想让父进程挂起,可以在父进程中加入一条语句:signal(SIGCHLD,SIG_IGN);表示父进程忽略SIGCHLD信号,该信号是子进程退出的时候向父进程发送的。

  • ==父进程调用wait/waitpid等函数等待子进程结束==,如果尚无子进程退出wait会导致父进程阻塞。waitpid可以通过传递WNOHANG使父进程不阻塞立即返回。

  • 如果父进程很忙可以用==signal注册信号处理函数,==在信号处理函数调用wait/waitpid等待子进程退出。

  • 通过两次调用fork。父进程首先调用fork创建一个子进程然后waitpid等待子进程退出,子进程再fork一个孙进程后退出。这样子进程退出后会被父进程等待回收,而对于孙子进程其父进程已经退出所以孙进程成为一个孤儿进程,孤儿进程由init进程接管,孙进程结束后,init会等待回收。

29. 孤儿进程是什么?有危害吗?

  • 父进程先于子进程结束,那么剩下的子进程就成为孤儿进程
  • 孤儿进程是无害的,不需要进行回收
  • 孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

30. 进程空间包含什么?

  • 进程空间可以分为两部分
    • 进程独占的用户空间
    • 进程共享的内核空间
      • 用户程序无法读写内核空间

31. 进程通信有哪些方式?

31.1 无名管道

  • 相当于内核空间的一个特殊文件,无名管道的无名就是指这个虚幻的“文件”,没有名字

  • 可以通过pipe函数来创建

  • pipe函数会在进程内核空间申请一块内存(比如一个内存页,一般是 4KB),然后把这块内存当成一个先进先出(FIFO)的循环队列来存取数据,这一切都由操作系统帮助我们实现了

  • 无名管道只能进行半双工通信

  • 如果想要实现全双工,可以再多开一个无名管道

  • 本质上时内核缓冲区,当缓冲区满了或者空了,会控制相应的读写进程进入等待队列,等缓冲区满足条件后再唤醒对应的进程

  • 由于无名管道没有名字,只是内核空间的一块内存,因此只适用于父子进程间的通信

    • 父进程通过pipe创建管道,获得读写文件描述符后,通过fork函数创建子进程
    • 此时子进程拥有同样的文件描述符
    • 由于无名管道半双工的特点,父子进程只能一个保留写一个保留读

31.2 有名管道

  • 有名管道使用一个实实在在的FIFO类型的文件,意味着即时没有亲缘关系的进程也可以相互通信了,只要不同的进程打开相同的FIFO文件即可
  • 通过命令 mkfifo 创建FIFO类型文件
  • FIFO文件特点:
    • 文件属性前面标注的文件类型是 p,代表管道
    • 文件大小是 0
    • fifo 文件需要有读写两端,否则在打开 fifo 文件时会阻塞
    • 也会半双工

31.3 共享内存

  • 过程

    • 根据已知的key使用shmget函数获取或者创建内核对象,并且返回内核对象的id号
      • key一般是事先约定好的
    • 根据id号获取内存地址
    • 向内存读写数据
  • 使得多个进程可以直接读写同一个内核中的内存空间

  • 由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)达到进程间的同步和互斥

  • 速度相对来说是比较快的

  • 因为两个进程的虚拟地址空间直接映射到同一个物理内存中

  • 不用每次将数据从内核拷贝到用户空间再拷贝回去

  • 基本上共享内存中的数据直到通信完成才会写回去文件中

  • 因此一个共享内存只有两次复制,一次从文件到内存,一次从内存到文件

  • 比较典型的linux内核支持方式就是mmap调用

31.4 内核对象

  • 内核对象是位于内核空间的一个结构体

  • 对于共享内存、消息队列和信号量,在内核空间中均有对应的结构体来描述

  • 每个内核对象都有自己的id号,供用户空间使用

  • 获取方法:

    • shmget:共享内存
    • msgget:消息队列
    • semget:信号量

31.5 消息队列

  • 消息队列本质上是位于内核空间的链表,链表的每个节点都是一条消息。每一条消息都有自己的消息类型,消息类型用整数来表示,而且必须大于 0.

  • 不同类型的消息被挂载到对应类型的链表上

  • 消息类型为0的链表记录了所有消息加入队列的顺序

  • 消息数据格式:

    • 需要保证首4字节是一个整数,代表消息类型,大于0
    • 消息正文可以依照需求设计
  • 好处

    • 克服了消息传递信息小,管道只能承载无格式字节流以及缓冲区大小受限的不足
    • 它独立于发送和接收进程存在,避免有名管道的同步和阻塞问题
  • 核心函数:

    • msgsnd
    • msgrcv
    • msgget

31.6 信号量

  • 信号量是一个计数器,一般用于进程同步以及多进程对共享资源的访问
  • 等待(P(sv))就是将其值减一或者挂起进程,发送(V(sv))就是将其值加一或者将进程恢复运行

31.7 信号

  • 信号产生条件
    • 按键
    • 硬件异常
    • 进程调用kill函数将信号发送给另外一个进程
  • 信号传递的消息较少,一般用于通知进程某个事件的发生
    • 通过注册信号处理函数来对接收到的信号进行处理

31.7.1 如何通过kill结束进程?

  • https://blog.csdn.net/dghggij/article/details/84191519
  • kill -pid
    • 标准的kill命令通常都能达到目的。终止有问题的进程,并把进程的资源释放给系统。然而,如果进程启动了子进程,只杀死父进程,子进程仍在运行,因此仍消耗资源。为了防止这些所谓的“僵尸进程”,应确保在杀死父进程之前,先杀死其所有的子进程。
    • ps -ef | grep httpd 找到要杀死的父进程和子进程
  • kill -l PID
    • -l选项告诉kill命令用好像启动进程的用户已注销的方式结束进程。当使用该选项时,kill命令也试图杀死所留下的子进程。但这个命令也不是总能成功--或许仍然需要先手工杀死子进程,然后再杀死父进程
  • kill -TERM PPID
    • 给父进程发送一个TERM信号,试图杀死它和它的子进程。
  • kill -HUP PID
    • 该命令让Linux和缓的执行进程关闭,然后立即重启。在配置应用程序的时候,这个命令很方便,在对配置文件修改后需要重启进程时就可以执行此命令。
  • 绝杀 kill -9 PID
    • kill -s SIGKILL
    • 这个强大和危险的命令迫使进程在运行时突然终止,进程在结束后不能自我清理。危害是导致系统资源无法正常释放,一般不推荐使用,除非其他办法都无效。

31.8 套接字

  • 用于更为一般的进程间通信
  • 可以用于不同机器间的进程通信

31.9 不同通信方式的优缺点?

  • 管道:速度慢,容量有限;
  • Socket:任何进程间都能通讯,但速度慢;
  • 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题;
  • 信号量:不能传递复杂消息,只能用来同步;
  • 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存。

32. unix的IO模型有哪些?

  • 阻塞IO

  • 非阻塞IO

  • IO多路复用

    • 读写事件就绪后,需要自己负责进行读写操作,读写过程阻塞
  • 信号驱动IO

  • 异步IO

  • 前面四种可以归类为同步IO

  • 对于网络连接而言,一般是通过recvfrom/recv方法从socket上接收数据

32.1 阻塞IO

  • 程序通过阻塞IO获取数据的时候,会一直等待,直到数据到达内核,并且操作系统将数据拷贝到用户空间后,函数才会返回。此前,程序一直阻塞等待

32.2 非阻塞IO

  • 如果没有数据达到就立刻返回,如果有数据到达,就阻塞等待,直到数据被拷贝到用户空间
  • 通过设置socket的模式来实现非阻塞IO
    • 在创建socket的时候,指定对应的模式
    • 也可以在创建以后,通过fcntl函数来进行设置

32.3 IO多路复用

  • 一个线程能够监听多个IO,处理更多连接

32.4 信号驱动IO

32.5 异步IO

33. IO多路复用原理是什么?

  • IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。

33.1 select

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

  • fd_set 是一个用来装文件描述符的容器

33.1.1 select的返回值是什么?

  • select返回0,说明当前没有符合条件的文件描述符
  • <0,表示函数执行错误,或者select被信号打断
  • >0,表示有几个事件发生。是读集合中的事件数 + 写集合中的事件数 + 异常集合中的事件数。不同集合中的重复描述符会累计

33.1.2 select如何修改传入参数?

  • select返回后, 会把以前加入的但并无事件发生的fd从fd_set清除,因此需要重新调用select 前再次把关心的fd添加到FD_SET

  • 因此最好是每次监听的时候都将关心的fd添加到fd_set中

  • 如果某个集合中的描述符上有事件到来,在 select 返回时,会保留该描述符,所有其它未发生事件的描述符全部清除。

    • 通过该方式,当结果返回的时候,我们便可以通过遍历所有描述符,判断对应的描述符是否在对应的fd_set中,以此来判断对应连接是否有事件发生
  • 对于超时参数来说,如果在超时时间到达前发生异常或有事件到来,则该参数会被被更新为剩余时间。

  • 由于存放fd的是fd_set这个结构,fd_set容器的大小是固定的,默认是1024,底层是利用整型数组实现的

33.2 poll

  • 与select的区别:

33.3 epoll

  • 与前两者的区别:
    • 需要自己判断哪个描述符发生事件
      • 对于select和poll,再调用完毕后,需要自己去查询哪个描述符上发生了IO事件
      • epoll则不一样,它能够将所有发生事件的描述符保存到数组中,没有发生事件的描述符则不会保存到数组中,这意味着不再需要遍历所有描述符来判断谁发生了IO事件
    • 描述符复制
      • select和poll每次使用的时候都需要将监听的描述符传给他,即需要将描述符复制到内核空间
      • epoll只需事先复制一次,后续不再需要
  • 用法:

  • 触发方式:

    • 如果为 edge-triggered 方式,只有在缓冲区发生变化的情况下,epoll_wait 函数才会返回。
    • 如果为 level-triggered 方式,那么只要缓冲区有数据可读,或者缓冲区有空位可写,则 epoll_wait 就返回。
  • 触发方式的效率问题

    • 假设每次read的buf都比较小,无法一次性将缓冲区的数据全部读完
    • 因此,为了能够读取完数据,可以选用水平触发+阻塞读的方式,只要缓冲区还有数据就不停地读,但是这样子的话每次都会触发 epoll_wait 函数的返回
    • 另外一种方式是边沿模式+非阻塞读,触发一次 epoll_wait 后,然后read循环读
前者:while { epoll_wait + read } 后者:epoll_wait + while {read}

33.4 select poll epoll的区别是什么?

select poll epoll
操作方式 遍历 遍历 回调
底层实现 数组 链表 哈希表
IO效率 每次调用都进行线性遍历,时间复杂度为O(n) 每次调用都进行线性遍历,时间复杂度为O(n) 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1)
最大连接数 1024(x86)或2048(x64) 无上限 无上限
fd拷贝 每次调用select,都需要把fd集合从用户态拷贝到内核态 每次调用poll,都需要把fd集合从用户态拷贝到内核态 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝
  • 如果是连接数量不是特别多,但是经常会有连接加入或者退出的时候,就要考虑poll或者select了。

img

34. 同步IO与异步IO的异同?

  • 同步IO的情况下,最后需要自己主动调用read函数把内核缓冲区的数据复制到read函数的接收缓冲区,同理,write也是
  • 异步IO的情况下,最后是操作系统在某个未知的时刻帮你把数据复制到你指定的缓冲区中,write也是一样

35. 线程有哪几种?

35.1 用户线程

  • 线程在用户空间实现,操作系统检测不到线程的存在,只能看到进程
  • 好处
    • 用户线级线程只能参与竞争该进程的处理器资源,不能参与全局处理器资源的竞争。
    • 用户级线程切换都在用户空间进行,开销极低。
    • 用户级线程调度器在用户空间的线程库实现,内核的调度对象是进程本身,内核并不知道用户线程的存在。
  • 不足
    • 如果触发了引起阻塞的系统调用的调用,会立即阻塞该线程所属的整个进程。
    • 系统只看到进程看不到用户线程,所以只有一个处理器内核会被分配给该进程 ,也就不能发挥多核 CPU 的优势 。

35.2 内核线程

  • 操作系统内核支持的线程就是内核线程,线程的切换由内核完成
  • 内核线程建立和销毁都是由操作系统负责、通过系统调用完成
  • 程序员只需要使用系统调用,而不需要自己设计线程的调度算法和线程对 CPU 资源的抢占使用
  • 好处
    • 内核级线级能参与全局的多核处理器资源分配,充分利用多核 CPU 优势。
    • 每个内核线程都可被内核调度,因为线程的创建、撤销和切换都是由内核管理的。
    • 一个内核线程阻塞与他同属一个进程的线程仍然能继续运行。
  • 不足
    • 内核级线程调度开销较大。调度内核线程的代价可能和调度进程差不多昂贵,代价要比用户级线程大很多。
    • 线程表是存放在操作系统固定的表格空间或者堆栈空间里,所以内核级线程的数量是有限的。

35.3 轻量级进程

  • 由内核支持的用户线程

  • 每一个轻量级进程都与一个特定的内核线程关联

  • 一个进程中可以有一个或者多个轻量级进程

  • 在这种系统中,轻量级进程就是用户线程

  • 由于是基于内核线程实现的,因此优缺点与内核线程相同

  • Linux 并没有为线程准备特定的数据结构

  • Linux 眼中的线程只是与其它进程共享资源的轻量级进程

    • 轻量级是因为它只有一个最小的执行上下文和调度程序所需的统计信息,与父进程共享进程地址空间
  • 在Linux中,线程是一个轻量级进程,只是优化了线程调度的开销

  • 在 JVM 中的线程和内核线程是一一对应的,线程的调度完全交给了内核

35.4 用户线程加轻量级进程混合实现

  • 存在用户线程、内核线程和轻量级进程

  • 轻量级进程作为用户线程和内核线程之间的桥梁

  • 用户线程与轻量级进程的数量比是不定的,即为 N:M 的关系

  • 好处

    • 保留了用户线程创建、切换、析构的快速性
    • 轻量级进程的存在使得可以使用内核提供的线程调度功能,降低了整个进程被完全阻塞的风险
  • 轻量级进程是建立在内核之上并由内核支持的用户线程

  • 每一个轻量级进程都与一个特定的内核线程关联

  • 内核线程只能由内核管理并像普通进程一样被调度

  • 线程调度由内核完成了,而其他线程操作(同步、取消)等都是核外的线程库(Linux Thread)函数完成的

36. 协程是什么?

  • 比线程更加轻量级的微线程
  • 一个线程可以拥有多个协程
  • 用户空间线程

36.1 与线程相比有什么区别?

  • 线程是被内核调度的,涉及到内核态和用户态的切换,开销较多
  • 操作系统内核对协程一无所知,协程调度完全由用户程序掌控,无需进行状态切换,开销较小
  • 切换时只需要变更寄存器上下文和栈即可

37. ARP协议流程?

  • https://mp.weixin.qq.com/s/nD2uRNUQckcNRsVxBL1S6A

  • 当发送数据时,主机 A 会在自己的 ARP 缓存表中寻找是否有目标 IP 地址,如果找到,就把对应的目标 MAC 地址封装进帧里进行发送。

  • 如果没有找到,主机 A 就会向网络中发送一个广播(ARP request),和主机 A 同网段内的所有主机都会收到这个请求,该请求的目标 MAC 地址是"FF.FF.FF.FF.FF.FF",目标 IP 是主机 B 的 IP。

  • 只有主机 B 会接收这个请求,并且向主机 A 做出回应(ARP response),而其他主机接收到请求之后发现目标 IP 不是自己,就会选择丢弃。主机 B 从请求中获得主机 A 的 MAC 地址和 IP 地址,所以会以单播的方式进行回应,同时更新自己的 ARP 缓存表。

  • 主机 A 接收到主机 B 的响应之后,也会更新自己的 ARP 缓存,下次再访问主机 B 时,就直接从 ARP 缓存里查找即可。

  • ARP缓存超时

  • 如果一段时间内,某个条目一直没有使用,那么就进行删除操作,减少长度,加快查询速度

  • 缓存时间一般为20分钟,不完整条目可能为3分钟

    • 不完整条目,即对不存在的主机进行ARP请求(没有这个IP地址对应的主机)

38. ARP欺骗是什么?

  • ==ARP 报文没有任何认证==。攻击者可以发送伪造的 ARP 报文(尤其是应答报文),恶意修改网关或网络内其他主机的 ARP 表项,造成报文的转发异常,这就是 ARP 欺骗攻击( ARP spoofing

  • ==ARP 报文没有状态==。它不会去检查自己是否发过请求包,也不知道自己是否发过请求包,也不管应答是否合理,只要收到目标 MAC 是自己的 reply 或者 request 广播包,都会照单全收,写进自己的 ARP 缓存,原有相同的表项就会被替换。如果攻击者利用这一特性,发送大量伪造的 ARP 应答报文,造成主机 ARP 表项溢出,导致无法缓存正常的 ARP 表项,从而影响报文的正常转发,这就是大名鼎鼎的拒绝服务攻击(DDoS)或 ARP 泛洪攻击。

  • 后果

    • 网络不稳定,引发用户无法上网或者企业断网导致重大生产事故。
    • 非法获取游戏、网银、文件服务等系统的帐号和口令,给被攻击者造成利益上的重大损失。
  • 解决方法

    • 动态ARP检测方案(DAI)
      • 在网络设备(比如交换机和路由器)上来做,在攻击数据进入用户主机之前,就将它阻隔
      • 简单来说,就是交换机或者路由器维护着一个 DAI 表,表中记录这每个接口对应的 IP 和 MAC,比如:port<->mac<->ip。如果当某个接口收到的 ARP 应答包,IP 和 MAC 对应关系和 DAI 表中记录的不一致,那么就可以将这个包丢弃。
    • ARP***
      • 根据网络数据包的特征,自动识别局域网存在的 ARP 扫描和欺骗行为,并做出攻击判断(哪个主机做了攻击,IP 和 MAC 是多少)
    • 手动绑定局域网IP和MAC映射关系
      • 静态绑定的 ARP 项优先级高于动态学习到的

39. 证书的域名加密范围?

  • 单域名证书
    • 可以绑定一条带www和不带www的域名
  • 多域名证书
    • 一张证书可以保护多个不同的域名
  • 通配符域名证书
    • 能够保护一个域名以及该域名的所有下一级域名

40. https是对请求头还是请求体加密?

  • 两个都会加密
  • https相当于在http和TCP之间加上了一层SSL/TLS加密协议,因此http的全部数据都会进行加密

41. Content-Length首部字段的作用?

  • 用于描述HTTP消息实体的传输长度
  • 消息实体长度和消息实体的传输长度是有区别,比如说gzip压缩下,消息实体长度是压缩前的长度,消息实体的传输长度是gzip压缩后的长度

41.1 该字段是否必须?

  • 在Http 1.0及之前版本中,content-length字段可有可无

  • 在http1.1及之后版本。如果是 Connection: keep-alive,则content-length和chunk必然是二选一。

    • 服务器需要根据 Content-Length 这个字段来判断数据是否接收完成
    • 如果设置了Transfer-Encoding: chunked,那么就不可以再设置 Content-Length
    • 若是非keep alive,则和http1.0一样。content-length可有可无
  • Content-Length过短会导致实体数据被截断,过长会导致超时

42. 请求体的数据结构?

  • multipart/form-data
    • 表单形式,可以上传文件(键值对形式,可以传多个)
  • application/x-www-from-urlencoded
    • 以键值对的形式提交
  • raw
    • 文本形式
    • text,json,xml,html等
  • binary
    • 二进制数据,通常用来传输文件,没有键值对,因此一次只能传一个

43. 为什么要区分内核态和用户态?

  • 在CPU指令中,有一部分代码非常危险,一旦用错,那么整个系统都会崩溃

  • 因此,CPU将指令分为特权指令和非特权指令,特权指令只有处于内核态的情况下才能使用,非特权指令可以在用户态下使用

  • 当用户程序想要使用特权指令时,只能通过系统调用来请求操作系统提供的服务程序来完成工作

  • 避免用户程序直接调用特权指令导致系统崩溃

  • 将操作系统的运行状态分为用户态和内核态,主要是为了对访问能力进行限制,防止随意进行一些比较危险的操作导致系统的崩溃,比如设置时钟、内存清理,这些都需要在内核态下完成 。

  • 用户态

    • 只能访问受限的内存空间
  • 内核态

    • 可以访问内存的所有数据

44. 用户态切换到内核态时机

  • 系统调用
    • 用户进程主动切换到内核态
    • 用户态进程通过系统调用申请使用操作系统提供的服务完成工作
    • 核心是使用操作系统为用户提供的一个中断来实现
      • 例如Linux的int 80h中断
  • 异常
    • 运行的用户进程遭遇事先不可知的异常,切换到处理该异常的内核相关进程,转到了内核态
    • 比如:缺页异常
  • 外设中断
    • 外设完成相应操作后,会触发相应中断,切换到内核中对应的处理程序
    • 比如说,硬盘读写完成,会切换到相应的中断处理程序,执行后续操作

45. java中,主线程消亡子线程会如何?

  • 子线程与主线程其实没有强依赖关系
  • 线程对于其所在的进程才存在强依赖关系
  • 因此,只要进程没有退出,那么即时主线程消亡,子线程仍然会继续执行

46. 操作系统的进程空间划分是怎么样的?

  • 栈区

    • 由编译器自动分配释放,存放函数的参数值、局部变量等
  • 堆区

    • 一般有程序员分配释放,若程序员不释放,程序结束时由OS回收
  • 静态区

    • 存放全局变量和静态变量
  • 代码区

    • 存放函数体的二进制代码
  • 线程共享堆、静态区

47. 磁盘调度算法有哪些?

  • 先来先服务
    • 按照磁盘请求的顺序进行调度
    • 好处
      • 公平简单
    • 不足
      • 未对寻道做任何优化,寻道时间可能较长
  • 最短寻道时间优先
    • 优先调度与当前磁道最近的磁道
    • 好处
      • 平均时间缩短
    • 不足
      • 有可能会出现饥饿现象
  • 电梯算法
    • 磁头总是保持一个方向运行,直到该方向上没有请求为止,再转变方向
    • 好处
      • 考虑移动方向,所有请求都会满足
      • 解决最短寻道时间优先方法的饥饿问题

48. 互斥量是什么?

  • 个人感觉
    • 互斥量的使用与锁比较类似,在使用前需要进行分配或者初始化
    • 在操作共享变量前,先尝试上锁,上锁成功可以操作,否则不能操作

49. 产生死锁的条件?预防策略?避免策略?解决方法?银行家算法?

  • 四大条件

    • 互斥
    • 循环等待
    • 不剥夺
    • 请求并持有
  • 死锁预防

    • 破坏四大条件之一
    • 可以给资源编号,按照顺序请求资源
  • 死锁避免

  • 数据结构

    • 可利用资源向量Available
      • 是个含有m个元素的数组,其中的每一个元素代表一类可利用的资源数目。如果Available[j]=K,则表示系统中现有Rj类资源K个。
    • 最大需求矩阵Max
      • 这是一个n×m的矩阵,它定义了系统中n个进程中的每一个进程对m类资源的最大需求。如果Max[i,j]=K,则表示进程i需要Rj类资源的最大数目为K。
    • 分配矩阵Allocation
      • 这也是一个n×m的矩阵,它定义了系统中每一类资源当前已分配给每一进程的资源数。如果Allocation[i,j]=K,则表示进程i当前已分得Rj类资源的 数目为K。
    • 需求矩阵Need
      • 这也是一个n×m的矩阵,用以表示每一个进程尚需的各类资源数。如果Need[i,j]=K,则表示进程i还需要Rj类资源K个,方能完成其任务。
    • Need[i,j]=Max[i,j]-Allocation[i,j]
  • 两个向量

    • 工作向量Work:表示系统可提供给进程继续运行所需的各类资源数目,安全算法开始时,Work:=Available。
    • Finish[]:表示系统是否有足够的资源分配给进程,使之运行完成。开始时先令Finish[i]:=false,当有足够资源分配给进程时,再令Finish[i]:=true。
  • 对于进程每一次请求的资源,先判断是否大于它需要的资源,如果是,报错

  • 请求的资源如果小于系统目前有的资源,那么可以尝试分配,但是要校验分配后系统是否处于安全状态

  • 概念

    • 安全序列:

      是指一个进程序列{P1,…,Pn}是安全的,即对于每一个进程Pi(1≤i≤n),它以后尚需要的资源量不超过系统当前剩余资源量与所有进程Pj (j < i )当前占有资源量之和。

      存在一种资源分配的顺序,保证后续的进程都能拿到需要的资源并且完成工作

    • 安全状态:

      如果存在一个由系统中所有进程构成的安全序列P1,…,Pn,则系统处于安全状态。安全状态一定是没有死锁发生。

    • 不安全状态:

      不存在一个安全序列。不安全状态不一定导致死锁。

  • 具体处理方法

    • 鸵鸟政策
      • 因为解决死锁代价很高,而发生死锁的概率较低,因此可以直接采用鸵鸟政策
    • 死锁检测与死锁恢复
      • 不阻止死锁,但是检测到死锁会尝试恢复
      • 检测方法通过判断有向图是否存在环路

50. 进程、线程、协程的异同

进程 线程 协程
定义 资源分配和拥有的基本单位 程序执行的基本单位 用户态的轻量级线程,线程内部调度的基本单位
切换情况 进程CPU环境(栈、寄存器、页表和文件句柄等)的保存以及新调度的进程CPU环境的设置 保存和设置程序计数器、少量寄存器和栈的内容 先将寄存器上下文和栈保存,等切换回来的时候再进行恢复
切换者 操作系统 操作系统 用户
切换过程 用户态->内核态->用户态 用户态->内核态->用户态 用户态(没有陷入内核)
调用栈 内核栈 内核栈 用户栈
拥有资源 CPU资源、内存资源、文件资源和句柄等 程序计数器、寄存器、栈和状态字 拥有自己的寄存器上下文和栈
并发性 不同进程之间切换实现并发,各自占有CPU实现并行 一个进程内部的多个线程并发执行 同一时间只能执行一个协程,而其他协程处于休眠状态,适合对任务进行分时处理
系统开销 切换虚拟地址空间,切换内核栈和硬件上下文,CPU高速缓存失效、页表切换,开销很大 切换时只需保存和设置少量寄存器内容,因此开销很小 直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快
通信方面 进程间通信需要借助操作系统 线程间可以直接读写进程数据段(如全局变量)来进行通信 共享内存、消息队列
  • 线程和协程的区别:

  • 线程和进程都是同步机制,而协程是异步机制。

  • 线程是抢占式,而协程是非抢占式的。需要用户释放使用权切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。

  • 一个线程可以有多个协程,一个进程也可以有多个协程。

  • 协程不被操作系统内核管理,而完全是由程序控制。线程是被分割的CPU资源,协程是组织好的代码流程,线程是协程的资源。但协程不会直接使用线程,协程直接利用的是执行器关联任意线程或线程池。

  • 协程能保留上一次调用时的状态。

  • 线程和进程的切换流程区别:

  • 进程切换分两步:

    • 切换页表以使用新的地址空间,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。
    • 切换内核栈和硬件上下文。
  • 对于linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2步是进程和线程切换都要做的。

  • 因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。

51. 进程和线程的关系?

  • 进程 :
    • 进程是资源分配的基本单位。
    • 进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对PCB 的操作。
  • 线程 :
    • 线程是独立调度的基本单位。
    • 一个进程中可以有多个线程,它们共享进程资源。
  • 区别 :
    • 拥有资源 :
      • 进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。
    • 调度 :
      • 线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
    • 系统开销 :
      • 由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小
      • 由于不同进程的资源相互独立,因此在创建和销毁进程的时候,系统需要进行资源分配和回收,因此进程的创建和销毁的开销要大于线程的创建和销毁。
      • 在进行进程切换的时候,需要对CPU环境和主存进行切换,而线程切换只需要对少部分的寄存器内容进行切换,开销较小
    • 通信方面 :
      • 线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。

52. Linux下同步机制?

  • POSIX信号量:可用于进程同步,也可用于线程同步。

  • POSIX互斥锁 + 条件变量:只能用于线程同步。

53. 逻辑地址和物理地址的异同?

  • 编译时只需要确定变量相对于进程在内存中起始地址的相对地址即可
  • 实际运行时,通过进程的起始地址+相对地址就可以得到物理地址
  • 相对地址就是逻辑地址,绝对地址就是物理地址

54. 进程执行过程?

  • 编译:将源代码编译成若干模块

  • 链接:将编译后的模块和所需要的库函数进行链接。链接包括三种形式:静态链接,装入时动态链接(将编译后的模块在链接时一边链接一边装入),运行时动态链接(在执行时才把需要的模块进行链接)

  • 装入:将模块装入内存运行

  • 装入的时候会使用分页技术

55. PCB是什么?进程地址空间包含什么?

  • PCB就是进程控制块,是操作系统中的一种数据结构,用于表示进程状态,操作系统通过PCB对进程进行管理

  • PCB中包含有:进程标识符,处理器状态,进程调度信息,进程控制信息

  • 进程地址空间

    • 代码段text:存放程序的二进制代码
    • 初始化的数据Data:已经初始化的变量和数据
    • 未初始化的数据BSS:还没有初始化的数据

56. 内核空间和用户空间是怎样区分的?

  • 在Linux中虚拟地址空间范围为0到4G,最高的1G地址(0xC0000000到0xFFFFFFFF)供内核使用,称为内核空间,低的3G空间(0x00***00到0xBFFFFFFF)供各个进程使用,就是用户空间
  • 内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据

57. 同一个进程内的线程会共享什么资源?

  • 该进程的地址空间

  • 全局变量

  • 堆空间

  • 线程的栈空间是自己独有的

58. 字节序的大小端是什么含义?

  • 字节序是对象在内存中存储的方式,大端即为最高有效位在前面,小端即为最低有效位在前面

59. fork的时候,子进程和父进程间是什么关系?

  • https://blog.csdn.net/qq_22613757/article/details/88770579

  • fork的时候子进程会共享父进程的数据空间、代码段、数据段、堆栈

    • 复制了页表、复制了栈空间、但是没有复制物理页面,因此虚拟地址相同、物理地址相同,但是共享页标记为只读,一旦修改就需要复制物理页
  • 两者的虚拟空间不同(每个进程的虚拟空间都是独占的),但是一开始两者都共享相同的物理空间

  • 当父进程进行修改操作的时候,操作系统才会将父进程的数据拷贝一份给子进程

60. socket编程介绍一下

  • TCP的流式socket编程
// cli.c #include <stdio.h> #include <stdlib.h> #include <arpa/inet.h>  #define ERR_EXIT(msg) do { perror(msg); exit(1); } while(0)  int main() {  int sockfd, ret;  struct sockaddr_in servaddr;  struct sockaddr_in cliaddr;  socklen_t cliaddrlen;   // 目标网络进程的套接字地址  servaddr.sin_family = AF_INET;  servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");  servaddr.sin_port = htons(8080);   // 创建 socket  socket选项为流式套接字,也就是TCP协议  sockfd = socket(AF_INET, SOCK_STREAM, 0);   if (sockfd < 0) ERR_EXIT("socket");   // 发起连接  ret = connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));  if (ret < 0) ERR_EXIT("connect");   // 查看 connect 为我们将 socket 绑定到了哪个套接字地址上。  cliaddrlen = sizeof(cliaddr);  ret = getsockname(sockfd, (struct sockaddr*)&cliaddr, &cliaddrlen);  if (ret < 0) ERR_EXIT("getsockaddr");   printf("cliaddr: %s:%d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));   return 0; }  // 服务器处理程序案例 void server_routine() {  listenfd = socket(tcp);  // 创建套接字地址  servaddr = resolve(hostname, port);  bind(listenfd, servaddr);  listen(listenfd);   sockfd = accept(listenfd);   // do_server 中处理 IO  do_server(sockfd);  close(sockfd); }  do_server(int sockfd) {  while(1) {  // 从套接字读一行数据  n = readline(sockfd, buf);  // 如果等于 0 表示对端关闭。  if (n == 0) break;  // 将 buf 中的数据转换成大写  toUpper(buf);  // 发送给对方  writen(sockfd, buf, n);  } }   // 客户端处理程序案例 void client_routine() {  sockfd = socket(tcp);  servaddr = resolve(hostname, port);  // 连接服务器  connect(sockfd, servaddr);   doClient(sockfd);  close(sockfd); }   doClient(int sockfd) {  // 从标准输入读一行  while(fgets(buf, stdin)) {  // 发送给服务器  writen(sockfd, buf, strlen(buf));  // 从服务器接收数据  n = readline(sockfd, buf);  // 如果 n == 0 表示对端关闭  if (n == 0) break;  } }
  • UDP的数据包socket
/// 服务端程序  // serv.c #include <stdio.h> #include <stdlib.h> #include <arpa/inet.h>   #define ERR_EXIT(msg) do { perror(msg); exit(1); } while(0)  void upper(char* buf) {  char* p = buf;  while(*p) {  *p = toupper(*p);  ++p;  } }  int main() {  struct sockaddr_in servaddr, cliaddr;  int sockfd, clientfd, ret, n;  socklen_t cliaddrlen;  char buf[64];   // 1. create sockaddr  puts("1. create sockaddr");  servaddr.sin_family = AF_INET;  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  servaddr.sin_port = htons(8080);   // 2. create socket  puts("2. create socket");  // 注意第 2 个参数已经改成了 SOCK_DGRAM  这个参数表示当前socket是基于UDP协议的,使用报文传输  sockfd = socket(AF_INET, SOCK_DGRAM, 0);   if (sockfd < 0) ERR_EXIT("socket");   // 3. bind sockaddr  puts("3. bind sockaddr");  ret = bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));  if (ret < 0) ERR_EXIT("bind");   while(1) {  // 注意 recvfrom 的最后一个参数 addrlen 既是输入参数,也是输出参数,所以这里必须要传一个值给它。  cliaddrlen = sizeof(cliaddr);  n = recvfrom(sockfd, buf, 63, 0, (struct sockaddr*)&cliaddr, &cliaddrlen);  // 打印对端的 ip 地址和端口号  printf("%s:%d come in\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));  buf[n] = 0;  puts(buf);  upper(buf);  // 将转换后的数据发送给对端  sendto(sockfd, buf, n, 0, (struct sockaddr*)&cliaddr, cliaddrlen);  }   close(sockfd);   return 0; } 
 //// 客户端程序 #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h>  #define ERR_EXIT(msg) do { perror(msg); exit(1); } while(0)  int main() {  int sockfd, ret, n;  char buf[64];  struct sockaddr_in servaddr;  struct sockaddr_in cliaddr;  socklen_t servaddrlen, cliaddrlen;   memset(&servaddr, 0, sizeof(servaddr));  servaddr.sin_family = AF_INET;  servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");  servaddr.sin_port = htons(8080);   // 注意第 2 个参数已经改成了 SOCK_DGRAM  sockfd = socket(AF_INET, SOCK_DGRAM, 0);   if (sockfd < 0) ERR_EXIT("socket");   while(1) {  scanf("%s", buf);  if (buf[0] == 'q') break;  // 将数据发送给服务器  sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&servaddr, sizeof(servaddr));  // recvfrom 最后两个参数可以为空,表示我们并不关心对端的套接字地址(因为我们本来就知道……)  n = recvfrom(sockfd, buf, 63, 0, NULL, NULL);   buf[n] = 0;  puts(buf);  }   close(sockfd);  return 0; }

61. 套接字选项有哪些?

  • 通过getsockopt 和 setsockopt 函数可以获取和修改套接字的选项
  • 通过这些函数可以启动和关闭套接字的相关特性
  • 函数原型
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h>  int getsockopt(int sockfd, int level, int optname,  void *optval, socklen_t *optlen); int setsockopt(int sockfd, int level, int optname,  const void *optval, socklen_t optlen);

这里写图片描述

  • ==SO_REUSEADDR==

  • setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &onoff, sizeof(onoff));

  • 需要在bind监听套接字前设置,避免时序问题

  • 功能

    • 重用处于time_wait的端口
    • 一个端口上启动同一个服务器的多个实例,只要每个实例绑定不同的ip地址
    • 允许单一进程捆绑同一端口到多个套接字上,只要每次指定的 ip 地址不同即可
    • 针对 UDP 套接字,允许完全重复的捆绑(completely duplicate binding),即 ip 地址和端口号都重复
      • 针对这个,引出广播和多播问题
  • ==TCP_NODELAY==

  • 使用该选项可以关闭Nagle算法

  • nagle算法目的是为了减少网络中的小分组,但是有可能会导致数据的延时

    • 发送方会缓存数据,直到上一个小分组的数据的ack到达,或者缓存数据大小超过MSS
    • 考虑发送方有可能会使用延时ack,也就是携带确认或者累计确认,ack不一定会立即发送出来,因此有可能导数数据延时
  • ret = setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &onoff, sizeof(onoff));

  • ==TCP_CORK==

  • 用来完全避免小分组出现在网络上,将TCP发送缓冲区完全堵上

  • 满足以下条件之一就会发送数据

    • 缓冲区满了
    • 数据达到一定大小,比如说超过了MSS
    • 一段时间没有数据进来(比如200ms)
    • 遇到FIN

62. mmap和sendfile介绍一下?

Image

  • 首先,mmap将硬盘上的文件与逻辑地址空间的区域建立了对应关系

  • 结合上图可以看出,当ptr指向的文件没有加入内存的时候,会触发缺页中断,由中断处理函数将硬盘上的数据页拷贝到内存中去

  • 与read的区别

  • read是系统调用,在调用read读取硬盘数据的时候,需要先将硬盘数据读取到内核空间中的缓冲区,然后再复制到用户空间

  • mmap也是系统调用,但是在调用时只是建立了逻辑地址与硬盘文件的映射关系,真正的数据拷贝是在缺页中断处理时完成,这个时候是直接将文件从硬盘拷贝到用户空间,只用了一次数据拷贝,效率更高

    • 应该说用户逻辑空间与内核中的读缓冲空间建立起了联系比较合理
    • 这样子就不用将页再复制到用户空间了
  • 好处

  • 直接映射到内核空间的读缓冲区,减少了一次拷贝

  • 避免了将数据拷贝到用户空间,节省了一般的内存空间,适合大文件传输

  • 正常情况下,我们读取文件的流程为,先通过系统调用从磁盘读取数据,存入操作系统的内核缓冲区,然后在从内核缓冲区拷贝到用户空间,而内存映射,是将磁盘文件直接映射到用户的虚拟存储空间中,通过页表维护虚拟地址到磁盘的映射

  • 通过内存映射的方式读取文件的好处有,因为减少了从内核缓冲区到用户空间的拷贝,直接从磁盘读取数据到内存,减少了系统调用的开销,对用户而言,仿佛直接操作的磁盘上的文件,另外由于使用了虚拟存储,所以不需要连续的主存空间来存储数据

img

  • 在 Java 中,我们使用 MappedByteBuffer 来实现内存映射,这是一个堆外内存,在映射完之后,并没有立即占有物理内存,而是访问数据页的时候,先查页表,发现还没加载,发起缺页异常,然后在从磁盘将数据加载进内存,所以一些对实时性要求很高的中间件,例如rocketmq,消息存储在一个大小为1G的文件中,为了加快读写速度,会将这个文件映射到内存后,在每个页写一比特数据,这样就可以把整个1G文件都加载进内存,在实际读写的时候就不会发生缺页了,这个在rocketmq内部叫做文件预热.

  • sendfile

  • 数据页不进入用户空间,直接在内核空间传输到网卡

  • 因此只适用于静态文件传输,对用户来说,数据不可见

63. 什么是交换空间?

  • 操作系统把物理内存(physical RAM)分成一块一块的小内存,每一块内存被称为页(page)。当内存资源不足时,Linux把某些页的内容转移至硬盘上的一块空间上,以释放内存空间。硬盘上的那块空间叫做交换空间(swap space),而这一过程被称为交换(swapping)。物理内存和交换空间的总容量就是虚拟内存的可用容量。

  • 用途:

    • 物理内存不足时一些不常用的页可以被交换出去,腾给系统。
    • 程序启动时很多内存页被用来初始化,之后便不再需要,可以交换出去。

64. 缓冲区溢出是什么?有什么危害?

  • 缓冲区溢出是指当计算机向缓冲区填充数据时超出了缓冲区本身的容量,溢出的数据覆盖在合法数据上。

    危害有以下两点:

    • 程序崩溃,导致拒绝服务
    • 跳转并且执行一段恶意代码
  • 造成缓冲区溢出的主要原因是程序中没有仔细检查用户输入。

65. 硬链接和软链接有什么区别?

  • 硬链接就是在目录下创建一个条目,记录着文件名与 inode 编号,这个 inode 就是源文件的 inode。删除任意一个条目,文件还是存在,只要引用数量不为 0。但是硬链接有限制,它不能跨越文件系统,也不能对目录进行链接。
  • 符号链接文件保存着源文件所在的绝对路径,在读取时会定位到源文件上,可以理解为 Windows 的快捷方式。当源文件被删除了,链接文件就打不开了。因为记录的是路径,所以可以为目录建立符号链接。

66. 中断处理过程?

  • 保护现场:将当前执行程序的相关数据保存在寄存器中,然后入栈。
  • 开中断:以便执行中断时能响应较高级别的中断请求。
  • 中断处理
  • 关中断:保证恢复现场时不被新中断打扰
  • 恢复现场:从堆栈中按序取出程序数据,恢复中断前的执行状态。

67. 轮询和中断是什么?

  • 轮询:CPU对特定设备轮流询问。中断:通过特定事件提醒CPU。
  • 轮询:效率低等待时间长,CPU利用率不高。中断:容易遗漏问题,CPU利用率不高。

68. 操作系统如何实现锁?

  • 首先要搞清楚一个概念,在硬件层面,CPU提供了原子操作、关中断、锁内存总线的机制OS基于这几个CPU硬件机制,就能够实现锁;再基于锁,就能够实现各种各样的同步机制(信号量、消息、Barrier等)
  • 在多线程编程中,为了保证数据操作的一致性,操作系统引入了锁机制,用于保证临界区代码的安全。通过锁机制,能够保证在多核多线程环境中,在某一个时间点上,只能有一个线程进入临界区代码,从而保证临界区中操作数据的一致性。
  • 锁机制的一个特点是它的同步原语都是原子操作
  • 那么操作系统是如何保证这些同步原语的原子性呢?
  • 操作系统之所以能构建锁之类的同步原语,是因为==硬件已经为我们提供了一些原子操作==,例如:
    • 中断禁止和启用(interrupt enable/disable)
    • 内存加载和存入(load/store)测试与设置(test and set)指令
  • 禁止中断这个操作是一个硬件步骤,中间无法插入别的操作。同样,中断启用,测试与设置均为一个硬件步骤的指令。在这些硬件原子操作之上,我们便可以构建软件原子操作:锁,睡觉与叫醒,信号量等。

68.1 操作系统使用锁的原语操作有哪些?

  • 可以使用中断禁止,测试与设置两种硬件原语来实现软件的锁原语。这两种方式比较起来,显然测试与设置更加简单,也因此使用的更为普遍。此外,test and set还有一个优点,就是可以在多CPU环境下工作,而中断启用和禁止则不能

  • 使用中断启用与禁止来实现锁:

    • 要防止一段代码在执行过程中被别的进程插入,就要考虑在一个单处理器上,一个线程在执行途中被切换的途径。我们知道,要切换进程,必须要发生上下文切换,上下文切换只有两种可能:

      • 一个线程自愿放弃CPU而将控制权交给操作系统调度器通过yield之类的操作系统调用来实现);
      • 一个线程被强制放弃CPU而失去控制权(通过中断来实现)
    • 原语执行过程中,我们不会自动放弃CPU控制权,因此要防止进程切换,就要在原语执行过程中不能发生中断。所以采用禁止中断,且不自动调用让出CPU的系统调用,就可以防止进程切换,将一组操作变为原子操作。

    • 中断禁止:就是禁止打断,使用可以将一系列操作变为原子操作

    • 中断启用:就是从这里开始,可以被打断,允许操作系统进行调度

    • 缺点:使用中断实现锁,繁忙等待,不可重入

  • 使用测试与设置指令来实现锁

    • 测试与设置(test & set)指令:以不可分割的方式执行如下两个步骤:

      • 判断值是否与预期一致,如果一致就修改值变为新值
      • 设置操作:将1写入指定内存单元;
      • 读取操作:返回指定内存单元里原来的值(写入1之前的值)
    • 缺点:繁忙等待,不可重入

69. 操作系统中的堆栈是什么?有什么区别?

  • 操作系统的堆和栈是指对内存进行操作和管理的一些方式,这和数据结构中的堆和栈是有区别的
    • 栈也可以称之为栈内存,是一个具有动态内存区域,存储函数内部(包括main函数)的局部变量,方法调用及函数参数值
    • 由编译器/系统自动分配和释放。例如,声明在函数中一个局部变量,即int b,系统自动在栈中为变量b开辟空间
    • 栈存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈,满足:“先进后出”的原则存取,也就是位于栈内的元素,必须等到其上面(对应的地址为较低的地址)的数据或函数执行完成后,弹出后才可以进行下面的元素的操作
    • 栈是由系统自动分配的,一般速度较快(栈的速度高于堆的速度
    • 申请大小的限制:栈是向低地址扩展的,是一块连续的内存的区域。栈顶的地址和栈的最大容量是系统预先规定好的,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数 ) ,如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小
    • 一般由程序员分配释放,并指明大小,堆被程序申请使用的内存在被主动释放前一直有效。堆需要由由程序员手动释放,不及时回收容易产生内存泄露。 程序结束时可能由操作系统回收。
    • 栈是存放在一级缓存中的,而堆则是存放在二级缓存中的,堆的生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收),所以调用这些对象的速度要相对来得低一些,故堆的速度慢于栈的速度
    • 与数据结构中的堆是不同的,分配方式类似于链表(空闲链表法),堆是向高地址扩展的数据结构,是不连续的内存区域,这是由于系统是用链表来存储空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
  • 区别
    • 空间分配:栈由操作系统自动分配释放;堆一般由程序员分配释放
    • 申请效率对比:栈使用一级缓存,被调用时通常处于存储空间中,调用后被立即释放;.堆使用二级缓存,生命周期与虚拟机的GC算法有关,调用速度相对较低。
    • 申请大小的限制:栈是向低地址扩展的数据结构,是一块连续的内存的区域;堆是向高地址扩展的数据结构,是不连续的内存区域

70. cpu占用率和负载的区别?

  • CPU占用率:显示的是程序在运行期间实际占用的CPU百分比。也就是说在一段时间内,CPU真正运行的时长

  • CPU负载:显示的是一段时间内正在使用和等待使用CPU的平均任务数。

    • top命令中的load average:系统平均负载是CPU的Load,它所包含的信息不是CPU的使用率状况,而是在一段时间内CPU正在处理以及等待CPU处理的进程数之和的统计信息,也就是CPU使用队列的长度的统计信息。这个数字越小越好
    • Linux记录cpu负载的时候是将cpu队列中的运行进程数和不可中断进程数都统计在内的,这样在对cpu负载分析的时候就需要考虑不可中断的进程的情况
  • 对于单核CPU而言,如果每隔5分钟检测一次得出的负载均为1的话,那么说明CPU一直很忙,一般来说,0.7会比较合适,然后乘以核数,得出总的CPU负载

  • 高cpu使用率时,负载不一定很高 一个机器48个核如果全100%的使用率,那么整个机器使用率是100%,但是负载也可能只是48刚好满负载而已。

  • 高负载的时候,cpu使用率不一定高 可能由于多个进程运行不可中断的IO,导致活跃进程数量增加,这个时候负载会飚的很高,但是cpu可能很低

71. vim使用?

img

  • 刚启动为命令模式,输入i变为输入模式,输入:进入底线命令模式,任何模式下按esc进入命令模式

  • 底线命令模式

    • q 退出程序
    • w 保存文件
#高频知识点汇总##学习路径##面经#
全部评论
🎉恭喜牛友成功参与 【创作激励计划】高频知识点汇总专场,并通过审核! ------------------- 创作激励计划5大主题专场等你来写,最高可领取500元京东卡和500元实物奖品! 👉快来参加吧:https://www.nowcoder.com/discuss/804743
点赞 回复
分享
发布于 2021-11-24 13:22
感谢分享
点赞 回复
分享
发布于 2021-11-30 20:16
滴滴
校招火热招聘中
官网直投
感谢分享
点赞 回复
分享
发布于 2021-12-01 10:36
贴主分享一份PDF呗
点赞 回复
分享
发布于 2021-12-01 10:38
楼主,可以分享一下PDF吗,感谢啦
点赞 回复
分享
发布于 2021-12-02 14:25
求PDF!
点赞 回复
分享
发布于 2021-12-03 12:43
求PDF楼主
点赞 回复
分享
发布于 2021-12-03 13:56
求pdf
点赞 回复
分享
发布于 2021-12-03 20:52
求pdf
点赞 回复
分享
发布于 2021-12-03 23:38
求PDF
点赞 回复
分享
发布于 2021-12-04 17:58
求PDF
点赞 回复
分享
发布于 2021-12-08 15:28
求大佬的pdf
点赞 回复
分享
发布于 2021-12-11 11:54
求大佬的pdf
点赞 回复
分享
发布于 2021-12-12 09:10
求pdf
点赞 回复
分享
发布于 2021-12-13 10:43
求pdf
点赞 回复
分享
发布于 2021-12-21 19:36
求pdf
点赞 回复
分享
发布于 2021-12-24 00:34
求PDF !
点赞 回复
分享
发布于 2021-12-24 12:34
求pdf
点赞 回复
分享
发布于 2022-01-02 21:16
求一份pdf
点赞 回复
分享
发布于 2022-01-03 11:21
求pdf😃😃
点赞 回复
分享
发布于 2022-01-05 23:07

相关推荐

34 278 评论
分享
牛客网
牛客企业服务