文章目录

  • 面试系列文章
  • 进程和线程的区别、开销
  • fock 相关问题
  • 进程间通信
    • 管道pipe
    • 命名管道FIFO
    • 消息队列MessageQueue
    • 共享内存SharedMemory
    • 信号量Semaphore
    • 信号 ( sinal )
    • socket套接字通信
  • 线程池
    • 线程池七大参数
    • 线程池任务提交流程
  • 内存管理、虚拟内存、页表
    • 内存的分配与回收——连续分配
    • 内存的分配与回收——非连续分配
    • 内存扩充——覆盖&交换
    • 内存扩充——虚拟内存
    • 地址转换
    • 内存保护
  • 线程同步
    • 进程互斥的软件实现方法
    • 进程互斥的硬件实现方法
    • semaphore信号量
  • swap
  • linux 常用命令
    • 基本操作
    • vim编辑器操作
    • 文件查看
    • 权限修改
    • 解压缩
  • Linux进程调度策略
  • Linux进程/线程操作
  • IO复用和select/poll/epoll的区别
    • IO复用
    • select、poll、epoll
  • 为什么要分段分页
  • 分段分页内存碎片
  • 进程调度算法
  • 进程切换的上下文细节
  • 线程切换的上下文细节
  • 用户态和内核态切换

面试系列文章

(1)面试招聘——计算机网络专场(一)

进程和线程的区别、开销

进程:具有一定独立功能的程序,是系统进行资源分配和调度的一个独立单位;重点在系统调度和单独的单位,也就是说进程是可以独立运行的一段程序;

线程:是进程的一个实体,是CPU调度的基本单位,是比进程更小的能独立运行的基本单位;在运行时,只是暂用一些计数器、寄存器和栈

联系:

  1. 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程(通常说的主线程);
  2. 资源分配给进程,同一进程的所有线程共享该进程的所有资源;
  3. 线程在执行过程中,需要协作同步,不同进程的线程间要利用消息通信的办法实现同步;
  4. 处理机分给线程,即真正在处理机上运行的是线程;
  5. 线程是指进程内的一个执行单元,也是进程内的可调度实体;
  6. 进程之间可以并发执行,且同一个进程的多个线程之间也可以并发执行;

区别:

  • 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位;
  • 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源;
  • 线程的创建和切换开销较小,而创建进程、切换进程开销较大:

开销问题

  以Linux为例:Linux 中创建一个进程自然会创建一个线程,也就是主线程。创建进程需要为进程划分出一块完整的内存空间,有大量的初始化操作,比如要把内存分段(堆栈、正文区等)。创建线程则简单得多,只需要确定 PC 指针和寄存器的值,并且给线程分配一个栈用于执行程序,同一个进程的多个线程间可以复用堆栈。因此,创建进程比创建线程慢,而且进程的内存开销更大;

fock 相关问题

  一个进程,包括代码、数据和分配给进程的资源;fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同,相当于克隆了一个自己;

  fork()系统调用是以自身进程创建子进程的系统调用,一次调用,两次返回,如果返回是0,则是子进程,如果返回值>0,则是父进程(返回值是子进程的pid),这是众为周知的

关于fork的面试题:

题一:下面程序打印几个A

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{int i = 0;for(;i<2;++i){fork();printf("A\n");}exit(0);
}

  一共是6个A,进程程在 fork的时候,一并会把母进程的状态复制过去,然后从 fork处继续执行,搞懂这一点基本相关的题都没问题;
在Linux上运行亦是6个A:

题二:下面代码输出几个A?

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{fork() || fork();printf("A\n");exit(0);
}

上一题懂了的基本这一题都能做出来,答案是三个A;

题三:全局变量a=1,fork子进程后,子进程改变a的值,此时父进程读a有变化吗?如果是静态变量呢
对于第一种情况,a=1,fork子进程修改a的值,那么父进程读a有没有变化

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>int a=1;
int main()
{int pid=fork();if(!pid){a=2;}printf("the pid %d :a= %d\n",pid,a);exit(0);
}

如果父进程(pid不为0的进程)读a为2,那么说明子进程修改a对父进程有影响;
如果父进程读a为1,那么说明父子进程不共享全局变量;

父进程就是那个pid不为0的进程,读出的a仍为1,说明父子进程不共享全局变量;

  改为静态变量后,读出来仍是这个结果;仔细想一想,进程之间的确是相互独立的空间,仅仅是fork的时候复制了当时的状态过去而已,要是能修改的话,还要那么多让人头大的IPC机制干吗?

进程间通信

参考文章:进程间通讯的7种方式

管道pipe

  管道,通常指无名管道;是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
  它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

  管道分为pipe(无名管道)和fifo(命名管道)两种,除了建立、打开、删除的方式不同外,这两种管道几乎是一样的。他们都是通过内核缓冲区实现数据传输。

  管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据:管道一端的进程顺序地将进程数据写入缓冲区,另一端的进程则顺序地读取数据,该缓冲区可以看做一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,读出以后再缓冲区都不复存在了。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或写进程是否进入等待队列,当空的缓冲区有新数据写入或慢的缓冲区有数据读出时,就唤醒等待队列中的进程继续读写。

命名管道FIFO

  有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信;

消息队列MessageQueue

  消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

  消息队列,就是一个消息的链表,是一系列保存在内核中消息的列表。用户进程可以向消息队列添加消息,也可以向消息队列读取消息。消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。

  可以把消息看做一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程可以从消息队列中读取消息。

消息队列的常用函数如下表:

共享内存SharedMemory

  共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。

  共享内存允许两个或多个进程共享一个给定的存储区,这一段存储区可以被两个或两个以上的进程映射至自身的地址空间中,一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取错做读出,从而实现了进程间的通信。

  采用共享内存进行通信的一个主要好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝,对于像管道和消息队里等通信方式,则需要再内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次:一次从输入文件到共享内存区,另一次从共享内存到输出文件。

共享内存是进程间通信(Inter Process Communication)的最快方式,有两种实现方式:
(一)内存映射
即mmap方式,适用场景:父子进程之间,创建的内存非常大时;
(二)共享内存机制
即shmget方式,适用场景:同一台电脑上不同进程之间,创建的内存相对较小时;

通病:共享内存没有自带的同步机制,需要借助其他方式来进行同步。

信号量Semaphore

  信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

1、特点

  • 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存;
  • 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作;
  • 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数;
  • 支持信号量;

2、原型
  最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量;

信号 ( sinal )

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

socket套接字通信

  套接字( socket ) : 套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。

线程池

线程池七大参数

  在自定义线程池或者使用Executors创建线程池时,都会使用到ThreadPoolExecutor这个类的构造方法进行线程池的创建,而这个类的构造方法中就包含著名的七大参数:

  public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)

(1)corePoolSize:线程池中常驻核心线程数;
  核心池的大小。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;(相当于常开的服务窗口满了,顾客到候客区等待)

(2)maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
  一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接将任务交给这个空闲线程来执行,如果没有则会缓存到工作队列(即阻塞队列,相当于前面所讲的候客区)中,如果工作队列满了,才会创建一个新线程(候客区也满了,就会增加新的窗口),然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。

(3)keepAliveTime:多余的空闲线程存活时间。当前线程池数量超过corePoolSize时,当空闲时间到达keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止。
  表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;

(4)unit:keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:

(5)workQueue:任务队列,被提交但尚未执行的任务
  一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:

  • ArrayBlockingQueue;
  • LinkedBlockingQueue;
  • SynchronousQueue;

ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和 Synchronous。线程池的排队策略与BlockingQueue有关。
①ArrayBlockingQueue
  基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
