我的校招记录:校招笔记(零)_写在前面 ,以下是校招笔记总目录。

备注
算法能力(“刷题”) 这部分就是耗时间多练习,Leetcode-Top100 是很好的选择。 补充练习:codeTop
计算机基础(上)(“八股”) 校招笔记(一)__Java_Java入门 C++后端后续更新
校招笔记(一)__Java_面对对象
校招笔记(一)__Java_集合
校招笔记(一)__Java_多线程
校招笔记(一)__Java_锁
校招笔记(一)__Java_JVM
计算机基础(下)(“八股”) 校招笔记(二)__计算机基础_Linux&Git
校招笔记(三)__计算机基础_计算机网络
校招笔记(四)__计算机基础_操作系统
校招笔记(五)__计算机基础_MySQL
校招笔记(六)__计算机基础_Redis
校招笔记(七)__计算机基础_数据结构
校招笔记(八)__计算机基础_场景&智力题
校招笔记(九)__计算机基础_相关补充
项目&实习 主要是怎么准备项目,后续更新

四、操场系统

4.1 操作系统基础

0. (百度安全一面) 冯诺依曼结构有哪几个模块?分别对应现代计算机的哪几个部分?

  • 存储器:内存
  • 控制器:南桥北桥
  • 运算器:CPU
  • 输入设备:键盘
  • 输出设备:显示器、网卡

1. 什么是操作系统?

  1. 操作系统(Operating System,简称 OS)是管理计算机硬件与软件资源的程序,是计算机系统的内核与基⽯;
  2. 操作系统本质上是运行在计算机上的软件程序 ;
  3. 操作系统为用户提供⼀个与系统交互的操作界面 ;
  4. 操作系统分内核与外壳(我们可以把外壳理解成围绕着内核的应用程序,而内核就是能操作硬件的程序)。

2. 什么是系统调用呢? 能不能详细介绍⼀下?

根据进程访问资源的特点,我们可以把进程在系统上的运行分为两个级别:

  1. 用户态(user mode) : 用户态运行的进程或可以直接读取用户程序的数据。
  2. 系统态(kernel mode):可以简单的理解系统态运行的进程或程序⼏乎可以访问计算机的任何资源,不受限制。

我们运行的程序基本都是运行在用户态,如果我们调用操作系统提供的系统态级别的子功能咋办呢?那就需要系统调用了!

这些系统调用按功能大致可分为如下⼏类:

  • 设备管理。完成设备的请求或释放,以及设备启动等功能。
  • ⽂件管理。完成⽂件的读、写、创建及删除等功能。
  • 进程控制。完成进程的创建、撤销、阻塞及唤醒等功能。
  • 进程通信。完成进程之间的消息传递或信号传递等功能。
  • 内存管理。完成内存的分配、回收以及获取作业占用内存区大小及地址等功能。

3. CentOS 和 Linux的关系?

Linux意思较广广义的范围,多指是Linux内核。CentOS是RedHat的一个分支,RedHat是Linux的一个发行版本,RedHat与CentOS的区别在于,RedHat收费,CentOS免费。

4. 什么是分布式,优缺点?集群?

  • 分布式

    根据业务需求进行拆分成N个子系统,多个子系统相互协作才能完成业务流程子系统之间通讯使用RPC远程通讯技术。

  • 集群

    同一个工程部署在多个不同的服务器上。

  • 分布式优点

    1.把模块拆分,使用接口通信,降低模块之间的耦合度。

    2.把项目拆分成若干个子项目,不同的团队负责不同的子项目。

    3.增加功能时只需要再增加一个子项目,调用其它系统的接口就可以。

    4.可以灵活的进行分布式部署。

  • 分布式缺点

    1.系统之间交互需要使用远程通信,接口开发增加工作量。

    2.各个模块有一些通用的业务逻辑无法共用。

5. 在Linux/windows栈空间的大小?

  • Linux环境下由操作系统决定,一般是8KB , 通过ulimit命令查看以及修改

    在Linux下通过如下命令可查看和设置栈的大小:

    1
    2
    $ ulimit -a            # 显示当前栈的大小 (ulimit为系统命令,非编译器命令)       
    $ ulimit -s 32768 # 设置当前栈的大小为32MCopy to clipboardErrorCopied
  • Windows环境下由编译器决定,VC++6.0一般是1M \

    Windows平台下栈的大小是被记录在可执行文件中的(由编译器来设置),即:windows下可以由编译器决定栈大小,而在Linux下是由系统环境变量来控制栈的大小的。

6. ASCII、Unicode和UTF-8编码的区别?

  • ASCII : ASCII 只有127个字符,表示英文字母的大小写、数字和一些符号 ;

    常用中文需要两个字节,且不能和ASCII冲突,中国定制了GB2312编码格式。

  • Unicode: Unicode就是将不同语言统一到一套编码格式中,通常两个字节表示一个字符,而ASCII是一个字节表示一个字符 ;

    如果你编译的文本是全英文的,用Unicode编码比ASCII编码需要多一倍的存储空间,在存储和传输上就十分不划算。

  • UTF-8 : 把Unicode编码转化为 “可变长编码” UTF-8编码,UTF-8编码将Unicode字符按数字大小编码为1-6个字节,英文字母被编码成1个字节,常用汉字被编码成2个字节。

6.1 三者区别和联系
  • 计算机内存中,统一使用Unicode编码 ;

  • 当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码

举例说明:

例1 :记事本编辑(内存)→保存(磁盘)。

用记事本编辑的时候,从文件读取的UTF-8字符被转换为Unicode字符到内存里,编辑完成后,保存的时候再把Unicode转换为UTF-8保存到文件。

image-20210611131935690

例2:网络传输服务器→浏览器。

浏览网页的时候,服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器。

image-20210611132058826

7. 什么是并发和并行?同步和异步?

  • 并发和并行
    • 并发: 是指宏观上在一段时间内能同时运行多个程序
    • 并行 :则指同一时刻能运行多个指令
  • 同步和异步
    • 同步:可以理解为在执行完一个函数或方法之后,一直等待系统返回值或消息,这时程序是出于阻塞的,只有接收到返回的值或消息后才往下执行其他的命令。

    • 异步:执行完函数或方法后,不必阻塞性地等待返回值或消息,只需要向系统委托一个异步过程,那么当系统接收到返回值或消息时,系统会自动触发委托的异步过程,从而完成一个完整的流程。

