├── .gitignore ├── .idea ├── .gitignore ├── CPPInterview.iml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml └── vcs.xml ├── C++ └── README.md ├── ComputerNetwork └── README.md ├── DataStructure └── README.md ├── Database └── README.md ├── Leetcode ├── README.md ├── binary_search.md ├── dfs.md ├── dynamic_programming.md ├── hashmap.md ├── linked_list.md ├── monotonic_stack.md ├── two_pointers.md └── union_find.md ├── OperatingSystem └── README.md ├── README.md ├── Redis └── README.md ├── study_path.md ├── system.md └── system_interview.pdf /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/CPPInterview.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /C++/README.md: -------------------------------------------------------------------------------- 1 | # C++ 2 | - 基础知识 3 | - 面向对象 4 | - STL 5 | - 多线程 6 | ## 面向对象 7 | - 三大特征:封装,继承,多态。 8 | - 为什么要面向对象?C语言是结构化设计,无法解决重用,扩展和维护的问题。 9 | - 封装:将数据和操作数据的方法封装到一起。 10 | - 继承:直接继承已有类的属性和方法,减少代码重复。 11 | - 多态:对相同的方法做出不同的响应,提高代码灵活性。 12 | ## 特殊成员函数 13 | 1. 构造与析构 14 | 2. 拷贝构造与拷贝赋值 15 | 3. 移动构造与移动赋值 16 | 17 | ## new和malloc的区别? 18 | - malloc只分配内存不初始化;new不仅分配内存也初始化,new分配内存以后自动调用构造函数。 19 | - malloc分配内存时必须指定内存大小,而new可以自动计算。malloc分配完成后返回的是void*类型,需要强转,而new返回的是对应类型的指针。 20 | - malloc分配内存失败时返回NULL,而new分配内存失败时抛出bad_alloc异常。 21 | 22 | ## 多态原理 23 | - 多态分类两大类:静态多态和动态多态。 24 | - 静态多态是重载和模板。动态多态:也叫运行时多态,是通过继承和虚函数实现的。 25 | - 在具有继承关系的子类中,子类重写父类的虚函数,通过父类引用或指针指向子类对象时,产生不同的行为叫做多态。 26 | - 多态的核心在于虚函数表指针,每个对象都有一个虚函数表指针,虚函数表指针指向一张虚函数表,表中记录了虚函数的入口地址,如果子类重写虚函数后,这个地址就会替换掉。 27 | - 多态的好处:在于更方便程序的扩展。 28 | - 坏处在于每个对象多了一个4字节的指针,同时每次查询虚函数表需要耗时。 29 | 30 | ## 智能指针 31 | 1. unique_ptr独享指针的所有权,无法进行拷贝构造赋值的操作,只能通过move函数进行所有权的转换。 32 | 2. shared_ptr共享对象,它使用引用计数来保存当前有多少个智能指针在引用这个对象,当引用计数降为0时,对象会被销毁。 33 | 3. weak_ptr称为弱引用,用于辅助shared_ptr正常工作,主要解决shared_ptr可能会产生的环形引用问题。weak_ptr不会增加对象的引用计数,共享指针可以直接赋值给弱指针,同时弱指针可以使用lock函数来获取shared_ptr对象。 34 | 35 | ## static 36 | - static的使用可以分为两类,一类是用在普通变量和函数上,另一类是用在类中。 37 | 1. 普通变量分为全局变量和局部变量。声明为静态全局变量是在全局区分配内存,并且只在当前文件可见,在文件之外是不可见的。其他文件定义同名变量不会发生冲突。变量的值只在第一次执行时进行初始化。声明为静态局部变量时与全局变量类似,只是作用域为局部作用域。 38 | 2. 静态普通函数,只在当前文件中可见,其他文件中定义同名函数不会发生冲突。 39 | 3. static用在类中,首先是静态成员变量,在类中声明,类外初始化。所有对象共享一份数据。 40 | 4. 然后是静态成员函数:所有对象共享同一个函数,静态成员函数只能访问静态成员变量。 41 | 42 | 43 | 44 | 45 | ## 指针和引用的区别 46 | 1. 指针保存的是所指对象的地址,而引用是所指对象的别名。指针需要通过解引用间接访问对象的值,引用可以直接访问。 47 | 2. 指针可以有多级指针,而引用最多两级。并且两个取地址符是右值引用。右值引用是为了减少深拷贝的次数。 48 | 3. 指针可以不初始化,即使初始化以后也可以改变。而引用必须初始化,同时初始化以后不许改变。 49 | 4. 引用的本质是指针常量。指针常量不可以修改指向,但是可以修改指向的值。常量指针刚好与之相反。 50 | 51 | ## vector底层原理 52 | - 首先,vector的基类是三根指针,分别是start/finish/end_of_storage用来指示当前分配到的空间所用的起始位置,终止位置和容量尾部。然后,当finish指针到达end_of_storage的位置时,操作系统会寻找当前容量大小2倍的连续内存空间,并且将旧内存中的数据拷贝到新内存,然后释放旧内存。其次,如果重新分配了内存,原来的迭代器就会失效。频繁的开辟新内存比较耗时。如果可以预知使用的大小,可以使用reserve函数,预先开辟足够大的空间。或者使用swap函数收缩内存空间。 53 | 54 | 55 | 56 | ## 指针常量 57 | 58 | ```c 59 | int* const p = &a 60 | ``` 61 | 62 | - 指针常量必须初始化,一旦初始化完成,就不能再修改它的值,即指针的指向不可变。 63 | - 引用的本质是指针常量 64 | 65 | ## 声明和定义的区别 66 | 1. 声明是告诉编译器有这个变量和函数的存在,但是需要到其它地方去寻找。 67 | 2. 定义包含了声明,但是声明不包含定义,定义时才分配存储空间。 68 | 69 | ## struct和class的区别 70 | 1. 共同点:C++中,可以用struct和class定义类,都可以继承。 71 | 2. 不同点:struct默认继承权限和默认访问权限时public class类的默认继承权限和访问权限时private。 72 | ## const关键字 73 | - const可以用于限定变量,指针和函数不可改变,同时明确指定了类型,可以方便编译器做类型检查,也增加了代码的可读性。 74 | 1. const修饰变量必须初始化。如果是全局的const变量,通常放在静态区。在局部声明的const变量放在栈区。 75 | 2. const修饰成员函数时,函数中的成员变量不可改变,除非该变量特别声明为mutable 76 | 3. const可以用来修饰指针,称为常量指针const int *p 指针的指向可以改变,但是不能改变指针指向的值。 77 | 4. const修饰常量的指针叫做指针常量,int* const p 指针的指向不可以修改,指针指向的值可以修改。指针常量必须初始化。 78 | 79 | ### const和define的区别 80 | - const可以明确指定数据类型,而宏定义没有数据类型。 81 | - define宏是在预处理阶段展开,const常量是在编译运行阶段使用。 82 | - define宏不分配内存,变量定义分配内存。 83 | ## extern关键字 84 | 85 | 1. 引入同一模块在其他文件中定义的全局变量和函数。 86 | 2. 如果在C++里调用了C库定义函数,那么需要使用`extern "C"` 标识这个函数,告诉编译器使用C的方式进行编译,防止C++的编译方式导致命名重整,无法找到对应的C函数。命名重整的原因在于C++支持函数重载,而C不支持,所以C++编译时增加了函数参数的标识符。 87 | 88 | ## this关键字 89 | 1. 解决同名冲突 90 | 2. 返回对象本身 91 | - this指针的本质是指针常量,指针的指向不可以修改。 92 | 93 | ## std::move() 94 | - 将左值强制转换为右值引用,右值引用可以减少一次对象的析构和对象的构造。 95 | - 右值引用可以减少深拷贝的次数。 96 | 97 | ## 段错误 98 | - 段错误通常发生在访问非法内存地址的时候。系统会发送一个SIGSEGV11号信号告诉当前进程,进程采取默认的捕获方式,即终止进程。 99 | 1. 野指针 100 | 2. 试图修改字符串常量的内容 101 | 102 | ## auto关键字 103 | - 让编译器能够根据初始值的类型推断变量的类型。当处理复杂类型,比如STL中的类型时,优势最明显。`auto p = vt.begin()` 104 | 105 | ### 四种强制类型转换 106 | 1. static_cast低风险的转换,比如整数转浮点数,字符型转整形。 107 | 2. const_cast去掉const关键字的转换,可以去掉带const的指针和引用。 108 | 3. dynamic_cast使具有继承关系的基类转换为派生类,如果不可以转换则返回NULL。 109 | 4. reinterpret_cast指针或引用的转换,风险较高。 110 | 111 | ## RTTI 112 | - 运行时类型识别 113 | - run time type identification 114 | - 常常结合typeid()和dynamic_cast实现。可以根据当前调用的指针是何种类型,经过dynamic_cast转换后,调用非虚函数。 115 | - dynamic_cast只能用于指针和引用的转换,要转换的类型中必须包含虚函数,转换成功返回子类的地址,失败返回NULL。 116 | - typeid返回一个type_info对象的引用。 117 | 118 | ### 构造函数不能是虚函数 119 | - 虚函数是通过虚函数表指针来调用的,而虚函数表指针存在对象内存空间。当一个对象调用构造函数时,该对象还没有实例化,即没有分配内存空间,所以虚函数表指针无法找到。 120 | 121 | ### 析构函数尽量是虚函数 122 | - 析构函数不是虚函数容易引起内存泄漏。 123 | ```shell 124 | Animal *animal = new Cat(); 125 | ``` 126 | - 为了实现多态的动态绑定,通常将基类指针指向派生类对象。 127 | - 当指针销毁时,如果析构函数不是虚函数,根据析构函数在继承中的调用顺序,则派生类对象的析构函数将不会被执行,造成内存泄漏。 128 | 129 | ## 析构函数不能抛出异常 130 | 131 | - 析构函数抛异常,则异常点之后的的程序不会执行,如果异常点之后有释放资源的操作,则这部分资源无法释放,导致内存泄漏。noexcept 132 | 133 | ## 内存泄漏 134 | - 不再需要使用的内存单元,没有及时释放。 135 | - memcheck和valgrind检测内存泄漏的工具。 136 | - 使用RAII资源获取就是初始化和智能指针。 137 | 138 | ## 野指针 139 | 140 | - 一些内存的单元已被释放,之前指向它的指针还在被使用。 141 | 142 | ## vector和list的区别 143 | 144 | 1. vector是动态数组,在内存中分配一块连续的内存空间,因此可以使用下标进行快速的随机访问。但是删除和插入需要移动大量的元素。 145 | 2. list是双向链表,在内存中是不连续的空间,由指针将不同的地址连接在一起。list的插入和删除操作都是O(1)的。 146 | 3. 数组必须事先设定固定的长度,不能动态的增减,可能会造成资源浪费。链表可以动态的增减。 147 | 148 | 149 | 150 | ## 模板特化 151 | - 全特化:模板参数被指定未确定的类型 152 | - 偏特化:模板参数没有被全部确定,需要编译器在编译时进行确定。只能偏特化类模板,不能偏特化函数模板。 153 | - 别名模板和变量模板属于语法糖 154 | 155 | 156 | ## 右值引用 157 | - 右值引用指向要被销毁的对象。右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。 158 | - move函数将左值转换为右值,调用move函数后源对象只能赋值或销毁。 159 | 160 | ## override 161 | - override在子类中标记某个函数,表示想要覆盖已有的虚函数,如果没有覆盖,编译器会报错。 162 | 加作用域运算符调用特定类的虚函数 163 | 164 | ## 内联函数的优劣 165 | 1. 优点:减少函数调用的开销,包括寄存器值的保存和实参的拷贝等。 166 | 2. 缺点:增加函数体积,可能导致cache装不下,从而减少了cache的命中率。 167 | 168 | - inline只是一个请求,编译器有权拒绝。 169 | 170 | ## 拷贝构造函数 171 | 172 | 调用场景: 173 | 174 | 1. 一个对象以值传递传参 175 | 2. 一个对象以值传递的方式从函数返回 176 | 3. 一个对象通过另一个对象初始化 177 | 178 | ## 空类 179 | - 占有一个字节 180 | - 有构造,析构,拷贝,赋值运算符,取地址运算符。 181 | - 构造函数可以被重载,析构函数不可以被重载且不能带参数。 182 | 183 | ## explicit 184 | 185 | - explicit取消隐式转换,类中构造函数默认是implicit 186 | 187 | - explicit关键字的作用是防止类构造哈桑农户的隐式自动转换,只对有一个参数的构造函数有效。 188 | 189 | ## 堆和栈的区别 190 | 1. 栈连续,堆不一定连续。 191 | 2. 申请方式不同。栈由操作系统自动分配,堆需要程序员自己申请。 192 | 3. 生长方向不同。栈由高地址向地址生长,是一块连续的内存区域。堆由地址向高地址生长,是不连续的内存区域。在一个链表中记录空间内存地址。 193 | 4. 分配速度。栈由系统分配,速度较快。堆使用new分配,速度较慢,且容易产生内部碎片。 194 | 195 | ## static的作用 196 | - static可以用来修饰函数和变量。修饰全局变量和局部变量时都是放在静态区,static变量只初始化一次,在程序结束时销毁,全局和局部的区别在于作用域不同。static可以修饰普通成员函数,表明这个函数只在本文件中有效。static修饰类成员变量是,这些变量为这个类所共享,static修饰类成员函数时,也是所有对象共享这个函数,该函数中没有this指针。同时static类成员函数中只能调用static修饰的函数。 197 | 198 | ## 静态存储区 199 | 200 | 存放的static修饰的全局变量和局部变量,const修饰的变量以及字符串。 201 | 202 | ## 数据段和静态区的区别 203 | 204 | - 数据段存放的是代码的二进制指令。静态区是变量。 205 | -------------------------------------------------------------------------------- /ComputerNetwork/README.md: -------------------------------------------------------------------------------- 1 | # 计算机网络 2 | - TCP 3 | - UDP 4 | - HTTP 5 | - HTTPS 6 | - TLS 7 | ### TCP的特点 8 | > 这部分内容可以看陶辉的慕课视频,讲得非常清晰 9 | 1. 面向连接的流式协议 10 | 2. 可靠 11 | 3. 超时重传 12 | 4. 重复确认 13 | 5. 拥塞避免 14 | ### UDP的特点 15 | 1. 面向无连接的报文协议 16 | 2. 不可靠 17 | ### TCP头部 18 | 1. 16位源端口 19 | 2. 16位目的端口 20 | 3. 32位序号 21 | 4. 32位确认序号 22 | 5. 4位TCP头部长度 23 | 6. 6位标志位 24 | 7. 16位滑动窗口 25 | 8. 16位校验和 26 | 9. 16位紧急指针 27 | 28 | - 注意:TCP的包没有IP地址,只有源端口和目的端口。一个TCP连接需要4个元组来表示一个连接。(源端口,目的端口,源IP,目的IP) 29 | 30 | ## 为什么建立连接需要3次握手 31 | - 主要是初始化序列号和协商最大报文段长度 32 | 33 | ## 粘包问题 34 | 35 | - udp不存在粘包的问题,因为udp是个数据包协议,也就是两段数据间有界限的。要么收不到,要么全收。 36 | 37 | - 产生粘包的原因:nagle算法为了改善网络传输效率,延迟发送数据。应用层由于某些原因不能及时取出TCP的数据,导致TCP缓冲区存放了多段数据。 38 | 39 | - 解决方式:封包和拆包。包头存放一个变量记录包体的长度。在所发送的内容前,加上发送内容的长度。 40 | 41 | ## HTTP原理 42 | - http协议是应用层协议,通过请求响应的方式在客户端和服务器端进行通信。 43 | - http协议是以明文的方式进行传输,并且是无状态的通信协议。 44 | 45 | ## http与https的区别 46 | 1. 传输方式:http是明文传输,极易被监听和篡改。而https加入了ssl层,数据经过了加密,从而保护了传输数据的隐私和完整性。 47 | 2. 身份认证: http没有身份认证,而https经过证书颁发机构的多重认证。 48 | 3. 连接端口:http为80,https为443。 49 | 4. 实现成本:http基本没有成本,https需要申请证书,同时在加密解密上需要消耗更多的CPU资源,访问速度有可能降低。 50 | 5. 加锁的图标显示 谷歌和百度搜索的排名会对非https的排名有影响 51 | 52 | - 私钥能解密,但是不能确认是哪个客户端发送的消息,任何人都可以抵赖。为了防止抵赖,可以使用数字签名。 53 | - https是http的安全版,在http的基础上增加了SSL安全层。 54 | - 基于性能的考虑,https一般使用非对称加密算法获得密钥,再用对称加密算法对消息内容进行加密。 55 | 56 | https发送请求的过程: 57 | 1. 第一步,客户端和服务器端交换SSL版本和加密组件列表,同时服务器端将密钥和签名证书发给客户端。 58 | 2. 第二步:客户端根据证书和密钥进行验证,通过以后协商传输的密钥。这一步使用非对称加密算法。 59 | 3. 第三步:当双方都获得密钥,且校验码没有问题。则进行TCP三次握手,此时采用对称加密算法,提高效率。 60 | 61 | ## http1.0 与 http1.1的区别 62 | 1. 长连接:HTTP 1.1支持长连接和请求的流水线处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。 63 | 2. 带宽优化:HTTP/1.1中在请求消息中引入了range头域,它允许只请求资源的某个部分。 64 | 3. 新增响应状态码:100 continue 已经收到第一部分,正等待剩余部分。 101 switch protocols 服务器已确认切换协议。 65 | 4. host头域:允许物理主机上多个虚拟主机共享一个IP 66 | 5. 缓存机制更灵活,新增control-cache头域 67 | 6. 增加了5个请求方法:put delete connect options trace 68 | 69 | ## get和post的区别 70 | - 区别在于: 71 | 1. 用途上:get一般用于获取资源,post一般用于创建资源。 72 | 2. 位置上:get请求的的数据会在地址栏上显示出来,以问号分割url与传输数据,多个参数用取地址符连接。而post的数据放在请求体中。 73 | 3. 安全性上:优于get将信息显示在地址栏,所以对于用户密码等个人隐私信息很不安全,而post放在请求体中,在安全性上要稍微好点。 74 | 4. 长度限制:get使用地址栏发送数据,而地址栏的长度是有限的。 75 | 5. 幂等性上:get操作没有副作用,多次操作产生的副作用相同,所以get是幂等的,而post用于创建资源是会又副作用的,所以post不是幂等的。 76 | 77 | 78 | 79 | ## cookie和session 80 | - cookie和session都是跟踪会话的机制。 81 | 1. 存储位置:cookie保存在客户端用来记录信息和确定用户身份,session保存在服务端同样用来记录和确定身份。 82 | 2. 安全性:cookie放在客户端很容易被查看或者破解,没有session安全。 83 | 3. 关联性:session的运行依赖于session id 而session id 存在cookie中。如果浏览器禁止了cookie,可以使- 用url地址重写来传递session id 84 | 4. 性能上:session会在有效期内存在于服务器的数据库或者文件,当请求过多时,服务器性能会下降。 85 | 5. 大小上:单个cookie保存的大小不能超过4k 86 | 87 | 使用cookie来管理session以弥补http中无状态特性。通过对set-cookie头域写入session ID可以免登录,提高访问的效率。 88 | 89 | 握手优化:session缓存, session key 放在内存,有内存消耗, 负载均衡后找不session key。session ticket 集群可以共享。 90 | 91 | ## TCP和UDP的区别 92 | 1. TCP: 面向连接的安全的流式协议,连接的时候进行三次握手,数据发送的时候会进行数据确认,数据丢失之后,会进行数据重传。 确认和重传机制。 93 | 2. UDP: 面向无连接的不安全的报文传输,发出去就不管了,收则全收,丢则全丢。 94 | 95 | 96 | 97 | ## 3次握手 98 | - TCP三次握手:客户端向服务器端:发送SYN=1和序号seq 99 | - 服务器端向客户端:回应确定信号同意连接ACK=1以及自己的连接请求SYN=1还有序号seq 100 | - 客户端回应服务器端:ACK=1告诉对方它已经知道了服务器端同意,连接成功。 101 | 102 | - TCP四次挥手:主动关闭方发送关闭信号,被动关闭方收到信号。然后进入半关闭状态,关闭的一方能接收数据但是不能发送数据。 等到另一个未关闭的一方,发起关闭信号以后,进入TIME_WAIT状态,等待对方2MSL之后,彻底关闭。 103 | 104 | 105 | 106 | ### 3次握手 107 | - 客户端发起连接,也就是C语言中的connect函数,发送一个SYN=1的标志位,同时携带一个序号。 108 | - 服务器端有一个accept函数,用于响应连接。服务器端响应连接后回复一个ACK=1的标志位,并且也发送一个SYN=1的标志位建立连接。 109 | - 客户端收到服务器端的ACK应答以后,说明建立成功。两者都同时进入established状态。同时accept和connect函数调用成功,并返回1。 110 | 111 | ### 4次挥手 112 | - 主动关闭方向被动关闭放发送FIN标志位,表示要断开连接。被动关闭方同意关闭,并回发ACK标志位。此时主动关闭放进入FIN_WAIT_2状态。以后主动关闭方仍然可以接收数据,但是不可以再发送数据。 113 | - 当另一方也决定关闭时,会发送FIN标志位,接收方回复ACK同意关闭,并且自身进入Time_wait状态,等待2MSL时长后关闭。发送方如果收到ACK应答后,就直接关闭,如果没有收到会一直发FIN标志位。 114 | ## 滑动窗口 115 | 116 | - 流量控制:防止发送方发的太快,耗尽接收方的资源。 117 | - 控制机制:滑动窗口 118 | - 在TCP报文的头部有一个16位的窗口大小,用于告诉发送方接收方可用的缓冲区大小。 119 | 120 | ## 拥塞窗口 121 | - 拥塞控制:防止发送方发的太快,使网络来不及处理,从而导致网络拥塞 122 | - 控制机制:拥塞窗口 123 | 124 | ## 慢启动 拥塞避免 快重传 快恢复 125 | 126 | 1. 慢启动:为了防止大量数据瞬间注入网络,引起网络阻塞。慢启动算法设定,最开始窗口为1个最大报文长度。一个传输轮次增加一倍的窗口大小。当达到慢开始门限后,执行拥塞避免算法。 127 | 2. 拥塞避免: 每个传输轮次将窗口增加一个单位,即加法增长。 128 | 3. 快重传: 当收到3个重复确认以后,执行快恢复算法。慢开始门限和发送窗口减半,然后发缺失的数据,进行加法增长,重新进入拥塞避免阶段。 129 | 4. 快恢复:慢开始门限减半,发送拥塞窗口设定为门限加3。如果后面依旧收到重复的ACK则进行加法增长窗口,如果收到新的ACK,则拥塞窗口设定为慢开始门限的值,并重新进入拥塞避免阶段。 130 | 131 | 超时进入的是慢启动,重复确认才进入快恢复。 132 | 133 | 选择性重传在options中left edge和right edge告诉发送方已经收到的报文序号 134 | 135 | ## TCP粘包问题 136 | 137 | udp不会出现粘包。发送方发送的若干包数据到接收方接收时,包粘在了一起。 138 | 139 | 造成粘包的原因时因为发送端延迟发送或者接收方没有及时接收缓冲区中的数据。 140 | 141 | - 通常可以使用以下三种方式来解决 142 | 143 | 1. 编程时设定立即发送的操作指令 144 | 2. 把数据长度与消息一起发送。 145 | 3. 使用特殊标记来区分消息的间隔 146 | 147 | ## HTTP和HTTPS有什么不同 148 | 149 | HTTP协议是一种使用明文数据传输的网络协议。HTTPS协议可以理解为HTTP协议的升级,就是在HTTP的基础上增加了数据加密。在数据进行传输之前,对数据进行加密,然后再发送到服务器。这样,就算数据被第三者所截获,但是由于数据是加密的,所以你的个人信息让然是安全的。这就是HTTP和HTTPS的最大区别。 150 | 151 | ## 在浏览器地址栏键入URL,按下回车之后会经历以下流程 152 | 153 | 1. 浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;(递归式和迭代式) 154 | 2. 解析出 IP 地址后,**根据该 IP 地址和默认端口 80**,和服务器建立TCP连接; 155 | 3. 浏览器发出读取文件(URL 中域名后面部分对应的文件)的HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器; 156 | 4. 服务器对浏览器请求作出响应,并把对应的 html 文本发送给浏览器; 157 | 5. 释放 TCP连接; 158 | 6. 浏览器将该 html 文本并显示内容;   159 | 160 | 161 | 162 | ## UDP如何做到可靠 163 | - 想要做到可靠,必须要做到无重复,无丢失,无错误,无失序。借鉴TCP的可靠机制 164 | 1. 发送时进行编号 165 | 2. 接收方收到数据发出应答信号(超时重传,3次确认重传) 166 | 3. 增加校验位 167 | 168 | ## NAT DNS ARP 169 | 1. NAT用于实现从内部IP地址到外部IP地址的映射 170 | 2. DNS提供域名到IP地址的映射或者反过来 171 | 3. ARP提供IP到MAC地址的映射 172 | 173 | ## DNS 174 | 175 | 基于UDP的协议 176 | 177 | 1. 递归查询:父域名代替当前服务器递归查询,最后依次返回 178 | 2. 迭代查询:父域名服务器告诉当前服务器下一次查询的位置 179 | 180 | ## http状态码 181 | 1. 100 continue 等待继续发送 182 | 2. 200 ok 请求成功 183 | 3. 206 partial content 部分资源 184 | 4. 301 永久重定向 185 | 5. 302 临时重定向 307 186 | 6. 400 客户端请求报文语法错误 187 | 7. 403 禁止访问 188 | 8. 404 资源不存在 189 | 9. 408 请求超时 190 | 10. 500 服务器内部错误 191 | 11. 503 服务器不可用 192 | ## http 2.0 SPDY 193 | 1. 二进制分帧 194 | 2. 多路复用 195 | 3. 首部压缩 196 | 4. 服务器推送 197 | 198 | ## http 3.0 QUIC+UDP 199 | 1. 0 RTT 200 | 2. 没有队头阻塞的多路复用 201 | 3. 前向纠错 202 | 203 | ## 网络安全 204 | 1. sql注入:用户提交一段数据库查询代码,根据程序返回的结果获得它想得知的数据。 205 | 2. dos攻击: 让运行的服务器呈停止状态。集中请求造成资源过载,攻击安全漏洞使服务停止。 206 | ## 七层模型 207 | 物数网传会表应 208 | ping是从应用层直接使用网络层的ICMP协议的,不经过传输层。原始套接字直接使用网络层的IP。 209 | 两台电脑通信:网线+不同的IP地址和子网掩码,即处于同一网段。 210 | TCP和UDP可以同时使用相同的端口。 211 | 应用程序可以同时使用TCP和UDP两个协议。 212 | ## hub集线器 213 | 214 | 可以实现多个IP主机通信,但是hub的实现方式是广播,容易产生拥堵。 215 | 216 | ## switch交换机 217 | 218 | 是集线器的升级版,可以广播可单播。ARP不知道对方MAC地址时,先广播6个ff的MAC地址,所有网卡都会接收,但是只有目的IP会单播回应,其他的都会丢弃。然后发送方收到正确的MAC后再单播传输数据。 219 | 220 | ## ARP攻击 221 | 222 | 给两个MAC地址响应ARP广播的目的IP,经由中间人,窃取信息后再转发到正确的地址。 223 | 224 | ## 默认网关 225 | 226 | 在同一个交换机连接的网络中,属于同一网段,用不到默认网关。网关用来传递两个不同网段的通信,默认网关通常是路由器。当通信的数据不在当前网段时,即发给默认网关。路由器就是用来连接不同网段的,用来构建一个更大的网络。在传输不同的网段信息时,源IP和目的IP是不变的,源MAC和目的MAC是改变的,每经过一个路由器修改一次,记录的是下一次的目的,和这一次的发送MAC。 227 | 228 | ## 延迟确认 229 | 230 | ack会随着响应数据发送给对方,如果没有响应的数据就会等待200ms左右,在这期间如果有对方确认到达则立即发送。如果200ms后仍然没有数据需要发送则单独发送ACK。目的是节省带宽。 231 | 232 | ## Nagle算法 233 | 234 | 1. 没有已发送未确认报文段时,立即发送数据。 235 | 2. 存在未确认报文段时,达到mss时再发。 236 | 237 | 同时有nagle算法和延迟确认存在时会导致网络效率下降,通常会关闭延迟确认和nagle算法。 238 | 239 | ```c 240 | setsockopt(s,IPPROTO_TCP,TCP_QUICKACK,(int*){1}, sizeof(int)); //关闭延迟确认 241 | setsockopt(client_fd, SOL_TCP, TCP_NODELAY,(int[]){1}, sizeof(int)); //关闭nagle算法 242 | ``` 243 | 244 | ## 忽略SIGPIPE信号 245 | - 客户端和服务器端连接建立后,若某一端关闭连接,而另一端仍然向它写数据,第一次写数据后会收到RST响应,第二次写数据时,内核会向进程发送一个SIGPIPE信号,通知进程此连接已断开,而这个信号的默认处理方式是终止进程,服务器直接关闭。 246 | 247 | ```c 248 | signal(SIGPIPE, SIG_IGN) 249 | ``` 250 | 251 | SIGSEGV 11 访问地址无效 SIGIO 29异步通知信号 SIGKILL 9 无条件终止 252 | 253 | ## 传输层与网络层的区别 254 | 255 | 1. 传输层位于网络层之上,为不同主机上的应用进程提供逻辑通信。端到端传输。 256 | 2. 网络层负责IP数据报的产生以及ip数据包在网络中的路由转发。 257 | 258 | ## 状态码499 259 | - 在Nginx服务器端处理的时间过长,客户端主动关闭了连接。 260 | 261 | ## 分块编码 262 | - transfer-encoding:chunked 响应头域 263 | - 它允许服务器发送给客户端的数据分成多个部分,并且不需要预先直到发送数据的总大小。 264 | 265 | ## close_wait 266 | - 基本的思想就是要检测出对方已经关闭的socket,然后关闭它。维持一个心跳包或者设置一个超时时间。 267 | ## 1. 状态码 268 | 269 | - 301 move permanently 270 | - 302 found POST方法的重定向在未询问用户的情况下就变成GET 271 | - 303 see other POST重定向为GET 272 | - 307 temporary redirect 当客户端的POST请求收到服务端307状态码响应时,需要跟用户询问是否应该在新URI上发起POST方法,也就是说,307是不会把POST转为GET的。 273 | 274 | ### TCP拥塞控制 275 | 276 | - 慢开始:初始窗口,每个往返事件RTT增加一倍的窗口,呈现指数增长。 277 | - 拥塞避免:当窗口大小达到阈值以后,每个RTT增加一个窗口的大小。 278 | - 超时后,进行阈值变为当前窗口的一半,窗口大小从1进行慢启动 279 | - 重复确认:阈值和窗口同时变为当前窗口的一半,进行拥塞避免,称为快恢复。 280 | 281 | ### TCP状态转换 282 | 283 | - close_wait半关闭的被动关闭方 284 | - time_wait被动关闭方,等待2MSL的时间,保证ACK可以顺序送达。使用端口复用可以消除time_wait,修改TCP的参数可以增大或减少这个时间长度。 285 | 286 | ## http2.0和http3.0 287 | 288 | http2.0 spdy 289 | 290 | - 首部压缩 291 | - 多路复用 292 | - 服务器推送 293 | 294 | - http3.0 quic+udp 295 | 296 | - 0 RTT 297 | - 前向纠错 298 | - 没有队头阻塞的多路复用 299 | 300 | 301 | ## 流量控制和拥塞控制 302 | - 流量控制 303 | 定义和目的:如果发送者发送数据过快,接收者来不及接收,那么就会有分组丢失。为了避免分组丢失,控制发送者的发送速度,使得接收者来得及接收,这就是流量控制。流量控制根本目的是防止分组丢失,它是构成TCP可靠性的一方面。 304 | 305 | 实现机制:由滑动窗口协议(连续ARQ协议)实现。滑动窗口协议既保证了分组无差错、有序接收,也实现了流量控制。主要的方式就是接收方返回的 ACK 中会包含自己的接收窗口的大小,并且利用大小来控制发送方的数据发送 306 | 307 | 可能会引发死锁,怎么避免:当发送者收到了一个窗口为0的应答,发送者便停止发送,等待接收者的下一个应答。但是如果这个窗口不为0的应答在传输过程丢失,发送者一直等待下去,而接收者以为发送者已经收到该应答,等待接收新数据,这样双方就相互等待,从而产生死锁。为了避免流量控制引发的死锁,TCP使用了持续计时器。每当发送者收到一个零窗口的应答后就启动该计时器。时间一到便主动发送报文询问接收者的窗口大小。若接收者仍然返回零窗口,则重置该计时器继续等待;若窗口不为0,则表示应答报文丢失了,此时重置发送窗口后开始发送,这样就避免了死锁的产生。 308 | 309 | 流量控制与拥塞控制的区别 310 | 311 | 拥塞控制:拥塞控制是作用于网络的,它是防止过多的数据注入到网络中,避免出现网络负载过大的情况;常用的方法就是: 312 | 1. 慢开始、拥塞避免 313 | 2. 快重传、快恢复。 314 | 315 | 流量控制:流量控制是作用于接收者的,它是控制发送者的发送速度从而使接收者来得及接收,防止分组丢失的。 316 | 317 | **拥塞控制算法** 318 | 319 | 假定:1、数据是单方向传递,另一个窗口只发送确认;2、接收方的缓存足够大,因此发送方的大小的大小由网络的拥塞程度来决定。 320 | 321 | 1. 慢开始算法 322 | 323 | 发送方维持一个叫做拥塞窗口cwnd(congestion window)的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。发送方让自己的发送窗口等于拥塞窗口,另外考虑到接受方的接收能力,发送窗口可能小于拥塞窗口。 324 | 325 | 慢开始算法的思路就是,不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小 326 | 327 | 这里用报文段的个数作为拥塞窗口的大小举例说明慢开始算法,实际的拥塞窗口大小是以字节为单位的。如下图: 328 | 329 | 330 | 从上图可以看到,一个传输轮次所经历的时间其实就是往返时间RTT,而且没经过一个传输轮次(transmission round),拥塞窗口cwnd就加倍。 331 | 332 | 为了防止cwnd增长过大引起网络拥塞,还需设置一个慢开始门限ssthresh状态变量。ssthresh的用法如下:当cwnd < ssthresh时,使用慢开始算法。 333 | 1. 当cwnd>ssthresh时,改用拥塞避免算法。 334 | 2. 当cwnd=ssthresh时,慢开始与拥塞避免算法任意 335 | 336 | 注意,这里的“慢”并不是指cwnd的增长速率慢,而是指在TCP开始发送报文段时先设置cwnd=1,然后逐渐增大,这当然比按照大的cwnd一下子把许多报文段突然注入到网络中要“慢得多”。 337 | 338 | 2. 拥塞避免算法 339 | 340 | 拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口按线性规律缓慢增长。 341 | 342 | 无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有按时收到确认,虽然没有收到确认可能是其他原因的分组丢失,但是因为无法判定,所以都当做拥塞来处理),就把慢开始门限ssthresh设置为出现拥塞时的发送窗口大小的一半(但不能小于2)。然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕 343 | 344 | 345 | 3. 快重传算法 346 | 347 | 快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方,可提高网络吞吐量约20%)而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期 348 | 349 | 4. 快恢复算法 350 | 351 | ## http和https的区别 352 | 353 | https是安全版的http协议。 354 | 355 | ## get和post的区别 356 | 357 | get用来获取资源,post用来创建资源。 358 | 359 | get将数据放在url地址栏中,使用问号与url分割,数据间用取地址符分割。post将数据放在请求体中。 360 | 361 | 安全性上get放在url容易暴露隐私信息,而post放在请求体中可以适当的避免。 362 | 363 | get在传输的数据受url地址栏的限制,post不受这种限制 364 | 365 | get的操作是幂等的,多次操作产生的影响相同,而post是非幂等的 366 | 367 | ## 断点续传 368 | 369 | [视频辅助](https://www.bilibili.com/video/BV1RE411A7wd?p=22) 370 | 371 | 1. 客户端在header中的range字段中指明,请求传输的区间[l, r] 372 | 2. 服务器端在header中的content-range返回当前接受的范围和文件总大小。并返回206patial content 状态码。 373 | 374 | ## TLS协议 375 | 376 | [视频](https://www.bilibili.com/video/BV1LK4y1k7N4?p=70) 377 | 378 | 379 | TLS握手协议:1.交换加解密的安全套件 2.验证通讯双方的身份 3.协商加密的参数。 380 | 381 | https是在TCP之上增加了一层TLS层 382 | 383 | TLS1.3版本限定了安全套件的数量,防止低版本的安全组件被暴力破解。开始时客户端发送hello并携带一个自己生成的公钥,服务器端选择一个安全套件并将自己生成的公钥发给客户端,客户端收到公钥以后,两段此时都有一个对方发来的公钥和自己的私钥。此时使用ECDH椭圆曲线非对称加密算法,生成一个相同的密钥,这个密钥就用来进行后续的对称加密。 384 | 385 | 校验过程:判断数字证书中的哈希值和公钥解码的哈希值是否一致。 386 | 387 | ## TCP两次握手会产生什么问题 388 | 389 | [参考链接](https://blog.csdn.net/Runner1st/article/details/88242692) 390 | 391 | 长期处于半连接状态可能会造成TCP内核中SYN队列的爆满,服务器会在一定时间内终止半连接,并回收资源。如果使用syn洪泛攻击,也可能造成SYN队列爆满,可以使用SYN cookie来解决这个问题。即SYN到达时并不放入SYN队列中,而是将所有的信息写入cookie,当客户端ACK到达时验证cookie中的信息后再分配资源。 392 | 393 | TCP两次握手会产生什么问题:已经失效的连接报文段突然又被服务器端收到,造成双方的不一致,进而造成资源浪费。此时如果服务器端发送连接到失效的请求,并返回SYN和ACK后,自认为连接已经建立好了,所以会频繁的发送数据到客户端,而客户端处于closed状态,直接把数据丢弃。同时如果此时客户端想要建立新连接,但是已经又连接占用,也会导致客户端无法建立真正的需求。当客户端老是收到丢弃的数据,客户端就会发一个RST强制服务端关闭连接。 394 | 395 | RST和ACK收到时不用再回ACK 396 | 397 | ## DNS污染 398 | 399 | - 中间人对DNS进行了一些操作导致无法通过域名获得正确的IP。 400 | 401 | - 解决方法:1. 使用第三方DNS解析服务 2.搭建自己的DNS服务器 402 | 403 | ## http协议 404 | 405 | 406 | http协议用韵都是客户端发起请求,服务器返回响应,这样就会使得无法实现客户端未发起的请求,而服务器将消息推送给客户端。 407 | 408 | 请求报文组成部分:1. 请求行 2.请求头 3. 请求空行 4. 请求体 409 | 410 | http在1.1版本中,所有的请求头除host外都是可选的。host主要用于指定被请求资源的Internet主机和端口号,它通常从HTTP的URL中提取出来的。不包含host主机头域,服务器会返回400状态码。 411 | 412 | 1xx: 提示信息,表示请求已接收,继续处理。 413 | 414 | 2xx: 成功, 表示请求已被成功接收,理解,接受。 415 | 416 | 3xx: 重定向, 要完成请求必须进行更进一步的操作。 417 | 418 | 4xx: 客户端错误,请求有语法错误或请求无法实现。 419 | 420 | 5xx: 服务器错误,服务器未能实现合法的请求。 421 | 422 | ssl通信机制:1. 客户端发送client hello报文开始通信。报文中包含SSL指定版本号,加密组件列表,有加密算法和密钥长度。2. 服务器端回应server hello报文作为应答。筛选出SSL版本和加密组件,并发送CA证书,其中包括公钥。3. 第二次交互,客户端生成一个pre-master secret的随机密码串。并使用CA证书中的公钥加密。 423 | ## http和https的区别 424 | - https是http的安全版。 425 | 426 | 主要区别在于: 427 | 428 | 1. http是明文传输,https是密文传输。 429 | 2. http默认端口是80, https的默认443 430 | 3. https需要验证服务器端的身份,如果CA证书不正确则会中断通信。 431 | 4. CA证书需要成本,加密解密的过程增加CPU和内存的开销。 432 | 433 | - https增加了ssl层,用于确保传输的安全性。 434 | 435 | - 通信前先进行ssl层的握手,首先客户端ssl版本号和加密组件发送给服务器端。 436 | 437 | - 服务器端筛选出可用的ssl版本号和加密算法同时加上CA证书发送给客户端。 438 | 439 | - 客户端验证CA证书的有效性,如果无效则中断通信。 440 | 441 | - 若有效客户端发送加密的pre-master secret随机密码串,这一步使用的是非对称加密,用于协商后面对称加密的密钥,所以这一步不能被篡改和截获。客户端得到服务器端的响应后,且验证通过后,后续就使用对称加密加密算法进行加密。然后进行TCP三次握手。 442 | -------------------------------------------------------------------------------- /DataStructure/README.md: -------------------------------------------------------------------------------- 1 | # 数据结构与算法 2 | 1. B+树 3 | 2. 红黑树 4 | 3. LRU 5 | 4. 布隆过滤器 6 | 5. 一致性哈希 7 | 6. 跳表 8 | 7. 位图 9 | 10 | ## 平衡树和红黑树的区别 11 | - 平衡树是严格的左右子树高度差不超过1 12 | - 红黑树在平衡性上做了妥协,自定义了四条规则 13 | - 所有的根节点都是黑色 14 | - 红色的孩子是黑色 15 | - 任意节点到叶子节点的路径上黑色节点的数目相同 16 | - 所有叶子节点都是黑色 17 | - 红黑树牺牲查找的效率,换取了插入和删除的效率,但是平均时间复杂度还是log(N) 18 | ## 单例模式 19 | 20 | ```c 21 | class A{ 22 | public: 23 | static A* getInstance(); 24 | static A* instance; 25 | private: 26 | A(); 27 | ~A(); 28 | }; 29 | A* A::instance = nullptr; 30 | //内存读写reorder不安全 导致双检查锁失效 31 | //先分配内存 再执行构造器 最后返回给实例 32 | //编译器的优化 33 | A* A::getInstance(){ 34 | //读没有问题 写就有问题 35 | //锁前检查 锁的粒度过大 锁后检查安全性 36 | if(instance==nullptr){ 37 | mutex.lock(); 38 | if(instance==nullptr) instance = new A(); 39 | mutex.unlock(); 40 | } 41 | return instance; 42 | } 43 | 44 | ``` 45 | 46 | 47 | 48 | ## 快排 归并 堆排 49 | 50 | ```c 51 | #include 52 | #include 53 | using namespace std; 54 | 55 | //快排 时间复杂度 平均nlogn 空间复杂度O(1) 56 | void quick_sort(int arr[], int l, int r){ 57 | if(l >= r) return; 58 | int x = arr[l+r >> 1]; 59 | int i = l-1, j = r+1; 60 | while(i < j){ 61 | while(arr[++i] < x); 62 | while(arr[--j] > x); 63 | if(i < j) swap(arr[i], arr[j]); 64 | } 65 | quick_sort(arr, l, j); 66 | quick_sort(arr, j+1, r); 67 | } 68 | //归并 69 | //时间复杂度NlongN 空间复杂度N 70 | 71 | int tmp[105]; 72 | void merge_sort(int arr[], int l, int r){ 73 | if(l >= r) return; 74 | int mid = (l + r)>>1; 75 | merge_sort(arr, l, mid); merge_sort(arr, mid+1, r); 76 | //合并 77 | int i = l, j = mid+1, k = 0; 78 | while(i <= mid && j <= r){ //i和j注意了 79 | if(arr[i] < arr[j]) tmp[k++] = arr[i++]; 80 | else tmp[k++] = arr[j++]; 81 | } 82 | while(i <= mid) tmp[k++] = arr[i++]; 83 | while(j <= r) tmp[k++] = arr[j++]; 84 | for(int i = 0; i < k; i++){ 85 | arr[l+i] = tmp[i]; 86 | } 87 | } 88 | 89 | //大根堆 把大交换上去 90 | void heapify(int arr[], int r, int n){ 91 | int i = 2*r+1, j = 2*r+2; 92 | int mx = r; 93 | if(i < n && arr[i] > arr[mx]) mx = i; 94 | if(j < n && arr[j] > arr[mx]) mx = j; 95 | if(mx != r) { 96 | swap(arr[mx], arr[r]); 97 | heapify(arr, mx, n); 98 | } 99 | } 100 | 101 | //堆排序 堆化 堆顶取出交换到末尾去 102 | void heap_sort(int arr[], int n){ 103 | for(int i = n; i >= 0; i--){ 104 | heapify(arr, i, n); //当前的点 总共的点 105 | } 106 | //交换完成 107 | for(int i = n-1; i >= 0; i--){ 108 | swap(arr[0], arr[i]); 109 | heapify(arr, 0, i); //总量减1 110 | } 111 | } 112 | 113 | int main(){ 114 | int arr[]{1,93,6,45,2,7,8,45,2123,239,0,3}; 115 | int n = 12; 116 | // merge_sort(arr, 0, n-1); 117 | //如果从0编号 那么儿子节点为2i+1 2i+2 118 | //最后一个节点是啥 119 | heap_sort(arr, n); 120 | for(int i = 0; i < n; i++) printf("%d ", arr[i]); 121 | puts(""); 122 | 123 | return 0; 124 | } 125 | ``` 126 | 127 | ## kmp算法 128 | 129 | ```c 130 | #include 131 | #include 132 | #include 133 | using namespace std; 134 | int Next[105]; 135 | void getNext(string p){ 136 | //按照左神讲的 考查的是当前位置的前一个字符串的最长前后缀 137 | Next[0] = -1; 138 | Next[1] = 0; 139 | int cn = 0, i = 2; 140 | int n = p.size(); 141 | while(i < n){ 142 | if(p[cn] == p[i-1]) Next[i++] = ++cn; 143 | else if(cn) cn = Next[cn]; 144 | else Next[i++] = 0; //前后缀为0 145 | } 146 | } 147 | void kmp(string p, string s){ 148 | //首先求next数组 149 | getNext(p); 150 | int i = 0, j = 0; 151 | int m = s.size(); 152 | int n = p.size(); 153 | while(i < m && j < n){ 154 | if(s[i] == p[j]) i++, j++; 155 | else if(Next[j] == -1) i++; //开头不匹配 156 | else j = Next[j]; 157 | if(j==n){ 158 | cout< 179 | #include 180 | #include 181 | #include 182 | using namespace std; 183 | //内存拷贝函数 按字节拷贝 184 | void* my_memcpy(void* dest, void* src, size_t count){ 185 | if(dest == NULL || src == NULL) return NULL; 186 | char* pdest = (char*) dest; 187 | char* psrc = (char*) src; 188 | while(count--){ 189 | *pdest++ = *psrc++; 190 | } 191 | return dest; 192 | } 193 | //字符串拷贝函数 仅用来拷贝字符串 194 | char* my_strcpy(char* dest, const char* src){ 195 | if(dest == NULL || src ==NULL) return NULL; 196 | char* pdest = dest; 197 | while((*dest++ = *src++) != '\0'); 198 | return pdest; 199 | } 200 | int main(){ 201 | char src[] ="hello"; 202 | char dest[100]; 203 | 204 | //my_memcpy(dest, src, strlen(src)); 205 | my_strcpy(dest, src); 206 | printf("%s\n", src); 207 | return 0; 208 | } 209 | ``` 210 | 211 | ## string类 212 | 213 | ```c 214 | class String{ 215 | public: 216 | String(const char* str = NULL); //通用构造函数 217 | String(const String& str); //拷贝构造函数 218 | ~String(); 219 | String& operator+(const String &str); 220 | String& operator+=(const String &str); 221 | char& operator[](const int n) const; 222 | String operator=(const String &str) const; 223 | bool operator==(const String &str)const; 224 | bool operator<(const String &str) const; 225 | bool operator<(const String &str) const; 226 | size_t size const; //获取长度 227 | //流运算符>> << 228 | private: 229 | char* data; //字符串 230 | size_t length; //长度 231 | } 232 | String::String(const char* str){ //通用构造函数 233 | if(!str){ 234 | length = 0; 235 | data = new char[1]; 236 | *data='\0'; 237 | }else{ 238 | length = strlen(str); //对空指针调用strlen会导致内存错误 239 | data = new char[length+1]; 240 | strcpy(data, str); 241 | } 242 | } 243 | String::String(const String& str){ //拷贝构造 244 | length = str.length; 245 | data = new char[length+1]; 246 | strcpy(data, str.data); 247 | } 248 | //析构 249 | String::~String(){ 250 | delete []data; 251 | length = 0; 252 | } 253 | //赋值重载 254 | String& String::operator=(const String &str){ 255 | delete []data; 256 | length = str.length; 257 | data = new char[length+1]; 258 | strcpy(data, str.data); 259 | return *this; 260 | } 261 | ``` 262 | 263 | ## 智能指针shared_ptr 264 | 265 | ```c 266 | #include 267 | #include 268 | #include 269 | using namespace std; 270 | template 271 | class SmartPtr{ 272 | private: 273 | T * ptr; 274 | int* use_count(); 275 | SmartPtr(T* p); 276 | SmartPtr(const SmartPtr & orig);//拷贝 277 | SmartPtr& operator=(const SmartPtr& orig); 278 | ~SmartPtr(); 279 | } 280 | SmartPtr::SmartPtr(T* p){ 281 | ptr = p; 282 | *use_count = 1; 283 | } 284 | SmartPtr::~SmartPtr(){ //析构 285 | delete ptr; 286 | ptr = nullptr; 287 | delete use_count; 288 | use_count = nullptr; 289 | } 290 | int main(){ 291 | return 0; 292 | } 293 | ``` 294 | 295 | ## 写一个函数在main函数执行前先运行 296 | 297 | ```c++ 298 | #include 299 | using namespace std; 300 | class Hello{ 301 | public: 302 | Hello(){ 303 | cout<<"before main!"< 317 | #include 318 | #include 319 | int main(){ 320 | pid_t fd[2]; 321 | pipe(fd); //创建管道 322 | int ret = fork(); 323 | if(ret > 0){ //父进程 父写 fd[0]读 fd[1]写 324 | close(fd[0]); 325 | char *str ="hello world\n"; 326 | write(fd[1], str, strlen(str)); 327 | sleep(1); 328 | }else if(ret == 0){ //子进程 子读 fd[1]关闭 329 | close(fd[1]); 330 | char buf[1024]; 331 | int n = read(fd[0], buf, sizeof(buf)); 332 | //写到显示器上 333 | write(1, buf, n); 334 | } 335 | return 0; 336 | } 337 | ``` 338 | 339 | ## rand7产生rand10 340 | 341 | [leetcode470](https://leetcode-cn.com/problems/implement-rand10-using-rand7/) 左程云BAT精讲 牛课堂系列算法讲座2.1 342 | 343 | ```c 344 | class Solution { 345 | public: 346 | int rand10() { 347 | int t; 348 | while(true){ 349 | t = rand7() + (rand7()-1) * 7; 350 | if(t <= 40) return t%10 +1; 351 | }; 352 | return 0; 353 | } 354 | }; 355 | ``` 356 | 357 | ## 反转二叉树 358 | 359 | ```c 360 | class Solution { 361 | public: 362 | TreeNode* invertTree(TreeNode* root) { 363 | //遍历所有点 然后交换左右孩子 364 | dfs(root); 365 | return root; 366 | } 367 | void dfs(TreeNode* root){ 368 | if(!root) return; 369 | dfs(root->left); 370 | dfs(root->right); 371 | swap(root->left, root->right); 372 | } 373 | }; 374 | ``` 375 | ```c++ 376 | #include 377 | using namespace std; 378 | class Person{ 379 | public: 380 | Person(int age, int height){ 381 | this->age = age; 382 | this->height = new int(height); 383 | } 384 | Person(const Person& p){ 385 | this->age = p.age; 386 | this->height = new int(*p.height); 387 | } 388 | ~Person(){ 389 | cout<<"析构"< 413 | #include 414 | using namespace std; 415 | int main(){ 416 | //按照关键字划分 返回不满足条件的首个位置下标 417 | vector v{19,8,7,3,5,1,0}; 418 | int key = 6; 419 | auto t = partition(v.begin(), v.end(), [key](int a){ 420 | return a < key; 421 | }) - v.begin(); 422 | cout< 434 | #include 435 | #include 436 | #include 437 | using namespace std; 438 | using std::placeholders::_1; 439 | using std::placeholders::_2; 440 | bool cmp( int a, int b){ 441 | return a > b; 442 | } 443 | int main(){ 444 | //使用bind()将函数转换为仿函数 445 | //从大到小 446 | vector v{1,0,99,3,4,12,2}; 447 | sort(v.begin(), v.end(), bind(cmp, _1, _2)); 448 | for(auto x: v) cout< 460 | #include 461 | #include 462 | #include 463 | using namespace std; 464 | int main(){ 465 | //lambda表达式 匿名函数对象 466 | auto f = [](const int &a){ 467 | return a*a; 468 | }; 469 | cout< 480 | #include 481 | #include 482 | #include 483 | using namespace std; 484 | int f(int a){ 485 | return a*a; 486 | } 487 | int main(){ 488 | //function是一个模板 相当于函数指针 function ff = func; 489 | function ff = f; 490 | cout< 501 | using namespace std; 502 | //x_{n+1} = a*x_n + c mod m 503 | //a = 48271 504 | int main(){ 505 | long long a = 48271, c = 0, x = 1; 506 | long long m = INT_MAX; 507 | for(long long i = 0; i < 10; i++){ 508 | x = (a*x+c) % m; 509 | cout<(str.c_str()); 519 | ``` 520 | 521 | -------------------------------------------------------------------------------- /Database/README.md: -------------------------------------------------------------------------------- 1 | # 数据库系统 2 | - SQL引擎 3 | - 存储引擎 4 | ## 算子 5 | - 扫描算子 6 | - 聚集算子 7 | ## 查询优化 8 | - 谓词下推 9 | - 常量折叠 10 | ## 两阶段锁 11 | - 强两阶段锁 12 | - 严格两阶段锁 13 | ## MVCC 14 | - 快照 15 | ## 日志 16 | - undo log 17 | - batch write 18 | ## 压缩 19 | - zstd 20 | - snappy 21 | ## 复制 22 | - 逻辑复制 23 | - 物理复制 24 | ## 索引 25 | - 索引是数据库中对表的字段进行排序的一种数据结构。 26 | 1. B+ 27 | 2. AVL 28 | 3. Hash 29 | 4. RBtree 30 | - 哈希表不利于范围查找。 31 | - 红黑树在数据量大的时候性能会下降。 32 | 33 | ## 联合索引 34 | - 对多个字段同时建立的索引。Mysql从左到右的使用索引中的字段,一个查询可以只使用索引中的一部份,但只能是最左侧部分,跳跃索引查询就会导致索引失效。 35 | 36 | ### B树 37 | - 关键字所有节点中只出现一次 38 | - 查询可能在非叶子节点结束 39 | ### B+树 40 | - [参考文档](https://zhuanlan.zhihu.com/p/98021010) 41 | 1. 非叶子节点只存索引。 42 | 2. 数据存储在叶节点, 43 | 3. 叶节点间使用双向指针相连。 44 | - 优点 45 | 1. 查询时间复杂度固定,都在叶子节点结束查询。 46 | 2. 非叶子节点索引范围更大。 47 | 3. 叶子节点双向链表方便范围查询。 48 | 4. 树的高度更低,用在数据库中磁盘IO次数更少。 49 | ## 数据库三大范式 50 | 1. 数据库中的所有字段都是不可分割的原子值。原子字段。 51 | 2. 满足第一范式的前提下,除主键外的每一列都必须完全依赖于主键。如果不完全依赖,只能发生在联合主键下。仅依赖主键。 52 | 3. 满足第二范式的前提下,除开主键列的其他列之间不能有传递依赖关系。各列无传递依赖。 53 | 54 | ## ACID 55 | 1. 原子性(Atomicity): 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; 56 | 2. 一致性(Consistency): 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的; 57 | 3. 隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的 58 | 4. 持久性(Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。 59 | 60 | ## 隔离级别 61 | 1. READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。 62 | 2. READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生 63 | 3. REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生 64 | 4. SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读 65 | 66 | ## 不同隔离级别存在的问题 67 | 1. 脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的 68 | 2. 不可重复读(Unrepeatable read): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读 69 | 3. 幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录。 70 | 71 | ## 左右联接 72 | - inner join: 只保留两表完全匹配的结果集 73 | - left join: 返回左表所有的行,右表中没有返回为null 74 | - full join: 全外联接,返回左表和右表中所有没有匹配的行。mysql不支持 full join,使用left join union right join来实现。 75 | 76 | ## 主键 77 | - 唯一且非空。 78 | - 一个表有且只能由一个主键约束。 79 | - 创建主键会自动创建对应的索引,同样删除主键,对应的索引也会被删除。 80 | ## 外键 81 | - 如果定义了外键约束,主表中没有的数据在子表中是不可以被使用的。 82 | - 主表中的记录被子表引用,是不可以被删除的。 83 | ## 查询 84 | - 分组查询:count() sum() max() min() avg() 85 | - 聚合查询:7种 A B A∪B A∩B A - A∩B B - A∩B A∪B - A∩B 86 | - 左连接: A - A∩B 右连接:B - A∩B 内连接:A∩B 87 | ## 悲观锁和乐观锁 88 | 1. 悲观锁: 每次去拿数据时都认为别人会修改,所以每次在拿数据的时候都会上锁。悲观锁由数据库自己实现,共享锁和排他锁是悲观锁的不同实现。悲观锁的缺点:效率低,并行差,增加死锁的概率。 89 | 2. 乐观锁:每次去拿数据都认为别人不会修改,所以不会上锁。乐观锁适用于读多,写少的场景。乐观锁常见的实现方式:版本号机制和CAS自旋算法。乐观锁的缺点:ABA问题,循环时间长开销大,只能保证一个共享变量的原子操作。 90 | 91 | ## 什么时候不应该创建索引 92 | 1. where条件里用不到的字段 93 | 2. 频繁更新的字段 94 | 3. 表记录太少<300W 95 | 4. 重复且平均的表字段 96 | 97 | ## explain 查看执行计划 98 | - 使用explain关键字可以模拟优化器执行sql查询语句,从而知道mysql是如何处理sql语句的。 99 | 100 | ## 聚簇索引 101 | - 聚簇索引的叶子节点都是数据节点。 102 | - 非聚簇索引的叶子节点是索引节点,有指向对应数据块的指针。 103 | 104 | ## MySQL优化 105 | - explain 106 | - extra中显示file sort进行了文件排序,提示建立索引。 107 | - using index condition使用了索引但是,进行了回表查询。 108 | - show profile 109 | - SQL语句优化 110 | - 模糊查询like% 111 | - 避免使用select * 112 | - insert尽量使用批量查询 113 | - 字符串单引号 114 | - left join对右边的数据建立索引 115 | - 范围查询右边的列索引失效 116 | - 不等号全表查询 117 | - is null或者is not null都无法使用索引 118 | 119 | ## read view 120 | - 快照读时产生的读视图 121 | - 在RC,每个快照读都生成最新的read view 122 | - 在RR,同一事务在第一个快照读时创建read view 123 | - RC读提交,可以读到最新的commit。 124 | - RR可重复读,读的是快照版。 125 | 126 | ## 行锁 127 | - 记录锁 record lock 128 | - 间隙锁 gap lock 129 | - 临键锁 next-key lock 130 | ## SQL优化 131 | - 对where和order by的列建立索引 132 | - 避免在where子句中对空值进行不等于判断 133 | - 避免在where子句中使用or连接,否则会使引擎放弃索引而进行全表扫描 134 | - 少用in和not in 135 | - 少用like查询 136 | - 避免在where中使用函数操作 137 | - 不要使用select 138 | - 尽量避免大事务操作 139 | - join字段提前加上索引 140 | 141 | ### 关键字 142 | - group by分组 143 | - having作用于组 144 | - order by对某一列进行排序 145 | - where后不能有聚合 146 | - limit row: offset 147 | - 7个关键字执行顺序: 148 | - from 149 | - where 150 | - group by 151 | - having 152 | - select 153 | - order by 154 | - limit 155 | 156 | ## 最左前缀法则(理解成爬楼梯) 157 | 158 | [视频辅助](https://www.bilibili.com/video/BV1zJ411M7TB?p=55) 159 | 160 | - sql查询条件中需要包含复合索引中的最左列,不能跳跃索引,否则索引失效。查询条件在where中出现的顺序没关系,只要按照最左前缀原则出现了,就会走索引。如果跳跃了索引,查询条件中满足最左前缀的部分走索引,到跳跃的部分时索引失效。 161 | 162 | ## 索引失效 163 | 164 | 1. 范围查询后其右边的列,索引失效。即索引某个字段使用了范围查询,他右边的索引将不再走索引。 165 | 2. 在索引列上进行运算操作,索引失效。(子串匹配查询) 166 | 3. 字符串不加单引号,索引失效。 167 | 4. 用or分割的条件,如果or前的条件中的列有索引,而后面的列中没有索引,那么涉及的索引都不会被用到。 168 | 5. 以%开头的like模糊查询,索引失效。 169 | 6. 如果mysql评估全表扫描更快,索引失效。 170 | 7. is NULL, is NOT NULL, 有时索引失效。 171 | 8. in走索引,而not in索引失效。 172 | 173 | ## 优化 174 | 175 | 1. 使用索引。 176 | 2. 根据sql实际解析的顺序,调整索引的顺序。 177 | 3. 尽量使用覆盖索引,避免select。覆盖索引是指只出现在索引中的字段。 178 | 4. 尽量使用复合索引,而少使用单列索引。 179 | 5. 优化insert。一次插入多条数据。事务改为手动提交,分段提交。按主键顺序插入。 180 | 6. 优化order by尽量使用using index 而避免使用filesort 181 | 182 | 183 | ## mysql分库分表 184 | 185 | [视频辅助](https://www.bilibili.com/video/BV1bE411d7FF?p=14) 186 | 187 | 主从集群也就是读写分离,读写分离只是分担了访问的压力,存储的压力并没有解决。数据库集群环境后都是多台slave,基本满足了读取操作,但是频繁写入堆master性能影响比较大,这个时候,单库并不能解决大规模并发写入的问题。 188 | 189 | 分库分表带来的问题:1. 联合查询问题,join不再适用。2.事务问题,变成了分布式事务。好处:减少大量数据写入时锁对查询的影响。按照存储类别分:用户库,业务库,内存库,图片库,日志库,统计库。 190 | 191 | 分表:垂直分表和水平分表。解决单张表记录太多的问题。切分策略和导航路由。单表的容量超过500W时建议水平拆分。不到最后一步,不要轻易进行水平分表。 192 | 193 | 开源方案:1. msyql fabric 2.atlas 3.TDDL 4.mysql proxy 小巧精干,能力有限。+ master/slave 构成一个简单版的读写分离和负载均衡。 194 | 195 | 主从复制,读写分离---> 垂直分库(每个库可以带slave)--->分区---->水平分表。中间各种通信,调度,维护和编码要求更高。 196 | 197 | ## 主从复制 198 | - mysql复制是异步并串行化的。原理:slave从master读取binlog进行数据同步。主要分为3步: 199 | 1. master将改变记录到二进制日志(binary log)。这些记录过程叫做二进制日志事件binary log events。 200 | 2. slave从master的binary log events拷贝到它的中继日志relay log 201 | 3. 重做中继日志中的事件,将改变应用到自己的数据库中。 202 | 203 | ## 锁机制 204 | 205 | 锁可以对有限的资源进行保护,解决隔离和并发的矛盾。通过锁机制可以实现事务的隔离性要求,使得事务可以并发地工作。 206 | 207 | 按操作分:读锁(共享锁) 写锁(排他锁) 208 | 209 | 按粒度分:行锁(偏写)表锁(偏读) 210 | 211 | 锁使用的考虑点:开销,加锁速度,死锁,粒度,并发性能。 212 | 213 | 行锁:innoDB 开销大,加锁慢,会出现死锁,粒度小,锁冲突概率低,并发高。 214 | 215 | 表锁:myisam 开销小,加锁快,无死锁,粒度大,锁冲突概率高,并发性低。 216 | 217 | 行锁的三种算法:1. record lock 2.gap lock 3. next-key lock 218 | 219 | 锁带来的三种问题:1. 脏读 2. 不可重复读 3. 丢失更新 220 | 221 | 意向锁是将锁定的对象分为多个层次,对最细粒度的对象进行上锁,首先需要对粗粒度的对象上锁。 222 | 223 | ## 视图 224 | - 视图的主要用途是被用作一个抽象装置,只需要按照视图定义来取数据或更新数据。 225 | - 视图是一种虚拟的表,由表中某些字段组成。 226 | 227 | ## 主键索引与唯一索引的区别 228 | 1. 主键是一种约束,唯一索引是一种索引。两者在本质上是不同的。 229 | 2. 主键创建后一定包含一个唯一索引,但是唯一索引不一定是主键。 230 | 3. 主键不允许为空,而唯一索引可以为空。 231 | 4. 一个表最多只能创建一个主键,但是可以创建多个唯一索引。 232 | 233 | ## 索引的优缺点 234 | 235 | - 索引是帮助mysql高效获取数据的数据结构。 236 | - 优点:提高数据查询的效率,降低数据库的IO成本。通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗。 237 | - 缺点:实际上索引也是一张表,也需要占用空间。虽然索引大大提高了查询的速度,但是也降低了更新表的速度。索引是不断完善的,需要根据实际需求进行优化调整。 -------------------------------------------------------------------------------- /Leetcode/README.md: -------------------------------------------------------------------------------- 1 | # Leetcode 面试算法常考题 2 | - 链表 3 | - 数组 4 | - 哈希表 5 | - DFS 6 | - BFS 7 | - 双指针 8 | - 排序(归并,快排,堆排) 9 | - 二分查找 10 | - 回溯 11 | - 动态规划(一维,树型) 12 | - 并查集 13 | - 拓扑排序 -------------------------------------------------------------------------------- /Leetcode/binary_search.md: -------------------------------------------------------------------------------- 1 | # binary search 2 | - This algorithm very fast. It can reach O(logN). 3 | - If you want to use this algorithm, you need to find some monotonic trait. 4 | - One side can satisfy the condition, another side cannot. 5 | - In most situation the array is sorted or we need to sorted firstly. 6 | ## 35. Search Insert Position 7 | ```c 8 | class Solution { 9 | public: 10 | int searchInsert(vector& nums, int target) { 11 | // binary search 12 | if(nums.empty() || nums.back() < target) return nums.size(); 13 | int n = nums.size(); 14 | int l = 0, r = n-1; 15 | while(l < r){ 16 | int mid = (l + r)/2; 17 | // we need to find the first position which greater than or equals target 18 | // so we use this template 19 | // but we can not deal with the value bigger than the maximum value of the array 20 | if(nums[mid] >= target) { 21 | r = mid; 22 | }else{ 23 | l = mid + 1; 24 | } 25 | 26 | } 27 | return l; 28 | } 29 | }; 30 | ``` 31 | ## 875. Koko Eating Bananas 32 | 33 | ```c 34 | class Solution { 35 | public: 36 | int minEatingSpeed(vector& piles, int h) { 37 | // from k all bananas can finish eating under h hours 38 | //[ xxxxxyyyyyy] find the position of the first y 39 | int n = piles.size(); 40 | int l = 1, r = 1e9; 41 | while(l < r){ 42 | int mid = l + (r - l)/2; 43 | if(check(mid, piles) <= h) { // satisfy our condition 44 | r = mid; 45 | }else{ 46 | l = mid + 1; 47 | } 48 | } 49 | return l; 50 | } 51 | int check(int m, vector &piles) { 52 | int total_hours = 0; 53 | for(auto x: piles){ 54 | total_hours += (x + m - 1)/m; 55 | } 56 | return total_hours; 57 | } 58 | }; 59 | ``` -------------------------------------------------------------------------------- /Leetcode/dfs.md: -------------------------------------------------------------------------------- 1 | # DFS 2 | ## 112. Path Sum 3 | ```c 4 | /** 5 | * Definition for a binary tree node. 6 | * struct TreeNode { 7 | * int val; 8 | * TreeNode *left; 9 | * TreeNode *right; 10 | * TreeNode() : val(0), left(nullptr), right(nullptr) {} 11 | * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} 12 | * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} 13 | * }; 14 | */ 15 | class Solution { 16 | public: 17 | bool ans; 18 | bool hasPathSum(TreeNode* root, int targetSum) { 19 | ans = false; 20 | dfs(root, targetSum); 21 | return ans; 22 | } 23 | void dfs(TreeNode* root, int t){ 24 | if(!root) return; 25 | if(!root->left && !root->right && t == root->val){ 26 | ans = true; 27 | return; 28 | } 29 | if(root->left) dfs(root->left, t - root->val); 30 | if(root->right) dfs(root->right, t - root->val); 31 | } 32 | }; 33 | ``` 34 | ## 200. Number of Islands 35 | 36 | ```c 37 | class Solution { 38 | public: 39 | int st[305][305]; 40 | int m, n; 41 | int dx[4]={0, 0, 1, -1}; 42 | int dy[4]={1, -1, 0, 0}; 43 | void dfs(int i, int j, vector>& g){ 44 | if(i < 0 || j < 0 || i >= m || j >= n) return; 45 | if(g[i][j] == '0' || st[i][j]) return; 46 | st[i][j] = 1; 47 | for(int u = 0; u < 4; u++) { 48 | int a = i + dx[u]; 49 | int b = j + dy[u]; 50 | dfs(a, b, g); 51 | } 52 | } 53 | int numIslands(vector>& grid) { 54 | // attention: the matrix is not int but char 55 | // time complexity O(M*N*M*N) 56 | memset(st, 0, sizeof(st)); 57 | m = grid.size(); 58 | if(!m) return 0; 59 | n = grid[0].size(); 60 | 61 | int res = 0; 62 | for(int i = 0; i < m; i++) { 63 | for(int j = 0; j < n; j++) { 64 | if(grid[i][j]=='1' && !st[i][j]) { 65 | dfs(i, j, grid); 66 | res++; 67 | } 68 | } 69 | } 70 | return res; 71 | } 72 | }; 73 | ``` -------------------------------------------------------------------------------- /Leetcode/dynamic_programming.md: -------------------------------------------------------------------------------- 1 | # Dynamic Programming 2 | 3 | ## 62. Unique Paths 4 | ```c 5 | class Solution { 6 | public: 7 | int dp[110][110]; 8 | int uniquePaths(int m, int n) { 9 | memset(dp, 0, sizeof(dp)); 10 | // first column 11 | for(int i = 0; i < m; i++) dp[i][0] = 1; 12 | // first row 13 | for(int j = 0; j < n; j++) dp[0][j] = 1; 14 | 15 | for(int i = 1; i < m; i++) { 16 | for(int j = 1; j < n; j++) { 17 | dp[i][j] = dp[i-1][j] + dp[i][j-1]; 18 | } 19 | } 20 | return dp[m-1][n-1]; 21 | } 22 | }; 23 | ``` -------------------------------------------------------------------------------- /Leetcode/hashmap.md: -------------------------------------------------------------------------------- 1 | ## 1 2 | ```c 3 | class Solution { 4 | public: 5 | vector twoSum(vector& nums, int target) { 6 | map mp; 7 | for(int i = 0; i < nums.size(); i++) { 8 | int t = target - nums[i]; 9 | if(mp.find(t) != mp.end()){ 10 | return {i, mp[t]}; 11 | } 12 | mp[nums[i]] = i; 13 | } 14 | return {-1, -1}; 15 | } 16 | }; 17 | ``` 18 | ## 49 19 | ```c 20 | class Solution { 21 | public: 22 | vector> groupAnagrams(vector& strs) { 23 | // 排序 + 哈希表 24 | map> mp; 25 | vector> res; 26 | for(auto y: strs){ 27 | 28 | string temp = y; 29 | sort(y.begin(), y.end()); 30 | mp[y].push_back(temp); 31 | 32 | } 33 | for(auto [k,v]: mp){ 34 | res.push_back(v); 35 | } 36 | return res; 37 | } 38 | }; 39 | ``` 40 | 41 | ## 128 42 | - Tips: hash table 43 | - Ideas of solving a problem 44 | 1. The big obstacle is keep the complexity under O(N). 45 | 2. We can store the value in hash table, however we need to loop ever value and find the longest sub_array. 46 | 3. There is a way to optimize the inner loop. If we check every start value greater than that queried, we can reduce many no meaningful query. 47 | ```c 48 | class Solution { 49 | public: 50 | int longestConsecutive(vector& nums) { 51 | map mp; 52 | for(auto x: nums){ 53 | mp[x]++; 54 | } 55 | int res = 0; 56 | int temp = -1e9; 57 | for(auto [k, v]: mp){ 58 | if(k < temp) continue; 59 | temp = k; 60 | int local_res = 0; 61 | while(mp.count(temp)){ 62 | temp++; 63 | local_res++; 64 | } 65 | res = max(res, local_res); 66 | } 67 | return res; 68 | } 69 | }; 70 | ``` 71 | ## 560. Subarray Sum Equals K 72 | ``` 73 | class Solution { 74 | public: 75 | int subarraySum(vector& nums, int k) { 76 | // by means of hash table 77 | // how to caculate the subarray? we always use total sum subtract prefix check if the result equals k 78 | // if we change our mind to use total sum subtract k and then check the answer whether exist in hashtable 79 | unordered_map mp; 80 | int prefix_sum = 0; 81 | int res = 0; 82 | // 0 is a special prefix 83 | mp[0] = 1; 84 | for(auto x: nums){ 85 | prefix_sum += x; 86 | if(mp.find(prefix_sum - k) != mp.end()){ 87 | res += mp[prefix_sum - k]; 88 | } 89 | mp[prefix_sum]++; 90 | } 91 | return res; 92 | } 93 | }; 94 | ``` -------------------------------------------------------------------------------- /Leetcode/linked_list.md: -------------------------------------------------------------------------------- 1 | # Linked List 2 | ## 206 3 | ```c 4 | /** 5 | * Definition for singly-linked list. 6 | * struct ListNode { 7 | * int val; 8 | * ListNode *next; 9 | * ListNode() : val(0), next(nullptr) {} 10 | * ListNode(int x) : val(x), next(nullptr) {} 11 | * ListNode(int x, ListNode *next) : val(x), next(next) {} 12 | * }; 13 | */ 14 | class Solution { 15 | public: 16 | ListNode* reverseList(ListNode* head) { 17 | ListNode* prev = nullptr; 18 | ListNode* nxt = nullptr; 19 | while(head != nullptr){ 20 | nxt = head->next; 21 | head->next = prev; 22 | prev = head; 23 | head = nxt; 24 | } 25 | return prev; 26 | } 27 | }; 28 | ``` -------------------------------------------------------------------------------- /Leetcode/monotonic_stack.md: -------------------------------------------------------------------------------- 1 | ## 239. Sliding Window Maximum 2 | - tips: monotonic decrease stack 3 | - if I want get the result of sliding window minimum, how do you resolve it? 4 | ``` 5 | class Solution { 6 | public: 7 | vector maxSlidingWindow(vector& nums, int k) { 8 | // keep all the window elements in a stack 9 | // maintain this stack to monotonic decrease 10 | // after move the window 11 | // all the maximum element still in the head 12 | deque dq; 13 | int n = nums.size(); 14 | vector res; 15 | for(int i = 0; i < n; i++) { 16 | 17 | // 1. pop outdated data 18 | if(!dq.empty() && dq.front() <= i-k){ 19 | dq.pop_front(); 20 | } 21 | // 2. check insert position 22 | while(!dq.empty() && nums[dq.back()] <= nums[i]){ 23 | dq.pop_back(); 24 | } 25 | // why we need to insert index rather than value? 26 | // because we need to decide which element could evict based on the index 27 | dq.push_back(i); 28 | 29 | // 3. if there are k elements 30 | if(i >= k - 1) { 31 | res.push_back(nums[dq.front()]); 32 | } 33 | 34 | } 35 | return res; 36 | } 37 | }; 38 | ``` 39 | ## 739. Daily Temperatures 40 | ```c 41 | class Solution { 42 | public: 43 | vector dailyTemperatures(vector& t) { 44 | // right to left maintain a stack monotonic decrease 45 | stack stk; 46 | int n = t.size(); 47 | vector res(n); 48 | for(int i = n - 1; i >= 0; i -- ) { 49 | // insert new value 50 | while (!stk.empty() && t[stk.top()] <= t[i]){ 51 | stk.pop(); 52 | } 53 | // get value 54 | if(stk.empty()) res[i] = 0; 55 | else res[i] = stk.top() - i; 56 | // insert value 57 | stk.push(i); 58 | } 59 | return res; 60 | } 61 | }; 62 | ``` -------------------------------------------------------------------------------- /Leetcode/two_pointers.md: -------------------------------------------------------------------------------- 1 | # Two Pointers 2 | - another name is sliding window. 3 | ## 10 4 | ```c 5 | class Solution { 6 | public: 7 | int maxArea(vector& h) { 8 | // greedy + two pointers 9 | int l = 0, r = h.size() - 1; 10 | int n = h.size(); 11 | int res = 0; 12 | while(l < r){ 13 | // choosing the lowest side and moving it closer another side 14 | res = max(res, (r - l )* min(h[l], h[r])); 15 | if(h[l] > h[r]){ 16 | r--; 17 | }else{ 18 | l++; 19 | } 20 | } 21 | return res; 22 | } 23 | }; 24 | ``` 25 | ## 3 26 | ```c 27 | class Solution { 28 | public: 29 | int lengthOfLongestSubstring(string s) { 30 | // must keep array consective 31 | // use hash table record the repeat value 32 | // If a char is repeat, we will move the start position until there is no repeat char 33 | int hashtable[256]; 34 | memset(hashtable, 0, sizeof(hashtable)); 35 | int start = 0, len = 0; 36 | int start_pos = 0; 37 | int i = 0; 38 | for(auto c: s){ 39 | hashtable[c ]++; 40 | while(hashtable[c] > 1){ 41 | hashtable[s[start_pos]] --; 42 | start_pos++; 43 | } 44 | // If code arrive here, keep no repeat 45 | if(i - start_pos + 1 > len) { 46 | start = start_pos; 47 | len = i - start_pos + 1; 48 | } 49 | i++; 50 | } 51 | return len; 52 | 53 | } 54 | }; 55 | ``` 56 | ## 438 57 | - sliding window has some property. 58 | - the sub_array length is fixed. 59 | - we only need to do two things: add new element and delete old element. 60 | ```c 61 | class Solution { 62 | public: 63 | vector findAnagrams(string s, string p) { 64 | // 1. same char type 65 | // 2. same appear times 66 | int valid_type = 0; 67 | unordered_map mp, smp; 68 | for(auto c: p){ 69 | mp[c]++; 70 | } 71 | 72 | vector res; 73 | // length must keep the same 74 | // check every sub array that length equals p's length 75 | int m = p.size(); 76 | int n = s.size(); 77 | int valid = mp.size(); 78 | for(int i = 0; i < n; i++) { 79 | // length is constant variable 80 | // add new char 81 | smp[s[i]]++; 82 | if(smp[s[i]] == mp[s[i]]) { 83 | valid_type++; 84 | } 85 | // delete old char 86 | if(i >= m){ 87 | if(smp[s[i-m]] == mp[s[i-m]]){ 88 | valid_type--; 89 | } 90 | smp[s[i-m]]--; 91 | } 92 | if(valid_type == valid){ 93 | res.push_back(i-m+1); 94 | } 95 | // cout< 打开文件表是PCB进程控制块的一部分,进程控制块是个task_struct结构体包括: 533 | - 进程号 534 | - 打开文件描述符表 535 | - 等等 536 | 537 | ## 文件系统 538 | 539 | 文件系统应具有以下功能 540 | 541 | 1. 完成文件存储系统的管理,即分配空间和回收空间。 542 | 2. 实现文件名到物理地址的映射。 543 | 3. 实现文件和目录的建立,读写管理。 544 | 4. 向用户提供有关文件和目录操作的接口。 545 | 546 | ## 文件系统层次结构 547 | 548 | - 用户空间 549 | - 内核空间 550 | - 虚拟文件系统VFS 551 | - 块设备文件系统、内存文件系统、闪存文件系统 552 | - 页缓存 Page Cache 553 | - 块设备层 554 | - 块缓存 555 | - IO调度器 556 | - 块设备驱动程序 557 | - 硬件 558 | - 机械硬盘、固态硬盘 559 | - 闪存 560 | 561 | ## IO调度算法 562 | - NOOP:FIFO 563 | - CFQ完全公平:该算法为每个进程分配一个时间窗口,在时间窗口内允许进程发出IO请求 564 | - Deadline:每个IO请求有一个最后执行期限。 565 | 566 | ## 分页管理 567 | 568 | - 从进程虚拟地址空间映射到内存地址空间。 569 | - 页面大小相同,内存和进程都进行划分。由于页面较小,每个进程平均产生半个页的内部碎片。 570 | - 逻辑地址:页号 + 页内偏移 571 | - 地址转换:逻辑地址---快表TLB---页表----物理地址 572 | - 两次访问主存,如果使用了快表就可以实现只访问一次主存拿数据和指令 573 | 574 | ### 虚拟文件系统VFS 575 | 576 | - 对用户友好,屏蔽底层细节。 577 | - 管理和存储文件 578 | - 文件系统的主要功能:安装,卸载,创建,删除,读写。 579 | - 文件系统的结构分为三层:用户层,内核层,硬件层。 580 | - 用户层的函数:fopen fclose fwrite fflush fread fseek 581 | - 内核层函数:mount unmount reade write open lseek fsync fdatasync 582 | - 内核中有个页缓存:page cache 583 | 584 | ## mount挂载 585 | 586 | - 一个存储设备上的文件系统,只有挂载到内存中目录树的某个目录下,进程才能访问这个文件系统。 587 | 588 | ## linux上运行可执行文件的过程 589 | 590 | - a.out是ELF文件格式 591 | - linux每个程序都会运行在一个进程上下文中,这个进程上下文有自己的虚拟地址空间。这个进程是由shell进程fork出来的子进程。 592 | 593 | ## 内存管理 594 | 595 | - 分页内存管理 596 | - 内存和进程划分为大小相等的页面,通常是4k,页面大小是对用户透明的,分页是从计算机的角度去考虑的,为了减少内存碎片,由于页面较小,每个进程平均产生半个内存碎片。 597 | - 分段 598 | - 分段是从用户的角度考虑,为了满足编程的方便,信息保护和共享等方面的要求。 599 | - 段页 600 | - 段号+页号+页内偏移 601 | 602 | ## 虚拟地址空间 603 | 604 | - 基于局部性原理,使用页面置换算法,是内存的可用空间更大。 605 | - 进程隔离 606 | - 内存的管理 607 | - 不连续的空间 608 | 609 | ## 读写文件的方式 610 | 611 | - 调用内核提供的系统调用read/write 612 | - 调用glibc库封装的标准IO流函数fread/fwrite。用户空间有缓冲区,能减少系统调用的次数,提高性能。 613 | - 创建基于文件的内存映射,把文件的一个区间映射到进程的虚拟地址空间,然后直接读内存。mmap避免系统调用,性能最高。 614 | 615 | ## IO控制方式 616 | 617 | - 轮询 618 | - 中断 CPU字节干预IO 619 | - 直接存储器访问DMA 一个块数据 干预一次 620 | - 通道 一组块数据 干预一次 621 | - 目的:减少CPU对IO的干预,提高其处理数据的效率。 622 | 623 | ## 中断机制 624 | 625 | [参考博客](https://blog.csdn.net/u011387521/article/details/106632326) 626 | 627 | - 外设速度远远慢于CPU的速度,所以需要外设资源准备好以后,利用中断机制主动通知操作系统。 628 | - 内部中断/异常:CPU执行指令期间检测到非法的条件(除零,地址访问越界) 629 | - 硬中断/外中断/中断:IO中断,时钟中断,信号中断产生的时间不确定。 630 | - 软中断:应用程序使用系统调用而引发的事件。 631 | - 中断描述符表 632 | - os中预先设置一些中断处理函数,当CPU接收到中断时,会根据中断号去查对应的处理函数,中断向量表就是记录中断号与中断函数映射关系的表。 633 | - 中断机制是为了弥补CPU速度和外设速度数量级差异的机制,它的核心是中断向量表。 634 | 635 | ## VFS中的数据结构 636 | - 超级块 super block 637 | - 索引节点 iNode 638 | - 目录项 dentry 639 | - 文件 file 640 | 641 | ## 超级块Super Block 642 | - 描述文件系统的总体信息,挂载文件系统时在内存中创建超级块的副本。 643 | - 一个文件系统只有挂载到内存中目录树的一个目录下,进程才能访问这个文件系统。 644 | - 每次挂载文件系统,VFS会创建一个挂载描述符:mount结构体,并读取文件系统的超级块,在内存中创建超级块的副本。 645 | - 每种文件系统的超级块的格式不同,需要向VFS注册文件系统类型,并实现mount方法用来读取和解析超级块。 646 | - ext4----register----mount-----super block 647 | 648 | ## 索引节点iNode 649 | - 每个文件对应一个索引节点,每个索引节点有个唯一的编号。当内核访问存储设备上的一个文件时,会在内存中创建索引节点的副本:结构体inode 650 | - 一个块由多个扇区组成4k=8sector 651 | - inode文件的元信息:创建者,创建日期,文件的大小。 652 | - 查看系统中iNode的用量df -i 653 | - df -h 输出文件系统分区情况 654 | - du -h 文件、目录 655 | 656 | ## 目录项 657 | - 文件系统把目录看作文件的一种类型,目录的数据是由目录项组成,每个目录项存储一个子目录或文件的名称以及对应的索引节点号。 658 | 659 | ## 打开文件表 660 | - 当进程打开一个文件的时候,VFS会创建文件的一个打开实例:file结构体,然后在进程的打开文件描述符表中分配一个索引,这个索引被称为文件描述符,最后把文件描述符和file结构体的映射添加到文件描述符表中。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | GitHub 3 | 4 | 5 | 6 |

