1 进程与线程
1.1 进程
进程:进程是具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。可以理解为是程序的实际执行实例,当程序在计算机上运行时,操作系统会为它创建一个进程,分配资源(如内存、CPU时间、文件描述符等),并在计算机上执行程序的指令。
1.1.1 进程分类
操作系统分类:
- 协作式多任务:早期 windows 系统使用,即一个任务得到了 CPU 时间,除非它自己放弃使用CPU,否则将完全霸占 CPU ,所以任务之间需要协作——使用一段时间的 CPU ,主动放弃使用。
- 抢占式多任务:Linux内核,CPU的总控制权在操作系统手中,操作系统会轮流询问每一个任务是否需要使用 CPU ,需要使用的话就让它用,不过在一定时间后,操作系统会剥夺当前任务的 CPU使用权,把它排在询问队列的最后,再去询问下一个任务。
进程类型:
- 守护进程: daemon,在系统引导过程中启动的进程,和终端无关进程(后台执行,ps aux中TTY为?的)
- 前台进程:跟终端相关,通过终端启动的进程(占用终端资源)
- 注意:两者可相互转化
按进程资源使用的分类:
- CPU-Bound:CPU 密集型,非交互
- IO-Bound:IO 密集型,交互
1.1.2 进程的状态和转换
基本状态
- 创建状态:进程在创建时会首先完成资源分配。如果创建工作无法完成,比如资源无法满足,就无法被调度运行。在这个状态下,进程只是被初始化,但尚未分配 CPU 资源。
- 就绪状态:进程已准备好,已分配到所需资源,只要分配到CPU就能够立即运行,其实就是进入了任务队列,在就绪状态下,进程等待操作系统的调度以获得 CPU 时间片来执行。
- 运行状态:当操作系统调度进程并将其分配到 CPU 时,进程进入运行状态。在运行状态下,进程将会执行其指令和代码。
- 阻塞状态:如果一个进程在执行过程中需要等待某些事件(I/O操作完成,申请缓存区失败,等待资源释放)而暂时无法运行,进程受到阻塞。在满足请求时进入就绪状态等待系统调用,在阻塞状态下,进程不会消耗CPU时间,直到等待的事件发生。
- 终止状态:进程结束,或出现错误,或被系统终止,进入终止状态。无法再执行,在终止状态下,进程的所有资源被释放。
其他状态
- 睡眠态:分为两种,可中断:interruptable,不可中断:uninterruptable
- 可中断睡眠态的进程在睡眠状态下等待特定事件发生,即使特定事件没有产生,也可以通过其它手段唤醒该进程,比如,发信号,释放某些资源等。
- 不可中断睡眠态的进程在也是在睡眠状态下等待特定事件发生,但其只能被特定事件唤醒,发信号或其它方法都无法唤醒该进程。
- 停止态:stopped,暂停于内存,但不会被调度,除非手动启动
- 僵死态:zombie,僵尸态。父进程结束前,子进程关闭,杀死父进程可以关闭僵死态的子进程
状态转换
-
运行——>就绪
- 主要是进程占用CPU的时间过长,而系统分配给该进程占用CPU的时间是有限的;
- 在采用抢先式优先级调度算法的系统中,当有更高优先级的进程要运行时,该进程就被迫让出CPU,该进程便由执行状态转变为就绪状态
-
就绪——>运行:运行的进程的时间片用完,调度就转到就绪队列中选择合适的进程分配CPU
-
运行——>阻塞:正在执行的进程因发生某等待事件而无法执行,则进程由执行状态变为阻塞状态,如发生了I/O请求
-
阻塞——>就绪:进程所等待的事件已经结束,就进入就绪队列
以下两种状态是不可能发生的:
-
阻塞——>运行:即使给阻塞进程分配CPU,也无法执行,操作系统在进行调度时不会从阻塞队列进行挑选,而是从就绪队列中选取
-
就绪——>阻塞:就绪态根本就没有执行,谈不上进入阻塞态
1.1.3 僵尸进程和孤儿进程的区别
僵尸进程是子进程已经结束,但父进程可能处于停止状态,未回收其资源导致的状态,所以这个子进程被称为"僵尸"。僵尸进程不占用系统资源,但在系统中存在时,会占用进程号(PID)等资源,影响系统性能。僵尸进程通常发生在子进程已经结束运行
孤儿进程是指子进程的父进程自己先提前退出或异常终止,导致子进程失去了父进程。这时候,孤儿进程会被操作系统的init进程(通常具有PID 1)接管。init进程会成为孤儿进程的新父进程,负责收养和管理它们。以避免它们变成僵尸进程。
1.1.4 进程之间的通信
-
管道(pipe):单向传输,只能用于父子进程(有亲缘关系)之间的通信,随进程的创建而建立,随进程的结束而销毁。
-
命名管道(FIFO):允许无亲缘关系进程间的通信,不适合进程间频繁地交换数据。
-
消息队列(MessageQueue):
- 解决了FIFO的缺点。消息队列实际上是链表,链表的每个节点都是一条消息。每一条消息都有自己的消息类型,用整数来表示,而且必须大于 0,但不适合比较大的数据的传输。
- 读取和写入的过程,都会发生用户态与内核态之间的消息拷贝过程。
-
共享存储(SharedMemory):
- 共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。
- 每个进程都会维护一个从内存地址到虚拟内存页面之间的映射关系。尽管每个进程都有自己的内存地址,不同的进程可以同时将同一个内存页面映射到自己的地址空间中,从而达到共享内存的目的。
- 比如a把数据全都丢到共享内存里面,b去共享内存拿数据,而且b可以按需选择拿哪些数据。
-
信号(sinal): 用于通知接收进程某个事件已经发生。
-
信号量(Semaphore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
-
套接字(Socket):
- 套接字文件,双工通信,网络进程间通信,比如A与B要通信,需要A将数据发生给套接字文件,B从套接字文件接收,或者B发A收。
- 它既解决了管道只能在相关进程间单向通信的问题,又解决了网络上不同主机之间无法通信的问题。
1.1.5 用户态和内核态
用户态:当程序运行在用户态时,不能直接使用系统资源,也不能改变 CPU 的工作状态,并且只能访问这个用户程序自己的存储空间,所以用户态不能直接创建进程。
内核态:系统中既有操作系统的程序,也有普通用户程序。为了安全性和稳定性,操作系统的程序不能随便访问,这就是内核态。即需要执行操作系统的程序就必须转换到内核态才能执行,它可以访问计算机的所有资源。
为什么要区分用户态和内核态?
在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。比如:清内存、设置时钟等。
所以,CPU将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通的应用程序只能使用那些不会造成灾难的指令。
如此设计的本质意义是进行权限保护。 限定用户的程序不能乱搞操作系统,如果人人都可以任意读写任意地址空间软件管理便会乱套。
1.1.6 用户空间和内核空间
用户空间:指的是操作系统为用户程序分配的内存区域。每个运行在用户空间的应用程序都有其独立的地址空间,这有助于保护系统免受恶意或错误代码的影响。用户空间的应用程序不能直接访问硬件资源或关键的操作系统数据结构。当程序以用户态运行时,它只能访问属于自己的那部分用户空间内存,而不能直接访问内核空间或其他进程的空间。
内核空间:是操作系统内核使用的内存区域。这里存放了内核代码、设备驱动程序、以及所有需要直接访问硬件资源的数据结构等。内核空间允许直接操作硬件资源和管理整个系统的状态。
1.2 线程
在早期的操作系统中并没有线程的概念,后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程。
线程是程序执行的最小单位,是进程内的一个执行单元,一个进程可以有一个或多个线程,各个线程之间共享进程的内存空间。
1.2.1 线程的状态和转换
状态 | 含义 |
---|---|
就绪(Ready) | 线程能够运行,但在等待被调度。可能线程刚刚创建启动,或刚刚从阻塞中恢 复,或者被其他线程抢占 |
运行 (Running) | 线程正在运行 |
阻塞 (Blocked) | 线程等待外部事件发生而无法运行,如I/O等待操作 |
终止 (Terminated) | 线程完成,或退出,或被取消 |
1.2.2 进程与线程的区别
在概念上:进程是操作系统分配资源的最小单位;线程是程序执行的最小单位;一个进程由一个或多个线程组成。
在资源分配上:进程是独立的资源拥有单位,每个进程都有独立的地址空间和系统资源,某进程内的线程在其它进程不可见;而线程是共享所属进程的资源,多个线程共享同一个地址空间和系统资源,包括内存、文件和其他系统资源。
在创建和销毁上:进程的开销通常比线程大,因为进程需要为其分配独立的内存空间和资源;而线程的创建和销毁开销相对较小,因为它们共享了进程的资源。
在通信上,也是在并发性上:线程之间的通信和同步相对容易,并发性较高,因为它们共享同一地址空间;而进程之间的通信和同步则需要额外的机制,如管道、消息队列等,并发性较低,因为它们通常是相互独立的。
1.3 多进程和多线程
多进程:多进程是指在一个应用程序中同时运行多个进程,每个进程都有独立的地址空间和资源,可以较充分地利用多处理器。
多线程:顾名思义,多个线程,一个进程中如果有多个线程运行,就是多线程,实现一种并发。
1.3.1 并行和并发
并行:指的是多个任务真正地在同一时刻执行,通常需要多个处理器核心来实现。
并发:指的是多个任务在同一时间段内交替执行,但并不一定是同时执行。操作系统通过时间片轮转等调度机制使得多个任务看起来像是同时进行的。
单核CPU:无论是多进程还是多线程,都是通过时间片轮转的方式实现并发,不能实现真正的并行执行。
多核CPU:可以实现真正的并行执行,即多个进程或线程可以在不同的核心上同时执行,从而提高系统的性能和效率。
1.3.2 python中的GIL锁
GIL 保证CPython进程中,只有一个线程执行字节码。甚至是在多核CPU的情况下,也只允许同时只能有一个CPU核心上运行该进程的一个线程。所以python中的多线程是假并行。
1.3.3 CPU密集型和IO密集型
CPU密集型任务:
- 指那些需要大量CPU计算时间的任务,例如视频编码、图像处理、科学计算等。这类任务主要消耗的是CPU资源。
- 适用模型:多进程
- 因为其主要依赖于CPU计算能力,用多进程可以充分利用多核CPU的优势,实现真正的并行计算;而使用多线程可能不会带来显著的性能提升,因为在Python中有GIL(全局解释器锁)。
IO密集型任务:
- 指那些花费大量时间等待外部资源响应的任务,例如读写文件、网络请求、数据库查询等。这类任务大部分时间都在等待I/O操作完成,而非占用CPU资源。
- 适用模型:多线程或多协程
- 当一个线程执行 I/O 操作(如网络请求、文件读写等)时,CPython 会检测到这是一个 I/O 操作,并允许该线程暂时释放 GIL。这使得其他线程有机会获取 GIL 并执行它们的 Python 字节码。一旦 I/O 操作完成,线程重新获取 GIL 并继续执行后续代码。这种机制对于 I/O 密集型任务非常重要,因为它允许在等待 I/O 的时候让出 CPU 给其他线程使用,从而提高程序的整体效率和响应速度。
协程是一种用户级别的轻量级线程,由程序员显式控制其执行流程。协程可以在特定点暂停(yield),然后在之后恢复执行,而且无需内核态与用户态之间的转换,这使得它非常适合于异步编程模型。例如可以使用Python中的asyncio模块来实现。
2 内存管理
2.1 物理内存和虚拟内存
物理内存是计算机系统中实际存在的硬件内存,通常是 RAM(随机访问存储器)的形式
虚拟内存是为了满足物理内存不足而提出的策略,通过将内存地址空间划分为固定大小的块(通常称为页或页帧),并在需要时将这些页面从磁盘交换到物理内存中来工作。这使得每个进程都有自己的独立地址空间,而不需要直接管理物理内存的分配。
当运行某个大程序、大游戏,需要的内存超过空闲内存但小于物理内存总量时,会暂时把内存里这些数据放到磁盘上的虚拟内存里,空出物理内存运行游戏。等退出游戏后,又会把虚拟内存里的东西读出来,放回物理内存。所以,虚拟内存并不是用来虚拟物理内存的,而是暂存数据的。所以虚拟内存不是代替物理内存来运行程序的。
2.2 页高速缓存与页写回机制
在当今的计算机系统中,处理器的运行速度是非常快的,但 RAM 和磁盘并没有质的飞跃(尤其是磁盘读写速度),这就导致了系统整体性能并没有因为处理器速度的提升而提升。于是就使用到了缓存技术(其实就是内存缓存的技术),通过缓存机制解决了处理器和磁盘直接速度的不平衡。
页高速缓存通常以页面的单位来存储数据,因此被称为"页"高速缓存。页高速缓存是操作系统在物理内存中维护的一个缓存,用于存储磁盘上的文件数据的副本。Linux 系统中当一个文件的数据(内容)被读取时,操作系统将数据从磁盘读取到页高速缓存中,以便后续的读取操作可以直接去内存中读取数据,更快速地访问数据。
因此页高速缓存提高了文件的读取性能,因为它允许频繁访问的数据保留在快速的内存中,而不是每次都从慢速的磁盘中读取。
页写回机制是一种优化技术,用于减少文件写入操作对性能的影响,提高磁盘I/O的性能。当文件数据被修改并需要写回磁盘时,操作系统通常不会立即将数据写回磁盘,而是将数据标记为"脏",并将其保留在页高速缓存中。操作系统通过一种策略,例如延迟写回或按需写回,决定何时将脏数据写回磁盘。这允许操作系统将多个写操作合并,以减少磁盘写入的次数,提高性能。
页写回机制可以防止频繁的磁盘写入操作对系统性能造成明显的影响,因为它允许系统在更高效的时间进行磁盘写入,而不是在每个写操作之后立即进行。
2.3 Swap Space
Swap Space(交换空间)是 Linux 中虚拟内存的一个实现方式,除了填补因物理内存不足的空缺外,还将会在适当的时候将物理内存中不经常读写的数据块自动交换到 Swap 交换空间(这个交换的操作是由 Linux 内核来执行的),从而侧面将经常读写的数据保留在了物理内存。
说白了就是,它为什么叫交换空间,就是要实现数据的交换(将不常用数据交换到作为逻辑内存的磁盘空间,而保留常用数据在真正的物理内存空间中)。这个交换的策略由 Linux 系统内核定时执行,目的就是为了保持尽可能多的空闲物理内存。
那什么叫做在适当的时候会进行数据交换,我们所说的适当时间其实是 Linux 内核根据“最近最经常使用”算法(LRU 算法),定时地将一些不经常使用的页面文件(其实就是文件数据,因为内存是以页面存储数据的)交换到 Swap。
有时候会发现:Linux 物理内存还有很多,而 Swap 的数据占用却很大,这是什么原因呢?其实这是正常现象。如果一个内存占用很大的进程正在运行,必然就会耗费大量的内存资源,此时 Linux 内核就会将一些不常用的页面文件交换到 Swap Space 中。当该进程终止后,Linux 就会释放该进程占用的大量内存资源,而此时被交换出去的页面文件数据并不会自动又交换到物理内存中来(除非有这个必要),那此时看到的就是物理内存空间空闲,Swap Space 占用较大的现象了。
2.4 页面缺失和页面调度
程序要读虚拟内存中的某个页面数据时,而恰好这个页面数据位于 Swap Space 中。那此时的流程就是:交换空间(Swap Space)中的数据在被读取时通常会首先被交换到物理内存,然后才能被程序访问。
页面缺失(Page Fault):当程序尝试访问一个在物理内存中不存在的内存页时,会触发一个页面缺失。这可能是因为该页已经被交换到了交换空间中,或者是因为程序首次访问该页。在任何情况下,操作系统会注意到页面缺失。
页面调度(Page Scheduling):操作系统负责页面调度,决定哪些页面从交换空间加载到物理内存中以满足程序的需求。通常,操作系统会使用一种页面替换算法(例如LRU - 最近最少使用)来选择哪些页从交换空间中加载,以便最大限度地减少性能开销。一旦数据加载到物理内存中,操作系统会更新进程的页表,以指示这些页面现在位于物理内存中,程序可以访问它们。页表是一个数据结构,用于映射虚拟地址到物理地址。
2.5 OOM (Out of Memory) 机制
OOM 是 Linux 系统中一种保护机制,指内存不足/内存溢出。当系统内存被过度占用时,Linux 会启动 OOM 机制来避免系统崩溃。
触发条件
- 系统级 OOM:当一台机器上运行的进程对内存的占用达到物理机的最大内存使用量时,会触发系统级 OOM,整个系统内的进程都有可能成为被终止的目标。
- 容器级 OOM:如果物理机内存足够,但某个容器内的进程对内存的使用量达到了 cgroup 的限制,也会触发 OOM 机制,此时只针对该控制组内的进程进行终止。
终止进程的原因及选择
- 原因:Linux 系统内存超配(overcommit),很多进程申请的虚拟内存可能实际只使用了一部分。当物理内存不足,若超配的内存空间真的投入使用而没有足够的物理内存,正常运行的进程会挂掉,所以必须立即触发 OOM 来释放内存。
- 进程选择:当系统级(非容器级)OOM发生时,并不会随机杀进程,具体啥哪个进程是通过内核的 oom_badness() 函数评定,主要依据进程已经使用的物理内存页面数和每个进程的 OOM 校准值 oom_score_adj。
- 评分 = 系统总的可用页面数 * oom_score_adj 值 + 进程已经使用的物理页面数
- 评分越高,被 OOM kill 掉的几率越大,可通过调整每个进程的 /proc//oom_score_adj 文件中的值(-1000 到 1000)来改变该进程被 OOM kill 的几率。
2.5.1 oom 日志的分析
1、关键内容
- 进程信息:查看被终止进程的 mem_alloc,了解其占用的 rss(物理内存的页数,默认每页 4 kB)和 oom_score_adj。若列出的是整个操作系统的进程,则为系统级 OOM。
- 容器或控制组信息:明确发生 OOM 的容器或控制组,判断是哪个容器出现问题。
- 被杀进程的 PID:找到最后被 oom killer 杀死的进程的 PID 号。
2、处理方案
- 被杀进程需要大量内存,可调大 memory.limit_in_bytes。
- 若进程因内存泄漏达到 memory.limit_in_bytes 限制,需解决内存泄漏问题。
2.5.2 swap 分区对 memory 的影响
- 系统级 OOM
启用 swap 分区后,可用内存 = 物理可用内存大小 + swap 分区大小。物理内存不足时会使用 swap,降低运行效率。系统级 OOM 在物理内存和 swap 分区用完的情况下触发。系统级 OOM 评分计算公式为:评分 = 系统当前剩余可用内存 * oom_score_adj + 当前进程占用的物理内存。 - 容器级 OOM
swap 分区开启时,若容器内进程占用的内存量达到容器最大内存限制,不会立即触发 OOM,而是开始使用 swap 分区。例如,使用命令 docker run -m 200 M --memory-swap=300 M。
2.5.3 控制全局与局部的 swap 使用
- 全局设置
启用 swap,通过调整系统 swappiness 参数(位于 /proc/sys/vm/swappiness)控制 swap 分区与内存、page cache 释放的比重。 - 局部设置
针对个别容器,可通过 docker run -it --memory-swappiness=0 ubuntu:16.04 /bin/bash 关闭 swap 的使用。容器的 memory.swappiness 优先级高于全局 swappiness,设置为 0 会彻底关闭容器对 swap 分区的使用。
2.5.4 可压缩资源与不可压缩资源
- 可压缩资源(如 CPU)
即使进程对 CPU 的使用率超过限制,也不会被终止,而是限制其最大使用量。CPU 时间片共享,操作系统可随时夺走进程的执行权限,进程进入就绪状态等待重新调度。 - 不可压缩资源(如内存、磁盘)
进程占用的内存和磁盘达到限制会触发 OOM 机制,将其终止。内存超配,一旦不足必须通过 OOM 杀掉进程释放内存来应对。
3 I/O模型
3.1 IO的分类
3.1.1 磁盘IO和网络IO
磁盘I/O处理过程
- 进程向内核发起系统调用,请求磁盘上的某个资源比如是html 文件或者图片
- 然后内核通过相应的驱动程序将目标文件加载到内核的内存空间
- 加载完成之后把数据从内核内存再复制给进程内存,如果是比较大的数据也需要等待时间。
网络I/O 处理过程
- 获取请求数据,客户端与服务器建立连接发出请求,服务器接受请求
- 构建响应,当服务器接收完请求,并在用户空间处理客户端的请求,直到构建响应完成
- 返回数据,服务器将已构建好的响应再通过内核空间的网络 I/O 发还给客户端
不论磁盘和网络I/O,每次I/O,都要经由两个阶段:
- 第一步:将数据从文件先加载至内核内存空间(缓冲区),等待数据准备完成,时间较长
- 第二步:将数据从内核缓冲区复制到用户空间的进程的内存中,时间较短
3.1.2 同步IO和异步IO
函数或方法被调用的时候,调用者是否得到最终结果的。
-
直接得到最终结果结果的,就是同步调用;
-
不直接得到最终结果的,就是异步调用。
3.1.3 阻塞IO和非阻塞IO
函数或方法调用的时候,是否立刻返回。
-
立即返回就是非阻塞调用;
-
不立即返回就是阻塞调用。
阻塞I/O (Blocking I/O)
- 当一个进程发起一个I/O请求时,它会暂停执行,直到I/O操作完成。
- 这是最常见的I/O模型,例如当你打开一个文件进行读取时。
非阻塞I/O (Non-blocking I/O)
- 在非阻塞模式下,当进程尝试发起I/O操作时,如果操作不能立即完成,则会立即返回一个错误或特定值,而不是等待。
- 进程需要不断地轮询检查I/O操作的状态,这称为“忙等”(busy-waiting)。
3.1.4 联系
同步阻塞,我啥事不干,就等你打饭打给我。打到饭是结果,而且我啥事不干一直等,同步加阻塞。
同步非阻塞,我等着你打饭给我,饭没好,我不等,但是我无事可做,反复看饭好了没有。打饭是结果,但是我不一直等。
异步阻塞,我要打饭,你说等叫号,并没有返回饭给我,我啥事不干,就干等着饭好了你叫我。例如,取了号什么不干就等叫自己的号。
异步非阻塞,我要打饭,你给我号,你说等叫号,并没有返回饭给我,我去看电视、玩手机,饭打好了叫我。
3.2 同步IO模型
3.2.1 阻塞IO模型
进程等待(阻塞),直到读写完成。(全程等待)
read() write() recv() send() accept() connect()
等…都是阻塞IO
当用户调用了read()
,kernel 就开始了IO的第一个阶段:准备数据。对于网络IO来说,很多时候数据在一开始还没有达到(比如,还没有收到一个完整的数据包),这个时候kernel 就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel 一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来
所以,阻塞IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被阻塞了。这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够
3.2.2 非阻塞IO模型
进程调用recvfrom操作,如果IO设备没有准备好,立即返回ERROR,进程不阻塞。用户可以再次发起系统调用(可以轮询),如果内核已经准备好,就阻塞,然后复制数据到用户空间。可防止进程阻塞在IO操作上,需要轮询。
第一阶段数据没有准备好,可以先忙别的,等会再来看看。检查数据是否准备好了的过程是非阻塞的。
第二阶段是阻塞的,即内核空间和用户空间之间复制数据是阻塞的。
淘米、蒸饭我不阻塞等,反复来询问,一直没有拿到饭。盛饭过程我等着你装好饭,但是要等到盛好饭才算完事,这是同步的,结果就是盛好饭。
所以,在非阻塞式 IO 中,用户进程其实是需要不断的主动询问(轮询) kernel 数据准备好了没有。 轮询的时间不好把握,这里是要猜多久之后数据才能到。等待时间设的太长,程序响应延迟就过大;设的太短,就会造成过于频繁的重试,干耗CPU而已,是比较浪费CPU的方式。
3.2.3 多路复用IO模型
以前一个应用程序需要对多个网络连接进行处理,传统的方法是使用多线程或多进程模型,为每个连接创建一个线程或进程进行处理。这种方法存在一些问题,例如线程或进程的创建和销毁需要消耗大量的系统资源,且容易导致线程或进程的数量过多,进而导致系统崩溃或运行缓慢。所以便出现了IO多路复用。
多路复用思想是将操作的所有文件描述符保存在一张文件描述符表中,然后将文件描述符表交给内核,让内核检测当前是否有准备就绪的文件描述符(例如有数据可读或可写),如果有则通知应用程序,操作就绪的文件描述符。这种方式可以让一个进程处理多个并发的IO操作,而不需要为每个IO操作创建一个独立的线程或进程。
以select为例,当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
相比非阻塞IO
- 在非阻塞IO模型中,如果一个文件描述符还没有准备好进行读写操作,应用程序需要不断地轮询该描述符的状态(即不断调用
read()
或write()
)。这会导致较高的CPU使用率,因为即使在没有数据的情况下,程序也需要不断地执行这些系统调用。 - 相比之下,使用IO多路复用(如
select()
,poll()
, 或epoll()
),程序只需要在一个调用中指定多个文件描述符,然后等待至少有一个描述符准备就绪。这减少了CPU的轮询开销。
3.2.3.1 select
- 这是最早的多路复用函数之一,它可以监控多个文件描述符,并且在任何一个描述符上有事件发生时返回。但是它的最大限制是文件描述符的数量受限于系统定义的最大值。
- 通过将已连接的Socket放入一个文件描述符集合中,然后调用select函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生。然而,select存在一些缺点,如每次调用select都需要将fd集合从用户态拷贝到内核态,且仅仅返回可读文件描述符的个数,具体哪个可读还需要用户自己以轮询的方式线性扫描,效率不高。
3.2.3.2 poll
- 与select()类似,但没有文件描述符数量的限制。poll()使用链表来跟踪描述符的状态变化。
- 修复了select的一些问题,不再使用BitsMap来存储所关注的文件描述符,改用动态数组,以链表形式来组织,突破了select的文件描述符个数限制。然而,poll和select并没有太大的本质区别,都是使用线性结构存储进程关注的Socket集合,因此都需要遍历文件描述符集合来找到可读或可写的Socket。
3.2.3.3 epoll
- 这是Linux系统提供的一个更为高效的多路复用接口,它可以高效地处理大量的文件描述符。epoll使用事件驱动的方式,只有当事件发生时才会通知应用程序。
- 它针对select和poll的缺点进行了优化。epoll在内核中保存一份文件描述符集合,无需用户每次都重新传入,只需告诉内核修改的部分。内核不再通过轮询的方式找到就绪的文件描述符,而是通过异步I0事件唤醒。这使得epoll在处理大量并发连接时具有更高的性能和效率。
3.2.4 信息驱动IO模型
进程在IO访问时,先通过sigaction系统调用,提交一个信号处理函数,立即返回,进程不阻塞。当内核准备好数据后,产生一个SIGIO信号并投递给信号处理函数(由内核通知,发送信号)。可以在此函数中调用recvfrom函数操作数据从内核空间复制到用户空间,这段过程进程阻塞。
此模型的优势在于等待数据报到达期间进程不被阻塞。用户主程序可以继续执行,只要等待来自信号处理函数的通知。但是信号 I/O 在大量 IO 操作时可能会因为信号队列溢出导致没法通知。
3.3 异步IO模型
进程发起异步IO请求,立即返回。内核完成IO的两个阶段,内核给进程发一个信号。异步I/O允许进程发起一个I/O操作后继续执行其他任务,当I/O操作完成时,操作系统会通知进程。所以IO两个阶段,进程都是非阻塞的。
异步I/O 与 信号驱动I/O最大区别在于,信号驱动是内核通知用户进程何时开始一个I/O操作,而异步I/O是由内核通知用户进程I/O操作何时完成。
但是Linux的 aio 的系统调用,内核是从版本2.6开始支持,只用在磁盘IO读写操作,不用于网络IO。
3.4 IO模型总结
4 磁盘管理
4.1 文件的 I/O 模式
- Buffer I/O (标准 I/O)
- 原理:数据先写入内核的缓冲区(页面缓存),内核异步将缓冲区的脏数据写入磁盘。
- 优点:
- 读操作:大多数情况下直接从 RAM 缓存读取数据,速度快。
- 写操作:页面缓存减少磁盘 I/O 次数,提高写性能。
- 缺点:
- 可能存在数据丢失风险,如系统崩溃或断电。
- 缓存溢出可能导致性能问题。
- 适用场景:适合大多数场景,尤其是读操作较多的应用。
- Direct I/O (直接 I/O)
- 原理:数据直接写入磁盘,绕过页面缓存。
- 优点:避免缓冲区数据丢失的风险。
- 缺点:性能较低,因为每次写操作都需要直接与磁盘交互。
- 适用场景:适合对数据安全性要求较高的应用,如数据库系统。
4.2 page cache
- 脏数据 (Dirty Pages)
- 定义:缓冲区中未被写入磁盘的数据,称为脏数据。
- 作用:提高写性能,减少磁盘 I/O 次数。
- 脏数据落盘机制
- 落盘工具:Linux 内核中的 kworker/flush 线程负责将脏数据写入磁盘。
- 落盘时机:
- 当脏数据比例超过 vm.dirty_background_ratio 时,开始异步写入。
- 当脏数据比例超过 vm.dirty_ratio 时,同步阻塞写入。
- 脏数据存活时间超过 vm.dirty_expire_centisecs 时,会强制落盘。
- 内核定期 (vm_dirty_writeback_centisecs 时间间隔) 唤醒 flush 线程处理脏数据。
4.3 衡量硬盘性能的常见指标
IOPS(每秒 I/O 操作次数)
- 定义:每秒磁盘完成的读或写操作次数。
- 计算公式:IOPS = 1000 ms / (寻道时间 + 旋转延迟)。
BPS(每秒 I/O 流量)
- 定义:一秒内磁盘写入和读出的数据总量,单位为 MB/s。
- 计算公式:BPS = IOPS × 数据块大小
IO 操作的基本单位
- 扇区:硬盘的最小读写单位,默认大小为 512Bytes。
~# fdisk -l
磁盘 /dev/sda: 21.5 GB, 21474836480 字节, 41943040 个扇区
Units = 扇区 of 1 * 512 = 512 bytes
扇区大小(逻辑/物理): 512 字节 / 512 字节
I/O 大小(最小/最佳): 512 字节 / 512 字节
磁盘标签类型: dos
磁盘标识符: 0x0009ecfa
- 块:操作系统的最小读写单位,通常指文件系统的逻辑块。(4096Bytes)
~# stat /dev/sda3
文件: "/dev/sda3"
大小: 0 块: 0 IO 块: 4096 块特殊文件
设备: 5h/5d Inode: 10432 硬链接: 1 设备类型: 8,3
权限: (0660/brw-rw----) Uid: ( 0/ root) Gid: ( 6/ disk)
最近访问: 2025-01-30 22:29:32.161000073 +0800
最近更改: 2025-01-30 22:29:32.161000073 +0800
最近改动: 2025-01-30 22:29:32.161000073 +0800
创建时间: -
4.4 实际读写硬盘涉及的因素
在实际读写硬盘的过程中,性能和行为会受到多个因素的影响,包括应用程序、操作系统和硬盘本身的特性。
1、应用程序
并发任务数
- 定义:应用程序同时发起的 I/O 请求数量。
- 影响:
- 并发任务数越高,磁盘的负载越大。
- 如果并发任务数超过磁盘的最大 IOPS 能力,会导致性能下降,甚至出现 I/O 队列积压。
提交方式
-
同步提交
- 定义:应用程序提交一个 I/O 请求后,会等待该请求完成后再提交下一个请求。
- 影响:
- 适合顺序读写操作。
- 性能较低,因为每次 I/O 操作都需要等待完成。
-
异步提交
- 定义:应用程序提交一个 I/O 请求后,不需要等待该请求完成,可以直接提交下一个请求。
- 影响:
- 适合高并发场景。
- 性能较高,因为可以充分利用磁盘的并行处理能力。
2、操作系统
- 调度算法:操作系统使用的调度算法会影响 I/O 请求的处理顺序和效率。
- 缓存机制:操作系统中的缓存机制(如 page cache)可以提高读写性能,但也可能导致数据一致性问题。
- 文件系统:不同的文件系统(如 ext4、xfs 等)有不同的性能特性和优化策略。
- 文件 IO 模式:选择合适的 IO 模式可以平衡性能和数据安全性。
3、硬盘本身
- 类型:不同类型的硬盘(如 HDD、SSD)有不同的性能特点。HDD 通常具有较高的寻道时间和旋转延迟,而 SSD 则具有较低的延迟和更高的 IOPS。
- 容量:硬盘的容量大小也会影响其性能。大容量硬盘可能需要更长的时间来寻道和旋转。
- 接口:硬盘接口(如 SATA、SAS、NVMe)也会影响其传输速率和延迟。
- 磁盘访问方式
-
随机访问
- 定义:每次 I/O 操作访问的逻辑块地址不连续。
- 影响:
- 性能较低,因为每次访问都需要经历平均延迟和平均寻道时间。
- 适合需要频繁随机读写的应用,如数据库查询。
-
顺序访问
- 定义:每次 I/O 操作访问的逻辑块地址连续。
- 影响:
- 性能较高,因为只需要经历一次平均延迟和平均寻道时间,后续的块可以连续读取。
- 适合需要大量顺序读写的应用,如文件传输。
-
5 CPU管理
更多了解可以看 linux系统性能分析
5.1 CPU利用率
1、监控方法
通过 /proc 文件系统:
- 进程的CPU时间(用户态 utime + 内核态 stime)存储在
/proc/[pid]/stat
中。 - 公式:CPU利用率 =
[(utime2−utime1)+(stime2−stime1)/时间间隔 * HZ] * 100%
- 其中 HZ 是内核时钟频率(通常为100或1000)
通过 top 或 htop:
- 直接查看进程的 %CPU 列,表示进程对单个CPU核心的占用率。
- 若进程使用率为 200%,表示占用了2个CPU核心。
通过 cAdvisor:
- 访问 http://localhost:8080 查看容器的CPU使用率。
curl http://localhost:8080/api/v1.3/docker/<container_id>
2、问题分析
CPU利用率低但应用慢:可能是进程因等待I/O(如磁盘或网络)进入睡眠状态(D/S),导致CPU空闲。
CPU利用率高但应用慢:可能是计算密集型任务导致CPU过载,需检查进程是否频繁占用CPU(如死循环)。
3、解决方案
- 定位高CPU进程
top -c # 按 CPU 使用率排序
pidstat -u 1 5 # 每1秒采样,共5次
- 优化CPU密集型任务:
- 调整线程/进程并发数。
- 使用性能分析工具(如 perf、py-spy)定位代码热点。
5.2 Load Average
Load Average 表示在某段时间内平均活跃的进程数。活跃的进程包括正在运行的进程、就绪的进程(可运行队列)以及不可中断睡眠的进程。
为何不可中断睡眠的进程被视为活跃?不可中断睡眠的进程虽然在等待 I/O 操作完成,但这些操作仍在使用系统资源