1、线程切换进行了哪些动作
在操作系统中,线程切换(也称为上下文切换)是指操作系统将 CPU 的控制权从一个线程转移到另一个线程的过程。这个过程涉及多个步骤和动作,主要包括以下几个方面:
1. 保存当前线程的上下文
在切换线程之前,操作系统需要保存当前线程的状态,以便在将来恢复时能够继续执行。这个状态通常包括:程序计数器(PC):指示当前线程执行到哪一条指令。寄存器状态:保存 CPU 寄存器的值,包括通用寄存器、堆栈指针等。线程状态:保存线程的状态信息(如运行、就绪、阻塞等)
2. 更新线程控制块(TCB)
每个线程都有一个线程控制块(Thread Control Block, TCB),用于存储线程的相关信息。在切换线程时,操作系统需要:更新当前线程的 TCB,设置其状态为“就绪”或“阻塞”。更新即将运行的线程的 TCB,设置其状态为“运行”。
3. 选择下一个线程
操作系统需要根据调度算法选择下一个要运行的线程。常见的调度算法包括:先来先服务(FCFS);短作业优先(SJF);时间片轮转(Round Robin);优先级调度。
4. 加载新线程的上下文
一旦选择了下一个线程,操作系统需要加载该线程的上下文,包括:恢复程序计数器(PC)的值。恢复寄存器的值。更新堆栈指针等。
5. 更新调度信息
在切换线程后,操作系统可能需要更新一些调度信息,例如:更新 CPU 使用时间,更新线程的优先级(如果适用)。
6. 切换到用户模式
在完成所有上下文切换的准备工作后,操作系统将控制权转移到新的线程,通常需要切换到用户模式,以便线程可以开始执行。
7. 恢复执行
最后,操作系统将控制权交给新线程,恢复执行。
2、进程的几种状态
新建状态 | 进程正在被创建。在此状态下,操作系统为进程分配资源并初始化其控制块(PCB)。此时,进程尚未被调度执行 |
就绪状态 | 进程已准备好执行,但尚未获得 CPU 资源。处于就绪状态的进程可以随时被调度执行。操作系统会根据调度算法选择一个就绪进程来运行 |
运行状态 | 进程正在 CPU 上执行。在这个状态下,进程正在进行实际的计算或操作。一个时刻只能有一个进程处于运行状态(在单核 CPU 上) |
阻塞状态 | 进程因等待某些事件(如 I/O 操作完成、资源可用等)而无法继续执行。当进程在运行状态中请求某些资源而无法获得时,它会被置于阻塞状态。此时进程无法被调度执行,直到等待的事件发 |
终止状态 | 进程已经完成执行或被强制终止。在这个状态下,进程的所有资源都被释放,进程的控制块(PCB)也被删除。此时,进程的执行已经结束。 |
状态转换
新建 → 就绪 | 进程创建完成,准备好执行。 |
就绪 → 运行 | 操作系统调度进程执行。获得CUP执行 |
运行 → 阻塞 | 进程请求的资源不可用,进入阻塞状态。 |
运行 → 就绪 | 进程被抢占,返回就绪状态。 |
阻塞 → 就绪 | 等待的事件发生,进程返回就绪状态。 |
运行 → 终止 | 进程执行完成或被终止。 |
3、软、硬中断
中断是指一种机制,用于处理异步事件,允许处理器暂停当前执行的任务,转而执行其他任务。中断通常分为两种类型:软中断和硬中断。硬中断:由硬件设备发出,异步、不可预测,通常用于处理外部事件。软中断:由软件程序发出,同步、可控,通常用于请求操作系统服务
硬中断(Hardware Interrupt)
定义:硬中断是由硬件设备发出的信号,通知处理器需要处理某个事件或请求。
来源:硬中断通常来自外部设备,如键盘、鼠标、网络接口、磁盘驱动器等。例如,当用户按下键盘时,键盘控制器会发送一个硬中断信号给 CPU,通知它有新的输入数据。
特点:异步性:硬中断是异步的,发生的时间不受程序控制。 优先级:硬中断通常具有优先级,某些设备的中断可能会比其他设备的中断更重要。中断处理:当硬中断发生时,CPU 会保存当前的执行状态,转而执行与该中断相关的中断处理程序(Interrupt Service Routine, ISR)
示例:硬盘完成数据读取、网络数据包到达、定时器溢出等。
软中断(Software Interrupt)
定义:软中断是由软件程序发出的中断信号,通常用于请求操作系统执行某些特定的服务或操作。
来源:软中断通常由程序通过特定的指令或系统调用触发。例如应用程序可能会通过调用操作系统提供的 API 来请求文件操作、内存分配等服务。
特点:同步性:软中断通常是同步的,发生在程序执行的特定点。控制性:程序可以控制何时触发软中断,通常用于与操作系统进行交互。中断处理:当软中断发生时,操作系统会执行相应的处理程序,完成请求的服务。
示例:系统调用(如 fork、exec)、异常处理(如除零错误)等。
4、为什么要有虚拟内存
扩展可用内存 | 虚拟内存允许程序使用比实际物理内存更大的地址空间。 通过将部分数据存储在磁盘上(通常称为交换空间或页面文件),操作系统可以让程序认为它拥有更多的内存。这对于运行大型应用程序或多个程序时尤为重要。 |
内存隔离与保护 | 虚拟内存为每个进程提供独立的地址空间。这样可以防止一个进程访问或修改另一个进程的内存数据,提高了系统的安全性和稳定性。每个进程只能看到自己的虚拟地址空间,操作系统负责将虚拟地址映射到物理地址 |
简化内存管理 | 虚拟内存使得内存管理更加简单和高效。操作系统可以使用分页或分段等技术来管理内存,而不必关心物理内存的实际布局。这种抽象使得内存分配和回收变得更加灵活 |
支持多任务处理 | 虚拟内存使得多个进程可以同时运行而不会相互干扰。 通过为每个进程提供独立的虚拟地址空间,操作系统可以有效地管理多个进程的内存需求,确保它们能够并发执行 |
提高内存利用率 | 虚拟内存可以将不常用的数据或代码存储在磁盘上,从而释放物理内存。 操作系统可以根据需要将不活跃的页面换出到磁盘,而将活跃的页面保留在物理内存中。这种机制提高了内存的利用率,减少了内存浪费 |
支持内存映射文件 | 虚拟内存允许将文件映射到进程的地址空间。 这使得文件可以像内存一样被访问,简化了文件的读写操作,提高了文件操作的效率 |
5、分段、分页
分段和分页是两种内存管理技术,用于提高计算机系统的内存利用率和管理效率。
分段(Segmentation)
定义:分段是一种将程序的逻辑地址空间划分为多个不同大小的段(segment)的内存管理方式。每个段代表程序中的一个逻辑部分,如函数、数组、数据结构等。
特点:逻辑划分:分段是基于程序的逻辑结构进行划分的,每个段可以是不同大小,反映了程序的实际组织。段表:操作系统维护一个段表,记录每个段的基地址和长度。逻辑地址由段号和段内偏移量组成。灵活性:分段允许程序员根据需要定义段的大小和数量,提供了更大的灵活性。保护与共享:不同的段可以设置不同的访问权限,支持段的共享和保护。
示例:一个程序可能被划分为多个段,如:段 0:代码段(程序指令);段 1:数据段(全局变量);段 2:堆段(动态分配内存);段 3:栈段(函数调用和局部变量)
分页(Paging)
定义:分页是一种将程序的逻辑地址空间划分为固定大小的页面(page)和物理内存划分为固定大小的页框(frame)的内存管理方式。每个页面可以映射到任意的页框中。
特点:固定大小:分页使用固定大小的页面和页框,通常大小相同(如 4KB),简化了内存管理。页表:操作系统维护一个页表,记录每个页面与物理内存页框之间的映射关系。逻辑地址由页号和页内偏移量组成。避免外部碎片:由于页面大小固定,分页技术有效避免了外部碎片问题,但可能会产生内部碎片(即页内未使用的空间)。简单的内存管理:分页使得内存分配和回收变得简单,操作系统可以轻松地将页面加载到内存中。
示例:假设一个程序的逻辑地址空间被划分为 10 个页面,每个页面大小为 4KB,物理内存中有 5 个页框。操作系统通过页表将逻辑页面映射到物理页框中。
分段与分页的比较
特点 | 分段 | 分页 |
---|---|---|
划分方式 | 逻辑划分,大小不固定 | 固定大小的页面 |
内存管理 | 段表 | 页表 |
碎片问题 | 避免内部碎片,可能产生外部碎片 | 避免外部碎片,可能产生内部碎片 |
灵活性 | 更灵活,适应程序结构 | 简单,易于管理 |
保护与共享 | 支持段级别的保护与共享 | 页级别的保护与共享 |
6、I/O相关问题
I/O(输入/输出)是计算机系统中用于描述数据传输的过程,涉及到计算机与外部设备(如硬盘、网络、显示器等)之间的数据交换。I/O操作通常是计算机系统中性能瓶颈的主要来源之一,因为它们通常比CPU操作慢得多。
什么是I/O?
I/O操作可以分为以下几类:
输入操作:从外部设备(如键盘、鼠标、传感器等)获取数据。
输出操作:将数据发送到外部设备(如显示器、打印机、网络等)。
存储操作:在存储设备(如硬盘、SSD等)上读写数据。
为什么网络I/O会被阻塞?
等待数据传输:在进行网络I/O操作时,程序可能需要等待数据从网络中传输到本地,或者等待数据从本地发送到网络。这种等待会导致程序阻塞,直到I/O操作完成。
网络延迟:网络通信通常会受到延迟的影响,尤其是在高延迟的网络环境中,数据包的传输时间可能会很长,导致I/O操作被阻塞。
资源竞争:多个进程或线程可能同时请求网络I/O资源,导致某些请求被阻塞,直到资源可用。
缓冲区满:在进行网络发送时,如果发送缓冲区已满,程序会被阻塞,直到缓冲区有足够的空间来发送新的数据。
I/O模型
I/O模型主要用于描述程序如何处理I/O操作
阻塞I/O(Blocking I/O):在阻塞I/O模型中,I/O操作会阻塞调用它的线程,直到操作完成。程序在等待I/O操作完成期间无法执行其他任务。
优点:编程简单,易于理解。缺点:效率低,尤其在高延迟的网络环境中。
非阻塞I/O(Non-blocking I/O):在非阻塞I/O模型中,I/O操作会立即返回,不会阻塞调用线程。如果I/O操作无法立即完成,程序可以继续执行其他任务。
优点:提高了程序的并发性和效率。缺点:编程复杂,需要处理I/O操作的状态。
I/O复用(I/O Multiplexing):I/O复用模型允许一个线程同时监视多个I/O流,通过系统调用(如select、poll、epoll等)来检测哪些I/O操作可以进行。
优点:高效地处理多个I/O操作,适合高并发场景。缺点:编程复杂,需要管理多个I/O流的状态。
信号驱动I/O(Signal-driven I/O):在信号驱动I/O模型中,程序可以注册信号处理程序,当I/O操作准备好时,系统会发送信号通知程序。优点:避免了轮询,提高了效率。缺点:信号处理的复杂性,可能导致程序的可读性降低。
异步I/O(Asynchronous I/O):在异步I/O模型中,程序可以发起I/O操作并继续执行其他任务,I/O操作完成后,系统会通知程序(通常通过回调函数)。
优点:高效,适合处理大量并发I/O操作。缺点:编程复杂,需要处理回调和状态管理。
7、同步与异步
同步 | 异步 |
定义:调用者会等待被调用的任务完成后再继续执行后续操作。任务的执行是顺序的,控制流是线性的。 | 定义:调用者可以发起任务并立即继续执行后续操作,而不需要等待任务完成。任务的执行可以并行进行,控制流是非线性的。 |
特点:阻塞:调用者在等待任务完成时会被阻塞,无法执行其他操作。 简单易懂:由于控制流是线性的,代码逻辑相对简单,易于理解和维护。 资源利用率低:在等待I/O操作完成时,CPU可能处于闲置状态,导致资源利用率低。 | 特点:非阻塞:调用者在发起任务后可以继续执行其他代码,不会被阻塞。 提高资源利用率:由于不需要等待I/O操作完成,CPU可以在等待期间执行其他任务,从而提高资源利用率。 编程复杂性:异步编程通常需要处理回调、Promise、Future等概念,代码逻辑可能变得复杂。 |
适用于简单的任务处理,如文件读取、数据库查询等,任务之间的依赖关系较强时。 适合对实时性要求不高的场景 | 适用于高并发场景,如网络请求、用户界面交互等,能够有效提高程序的响应能力。 适合处理I/O密集型任务能够在等待I/O操作时执行其他计算 |
通过直接调用函数或方法来实现,通常是阻塞的I/O操作。 | 通过回调函数、Promise、Future或异步编程模型(如async/await)来实现。 |
同步
# 同步读取文件
def read_file_sync():
with open('example.txt', 'r') as file:
content = file.read() # 阻塞,直到文件读取完成
print(content)
read_file_sync()
异步
import asyncio
async def read_file_async():
loop = asyncio.get_event_loop()
with open('example.txt', 'r') as file:
content = await loop.run_in_executor(None, file.read) # 非阻塞
print(content)
asyncio.run(read_file_async())
8、CFS
CFS(Completely Fair Scheduler)是 Linux 内核中使用的一种调度算法,旨在实现公平的 CPU 时间分配。CFS 是在 2005 年由 Ingo Molnar 提出的,作为对传统调度器(如 O(1) 调度器)的改进。
1、设计目标是:
公平性:确保每个进程获得相应的 CPU 时间,避免某些进程长时间占用 CPU。
低延迟:尽量减少进程的响应时间,特别是对交互式应用程序。
高效性:在实现公平性的同时,保持调度的高效性。
2、工作原理
CFS 的核心思想是使用一个红黑树(Red-Black Tree)来管理可运行的进程。每个进程在树中根据其“虚拟运行时间”(vruntime)进行排序。以下是 CFS 的工作原理:
1)虚拟运行时间(vruntime)
每个进程都有一个 vruntime 值,表示该进程在 CPU 上运行的时间。CFS 通过这个值来决定哪个进程应该被调度。vruntime 的增加速度与进程的优先级有关,优先级高的进程 vruntime 增加得更慢,从而获得更多的 CPU 时间。
2)红黑树
CFS 使用红黑树来维护所有可运行进程。树的每个节点代表一个进程,节点的排序依据是其 vruntime 值。当调度器需要选择下一个运行的进程时,它会选择 vruntime 最小的进程(即树的最左侧节点)。
3)时间片
CFS 不使用固定的时间片,而是根据进程的 vruntime 动态调整。每个进程在 CPU 上运行的时间取决于其 vruntime 的值。当一个进程被调度运行时,其 vruntime 会增加,调度器会根据新的 vruntime 值重新调整进程在红黑树中的位置。
3、优点与缺点
公平性:CFS 通过 vruntime 的机制,确保每个进程获得公平的 CPU 时间。
响应性:对于交互式进程,CFS 能够快速响应用户输入,提供良好的用户体验。
可扩展性:CFS 适用于多核处理器,能够有效地利用多核系统的资源。
复杂性:CFS 的实现相对复杂,尤其是红黑树的维护和 vruntime 的计算。
上下文切换开销:在高负载情况下,频繁的上下文切换可能导致性能下降。
9、select、poll 和 epoll
select、poll 和 epoll 是 Linux 系统中用于 I/O 多路复用的三种系统调用,它们的主要作用是允许一个进程同时监视多个文件描述符,以便在其中一个或多个文件描述符变为可读、可写或出现异常时进行处理。
1、select
特点:select 使用一个固定大小的文件描述符集合,最大值通常为 1024(可以通过修改系统参数来增加)。每次调用 select 时,必须重新设置文件描述符集合。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include <fcntl.h>
int main() {
int fd1 = open("file1.txt", O_RDONLY);
int fd2 = open("file2.txt", O_RDONLY);
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fd1, &readfds);
FD_SET(fd2, &readfds);
int maxfd = (fd1 > fd2) ? fd1 : fd2;
struct timeval timeout;
timeout.tv_sec = 5; // 5秒超时
timeout.tv_usec = 0;
int ret = select(maxfd + 1, &readfds, NULL, NULL, &timeout);
if (ret > 0) {
if (FD_ISSET(fd1, &readfds)) {
printf("file1.txt is ready to read.\n");
}
if (FD_ISSET(fd2, &readfds)) {
printf("file2.txt is ready to read.\n");
}
} else if (ret == 0) {
printf("Timeout occurred.\n");
} else {
perror("select");
}
close(fd1);
close(fd2);
return 0;
}
2、Poll
特点:poll
使用一个动态数组来存储需要监视的文件描述符和事件类型,没有固定大小限制。不需要每次调用时重新设置文件描述符。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <poll.h>
#include <fcntl.h>
int main() {
int fd1 = open("file1.txt", O_RDONLY);
int fd2 = open("file2.txt", O_RDONLY);
struct pollfd fds[2];
fds[0].fd = fd1;
fds[0].events = POLLIN; // 可读事件
fds[1].fd = fd2;
fds[1].events = POLLIN;
int ret = poll(fds, 2, 5000); // 5秒超时
if (ret > 0) {
if (fds[0].revents & POLLIN) {
printf("file1.txt is ready to read.\n");
}
if (fds[1].revents & POLLIN) {
printf("file2.txt is ready to read.\n");
}
} else if (ret == 0) {
printf("Timeout occurred.\n");
} else {
perror("poll");
}
close(fd1);
close(fd2);
return 0;
}
3、epoll
特点:epoll 是 Linux 特有的高效 I/O 多路复用机制,适用于处理大量并发连接。使用事件驱动的方式,支持边缘触发和水平触发。通过 epoll_ctl 添加、修改或删除文件描述符。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>
int main() {
int fd1 = open("file1.txt", O_RDONLY);
int fd2 = open("file2.txt", O_RDONLY);
int epfd = epoll_create1(0);
struct epoll_event event;
event.events = EPOLLIN; // 可读事件
event.data.fd = fd1;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd1, &event);
event.data.fd = fd2;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd2, &event);
struct epoll_event events[2];
int ret = epoll_wait(epfd, events, 2, 5000); // 5秒超时
if (ret > 0) {
for (int i = 0; i < ret; i++) {
if (events[i].data.fd == fd1) {
printf("file1.txt is ready to read.\n");
} else if (events[i].data.fd == fd2) {
printf("file2.txt is ready to read.\n");
}
}
} else if (ret == 0) {
printf("Timeout occurred.\n");
} else {
perror("epoll_wait");
}
close(fd1);
close(fd2);
close(epfd);
return 0;
}
特性 | select | poll | epoll |
---|---|---|---|
文件描述符限制 | 通常为 1024(可配置) | 没有固定限制 | 理论上可以支持无限数量的文件描述符 |
数据结构 | 使用固定大小的数组 | 使用动态数组 | 使用红黑树 |
重新设置 | 每次调用都需要重新设置 | 不需要每次重新设置 | 通过 epoll_ctl 管理文件描述符 |
性能 | 在大量文件描述符时性能较差 | 性能比 select 好 | 在大量文件描述符时性能最佳 |
适用场景 | 适合少量文件描述符的简单应用 | 适合中等数量的文件描述符 | 适合高并发、大量文件描述符的应用 |