【面试复习】汇总

【C++】

static

  • 静态局部变量
    用于函数体内部修饰变量,这种变量的生存期长于该函数。
  • 静态全局变量
    定义在函数体外,用于修饰全局变量,表示该变量只在本文件可见。
  • 静态函数
    同静态全局变量的作用类似
  • 静态成员变量
    生存期大于 class 的对象。普通数据成员是每个对象(实例)有一份,而静态数据成员是每个 class 有一份。
    静态成员变量在对象创建之前就已经被分配了内存空间
    静态成员变量是在程序编译时分配空间,而在程序结束时释放空间。
  • 静态成员函数
    用于修饰 class 的成员函数。
    没有this指针,因为不知道应该访问哪个对象中的数据。
    不可以用静态成员函数访问类中的普通变量
    不能通过类名来调用类的非静态成员函数
    类的对象可以使用静态成员函数和非静态成员函数。
    静态成员函数中不能引用非静态成员。
    类的非静态成员可以调用静态成员函数,但反之不能。
    类的静态成员变量必须先初始化再使用。

const

  • 修饰变量:
    变量的值不能改变
  • 修饰指针:
    如果const位于指针的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量
    指针常量:不能通过指针来修改变量的值。
    如果const位于指针右侧,const就是修饰指针本身,即指针本身是常量
    常量指针:一直指向该变量,不能给该指针赋予其他地址
  • 修饰函数参数:
    在函数内部不可以改变其值
  • 修饰类的成员变量:
    表示成员变量不能被修改,同时只能在初始化列表中赋值(冒号语法)
  • 修饰成员函数:
    该函数不能改变对象的成员变量
    不能调用非const成员函数,只能调用const成员函数,不能与static关键字同时使用。
  • 修饰类对象:
    对象的任何成员都不能被修改,只能调用const成员函数

内联函数/宏定义

  • 宏定义函数的展开是在预编译阶段,内联函数是在编译阶段
  • 宏定义函数不会检查语法错误,而内联函数会
  • 宏定义与参数数据类型无关,而内联函数有关
  • 宏定义不能调试,内联函数能调试
  • 内联函数只是对于编译器的建议
  • 类中的成员函数是默认的内联函数

引用和指针的区别

  • 指针可以指向空;引用一定非空
  • 指针可以改变指向,而指向其它对象;引用不可以改变指向,生命周期内唯一绑定
  • 指针大小为4字节,引用大小为源对象本身大小(别名)
  • 自增(++)意义不同

结构体内存对齐

struct nod1 {
    char a;
    int b;
    char c;
};// 12

struct nod2 {
    char a;
    char c;
    int b;
};// 8

空结构体的大小为0
单个成员的结构体大小为本身字节大小
多个成员的字节大小需要分析,其大小为成员里面的字节数最大的整数倍
因为在32位操作系统中,数据总线和地址总线是32位。 地址总线是32位,意味着寻址空间是按4递增的;数据总线32位意味着一次可读写4bit。假如不对齐的话则可能需要读取两次,造成I/O上的浪费(减少CPU寻址次数)。

为什么malloc要大小

当调用malloc(size)时,实际分配的内存大小大于size字节,这是因为在分配的内存区域头部有类似于

struct control_block {
    unsigned size;
    int used;
};

这样的一个结构,如果malloc函数内部得到的内存区域的首地址为void *p,那么它返回给你的就是p + sizeof(control_block),而调用free(p)的时候,该函数把p减去sizeof(control_block),然后就可以根据((control_blcok*)p)->size得到要释放的内存区域的大小。这也就是为什么free只能用来释放malloc分配的内存,如果用于释放其他的内存,会发生未知的错误。

迭代器失效

1、对于序列式容器(如vector,deque),序列式容器就是数组式容器,删除当前的iterator会使后面所有元素的iterator都失效。这是因为vetor,deque使用了连续分配的内存,删除一个元素导致后面所有的元素会向前移动一个位置。erase方法可以返回下一个有效的iterator。

2、对于list,map,set等,删除后该迭代器自身失效,其后面的迭代器以然有效

for (auto iter = container.begin(); iter != container.end(); iter++) {
	if (*iter > 3)
		container.erase(iter);
}
*/ //error
// vector/deque/map/set/list
for (auto iter = container.begin(); iter != container.end(); ) {
    if (*iter > 3)
        iter = container.erase(iter); // 返回下一个有效迭代器
    else
        iter++;
}
// map/set/list
for (auto iter = container.begin(); iter != container.end(); ) {
    if (*iter > 3)
        container.erase(iter++); // 传iter iter自增 iter删除失效 
    else
        iter++;
}