8. 什么是共享?

  • 共享定义: 系统中的资源可以被多个并发进程共同使用 ;

  • 共享方式互斥共享和同时共享:

    • 互斥共享: 在同一时刻只允许一个进程访问,需要用同步机制来实现互斥访问 ,如:打印机。

4.2 进程和线程

1.请问64位和32位的区别

  1. 运行能力不同:64位可以一次性可以处理8个字节的数据量,而32位一次性只可以处理4个字节的数据量,因此64位比32位的运行能力提高了一倍。
  2. 内存寻址不同:64位最大寻址空间为2的64次方,理论值直接达到了16TB,而32位的最大寻址空间为2的32次方,为4GB,换而言之,就是说32位系统的处理器最大只支持到4G内存,而64位系统最大支持的内存高达亿位数。
  3. 运行软件不同:由于32位和64位CPU的指令集是不同的。所以需要区分32位和64位版本的软件。
    为了保证兼容性,64位CPU上也能运行老的32位指令,但反过来32位系统不可以运行64位的软件。

2.介绍一下线程和进程的区别?

  1. 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

  2. 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小

  3. 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程

  4. 内存分配:同一进程的线程共享本进程的【地址空间和资源】,而进程之间的地址空间和资源是相互独立的

  5. 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉所以多进程要比多线程健壮

    线程没有独立的地址空间,如果崩溃,会发信号,如果没有错误处理的handler,OS一般直接杀死进程。

  6. 能否独立:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

2.1 线程和协程之间的区别?

进程是资源调度的基本单位运行一个可执行程序会创建一个或多个进程,进程就是运行起来的可执行程序 ;

线程是程序执行的基本单位,是轻量级的进程。每个进程中都有唯一的主线程,和多个线程,且只能有一个,主线程和进程是相互依存的关系,主线程结束进程也会结束;

协程是用户态的轻量级线程也是线程内部调度的基本单位

协程和线程的区别如下(补充了和进程的区别,方便对比)。

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

一个进程可用虚拟空间是(C++)2G,默认情况下,(假设)线程的栈的大小是1MB(Linux是8kb),则理论上最多只能创建2048个线程。如果要创建多于2048的话,必须修改编译器的设置。

2.3 进程之间的同步方式?(区分通信方式)
  1. 临界区。 对临界资源进行访问的那段代码称为临界区。

    为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查。

  2. 同步和互斥

    • 同步:多个进程因为合作产生的直接制约关系,使得进程有一定的先后执行关系。
    • 互斥:多个进程在同一时刻只有一个进程能进入临界区。
  3. 信号量。 常见的 P 和 V 操作。

    • 特别的,如果信号量的取值只能为 0 或者 1,那么就成为了互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。
  4. 条件变量

    使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。

    管程引入了条件变量 以及相关的操作:wait()signal() 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    // 管程:解决生产者、消费者问题
    monitor ProducerConsumer
    condition full, empty;
    integer count := 0;
    condition c;

    procedure insert(item: integer);
    begin
    if count = N then wait(full);
    insert_item(item);
    count := count + 1;
    if count = 1 then signal(empty);
    end;

    function remove: integer;
    begin
    if count = 0 then wait(empty);
    remove = remove_item;
    count := count - 1;
    if count = N -1 then signal(full);
    end;
    end monitor;

    // 生产者客户端
    procedure producer
    begin
    while true do
    begin
    item = produce_item;
    ProducerConsumer.insert(item);
    end
    end;

    // 消费者客户端
    procedure consumer
    begin
    while true do
    begin
    item = ProducerConsumer.remove;
    consume_item(item);
    end
    end;

3.请问【进程】之间如何进行通信

可以分为如下两个方面:

  1. 本地进程之间的通信方式
  2. 远程进程之间的通信方式

1. 本地进程之间的通信方式(没有同步互斥!!

  • 无名管道 :半双工通信方式,数据(消息)单向流动,只能是字节流格式的消息。

    • 优点:简单方便
    • 缺点:单向通信、只能用于具有亲缘关系(一般指父子)的进程之间、缓冲区有限
  • 有名管道:半双工通信方式,数据也称为命名管道:是一种文件类型,以一种特殊设备文件形式存在于文件系统中。

    • 优点:可以实现任意关系的进程间的通信(无法同步)
    • 缺点: 长期存于系统中,使用不当容易出错、缓冲区有限
  • 消息队列:消息队列是消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点。

    • 优点可以实现任意进程间的通信,并通过系统调用函数来实现消息发送和接收之间的同步,无需考虑同步问题
    • 缺点:信息的复制需要额外消耗CPU的时间,不适宜于信息量大或操作频繁的场合
  • 共享内存

    直接对内存存取,通信快,但是多个进程可以同时操作,需要用信号量进行同步

  • 信号量

    信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

    • 优点:可以同步进程;

    • 缺点:信号量有限

      注解:P操作就是对S减一,V操作就是对S加一

      • 同步:S = 0,进程A执行完进行V操作,进程B执行前执行P操作,这样B就可以等A执行完再执行;
      • 互斥:S = 1,进程执行前进行P操作,执行后进行V操作。

2.远程进程之间的通信方式

首要解决的问题是如何唯一标识一个进程?本地上采用PID即可,但是网络中 TCP/IP五层网络模型中传输层的 “套接字:IP+端口

  • 套接字交互

    • 优点:1)传输数据为字节级,传输数据可自定义,数据量小效率高;2)传输数据时间短,性能高;3) 适合于客户端和服务器端之间信息实时交互;4) 可以加密,数据安全性强
    • 缺点:1) 需对传输的数据进行解析,转化成应用级的数据。
  • 远程过程调用(RPC)

4. 请问【线程】间同步方式(通信方式)?

image-20210611000107784

