当前位置: 首页 > web >正文

C++Linux八股

一、C++ 语法与语言特性

  • 面向对象的三大基本特性

1.封装:将数据(成员变量)和对数据操作的方法(成员函数)封装在一个类里,对外隐藏实现细节,只提供接口进行调用。
2.继承:子类继承父类的成员和函数,在此基础上可以进行拓展,新增成员变量/函数。
3.多态:相同的接口,但表现出不同的行为。可以分为编译时多态(函数重载/模板)和运行时多态(虚函数)。

  • 多态的实现原理(静态多态 vs 动态多态)

多态分为静态多态和动态多态

1.静态多态 指的是在编译时就确定好的函数调用关系  函数重载 模板

2.动态多态 指的是在函数运行的过程根据 实际对象的类型动态决定需要调用的函数 虚函数调用机制

虚函数调用机制是怎么实现的?

虚函数表+虚函数指针

当一个类中定义了虚函数时,编译器会为该类生成一张虚函数表(vtable),表中存放指向该类虚函数实现地址,在对象初始化时创建指向虚表指针。如果子类继承父类,并重写了某个虚函数会拷贝一份父类的虚表 并把这个函数地址会被替换为子类虚函数的地址。(写时拷贝)

父类 子类都有自己的虚表 和指针。所以父类指针指向子类对象,调用重写的虚函数时,是通过实际对象里面的虚函数指针找到虚表 根据虚表存储的地址调用重写的函数。

这也是为什么构造函数不能是虚函数,对象没创建 没有虚表指针怎么进行虚函数调用。

怎么理解动态?

编译时不知道父类指针指向的对象是什么,在运行时找到实际的对象 根据实际对象的虚函数指针 找到对应虚表完成调用。

  • 为什么析构函数要声明为 virtual?

目的是为了确保通过 基类指针删除子类对象 时,能够通过虚函数调用机制 正确调用子类的

析构函数

  • 虚函数和纯虚函数

虚函数 virtual void func(); 

可以默认实现,子类可以选择重写或者不重写,可以实例化 用来支持多态

纯虚函数:virtual void func()=0; 

没有实现,子类继承必须重写,有纯虚函数的类就是抽象类 抽象类不能实例化,用于统一接口 强制子类实现。

比较项虚函数(virtual)纯虚函数(pure virtual)
语法virtual void func();virtual void func() = 0;
是否有函数体✅ 可以有默认实现❌ 没有实现(仅声明)
子类是否必须重写❌ 可选(不重写也行)✅ 必须重写,否则子类也是抽象类
所在类是否可实例化✅ 可以❌ 不行(含纯虚函数的类为抽象类)
作用支持运行时多态用于定义接口/规范,强制子类实现
  • lambda 表达式

lambda格式:

[捕获列表] (参数) mutable -> 返回值类型 { 函数体}

mutable:默认情况下,lambda函数总是一个const函数,加上mutable可以取消其常量

捕获方式:

[=] 全部按值捕获 [&]全部引用捕获 [&,=x]全部&但x变量值捕获

虽然说[=]/[&]全部捕获 其实只有用到的才会捕获 以及指定捕获的

lambda原理:

定义了一个lambda表达式,编译器会生成一个类,里面会实现重载operator() 参数就是lambda传入的参数 具体实现就是函数体,捕获列表相当于类的成员变量。

auto lambda = [a](int x) { return x + a; };
class __lambda_uniq {int a;
public:__lambda_uniq(int _a) : a(_a) {}int operator()(int x) const {return x + a;}
};

每个 Lambda 表达式都会生成唯一类型,lambda函数命名方式是lambda_uuid 后面加上唯一识别码,虽然两个lambda函数实现完全相同,但它们类型是不相同的,地址也不同 不能进行赋值。

  • 什么是this指针 作用是什么

this 指针是指向类对象本身的指针,每个非静态成员函数都会有一个 this 指针,它由编译器在函数调用时隐式传入指向调用该函数的对象

这个this指针有三个作用 1.访问成员变量,如果传入的参数名和成员变量名一致可以进行区分(this->a=a) 2. 返回当前对象 *this实现链式调用 return *this,A a.set(id).set(name)

3. 判断是否为是自己,在运算符重载时 要先判断 if (this == &other) return *this;

为什么说非静态的函数有this,静态的就没有吗?

因为this是指向调用该函数的对象,而调用静态的成员函数 不需要创建对象来调用(参数中没有this指针)。或者说非静态成员函数的参数需要this指针,而静态函数属于类 不需要this指针。

非静态成员函数属于“对象”,所以有 this;静态成员函数属于“类”,不需要 this。

  • 左值引用 右值引用 完美转发

1.左值就是有名字 可以获取地址的变量。左值引用就是引用左值,给左值取别名。int&x=y;

2.右值就是没有名字 不能取地址 。一些常量 12 "asd" 临时变量 X+Y 函数返回值。int&&x=10;  10是常量是右值,那x是右值吗?x是左值,因为x要有空间存储10,所以有地址。可以被修改 如果被const修饰就不能。

1.左值引用 一定引用左值吗?不一定 const int&x=10 加上const左值引用可以引用右值

2.右值引用 一定引用右值吗?不一定 int&&x=std::move(y) move移动将左值强制为右值,在底层左值和右值没有区别 move只是为了编译可以通过。

左值引用一般用于 减少拷贝 在函数传参 返回时可以用左值引用减少拷贝,对于右值也可以加上const进行引用。

那左值引用左值右值都能引用,右值引用还有什么用?

const T& 左值引用是一个只读引用,通过它不能修改对象。

右值引用作用:

右值引用提供一个可修改右值的接口,可以实现移动语义(减少拷贝)和完美转发(保留原本变量左值右值属性)。

移动构造vs拷贝构造 引用右值

void set(std::string&& s) {this->name = std::move(s); // 能移动(高效)
}void set(const std::string& s) {this->name = s; // 只能拷贝(性能差)
}

1.拷贝构造引用右值 constT& s 引用的值不能修改,修改必须拷贝一份。那我move(s),不行这样实际上还是拷贝,因为移动要求可修改。

2.移动构造引用右值,T&& s 引用的值可以修改,s引用的右值 但它本身是左值存储指向右值对象的地址。对右值进行修改时通过move()转移资源所有权不需要再进行拷贝。

 eg.std::string&& s =std::string("sdasaacsdvsdvcasascas")  (大于15字节 存储堆上)

std::string&& s 在栈上保存的是一个指向右值对象 std::string("sda...") 的地址,而这个右值对象本身内部又包含一个指向 "sda..." 内容的堆区指针。

const T& 可绑定右值但不可修改,因此无法移动;T&& 可修改右值,但本身是左值,需用 std::move(s) 显式触发移动操作,否则仍会退化为拷贝。

 

右值引用的作用是:在 C++ 中识别并绑定临时对象,实现“资源的转移而非拷贝”,从而支持移动语义+完美转发。

完美转发

什么是完美转发,就是将函数参数原封不动的传递给另一个函数。类型 const 左值/右值

如果没有右值引用 怎么接收右值,加const 又会改变左值/右值的属性。

template<typename T>
void wrapper(T&& arg) {target(std::forward<T>(arg));  // ✅ 完美转发
}