vector扩容

c a p i = 2 ∗ c a p i − 1 cap_i=2*cap_{i-1} capi=2capi1
c a p i = f l o o r ( 3 ∗ c a p i − 1 2 ) cap_i=floor(\frac {3*cap_{i-1}}{2}) capi=floor(23capi1)
可以使用reverse预分配

C++内存空间

  • 栈区、堆区、静态区(全局区)、常量区、代码区
  • 栈区向下增长,由编译器自动分配释放,会在生命周期结束后自动调用析构函数
  • 堆区向上增长,由程序员分配释放
int* a = new int(2); // 堆
int* b = new int(3);
int c = 1; // 栈
int d = 2;
// a < b
// &a > &b
// &c > &d

内存泄漏

  • malloc和free/new和delete不匹配
  • delete [] p / delete p
  • delet void * 的指针,导致没有调用到对象的析构函数
  • 没有将基类的析构函数定义为虚函数,当基类的指针指向子类时,delete该对象时,不会调用派生类的析构函数
  • 野指针如被free或者delete后,没有置为NULL

RAII

RAII是Resource Acquisition Is Initialization(资源获取就是初始化)的简称,是C++语言的一种管理资源、避免泄漏的惯用法。利用的就是C++构造的对象最终会被销毁的原则。RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。例如C++11 mutex

拷贝构造与赋值构造(operator=)

对于在赋值操作之前,还未构造的变量,调用拷贝构造函数(Copy Constructor);
对于在赋值之前,已构造的变量,调用赋值操作(Assignment Operator);
// if (*this == &a) return *this;

虚函数

虚函数link
子类对象调用了未被覆盖函数g,g调用了被覆盖的函数f

class A {
public:
    A() {}
    void f() { cout << "A::f()" << endl; }
    void g() { f(); }
};
class B : public A {
public:
    B() {}
    void f() { cout << "B::f()" << endl; }
};
B b;
b.g();

基类指针调用被覆盖的函数