1. Linux下线程通知方式

  1. 互斥量(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有⼀个,所以可以保证公共资源不会被多个线程同时访问。
    • ⽐如 Java 中的synchronized 关键词和各种 Lock锁 都是这种机制。
  2. 信号量(Semphares) :它允许同⼀时刻多个线程访问同⼀资源,但是需要控制同⼀时刻访问此资源的最大线程数量
  3. 条件变量 : 通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级
    • 比如Wait/Notify

2. Windows下线程通知方式

  1. 全局变量:需要有多个线程来访问一个全局变量时,通常我们会在这个全局变量前加上volatile声明,以防编译器对此变量进行优化 ;
  2. CEvent对象:CEvent为MFC中的一个对象,可以通过对CEvent的触发状态进行改变,从而实现线程间的通信和同步,这个主要是实现线程直接同步的一种方法;
  3. Message消息机制:常用的Message通信的接口主要有两个:PostMessage和PostThreadMessage,PostMessage为线程向主窗口发送消息。而PostThreadMessage是任意两个线程之间的通信接口。

5.什么时候用多线程 / 多进程

  • 需要频繁创建销毁的优先用线程 :创建和销毁的代价是很难承受的 ;
  • 需要进行大量计算的优先使用线程 :此时耗费很多CPU,切换频繁,用线程更轻量;
  • 任务间相关性比较强的用多线程,相关性比较弱的用多进程。因为线程之间的数据共享和同步比较简单;

考虑多进程:

  • 扩展到多机分布的用多进程,多核分布的用多线程
  • 其它一般用多线程比较好

6.【线程】调度算法?

在资源一定的情况下,调度算法需要在吞吐量(Throughput)、平均响应时间(延迟,Average Response Time)、公平性、调度引起的额外开销(overhead)等几个方面做权衡。

  1. 先进先出算法(FIFO,First-In-First-Out)

    • 优点
      • 最少的任务切换开销(因为没有在任务执行过程中发生切换,故任务切换开销为0)
      • 最大的吞吐量(因没有任务切换开销,在其他一定的情况下,吞吐量肯定是最大的)
      • 最朴实的公平性(先来先做)
    • 缺点
      • 平均响应时间高:耗时只需10毫秒的任务若恰巧在耗时1000毫秒的任务后到来,他则需要1010毫秒才能执行完成,绝大部分时间都花在等待被调度。
  2. 最短耗时任务优先算法

    优先调度耗时短的任务,需要预先知道每个任务的耗时情况,这在实际情况中是不大现实的。

    • 优点平均响应时间较低:这里有一点,因为将时间长的任务无限往后推移,实际计算的平均响应时间的任务都是执行较快的任务,统计出来的平均响应时间必然较低的。
    • (缺点耗时长任务饥饿:耗时长的任务迟迟得不到调度,不公平,容易形成饥饿 。
    • (缺点开销大频繁的任务切换,调度的额外开销大。
  3. 时间片轮转算法

    给队列中的每个任务一个时间片,第一个任务先执行,时间片到了之后,将此任务放到队列尾部,切换到下个任务执行,解决最短耗时任务优先算法中耗时长任务饥饿的问题

    • (特点)时间片设置问题: 算法介于FIFO和SJF之间,若时间片足够,则退化到FIFO ;若分片足够小(假设不考虑任务切换的开销),则任务的完成时间顺序是以耗时从小到大排列。
    • 优点)公平调度:每个任务都能够得到公平的调度
      • 优点)不会饥饿:耗时短的任务即使落在耗时长的任务后面,也能够较快的得到调度执行
    • (缺点)开销大任务切换引起的调度开销较大,需要多次切换任务上下文
      • (缺点)时间片不太好设置
  4. 最大最小公平算法

7.【进程】调度算法?

  • 先来先服务调度算法
  • 短作业(进程)优先调度算法
  • 时间片轮转法
  • 多级反馈队列调度算法
  • 优先权调度算法

8. CPU上下文切换?有什么类型?线程发生在什么地方?

参考:https://zhuanlan.zhihu.com/p/52845869

  • 什么是 CPU 上下文

    CPU 寄存器和程序计数器 就是 CPU 上下文,因为它们都是 CPU 在运行任何任务前,必须的依赖环境

    • CPU 寄存器 是 CPU 内置的容量小、但速度极快的内存。
  • 什么是 CPU 上下文切换?

    通常指以下过程:

    1. 前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来;
    2. 然后加载新任务的上下文到这些寄存器和程序计数器;
    3. 最后再跳转到程序计数器所指的新位置,运行新任务。
  • CPU 上下文切换的类型

    根据任务的不同,可以分为以下三种类型 : 进程上下文切换 - 线程上下文切换 - 中断上下文切换

    1. 进程上下文切换

      进程在用户空间运行时,被称为进程的用户态,而陷入内核空间的时候,被称为进程的内核态。

      • 内核空间(Ring 0)具有最高权限,可以直接访问所有资源;

      • 用户空间(Ring 3)只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问这些特权资源。

      从用户态到内核态的转变,需要通过系统调用来完成,在这个过程中就发生了 CPU 上下文切换(次,用户态-内核态-用户态))

      系统调用 : 查看文件时read()、wirte() 操作就发生了系统调用。

      但是,系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程

      img

      进程上下文切换 ,比系统调用时多了一步:在保存内核态资源(当前进程的内核状态和 CPU 寄存器)之前,需要先把该进程的用户态资源虚拟内存、栈等)保存下来。

    2. 线程上下文调用

      线程是调度的基本单位,而进程则是资源拥有的基本单位。

      【面试高频】发生线程上下文切换的场景

      • 前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。
      • 前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据(栈、寄存器等)不共享的数据
    3. 中断上下文切换

9.如何杀死一个进程?进程终止的方式?

  1. linux命令:kill -9 <pid>
  2. 接受能导致进程终止的信号:ctrl+c (^C)、SIGINT(SIGINT中断进程)
  3. main函数的自然返回,return
  4. 调用exit函数,属于c的函数库 3、调用_exit函数,属于系统调用
  5. 调用abort函数,异常程序终止,同时发送SIGABRT信号给调用进程
9.1 终端退出,终端运行的进程会怎么样?
  1. 终端在退出时会发送SIGHUP给对应的bash进程,

  2. bash进程收到这个信号后首先将它发给session下面的进程

    一个session就是一个shell终端会话窗口。

  3. 如果程序没有对SIGHUP信号做特殊处理,那么进程就会随着终端关闭而退出

9.2 怎么让进程后台运行?
  1. 命令 + & 即可,实际上,这样是将命令放入到一个作业队列中了
  2. ctrl + z 挂起进程,使用jobs查看序号,在使用bg %序号后台运行进程
  3. nohup + &,将标准输出和标准错误缺省会被重定向到 nohup.out 文件中,忽略所有挂断(SIGHUP)信号
  4. setsid + 命令,使其父进程编程init进程,不受HUP信号的影响
  5. 命令+ &放在()括号中,也可以是进程不受HUP信号的影响

10. 外中断和异常的区别?

  • 外中断是指由 CPU 执行指令以外的事件引起,如 I/O 完成中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等。

  • 异常时由 CPU 执行指令的内部事件引起,如非法操作码、地址越界、算术溢出等。