std::forward<T>(arg) 会根据 T 是 T& (左值)还是 T(右值),把 arg 强制恢复成对应的左值或右值表达式。

  • 智能指针区别(unique_ptr / shared_ptr

智能指针就是为了防止我们new申请了资源忘记释放 程序异常提前返回 导致资源泄漏,用智能指针管理的对象在生命周期结束时自动释放。 

智能指针的作用是:通过 RAII(资源获取即初始化)机制,用类对象管理资源,在离开作用域时自动调用析构函数释放资源,从而避免手动管理的错误。

常用的智能指针有unique_ptr shared_ptr weak_ptr

(还有个auto_ptr被丢弃,会发生隐式的所有权转移 A=B把B资源的所有权转移给A,但你可能不知道,所以访问B会发生野指针的问题。反观unique_ptr会强制让你A=std::move(B) 显示转移资源)

1.unique_ptr 独占一个资源的所有权,一个对象只能有一个unique_ptr管理,不能进行拷贝/赋值,只能通过move()转移资源的所有权 进行移动构造/赋值。unique_ptr指针的生命周期解释就会释放管理的资源。

2.shared_ptr 共享一个资源的所有权,一个对象可以由多个shared_ptr指针来进行管理,可以进行拷贝/赋值。所有指向该资源的shared_ptr全析构 才能释放资源。shared_ptr会额外申请一个计数器来记录指针的数量,--到0时释放。

make_shared<T> 和 直接 shared_ptr<T>(new T) 一个对象管理有什么区别?

        1.减少内存碎片化 shared管理的对象要申请两部分空间,1.记录指针数的int* count空间 2.对象申请的空间。如果是new会分别申请,会造成内存碎片化。make_shared分配一块连续内存。
2.安全性,如果是 shared_ptr<T>(new T) new对象成功了,但构造shared_ptr失败会导致内存泄漏。

shared_ptr循环引用 为什么会导致资源泄漏?怎么解决?

两个shared_ptr管理的对象里面相互包含了对方的指针,导致指针的引用计数永远减不到0,外部管理的shared_ptr指针析构了,但对象内部的shared_ptr指针相互引用着。

怎么解决?对象内部相互指向让引用计数不++就可以了。

3.用weak_ptr代替shared,能指向对象但并不会++。

  • C++的内存管理

1.C++内存分布 自上到下 从高地址到低地址

内核空间 栈-> 内存映射段 <-堆 数据段 代码段

栈:局部变量 内存映射段:共享内存 静态库 堆:new malloc申请的空间

数据段:静态变量 全局变量 代码段:代码 常量

2.栈和堆

栈上一般存放我们定义的局部变量,空间由系统自动分配 生命周期结束自动回收,分配速度快,栈空间小。

堆放new malloc申请的空间,用户主动申请 delete主动释放,分配速度满慢 堆空间大

  • new delete malloc free

new是一个运算符申请空间,malloc函数申请空间。但new会申请空间并调用对象的构造函数,申请失败会抛异常。malloc只申请空间失败返回nullptr。同理delete先会调用对象的析构函数再释放空间。

具体来说new=operator new+构造函数。operator new底层调用malloc申请空间还会完成失败抛异常的操作。

总结:1.new运算符 malloc函数 2.new会抛异常 3.new会调用构造函数

new = operator new(malloc申请空间+失败抛异常) + 对象构造函数

可以new对象 free释放吗?

不能,如果说new对象的构造函数中给成员变量申请了空间,而free只会释放成员变量的空间,而里面int*成员变量申请的空间没有通过析构函数进行释放就会造成内存泄漏

同理 malloc delete,delete会先调用对象的析构函数 而malloc只是申请了一块空间 没有构造对象,自然无法调用析构函数 造成未定义行为。

new[] delete[]

A* arr = new A[10]; 相当于 operator new一次申请10个A对象的空间,并调用10次构造函数。批量一次性分配 + 就地批量构造

如果是自定义类型,还会额外申请8字节的空间存储这个数组的元素的个数,就是为了后面delete[] arr不用传数字就能调用对应次数的delete把数组对象全部一个一个析构。 

但如果是基本数据类型 int bool... 没有构造析构函数,new[]就不会额外申请空间 直接申请一块空间,delete也不会调用析构,直接释放整块内存。

new[]申请 delete释放呢?不行这样只会释放一个对象,或者错误。

new[] + 类对象 → 多分配元信息 + 构造/析构循环
new[] + 基础类型 → 不插入额外信息,简单分配释放

  • 单例模式实现(懒汉/饿汉 + 线程安全版本)

单例模式分为饿汉模式 懒汉模式。

饿汉是 类内声明单例对象 类外定义,程序运行起来的时候就创建。

懒汉模式 类内get()获取单例函数内 声明定义单例对象,第一次获取单例对象的时候才会创建。
构造和析构函数都私有,拷贝/赋值构造函数私有并禁止。 

  • 指针和引用区别

指针是一个变量用一块内存存储指向对象的地址。而引用更像是给被引用的对象取别名,这个别名就相当于变量本身。

1.内存分配:指针是一个对象需要内存存储目标变量地址,而引用不会分配内存(除非是类的成员变量)。

2.解引用语法:指针通过*解引用来访问对象进行操作,引用通过直接对别名进行操作就行。

3.初始化要求:指针可以指向空,而引用一开始必须要初始化 不能为空。

4.可变性:指针可以更改指向对象,而引用不能更改引用的对象

5.多级结构支持:指针有多级指针,而引用没有多级 只能说一个对象有多个引用。

  • 内存对齐

1.第一个成员在结构体偏移量为 0 的地址处。

2.其他成员变量要对齐到其对齐数的整数倍的地址处。

        对齐数 = 编译器默认的对齐数(32位 4字节 64位 8字节) 与该成员大小的较小值。

3.结构体总大小 = 所有成员对齐数中的最大值的整数倍。

4.结构体的整体对齐数 = 其所有成员(包括嵌套结构体)的最大对齐数

eg.Inner 大小 结构体对齐数 Outer 大小?

默认对齐数为8字节

struct Inner {char x;    // 1 字节int y;     // 4 字节
};struct Outer {double d;  // 8 字节Inner in;  // 结构体short s;   // 2 字节
};

1.先算Inner各成员的 对齐数

该成员类型和默认对齐数取最小值

        1.char x  1<8 =>1 2.int y 4<8 => 4

2.再算Inner结构体的整体对齐数

结构体的整体对齐数等于 成员对齐数的最大值

        1<4 =>4

3.计算Outer大小

        1.先算各成员对齐数 1.double 8=8 =>8 2.Inner 4<8=>4 3.short 2<8 => 2

        2.计算大小 

                1.其他成员变量要对齐到其对齐数的整数倍的地址处 8+4+2=14

                2.结构体总大小 = 所有成员对齐数中的最大值的整数倍 14%8!=0 补齐到24

所以Outer大小为24

结构体成员偏移填充说明
Innerchar x0
padding1-3int y 对齐到 4
int y4
sizeof(Inner)8对齐到 4 的倍数
Outerdouble d0
Inner in8对齐到 4,因 alignof(Inner)=4
short s16对齐到 2
padding18-23对齐结构体整体到 8 的倍数
sizeof(Outer)24最大对齐数 = 8

二、STL 与数据结构

  • 排序算法

排序算法最好时间复杂度最坏时间复杂度平均时间复杂度空间复杂度稳定性
选择排序O(n²)O(n²)O(n²)O(1)✘ 不稳定
冒泡排序O(n)O(n²)O(n²)O(1)✔ 稳定
堆排序O(nlogn)O(nlogn)O(nlogn)O(1)✘ 不稳定
插入排序O(n)O(n²)O(n²)O(1)✔ 稳定
希尔排序O(nlogn)~O(n¹.³)O(n²)O(nlog²n)O(1)✘ 不稳定
快速排序O(nlogn)O(n²)O(nlogn)O(logn)✘ 不稳定
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)✔ 稳定
计数排序O(n+k)O(n+k)O(n+k)O(n+k)✔ 稳定