②LinkedBlockingQueue
  基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
③SynchronousQuene
  一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
④PriorityBlockingQueue
  具有优先级的无界阻塞队列,优先级通过参数Comparator实现。

(6)threadFactory:表示生成线程池中的工作线程的线程工厂,用于创建线程,一般为默认线程工厂即可

(7)handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝来请求的Runnable的策略
  拒绝策略,当线程池的线程数已经达到线程池能够创建的最大线程数且阻塞队列也已经满了的时候,还有线程想要被创建执行,就会执行拒绝策略
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

线程池任务提交流程


参考文章:线程池的执行流程
列举一个线程池max=5,core=3,任务队列taskQueue=5;采用饱和策略为1)

则我们看看提交任务给此线程池的执行逻辑如下:

1)首先我们提交第一个任务到线程池,此时核心线程数都还没有用,所以会启动核心线程之一来执行任务,记住为了说明这个流程,我们的任务的占用时间都很长,所以短时间内不会结束;

2)接着提交第二个第三个任务到线程池,他们的执行逻辑同第一个任务是一模一样的,线程池会启动核心线程池中剩下的两个线程来执行你新提交的任务。

3)接着又有新的任务提交过来,这个时候线程池发现核心线程池中的线程已经都在工作中,所以会去看任务队列taskQueue是否满了,发现并没有,是空的,所以将这个任务放入任务队列中等待核心线程池中有空闲线程时自己来取任务执行。

4)接着又提交了4个任务到线程池,他们分别判断核心线程是否空闲,不空闲,然后判断任务队列是否已满,不满,则直接将任务放入队列;

5)接着新的任务又来,则在判断核心线程池和任务队列之后,发现任务依然没有办法处理,则会判断是否线程数达到最大,发现没有,则新启动线程来执行任务;

6)接着又来一个任务,执行流程同5);

7)再来一个任务,发现核心线程池在忙,任务队列也满了,线程池中的全部线程也都在工作,没有办法处理他了,所以他找到了饱和策略,因为饱和策略是默认的抛异常,所以线程池会告诉提交任务的线程,已经没有可以用的线程了。

  以上就一个核心线程数是3,总线程数是5,任务队列长度为5,默认策略采用抛异常的策略的从最开始到最后线程池满负荷运作的过程。

内存管理、虚拟内存、页表

操作系统这里王道讲得贼好,就不码字了,截点图或者B站看视频懂得快!

内存管理必须要做的几件事:
内存的分配与回收
对内存空间进行扩充=>虚拟内存:把物理上很小的内存拓展为逻辑上很大的内存;
③提供地址转换功能,负责程序的逻辑地址与物理地址转换;
内存保护:保证各进程在各自存储空间内运行,互不干扰;

内存的分配与回收——连续分配

操作系统如何进行内存的分配与回收?
有连续分配和非连续分配两种形式;
连续分配管理方式
(一)单一连续分配

固定分区分配


动态分区分配



动态分区分配算法

如何进行分区的分配与回收操作?
如果是空闲分区表,那么分配的时候 =>

①如果采用首次适应算法,把进程5分配给分区大小为20MB的空闲分区,那么空闲分区的数量不变,1号分区的大小和地址变化;

②如果采用最佳适应算法,给进程5分配给3号空闲分区,那么空闲分区数量减少;

回收的时候 =>




动态分区分配没有内部碎片,但是有外部碎片;
内部碎片,分配给某进程的内存区域中,如果有些部分没有用上。外部碎片,是指内存中的某些空闲分区由于太小而难以利用;

  如果内存中空闲空间的总和本来可以满足某进程的要求,但由于进程需要的是一整块连续的内存空间,因此这些“碎片”不能满足进程的需求。可以通过紧凑(拼凑,Compaction)技术来解决外部碎片。

内存的分配与回收——非连续分配

① 基本分页存储管理

基本地址变换机构

快表+地址变换

多级页表

② 基本分段存储管理

③ 段页式管理

内存扩充——覆盖&交换

覆盖
为什么要引入覆盖技术?
  早期计算机内存太小,怎么才能把过大的应用程序放入较小的内存中?这就引入了覆盖技术,解决程序大小超过物理内存总和的问题;

比如玩DNF,不可能同时记载所有的地图,你打僵尸图就不用这时候加载天空之城的图;

  如果程序遵循某个逻辑结构,那么可以让一些不可能同时访问的程序段共享一个覆盖区,只有其中某个模块需要调用时才放在调用区里(覆盖区取最大不可同时访问程序段);

交换技术

交换技术的设计思想:内存空间紧张时,系统将内存中某些进程暂时换出外存,把外存中某些已具备运行条件的进程换入内存(进程在内存与磁盘间动态调度)


高级调度:作业调度;
中级调度:内存调度;
低级调度:进程调度;


覆盖是在同一个进程中进行,而交换是在不同进程之间的;

内存扩充——虚拟内存

① 请求分页存储管理
缺页中断

页面置换算法

页面分配策略

地址转换

内存保护


保证进程1只能访问/修改自己的这部分空间,不会越界访问操作系统的空间和其他进程的空间;

内存保护的两种措施:
①在CPU中设置一对上下限寄存器,存放进程的上下限地址;进程的指令要访问某个地址时,CPU检查该地址是否越界;

②重定位寄存器(基址寄存器) + 界地址寄存器的组合进行越界检查;
重定位寄存器中存放的是进程的起始物理地址,界地址寄存器中存放的是进程的最大逻辑地址;

线程同步

截点图把重点都记下,王道的操作系统还是讲得很好的,覆盖面很广,看图就能回顾以前的知识了,懒得打字:
https://www.bilibili.com/video/BV1YE411D7nH?p=21&spm_id_from=pageDriver

线程同步:同步亦称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而产生的制约关系。进程间的直接制约关系就是源于它们之间的相互合作;

进程互斥
进程的“并发”需要“共享”的支持。各个并发执行的进程不可避免的需要共享一些系统资源(比如内存,又比如打印机、摄像头这样的I/O设备);

把一个时间段内只允许一个进程使用的资源称为临界资源。许多物理设备(比如摄像头、打印机)都属于临界资源。此外还有许多变量、数据、内存缓冲区等都属于临界资源。

对临界资源的访问,必须互斥地进行。互斥,亦称间接制约关系。进程互斥指当一个进程访问某临界资源时,另一个想要访问该临界资源的进程必须等待。当前访问临界资源的进程访问结束,释放该资源之后,另一个进程才能去访问临界资源。

注意:
临界区是进程中访问临界资源的代码段。
进入区和退出区是负责实现互斥的代码段。
临界区也可称为“临界段”。

访问临界区需要遵循的策略:

进程互斥的软件实现方法

单标志法


双标志先检查

双标志后检查

peterson算法

进程互斥的硬件实现方法

中断屏蔽方法

TestAndSet

Swap指令

semaphore信号量

1.在双标志先检查法中,进入区的“检查”、“上锁”操作无法一气呵成,从而导致了两个进程有可能同时进入临界区的问题;
2.所有的解决方案都无法实现“让权等待”

整形信号量

记录型信号量



信号量实现进程互斥

信号量实现进程同步


著名的生产者消费者问题


swap

Linux内核为了提高读写效率与速度,会将文件在内存中进行缓存,这部分内存就是Cache Memory(缓存内存)。即使你的程序运行结束后,Cache Memory也不会自动释放。这就会导致你在Linux系统中程序频繁读写文件后,你会发现可用物理内存变少。当系统的物理内存不够用的时候,就需要将物理内存中的一部分空间释放出来,以供当前运行的程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间被临时保存到Swap空间中,等到那些程序要运行时,再从Swap分区中恢复保存的数据到内存中。这样,系统总是在物理内存不够时,才进行Swap交换。