11. 什么是父进程、子进程、进程组、作业和会话?

  • 父进程: 已创建一个或多个子进程的进程 ;

  • 子进程: 由fork创建的新进程被称为子进程(child process),函数被调用一次,但返回两次;

    fork之后,操作系统会复制一个与父进程完全相同的子进程,虽说是父子关系,但是在操作系统看来,他们更像兄弟关系:

    • (1)它们共享代码空间,(2)数据空间是互相独立的,但子进程数据空间中的内容是父进程的完整拷贝,(3)指令指针也完全相同,(4)子进程拥有父进程当前运行到的位置(两进程的程序计数器pc值相同)。

    除了:fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号pid

  • 进程组: 进程组就是多个进程的集合,其中肯定有一个组长,其进程PID等于进程组的PGID ;

  • 作业: shell分前后台来控制的不是进程而是作业(job)或者进程组(Process Group)。

    一个前台作业可以由多个进程组成,一个后台也可以由多个进程组成,shell可以运行一个前台作业和任意多个后台作业,这称为作业控制。、

  • 会话。 一个或多个进程组的集合一个会话可以有一个控制终端。在xshell或者WinSCP中打开一个窗口就是新建一个会话。

12. 什么是守护进程、僵尸进程、孤儿进程?

参考:Linux 之守护进程、僵死进程与孤儿进程

  • 守护进程

    在后台运行的,没有控制终端与之相连的进程。它独立于控制终端,周期性地执行某种任务。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。

    • 举例:常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。

    一个守护进程的父进程是init进程,也是一个孤儿进程 ,一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。

  • 孤儿进程

    一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

  • 僵尸进程

    一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息(子进程必须等到父进程捕获到了子进程的退出状态才真正结束),那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

11.1 如何避免僵尸进程?
  • 通过signal(SIGCHLD, SIG_IGN)通知内核对子进程的结束不关心,由内核回收;

    如果不想让父进程挂起,可以在父进程中加入一条语句:signal(SIGCHLD,SIG_IGN); 表示父进程忽略SIGCHLD信号,该信号是子进程退出的时候向父进程发送的。

  • 父进程调用wait/waitpid等函数等待子进程结束,如果尚无子进程退出wait会导致父进程阻塞;

    waitpid可以通过传递WNOHANG使父进程不阻塞立即返回。

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

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

4.3 socket编程

暂略

4.4 内存管理

1.介绍一下操作系统的堆和栈?

  • 栈内存:栈内存首先是一片内存区域,存储的都是局部变量,栈内存的更新速度很快,因为局部变量的生命周期都很短。

    局部变量:方法内的变量,for循环内部定义的也是局部变量等。

  • 堆内存:存储的是数组对象(其实数组就是对象),凡是new建立的都是在堆中,堆中存放的都是实体(对象)。堆里的实体虽然不会被释放,但是会被当成垃圾,Java有垃圾回收机制不定时的收取。

1.1 什么时候会栈溢出?

栈能使用的内存是有限的,一般是 1M~8M,这在编译时就已经决定了,程序运行期间不能再改变。

  • 如果程序使用的栈内存超出最大值,就会发生栈溢出(Stack Overflow)错误,程序就崩溃了;
  • 一般常见的情况,如递归过深

2. 介绍一下什么内存管理?常用的内存管理机制?

  • 内存管理

    操作系统的内存管理主要负责内存的(1)分配与回收(malloc 函数:申请内存,free 函数:释放内存),(2)另外地址转换也就是将逻辑地址转换成相应的物理地址等功能也是操作系统内存管理做的事情。

  • 常用内存管理机制

    简单分为连续分配管理方式非连续分配管理方式这两种。连续分配管理方式是指为⼀个用户程序分配⼀个连续的内存空间,常见的如块式管理 。同样地,非连续分配管理方式允许⼀个程序使用的内存分在离散或者说不相邻的内存中,常见的如页式管理 和 段式管理

    1. 块式管理 : 远古时代的计算机操系统的内存管理方式。将内存分为⼏个固定大小的块,每个块中只包含⼀个进程。如果程序运行需要内存的话,操作系统就分配给它⼀块,如果程序运行只需
      要很小的空间的话,分配的这块内存很大⼀部分⼏乎被浪费了。这些在每个块中未被利用的空间,我们称之为碎片。

    2. 页式管理 :把主存分为大小相等且固定的⼀页⼀页的形式,页较小,相对相⽐于块式管理的划分⼒度更大,提高了内存利用率,减少了碎片。页式管理通过页表对应逻辑地址和物理地址

    3. 段式管理 : 页式管理虽然提高了内存利用率,但是页式管理其中的页实际并无任何实际意义。段式管理把主存分为⼀段段的,每⼀段的空间⼜要⽐⼀页的空间小很多

    但是,最重要的是段是有实际意义的,每个段定义了⼀组逻辑信息,例如,有主程段 MAIN、子程序段 X、数据段 D及栈段 S 等。 段式管理通过段表对应逻辑地址和物理地址。

    1. 段页式管理机制 。段页式管理机制结合了段式管理和页式管理的优点。简单来说段页式管理机制就是把主存先分成若⼲段,每个段⼜分成若⼲页,也就是说 段页式管理机制中段与段之间以及段的内部的都是离散的。
2.1 介绍一下逻辑地址和物理地址?
  • 我们编程⼀般只有可能和逻辑地址打交道,⽐如在 C 语⾔中,指针⾥面存储的数值就可以理解成为内存⾥的⼀个地址,这个地址也就是我们说的逻辑地址,逻辑地址由操作系统决定。
  • 物理地址指的是真实物理内存中地址,更具体⼀点来说就是内存地址寄存器中的地址。
2.2 操作系统在内存管理需要做什么?
  • 内存空间的分配与回收;
  • 从逻辑上对内存空间进行扩充;
  • 逻辑地址与物理地址的转换;
  • 操作系统需要提供内存保护功能。保证各进程在各自存储空间内运行,互不干扰。

3. 介绍一下快表和多级页表?

在分页内存管理中,很重要的两点是:

  1. 虚拟地址到物理地址的转换要快。
  2. 解决虚拟地址空间大,页表也会很大的问题。
快表介绍

快表理解为⼀种特殊的高速缓冲存储器(Cache),其中的内容是页表的⼀部分或者全部内容。作为页表的 Cache,它的作用与页表相似,但是提高了访问速率。

但有时快表不命中要访问两次缓存,不过总体还是提高了性能。

多级页表介绍