A* p = new B;
p->f();
  • 有虚函数或者继承自由虚函数表的基类 大小+4(一个指向虚函数表的 虚(表)指针(vptr)
  • 构造函数不能是虚函数(依赖构造虚表指针,此时要取构造这个指针),构造函数可以调用虚函数
  • 析构函数最好是虚函数【没有将基类的析构函数定义为虚函数,基类指针delete子类对象时,不会调用子类的析构函数】

构造析构顺序

class A;
class B : public A;
A::A();B::B();
B::~B();A::~A();

交叉打印

#include <iostream>
#include <condition_variable>

using namespace std;

mutex mtx;
condition_variable cond_var;
int nxt_index = 0;
const int THREAD_CNT = 6;
const int MAX = 100;
void print(void* arg) {
    int tim = 0;
    for (int tim = 0; tim < (MAX + THREAD_CNT - 1) / THREAD_CNT; tim++) {
        int index = *((int*)arg);
        unique_lock<mutex> lck(mtx);
        cond_var.wait(lck, [index] {return index == nxt_index; });
        
        int val = index + tim * THREAD_CNT;
        if (val < MAX)
            cout << "thread " << index << ": " << val << endl;
        // this_thread::sleep_for(chrono::seconds(1));
        nxt_index = (nxt_index + 1) % THREAD_CNT;
        cond_var.notify_all();
    }
}
int main() {
    int id[THREAD_CNT];
    thread t[THREAD_CNT];
    for (int i = 0; i < THREAD_CNT; i++) {
        id[i] = i;
        t[i] = thread(print, &id[i]);
    }
    for (int i = 0; i < THREAD_CNT; i++) t[i].join();
}

三种cast类型转换

  • const_cast
    用法:const_cast<type_id> (expression)
    用于修改类型的const或volatile属性,一般用于强制消除对象的常量性,c中不提供消除这const的机制
  • static_cast
    用法:static_cast<type_id> (expression)
    该转换和c风格的转换很类似,没有运行时类型检查,所以无法保证转换的安全性。主要有以下几种用法:
    (1)用于基本数据类型,或者non_const到const(反过来必须用const_cast)
    (2)把空指针转换为目标类型的指针
    (3)将任何类型的表达式转换为void类型
    (4)可以将子类类型的指针转换为父类类型的指针
  • dynamic_cast
    用法:dynamic_cast<type*>(expression)
    他只用于对象和引用,主要用于执行安全的向下转型,他可以将指向子类的父类指针转换为子类指针,但是要求父类有虚函数,如果转换为指针类型失败则返回NULL,如果是引用类型转换失败则跑出bad_cast的异常

C++11

智能指针

link
unique_ptr:
unique_ptr对象始终是关联的原始指针的唯一所有者。我们无法复制unique_ptr对象,必须使用std::move()转移其管理的指针,转移后原unique_ptr为空。
由于每个unique_ptr对象都是原始指针的唯一所有者,因此在其析构函数中它直接删除关联的指针,不需要任何参考计数。
shared_ptr:
shared_ptr 对象本身的线程安全级别(原子操作),不是它管理的对象的线程安全级别
如果要从多个线程读写同一个 shared_ptr 对象,那么则需要加锁。
weak_ptr:
用来解决shared_ptr循环引用的问题,只能指向shared_ptr对象,不会增加个其引用计数

auto:

自动类型推断

decltype:

编译器分析表达式并得到它的类型,却不实际计算表达式的值。
拖尾返回类型

auto add(int x, int y) ->decltype(x+y){
	return x + y;
}

区间迭代:
Lambda 表达式

  1. []不捕获任何变量。
  2. [&]捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。
  3. [=]捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。被创建时拷贝,而非调用时才拷贝
右值引用/移动语义

移动语义可以减少无谓的内存拷贝,要想实现移动语义,需要实现移动构造函数和移动赋值函数。

std::move()将一个左值转换成一个右值,强制使用移动拷贝和赋值函数,这个函数本身并没有对这个左值什么特殊操作

【操作系统】

Linux可执行文件

  • Linux中可执行文件是ELF文件,其文件格式是ELF文件格式
  • 可执行文件(Excutable File)、
  • 可重定位目标文件(Rellocatable**Object File)、
  • 共享目标文件(SharedObject**File)、
  • 核心转储文件(Core Dump**File
  • 其均为ELF格式文件。

进程和线程的区别

进程是操作系统资源分配的最小单位,而线程是任务调度和执行的最小单位

进程有自己独立的地址空间,每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段,线程没有独立的地址空间,它使用相同的地址空间共享数据;(线程比进程开销小)

对资源的管理和保护要求高,不限制开销和效率时,使用多进程。
而要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程。

线程独享:线程ID,寄存器,堆栈
线程共享:进程代码段、进程的公有数据、进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组ID
死锁是指两个的线程在执行过程中,由于竞争资源或彼此通信而造成的一种阻塞的现象。

在这里插入图片描述

孤儿进程和僵尸进程

  • 孤儿进程就是说一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被挂到init进程之下,并由init进程对它们完成状态收集工作。
  • 僵尸进程就是一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过wait()waitpid()获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用wait()waitpid(),那么子进程的进程描述符仍然保存在系统中,被称为僵尸进程。
  • 系统所能使用的进程号是有限的,如果产生大量僵尸进程,可能会因为没有可用的进程号而导致系统不能产生新的进程。如果要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时僵尸进程就会变成孤儿进程,从而被 init 进程所收养,这样 init 进程就会释放所有的僵尸进程所占有的资源,从而结束僵尸进程。

用户级、内核级线程

  • 用户级线程(user level thread)
    对于这类线程,有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在。在应用程序启动后,操作系统分配给该程序一个进程号,以及其对应的内存空间等资源。应用程序通常先在一个线程中运行,该线程被成为主线程。在其运行的某个时刻,可以通过调用线程库中的函数创建一个在相同进程中运行的新线程。用户级线程的好处是非常高效,不需要进入内核空间,但并发效率不高。
  • 内核级线程(kernel level thread)
    对于这类线程,有关线程管理的所有工作由内核完成,应用程序没有进行线程管理的代码,只能调用内核线程的接口。内核维护进程及其内部的每个线程,调度也由内核基于线程架构完成。内核级线程的好处是,内核可以将不同线程更好地分配到不同的CPU,以实现真正的并行计算。

进程间的通信方式

  • 管道(pipe)
    管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有血缘关系的进程间使用。进程的血缘关系通常指父子进程关系。管道分为pipe(无名管道)和fifo(命名管道)两种,有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间通信。
  • 信号量(semophore)
    信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它通常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  • 消息队列(message queue)
    消息队列是由消息组成的链表,存放在内核中 并由消息队列标识符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。
  • 共享内存(shared memory)
    共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问,共享内存是最快的IPC方式,它是针对其他进程间的通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。
  • 套接字(socket)

上下文切换

对于单核单线程CPU而言,在某一时刻只能执行一条CPU指令。上下文切换是一种将CPU资源从一个进程分配给另一个进程的机制。从用户角度看,计算机能够并行运行多个进程,这恰恰是操作系统通过快速上下文切换造成的结果。在切换的过程中,操作系统需要先存储当前进程的状态(包括内存空间的指针,当前执行完的指令等等),再读入下一个进程的状态,然后执行此进程。

死锁相关

主要原因

  • 系统的资源不足。
  • 进程(线程)推进的顺序不对。
  • 资源的分配不当。‘

必要条件

  • 互斥条件:进程(线程)申请的资源在一段时间中只能被一个进程(线程)使用。
  • 请求与等待条件:进程(线程)已经拥有了一个资源,但是又申请新的资源,拥有的资源保持不变 。
  • 不可剥夺条件:在一个进程(线程)没有用完,主动释放资源的时候,不能被抢占。
  • 循环等待条件:多个进程(线程)之间存在资源循环链。

解决死锁

  • 预防死锁:破坏死锁产生的四个条件之一,注意,互斥条件不能破坏。
  • 避免死锁:合理的分配资源(银行家算法)。
  • 检查死锁:利用专门的死锁机构检查死锁的发生,然后采取相应的方法。
  • 解除死锁:发生死锁时候,采取合理的方法解决死锁。一般是强行剥夺资源。

锁相关

  • 自旋锁
    线程不会挂起,CPU一直运行来循环询问锁是否可用
  • 互斥锁
    线程挂起,CPU保存上下文,切换到其他线程继续运行;
    当锁可用时,再唤起线程。线程挂起的时候,CPU可以做其他事情
    原子性:把一个互斥量锁定为一个原子操作,保证了如果一个线程锁定了一个互斥量,没有其他线程在同一时间可以成功锁定这个互斥量;
    唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量;
    mutex的问题是,它一旦上锁失败就会进入sleep,让其他thread运行,这 就需要内核将thread切换到sleep状态,如果mutex又在很短的时间内被释放掉了,那么又需要将此thread再次唤醒,这需要消耗许多CPU 指令和时间,这种消耗还不如让thread去轮讯。也就是说,其他thread解锁时间很短的话会导致CPU的资源浪费。
  • 读写锁
    也叫做共享互斥锁,读模式共享,写模式互斥。有点像数据库负载均衡的读写分离模式。它有三种模式:读加锁状态,写加锁状态和不加锁状态。简单来说就是只有一个线程可以占有写模式的读写锁,但是可以有多个线程占用读模式的读写锁。
    当写加锁的模式下,任何线程对其进行加锁操作都会被阻塞,直到解锁。
    当在读加锁的模式下,任何线程都可以对其进行读加锁的操作,但所有试图进行写加锁操作的线程都会被阻塞。直到所有读线程解锁。但是当读线程太多时,写线程一直被阻塞显然是不对的,所以一个线程想要对其进行写加锁时,就会阻塞读加锁,先让写加锁线程加锁
    多个读者可以同时进行读
    写者必须互斥(只允许一个写者写,也不能读者、写者同时进行)
    写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)
  • 乐观锁
    这其实是一种思想,当线程去拿数据的时候,认为别的线程不会修改数据,就不上锁,但是在更新数据的时候会去判断以下其他线程是否修改了数据。通过版本来判断,如果数据被修改了就拒绝更新,之所以叫乐观锁是因为并没有加锁。
  • 悲观锁
    当线程去哪数据的时候,总以为别的线程会去修改数据,所以它每次拿数据的时候都会上锁,别的线程去拿数据的时候就会阻塞。
    这两种锁一般用于数据库,当一个数据库的读操作远远大于写的操作次数时,使用乐观锁会加大数据库的吞吐量。

大小端模式

  • 小段模式:低字节对应低位
  • 大端模式:高字节对应低位
    判断:

【计算机网络】

TCP四种定时器

  • 建立连接定时器(connection-establishment timer)
    建立连接的过程中,在发送SYN时, 会启动一个定时器(默认应该是3秒),如果SYN包丢失了, 那么一定时间以后会重新发送SYN包
  • 重传定时器(retransmission timer)
     重传定时器在TCP发送数据时设定,在计时器超时后没有收到返回的确认ACK,发送端就会重新发送队列中需要重传的报文段。重传计时器保证了接收端能够接收到丢失的报文段,继而保证了接收端交付给接收进程的数据始终的有序完整的。因为接收端永远不会把一个失序不完整的报文段交付给接收进程。
  • 延迟应答定时器(delayed ACK timer)
    服务端收到客户端的数据后, 不是立刻回ACK给客户端, 而是等一段时间(一般最大200ms),这样如果服务端要是有数据需要发给客户端,那么这个ACK就和服务端的数据一起发给客户端了, 这样比立即回给客户端一个ACK节省了一个数据包。
  • 坚持定时器(persist timer)
    TCP通过让接收方指明希望从发送方接收的数据字节数(即窗口大小)来进行流量控制。如果窗口大小为 0将阻止发送方传送数据,直到窗口变为非0为止。接收端窗口变为非0后,就会发送一个确认ACK指明需要的报文段序号以及窗口大小。如果这个确认ACK丢失了,则双方就有可能因为等待对方而使连接终止:接收方等待接收数据(因为它已经向发送方通告了一个非0的窗口),而发送方在等待允许它继续发送数据的窗口更新。为防止这种死锁情况的发生,发送方使用一个坚持定时器 (persist timer)来周期性地向接收方查询,以便发现窗口是否已增大。这些从发送方发出的报文段称为窗口探查 (window probe)。
  • 保活定时器(keepalive timer)
     在TCP连接建立的时候指定了SO_KEEPALIVE,保活定时器才会生效。如果客户端和服务端长时间没有数据交互,那么需要保活定时器来判断是否对端还活着,但是这个其实很不实用,因为默认是2小时没有数据交互才探测,时间实在是太长了。(建议心跳包替代)
  • FIN_WAIT_2定时器(FIN_WAIT_2 timer)
    当TCP主动关闭一端调用了close()来执行连接的完全关闭时会执行以下流程,本端发送FIN给对端,对端回复ACK,本端进入FIN_WAIT_2状态,此时只有对端发送了FIN,本端才会进入TIME_WAIT状态,为了防止对端不发送关闭连接的FIN包给本端,将会在进入FIN_WAIT_2状态时,设置一个FIN_WAIT_2定时器,如果该连接超过一定时限,则进入最后的TIME_WAIT阶段。
  • TIME_WAIT定时器 (TIME_WAIT timer, 也叫2MSL timer)
    TIME_WAIT是主动关闭连接的一端最后进入的状态, 而不是直接变成CLOSED的状态。第一个原因是万一被动关闭的一端在超时时间内没有收到最后一个ACK, 则会重发最后的FIN,2MSL(报文段最大生存时间)等待时间保证了重发的FIN会被主动关闭的一段收到且重新发送最后一个ACK。

三次四次

在这里插入图片描述
在这里插入图片描述

2MSL

  • 为了保证A发送的最后一个ACK报文能够到达B,防止出现丢包后超时重传的现象。这个ACK报文段有可能丢失,因而使处在LAST-ACK状态的B收不到对已发送的FIN+ACK报文段的确认。B会超时重传这个FIN+ACK报文段,而A就能在2MSL时间内收到这个重传的FIN+ACK报文段。如果A在TIME-WAIT状态不等待一段时间,而是在发送完ACK报文段后就立即释放连接,就无法收到B重传的FIN+ACK报文段,因而也不会再发送一次确认报文段。这样,B就无法按照正常的步骤进入CLOSED状态。

为什么要三次挥手?两次不行吗?

  • 可能会出现已失效的连接请求报文段又传到了服务器端。 client 发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达 server。本来这是一个早已失效的报文段。但 server 收到此失效的连接请求报文段后,就误认为是 client 再次发出的一个新的连接请求。于是就向 client 发出确认报文段,同意建立连接。假设不采用 “三次握手”,那么只要 server 发出确认,新的连接就建立了。由于现在 client 并没有发出建立连接的请求,因此不会理睬 server 的确认,也不会向 server 发送数据。但 server 却以为新的运输连接已经建立,并一直等待 client 发来数据,这样server 会白白浪费很多资源。三次握手可以防止上述现象发生。
  • 其次,两次握手无法保证Client正确接收第二次握手的报文(Server无法确认Client是否收到),无法保证Client和Server之间成功互换初始序列号。

为什么要四次挥手?三次不行吗?

  • 因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可能未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后

TCP和UDP的区别有哪些

基于连接与无连接
TCP可靠传输,UDP不可靠(尽最大努力进行报文的交付)
TCP要求系统资源较多,UDP较少
UDP程序结构较简单
流模式(TCP)与数据报模式(UDP)
TCP保证数据顺序,UDP不保证

  • TCP头部
    在这里插入图片描述
  • UDP头部
    在这里插入图片描述

TCP如何保证传输的可靠性

  • 数据包校验
  • 对失序数据包重新排序(TCP报文具有序列号)
  • 丢弃重复数据
  • ACK应答+超时重传:接收方收到数据之后,会发送一个确认。而发送方发出数据之后,启动一个定时器,超时未收到接收方的确认,则重新发送这个数据;
  • 流量控制:确保接收端能够接收发送方的数据而不会缓冲区溢出

TCP与UDP的选择

  • 对某些实时性要求比较高的情况,选择UDP,比如游戏、媒体通信、直播,即使出现传输错误也可以容忍。
  • 其它大部分情况下,HTTP都是用TCP,因为要求传输的内容可靠,不出现丢失。

TCP拥塞控制

在这里插入图片描述

  • 慢开始:【发送窗口】cwnd=cwnd*2指数增加到【拥塞窗口】ssthresh
  • 拥塞控制:线性增加。如发生超时重传,ssthresh=cwnd/2cwnd=1
  • 快重传&快恢复:连续收到3个重复确认,cwnd=ssthresh=cwnd/2,直接进入拥塞避免,线性增加

五层网络模型

  • 应用:HTTP,FTP
  • 传输:TCP,UDP
  • 网络:IP
  • 数据链路:封装成帧
  • 物理层:bit,网线

session和cookie

  • session:
    Session是服务器的会话技术,是存储在服务器的。
  • cookie:
    Cookie相当于服务器给浏览器的一个通行证,是一个唯一识别码,服务器发送的响应报文包含 Set-Cookie 首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。客户端之后对同一个服务器发送请求时,会从浏览器中取出 Cookie 信息并通过 Cookie 请求首部字段发送给服务器,服务器就可以识别是否是同一个客户。

区别:

  • Cookie只能存储ASCII 码字符串,而 Session 则可以存储任何类型的数据,因此在考虑数据复杂性时首选Session。
  • Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密。
  • 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。

Http协议

对器客户端和服务器端之间数据传输的格式规范,格式简称为“超文本传输协议”。
无状态协议:无状态协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传。Cookie&Session

  • 请求格式
    请求行:包含请求方法、URI、HTTP版本信息
    请求首部字段
    请求内容实体
  • 状态码:
    200:成功
    302:重定向
    404:请求失败,请求希望得到的资源未在服务器发现。
    502:无效的响应
    400:请求没有进入到后台服务里
  • HTTPS(对称式加密+非对称式加密)
    1)完成TCP三次同步握手
    2)客户端验证服务器数字证书,通过,进入步骤3
    3)DH算法协商对称加密算法的密钥、hash算法的密钥
    4)SSL安全加密隧道协商完成
    5)网页以加密的方式传输,用协商的对称加密算法和密钥加密,保证数据机密性;用协商的hash算法进行数据完整性保护,保证数据不被篡改
  • 使用非对称加密传输一个对称密钥K,让服务器和客户端都得知。然后两边都使用这个对称密钥K来加密解密收发数据。因为传输密钥K是用非对称加密方式,很难破解比较安全。而具体传输数据则是用对称加密方式,加快传输速度。两全其美。
  • 区别
    1)https有ca证书,http一般没有
    2)https有具有安全性的SSL/TLS加密传输协议
    3)http默认80端口,https默认443端口