在Linux下,SWAP的作用类似Windows系统下的“虚拟内存”。当物理内存不足时,拿出部分硬盘空间当SWAP分区(虚拟成内存)使用,从而解决内存容量不足的情况。

SWAP意思是交换,顾名思义,当某进程向OS请求内存发现不足时,OS会把内存中暂时不用的数据交换出去,放在SWAP分区中,这个过程称为SWAP OUT。当某进程又需要这些数据且OS发现还有空闲物理内存时,又会把SWAP分区中的数据交换回物理内存中,这个过程称为SWAP IN。

当然,swap大小是有上限的,一旦swap使用完,操作系统会触发OOM-Killer机制,把消耗内存最多的进程kill掉以释放内存

数据库系统为什么嫌弃swap?

显然,swap机制的初衷是为了缓解物理内存用尽而选择直接粗暴OOM进程的尴尬。但坦白讲,几乎所有数据库对swap都不怎么待见,无论MySQL、Oracal、MongoDB抑或HBase,为什么?这主要和下面两个方面有关:

  1. 数据库系统一般都对响应延迟比较敏感,如果使用swap代替内存,数据库服务性能必然不可接受。对于响应延迟极其敏感的系统来讲,延迟太大和服务不可用没有任何区别,比服务不可用更严重的是,swap场景下进程就是不死,这就意味着系统一直不可用……再想想如果不使用swap直接oom,是不是一种更好的选择,这样很多高可用系统直接会主从切换掉,用户基本无感知。

  2. 另外对于诸如HBase这类分布式系统来说,其实并不担心某个节点宕掉,而恰恰担心某个节点夯住。一个节点宕掉,最多就是小部分请求短暂不可用,重试即可恢复。但是一个节点夯住会将所有分布式请求都夯住,服务器端线程资源被占用不放,导致整个集群请求阻塞,甚至集群被拖垮。

既然数据库们对swap不待见,那是不是就要使用swapoff命令关闭磁盘缓存特性呢?非也,大家可以想想,关闭磁盘缓存意味着什么?实际生产环境没有一个系统会如此激进,要知道这个世界永远不是非0即1的,大家都会或多或少选择走在中间,不过有些偏向0,有些偏向1而已。很显然,在swap这个问题上,数据库必然选择偏向尽量少用。HBase官方文档的几点要求实际上就是落实这个方针:尽可能降低swap影响。知己知彼才能百战不殆,要降低swap影响就必须弄清楚Linux内存回收是怎么工作的,这样才能不遗漏任何可能的疑点。

简单来说,Linux会在两种场景下触发内存回收,一种是在内存分配时发现没有足够空闲内存时会立刻触发内存回收;一种是开启了一个守护进程(swapd进程)周期性对系统内存进行检查,在可用内存降低到特定阈值之后主动触发内存回收。第一种场景没什么可说,来重点聊聊第二种场景,如下图所示:

linux 常用命令

取自文章:Linux常用命令

基本操作

/* 开关机 */
shutdown -h now     立刻关机
shutdown -h 5       5分钟后关机
poweroff           立刻关机
shutdown -r now    立刻重启
reboot             立刻重启/* 帮助 */
命令 --help/* 目录 */
命令:cd 目录
cd /       切换到根目录
cd /usr    切换到根目录下的usr目录
cd ../     切换到上一级目录 或者  cd ..
cd ~       切换到home目录
cd -       切换到上次访问的目录命令:ls [-al]
ls                查看当前目录下的所有目录和文件
ls -a            查看当前目录下的所有目录和文件(包括隐藏的文件)
ls -l 或 ll       列表查看当前目录下的所有目录和文件(列表查看,显示更多信息)
ls /dir            查看指定目录下的所有目录和文件   如:ls /usr命令:mkdir 目录
mkdir    aaa            在当前目录下创建一个名为aaa的目录
mkdir    /usr/aaa    在指定目录下创建一个名为aaa的目录命令:rm [-rf] 目录
删除文件:
rm 文件        删除当前目录下的文件
rm -f 文件    删除当前目录的的文件(不询问)
删除目录:
rm -r aaa    递归删除当前目录下的aaa目录
rm -rf aaa    递归删除当前目录下的aaa目录(不询问)
全部删除:
rm -rf *    将当前目录下的所有目录和文件全部删除
注意:rm不仅可以删除目录,也可以删除其他文件或压缩包,为了方便大家的记忆,无论删除任何目录或文件,都直接使用 rm -rf 目录/文件/压缩包命令:mv可以重命名、移动
mv 当前目录  新目录
cp -r 目录名称 目录拷贝的目标位置   -r代表递归
find 目录 参数 文件名称
示例:find /usr/tmp -name 'a*'    查找/usr/tmp目录下的所有以a开头的目录或文件

vim编辑器操作

文件查看

文件的查看命令:cat/more/less/tail

cat:看最后一屏

示例:使用cat查看/etc/sudo.conf文件,只能显示最后一屏内容
cat sudo.conf

more:百分比显示

示例:使用more查看/etc/sudo.conf文件,可以显示百分比,回车可以向下一行,空格可以向下一页,q可以退出查看
more sudo.conf

less:翻页查看

示例:使用less查看/etc/sudo.conf文件,可以使用键盘上的PgUp和PgDn向上 和向下翻页,q结束查看
less sudo.conf

tail:指定行数或者动态查看

示例:使用tail -10 查看/etc/sudo.conf文件的后10行,Ctrl+C结束
tail -10 sudo.conf

权限修改

rwx:r代表可读,w代表可写,x代表该文件是一个可执行文件,如果rwx任意位置变为-则代表不可读或不可写或不可执行文件。

示例:给aaa.txt文件权限改为可执行文件权限,aaa.txt文件的权限是-rw-------

第一位:-就代表是文件,d代表是文件夹
第一段(3位):代表拥有者的权限
第二段(3位):代表拥有者所在的组,组员的权限
第三段(最后3位):代表的是其他用户的权限

命令:chmod +x aaa.txt
或者采用8421法
命令:chmod 100 aaa.txt

解压缩

命令:tar [-zxvf] 压缩文件
其中:x:代表解压
示例:将/usr/tmp 下的ab.tar解压到当前目录下

Linux进程调度策略

见文章:Linux进程及其调度策略

  进程是操作系统虚拟出来的概念,用来组织计算机中的任务。它从诞生到随着CPU时间执行,直到最终消失。不过,进程的生命都得到了操作系统内核的关照。就好像疲于照顾几个孩子的母亲内核必须做出决定,如何在进程间分配有限的计算资源,最终让用户获得最佳的使用体验。内核中安排进程执行的模块称为调度器(scheduler)。

进程状态

  调度器可以切换进程状态(process state)。一个Linux进程从被创建到死亡,可能会经过很多种状态,比如执行、暂停、可中断睡眠、不可中断睡眠、退出等。我们可以把Linux下繁多的进程状态,归纳为三种基本状态。

  进程创建后,就自动变成了就绪状态。如果内核把CPU时间分配给该进程,那么进程就从就绪状态变成了执行状态;在执行状态下,进程执行指令,最为活跃。正在执行的进程可以主动进入阻塞状态,比如这个进程需要将一部分硬盘中的数据读取到内存中。在这段读取时间里,进程不需要使用CPU,可以主动进入阻塞状态,让出CPU。

  当读取结束时,计算机硬件发出信号,进程再从阻塞状态恢复为就绪状态。进程也可以被迫进入阻塞状态,比如接收到SIGSTOP信号。

  调度器是CPU时间的管理员。Linux调度器需要负责做两件事:一件事是选择某些就绪的进程来执行;另一件事是打断某些执行中的进程,让它们变回就绪状态。不过,并不是所有的调度器都有第二个功能。

  调度器在让一个进程变回就绪时,就会立即让另一个就绪的进程开始执行。多个进程接替使用CPU,从而最大效率地利用CPU时间。当然,如果执行中进程主动进入阻塞状态,那么调度器也会选择另一个就绪进程来消费CPU时间。

  所谓的上下文切换(context switch)就是指进程在CPU中切换执行的过程。内核承担了上下文切换的任务,负责储存和重建进程被切换掉之前的CPU状态,从而让进程感觉不到自己的执行被中断。应用程序的开发者在编写计算机程序时,就不用专门写代码处理上下文切换了。