引⼊多级页表的主要⽬的是为了 避免把全部页表⼀直放在内存中占用过多空间,特别是那些根本就不需要的页表就不需要保留在内存中。多级页表属于时间换空间的典型场景。

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

  1. 共同点 :
    • 分页机制和分段机制都是为了提高内存利用率,较少内存碎片
    • 页和段都是离散存储的,所以两者都是离散分配内存的方式。但是,每个页和段中的内存是连续的。
  2. 区别 :
    • 页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序。
    • 分页仅仅是为了满⾜操作系统内存管理的需求,而段是逻辑信息的单位,在程序中可以体现为代码段,数据段,能够更好满⾜用户的需要。

5. 【待扩充】CPU 寻址了解吗?为什么需要虚拟地址空间?

  • 现代处理器使用的是⼀种称为 虚拟寻址(Virtual Addressing) 的寻址⽅式。使用虚拟寻址,CPU 需要虚拟地址翻译成物理地址,这样才能访问到真实的物理内存。

    实际上完成虚拟地址转换为物理地址转换的硬件是 CPU 中含有⼀个被称为 内存管理单元(Memory Management Unit, MMU) 的硬件。

  • 如果直接把物理地址暴露出来的话会带来严重问题,⽐如可能对操作系统造成伤害以及给同时运行多个程序造成困难。

6. 如果系统中具有快表后,那么地址的转换过程变成什么样了?

简单来说:cup计算页号 → 快表查询是否有该页号 → 否则页表查询

  1. 计算页号和页偏移量。 CPU给出逻辑地址,由某个硬件算得页号、页内偏移量;
  2. 快表中查找内存块号。 将页号与快表中的所有页号进行比较,如果找到匹配的页号,则直接从中取出该页对应的内存块号,再将内存块号与页内偏移量拼接形成物理地址
  3. 页表中查找内存块号。 如果快表中查找不存在,访问内存中的页表,找到对应页表项,得到页面存放的内存块号,再将内存块号与页内偏移量拼接形成物理地址,最后,访问该物理地址对应的内存单元。