输入一个URL

  • 游览器检查是否有缓存(游览器缓存-系统缓存-路由器缓存)。如果有,直接显示。如果没有,跳到第三步。
  • 在发送http请求前,需要域名解析(DNS解析),解析获取对应过的ip地址。
  • 游览器向服务器发起tcp链接,与游览器三次握手
  • 握手成功后,游览器向服务器发送http请求,请求数据包
  • 服务器收到处理的请求,将数据返回至游览器
  • 游览器收到http响应。
  • 游览器解析响应。如果响应可以缓存,则存入缓存
  • 游览器发送请求获取嵌入在HTML中的资源(html,css,JavaScript,图片,音乐等)
    页面全部渲染结束。

Nagle算法

Nagle算法要求,一个TCP连接在任意时刻,最多只能有一个没有被确认的小段。所谓“小段”指的是小于MSS的数据块,“没有被确认”指的是一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。
Nagle算法的实现规则:

  1. 如果包长度达到MSS,则允许发送;
  2. 如果该包含有FIN,则允许发送;
  3. 设置了TCP_NODELAY选项,则允许发送;
  4. 未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;
  5. 上述条件都未满足,但发生了超时(一般为200ms),则立即发送。

延迟确认

接收方收到数据包以后如果暂时没有数据要发给对端,它可以等一段时再确认(Linux上默认是40ms)。如果这段时间刚好有数据要传给对端,Ack就随着数据传输,而不需要单独发送一次Ack。如果超过时间还没有数据要发送,也发送Ack,避免对端以为丢包。

