操作系统八股文整理
- 一、进程和线程的区别
- 二、进程与线程的切换过程
- 一、进程切换
- 进程切换的步骤:
- 二、线程切换
- 线程切换的步骤:
- 三、进程切换与线程切换的对比
- 四、上下文切换的优化
- 三、系统调用
- 一、系统调用的触发
- 二、从用户空间切换到内核空间
- 三、执行内核函数
- 四、从内核空间返回用户空间
- 五、系统调用的完整流程示例
- 六、系统调用的性能开销
- 四、进程间通讯方式
- 管道demo
- 命名管道demo
- 1. 创建命名管道
- 2. 写入者进程
- 3. 读取者进程
- 编译与运行
- 输出示例
- 注意事项
- 信号量
- 1. 信号量的分类
- 2. 信号量的实现
- 2.1 系统V信号量
- 示例代码:
- 2.2 POSIX信号量
- 示例代码:
- 2.3 Boost信号量
- 示例代码:
- 2.4 C++20 `std::semaphore`
- 示例代码:
- 3. 信号量的使用场景
- 4. 注意事项
- 总结
- 消息队列demo
- 1. 系统V消息队列
- 示例代码:
- 2. POSIX消息队列
- 示例代码:
- 3. Boost.Interprocess消息队列
- 示例代码:
- 4. 自定义线程安全消息队列
- 示例代码:
- 总结
- 五、进程的调度
- 进程调度的算法
- (1)先来先服务(FCFS,First-Come-First-Served)
- (2)短作业优先(SJF,Shortest Job First)
- (3)优先级调度(Priority Scheduling)
- (4)时间片轮转(RR,Round Robin)
- (5)多级反馈队列(Multilevel Feedback Queue)
- 六、线程同步的方式
一、进程和线程的区别
二、进程与线程的切换过程
进程和线程的切换是操作系统中非常重要的概念,它们涉及到系统资源的分配、调度以及上下文切换等操作。以下是进程和线程切换过程的详细解释:
一、进程切换
进程切换是指操作系统将 CPU 从一个进程切换到另一个进程的过程。它通常发生在以下几种情况:
- 时间片用完:当前进程的时间片用完,操作系统需要选择下一个进程运行。
- 进程阻塞:当前进程因等待资源(如 I/O)而阻塞,操作系统需要切换到其他就绪的进程。
- 更高优先级进程到来:有更高优先级的进程就绪,操作系统需要切换到该进程。
进程切换的步骤:
- 保存当前进程的上下文:
- 保存程序计数器(PC):记录当前进程执行到的位置。
- 保存寄存器状态:包括通用寄存器、状态寄存器等,这些寄存器保存了进程执行时的临时数据和状态。
- 保存进程状态信息:如进程的优先级、资源使用情况等。
- 更新进程控制块(PCB):
- 将当前进程的状态从“运行态”改为“就绪态”或“阻塞态”,并更新其在进程队列中的位置。
- 选择下一个进程:
- 操作系统根据调度算法(如轮转调度、优先级调度等)选择下一个要运行的进程。
- 如果没有就绪的进程,可能会选择进入空闲进程或等待外部事件。
- 恢复新进程的上下文:
- 恢复程序计数器(PC):将新进程上次运行时的位置加载到 PC 中。
- 恢复寄存器状态:将新进程的寄存器状态加载到 CPU 的寄存器中。
- 更新新进程的状态:将新进程的状态从“就绪态”改为“运行态”。
- 开始新进程运行:
- 将 CPU 的控制权交给新进程,新进程从上次保存的位置继续执行。
二、线程切换
线程切换是指操作系统将 CPU 从一个线程切换到另一个线程的过程。线程切换的开销通常比进程切换小,因为线程共享进程的资源,切换时不需要切换进程的上下文。
线程切换的步骤:
- 保存当前线程的上下文:
- 保存线程的程序计数器(PC):记录当前线程执行的位置。
- 保存线程的寄存器状态:线程有自己的线程控制块(TCB),保存线程的局部变量、栈指针等信息。
- 保存线程状态信息:如线程的优先级、阻塞原因等。
- 更新线程控制块(TCB):
- 将当前线程的状态从“运行态”改为“就绪态”或“阻塞态”,并更新其在线程队列中的位置。
- 选择下一个线程:
- 操作系统根据线程调度算法(如时间片轮转、优先级调度等)选择下一个要运行的线程。
- 如果没有就绪的线程,可能会选择进入空闲线程或等待外部事件。
- 恢复新线程的上下文:
- 恢复线程的程序计数器(PC):将新线程上次运行时的位置加载到 PC 中。
- 恢复线程的寄存器状态:将新线程的寄存器状态加载到 CPU 的寄存器中。
- 更新新线程的状态:将新线程的状态从“就绪态”改为“运行态”。
- 开始新线程运行:
- 将 CPU 的控制权交给新线程,新线程从上次保存的位置继续执行。
三、进程切换与线程切换的对比
- 上下文切换的开销:
- 进程切换:开销较大,需要切换进程的代码段、数据段、寄存器状态、文件描述符等。
- 线程切换:开销较小,只需要切换线程的寄存器状态和栈指针等局部信息。
- 资源共享:
- 进程切换:进程之间是独立的,切换时需要切换资源。
- 线程切换:线程共享进程的资源,切换时不需要切换资源。
- 调度单位:
- 进程切换:以进程为单位进行调度。
- 线程切换:以线程为单位进行调度,线程是 CPU 调度的最小单位。
- 适用场景:
- 进程切换:适用于需要独立资源的场景,如运行不同的程序。
- 线程切换:适用于需要高并发的场景,如多线程服务器。
四、上下文切换的优化
上下文切换虽然可以提高系统的并发性,但频繁的上下文切换会增加系统的开销,降低系统性能。因此,操作系统和应用程序需要采取一些优化措施:
- 减少上下文切换的次数:
- 增加时间片的长度,减少进程或线程切换的频率。
- 使用高效的调度算法,减少不必要的上下文切换。
- 优化上下文切换的开销:
- 减少保存和恢复的寄存器数量。
- 使用缓存技术,减少对内存的访问。
- 减少线程的阻塞时间:
- 提高 I/O 操作的效率,减少线程因 I/O 阻塞而切换的次数。
- 使用非阻塞 I/O 或异步 I/O,减少线程的阻塞时间。
总之,进程切换和线程切换是操作系统中非常重要的机制,它们的合理使用可以提高系统的并发性和性能。
三、系统调用
系统调用是用户程序与操作系统内核之间进行交互的桥梁。它允许用户程序请求操作系统提供的服务,例如文件操作、进程控制、通信等。系统调用的整个流程涉及用户空间和内核空间的切换,以及参数传递和结果返回。以下是系统调用的详细流程:
一、系统调用的触发
用户程序需要操作系统服务时,会通过系统调用来请求。这通常通过以下方式触发:
- 使用特定的指令:
- 在 x86 架构中,通常使用
int 0x80
(中断指令)或syscall
指令来触发系统调用。 - 在 ARM 架构中,使用
svc
指令。 - 这些指令会中断用户程序的执行,将控制权转移到操作系统内核。
- 在 x86 架构中,通常使用
- 设置系统调用号和参数:
- 在触发系统调用之前,用户程序需要将系统调用号(标识要调用的服务)和参数(如文件名、缓冲区地址等)放置在特定的寄存器或栈中。
- 例如,在 x86 架构中,系统调用号通常放在
eax
寄存器中,参数放在ebx
、ecx
、edx
等寄存器中。
二、从用户空间切换到内核空间
当用户程序触发系统调用时,CPU 会从用户态切换到内核态,同时操作系统会进行以下操作:
- 保存用户态上下文:
- 操作系统会保存用户程序的上下文,包括寄存器状态(如程序计数器、栈指针等)和 CPU 的当前状态(用户态或内核态)。
- 这些信息通常保存在内核为每个进程分配的内核栈中。
- 切换到内核态:
- CPU 的特权级别从用户态(较低特权级别)切换到内核态(较高特权级别)。
- 内核态允许访问系统的全部资源,包括硬件设备和内核内存。
- 查找系统调用表:
- 操作系统根据系统调用号在系统调用表中查找对应的内核函数。
- 系统调用表是一个数组,每个系统调用号对应一个内核函数指针。
三、执行内核函数
找到对应的内核函数后,操作系统会执行以下操作:
- 参数传递:
- 内核函数会从寄存器或栈中获取用户程序传递的参数。
- 如果参数是用户空间的地址(如文件名字符串),内核需要进行地址检查,确保这些地址是合法的。
- 执行内核函数:
- 内核函数根据用户请求执行相应的操作,例如:
- 打开文件时,内核会查找文件系统,分配文件描述符。
- 写文件时,内核会将数据写入磁盘缓冲区。
- 创建进程时,内核会分配内存和资源,创建新的进程控制块(PCB)。
- 内核函数根据用户请求执行相应的操作,例如:
- 处理错误和异常:
- 如果操作失败(如文件不存在、权限不足),内核会设置错误码(如
errno
)。
- 如果操作失败(如文件不存在、权限不足),内核会设置错误码(如
四、从内核空间返回用户空间
内核函数执行完毕后,操作系统需要将控制权返回给用户程序:
- 保存内核态上下文:
- 操作系统保存内核态的上下文信息,包括内核函数的返回值(通常放在某个寄存器中)。
- 恢复用户态上下文:
- 操作系统从内核栈中恢复用户程序的上下文,包括寄存器状态和程序计数器。
- 这样用户程序可以从上次中断的地方继续执行。
- 切换回用户态:
- CPU 的特权级别从内核态切换回用户态。
- 返回结果:
- 内核将系统调用的结果(如文件描述符、返回值等)传递给用户程序。
- 如果发生错误,用户程序可以通过错误码(如
errno
)获取错误信息。
五、系统调用的完整流程示例
假设用户程序要调用 write()
系统调用来写文件,其流程如下:
- 用户程序准备参数:
- 将系统调用号(如
1
表示write
)放入eax
寄存器。 - 将文件描述符、缓冲区地址和写入字节数分别放入
ebx
、ecx
和edx
寄存器。
- 将系统调用号(如
- 触发系统调用:
- 用户程序执行
int 0x80
指令,触发中断。
- 用户程序执行
- 切换到内核态:
- 操作系统保存用户态上下文,切换到内核态。
- 根据系统调用号
1
,查找系统调用表,找到write()
的内核函数。
- 执行内核函数:
- 内核函数从寄存器中获取参数(文件描述符、缓冲区地址等)。
- 检查文件描述符是否有效,缓冲区地址是否合法。
- 将数据从用户空间的缓冲区复制到内核空间的缓冲区。
- 写入数据到磁盘缓冲区。
- 如果成功,返回写入的字节数;如果失败,设置错误码。
- 返回用户态:
- 操作系统保存内核态上下文,恢复用户态上下文。
- 切换回用户态,将返回值放入用户程序的寄存器中。
- 用户程序继续执行:
- 用户程序根据返回值判断写操作是否成功,并继续执行后续代码。
六、系统调用的性能开销
系统调用涉及用户空间和内核空间的切换,因此会产生一定的性能开销:
- 上下文切换开销:
- 保存和恢复寄存器状态、切换特权级别等操作会消耗时间。
- 参数传递和检查开销:
- 内核需要验证用户空间的地址是否合法,这可能涉及额外的内存访问。
- 内核函数执行开销:
- 内核函数的执行时间取决于系统调用的复杂性(如 I/O 操作可能涉及磁盘 I/O 延迟)。
- 系统调用的优化:
- 现代操作系统通过减少上下文切换的次数、使用更快的切换指令(如
syscall
)等方式来优化系统调用的性能。 - 一些系统调用(如
getpid()
)可以通过轻量级的机制(如vsyscall
或vdso
)直接在用户空间执行,避免切换到内核态。
- 现代操作系统通过减少上下文切换的次数、使用更快的切换指令(如
总之,系统调用是用户程序与操作系统交互的重要机制,其流程涉及用户空间和内核空间的切换、参数传递、内核函数执行以及结果返回。虽然系统调用会产生一定的开销,但它是实现操作系统功能的关键机制。
四、进程间通讯方式
管道demo
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <cstdlib>int main() {int pipefd[2]; // 用于存储管道的文件描述符pid_t pid;// 创建管道if (pipe(pipefd) == -1) {exit(EXIT_FAILURE);}// 创建子进程pid = fork();if (pid == -1) {exit(EXIT_FAILURE);}if (pid > 0) { // 父进程// 关闭管道的写端close(pipefd[1]);// 从管道的读端读取数据char buffer[128];ssize_t bytes_read = read(pipefd[0], buffer, sizeof(buffer) - 1);if (bytes_read > 0) {buffer[bytes_read] = '\0'; // 确保字符串以 null 结尾std::cout << "Parent received: " << buffer << std::endl;} else {std::cerr << "Failed to read from pipe" << std::endl;}// 关闭管道的读端close(pipefd[0]);} else { // 子进程// 关闭管道的读端close(pipefd[0]);// 向管道的写端写入数据const char *msg = "Hello from child process!";ssize_t bytes_written = write(pipefd[1], msg, strlen(msg));if (bytes_written < 0) {std::cerr << "Failed to write to pipe" << std::endl;}// 关闭管道的写端close(pipefd[1]);}return 0;
}
命名管道demo
1. 创建命名管道
我们首先需要创建一个命名管道。这可以通过命令行工具 mkfifo
或在程序中使用 mkfifo()
系统调用来完成。
创建管道的命令行方式:
mkfifo /tmp/myfifo
创建管道的程序方式:
#include <sys/types.h>
#include <sys/stat.h>
#include <iostream>int main() {// 创建命名管道if (mkfifo("/tmp/myfifo", 0666) == -1) {perror("mkfifo");return -1;}std::cout << "Named pipe created at /tmp/myfifo" << std::endl;return 0;
}
2. 写入者进程
写入者进程向命名管道中写入数据。
writer.cpp:
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>int main() {const char *fifo_path = "/tmp/myfifo";const char *message = "Hello from writer process!\n";// 打开管道文件(写模式)int fd = open(fifo_path, O_WRONLY);if (fd == -1) {perror("open");return -1;}// 向管道写入数据if (write(fd, message, strlen(message)) == -1) {perror("write");close(fd);return -1;}std::cout << "Message sent: " << message << std::endl;// 关闭管道文件描述符close(fd);return 0;
}
3. 读取者进程
读取者进程从命名管道中读取数据。
reader.cpp
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>int main() {const char *fifo_path = "/tmp/myfifo";char buffer[128];// 打开管道文件(读模式)int fd = open(fifo_path, O_RDONLY);if (fd == -1) {perror("open");return -1;}// 从管道读取数据ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1);if (bytes_read == -1) {perror("read");close(fd);return -1;}buffer[bytes_read] = '\0'; // 确保字符串以 null 结尾std::cout << "Message received: " << buffer << std::endl;// 关闭管道文件描述符close(fd);return 0;
}
编译与运行
- 创建管道:
- 如果没有提前创建管道,运行
writer
时会阻塞,直到管道被创建。 - 可以手动创建管道:bash复制
- 如果没有提前创建管道,运行
mkfifo /tmp/myfifo
- <font style="color:rgb(6, 6, 7);">或者运行创建管道的程序。</font>
- 编译代码:
- 将
writer.cpp
和reader.cpp
分别保存为文件。 - 使用以下命令编译:bash复制
- 将
g++ -o writer writer.cpp
g++ -o reader reader.cpp
- 运行程序:
- 在一个终端运行读取者程序:
./reader
- <font style="color:rgb(6, 6, 7);">在另一个终端运行写入者程序:</font>
./writer
输出示例
- reader终端:
Message received: Hello from writer process!
- writer终端:
Message sent: Hello from writer process!
注意事项
- 阻塞特性:
- 默认情况下,读取者会阻塞,直到有数据可读。
- 写入者会阻塞,直到有读取者打开管道。
- 如果不希望阻塞,可以在
open()
时使用O_NONBLOCK
标志。
- 管道文件路径:
- 确保管道文件路径一致(这里是
/tmp/myfifo
)。 - 如果路径不存在,写入者会阻塞,直到管道被创建。
- 确保管道文件路径一致(这里是
- 清理:
- 使用完管道后,可以手动删除管道文件:bash复制
rm /tmp/myfifo
- 跨进程通信:
命名管道允许不相关的进程之间通信,因此读取者和写入者可以是完全独立的程序。
通过这个示例,你可以看到命名管道如何实现进程间通信,非常适合需要跨进程传递数据的场景。
信号量
信号量(Semaphore)是一种用于进程间或线程间同步的机制,用于控制对共享资源的访问。它通过维护一个计数器来实现同步,当计数器大于零时,表示资源可用;当计数器为零时,表示资源已被占用。信号量通常用于解决互斥(Mutex)和同步(Sync)问题。
以下是信号量的基本操作:
- P操作(Wait/Down/Decrease):将信号量的值减1。如果减1后信号量的值小于0,则调用进程或线程阻塞,等待信号量的值变为非负。
- V操作(Signal/Up/Increase):将信号量的值加1。如果加1后信号量的值大于0,则唤醒一个等待该信号量的进程或线程。
1. 信号量的分类
信号量主要分为两种:
- 二进制信号量(Binary Semaphore):值只能为0或1,用于互斥访问。
- 计数信号量(Counting Semaphore):值可以是任意非负整数,用于控制多个资源的访问。
2. 信号量的实现
在C++中,信号量可以通过多种方式实现,包括系统V IPC信号量、POSIX信号量、Boost库以及C++20标准中的std::semaphore
。
2.1 系统V信号量
系统V信号量是基于IPC机制的信号量实现,使用semget
、semop
等函数。
示例代码:
cpp复制
#include <iostream>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>// P操作
void P(int semid) {struct sembuf sb;sb.sem_num = 0;sb.sem_op = -1; // 等待信号量sb.sem_flg = 0;semop(semid, &sb, 1);
}// V操作
void V(int semid) {struct sembuf sb;sb.sem_num = 0;sb.sem_op = 1; // 释放信号量sb.sem_flg = 0;semop(semid, &sb, 1);
}int main() {key_t key = ftok("semfile", 65); // 创建IPC keyint semid = semget(key, 1, 0666 | IPC_CREAT); // 创建信号量集// 初始化信号量union semun {int val;struct semid_ds* buf;unsigned short* array;} arg;arg.val = 1; // 初始值为1semctl(semid, 0, SETVAL, arg);// 模拟生产者和消费者if (fork() == 0) {// 子进程:消费者sleep(1); // 等待生产者P(semid); // 等待信号量std::cout << "Consumer: Consumed resource" << std::endl;V(semid); // 释放信号量} else {// 父进程:生产者P(semid); // 等待信号量std::cout << "Producer: Produced resource" << std::endl;V(semid); // 释放信号量}wait(nullptr); // 等待子进程结束semctl(semid, 0, IPC_RMID, arg); // 删除信号量集return 0;
}
2.2 POSIX信号量
POSIX信号量是另一种实现方式,使用sem_open
、sem_wait
和sem_post
等函数。
示例代码:
cpp复制
#include <iostream>
#include <semaphore.h>
#include <thread>
#include <unistd.h>int main() {sem_t sem;sem_init(&sem, 0, 1); // 初始化信号量,初始值为1// 生产者线程std::thread producer([&sem]() {sem_wait(&sem); // 等待信号量std::cout << "Producer: Produced resource" << std::endl;sem_post(&sem); // 释放信号量});// 消费者线程std::thread consumer([&sem]() {sleep(1); // 等待生产者sem_wait(&sem); // 等待信号量std::cout << "Consumer: Consumed resource" << std::endl;sem_post(&sem); // 释放信号量});producer.join();consumer.join();sem_destroy(&sem); // 销毁信号量return 0;
}
2.3 Boost信号量
Boost库提供了跨平台的信号量实现,使用boost::interprocess::named_semaphore
。
示例代码:
cpp复制
#include <boost/interprocess/sync/named_semaphore.hpp>
#include <iostream>
#include <thread>
#include <unistd.h>int main() {using namespace boost::interprocess;named_semaphore sem(open_or_create, "test_semaphore", 1); // 创建或打开信号量// 生产者线程std::thread producer([&sem]() {sem.wait();std::cout << "Producer: Produced resource" << std::endl;sem.post();});// 消费者线程std::thread consumer([&sem]() {sleep(1); // 等待生产者sem.wait();std::cout << "Consumer: Consumed resource" << std::endl;sem.post();});producer.join();consumer.join();named_semaphore::remove("test_semaphore"); // 删除信号量return 0;
}
2.4 C++20 std::semaphore
C++20标准引入了std::semaphore
,使得信号量的使用更加简洁。
示例代码:
cpp复制
#include <iostream>
#include <semaphore>
#include <thread>
#include <unistd.h>int main() {std::counting_semaphore<1> sem(1); // 创建信号量,初始值为1// 生产者线程std::thread producer([&sem]() {sem.acquire(); // 等待信号量std::cout << "Producer: Produced resource" << std::endl;sem.release(); // 释放信号量});// 消费者线程std::thread consumer([&sem]() {sleep(1); // 等待生产者sem.acquire(); // 等待信号量std::cout << "Consumer: Consumed resource" << std::endl;sem.release(); // 释放信号量});producer.join();consumer.join();return 0;
}
3. 信号量的使用场景
信号量主要用于以下场景:
- 互斥(Mutex):确保多个线程或进程不会同时访问共享资源。
- 同步(Sync):协调线程或进程的执行顺序,例如生产者-消费者问题。
4. 注意事项
- 死锁:如果信号量的使用不当,可能会导致死锁。例如,多个线程或进程同时等待同一个信号量。
- 资源泄漏:在使用系统V或POSIX信号量时,需要确保信号量在程序结束时被正确删除,否则可能会导致资源泄漏。
- 性能:信号量的使用会引入上下文切换的开销,因此需要合理设计同步机制。
总结
信号量是一种强大的同步机制,适用于多种并发场景。根据具体需求,可以选择系统V信号量、POSIX信号量、Boost信号量或C++20标准中的std::semaphore
。
消息队列demo
在C++中,消息队列是一种常见的进程间通信(IPC)机制,允许不同进程之间以异步方式交换消息。以下是关于C++消息队列的实现和使用方法的总结:
1. 系统V消息队列
系统V消息队列是一种传统的IPC机制,基于msgget
、msgsnd
和msgrcv
等函数。
示例代码:
cpp复制
#include <sys/ipc.h>
#include <sys/msg.h>
#include <iostream>
#include <cstring>struct message {long msg_type;char msg_text[100];
};int main() {key_t key = ftok("progfile", 65); // 创建IPC keyint msgid = msgget(key, 0666 | IPC_CREAT); // 创建消息队列message msg;msg.msg_type = 1;strcpy(msg.msg_text, "Hello World!");msgsnd(msgid, &msg, sizeof(msg), 0); // 发送消息std::cout << "Message sent: " << msg.msg_text << std::endl;// 接收消息msgrcv(msgid, &msg, sizeof(msg), 1, 0);std::cout << "Message received: " << msg.msg_text << std::endl;msgctl(msgid, IPC_RMID, nullptr); // 删除消息队列return 0;
}
此代码展示了如何创建消息队列、发送和接收消息。
2. POSIX消息队列
POSIX消息队列提供了另一种实现方式,使用mq_open
、mq_send
和mq_receive
等函数。
示例代码:
cpp复制
#include <mqueue.h>
#include <iostream>
#include <cstring>int main() {mqd_t mq;struct mq_attr attr;attr.mq_flags = 0;attr.mq_maxmsg = 10;attr.mq_msgsize = 1024;attr.mq_curmsgs = 0;mq = mq_open("/my_mq", O_CREAT | O_WRONLY, 0644, &attr); // 创建消息队列std::string msg = "Hello POSIX!";mq_send(mq, msg.c_str(), msg.size(), 0); // 发送消息std::cout << "Message sent: " << msg << std::endl;mq_close(mq);mq_unlink("/my_mq"); // 删除消息队列return 0;
}
3. Boost.Interprocess消息队列
Boost库提供了跨平台的消息队列实现,基于共享内存。
示例代码:
cpp复制
#include <boost/interprocess/ipc/message_queue.hpp>
#include <iostream>int main() {using namespace boost::interprocess;// 发送端message_queue mq(open_or_create, "test_mq", 100, sizeof(int));int msg = 42;mq.send(&msg, sizeof(msg), 0);std::cout << "Message sent: " << msg << std::endl;// 接收端int rcv_msg;size_t msg_size;unsigned priority;mq.receive(&rcv_msg, sizeof(rcv_msg), msg_size, priority);std::cout << "Message received: " << rcv_msg << std::endl;message_queue::remove("test_mq"); // 删除消息队列return 0;
}
4. 自定义线程安全消息队列
如果需要在多线程环境中使用消息队列,可以结合std::queue
、std::mutex
和std::condition_variable
实现线程安全的消息队列。
示例代码:
cpp复制
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <iostream>struct Message {int messageType;std::string payload;
};class MessageQueue {
public:void enqueue(const Message& message) {std::unique_lock<std::mutex> lock(mutex_);queue_.push(message);condition_.notify_one();}Message dequeue() {std::unique_lock<std::mutex> lock(mutex_);condition_.wait(lock, [this] { return !queue_.empty(); });Message message = queue_.front();queue_.pop();return message;}private:std::queue<Message> queue_;std::mutex mutex_;std::condition_variable condition_;
};void producer(MessageQueue& mq) {Message msg{1, "Hello"};mq.enqueue(msg);
}void consumer(MessageQueue& mq) {Message msg = mq.dequeue();std::cout << "Received: " << msg.payload << std::endl;
}int main() {MessageQueue mq;std::thread prod(producer, std::ref(mq));std::thread cons(consumer, std::ref(mq));prod.join();cons.join();return 0;
}
总结
- 系统V和POSIX消息队列适用于进程间通信,但需要处理IPC资源的创建和销毁。
- Boost.Interprocess提供了跨平台的实现,基于共享内存。
- 自定义线程安全消息队列适用于多线程环境。
根据具体需求选择合适的消息队列实现方式。
五、进程的调度
进程调度的算法
进程调度的核心是调度算法,不同的算法适用于不同的场景。常见的调度算法包括:
(1)先来先服务(FCFS,First-Come-First-Served)
- 原理:按照进程到达的顺序分配CPU。
- 优点:简单直观。
- 缺点:可能导致短作业等待时间过长(“短作业饥饿”问题)。
(2)短作业优先(SJF,Shortest Job First)
- 原理:优先调度预计运行时间最短的进程。
- 优点:可以有效减少平均等待时间。
- 缺点:可能导致长作业饥饿,且需要预估进程运行时间。
(3)优先级调度(Priority Scheduling)
- 原理:根据进程的优先级分配CPU,优先级高的进程优先运行。
- 优点:可以满足不同进程的紧急程度需求。
- 缺点:低优先级的进程可能会被饿死(“优先级倒置”问题)。
(4)时间片轮转(RR,Round Robin)
- 原理:将CPU时间分成固定长度的时间片(Time Quantum),每个就绪态进程轮流运行一个时间片。
- 优点:公平性好,响应速度快,适合交互式系统。
- 缺点:时间片大小的选择会影响系统性能。
(5)多级反馈队列(Multilevel Feedback Queue)
- 原理:将就绪队列分为多个优先级队列,每个队列采用不同的调度算法。进程在不同队列之间动态迁移。
- 优点:综合了多种调度算法的优点,兼顾公平性和效率。
- 缺点:实现复杂,需要合理设计队列之间的迁移策略。