操作系统

一、进程与线程

进程是资源分配的最小单位,线程是 CPU 调度的最小单位。 一个进程拥有独立的内存空间(代码段、数据段、堆),进程之间互相隔离,一个崩溃不影响另一个。而一个进程内的多个线程共享该进程的内存,各自只独占栈和寄存器。

YARN 的 Container 本质是对进程资源的封装;Spark 的 Executor 是常驻进程、Task 以线程方式跑在里面(这正是它比 MapReduce 每个 Task 起一个 JVM 进程快的原因之一);跨节点通信则全部落到 Socket 上。

这个共享带来了两面性:

  • 好处是通信便宜。同进程内的线程直接读写同一块内存就能交换数据,不需要任何额外机制;进程间则必须借助专门的手段。
  • 代价是必须处理竞态。多个线程同时改同一个变量,结果取决于执行顺序,于是有了锁、信号量这一整套同步机制。

切换开销也差得远:进程切换要换页表、刷 TLB、换整个地址空间;线程切换只换寄存器和栈指针。

1、进程的状态流转

进程在生命周期里主要在三个状态间转换:

text
1 CPU
2
3 CPU
4
5 IO CPU

关键区别是就绪和阻塞都不占用 CPU,但性质完全不同:就绪态只差 CPU,调度到就能跑;阻塞态即使给它 CPU 也跑不了,必须等外部事件(比如磁盘读完)。所以调度器只从就绪队列里挑,阻塞的进程要先被唤醒变成就绪。

加上创建和终止两个瞬时状态,就是常说的五状态模型。

2、进程间通信

因为进程内存隔离,交换数据必须走操作系统提供的通道。常见几种,按「数据量」和「是否需要同步」来选:

方式特点
管道半双工、亲缘进程间,简单但只能单向流
消息队列有格式的消息,可异步,内核维护
共享内存最快,直接映射同一块物理内存,无需拷贝;但必须自己配合信号量做同步
信号量本身不传数据,是同步原语,常配合共享内存使用
Socket唯一能跨机器的,分布式系统的基础

共享内存快是因为它省掉了「用户态 → 内核态 → 用户态」的两次数据拷贝,其他方式都逃不掉这一步。

3、死锁
  • 定义:指多个线程因竞争共享资源而造成的一种僵局,若无外力作用,这些线程都将永远不能再 向前推进。
  • 产生原因:竞争系统资源、进程推进顺序非法。
  • 产生死锁必要条件:互斥条件、请求和保持条件、不剥夺条件、循环等待条件。
  • 解决方法:预防、避免(银行家算法)、检测(资源分配表)、解除(剥夺资源、撤销进程)。
二、乐观锁和悲观锁
  • 悲观锁基于悲观的假设,认为共享资源在每次访问时都会发生冲突,因此在每次操作时都会加锁。这种锁机制会导致其他线程阻塞,直到锁被释放。Java 中的 synchronizedReentrantLock 是悲观锁的典型实现方式。虽然悲观锁能有效避免数据竞争,但在高并发场景下会导致线程阻塞、上下文切换频繁,从而影响系统性能,并且还可能引发死锁问题。
  • 乐观锁基于乐观的假设,认为共享资源在每次访问时不会发生冲突,因此无须加锁,只需在提交修改时验证数据是否被其他线程修改。Java 中的 AtomicIntegerLongAdder 等类通过 CAS(Compare-And-Swap)算法实现了乐观锁。乐观锁避免了线程阻塞和死锁问题,在读多写少的场景中性能优越。但在写操作频繁的情况下,可能会导致大量重试和失败,从而影响性能。
  • 乐观锁主要通过版本号机制或 CAS 算法实现。版本号机制通过比较版本号确保数据一致性,而 CAS 通过硬件指令实现原子操作,直接比较和交换变量值。
1、乐观锁

乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。

版本号机制

一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

举一个简单的例子:假设数据库中帐户信息表中有一个 version 字段,当前值为 1;而当前帐户余额字段(balance)为 $100。

  1. 操作员 A 此时将其读出(version=1),并从其帐户余额中扣除 $50($100-$50)。
  2. 在操作员 A 操作的过程中,操作员 B 也读入此用户信息(version=1),并从其帐户余额中扣除 $20($100-$20)。
  3. 操作员 A 完成了修改工作,将数据版本号(version=1),连同帐户扣除后余额(balance=$50),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2。
  4. 操作员 B 完成了操作,也将版本号(version=1)试图向数据库提交数据(balance=$80),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1,而数据库记录当前版本为 2,不满足 “ 提交版本必须等于当前版本才能执行更新 ” 的乐观锁策略,因此,操作员 B 的提交被驳回。

这样就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。

CAS算法

CAS 的全称是 Compare And Swap(比较与交换),用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。

CAS 涉及到三个操作数:

  • V:要更新的变量值(Var)
  • E:预期值(Expected)
  • N:拟写入的新值(New)

当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。

举一个简单的例子:线程 A 要修改变量 i 的值为 6,i 原值为 1(V = 1,E=1,N=6,假设不存在 ABA 问题)。

  1. i 与 1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6。
  2. i 与 1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。

当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。

2、悲观锁

悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程

三、常见的Linux命令
1.怎么操作文件和目录
  • ls‌:列出目录内容,加 -l 显示详细信息,-a 显示隐藏文件 。
  • cd‌:切换目录,cd .. 返回上一级,cd ~ 回到家目录 。
  • pwd‌:显示当前所在的绝对路径 。
  • cp/mv/rm‌:分别用于复制、移动/重命名、删除文件,rm -rf 极度危险需慎用 。
  • mkdir/touch‌:创建目录或空文件 。‌‌‌
2.怎么查看文件内容
  • cat‌:一次性输出小文件全部内容 。
  • less/more‌:分页查看大文件,支持上下翻页和搜索 。
  • head/tail‌:查看文件开头或结尾,默认显示 10 行,tail -f 可实时跟踪日志 。
  • grep‌:在文件中搜索关键词,支持递归搜索目录 。‌‌‌
3.怎么查看系统状态
  • ps/top‌:查看运行中的进程,top 可动态监控资源占用 。
  • df/du‌:查看磁盘空间占用和目录大小,常用 -h 以人类可读方式显示 。
  • free‌:查看内存使用情况 。
  • netstat/ss‌:查看网络连接和端口占用情况 。‌‌‌

评论 (0)

登录后参与评论。

还没有评论,来做第一个。

登录后可以选中正文添加批注(仅自己可见)。

操作系统