├── .vscode └── settings.json ├── Architecture ├── q1.md └── q2.md ├── Compiler ├── images │ └── Steps_for_Compiling_C_Program.png ├── q1.md ├── q3.md ├── q4.md └── q5.md ├── DataStructure └── q1.md ├── Database └── q2.md ├── Networking └── computer_network.md ├── OperatingSystem ├── q1.md ├── q10.md ├── q11.md ├── q4.md ├── q6.md ├── q7.md └── q9.md ├── README.md ├── link.md └── update.sh /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "vector": "cpp" 4 | }, 5 | "python.pythonPath": "/usr/local/bin/python3" 6 | } -------------------------------------------------------------------------------- /Architecture/q1.md: -------------------------------------------------------------------------------- 1 | # 为什么C++里面的浮点数比较要用1e-6作基准? 2 | 3 | 以IEEE 754中规定的32位浮点数为例子,我们知道32位浮点数中1位是符号位,8位是指数位,剩下的23位是尾数位(也即二进制科学记数法小数点右边的部分)。这样一来,对于同一个数量级的两个实数,使用32位浮点数能够区分的最小分辨率是1 / (2^23),这个数大约是1.19e-7,也就是说如果使用1e-6作为epsilon,当两个浮点数的差小于1e-6时,我们大致能认为两个数相等。 4 | 5 | 来自cppreference的浮点数比较代码片段: 6 | 7 | ```cpp 8 | template 9 | typename std::enable_if::is_integer, bool>::type 10 | almost_equal(T x, T y, int ulp = 6) 11 | { 12 | // the machine epsilon has to be scaled to the magnitude of the values used 13 | // and multiplied by the desired precision in ULPs (units in the last place) 14 | return std::abs(x-y) <= std::numeric_limits::epsilon() * std::abs(x+y) * ulp 15 | // unless the result is subnormal 16 | || std::abs(x-y) < std::numeric_limits::min(); 17 | } 18 | ``` -------------------------------------------------------------------------------- /Architecture/q2.md: -------------------------------------------------------------------------------- 1 | Why Way prediction can optimize Cache Performance? 2 | ===== 3 | 4 | ### What is Way Prediction? 5 | We know that direct-mapped caches are better than set-associative caches in terms of the cache hit time but set-associative cache has higher hit rates. We want to combine the benefit of direct-mapped caches and set-associative ones and the way prediction come out for this reason. Let's take the two-way set-associative cache for example. Each set of this cache has two ways(two lines) in it. By doing way prediction, we are effectively using one line of these at first. Only if we don't find it there, we look at the other one. So effectively, a way prediction cache first tries to access what looks like a smaller direct-mapped cahce then it will try the entire set-associative cache. 6 | 7 | 8 | 9 | 10 | ||32KB, 8-way SA|4KB DM|32KB, 8-way SA Way Pred| 11 | |-|-|-|-| 12 | |Hit rate|90%|70%|90%| 13 | |Hit Latency|2|1|1 or 2| 14 | |Miss Penality|20|20|20| 15 | |AMAT|2+10%*20=4|1+30%*20=7|0.7*1+0.3*(2+0.1*20)=1.9| -------------------------------------------------------------------------------- /Compiler/images/Steps_for_Compiling_C_Program.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liusy58/WhyThis/d0619864ed0ae528760a35477161020ab6751f28/Compiler/images/Steps_for_Compiling_C_Program.png -------------------------------------------------------------------------------- /Compiler/q1.md: -------------------------------------------------------------------------------- 1 | ## 为什么目标文件中未初始化的全局/静态变量要使用COMMON块? 2 | 3 | 首先需要明确的是什么是COMMON块。在目标文件中,编译器会将未初始化的全局/静态变量放到COMMON块里面去。 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Compiler/q3.md: -------------------------------------------------------------------------------- 1 | ## 为什么需要汇编器? 2 | 3 | 要回答这个问题,我们首先回忆一下汇编器(Assembler)。第一次听说汇编器应该是在讲C程序的执行全过程,如下图: 4 | ![](./images/Steps_for_Compiling_C_Program.png) 5 | 6 | 可能我们听到最多的一句话就是汇编器是将汇编代码转换成机器码,是一个translator,可能更进一步的话知道一步汇编或者多步汇编。 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Compiler/q4.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ```asm 5 | L1: slt $vc0, $0, $0 6 | beq $t0, $0, L2 7 | addi $a1, $a1,-1 8 | L2: add $t1, $a0, $a1 9 | ``` -------------------------------------------------------------------------------- /Compiler/q5.md: -------------------------------------------------------------------------------- 1 | -O0 2 | 3 | ```asm 4 | 00000000004008e9 : 5 | 4008e9: 55 push %rbp 6 | 4008ea: 48 89 e5 mov %rsp,%rbp 7 | 4008ed: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 8 | 4008f4: eb 16 jmp 40090c 9 | 4008f6: 48 8b 05 73 07 20 00 mov 0x200773(%rip),%rax # 601070 10 | 4008fd: 48 83 c0 01 add $0x1,%rax 11 | 400901: 48 89 05 68 07 20 00 mov %rax,0x200768(%rip) # 601070 12 | 400908: 83 45 fc 01 addl $0x1,-0x4(%rbp) 13 | 40090c: 81 7d fc ff e0 f5 05 cmpl $0x5f5e0ff,-0x4(%rbp) 14 | 400913: 7e e1 jle 4008f6 15 | 400915: 90 nop 16 | 400916: 5d pop %rbp 17 | 400917: c3 retq 18 | ``` 19 | 20 | 21 | -O1 22 | 23 | ```asm 24 | 00000000004007ac : 25 | 4007ac: 48 8b 15 b5 08 20 00 mov 0x2008b5(%rip),%rdx # 601068 26 | 4007b3: b8 00 e1 f5 05 mov $0x5f5e100,%eax 27 | 4007b8: 83 e8 01 sub $0x1,%eax 28 | 4007bb: 75 fb jne 4007b8 29 | 4007bd: 48 8d 82 00 e1 f5 05 lea 0x5f5e100(%rdx),%rax 30 | 4007c4: 48 89 05 9d 08 20 00 mov %rax,0x20089d(%rip) # 601068 31 | 4007cb: c3 retq 32 | 33 | ``` 34 | 35 | 36 | -O2 37 | 38 | ```asm 39 | 0000000000400830 : 40 | 400830: 48 81 05 2d 08 20 00 addq $0x5f5e100,0x20082d(%rip) # 601068 41 | 400837: 00 e1 f5 05 42 | 40083b: c3 retq 43 | 40083c: 0f 1f 40 00 nopl 0x0(%rax) 44 | ``` -------------------------------------------------------------------------------- /DataStructure/q1.md: -------------------------------------------------------------------------------- 1 | 为什么并查集需要有路径压缩? 2 | ====== 3 | 4 | 我认为有必要对并查集这个数据结构整理一篇文章出来。如果对并查集不熟悉的人,可能对于这个问题感觉非常的奇怪,所以这一篇文章会从并查集的最开始讲起。 5 | 6 | 什么是并查集?并查集是一种针对动态连通性查找的方法,它主要解决两个问题:连通两个节点和判断两个节点是否连通。 7 | 8 | 这样的话,它的ADT很明显如下: 9 | 10 | ```java 11 | public interface DisjointSets { 12 | /** Connects two items P and Q. */ 13 | void connect(int p, int q); 14 | 15 | /** Checks to see if two items are connected. */ 16 | boolean isConnected(int p, int q); 17 | } 18 | 19 | ``` 20 | 21 | 所以这篇文章会从这两个API的优化展开。 22 | 23 | 请记住一点:在并查集中我们选择将每一个item表示成一个整数,因此,每一个item都都可以对应于数组中的一个下标,因此数组中的值都可以表示某个item所属于的集合。 24 | 25 | 一般来说,并查集有多个实现版本,第一个版本叫做`QuickFind`。这是最简单的一种思路,当我们连接两个item p,q的时候,将p所在的集合中的每一个item的id改成q。这样最大的问题就是连接的代价太大了。 26 | 27 | 28 | `Quick union`是对`QuickFind`的改进,id[]存的是父节点的index。但是这样connect和isconnect的时间复杂度都是`O(n)` 29 | 30 | 31 | `Weighted quick union`是进一步改进,增加size变量使得树能矮一点,所以复杂度是`O(lgn)` 32 | 33 | 路径压缩是让树更矮。 34 | 35 | 36 | 37 | 参考资料 38 | 39 | [CS61B](https://docs.google.com/presentation/d/1J7q2RImSbg26vrWMaYQwYo6_zPDrrdGRmwm_U2oY20s/edit#slide=id.g5347e2c8f_2381) 40 | -------------------------------------------------------------------------------- /Database/q2.md: -------------------------------------------------------------------------------- 1 | 为什么需要NoSQL? 2 | ------ 3 | 4 | 关系型数据库,为了满足范式的要求,避免了数据冗余,然而在海量的数据场景下,引入数据冗余避免数据库多表关联反而可能带来更多的收益。 5 | 6 | 只有基于主键的增删查改,关系型数据库的性能比不过Key-Value存储系统。 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Networking/computer_network.md: -------------------------------------------------------------------------------- 1 | # Networking 2 | 3 | Why Design Like This: network series 4 | 5 | ## Why IPv6 ? 6 | 7 | - **More addresses**. Since IPv4 has only 2^32 addresses at most, IPv6 could assign one address to every grain of sand on earth. This benefits IoT and some other modern techniques. 8 | 9 | - **No more NAT**. Any pair of devices could connect each other directly once they are assigned with IPv6 addresses both without interruption of Network Address Translation, since the IPv6 address are unique for each device. One of the most famous application adopted this feature is the PT site. 10 | 11 | - **More distributed Internet**. Since any two devices could be connected, there is no need to store data in centralized servers. We can distribute interested data on peers and fetch the data from some of them when we need. 12 | 13 | - **Other**. End-to-end encryption, more secure name resolution (Secure Neighbor Discovery, SEND protocol)... -------------------------------------------------------------------------------- /OperatingSystem/q1.md: -------------------------------------------------------------------------------- 1 | 为什么进程退出的时候没有内存泄漏? 2 | -------- 3 | -------- 4 | 5 | 6 | 最近,我一直在看关于内存相关的东西,有一个学弟问了我一个东西觉得还挺有意思的,就是他的C程序(很简单的一个)的内存分配`malloc()`和`free()`没有成对的出现,但是他惊奇的发现即使运行很多次(数量比较大,十几万次),系统并没有崩溃,甚至可以说没有发生内存泄漏,听到这里,我就懂了他对什么没有理解了。 7 | 8 | 9 | 学过C语言的人都知道,我们的`malloc`和`free`要成对出现,否则会发生内存泄漏,没错,这句话本身没错,但是如果这句话脱离了一些语境,那么这句话就是错的了,这句话其实是针对一个程序因为内存不够导致崩溃来说的,对于一个长时间运行的程序,比如我们的web服务器,数据库管理系统,操作系统(of course~),那么你长时间的不释放掉一些内存就会出现很严重的问题,可能会出现内存不足而导致程序的崩溃,这就是说我们为什么一旦不用某些内存的时候需要调用free的真正含义,但是对于一个很小的程序,如果你在堆上面申请一些内存的话,那么在程序结束前即使你不释放的话,操作系统也会来帮你释放! 10 | 11 | 写过操作系统内核的人都知道在管理进程的时候,进程退出的时候,操作系统会回收进程的所有资源,所以从这个角度来看的话,只要操作系统是正确的话,进程退出的时候是没有内存泄漏。 12 | 13 | 14 | > 那么如今我们还需要一个要程序员手动释放内存的语言吗?自己掌控内存的优势是什么? 15 | 16 | 我认为手动内存管理的价值主要体现在 (1) 底层程序的开发、(2) 受系统限制难以接受垃圾回收开销以及 (3) 需要高度实时性的系统开发上。 17 | 18 | 对于底层程序,比如操作系统,我们显然需要一个手动管理内存的开发方案。我们需要直接利用内存信息表等信息来分配内存,以供内核和上层应用程序利用。对于垃圾回收系统来说,其也需要运行在一个可以手动管理内存的开发方案上。 19 | 20 | 在嵌入式开发领域,很多机器的 CPU 主频极低、内存极其小,例如有些单片机的主频只有 20MHz,片上内存只有 64B。在这样的设备上进行应用开发,垃圾回收的开销可能比应用程序本身都要大。对于极小内存的设备,往往使用固定的内存地址;而对于内存稍大的设备,则引入手动内存管理。 21 | 22 | 最后,虽然现在出现了一些无暂停(zero GC-stop)的垃圾回收器设计,但是目前来看,普遍应用的垃圾回收器往往会引入定期的、较大的内存回收暂停。在这段时间,应用程序无法响应外部的一切交互,这种暂停对于一些高度实时的应用场景来说是无法接受的。 23 | 24 | > 像golang这种具有垃圾回收,但在效率敏感的应用上也表现出了很好的效果。 25 | 26 | 从 Golang 的发展过程来看,早期版本孱弱的垃圾回收器极大地影响了 Golang 应用程序的性能表现。此外,目前来看 Golang 的性能表现还是要[显著弱于](https://www.techempower.com/benchmarks/)相当一部分进行手动内存管理的开发方案。 27 | 28 | 当然,从现实角度来看,以绝大部分网络应用程序的性能需要来看,垃圾回收的性能影响并不是非常显著,事实上这些应用程序也普遍选择了带有垃圾回收的开发方案(如 Java、C#、Golang),因此垃圾回收的引入确实能够提高这些不需要极高性能的开发场景的工作效率。 29 | 30 | > 所以为了那一点效率(有待于验证)引入可能内存泄露的风险到底值不值得? 31 | 32 | 综上,根据以上的讨论,处于以下几种情景时,我认为手动内存管理是非常必要的: 33 | 34 | * 操作系统与系统应用的开发,包括垃圾回收器的开发 35 | * 嵌入式和低性能设备的开发 36 | * 要求系统具备实时性的应用场景 37 | * 高度要求性能的应用场景 38 | 39 | 当然,对于目前的程序开发来说,我认为 75% 的情况下,以垃圾回收来交换可靠、高效的开发体验,还是有很显著的意义的。 40 | 41 | 此外,需要注意的是,引入垃圾回收器并不代表就不存在内存泄漏的问题。[不恰当地构造的对象](https://auth0.com/blog/four-types-of-leaks-in-your-javascript-code-and-how-to-get-rid-of-them/),以及内存回收器本身的设计缺陷,都可能导致应用程序发生内存泄漏。 42 | 43 | 最后,恰当地遵循一些程序设计规则,可以有效降低发生内存泄漏的风险,包括使用 RAII、所有权的概念。此外,2009 年出现的 Rust 语言通过引入[生命周期](https://doc.rust-lang.org/rust-by-example/scope/lifetime.html)的概念,提供了编译期检查的内存安全,将内存泄漏的风险降低到了最低。 44 | 45 | > 或者说目前有语言将这两个特性结合起来吗? 46 | 47 | 这里举出几个例子,虽然和「可以认定为发生了内存泄露,这时候再启动垃圾回收机制」有一些距离,但是也算作是在手动内存管理和自动内存管理之间的一个平衡。 48 | 49 | C++ 11 标准引入了智能指针,允许用户创建采用引用计数机制进行自动内存回收的对象 [`std::shared_ptr`](https://en.cppreference.com/w/cpp/memory/shared_ptr)。 50 | 51 | ```C++ 52 | std::shared_ptr p = std::make_shared(); 53 | 54 | std::cout << "Created a shared Derived (as a pointer to Base)\n" 55 | << " p.get() = " << p.get() 56 | << ", p.use_count() = " << p.use_count() << '\n'; 57 | std::thread t1(thr, p), t2(thr, p), t3(thr, p); 58 | p.reset(); // release ownership from main 59 | std::cout << "Shared ownership between 3 threads and released\n" 60 | << "ownership from main:\n" 61 | << " p.get() = " << p.get() 62 | << ", p.use_count() = " << p.use_count() << '\n'; 63 | t1.join(); t2.join(); t3.join(); 64 | std::cout << "All threads completed, the last one deleted Derived\n"; 65 | ``` 66 | 67 | C++/CLR 中,既允许出现传统的指针和手动内存管理机制,又允许开发者[创建在堆上分配的对象](https://docs.microsoft.com/en-us/cpp/dotnet/how-to-use-tracking-references-in-cpp-cli?view=msvc-160)。 68 | 69 | 70 | ```go 71 | G ^ g1 = gcnew G; 72 | G ^% g2 = g1; 73 | g1 -> i = 12; 74 | ``` 75 | 76 | Rust 采用 lifetime 实现了编译期确定的自动内存管理,但 Rust 也允许在 [unsafe](https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html) 块中手动分配内存和使用[野指针](https://doc.rust-lang.org/std/primitive.pointer.html)。 77 | 78 | ```go 79 | let address = 0x01234usize; 80 | let r = address as *mut i32; 81 | 82 | let slice: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) }; 83 | ``` 84 | 85 | 86 | 至于「可以认定为发生了内存泄露,这时候再启动垃圾回收机制」的机制,可以认为是很难实现的。现有的垃圾回收器大体上可以分为追踪(tracking)和引用计数(reference count)两种。这两种技术都依赖在分配内存时加入额外的信息来实现,而这种信息需要恰当的维护,这和手动内存管理本身就有一定的冲突。 87 | 88 | 89 | -------------------------------------------------------------------------------- /OperatingSystem/q10.md: -------------------------------------------------------------------------------- 1 | 为什么用不同的编译选项程序最终输出的结果不一样? 2 | ------- 3 | 4 | (再次收录jyy的一些内容) 5 | 6 | jyy在 7 | 8 | -------------------------------------------------------------------------------- /OperatingSystem/q11.md: -------------------------------------------------------------------------------- 1 | ## 为什么要有 DMA 技术? 2 | 3 | ### 在没有 DMA 技术前,I/O 的过程是这样的: 4 | - CPU 发出对应的指令给磁盘控制器,然后返回; 5 | - 磁盘控制器收到指令后,于是就开始准备数据,会把数据放入到磁盘控制器的内部缓冲区中,然后产生一个中断; 6 | - CPU 收到中断信号后,停下手头的工作,接着把磁盘控制器的缓冲区的数据一次一个字节地读进自己的寄存器,然后再把寄存器里的数据写入到内存,而在数据传输的期间 CPU 是无法执行其他任务的。 7 | - 整个数据的传输过程,都要需要 CPU 亲自参与搬运数据的过程,而且这个过程,CPU 是不能做其他事情的。 8 | 9 | 简单的搬运几个字符数据那没问题,但是如果我们用千兆网卡或者硬盘传输大量数据的时候,都用 CPU 来搬运的话,肯定忙不过来。 10 | 11 | 计算机科学家们发现了事情的严重性后,于是就发明了 DMA 技术,也就是直接内存访问(Direct Memory Access) 技术。 12 | 13 | ### 什么是 DMA 技术? 14 | 简单理解就是,在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务。 15 | 16 | ### 具体过程: 17 | - 用户进程调用 read 方法,向操作系统发出 I/O 请求,请求读取数据到自己的内存缓冲区中,进程进入阻塞状态; 18 | - 操作系统收到请求后,进一步将 I/O 请求发送 DMA,然后让 CPU 执行其他任务; 19 | - DMA 进一步将 I/O 请求发送给磁盘; 磁盘收到 DMA 的 I/O 请求,把数据从磁盘读取到磁盘控制器的缓冲区中,当磁盘控制器的缓冲区被读满后,向 DMA 发起中断信号,告知自己缓冲区已满; 20 | - DMA 收到磁盘的信号,将磁盘控制器缓冲区中的数据拷贝到内核缓冲区中,此时不占用 CPU,CPU 可以执行其他任务; 21 | - 当 DMA 读取了足够多的数据,就会发送中断信号给 CPU; 22 | - CPU 收到 DMA 的信号,知道数据已经准备好,于是将数据从内核拷贝到用户空间,系统调用返回; 23 | 24 | 整个数据传输的过程,CPU 不再参与数据搬运的工作,而是全程由 DMA 完成,但是 CPU 在这个过程中也是必不可少的,因为传输什么数据,从哪里传输到哪里,都需要 CPU 来告诉 DMA 控制器。 25 | 早期 DMA 只存在在主板上,如今由于 I/O 设备越来越多,数据传输的需求也不尽相同,所以每个 I/O 设备里面都有自己的 DMA 控制器。 26 | -------------------------------------------------------------------------------- /OperatingSystem/q4.md: -------------------------------------------------------------------------------- 1 | 为什么main"不是"程序的入口? 2 | ----- 3 | 4 | 今天在听jyy的操作系统公开课,他当时举了一个例子,让我一下子回忆起了之前对于《程序员的自我修养》里面的一些知识点,于是想记录下来。 5 | 6 | 刚学C语言的时候,几乎国内所有的老师都会告诉你“main函数是程序的入口”,这句话听起来也是没有什么错的,但是随着学习的深入,我认为一个合格的大学生应该不能停留在如此肤浅的认识,而应该能够彻底理解程序到底是怎样执行的,至少要到程序是需要装载才能执行的层面。 7 | 8 | 话不多说,jyy举的例子是这样的 9 | 10 | ```C++ 11 | int main(){ 12 | 13 | } 14 | ``` 15 | 16 | 这是一个什么都不做的程序,讲道理来说这个函数因为不依赖于任何的库函数,那么我们编译之后再用链接器按照静态链接的方法将其生成可执行文件应该是没有任何问题的,但是当我们执行下面的指令之后却发生了错误。 17 | 18 | 19 | ```sh 20 | gcc -c test.c 21 | ld -static -e _main test.o 22 | ``` 23 | 24 | 出现了段错误. 25 | 26 | ```sh 27 | [1] 15456 segmentation fault ./a.out 28 | ``` 29 | 30 | 按照jyy说的,那肯定得上gdb了,我们用`starti`指令,确实一开始就进入到了main函数里面去,但是main函数的最后一句是一个`ret`指令,学过汇编语言的人都知道,在x86体系结构里面,`ret`会弹出栈顶元素同时跳转到该位置。那么因为没有函数调用过`main`函数,那么肯定会发生错误,所以在程序装载的时候,操作系统肯定是加入了其他的代码来跳转到 `main`函数的执行的,加入的这些代码负责准备好`main`函数执行所需要的环境,包括堆、I/O、线程、全局变量构造,通过正常的代码,以`glibc`为例,我们可以发现`glibc`程序的入口叫做`_start`,我们可以看到核心代码是 31 | 32 | 33 | ```asm 34 | xorl %ebp, %ebp 35 | popl %esi 36 | movl %esp, %ecx 37 | 38 | pushl %esp 39 | pushl %edx 40 | pushl $__libc_csu_fini 41 | pushl $__libc_csu_init 42 | pushl %ecx 43 | pushl %esi 44 | push %main 45 | 46 | call __libc_start_main 47 | 48 | ``` 49 | 50 | 我们可以把这段代码改写成更直观的C代码 51 | ```C 52 | %ebp = 0; 53 | int argc = pop from stack 54 | char** argv = top of stack 55 | 56 | __lib_start_main(main,argc,argv,__libc_csu_init,__libc_csu_fini,edx,top of stack) 57 | ``` 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /OperatingSystem/q6.md: -------------------------------------------------------------------------------- 1 | 为什么有伙伴系统分配器还需要SLAB分配器? 2 | ------ 3 | 4 | 伙伴系统最小的分配单位是一个物理页(4KB),但是大多数情况下,内核需要分配的内存大小通常是几十字节或者几百个字节而已,远远小于一个物理页,如果仅仅使用伙伴系统进行分配内存的话,会出现严重的内部碎片问题,从而导致内存的资源利用率极低,因此操作系统开发人员又设计了另外一套内存分配机制用于分配小内存,这个机制的实现方法之一就是SLAB分配器。 5 | -------------------------------------------------------------------------------- /OperatingSystem/q7.md: -------------------------------------------------------------------------------- 1 | 为什么需要mmap? 2 | ---- 3 | 4 | 在操作系统内核里面,我们可以非常灵活的使用虚拟内存的页表,比如我们可以用Lazy Allocation的策略,当应用程序申请堆空间的时候不会立刻给它分配,从而将分配的时间移到了当真的需要读/写这个地址空间时候,发生页表中断,然后陷入内核,这个时候会为这个地址分配一个物理页并且进行页表的映射,这就是所谓的Lazy,再比如,操作系统会使用Copy-and-Write策略,当fork出一个进程的时候,不会立刻将父进程的所有地址空间拷贝,而是只申请页表空间,将子进程的页表与父进程的页表设置成一样的映射,这样只有发生需要写地址的时候会发生中断从而重新申请内存。那么用户程序能否也从灵活的虚拟内存中获得这样的收益呢? 5 | 6 | 基于这样的出发点,为了支持应用程序使用虚拟内存,*nix系统创建了一个也许也是最重要的一个系统调用,它叫做mmap。 7 | 8 | 下面我将会详细的介绍mmap,它的具体实现以及它的一些优点。 9 | 10 | -------------------------------------------------------------------------------- /OperatingSystem/q9.md: -------------------------------------------------------------------------------- 1 | 为什么说Linux没有线程的概念? 2 | ------- 3 | 4 | 5 | 学过操作系统的人都会知道,进程是资源分配的单元,线程是调度的基本单位,如果更深入一点的话,你也会听说一些观点,比如说在Linux是没有线程的概念的,如果你现在听的比较迷茫的话,那么恭喜你,这篇文章就是为你准备的。 6 | 7 | 首先树立一个观点,在Linux里面确实是没有线程这个概念的,也就是说,Linux并没有为线程这个概念抽象出来任何的数据结构,也未给线程实现任何的调度方案。这是为什么呢?因为在Linux诞生的时候,根本就还没有出现线程这个概念,但是随着历史进程的发展,人们渐渐意识到进程的一些缺陷(进程的切换实在是太太太耗时了,而且进程一旦切换TLB和Cache全部得刷新),从而发明出了线程这个概念,但是要修改一个操作系统来支持这个新概念,那可不是一件容易的事情,但是里面用了一些trick罢了,从Linux2.6的内核版本开始,支持了“线程”这个概念,那么到底是怎么实现的呢?其实仍然和原来一样,用进程这个数据结构来实现线程,怎么说呢?也就是说线程的数据结构跟线程一样。wtf?你在逗我?然而事实就是这样。CS嘛,只要满足了你的要求,你管我是怎么实现的,只要跟你所想的要求一致就可以了。当然我们这里说的都是内核线程。用户线程没啥好说的,直接用库实现就好了。所以,在Linux,线程也称为轻量级进程(LWP)。 8 | 9 | Linux是如何创建线程的呢?是通过`clone`这个系统调用,创建一个和父进程共享上下文的进程。等等,你是不是感觉跟`fork`好像?没错,就是很像,而且clone也直接或者间接用到了`fork`。再来说一下,为什么我还是感觉受到了欺骗,写过OS的人应该都用到了COW这种技术,也就是说,在实现`fork`的时候,我们不会将父进程的所有空间都拷贝,而是先跟父进程共享,怎么做到的呢?通过页表映射就可以了,将相同的虚拟地址映射到同一块物理空间去(没听说过?那快去看看COW吧),直到需要修改的时候,子进程才会重新申请内存然后修改,那么你会问了,既然有COW,那么是不是线程是不是也是通过COw实现的呢?nonono,虽然COW可以极大的改善性能,但是拷贝页表同样需要时间,而在创建线程的时候,我们甚至不需要拷贝页表,直接共用同一个页表就好啦!! 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Anyone who wants to join us, contact me and look forward for your contribution. 2 | ------- 3 | ------- 4 | 5 | 6 | 我们大概可以按照下面的顺序添加内容: 7 | 在该文件`README.md`下找到你的问题的相关课题,比如,我想创建一个问题叫做`为什么需要父进程来回收子进程的资源,子进程不能自己回收吗?`,该问题应该是属于操作系统相关的,于是我们在OperatingSystem这个目录项下面加上这个问题,然后再OperatingSystem这个文件夹下创建该问题的md文件就好。You can contribute by making a pull request~ If you want to join us and help us maintain this repo, I can add you to the collaborator list. And the most simple way is to new a discussion. 8 | ![image](https://user-images.githubusercontent.com/45984215/117623495-74f78c80-b1a6-11eb-929d-0901902d049c.png) 9 | 10 | **注意:这绝对不是一本求职者手册,如果你将该仓库定位到一份面经,那么很遗憾,这不是为你准备的,因为这里面的很多问题可能永远不会被面试官问到,这也绝对不能够作为一本教材,仅仅通过这些问题是完全不足以构造一个系统体系的,这充其量能够作为一个饭后甜点帮助你来更好的理解系统里面的一些知识点,我们希望能过通过这个仓库让读者们爱上计算机科学与技术,相信我,如果你现在认为计算机是一门很枯燥的课程,那么通过类似于十万个为什么的形式可能会让你重新爱上计算机,鉴于还有一部分外国读者,我们可能会考虑双语版,但是现阶段我们希望能够写出更多好的文章帮助读者理解系统设计的一些精妙的地方。我们保证全部免费,如果你觉得对你的学习有所帮助,那么欢迎你帮助我们推广,让更多的人加入我们,我们争取每周都能写一到两篇优质的文章出来。** 11 | 12 | 13 | ## Overview 14 | 15 | ### Compiler 16 | 1. [为什么目标文件中未初始化的全局/静态变量要使用COMMON块?](./Compiler/q1.md) 17 | 2. [为什么静态运行库里面一个目标文件只包含一个函数?](./Compiler/q2.md) 18 | 3. [为什么需要汇编器?]() 19 | 4. [为什么汇编器一般是两次扫描?]() 20 | 5. [为什么并发程序需要额外考虑优化等级?]() 21 | 22 | 23 | ### OperatingSystem 24 | 1. [为什么进程退出的时候没有内存泄漏?](./OperatingSystem/q1.md) 25 | 2. [分段真的是很糟粕的东西吗?]() 26 | 3. [为什么需要有memory allocator?]() 27 | 4. [为什么main“不是”程序的入口?](./OperatingSystem/q4.md) 28 | 5. [为什么需要多级页表,单级页表为何不妥?]() 29 | 6. [为什么有伙伴系统分配器还需要SLAB分配器?](./OperatingSystem/q6.md) 30 | 7. [为什么需要mmap?](./OperatingSystem/q7.md) 31 | 8. [为什么不用fork来创建线程?]() 32 | 9. [为什么说Linux没有线程的概念?](./OperatingSystem/q9.md) 33 | 10. [为什么用不同的优化级别编译程序最终输出的结果不一样?]() 34 | 11. [为什么要有 DMA 技术?](./OperatingSystem/q11.md) 35 | 36 | 37 | ### Database 38 | 39 | 1. [为什么需要DBMS?]() 40 | 2. 41 | 42 | ### Networking 43 | 44 | 45 | ### Architecture 46 | 1. [为什么C++里面的浮点数比较要用1e-6作基准?](./Architecture/q1.md) 47 | 2. [Why Way prediction can optimize Cache Performance?]() 48 | 3. 49 | 50 | 51 | 52 | 53 | ### Data Structure 54 | 55 | 1. [为什么并查集需要有路径压缩?]() 56 | 2. 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /link.md: -------------------------------------------------------------------------------- 1 | This file will contain my collections of some useful links. 2 | 3 | [carbon](https://carbon.now.sh/) 4 | Create and share beautiful images of your source code. 5 | 6 | [diagram](https://app.diagrams.net/) 7 | 8 | -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git add .; 4 | git commit -m "update on `date +'%Y-%m-%d %H:%M:%S'`"; 5 | git push origin lsy; --------------------------------------------------------------------------------