插入排序和希尔排序

1.插入排序   0~n个元素  对于第 i+1 个元素,此时0~i 的元素都是有序的,end=i表示最后一个有序的元素,tmp=a[i+1]表示要插入的元素值  然后一直和前面有序的元素进行比较,直到找到合适的位置进行插入。
最好时间复杂度O(n):有序不需要排序 最差时间复杂度:O(n^2)逆序

空间复杂度:O(1)稳定
2.希尔排序 插入的排序的升级版,但不稳定

希尔排序可以分为预排序 细排。

预排序的目的是把无序的数组变成接近有序。我们可以把一组数组分为多组,在同一组中每个元素在原数组的下标之间的距离(gap)都是一样的,然后对每一组进行插入排序,使原数组接近有序。

细排通过插入排序(gap==1)把接近有序的数组变成有序。

坏处是插入排序是稳定的,但希尔排序是不稳定的。

快速排序

  • mapunordered_map 的底层实现与差异(红黑树 vs 哈希表)

map底层用的数据结构是红黑树,会以key值进行排序,默认是升序,即中序遍历红黑树key值是升序的,如果是自定义key值,或者想降序 需要自己实现比较器。查找 删除的效率是O(logN)

unordered_map底层是哈希表,它是无序的,通过哈希函数 将key映射到一个数组下标,数字一般是取模 字符串通过一种算法,不同的key可能会映射到同一个哈希痛中,这就会引发哈希冲突,一般有两种解决方法 1.链表法(STL默认) val存放的是一个链表/vector 把当前元素连入链表的末尾2.开放定址法 往后面遍历直到找到空位置。 同理自定义的key就需要自己实现哈希函数 1.自定义结构体中重载operator==  2.传入自定义的哈希函数 将自定义的key转换为一个哈希值。它的查找删除效率位O(1)~O(n)

对key进行排序 范围查找一般用map。无序 注重查找 删除效率用unordered_map 最快O(1),但会有冲突链 占用内存稍大。

 和set unordered_set比较怎么样?

需要进行排序 范围查找的选择map底层用的是红黑树,想进行快速查找 快速插入删除操作的选择unordered_map底层是哈希桶(STL默认是链表法)。 set unordered_set和它们的区别就是,set是没有val值的,只会存储key。

  • emplace_back push_back

push_back在容器的尾部插入一个对象

emplace_back在容器的尾部直接构造一个对象

push_back("asd")=构造临时对象string+移动构造

emplace("asd")=构造

emplace通过完美转发参数,避免了构建中间临时变量的生成和额外的临时拷贝/移动操作,如果没有构建对象的话,建议用emplace。当然如果对象已经构建好了,也可以用emplace但就等同于push_back还可能降低代码可读性。

  • STL 容器使用场景对比(vector / list / map)

vector:1.需要高效随机访问的 2.只在尾部进行插入/删除的 3.内存连续 对缓存友好

        1.不适合 需要在中间进行插入/删除操作的

list:1.频繁在任意位置进行插入/删除元素的 2.遍历可以完成顺序输出 但不支持随机查找

        1.不适合频繁查找元素的 2.不适合频繁进行排序的

map:1.可以存入一对键值key val,基于Key完成一个顺序排序 元素自动完成排序 2.适合快速查找 插入 删除一对键值 

        1.不适合非Key值索引访问

需求/特性推荐容器
按下标访问 + 顺序处理vector
频繁插入/删除任意位置list
需要按key排序 + 快速查找map
只需要key存在性判断,无序字典unordered_map
模拟栈、队列等顺序容器vector / list
需要最大/最小值等自动排序结构map / set
链表vs数组
对比项数组(Array)链表(Linked List)
内存结构连续内存零散内存 + 每个节点保存指针
访问方式下标随机访问:O(1)从头逐个遍历:O(n)
插入删除效率插入删除慢:需移动元素 O(n)插入删除快:只改指针 O(1)*
空间效率分配时必须指定大小,扩容麻烦动态申请内存,灵活
内存利用率无额外开销每个节点多占一个指针的空间
适合场景需要频繁访问元素,比如查表、排序需要频繁插入/删除,比如队列、链表
  • vector<>中迭代器失效的问题

迭代器本质就是指针,迭代器失效就是原本我指向内存空间已经不属于vector或者销毁了移动了。

会引发迭代器失效的行为 1.扩容 新找一块更大的空间 复制原空间数据并释放原空间。这就导致指向原空间的迭代器全部失效。2.插入 在内存中间进行插入 前面空间的迭代器不会失效 但后面的值移动了 导致后面的迭代器失效 3.删除 和插入同理 后面空间内的值移动了。

具体函数:1.push_back 触发扩容时 迭代器全失效

2.insert 1.触发扩容 全失效 2.插入 插入位置已经后面位置失效 (insert返回新插入位置的迭代器,根据新插入位置的迭代器 可以往后找)

3.erase 删除 删除位置和后面的位置迭代器失效 (erase返回删除位置后面一个位置)

函数名失效条件与范围返回值说明
push_back()扩容时:所有迭代器失效无返回值
insert(pos)不扩容:插入位置及其后失效;扩容:全部失效返回新插入元素的迭代器
erase(pos)删除位置及其后迭代器失效返回被删除元素之后的位置迭代器

vector<>函数 reserve resize  
1.reserve 扩容。改变capacity大小,不改变元素个数size。提前分配空间避免频繁扩容
2.resize 调整元素个数。改变size大小,调整后的size>capacity会改变capacity大小。
调整元素个数 调用析构/构造,既可以减少多余的元素,也能增加新元素(resize(调整后的size大小,初始值) 第二个参数有缺省值=0 也可以不传) 

std::reverse 来反转元素

  •  deque 双端队列 和 list vector对比

deque 双端队列,支持随机访问 并可以在头尾两端进行高效插入删除的容器,底层是一段段的连续的内存块组成。

底层原理:一组组大小固定的内存块+map指针数组进行管理。

1.随机访问:deque[i]

map中元素按顺序指向每个内存块的起始地址,而每个内存块可以存入多个元素。可以把map理解成一个目录,map[i/block_size]找出在第几个块 block[i%block_size]根据偏移量 完成随机访问。

2.push_back / push_front(尾插 / 头插)

直接在头/尾块内进行插入。如果块满了,就新建块进行插入 并在map新增一个指针指向这个新块。

但如果在头部新增块,那map也要头插元素指向新增块,但map本质是数组不适合头插?

实际上map是个预留空间的环形数组,比如说数组空间有64 但实际一般用中间的32个 前面和后面会空出几个空间 留给头尾插入时新增元素。这样就不用移动整个数组,如果没有空间用就会重新分配。


deque,对比链表支持随机访问,对比数组支持快速的头插。

使用场景:

任务队列(如线程池任务)

多线程或后台服务中,任务可能要按顺序执行,或者支持优先从前面插入紧急任务,这时 deque 就很合适。

  • 二叉树的遍历方式 + 层序遍历代码实现


三、操作系统(Linux 系统原理)

  • 静态库 动态库

静态库 动态库最大的区别在于 1.解析符号的时间和方式不同 2.可执行文件体积不同 3.是否依赖库

静态库链接时对外部符号(变量名 函数名)进行解析,把外部符号替换成库中的代码。因此,生成的可执行文件体积较大,但在运行时不再依赖外部库文件,具备良好的独立性。

