Skip to content

Momenta后端C++实习面经,C++实现线程池

来源:https://www.nowcoder.com/share/jump/1741012808362

1、Linux内存管理 页表和虚拟地址 为什么要有页表 物理内存不够了怎么办

虚拟内存与地址空间

虚拟内存是一种内存管理技术,为进程提供一个抽象的、连续的内存空间,研究表明这允许进程感知的内存可能大于实际物理内存。

  • 虚拟地址:进程使用的逻辑地址,例如程序中的指针值。它不是直接对应RAM的物理位置,而是通过页表映射。
  • 物理地址:RAM中实际存储数据的地址,由内存管理单元(MMU)根据页表翻译得到。

比如说,两个进程可能都有虚拟地址0x1000,但映射到不同的物理地址,确保隔离。

Linux通过虚拟内存实现多进程并发,每个进程有独立地址空间(通常4GB在32位系统,64位系统更大),这由内核管理。

页表的作用与结构

页表是操作系统维护的数据结构,用于存储虚拟地址到物理地址的映射。

  • 结构:在x86架构中,页表是多级结构(如PML4、PDPT、PD、PT),每个页面通常4KB。
  • 功能:
    • 地址翻译:MMU使用页表将虚拟地址分解为页号和偏移,查找物理页框。
    • 内存保护:页表条目包含权限(如读/写/执行),防止进程访问非法内存。
    • 内存共享:多个进程可映射同一物理页,如共享库。
    • 按需分页:仅在需要时加载页面到内存,优化资源。

为什么需要页表

  1. 内存保护:确保进程只能访问自己内存,防止崩溃或攻击。例如,进程A不能读写进程B的内存。
  2. 内存共享:如多个进程共享同一程序代码(如libc),减少内存占用。
  3. 地址空间隔离:每个进程有独立地址空间,简化编程。
  4. 高效内存利用:通过按需分页和页面置换,允许运行超过物理内存的进程。
  5. 硬件支持:现代CPU依赖MMU和页表实现虚拟内存,Linux充分利用此功能。

物理内存不足时的处理

当RAM(物理内存)不足,Linux使用交换机制:

  • 交换空间:可以是专用分区或文件,扩展内存容量。
  • 分页:虚拟内存分为固定大小的页面(通常4KB),当进程访问不在内存的页面,触发页面错误(page fault)。
  • 页面置换:若RAM满,内核选择一个页面移出,写入交换空间,释放内存。例如,使用LRU(最近最少使用)算法选择牺牲页面。
  • 过程:
    1. 进程访问虚拟地址,MMU发现页面不在RAM,触发页面错误。
    2. 内核检查页面是否在交换空间,若是,从磁盘读入RAM,更新页表。
    3. 若RAM无空闲,选一个页面(可能脏页)写入交换区,腾出空间。
    4. 完成加载,进程继续运行。
  • 性能影响:磁盘I/O远慢于RAM,频繁交换可能导致“抖动”,系统更多时间处理页面而非执行任务,性能下降。

2、每个进程的地址空间同样大吗?

进程地址空间是指进程可以访问的虚拟内存范围,它由操作系统管理,不是直接对应物理内存。每个进程有自己的地址空间,确保隔离和安全。

在32位系统上,每个进程的地址空间通常为4GB,但操作系统可能分给用户空间2-3GB,剩余给内核。

在64位系统上,Windows通常给每个进程8TB用户地址空间,Linux可能高达128TB,但实际可用范围受配置限制。

给个表帮助理解

系统/架构地址空间大小用户空间内核空间备注
32位Windows4GB2GB2GB可配置
32位Linux4GB2-3GB1-2GB配置依赖
64位Windows16EB(理论)8TB8TB+用户模式实际8TB
64位Linux128TB(用户)128TB剩余配置可能限制

3、OOP三特性 C++多态(静态和动态) ?

面向对象编程的三大特性

封装

封装是将数据和操作数据的函数绑定在一起,隐藏内部实现,只暴露必要的接口。例如,银行账户类可以隐藏余额,只提供存款和取款方法,保护数据安全。

继承

继承允许一个类从另一个类继承属性和方法,促进代码复用。例如,储蓄账户可以继承银行账户的基本功能,并添加利息计算。

多态