7 | 8 | # C/C++基础面试问题 9 | ## 类中成员函数有两个`void hello()`和`void hello() const`,怎么在调用是区分调的哪一个? 10 | 根据创建的实例对象而决定,如果实例对象是const则自动调第二个,如果非const调用第一个。 11 | 12 | ## 析构函数是否可以重载?析构函数是否可以是虚函数?如果不是虚函数会产生什么问题?举个例子 13 | 析构函数不可以重载。 14 | 析构函数不重载,可能导致父类资源无法释放。 15 | 构造函数的调用顺序是,先构造父类,再构造子类。析构函数的顺序是反过来的,先析构子类,再析构父类。 16 | ```c++ 17 | #include 18 | 19 | class Base { 20 | public: 21 | Base() { 22 | std::cout << "Base constructor called." << std::endl; 23 | } 24 | 25 | // 注意:这里没有将析构函数声明为虚函数 26 | ~Base() { 27 | std::cout << "Base destructor called." << std::endl; 28 | } 29 | }; 30 | 31 | class Derived : public Base { 32 | public: 33 | Derived() { 34 | std::cout << "Derived constructor called." << std::endl; 35 | } 36 | 37 | ~Derived() { 38 | std::cout << "Derived destructor called." << std::endl; 39 | } 40 | }; 41 | 42 | int main() { 43 | Base* ptr = new Derived(); // 创建一个Derived对象,并用Base指针指向它 44 | delete ptr; // 通过基类指针删除对象 45 | 46 | return 0; 47 | } 48 | // 如果析构不是虚函数,那么只会释放父类的资源,而泄露了子类的资源 49 | ``` 50 | 51 | ## 什么场景需要用dynamic_cast? 52 | 如果想要将父类转换为子类,需要父类中至少有一个虚函数,dynmic_cast依赖于运行时类型信息(RTTI),因此需要虚函数的存在。 53 | ```c++ 54 | 55 | ## #include 56 | #include 57 | 58 | class Base { 59 | public: 60 | virtual ~Base() {} // 基类需要至少一个虚函数(通常是析构函数),以支持RTTI 61 | }; 62 | 63 | class Derived1 : public Base { 64 | public: 65 | void derived1Function() { 66 | std::cout << "Derived1 function called." << std::endl; 67 | } 68 | }; 69 | 70 | class Derived2 : public Base { 71 | public: 72 | void derived2Function() { 73 | std::cout << "Derived2 function called." << std::endl; 74 | } 75 | }; 76 | 77 | int main() { 78 | Base* basePtr1 = new Derived1(); 79 | Base* basePtr2 = new Derived2(); 80 | 81 | // 尝试将 basePtr1 转换为 Derived1* 82 | Derived1* derivedPtr1 = dynamic_cast(basePtr1); 83 | if (derivedPtr1) { 84 | derivedPtr1->derived1Function(); 85 | } else { 86 | std::cout << "Conversion to Derived1 failed." << std::endl; 87 | } 88 | 89 | // 尝试将 basePtr2 转换为 Derived1*(应该失败) 90 | Derived1* derivedPtr2 = dynamic_cast(basePtr2); 91 | if (derivedPtr2) { 92 | derivedPtr2->derived1Function(); 93 | } else { 94 | std::cout << "Conversion to Derived1 failed (as expected)." << std::endl; 95 | } 96 | 97 | // 清理内存 98 | delete basePtr1; 99 | delete basePtr2; 100 | 101 | return 0; 102 | } 103 | ``` 104 | 105 | ## 什么是柔性数组?有什么用处? 106 | 107 | ## 什么是大小端?如何用代码判断大小端? 108 | 109 | ## 如何定位内存泄露问题? 110 | 111 | ## 如何使用perf分析程序性能? 112 | 113 | ## C/C++内存模型 114 | 115 | ## 什么是内存对齐?为什么内存要对齐? 116 | 117 | ## 提高C/C++程序性能的技巧? 118 | 119 | ## C/C++互相调用通常在头文件中做什么处理? 120 | 121 | ## static在类成员变量和类成员函数中有什么作用? 122 | 静态成员函数只能访问静态成员变量。静态成员变量在所有对象中共享。 123 | ## 如何实现类的单例模式? 124 | 125 | ## 如何判断链表是否有环?如何找到链表的中点?如何对链表排序?如何对二叉树做层序遍历?给定四个坐标如何判断是否是正方形? 126 | 127 | ## 如何提高TLB缓存命中率? 128 | 129 | ## 什么时候需要用虚函数? 130 | 131 | ## 什么时候需要用智能指针? 132 | 想要malloc和new时,不用显式分配内存和管理内存,就可以使用智能指针。 133 | 134 | ## 移动语义与完美转发 135 | 136 | ## C++20协程 137 | 138 | ## 源文件生成可执行文件的过程 139 | 140 | ## 内存管理?如何避免内存碎片? 141 | 142 | ## 线程上下文切换会做哪些工作? 143 | 144 | ## 虚拟地址空间到物理地址空间的流程说一下 145 | 146 | ## 如何减小锁的粒度?如何避免死锁? 147 | 148 | ## 如何保证程序是线程安全的? 149 | 150 | ## 如何实现一个线程安全的延迟队列? 151 | 152 | ## 如何实现原子操作CAS 153 | 154 | ## 你技术上最大的优势是什么?你最擅长什么技术?C/C++功能实现,算法与数据结构,linux内核,操作系统,还是协作沟通,英语等? -------------------------------------------------------------------------------- /Redis/README.md: -------------------------------------------------------------------------------- 1 | # Redis 2 | - C语言编写的基于内存可持久化的key-value内存数据库。 3 | ## 理论篇 4 | ### 参考视频 5 | - [黑马程序员2022Redis](https://www.bilibili.com/video/BV1cr4y1671t?p=99&vd_source=e9f1ced96b267a4bc02ec41ca31d850a) 6 | ### Redis的优点 7 | - 数据结构丰富 8 | - 持久化 9 | - 支持事务 10 | - 分布式锁 11 | - 支持主从复制 12 | - 读写性能优异 13 | ### Redis的缺点 14 | - 数据库容量受物理内存的限制 15 | - 不能用作海量数据的高性能读写 16 | ### 单线程的好处 17 | - 避免多线程的竞争条件和上下文切换,不用考虑各种锁的问题 18 | - 使用IO多路复用,非阻塞IO 19 | ### 数据类型 20 | 1. string 21 | 2. list 22 | 3. set 23 | 4. hash 24 | 5. zset 25 | ### 字符串 26 | - Redis使用SDS(simple dynamic string)简单动态字符串作为字符串表示 27 | - 常数复杂度获取字符串长度。 28 | - 杜绝缓冲区溢出。 29 | - 减少修改字符串长度时所需的内存重分配次数。 30 | - 二进制安全。 31 | - 兼容部分C字符串函数 32 | - Redis的字典使用哈希表作为底层实现 33 | - Redis使用跳表作为有序集合键的底层实现之一 34 | - Redis只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构 35 | 36 | ### 过期删除机制 37 | 1. 定期删除:redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。 38 | 2. 惰性删除:查询的时候再删。 39 | 40 | ## 内存淘汰机制 41 | 1. volatile-lru 42 | 2. volatile-ttl 43 | 3. volatile-random 44 | 4. allkeys-lru 45 | 5. allkeys-random 46 | 6. no-eviction 47 | 48 | ### 持久化 49 | 1. RDB快照(默认) 50 | 2. AOF日志追加 51 | - RDB是一个经过压缩的二进制文件,SAVE命令阻塞服务器去保存,BGSAVE不阻塞服务器,而是由子进程进行保存,可以设置定期保存的条件。RDB文件记录的是当前状态有哪些数据。 52 | - AOF记录的是操作的命令。AOF和RDB同时开启时,AOF优先级更高。AOF恢复时需要重新执行命令,比较耗时,因此提供了AOF重写操作。 53 | - Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 `aof-use-rdb-preamble` 开启)。 54 | 55 | 56 | ### RDB的优缺点 57 | - 优点:适合大规模的数据恢复,对数据的完整性要求不高。 58 | 1. 性能好。fork子进程来完成写操作。 59 | 2. 恢复快。直接解析RDB二进制文件。 60 | - 缺点:最后一次持久化的数据可能丢失,fork子进程占用内存空间。 61 | - 有数据丢失的风险。 62 | - 主程序有大量写入操作时,会触发copy on write,此时父子进程各持有独立的一份数据,大量写入会产生大量的分页错误。 63 | 64 | - 触发持久化的机制 65 | 1. sava配置中的条件 66 | 2. flushall 67 | 3. 退出redis 68 | - 恢复dump.rdb文件,只需将rdb文件放入redis启动目录下 69 | 70 | ### AOF的优缺点 71 | - AOF日志模式:将所有命令都记录下来,恢复的时候把这个文件全部再执行一遍。 72 | - 默认是不开启的,手动开启`appendonly yes` 73 | - 默认每秒修改存储一次,重启后生效。 74 | - 修复aof文件`redis-check-aof --fix appendonly.aof` 75 | - 优点:每次修改都同步,文件的完整性更好。每秒同步一次,可能会丢失一秒的数据。 76 | - 数据安全,无丢失。 77 | - 缺点:aof文件大小远远大于rdb,修复速度也比rdb慢。启动效率低。 78 | 79 | ### 缓存雪崩(redis宕机) 80 | 1. 缓存集中同时失效 81 | 2. 大量不同请求打到数据库 82 | - 缓存同一时间大面积的失效,所以后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。 83 | - 缓存雪崩:是指在某一时间段,缓存集中过期失效,redis宕机/断网。 84 | - 解决方案:搭建集群,限流降级,数据预热(把可能访问的数据预先加载到缓存)。 85 | - 解决办法: 86 | 1. 事前:尽量保证整个redis集群的高可用性,发现机器宕机尽快补上。 87 | 2. 事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉 88 | 3. 事后:利用redis持久化机制保存的数据尽快恢复缓存 89 | ### 缓存穿透(缓存没有,恶意) 90 | 1. 不存在的数据 91 | 2. 大量同一个请求打到数据库 92 | - 一般是黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。 93 | - 解决方案: 94 | 1. 布隆过滤器 95 | 2. 缓存空对象 96 | - 解决办法: 有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。 97 | 98 | ### 缓存击穿(缓存有) 99 | 1. 缓存过期 100 | 2. 大量同一(热点)请求打到数据库 101 | 102 | - 缓存击穿(量太大,缓存过期):高并发的热点key在过期的瞬间,持续的高并发直接请求数据库。 103 | - 解决方案:设置热点数据永不过期,加互斥锁,设二级缓存,保证过期时间不集中。 104 | - 使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。 105 | 106 | ### 数据一致性 107 | - 缓存和数据库之间要保证一致性要强调两点前提: 108 | 1. 缓存必须要有过期时间 109 | 2. 保证缓存和数据的最终一致性,强一致性不容易做到 110 | 111 | 为什么需要有过期时间? 112 | 113 | 首先对于缓存来说,当它的命中率越高的时候,我们的系统性能也就越好。如果某个缓存项没有过期时间,而它命中的概率又很低,这就是在浪费缓存的空间。而如果有了过期时间,且在某个缓存项经常被命中的情况下,我们可以在每次命中的时候都刷新一下它的过期时间,这样也就保证了热点数据会一直在缓存中存在,从而保证了缓存的命中率,提高了系统的性能。 114 | 115 | 设置过期时间还有一个好处,就是当数据库跟缓存出现数据不一致的情况时,这个可以作为一个最后的兜底手段。也就是说,当数据确实出现不一致的情况时,过期时间可以保证只有在出现不一致的时间点到缓存过期这段时间之内,数据库跟缓存的数据是不一致的,因此也保证了数据的最终一致性 116 | 117 | 为什么不追求强一致性? 118 | 119 | 这个主要是个权衡的问题。数据库跟缓存,以Mysql跟Redis举例,毕竟是两套系统,如果要保证强一致性,一般都要引入分布式一致性协议或者分布式锁等等,一是实现上有一定的难度,增加系统的复杂性,对性能会有一定的影响,如果真的需要强一致性,缓存这一层的必要性有待具体分析 120 | 121 | DB和缓存的读写顺序 122 | 123 | 经典的[Cache-Aside pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/cache-aside),这个方案的思路是: 124 | 125 | 1. 失效:程序先从缓存中读取数据,如果没有命中,则从数据库中读取,成功之后将数据放到缓存中 126 | 2. 命中:程序先从缓存中读取数据,如果命中,则直接返回 127 | 3. 更新:程序先更新数据库,再删除缓存 128 | 129 | 一般对于1和2两步中需要考虑的细节较少,主要是第三步的更新,如何确保更新的时候仍然可以确保缓存数据的一致性 130 | 131 | 一共有四种可能 132 | 133 | 1. 先更新缓存,再更新数据库 134 | 2. 先更新数据库,再更新缓存 135 | 3. 先删除缓存,再更新数据库 136 | 4. 先更新数据库,再删除缓存 137 | 138 | 1、先更新缓存,再更新数据库 139 | 不管是操作数据库还是操作缓存,都有失败的可能。如果我们先更新缓存,再更新数据库,假设更新数据库失败了,那数据库中就存的是老数据。当然你可以选择重试更新数据库,那么再极端点,负责更新数据库的机器也宕机了,那么数据库中的数据将一直得不到更新,并且当缓存失效之后,其他机器再从数据库中读到的数据是老数据,然后再放到缓存中,这就导致先前的更新操作被丢失了,因此这么做的隐患是很大的 140 | 141 | 从数据持久化的角度来说,数据库当然要比缓存做的好,我们也应当以数据库中的数据为主,所以需要更新数据的时候我们应当首先更新数据库,而不是缓存 142 | 143 | **2. 先更新数据库,再更新缓存** 144 | 145 | 主要有两个问题,首先是并发的问题:假设线程A(或者机器A,道理是一样的)和线程B需要更新同一个数据,A先于B但时间间隔很短,那么就有可能会出现: 146 | 147 | 1. 线程A更新了数据库 148 | 2. 线程B更新了数据库 149 | 3. 线程B更新了缓存 150 | 4. 线程A更新了缓存 151 | 152 | 按理说线程B应该最后更新缓存,但是可能因为网络等原因,导致线程B先于线程A对缓存进行了更新,这就导致缓存中的数据不是最新的。 153 | 154 | 第二个问题是,我们不确定要更新的这个缓存项是否会被经常读取,假设每次更新数据库都会导致缓存的更新,有可能数据还没有被读取过就已经再次更新了,这就造成了缓存空间的浪费。另外,缓存中的值可能是经过一系列计算的,而并不是直接跟数据库中的数据对应的,频繁更新缓存会导致大量无效的计算,造成机器性能的浪费 155 | 156 | 这种方案不可取,应当考虑删除缓存 157 | 158 | **3. 先删除缓存,再更新数据库** 159 | 这个方案的问题也是很明显的,假设现在有两个请求,一个是写请求A,一个是读请求B,那么可能出现如下的执行序列: 160 | 161 | 1. 请求A删除缓存 162 | 2. 请求B读取缓存,发现不存在,从数据库中读取到旧值 163 | 3. 请求A将新值写入数据库 164 | 4. 请求B将旧值写入缓存 165 | 166 | 这样就会导致缓存中存的还是旧值,在缓存过期之前都无法读到新值。这个问题在数据库读写分离的情况下会更明显,因为主从同步需要时间,请求B获取到的数据很可能还是旧值,那么写入缓存中的也会是旧值 167 | 168 | **4. 先更新数据库,再删除缓存** 169 | 170 | 这种方式是最常用的,但并不代表没有任何问题。 171 | 依然假设有两个请求,请求A是查询请求,请求B是更新请求,那么可能会出现下述情形 172 | 173 | 1. 先前缓存刚好失效 174 | 2. 请求A查数据库,得到旧值 175 | 3. 请求B更新数据库 176 | 4. 请求B删除缓存 177 | 5. 请求A将旧值写入缓存 178 | 179 | 上述情况确实有可能出现,但是出现的概率可能不高,因为上述情形成立的条件是在读取数据时,缓存刚好失效,并且此时正好又有一个并发的写请求。考虑到数据库上的写操作一般都会比读操作要慢,(这里指的是在写数据库时,数据库一般都会上锁,而普通的查询语句是不会上锁的。当然,复杂的查询语句除外,但是这种语句的占比不会太高)并且联系常见的数据库读写分离的架构,可以合理认为在现实生活中,读请求的比例要远高于写请求,因此我们可以得出结论。这种情况下缓存中存在脏数据的可能性是不高的 180 | 181 | 那如果是读写分离的场景下,按照如下所述的执行序列,一样会出问题: 182 | 183 | 1. 请求A更新主库 184 | 2. 请求A删除缓存 185 | 3. 请求B查询缓存,没有命中,查询从库得到旧值 186 | 4. 从库同步完毕 187 | 5. 请求B将旧值写入缓存 188 | 189 | 如果数据库主从同步比较慢的话,同样会出现数据不一致的问题。事实上就是如此,毕竟我们操作的是两个系统,在高并发的场景下,我们很难去保证多个请求之间的执行顺序,或者就算做到了,也可能会在性能上付出极大的代价 190 | 191 | 那为什么我们应当先更新数据库呢?因为缓存在数据持久化这方面往往没有数据库做得好,而且数据库中的数据是不存在过期这个概念的,我们应当以数据库中的数据为主,缓存因为有着过期时间这一概念,最终一定会跟数据库保持一致。 192 | 193 | **还有没其他办法** 194 | 195 | 1. 引入消息队列,将删除缓存的操作作为一条消息,放入消息队列 196 | 2. 订阅Binlog,比如DataBus来监控Binlog,一旦数据发生变更,Binlog消息通知回来去删除缓存 197 | 198 | - 删缓存有可能出现缓存击穿问题怎么解决 199 | 采用删除缓存的方案,在高并发场景下可能会导致缓存击穿(这个跟缓存穿透还有点区别),也就是大量的请求同时去查询同一个缓存,但是这个缓存又刚好过期或者被删除了,那么所有的请求全部都会打到数据库上,导致严重的性能问题。 200 | 201 | 当一个线程需要去访问这个缓存的时候,如果发现缓存为空,则需要先去竞争一个锁,如果成功则进行正常的数据库读取和写入缓存这一操作,然后再释放锁,否则就等待一段时间之后,重新尝试读取缓存,如果还没有数据就继续去竞争锁。这个是单机场景 202 | 203 | 如果有多台机器同时去访问同一个缓存项该怎么办呢?如果机器数不是很多的话,这种情况一般来说也不会成为一个问题,不过这里有个优化点,就是从数据库读取到数据之后,再对缓存做一次判断,如果缓存中已经存在数据,就不需要再写一遍缓存了。但是如果机器数也很多的话,那么就得考虑上分布式锁了 204 | 205 | 但是需要强调锁的代价,分布式锁对性能有影响,不能盲目上分布式锁,系统越简单越好 206 | 207 | **需要强一致性怎么做** 208 | 209 | 对缓存增加分布式读写锁,读写锁的特性可以多读,但是只能单写,利用读写锁,可以保证读请求不会读取到旧数据,但是写的过程所有读请求都会被阻塞,这个问题再高并发的情况下有可能把会服务器的请求线程池打满 210 | 211 | 这里用到的分布式读写锁并没有解决缓存击穿的问题,因为从读请求的视角来看,如果发生了更新数据库的情况,读请求要么被阻塞,要么就是缓存为空,需要从数据库读取数据再写入缓存。为了防止因缓存失效或被删除导致大量请求直接打到数据库上导致数据库崩溃 212 | ### 数据更新 213 | - 先更新数据,再删除缓存,依然有潜在的并发问题。 214 | 215 | 216 | ## 实践篇 217 | ### 启动 218 | brew services start redis 启动并前台运行 219 | brew services stop redis 停止服务 220 | redis-server /usr/local/etc/redis.conf 启动并后台运行 221 | mysql -uroot 本地登录 222 | brew services start mysql 前台 223 | mysql.server start 后台 224 | ### string 225 | 226 | - string的底层采用简单动态字符串SDS。 227 | 228 | ```shell 229 | set name longwang 230 | get name 231 | mset age 20 address shanghai 232 | ``` 233 | 234 | ### hash 235 | 236 | - hash底层实现为一个字典dict。 237 | - 当数据量比较小或者单个元素比较小时,底层用ziplist存储。 238 | 239 | ```shell 240 | hset user name chuangwang 241 | hget user name 242 | hmset user age 20 address shanghai 243 | hmget user age address 244 | hgetall user 245 | hdel user age address 246 | ``` 247 | 248 | ### list 249 | 250 | - list的底层实现采用quicklist和ziplist。 251 | - quicklist是双向链表。 252 | - ziplist是一种更紧凑,更节省内存空间的链表。 253 | 254 | ```shell 255 | lpush students longwang haoge 256 | rpush students bingge 257 | lrange students 0 -1 查看全部 258 | llen 查看长度 259 | lrem student 1 haoge //移除左边的 260 | ``` 261 | 262 | ### set 263 | 264 | - set为无序的,自动去重的数据类型。 265 | - set底层为一个value为null的字典dict。 266 | 267 | ```shell 268 | sadd letters aaa bbb ccc ddd 269 | scard letters 270 | srem letters aaa 移除 271 | ``` 272 | 273 | ### zset 274 | 275 | - zset有序,自动去重的数据类型。 276 | - zset底层实现为字典dict+跳表skiplist。 277 | - 当数据比较少时,用ziplist存储。 278 | 279 | ```shell 280 | zadd users 10 zhangsan 8 lisi 281 | zcard users 总数量 282 | zrange users 0 5 查看 283 | zrem users zhangsan 移除 284 | ``` 285 | 286 | ### 失效时间 287 | 288 | ```shell 289 | set code test ex 30 秒 290 | set code test px 30000 毫秒 291 | ttl code 查看剩余失效时间 -1代表永不失效 292 | expire code 20 分批次设置失效20秒后 293 | pttl code 查看剩余失效毫秒 294 | set code test nx 不存在时设置成功 295 | set code test xx 存在时设置成功 296 | ``` 297 | ## redis事务 298 | - 监控字段watch 299 | - 开启事务multi 300 | - 命令入队 301 | - 执行事务exec 302 | - redis单条命令是保证原子性的,但是redis的事务是不保证原子性。 303 | redis事务没有隔离级别的概念,一个事务的所有命令都会被序列化,在事务执行过程中,按照顺序执行。所有的命令在事务中,并没有直接执行,只有发起执行命令的时候才执行,Exec。 304 | 305 | **必须在事务开启之前watch,也就是不能再事务中间开启监控。** 306 | 对key添加监视锁watch 307 | 分布式锁 308 | setnx lock_key value 309 | 使用setnx设置一个公共锁,利用setnx命令的返回值特征,**有值则返回设置失败0,无值则返回设置成功1。** 310 | 311 | - 成功。拥有控制权,执行下一步业务,用完后del释放锁。 312 | 313 | - 失败。不具有控制权,排队等待。 314 | 315 | 316 | **如果释放锁之前宕机,就会导致死锁,可以设置锁的过期时间来解决。** 317 | **set num 666 nx ex 100** 318 | 误删锁问题:如何得知获取的锁是否是自己的锁。**设置锁的名字,删除之前先对比。** 319 | 重入性问题:获取锁之后,执行代码的过程中,尝试再次获取锁。记录锁的名称,比较一下,如果相同就继续让执行,再释放锁时记录调用层数,在最外层释放锁。使用hash来实现。 320 | hset user name jack 321 | hset user age 21 322 | **取消或放弃事务discard** 323 | 一旦事务执行成功后,监控自动取消掉。如果想手动关闭监控使用unwatch。 324 | exec返回nil表示事务执行失败 325 | redis可以实现乐观锁 326 | 乐观锁:不加锁,更新数据时才去判断,获取version,然后比较version,监控某个值,执行事务之前,另一个线程修改了这个值,就会导致事务执行失败。 327 | 悲观锁:总是加锁。 328 | 事务执行中的错误: 329 | 330 | - 语法错误:exec时,所有事务都不会执行。 331 | 332 | - 运行时错误:比如对一个字符串自增,incr str 其他事务正常执行,只有出错的命令执行失败。 333 | 334 | ## 面试篇 335 | ### 考点 336 | - 五种基本数据结构 337 | - 三种特殊数据类型:geo/hyperloglog/bitmap 338 | - 持久化 339 | - 事务 340 | - 过期删除策略 341 | - 内存淘汰策略 342 | - 集群 343 | - 哨兵 344 | - 缓存穿透 345 | - 缓存击穿 346 | - 缓存雪崩 347 | - 分布式锁 348 | ### 常见问题 349 | 1. redis可以用作集群吗?集群中数据同步是怎么实现的?分布式事务是怎么实现的? 350 | 2. 分布式事务的实现方式有哪些? 351 | 3. redis支持多线程吗?并发控制是怎么解决的? 352 | 4. redis支持分布式吗?支持多集群吗? 353 | -------------------------------------------------------------------------------- /study_path.md: -------------------------------------------------------------------------------- 1 |