动态库在链接阶段保留外部符号引用 。程序运行时,由动态链接器(如 ld.so)对外部符号进行解析,将外部符号的占位地址替换为库中实际的地址,从而完成符号重定位和库加载。

简单来说:静态库是在链接时“复制代码”,动态库是在运行时“绑定地址”。

静态库优点:1.运行快 2.运行不依赖外部库 3.运行稳定
缺点:1.可执行文件体积大 2.更新时需要重新编译链接 3.浪费内存 用同一个静态库的可执行程序可能会出现大量相同的代码内容
动态库优点:1.生成的可执行文件小 2.支持热更新 不需要重新编译 3.节省内存 将动态库放入内存映射段 通过改变符号的地址找到动态库中的实际的地址。
缺点:1.运行时需要解析符号 耗时长 2.依赖外部库 3.不稳定 容易发生版本地狱(多个程序或库依赖同一个动态库的不同版本) 版本冲突

动态库的整个文件会映射进虚拟地址空间,但只有用到的部分才会被操作系统按页加载进实际物理内存。

静态库只会把你用到的部分复制进可执行文件,其它部分不会被打包进去,也不会占用空间或进入最终程序。

动态库:整个映射进虚拟内存,但未访问的部分不会占用物理内存。

静态库:只复制用到的部分进可执行文件,不会浪费;

  • 线程与进程的区别

1.本质区别:进程是系统分配资源最基本的单位(内存 CPU时间),线程是CPU调度的基本单位。

2.包含关系:进程包含线程 进程至少包含一个线程

3.资源开销:进程间切换涉及内存页表、上下文、缓存清除,开销较大;线程间共享内存空间切换小。

4.影响关系:一个进程崩溃后,不会影响其它进程但是一个线程崩溃可能导致整个进程被操作系统杀掉,所以多进程要比多线程健壮。

什么是写时拷贝?

什么是写时拷贝,简单来说就是修改的时候进行拷贝。进程虚拟地址到物理地址的映射是通过页目录+页表找到具体物理地址中哪一个页框内 最后根据虚拟地址(32位)后12为当作偏移量找到具体的起始地址来完成的。页表中除了存储映射的物理地址 还要标志位,代表对应的数据是否可读可写。

fork()子进程后 子进程会复制一份父进程的页表 父子进程的页表标志位都改为只读 并标记为COW,当有进程进行修改时 就会触发缺页中断,查看标志位为COW 是写时拷贝,内核分配一块新的页框复制原内容,将当前进程的页表项指向新页框,并设置为可读可写,写操作重新执行,写入新页框。

为什么线程切换成本小?

线程只切换 执行上下文(栈指针 CPU寄存器 计数器PC),

进程切换还需要切换 资源上下文(页表 TLB(快表)存储虚拟地址到物理地址映射 文件描述符表)

组件作用必须保存原因
程序计数器 PC知道接下来执行哪条指令保证能“从断点继续运行”
栈指针 SP管理函数调用栈、局部变量不保存就会“用错自己的栈”
CPU 寄存器组保存计算中间值、变量地址否则变量结果就丢失或错乱
栈 / TLS线程私有数据、调用状态不保存就串线程、数据错乱
  • 页表切换省略:

    • 每个进程拥有独立的页表(虚拟地址 → 物理地址映射);

    • 进程切换时必须切换页表,会导致 TLB(快表)失效

    • 而同一进程内线程共享页表,切换时不需要重建内存映射

  • 上下文保存量小:

    • 线程只需要保存 程序计数器、栈指针、寄存器组

    • 而进程还需要保存/恢复更多资源状态(内存映射、文件句柄状态等)。

  • 缓存命中率高:

    • 线程共享内存、代码区,CPU 缓存命中率高

    • 而进程切换后,缓存可能被新进程的数据污染,重新加载数据浪费时间。

  • 进程间通信方式:pipeshm(共享内存)、mq(消息队列)信号量 信号 socket套接字

1.管道:管道分为匿名管道 命名管道,匿名管道是 父进程pipe()创建管道 fork()子进程 父子进程通过相同的文件描述符表看到相同的文件 一般是单向的 父进程关闭写端,子进程关闭读端。或者反过来。命名管道mkfifo()通过路径名让不相关的进程间看到同一个文件。

2.消息队列:在系统内核中提供一个队列,进程可以把自己的消息放入队列中,也可以中队列中获取其它进程的消息(有标志位 防止获取到自己的消息)。消息队列生命周期独立于进程,进程退出消息依然存在,适合解耦式通信。

3.共享内存:在内存地址映射段,申请一块空间。每个进程通过挂载能看到这一块空间,直接访问内存空间 减少拷贝次数 速度最高,但一般需要信号量来进行进程间同步。

4.信号量:用于进程间的同步操作,不传数据。用一个计数器来管理多份资源,进程进入P操作计数器--,进程出去V操作计数器++,为0进程进入会阻塞等待 直到有进程出去V操作。

5.信号:用于异步事件通知,不传数据。如进程间终止 暂停。

6.套接字:可以进行本地进程 或者和其它主机进行通信。支持全双工

pipe管道本质是一个文件,父进程创建一个管道 pipe(int fd[2])  会有两个文件描述符fd[0]读端 fd[1]写端并由文件描述符表进行管理(本质数组)。fork()一个子进程拷贝了父进程的文件描述符表,管道的设计是为了单向通信,父子进程只需要留一端就可以了,父进程关闭读端 子进程关闭写端 或者反过来,这样父子进程就可以通过通信了。

shm共享内存 就是在物理内存上申请一块空间,多个进程可以在这块空间上进行读取。

1.shmget()创建/获取一个共享内存 并返回一个共享内存标识符 2.shmat()将共享内存挂载到自己进程的地址空间中 3.之后就可以读写共享内存了 4.shmdt()卸载共享内存 5.shmctl()标志共享内存为删除,所有进行挂载的进程卸载完,才能进行删除。

为什么说共享内存速度最快?多个进程可以访问同一块内存空间,不要频繁切换用户态 内核态,减少拷贝次数。比如说管道,有四次拷贝 1.外设到A内核 2.A内核到管道 3.管道到B内核 4.B内核到内存。而共享内存 只有两次 1.A写入共享内存 2.共享内存到B内存。

像shmget shmat进行系统调用需要进行切换,但挂载完成后就可以直接在用户态访问共享内存。

因为多个进程访问公共资源会出现冲突,所以要加上同步机制 1锁 2条件变量 3信号量

什么是信号量?

信号量是一种用于线程或进程同步的计数机制,控制对共享资源的访问。

1.用一个count计数器记录里面还要多少份资源。

2.进程进入P操作count-- 出去V操作count++

3.count==0 没有资源就阻塞,直到有进程出去 count++

问题:

1.count怎么让不同进程共享 管道 共享内存

2.怎么保证count操作是原子的 互斥锁 原子变量(如 std::atomic<int>,仅限多线程场景)

信号量就是“内核帮你维护的资源计数器 + 自动加锁机制”,可以安全地让多个进程/线程有序地访问有限资源。所以说只有一份资源的信号量,本质上就是一个互斥锁

信号量就相当于管理多份资源的互斥锁,互斥锁就管理一份资源,有一个进程申请了,其它进程就得阻塞。而信号量有多份资源,用一个计数器count进行记录当前还有多少份资源,每申请一份资源P操作count--,为0说明没有资源就会阻塞,需等待占有的进程进行释放V操作count++,才能进入。
(锁一般用于线程间同步,因为线程间天然共享内容 可以看到同一个锁。用于进程间同步的话,需要让不同进程看到同一个锁 比如将锁存放在共享内存中)

  • 用户态和内核态