进程优先级

  调度器分配CPU时间的基本依据,就是进程的优先级。根据程序任务性质的不同,程序可以有不同的执行优先级。根据优先级特点,我们可以把进程分为两种类别。

1、实时进程(Real-Time Process):优先级高、需要尽快被执行的进程。它们一定不能被普通进程所阻挡,例如视频播放、各种监测系统。

2、普通进程(Normal Process):优先级低、更长执行时间的进程。例如文本编译器、批处理一段文档、图形渲染。

  普通进程根据行为的不同,还可以被分成互动进程(interactive process)和批处理进程(batch process)。互动进程的例子有图形界面,它们可能处在长时间的等待状态,例如等待用户的输入。一旦特定事件发生,互动进程需要尽快被激活。一般来说,图形界面的反应时间是50到100毫秒。批处理进程没有与用户交互的,往往在后台被默默地执行。

  实时进程由Linux操作系统创造,普通用户只能创建普通进程。两种进程的优先级不同,实时进程的优先级永远高于普通进程。进程的优先级是一个0到139的整数。数字越小,优先级越高。其中,优先级0到99留给实时进程,100到139留给普通进程。

  一个普通进程的默认优先级是120。我们可以用命令nice来修改一个进程的默认优先级。例如有一个可执行程序叫app,执行命令:&emsp;&emsp;nice -n -20 ./app

  命令中的-20指的是从默认优先级上减去20。通过这个命令执行app程序,内核会将app进程的默认优先级设置成100,也就是普通进程的最高优先级。命令中的-20可以被换成-20至19中任何一个整数,包括-20 和 19。默认优先级将会变成执行时的静态优先级(static priority)。调度器最终使用的优先级根据的是进程的动态优先级:动态优先级 = 静态优先级 – Bonus + 5

  如果这个公式的计算结果小于100或大于139,将会取100到139范围内最接近计算结果的数字作为实际的动态优先级。公式中的Bonus是一个估计值,这个数字越大,代表着它可能越需要被优先执行。如果内核发现这个进程需要经常跟用户交互,将会把Bonus值设置成大于5的数字。如果进程不经常跟用户交互,内核将会把进程的Bonus设置成小于5的数。

O(n)和O(1)调度器

  下面介绍Linux的调度策略。最原始的调度策略是按照优先级排列好进程,等到一个进程运行完了再运行优先级较低的一个,但这种策略完全无法发挥多任务系统的优势。因此,随着时间推移,操作系统的调度器也多次进化。

  先来看Linux 2.4内核推出的O(n)调度器。O(n)这个名字,来源于算法复杂度的大O表示法。大O符号代表这个算法在最坏情况下的复杂度。字母n在这里代表操作系统中的活跃进程数量。O(n)表示这个调度器的时间复杂度和活跃进程的数量成正比。

  O(n)调度器把时间分成大量的微小时间片(Epoch)。在每个时间片开始的时候,调度器会检查所有处在就绪状态的进程。调度器计算每个进程的优先级,然后选择优先级最高的进程来执行。一旦被调度器切换到执行,进程可以不被打扰地用尽这个时间片。如果进程没有用尽时间片,那么该时间片的剩余时间会增加到下一个时间片中。

  O(n)调度器在每次使用时间片前都要检查所有就绪进程的优先级。这个检查时间和进程中进程数目n成正比,这也正是该调度器复杂度为O(n)的原因。当计算机中有大量进程在运行时,这个调度器的性能将会被大大降低。也就是说,O(n)调度器没有很好的可拓展性。O(n)调度器是Linux 2.6之前使用的进程调度器。

  为了解决O(n)调度器的性能问题,O(1)调度器被发明了出来,并从Linux 2.6内核开始使用。顾名思义,O(1)调度器是指调度器每次选择要执行的进程的时间都是1个单位的常数,和系统中的进程数量无关。这样,就算系统中有大量的进程,调度器的性能也不会下降。

  O(1)调度器的创新之处在于,它会把进程按照优先级排好,放入特定的数据结构中。在选择下一个要执行的进程时,调度器不用遍历进程,就可以直接选择优先级最高的进程。

  和O(n)调度器类似,O(1)也是把时间片分配给进程。优先级为120以下的进程时间片为:(140–priority)×20毫秒,优先级120及以上的进程时间片为:(140–priority)×5 毫秒
  O(1)调度器会用两个队列来存放进程。一个队列称为活跃队列,用于存储那些待分配时间片的进程。另一个队列称为过期队列,用于存储那些已经享用过时间片的进程。

  O(1)调度器把时间片从活跃队列中调出一个进程。这个进程用尽时间片,就会转移到过期队列。当活跃队列的所有进程都被执行过后,调度器就会把活跃队列和过期队列对调,用同样的方式继续执行这些进程。

  上面的描述没有考虑优先级。加入优先级后,情况会变得复杂一些。操作系统会创建140个活跃队列和过期队列,对应优先级0到139的进程。一开始,所有进程都会放在活跃队列中。

  然后操作系统会从优先级最高的活跃队列开始依次选择进程来执行,如果两个进程的优先级相同,他们有相同的概率被选中。执行一次后,这个进程会被从活跃队列中剔除。如果这个进程在这次时间片中没有彻底完成,它会被加入优先级相同的过期队列中。当140个活跃队列的所有进程都被执行完后,过期队列中将会有很多进程。调度器将对调优先级相同的活跃队列和过期队列继续执行下去。过期队列和活跃队列,如图2所示:

图2 过期队列和活跃队列(需要替换)

我们下面看一个例子,有五个进程,如表1所示。

表1 进程

Linux操作系统中的进程队列(run queue),如表2所示。

表2 进程队列

  那么在一个执行周期,被选中的进程依次是先A,然后B和C,随后是D,最后是E。

  注意,普通进程的执行策略并没有保证优先级为100的进程会先被执行完进入结束状态,再执行优先级为101的进程,而是在每个对调活跃和过期队列的周期中都有机会被执行,这种设计是为了避免进程饥饿(starvation)。所谓的进程饥饿,就是优先级低的进程很久都没有机会被执行。

  我们看到,O(1)调度器在挑选下一个要执行的进程时很简单,不需要遍历所有进程。但是它依然有一些缺点。进程的运行顺序和时间片长度极度依赖于优先级。比如,计算优先级为100、110、120、130和139这几个进程的时间片长度,如表3所示。

表3 进程的时间片长度

  从表格中你会发现,优先级为110和120的进程的时间片长度差距比120和130之间的大了10倍。也就是说,进程时间片长度的计算存在很大的随机性。O(1)调度器会根据平均休眠时间来调整进程优先级。该调度器假设那些休眠时间长的进程是在等待用户互动。这些互动类的进程应该获得更高的优先级,以便给用户更好的体验。一旦这个假设不成立,O(1)调度器对CPU的调配就会出现问题。