当有两个未确认的包需要确认时,立即发送ACK

【数据库】

事务ACID

  • 原子性(Atomicity)
    原子性是指事务是一个最小的、不可分割的工作单位,事务要么都发生,要么都不发生(要么全部提交成功,要么全部失败回滚)
  • 一致性(Consistency)
    事务前后数据的完整性必须保持一致。
  • 隔离性(Isolation)
    事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
  • 持久性(Durability)
    持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响

隔离级别

  • 脏读:
    针对未提交数据
    A[1, 7] B[2, 8]
  • 不可重复读:
    针对其他提交前后,读取数据本身的对比
    A[1, 7] B[2, 6]
  • 幻读:
    针对其他提交前后,读取数据条数的对比
    MySQL事务隔离级别
    在这里插入图片描述
  • Read Uncommitted:
    最低的隔离级别,Read Uncommitted最直接的效果就是一个事务可以读取另一个事务并未提交的更新结果。
  • Read Committed:
    该事务提交之后,另一个事务才可能读取到同一笔数据更新后的结果(大部分数据库默认隔离级别)
  • Repeatable Read:
    保证在整个事务的过程中,对同一笔数据的读取结果是相同的(Mysql默认隔离级别)
  • Serializable:
    所有的事务操作都必须依次顺序执行,可以避免其他隔离级别遇到的所有问题,是最为安全的隔离级别, 但同时也是性能最差的隔离级别,因为所有的事务在该隔离级别下都需要依次顺序执行,所以,并发度下降,吞吐量上不去,性能自然就下来了。