多态让不同类的对象能以相同方式处理,分为静态多态(编译时)和动态多态(运行时)。

静态多态

编译时多态,编译器根据参数类型决定调用哪个函数。

实现方式:

  1. 函数重载:同名函数,参数列表不同。
c++
void print(int num) {
    std::cout << "Integer: " << num << std::endl;
}
void print(double num) {
    std::cout << "Double: " << num << std::endl;
}
int main() {
    print(5); // 调用print(int)
    print(3.14); // 调用print(double)
    return 0;
}
  1. 模板:泛型编程,编译时生成特定类型版本。
c++
template<typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
}
int main() {
    std::cout << max(5, 10) << std::endl; // max<int>
    std::cout << max(3.5, 2.8) << std::endl; // max<double>
    return 0;
}
动态多态

运行时多态,根据对象实际类型决定调用哪个函数。

实现方式:通过虚函数和继承。

  • 基类声明虚函数(virtual),派生类可重写(override)。
  • 使用基类指针或引用调用,运行时通过虚表(vtable)动态绑定。
c++
class Shape {
public:
    virtual void draw() = 0; // 纯虚函数
    virtual ~Shape() {} // 虚析构函数
};

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a circle" << std::endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a rectangle" << std::endl;
    }
};

int main() {
    Shape* shapes[] = {new Circle(), new Rectangle()};
    for (auto shape : shapes) {
        shape->draw(); // 运行时调用正确版本
    }
    for (auto shape : shapes) {
        delete shape;
    }
    return 0;
}
对比表
特性静态多态动态多态
实现方式函数重载、模板虚函数、继承
绑定时间编译时运行时
性能高,无运行时开销低,虚表查找有开销
灵活性低,类型需编译时确定高,适合运行时类型变化
典型应用数学库、通用算法图形库、事件系统
代码复杂度简单,模板可能膨胀复杂,需管理虚函数和继承

4、函数重载可以仅返回值不同吗 为什么不可以?

函数重载不能仅基于返回值类型不同。

C++中,函数重载主要看函数名和参数列表(包括参数类型和数量),返回值类型不被考虑。如果两个函数有相同的名字和参数列表,但返回值不同,编译器会报错,因为它无法根据调用时的参数决定使用哪个函数。例如,int func()和double func()会引发编译错误,因为参数列表完全相同,编译器无法区分。

原因是,函数调用时我们提供参数,编译器根据参数匹配函数,而返回值类型在调用时并不参与匹配。所以,C++不允许仅靠返回值类型来重载函数,以避免歧义。

5、多进程 多线程 线程池区别 使用场景举例?

多进程

一个应用由多个进程组成,每个进程有独立的内存空间,通过进程间通信(IPC)如管道、套接字或共享内存交换数据。

特点
  • 隔离性:进程独立,崩溃不影响其他进程。
  • 资源管理:每个进程有自己的代码段、数据段、堆和栈,内存开销大。
  • 通信:通过IPC,效率较低,如管道、消息队列。
  • 创建开销:启动新进程(如fork())较慢,涉及内存复制。
优缺点
  • 优点:
    • 高稳定性,适合需要隔离的任务。
    • 可跨机器分布,扩展性强。
  • 缺点:
    • 通信开销高,资源消耗大。
    • 进程切换开销高,影响性能。

多线程

在一个进程内运行多个线程,所有线程共享同一内存空间,通过共享变量通信,但需同步避免竞争。

特点
  • 依赖性:线程依赖进程,共享代码段、数据段和堆,私有栈。
  • 通信:通过共享内存,效率高,但需锁(如std::mutex)同步。
  • 创建开销:启动线程(如std::thread)较快,适合频繁创建。
  • 稳定性:线程崩溃可能影响整个进程。
优缺点
  • 优点:
    • 通信快,适合共享数据场景。
    • 创建和切换开销低,性能优。
  • 缺点:
    • 同步复杂,易引发竞争条件。
    • 线程错误可能导致进程崩溃。

线程池

一种线程管理机制,预创建一组线程,复用执行任务队列中的任务,减少创建销毁开销。

特点
  • 资源管理:固定线程数,任务通过队列分配,防止过载。
  • 效率:避免频繁创建线程,适合短任务。
  • 同步:任务需独立或同步,共享内存需锁。
  • 扩展性:池大小可调,适应负载变化。
