Skip to content

Latest commit

 

History

History
322 lines (197 loc) · 18 KB

File metadata and controls

322 lines (197 loc) · 18 KB

操作系统

程序和进程的区别

  • 而程序是一组有序的指令集合,是一种静态概念;进程是程序的一次执行过程,是一种动态概念
  • 程序可以长期保存;进程是是动态地创建和消亡的,具有一定的生命周期
  • 一个程序也以构成多个进程;一个进程也可以执行多个程序

进程和线程的区别

  • 进程是操作系统资源调度的基本单位,线程是任务的调度执行的基本单位

  • 线程在进程下运行(单纯的车厢无法运行)

  • 一个进程可以包含多个线程(一辆火车可以有多个车厢)

  • 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)

    同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)

  • 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)

  • 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)

  • 进程有单独的内存空间,因此切换上下文开销大

协程是什么

  • 协程是一种用户态的轻量级线程,协程的调度完全由用户控制
  • 一个线程可以多个协程,一个进程也可以单独拥有多个协程
  • 线程进程都是同步机制,而协程则是异步
  • 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态

孤儿进程和僵尸进程

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

僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。

有什么危害

  • 僵尸进程会占用系统资源,如果很多,则会严重影响服务器的性能
  • 孤儿进程不会占用系统资源,最终是由init进程托管,由init进程来释放它。

进程的几种状态

这里写图片描述

线程的几种状态

img