用户态和内核态是用户运行的两种等级,1.用户态是程序正常运行的状态,只能访问一部分指令。2.内核态 是操作系统内核运行的状态 有全部硬件控制权限,能访问全部指令(读文件 开线程)。

区分这两种状态是为了安全性,在CPU有些指令是非常危险的 如果用错会导致系统崩溃,比如清理内存 设置时钟。

内核态 用户态切换时机?

1.系统调用(read write) 2.硬件中断(处理外设键盘/网盘数据 时钟中断进行进程切换 ) 3.异常/错误(非法操作:除0 访问非法地址段错误)3.进程切换 4.缺页异常 5.信号处理机制 如 SIGINT、SIGSEGV 等

  • 如何设计内存池,遇到内存碎片如何减少

设计内存池的目的就是为了1.提高性能 减少 malloc/free系统函数调用的次数 2.减少内存碎片化 3.提升分配速度(链表O(1)分配)

设计内存池:

1.先向系统申请一大块连续的内存 

2.将申请的内存分块 有两个策略 1.每个内存块大小固定 2.先分区 再分块 区内块大小一样,但每个区的块大小不一样(32B/64B/128B…,用多个 free list 管理)

3.通过链表对划分的内存块进行管理,分配内存块从链表中取出删除,释放进行回收头插 效率高都是O(1)操作。

1.为什么用链表管理,插入删除效率高。

2.为什么分区?内存碎片化分为 外部内存碎片化+内部内存碎片化,外部表示两块分配的空间中有空闲的内存 但大小太小 分配不出去。 内存内存碎片化指 分配的内存块大小64KB,但我指需要20KB大小 导致内存浪费。
因此有了多级大小块,分配合适大小的内存块 减少内部内存碎片化。


内存池由于按固定大小或预分级的方式顺序分配内存块,避免了系统堆那种大小混杂、频繁切割的情况,因此不存在外部碎片化,只会出现内部碎片。

  • 生产者消费者模型(mutex + condition variable)

  • 什么是死锁? 怎么解决?

死锁,比如说访问临界资源需要申请锁1 锁2 ,A进程持有1 B进程持有2,但它们都占有锁 不释放,相互申请 阻塞等待。再比如单线程操作不当 重复申请两次锁 导致死锁

死锁的四个必要条件:

1.互斥:资源只能被一个线程持有

2.占有且等待:线程持有资源不释放 并申请其它资源

3.不剥夺:不能剥夺其它线程持有的资源

4.循环等待:多个线程循环等待其它线程释放资源

怎么解决?

1.破坏等待循环 统一加锁顺序。所有线程统一加锁顺序 先申请A锁 再申请B锁

2.破坏占用且等待 线程进入临界区必须一次性申请所有锁 或者超出一定时间就释放锁


四、Linux 网络编程

  • Linux常见命令

文件与目录操作

1.ls -l 查看当前目录文件

2.cd /path/ 切换路径

3.pwd显示当前路径

4.mkdir 创建文件夹

5.rm -rf 强制递归删除文件或者目录

6.cp a.txt b.txt 复制文件

7.mv a.txt dir/ 移动

8.touch file.txt 新建文件

查看日志与文本操作

1.cat 查看整个文件内容

2.less 上下翻页查看大文件

3.tail -n 50 查看日志最后50行   不加-n默认显示10行(tail == tail -n 10)

   tail -f log.txt 实时查看文件追加内容(适合看日志)

        找文件内容

4.grep "error" test.log 查找包含"error"的行

5.grep -i "fail" test.log 忽略大小写查找包含"fail"的行

        找文件

6.find / -name "file.txt" 查找整个系统中名字为 file.txt 的文件

7.find . -name "*.log" 在当前目录及其子目录下查找以 .log 结尾的文件

系统资源查看

1.top 实时查看CPU 内存 进程使用情况

2.free -h 查看内存使用情况

3.df -h 查看磁盘空间

进程/服务相关

1.ps -ef | grep java     ps -ef显示所有进程 | grep java 找出包含java的进程

        ps -ef | grep java | grep -v grep  删除grep本身的行
1.kill -9 进程ID   杀死目标进程
2.chmod 777 a.txt   修改权限 (u/g/o)->777  7=4'r'+2'w'+1'x'   读 写 执行

网络相关
netstat -anp    查看所有端口监听和连接状态

netstat -anp | grep 8080  找占用8080端口号的进程

  • OSI七层模型

层级名称功能描述典型设备
1️⃣ 物理层(Physical)比特流传输定义电压、电缆、光信号等物理介质传输规则网线、网卡、集线器
2️⃣ 数据链路层(Data Link)帧传输、差错检测解决物理层错误、MAC地址寻址交换机、网卡
3️⃣ 网络层(Network)路由选择、IP寻址实现不同网络之间的通信(IP)路由器
4️⃣ 传输层(Transport)端到端通信,流量控制提供可靠传输(TCP)或尽力而为(UDP)操作系统协议栈
5️⃣ 会话层(Session)建立、维护和终止会话管理会话状态,处理断点恢复软件(理论存在)
6️⃣ 表示层(Presentation)编码、加密、压缩负责数据格式转换,如ASCII转JPEG软件(理论存在)
7️⃣ 应用层(Application)提供网络服务接口与用户交互,如HTTP/FTP等协议浏览器、客户端程序等
各IO层的常见协议
层级功能描述常见协议
应用层提供用户服务,支持应用程序间的通信HTTP、HTTPS、FTP、DNS(域名->ip)、SMTP、SSH、DHCP
传输层负责进程到进程之间的通信,提供可靠或高效的数据传输,“找对应用”TCP(可靠,三次握手)、UDP(无连接)
网络层负责主机到主机之间的数据传输与路由选择,实现“找对机器”IP(IPv4/IPv6)、ICMP、ARP(IP->MAC)、RARP,NAT/NATP(私网ip->公网ip)
数据链路层实现同一链路上的设备通信,封装/识别帧Ethernet(以太网协议)、PPP、VLAN

应用层有HTTP HTTPS,传输层 UDP TCP ,网络层 IP ARP(ip->mac) NAT/NATP(私网ip->公网ip) , 数据链路层 以太网协议

但实际上一般只有5层,表示层和会话层因为高度依赖会话层 操作系统无法完成统一实现,会融入会话层。

物理层: 负责光/电信号的传递方式
数据链路层: 负责设备之间的数据帧的传送和识别.

网络层: 负责地址管理和路由选择.

传输层: 负责两台主机之间的数据传输.

应用层: 负责应用程序间沟通

🔹 表示层

表示层负责数据格式的转换、加密与解密,但具体的转换规则由应用层决定,不同应用对编码格式、压缩方式要求不一,因此操作系统无法提供统一实现,最终这部分功能被集成进应用层程序中完成。

🔹 会话层

会话层负责建立、维护和终止通信双方的会话,但会话的管理 保存的内容依赖于具体应用(如网页应用 需要登录状态、用户ID、权限、购物车内容),无法由操作系统统一封装,因此也被应用层所取代

  • 一个主机发送消息到另一个主机 的全过程(封装过程+路由器转发寻址过程):

1.A主机发生HTTP消息, 1.应用层准备请求数据 2.传输层 TCP协议填加TCP头部 添加源/目标端口号 3.网络层 添加IP头 源/目标IP 4.数据链路层 以太网协议 添加源/目标MAC地址

最终形成以太帧-> [以太网头][IP头][TCP头][HTTP数据] 从A网卡发送出去