完全公平调度器

  从2007年发布的Linux 2.6.23版本起,完全公平调度器(CFS,Completely Fair Scheduler)取代了O(1)调度器。CFS调度器不对进程进行任何形式的估计和猜测。这一点和O(1)区分互动和非互动进程的做法完全不同。

  CFS调度器增加了一个虚拟运行时(virtual runtime)的概念。每次一个进程在CPU中被执行了一段时间,就会增加它虚拟运行时的记录。在每次选择要执行的进程时,不是选择优先级最高的进程,而是选择虚拟运行时最少的进程。完全公平调度器用一种叫红黑树的数据结构取代了O(1)调度器的140个队列。红黑树可以高效地找到虚拟运行最小的进程。

  我们先通过例子来看CFS调度器。假如一台运行的计算机中本来拥有A、B、C、D四个进程。内核记录着每个进程的虚拟运行时,如表4所示。

表4 每个进程的虚拟运行时

  系统增加一个新的进程E。新创建进程的虚拟运行时不会被设置成0,而会被设置成当前所有进程最小的虚拟运行时。这能保证该进程被较快地执行。在原来的进程中,最小虚拟运行时是进程A的1 000纳秒,因此E的初始虚拟运行时会被设置为1 000纳秒。新的进程列表如表5所示。

  假如调度器需要选择下一个执行的进程,进程A会被选中执行。进程A会执行一个调度器决定的时间片。假如进程A运行了250纳秒,那它的虚拟运行时增加。而其他的进程没有运行,所以虚拟运行时不变。在A消耗完时间片后,更新后的进程列表,如表6所示。

表6 更新后的进程列表

  可以看到,进程A的排序下降到了第三位,下一个将要被执行的进程是进程E。从本质上看,虚拟运行时代表了该进程已经消耗了多少CPU时间。如果它消耗得少,那么理应优先获得计算资源。

  按照上述的基本设计理念,CFS调度器能让所有进程公平地使用CPU。听起来,这让进程的优先级变得毫无意义。CFS调度器也考虑到了这一点。CFS调度器会根据进程的优先级来计算一个时间片因子。同样是增加250纳秒的虚拟运行时,优先级低的进程实际获得的可能只有200纳秒,而优先级高的进程实际获得可能有300纳秒。这样,优先级高的进程就获得了更多的计算资源。

  以上就是调度器的基本原理,以及Linux用过的几种调度策略。调度器可以更加合理地把CPU时间分配给进程。现代计算机都是多任务系统,调度器在多任务系统中起着顶梁柱的作用。

Linux进程/线程操作

进程状态:
R(running):该程序正在运行中

S(sleep):该程序正在睡眠状态(idle),但是可以被唤醒(signal)

D:不可被唤醒状态,通常进程可能在等待I/O的情况

T:停止状态(stop),可能是在工作控制(背景暂停)或出错(traced)状态

Z:(zombie):僵尸状态,程序已经中之但是无法移出内存外

ps命令
——查看静态的进程统计信息(Processes Statistic)

常见的选项:

a:显示当前终端下的所有进程信息,包括其他用户的进程。

u:使用以用户为主的格式输出进程信息。

x:显示当前用户在所有终端下的进程。

-e:显示系统内的所有进程信息。

-l:使用长(long)格式显示进程信息。

-f:使用完整的(full)格式显示进程信息。

top命令
——查看进程动态信息

以全屏交互式的界面显示进程排名,及时跟踪包括CPU、内存等系统资源占用情况,默认情况下每三秒刷新一次,其作用基本类似于Windows系统中的任务管理器。

示例:

IO复用和select/poll/epoll的区别

详情看:linux 高并发之IO多路复用select、poll和epoll的区别

IO复用

  文件描述符(fd) 表示的是对某个文件操作的句柄。当然socket套接字也算是fd。一般来说,想对fd进行读写操作,就要操作到fd,例如 read(),但read()本身是BIO,即阻塞IO,当对fd调用read()时,如果暂时没有数据输入到fd,那么read()将会处于阻塞状态,直到有数据输入,read()才会返回。

  那么我们就可以想,如果现在有一个客户端连接进服务器,想要跟服务端通信,那么服务端就 对表示这个服务器的 sd(socket也能当作fd),调用read(),此时,若客户端有信息进入,read()返回,否则,read()会一直阻塞。

  那么如果有两个客户端连接进来了,也想跟服务器通信,那怎么办呢?答案是开多一条线程,让另一个线程对第二个客户端调用read(),并阻塞到客户端有信息进入服务器为止。那这样就出现问题了,实际应用中,客户端不可能只有几个啊,可能有上万个客户端想要跟服务端通信,那么也要开上万个线程?那显然是不实际的。

  所以解决方法就是 IO多路复用。IO多路复用一般有 select()、poll()、epoll()方式,他们都是对连接进服务端的 客户端socket就行监控,例如现在有100个 客户端socket,那么就监控这100个,如果这100个socket中有信息进入,则IO多路复用会返回,否则,就阻塞。即IO多路复用可以同时阻塞多个I/O操作,而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写(就是监听多个socket)。

正因为阻塞I/O只能阻塞一个I/O操作,而I/O复用模型能够阻塞多个I/O操作,所以才叫做多路复用。

select、poll、epoll

select的大致过程

  我们先是设置了要监控的各个I/O的文件描述符到fd_set集合,然后调用select(),最后fd_set集合只剩下有"异常"(包括读、写、异常)的文件描述符,举例就是,select前,readfds里的是 要监控的文件描述符的集合,select后,readfds里的则是 有数据信息进入的文件描述符的集合。

  注意: fd_set全是位图,位图就是只有0、1值的数组。 三组fd_set均将某些fd位 置0,只有那些可读,可写以及有异常条件待处理的fd位仍然为1。由于 select的底层是位图,位图是数组,所以select所能监控的文件描述符的数量是有上限的,因为数组就是定长的嘛。

select 的缺点:

  1. 内核/用户数据拷贝频繁,操作复杂。

在调用 select() 之前,需要手动在 程序中 维护一个包含要监控的文件描述符的 文件描述符集合 fd_set。把需要监听的文件描述符加到fd_set中。用户为了检测时间是否发生,还需要在用户程序手动维护一个数组,存储监控文件描述符。当内核事件发生,在将fd_set集合中没有事件发生的文件描述符清空,然后拷贝到用户区,和数组中的文件描述符进行比对。再调用select也是如此。每次调用,都需要来回拷贝。

  1. 单个进程监控的文件描述符有限,通常为1024*8个文件描述符

  2. 轮询时间效率低

select 检测时间是否发生的方式是通过轮询各个 文件描述符。当文件描述符的数量大的时候,轮询的效率很低,所以select 监控时的时间复杂度为O(n)。

poll
不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
优缺点:

  1. 相对于select,poll 没有监听文件描述符的数目上限。

  2. 由于 poll 监听文件描述符的方式都是轮询,跟select 一样,所以 poll 在高并发下的表现也不是特别好。

  从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

  所以对于poll来说,select的大部分问题,poll都具有。拿select为例,加入我们的服务器需要支持100万的并发连接。则在FD_SETSIZE(最大fd连接数)为1024的情况下,我们需要开辟100个并发的进程才能实现并发连接。除了进程上下调度的时间消耗外。从内核到用户空间的无脑拷贝,数组轮询等,也是系统难以接受的。因此,基于select实现一个百万级别的并发访问是很难实现的。

  epoll是在 Linux内核2.6版本中提出的,epoll可以看作是 select 和 poll 的增强版。

  select、poll监听文件描述符的方式是轮询,epoll是通过回调函数,采用回调的方式,只有活跃可用的fd才会调用callback函数,也就是说 epoll 只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll。通俗形容如下:

