适用场景树状数组是一种适用于多次单点修改统计区间和问题的数据结构。基本思想Binary Indexed Tree 求和的基本思想在于，给定需要求和的位置 i ，例如 13 ，我们可以利用其二进制表示法来进行分段（或者说分层）求和：13 = 2^3 + 2^2 + 2^0，则prefixSum(13) = RANGE(1, 8) + RANGE(9, 12) + RANGE(13, 13) （注意此处的RANGE(x, y)表示数组中第x个位置到第y个位置的所有数字求和）。如下面例子中所示：arr = [1, 7, 3, 0, 5, 8, 3, 2, 6, 2, 1, 1, 4, 5] prefixSum(13) = RANGE(1, 8) + RANGE(9, 12) + RANGE(13, 13) = 29 + 10 + 4 = 43那么只要我们提前计算好这些RANGE(x,y)，便可以对相应的序列进行快速求和。下面是构造树状数组的方法（填坑法）：图中最上面是原数组，中间代表了树状数组各元素的意义，即RANGE(x,y)，而最下面便是原数组对应的树状数组。利用树状数组求区间和利用树状数组，我们可以发现：prefixSum(13) = prefixSum(0b00001101) = BIT[13] + BIT[12] + BIT[8] = BIT[0b00001101] + BIT[0b00001100] + BIT[0b00001000]其中BIT即树状数组，而从二进制位我们可以发现一个很有意思的结论，即我们实际上是在将下标13每次减少一个最低位的1所在位对应的数字，直到小于1为止，将这些下标对应的元素累加起来便是RANGE(1,13)的和。总结起来就像这样：这个结构可以抽象成一颗树一样：寻找父节点的操作便是lowbit ，也即上述所发现的结论，假设当前节点下标为 x ，则 lowbit(x) = x & -x。这样，我们就可以通过树状数组快速求区间和了。更新数组中的元素而关于如何进行单点修改，我们以update(5,2)为例：我们可以发现，单点修改的过程恰好与求区间和的过程相反，在此处，我们需要从5开始，对5进行lowbit的增加，直至超出数组范围，对所经过的下标对应的元素均进行累加，即可完成一次单点修改。树状数组的建立第一种方法很简单，遍历数组中的每一个元素，对其使用update(i + 1, elem)即可，这是一种O(nlog n)时间复杂度的构造方法第二种方法是一种时间复杂度O(n)的方法，其步骤如下(数组下标从0开始)：给定一个长度为n的输入数组list。初始化长度为n + 1的Binary Indexed Tree数组bit，并将list中的数字对应地放在bit[1]到bit[n]的各个位置。对于1到n的每一个i，进行如下操作：令j = i + (i & -i)，若j < n + 1，则bit[j] = bit[j] + bit[i]代码实现class BinaryIndexTree { private: int *tr; inline int lowbit(int x) { return x & -x; } public: BinaryIndexTree(int arr[], int n) { tr = new int[n + 1]; for (int i = 0; i < n; ++i) tr[i + 1] = arr[i]; for (int i = 1; i < n + 1; ++i) { int j = lowbit(i); if (j < n + 1) tr[j] += tr[i]; } } void add(int x, int v) { for(int i = x; i < n + 1; i += lowbit(i)) tr[i] += v; } int query(int x) { int ans = 0; for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i]; return ans; } }参考文章树状数组（Binary Indexed Tree），看这一篇就够了
函数调用实现原理小结+RISC-V架构学习笔记
从汇编语言的视角来看在函数调用的时候，需要有一条指令跳转到被调用函数的位置，这个看起来和其他控制结构没什么不同；但是在被调用函数返回的时候，我们却需要返回那条跳转过来的指令的下一条继续执行。这次用来返回的跳转究竟跳转到何处，在对应的函数调用发生之前是不知道的。比如，我们在两个不同的地方调用同一个函数，显然函数返回之后会回到不同的地址。这是一个很大的不同：其他控制流都只需要跳转到一个 编译期固定下来 的地址，而函数调用的返回跳转是跳转到一个 运行时确定 （确切地说是在函数调用发生的时候）的地址。对此，指令集必须给用于函数调用的跳转指令一些额外的能力，而不只是单纯的跳转。对于8086汇编来说，有一条伪指令符合该特征，也就是 call 伪指令，它会使得 sp 寄存器 减少对应的字节数，然后将当前的 IP 保存到栈顶（在执行当前指令之前，IP 就已经指向下一条指令了），然后使得 IP 指向被调用的函数地址。而 ret 伪指令则是该过程的逆过程，即将保存在栈内的返回地址出栈放入 IP 内。而在 RISC-V 架构上，有两条指令即符合这样的特征：指令指令功能$jal\ rd,\ imm[20:1]$$rd\leftarrow pc+4$$pc\leftarrow pc+imm$$jalr\ rd,\ (imm[11:0])rs$$rd\leftarrow pc+4$$pc\leftarrow rs+imm$[scode type="blue" size=""]RISC-V 指令各部分含义在大多数只与通用寄存器打交道的指令中， rs 表示 源寄存器 (Source Register)， imm 表示 立即数 (Immediate)，是一个常数，二者构成了指令的输入部分；而 rd 表示 目标寄存器 (Destination Register)，它是指令的输出部分。rs 和 rd 可以在 32 个通用寄存器 x0~x31 中选取。但是这三个部分都不是必须的，某些指令只有一种输入类型，另一些指令则没有输出部分。[/scode]从中可以看出，这两条指令在设置 pc 寄存器完成跳转功能之前，还将当前跳转指令的下一条指令地址保存在 rd 寄存器中，即 $rd\leftarrow pc+4$ 这条指令的含义。（这里假设所有指令的长度均为 4 字节）在 RISC-V 架构中，通常使用 ra 寄存器（即 x1 寄存器）作为其中的 rd 对应的具体寄存器，因此在函数返回的时候，只需跳转回 ra 所保存的地址即可。事实上在函数返回的时候我们常常使用一条 伪指令 (Pseudo Instruction) 跳转回调用之前的位置： ret 。它会被汇编器翻译为 jalr x0, 0(x1)，含义为跳转到寄存器 ra 保存的物理地址，由于 x0 是一个恒为 0 的寄存器，在 rd 中保存这一步被省略。总结一下，在进行函数调用的时候，我们通过 jalr 指令保存返回地址并实现跳转；而在函数即将返回的时候，则通过 ret 伪指令回到跳转之前的下一条指令继续执行。这样，RISC-V 的这两条指令就实现了函数调用流程的核心机制。可以发现，汇编指令在实现函数调用流程的核心机制大差不差，其思想都是在调用函数前保存返回地址，在函数调用结束时，恢复到之前的地址。在栈内保存函数地址还好办，但是像 RISC-V 架构这样，指令将地址保存在 ra 寄存器中，我们就得保证 ra 寄存器在函数调用过程中不被篡改了，单次调用还好说，但是在现实应用中，我们离不开函数 多层嵌套调用 的情形。我们很容易想象，如果函数不支持嵌套调用，那么编程将会变得多么复杂。如果我们试图在一个函数 f 中调用一个子函数，在跳转到子函数 g 的同时，ra 会被覆盖成这条跳转指令的下一条的地址，而 ra 之前所保存的函数 f 的返回地址将会 永久丢失 。因此，若想正确实现嵌套函数调用的控制流，我们必须通过某种方式保证：在一个函数调用子函数的前后，ra 寄存器的值不能发生变化。但实际上，这并不仅仅局限于 ra 一个寄存器，而是作用于所有的通用寄存器。这是因为，编译器是独立编译每个函数的，因此一个函数并不能知道它所调用的子函数修改了哪些寄存器。而站在一个函数的视角，在调用子函数的过程中某些寄存器的值被覆盖的确会对它接下来的执行产生影响。因此这是必要的。我们将由于函数调用，在控制流转移前后需要保持不变的寄存器集合称之为 函数调用上下文 (Function Call Context) 。由于每个 CPU 只有一套寄存器，我们若想在子函数调用前后保持函数调用上下文不变，就需要物理内存的帮助。确切的说，在调用子函数之前，我们需要在物理内存中的一个区域 保存 (Save) 函数调用上下文中的寄存器；而在函数执行完毕后，我们会从内存中同样的区域读取并 恢复 (Restore) 函数调用上下文中的寄存器。实际上，这一工作是由子函数的调用者和被调用者（也就是子函数自身）合作完成。函数调用上下文中的寄存器被分为如下两类：被调用者保存(Callee-Saved) 寄存器 ：被调用的函数可能会覆盖这些寄存器，需要被调用的函数来保存的寄存器，即由被调用的函数来保证在调用前后，这些寄存器保持不变；调用者保存(Caller-Saved) 寄存器 ：被调用的函数可能会覆盖这些寄存器，需要发起调用的函数来保存的寄存器，即由发起调用的函数来保证在调用前后，这些寄存器保持不变。从名字中可以看出，函数调用上下文由调用者和被调用者分别保存，其具体过程分别如下：调用函数：首先保存不希望在函数调用过程中发生变化的 调用者保存寄存器 ，然后通过 jal/jalr 指令调用子函数，返回之后恢复这些寄存器。被调用函数：在被调用函数的起始，先保存函数执行过程中被用到的 被调用者保存寄存器 ，然后执行函数，最后在函数退出之前恢复这些寄存器。我们发现无论是调用函数还是被调用函数，都会因调用行为而需要两段匹配的保存和恢复寄存器的汇编代码，可以分别将其称为 开场 (Prologue) 和 结尾 (Epilogue)，它们会由编译器帮我们自动插入，来完成相关寄存器的保存与恢复。一个函数既有可能作为调用者调用其他函数，也有可能作为被调用者被其他函数调用。[scode type="blue" size=""]寄存器保存与编译器优化这里值得说明的是，调用者和被调用者实际上只需分别按需保存调用者保存寄存器和被调用者保存寄存器的一个子集。对于调用函数而言，在调用子函数的时候，即使子函数修改了调用者保存寄存器，编译器在调用函数中插入的代码会恢复这些寄存器；而对于被调用函数而言，在其执行过程中没有使用到的被调用者保存寄存器也无需保存。编译器在进行后端代码生成时，知道在这两个场景中分别有哪些值得保存的寄存器。从这一角度也可以理解为何要将函数调用上下文分成两类：可以让编译器尽可能早地优化掉一些无用的寄存器保存与恢复操作，提高程序的执行性能。[/scode]调用规范调用规范 (Calling Convention) 约定在某个指令集架构上，某种编程语言的函数调用如何实现。它包括了以下内容：函数的输入参数和返回值如何传递；函数调用上下文中调用者/被调用者保存寄存器的划分；其他的在函数调用流程中对于寄存器的使用方法。调用规范是对于一种确定的编程语言来说的，因为一般意义上的函数调用只会在编程语言的内部进行。当一种语言想要调用用另一门编程语言编写的函数接口时，编译器就需要同时清楚两门语言的调用规范，并对寄存器的使用做出调整。[scode type="blue" size=""]RISC-V 架构上的 C 语言调用规范RISC-V 架构上的 C 语言调用规范可以在 这里 找到。 它对通用寄存器的使用做出了如下约定：寄存器组保存者功能a0~a7（x10~x17 ）调用者保存用来传递输入参数。其中的 a0 和 a1 还用来保存返回值。t0~t6(x5~x7,x28~x31 )调用者保存作为临时寄存器使用，在被调函数中可以随意使用无需保存。s0~s11(x8~x9,x18~x27 )被调用者保存作为临时寄存器使用，被调函数保存后才能在被调函数中使用。剩下的 5 个通用寄存器情况如下：zero( x0 ) 之前提到过，它恒为零，函数调用不会对它产生影响；ra( x1 ) 是调用者保存的，不过它并不会在每次调用子函数的时候都保存一次，而是在函数的开头和结尾保存/恢复即可。虽然 ra 看上去和其它被调用者保存寄存器保存的位置一样，但是它确实是调用者保存的。sp( x2 ) 是被调用者保存的。这个是之后就会提到的栈指针 (Stack Pointer) 寄存器。fp( s0 )，它既可作为s0临时寄存器，也可作为栈帧指针（Frame Pointer）寄存器，表示当前栈帧的起始位置，是一个被调用者保存寄存器。[/scode]其实光看上面的介绍，就应该知道，其实在 RISC-V 指令集中，对于函数调用上下文的保存也是需要利用到栈的，不过这里再补充几个关键点的解释。sp 寄存器常用来保存 栈指针 (Stack Pointer)，它指向内存中栈顶地址。在 RISC-V 架构中，栈是从高地址向低地址增长的。在一个函数中，作为起始的开场代码负责分配一块新的栈空间，即将 sp 的值减小相应的字节数即可，于是物理地址区间 $[新sp,旧sp)$ 对应的物理内存的一部分便可以被这个函数用来进行函数调用上下文的保存/恢复，这块物理内存被称为这个函数的 栈帧 (Stack frame)。同理，函数中的结尾代码负责将开场代码分配的栈帧回收，这也仅仅需要将 sp 的值增加相同的字节数回到分配之前的状态。这也可以解释为什么 sp 是一个被调用者保存寄存器。[scode type="blue" size=""]栈帧 stack frame我们知道程序在执行函数调用时，调用函数和被调用函数使用的是同一个栈。在通常的情况下，我们并不需要区分调用函数函数和被调用函数分别使用了栈的哪个部分。但是，当我们需要在执行过程中对函数调用进行调试或backtrace的时候，这一信息就很重要了。简单的说，栈帧（stack frame）就是一个函数所使用的栈的一部分区域，所有函数的的栈帧串起来就组成了一个完整的栈。一般而言，当前执行函数的栈帧的两个边界分别由栈指针 (Stack Pointer)寄存器和栈帧指针（frame pointer）寄存器来限定。[/scode]在合适的编译选项设置之下，一个函数的栈帧内容可能如下图所示：它的开头和结尾分别在 sp(x2) 和 fp(s0) 所指向的地址。按照地址从高到低分别有以下内容，它们都是通过 sp 加上一个偏移量来访问的：ra 寄存器保存其返回之后的跳转地址，是一个调用者保存寄存器；父亲栈帧的结束地址 fp ，是一个被调用者保存寄存器；其他被调用者保存寄存器 s1 ~ s11 ；函数所使用到的局部变量。因此，栈上多个 fp 信息实际上保存了一条完整的函数调用链，通过适当的方式我们可以实现对函数调用关系的跟踪。至此，就基本上说明了函数调用是如何基于栈来实现的。
最近看了操作系统相关的知识，结合上《程序员的自我修养》这本书所讲的链接装载与库，感觉对于一个程序从编译，到汇编转换成机器码，再到多目标文件与库的链接，是一个非常复杂的过程。所以想要浅显易懂的总结一下，也刚好是找到了一个算是浅显易懂的总结，所以在自己的理解上再进行了一些润色。编译流程从源代码得到可执行文件的编译流程可被细化为多个阶段（虽然输入一条命令便可将它们全部完成）：编译器 (Compiler) 将每个源文件从某种高级编程语言转化为汇编语言，注意此时源文件仍然是一个 ASCII 或其他编码的文本文件；汇编器 (Assembler) 将上一步的每个源文件中的文本格式的汇编指令转化为机器码，得到一个二进制的 目标文件 (Object File)；链接器 (Linker) 将上一步得到的所有目标文件以及一些可能的外部目标文件链接在一起形成一个完整的可执行文件。汇编器输出的每个目标文件都有一个独立的程序内存布局，它描述了目标文件内各段所在的位置。而链接器所做的事情是将所有输入的目标文件整合成一个整体的内存布局。在此期间链接器主要完成两件事情：第一件事情是将来自不同目标文件的段在目标内存布局中重新排布。如下图所示，在链接过程中，分别来自于目标文件 1.o 和 2.o 段被按照段的功能进行分类，相同功能的段被排在一起放在拼装后的目标文件 output.o 中。注意到，目标文件 1.o 和 2.o 的内存布局是存在冲突的，同一个地址在不同的内存布局中存放不同的内容。而在合并后的内存布局中，这些冲突被消除。第二件事情是将符号替换为具体地址。这里的符号指什么呢？我们知道，在我们进行模块化编程的时候，每个模块都会提供一些向其他模块公开的全局变量、函数等供其他模块访问，也会访问其他模块向它公开的内容。要访问一个变量或者调用一个函数，在源代码级别我们只需知道它们的名字即可，这些名字被我们称为符号。取决于符号来自于模块内部还是其他模块，我们还可以进一步将符号分成内部符号和外部符号。然而，在机器码级别（也即在目标文件或可执行文件中）我们并不是通过符号来找到索引我们想要访问的变量或函数，而是直接通过变量或函数的地址。例如，如果想调用一个函数，那么在指令的机器码中我们可以找到函数入口的绝对地址或者相对于当前 PC 的相对地址。[scode type="blue" size=""]PC (Program Counter) 程序计数器，用来存放当前欲执行指令的地址。可自动形成指向下一条指令的地址。一般来讲，这其实是一种行为的代称而不是真实存在的寄存器，在 x86 体系，一般是由 CS:IP （段地址：偏移地址）的组合来构成这个逻辑，而在 ARM 体系中 R15 就是 PC，当然 ARM 和 IA-32、x64 都支持高级内存管理，所以 PC 的内容未必是当前指令在内存中的绝对位置。而且所谓的每次增加1，其实是逻辑上的增加1，并不代表内存地址的增量为1，这受到指令长度的影响。[/scode]那么，符号何时被替换为具体地址呢？因为符号对应的变量或函数都是放在某个段里面的固定位置（如 全局变量往往放在 .bss 或者 .data 段中，而函数则放在 .text 段中），所以我们需要等待符号所在的段确定了它们在内存布局中的位置之后才能知道它们确切的地址。当一个模块被转化为目标文件之后，它的内部符号就已经在目标文件中被转化为具体的地址了，因为目标文件给出了模块的内存布局，也就意味着模块内的各个段的位置已经被确定了。然而，此时模块所用到的外部符号的地址无法确定。我们需要将这些外部符号记录下来，放在目标文件一个名为符号表（Symbol table）的区域内(对于ELF文件一般在.symtab)。由于后续可能还需要重定位，内部符号也同样需要被记录在符号表中。外部符号需要等到链接的时候才能被转化为具体地址。假设模块 1 用到了模块 2 提供的内容，当两个模块的目标文件链接到一起的时候，它们的内存布局会被合并，也就意味着两个模块的各个段的位置均被确定下来。此时，模块 1 用到的来自模块 2 的外部符号可以被转化为具体地址。同时我们还需要注意：两个模块的段在合并后的内存布局中被重新排布，其最终的位置有可能和它们在模块自身的局部内存布局中的位置相比已经发生了变化。因此，每个模块的内部符号的地址也有可能会发生变化，我们也需要进行修正。上面的过程被称为重定位（Relocation），这个过程形象一些来说很像拼图：由于模块 1 用到了模块 2 的内容，因此二者分别相当于一块凹进和凸出一部分的拼图，正因如此我们可以将它们无缝地拼接到一起。
操作系统学习笔记（4）—— 操作系统的特征
操作系统的特征从总体上看，操作系统具有五个方面的特征：虚拟化 (Virtualization)、并发性 (Concurrency)、异步性、共享性和持久性 (Persistency)。操作系统的虚拟化可以理解为它对内存、CPU 的抽象和处理；并发性和共享性可以理解为操作系统支持多个应用程序“同时”运行；异步性可以从操作系统调度、中断处理对应用程序执行造成的影响等几个方面来理解；持久性则可以从操作系统中的文件系统支持把数据方便地从磁盘等存储介质上存入和取出来理解。虚拟性内存虚拟化程序员在写应用程序的时候，不用考虑其程序的起始内存地址要放到计算机内存的具体某个位置，而是用字符串符号定义了各种变量和函数，直接在代码中便捷地使用这些符号就行了。是由于操作系统建立了一个 地址固定 ， 空间巨大 的虚拟内存给应用程序来运行，这是 内存虚拟化 ，其实是一种 “空间虚拟化” ， 可进一步细分为 内存地址虚拟化 和 内存大小虚拟化 。这里的每个符号在运行时是要对应到具体的内存地址的。这些内存地址的具体数值是什么？程序员不用关心。为什么？因为编译器会自动帮我们把这些符号翻译成地址，形成可执行程序。程序使用的内存是否占得太大了？在一般情况下，程序员也不用关心。[scode type="blue" size=""]实际上，编译器 (Compiler，比如 gcc) 和链接器 (linker，比如 ld) 也不知道程序每个符号对应的地址应该放在未来程序运行时的哪个物理内存地址中。所以，编译器的一个简单处理办法就是，设定一个固定地址（比如 0x10000）作为起始地址开始存放代码，代码之后是数据，所有变量和函数的符号都在这个起始地址之后的某个固定偏移位置。假定程序每次运行都是位于一个不会变化的起始地址。这里的变量指的是全局变量，其地址在编译链接后会确定不变。但局部变量是放在堆栈中的，会随着堆栈大小的动态变化而变化。这里编译器产生的地址就是虚拟地址。这里，编译器和链接器图省事，找了一个适合它们的解决办法。当程序要运行的时候，这个符号所对应的虚拟内存地址到计算机的物理内存地址的映射必须要解决了，于是这个任务便推到了操作系统身上。操作系统会把编译器和链接器生成的执行代码和数据放到空闲的物理内存中，并建立虚拟地址到物理地址的映射关系。由于物理内存中的空闲区域是动态变化的，这导致虚拟地址到物理地址的映射关系也是动态变化的，需要操作系统来维护好可变的映射关系，确保编译器“固定起始地址”的假设成立。只有操作系统维护好了这个映射关系，才能让程序员只需写一些易于人理解的字符串符号来代表一个内存空间地址，且编译器只需确定一个固定地址作为程序的起始地址就可以不用考虑将来这个程序要在哪里运行的问题，从而实现了 内存地址虚拟化 。在这里，虚拟地址到物理地址的转换关系由操作系统与 MMU(Memory Management Unit) 进行交互从而处理映射关系。[/scode]应用程序在运行时不用考虑当前物理内存是否够用。如果应用程序需要一定空间的内存，但由于在某些情况下，物理内存的空闲空间可能不多了，这时操作系统通过把物理内存中最近没使用的空间（不是空闲的，只是最近用得少）换出（就是“挪地”）到硬盘上暂时缓存起来，这样空闲空间就大了，就可以满足应用程序的运行时内存需求了，从而实现了 内存大小虚拟化 。[scode type="green" size=""]这里其实就涉及到了内存页的概念，通过将内存按页面大小的映射，从而实现了方便的装载与卸载，等到需要使用时再重新建立映射关系[/scode]CPU虚拟化不同的应用程序可以在内存中并发运行，相同的应用程序也可有多个拷贝在内存中并发运行。而每个程序都“认为”自己完全独占了 CPU 在运行，这是 “CPU虚拟化” ，也是一种 “时间虚拟化” 。操作系统给了运行的应用程序一个幻象，即操作系统把时间分成小段，每个应用程序占用其中一小段时间片运行，用完这一时间片后，操作系统会切换到另外一个应用程序，让它运行。由于时间片很短，操作系统的切换开销也很小，应用程序或使用应用程序的用户基本上是看不出的，反而感觉到多个程序各自在独立“并行”执行，从而实现了 CPU虚拟化 。[scode type="yellow" size=""]并行 (Parallel) 是指两个或者多个事件在同一时刻发生；并发 (Concurrent) 是指两个或多个事件在同一时间间隔内发生。对于基于单 CPU 的计算机而言，各个“同时”运行的程序其实是串行分时复用一个 CPU ，任一个时刻点上只有一个程序在 CPU 上运行。 这些虚拟性的特征给应用程序的开发和执行提供了非常方便的执行环境，但也给操作系统的设计与实现提出了很多挑战。[/scode]并发性操作系统为了能够让 CPU 充分地忙起来，并充分利用各种资源，就需要有多种不同的应用程序在执行。这些应用程序是分时执行的，并由操作系统来完成各个应用在运行时的任务切换。并发性虽然能有效改善系统资源的利用率，但也带来了对共享资源的争夺问题，即同步互斥问题。还会带来执行时间的不确定性问题，即并发程序在执行中是走走停停，断续推进的，使得应用程序的完成时间是不确定的。并发性对操作系统的设计也带来了很多挑战，一不小心就会出现程序执行结果不确定，程序死锁等很难调试和重现的问题。异步性在这里，异步是指由于操作系统的调度和中断等，会不时地暂停或打断当前正在运行的程序，使得程序的整个运行过程走走停停。在应用程序运行的表现上，特别它的执行完成时间是不可预测的。但需要注意，只要应用程序的输入是一致的，那么它的输出结果应该是符合预期的。共享性共享是指多个应用并发运行时，宏观上体现出它们可同时访问同一个资源，即这个资源可被共享。但其实在微观上，操作系统在硬件等的支持下要确保应用程序互斥访问这个共享的资源。比如，对于两个应用同时访问同一个内存单元的情况，从宏观的应用层面上看，二者都能正确地读出同一个内存单元的内容；而在微观上，操作系统会调度应用程序的先后执行顺序，确保在任何一个时刻，只有一个应用去访问存储单元。持久性操作系统提供了文件系统来从可持久保存的存储介质（磁盘， SSD 等，以后以硬盘来代表）中取数据和代码到内存中，并可以把内存中的数据写回到硬盘上。硬盘在这里是外设，具有持久性，以文件系统的形式呈现给应用程序。[scode type="share" size=""]文件系统也可看成是操作系统对存储外设（如硬盘、SSD 等）的虚拟化。 这种持久性的特征进一步带来了共享属性，即在文件系统中的文件可以被多个运行的程序所访问，从而给应用程序之间实现数据共享提供了方便。即使掉电，存储外设上的数据还不会丢失，可以在下一次机器加电后提供给运行的程序使用。持久性对操作系统的执行效率提出了挑战，如何让数据在高速的内存和慢速的硬盘间高效流动是需要操作系统考虑的问题。[/scode]
站在用户和应用程序的角度看操作系统操作系统内核是一个需要提供各种服务的软件，其服务对象是应用程序，而用户（这里可以理解为一般使用计算机的人）是通过应用程序的服务间接获得操作系统的服务的，因此操作系统内核藏在一般用户看不到的地方。但应用程序需要访问操作系统获得操作系统的服务，这就需要通过操作系统的接口才能完成。操作系统的接口形式就是应用程序二进制接口 (ABI, Application Binary Interface)。操作系统不能只提供面向单一编程语言的函数库的编程接口 (API, Application Programming Interface) ，它的接口需要考虑对基于各种编程语言的应用支持，以及访问安全等因素，使得应用软件不能像访问函数库一样的直接访问操作系统内部函数，更不能直接读写操作系统内部的地址空间。为此，操作系统设计了一套安全可靠的接口，我们称为系统调用接口 (System Call Interface)。系统调用接口通常面向应用程序提供了API的描述，但在具体实现上，还需要提供ABI的接口描述规范。[scode type="blue"]一些思考：提供ABI规范是便于底层开发人员进行驱动程序的开发，以及进行汇编级别的优化和高级编程语言运行库的开发等操作[/scode]在现代处理器的安全支持（特权级隔离，内存空间隔离等）下，应用程序就不能直接以函数调用的方式访问操作系统的函数，以及直接读写操作系统的数据变量。[scode type="red"]这里的不能直接以函数调用的方式访问操作系统的函数，我认为指的是操作系统内部使用的函数而非暴露出来的系统调用接口[/scode]不同类型的应用程序可以通过符合操作系统规定的ABI规范的系统调用接口，发出系统调用请求，来获得操作系统的服务。操作系统提供完服务后，返回应用程序继续执行。[scode type="share"]API 与 ABI 的区别应用程序二进制接口 ABI 是不同二进制代码片段的连接纽带。ABI 定义了二进制机器代码级别的规则，主要包括基本数据类型、通用寄存器的使用、参数的传递规则、以及堆栈的使用等等。ABI 与处理器和内存地址等硬件架构相关，是用来约束链接器 (Linker) 和汇编器 (Assembler) 的。在同一处理器下，基于不同高级语言编写的应用程序、库和操作系统，如果遵循同样的 ABI 定义，那么它们就能正确链接和执行。应用程序编程接口 API 是不同源代码片段的连接纽带。API 定义了一个源码级（如 C 语言）函数的参数，参数的类型，函数的返回值等。因此 API 是用来约束编译器 (Compiler) 的：一个 API 是给编译器的一些指令，它规定了源代码可以做以及不可以做哪些事。API 与编程语言相关，如 libc 是基于 C 语言编写的标准库，那么基于 C 的应用程序就可以通过编译器建立与 libc 的联系，并能在运行中正确访问 libc 中的函数。简单来说，API是编译器级别的，ABI是汇编级别的。[/scode]系统调用接口的一些比较重要的功能与相应的抽象对于系统调用接口提供的功能，下面列出了一些值得了解的功能：一个运行的程序可以创建另一个程序的实例吗？需要等待另外一个程序执行完成吗？操作系统能停止或恢复另一个正在运行的程序吗？一个运行的程序如何与连接到计算机的设备通信并通过它们与物理世界通信？多个运行的程序如何同步互斥地对共享资源进行访问？一个运行的程序可以要求更多（或更少）的内存空间吗？一个运行的程序如何持久地存储用户数据？一个运行的程序如何能输出字符信息？如何能获得输入字符信息？下面列出了一些相对比较重要的操作系统接口或抽象：进程（即程序运行过程）管理：复制创建进程 fork 、退出进程 exit 、执行进程 exec 等。线程管理：线程（即程序的一个执行流）的创建、执行、调度切换等。线程同步互斥的并发控制：互斥锁 mutex 、信号量 semaphore 、管程 monitor 、条件变量 condition variable 等。进程间通信：管道 pipe 、信号 signal 、事件 event 等。虚存管理：内存空间映射 mmap 、改变数据段地址空间大小 sbrk 、共享内存 shm 等。文件I/O操作：对存储设备中的文件进行读 read 、写 write 、打开 open 、关闭 close 等操作。外设I/O操作：外设包括键盘、显示器、串口、磁盘、时钟 … 但接口均采用了文件 I/O 操作的通用系统调用接口。[scode type="share"]上述表述在某种程度上说明了操作系统对计算机硬件重要组成的抽象和虚拟化，使得应用程序只需基于对简单的抽象概念的访问来达到对计算机系统资源的使用：文件 (File) 是外设的一种抽象和虚拟化。特别对于存储外设而言，文件是持久存储的抽象。地址空间 (Address Space) 是对内存的抽象和虚拟化。进程 (Process) 是对计算机资源的抽象和虚拟化。而其中最核心的部分是对CPU的抽象与虚拟化。[/scode]有了这些系统调用接口，简单的应用程序就不用考虑底层硬件细节，可以在操作系统的服务支持和管理下简洁地完成其应用功能了。
操作系统学习笔记（1）——什么是操作系统
前言最近找到一个好文档，关于如何用Rust编写一个操作系统，里面系统的讲述了操作系统的相关知识和源代码等等，所以就打算跟着这个文档开始学习操作系统相关的知识(之后就可以去读CSAPP了从认识来看一个操作系统（OS）是一个软件，它帮助用户和应用程序使用和管理计算机的资源。操作系统可能对最终用户不可见，但控制着嵌入式设备、更通用的系统（如智能手机、台式计算机和服务器）以及巨型机等各种计算机系统。从功能开始操作系统主要做的事情就只有两件：一是向下管理计算机硬件和各种外设，二是向上给应用软件提供各种服务帮助。进一步描述操作系统是一个可以管理CPU、内存和各种外设，并管理和服务应用软件的系统软件。[scode type="share"]系统软件是在一个计算机系统范围内使用的软件，可涉及到整个计算机系统。文本编辑器这样的软件只涉及到对文本文件的编辑，并不能被叫做系统软件。而在计算机中安装的 C 标准库 libc（类似的有 Rust 标准库 等）可以算是一个，因为它提供了与OS交互的系统调用接口，其功能覆盖了整个计算机系统，会被许多不同的软件访问。[/scode]为了完成这些工作，操作系统需要知道如何与硬件打交道，如何更好地给应用软件提供服务，这就有一系列操作系统相关的理论、抽象、设计等来支持如何做和做得好的需求。[scode type="blue"]在不同的应用场景下，操作系统的边界也是不同的，我们可以把运行时库、图形界面支持库等这些可支持不同应用的系统软件 (System Software) 也看成是操作系统的一部分。比如说嵌入式环境，一般操作系统以运行时库的形式与应用程序紧密结合在一起，形成一个可在嵌入式硬件上单一执行的嵌入式应用。而 Android 应用运行在 ARM 处理器上 Android 操作系统的执行环境中。[/scode]简化描述站在应用程序的角度来看，我们可以发现常见的应用程序其实是运行在由硬件、操作系统内核、运行时库、图形界面支持库等所包起来的一个 执行环境 (Execution Environment) 中，应用程序只需根据与系统软件约定好的应用程序二进制接口 (ABI, Application Binary Interface) 来请求执行环境提供的各种服务或功能，从而完成应用程序自己的功能。于是我们可以将操作系统的定义简化成：应用程序的软件执行环境。一直在变化的定义操作系统的内涵和外延随着历史的发展也一直在变化，并没有类似于“1+1=2”这样的明确定义。如今在二十一世纪初期的大众眼中，操作系统就是他们的手机/终端上的软件系统，包括各种应用程序集合，图形界面和网络浏览器是其中重要的组成部分。但无论操作系统的内在实现和具体目标如何变化，其管理计算机硬件，给应用提供服务的核心定位没有变化。
[scode type="share"]上篇文章写的有些简略，而且有些地方在实际开发中还是有一定问题存在的，所以就有了这个进阶篇，这篇文章也是因为最近在搞前端开发，所以顺便总结一下[/scode]大纲首先我先说明一下这次使用的技术栈：less css预处理器，知名的还有sass、scss、stylus等react 主要的开发依赖库typescript 提供静态类型等诸多好处webpack 打包工具webpack-dev-server 开发用服务器react-router 路由管理库axios 网络请求库babel es新特性等代码的polyfill以这些作为主要的开发工具和生产依赖，除此之外还有一些关于webpack打包的插件，babel的插件之类的都会在后续进行一一说明步骤首先，我们从新建文件夹开始mkdir my-app \ && cd my-app \ && npm init -y然后我们就开始装依赖了npm install react react-dom react-router-dom axios --save-exact以上是几个生产环境所需要的依赖，所以放在一起进行安装，这里的--save-exact是为了把下载下来的依赖库版本固定，从而避免不同人开发时因为版本更新导致的诸多问题现在我们开始安装开发方面的依赖库，这里要使用一个参数--save-dev，说明这些依赖只在开发时使用npm install @babel/core @babel/plugin-transform-runtime @babel/preset-env @babel/preset-react @babel/runtime @babel/preset-typescript --save-dev \ && npm install webpack webpack-cli webpack-dev-middleware webpack-dev-server webpack-merge --save-dev \ && npm install typescript @types/react @types/react-dom @types/webpack @types/node ts-node --save-dev \ && npm install less less-loader css-loader babel-loader style-loader --save-dev \ && npm install clean-webpack-plugin css-minimizer-webpack-plugin fork-ts-checker-webpack-plugin html-webpack-plugin mini-css-extract-plugin --save-dev这里可以看到，有非常多的开发方面的依赖，在这里我一行一行的进行解释首先第一行，我们可以看到都是关于babel方面的一些库，下面是它们的作用@babel/core babel的核心库@babel/plugin-transform-runtime 和 @babel/runtime 提供对async/await功能的支持@babel/preset-env 提供对ES规范新功能的支持@babel/preset-react 提供对react相关代码规范的支持@babel/preset-typescript 提供对typescript代码的支持babel主要的作用就是为了提供对一些浏览器不支持的语法进行降级转换的作用，使得其可以在浏览器运行然后是第二行，关于webpack的一些功能模块webpack 和 webpack-cli 一个是核心库，一个是命令行支持webpack-dev-middleware 和 webpack-dev-server 对开发环境提供热替换开发服务器功能webpack-merge 用于对配置文件的merge，方便进行配置文件的区分管理（ts原生也可以合并，但是这个比较智能）第三行，关于typescript方面的一些模块声明文件还有typescript自身，这里就不做详细解释了第四行，提供了less支持和一些webpack读取相关文件的相应loader第五行，关于webpack的一些插件clean-webpack-plugin 在打包生成文件时会对之前的文件进行相应的清理操作，主要是为了修复webpack-dev-middleware在启用writeToDisk功能时，webpack原生配置启用自动清理却无法清理的bugcss-minimizer-webpack-plugin 对打包生成的css进行最小化压缩处理fork-ts-checker-webpack-plugin 提供在打包编译前先对源代码进行ts类型检查的功能html-webpack-plugin 提供对html模板的处理功能，比如改改标题，挂载打包生成的js和css文件之类的mini-css-extract-plugin 将 CSS 提取到单独的文件中，为每个包含 CSS 的 JS 文件创建一个 CSS 文件，并且支持 CSS 和 SourceMaps 的按需加载。[scode type="blue"]这里的SourceMaps实际上与压缩时采用的一种技术有关，因为压缩后生成的文件的可读性非常差，而SourceMaps其实就相当于一本密码本可以把压缩前的源文件给复原出来，从而方便开发人员调试，具体压缩时采用了VLQ编码，感兴趣的同学可以去看看阮一峰的这篇文章[/scode]到这里，我们就把所安装的所有依赖给说明完毕了，接下来就是这些依赖的配置篇了配置babel的相关配置我们首先在项目根目录下创建一个.babelrc文件然后填入以下内容{ "presets": [ "@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript" ], "plugins": [ [ "@babel/plugin-transform-runtime", { "regenerator": true } ] ] }很容易理解，上面三项是关于上述的一些预设的转换规则的填入，而这里的plugin中的regenerator配置项，是有关js的generator函数方面的，也即形如下面的函数function* name([param[, param[, ... param]]]) { statements }这方面的内容可以参考MDN这里的配置主要是为了防止形如上述的函数污染全局变量域webpack的配置因为我们引入了webpack的types和ts-node，所以我们可以使用typescript进行webpack相关配置文件的编写，同时因为我们引入了webpack-merge，我们可以针对生产环境和开发环境采用不同的配置文件。所以我们可以创建webpack.common.ts webpack.dev.ts和webpack.prod.ts三个文件以下是common相关配置import HtmlWebpackPlugin from "html-webpack-plugin"; import { CleanWebpackPlugin } from "clean-webpack-plugin"; import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin"; import path from "path"; import { Configuration } from "webpack"; const commonConfig: Configuration = { entry: { app: ["./src/index.tsx"], }, output: { path: path.resolve(__dirname, "dist"), }, resolve: { alias: { "@": path.resolve(__dirname, "src"), "@component": path.resolve(__dirname, "src/components"), "@page": path.resolve(__dirname, "src/pages"), "@lib": path.resolve(__dirname, "src/libs"), }, extensions: [".ts", ".tsx", ".js", "jsx", ".json"], }, module: { rules: [ { test: /\.(ts|js)x?$/, exclude: /node_modules/, use: { loader: "babel-loader", options: { presets: [ "@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript", ], }, }, }, { test: /\.(png|jpg|gif)$/, type: "asset/resource", generator: { filename: "images/[hash][ext][query]", }, }, { test: /\.svg$/, type: "asset/inline", }, ], }, plugins: [ new HtmlWebpackPlugin({ title: "my_app", filename: "index.html", template: "./public/index.html", }), new CleanWebpackPlugin(), new ForkTsCheckerWebpackPlugin({ async: false, //set this to false so that Webpack waits for the type checking process to finish before it emits any code. }), ], }; export default commonConfig;可以看到，我们这里引入了三个plugin，作用在上面已经解释过了然后我们看到entry就是相关的入口文件，在react里面，我们一般就会有以下的入口文件定义import * as React from "react"; import * as ReactDOM from "react-dom"; import { BrowserRouter } from "react-router-dom"; import App from "@/App"; ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById("app") );然后关于相关的html模板则会有如下的定义 <%= htmlWebpackPlugin.options.title %> 这里有一个<%= htmlWebpackPlugin.options.title %>，这个是为了能够让html-webpack-plugin中配置的标题能够注入进模板中而这里关于所谓的@别名其实就与webpack中配置的resolve中的alias有关了，具体配置方法可以参照以上配置文件，但是需要注意的是，仅仅只在webpack配置文件配置这个别名是不够的，还必须在tsconfig.json中有相关的配置，这里给出tsconfig的配置文件{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"], "@page/*": ["src/pages/*"], "@component/*": ["src/components/*"], "@lib/*": ["src/libs/*"] }, "lib": [ "dom", "DOM.Iterable", "esnext" ] /* Specify library files to be included in the compilation. */, "allowJs": true, "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, "sourceMap": true /* Generates corresponding '.map' file. */, "outDir": "./dist/" /* Redirect output structure to the directory. */, "removeComments": false /* Do not emit comments to output. */, "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, "preserveConstEnums": true, "resolveJsonModule": true, "moduleResolution": "node" /* Specify module resolution strategy: 'node', 'classic' or 'none'. */, "forceConsistentCasingInFileNames": true /* Ensures that the casing of referenced file names is consistent during the type checking process. */, "skipLibCheck": false, "isolatedModules": true, "strict": true, "noEmit": true, "typeRoots": [ "node_modules/@types" ] /* List of folders to include type definitions from. */ }, "include": ["src/**/*"] }可以看到，我们需要在paths中配置相关的别名，这里主要解释几个配置项，一个是 include ，配置我们需要读取的源文件在哪里，还有就是 lib ，告诉编译器我们需要编译出的js代码的对象是提供给浏览器使用的，还有就是编译成最新的es规范代码，其余的一些配置项详情可以在以下页面进行查看然后回到我们的webpack配置，可以看到module里面有关于不同文件的不同处理方式，这些东西大多参照自webpack官方文档，没有什么非常需要解释的，总的来说，就是对匹配到的不同类型的文件进行不同的打包处理，而关于 resolve 下的 extensions ，其实就是定义了同名文件的解析顺序，如果有多个文件的名字相同而类型不同，就会按照定义的顺序进行解析，如果解析成功就会跳过之后的文件后缀名。然后再来讲讲开发环境下webpack的特殊配置import { merge } from "webpack-merge"; import { Configuration as WebpackConfiguration } from "webpack"; import { Configuration as WebpackDevServerConfiguration } from "webpack-dev-server"; import commonConfig from "./webpack.common"; interface Configuration extends WebpackConfiguration { devServer?: WebpackDevServerConfiguration; } const devConfig: Configuration = merge(commonConfig, { mode: "development", output: { filename: "js/[name].[hash].bundle.js", }, module: { rules: [ { test: /\.css$/i, use: ["style-loader", "css-loader"], }, { test: /\.less$/i, use: ["style-loader", "css-loader", "less-loader"], }, ], }, devtool: "inline-source-map", optimization: { runtimeChunk: "single", }, devServer: { historyApiFallback: true, devMiddleware: { writeToDisk: true, }, hot: true, }, }); export default devConfig;这里由于webpack自身和webpack-dev-server的配置文件类型中有所不同，所以为了包含webpack-dev-server的配置文件，在这里自己定义了一个接口来进行整合处理。然后这里关于dev-server的配置，historyApiFallback是为了适配react-router的特殊路由系统，hot则是启用HMR（热模块重载）功能，然后就是重新定义了输出的js文件名和关于样式文件的处理方式与生产环境有所不同import { merge } from "webpack-merge"; import commonConfig from "./webpack.common"; import CssMinimizerPlugin from "css-minimizer-webpack-plugin"; import MiniCssExtractPlugin from "mini-css-extract-plugin"; import { Configuration } from "webpack"; const prodConfig: Configuration = merge(commonConfig, { devtool: "source-map", mode: "production", output: { filename: "js/[name].[contenthash].bundle.js", }, module: { rules: [ { test: /\.css$/i, use: [MiniCssExtractPlugin.loader, "css-loader"], }, { test: /\.less$/i, use: [MiniCssExtractPlugin.loader, "css-loader", "less-loader"], }, ], }, plugins: [ new MiniCssExtractPlugin({ filename: "css/[name].css", }), ], optimization: { minimizer: [new CssMinimizerPlugin()], splitChunks: { chunks: "all", }, }, }); export default prodConfig;总体来讲，这两种配置的差异主要就是输出的js文件命名方式不同，还有样式文件的输出方式不同，除此之外就只有sourcemap的生成方式不同配置完了这些，我们就可以来配置几个快速指令了，打开package.json，在scripts写入以下配置{ "start": "webpack-dev-server --open --config webpack.dev.ts", "build": "webpack --config webpack.prod.ts" }然后我们就可以使用npm start来启动这个项目了，当然在此之前，我们还得写一个App.tsx，我想这个应该就不用赘述了。效率提高为了统一的代码风格，我们可以引入prettier进行代码格式化管理npm install --save-dev prettier然后在项目根目录创建.prettierrc.json和.prettierignore.prettierrc.json写入以下内容{ "trailingComma": "es5", "tabWidth": 4, "semi": true, "singleQuote": false }.prettierignore填入node_modules # Ignore artifacts: dist关于prettier的具体配置可以查看官网最后这段时间也是正好在写一个前端的项目，自己也翻了无数的资料文档，最终才总结出来了这么一些配置，前端的工具真的是太多太多了，所以也是让人看的眼花缭乱，光是一个webpack的配置就已经非常的复杂了，我看了好几天的webpack文档，通过一点点的去尝试修改，才最终获得了一个满意的开发环境。可以说，从零开始配置前端开发环境是真的非常困难的一件事情，在如今这样一个前端工具漫天飞的时代，也许脚手架才是更好的选择，但是偶尔停下脚步，去探究一下黑盒之下的东西，也是为了更好的理解脚手架的工作，以及懂得脚手架带来的便利是一个多么伟大的进步。
ps：很久没写博客，最近学了一些前端，觉得还是需要沉淀一些东西，于是便有了从零开始构建react开发环境的这篇文章开始初始化项目新建一个my-app文件夹mkdir my-app进入my-app文件夹cd my-app然后在my-app目录下创建src文件夹用来存放React代码mkdir src使用npm初始化项目npm init -y 此时的项目架构如下：安装typescript和react安装typescript全局安装是为了使用tsc命令，开发安装是必须的npm install -g typescript # 全局安装 npm install typescript --save-dev[scode type="blue"]注意：在这里简单介绍一下npm install命令的参数 --save-dev 和 --save的区别，一般来说使用--save-dev参数安装的npm包在最终打包的时候不会被包括到源码里去，所以类似babel和webpack这种进行项目工程构建或者代码编译的库应该用--save-dev来安装，而--save则是安装代码运行必须的库，比如react等。[/scode]配置typescript安装好之后输入tsc --init打开 tsconfig.json，进行以下配置。这将告诉 TypeScript 在哪找到我们的代码。{ "compilerOptions": { "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ "lib": ["es5", "es6", "dom"], /* Specify library files to be included in the compilation. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ "sourceMap": true, /* Generates corresponding '.map' file. */ "outDir": "./dist/", /* Redirect output structure to the directory. */ "removeComments": true, /* Do not emit comments to output. */ "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ "preserveConstEnums": true }, "include": [ "./src/**/*" ] }安装react和react的typescript支持npm install react react-dom --save && npm install @types/react --save-dev && npm install @types/react-dom --save-dev安装webpack在终端输入以下指令npm install webpack --save-dev && npm install webpack-cli --save-dev && npm install webpack-dev-server --save-dev && npm install ts-loader --save-dev && npm install html-webpack-plugin --save-dev现在，除了 Webpack ，我们还安装了另外4个开发依赖。webpack-cli - 在终端提供有用的 Webpack 命令。webpack-dev-server - 为我们的应用提供服务端的支持，如果我们修改代码，可以帮我们刷新浏览器的页面（热更新）。ts-loader - 读取tsconfig.json的配置，帮助webpack编译typescript代码html-webpack-plugin - 简化HTML文件的创建，为 Webpack 提供打包服务。webpack的配置在根目录my-app建立webpack.config.js文件，然后写入以下配置const HtmlWebpackPlugin = require("html-webpack-plugin"); const path = require("path"); module.exports = { entry: { app: ["./src/main.tsx"], vendor: ["react", "react-dom"], }, output: { filename: "js/[name].bundle.js", path: path.resolve(__dirname, "dist"), }, devtool: "source-map", resolve: { extensions: [".ts", ".tsx", ".js", "jsx", ".json"], }, module: { rules: [ { test: /\.tsx?$/, exclude: /node_modules/, use: { loader: "ts-loader", }, }, ], }, plugins: [ new HtmlWebpackPlugin({ title: "react app", filename: "index.html", template: "./src/index.html", }), ], };在entry中，vendor 告诉 webpack 我们的依赖包含了 react 和 react-domoutput 告诉 webpack 在哪里打包我们的应用程序，当前的例子是在 dist 文件夹里面。在 module 下我们添加了ts-loader在 plugins 下，我们使用 HtmlWebPackPlugin 添加了 index.html 源文件。压缩后的 html 文件将放在我们的 dist 文件夹中，同时引用我们打包后的js文件。接下来，新创建一个 index.html 文件并将其添加到我们的src文件夹中。并确保已被添加在 index.html 文件中。这是我们的React应用程序将要渲染的位置。 React 配置开发调试命令将package.json中的script修改成以下配置{ "scripts": { "dev": "webpack-dev-server --open --mode development" } }以下是配置完成后的package.json，仅供参考{ "name": "my-app", "version": "1.0.0", "description": "", "main": "main.tsx", "scripts": { "dev": "webpack-dev-server --open --mode development" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@types/react": "^17.0.38", "@types/react-dom": "^17.0.11", "html-webpack-plugin": "^5.5.0", "ts-loader": "^9.2.6", "webpack": "^5.68.0", "webpack-cli": "^4.9.2", "webpack-dev-server": "^4.7.4" }, "dependencies": { "react": "^17.0.2", "react-dom": "^17.0.2" } }完成新建main.tsx和App.tsxsrc/main.tsximport React from "react"; import ReactDOM from "react-dom"; import App from "./App"; ReactDOM.render(<App />, document.getElementById("app"));src/App.tsximport React from "react"; function App() { return ( Hello World! ); } export default App;运行以下命令，开发环境构建成功！npm run dev最终项目文件结构参考文章使用 React，TypeScript 和 Webpack 开始一个项目如何从零开始创建React项目（三种方式）ts-loader github
最近在看js的教程，然后看到里面所讲述的关于提升代码质量的文字，深受所启发，现将之摘抄下来便于回忆：[scode type="green"]很棒的原则（如雷贯耳，听得我想remake）：“如果代码不够清晰以至于需要一个注释，那么或许它应该被重写。”[/scode]关于提高代码质量的两个配方，分别是分解函数和创建函数好的注释应该在什么地方描述架构对组件进行高层次的整体概括，它们如何相互作用、各种情况下的控制流程是什么样的……简而言之 —— 代码的鸟瞰图。记录函数的参数和用法例如：/** * 返回 x 的 n 次幂的值。 * * @param {number} x 要改变的值。 * @param {number} n 幂数，必须是一个自然数。 * @return {number} x 的 n 次幂的值。 */ function pow(x, n) { ... }这种注释可以帮助我们理解函数的目的，并且不需要研究其内部的实现代码，就可以直接正确地使用它。为什么任务以这种方式解决？写了什么代码很重要。但是为什么 不 那样写可能对于理解正在发生什么更重要。为什么任务是通过这种方式解决的？代码并没有给出答案。如果有很多种方法都可以解决这个问题，为什么偏偏是这一种？尤其当它不是最显而易见的那一种的时候。ps:我认为这点是最关键的，当实现一个逻辑的时候，要写下为什么这样写。因为这次解决问题你应该基本上考虑到了所有的情况，而当下一次来review的时候如果这里没写注释，就很有可能理所当然的想到某种更加简单的解决方案，然而这种解决方案很有可能是上次就已经想到的但具有缺陷的解决方法，从而当自己意识到这个解决方案有缺陷的时候，又要重新写回之前的样子，浪费了大量的时间。代码有哪些巧妙的特性？它们被用在了什么地方？如果代码存在任何巧妙和不显而易见的方法，那绝对需要注释。避免注释描述“代码如何工作”和“代码做了什么”。避免在代码已经足够简单或代码有很好的自描述性而不需要注释的情况下，还写些没必要的注释。当然现实总不会那样的完美，例如在一些复杂的算法中，会有一些出于优化的目的而做的一些巧妙的“调整”。但是通常情况下，我们应该尽可能地保持代码的简单和“自我描述”性。记录有感：看完注释的艺术，感觉对自身是一个极大的提升，以后还是要经常学习编程方面的艺术啊！