7. 进程内存分配动态分区算法?

  1. 首次适应法

    • 算法思想:每次都从低地址开始查找,找到第一个能满足大小的空闲分区;

    • 实现方式:空闲分区以地址递增的次序排列。每次分配内存时顺序查找空闲分区链( 或空闲分[表),找到大小能满足要求的第-一个空闲分区。

    • 优点: 综合性能最好,开销小。

      image-20210610231604639

  2. 最佳适应法

    • 算法思想:为了保证当“大进程”到来时能有连续的大片空间,可以尽可能多地留下大片的空闲区,即,优先使用更小的空闲区;
    • 实现方式: 空闲分区按容量递增次序链接。每次分配内存时顺序查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区。
    • 优点: 会有更多的大分区被保留下来,更能满足大进程需求
    • 缺点: 产生很多太小的、难以利用的碎片,算法开销大
  3. 最坏适应法

    • 算法思想: 为了解决最佳适应算法的问题—即留下太多难以利用的小碎片,可以在每次分配时优先使用最大的连续空闲区,这样分配后剩余的空闲区就不会太小,更方便使用。
    • 实现方式:空闲分区按容量递减次序链接。每次分配内存时顺序查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区。
    • 优点: 可以减少难以利用的小碎片
    • 缺点: 大分区容易被用完,不利于大进程,算法开销大
  4. 领近适应法

    • 算法思想: 首次适应算法每次都从链头开始查找的。这可能会导致低地址部分出现很多小的空闲分区,而每次分配查找时,都要经过这些分区,因此也增加了查找的开销。
    • 实现方式:空闲分区以地址递增的顺序排列(可排成一个循环链表)。每次分配内存时从上次查找结束的位置开始 ,查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区。
    • 优点: 算法开销小

8. 什么是内存覆盖和内存交换?

  • 内存覆盖
    • 思想: 把用户空间分成为一个固定区若干个覆盖区。将经常活跃的部分放在固定区,其余部分按照调用关系分段,首先将那些即将要访问的段放入覆盖区,其他段放在外存中,在需要调用前,系统将其调入覆盖区,替换覆盖区中原有的段。
    • 特点打破了必须将一个进程的全部信息装入内存后才能运行的限制 。
  • 内存交换
    • 思想内存空间紧张 时,系统将内存中某些进程暂时换出外存,把外存中某些已具备运行条件的进程换入内存

4.5 虚拟内存

0. 什么是虚拟技术 ?从时间和空间两方面来说。

虚拟技术把一个物理实体转换为多个逻辑实体

  • 时分复用技术 : 如多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占用处理器,每次只执行一小个时间片并快速切换
  • 空分复用技术物理内存抽象为地址空间,每个进程都有各自的地址空间。地址空间的页被映射到物理内存,地址空间的页并不需要全部在物理内存中,当使用到一个没有在物理内存的页时,执行页面置换算法,将该页置换到内存中。

1. 介绍一下局部性原理吧?

局部性原理是虚拟内存技术的基础,正是因为程序运行具有局部性原理,才可以只装⼊部分程序到内存就开始运行。

局部性原理表现在以下两个⽅面:

  1. 时间局部性:如果程序中的某条指令⼀旦执行,不久以后该指令可能再次执行;如果某数据被访问过,不久以后该数据可能再次被访问。产⽣时间局部性的典型原因,是由于在程序中存在着大量的循环操作。

时间局部性是通过将近来使用的指令和数据保存到高速缓存存储器中,并使用高速缓存的层次结构实现。

  1. 空间局部性:⼀旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即程序在⼀段时间内所访问的地址,可能集中在⼀定的范围之内,这是因为指令通常是顺序存放、顺序执行的,数据也⼀般是以向量、数组、表等形式簇聚存储的。

空间局部性通常是使用教大的高速缓存,并将预取机制集成到高速缓存控制逻辑中实现。

虚拟内存技术实际上就是建⽴了 “内存⼀外存”的两级存储器的结构,利用局部性原理实现髙速缓存。

2.介绍一下,什么是虚拟内存?页式存储

  • 虚拟内存

    虚拟内存是一种存储机制,可为用户提供一个拥有很大主内存的错觉。通过将辅存的一部分作为主存来完成。在虚拟内存中,用户可以存储比可用主内存更大的进程。

    每个进程创建加载的时候,会被分配一个大小为4G的连续的虚拟地址空间,仅仅是每个进程“认为”自己拥有4G的内存。等到进程真正运行的时候,需要某些数据并且数据不在物理内存中,才会触发缺页异常,进行磁盘数据拷贝到物理内存中

    img

  • 页式存储

    大部分虚拟存储系统采用的是一种称为分页(paging)的技术。这种方式叫做虚拟页式存储管理。

    • 物理内存空间划分为固定大小的内存块,称为物理页面,或者是页框(page frame)

    • 虚拟地址空间也划分成大小相同的块,称为虚拟页面,或者简称页面(page)

      页表:将虚拟页面映射为相应的物理页面

3. 虚拟内存的技术实现 ?

虚拟内存的实现需要建⽴在离散分配的内存管理⽅式的基础上。 虚拟内存的实现有以下三种⽅式:

  1. 请求分页存储管理 :建⽴在分页管理之上,为了支持虚拟存储器功能而增加了请求调页功能和页面置换功能。请求分页是⽬前最常用的⼀种实现虚拟存储器的⽅法。请求分页存储管理系统中,在作业开始运行之前,仅装⼊当前要执行的部分页即可运行。假如在作业运行的过程中发现要访问的页面不在内存,则由处理器通知操作系统按照对应的页面置换算法将相应的页面调⼊到主存,同时操作系统也可以将暂时不用的页面置换到外存中。
  2. 请求分段存储管理:建⽴在分段存储管理之上,增加了请求调段功能、分段置换功能。请求分段储存管理⽅式就如同请求分页储存管理⽅式⼀样,在作业开始运行之前,仅装⼊当前要执行的部分段即可运行;在执行过程中,可使用请求调⼊中断动态装⼊要访问但⼜不在内存的程序段;当内存空间已满,而⼜需要装⼊新的段时,根据置换功能适当调出某个段,以便腾出空间而装⼊新的段。
  3. 请求段页式存储管理

4. 请你介绍一下页面置换算法?

当发⽣缺页中断时,如果当前内存中并没有空闲的页面,操作系统就必须在内存选择⼀个页面将其移出内存,以便为即将调⼊的页面让出空间。用来选择淘汰哪⼀页的规则叫做页面置换算法,我们可以把页
⾯置换算法看成是淘汰页面的规则。

  • OPT (最佳页面置换算法):最佳(Optimal, OPT)置换算法所选择的被淘汰页面将是以后永不使用的,或者是在最⻓时间内不再被访问的页面,这样可以保证获得最低的缺页率。

    但由于⼈们⽬前无法预知进程在内存下的若千页面中哪个是未来最⻓时间内不再被访问的,因⽽该算法无法实现。⼀般作为衡量其他置换算法的⽅法。

  • FIFO(First In First Out) (先进先出页面置换算法): 总是淘汰最先进⼊内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。

  • LRU (Least Currently Used)(最近最久未使用页面置换算法):LRU算法赋予每个页面⼀个访问字段,用来记录⼀个页面⾃上次被访问以来所经历的时间 T,当须淘汰⼀个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰。

  • LFU (Least Frequently Used)(最少使用页面置换算法): 该置换算法选择在前时期使用最少的页面作为淘汰页。

5.你怎么理解操作系统里的内存碎片,有什么解决办法

内存碎片分为:内部碎片和外部碎片。

  • 内部碎片: 已经被分配出去(能明确指出属于哪个进程)却不能被利用的内存空间;

    内存泄漏:不再会被使用的对象的内存不能被回收

  • 外部碎片: 还没有被分配出去(不属于任何进程),但由于太小了无法分配,给申请内存空间的新进程的内存空闲区域。

6. 什么是内存抖动?

  • 现象:刚刚换出的页面马上又要换入内存,刚刚换入的页面马上又要换出外存,这种频繁的页面调度行为称为抖动,或颠簸;
  • 原因: 程频繁访问的页面数目高于可用的物理块数(分配给进程的物理块不够) ;
  • 解决: 分配更多的物理块

4.6 并发和死锁

0. 介绍几种典型的锁?

  1. 读写锁。 可以同时读,但写必须互斥,只允许一个写;
  2. 互斥锁。 一次只能一个线程拥有锁,其它只能等待;
  3. 条件变量: 互斥锁一个明显的缺点是他只有两种状态:锁定和非锁定;而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足;
  4. 自旋锁。 如果进线程无法取得锁,进线程不会立刻放弃CPU时间片,而是一直循环尝试获取锁,直到获取为止。

1. 【重点什么是线程死锁?什么情况下会发生死锁?解决死锁的策略有哪些

  • 什么是死锁

    死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。

  • 发生死锁的条件

    1. 互斥:一个资源只能给一个进程使用;
    2. 占有并等待:进程持有资源并申请新资源,在申请到需要的资源之前,已有的资源不释放
    3. 不可剥夺:进程申请到的资源在使用完之前,不可以被其他进程使用;
    4. 循环等待:各个进程的资源请求形成首尾连接循环等待。
  • 解决方法:预防,避免,检测与恢复三种

    1. 预防:破坏死锁会发生的四个条件
      • 破坏互斥:这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的
      • 破坏请求和保持:实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源;
      • 破坏不剥夺条件 :占用部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源
      • 破坏循环等待:资源分类标号,进行有序分配。
    2. 避免它不限制进程有关申请资源的命令,而是对进程所发出的每一个申请资源命令加以动态地检查,并根据检查结果决定是否进行资源分配
      • 安全序列:找到一种能让进程安全执行完的有序序列{P1,P2,...,Pn}
      • 银行家算法:系统在为进程分配资源之前,首先计算此次资源分配的安全性,如果是安全的,则进行分配;如果这次分配会导致进入不安全状态,不进行分配。
    3. 恢复: 用资源分配图、进程等待图来协助这种检测出死锁,然后进行恢复。
      • 系统重新启动,但代价很大
      • 撤消参与死锁的全部或部分进程,剥夺资源
1.1 什么时候需要使用分布式锁
  • 单体应用的时候,如果多个线程要访问共享资源的时候,我们通常线程间加锁的机制,在某一个时刻,只有一个线程可以对这个资源进行操作,其他线程需要等待锁的释放,Java中也有一些处理锁的机制,比如synchronized。
  • 而到了分布式的环境中,当某个资源可以被多个系统访问使用到的时候(例如,有多个客户端需要访问并操作同一个资源,还需要保持这个资源一致性的时候,就需要使用【分布式锁),为了保证大家访问这个数据是一致性的,那么就要求再同一个时刻,只能被一个系统使用,这时候线程之间的锁机制就无法起到作用了,因为分布式环境中,系统是会部署到不同的机器上面的,那么就需要【分布式锁】了。

2. (待补充)请你解释一下,通常系统CPU比较高是什么原因

  1. 首先查看是哪些进程的CPU占用率最高

3.说一下NIO,BIO,AIO区别?

参考:JAVaGuide

BIO(同步阻塞)

  • BIO:同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到在内核把

    图源:《深入拆解Tomcat & Jetty》

NIO(同步非阻塞)

NIO 本身是基于 事件驱动 的思想来实现的,其目的就是解决 BIO 的大并发问题:

  • BIO 模型中,如果需要并发处理多个 I/O 请求,那就需要多线程来支持
  • IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用read 调用的过程(数据从内核空间->用户空间)还是阻塞的
img

AIO(异步非阻塞)

Java 7 中引入了 NIO 的改进版 NIO 2,它是异步 IO 模型 。

AIO: 异步非阻塞无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。

异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

img0.

4. 【重点】介绍一下select,poll,epoll 原理?

参考

  1. select、poll、epoll的原理与区别
4.1 从阻塞I/O & 非阻塞I/O说起
  • 阻塞I/O

    在linux中,默认情况下所有的socket都是阻塞的。

    image-20210517125922790

    1. 当用户进程调用了read()/recvfrom()等系统调用函数,它会进入内核空间中;
    2. 当这个网络I/O没有数据的时候,内核就要等待数据的到来,此时用户进程被阻塞
    3. 当内核空间的数据准备好了,它就会将数据从内核空间中拷贝到用户空间
    4. 用户进程才解除阻塞的的状态,重新运行读取数据。
  • 非阻塞I/O

    linux下,可以通过设置socket使其变为非阻塞模式,这种情况下,当内核空间并无数据的时候,它会马上返回结果而不会阻塞

    image-20210517130232559

    1. 当用户进程调用了read()/recvfrom()等系统调用函数,它会进入内核空间中;
    2. 如果内核空间中的数据还没有准备好,那么它并不会阻塞用户进程,而是立刻返回一个error
    3. 对于应用进程来说,它发起一个read()操作后,并不需要等待,那么它可以再次调用read()/recvfrom()等函数;
    4. 当内核空间的数据准备好了,它就会将数据从内核空间中拷贝到用户空间;
    5. 用户进程才解除阻塞的的状态,重新运行读取数据。

多路复用I/O就是我们说的select,poll,epoll等操作,复用的好处就在于单个进程就可以同时处理多个网络连接的I/O,能实现这种功能的原理就是select、poll、epoll等函数会不断的轮询它们所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

4.2 select原理

更加深刻对比理解:Linux编程之select

1
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • select函数监视的文件socket描述符分3类,分别是writefds、readfds、和exceptfds;
  • 调用后select函数会阻塞(不是线程),直到有描述符就绪(有数据 可read、可write、except、超时timeout),函数返回。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
select(socket);
while(1)
{
sockets = select(); // 还是要从内核拷贝到用户
for(socket in sockets)
{
if(can_read(socket))
{
read(socket, buffer);
process(buffer);
}
}
}
}