innodb

  • 粒度锁
    • innodb 的锁支持多粒度锁定。为了实现多粒度锁,innodb 通过意向锁(IS共享意向锁、IX共享排他锁)的方式实现。
    • 在细粒度上加锁,则需要先在粗粒度上加意向锁。比如,如果需要在记录行加锁,则先要在表上加意向锁,最后在行上加上 X/S 锁 。任何一个加锁操作导致的等待都需要等待粗粒度锁释放。
    • 由于 innodb 支持的是行级锁,所以意向锁并不会阻塞除扫描全表外的任何请求。
    • https://blog.csdn.net/crowhyc/article/details/81908842

【Redis】

数据结构

  • string
    • set
    • get
  • hash table
    • hset user name guan
    • hget user name
    • 结构体
  • list(双向链表)
  • set
  • sorted set (有续集合)

集群同步

Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将RDB文件全量同步到复制节点,复制节点接受完成后将RDB镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。后续的增量数据通过AOF日志同步即可。

雪崩、击穿、穿透

  • 雪崩
    同一时间大面积过期失效,瞬间打db
    批量存数据时,每个Key的失效时间都加个随机值
  • 击穿
    热点key失效,瞬间打db
    设置热点数据永远不过期。
  • 穿透
    每次问-1INF等不存在的key
    添加检验,布隆过滤器