epoll 与 select/poll 的流程对比
select在每次被调用之前,都要把要监控的文件描述符fd加到监控的集合(也可以叫做等待队列),然后再调用select阻塞,直到有fd返回。这是select低效的原因之一------将“维护等待队列”和“阻塞进程”两个步骤合二为一。而epoll 则把这两个步骤分开,先用epoll_ctl维护等待队列,再调用epoll_wait阻塞进程(解耦)。显而易见的,效率就能得到提升。如下图。


为什么要分段分页

首先分页和分段都是为了更好的管理内存,是内存的管理方式。

想象一下,假如没有分段和分页机制的情况是什么样的? 这种情况下相当于直接操作内存,那么程序员在写代码的时候要自己考虑并写死用哪些物理地址!而且程序的运行一定需要连续的地址来一次装入程序!

上面的情况将引来三个问题:

  程序之间很容易相互影响,因为是直接操作内存的物理地址,那么程序B可能会出现修改覆盖程序A地址的情况!这种问题可以称为:没有有效隔离进程的地址空间!
  程序的地址难以把握,因为物理地址是写死的,假如程序写死操作的内存地址范围是0x000000100x00000020,但是装入程序的时候放在了0x000000300x00000040上,那么程序操作的压根不是程序占有的内存地址!
  换入换出的时候,因为地址必须连续,导致无法利用离散的小内存块!这种问题是内存利用率低!而分段的引入解决了前两个问题。有了分段机制,会对不同段间实现隔离!跨越段间的访问将会进行权限检查,这实现了隔离保护。并且分段后的地址是虚拟地址,到物理地址的转换是:段基地址+段内偏移地址(未开启分页情况),那么程序员可以不用再费劲的考虑使用那些物理地址,地址都可以从头开始.

  而分页的引入解决了第三个问题。分页机制和分段机制是非常类似的,他们都实现了隔离保护,分页是不同进程使用不同的页映射关系(页表),因此不同进程间互不影响。而且分页也不需要考虑如何操作物理地址了,分页后的地址称为线性地址,其关系是通过页表实现虚拟地址到物理地址的一一对应,这个对应是由MMU硬件实现的,不用程序员操心!而且分页和分段对于虚拟地址的映射方式基本一致,分段是通过段表GDT,LDT中的段描述符来确定段的属性和物理地址范围; 而分页是通过页表项(页表描述符)来记录页框的属性和物理地址位置!

  解决第三个问题,分页和分段最大的差别在于粒度。分页机制机制以4k字节大小的空间为划分单位,页内是连续的,但是不同页间是不连续的!这样就可以利用其那些不连续的小内存块了。

分段分页内存碎片

分页与分段都是磁盘的存储单位。
(1)分页:
①定义:在内存空间中,将内存空间划分为一个又一个大小相等的基本单位,称为“块”,也称为“页框”。将用户程序的地址空间按照"块"为基本单位划分成若干个大小相等的区域,这一个又一个的区域就称为页。

②内存分配规则:以块为单位进行存储。每一页存储在指定的块中,每一页在计算机中可以不相邻存储, 可以存储在不相邻的页框中。它是磁盘和内存之间传输数据块的最小单位。
(注意:大小不足一个页也必须占据一个块。这也是产生内存碎片的原因。)

(2)分段:
①定义:将用户程序的地址空间按照自身的逻辑关系划分成若干个大小不等的区域,称为“段”。每一个段有一个段名, 每一个段从0开始编址。一般而言,这个区域比分页中的区域大,因此它可以存储更多的内容,存储更加完整的一段信息。

②内存分配规则:以段为单位进行分配,每个段在内存中占据连续空间,但各段之间可以不相邻。

(3)分段与分页的对比:
①分页中的区域大小相等;分段中的则不等。
②分页是信息的物理地址,磁盘和内存之间传输数据块的最小单位;分段是信息的逻辑地址,它是相对于信息来划分的。
③分页中作业的地址空间是一维的;给出一个地址,那么就能够确定其页号和页内地址(页的长度),因此就能够找到相应的内容
分段中作业的地址空间是二维的。需要通过段号和段内地址来确定。
分析:
  都是采用线性方式计算的,但是每一页的长度是确定的,而每个段的长度是不同的。所以说分段是二维的,因为需要确定两个变量段号和段的长度;而分页是一维的,因为只需要确定页号即可
④分页允许存储于不连续的区块,因为每一页就对应于一个块,而每一页是可以非连续存储的,所以就可以了;
对于分段,每一段的所有内容必须存储在连续的区域中,不同的段可以存储在不连续的区域中。
⑤分页对于用户来说是不可见的,是系统自己确定的;分段对于用户来说则是确定的
⑥分页存储会产生内存碎片,不会产生外存碎片;
分段存储不会产生内存碎片,会产生外存碎片。

注意:段页式存储:
首先进行分段,接着再对每一段进行分页。此时每一段需要存储于连续的区域,而因为此时以块作为基本的存储单位,所以在一定情况下,一个段就需要存储在多个连续的块中(除非段的长度==块的长度),不同的段则可以存储在不连续的存储区域。

2、内存碎片&&外存碎片:
(1)性质不同:
①内存碎片:指的是已经被分配出去的,但是却没有被使用的内存空间。 因为基本存储单位的限制
②外存碎片:指的是还没有被分配的,但是由于太小或者是不连续,而导致不满足要求,所以没办法被分配的内存空间

(2)存储位置不同:
①内存碎片是存储于已分配区域内部的
②外存碎片是存储于未分配区域的

(3)状态不同:
①内存碎片:其他进程没办法使用它,因为它被某一个进程占有
②外存碎片:其他进程没办法使用它,因为它可存储的位置不连续或者是太小了

3、存储方式与碎片的关系:
(1)分页存储会产生内存碎片、不会产生外存碎片。
(2)分段存储:会产生外存碎片、不会产生内存碎片。
(3)段页式存储:产生内存碎片、外存碎片。

参考文章:分段、分页&&内存碎片、外存碎片

进程调度算法

参考文章:进程调度详解算法
先来先服务(FCFS)调度算法:处于就绪态的进程按先后顺序链入到就绪队列中,而FCFS调度算法按就绪进程进入就绪队列的先后次序选择当前最先进入就绪队列的进程来执行,直到此进程阻塞或结束,才进行下一次的进程选择调度。FCFS调度算法采用的是不可抢占的调度方式,一旦一个进程占有处理机,就一直运行下去,直到该进程完成其工作,或因等待某一事件而不能继续执行时,才释放处理机。操作系统如果采用这种进程调度方式,则一个运行时间长且正在运行的进程会使很多晚到的且运行时间短的进程的等待时间过长。

短作业优先(SJF)调度算法:其实目前作业的提法越来越少,我们姑且把“作业”用“进程”来替换,改称为短进程优先调度算法,此算法选择就绪队列中确切(或估计)运行时间最短的进程进入执行。它既可采用可抢占调度方式,也可采用不可抢占调度方式。可抢占的短进程优先调度算法通常也叫做最短剩余时间优先(Shortest Remaining Time First,SRTF)调度算法。短进程优先调度算法能有效地缩短进程的平均周转时间,提高系统的吞吐量,但不利于长进程的运行。而且如果进程的运行时间是“估计”出来的话,会导致由于估计的运行时间不一定准确,而不能实际做到短作业优先。