优缺点
  • 优点:
    • 高效管理线程,减少开销。
    • 适合高并发短任务,负载均衡。
  • 缺点:
    • 实现复杂,需管理队列和分配。
    • 不适合长任务,池可能阻塞。

对比表

特性多进程多线程线程池
隔离性高,进程独立低,线程共享进程低,池内线程共享内存
通信效率低,需IPC高,共享内存高,池内共享
创建开销高,进程启动慢低,线程创建快低,复用线程
稳定性高,崩溃不影响其他低,线程错影响进程低,池内错误影响进程
适用场景服务器、批处理UI、计算任务Web服务器、短任务
示例Apache Prefork浏览器渲染Nginx请求处理

6、操作系统堆和栈的区别 二者的大小?

区别

  • 堆:程序运行时动态分配的内存区域,由程序员通过new(C++)或malloc(C)申请,使用delete或free释放。
  • 栈:自动管理的内存区域,遵循“后进先出”(LIFO)原则,由编译器分配和释放,主要存储局部变量和函数调用信息。

管理方式

堆需要程序员显式分配和释放,容易出现内存泄漏;栈由系统自动处理,效率高但大小受限。

大小

  • 堆:堆初始小,可根据需要增长,最大受限于可用物理内存和交换空间。
  • 栈:栈大小通常由操作系统或编译器设定,每个线程有独立栈。

堆可以根据需要增长,通常受系统内存限制;栈大小固定,Linux默认线程栈为8MB,Windows可能为1MB(GUI)或256KB(控制台)。

区别表

特性堆(Heap)栈(Stack)
管理方式手动(new/delete)自动(编译器)
分配速度较慢(操作系统查找内存块)很快(调整栈指针)
生命周期程序员控制,存活至释放作用域结束自动释放
内存布局非连续,易碎片连续,无碎片
大小限制较大(受物理内存限制)较小(OS设定,如8MB)
线程安全需同步(如锁)线程私有,天然安全
溢出风险内存不足(分配失败)栈溢出(递归或大变量)
典型用途动态对象、共享资源局部变量、调用帧

7、C++实现线程池?

线程池是一种线程管理模式,预先创建固定数量的线程,任务通过队列分配给空闲线程执行。

线程池的主要作用是:

  • 减少线程创建和销毁的开销,适合短任务频繁执行。
  • 控制并发数量,防止系统过载。
  • 提高资源利用率,优化性能。

实现线程池的步骤:

  1. 任务队列:存储待执行的任务,通常用std::queue或std::deque,每个任务可以是函数对象(如std::function)。
  2. 线程管理:预创建一组线程,循环从任务队列中取任务执行。
  3. 同步机制:使用条件变量(std::condition_variable)和互斥锁(std::mutex)确保线程安全,防止竞争条件。
  4. 任务提交和执行:提供接口提交任务,线程池调度执行。
c++
#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <vector>

class ThreadPool {
private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;

public:
    ThreadPool(size_t num_threads) : stop(false) {
        // 创建num_threads个线程
        for (size_t i = 0; i < num_threads; ++i) {
            workers.emplace_back([this]() {
                while (true) {
                    std::function<void()> task;
                    {
                        std::unique_lock<std::mutex> lock(queue_mutex);
                        condition.wait(lock, [this]() { return stop || !tasks.empty(); });
                        if (stop && tasks.empty()) return;
                        task = std::move(tasks.front());
                        tasks.pop();
                    }
                    task(); // 执行任务
                }
            });
        }
    }

    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            stop = true;
        }
        condition.notify_all();
        for (auto& worker : workers) {
            worker.join();
        }
    }

    // 提交任务
    template<typename F>
    void enqueue(F&& f) {
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            tasks.emplace(std::forward<F>(f));
        }
        condition.notify_one();
    }
};

int main() {
    ThreadPool pool(4); // 创建4个线程的线程池
    for (int i = 0; i < 8; ++i) {
        pool.enqueue([i]() {
            std::cout << "Task " << i << " executed by thread " << std::this_thread::get_id() << "\n";
            std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟工作
        });
    }
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 等待任务完成
    return 0;
}

8、排序链表?