2.寻找目标主机  目标IP地址不变(除NAT把私网ip->公网ip),MAC地址每跳都会变

        1.先判断目标主机B是否在当前局域网中。通过比较网络号来判断,网络号=IP&子网掩码,如果网络号相同 说明在同一局域网中,通过ARP协议 发送ARP请求广播给当前局域网的所有主机 但只有IP地址等于ARP请求中的目标IP的主机才会返回ARP应答(包含自己的MAC地址),获取到目标主机的MAC地址 发送数据帧给目标主机。

        2.如果不在同一个子网,就把目标MAC地址改为默认网关(路由器)的MAC地址,数据帧发给路由器。路由器查看IP头中的目标IP 查找路由表中网络号最匹配的下一跳(没有就走默认缺省的下一跳),并更改源MAC和目标MAC地址(下一跳路由器的MAC) 。不断转发进行下一跳直到找到目标主机的局域网。通过ARP协议 获取目标主机MAC地址。

每跳只改 MAC,不改 IP,最终一跳用 ARP 找目标主机 MAC。

  • HTTP 请求和响应报文

  1.  HTTP请求 应答格式

HTTP 请求格式:

请求行(Request Line)
请求头(Request Headers)
(空行)
请求体(Request Body,可选)
部分内容说明
请求行请求方法(GET/POST等) + 路径 + 协议版本
请求头描述客户端能力(User-Agent、Accept、Host、Cookie 等)
空行请求头与请求体之间必须有一个空行
请求体仅 POST、PUT 等才有,请求提交的数据(表单、JSON 等)

HTTP 应答格式:

状态行(Status Line)
响应头(Response Headers)
(空行)
响应体(Response Body)
部分内容说明
状态行协议版本 + 状态码(如 200、404、500)+ 状态描述
响应头说明响应类型、编码、缓存策略、Cookie 设置等
空行必须空行
响应体返回给浏览器的数据内容,如 HTML、JSON、图片等
  1. get和post区别:

1.数据传输位置不同:get一般用url传数据,因为url长度有限制 get传输的数据比较小,一般用来获取页面。
2.数据大小限制不同:post用请求体传数据,理论上传输数据没有大小限制,一般用来传文件或者比较复杂的数据。

POST 并不比 GET 更安全。

GET 和 POST 默认都是明文传输(HTTP),只是 GET 暴露在 URL 更直观。

只有使用 HTTPS 协议时,数据才会被真正加密保护!

GET 请求将参数附加在 URL 中,适合传递少量数据,常用于数据查询;而 POST 请求将数据放在请求体中,支持传输较大或结构复杂的数据,常用于提交表单或文件上传。虽然 POST 参数不显示在地址栏,但其本身并不比 GET 更安全,只有使用 HTTPS 才能真正加密数据。

状态码:

状态码中文名称含义说明
200 OK请求成功请求已被服务器正确处理
301 永久重定向域名或资源永久迁移浏览器会缓存重定向地址
302 临时重定向临时跳转资源位置临时更改,浏览器不应缓存
304 未修改缓存命中,不再下载客户端可继续使用本地缓存的资源
400 错误请求请求格式或参数错误通常是客户端请求语法错误
401 未授权未登录或 Token 失效需要进行身份认证
403 禁止访问权限不足,拒绝访问已登录但无权访问资源
404 页面不存在资源未找到请求的页面或接口不存在
500 服务器错误后端程序内部出错服务器执行请求时崩溃
502 网关错误代理服务器接收失败常见于 Nginx 转发失败
503 服务不可用服务器超载或维护中暂时无法处理请求

200 成功干活好,301跳转别忘掉;404网页找不到,500 服务器崩掉。

  • HTTP 和HTTPS 区别 HTTPS 握手流程

HTTPS 就是在 HTTP 协议之上,通过 TLS 协议建立一条加密的安全通信通道,再传输 HTTP 数据。 HTTP 是应用层协议,定义了浏览器与服务器之间的请求响应格式。

它们端口号不一样,HTTP 80 HTTPS 443

所以HTTPS 握手流程 = TCP 连接建立 + TLS 握手 + 加密的 HTTP 通信

三次握手过程:

1.客户端 → 服务端:SYN

        客户端发送一个请求,表示想建立连接

2.服务端 → 客户端:SYN + ACK

        服务端同意并回复确认信息

3.客户端 → 服务端:ACK

        客户端再次确认,连接建立完成

TLS握手过程:

1.客户端向服务端发送 1.随机数1 2.支持的TLS版本 3.加密算法列表

2.服务端发送 1.随机数2 2.选择的TLS版本 3.从列表中选的加密算法

3.服务端发送 服务器证书(1.公钥 2.域名信息 3.CA签名 用来证明网站的可信性)

4.客户端生成预主密钥 并用服务器的公钥进行加密发送给服务端(只有服务端的私钥才能解密)

5.服务端 客户端根据 随机数1+随机数2+预主密钥 = 会话密钥 (虽然随机数12是通过明文传输的,但预主密钥只有服务端 客户端知道)

6.后面双方通过相同的会话密钥 对消息进行加密解密。

TLS本质就是 通过非对称加密安全交换密钥,再用对称加密高效通信。 

加密的 HTTP 通信阶段:

1.客户端构建并发送加密的 HTTP 请求

2.服务器构建并发送加密的 HTTP 响应

  • select / poll / epoll 区别及 epoll 的高效机制

select是用位图来表示需要监听的事件 比如说需要监听文件描述符fd=1的写和读 就在对应写位图以及读位图的bit位上即第二位都置为1,拷贝到内核态,通过轮询遍历的方式查找哪些监听事件就绪了,并在原位图中修改哪些就绪了,最后全部返回。

poll在此基础上的改进 不是用位图了,而是用一个结构体数组,这个结构体中有fd 需要监听的事件 实际就绪的事件。这样带来的好处是1.监听数量没有限制 位图的bit位是有限的,但数组可以扩容。 2.可以重复利用原数组,监听事件和就绪事件分离,不会在原数据上修改。

现在poll还存在的问题是 1.用户态和内核态的切换 每次都要进行拷贝 O(n)。2.必须要轮询检测监听的数据是否就绪 O(n)。3.返回时 会把整个数组全部返回。一方面拷贝需要全拷贝 另一个方面 我们还需要遍历查看是否就绪O(n)。

epoll 1.我们是在内核中有一个红黑树结构用来管理需要监听的事件 每次添加删除监听事件时直接在红黑树中进行添加删除进行 O(logN) ,这样就不用频繁在用户态切换到内核态进行数据拷贝。 2.epoll采用的事件回调机制 有事件就绪了就会进行回调 把该事件放到就绪队列中。不用轮询检测.O(1) 3.在内核中还有就绪队列 这个队列中存放的都是就绪的事件 上面获取时就不需要进行遍历找就绪的事件,虽然时间复杂度仍是O(n) 但n的数量不包含没有就绪的事件 这样也减少了拷贝。

 0.

简单来说epoll高效的原因:1.内核中红黑树结构存储监听事件 减少用户态内核态切换的拷贝

2.底层回调机制 有事件就绪就触发回调 不用遍历检测事件是否就绪

3.就绪队列 只返回就绪的事件 减少拷贝 用户能直接获取就绪的事件 不用遍历检查

所以 epoll 的高效本质来源于 内核态保存状态(红黑树)、事件驱动触发(回调机制)和只返回就绪事件(就绪队列)