时间片轮转(RR)调度算法:RR 调度算法与FCFS 调度算法在选择进程上类似,但在调度的时机选择上不同。RR调度算法定义了一个的时间单元,称为时间片(或时间量)。一个时间片通常在1~100 ms之间。当正在运行的进程用完了时间片后,即使此进程还要运行,操作系统也不让它继续运行,而是从就绪队列依次选择下一个处于就绪态的进程执行,而被剥夺CPU使用的进程返回到就绪队列的末尾,等待再次被调度。时间片的大小可调整,如果时间片大到让一个进程足以完成其全部工作,这种算法就退化为FCFS调度算法;若时间片设置得很小,那么处理机在进程之间的进程上下文切换工作过于频繁,使得真正用于运行用户程序的时间减少。时间片可以静态设置好,也可根据系统当前负载状况和运行情况动态调整,时间片大小的动态调整需要考虑就绪态进程个数、进程上下文切换开销、系统吞吐量、系统响应时间等多方面因素。

高响应比优先(Highest Response Ratio First,HRRF)调度算法:HRRF调度算法是介于先来先服务算法与最短进程优先算法之间的一种折中算法。先来先服务算法只考虑进程的等待时间而忽视了进程的执行时间,而最短进程优先调度算法只考虑用户估计的进程的执行时间而忽视了就绪进程的等待时间。HRRF调度算法二者兼顾,既考虑进程等待时间,又考虑进程的执行时间,为此定义了响应比(Rp)这个指标:

Rp=(等待时间+预计执行时间)/执行时间=响应时间/执行时间
上个表达式假设等待时间与预计执行时间之和等于响应时间。HRRF调度算法将选择Rp最大值的进程执行,这样既照顾了短进程又不使长进程的等待时间过长,改进了调度性能。但HRRF调度算法需要每次计算各各个进程的响应比Rp,这会带来较大的时间开销(特别是在就绪进程个数多的情况下)。

多级反馈队列(Multi-Level Feedback Queue)调度算法:在采用多级反馈队列调度算法的执行逻辑流程如下:

设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二队次之,其余队列优先级依次降低。仅当第1~i-1个队列均为空时,操作系统调度器才会调度第i个队列中的进程运行。赋予各个队列中进程执行时间片的大小也各不相同。在优先级越高的队列中,每个进程的执行时间片就越小或越大(Linux-2.4内核就是采用这种方式)。
当一个就绪进程需要链入就绪队列时,操作系统首先将它放入第一队列的末尾,按FCFS的原则排队等待调度。若轮到该进程执行且在一个时间片结束时尚未完成,则操作系统调度器便将该进程转入第二队列的末尾,再同样按先来先服务原则等待调度执行。如此下去,当一个长进程从第一队列降到最后一个队列后,在最后一个队列中,可使用FCFS或RR调度算法来运行处于此队列中的进程。
如果处理机正在第i(i>1)队列中为某进程服务时,又有新进程进入第k(k<i)的队列,则新进程将抢占正在运行进程的处理机,即由调度程序把正在执行进程放回第i队列末尾,重新将处理机分配给处于第k队列的新进程。
从MLFQ调度算法可以看出长进程无法长期占用处理机,且系统的响应时间会缩短,吞吐量也不错(前提是没有频繁的短进程)。所以MLFQ调度算法是一种合适不同类型应用特征的综合进程调度算法。

最高优先级优先调度算法:进程的优先级用于表示进程的重要性及运行的优先性。一个进程的优先级可分为两种:静态优先级和动态优先级。静态优先级是在创建进程时确定的。一旦确定后,在整个进程运行期间不再改变。静态优先级一般由用户依据包括进程的类型、进程所使用的资源、进程的估计运行时间等因素来设置。一般而言,若进程需要的资源越多、估计运行的时间越长,则进程的优先级越低;反之,对于I/O bounded的进程可以把优先级设置得高。动态优先级是指在进程运行过程中,根据进程执行情况的变化来调整优先级。动态优先级一般根据进程占有CPU时间的长短、进程等待CPU时间的长短等因素确定。占有处理机的时间越长,则优先级越低,等待时间越长,优先级越高。那么进程调度器将根据静态优先级和动态优先级的总和现在优先级最高的就绪进程执行。

进程切换的上下文细节

参考文章:操作系统之进程切换
进程切换指从正在运行的进程中收回处理器,让待运行进程来占有处理器运行。

实质上就是被中断运行进程与待运行进程的上下文切换。

进程切换必须在操作系统内核模式下完成,这就需要模式切换。

模式切换又称处理器切换,即用户模式和内核模式的互相切换。

进程切换的工作过程
1、(中断/异常等触发)正向模式切换并压入PSW/PC 。 (Program Status Word 程序状态字。program counter 程序计数器。指向下一条要执行的指令)

2、保存被中断进程的现场信息。

3、处理具体中断、异常。

4、把被中断进程的系统堆栈指针SP值保存到PCB。(Stack Pointer 栈指针。Process Control Block 进程控制块。)

5、调整被中断进程的PCB信息,如进程状态)。

6、把被中断进程的PCB加入相关队列。

7、选择下一个占用CPU运行的进程。

8、修改被选中进程的PCB信息,如进程状态。

9、设置被选中进程的地址空间,恢复存储管理信息。

10、恢复被选中进程的SP值到处理器寄存器SP。

11、恢复被选中进程的现场信息进入处理器。

12、(中断返回指令触发)逆向模式转换并弹出PSW/PC。

进程切换何时发生呢?

进程切换一定发生在中断/异常/系统调用处理过程中,常见的有以下情况:

1、阻塞式系统调用、虚拟地址异常。

导致被中断进程进入等待态。

2、时间片中断、I/O中断后发现更改优先级进程。

导致被中断进程进入就绪态。

3、终止用系统调用、不能继续执行的异常。

导致被中断进程进入终止态。

但是并不意味着所有的中断/异常都会引起进程切换。
有一些中断/异常不会引起进程状态转换,不会引起进程切换,只是在处理完成后把控制权交还给被中断进程。

以下是处理流程:

1、(中断/异常等触发)正向模式切换并压入PSW/PC 。

2、保存被中断进程的现场信息。

3、处理具体中断、异常。

4、恢复被中断进程的现场信息。

5、(中断返回指令触发)逆向模式转换并弹出PSW/PC。

线程切换的上下文细节

线程切换,同一进程中的两个线程之间的切换;

每个线程都有一个程序计数器(记录要执行的下一条指令),一组寄存器(保存当前线程的工作变量),堆栈(记录执行历史,其中每一帧保存了一个已经调用但未返回的过程)。

寄存器 是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。

程序计数器是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置。

  1. 挂起当前任务(线程/进程),将这个任务在 CPU 中的状态(上下文)存储于内存中的某处
  2. 恢复一个任务(线程/进程),在内存中检索下一个任务的上下文并将其在 CPU 的寄存器中恢复
  3. 跳转到程序计数器所指向的位置(即跳转到任务被中断时的代码行),以恢复该进程在程序中


上下文切换会导致额外的开销,常常表现为高并发执行时速度会慢串行,因此减少上下文切换次数便可以提高多线程程序的运行效率。

  • 直接消耗:指的是CPU寄存器需要保存和加载, 系统调度器的代码需要执行, TLB实例需要重新加载, CPU 的pipeline需要刷掉
  • 间接消耗:指的是多核的cache之间得共享数据, 间接消耗对于程序的影响要看线程工作区操作数据的大小

如何减少上下文切换

既然上下文切换会导致额外的开销,因此减少上下文切换次数便可以提高多线程程序的运行效率。减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。

  • 无锁并发编程。多线程竞争时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash取模分段,不同的线程处理不同段的数据
  • CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁
  • 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态
  • 协程。在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换

用户态和内核态切换

