操作系统
线程&进程&协程
线程与进程的比较如下:
- 进程是资源分配的单位,线程是 CPU 调度的单位;
- 进程拥有完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈;
- 线程同样具有就绪、阻塞、执行三种基本状态,具有状态之间的转换关系;
- 线程能减少并发执行的时间和空间开销
对于,线程相比进程能减少开销,体现在:
- 线程的创建时间比进程快,因为进程在创建的过程中,还需要资源管理信息,比如内存管理信息、文件管理信息,而线程在创建的过程中,不会涉及这些资源管理信息,而是共享它们
- 线程的终止时间比进程快,因为线程释放的资源相比进程少很多
- 同一个进程内的线程切换比进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同一个进程的线程都具有同一个页表,那么在切换的时候不需要切换页表。而对于进程之间的切换,切换的时候要把页表给切换掉,而页表的切换过程开销是比较大的
- 由于同一进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更高了; 所以,线程比进程不管是时间效率,还是空间效率都要高
协程的切换效率线程更高:1. 不需要保存栈状态 2. 只在用户态完成,不需要系统调用
协程:用户态的轻量级线程,由程序员在代码中显式控制其调度,适用于I/O密集型任务
进程私有:代码段、数据段、堆、栈,PCB,公有:共享内存
线程之间私有和共享的资源 私有:线程栈,寄存器,程序计数器 共享:堆,地址空间,全局变量,静态变量
多线程
线程是CPU调度的基本单位,多线程可以提升CPU的利用率,更有效利用多核CPU,每个线程会有新建、就绪、运行、阻塞、终止状态,多个线程访问共享资源的时候,可能会出死锁、饥饿的现象。
使用线程池来管理多线程:
- 减少线程创建和销毁的开销
- 控制并发线程数量
- 提供更灵活的任务调度
僵尸进程&孤儿进程
僵尸进程:是指已经终止(exit
),但其父进程尚未调用 wait()
或 waitpid()
来回收其资源(如退出状态码)的进程,它仍然占用 进程表(Process Table) 中的一个条目,但不再占用系统资源(如内存、CPU)。
孤儿进程:父进程已经终止,但是子进程还在运行,由init(PID=1)接管
大端序和小端序
小端序:低位低地址
区别在于数据的高位字节和低位字节在内存中的存储顺序,需要注意跨平台数据交换的时候,可能要进行交换。如果需要逐位运算,或者需要到从个位数开始运算,都是小端序占优势。反之,如果运算只涉及到高位,或者数据的可读性比较重要,则是大端序占优势。
系统调用
系统调用(System Call) 是用户程序请求操作系统内核提供服务的一种机制。由于操作系统内核负责管理硬件资源和提供核心功能(如文件读写、进程管理、网络通信等),而用户程序通常运行在用户模式下,无法直接访问这些资源。
线程不安全的原因
- 操作系统的随机调度/抢占式执行
- 多个线程修改同一个变量
- 有些修改操作,不是原子的
- 内存改了,但是在优化的背景下,但是优化后读不到
- 指令重排序,也是编译器,操作系统等的优化,调整了代码的执行顺序
进程之间的通信方式
管道 (1)有名管道:一种半双工的通信方式,它允许无亲缘关系进程间的通信 ①优点:可以实现任意关系的进程间的通信 ②缺点:a、长期存于系统中,使用不当容易出错;b、缓冲区有限 (2)无名管道:一种半双工的通信方式,只能在具有亲缘关系的进程间使用(父子进程) ①优点:简单方便 ②缺点:a、局限于单向通信;b、只能创建在它的进程以及其有亲缘关系的进程之间;c、缓冲区有限
信号量:一个计数器,可以用来控制多个线程对共享资源的访问,PV操作,通过信号量实现临界区的互斥
P操作:申请资源,进入临界区,s = s - 1,如果s ≥ 0,继续执行,如果s < 0,放入等待队列(资源不足)
V操作:释放资源,退出临界区,s = s + 1,如果s ≤ 0,从等待队列中唤醒一个阻塞的进程
条件变量:一种比较复杂的通信方式,用于通知接收进程某个事件已经发生
消息队列:是消息的链表,存放在内核中并由消息队列标识符标识,优点:可以实现任意进程间的通信,并通过系统调用函数来实现消息发送和接收之间的同步,无需考虑同步问题,方便;缺点:信息的复制需要额外消耗 CPU 的时间,不适宜于信息量大或操作频繁的场合
共享内存:映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问,无须复制,快捷,信息量大,但是涉及进程间的读写操作的同步问题
套接字:可用于不同计算机间的进程通信
线程之间的通信方式
锁机制 包括互斥锁、读写锁、自旋锁、条件变量
- 互斥锁:提供了以排他方式防止数据结构被并发修改的方法。
- 读写锁:允许多个线程同时读共享数据,而对写操作是互斥的。
- 自旋锁与互斥锁类似,都是为了保护共享资源。互斥锁是当资源被占用,申请者进入睡眠状态;而自旋锁则循环检测保持者是否已经释放锁。
- 条件变量:可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
条件变量,通常与 互斥锁(Mutex) 配合使用,用于让线程等待某个条件成立后再继续执行。
悲观锁&乐观锁
概念
悲观锁:假设并发冲突一定会发生,因此在访问共享资源前先加锁,防止其他事务修改数据
乐观锁:假设并发冲突较少发生,不加锁,而是在提交时检查数据是否被修改
场景
悲观锁:银行转账、秒杀系统
乐观锁:商品页(读多写少)
GIL锁
同一时刻只有一个线程执行 Python 字节码,是一个全局互斥锁;
可以用多个进程绕过,每个进程都有一个独立的GIL锁
引用计数
引用计数是一种内存管理技术,用于跟踪对象被引用的次数。当对象的引用计数降为 0 时,表示该对象不再被任何变量引用,可以被安全地回收。
核心思想
- 每个对象维护一个 引用计数器,记录有多少变量指向它
- 当变量引用该对象时,计数器 +1
- 当变量不再引用该对象时,计数器 -1
- 如果计数器变为 0,则释放该对象占用的内存
什么是死锁,解决方法
死锁:死锁是指两个或两个以上的进程(线程)在运行过程中因争夺资源而造成的一种僵局,若无外力作用,这些进程(线程)都将无法向前推进。详见: 死锁四个必要条件及死锁的预防、检测、避免、解除
死锁产生的四个必要条件
互斥:一次只有一个进程可使用一个资源,其他进程不能访问已分配给其他进程的资源。
占有且等待:当一个进程在等待分配得到其他资源时,其继续占有已分配得到的资源。
非抢占:不能强行抢占进程中已占有的资源
循环等待:存在一个封闭的进程链,使得每个资源至少占有此链中下一个进程所需要的一个资源
死锁的处理四种方法
- 死锁预防:通过确保死锁的一个必要条件不会满足,保证不会发生死锁
- 死锁检测:允许死锁的发生,但是可以通过系统设置的检测结构及时的检测出死锁的发生,采取一些措施,将死锁清除掉
- 死锁避免:在资源分配过程中,使用某种方法避免系统进入不安全的状态,从而避免发生死锁
- 死锁解除:与死锁检测相配套的一种措施。当检测到系统中已发生死锁,需将进程从死锁状态中解脱出来
用户态和内核态
系统调用:
- 用户程序调用
syscall
指令(x86)或svc
(ARM)。 - CPU 切换到内核态,跳转到内核的系统调用入口(如
entry_SYSCALL_64
)。 - 内核执行对应的系统调用处理函数(如
sys_read()
)。 - 返回用户态时,通过
sysret
或iret
恢复用户程序。
硬件中断、异常,也会发生用户态和内核态的切换
数据从内存写到磁盘
- 数据先写入内核缓冲区,page cache,虚拟内存的一部分,以page为单位管理,维护需要写回磁盘的标记
- 文件系统负责组织数据到磁盘,元数据更新+日志(先记录,再写入,防止崩溃)
- 通过DMA直接内存访问
虚拟内存
简化内存管理,不同进程之间的隔离性
逻辑地址(虚拟地址):由CPU生成的地址,分为页号和页内偏移量。
物理地址:实际RAM中的地址
- 页表(Page Table):存储虚拟页到物理页的映射,由MMU硬件访问。
- TLB(快表):缓存常用页表项,加速地址转换。
- 多级页表:解决大地址空间页表占用过多内存的问题(如x86-64的4级页表)。
- 页框(Page Frame):物理内存中的固定大小块,用于存放页
页面置换算法
当物理内存不足时,需将部分页换出到磁盘(交换空间/页面文件),常用算法:
- FIFO(先进先出):简单但可能引发Belady异常(增加页框数反而增加缺页)。
- LRU(最近最少使用):近似最优,但实现成本高(需硬件支持或近似算法如Clock)。
- OPT(最佳置换):理论最优,但无法实际实现(需预知未来访问)。
- Clock算法:改进的近似LRU,通过引用位实现
任务调度
- 先来先服务
- 短作业优先(非抢占式)
- 短作业优先(抢占式,可能造成长任务被无线延迟)
- 优先级调度
- 时间片轮转
生产者消费者
semaphore mutex=1; //临界区互斥信号量
semaphore empty=n; //空闲缓冲区
semaphore full=0; //缓冲区初始化为空
producer ()//生产者进程
{
while(1)
{
produce an item in nextp; //生产数据
P(empty); //获取空缓冲区单元
P(mutex); //进入临界区.
add nextp to buffer; //将数据放入缓冲区
V(mutex); //离开临界区,释放互斥信号量
V(full); //满缓冲区数加1
}
}
consumer ()//消费者进程
{
while(1)
{
P(full); //获取满缓冲区单元
P(mutex); // 进入临界区
remove an item from buffer; //从缓冲区中取出数据
V (mutex); //离开临界区,释放互斥信号量
V (empty) ; //空缓冲区数加1
consume the item; //消费数据
}
}