C++
虚函数表和虚函数表指针的创建
虚函数表实际是一个虚函数地址的数组,其指向为具体的代码区的位置。
- 背景:用来实现多态机制
- 生成:编译器编译的时候生成,由virtual关键字修饰。
- 存放位置:可执行程序状态下,存放在磁盘;运行状态,存放在内存中。
虚函数默认参数的特殊行为
当虚函数有默认参数且被重载时,默认参数的值由调用时的静态类型决定,而不是运行时实际对象的类型。这是因为默认参数是编译时确定的,而虚函数调用是运行时动态绑定的。
class Base {
public:
virtual void foo(int x = 10) {
cout << "Base::foo(" << x << ")" << endl;
}
};
class Derived : public Base {
public:
void foo(int x = 20) override {
cout << "Derived::foo(" << x << ")" << endl;
}
};
int main() {
Base* b = new Derived();
b->foo(); // 输出:Derived::foo(10)
delete b;
return 0;
}
关键点:
- 虽然实际调用的是Derived::foo(),但使用的默认参数是Base::foo()的10
- 默认参数值在编译时根据指针类型(Base*)确定
- 虚函数调用在运行时根据实际对象类型(Derived)确定
最佳实践:
- 避免在虚函数中使用默认参数
- 如果必须使用,确保派生类和基类的默认参数一致
- 可以使用非虚函数提供默认参数,调用虚函数实现:
class Base {
public:
void foo(int x = 10) { do_foo(x); }
protected:
virtual void do_foo(int x) { /*...*/ }
};
- 虚拟内存分区:栈区(栈区之上时内核空间)、文件映射区、堆区、数据区(静态存储区)和代码区。 可执行程序中不同部分存储的内容如下:
| 可执行程序中(磁盘) | 作用 | 对应虚拟内存分区(内存) |
|---|---|---|
| .bss | 未初始化的或初始化为0的全局、静态变量 | 静态存储区 |
| .data | 初始化的全局、静态变量 | 静态存储区 |
| .rodata | 只读数据段和虚函数表 | 代码区 |
| .text | 具体代码表 | 代码区 |
- 虚函数表指针的创建时机:
- 类对象构造的时候,把类的虚函数表地址赋值给vptr。
- 没有构造函数,编译器会生成默认的构造函数(有第一条的作用)。
- 继承的情况下(例如B继承A),先调用基类的构造函数,把A的虚函数表的地址赋值给vptr。再调用子类构造函数,把B的虚函数表的地址赋值给vptr。
虚函数表和虚函数表指针的关系
每个类只有一个虚函数表,类的不同对象通常来说虚函数表指针vptr(每个类实例有一个)是不一样的(自身地址不一样,指向的内容一样,为深拷贝。浅拷贝会导致一个置null后另一个类实例找不到虚函数表)。使用的时候需要显式地定义拷贝构造函数或重载赋值运算符。
默认拷贝构造函数何时生成
背景 不提供的话,位拷贝,全盘复制。
- 危害1:堆上资源也会复制
- 危害2:文件句柄,socket也复制 持有相同的对象资源,句柄,一者释放,另一者就拿不到数据。
触发默认构造函数时机
- A b;
- A a(b);
- A a=b;
- 函数传参,形参为类对象
- 函数返回值为类对象(有返回值优化),无优化会多次调用。(C++11之后还会看是否有移动构造,然后看类有没有默认构造,再没有,报错)
生成时间 编译时生成。
- 类成员变量是一个类,该成员类有默认构造函数。
- 类继承自基类,基类有默认构造函数。
- 类成员有虚函数。
- 类继承自基类,基类有虚函数。
进程和线程的区别
本质:进程是资源费配的基本单位,线程是CPU调度的基本单位。
- 并发性:切换效率。上下文切换(如redis to mysql),运行环境(寄存器,程序计数器,用户空间信息与内核空间,pcb)。如果同一个进程的线程切换,效率高,不用动用户空间的数据。
- 内存:线程不具备独立分配虚拟内存的空间。进程有独立的地址空间。但线程有栈,pc,本地存储等独立空间。
- 所属关系:线程依附于进程,进程可以有多个线程。
- 健壮性:进程与进程之间隔离,独立的。线程共享一个进程,互相影响。进程健壮性高于线程。
系统调用的整个流程
- 什么是系统调用 用户态进入内核态的一种形式。用户态需要操作到内核态的资源。 用户程序->函数库->系统调用->内核
- 中断 系统调用是软中断(0x80)。中断号、中断向量表、中断处理程序。
- 调用流程 触发中断->切换堆栈->执行中断程序->从中断处理程序返回(iret软中断返回)
// 示例:通过`syscall`指令或软中断触发
int main() {
write(1, "Hello", 5); // 调用write系统调用
};
x86架构示例(通过0x80中断)
mov eax, 4 ; 系统调用号(sys_write)
mov ebx, 1 ; 文件描述符
mov ecx, msg ; 缓冲区地址
mov edx, len ; 数据长度
// 系统调用表示例(Linux内核)
void *sys_call_table[] = {
[0] = sys_restart_syscall,
[1] = sys_exit,
[4] = sys_write, // 对应示例中的write调用
//...
};
// sys_write内核函数简化逻辑
asmlinkage long sys_write(
unsigned int fd,
const char __user *buf,
size_t count
) {
struct file *file = fget(fd);
return vfs_write(file, buf, count, &file->f_pos);
}
// 通过iret指令恢复现场
ENTRY(ret_from_sys_call)
RESTORE_ALL
iret
- 如果使用阻塞的io且io未就绪,将线程或者进程切换,运行态->阻塞态
网络相关概念
MTU和MSS
- MTU(Maximum Transmission Unit): 最大传输单元,指网络接口层能够传输的最大数据包大小,单位为字节。以太网默认MTU为1500字节。
- MSS(Maximum Segment Size): 最大分段大小,指TCP报文段中数据部分的最大长度,由MTU减去IP和TCP头部长度(通常40字节)得到,默认1460字节。
- 关系:MSS = MTU - IP头(20字节) - TCP头(20字节)
- 作用:MTU/MSS用于控制数据包大小,避免IP分片,提高传输效率。
TCP粘包问题
- 定义:TCP是字节流协议,接收方无法区分消息边界,多个应用层数据包可能被粘在一起接收。
- 原因:
- Nagle算法将多个小包合并发送
- 接收方缓冲区中数据堆积
- 解决方案:
- 固定长度消息
- 特殊分隔符(如\n)
- 消息头包含长度字段(常用)
TCP可靠传输机制
- 序列号和确认应答:
- 每个字节都有唯一序列号
- 接收方发送ACK确认收到的数据
- 选择性确认(SACK)可提高效率
- 超时重传:
- 发送数据后启动定时器
- 超时未收到ACK则重传
- 动态计算RTO(重传超时时间)
- 流量控制:
- 滑动窗口机制
- 接收方通过窗口大小通告可用缓冲区
- 零窗口探测防止死锁
- 拥塞控制:
- 慢启动:指数增长窗口
- 拥塞避免:线性增长窗口
- 快速重传:收到3个重复ACK立即重传
- 快速恢复:避免过度降低发送速率
// 简化的TCP发送端伪代码
void tcp_send() {
while(has_data_to_send) {
send_packet(next_seq);
start_timer(next_seq);
next_seq += packet_size;
if(received_ack) {
adjust_window_size();
stop_timer(acked_seq);
} else if(timeout) {
retransmit_packet();
adjust_congestion_window();
}
}
}
线程池手撕
#include <vector>
#include <thread>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>
#include <future>
class ThreadPool {
public:
explicit ThreadPool(size_t threads = std::thread::hardware_concurrency()) {
for(size_t i = 0; i < threads; ++i)
workers.emplace_back([this] {
for(;;) {
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();
}
});
}
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args) -> std::future<decltype(f(args...))> {
using return_type = decltype(f(args...));
auto task = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
std::future<return_type> res = task->get_future();
{
std::unique_lock<std::mutex> lock(queue_mutex);
if(stop)
throw std::runtime_error("enqueue on stopped ThreadPool");
tasks.emplace([task](){ (*task)(); });
}
condition.notify_one();
return res;
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
condition.notify_all();
for(std::thread &worker : workers)
worker.join();
}
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queue_mutex;
std::condition_variable condition;
bool stop = false;
};
移动语义
C++11引入了移动语义,可以减少拷贝构造函数的调用,提高效率。 对象移动时,会将对象内的资源转移到另一个对象,而不是拷贝。避免资源的重新分配。
- 移动构造函数:将右值对象内的资源转移到左值对象内。
- 移动赋值运算符:将右值对象内的资源转移到左值对象内。
class A {
public:
A(A&& other) noexcept {
std::cout << "move constructor" << std::endl;
// 资源转移
data = other.data;
other.data = nullptr;
}
A& operator=(A&& other) noexcept {
std::cout << "move assignment operator" << std::endl;
if(this!= &other) {
// 资源转移
data = other.data;
other.data = nullptr;
}
return *this;
}
A(const A& other) {
std::cout << "copy constructor" << std::endl;
// 拷贝构造
data = new int(other.data);
}
A& operator=(const A& other) {
std::cout << "copy assignment operator" << std::endl;
if(this!= &other) {
// 拷贝赋值
delete data;
data = new int(other.data);
}
return *this;
}
~A() {
std::cout << "destructor" << std::endl;
delete data;
}
int* data;
};
int main() {
A a1(10);
A a2(std::move(a1)); // 移动构造
A a3 = std::move(a2); // 移动赋值
return 0;
}
输出:
move constructor
move constructor
move assignment operator
destructor
destructor
移动语义在stl中的应用
- std::move:将左值转换为右值。
- std::forward:将参数类型转换为右值引用。
- std::move_if_noexcept:判断类型是否支持移动语义。
- std::swap:交换两个对象内的资源。
完美转发
在模板中,将模板参数完美地转发到内部函数(内部调用)参数中去。转发时保证被转发的参数的值属性不变。
template<typename F, typename... Args>
auto invoke(F&& f, Args&&... args) -> decltype(std::forward<F>(f)(std::forward<Args>(args)...)) {
return std::forward<F>(f)(std::forward<Args>(args)...);
}
- 借用万能引用,模板参数T或者auto,即需要类型推导,来接受左右属性。
- 应用折叠规则,模板参数T&&,即右值引用。T为左值或者左值引用,转为左值引用。T为右值或者右值引用,转为右值引用。(例如int& &&->int&,int&& &&->int&&,int &&->int &)
- 无论是左值引用还是右值引用,都是左值,具名。左值和右值是指对象是否拥有资源的这个本质(或者叫属性),而不是对象的值。
- std::forward
(v):T为左值引用,v将转化为T类型的左值。T为右值引用,v将转化为T类型的右值。去除了引用,转化为T类型的对象。
智能指针
- 指针管理困境:
- 资源释放问题:指针失效,资源泄露。
- 资源管理问题:资源泄露,资源管理复杂。(重复释放,释放时机不确定,释放顺序不确定)
- 如何解决:智能指针。RAII(Resource Acquisition Is Initialization)思想。根据对象的生命周期进行资源的控制。
- shared_ptr实现原理:
- 引用计数:每个对象维护一个引用计数,当引用计数为0时,释放资源。
- 自动释放:智能指针管理的资源,在离开作用域时自动释放。
- 异常安全:智能指针保证异常安全,即使在构造函数或析构函数中抛出异常,也能保证资源的正确释放。
- 常见智能指针:
- unique_ptr:独占所有权,只能有一个指针指向对象。
- shared_ptr:共享所有权,多个指针指向对象。
- weak_ptr:弱引用,指向shared_ptr对象,但不影响引用计数。
单例模式C++11之后的最佳解决————Meyers’ Singleton
对于local static 对象,其初始化发生在控制流第一次执行到该对象的初始化语句时。多个线程的控制流可能同时到达其初始化语句。
在C++11之前,在多线程环境下local static对象的初始化并不是线程安全的。具体表现就是:如果一个线程正在执行local static对象的初始化语句但还没有完成初始化,此时若其它线程也执行到该语句,那么这个线程会认为自己是第一次执行该语句并进入该local static对象的构造函数中。这会造成这个local static对象的重复构造,进而产生内存泄露问题。所以,local static对象在多线程环境下的重复构造问题是需要解决的。
而C++11则在语言规范中解决了这个问题。C++11规定,在一个线程开始local static 对象的初始化后到完成初始化前,其他线程执行到这个local static对象的初始化语句就会等待,直到该local static 对象初始化完成。
Meyers Singleton的实现:
class Singleton
{
private:
Singleton() { };
~Singleton() { };
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
public:
static Singleton& getInstance()
{
static Singleton instance;
return instance;
}
};
模板工厂模式
- 该模式用来封装和管理类的创建,终极目的是为了解耦,实现创建者和调用者的分离。
template <class ProductClass, typename... ArgType>
void* objCreateFunc(ArgType... arg)
{
return new ProductClass(arg...);
}
#ifndef ReflectRegister
# define ReflectRegister(ProductClass, ...) \
static int __type##ProductClass = \
ReflectFactory::registerObjCreateFunc(#ProductClass, (void*)&objCreateFunc<ProductClass, ##__VA_ARGS__>);
#endif
class ReflectFactory
{
public:
template <class BaseClass, typename... ArgType>
static BaseClass* createObj(const std::string& className, ArgType... arg)
{
typedef BaseClass* (*_CreateFactory)(ArgType...);
auto& _funcMap = getObjCreateFuncMap();
auto iFind = _funcMap.find(className);
if (iFind == _funcMap.end())
return nullptr;
else
return reinterpret_cast<_CreateFactory>(_funcMap[className])(arg...);
}
static int registerObjCreateFunc(const std::string& className, void* func)
{
getObjCreateFuncMap()[className] = func;
return 0;
}
private:
static std::unordered_map<std::string, void*>& getObjCreateFuncMap()
{
static std::unordered_map<std::string, void*> s_classRefectionMap;
return s_classRefectionMap;
}
};
- 这里reinterpret_cast<_CreateFactory>(_funcMap[className])(arg...);转换的时void* objCreateFunc(ArgType... arg)这个函数指针,将它的返回值变成了父类指针,但是具体执行的时候return new ProductClass(arg...);还是创建了子类对象,实现了多态!
环形缓冲区
环形缓冲区是一种数据结构,它可以实现在一段连续的内存中存储和读取数据,但是只能以一种循环的方式来访问。
环形缓冲区的实现原理:
- 环形缓冲区由一个缓冲区数组和两个指针组成。
- 缓冲区数组中存储的数据是环形的,即缓冲区数组的首尾相连。
- 两个指针:
- read_ptr:指向当前读入缓冲区的位置。
- write_ptr:指向下一个待写入缓冲区的位置。
- 读操作:
- 读操作从缓冲区中读取数据,首先从read_ptr指向的位置开始读取,直到缓冲区的末尾,然后从缓冲区的开头开始读取,直到read_ptr指向的位置。
- 读操作完成后,read_ptr指向下一个待读取的位置。
- 写操作:
- 写操作向缓冲区中写入数据,首先将数据写入write_ptr指向的位置,然后将write_ptr指向下一个位置。
- 如果写操作导致缓冲区的末尾被覆盖,则将缓冲区的开头到末尾的数据拷贝到缓冲区的开头,然后将write_ptr指向缓冲区的开头。
- 写操作完成后,write_ptr指向下一个待写入的位置。
环形缓冲区的优点:
- 环形缓冲区可以实现数据的循环读写,即使缓冲区的容量不足,也可以循环使用。
- 环形缓冲区可以实现数据的异步读写,即使读写操作发生在不同的线程中,也可以保证数据的完整性。
- 环形缓冲区可以实现数据的缓冲,即使数据生产的速度远远大于消费的速度,也可以缓冲数据。
手撕ringbuffer
#include <iostream>
#include <mutex>
#include <condition_variable>
template<typename T>
class RingBuffer {
public:
RingBuffer(size_t size) : m_size(size), m_buffer(new T[size]), m_read_pos(0), m_write_pos(0) {}
~RingBuffer() { delete[] m_buffer; }
bool push(const T& data) {
std::unique_lock<std::mutex> lock(m_mutex);
if (isFull()) {
return false;
}
m_buffer[m_write_pos] = data;
m_write_pos = (m_write_pos + 1) % m_size;
m_cv.notify_one();
return true;
}
bool pop(T& data) {
std::unique_lock<std::mutex> lock(m_mutex);
if (isEmpty()) {
return false;
}
data = m_buffer[m_read_pos];
m_read_pos = (m_read_pos + 1) % m_size;
m_cv.notify_one();
return true;
}
bool peek(T& data) {
std::unique_lock<std::mutex> lock(m_mutex);
if (isEmpty()) {
return false;
}
data = m_buffer[m_read_pos];
return true;
}
bool isFull() const {
return (m_write_pos + 1) % m_size == m_read_pos;
}
bool isEmpty() const {
return m_read_pos == m_write_pos;
}
private:
size_t m_size;
T* m_buffer;
size_t m_read_pos;
size_t m_write_pos;
std::mutex m_mutex;
std::condition_variable m_cv;
};
内存对齐的ringbuffer
#include <iostream>
#include <mutex>
#include <condition_variable>
template<typename T, size_t ALIGNMENT = 64>
class AlignedRingBuffer {
public:
AlignedRingBuffer(size_t size) : m_size(size), m_buffer(new char[size * sizeof(T) + ALIGNMENT]), m_read_pos(0), m_write_pos(0) {
m_buffer = (T*)(((size_t)m_buffer + ALIGNMENT - 1) & ~(ALIGNMENT - 1));
}
~AlignedRingBuffer() { delete[] m_buffer; }
bool push(const T& data) {
std::unique_lock<std::mutex> lock(m_mutex);
if (isFull()) {
return false;
}
*(T*)(m_buffer + m_write_pos) = data;
m_write_pos = (m_write_pos + 1) % m_size;
m_cv.notify_one();
return true;
}
bool pop(T& data) {
std::unique_lock<std::mutex> lock(m_mutex);
if (isEmpty()) {
return false;
}
data = *(T*)(m_buffer + m_read_pos);
m_read_pos = (m_read_pos + 1) % m_size;
m_cv.notify_one();
return true;
}
bool peek(T& data) {
std::unique_lock<std::mutex> lock(m_mutex);
if (isEmpty()) {
return false;
}
data = *(T*)(m_buffer + m_read_pos);
return true;
}
bool isFull() const {
return (m_write_pos + 1) % m_size == m_read_pos;
}
bool isEmpty() const {
return m_read_pos == m_write_pos;
}
private:
size_t m_size;
char* m_buffer;
size_t m_read_pos;
size_t m_write_pos;
std::mutex m_mutex;
std::condition_variable m_cv;
};
缓存一致性
缓存一致性(Cache Coherency)是指多个CPU或多个处理器共享同一块内存,当其中一个处理器对内存进行修改时,其他处理器需要知道这个修改,并将其缓存中的数据更新。缓存一致性保证了共享内存的正确性,是计算机系统的重要组成部分。
缓存一致性协议:MESI协议,基于总线嗅探,实现了十五的串行化。
- M(Modified):缓存行中的数据被修改,但还没有被写回主存。
- E(Exclusive):缓存行中的数据被独占使用,其他处理器不能访问。
- S(Shared):缓存行中的数据可以被多个处理器共享。
- I(Invalid):缓存行中的数据无效,需要从主存中重新加载。
MESI协议的特点:
- 缓存行的大小一般为64字节。
- 缓存行的状态变化必须通过总线嗅探来通知其他处理器。
- 缓存行的状态变化必须遵循先行发生原则。
- 缓存行的状态变化必须遵循容许延迟的原则。
- 缓存行的状态变化必须遵循写回策略。
- 缓存行的状态变化必须遵循写直达的原则。
- 缓存行的状态变化必须遵循写回拒绝的原则。
- 缓存行的状态变化必须遵循失效拒绝的原则。
CPU 在读写数据的时候,都是在 CPU Cache 读写数据的,原因是 Cache 离 CPU 很近,读写性能相比内存高出很多。对于 Cache 里没有缓存 CPU 所需要读取的数据的这种情况,CPU 则会从内存读取数据,并将数据缓存到 Cache 里面,最后 CPU 再从 Cache 读取数据。
而对于数据的写入,CPU 都会先写入到 Cache 里面,然后再在找个合适的时机写入到内存,那就有「写直达」和「写回」这两种策略来保证 Cache 与内存的数据一致性:
- 写直达,只要有数据写入,都会直接把数据写入到内存里面,这种方式简单直观,但是性能就会受限于内存的访问速度。
- 写回,当缓存行中的数据被修改,并通知其他 CPU 后,缓存行的状态就会变成「M」,这时缓存行中的数据就被标记为「脏」,然后 CPU 就开始将缓存行中的数据写入到内存中,写入完成后,缓存行的状态就会变成「S」,其他 CPU 就可以从内存中读取数据了。写回,对于已经缓存在 Cache 的数据的写入,只需要更新其数据就可以,不用写入到内存,只有在需要把缓存里面的脏数据交换出去的时候,才把数据同步到内存里,这种方式在缓存命中率高的情况,性能会更好。 当今 CPU 都是多核的,每个核心都有各自独立的 L1/L2 Cache,只有 L3 Cache 是多个核心之间共享的。所以,我们要确保多核缓存是一致性的,否则会出现错误的结果。
要想实现缓存一致性,关键是要满足 2 点:
- 第一点是写传播,也就是当某个 CPU 核心发生写入操作时,需要把该事件广播通知给其他核心。
- 第二点是事物的串行化,这个很重要,只有保证了这个,次啊能保障我们的数据是真正一致的,我们的程序在各个不同的核心上运行的结果也是一致的。
基于总线嗅探机制的 MESI 协议,就满足上面了这两点,因此它是保障缓存一致性的协议。 MESI 协议,是已修改、独占、共享、已实现这四个状态的英文缩写的组合。整个 MSI 状态的变更,则是根据来自本地 CPU 核心的请求,或者来自其他 CPU 核心通过总线传输过来的请求,从而构成一个流动的状态机。另外,对于在「已修改」或者「独占」状态的 Cache Line,修改更新其数据不需要发送广播给其他 CPU 核心。
定时器
定时器是操作系统提供的一种机制,用于在指定的时间点后执行某个任务。
#include <sys/epoll.h>
#include <functional>
#include <chrono>
#include <set>
#include <memory>
#include <iostream>
using namespace std;
struct TimerNodeBase
{
time_t expire;
int64_t id;
};
// TimerNode 继承 TimerNodeBase
struct TimerNode:public TimerNodeBase
{
// C++ 11特性,使用函数对象。降低拷贝消耗,提高效率
using Callback = std::function<void(const TimerNode &node)>;
Callback func;
// 构造函数,只构造一次
TimerNode(int64_t id,time_t expire,Callback func):func(func){
this->id = id;
this->expire = expire;
}
};
// 基类引用,多态特性
bool operator<(const TimerNodeBase &lhd, const TimerNodeBase &rhd)
{
if (lhd.expire < rhd.expire)
return true;
else if (lhd.expire > rhd.expire)
return false;
return lhd.id < rhd.id;
}
class Timer
{
public:
static time_t GetTick()
{
/* C++ 11时间库chrono */
//表示一个具体时间
auto sc = chrono::time_point_cast<chrono::milliseconds>(chrono::steady_clock::now());
auto tmp = chrono::duration_cast<chrono::milliseconds>(sc.time_since_epoch());
return tmp.count();
}
TimerNodeBase AddTimer(time_t msec, TimerNode::Callback func)
{
time_t expire = GetTick() + msec;
//避免拷贝、移动构造
auto ele = timermap.emplace(GenID(), expire, func);
return static_cast<TimerNodeBase>(*ele.first);
}
bool DelTimer(TimerNodeBase &node)
{
// C++ 14新特性,不在需要传一个key对象,传递一个key的等价值
auto iter = timermap.find(node);
if (iter != timermap.end())
{
timermap.erase(iter);
return true;
}
return false;
}
bool CheckTimer()
{
auto iter = timermap.begin();
if (iter != timermap.end() && iter->expire <= GetTick())
{
iter->func(*iter);
timermap.erase(iter);
return true;
}
return false;
}
time_t TimeToSleep()
{
auto iter = timermap.begin();
if (iter == timermap.end())
return -1;//没有定时任务,设置epoll一直阻塞。
time_t diss = iter->expire - GetTick();
return diss > 0 ? diss : 0;
}
private:
static int64_t GenID()
{
return gid++;
}
static int64_t gid;
set<TimerNode, std::less<>> timermap;
};
int64_t Timer::gid = 0;
#define EPOLL_EV_LENFTH 1024
int main()
{
int epfd = epoll_create(1);
unique_ptr<Timer> timer = make_unique<Timer>();
int i = 0;
timer->AddTimer(1000, [&](const TimerNode &node) {
cout << Timer::GetTick() << "node id:" << Home | NODE.ID << " revoked times:" << ++i << endl;
});
timer->AddTimer(1000, [&](const TimerNode &node) {
cout << Timer::GetTick() << "node id:" << Home | NODE.ID << " revoked times:" << ++i << endl;
});
timer->AddTimer(3000, [&](const TimerNode &node) {
cout << Timer::GetTick() << "node id:" << Home | NODE.ID << " revoked times:" << ++i << endl;
});
auto node = timer->AddTimer(2100, [&](const TimerNode &node) {
cout << Timer::GetTick() << "node id:" << Home | NODE.ID << " revoked times:" << ++i << endl;
});
timer->DelTimer(node);
cout << "now time:" << Timer::GetTick() << endl;
epoll_event evs[EPOLL_EV_LENFTH] = { 0 };
while (1)
{
int nready = epoll_wait(epfd, evs, EPOLL_EV_LENFTH, timer->TimeToSleep());
for (int i = 0; i < nready; i++)
{
/*处理IO事件*/
}
// timer检测和处理
while (timer->CheckTimer());
}
return 0;
}
IO多路复用
IO多路复用是指通过一种机制,一个进程可以监视多个文件句柄fd,一旦某个文件句柄状态改变(比如可读,可写,网络数据到达等),能够通知程序进行相应的读写操作。
select/poll/epoll都是IO多路复用的机制,本质上都是同步I/O模型,都需要在读写事件就绪后自己负责读写,也就是说都需要自己处理读写事件。
select的缺点:
- 单个进程能够监视的文件描述符数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核解决。
- 对于大量连接的服务器,需要维护大量的描述符,内存消耗大。
- 效率不高,每次调用select都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时是比较大的。
select与poll的区别在于:
- poll使用了pollfd结构体,而不是bitmap,减少了内存消耗,且select的bitmap直接在fd置位不可重用,需要每次重置它。poll选择修改pollfd的revents字段(之后可以置0,就可以重用)。
- 大小没有了1024的限制,可以处理更多的连接。
epoll的优点:
- 没有最大描述符限制,能打开的FD上限远大于1024。
- 效率很高,只有活跃的连接才会调用回调函数,不会浪费CPU资源。
- 内存使用效率高,不会随着FD数目的增加而线性增长。
- 采用事件驱动模型,只需要注册一次事件,之后只要该事件发生,内核会通知进程。
- epfd用户态和内核态共享。置位->重排
简易内存池
#include <cstdlib>
// 每个内存块的前4/8字节用于存储链表指针
struct MemoryBlock {
MemoryBlock* next;
};
class FixedMemoryPool {
private:
MemoryBlock* free_list; // 空闲链表指针
size_t block_size; // 实际供用户使用的块大小
void* pool_begin; // 初始内存池首地址
void* pool_end; // 初始内存池结束地址
public:
// 初始化预分配内存块
FixedMemoryPool(size_t user_block_size, int num_blocks) {
block_size = user_block_size;
// 总内存 = 块数量 * (用户需求大小 + 指针大小)
size_t total_size = num_blocks * (block_size + sizeof(MemoryBlock*));
pool_begin = malloc(total_size);
pool_end = (char*)pool_begin + total_size;
// 初始化空闲链表
free_list = static_cast<MemoryBlock*>(pool_begin);
MemoryBlock* current = free_list;
for(int i=0; i<num_blocks-1; ++i) {
current->next = reinterpret_cast<MemoryBlock*>(
(char*)current + block_size + sizeof(MemoryBlock*)
);
current = current->next;
}
current->next = nullptr;
}
// 分配内存
void* alloc() {
if(!free_list) return nullptr; // 可扩展为自动扩容
MemoryBlock* block = free_list;
free_list = free_list->next;
return (void*)(block + 1); // 返回指针后移一个指针大小的位置
}
// 回收内存
void dealloc(void* ptr) {
if(!ptr) return;
// 指针回退得到完整的MemoryBlock结构
MemoryBlock* block = static_cast<MemoryBlock*>(ptr) - 1;
block->next = free_list;
free_list = block;
}
~FixedMemoryPool() {
free(pool_begin);
}
};
设计原则SOLID
- 单一职责原则 一个类应该只有一个因其他变化的原因,一个类应该只负责一个职责或者功能。
- 开闭原则 软件实体应该是可扩展的,但是不可修改的。我们应该在不修改现有代码的情况下添加新的功能。对扩展开放,对修改关闭。
- 里氏替换原则 子类型必须能够替换掉它们的基类型。
- 接口隔离原则 客户端不应该被强迫依赖它们不适用的接口。
- 依赖倒置原则 高层模块不应该依赖于低层模块。两者都应该依赖于抽象。抽象不应该依赖于细节。细节应该依赖于抽象。底层细节实现可以根据情况进行替换,高层调用的是抽象。
内存对其原则
TCP滑动窗口实现细节
滑动窗口工作原理
- 发送方维护一个发送窗口,包含:
- 已发送待确认的数据
- 可以发送但未发送的数据
- 接收方维护一个接收窗口,通告剩余缓冲区大小
- 窗口滑动条件:
- 收到新的ACK确认
- 接收方窗口更新
// 简化的滑动窗口实现
class TcpSlidingWindow {
private:
uint32_t send_base; // 发送窗口起始序号
uint32_t next_seq; // 下一个要发送的序号
uint32_t window_size; // 当前窗口大小
std::map<uint32_t, Packet> sent_packets; // 已发送未确认的数据包
public:
void on_ack_received(uint32_t ack_num) {
// 移动发送窗口
while (send_base < ack_num) {
sent_packets.erase(send_base);
send_base++;
}
}
void on_window_update(uint32_t new_window) {
window_size = new_window;
}
bool can_send() const {
return (next_seq - send_base) < window_size;
}
};
拥塞控制算法实现
1. 慢启动算法
- 窗口大小从1个MSS开始
- 每收到一个ACK,窗口大小增加1个MSS
- 指数增长直到达到阈值(ssthresh)
2. 拥塞避免算法
- 窗口大小线性增长
- 每RTT时间窗口增加1个MSS
- 公式:cwnd = cwnd + 1/cwnd
3. 快速重传算法
- 收到3个重复ACK立即重传丢失报文
- 无需等待超时
4. 快速恢复算法
- 重传后不将窗口降为1
- 将窗口设置为ssthresh + 3*MSS
- 进入拥塞避免阶段
class TcpCongestionControl {
private:
uint32_t cwnd; // 拥塞窗口大小
uint32_t ssthresh; // 慢启动阈值
uint32_t dup_ack_count; // 重复ACK计数器
public:
void on_packet_loss() {
ssthresh = cwnd / 2;
cwnd = 1; // 慢启动
dup_ack_count = 0;
}
void on_dup_ack() {
dup_ack_count++;
if (dup_ack_count == 3) {
// 快速重传
retransmit_packet();
ssthresh = cwnd / 2;
cwnd = ssthresh + 3; // 快速恢复
}
}
void on_new_ack() {
if (cwnd < ssthresh) {
// 慢启动阶段
cwnd += 1; // 指数增长
} else {
// 拥塞避免阶段
cwnd += 1.0 / cwnd; // 线性增长
}
dup_ack_count = 0;
}
};
(1) 结构体第一个成员的偏移量(offset)为0,以后每个成员相对于结构体首地址的 offset 都是该成员大小与有效对齐值中较小那个的整数倍,如有需要编译器会在成员之间加上填充字节。
(3) 结构体的总大小为 有效对齐值 的整数倍,如有需要编译器会在最末一个成员之后加上填充字节
TCP与UDP协议对比
1. 基本概念
TCP (传输控制协议)
- 面向连接的协议
- 提供可靠的、有序的数据传输
- 具有流量控制和拥塞控制机制
- 基于字节流的传输方式,分段,大于MSS的包会被拆分
UDP (用户数据报协议)
- 无连接的协议
- 提供不可靠的数据传输服务
- 传输速度快但无法保证数据到达
- 基于数据报的传输方式,不分段,不保证包的顺序
2. 主要区别
| 特性 | TCP | UDP |
|---|---|---|
| 连接方式 | 面向连接(三次握手建立连接) | 无连接 |
| 可靠性 | 可靠传输(确认、重传机制) | 不可靠传输 |
| 数据顺序 | 保证数据按序到达 | 不保证顺序 |
| 流量控制 | 有(滑动窗口) | 无 |
| 拥塞控制 | 有(多种算法) | 无 |
| 传输效率 | 相对较低 | 相对较高 |
| 头部开销 | 20-60字节 | 8字节 |
| 双工性 | 全双工 | 可以是全双工 |
3. 应用场景
TCP典型应用
- Web浏览(HTTP/HTTPS)
- 文件传输(FTP)
- 电子邮件(SMTP/POP3/IMAP)
- 远程登录(SSH/Telnet)
UDP典型应用
- 视频/音频流媒体
- 实时在线游戏
- DNS查询
- VoIP(如Skype)
- 广播/多播应用
4. 选择建议
- 需要可靠数据传输时选择TCP
- 需要低延迟、可容忍少量数据丢失时选择UDP
- 实时性要求高的应用通常选择UDP
- 需要确保数据完整性的应用选择TCP
5. 总结
TCP和UDP各有优缺点,没有绝对的优劣之分,关键是根据应用场景的需求选择合适的协议。现代网络应用中,很多场景会同时使用两种协议,发挥各自的优势。
6. 在UDP上实现可靠传输
虽然UDP本身不提供可靠性保证,但可以在应用层实现类似TCP的可靠传输机制。以下是常见的实现方案:
6.1 基本实现思路
- 序列号机制:为每个数据包分配唯一序列号
- 确认应答机制:接收方返回ACK确认收到的数据
- 超时重传机制:未收到ACK时重传数据
- 流量控制:通过滑动窗口控制发送速率
- 拥塞控制:实现简单的拥塞避免算法
6.2 实现示例
class ReliableUDP {
private:
uint32_t next_seq; // 下一个发送序号
uint32_t ack_seq; // 已确认序号
std::map<uint32_t, Packet> unacked_packets; // 未确认数据包
std::mutex mtx;
// 重传定时器
void start_retransmit_timer(uint32_t seq) {
std::thread([this, seq](){
std::this_thread::sleep_for(std::chrono::milliseconds(RTO));
std::lock_guard<std::mutex> lock(mtx);
if (unacked_packets.count(seq)) {
resend_packet(seq);
}
}).detach();
}
public:
void send(const std::string& data) {
std::lock_guard<std::mutex> lock(mtx);
Packet packet = create_packet(next_seq++, data);
unacked_packets[packet.seq] = packet;
udp_send(packet);
start_retransmit_timer(packet.seq);
}
void on_ack_received(uint32_t ack_seq) {
std::lock_guard<std::mutex> lock(mtx);
// 移除已确认的数据包
while (!unacked_packets.empty() &&
unacked_packets.begin()->first <= ack_seq) {
unacked_packets.erase(unacked_packets.begin());
}
this->ack_seq = ack_seq;
}
};
6.3 开源实现参考
- QUIC:Google开发的基于UDP的可靠传输协议
- UDT:高性能UDP数据传输协议
- ENET:游戏网络库,提供可靠UDP传输
- RUDP:可靠UDP协议实现
6.4 注意事项
- 性能开销:可靠性机制会增加延迟和CPU使用率
- 实现复杂度:需要处理各种边界条件和异常情况
- 不适合所有场景:简单应用可能不需要完整可靠性
- 缓冲区管理:需要合理设置发送和接收缓冲区大小
6.5 选择建议
- 需要低延迟但又要一定可靠性的场景适合UDP+应用层可靠性
- 完全可靠性要求高的场景建议直接使用TCP
- 实时性要求极高的场景可以使用原生UDP