持久化

  • RDB持久化
    经过压缩的二进制格式
    fork子进程dump,可能会造成瞬间卡顿&数据丢失
  • AOF持久化
    先执行指令再将日志存盘
    保存所有修改数据库的命令
    先写aof缓存,再同步到aof文件
  • AOF重写
    达到阈值时触发,为了减少aof文件的大小
    数据不一致:AOF重写缓存,这个缓存在fork出子进程之后开始启用,Redis服务器主进程在执行完写命令之后,会同时将这个写命令追加到AOF缓冲区和AOF重写缓冲区

渐进式rehash

在第拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中。把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。

【设计模式】

单例模式

饱汉(需要再创建)

class Singleton {
public:
	// 返回引用
	static Singleton& getinstance() {
		static Singleton value;
		return value;
	}

private:
	Singleton() = default;
	Singleton(const Singleton& other) = delete;		 //禁止使用拷贝构造函数
	Singleton& operator=(const Singleton&) = delete; //禁止使用拷贝赋值运算符
};
// 局部静态变量不仅只会初始化一次,还保证线程安全C++11。
#include <mutex>
std::mutex mt;
class Singleton {
public:
    static Singleton* getinstance() {
        if (_instance == 0) {
            mt.lock();
            if (_instance == 0)
                _instance = new Singleton();
            mt.unlock();
        }
        return _instance;
    }
private:
    Singleton() {}
    static Singleton* _instance;
};
Singleton* Singleton::_instance = 0;