详细执行原理如下:

  1. 用户首先将需要进行IO操作的socket添加到select中,然后阻塞函数select(不是线程)等待系统调用返回

  2. 当数据到达时,socket被激活,select函数返回,会唤醒其等待队列上睡眠的内核进程,即在socket可读写时唤醒,或者在超时后唤醒;

    每次调用select查看fd,都需要把fd集合拷贝进行系统调用陷入内核态

  3. 返回select()函数的调用结果给用户进程,返回就绪socket描述符的数目,超时返回0,出错返回-1;

  4. 在select()函数返回后还是需要轮询去找到就绪的socket描述符的(将此前传入内核空间的fd_set拷贝到用户空间),此时用户进程才可以去操作socket;

  5. 进程调用read() / recvfrom() 读取数据 。

select优点

从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。

那为什么还要使用select?

  • 使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的;
  • 而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的

select缺点

  1. 描述符数量select支持的文件描述符数量太小了,默认是1024

  2. 系统开销:每次调用select都需要把fd集合拷贝进行系统调用陷入内核态,这个开销在fd很多时会很大 ;

  3. 二次轮询select需要二次查询拷贝所有文件描述fd_set进行遍历查看是否有描述符准备就绪。

4.3 poll原理
1
2
3
4
5
6
7
int poll (struct pollfd *fds, unsigned int nfds, int timeout);

struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};

不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd指针实现。

poll使用链表维护这些socket描述符,而select使用的是数组(位图)。

其他的都差不多和select()函数一样,poll()函数返回后,需要轮询pollfd来获取就绪的描述符,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制

poll缺点

解决了selec第一个缺点(文件描述符数量太少),但是依旧存在后面两个缺点。

  1. 系统开销:每次调用poll都需要把fd集合拷贝进行系统调用陷入内核态,这个开销在fd很多时会很大 ;

  2. 二次轮询poll需要二次查询拷贝所有文件描述fd_set进行遍历查看是否有描述符准备就绪。

4.3 epoll原理
1
2
3
4
5
6
7
// epoll只有epoll_create()、epoll_ctl()、epoll_wait() 3个系统调用函数。
int epoll_create(int size);

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
  • epoll_create

    创建一个epoll文件描述符的epfd(或者称之为句柄), epoll使用一个epfd文件描述符管理多个socket描述符

    当创建好epoll句柄后,它就是会占用一个fd值,必须调用close()关闭,否则可能导致fd被耗尽 。

  • epoll_ctl

    该函数用于控制某个epoll文件描述符上的事件,可以注册事件,修改事件,以及删除事件。相关参数:

    使用红黑树对监视的文件描述符进行:添加、修改、删除等。

    • epdf:由epoll_create()函数返回的epoll文件描述符(句柄);

    • op : op是操作的选项,注册要监听的目标socket描述符fd到epoll句柄中 ;修改epoll句柄已经注册的fd的监听事件;从epoll句柄删除已经注册的socket描述符 ;

    • fd:指定监听的socket描述符;

    • event:事件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      typedef union epoll_data {
      void *ptr;
      int fd;
      uint32_t u32;
      uint64_t u64;
      } epoll_data_t;

      struct epoll_event {
      uint32_t events; /* Epoll events */
      epoll_data_t data; /* User data variable */
      };
  • epoll_wait()

    epoll_wait()函数的作用就是等待监听的事件的发生,类似于调用select()函数。 相关参数如下:

    函数的返回值表示需要处理的事件数目,如返回0表示已超时。

    • events:用来从内核得到事件的集合。
    • maxevents :告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的指定的size。
    • timeout:超时时间。

epoll高效运行过程

