计算机系统 进程 概念 进程是程序在执行过程中的一个实例。 静态代码文件经过编译后生成二进制可执行文件,当我们运行这个可以执行文件后,CPU会执行程序中的每一条指令,这个运行中的程序,就称为进程 状态 7 种状态 创建 进程被创建时的状态 就绪 可运行,由于其他进程处于运行状态而暂时停止运行 运行 当前进程占用 CPU 阻塞 该进程正在等待某一事件发生(如等待输入/输出操作的完成)而暂时停止运 结束 进程从系统中消失时的状态 挂起(两种挂起状态) 描述进程没有占用实际的物理内存空间的情况,被换出到硬盘,这个状态就是挂起状态 就绪挂起 进程在外存(硬盘),但只要进入内存,即刻立刻运行 阻塞挂起 进程在外存(硬盘)并等待某个事件的出现; 3中情况会被挂起 系统行为 进程所使用的内存空间不在物理内存 用户行为 通过 sleep 让进程间歇性挂起,其工作原理是设置一个定时器,到期后唤醒进程 用户希望挂起一个程序的执行,比如在 Linux 中用 Ctrl+Z 挂起进程; 状态转换 状态流转图
7种状态转换
NULL -> 创建状态
一个新进程被创建时的第一个状态
创建状态 -> 就绪状态
当进程被创建完成并初始化后,一切就绪准备运行时,变为就绪状态,这个过程是很快的;
就绪态 -> 运行状态
处于就绪状态的进程被操作系统的进程调度器选中后,就分配给 CPU 正式运行该进程;
运行状态 -> 结束状态
当进程已经运行完成或出错时,会被操作系统作结束状态处理;
运行状态 -> 就绪状态
处于运行状态的进程在运行过程中,由于分配给它的运行时间片用完,操作系统会把该进程变为就绪态,接着从就绪态选中另外一个进程运行;
运行状态 -> 阻塞状态
当进程请求某个事件且必须等待时,例如请求 I/O 事件
阻塞状态 -> 就绪状态
当进程要等待的事件完成时,它从阻塞状态变到就绪状态
控制结构
进程控制块(process control block,PCB)
进程存在的唯一标识 进程描述信息 进程标识符:标识各个进程,每个进程都有一个并且唯一的标识符 用户标识符:进程归属的用户,用户标识符主要为共享和保护服务; 进程控制和管理信息 进程当前状态,如 new、ready、running、waiting 或 blocked 等 进程优先级:进程抢占 CPU 时的优先级 资源分配清单 虚拟内存地址空间、文件资源和 I/O设备信息 CPU 相关信息 CPU 中各个寄存器和计数器的值,当进程被切换时,CPU 的状态信息都会被保存在相应的 PCB 中,以便进程重新执行时,能从断点处继续执行。 PCB的连接方式 单链表形式 把具有相同状态的进程链在一起,组成各种队列
进程的控制
4个控制过程
创建进程
终止进程
3 种方式
正常结束
异常结束
外接干扰,如:kill掉
当子进程被终止时,其在父进程处继承的资源应当还给父进程。而当父进程被终止时,该父进程的子进程就变为孤儿进程,会被 1 号进程收养,并由 1 号进程对它们完成状态收集工作
1 号进程也称为 init 进程,是系统启动时最早创建的进程,负责初始化系统的各个组件和服务
在现代 Linux 系统中,init 进程已经被更先进的 init 系统(如 Systemd、Upstart)取代 阻塞进程 当进程需要等待某一事件完成时,它可以调用阻塞语句把自己阻塞等待。 一旦被阻塞等待,它只能由另一个进程唤醒。 唤醒进程 进程由「运行」转变为「阻塞」状态是由于进程必须等待某一事件的完成,所以处于阻塞状态的进程是绝对不可能叫醒自己的。 如果某进程正在等待 I/O 事件,需由别的进程发消息给它,则只有当该进程所期待的事件出现时,才由发现者进程用唤醒语句叫醒它。 进程上下文切换 上下文切换是操作系统在多任务处理环境中,将 CPU 从一个进程切换到另一个进程的过程 切换过程 保存当前进程的上下文:操作系统保存当前进程的 CPU 寄存器,程序状态等关键信息 选择下一个进程:调度程序选择下一个要执行的进程 恢复上一个进程的上下文 切换到下一个进程 进程上下文资源 用户空间资源 虚拟内存、堆栈、全局变量 内核空间资源 内核堆栈、寄存器等 5 个切换场景 CPU分配给进程的时间片不足以让进程运行完,这个时候进程就从运行状态变为就绪状态,系统从就绪队列选择另外一个进程运行 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行; 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度; 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行; 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序; 进程间通信6 种方式 管道 所谓的管道,就是内核里面的一串缓存。 从管道的一端写入的数据,实际上是缓存在内核中的,另一端读取,也就是从内核中读取这段数据,遵循先进先出原则 生命周期 随进程的创建而建立,随进程的结束而销毁 两种管道 匿名管道 「|」表示 通信范围是存在父子关系的进程 命名管道(也叫FIFO) 命名管道创建:mkfifo myPipe 可以在不相关的进程间也能相互通信 缺点 通信方式效率低,不适合进程间频繁地交换数据 传输的数据是无格式的流且大小受限 支持 lseek 之类的文件定位操作 消息队列 消息队列是保存在内核中的消息链表,发送的数据,会分成一个一个独立的数据单元 如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。 生命周期 消息队列生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在 优点 适合进程间频繁地交换数据 缺点 通信不及时 数据大小有限制,不适合比较大数据的传输 存在用户态与内核态之间的数据拷贝开销 因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另 共享内存 共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中 优点 不需要做数据拷贝 通信速度快 缺点 存在多进程竞争共享资源,造成数据错乱的风险 信号量 信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。 2 种原子操作 P操作 用在进入共享资源之前,这个操作会把信号量减去 1 相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待; 相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行 V操作 用在离开共享资源之后,这个操作会把信号量加上 1 相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行 相加后如果信号量 > 0,则表明当前没有阻塞中的进程; 2 种初始值 信号量初始化为 1,就代表着是互斥信号量 可保证任意时刻只有一个线程访问共享资源 示例:进程 A先执行 P操作,进程 B后执行 P操作 信号量初始化为 0,就代表着是同步信号量 可保证线程 A 应在线程 B 之前执行 示例:进程 B比进程 A先执行 优点 避免多进程竞争共享资源,确保任意时刻只有一个进程访问共享资源 信号(异常情况下的工作模式) 对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。 信号是进程间通信机制中唯一的异步通信机制 3 种处理方式 执行默认操作 捕捉信号,自定义处理 忽略信号 有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,它们用于在任何时候中断或结束某一进程 Socket(不同主机上进程之间的通信) Socket 实际上不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信 3 种通信方式 基于 TCP协议的通信方式 基于 UDP协议的通信方式 本地进程间通信方式 僵尸进程、孤儿进程 僵尸进程 僵尸进程是已完成且处于终止状态,但在进程表中却仍然存在的进程 如果子进程退出,而父进程并没有调用 wait() 或 waitpid(),那么子进程的进程描述符仍然保存在系统中 孤儿进程 一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程 孤儿进程将被 init 进程回收,不会对系统造成危害 用户态和内核态 用户态 是操作系统为应用程序(如用户运行的进程)分配的内存区域,用户空间中的进程不能直接访问硬件或内核数据结构,只能通过系统调用与内核通信 内核态 是操作系统内核代码及其运行时数据结构所在的内存区域,拥有对系统所有资源的完全访问权限,如进程管理、内存管理、文件系统、网络堆栈等 用户态和内核态的切换 当应用程序执行系统调用时(如文件操作(如 open、read、write)、进程控制(如 fork、exec)、内存管理(如 mmap)),CPU 将从用户态切换到内核态,进入内核空间执行相应的内核代码,然后再切换回用户态。 阻塞I/O 和 非阻塞I/O 同步I/O 和 异步I/O 阻塞I/O 当⽤户程序执⾏ read ,线程会被阻塞,⼀直等到内核数据准备好,并把数据从内核缓冲区拷⻉到应⽤程序的缓冲区中,当拷⻉过程完成, read 才会返回。
非阻塞I/O
⾮阻塞的 read 请求在数据未准备好的情况下⽴即返回,可以继续往下执⾏,此时应⽤程序不断轮询内核,直到数据准备好,内核将数据拷⻉到应⽤程序缓冲区, read 调⽤才可以获取到结果
非阻塞I/O 应用程序要一直轮询,这个过程没法干其它事情,所以引入了I/O **多路复⽤**技术:
当内核数据准备好时,以事件通知应⽤程序进⾏操作 同步I/O 无论是阻塞I/O 还是 非阻塞I/O 都是同步调⽤
因为它们在 read 调⽤时,内核将数据从内核空间拷⻉到应⽤程序空间,过程都是需要等待的,也就是说这个过程是同步的, 异步I/O 真正的异步 I/O 是内核数据准备好和数据从内核态拷⻉到⽤户态这两个过程都不⽤等待 发起 aio_read 之后,就⽴即返回,内核⾃动将数据从内核空间拷⻉到应⽤程序空间,这个拷⻉过程同样是异步的,内核⾃动完成的,和前⾯的同步操作不⼀样,应⽤程序并不需要主动发起拷⻉动作
内核数据拷贝完成后,会主动通知程序进程
线程
概念
线程是进程当中的一条执行流程
优点
一个进程中可以同时存在多个线程
各个线程之间可以并发执行
同个进程中的各个线程之间可以共享地址空间和文件等资源
缺点
当进程中的一个线程崩溃时,会导致其所属进程的所有线程崩溃(这里是针对 C/C++ 语言,Java语言中的线程奔溃不会造成进程崩溃
线程切换
两个线程不是属于同一个进程
切换的过程就跟进程上下文切换一样
两个线程是属于同一个进程
因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据栈、寄存器等不共享的数据;
3 种实现
用户线程
在用户空间实现的线程,由用户态的线程库来完成线程的创建和管理;
缺点
由于内核不知道用户线程的存在,用户线程可能无法有效地利用多核处理器资源
如果一个线程阻塞,整个进程的所有线程都可能受阻
优点
创建和销毁用户线程的开销较低,因为不需要操作系统内核的介入
用户线程的调度灵活,适合某些高效的应用场景
内核线程
内核线程是由操作系统内核管理的线程,内核负责调度和执行
优点
由于内核线程与操作系统直接交互,可以有效利用多核CPU
线程的阻塞不会影响到其他线程,因为每个内核线程都是独立调度的
缺点
创建和销毁内核线程的开销较大,因为涉及到操作系统内核的参与
轻量级进程 (Light-weight process,LWP)
是内核支持的用户线程,一个进程可有一个或多个 LWP,每个 LWP 是跟内核线程一对一映射的,也就是 LWP 都是由一个内核线程支持,而且 LWP 是由内核管理并像普通进程一样被调度。
线程间通信方式
同个进程下的线程之间都是共享进程的资源,只要是共享变量都可以做到线程间通信,比如全局变量
对于线程间关注的不是通信方式,而是关注多线程竞争共享资源的问题,信号量也同样可以在线程间实现互斥与同步
信号量
进程和线程的(区别)比较
进程是资源(包括内存、打开的文件等)分配的单位,线程是 CPU 调度的单位;
进程拥有一个完整的资源平台,有自己独立的地址空间、全局变量、堆栈、和文件描述符等资源,
而线程共享进程的地址空间和资源,只独享必不可少的资源,如寄存器和栈; 进程是一个正在执行的程序的实例。每个进程有自己独立的地址空间、全局变量、堆栈、和文件描述符等资源。
线程是进程中的一个执行单元。一个进程可以包含多个线程,它们共享进程的地址空间和资源 线程能减少并发执行的时间和空间开销 线程相比进程能减少开销 线程的创建时间比进程快 因为进程在创建的过程中,还需要资源管理信息,比如内存管理信息、文件管理信息,而线程在创建的过程中,不会涉及这些资源管理信息,而是共享它们; 线程的终止时间比进程快 因为线程释放的资源相比进程少很多 同一个进程内的线程切换比进程切换快 因为线程具有相同的地址空间(虚拟内存共享),这意味着同一个进程的线程都具有同一个页表,那么在切换的时候不需要切换页表。而对于进程之间的切换,切换的时候要把页表给切换掉,而页表的切换过程开销是比较大的 线程之间的数据交互效率更高 由于同一进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了 一个进程崩溃不会影响其他进程 一个线程崩溃可能导致整个进程崩溃 协程 特点 协程是一种用户态的轻量级线程,协程的调度完全由用户控制 一个线程可以多个协程 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态 协程比线程有哪些优势 轻量级调度 协程是用户级的调度,由程序自身管理,无需操作系统参与,因此切换速度极快 线程切换涉及内核级别的上下文切换,开销更大 创建资源开销 协程的创建和销毁的开销很小,通常一个协程只有2KB左右 线程的创建和销毁需要更多的系统资源 无锁并发 由于协程通常运行在同一个线程中,它们避免了线程间竞争的开销(如锁、信号量等) 更高的控制力 开发者对协程的调度有完全的控制,可以根据具体需求设计出更加高效的调度策略 线程调度则由操作系统管理 高并发性能 每个请求一个线程,如果有成千上万个并发请求,会导致线程数剧增,系统资源(如CPU、内存)消耗迅速增大,同时大量的上下文切换带来性能损失 每个请求一个协程,协程在等待I/O操作时主动让出控制权给其他协程,无需频繁的上下文切换和资源分配。高并发处理的性能会显著提升,资源消耗也较低。 锁 死锁 概念 当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,那么这两个互斥锁应用不当的时候,可能会造成两个线程都在等待对方释放锁,在没有外力的作用下,这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁 产生死锁的 4 个条件 互斥条件 指多个线程不能同时使用同一个资源
持有并等待
指当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1。
不可剥夺条件
指当线程已经持有了资源 ,在自己使用完之前不能被其他线程获取,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。
环路等待条件
指在死锁发生的时候,两个线程获取资源的顺序构成了环形链。比如,线程 A 已经持有资源 2,而想请求资源 1, 线程 B 已经获取了资源 1,而想请求资源 2,这就形成资源请求等待的环形图
如何避免死锁
最常见的并且可行的就是使用资源有序分配法,来破环环路等待条件。
线程 A 和 线程 B 获取资源的顺序要一样,当线程 A 是先尝试获取资源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。
饥饿锁、活锁
饥饿锁
饥饿锁是指在多线程环境中,由于线程调度策略或资源分配策略的不当,导致某些线程长时间无法获得所需资源,从而无法执行的状态
活锁
活锁是指在多线程或分布式系统中,多个线程或进程在尝试执行任务时,由于不断地进行条件检查和调整自己的状态以避免与其他线程冲突,导致没有一个线程能够成功执行任务的状态
区别
活锁与死锁不同,线程是处于活跃状态的,但无法取得进展
并发、并行
并发
并发是指在同一个时间段内处理多个任务。
并发通常在单个处理器上实现,例如多线程应用程序,在同一时刻执行多个任务,但不一定同时。 并行 并行是指同时执行多个任务或活动,每个任务使用单独的处理器。 并行通常需要多个处理器、多核心或多台计算机 硬链接和软链接 硬链接 具有相同inode节点号的多个文件互为硬链接文件 删除硬链接文件或者删除源文件任意之一,文件实体并未被删除
只有删除了源文件和所有对应的硬链接文件,文件实体才会被删除
当硬链接数为0时文件就被删除(可以用rm删除)。注意:如果有进程正在调用,则无法删除或者即使文件名被删除但空间不会释放。 如果有进程正在调用,则无法删除或者即使文件名被删除但空间不会释放 硬链接文件是文件的另一个入口,可以通过给文件设置硬链接文件来防止重要文件被误删 创建硬链接命令 ln 源文件 硬链接文件 软链接 软链接类似windows系统的快捷方式 软链接里面存放的是源文件的路径,指向源文件 删除源文件,软链接依然存在,但无法访问源文件内容 软链接和源文件是不同的文件,文件类型也不同,inode号也不同 创建软链接命令 ln -s 源文件 软链接文件 调度 概念 选择一个进程运行这一功能是在操作系统中完成的,通常称为调度程序(scheduler)。 这里的进程指只有主线程的进程,所以调度主线程就等于调度了整个进程。
那为什么干脆不直接取名线程调度?主要是操作系统相关书籍,都是用进程调度这个名字,所以我也沿用了这个名字。 调度时机 从就绪态 -> 运行态 当进程被创建时,会进入到就绪队列,操作系统会从就绪队列选择一个进程运行 从运行态 -> 阻塞态 当进程发生 I/O 事件而阻塞时,操作系统必须选择另外一个进程运行; 从运行态 -> 结束态 当进程退出结束后,操作系统得从就绪队列选择另外一个进程运行; 非抢占式调度和抢占式调度 非抢占式调度 选一个进程,然后让该进程运行直到被阻塞,或者直到该进程退出,才会调用另外一个进程,也就是说不会理时钟中断这个事情。 抢占式调度 挑选一个进程,然后让该进程只运行某段时间,如果在该时段结束时,该进程仍然在运行时,则会把它挂起,接着调度程序从就绪队列挑选另外一个进程 这种抢占式调度处理,需要在时间间隔的末端发生时钟中断,以便把 CPU 控制返回给调度程序进行调度,也就是常说的时间片机制 5 种调度原则 CPU 利用率 调度程序应确保 CPU 是始终匆忙的状态,这可提高 CPU 的利用率; 系统吞吐量 吞吐量表示的是单位时间内 CPU 完成进程的数量,长作业的进程会占用较长的 CPU 资源,因此会降低吞吐量,相反,短作业的进程会提升系统吞吐量 周转时间 周转时间是进程运行+阻塞时间+等待时间的总和,一个进程的周转时间越小越好; 等待时间 这个等待时间不是阻塞状态的时间,而是进程处于就绪队列的时间,等待的时间越长,用户越不满意; 响应时间 用户提交请求到系统第一次产生响应所花费的时间,在交互式系统中,响应时间是衡量调度算法好坏的主要标准。 6 种调度算法(单核 CPU 系统) 先来先服务(First Come First Serve, FCFS)算法 每次从就绪队列选择最先进入队列的进程,然后一直运行,直到进程退出或被阻塞,才会继续从队列中选择第一个进程接着运行。 当一个长作业先运行了,那么后面的短作业等待的时间就会很长,不利于短作业。 最短作业优先(Shortest Job First, SJF)调度算法 有助提高系统吞吐量 但就绪队列有非常多的短作业,那么就会使得长作业不断的往后推,周转时间变长,致使长作业长期不会被运行。 高响应比优先 (Highest Response Ratio Next, HRRN)调度算法(理想型调度算法,实现不了) 每次进行进程调度时,先计算「响应比优先级」,然后把「响应比优先级」最高的进程投入运行 计算公式: 时间片轮转(Round Robin, RR)调度算法(最公平、使用最广) 每个进程被分配一个时间段,称为时间片(Quantum),即允许该进程在该时间段中运行。 时间片设为 20ms~50ms 通常是一个比较合理的折中值
最高优先级(Highest Priority First,HPF)调度算法
从就绪队列中选择最高优先级的进程进行运行
优先级
静态优先级
创建进程时候,就已经确定了优先级了
动态优先级
根据进程的动态变化调整优先级,比如如果进程运行时间增加,则降低其优先级,如果进程等待时间(就绪队列的等待时间)增加,则升高其优先级
处理方式
非抢占式:当就绪队列中出现优先级高的进程,运行完当前进程,再选择优先级高的进程。
抢占式:当就绪队列中出现优先级高的进程,当前进程挂起,调度优先级高的进程运行。
多级反馈队列(Multilevel Feedback Queue)调度算法
概念
「多级」表示有多个队列,每个队列优先级从高到低,同时优先级越高时间片越短。
「反馈」表示如果有新的进程加入优先级高的队列时,立刻停止当前正在运行的进程,转而去运行优先级高的队列;
工作流程
设置了多个队列,赋予每个队列不同的优先级,每个队列优先级从高到低,同时优先级越高时间片越短;
新的进程会被放入到第一级队列的末尾,按先来先服务的原则排队等待被调度,如果在第一级队列规定的时间片没运行完成,则将其转入到第二级队列的末尾,以此类推,直至完成
当较高优先级的队列为空,才调度较低优先级的队列中的进程运行。如果进程运行时,有新进程进入较高优先级的队列,则停止当前运行的进程并将其移入到原队列末尾,接着让较高优先级的进程运行;
问题
进程写文件时,进程发生了崩溃,已写入的数据会丢失吗?
因为进程在执行 write (使用缓冲 IO)系统调用的时候,实际上是将文件数据写到了内核的 page cache,它是文件系统中用于缓存文件数据的缓冲,所以即使进程崩溃了,文件数据还是保留在内核的 page cache,我们读数据的时候,也是从内核的 page cache 读取,因此还是依然读的进程崩溃前写入的数据。
内核会找个合适的时机,将 page cache 中的数据持久化到磁盘。但是如果 page cache 里的文件数据,在持久化到磁盘化到磁盘之前,系统发生了崩溃,那这部分数据就会丢失了。 Nginx 七种负载均衡策略 轮询(Round Robin) 默认的负载均衡策略。在这种策略中,Nginx会将请求按顺序分发给每个后端服务器。如果所有的服务器都正常运行,那么请求将均匀地分发到每个服务器 最少连接(Least Connections) Nginx会将新的请求发送到当前活跃连接数最少的服务器。这种策略特别适合处理工作负载不均衡的情况,即某些请求可能需要处理更长的时间。 IP哈希(IP Hash) Nginx会根据客户端的IP地址计算哈希值,并根据这个哈希值将请求发送到特定的服务器。这种策略可以保证来自同一客户端的请求总是发送到同一服务器,从而可以处理需要维持会话状态的应用。 权重(Weight) 为每个服务器指定一个权重,权重越高的服务器会接收到更多的请求。权重可以根据服务器的性能和负载能力来设置。 URL哈希(URL Hash) 通过哈希请求的URL确定请求转发的服务器,可以保证相同URL的请求总是转发到同一后端服务器,适用于缓存服务器的场景 公平(Fair) 根据后端服务器的响应时间来分配请求,响应时间短的优先分配 通用哈希(Generic Hash) 通过哈希一些特定的变量(如请求头、URI等)来确定请求转发的服务器