进程间通信的方式

  • 无名管道( pipe ):半双工,只能在具有亲缘关系的进程间使用
  • 有名管道 (named pipe) :半双工,允许无亲缘关系进程间的通信。
  • 高级管道(popen):将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程
  • 消息队列( message queue ) :消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 信号量( semaphore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。
  • 信号 ( signal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
  • 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,
  • 套接字( socket ) : 套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。

轻量级的通信

ZeroMQ

点对点无中间节点:传统的消息队列都需要一个消息服务器来存储转发消息。而 ZeroMQ则放弃了这个模式,把侧重点放在了点对点的消息传输上,并且(试图)做到极致。以为消息服务器最终还是转化为服务器对其他节点的点对点消息传输上。ZeroMQ能缓存消息,但是是在发送端缓存。ZeroMQ里有水位设置的相关接口来控制缓存量。当然,ZeroMQ也支持传统的消息队列(通过 zmq_device来实现)。

线程同步机制

线程同步指的是线程之间要排队,一个一个对共享资源进行操作

  • 临界区(Critical Section):在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占
  • 互斥量(Mutex):互斥对象和临界区很像,采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限
  • 信号量(Semaphore):信号量用于限制对临界资源的访问数量,保证了消费数量不会大于生产数量。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。每增加一个线程对共享资源的访问,当前可用资源计数就会减1 ,只要当前可用资源计数是大于0 的,就可以发出信号量信号
  • 事件对象(Event):通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作

进程/线程上下文切换的开销

  • 切换页目录
  • 切换内核态堆栈
  • 切换硬件上下文(进程恢复前,必须装入寄存器的数据统称为硬件上下文)

进程切换与线程切换的区别

进程切换涉及到虚拟地址空间的切换,而线程切换则不会。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的

多进程和多线程的优缺点

多进程优点

  • 每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系;
  • 通过增加CPU,就可以容易扩充性能;

多进程缺点

  • 逻辑控制复杂,需要和主程序交互;
  • 需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算
  • 多进程调度开销比较大;

多线程的优点

  • 无需跨进程边界;
  • 程序逻辑和控制方式简单;
  • 所有线程可以直接共享内存和变量等;
  • 线程消耗资源比进程少;

多线程的缺点

  • 每个线程与主程序共用地址空间,受限于2GB地址空间;
  • 线程之间的同步和加锁控制比较麻烦;
  • 一个线程的崩溃会导致整个进程崩溃;
  • 到达一定的线程数程度后,即使再增加CPU也无法提高性能,例如Windows Server 2003,大约是1500个左右的线程数就快到极限了(线程堆栈设定为1M),如果设定线程堆栈为2M,还达不到1500个线程总数;

守护进程

  • Linux系统启动时会启动很多系统服务进程,这些进程没有控制终端,不能直接和用户交互,这种进程称为守护进程
  • 守护进程独立于控制终端并周期性的执行某种任务或者等待处理某些发生的事件

一个线程OOM其他线程还能运行

当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行

虚拟内存

虚拟内存是什么

虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续可用的内存,而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部存储器上。通过虚拟内存可以让程序可以拥有超过系统物理内存大小的可用内存空间

虚拟内存解决了什么问题

  • 给进程提供了一个更大的内存空间,不再受物理内存大小的限制,更加高效的使用主存
  • 为程序提供内存管理,保护了每个进程的地址空间不被其他进程破坏
  • 给每个进程提供了一致的、完整的地址空间,从而简化了存储器管理

虚拟内存的实现方法

  • 如果一部分程序并不经常执行,那我们可以考虑只加载需要执行的部分。
  • 那么们可以构造一个大的虚拟内存空间,然后将其映射到较小的物理内存。
  • 这个虚拟内存空间存储我们进程的所有信息,而当进程执行时,我们只加载需要执行的部分,需要什么再加载什么
  • 之后,再通过调页功能及页面置换功能陆续的把即将要运行的页面调入内存,同时把暂不运行的页面换出到外存上

页面置换算法

什么是页面置换算法

进程运行时,若其访问的页面不在内存而需将其调入,但内存已无空闲空间时,就需要从内存中调出一页程序或数据,送入磁盘的对换区,其中选择调出页面的算法就称为页面置换算法。

页面置换算法的作用:实现虚拟存储管理

常见的页面置换算法

  • OPT(最佳页面置换算法) :淘汰以后不会使用的页面。
  • FIFO(先进先出算法) : 优先淘汰最早进入内存的页面
  • LRU(最近最少使用淘汰算法) :淘汰最近最少使用的页面。
  • LFU(最不经常使用淘汰算法) : 内存内使用越频繁的页面,被保留的时间也相对越长。

阻塞/非阻塞和同步/异步的区分

  • 阻塞/非阻塞:应用程序的调用是否立即返回
  • 同步/异步:程序在处理完成后,结果如何返回给调用者(同步:等待;异步:回调函数或事件)

Reactor和Proactor的区别

Reactor模式是基于同步I/O的,而 Proactor模式是异步I/O的

  • Reactor:来了事件操作系统通知应用进程,让应用进程来处理
  • Proactor:来了事件操作系统来处理,处理完再通知应用进

Proactor的好处

读写工作全程由操作系统来做,并不需要像 Reactor 那样还需要应用进程主动发起 read/write 来读写数据,操作系统完成读写工作后,就会通知应用进程直接处理数据

死锁

死锁是什么

当线程A持有独占锁a,并尝试去获取独占锁b的同时,线程B持有独占锁b,并尝试获取独占锁a的情况下,就会发生AB两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁

造成死锁必须达成的4个条件

  1. 互斥条件:一个资源每次只能被一个线程使用。
  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
  4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

死锁预防

1、破坏互斥条件

使资源同时访问而非互斥使用,就没有线程会阻塞在资源上,从而不发生死锁

2、破坏请求与保持条件

每个线程运行前必须一次性申请它所要求的所有资源,且仅当该进程所要资源均可满足时才给予一次性分配。这种实现会使得资源的利用率很低

3、破坏不剥夺条件

占有资源的线程若要申请新资源,必须主动释放已占有资源,若需要此资源,应该向系统重新申请

4、破坏循环等待条件

给系统的所有资源编号,规定线程请求所需资源的顺序必须按照资源的编号依次进行。

死锁避免

银行家算法:实质就是要设法保证系统动态分配资源后不进入不安全状态,以避免可能产生的死锁。 即每当线程提出资源请求且系统的资源能够满足该请求时,系统将判断满足此次资源请求后系统状态是否安全,如果判断结果为安全,则给该线程分配资源,否则不分配资源,申请资源的线程将阻塞。

死锁解除

  • 从其他进程强制剥夺资源给死锁进程
  • 撤销死锁进程,撤销的原则可以按进程的优先级和撤销进程代价的高低进行

排查死锁

Java:jstack

Linux C++:pstack + gdb

同步锁

每个对象有一个监视器锁(monitor),当monitor被占用时就会处于锁定状态

  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者
  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1
  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权

Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的

互斥锁

互斥锁:确保同一时间只有一个线程访问共享资源,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。

自旋锁

没有获取到锁的线程就一直循环等待判断该资源是否已经释放锁,自旋锁避免了线程上下文切换的开销,但它会一直占着cpu

线程池

线程池(Thread Pool)是一种基于管理线程的工具

使用线程池的好处

  • 降低资源消耗:通过池化技术重复利用已创建的线程,降低创建和销毁线程造成的损耗。
  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池 ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

线程池的工作原理

img

  • 当线程池中有任务需要执行时
  • 线程池会判断如果线程数量没有超过核心数量,就会新建线程池进行任务执行;如果线程池中的线程数量已经超过核心线程数,这时候任务就会被放入任务队列中排队等待执行;
  • 如果任务队列超过最大队列数,并且线程池没有达到最大线程数,就会新建线程来执行任务;如果超过了最大线程数,就会执行拒绝执行策略。

线程池为什么要把任务放到队列中

因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成 cpu过度切换。

线程池为什么要使用阻塞队列

  • 阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放 cpu资源
  • 当队列中有任务时才唤醒对应线程从队列中取出消息进行执行

线程复用原理

线程池将线程和任务进行解耦,线程是线程,任务是任务

在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式将只使用固定的线程就将所有任务的 run 方法串联起来。

线程池状态

08000847-0a9caed4d6914485b2f56048c668251a.jpg

  • RUNNING:线程池的初始化状态,可以添加待执行的任务
  • SHUTDOWN:线程池处于待关闭状态,不接收新任务仅处理已经接收的任务
  • STOP:线程池立即关闭,不接收新的任务,放弃缓存队列中的任务并且中断正在处理的任务
  • TIDYING:当所有的任务已终止,任务数量为0,线程池会变为TIDYING状态
  • TERMINATED:线程池终止状态

计算机运行程序的过程

  • 代码存储在硬盘上
  • 要执行代码,需要将代码读到内存中(二进制)
  • CPU读内存
    • 从内存中读取数据,然后放到寄存器中
    • 把寄存器中的数据写入到内存
    • 进行数学运算和逻辑运算
    • 执行下一条指令

代码到可执行文件的四个过程

预处理:展开头文件

编译:将代码转成汇编代码

汇编:将汇编代码转成机器可以执行的指令

链接:将 .o文件和库(动态库、静态库)链接到一起生成可执行 .exe文件

编译和链接

  • 编译:把源代码翻译成机器语言,并形成目标文件
    • 编译器只检测程序语法,函数、变量是否被声明
  • 链接:把目标文件、操作系统的启动代码和库文件组织起来形成可执行程序

动态链接和静态链接的区别

  • 静态链接方式:在程序执行前完成所有的组装工作,生成一个可执行的目标文件(EXE文件)。
  • 动态链接方式:在程序执行时完成链接工作,并且在内存中一般只保留该编译单元的一份拷贝

采用动态链接库的优点:

  • 更加节省内存
  • DLL文件与EXE文件独立,只要输出接口不变,更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性。

静态库:该LIB包含函数代码本身(即包括函数的索引,也包括实现),在编译时直接将代码加入程序当中

动态库:该LIB包含了函数所在的DLL文件和文件中函数位置的信息(索引),函数实现代码由运行时加载在进程空间中的DLL提供

总之,lib是编译时用到的,dll是运行时用到的。如果要完成源代码的编译,只需要lib;如果要使动态链接的程序运行起来,只需要dll

硬链接与软链接

  • 硬链接:引用同一文件系统中的文件。它引用的是文件在文件系统中的物理索引(也称为 inode)。移动或删除原始文件时,硬链接不会被破坏,因为它所引用的是文件的物理数据而不是文件在文件结构中的位置
  • 软链接:也称为符号链接,新建的文件以“路径”的形式来表示另一个文件。删除原来的文件,则相应的软连接也不可用