双检查锁

1、第一个条件是说,如果实例创建了,那就不需要同步了,直接返回就好了。

2、不然,我们就开始同步线程。

3、第二个条件是说,如果被同步的线程中,有一个线程创建了对象,那么别的线程就不用再创建了。
饿汉(事先创建)

class Singleton {
public:
    static Singleton* getinstance() {
        return _instance;
    }
private:
    Singleton() {}
    static Singleton* _instance;
};
Singleton* Singleton::_instance = new Singleton()

【其他】

Raft选举

raft

GRPC

  • gRPC使用的http2.0
  • 通过protobuf来定义接口

【Golang】

GMP协程

  • 占用栈空间小(最小2KB);N:M调度;上下文切换都在用户态切换,不会涉及到内核态
  • G(Goroutine):Go协程结构体。使用 go func() 时,就会创建一个G
  • M(Machine):操作系统线程。处理线程操作的结构体,与操作系统直接交互
  • P(Processor):管理 goroutine 资源的处理器
  • 典藏版
    -在这里插入图片描述

Channel

type hchan struct {
  qcount   uint  			// 队列中的总元素个数
  dataqsiz uint  			// 环形队列大小,即可存放元素的个数
  buf      unsafe.Pointer 	// 环形队列指针
  elemsize uint16  			//每个元素的大小
  closed   uint32  			//标识关闭状态
  elemtype *_type 			// 元素类型
  sendx    uint   			// 发送索引,元素写入时存放到队列中的位置

  recvx    uint   			// 接收索引,元素从队列的该位置读出
  recvq    waitq  			// 等待读消息的goroutine队列
  sendq    waitq  			// 等待写消息的goroutine队列
  lock mutex  				// 互斥锁,chan不允许并发读写
}

向 channel 写数据

  • 若等待接收队列 recvq 不为空,则缓冲区中无数据或无缓冲区,将直接从 recvq 取出 G ,并把数据写入,最后把该 G 唤醒,结束发送过程。
  • 若缓冲区中有空余位置,则将数据写入缓冲区,结束发送过程。
  • 若缓冲区中没有空余位置,则将发送数据写入 G,将当前 G 加入 sendq ,进入睡眠,等待被读 goroutine 唤醒。

从 channel 读数据

  • 若等待发送队列 sendq 不为空,且没有缓冲区,直接从 sendq 中取出 G ,把 G 中数据读出,最后把 G 唤醒,结束读取过程。
  • 如果等待发送队列 sendq 不为空,说明缓冲区已满,从缓冲区中首部读出数据,把 G 中数据写入缓冲区尾部,把 G 唤醒,结束读取过程。
  • 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程。
  • 将当前 goroutine 加入 recvq ,进入睡眠,等待被写 goroutine 唤醒。

关闭 channel

  • 关闭 channel 时会将 recvq 中的 G 全部唤醒,本该写入 G 的数据位置为 nil。将 sendq 中的 G 全部唤醒,但是这些 G 会 panic。

panic 出现的场景还有:

  • 关闭值为 nil 的 channel
  • 关闭已经关闭的 channel
  • 向已经关闭的 channel 中写数据

new和make的区别

  • make 仅用来分配及初始化类型为 slice、map、chan 的数据。
  • new 可分配任意类型的数据,根据传入的类型申请一块内存,返回指向这块内存的指针,即类型 *Type。
  • make 返回引用,即 Type,new 分配的空间被清零, make 分配空间后,会进行初始。

内存泄漏

内存逃逸

  • go build '-m -l' main.go
  • 函数内将局部变量指针返回
  • 对象太大或编译器变量大小不确定
  • 闭包引用逃逸
  • 动态类型逃逸
  • 在切片上存储指针或带指针的值

Sync.Map

link

【有状态扩缩容】

  • 可选:延迟退出
  • 可选:延迟应用,牺牲了服务可用性来换取数据的一致性
  • 可选:路由转发,问题:服务时间差
  • 请求转发+请求缓存
  • 11111
  • 将所有的key值收敛到一个稳定值域->slot,根据slot进行hash
  • 基础组件tbuspp或者etcd维护server<->slot的关系
  • 外部协调模块分布式lock住slot后问master该slot需要处理的key的消息
  • 外部协调模块分布式lock主要是解决迁移时数据一致性的问题,颗粒度为slot级别

TODO

  • golang内存模型
  • golang gc原理 过程
  • kill原理
  • 线程间同步方式
  • 父进程调用fork后,不调用waitpid会怎怎样
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值