文章目录
- 锁
- 自旋锁
- 互斥锁
- 悲观锁和乐观锁
- 内存管理
- 物理/虚拟内存
- 页表
- 段表
- 虚拟内存布局
- 写时复制copy on write
- brk,mmap
- 页面置换算法
- 中断
- 中断分类
- 中断流程
- 网络I/O
- I/O模型
- 服务器处理并发请求
锁
自旋锁
自旋锁是一种基于忙等待(Busy-Waiting)的同步机制。
通过 CPU 提供的 CAS 函数),完成加锁解锁操作:
第一步:查看锁的状态,为空,则执行第二步
第二步:将锁设置为当前线程持有
这两步是原子指令,要么一次性执行完,要么都不执行。
当线程尝试获取锁失败时,它会循环检查锁的状态(“自旋”),直到它拿到锁。
等待时间较短的情况下效率较高,因为避免了线程上下文切换的开销。但长时间等待会导致CPU资源的浪费。
适用于多核系统,且临界区代码执行时间非常短的场景。
互斥锁
在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁,确保同一时间只有一个线程访问共享资源。
自旋锁和互斥锁的区别
工作机制:自旋锁在获取不到锁时会一直循环检查,而互斥锁会让线程进入睡眠(挂起)状态,调度其他线程运行。
适用场景:自旋锁适合保护执行时间极短的代码段,自旋的代价可能小于线程切换的代价。互斥锁适合保护执行时间较长的代码段
性能影响:自旋锁无上下文切换开销,但一直占用CPU,互斥锁挂起唤醒线程产生下上文切换开销,但等待期间CPU执行其他任务,资源利用率更高.
实现复杂度:自旋锁依赖CAS原子操作,不需要操作系统调度,而互斥锁复杂度高,依赖操作系统调度
悲观锁和乐观锁
悲观锁:先加锁,再操作
认为并发操作一定会发生冲突,因此每次访问数据时都会加锁,比如synchronized和ReentrantLock。
适用写多的场景。
举个例子:出门时锁门(默认有小偷)
乐观锁:先操作,提交时再检查冲突
认为并发操作很少发生冲突,只在提交操作时检查是否冲突,比如CAS操作,数据库的乐观锁和Java中的Atomic类。
适用读多写少。
举个例子:
1.购物车结算时才检查库存(默认没人抢购)
2.或者在网上订票,系统显示还有1个座位,你点击预订,系统会先让你填写信息,然后提交的时候检查是否还有座位。如果有,预订成功;如果没有,提示你重新选择
内存管理
计算机系统的核心功能之一,其目标是高效、安全地管理物理内存和虚拟内存资源,确保多个进程能共享内存且互不干扰
物理/虚拟内存
-
物理内存
计算机硬件中的实际内存芯片,容量固定,由硬件决定,直接由CPU通过物理地址访问,是程序运行的真实存储空间。
-
虚拟内存
操作系统为每个进程提供的逻辑地址空间,独立于物理内存,通过通过页表、MMU(内存管理单元,负责管理内存访问和地址转换)、缺页中断等机制管理,将虚拟地址映射到物理地址或磁盘空间。
-
虚拟内存允许程序使用比物理内存更大的地址空间
-
每个进程拥有独立的虚拟地址空间,彼此无法直接访问对方内存,避免恶意或错误操作。
-
页表
操作系统与硬件(如MMU)协作实现虚拟内存的核心数据结构,负责记录虚拟地址到物理地址的映射关系。
核心作用
- 地址映射
将进程的虚拟页号 映射到物理内存的物理页号。 - 权限控制
通过页表项的权限位(读/写/执行)限制内存访问。 - 状态标记
记录页是否在物理内存中、是否被修改过等状态信息。
工作流程
- 把虚拟内存地址,切分成页号和页内偏移量
- 根据页号,从页表里面,查询对应的物理页号
- 直接拿物理页号,加上前面的偏移量,就得到了物理内存地址。
如果在页表中没有相应页号,触发缺页中断。此时进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。
段表
虚拟地址也可以通过段表与物理地址进行映射的
- 程序的内存空间被划分为多个逻辑段,每个段代表一个逻辑单元
- 每个段有独立的基址(起始地址)和界限(长度)
- 段的大小可变,与程序逻辑直接对应(例如代码段大小取决于代码量)
- 物理地址 = 段基址 + 段内偏移
虚拟内存布局
1. 代码段
- 位置:低地址起始(如
0x400000
)。 - 内容:编译后的机器指令(可执行代码)。
- 权限:只读+可执行(防止代码被篡改)。
- 示例:
main
函数、库函数的指令。
2. 数据段
- 位置:紧接代码段。
- 内容:已初始化的全局变量和静态变量。
- 权限:可读写,不可执行。
- 示例:
int global_var = 42;
。
3. BSS段
- 位置:紧接数据段。
- 内容:未初始化的全局变量和静态变量。
- 权限:可读写。
- 示例:
int uninitialized_var;
。
4. 堆
- 位置:BSS段之上,向高地址增长。
- 管理:通过
malloc
、free
动态分配内存。 - 特点:碎片化问题常见,需手动管理(或依赖垃圾回收)。
- 示例:
int *arr = malloc(100 * sizeof(int));
。
5. 文件映射区域
- 位置:堆与栈之间。
- 内容:
- 共享库(如
libc.so
、libm.so
)。 - 内存映射文件(通过
mmap
映射的文件)。 - 匿名映射(用于大块内存分配,如
malloc
可能使用mmap
)。
- 共享库(如
- 权限:按需设置(如可读写、可执行)。
6. 栈
- 位置:高地址区域(如
0x7FFFFFFFFFFFF000
),向下增长。 - 内容:函数调用栈帧(局部变量、返回地址、函数参数)。
- 管理:自动分配/释放内存,由编译器控制。
- 限制:栈大小固定(默认几MB,可通过
ulimit
调整)。 - 示例:
int local_var = 10;
。
7. 内核空间
- 位置:虚拟地址空间的高位(如64位Linux中高128TB)。
- 权限:仅内核态可访问,用户进程无权直接读写。
- 内容:内核代码、数据结构、设备内存映射等。
写时复制copy on write
COW,一种内存管理优化技术,延迟数据的物理拷贝,直到真正需要修改数据时才进行复制。
-
问题场景:
假设父进程和子进程共享同一物理内存页(未复制),且子进程修改了某个变量。如果此时父进程读取该变量,会发现值被意外改变。- 示例:
父进程定义变量int x = 42
,子进程修改x = 100
。若未复制,父进程的x
也会变为 100,导致逻辑错误。
- 示例:
-
COW 的解决方案:
当子进程尝试写入x
时,触发复制,子进程获得独立的物理页副本。- 父进程的
x
保持 42,子进程的x
变为 100,两者互不影响。
- 父进程的
具体步骤:
- 步骤1:共享内存页
调用fork()
时,子进程与父进程共享所有物理内存页,页表项标记为只读。 - 步骤2:触发复制
- 当父进程或子进程尝试写入共享页时,触发缺页中断。
- 操作系统捕获中断,检查触发原因是 COW,执行以下操作:
- 分配新的物理页,复制原页内容到新页。
- 更新触发写入的进程的页表项,指向新物理页,并标记为可写。
- 另一进程仍指向原物理页(保持只读,直到其写入时触发复制)。
- 步骤3:后续操作
- 修改后的页独立于原页,后续写入不再触发复制。
COW有什么好?
fork()的时候,子进程不需要复制父进程的物理内存,只需要复制父进程的页表,避免了不必要的内存复制开销,这时候父子进程的页表指向的都是共享的物理内存。
只有当父子进程任何有一方对这片共享的物理内存发生了修改操作,这时候才会复制发生修改操作的物理内存。
brk,mmap
int *arr = malloc(100 * sizeof(int))
这里的动态分配内存malloc():
1.如果请求分配的内存<128KB,则通过brk()申请
2.如果请求分配的内存>128KB,则通过mmap()申请
-
brk:修改指针,向高地址移动,获得新内存空间
-
mmap:从文件映射区申请一块内存
如果物理内存不足
当使用malloc()申请虚拟内存时,如果虚拟内存还没有映射到物理内存,CPU产生缺页中断,缺页中断函数查看是否有空闲物理内存,有则建立虚拟与物理内存的映射,如果没有,则开始回收内存(页面置换/丢弃/终止):
回收内存,包括:
- 页面置换
- 将部分物理内存页换出到磁盘(Swap空间),释放物理内存供其他进程使用。
- 释放可丢弃的干净页
- 直接丢弃未被修改的页(如代码段、文件缓存页),无需写回磁盘。
- 终止进程(OOM Killer)
- 强制终止占用内存过多的进程,释放其所有内存(极端情况下的最后手段)。
1.后台内存回收:唤醒 kswapd 内核线程来回收内存(异步,不阻塞进程执行)
2.直接内存回收:如果异步回收速度跟不上申请内存速度,则直接回收(同步,阻塞进程执行)
哪些内存可以被回收?
- 文件页
内核缓存的磁盘数据(Buffer)和内核缓存的文件数据(Cache)都叫作文件页。需要读时,重新读取磁盘即可。而如果修改过但还没写入磁盘的数据(脏页)需要先写进磁盘,才进行回收。
- 匿名页
无实际载体,所以不能直接释放内存。会把不常访问的内存先写到磁盘,再释放这些内存。
如果经过上面的步骤物理内存仍不满足,则触发Out-Of-Memory Killer
- OOM根据算法杀死一个占用物理内存较高的进程,释放其内存资源
- 如果依然不满足,继续杀死,直至释放足够物理内存
页面置换算法
页面置换,内存回收的核心手段之一。
选择一个物理页面换出到磁盘,然后把需要访问的页面换入到物理页。
-
最佳页面OPT
思想:置换出在未来长时间不会使用的页面
-
先进先出FIFO
思想:换出在内存中占用时间最长的页面
-
最近最久未使用LRU
思想:换出前面长时间没有访问过的页面
-
时钟页面LOCK
思想:将所有页面存在环形链表,每页需要一个访问位(0:未访问1:已访问)
初始:所有页访问位为0
缺页时,指针移动,
访问位为1:重置为0,指针后移
访问位为0:选择,换出内存
-
最不常用LFU
思想:对每个页面增加一个访问计数器,被访问一次,计数器+1,发生缺页中断时,换出访问次数最少的页面,
中断
计算机系统响应外部或内部事件的机制,允许 CPU 暂停当前任务,转而处理紧急或异步事件(如键盘输入)。处理完后回来继续执行刚才的任务。
中断机制是操作系统实现多任务、设备管理、错误处理等功能的基础
中断分类
外部中断(硬件中断)
- 触发源:外部设备(如键盘、磁盘、定时器)。
- 类型:
- 可屏蔽中断:可通过中断屏蔽位(如
IF
标志)暂时关闭,可随时处理,甚至不处理。 - 不可屏蔽中断:必须立即处理(如硬件故障、内存校验错误)。
- 可屏蔽中断:可通过中断屏蔽位(如
内部中断(软件中断)
- 触发方式:由程序执行特定指令或发生异常。
- 异常:同步触发,由 CPU 执行指令时检测到错误(如缺页、除零)交由故障处理程序处理。
- 陷阱:一般是在编写程序时故意设下的陷阱指令,而后执行到陷阱指令后,CPU将会调用特定程序进行相应的处理,处理结束后返回到陷阱指令的下一条指令。
- 终止:发生致命错误,不可修复,程序无法继续运行,直接终止。
中断流程
-
发生中断:设备或程序触发中断,发送信号到 CPU 的中断控制器
-
中断响应:保存上下文,CPU 暂停当前任务,将程序计数器(PC)、寄存器等压入内核栈,保存当前执行现场,以便处理完中断后恢复执行。
-
中断处理:CPU根据中断向量表找到对应中断处理程序入口地址,进行中断处理
-
恢复上下文:继续执行之前的程序
网络I/O
I/O模型
网络I/O模型定义了应用程序如何管理输入和输出操作,尤其是在处理多个并发连接时如何高效利用资源
- 阻塞I/O
- 工作原理:
应用程序发起I/O操作后,进程会被阻塞,直到数据就绪或操作完成。在此期间,进程无法执行其他任务。 - 优点:
实现简单,代码直观,适合低并发场景。 - 缺点:
每个连接需独立线程/进程,资源消耗大,无法支撑高并发。 - 适用场景:
简单的客户端应用或低负载服务(实时性要求不高)。
- 工作原理:
- 非阻塞I/O
- 工作原理:
I/O操作立即返回结果(成功或错误),不会被阻塞。若数据未就绪,返回错误,应用需轮询检查状态。 - 优点:
单线程可管理多个连接,避免线程阻塞。 - 缺点:
轮询消耗CPU资源,延迟较高。 - 适用场景:
需要同时处理少量连接且对延迟不敏感的场景(多路复用)。
- 工作原理:
- I/O多路复用
- 工作原理:
使用select
、poll
、epoll
(Linux)等系统调用,同时等待多个I/O操作,当任一I/O准备就绪时通知应用处理。 - 优点:
单线程处理高并发连接,资源利用率高。 - 缺点:
事件通知后仍需同步处理I/O,编程复杂度较高。 - 适用场景:
Web服务器(如Nginx)、即时通讯等高并发服务。
- 工作原理:
- 信号驱动I/O
- 工作原理:
发起I/O请求后继续做其他事情,当I/O操作就绪时,内核发送信号通知处理,应用执行I/O操作。 - 优点:
避免轮询,减少CPU占用。 - 缺点:
信号处理复杂,可能丢失事件,多线程中难以管理。 - 适用场景:
需要异步I/O通知的场景,提高系统并发能力。
- 工作原理:
- 异步I/O
- 工作原理:
应用发起I/O请求后立即做其他事情,内核完成整个I/O操作后通知应用。 - 优点:
无阻塞,资源利用最优,适合高吞吐场景。 - 缺点:
实现复杂,需操作系统和库支持,调试困难。 - 适用场景:
高并发,高性能场景,减少系统调用,提高系统效率。
- 工作原理:
服务器处理并发请求
-
单线程web服务器
一次处理一个请求,性能低
-
多进程/多线程web服务器
服务器生成多个进程/线程并行处理多个用户请求,消耗大量系统资源
-
I/O多路复用web服务器
只用一个线程处理多个用户请求
-
多路复用多线程web服务器
避免一个进程服务于过多用户请求,生成多个进程,一个进程生成多个线程,每个线程处理一个请求
考完操作系统不久,结合小林Coding写了些笔记,感谢大家的点赞收藏>W<