epoll 就绪队列为什么用“双向循环链表”?

  1. 就绪队列中需要快速删除/插入就绪事件:

    • 如果某 fd 在 epoll 里触发后马上被移除,用双向结构可以 常数时间删除它

    • 单向链表需要遍历整个链表找前驱 → 性能差。

  2. 避免边界 NULL 问题:

    • 循环结构让头尾首尾相连,无需判断 “是否头结点/尾结点”。

  3. 综合来看:

    双向 = 快速插入/删除,
    循环 = 无需判断 NULL,简化边界逻辑,

    链表 = 相比数组结构更适合动态增删。

  • epoll是异步吗?

🚫 epoll ≠ 异步 I/O(async I/O)

epoll 的性能非常高,但它是同步非阻塞 I/O + 多路事件通知机制。

工作方式:

1.用户调用epoll_wait() 进入阻塞或等待状态

2.内核检测哪些文件描述符就绪

3.通知用户就绪的 fd 列表

4.用户自己调用read()/write()主动处理

epoll通过事件回调机制通知用户哪些fd就绪了,让用户手动处理。而异步是用户告诉异步线程要干什么,异步线程处理完再去通知用户,用户不需要主动处理

  • epoll的水平触发LT 边缘触发ET

水平触发LT:事件就绪会一直进行通知,如果数据没处理完一直通知。

        系统调用次数较多,效率稍低,不适合极端高并发。但编程简单

边缘触发ET:  事件就绪只会通知一次,后面不管数据有没有处理完。

        通知次数少,系统调用开销低 适合高并发服务端;处理不当 容易丢失数据。

对于读/写事件 边缘触发ET什么时候会通知?

先明白读/写事件就绪意味着什么,读就绪:缓冲区中有数据可以读 写就绪:对方缓冲区有空间,可以发送数据。所以写事件天然就是就绪的,所以一开始不会监听写事件,只有说对面缓冲区满了,才会把对写事件进行监听。

所以 对于读事件:有新数据到来 就会通知一次  写事件:(对面缓冲区满了才监听) 如果对面缓冲区有空间了,LT的话会通知,但ET模式下对面缓冲区为空 状态发生变化时才会通知。

为什么边缘触发ET必须非阻塞 循环读取?

因为ET有新数据到了才会通知一次,如果没处理完 就可能造成读取不完整。

非阻塞循环读,读完才会返回。那直接read阻塞读完不就行了?

不行,非阻塞循环读,可以读取一部分先来的数据,先进行处理,然后再循环读取下一部分。而阻塞read会一直等待 不会并发处理其它事情。

(ET 模式 1000不是分两次来 一次800 200

第一次事件:到 800 字节 → 触发 → 如果没一次性读空,后面 200 来时不会再提示。

解决方法:非阻塞循环读到 EAGAIN。

)

简单来说:ET+非阻塞循环读 将原本“阻塞等待 I/O 数据”的时间,变成了并发处理其他数据/连接/任务的时间。

虽然都是读完返回,但ET + 非阻塞 I/O 是“主动轮询 + 自己控制退出”;而阻塞 I/O 是“被动等待 + 系统帮你判断返回”

  • socket编程 如何编写一个服务器和一个客户端?

一.UDP socket

1.服务端

1.socket创建用于接收所有客户端的消息,并返回所有客户端的消息。

udp是无连接的 怎么明确哪个客户端发我的,怎么发给指定的客户端?

创建sockaddr_in socklen_t 保存客户端的信息 recvfrom接收消息的时候进行存入,sendto发送的时候带上客户端信息就能找到对应客户端。

int socket(int domain, int type, int protocol);地址族 套接字类型 协议

参数:

        1.地址族AF_INET网络通信 AF_UNIX本地通信

        2.SOCK_DGRAM(UDP)SOCK_STREAM(TCP)

        3.0 

2.sockaddr_in 设置服务端ip+port

        1.设置地址族 2.设置端口号htons()需要把主机序列转网络字节序 3.设置ip inet_pton()将字符串的ip转换为二进制ip且为网络字节序 或者直接等于INADDR_ANY 表示绑定本机的所有网卡/所有 IP 地址,只要客户端发往该端口(如 8080),无论目标是哪个本地 IP,都能被接收。

3.bind绑定 将socket和本地ip+port绑定。告诉操作系统监听哪个“地址 + 端口”

        bind(sockfd,(sockaddr*)(&server_addr),sizeof(server_addr));

4.recvfrom接收信息并获取客户端信息 

接收的信息放在buffer中,要保存客户端信息会进行修改 所以& 

(注意一定要client_len=sizeof(client_addr)获取原本sockaddr的大小,否则会否则系统不知道你的地址结构体有多大,会导致地址信息无法正确填充或出现内存错误。)

        ssize_t n=recvfrom(sockfd,buffer,1023,0,(sockaddr*)(&client_addr),&client_len);

5.sendto发送应答

带上客户端信息 否则不知道发给谁

    sendto(sockfd,message.c_str(),message.size(),0,(sockaddr*)(&client_addr),client_len);

6.close关闭套接字    close(sockfd);

2.客户端

1.socket创建套接字

2.sockaddr_in 填写服务端信息ip+port

(客户端不需要显示调用bind,系统会自动分配合适的本地ip+port)

3.sendto发消息

4.recvfrom接收消息

5.close关闭套接字 0

二.TCP socket

1.服务端

1.socket创建一个listenfd套接字 (此套接字只用于获取服务端连接请求)

        int listenfd=socket(AF_INET,SOCK_STREAM,0);

2.sockaddr_in 设置服务端ip+port

3.bind绑定套接字和本地ip+port

4.listen()监听套接字 并设置最大连接等待队列

        ret=listen(listenfd,8);

等待客户端连接

5.accept()从监听套接字中获取一个客户端的连接请求,并返回一个新的socket文件描述符(clientfd),用于与该客户端进行实际的通信。(与udp不同 一个客户端对应一个socket套接字)

提前创建sockaddr_in socklen_t获取客户端的信息

        sockaddr_in client_addr={};

        socklen_t client_len=sizeof(client_addr);

        int clientfd=accept(listenfd,(sockaddr*)(&client_addr),&client_len);

6.recv接收消息 提前申请空间用来存放消息 返回收到的字节数。

如果==0不是收到0字节,而是意味着客户端断开连接,==-1发生错误。n<=0服务端就需要断开连接(关闭套接字+return)

不需要带客户端信息 因为一个套接字对应一个客户端

    ssize_t n=recv(clientfd,buffer,1023,0);

7.send发送消息 

    send(clientfd,message.c_str(),message.size(),0);

8.close关闭套接字

记得监听套接字listenfd 客户端套接字clientfd都要关

2.客户端

1.socket创建套接字

2.sockaddr_in 填写服务端ip+port

3.connect连接服务端

        int ret=connect(sockfd,(sockaddr*)(&server_addr),sizeof(server_addr));

4.send发送消息

5.recv接收消息

6.close关闭套接字

  • TCP和UDP

UDP无连接 不可靠 面向数据报。udp给对方发消息不需要建立连接,sendto发几次数据包 对面就recvfrom接收几次。如果出现丢包 不会进行重传,不可靠。udp没有传统意义上的发送缓冲区 不管对方是否收到直接发送,有接收缓冲区但只管接收 不管顺序,如果接收缓冲区满了后面的报文就会直接丢弃 不可靠。

TCP有连接 可靠 面向字节流。tcp需要通过三次握手建立连接,然后发送数据报 里面有确认序列号(我希望接收到的下一个字节位置) 序列号(发送数据的起始字节位置) 通过这个来判断收到数据是否完整 以及有序,如果报文丢失有超时重传 快重传保证可靠性。tcp有发送缓冲区和接收缓冲区 