参考:彻底搞懂epoll高效运行的原理

  1. 执行epoll_create会在内核的高速cache区中建立一颗红黑树以及就绪链表(该链表存储已经就绪的文件描述符)。接着用户执行epoll_ctl 函数添加文件描述符会在红黑树上增加相应的结点。

    select:创建3个文件描述符集并拷贝到内核中 ; poll:将传入的struct pollfd结构体数组拷贝到内核中进行监听。

  2. 调用epoll_wait 阻塞,等待可读事件

  3. 内核在检测到满足条件的socket描述符会调用回调函数 ,回调函数将文件描述符放在就绪链表中 ;

    而select/poll 都需要 ,遍历所有文件描述符fd_set 。

    而执行epoll_ctl的add操作时,不仅将文件描述符放到红黑树上,而且也注册了回调函数,只需调用回调函数。

  4. epoll_wait只用观察就绪链表中有无数据即可,最后将链表的数据返回给读写事件数组events &返回就绪的数量,只用遍历events依次处理即可。

    这里返回的文件描述符是通过mmap让内核和用户空间共享同一块内存实现传递的,减少了不必要的拷贝。

    而select/poll 只返回socket就绪数目, 还需要将所有的文件描述符再次从内核→用户,遍历就绪的socket文件描述符。

4.4 select,poll,epoll 各自区别?

不错的文章:https://www.codenong.com/cs105364662/

相同点

  • select,poll,epoll 都是 IO 多路复用的机制(NIO?yes);

    IO 多路复用的本质是通过一种机制,让单个进程可以监视多个描述符,当发现某个描述符就绪之后,能够通知程序进行相应的操作。

  • select,poll,epoll 都是同步 IO 。

不同点

image-20210517125057917
  1. IO 效率:(1)select 只知道有 IO 事件发生,却不知道是哪几个流,只能采取轮询所有流( fd_set 集合)的方式,故其具有 O(n) 的无差别轮询复杂度,处理的流越多,无差别轮询时间就越长;(2)poll 与 select 并无区别,它的时间复杂度也是O(n);(3)epoll 会将哪个流发生了怎样的 IO 事件通知我们(当描述符就绪时,系统注册的回调函数会被调用,将就绪描述符放到 readyList 里面),它是事件驱动的,其时间复杂度为 O(1);
  2. 操作方式:select 和 poll 都是采取遍历的方式,而 epoll 则是采取了回调的方式;
  3. 底层实现:select 的底层实现为数组,poll 的底层实现为链表;而 epoll 的底层实现为红黑树;
  4. 最大连接数:select 的最大连接数为 1024 或 2048;而 poll 和 epoll 是无上限的;
  5. 对描述符的拷贝:select 和 poll 每次被调用时都会把描述符集合从用户态拷贝到内核态,而 epoll 在调用 epoll_ctl 时会拷贝进内核并保存,之后每次 epoll_wait 时不会拷贝;
  6. 性能epoll 在绝大多数情况下性能远超 select 和 poll,但在连接数少并且连接都十分活跃的情况下,select 和 poll 的性能可能比 epoll 好,因为 epoll 的通知机制需要很多函数回调 。
4.5 ET , LT 模式介绍?各自优缺点?

参考:https://cloud.tencent.com/developer/article/1636224

  • 边缘触发模式Edge Trigger,ET),只有一个事件从无到有才会触发;

    1. 低电平 => 高电平 。
  • 水平触发模式Level Trigger,LT),一个事件只要有,就会一直触发。

    1. 低电平 => 高电平 ; 2. 处于高电平状态
  • 举例说明

    • socket 的读事件
      • LT模式,只要 socket 上有未读完的数据,就会一直产生 EPOLLIN 事件;
      • ET模式,socket 上每新来一次数据就会触发一次,如果上一次触发后,未将 socket 上的数据读完,也不会再触发,除非再新来一次数据。
    • 对于 socket 写事件
      • LT模式,如果 socket 的 TCP 窗口一直不饱和,会一直触发 EPOLLOUT 事件;
      • ET模式,只会触发一次,除非 TCP 窗口由不饱和变成饱和再一次变成不饱和,才会再次触发 EPOLLOUT 事件。
  • 优缺点

    • 使用 LT 模式,我们可以自由决定每次收取多少字节(对于普通 socket)或何时接收连接(对于侦听 socket),但是可能会导致多次触发
    • 使用 ET 模式,我们必须每次都要将数据收完(对于普通 socket)或必须理解调用 accept 接收连接(对于侦听socket),其优点是触发次数少

5. 操作系统底层是怎么实现原子操作的?

处理器使用基于对缓存加锁总线加锁的方式,来实现多处理器之间的原子操作。

  1. 总线锁: 处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存

    总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据

  2. 缓存锁:相比总线锁,缓存锁即降低了锁的力度。核心机制是基于缓存一致性协议来实现的。

    详细参考:JMM基础(总线锁、缓存锁、MESI缓存一致性协议、CPU 层面的内存屏障)

4.7 其它

1. 常见的磁盘调度算法?

读写一个磁盘块的时间的影响因素有:

  • 旋转时间(主轴转动盘面,使得磁头移动到适当的扇区上)
  • 寻道时间(制动手臂移动,使得磁头移动到适当的磁道上)
  • 实际的数据传输时间

其中,寻道时间最长,因此磁盘调度的主要目标是使磁盘的平均寻道时间最短。

  1. 先来先服务。 按照磁盘请求的顺序进行调度。

    • 优点:公平、简单;
    • 缺点:未对寻道做任何优化,使平均寻道时间可能较长。
  2. 最短寻道优先。 优先调度与当前磁头所在磁道距离最近的磁道。

    • 优点: 平均寻道时间比较低;
    • 缺点:不公平,如果新到达的磁道请求总是比一个在等待的磁道请求近,那么在等待的磁道请求会一直等待下去。
  3. 电梯扫描算法。 电梯总是保持一个方向运行,直到该方向没有请求为止,然后改变运行方向。

    • 优点: 性能较好,同时不会存在饥饿现象。

2. 服务器高并发的解决方案你知道多少?

  • 应用数据与静态资源分离:将静态资源(图片,视频,js,css等)单独保存到专门的静态资源服务器中,在客户端访问的时候从静态资源服务器中返回静态资源,从主服务器中返回应用数据
  • 客户端缓存 :例如先生成静态页面,然后用ajax异步请求获取动态数据;
  • 集群和分布式 :使用服务器集群和分布式架构,使得原本属于一个服务器的计算压力分散到多个服务器上。同时加快请求处理的速度;
  • 反向代理: 在访问服务器的时候,服务器通过别的服务器获取资源或结果返回给客户端。