2 | GitHub 3 | 4 | 5 | 6 |

7 | 8 | # C/C++系统开发 9 | 10 | 11 | ### 基础知识 12 | - [C语言](C/README.md) 13 | - [C++编程语言](C++/README.md) 14 | - [数据结构与算法](DataStructure/README.md) 15 | - [操作系统](OperatingSystem/README.md) 16 | - [数据库](Database/README.md) 17 | - [计算机网络](ComputerNetwork/README.md) 18 | - [分布式系统](DistributedSystem/README.md) 19 | - [项目](Project/README.md) 20 | 21 | 22 | 23 | ### 高频算法类型 24 | 25 | 1. 模拟 ★★★★★ 可难可易 大部分题都是模拟中使用某个算法优化 26 | 2. 贪心 ★★★ 按照某种规则排序 27 | 3. 字符串 ★★★ 输入输出容易搞人 双指针处理字符串空格单词 28 | 4. DFS/BFS ★★★ 有多少种组合数 图的最短时间 29 | 5. 滑动窗口/双指针 ★★ 最短或最长的子串 最多可变 k 次 30 | 6. DP ★★ 看运气 背包问题居多 遍历时维护最值 31 | 7. 堆 ★★ 学会自定义数据的排序规则 32 | 8. 并查集 ★★ 寻找连通分量和最大集合数量 33 | 9. 找规律/数学 ★ 看运气 34 | 10. 前缀和 ★ 子数组范围较小时直接用 35 | 11. 二分法 ★ 灵活运用库函数 lower_bound 和 upper_bound 36 | 12. 单调栈 ★ 通常跟数组有关 37 | 13. 迪杰斯特拉算法/弗洛伊德 ★ 通常不考 38 | 14. 字典树 ★ 出现前缀字符串查询或者异或值直接用 39 | 15. 高精度 ★ 大数乘法或大数加法 40 | 41 | ### 笔试技巧 42 | 43 | 1. 数值较大的结果考虑用 long long 44 | 2. 动态规划写不来,先写暴力过部分测试样例 45 | 3. 特殊输出先提交一下,骗点分了再说 46 | 4. 练习处理诡异输入的能力 47 | 48 | ## 书单 49 | 50 | ### C++ 51 | - 《C++ Primer》第五版 52 | - 《后台开发: 核心技术与应用实践》 徐晓鑫 53 | ### 计算机系统基础 54 | - 《深入理解计算机系统》第三版 55 | - 《Linux 高性能服务器编程》 游双 56 | - 《数据库系统概念》第七版 57 | 58 | ### 算法 59 | - 《剑指 offer》何海涛 60 | - 《程序员面试指南》左程云 61 | 62 | ## 计算机优质课程 63 | 64 | - 《操作系统》[蒋炎岩](https://www.bilibili.com/video/BV1N741177F5) 65 | - 《数据结构与算法》 [陈越](https://www.bilibili.com/video/BV1H4411N7oD/?spm_id_from=333.337.search-card.all.click&vd_source=e9f1ced96b267a4bc02ec41ca31d850a) 66 | - 《设计模式》[李建忠](https://www.bilibili.com/video/BV1Eb4y1m7Uj?from=search&seid=8468035381340447890) 67 | - 《深入理解计算机系统》[yaaangmin](https://space.bilibili.com/4564101) 68 | 69 | ## 刷题网站 70 | 71 | 1. [PAT 系统](https://pintia.cn/problem-sets/15/problems/type/7) 72 | 2. [LeetCode](https://leetcode.cn/problemset/all/) 73 | 3. [AcWing](www.acwing.com) 74 | 75 | ## 如何准备Leetcode面试算法题 76 | [知乎](https://zhuanlan.zhihu.com/p/349940945) 77 | -------------------------------------------------------------------------------- /system.md: -------------------------------------------------------------------------------- 1 | ## 操作系统与系统编程面试高频问题 2 | 3 | 4 | 5 | ## Linux内存模型 6 | - 从高地址到低地址 7 | 1. 环境变量和命令行参数 8 | 2. 栈区 9 | 3. 共享区 10 | 4. 堆区 11 | 5. 未初始化数据段.bss 12 | 6. 初始化数据段.data 13 | 7. 代码段.text 14 | 15 | ## 代码生成可执行文件的过程 16 | - 主要分为四个步骤 17 | 1. 预编译阶段:对g++编译器指定-E参数,生成.i文件。这个阶段的主要工作是将所有的宏展开,去掉所有的条件预编译指令,将所有的头文件包含进来,删除注释等。 18 | 2. 编译阶段:对g++编译器指定-S参数,生成.s汇编文件。这个阶段的主要工作是对代码的语法,语义和词法等进行分析。 19 | 3. 汇编阶段: 对g++编译器指定-c参数,生成.o二进制文件。 20 | 4. 链接阶段:将各个模块之间的相互引用处理好。把所有的静态库用到的目标文件装入程序中,并进行统一编址,然后进行重定位,即逻辑地址到物理地址的转换。 21 | 22 | ## 静态库与动态库 23 | 1. 静态库:命名方式为lib开头加上自定义的静态库名,然后以.a结尾。静态库实际上是一组目标文件的集合,再链接阶段与调用的程序生成可执行文件。 24 | 优点:代码加载速度快,发布程序时,不需要提供对应的库; 25 | 缺点:可执行文件体积大,同时如果静态库有修改,调用的程序需要重新编译,而编译的耗时比较久。 26 | 27 | 2. 动态库:命名方式为lib开头加上自定义的动态库名,然后以.so结尾。动态库首先生成与位置无关的目标文件,然后在运行时加载到内存。 28 | 优点:动态库可以共享,节省了系统资源,动态库进行修改后,无需重新编译。 29 | 缺点:加载速度比静态链接慢,发布程序时,需要单独提供动态库。 30 | 31 | 32 | ## 内存对齐 33 | - union最大成员所占的整数倍,同时能容纳其他的成员。union中变量共用内存,应以最长的为准。 34 | - struct按照成员的声明顺序,依次安排内存,偏移量为成员大小的整数倍,最后结构体的大小为最大成员所占大小的整数倍。在C++中,空结构体和空类的内存所占大小为1个字节。C中空结构体所占大小为0。 35 | - 为什么要有内存对齐: 36 | 1. 硬件原因:加速CPU的访问速度。因为CPU和内存数据交换的基本单位是块,块的大小为2的n次方字节。内存未对齐可能需要多次访问内存。2. 平台原因:不是所有的平台都支持任意地址的数据访问。 37 | 38 | ```c 39 | #include 40 | using namespace std; 41 | typedef union{ 42 | long long i; //8 bytes 43 | int k[5]; //4 bytes 最长的成员不是20 44 | char c; // 1 byte 45 | }UDATE; 46 | //联合体共用内存 最长成员为8字节 结果要为8的倍数 同时要能容纳其他成员,即大于等于20字节 所以为24字节 47 | struct data{ 48 | int cat; // 4 bytes 49 | UDATE cow; //24 bytes 但是需要先拆开来 最长成员为8字节 50 | double dog; //8 bytes 51 | }too; 52 | //结构体顺序考虑,结果为最大成员的整数倍,如果后一个成员的长度的开始位置不是整数倍需要填充字节 53 | //cat占4个字节 填充4个字节 54 | //起始位置为8 满足整数倍 cow占用24字节 55 | //起始位置为32 满足整数倍 doule占用4字节 56 | //所以结构体总共占用40字节,同时40也是8的倍数。 57 | UDATE temp; 58 | int main(){ 59 | cout< 阻塞与非阻塞是从IO请求的发起者角度出发,是等待IO(阻塞)执行完成还是立马返回(非阻塞)。 196 | - 阻塞的文件描述符为阻塞IO 197 | - 非阻塞的文件描述符为非阻塞IO 198 | 199 | ### 同步IO和异步IO 200 | > 同步与异步是从IO请求的执行者角度出发,执行完成后是应用程序主动询问(同步),还是内核通知应用程序 201 | (异步)。 202 | - 同步IO向应用程序通知的是IO就绪事件。要求用户代码自行执行读写操作,将数据从内核缓冲区读入用户缓冲区。 203 | - 异步IO向应用程序通知的是IO完成事件。由内核来执行IO读写操作。 204 | 205 | 206 | ## Reactor模式的工作流程 207 | 208 | 1. 主线程往epoll内核事件表中注册socket上的就绪事件。 209 | 2. 主线程调用epoll_wait等待socket上有数据可读。 210 | 3. 当socket上有数据可读时,epoll_wait通知主线程。主线程将socket可读事件放入请求队列。 211 | 4. 睡眠在请求队列上的某个工作线程被唤醒,它从socket读取数据,并处理客户请求,然后往epoll内核事件表中注册该socket上的写就绪事件。 212 | 5. 主线程调用epoll_wait等待socket可写。 213 | 6. 当socket可写时,epoll_wait通知主线程。主线程将socket可写事件放入请求队列。 214 | 7. 睡眠在请求队列上的某个工作线程被唤醒,它往socket上写入服务器处理客户请求的结果。 215 | 216 | 217 | 218 | ## 虚拟地址空间 219 | 220 | - 虚拟地址是操作系统管理内存的一种方式。 221 | - 方便不同进程使用的虚拟地址彼此隔离。 222 | - 方便物理内存中不相邻的内存在虚拟地址上视为连续的来使用。 223 | - 虚拟地址和物理地址的映射是通过MMU页表进行的。 224 | - 虚拟内存对实际内存有保护作用。 225 | 226 | ### 内核态和用户态 227 | 228 | - 内核态用于执行一些特权指令,比如中断机制,原语,进程管理等,目的是保护系统程序。 229 | 230 | - 陷入内核态的三种情况: 231 | 1. 系统调用 232 | 2. 中断 233 | 3. 异常 234 | 235 | - 中断和异常都可以实现用户态到内核态的切换,是通过硬件实现的。 236 | 237 | - 中断:IO完成,定时器时钟中断。 238 | 239 | - 异常:越界,溢出,缺页,异常不能被屏蔽,一旦出现应立即处理。 240 | 241 | 242 | 243 | ## 进程调度方式 244 | 245 | 1. 抢占式:立马停止。 246 | 2. 非抢占式:时间片用完或者等待资源时,再调用另一个进程。 247 | 248 | ## 进程调度算法 249 | 250 | 1. 先来先服务 251 | 2. 短作业优先 252 | 3. 优先级调度 253 | 4. 时间片轮转 254 | 5. 高响应比优先 255 | 256 | ## 管道 257 | - 管道是一种伪文件,实质为内核缓冲区大小为4K,内核借用环形队列实现。 258 | - 管道是半双工的,数据只能单向流动,不可重复读取,只能用于有血缘关系的进程。 259 | 260 | ## 大端字节序和小端字节序 261 | 262 | 1. 大端字节序:网络字节序(高位存低位) 263 | 2. 小端字节序:主机字节序,现代PC机采用小端字节序(低位存低位,高位存高位) 264 | 265 | >比如0x1f3f5f7f 地址0x1000 0x1001 0x1002 0x1003 266 | > 267 | >大端法:7f存在0x1003 5f存0x1002 3f存0x1001 1f存0x1000 低存高 268 | > 269 | >小端法:7f存在0x1000 5f存0x1001 3f存0x1002 1f存0x1003 低存低 270 | 271 | ## socket server 272 | > socket 创建socket文件描述符 273 | > bind 绑定IP和端口号 274 | > listen 设置监听连接数的上限 275 | > accept 阻塞等待数据到来 276 | > read/write 处理客户端的业务 277 | > close 关闭文件描述符 278 | 279 | ## socket client 280 | > socket 创建套接字文件描述符 281 | > bind 绑定IP和端口号(也可以隐式绑定) 282 | > connect 尝试连接服务器(建立TCP连接) 283 | > write/read 处理服务器端的业务 284 | 285 | ## 五种网络IO模型 286 | 1. 同步阻塞IO 287 | 2. 同步非阻塞IO 288 | 3. IO多路复用 289 | 4. 信号驱动IO 290 | 5. 异步IO 291 | 292 | ## 协程 293 | - 协程是一种用户态的轻量级线程。 294 | - 协程的开销远远小于线程的开销。 295 | - 协程是一种比线程更加轻量级的存在,一个线程可以拥有多个协程。 296 | - 无论是进程还是线程,都是由操作系统所管理的。而协程不是被OS所管理,而完全是由程序所控制(也就是在用户态执行)。 297 | 298 | ## 信号 299 | 300 | - 信号是一种不精确通信。 301 | - 常用的信号有SIGKILL 9 无条件终止信号 302 | - SIGSEGV 11 无效存储访问 303 | - SIGPOLL 8 轮询事件信号。 304 | - 信号有三种处理方式:忽略,捕获,默认。 305 | - kill命令向进程发送信号 306 | 307 | ## 什么是死锁 308 | 309 | - 因为资源调度的方式不合理或者资源的稀缺性,导致进程间的相互等待。 310 | 311 | - 死锁的四个必要条件: 312 | 1. 互斥条件 313 | 2. 请求和保持条件 314 | 3. 环路等待条件 315 | 4. 不可剥夺条件 316 | 317 | - 死锁的预防只要破坏死锁产生的四个必要条件。通常采用预先静态分配方法,可以破坏请求和保持条件。 318 | - 死锁的避免:采用银行家算法,只要系统处于安全状态,系统便可避免死锁。 319 | - 死锁的解决:撤销进程,剥夺资源。 320 | - 死锁检测:依赖关系图是否有环。 321 | 322 | 323 | 324 | ## fork函数 325 | 326 | - fork函数用来创建子进程 一次调用,两次返回。在父进程中返回子进程的PID,在子进程中返回0 327 | 328 | ## exec族 329 | 330 | - 在程序中调用另一个可执行程序,但是进程ID不改变。 331 | 332 | 333 | ## mmap存储映射 334 | 335 | - 将磁盘空间映射到进程空间,使进程可以采用指针的方式操作这段内存,而不用调用read和write函数。 336 | - 提高了读写的效率,同时也可以实现进程间的通信。 337 | 338 | ## 异步IO原理 339 | - 底层将数据准备好后,内核会给进程发送一个异步通知信号SIGIO29通知进程,然后进程调用信号处理函数去读数据,没准备好,数据就忙自己的事情。 340 | 341 | ### 文件 342 | 343 | - open/read/write操作文件描述符三连 344 | 345 | ```c 346 | ret = read(fds[0], buf, sizeof(buf)); //返回读取到的字节数 347 | write(fds[1], str, strlen(str)); 348 | ``` 349 | 350 | ### fork 351 | 352 | ```c 353 | int pid = fork() 354 | if(pid > 0){} 父进程 355 | else if(pid == 0){}子进程 -1表示失败 356 | ``` 357 | 358 | ### pipe 359 | - 本质是内核缓冲区,使用环形队列实现,默认大小是4k。 360 | - 半双工,单方向流动数据 361 | - 一读一写 362 | 363 | ```c 364 | itn fds[2]; 365 | int ret = pipe(fds); 366 | 0表示成功 -1表示失败 367 | ``` 368 | 369 | ### exec 370 | 371 | - fork出子进程后,希望子进程执行另外一个可执行程序。 372 | 373 | - 调用exec函数以后,进程的用户空间数据(.text和.data)被新程序所替换,进程id不改变。 374 | 375 | ```c 376 | execlp("ls","随便什么都可以","-l","-a",NULL); //最后一定要传入NULL用于指示不定长参数的结尾 377 | ``` 378 | 379 | ### 信号 380 | 381 | - 信息量比较小,开销小,不可靠,有时延。每个进程收到的所有信号,都是由内核负责发送的,内核处理。 382 | 383 | - 产生信号:kill raise abort 段错误 除0 按键 384 | 385 | - 信号机制:信号屏蔽字(阻塞信号集),未决信号集。放在PCB进程控制块中。 386 | 387 | - 未决信号集bitmap 388 | 389 | - 处理方式:忽略/捕获/默认 390 | 391 | - 注册信号捕捉函数 392 | 393 | - SIGSEGV 394 | - SEGKILL  395 | - SIGALRM 396 | - SIGINT 397 | 398 | ```c 399 | void my_handler(int sig){ 400 | printf("hello world!\n"); 401 | abort(); //终止进程 402 | } 403 | int main(){ 404 | signal(SIGALRM, my_handler); //注册信号捕捉函数 405 | alarm(1); //定时 406 | for(int i = 0; ; i++){ 407 | printf("%d\n",i); 408 | } 409 | return 0; 410 | } 411 | ``` 412 | 413 | ### alarm 414 | 415 | 定时器,只能支持到秒级,与进程状态无关,进程挂起时也在计时中。 416 | 417 | 每个进程都有且只有一个唯一个定时器,二次调用覆盖原来的定时器,返回原来定时的剩余时间。 418 | 419 | ```c 420 | alarm(5) //5秒后程序终止 421 | alarm(0) //取消闹钟 422 | ``` 423 | 424 | setitimer微秒级定时 425 | 426 | ### 动态分配内存 427 | - 分配策略 428 | - 首次适应:地址递增最先满足条件的内存 429 | - 最佳适应:容量递增最先满足条件的内存 430 | - 循环适应:首次适应的优化版,从上次分配的位置开始。 431 | 432 | ### 分页内存管理 433 | - 固定分区会产生内部碎片,动态分区会产生外部碎片。分页使用相等的页面大小,内存和进程都进行划分,由于页面较小,所以每个进程平均产生半个页的内部碎片。分页管理是从计算机的角度考虑设计的,页面的大小对用户是透明的。分段管理是从用户的角度去设计的,为了满足编程的方便,信息保护和共享等多方面的需要。 434 | - 逻辑地址:页号P + 页内偏移量W 435 | - 物理地址:b*L + W 436 | - 页表项:页号P + 物理内存的块号b 437 | - 页面大小必须相等 438 | - 页表:由页表项组成。 439 | - 地址转换机构:逻辑地址---快表TLB---页表---物理地址 440 | - 两次访问主存,如果使用了快表就可以实现只访问一次主存拿数据和指令 441 | - 多级页表:减少页表所占的连续内存空间。 442 | 443 | ### 分段内存管理 444 | - 逻辑地址:段号S + 段内偏移量W 445 | - 段表:由段表项组成。 446 | - 段表项:段号S + 段长L + 起始地址b 447 | - 物理地址:b + W (需要判断b是否小于等于L) 448 | ### 段页内存管理 449 | - 逻辑地址:段号S + 页号P + 页内偏移量W 450 | - 每个进程有一个段表,每个段有一个页表。 451 | - 段页式存储既有分页也有分段的优点,采用分段来分配和管理用户地址空间,用分页来管理物理存储空间,但它的开销最大。 452 | 453 | ### 虚拟内存 454 | - 局部性原理:时间局部性和空间局部性。 455 | - 部分页面装入内存 456 | - 缺页中断机制 457 | - 页面置换算法: 458 | 1. 最佳置换算法 459 | 2. 最近最少使用 460 | 3. 先进先出 461 | 4. 时钟算法 462 | - 抖动:页面频繁调度 463 | - Belady问题:FIFO中随着物理块的增加,缺页故障反而增加。 464 | 465 | ### 进程调度算法 466 | - 先来先服务:属于不可剥夺算法,对长作业有利,对短作业不利,有利于CPU繁忙,不利于IO繁忙。 467 | - 短作业优先:对于长作业会产生饥饿,即长期不被调度。 468 | - 优先级调度:从就绪队列中选择优先级最高的进程,分配CPU 469 | - 高响应比优先调度:主要用于作业调度,克服了饥饿状态,兼顾了长作业。 470 | - 时间片轮转:主要适用于分时系统,剥夺式算法 471 | - 多级反馈队列 472 | ## 实践篇 473 | ### 文件描述符限制 474 | 475 | - 一个进程打开的文件描述符上限是1024 476 | - ulimit -u 获取文件描述符个数 477 | - im /etc/security/limits.conf 打开该文件修改进程的文件描述符上限 478 | - 查看系统的文件描述符上限 cat /proc/sys/fs/file-max 479 | - select的数组大小要修改只能重新编译内核 480 | 481 | ## 自旋锁 482 | 483 | 484 | - 保护的临界数据处理的时间尽可能短,否则很浪费CPU 485 | - 在多CPU的系统上,自旋锁才有价值。 486 | - 执行自旋锁内部代码时不能主动让出CPU,否则会引起死锁等问题。 487 | - 自旋锁内是不允许存在信号量操作的,反之,信号量保护的代码里面是可以有自旋锁操作的。 488 | 489 | ## 伙伴系统 490 | 491 | 492 | 493 | - buddy system 494 | - 基于2的幂次开辟内存。 495 | - 释放时自动合并成更大的空间。 496 | 497 | ## slab机制 498 | 499 | - 是linux系统的一种内存分配机制。它是针对经常分配和释放的对象。slab分配器基于对象进行管理,每次从slab列表中分配一个同样大小的单元。释放时不直接返回给伙伴系统,而是保存在slab列表中。 500 | 501 | 502 | ## 僵尸进程 503 | 504 | - 子进程死了,父进程没有进行回收。waitpid回收指定进程。 505 | 506 | ## 孤儿进程 507 | 508 | - 父进程死了,子进程仍然存活。系统会让init进程领养孤儿进程。 509 | 510 | 511 | ## 内存空间分布 512 | - 内核区 513 | - 用户区 514 | - 环境变量 515 | - 命令行参数 516 | - 栈区 517 | - 共享区【加载动态链接库或者建立内存映射】 518 | - 堆区 519 | - .bss段未初始化变量 520 | - .data段初始化数据 521 | - .text段代码段 522 | - 0-4k的受保护区域NULL 523 | ## 零拷贝技术 524 | - IO系统调用:read/write用户态没有缓冲区 525 | - 标准IO库:fread/fwrite用户态有缓冲区 526 | - 零拷贝:是指不用将数据从内核态到用户态的频繁拷贝,节省了CPU的周期和内存。 527 | - sendfile发送文件时只需要一次系统调用 528 | - 将数据从磁盘读取到内核缓冲区 529 | - 在socket buffer中记录内核缓冲区的位置和偏移量 530 | - 根据socket buffer中的记录将数据copy到网卡设备中 531 | - 避免了内核态到用户态的频繁拷贝,减少系统调用的次数,降低了上下文切换的开销。 532 | - mmap:将磁盘上的物理空间映射到共享区,避免了read和write的系统调用,可以直接通过指针操作文件。 533 | 534 | ## 上下文切换开销 535 | - 页表目录项更新 536 | - 寄存器中的数据 537 | - 切换内核态栈 538 | - 刷新TLB 539 | - L1到L3的缓存间接失效,直接访问内存。 540 | ## .C文件到ELF文件的流程 541 | - 预编译 542 | - 编译 543 | - 汇编 544 | - 链接 545 | 546 | ### ELF文件执行的流程 547 | - 核心函数是load_elf_binary,它通过读存放在ELF文件中的信息为当前进程建立一个新的执行环境。 548 | - elf是静态文件,程序执行时所需要的指令和数据必需在内存中才能够正常运行。load_elf_binary 就是将elf里的指令和数据加载到内存中。 549 | - 进程的创建 550 | - 创建一个独立的虚拟地址空间(先共享父进程的页框,即COW机制) 551 | - 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系(在子进程需要加载新elf文件时) 552 | - 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行 553 | - 完整流程 554 | - shell首先fork一个子进程,子进程通过execve系统调用启动加载器 555 | - 读取elf文件头 556 | - 调用load_elf_binary函数加载代码段和动态链接库,分配虚拟地址空间。 557 | - 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行。 558 | - 往磁盘上写入数据,调用write函数,触发系统调用sys_write,写入内核缓冲区,可以使用fsync强制刷新到磁盘。 559 | 560 | ## 文件系统的主要功能 561 | - 创建 562 | - 删除 563 | - 读写 564 | - 挂载 565 | 566 | ## VFS 虚拟文件系统 567 | 568 | - 为了屏蔽底层文件系统和驱动程序等细节,提供友好的统一用户接口 569 | - EXT4 块文件系统 570 | - FAT 571 | - swap 虚拟内存的文件系统 572 | 573 | ## 日志文件系统 574 | 575 | - 日志文件系统可以在系统发生断电或者其它系统故障时保证整体数据的完整性 576 | 577 | ## 打开文件描述符表 578 | 打开文件描述符表,简称为打开文件表。 579 | 当进程打开一个文件的时候,虚拟文件系统会创建文件的一个打开实例:file结构体,然后在进程的打开文件表中分配一个索引inode,这个索引称为文件描述符,最后把文件描述符和file结构体的映射添加到打开文件表中。 580 | file结构体包括: 581 | 582 | - inode对象指针 583 | - 文件偏移量 584 | - 访问模式 585 | 586 | > 打开文件表是PCB进程控制块的一部分,进程控制块是个task_struct结构体包括: 587 | - 进程号 588 | - 打开文件描述符表 589 | - 等等 590 | 591 | ## 文件系统 592 | 593 | 文件系统应具有以下功能 594 | 595 | 1. 完成文件存储系统的管理,即分配空间和回收空间。 596 | 2. 实现文件名到物理地址的映射。 597 | 3. 实现文件和目录的建立,读写管理。 598 | 4. 向用户提供有关文件和目录操作的接口。 599 | 600 | ## 文件系统层次结构 601 | 602 | - 用户空间 603 | - 内核空间 604 | - 虚拟文件系统VFS 605 | - 块设备文件系统、内存文件系统、闪存文件系统 606 | - 页缓存 Page Cache 607 | - 块设备层 608 | - 块缓存 609 | - IO调度器 610 | - 块设备驱动程序 611 | - 硬件 612 | - 机械硬盘、固态硬盘 613 | - 闪存 614 | 615 | ## IO调度算法 616 | - NOOP:FIFO 617 | - CFQ完全公平:该算法为每个进程分配一个时间窗口,在时间窗口内允许进程发出IO请求 618 | - Deadline:每个IO请求有一个最后执行期限。 619 | 620 | ## 分页管理 621 | 622 | - 从进程虚拟地址空间映射到内存地址空间。 623 | - 页面大小相同,内存和进程都进行划分。由于页面较小,每个进程平均产生半个页的内部碎片。 624 | - 逻辑地址:页号 + 页内偏移 625 | - 地址转换:逻辑地址---快表TLB---页表----物理地址 626 | - 两次访问主存,如果使用了快表就可以实现只访问一次主存拿数据和指令 627 | 628 | ### 虚拟文件系统VFS 629 | 630 | - 对用户友好,屏蔽底层细节。 631 | - 管理和存储文件 632 | - 文件系统的主要功能:安装,卸载,创建,删除,读写。 633 | - 文件系统的结构分为三层:用户层,内核层,硬件层。 634 | - 用户层的函数:fopen fclose fwrite fflush fread fseek 635 | - 内核层函数:mount unmount reade write open lseek fsync fdatasync 636 | - 内核中有个页缓存:page cache 637 | 638 | ## mount挂载 639 | 640 | - 一个存储设备上的文件系统,只有挂载到内存中目录树的某个目录下,进程才能访问这个文件系统。 641 | 642 | ## linux上运行可执行文件的过程 643 | 644 | - a.out是ELF文件格式 645 | - linux每个程序都会运行在一个进程上下文中,这个进程上下文有自己的虚拟地址空间。这个进程是由shell进程fork出来的子进程。 646 | 647 | ## 内存管理 648 | 649 | - 分页内存管理 650 | - 内存和进程划分为大小相等的页面,通常是4k,页面大小是对用户透明的,分页是从计算机的角度去考虑的,为了减少内存碎片,由于页面较小,每个进程平均产生半个内存碎片。 651 | - 分段 652 | - 分段是从用户的角度考虑,为了满足编程的方便,信息保护和共享等方面的要求。 653 | - 段页 654 | - 段号+页号+页内偏移 655 | 656 | ## 虚拟地址空间 657 | 658 | - 基于局部性原理,使用页面置换算法,是内存的可用空间更大。 659 | - 进程隔离 660 | - 内存的管理 661 | - 不连续的空间 662 | 663 | ## 读写文件的方式 664 | 665 | - 调用内核提供的系统调用read/write 666 | - 调用glibc库封装的标准IO流函数fread/fwrite。用户空间有缓冲区,能减少系统调用的次数,提高性能。 667 | - 创建基于文件的内存映射,把文件的一个区间映射到进程的虚拟地址空间,然后直接读内存。mmap避免系统调用,性能最高。 668 | 669 | ## IO控制方式 670 | 671 | - 轮询 672 | - 中断 CPU字节干预IO 673 | - 直接存储器访问DMA 一个块数据 干预一次 674 | - 通道 一组块数据 干预一次 675 | - 目的:减少CPU对IO的干预,提高其处理数据的效率。 676 | 677 | ## 中断机制 678 | 679 | 680 | 681 | - 外设速度远远慢于CPU的速度,所以需要外设资源准备好以后,利用中断机制主动通知操作系统。 682 | - 内部中断/异常:CPU执行指令期间检测到非法的条件(除零,地址访问越界) 683 | - 硬中断/外中断/中断:IO中断,时钟中断,信号中断产生的时间不确定。 684 | - 软中断:应用程序使用系统调用而引发的事件。 685 | - 中断描述符表 686 | - os中预先设置一些中断处理函数,当CPU接收到中断时,会根据中断号去查对应的处理函数,中断向量表就是记录中断号与中断函数映射关系的表。 687 | - 中断机制是为了弥补CPU速度和外设速度数量级差异的机制,它的核心是中断向量表。 688 | 689 | ## VFS中的数据结构 690 | - 超级块 super block 691 | - 索引节点 iNode 692 | - 目录项 dentry 693 | - 文件 file 694 | 695 | ## 超级块Super Block 696 | - 描述文件系统的总体信息,挂载文件系统时在内存中创建超级块的副本。 697 | - 一个文件系统只有挂载到内存中目录树的一个目录下,进程才能访问这个文件系统。 698 | - 每次挂载文件系统,VFS会创建一个挂载描述符:mount结构体,并读取文件系统的超级块,在内存中创建超级块的副本。 699 | - 每种文件系统的超级块的格式不同,需要向VFS注册文件系统类型,并实现mount方法用来读取和解析超级块。 700 | - ext4----register----mount-----super block 701 | 702 | ## 索引节点iNode 703 | - 每个文件对应一个索引节点,每个索引节点有个唯一的编号。当内核访问存储设备上的一个文件时,会在内存中创建索引节点的副本:结构体inode 704 | - 一个块由多个扇区组成4k=8sector 705 | - inode文件的元信息:创建者,创建日期,文件的大小。 706 | - 查看系统中iNode的用量df -i 707 | - df -h 输出文件系统分区情况 708 | - du -h 文件、目录 709 | 710 | ## 目录项 711 | - 文件系统把目录看作文件的一种类型,目录的数据是由目录项组成,每个目录项存储一个子目录或文件的名称以及对应的索引节点号。 712 | 713 | ## 打开文件表 714 | - 当进程打开一个文件的时候,VFS会创建文件的一个打开实例:file结构体,然后在进程的打开文件描述符表中分配一个索引,这个索引被称为文件描述符,最后把文件描述符和file结构体的映射添加到文件描述符表中。 -------------------------------------------------------------------------------- /system_interview.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SYaoJun/CPPInterview/f752c90e81cd53d4f55a17eca047523d30d777c9/system_interview.pdf --------------------------------------------------------------------------------