C/C++基础
1. 智能指针实现原理
智能指针通过 RAII 原则,利用 C++ 栈的生命周期来管理堆内存的动态分配和释放,避免手动 delete
操作。
unique_ptr
:管理独占资源,仅支持移动语义,不能复制。shared_ptr
:通过引用计数实现多个对象共享同一资源。weak_ptr
:辅助shared_ptr
,解决循环引用问题。
2. 智能指针的计数器何时改变
shared_ptr
的引用计数在:- 新的
shared_ptr
指向同一资源时增加。 shared_ptr
被销毁或赋值为其他对象时减少。- 当引用计数为 0 时,释放资源。
- 新的
3. 智能指针和管理对象分别在哪个区
- 智能指针本身存储在栈区。
- 它所管理的资源存储在堆区。
4. 面向对象的特性:多态原理
- 多态通过虚函数实现,允许不同类型的对象使用相同的接口。
底层实现:
- 虚函数通过虚表(vtable)实现。
- 每个包含虚函数的类都有一个虚表指针(vptr),在对象创建时初始化。
- 调用虚函数时,依据
vptr
定位到具体类的虚表,执行相应的函数。
5. 介绍虚函数及其实现
虚函数是通过
vtable
实现的。vtable
存储了虚函数的地址。- 每个对象都有一个指向其类的
vtable
的指针vptr
。 - 运行时通过
vptr
查找函数地址,实现动态绑定。
6. 多态和继承在什么情况下使用
- 多态:当需要根据不同子类行为实现动态调用时使用。
- 继承:当需要复用父类代码,且子类与父类有 "is-a" 关系时使用。
7. 除了多态和继承还有什么面向对象方法
- 封装:通过访问修饰符(
public
、protected
、private
)隐藏数据细节。 - 抽象:定义抽象类或接口,强调对象的行为而非实现。
8. C++ 内存分布
- 栈区:局部变量、函数调用信息。
- 堆区:动态分配的内存。
- 全局区:全局变量、静态变量。
- 常量区:字符串字面值、常量。
- 代码区:存储程序的二进制代码。
9. C++ 内存管理(RAII)
RAII 是通过构造函数获取资源,通过析构函数释放资源的设计模式。常用类包括:
std::unique_ptr
和std::shared_ptr
。- 文件流(如
std::fstream
)。
10. C++ 从源程序到可执行程序的过程
- 预处理:处理宏定义和头文件。
- 编译:将 C++ 源代码转换为汇编代码。
- 汇编:将汇编代码转换为机器代码。
- 链接:将多个目标文件和库合并为可执行程序。
11. 一个对象 = 另一个对象会发生什么
调用赋值运算符(operator=
),默认行为是逐成员赋值。可以重载该运算符以实现深拷贝。
12. new
出错直接 return
会导致内存泄漏,怎么办
- 使用智能指针(如
std::unique_ptr
)来管理动态资源。 - 利用 RAII 确保异常时资源自动释放。
13. C++11 的智能指针及循环引用
智能指针:
unique_ptr
:独占资源。shared_ptr
:共享资源。weak_ptr
:避免循环引用。
- 循环引用:两个
shared_ptr
相互引用,引用计数无法归零。通过weak_ptr
打破循环。
14. 多进程 fork
后的共享资源
共享资源:
- 程序代码段。
- 打开的文件描述符。
不共享:
- 堆、栈。
15. 多线程同步方式
- 互斥锁(
std::mutex
) - 读写锁(
std::shared_mutex
) - 条件变量(
std::condition_variable
) - 原子变量(
std::atomic
)
16. sizeof
是在编译期还是运行期确定
- 编译期:
sizeof
是一个编译期操作符,确定类型或对象的大小。
17. 函数重载机制
- 重载基于函数签名(函数名+参数类型)。
- 通过编译期的名称修饰(name mangling)实现。
18. 指针常量与常量指针
- 指针常量(
T* const
):指针地址不可变,但指向的内容可变。 - 常量指针(
const T*
):指针指向的内容不可变,但地址可变。
19. vector
的原理与扩容
- 原理:基于动态数组。
- 扩容:当容量不足时,分配更大的内存块并迁移数据。
20. const
的作用
- 修饰变量:只读变量。
- 修饰函数:保护成员变量不被修改。
- 修饰指针:见问题 18。
21. 引用与指针的区别
- 引用必须初始化,指针可以为空。
- 引用不可改变绑定的对象,指针可以。
22. C++11 新特性
- 智能指针(
unique_ptr
、shared_ptr
)。 - 右值引用与移动语义。
auto
、decltype
。- Lambda 表达式。
继续解答 C/C++ 基础部分的问题:
23. 类型转换
C++ 提供以下类型转换方式:
- C 风格强制转换:
(T)value
。 C++ 风格强制转换:
static_cast
:常规转换,如基本类型间转换、子类到父类的转换。dynamic_cast
:用于多态类型,运行时检查安全性。const_cast
:用于去除或添加const
修饰。reinterpret_cast
:直接改变类型的位解释,例如指针转换为整数。
24. RAII 基于什么实现
RAII 基于对象的 生命周期 和 作用域:
- 资源在对象构造时获取,在对象析构时释放。
- 自动调用构造函数和析构函数确保资源安全。
25. 手撕代码:unique_ptr
实现
以下是简化版 unique_ptr
的实现:
template <typename T>
class UniquePtr {
private:
T* ptr;
public:
explicit UniquePtr(T* p = nullptr) : ptr(p) {}
~UniquePtr() { delete ptr; }
// 禁止拷贝
UniquePtr(const UniquePtr&) = delete;
UniquePtr& operator=(const UniquePtr&) = delete;
// 支持移动
UniquePtr(UniquePtr&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr;
}
UniquePtr& operator=(UniquePtr&& other) noexcept {
if (this != &other) {
delete ptr;
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}
T* get() const { return ptr; }
T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
};
26. unique_ptr
和 shared_ptr
区别
unique_ptr
:独占所有权,不支持拷贝,支持移动。shared_ptr
:共享所有权,支持多个指针共享同一资源,使用引用计数管理资源。
27. 右值引用
- 右值引用(
T&&
)是 C++11 引入的新特性,用于绑定右值(如临时对象)。 主要用于:
- 移动语义:避免拷贝,提高性能。
- 完美转发:结合
std::forward
实现函数模板的参数转发。
28. 函数参数可不可以传右值
可以通过右值引用(
T&&
)传递右值参数:void func(int&& x) { // 使用 x } func(10); // 10 是右值
29. 参考 C/C++ 堆栈实现自己的堆栈
以下是一个简单的基于数组实现的堆栈:
template <typename T>
class Stack {
private:
T* data;
size_t capacity;
size_t topIndex;
public:
explicit Stack(size_t size) : capacity(size), topIndex(0) {
data = new T[capacity];
}
~Stack() { delete[] data; }
void push(const T& value) {
if (topIndex == capacity) throw std::overflow_error("Stack overflow");
data[topIndex++] = value;
}
void pop() {
if (topIndex == 0) throw std::underflow_error("Stack underflow");
--topIndex;
}
T top() const {
if (topIndex == 0) throw std::underflow_error("Stack underflow");
return data[topIndex - 1];
}
bool empty() const { return topIndex == 0; }
};
30. STL 容器底层实现
vector
:动态数组,扩容倍增,线性地址存储。map
:红黑树,支持有序键值对存储。unordered_map
:哈希表,快速键值对查找。list
:双向链表,适合频繁插入和删除。
31. 完美转发
完美转发通过右值引用和 std::forward
实现:
template <typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg)); // 保持参数的值类别
}
- 去掉
std::forward
:可能导致参数的值类别改变,右值被传为左值。
32. unique_lock
和 lock_guard
区别
lock_guard
:简单易用,RAII 机制,构造时加锁,析构时解锁。unique_lock
:灵活控制锁,支持延迟加锁、解锁和重新加锁。
33. C 代码调用 C++ 代码报错原因
名称修饰(name mangling):
- C++ 编译器对函数名进行修饰,而 C 使用未修饰的名字。
解决方法:用
extern "C"
声明:extern "C" void func();
34. 静态多态
- 静态多态在编译期决定,如函数重载和模板。
- 虚函数原理:虚表(
vtable
)和虚表指针(vptr
)。 - 析构函数为何要虚函数:确保基类指针释放派生类对象时调用正确的析构函数。
35. map
用红黑树而非 AVL 树原因
- 红黑树旋转次数少于 AVL 树,插入和删除效率更高。
- AVL 树更适合频繁查询,红黑树更适合综合操作。
36. inline
失效场景
- 函数体太大或递归函数。
- 虚函数的动态调用。
- 编译器决定不内联。
37. struct
和 class
区别
默认访问权限:
struct
:默认public
。class
:默认private
。
- 其他特性相同。
38. 防止头文件多次 #include
使用 include guard:
#ifndef HEADER_FILE #define HEADER_FILE // 头文件内容 #endif
- 或者使用
#pragma once
。
39. Lambda 表达式捕获类型
- 按值捕获(
[=]
):捕获外部变量的副本。 - 按引用捕获(
[&]
):捕获外部变量的引用。 - 混合捕获:指定具体捕获方式,如
[=, &x]
。
40. 友元 (friend
) 的作用
- 允许非成员函数或其他类访问类的私有和保护成员。
41. std::move
- 用于显式将左值转为右值引用,触发移动语义,避免不必要的拷贝。
42. 模板类的作用
- 提供通用实现,支持不同数据类型复用代码。
43. 模板与泛型的区别
- 模板是 C++ 提供的特性,实例化时生成代码。
- 泛型是语言无关的概念,注重类型参数化。
44. new
和 malloc
的区别
new
:调用构造函数,支持类型安全。malloc
:仅分配内存,不调用构造函数。
45. new
可以重载吗?可以改写 new
函数吗?
可以重载
new
运算符:- 用户可以为特定类重载
operator new
和operator delete
以自定义内存分配逻辑。
- 用户可以为特定类重载
示例:
class MyClass { public: void* operator new(size_t size) { std::cout << "Custom new" << std::endl; return malloc(size); } void operator delete(void* ptr) { std::cout << "Custom delete" << std::endl; free(ptr); } }; int main() { MyClass* obj = new MyClass; delete obj; }
46. map
和 unordered_map
的区别与使用场景
实现原理:
map
:基于红黑树,支持有序键值对。unordered_map
:基于哈希表,键值对无序。
时间复杂度:
map
:O(log n)
。unordered_map
:O(1)
平均,O(n)
最坏。
使用场景:
- 如果需要键的有序性,使用
map
。 - 如果只需要快速查找,无需排序,使用
unordered_map
。
- 如果需要键的有序性,使用
47. map
和 unordered_map
是线程安全的吗?
- 都不是线程安全的。如果需要多线程访问,需显式加锁(如
std::mutex
)。
48. 优先队列在 C++ 标准库中的实现
实现原理:
- 基于最大堆(默认)。
- 使用
std::vector
存储数据,通过堆排序保证最大堆性质。
常用方法:
push
:插入元素。pop
:移除最大元素。top
:访问最大元素。
49. GCC 编译过程
四个阶段:
- 预处理:展开宏定义,处理头文件(
gcc -E
)。 - 编译:生成汇编代码(
gcc -S
)。 - 汇编:生成机器代码(
gcc -c
)。 - 链接:生成可执行文件(
gcc
)。
- 预处理:展开宏定义,处理头文件(
示例:
gcc -o program program.c
50. C++ Coroutine(协程)
- 协程是轻量级线程,通过状态保存和恢复实现非阻塞异步操作。
- C++20 引入协程,通过
co_await
、co_yield
和co_return
实现。
51. extern "C"
的作用
- 防止 C++ 编译器对函数名进行名称修饰(name mangling)。
用于在 C++ 中调用 C 函数或让 C 调用 C++ 函数:
extern "C" void func();
52. ELF 文件格式
ELF(Executable and Linkable Format) 是可执行文件格式。
- 包含:头部(header)、段表(section table)、程序头表(program header table)。
- 常见段:
.text
(代码段)、.data
(初始化全局变量)、.bss
(未初始化全局变量)。
53. 符号表
- 符号表包含程序中所有标识符(函数、变量)的名称和地址信息。
使用场景:
- 编译器生成目标文件。
- 调试器用于解析符号。
54. 单元测试在 C++ 中的实现
常用框架:
- Google Test(GTest)。
- Catch2。
测试内容:
- 功能性测试。
- 边界条件测试。
- 性能测试。
数据结构与算法
接下来开始解答 数据结构与算法 部分的问题。以下是逐一解答:
1. 数组和链表的区别与优缺点
数组:
- 优点:随机访问快,空间连续。
- 缺点:插入和删除需要移动元素,扩容耗时。
链表:
- 优点:插入和删除效率高(O(1))。
- 缺点:随机访问慢,需要遍历。
2. 快速排序原理
基于分治思想:
- 选择一个基准值。
- 分区:将小于基准值的元素放左边,大于的放右边。
- 对子序列递归排序。
时间复杂度:
- 平均:O(n log n)。
- 最坏:O(n²)(当选取的基准值极端时)。
3. 堆排序的实现
使用二叉堆(最大堆)实现。
- 构建最大堆。
- 交换堆顶与最后一个元素。
- 调整剩余元素为最大堆。
- 时间复杂度:O(n log n)。
4. 冒泡排序
- 相邻元素比较并交换,大的逐渐冒泡到最后。
时间复杂度:
- 平均:O(n²)。
- 最优(完全有序):O(n)。
5. 二分查找及其复杂度
- 前提:数组有序。
原理:
- 比较目标值与中间值。
- 根据大小决定查找左半部分或右半部分。
- 时间复杂度:O(log n)。
6. 哈希表数据太大导致 rehash 成本高,怎么办?
- 使用分布式哈希表(DHT)分散存储。
- 延迟 rehash:逐步迁移旧桶数据,避免集中操作。
7. 二叉树的前序遍历(非递归)
void preorderTraversal(TreeNode* root) {
if (!root) return;
stack<TreeNode*> s;
s.push(root);
while (!s.empty()) {
TreeNode* node = s.top();
s.pop();
cout << node->val << " ";
if (node->right) s.push(node->right);
if (node->left) s.push(node->left);
}
}
8. 链表反转
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* curr = head;
while (curr) {
ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return prev;
}
9. 二叉树输出每一层最右边的节点
使用层序遍历,每层最后一个节点加入结果。
vector<int> rightSideView(TreeNode* root) { if (!root) return {}; vector<int> result; queue<TreeNode*> q; q.push(root); while (!q.empty()) { int size = q.size(); for (int i = 0; i < size; ++i) { TreeNode* node = q.front(); q.pop(); if (i == size - 1) result.push_back(node->val); if (node->left) q.push(node->left); if (node->right) q.push(node->right); } } return result; }
10. 求数组中最大的 k 个数
使用最小堆:
vector<int> topK(vector<int>& nums, int k) { priority_queue<int, vector<int>, greater<int>> minHeap; for (int num : nums) { minHeap.push(num); if (minHeap.size() > k) minHeap.pop(); } vector<int> result; while (!minHeap.empty()) { result.push_back(minHeap.top()); minHeap.pop(); } return result; }
11. 二叉树层高计算
使用递归:
int maxDepth(TreeNode* root) { if (!root) return 0; return 1 + max(maxDepth(root->left), maxDepth(root->right)); }
12. 连续子数组中乘积最大的值
使用动态规划:
int maxProduct(vector<int>& nums) { int maxProd = nums[0], minProd = nums[0], result = nums[0]; for (int i = 1; i < nums.size(); ++i) { if (nums[i] < 0) swap(maxProd, minProd); maxProd = max(nums[i], nums[i] * maxProd); minProd = min(nums[i], nums[i] * minProd); result = max(result, maxProd); } return result; }
13. 排序算法的稳定性
- 稳定排序:冒泡排序、归并排序、插入排序。
- 不稳定排序:快速排序、选择排序、堆排序。
14. 树的深度和高度
- 深度:从根节点到当前节点的路径长度。
- 高度:从当前节点到叶节点的最长路径。
15. 布隆过滤器
- 原理:使用位数组和多个哈希函数来快速判断一个元素是否存在。
- 优点:内存高效。
- 缺点:可能产生误判(不存在的元素被认为存在)。
16. 为什么不用布隆过滤器
- 误判率:布隆过滤器可能会误判元素存在,但不能确定元素一定不存在。
- 适用场景:适用于大数据量、查找操作频繁且可以容忍误判的情况(例如缓存、搜索引擎等)。
- 不适用场景:需要高精度的数据结构,且需要频繁删除元素时。
17. 图的种类、表示方法与经典算法
图的种类:
- 有向图:边有方向。
- 无向图:边没有方向。
- 带权图:每条边有权重。
- 非带权图:边没有权重。
图的表示方法:
- 邻接矩阵:二维数组表示图的边关系,适合稠密图。
- 邻接表:链表数组表示图的边,适合稀疏图。
经典算法:
- 深度优先搜索(DFS):从一个节点开始,沿着图的分支尽可能深入。
- 广度优先搜索(BFS):从一个节点开始,访问所有邻接节点,逐层进行。
- Dijkstra 算法:求解单源最短路径,适用于带权图。
- Floyd-Warshall 算法:求解所有节点对之间的最短路径。
18. 求最大 K 个数
使用优先队列(堆)或快速选择算法:
- 堆法:使用最小堆保留 K 个最大元素,时间复杂度 O(n log k)。
- 快速选择法:基于快速排序思想,时间复杂度 O(n)。
19. 大数问题:转换为字符串处理
- 常用方法:将大数转为字符串处理,可以通过字符串操作进行加减乘除等运算。
20. 哈希解决冲突
开放定址法:如果哈希桶已被占用,依次检查下一个位置,直到找到空桶。
- 线性探测:检查下一个位置。
- 二次探测:按二次函数的方式跳跃查找。
- 链地址法:每个桶存储一个链表,冲突的元素通过链表链接。
- 再哈希法:当负载因子达到某个阈值时,重新哈希并将数据插入新的哈希表。
21. 链地址法与再哈希法的区别与应用场景
链地址法:
- 将所有哈希冲突的元素存储在链表中。
- 空间开销较大,但查找过程较为简单。
再哈希法:
- 当哈希表填充度过高时,重新哈希并增加表的大小。
- 比较适用于较为密集的数据集,能够减少冲突。
22. 链表和数组的底层结构设计、关联、区别、应用场景
链表:
- 存储在非连续内存位置,指针连接每个元素。
- 适合动态插入和删除,但随机访问慢。
数组:
- 存储在连续的内存块中,支持高效的随机访问。
- 适合频繁的读取操作,插入和删除操作较慢。
23. 死锁的概念与进程调度算法如何解决死锁
- 死锁:多个进程因相互等待对方释放资源而陷入无限等待状态。
死锁的四个必要条件:
- 互斥:至少一个资源被独占。
- 请求与保持:进程已持有一个资源,并请求另一个资源。
- 不可抢占:资源不可从进程中强制抢占。
- 循环等待:存在一种进程等待链。
解决方法:
- 资源分配图。
- 检测和恢复:通过定期检查死锁情况。
- 防止死锁:破坏死锁四个条件中的一个。
24. 数据结构相关:图的种类,表示方法,图的经典算法
图的表示方法:
- 邻接矩阵(适合稠密图)。
- 邻接表(适合稀疏图)。
图的经典算法:
- DFS:深度优先搜索,适合查找路径、连通分量等。
- BFS:广度优先搜索,适合寻找最短路径(无权图)。
- Dijkstra:求解单源最短路径,适用于带权图。
- Kruskal:最小生成树算法,使用并查集优化。
- Prim:最小生成树算法,适用于稠密图。
25. 如何优化一个大数据集的排序
使用外部排序方法:
- 归并排序:将数据分块存储在外部存储中,然后使用归并算法进行排序。
- 分布式排序:将数据分发到不同机器处理,最终合并结果。
26. 排序算法的稳定性和适用场景
稳定排序:冒泡排序、插入排序、归并排序。
- 稳定性:若两个相等的元素在排序后相对位置不变,则为稳定排序。
不稳定排序:快速排序、堆排序。
- 适用于不关心元素相对位置的情况。
GDB/GCC/G++
1. 怎么 debug,怎么看内存泄漏
调试:
- 使用
gdb
调试器进行代码调试,设置断点、单步执行、查看变量值等。 常用命令:
gdb ./program
启动调试器。break <line_number>
设置断点。run
启动程序。next
单步执行。print <variable_name>
查看变量值。
- 使用
查看内存泄漏:
使用 Valgrind 工具检查内存泄漏:
valgrind --leak-check=full ./program
- 它会显示内存泄漏的具体信息,如泄漏的内存地址和大小。
2. GDB 使用 -> 多线程程序切换到某线程栈帧 -> 如何查看寄存器值
切换到指定线程:
在 GDB 中使用命令:thread <thread_id>
这会切换到指定线程的栈帧。
查看寄存器值:
info registers
或者:
info register <register_name>
这样可以查看线程的寄存器状态。
3. 怎么分析 C++ 的 core 文件
生成 core 文件:
- 使用
ulimit -c unlimited
打开 core 文件生成。 - 在程序崩溃时,系统会生成 core 文件,包含崩溃时的堆栈信息。
- 使用
使用 GDB 分析:
gdb ./program core
然后使用
bt
(backtrace)查看崩溃时的调用栈。
4. GDB 有哪些命令
run
:启动程序。break
或b
:设置断点。next
或n
:执行下一行。step
或s
:进入函数内逐行执行。continue
或c
:继续执行直到下一个断点。print <variable>
:打印变量值。bt
:显示调用栈。info locals
:查看局部变量。info threads
:查看当前所有线程。
5. GCC 和 G++ 的区别
- GCC:GNU 编译器集合(GNU Compiler Collection),包括 C、C++、Fortran 等多种编译器。主要用于 C 语言的编译。
- G++:GNU C++ 编译器,专门用于编译 C++ 程序,它实际上是 GCC 的一部分,默认启用 C++ 编译选项。
6. Linux 下程序有问题,如何调试?(答 GDB 打开,打上 Breakpoint 进行调试)
使用 GDB 调试:
- 编译时使用
-g
选项生成调试信息。 - 通过
gdb ./program
启动调试器。 - 设置断点:
break <function_name>
或break <line_number>
。 - 启动程序:
run
。 - 步进执行:
next
、step
、continue
。 - 查看变量:
print <variable>
。
- 编译时使用
7. GDB 中多线程调试
- 查看当前线程:
info threads
。 - 切换线程:
thread <thread_id>
。 - 查看线程栈帧:
backtrace
。 - 查看寄存器:
info registers
。
操作系统
1. 线程和进程的区别、应用场景
进程:
- 操作系统中资源分配的基本单位。
- 进程拥有独立的地址空间、代码、数据和文件描述符。
- 应用场景:适合需要高隔离和独立资源的任务,如运行独立的应用程序。
线程:
- 是进程中的执行单元,多个线程共享进程的资源(如地址空间)。
- 线程之间的切换比进程切换更轻量。
- 应用场景:适合需要并发执行的任务,如 Web 服务器、并行计算等。
2. 多线程中各种锁,读写锁,互斥锁
- 互斥锁(Mutex):用来保证同一时间只有一个线程能访问共享资源。通常用于保护临界区。
读写锁(Read-Write Lock):
- 允许多个线程同时读共享数据,但写操作是独占的。
- 适合读多写少的场景,例如数据库缓存。
- 自旋锁(Spinlock):当线程无法获取锁时,它会在原地循环等待,而不是被阻塞。适合锁持有时间非常短的情况。
3. 内存池
- 内存池是一种预分配固定大小内存块的方式,用于减少频繁的内存分配和释放操作,避免内存碎片化。
典型应用:
- 在嵌入式系统中优化内存管理。
- 高性能的数据库引擎和游戏引擎常常使用内存池。
4. 内存管理
内存分配策略:
- 动态分配:操作系统通过堆和栈来管理内存,动态分配内存。
- 静态分配:程序编译时分配内存,通常用于全局变量和静态变量。
内存回收:
- 垃圾回收:自动管理内存的回收(如 Java 中的垃圾回收器)。
- 手动回收:如 C 和 C++ 中的
free()
和delete
。
5. 内存写漏
- 内存写漏(Memory Leak):程序在运行时分配了内存,但未释放,导致无法再使用这些内存。
解决方案:
- 定期进行内存泄漏检查,如使用 Valgrind。
- 使用智能指针(如
std::unique_ptr
和std::shared_ptr
)来自动管理内存。
6. 如果频繁进行内存的分配释放会有什么问题吗?
- 内存碎片:频繁的内存分配和释放会导致内存碎片,浪费内存空间,导致性能下降。
解决方法:
- 使用内存池技术预先分配一块大内存,避免频繁分配。
- 优化数据结构,尽量减少动态内存分配。
7. 如果频繁分配释放的内存很大(>128K),怎么处理?
- 内存池管理:为较大块内存分配一个专门的内存池,避免频繁的操作系统调用
malloc
和free
。 - 内存分配器优化:使用定制的内存分配器(如
tcmalloc
)来优化大内存块的分配和释放。
8. 虚拟内存以及堆栈溢出相关的问题,堆栈溢出怎么处理
- 虚拟内存:每个进程都有自己的虚拟地址空间,操作系统通过虚拟内存将物理内存和磁盘交换空间结合,提供隔离和内存管理。
堆栈溢出:由于递归或栈空间分配过大,导致栈空间溢出。
- 解决方法:减少递归深度,增加栈空间,使用堆代替栈。
9. 分段和分页的区别
- 分段:将程序分成逻辑上独立的段(如代码段、数据段),每个段可以有不同的大小。
- 分页:将内存分成固定大小的块(页),并通过页表来管理。分页可以避免内存碎片,适合进程间隔离。
10. 进程间通信原理和方式
- 进程间通信(IPC):用于不同进程之间交换数据,防止进程独立运行。
常见方式:
- 管道(Pipe):父子进程之间单向通信。
- 消息队列(Message Queue):进程之间的消息传递。
- 共享内存(Shared Memory):多个进程共享内存区域。
- 信号量(Semaphore):用于进程同步。
11. fork()
读时共享写时拷贝
fork()
创建一个子进程,父子进程共享代码段和只读数据,使用 写时拷贝(Copy-on-Write, COW) 技术优化内存分配。- 读时共享:父子进程共享代码、只读数据。
- 写时拷贝:当子进程写入时,操作系统复制内存页,避免父进程被修改。
12. 互斥锁 + 条件变量
- 互斥锁(Mutex):保证同一时间只有一个线程访问共享资源。
- 条件变量:允许线程在特定条件成立时等待,通常与互斥锁配合使用来解决线程间的同步问题。
13. 如果非堆内存一直在增长,可能哪个区域的内存出了问题(Java)
- 堆内存通常用于动态分配对象。
- 栈内存用于局部变量和函数调用信息。
- 如果非堆内存增长,可能是由于 栈溢出 或 全局/静态变量泄漏。
14. 堆和栈的区别。什么情况下会往堆里放
- 栈:用于局部变量、函数调用和返回地址。由操作系统自动管理。
- 堆:用于动态分配内存(如
malloc
或new
)。由程序员管理,需要手动释放。 什么时候往堆里放:
- 存储生命周期不确定的对象,或需要在多个函数间共享的对象。
15. fork()
函数返回值是怎么实现的
- 父进程:
fork()
返回子进程的 PID(进程 ID)。 - 子进程:
fork()
返回 0。
16. 用户级线程和内核级线程的区别
- 用户级线程:由用户空间的线程库管理,操作系统不直接调度用户级线程。线程调度更灵活,但缺乏内核支持。
- 内核级线程:由操作系统内核管理,每个线程都由内核调度。优点是支持多核处理器,但上下文切换和调度开销较大。
17. 线程池和线程开销
- 线程池:维护一组线程,任务提交时直接交给空闲线程处理。减少线程创建和销毁的开销,提高性能。
- 线程开销:线程创建、上下文切换和同步操作都需要一定的开销,线程池通过复用线程减少这些开销。
18. 线程切换的到底是什么
线程切换:指操作系统将 CPU 的控制权从一个线程转移到另一个线程。包括:
- 保存当前线程的状态(如寄存器、栈指针等)。
- 加载新线程的状态,恢复执行。
- 由于线程切换需要保存和恢复大量的上下文信息,因此开销较大。
19. 线程同步共享怎么实现
线程同步:通过互斥锁、信号量、条件变量等机制来确保多个线程访问共享资源时不会发生冲突。
- 互斥锁(Mutex):确保同一时刻只有一个线程访问共享资源。
- 读写锁(Read-Write Lock):允许多个线程同时读取数据,但写入时需要独占锁。
20. 互斥同步的方法
- 互斥锁:通过
std::mutex
或pthread_mutex_t
来保证临界区内只有一个线程能执行。 - 读写锁:通过
std::shared_mutex
或pthread_rwlock_t
来优化读取操作,支持多个线程并发读,但写时需要独占锁。
21. 信号量和自旋锁的区别
信号量:可以用于多线程同步和互斥,通常涉及线程阻塞和唤醒。
- 应用场景:线程间的资源管理、生产者消费者问题。
自旋锁:当一个线程无法获取锁时,它会在原地循环等待,不会阻塞线程。
- 应用场景:锁持有时间较短时,避免线程阻塞的开销。
22. 查看磁盘、CPU 占用、内存占用命令
- 磁盘占用:
df
、du
。 - CPU 占用:
top
、htop
、ps
。 - 内存占用:
free
、top
、vmstat
。
23. Linux 虚拟地址空间结构/动态库地址无关代码
- 虚拟地址空间:每个进程有独立的虚拟地址空间,包括堆、栈、代码段、数据段等。
- 动态库地址无关:程序和动态库的地址在进程中通过加载器分配,不受物理内存地址影响,支持地址空间布局随机化(ASLR)提高安全性。
24. top
命令排查高占有率进程,top
命令的占用率怎么算的
top
命令通过周期性地更新进程信息来显示系统的资源使用情况。- 占用率计算:
top
命令显示的 CPU 和内存占用率是每个进程相对于系统总资源的使用比例。
25. 谈谈进程创建后在 Linux 中的内存分布?(回答内存四区,虚拟地址空间,栈内存堆内存)
在 Linux 中,每个进程都有独立的虚拟地址空间。虚拟地址空间通常分为以下四个部分:
- 代码段(Text Segment):存放程序的可执行代码,通常是只读的。
- 数据段(Data Segment):存放程序的已初始化的全局变量和静态变量。
- BSS段(Block Started by Symbol):存放程序中未初始化的全局变量和静态变量。
- 堆(Heap):存放动态分配的内存(通过
malloc
或new
)。堆的增长方向是向上(地址增大)。 - 栈(Stack):存放函数的局部变量和调用信息。栈的增长方向是向下(地址减小)。
虚拟地址空间:
- 每个进程都有独立的虚拟地址空间,虚拟地址空间通过页表映射到物理内存。
- 操作系统使用 内存管理单元(MMU) 将虚拟地址转换为物理地址。
26. 在 Linux 系统下,使用 for 循环,一直进行 new 操作,会发生 heap-overflow 吗?如果不会,原因呢?(答应该不会,Linux 系统可能会对此情况进行处理,面试官追问如果不用 C++ 而用 Java 呢,答 Java 虚拟机等,胡扯了一些)
C++ 中
new
操作:在 C++ 中使用new
分配内存时,内存是从堆上分配的。堆内存的大小通常是由操作系统设定的,当堆内存用尽时,会抛出std::bad_alloc
异常,而不是发生堆溢出。- Linux 系统:操作系统通过 虚拟内存管理 和 分页机制 来处理堆内存的使用。即使堆内存达到最大限制,操作系统也能通过 交换空间(Swap Space) 将部分数据转移到磁盘,避免内存溢出。
- Java 中的内存分配:Java 通过 JVM 管理内存,类似于 C++ 中的堆,但是 Java 堆内存的管理更加自动化。JVM 会使用垃圾回收机制自动清理不再使用的对象,防止内存泄漏。
27. 死锁的概念,进程调度算法怎么解决死锁
死锁:发生在多个进程或线程相互等待对方释放资源,而导致无法继续执行的情况。死锁的四个必要条件:
- 互斥条件:至少有一个资源被独占。
- 请求与保持条件:一个进程已经持有至少一个资源,但请求更多资源并等待。
- 不可抢占条件:已分配的资源不能被抢占。
- 循环等待条件:存在一组进程,进程间互相等待对方释放资源。
解决死锁的方法:
- 死锁预防:通过破坏死锁的四个条件来避免死锁。例如,通过确保资源请求时所有资源都可用来避免请求与保持条件。
- 死锁避免:使用银行家算法来动态分配资源,确保系统始终处于安全状态。
- 死锁检测与恢复:通过定期检查系统状态是否进入死锁,并采取相应的措施(如回滚进程、终止进程等)。
28. 讲讲进程管理
进程管理:操作系统对进程的创建、调度、执行、终止等进行管理。
进程的状态:进程在执行过程中会经历多个状态:
- 就绪(Ready):进程已加载到内存,等待 CPU 时间片。
- 执行(Running):进程正在 CPU 上运行。
- 阻塞(Blocked):进程在等待某些资源(如 I/O 操作),无法继续执行。
- 终止(Terminated):进程执行完成或被强制终止。
进程调度:操作系统通过进程调度算法选择哪个进程使用 CPU。常见的调度算法有:
- 先来先服务(FCFS):按照进程到达的顺序调度。
- 最短作业优先(SJF):优先调度执行时间短的进程。
- 时间片轮转(RR):每个进程分配固定时间片,时间片耗尽后轮换。
- 优先级调度:根据进程的优先级来决定调度顺序。
- 进程控制块(PCB):操作系统为每个进程维护一个数据结构(PCB),用于存储进程的状态、程序计数器、CPU 寄存器、内存管理信息等。
系统编程
1. 除了 MQ 和 WebSocket,异步通信的其他方法
- 管道(Pipe):提供父子进程之间的单向通信。
- 共享内存(Shared Memory):不同进程可以访问同一块内存区域,实现高效的异步通信。
- 消息队列(Message Queue):在不同进程间传递数据,支持优先级。
- 信号量(Semaphore):通过信号量同步进程的操作。
2. 为什么要用多线程?多进程可以吗(WebServer的)
- 多线程:适用于 Web 服务器等高并发场景,可以减少进程切换的开销,多个线程共享同一进程的内存和资源,通信和同步更简单。
- 多进程:每个进程有独立的资源,适用于需要高度隔离的应用。多进程虽然隔离性好,但代价较高,进程间通信更复杂且需要额外资源。
3. 为什么要用线程池,线程池中的线程是怎么运作的?
- 线程池:通过维护一组线程来避免频繁创建和销毁线程的开销,提高性能。线程池中的线程在空闲时会等待任务,任务到来时会从任务队列中取出并执行,执行完后返回线程池中,等待下一个任务。
4. 生产者消费者,信号量的使用
生产者消费者:通过一个共享缓冲区来实现数据的生产和消费,使用信号量控制生产者和消费者的操作。
- 生产者:如果缓冲区满,等待空信号量;如果缓冲区未满,则生产数据并释放满信号量。
- 消费者:如果缓冲区为空,等待满信号量;如果缓冲区有数据,则消费数据并释放空信号量。
5. 队列空时,消费者和生产者会发生什么?线程池请求队列是用什么实现的?(链表)
- 消费者与生产者:当队列为空时,消费者将等待,直到生产者将数据放入队列。如果队列满,则生产者等待。
- 线程池请求队列:通常使用链表实现,能够动态地增加和删除任务。
6. C++ 多线程并发问题(千万级数量级怎么处理)
- 使用线程池来处理大量的并发任务,避免频繁创建销毁线程带来的性能损失。
- 使用无锁数据结构和原子操作(如
std::atomic
)来保证线程安全,减少锁的竞争。 - 对于大量数据,使用分布式系统来分担计算负载。
7. 哪几种常见的 signal? SIGSEGV... -> 正常终止程序的信号?-> kill 进程,几号信号?
常见信号:
- SIGSEGV:段错误,非法内存访问。
- SIGTERM:正常终止程序。
- SIGKILL:强制终止进程。
- SIGINT:由用户发出的中断(Ctrl+C)。
- 正常终止程序的信号:
SIGTERM
。 kill
进程信号:kill -9 <pid>
发送SIGKILL
信号。
8. 什么情况下会使用静态变量
静态变量:在函数或类中声明,生命周期贯穿程序的整个运行过程,适用于需要在多个函数调用间共享状态的场景。
- 示例:用静态变量统计函数被调用的次数。
9. 多线程读写同一个静态变量你是怎么解决的?
使用互斥锁(mutex)来同步访问静态变量,确保在同一时刻只有一个线程能够读写静态变量。
- 示例:
std::mutex mtx; std::lock_guard<std::mutex> lock(mtx);
- 示例:
10. 用过无锁编程吗,知道原子操作吗?
- 无锁编程:通过原子操作来避免线程加锁,从而提高性能,常用于多线程共享数据时的同步。
- 原子操作:如
std::atomic
提供的操作,可以确保某些数据操作是原子的,即使在多线程环境中也不会被中断。
定时器
1. 小根堆定时器是怎么弄的。如果一次 pop
一个的话,高并发情况下会不会有问题
- 小根堆定时器:小根堆是一个优先队列,其中根节点始终是最小的定时任务。定时器通常是通过维护一个小根堆来实现的。每个定时任务都由一个时间戳和任务本身组成,堆的根节点始终是最近到期的任务。
- 高并发问题:如果定时器使用
pop
从堆中取出任务,一次性pop
一个任务,可能会存在并发访问问题。在高并发场景下,可能会有多个线程同时访问堆,导致竞争条件或数据不一致。为了避免这个问题,可以使用线程安全的堆实现,例如使用互斥锁(mutex
)保护堆操作,或者使用无锁的并发数据结构(如std::atomic
和std::shared_lock
)。
2. 心跳检测如何实现
- 心跳检测:用于保证客户端与服务器之间的连接处于活动状态。心跳机制通过定期发送心跳消息来检测连接是否有效。
实现方式:
- 客户端和服务器约定一个时间间隔,例如每隔 30 秒,客户端向服务器发送一个心跳包。
- 如果服务器在预定时间内没有接收到心跳包,可以认为客户端已经断开连接。
- 可以使用 定时器 在客户端定期触发心跳包的发送,并在服务器端设定接收心跳的超时时间。
3. 为什么用小根堆实现定时器
小根堆实现定时器的优势:
- 高效:小根堆可以在对数时间内找到最小的定时任务(即最早到期的任务),这对于定时任务的管理是非常高效的。
- 操作简单:插入和删除任务的时间复杂度为 O(log n),能够快速处理任务的添加和移除。
- 自动排序:使用堆可以自动对任务进行排序,无需手动维护任务队列。
- 应用场景:适用于任务定时执行的场景,如定时任务调度、延迟任务等。
网络原理
1. 为什么握手是三次而挥手需要四次
三次握手:
- 客户端 -> 服务器:客户端发送 SYN 请求建立连接。
- 服务器 -> 客户端:服务器确认 SYN 请求,发送 SYN+ACK 包。
- 客户端 -> 服务器:客户端确认 ACK 包,连接建立成功。
四次挥手:
- 客户端 -> 服务器:客户端发送 FIN 请求关闭连接。
- 服务器 -> 客户端:服务器确认客户端的 FIN 包,发送 ACK。
- 服务器 -> 客户端:服务器发送 FIN 包,要求关闭连接。
- 客户端 -> 服务器:客户端确认服务器的 FIN 包,连接关闭。
三次握手是为了确保双方的连接都可以建立,而四次挥手是为了确保双方的连接都能干净地断开。
2. TCP 和 UDP 的原理、区别、应用场景
TCP:
- 连接导向:可靠的连接,数据传输前需要建立连接(三次握手)。
- 可靠性:通过校验和、序列号、确认号等机制确保数据的可靠传输。
- 流量控制和拥塞控制:保证网络拥塞时传输的稳定性。
- 应用场景:如网页浏览、电子邮件、文件传输等需要可靠性保证的应用。
UDP:
- 无连接:数据包发送后无需等待确认,适合实时性要求高的场景。
- 不保证可靠性:数据可能丢失或顺序错乱,但其速度更快。
- 应用场景:如视频流、实时游戏、DNS 查询等。
3. TCP 慢启动,拥塞控制实现
- 慢启动:当连接开始时,TCP 将拥塞窗口设置为 1,随着每个确认包的到来,拥塞窗口指数增长,直到达到阈值。
- 拥塞避免:当达到阈值时,增长变为线性。
- 快速重传:当丢包时,接收方发送重复的确认包,发送方会在收到三个重复确认包后立即重传丢失的数据包。
4. HTTP 是在 OSI 模型的哪一层
- HTTP 是应用层协议,位于 OSI 模型的第七层(应用层),主要用于客户端和服务器之间的请求和响应。
5. HTTPS 用到的是对称加密还是非对称加密?分别体现在哪里?
- 非对称加密:用于安全握手阶段,服务器使用公钥加密,客户端使用私钥解密。
- 对称加密:用于传输数据阶段,双方通过密钥加密数据,保证传输内容的机密性。
6. HTTP2 和 HTTP1 的区别
HTTP/1:
- 每个请求响应都需要建立新的连接(除非使用持久连接)。
- 存在队头阻塞问题。
HTTP/2:
- 支持多路复用,允许多个请求和响应共享一个连接。
- 使用二进制协议,提高了传输效率。
- 支持头压缩,减少数据传输的开销。
7. HTTP1.0 / 1.1 / 2 / 3
- HTTP/1.0:每个请求和响应使用独立的 TCP 连接,效率较低。
- HTTP/1.1:支持持久连接,减少了重复连接的开销,但仍存在队头阻塞问题。
- HTTP/2:解决了 HTTP/1.x 的问题,支持多路复用和流优先级,减少延迟。
- HTTP/3:基于 QUIC 协议,使用 UDP 传输,进一步减少延迟,改善网络环境下的性能。
8. GET 和 POST 区别
- GET:请求数据的方式,数据附加在 URL 后,适合获取资源,不建议传输敏感数据。
- POST:提交数据的方式,数据放在请求体中,适合提交表单数据,传输大量数据时更加安全。
9. WebSocket
- WebSocket 是一种全双工通信协议,建立在 TCP 连接之上,允许客户端和服务器之间进行实时双向通信,适合即时消息、在线游戏等应用。
10. TCP/IP 五层模型
- 应用层:处理高层协议如 HTTP、FTP、SMTP 等。
- 传输层:负责数据传输协议(TCP、UDP)的处理。
- 网络层:负责 IP 路由和地址分配。
- 数据链路层:处理物理地址和帧的传输。
- 物理层:实际的硬件传输。
11. DNS 服务器用的是什么协议
- DNS 使用的是 UDP 协议,因为 DNS 查询通常要求快速响应,不需要可靠传输。
12. Ping 命令用的是什么协议?在哪一层
- Ping 使用的是 ICMP(Internet Control Message Protocol) 协议,位于 网络层。用于测试网络连接性,检查目标主机是否可达。
13. 如果解析 HTTP 请求的时候,用户一次性没传完数据,(如果头部都没传完,请求报文长度字段都没传完,怎么办)
解决方法:
- 在接收 HTTP 请求时,通常使用 流式解析 方法。在接收到部分数据时,服务器将等待更多的数据直到完整的报文被接收为止。
- HTTP 请求报文 是通过数据流传输的,在传输过程中如果数据不完整,服务器会暂停解析,直到接收到完整的请求头和请求体。
- 如果头部信息不完整,服务器会继续等待,直到头部字段完全接收。
14. 路由表说一下
路由表 是操作系统中存储路由信息的数据结构。它用于根据目标 IP 地址决定数据包的转发路径。每个路由表条目通常包括:
- 目标网络:要到达的目的地 IP 地址或网络。
- 子网掩码:用于与目标 IP 地址匹配的掩码。
- 网关:下一跳路由器的 IP 地址。
- 接口:通过哪个网络接口发送数据包。
15. 路由表为空怎么找到下一跳
- 如果路由表为空,无法找到明确的下一跳。通常,操作系统会使用 默认路由(默认网关)。如果没有设置默认路由或网络路径不可达,数据包将无法被传递。
下一跳的查找:
- 通过操作系统的网络配置文件(如 Linux 上的
/etc/network/interfaces
或ip route
命令)来设置默认路由。 - 默认路由一般是指向本地网络外的网关。
- 通过操作系统的网络配置文件(如 Linux 上的
16. 粘包拆包是什么,发生在哪一层
粘包与拆包:这是 TCP 层面常见的问题,发生在数据传输过程中。
- 粘包:多个小包被合并成一个大包,接收方无法知道数据的边界。
- 拆包:一个大包被拆分成多个小包,接收方无法知道数据完整性。
解决方案:
- 在应用层通过 定长消息包 或 特殊分隔符(如 HTTP 协议中的换行符)来识别数据边界。
- TCP 协议本身 并不保证消息边界的完整性,因此这类问题通常发生在 应用层。
17. TCP 在什么情况下会出现大量 TIME_WAIT,哪个阶段出现
TIME_WAIT 状态:TCP 连接关闭后,客户端进入 TIME_WAIT 状态,等待足够长的时间以确保对方收到关闭连接的请求。
- 发生阶段:在 连接终止 阶段(由
FIN
包发起的关闭过程)。 - 原因:TIME_WAIT 状态的存在是为了确保在对方没有收到
FIN
包时,能重新发送。它确保即使出现网络延迟,关闭连接的请求也能完全传递。 - 问题:大量的
TIME_WAIT
状态会消耗系统资源,尤其在高并发场景中,可能导致端口耗尽。
- 发生阶段:在 连接终止 阶段(由
18. TCP 包头字段,标志位 -> 建立连接过程,终止连接过程 -> TIME_WAIT, CLOSE_WAIT 分析,属于哪一方?
SYN、ACK 标志位:
- SYN:同步标志位,用于连接请求。
- ACK:确认标志位,用于确认接收到的数据包。
建立连接过程(SYN, SYN-ACK, ACK):
- 客户端发送
SYN
包请求建立连接。 - 服务器响应
SYN-ACK
包确认。 - 客户端再发送
ACK
包,连接建立。
- 客户端发送
终止连接过程(FIN, ACK):
- 任何一方发起连接关闭时,发送
FIN
包表示关闭连接,另一方回应ACK
确认。 - TIME_WAIT 状态:通常由客户端进入,确保连接关闭后不会遗留数据。
- CLOSE_WAIT 状态:通常由服务器进入,表示服务器已收到关闭请求并正在准备关闭连接。
- 任何一方发起连接关闭时,发送
19. TCP 建立连接过程 -> SYN + ACK 包能不能拆开来发
不能拆开发:TCP 的连接建立过程严格按照顺序进行。
- SYN 包:用于发起连接请求。
- SYN + ACK 包:用于确认连接请求并返回给客户端。
- 如果将这两个包拆开发送,会导致连接建立失败,因为 TCP 规定客户端收到 SYN 后,必须发送带有 SYN 和 ACK 的响应包。
20. 讲讲 QUIC / 听说过哪些快速重传算法 / TIME_WAIT 状态干啥用的
QUIC(Quick UDP Internet Connections):是基于 UDP 协议的传输协议,由 Google 提出。相比 TCP,QUIC 具有以下优点:
- 更低的连接建立延迟,因为它结合了 TLS 和传输层功能。
- 多路复用和流量控制,避免了 TCP 的队头阻塞。
- 快速重传算法:当发送方收到三个重复的 ACK 包时,它会立即重传丢失的数据包,而不等待超时。
- TIME_WAIT 状态:如前所述,TIME_WAIT 状态用于确保连接的关闭消息能可靠传递,防止遗留的老数据影响新连接。
21. 提到了 TCP,粘包怎么解决?(固定包头接收,指定内存长度)
- 粘包问题:在 TCP 协议中,数据流是连续的,接收方无法知道消息的边界,导致多个包的数据被合并成一个包。
解决方法:
- 固定包头长度:通过约定每个消息的包头包含消息的总长度或数据块的长度,接收方可以根据包头信息确定数据包的边界。
- 分隔符:使用特定的字符(例如换行符、特殊符号)作为消息的结束标志,接收方根据这个分隔符来判断消息的边界。
- 定长消息:所有消息的长度是固定的,接收方每次读取固定长度的数据。
22. 查看网络状况(以为是 netstat,其实是 ping、traceroute,紧张忘记说了)
查看网络状况的常用命令:
netstat
:查看网络连接、路由表、接口统计等。常用于检查网络状态和排除故障。- 示例:
netstat -tuln
(查看所有监听的 TCP/UDP 端口)
- 示例:
ping
:检查目标主机是否可达,发送 ICMP Echo 请求并接收响应。- 示例:
ping <hostname/IP>
- 示例:
traceroute
:显示数据包从源主机到目标主机经过的所有路由节点。- 示例:
traceroute <hostname/IP>
- 示例:
ifconfig
/ip
:查看和配置网络接口。- 示例:
ip addr show
(查看网络接口的 IP 地址信息)
- 示例:
23. 抓包工具?(wireshark,紧张又给忘了靠)
Wireshark 是一款流行的网络抓包工具,用于分析网络数据包、诊断网络问题。Wireshark 可以捕获通过网络传输的所有数据包,并提供详细的协议分析。
功能:
- 实时抓包和分析。
- 支持多种协议,如 TCP、UDP、HTTP、DNS 等。
- 可以显示包的详细信息,帮助开发者调试网络应用。
用法:
- 安装 Wireshark 后,选择网卡开始抓包。
- 可以设置过滤器,仅显示感兴趣的数据包。
- 抓包完成后,可以保存文件,以便后续分析。
24. TCP 2MSL 说一下,为什么
2MSL(Maximum Segment Lifetime)是 TCP 协议中的一个概念,表示 TCP 连接在 TIME_WAIT 状态下持续的时间。
- MSL(Maximum Segment Lifetime):指一个数据包在网络中可能存在的最大时间,通常为 2 分钟。
- 2MSL:在连接关闭后,端点需要保持
TIME_WAIT
状态至少为 2 倍 MSL,确保所有的数据包都已从网络中消失,并且不会干扰未来的连接。 原因:
- 防止因网络延迟或数据包未及时到达,导致重复的老数据干扰到新连接。
- 确保所有的 TCP 数据包都能够被正确传送并确认,防止因数据重传导致问题。
25. 网络层中如何确保路由的选择、处理数据包的转发
网络层的主要任务是路由选择和数据包转发。它通过使用 路由表 来决定数据包的转发路径。
- 路由表 中包含目标地址和下一跳地址。当数据包到达某个路由器时,路由器根据目标地址查找路由表,选择下一个转发节点。
- 路由协议(如 RIP、OSPF、BGP)用于动态更新路由表,确保最优路径的选择。
数据包转发:
- 数据包根据目的地址经过每个路由器时,都会通过路由表查找下一跳的地址。
- 在最终目标到达时,数据包会通过 目的地网络接口 送达接收方。
26. DNS 中的负载均衡
DNS 负载均衡:通过 DNS 服务器提供多个 IP 地址(同一域名对应多个 IP),分配客户端请求到不同的服务器上,从而实现负载均衡。
方法:
- 轮询法(Round-robin DNS):DNS 服务器返回不同的 IP 地址,每次返回的顺序不同。
- 基于权重的负载均衡:为每个 IP 地址分配权重,较高权重的服务器会承担更多的请求。
- 地理位置负载均衡:根据客户端的地理位置,将请求引导到距离最近的服务器。
优点:
- 实现简单,无需额外的硬件设备。
- 支持横向扩展,通过添加更多的 IP 地址进行负载均衡。
网络编程
1. 为什么要用 epoll
epoll 是 Linux 提供的高效 I/O 多路复用机制。它与
select
和poll
相比,在高并发场景中具有显著优势,主要体现在以下几个方面:- 性能:
epoll
在文件描述符数量较多时,不会像select
和poll
一样每次遍历所有文件描述符,避免了 O(n) 的性能瓶颈。 - 事件驱动:
epoll
是基于事件驱动的,只有文件描述符发生变化时,才会通知应用程序,避免了无谓的资源消耗。 - 适用于大规模并发:
epoll
特别适合于需要同时处理大量并发连接的场景,如高性能的 Web 服务器。
- 性能:
2. epoll 实现原理,epoll 使用的哪种模式,除了 epoll,了解 select/poll 吗
epoll 实现原理:
epoll
基于内核和用户空间共享内存的机制。应用程序通过epoll_ctl
操作来注册、删除和修改感兴趣的文件描述符。- 内核会根据事件通知用户空间,用户空间再通过
epoll_wait
获取发生的事件。
模式:
- 水平触发(LT,Level Triggered):当文件描述符的状态满足条件时,
epoll
会通知用户。 - 边缘触发(ET,Edge Triggered):只有文件描述符的状态发生变化时,
epoll
才会通知用户。
- 水平触发(LT,Level Triggered):当文件描述符的状态满足条件时,
select/poll:
select
和poll
都是较早的 I/O 多路复用机制,但它们的性能在处理大量并发连接时较差。select
有最大文件描述符限制,poll
没有限制,但它们在高并发环境中会花费更多时间遍历文件描述符。
3. 怎么理解多路复用机制
多路复用机制:是指通过一个线程或进程处理多个 I/O 操作。在网络编程中,常见的多路复用技术如
select
、poll
和epoll
。- I/O 多路复用:允许一个线程同时监听多个文件描述符(如网络连接)。当文件描述符的状态发生变化时,操作系统通知应用程序。
- 非阻塞 I/O:当没有数据可用时,程序不会被阻塞,而是可以继续执行其他任务,提高资源利用率。
- 事件驱动:在多路复用中,当文件描述符准备好进行 I/O 操作时,会触发事件进行处理,而无需轮询所有文件描述符。
4. Reactor 和 Proactor 的好处和坏处,为什么要用 Reactor 而不用 Proactor
Reactor 模型:
- 工作原理:Reactor 模型是事件驱动的,事件处理通过回调函数来实现。应用程序通过注册感兴趣的事件,Reactor 负责监听并通知。
优点:
- 适用于 I/O 密集型应用,能够高效处理多个并发连接。
- 非阻塞,避免了线程被长时间占用。
缺点:
- 需要开发者手动管理事件循环和回调函数,较为复杂。
Proactor 模型:
- 工作原理:Proactor 模型将 I/O 操作的处理交给操作系统,应用程序只需要处理 I/O 完成事件。
优点:
- 不需要处理 I/O 操作,减少了程序的复杂性。
缺点:
- 操作系统对 I/O 操作的支持有限,且 Proactor 在部分操作系统上可能不被广泛支持。
为什么使用 Reactor 而不用 Proactor:
- Reactor 模型更加灵活,并且可以在所有平台上工作。Proactor 模型虽然适合某些场景,但它对操作系统的支持要求较高,且不适用于所有平台。
5. select 怎么用,底层原理
select 使用方法:
select
用于监听多个文件描述符的状态变化(是否可读、可写、异常)。- 使用
FD_SET
来设置感兴趣的文件描述符集合。 select
调用会阻塞,直到至少一个文件描述符就绪,或者超时。示例:
fd_set readfds; FD_ZERO(&readfds); FD_SET(socket_fd, &readfds); int n = select(socket_fd + 1, &readfds, NULL, NULL, NULL); if (n > 0) { // 处理已就绪的文件描述符 }
底层原理:
select
通过遍历所有文件描述符集合,检查哪些文件描述符的状态满足条件。这会导致较高的性能开销,尤其在高并发场景中。
6. select 为什么只能支持 1024 个,poll 和 epoll 是怎么解决这个问题的
- select 的文件描述符限制:早期的 UNIX 系统对
select
函数的文件描述符集合大小有一个限制,通常为 1024。这个限制是由FD_SETSIZE
定义的。 - poll:
poll
不再有文件描述符数量的硬性限制,它使用pollfd
结构来代替select
中的fd_set
,并且没有FD_SETSIZE
限制。但它依然需要遍历所有的文件描述符,性能在高并发场景中下降。 - epoll:
epoll
通过内核与用户空间共享内存的方式,高效地处理大量文件描述符,避免了select
和poll
的性能瓶颈。epoll
没有文件描述符数量的限制,且只返回已就绪的文件描述符,具有 O(1) 的性能。
7. epoll 底层为什么用红黑树不用 hash
红黑树:
epoll
使用红黑树来管理文件描述符,主要因为红黑树是自平衡的二叉搜索树,能保证插入、删除和查找的时间复杂度为 O(log n)。- 优势:红黑树能够保持有序性,并且能高效处理文件描述符的添加和删除。
- 哈希表:哈希表查找效率高,但可能会产生哈希冲突,需要额外的处理,因此相比红黑树,哈希表可能会增加额外的管理开销。
8. ET 和 LT 的区别,I/O 多路复用
ET(Edge Triggered):
- 只会在文件描述符的状态从不可用变为可用时触发一次事件,之后不会再触发,直到事件状态再次发生变化。
- 适用于高性能的场景,因为事件只在状态变化时通知一次,减少了不必要的调用。
- 使用时需要确保数据完全读取或写入,否则可能会丢失数据。
LT(Level Triggered):
- 每次检测到文件描述符的状态满足条件时,都会通知应用程序。这意味着,文件描述符处于可读状态时,应用程序会不断被通知直到数据被完全读取。
- 适用于相对简单的场景,但在高并发时,可能会有多次无意义的通知。
I/O 多路复用:
- I/O 多路复用是指通过单个线程或进程来监听多个 I/O 通道(如多个网络连接)。当某个连接的 I/O 状态发生变化时,应用程序被通知,可以进行数据读取或写入操作。
- 常见的 I/O 多路复用技术有
select
、poll
、epoll
,它们的优点是能高效地处理大量并发连接,避免了为每个连接创建一个线程或进程带来的性能开销。
9. 游戏中数据传输用啥协议(有没有改进的协议,基于 UDP 的可靠传输)
游戏中数据传输协议:
- 游戏通常需要高效的实时数据传输,因此 UDP 是游戏中的常用协议。UDP 提供了低延迟和高吞吐量,但不保证数据可靠性。
基于 UDP 的可靠传输:
- RUDP(Reliable UDP):为了弥补 UDP 的不可靠性,开发了 RUDP 协议。RUDP 在 UDP 的基础上增加了确认机制和重传机制,使得 UDP 在保持低延迟的同时,能够实现可靠的数据传输。
- QUIC:是一个基于 UDP 的传输协议,由 Google 提出。QUIC 提供了类似于 TCP 的可靠性,同时优化了连接建立时间、减少了延迟。QUIC 采用了多路复用和加密,广泛应用于 HTTPS 的加速。
其他协议:
- TCP:虽然 TCP 提供了可靠的数据传输,但在游戏中,由于其较高的延迟和拥塞控制机制,通常不会用于实时数据传输。
- WebRTC:对于实时通信应用(如多人游戏中的语音聊天),WebRTC 也采用了基于 UDP 的协议,并且增加了可靠性和加密保障。
10. 项目架构(Web Server)两种高并发模式(问的很细)
高并发模式:在设计高并发的 Web 服务器时,通常采用以下两种架构模式:
多进程模型:
- 每个请求由独立的进程来处理。进程之间相互独立,互不干扰。
优点:
- 高度隔离,每个进程的崩溃不会影响其他进程。
- 适合 CPU 密集型任务,因为可以在多核 CPU 上并行执行。
缺点:
- 进程的创建和销毁开销较大。
- 进程间通信(IPC)复杂且效率较低。
多线程模型:
- 使用多个线程来处理并发请求,线程共享进程的资源(如内存)。
优点:
- 线程创建和销毁比进程更轻量,效率较高。
- 线程间通信通过共享内存实现,开销较小。
缺点:
- 需要解决线程同步问题(如死锁、资源竞争等)。
- 单个线程的崩溃可能影响整个进程。
高并发 Web 服务器:
- 事件驱动模型(Reactor 模型):单线程或少量线程通过多路复用机制(如
epoll
)监听多个连接,避免为每个连接创建新的线程或进程。适用于 I/O 密集型场景。 - 线程池/进程池模型:对于 CPU 密集型任务,使用线程池或进程池处理请求,避免频繁创建和销毁线程/进程带来的性能损耗。
- 事件驱动模型(Reactor 模型):单线程或少量线程通过多路复用机制(如
11. 除了 Reactor 模型,还有什么模型
Reactor 模型 是一种事件驱动的编程模式,广泛应用于高并发服务器中,特别是 I/O 密集型的应用。除了 Reactor 模型,常见的还有以下几种模型:
Proactor 模型:
- 在 Proactor 模型中,操作系统负责处理 I/O 操作,应用程序不需要直接管理 I/O 事件的触发。应用程序发起 I/O 操作后,操作系统会异步地处理这些操作,并通过回调函数通知应用程序。
优点:
- 操作系统直接处理 I/O,简化了应用程序的编程。
- 没有事件循环的开销,适用于需要大量异步 I/O 的系统。
缺点:
- 操作系统对异步 I/O 的支持不如同步 I/O 稳定,且在某些平台(如 Linux)上,Proactor 模型的实现较为复杂。
Thread-per-Connection 模型:
- 每个客户端连接由一个独立的线程处理。每个请求都由新的线程处理,这种方式简单直观。
优点:
- 实现简单,易于理解。
- 适合连接数较少的应用。
缺点:
- 随着并发连接数增加,线程的创建和上下文切换开销变得很大。
- 资源消耗较多,可能导致系统崩溃。
Worker 模型:
- 使用固定数量的线程池来处理连接请求,任务由线程池中的线程处理。
优点:
- 减少了线程创建和销毁的开销。
- 可以限制线程的数量,避免过多线程占用系统资源。
缺点:
- 线程池的大小需要合理配置,过大或过小都会影响性能。
Asynchronous I/O 模型:
- 应用程序提交 I/O 请求后,继续处理其他任务。当 I/O 完成时,系统通知应用程序。通过回调机制或事件通知机制来处理数据。
优点:
- 非阻塞,能高效地处理大量并发请求。
- 不需要手动轮询或创建过多线程。
缺点:
- 编程模型复杂,需要合理的事件驱动或回调机制。
数据库
1. MySQL 有哪些引擎
MySQL 提供了多种存储引擎,最常见的有:
- InnoDB:支持事务、外键、行级锁,是默认的存储引擎,适用于高并发和事务需求的场景。
- MyISAM:不支持事务,表锁,适合于读取操作多的应用,但对于高并发和事务处理较弱。
- Memory:数据存储在内存中,速度很快,适合临时数据存储。
- CSV:将表格数据存储为 CSV 格式文件,适用于导出和导入操作。
- Archive:用于存储大量的归档数据,支持只写入且不修改的场景。
2. 数据库的架构
数据库架构通常包括以下组件:
- 存储引擎:决定数据的存储、管理和访问方式(如 InnoDB、MyISAM)。
- 数据库服务器:提供与数据库的连接,负责数据的处理和查询。
- 客户端:与数据库进行交互的用户端应用程序,通常通过 SQL 查询与数据库服务器通信。
- 数据库管理系统(DBMS):控制和管理数据库的操作,确保数据的一致性、完整性和安全性。
3. 不同引擎对索引的支持
InnoDB:
- 支持 主键索引 和 二级索引。主键索引是聚集索引,数据按主键顺序存储。二级索引存储的是指向主键的指针。
- 支持 全文索引(从 MySQL 5.6 开始支持)。
- 支持 空间索引(适用于地理信息数据)。
MyISAM:
- 只支持 非聚集索引,即所有索引都在单独的索引文件中,数据存储与索引分离。
- 支持 全文索引。
4. InnoDB 和 MyISAM 的区别
InnoDB:
- 支持事务,ACID 特性。
- 支持外键约束。
- 行级锁,适用于高并发写操作。
- 数据存储和索引都采用 B+ 树结构。
MyISAM:
- 不支持事务。
- 不支持外键约束。
- 表级锁,适用于高并发读操作。
- 数据存储和索引采用 B 树结构。
5. 隔离级别
数据库的隔离级别决定了事务之间如何互相影响,主要有以下四种:
- 读未提交(Read Uncommitted):允许脏读,一个事务可以读取另一个事务未提交的数据。
- 读已提交(Read Committed):只允许读取已提交的数据,防止脏读,但可能会发生不可重复读。
- 可重复读(Repeatable Read):确保事务中多次读取的数据一致,防止脏读和不可重复读,但可能会出现幻读。
- 串行化(Serializable):最高的隔离级别,强制事务串行执行,避免所有并发问题,但性能较差。
6. 最左前缀原则
最左前缀原则:在使用联合索引时,查询条件需要从左到右顺序匹配索引列。如果查询条件不按索引顺序给出,索引可能不能有效使用。
- 例如,如果索引是
(a, b, c)
,查询条件WHERE a=1 AND b=2
会使用该索引,但WHERE b=2 AND a=1
可能无法利用该索引。
- 例如,如果索引是
7. MySQL 的集群是用什么方式去增加并发量
- 读写分离:将读请求和写请求分配到不同的数据库实例上,通常通过 主从复制 来实现,主库负责写操作,从库负责读操作。
- 分片(Sharding):将数据分布到多个数据库实例上,减少每个实例的数据量,从而提高并发能力。可以通过水平分片和垂直分片实现。
- 负载均衡:通过负载均衡器将请求分发到多个数据库服务器,避免单点压力。
8. 除了读写分离还有吗?
读写分离是常见的提高并发量的方案,但还有以下方式:
- 分库分表:将数据按某种规则划分到多个数据库或多个表中,减少单一表或库的负载。
- 缓存层:通过 Redis 等缓存技术,将热点数据缓存到内存中,减少对数据库的访问频率,提升性能。
- 数据库索引优化:通过合理的索引设计,减少查询的时间,提高数据库的响应速度。
9. mysql的隔离级别和锁
锁:MySQL 提供了多种锁来控制并发事务访问:
- 共享锁(S锁):允许其他事务进行读取,但不能修改数据。
- 排他锁(X锁):只允许当前事务操作数据,其他事务无法读取或修改数据。
- 意向锁:主要用于表级锁的管理,在执行实际的行级锁时,使用意向锁来表明当前事务对某些行进行锁定。
10. 数据库 delete 和 truncate 区别
DELETE:
- 删除数据时,会逐行删除,每删除一行都会记录日志,因此比较慢。
- 可以添加 WHERE 子句来删除特定数据,支持回滚。
- 会触发触发器。
TRUNCATE:
- 删除表中所有数据,不逐行删除,而是通过删除整个数据页来清空表,速度更快。
- 不支持回滚,且不能添加 WHERE 子句。
- 不会触发触发器。
11. MySQL 索引(B+ 树)
- B+ 树是 MySQL 中最常用的索引数据结构,它是 B 树的变种,所有的数据都存储在叶子节点,内部节点仅存储索引信息。B+ 树支持 范围查询 和 高效查找,广泛用于 主键索引 和 二级索引。
12. B 树和 B+ 树的区别
B 树:
- 内部节点和叶子节点都存储数据。
- 支持查找、插入、删除等操作,但效率较低。
B+ 树:
- 所有数据都存储在叶子节点,内部节点只存储索引,增加了存储密度。
- 适用于范围查询,因为叶子节点按顺序连接,支持顺序遍历。
13. B+ 树树高怎么算?树高为 4 能支持多少数据量
树高的计算:树高是指从根节点到叶子节点的最大路径长度。假设每个节点的最大子节点数为
m
,那么树高h
可以通过以下公式计算:- 最多可以存储
m^h
个数据。 - 如果树高为 4,每个节点有
m
个子节点,则最多存储m^4
个数据。
- 最多可以存储
14. 数据库范式(1NF、2NF、3NF、BCNF)
1NF(第一范式):
- 数据表中的每个字段都必须是原子性的,即不能有多个值。
- 每个字段只存储一个数据项,不能是集合或数组。
2NF(第二范式):
- 满足 1NF 的基础上,所有非主键字段完全依赖于主键。
- 避免了部分依赖,即所有非主键属性必须依赖于主键的全部组成。
3NF(第三范式):
- 满足 2NF 的基础上,所有非主键字段必须直接依赖于主键,而不能传递依赖。
- 这意味着如果有一个非主键字段依赖于另一个非主键字段(间接依赖),则需要拆分成不同的表。
BCNF(博茨-科德范式):
- 满足 3NF 的基础上,所有的决定因素都是候选键。
- 即表中的每个决定因素(非主键字段决定其他字段的值)必须是候选键,消除了所有类型的依赖。
15. 数据库设计的目标
- 数据一致性:确保数据库中的数据是准确、完整的,避免出现不一致或错误的数据。
- 数据冗余最小化:通过范式化和合理设计表结构,减少数据冗余,降低维护复杂度。
- 数据安全性:保护数据不被非法访问或篡改,可以通过访问控制和加密等手段来实现。
- 高效性:确保数据库查询、插入、更新和删除操作能够高效执行,通常通过索引、优化查询等方式来实现。
- 可扩展性:数据库设计应考虑未来数据量的增长,确保能够灵活扩展。
16. 数据库事务(ACID)
ACID 是确保数据库事务可靠性的四个基本属性:
- 原子性(Atomicity):事务中的操作要么全部完成,要么全部不做,不能只做部分操作。
- 一致性(Consistency):事务开始前和结束后,数据库必须从一个一致的状态转变到另一个一致的状态。
- 隔离性(Isolation):多个事务并发执行时,必须保证它们之间的操作互不干扰,每个事务都应该像在独立运行一样。
- 持久性(Durability):一旦事务提交,它对数据库的修改就是永久的,即使系统崩溃,也应该能够恢复。
17. 数据库索引的类型和原理
索引类型:
- B+ 树索引:MySQL 默认的索引类型,数据按顺序排列,支持范围查询,效率高。
- 哈希索引:通过哈希算法计算数据项的地址,适用于等值查询,但不支持范围查询。
- 全文索引:专门用于处理文本字段,支持对文本内容的快速查找,适用于搜索引擎等场景。
- 空间索引:用于处理空间数据(如地图上的地理位置数据),常用 R 树或 R+ 树实现。
索引原理:
- 索引通过构建数据结构(如 B+ 树、哈希表等)来加速查询过程,减少全表扫描的时间。
- 索引通过将数据按特定的规则排序或映射,使得查询操作能够更高效地查找目标数据。
18. 什么是 SQL 注入,如何防止
- SQL 注入:一种通过在 SQL 查询中插入恶意 SQL 代码的攻击方式,攻击者通过操控 SQL 查询语句获取、修改数据库中的数据。
防止 SQL 注入:
- 使用预编译语句(Prepared Statements):使用数据库驱动提供的预编译语句机制,避免直接拼接 SQL 查询,防止恶意代码注入。
- 使用 ORM(对象关系映射):ORM 框架通常会自动对查询进行参数化处理,减少 SQL 注入的风险。
- 输入验证:对所有用户输入进行严格验证,确保输入数据符合预期格式。
- 最小化权限:数据库用户权限最小化,避免攻击者通过注入获取过多权限。
19. 数据库连接池的作用
- 数据库连接池:是为了管理数据库连接资源而设计的一种技术,允许多个应用线程共享固定数量的数据库连接。
作用:
- 提高性能:避免了频繁地创建和销毁数据库连接的开销,通过连接池复用连接,提高性能。
- 资源管理:通过连接池统一管理连接的生命周期,防止连接泄漏或资源浪费。
- 负载均衡:连接池可以根据负载情况动态调整连接的数量。
20. MySQL 数据库的备份与恢复
备份方式:
- 物理备份:直接复制数据库的数据文件和日志文件(如通过
mysqldump
或复制数据文件的方式)。 - 逻辑备份:导出数据库的结构和数据,通常使用
mysqldump
工具。逻辑备份生成的 SQL 文件可以用于恢复数据。
- 物理备份:直接复制数据库的数据文件和日志文件(如通过
恢复方式:
- 从物理备份恢复:直接使用备份的数据文件恢复。
- 从逻辑备份恢复:使用
mysqldump
生成的 SQL 文件来恢复数据库结构和数据。 - 增量备份与恢复:通过二进制日志(binlog)进行增量备份,只有变化的部分被备份,从而节省空间和时间。
21. 如何优化数据库查询
优化查询:
- 使用索引:对查询条件字段建立合适的索引,避免全表扫描。
- 避免使用 SELECT * 查询:尽量只查询所需的字段,减少数据传输量。
- 查询缓存:启用查询缓存,将常用查询的结果缓存起来,减少重复计算。
- SQL 语句优化:避免不必要的子查询,使用联合查询(JOIN)而非多个独立查询。
- 数据分区:通过分区表来管理大量数据,将数据分割成多个物理文件,提高查询效率。
22. 数据库分区(Partitioning)
- 分区:将大表分成更小、更易于管理的部分,减少查询的数据量,提高性能。
分区类型:
- 范围分区(Range Partitioning):基于某一字段的范围将数据分割成多个分区。
- 哈希分区(Hash Partitioning):使用哈希算法将数据均匀分布到不同的分区中。
- 列表分区(List Partitioning):根据具体的值将数据分配到不同的分区。
- 组合分区(Composite Partitioning):将两种或多种分区方式结合起来使用。
23. 如何加快数据检索的效率
加快数据检索效率的常见方法包括:
- 创建合适的索引:通过索引减少全表扫描的时间。可以根据查询条件中的字段创建索引。
- 避免使用 SELECT * 查询:只查询所需的字段,减少不必要的数据传输。
- 查询缓存:将常用查询的结果缓存起来,减少数据库的计算压力。
- 优化查询语句:避免不必要的子查询和 JOIN 操作,合理使用查询条件。
- 分区表:对于大型表,通过分区技术将数据拆分为多个更小的部分,提高查询效率。
24. 注册登录的用户名和密码存在哪里?
用户名和密码通常存储在数据库中。为了保证安全,密码应该进行加密(如使用哈希算法),并且不要直接存储明文密码。
常用的做法:
- 哈希加密:使用如
SHA-256
、bcrypt
等哈希算法将密码加密,防止泄漏明文密码。 - 盐值:为了防止使用相同密码的用户生成相同的哈希值,可以为每个密码添加一个随机的盐值(Salt),增加安全性。
- 哈希加密:使用如
25. 面试官灵魂4连问:乐观锁与悲观锁的概念、实现方式、场景、优缺点
乐观锁:
- 概念:假设不会发生冲突,在操作数据时不加锁。只有在提交时检测数据是否被其他事务修改,如果修改则回滚或重试。
- 实现方式:通常通过 版本号 或 时间戳 来标记数据的修改,提交时检查数据是否被修改。
- 场景:适用于冲突较少的场景,如并发读取较多的环境。
优缺点:
- 优点:减少锁的竞争,提高性能。
- 缺点:如果发生冲突,需要重试,可能会导致资源浪费。
悲观锁:
- 概念:假设会发生冲突,在操作数据时对数据加锁,直到操作完成才释放锁。
- 实现方式:使用 排他锁 或 共享锁 来控制对数据的访问。
- 场景:适用于冲突较多的场景,如金融交易、库存管理等需要保证一致性的环境。
优缺点:
- 优点:能够确保数据的一致性。
- 缺点:可能会导致死锁,影响性能,尤其在高并发环境下。
26. 一千五百万行数据如何快速找到某一行数据,给出方案,设计数据库表结构
方案:
- 索引:为要查询的字段(如用户ID、订单号等)创建索引,通过索引来快速定位数据,避免全表扫描。
- 分区表:将数据按照某种规则(如日期、区域等)进行分区,减少查询的数据量。
- 查询优化:合理设计 SQL 查询语句,避免不必要的 JOIN 和子查询,使用
LIMIT
限制返回的行数。
数据库表设计:
假设需要存储用户信息:
CREATE TABLE users ( user_id INT NOT NULL AUTO_INCREMENT, username VARCHAR(100) NOT NULL, email VARCHAR(100) NOT NULL, created_at DATETIME NOT NULL, PRIMARY KEY (user_id), INDEX (email) -- 为常用查询字段创建索引 );
27. SQL 优化
常见优化手段:
- 索引优化:根据查询条件创建合适的索引,减少扫描的行数。
- 查询重写:避免使用
SELECT *
,只查询所需的字段。避免复杂的子查询和不必要的 JOIN 操作。 - 避免全表扫描:确保查询条件使用索引,避免全表扫描。
- 使用连接池:避免频繁建立和关闭数据库连接,提高性能。
- 避免频繁的更新操作:对于频繁更新的数据,可以使用缓存系统(如 Redis)进行缓存,减少数据库的负载。
28. 事务
事务 是一组数据库操作的集合,保证其要么全部执行成功,要么全部失败回滚,保证数据的一致性。
四个特性(ACID):
- 原子性:事务中的操作要么全部完成,要么全部回滚。
- 一致性:事务执行前后,数据库必须从一个一致的状态变成另一个一致的状态。
- 隔离性:事务执行时,不受其他事务的干扰。
- 持久性:事务提交后,对数据库的修改会永久生效,即使系统崩溃也能恢复。
29. 什么情况下使用读已提交
读已提交(Read Committed) 隔离级别保证一个事务只能读取其他事务已提交的数据。适用于应用程序对数据一致性要求不高的场景。
使用场景:
- 当对数据一致性要求不是特别高时,可以使用读已提交,例如在读操作频繁的应用中,减少锁的竞争。
- 对于某些无需多次读取相同数据的应用,如简单的数据查询操作,使用读已提交可以提高性能。
30. 对于脏读的理解
脏读(Dirty Read) 是指一个事务读取了另一个事务尚未提交的数据。如果另一个事务回滚,这些数据将不再有效,导致读取的数据不一致。
- 在 读已提交(Read Committed) 隔离级别下,脏读被避免,因为只能读取已提交的数据。
31. 慢查询怎么看,怎么优化
慢查询:MySQL 提供了慢查询日志来记录执行时间较长的 SQL 查询。可以通过开启慢查询日志来监控哪些查询耗时较长。
查看慢查询:
- 启用慢查询日志:在 MySQL 配置文件中启用慢查询日志,设置
long_query_time
参数来定义查询超过多少秒会被记录。 - 使用工具分析慢查询日志,如
mysqldumpslow
或pt-query-digest
。
- 启用慢查询日志:在 MySQL 配置文件中启用慢查询日志,设置
优化慢查询:
- 创建索引:为查询条件字段创建索引,减少查询的扫描范围。
- 优化查询语句:避免使用复杂的
JOIN
和子查询,尽量简化查询。 - 避免使用
SELECT *
:只选择需要的字段,减少数据传输和计算量。 - 查询缓存:对于频繁执行的查询,可以启用查询缓存。
32. 联合索引(a,b,c),where a, b, c 和 where b, a, c 区别
联合索引:如果索引是
(a, b, c)
,MySQL 会根据索引列的顺序来建立索引。WHERE a, b, c
:可以利用索引的最左前缀原则,首先使用a
索引,接着使用b
和c
。WHERE b, a, c
:不能有效使用索引,因为b
和a
的顺序与索引列顺序不匹配,导致无法使用最左前缀原则。
33. 是否了解数据库底层
MySQL 的底层实现:
- MySQL 存储引擎通过 InnoDB(默认引擎)和 MyISAM 等不同的存储方式来管理数据。
- InnoDB:采用 B+ 树索引,支持事务、行级锁、外键等,数据存储在聚集索引中。
- MyISAM:采用 B 树索引,不支持事务和外键,适用于大量读操作的场景。
- 文件存储:数据库中的数据存储在不同的文件中(如
.ibd
文件、.frm
文件等),并且通过binlog
记录数据更改,用于恢复和复制。
Redis
1. Redis 的数据结构
Redis 提供了多种数据结构,用于处理不同的数据类型:
- String:最基本的键值对类型,支持设置、获取、加法、减法等操作。
- List:双向链表,支持在两端进行推送和弹出操作,适合实现队列、栈等结构。
- Set:无序集合,支持添加、删除和判断元素是否存在的操作,适用于去重。
- Sorted Set (Zset):有序集合,每个元素有一个分数,通过分数进行排序,支持范围查询。
- Hash:存储多个字段值的映射,适合存储对象。
- Bitmap:位图,常用于大规模的数据标记、状态存储等。
- HyperLogLog:用于基数统计的近似算法,适合处理海量数据。
- Geospatial:支持地理空间数据存储和操作,适用于地理位置相关的应用。
2. Redis 基本数据结构的底层实现
Sorted Set(Zset):
- Redis 的 Zset 使用 跳表(SkipList) 来实现。跳表是一种多层链表,通过增加多个索引层,提高查询和插入操作的效率。
跳表与红黑树对比:
- 跳表:使用多层链表,允许跳过多个节点,提供较高的查询效率。
- 红黑树:是一种自平衡的二叉搜索树,支持快速查找、插入和删除操作,常用于实现有序集合,但插入和删除的性能略低于跳表。
3. Redis 的高性能原因
- 内存存储:Redis 将数据存储在内存中,避免了磁盘 I/O 操作,显著提升了数据访问速度。
- 单线程架构:Redis 采用单线程处理所有客户端请求,避免了多线程上下文切换的开销。单线程的模型在高并发下表现优秀,因为它避免了锁竞争和上下文切换。
- 无锁操作:Redis 内部通过单线程执行命令,避免了在并发执行时的锁竞争。
4. Redis 的使用场景
- 缓存:Redis 被广泛用作缓存系统,尤其是在 Web 应用中,用于缓存热点数据,减轻数据库压力。
- 消息队列:Redis 的 List 和 Pub/Sub 机制非常适合实现消息队列系统,支持异步消息传递。
- 排行榜:使用 Redis 的 Zset 存储并排序用户得分,适用于实时排行榜应用。
- 会话管理:Redis 用于存储用户会话信息,如 session 数据,能够提供快速的存取速度。
5. 为什么 Redis 读取速度快
- 完全基于内存:Redis 是内存数据库,数据的存取完全在内存中完成,比传统的磁盘数据库要快得多。
- 避免不必要的上下文切换:Redis 采用单线程模型,避免了多线程编程中的上下文切换和加锁问题,这样就能高效地处理并发请求。
- 简化的操作:Redis 提供高效的底层数据结构,如哈希表、跳表等,能提供高效的查询和修改操作。
6. Redis 单线程为什么会快
- Redis 使用单线程模型来处理所有请求,这样可以避免由于多线程上下文切换而带来的性能损失。
- 避免 CPU 消耗和锁竞争:多线程在处理并发请求时通常需要加锁,而 Redis 的单线程模型避免了加锁带来的复杂性和性能开销。
服务器开发
1. 用户认证和鉴权(JWT)
JWT(JSON Web Token) 是一种轻量级的认证和授权方式,通常用于分布式应用中传递信息。JWT 主要由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。
- 头部:指定签名算法(如 HMAC SHA256 或 RSA)。
- 载荷:包含声明(Claims),如用户 ID、权限等。
- 签名:为了确保 JWT 的完整性,通过密钥对头部和载荷进行加密生成签名。
应用场景:
- 认证:客户端登录后,服务器生成 JWT 并返回,客户端在每次请求时附带 JWT,服务器验证其合法性,从而进行认证。
- 授权:在 JWT 中可以包含权限信息,服务器根据这些信息进行授权。
2. 从 URL 下载 10000 张图片,10 台机器并行下载
思路:
- 主线程:负责分配任务,获取图片 URL 列表。
- 子线程:每个子线程负责下载一部分图片。使用线程池来管理这些子线程,避免线程创建和销毁的开销。
- 并行下载:将 URL 列表分成 10 份,每台机器并行下载一部分。
代码:
import threading from queue import Queue import requests def download_image(url, queue): try: response = requests.get(url) # 假设文件名是 URL 的最后一部分 filename = url.split("/")[-1] with open(filename, "wb") as file: file.write(response.content) except Exception as e: print(f"Error downloading {url}: {e}") finally: queue.task_done() def main(): urls = ["http://example.com/image1.jpg", "http://example.com/image2.jpg", ...] queue = Queue() for url in urls: thread = threading.Thread(target=download_image, args=(url, queue)) thread.start() queue.join()
3. 雪花算法原理
雪花算法(Snowflake Algorithm)是 Twitter 提出的分布式唯一 ID 生成算法。它使用 64 位二进制表示 ID,具体结构为:
- 1 位:符号位(通常为 0)。
- 41 位:时间戳,表示自某个时间点(如当前时间戳)以来的毫秒数。
- 10 位:工作机器 ID,用于区分不同机器。
- 12 位:序列号,用于同一毫秒内生成多个 ID。
优点:
- 高效生成全局唯一的 ID。
- 时间顺序生成 ID,适合排序和分布式系统。
4. 分布式锁
分布式锁 是一种用于在分布式系统中确保同一时刻只有一个客户端能够访问共享资源的机制。
实现方式:
- 基于数据库:通过数据库的行锁来实现分布式锁,但可能存在性能瓶颈。
- 基于 Redis:利用 Redis 的 SETNX 命令实现锁的创建,设置过期时间避免死锁。
- Zookeeper:通过 Zookeeper 的临时节点实现分布式锁,适用于需要高可靠性的场景。
5. Redis 和 MySQL 数据一致性设计
Redis 和 MySQL 数据一致性问题:
读写一致性:为了确保 Redis 缓存和 MySQL 数据库中的数据一致性,可以通过以下方法:
- 缓存穿透:通过使用布隆过滤器等方法,避免频繁访问不存在的数据。
- 缓存更新策略:每当数据在 MySQL 中更新时,及时更新 Redis 缓存,避免读取过期数据。
- 双写策略:在写入数据时,确保同时更新 MySQL 和 Redis,保证数据一致性。
方案:
- 异步更新:通过消息队列(如 Kafka)异步更新缓存,减少数据库的负担。
- 事件驱动:通过数据库触发器或应用逻辑触发缓存更新。
6. 长连接与短连接的区别与应用场景
短连接:
- 每次请求建立一个新的连接,处理完请求后立即关闭连接。
- 优点:实现简单,适用于负载较小、请求数量少的场景。
- 缺点:频繁建立和销毁连接,带来较大的性能开销。
长连接:
- 连接在多个请求之间保持开放,避免了频繁的连接建立和销毁。
- 优点:减少连接建立和销毁的开销,适用于请求频繁的场景。
- 缺点:需要管理连接的生命周期和资源,可能导致连接池耗尽。
应用场景:
- 短连接:适用于 HTTP 请求、API 调用等。
- 长连接:适用于 WebSocket、数据库连接池、消息队列等。
7. RAII 实现数据库连接池
RAII(Resource Acquisition Is Initialization)是资源管理的一种方式,利用对象的生命周期自动管理资源。对于数据库连接池,可以通过 RAII 原则自动管理连接的创建和销毁。
实现步骤:
- 封装数据库连接:创建一个数据库连接类,构造函数负责获取连接,析构函数负责关闭连接。
- 连接池:实现一个连接池类,内部管理一定数量的数据库连接。
- 借用连接:从连接池中获取连接,并确保使用完毕后归还连接。
8. HTTP 服务器目标及实现方式
- 目标:HTTP 服务器的主要目标是接收 HTTP 请求、解析请求内容、生成响应并返回给客户端。
实现方式:
- 监听端口:服务器监听指定端口,等待客户端的连接请求。
- 解析请求:接收到请求后,解析 HTTP 请求头、请求体等信息。
- 处理请求:根据请求的路径、方法等信息,选择合适的处理逻辑(如查询数据库、生成文件等)。
- 返回响应:构造 HTTP 响应并发送给客户端。
9. 负载均衡的场景问题
负载均衡 是通过将请求分配到多个服务器,来提高服务的可用性和处理能力。常见的负载均衡方法包括:
- 轮询:每次请求轮流分配到各个服务器。
- 加权轮询:根据服务器的处理能力进行权重分配。
- 最少连接:将请求分配给连接数最少的服务器。
- IP 哈希:根据客户端的 IP 地址进行分配。
- 应用场景:负载均衡常用于高并发、大流量的应用场景,如 Web 服务、API 服务等。
rpc
1. gRPC怎么用的
gRPC 是 Google 开发的高性能、开源和通用的远程过程调用(RPC)框架,基于 HTTP/2 协议实现,支持多种编程语言。使用 gRPC 时,可以通过以下步骤:
- 定义服务:使用 Protocol Buffers(proto)文件定义服务接口和消息格式。
- 生成代码:通过 gRPC 提供的工具生成客户端和服务器端的代码。
- 实现服务:在服务器端实现服务定义的方法。
- 客户端调用:客户端通过 gRPC 客户端库发起 RPC 请求,服务器响应。
示例:
service Greeter { rpc SayHello (HelloRequest) returns (HelloResponse); } message HelloRequest { string name = 1; } message HelloResponse { string message = 1; }
2. 为什么 gRPC 速度快
- HTTP/2:gRPC 使用 HTTP/2 协议,提供了多路复用、二进制帧、头部压缩等特性,使得传输更高效。
- Protocol Buffers:gRPC 使用 Protocol Buffers(Protobuf)作为序列化协议,比传统的 JSON 或 XML 更紧凑、更高效。
- 流式传输:支持双向流式传输,可以高效地处理大量数据。
- 低延迟:通过高效的连接管理和消息压缩,减少了延迟和带宽消耗。
3. RPC 调用流程和机制,RPC 超时计时器在什么位置,如何处理超时
RPC 调用流程:
- 客户端发起 RPC 调用,传递请求数据。
- 客户端通过代理(stub)与服务器通信。
- 服务器收到请求后,执行相应的方法并返回响应数据。
- 客户端接收到响应并处理结果。
RPC 超时处理:
- RPC 调用的超时通常通过客户端指定的超时参数来控制,客户端设置请求超时后,RPC 客户端会在超时后终止请求。
- 超时计时器的位置:通常由客户端设置,当客户端发起请求时,超时计时器开始计时。如果服务器没有在规定的时间内返回结果,客户端会处理超时并可以选择重试或报告错误。
- 超时后连接的使用:超时并不意味着当前连接不可用,连接可以继续使用,除非出现其他网络错误。
评论已关闭