1.发送缓冲区分为三部分 1.发送完的数据 2.可以直接发送的数据 3.待发送的数据

其中可以直接发送的数据的部分 叫做滑动窗口,有什么意义?保证这部分数据到接收缓冲区不会溢出。滑动窗口的大小就是对方发送报文中窗口的大小(剩余缓冲区大小) ,但还和拥塞窗口有关,滑动窗口的起始位置就是确认序号。

2.接收缓冲区 tcp是面向字节流的,发送一次1000字节数据,可以分多次进行读取,也可以发送多次,一次读取。这就导致说接收缓冲区的数据的数据没有一个明确的边界,这就粘包问题。

怎么解决粘包问题的呢?解决粘包问题,就要明确每个包的界限。1.定长包 就按指定长度读取。2.变长包 长度前缀协议 添加一个定长字段代表后续tcp数据包的长度 通过读取定长的字段获取tcp数据包的长度 3.变长包 特殊分隔符协议 在每个数据包后面加个分割符划界限。

TCP怎么保证可靠传输的?

1.三次握手 确保连接可建立

2.序列号 表示数据的起始字节位置 检测数据包是否丢失 以及顺序

3.确认序列号 确认应答 ACK 收到应答发送ACK包

4.超时重传  快重传 对丢失的包进行重传

5.滑动窗口 保证发送的数据 对方缓冲区不会溢出

6.拥塞控制 保证网络不拥塞的情况下尽可能提供传输效率 减少丢包

7. 接收端排序与去重 接收方根据序列号将乱序包重组,丢弃重复数据

  • TCP 三次握手、四次挥手流程及关键细节

为什么进行三次握手?

低成本确保通信双方接收和发送能力都正常,如果只用两次握手,服务端不发送ACK客户端就不清楚服务端是否准备好,可能造成资源浪费或连接异常。

流程:

1.客户端发送SYN,表示要建立连接。

2.服务端收到,要发确认应答ACK+SYN,这两个可以合并到一块 捎带应答

3.客户端收到,返回ACK 确认应答

为什么进行四次挥手?

因为TCP是全双工通道,客户端需要关闭通道,服务端也需要关闭通道。为什么不能三次?可以,但一般不会。客户端断开连接发送FIN,服务端返回ACK 但不一定也断开连接,等服务端发送完数据之后才会发送FIN 断开连接,客户端返回ACK。本质就是客户端 服务端断开连接的时机不一样。

流程:

1.客户端断开连接 发送FIN

2.服务端收到 返回ACK

3.服务端断开连接 发送FIN

4.客户端收到FIN 进入TIME_WAIT状态,并发送ACK 等待2MSL后 进入CLOSED状态

(为什么等待2MSL?MSL指一个数据包在网络中的最长存活时间 保证发送的包都消失,避免再新建连接时 收到旧的数据包。这期间也可以处理 没有收到ACK 会超时重传FIN的情况)

5.服务端收到ACK 进入CLOSED 连接真正断开

  • 大规模并发的场景下time_wait有没有什么可以优化的点

当你创建一个 TCP 连接,系统会自动分配一个本地端口。如果你关闭这个连接进入 TIME_WAIT,这个 50000 端口不能立刻复用,即使你再次连接服务端,系统也不能再用50000 这个端口,必须换一个新的端口。如果你的程序不断新建短连接,每个都进入 TIME_WAIT 状态(等待 60 秒)那么系统很快就找不到空闲端口了。

TIME_WAIT 虽然只是一个连接状态,但它绑定着本地端口,在它消失前,这个端口不能再用于新连接。所以当有大量 TIME_WAIT 时,本地端口被耗光,就没法继续建立新连接了。

1.调整内核参数(Linux)缩短等待时间

2.用长连接 减少频繁创建断开连接

3.客户端主动断开连接让客户端承担TIME_WAIT状态

在我们rpc项目中,我优先选择了长连接策略,主要原因之一就是为了减少 TIME_WAIT 的压力。因为如果每次请求都建立短连接,主动断开后会进入 TIME_WAIT 状态,占用本地端口资源,导致高并发下连接耗尽的问题。尤其当连接频率较高、服务响应快时,这种问题更加突出。     

五.MySQL 数据库

  • 索引

索引是加快查询速度的数据结构,主要分为聚簇索引和非聚簇索引,它们的底层一般都是 B+树 实现。

在 B+树中:

非叶子节点不存储实际数据,仅包含 key 值与指向子节点的指针;

叶子节点存储具体的数据或数据指向,所有叶子节点通过链表连接,支持范围查询。

聚簇索引和非聚簇索引之间的区别如下:

聚簇索引

1.使用主键作为 key 值;

2.叶子节点存储的是整行数据本体,即数据就保存在聚簇索引中;

3.每张 InnoDB 表只能有一个聚簇索引(通常是主键索引)。

非聚簇索引

1.可选择任意字段建立索引;

2.叶子节点不存储完整数据,而是存储主键值或物理地址,具体取决于存储引擎;

3.查询非索引字段时,通过在非聚簇索引查找主键值,再在聚簇索引的表中进行再查找实际的物理数据,这就是回表操作。

非聚簇索引的行为因存储引擎而异:

InnoDB 中,非聚簇索引叶子节点保存的是主键值,查询时需通过主键回到聚簇索引中查整行数据 → 回表操作;

MyISAM 中,非聚簇索引叶子节点保存的是物理地址(磁盘偏移量),可直接定位到数据行,不需要回表。

    http://www.xdnf.cn/news/17751.html

    相关文章:

  • 【完美解决】在 Ubuntu 24.04 上为小米 CyberDog 2 刷机/交叉编译:终极 Docker 环境搭建指南
  • Web前端小游戏轮盘。
  • VisionPro——1.VP与C#联合
  • 派聪明RAG知识库----关于elasticsearch报错,重置密码的解决方案
  • 基于 Easy Rules 的电商订单智能决策系统:构建可扩展的业务规则引擎实践
  • 计算机网络摘星题库800题笔记 第2章 物理层
  • 【Redis在远程控制指令传递中的设计】
  • mysql参数调优之 sync_binlog (二)
  • Unity DOTS(一):ECS 初探:大规模实体管理与高性能
  • Apache Shiro
  • 小白学习pid环控制-实现篇
  • 知名车企门户漏洞或致攻击者远程解锁汽车并窃取数据
  • ENCOPIM, S.L. 参展 AUTO TECH China 2025 广州国际汽车技术展览会
  • SSH浅析
  • 【C#】正则表达式
  • Emscripten 指南:概念与使用
  • 科研人如何挖出SCI级创新选题?
  • [激光原理与应用-253]:理论 - 几何光学 - 变焦镜头的组成原理及图示解析
  • 《算法导论》第 21 章-用于不相交集合的数据结构
  • JavaWeb从入门到精通!第二天!(Servlet)
  • HTTPS服务
  • 小黑课堂计算机一级WPSOffice题库安装包1.44_Win中文_计算机一级考试_安装教程
  • 系统架构设计师备考之架构设计实践知识
  • Kafka跨机房双活方案中MM1与MM2
  • 新型Windows RPC攻击可劫持服务并完全攻陷Active Directory,PoC已公开
  • 开发npm包【详细教程】
  • 测试匠谈 | AI语音合成之大模型性能优化实践
  • 【c++】反向赋值:颠覆传统的数据交互范式
  • HeidiSQL 连接 MySQL 报错 10061
  • 工业相机终极指南:驱动现代智能制造的核心“慧眼”