1. 切换方式
从用户态到内核态切换可以通过三种方式,或者说会导致从用户态切换到内核态的操作:

  • 系统调用,这个上面已经讲解过了,在我公众号之前的文章也有讲解过。其实系统调用本身就是中断,但是软件中断,跟硬中断不同。系统调用机制是使用了操作系统为用户特别开放的一个中断来实现,如 Linux 的 int 80h 中断。
  • 异常:如果当前进程运行在用户态,如果这个时候发生了异常事件,会触发由当前运行进程切换到处理此异常的内核相关进程中
  • 外围设备中断:外围设备完成用户请求的操作之后,会向CPU发出中断信号,这时CPU会转去处理对应的中断处理程序。

2. 代价何在
当发生用户态到内核态的切换时,会发生如下过程(本质上是从“用户程序”切换到“内核程序”)

设置处理器至内核态。
保存当前寄存器(栈指针、程序计数器、通用寄存器)。
将栈指针设置指向内核栈地址。
将程序计数器设置为一个事先约定的地址上,该地址上存放的是系统调用处理程序的起始地址。
而之后从内核态返回用户态时,又会进行类似的工作。

I/O 频繁发生内核态和用户态切换,怎么解决。
首先要同意这个说法,即I/O会导致系统调用,从而导致内核态和用户态之间的切换。因为对I/O设备的操作是发生在内核态。那如何减少因为I/O导致的系统调用呢?答案是:使用户进程缓冲区。

用户进程缓冲区

你看一些程序在读取文件时,会先申请一块内存数组,称为buffer,然后每次调用read,读取设定字节长度的数据,写入buffer。之后的程序都是从buffer中获取数据,当buffer使用完后,在进行下一次调用,填充buffer。所以说:用户缓冲区的目的就是是为了减少系统调用次数,从而降低操作系统在用户态与核心态切换所耗费的时间。除了在进程中设计缓冲区,内核也有自己的缓冲区。

内核缓存区

当一个用户进程要从磁盘读取数据时,内核一般不直接读磁盘,而是将内核缓冲区中的数据复制到进程缓冲区中。但若是内核缓冲区中没有数据,内核会把对数据块的请求,加入到请求队列,然后把进程挂起,为其它进程提供服务。等到数据已经读取到内核缓冲区时,把内核缓冲区中的数据读取到用户进程中,才会通知进程,当然不同的IO模型,在调度和使用内核缓冲区的方式上有所不同。

小结

图中的read,write和sync都是系统调用。read是把数据从内核缓冲区复制到进程缓冲区。write是把进程缓冲区复制到内核缓冲区。当然,write并不一定导致内核的缓存同步动作sync,比如OS可能会把内核缓冲区的数据积累到一定量后,再一次性同步到磁盘中。这也就是为什么断电有时会导致数据丢失。所以说内核缓冲区,可以在OS级别,提高磁盘IO效率,优化磁盘写操作。

面试招聘——操作系统专场(一)相关推荐

  1. 【Django 开发】面试招聘信息网站(用户登录注册投在线递简历)

    该文章收录专栏 -Django从(图文并茂轻松上手教程)专栏-!! ??内容: [Djang | 增删改查]学生系统案例 [Django | 项目搭建]快速搭建自己的项目 [Django | alla ...

  2. 【游戏客户端面试题干货】-- 2021年度最新游戏客户端面试干货(操作系统篇)

    [游戏客户端面试题干货]-- 2021年度最新游戏客户端面试干货(操作系统篇)   大家好,我是Lampard~~   经过一番艰苦奋战之后,我终于是进入了心仪的公司.   今天给大家分享一下我在之前 ...

  3. 基于Springboot+MybatisPlus的学校企业就业求职面试招聘管理系统

    一.基于Springboot+MybatisPlus的学校企业就业求职面试招聘管理系统 1.1 项目概述 开发语言:Java8 数据库:Mysql5 前端技术:bootstrap layui echa ...

  4. 【面试招聘】计算机网络专场(一)

    关键词:[计网] [TCP/IP] [HTTP] 三次握手和四次挥手 三次握手   所谓的三次握手即TCP连接的建立.这个连接必须是一方主动打开,另一方被动打开的.以下为客户端主动发起连接的图解:   ...

  5. 一个想法照进现实-《IT连》创业项目:直觉型面试招聘的Bug

    前言: 创业转眼又过去了一个月,是时候抽时间写写文向大伙继续汇报进度了. 还记得上一篇创业文章,我还在说:创业时该不该用新手程序员. 嗯,然后,然后,报应就来了:所以这篇要写写自己在新人招聘上出现的问 ...

  6. 【面试招聘】一份转ML的面试心得记录

    这里是归辰的面经杂货铺,你想要的都有- 背景 作者是一名今年参加校招的应届生,本文写在校招结束后. 背景为:本科是北京某工科985,研究生在中科院某所,硕士研究生方向主要做图像语义分割,不过是偏门的雷 ...

  7. 【面试招聘】聊聊秋招中的面试技巧

    秋招的序幕已经拉开,很多公司都已经开启了秋招的进程,甚至有些互联网大厂的秋招都已经开始将近一个月的时间了,我在前面的文章中也写了很多关于秋招准备和一些技术面试相关的文章,那么今天我们从另外一个角度来聊 ...

  8. 【面试招聘】聊聊求职过程中的技术面试

    2020年的下半年开始了,很多大学都已经放了暑假,开始准备去找实习工作了,很多2021年毕业的同学也开始了秋招的进程,而对于一些社招人员,疫情结束,年终奖拿完,也开始准备跳槽.最近一段时间,无论是实习 ...

  9. 【面试招聘】美团+阿里 | 机器学习算法春招面经

    文章来源于NewBeeNLP,作者今天你leetcode 写在前面 2020春招实习投的职位都是机器学习算法工程师,这里主要说一下拿到offer的两家面试: 阿里云 (4.1 笔试0ac,对了10%, ...

最新文章

  1. 浅谈批处理获取管理员运行权限的几种方法
  2. 常用的linux命令和工具
  3. oracle数据库的性能测试工具有哪些,使用Oracle性能测试工具swingbench测试instance caging...
  4. 机器学习-sk-learn-Facebook数据集预测签到位置
  5. NVIDIA各个领域芯片现阶段的性能和适应范围
  6. html里下拉标记,HTML: select 标签
  7. @kafkalistener中id的作用_SSM框架(十一):Spring框架中的IoC(1)
  8. Elementui input不能再循环数据中每次自动聚焦的问题
  9. SVN服务的部署及使用
  10. django3,vue前后端分离数据请求
  11. HDU2186 一定要记住我爱你【水题】
  12. 基于virtualbox的centos7安装jdk1.8
  13. 万年历,java如何实现日历查询
  14. 使用命令行生成文件目录树
  15. ubuntu grub深入剖析个性设置
  16. dubbo(4) Dubbo源码解析之服务引入过程
  17. hooks useRef 报错object is possibly undefined
  18. jqGrid 表格底部汇总、合计行footerrow处理
  19. 国密标准官方查看地址
  20. TensorFlow学习笔记(二)手写体数字的识别——环境安装

热门文章

  1. js拼的onclick调用方法需要注意的地方 之二
  2. java20 创建服务器:ServerSocket
  3. JQuery Ajax 在asp.net中使用总结
  4. android端与windows端通信中文乱码问题
  5. liunx trac 插件使用之GanttCalendarPlugin
  6. T-SQL: Batches
  7. .net MVC之表单的使用
  8. 《VC++深入详解》学习笔记 第十六章 线程同步与异步套接字编程
  9. 项目管理的成功方程式
  10. 2道编程题:1.给定一个字符串,计算字符串中数值的个数并求和。