├── .nojekyll ├── 04~eBPF └── README.md ├── 02~存储 ├── 文件系统 │ ├── README.md │ ├── 文件检索.md │ ├── 分区与挂载.md │ └── 文件 │ │ └── README.md ├── README.md ├── 01~内存管理 │ ├── 段式存储管理 │ │ └── 段式存储管理.md │ ├── 内存寻址.md │ ├── 物理内存分配与回收.md │ ├── 页式存储管理 │ │ ├── mmap │ │ │ └── README.md │ │ ├── 页结构.md │ │ ├── 预调式管理 │ │ │ ├── 页面置换算法.md │ │ │ └── 页缓存.md │ │ └── 请页式管理 │ │ │ ├── README.md │ │ │ └── Page Fault.md │ ├── 虚拟存储管理.md │ └── 高速缓存.md ├── .DS_Store ├── 存储 IO │ ├── 99~参考资料 │ │ └── 2017-Different IO Access Methods for Linux, What We Chose for ScyllaDB, and Why.md │ ├── 磁盘 IO │ │ ├── SSD.md │ │ ├── 数据存取.md │ │ ├── README.md │ │ ├── 块 IO 栈.md │ │ └── AIO.md │ └── README.md ├── DMA.md └── 00~存储分层 │ └── 存储器的层次化结构.md ├── 10~Shell 命令 ├── 网络 │ ├── 网络工具.md │ ├── README.md │ ├── netstat.md │ ├── SSH.md │ ├── 网卡配置.md │ ├── 网络请求.md │ └── 路由与映射.md ├── 用户权限 │ ├── 权限控制.md │ ├── 系统权限.md │ └── 用户管理.md ├── 系统进程 │ ├── README.md │ └── 进程查看.md ├── Shell 编程 │ ├── 流程控制 │ │ └── README.md │ ├── 交互式 Shell │ │ ├── README.md │ │ ├── Funny Terminals.md │ │ └── 菜单与对话框.md │ ├── .DS_Store │ ├── README.md │ ├── 文件操作 │ │ └── README.md │ ├── 函数 │ │ ├── README.md │ │ ├── 2024~Shell 函数完全指南.md │ │ ├── 2024~Shell 函数调用详解.md │ │ └── 局部变量与返回.md │ └── 语法基础 │ │ ├── Shell 变量 │ │ └── 变量.md │ │ ├── README.md │ │ ├── 数值类型.md │ │ ├── 表达式.md │ │ └── 字符串.md ├── 文本处理 │ ├── README.md │ ├── 文本检索.md │ ├── Nano.md │ ├── Vim.md │ ├── awk.md │ └── sed.md ├── .DS_Store ├── 99~参考资料 │ └── 2021~《Bash 脚本教程》 │ │ └── README.md ├── 磁盘文件 │ ├── README.md │ ├── iostat.md │ └── 创建与读写.md ├── 01~快速开始 │ └── Unix 设计哲学.md ├── README.md ├── Nushell │ └── README.md ├── 命令执行 │ ├── 输入与输出.md │ ├── 重定向.md │ ├── 参数与返回.md │ └── README.md └── CentOS │ └── README.md ├── 01~进程与处理器 ├── 微处理器 │ ├── README.md │ └── 指令集.md ├── 设备管理 │ └── README.md ├── 进程管理 │ ├── README.md │ ├── 管理命令.md │ ├── Meltdown.md │ └── COW.md ├── 系统服务 │ ├── BootLoader.md │ └── 服务与初始化.md ├── 进程与线程 │ ├── 进程间通信.md │ ├── README.md │ ├── 02.用户线程与内核线程.md │ ├── 01.用户态与内核态 │ │ ├── 01.用户态与内核态.md │ │ └── 99~参考资料 │ │ │ └── 2021-从根上理解用户态与内核态.md │ ├── 进程状态.md │ └── 进程模型.md ├── GPU │ └── README.md ├── 总线 │ └── README.md ├── 系统调用 │ ├── README.md │ └── 中断与异常.md └── README.md ├── INTRODUCTION.md ├── .DS_Store ├── .gitattributes ├── 03~网络 ├── io_uring │ └── 99~参考资料 │ │ └── io_uring:基本原理、程序示例与性能压测.md ├── 网络操作系统 │ └── TencentOS.md ├── 网卡设备 │ └── README.md ├── 零拷贝 │ ├── README.md │ └── Linux 下的实现.md ├── README.md ├── 多路复用 │ ├── select │ │ └── select.md │ ├── README.md │ └── epoll │ │ ├── README.md │ │ ├── epoll 函数使用.md │ │ └── 99~参考资料 │ │ └── 2020-深入浅出让你彻底理解 epoll.md └── Linux 网络 IO 模型 │ └── README.md ├── .gitignore ├── README.md └── index.html /.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /04~eBPF/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /02~存储/文件系统/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /10~Shell 命令/网络/网络工具.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /01~进程与处理器/微处理器/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /01~进程与处理器/设备管理/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /10~Shell 命令/用户权限/权限控制.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /10~Shell 命令/网络/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /10~Shell 命令/网络/netstat.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /INTRODUCTION.md: -------------------------------------------------------------------------------- 1 | # 本篇导读 2 | -------------------------------------------------------------------------------- /02~存储/README.md: -------------------------------------------------------------------------------- 1 | # Linux 存储 2 | -------------------------------------------------------------------------------- /02~存储/文件系统/文件检索.md: -------------------------------------------------------------------------------- 1 | # Linux 中文件检索 2 | -------------------------------------------------------------------------------- /10~Shell 命令/系统进程/README.md: -------------------------------------------------------------------------------- 1 | # 系统进程 2 | -------------------------------------------------------------------------------- /01~进程与处理器/进程管理/README.md: -------------------------------------------------------------------------------- 1 | # Linux 进程管理 2 | -------------------------------------------------------------------------------- /02~存储/01~内存管理/段式存储管理/段式存储管理.md: -------------------------------------------------------------------------------- 1 | # 段式存储管理 2 | -------------------------------------------------------------------------------- /02~存储/文件系统/分区与挂载.md: -------------------------------------------------------------------------------- 1 | # Linux 中的分区与挂载 2 | -------------------------------------------------------------------------------- /10~Shell 命令/Shell 编程/流程控制/README.md: -------------------------------------------------------------------------------- 1 | # 流程控制 2 | -------------------------------------------------------------------------------- /10~Shell 命令/文本处理/README.md: -------------------------------------------------------------------------------- 1 | # Linux 中的文本处理 2 | -------------------------------------------------------------------------------- /10~Shell 命令/Shell 编程/交互式 Shell/README.md: -------------------------------------------------------------------------------- 1 | # 交互式 Shell 2 | -------------------------------------------------------------------------------- /10~Shell 命令/文本处理/文本检索.md: -------------------------------------------------------------------------------- 1 | # 文本检索 2 | 3 | # grep & ack 4 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/Linux-Notes/master/.DS_Store -------------------------------------------------------------------------------- /02~存储/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/Linux-Notes/master/02~存储/.DS_Store -------------------------------------------------------------------------------- /10~Shell 命令/网络/SSH.md: -------------------------------------------------------------------------------- 1 | # SSH 2 | 3 | # Links 4 | 5 | - https://iximiuz.com/en/posts/ssh-tunnels/ 6 | -------------------------------------------------------------------------------- /10~Shell 命令/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/Linux-Notes/master/10~Shell 命令/.DS_Store -------------------------------------------------------------------------------- /10~Shell 命令/99~参考资料/2021~《Bash 脚本教程》/README.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://wangdoc.com/bash/) 2 | 3 | # Bash 脚本教程 4 | -------------------------------------------------------------------------------- /01~进程与处理器/系统服务/BootLoader.md: -------------------------------------------------------------------------------- 1 | # BootLoader 2 | 3 | # Links 4 | 5 | - https://zhuanlan.zhihu.com/p/82357200 6 | -------------------------------------------------------------------------------- /01~进程与处理器/进程与线程/进程间通信.md: -------------------------------------------------------------------------------- 1 | # 进程间通信 2 | 3 | # Links 4 | 5 | - https://zhuanlan.zhihu.com/p/405664139 进程间通信的五种方式 -------------------------------------------------------------------------------- /01~进程与处理器/GPU/README.md: -------------------------------------------------------------------------------- 1 | # GPU 2 | 3 | # Links 4 | 5 | - https://www.cnblogs.com/timlly/p/11471507.html 深入GPU硬件架构及运行机制 -------------------------------------------------------------------------------- /10~Shell 命令/Shell 编程/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/Linux-Notes/master/10~Shell 命令/Shell 编程/.DS_Store -------------------------------------------------------------------------------- /01~进程与处理器/进程与线程/README.md: -------------------------------------------------------------------------------- 1 | # 进程与线程 2 | 3 | > 本篇迁移至了《[Concurrent-Notes/并发基础](https://github.com/wx-chevalir/Concurrent-Notes?q=)》 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.xmind filter=lfs diff=lfs merge=lfs -text 2 | *.zip filter=lfs diff=lfs merge=lfs -text 3 | *.pdf filter=lfs diff=lfs merge=lfs -text 4 | -------------------------------------------------------------------------------- /03~网络/io_uring/99~参考资料/io_uring:基本原理、程序示例与性能压测.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://arthurchiao.art/blog/intro-to-io-uring-zh/) TODO! 2 | 3 | # Linux 异步 I/O 框架 io_uring:基本原理、程序示例与性能压测 4 | -------------------------------------------------------------------------------- /02~存储/存储 IO/99~参考资料/2017-Different IO Access Methods for Linux, What We Chose for ScyllaDB, and Why.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://www.scylladb.com/2017/10/05/io-access-methods-scylla/) 2 | -------------------------------------------------------------------------------- /01~进程与处理器/总线/README.md: -------------------------------------------------------------------------------- 1 | # 总线 2 | 3 | 贯穿整个系统的是一组电子管道,称做总线,它携带信息字节并负责在各个部件间传递。通常总线被设计成传送定长的字节块,也就是字(word)。字中的字节数(即字长)是一个基本的系统参数,在各个系统中的情况都不尽相同。现在的大多数机器字长有的是 4 个字节(32 位),有的是 8 个字节(64 位)。 4 | -------------------------------------------------------------------------------- /03~网络/网络操作系统/TencentOS.md: -------------------------------------------------------------------------------- 1 | # TencentOS 2 | 3 | TencentOS tiny 提供精简的 RTOS 内核,其架构图如下: 4 | 5 | ![架构](https://s2.ax1x.com/2019/09/29/u8hhFK.jpg) 6 | 7 | TencentOS tiny 的官宣文档中对于其内核的描述如下:TencentOS tiny 实时内核包括任务管理、实时调度、时间管理、中断管理、内存管理、异常处理、软件定时器、链表、消息队列、信号量、互斥锁、事件标志等模块。 8 | -------------------------------------------------------------------------------- /10~Shell 命令/Shell 编程/交互式 Shell/Funny Terminals.md: -------------------------------------------------------------------------------- 1 | # Funny Terminals 2 | 3 | ```sh 4 | $ sudo apt-get install cowsay 5 | ``` 6 | 7 | 在终端中输入 cowsay hello 就会出现一只牛对你说 hello 了。如果你想要其他动物,那么可以使用 cowsay -l 命令就能看见有什么动物可以给你选了。比如我想让猫咪对我 hello,可以这样写:cowsay -f meow hello 8 | -------------------------------------------------------------------------------- /01~进程与处理器/系统调用/README.md: -------------------------------------------------------------------------------- 1 | # 系统调用 2 | 3 | 系统调用是操作系统的最小功能单位,这些系统调用根据不同的应用场景可以进行扩展和裁剪,现在各种版本的 Unix 实现都提供了不同数量的系统调用,如 Linux 的不同版本提供了 240-260 个系统调用,FreeBSD 大约提供了 320 个。 4 | 5 | 值得一提的是,系统调用的本质其实也是中断,相对于外围设备的硬中断,这种中断称为软中断,这是操作系统为用户特别开放的一种中断,如 Linux int 80h 中断。所以,从触发方式和效果上来看,这三种切换方式是完全一样的,都相当于是执行了一个中断响应的过程。但是从触发的对象来看,系统调用是进程主动请求切换的,而异常和硬中断则是被动的。 6 | -------------------------------------------------------------------------------- /02~存储/01~内存管理/内存寻址.md: -------------------------------------------------------------------------------- 1 | # 内存寻址 2 | 3 | 一个应用程序(源程序)经编译后,通常会形成若干个目标程序;这些目标程序再经过链接便形成了可装入程序。这些程序的地址都是从“0”开始的,程序中的其它地址都是相对于起始地址计算的。由这些地址所形成的地址范围称为“地址空间”,其中的地址称为“逻辑地址”或“相对地址”。此外,由内存中的一系列单元所限定的地址范围称为“内存空间”,其中的地址称为“物理地址”。在多道程序环境下,每道程序不可能都从“0”地址开始装入(内存),这就致使地址空间内的逻辑地址和内存空间中的物理地址不相一致。为使程序能正确运行,存储器管理必须提供地址映射功能,以将地址空间中的逻辑地址转换为内存空间中与之对应的物理地址。该功能应在硬件的支持下完成。 4 | -------------------------------------------------------------------------------- /10~Shell 命令/磁盘文件/README.md: -------------------------------------------------------------------------------- 1 | # 磁盘与文件 2 | 3 | # 磁盘占用 4 | 5 | ```sh 6 | # 查看磁盘剩余空间 7 | $ df -ah 8 | $ df --block-size=GB/-k/-m 9 | 10 | # 查看当前目录下的目录空间占用 11 | $ du -h --max-depth=1 /var/ | sort 12 | # 查看 tmp 目录的磁盘占用 13 | $ du -sh /tmp 14 | # 查看当前目录包含子目录的大小 15 | $ du -sm . 16 | 17 | # 查看目录下文件尺寸 18 | $ ls -l --sort=size --block-size=M 19 | ``` 20 | -------------------------------------------------------------------------------- /10~Shell 命令/Shell 编程/README.md: -------------------------------------------------------------------------------- 1 | # Shell 编程 2 | 3 | Shell 是命令解释器。它不仅是操作系统内核和用户之间的隔离层,而且还是一种功能强大的编程语言。Shell 程序(称为脚本)是一种易于使用的工具,用于通过“粘合”系统调用,工具,实用程序和已编译的二进制文件来构建应用程序。实际上,可通过 Shell 脚本调用 UNIX 命令,实用程序和工具的全部功能。如果这还不够,那么内部 shell 命令(例如测试和循环结构)将为脚本提供额外的功能和灵活性。Shell 脚本特别适合于管理系统任务和其他常规重复性任务,而这些任务不需要成熟的紧密结构化编程语言的花哨功能。 4 | 5 | # 初体验 6 | 7 | 如果希望复现好莱坞大片中的酷炫效果,可以使用如下命令: 8 | 9 | ```sh 10 | $ sudo apt-get install hollywood cmatrix 11 | ``` 12 | -------------------------------------------------------------------------------- /10~Shell 命令/网络/网卡配置.md: -------------------------------------------------------------------------------- 1 | # 网卡配置 2 | 3 | # Ubuntu 4 | 5 | 在 Ubuntu 中我们可以通过配置 `/etc/netplan` 来设置当前的网卡信息: 6 | 7 | ```yaml 8 | network: 9 | ethernets: 10 | eno1: 11 | addresses: [192.168.1.13/24] 12 | gateway4: 192.168.1.1 13 | dhcp4: true 14 | optional: true 15 | nameservers: 16 | addresses: [8.8.8.8, 8.8.4.4] 17 | version: 2 18 | ``` 19 | 20 | 然后直接应用该配置: 21 | 22 | ```sh 23 | $ netplan apply 24 | ``` 25 | -------------------------------------------------------------------------------- /10~Shell 命令/文本处理/Nano.md: -------------------------------------------------------------------------------- 1 | # Nano 2 | 3 | Nano 是 Linux 上一个简单的文本编辑器,比起 Vim 来说它轻量很多,易于上手。 4 | 5 | | 含义 | 快捷键 | 6 | | -------- | ------------------ | 7 | | 标记 | `Ctrl+6` / `Alt+A` | 8 | | 复制整行 | `Alt+6` | 9 | | 剪贴整行 | `Ctrl+K` | 10 | | 粘贴 | `Ctrl+U` | 11 | | 查找 | `Ctrl+W` (WhereIs) | 12 | | 继续查找 | `Alt+W` | 13 | | 上一页 | `Ctrl+Y` | 14 | | 下一页 | `Ctrl+V` | 15 | | 保存 | `Ctrl+O` | 16 | | 退出 | `Ctrl+X` | 17 | -------------------------------------------------------------------------------- /10~Shell 命令/Shell 编程/文件操作/README.md: -------------------------------------------------------------------------------- 1 | # 文件系统 2 | 3 | # 文件判断 4 | 5 | ```sh 6 | #!/bin/sh 7 | 8 | FILENAME= 9 | echo “Input file name:” 10 | read FILENAME 11 | if [ -c "$FILENAME" ] 12 | then 13 | cp $FILENAME /dev 14 | fi 15 | ``` 16 | 17 | ```sh 18 | if [ -d "$LINK_OR_DIR" ]; then 19 | if [ -L "$LINK_OR_DIR" ]; then 20 | # It is a symlink! 21 | # Symbolic link specific commands go here. 22 | rm "$LINK_OR_DIR" 23 | else 24 | # It's a directory! 25 | # Directory command goes here. 26 | rmdir "$LINK_OR_DIR" 27 | fi 28 | fi 29 | ``` 30 | 31 | # 文件遍历 32 | -------------------------------------------------------------------------------- /10~Shell 命令/文本处理/Vim.md: -------------------------------------------------------------------------------- 1 | # Vim 2 | 3 | - 搜索匹配与替换 4 | 5 | Vim 中可以使用 `:s` 命令来替换字符串: 6 | 7 | ```sh 8 | # 替换当前行第一个 vivian 为 sky 9 | :s/vivian/sky/ 10 | 11 | # 替换当前行所有 vivian 为 sky 12 | :s/vivian/sky/g 13 | 14 | # n 为数字,若 n 为 .,表示从当前行开始到最后一行 15 | # 替换第 n 行开始到最后一行中每一行的第一个 vivian 为 sky 16 | :n,$s/vivian/sky/ 17 | 18 | # 替换第 n 行开始到最后一行中每一行所有 vivian 为 sky 19 | :n,$s/vivian/sky/g 20 | 21 | # 替换每一行的第一个 vivian 为 sky 22 | :%s/vivian/sky/(等同于:g/vivian/s//sky/) 23 | 24 | # 替换每一行中所有 vivian 为 sky 25 | :%s/vivian/sky/g(等同于:g/vivian/s//sky/g) 26 | ``` 27 | 28 | # Links 29 | 30 | - https://learnxinyminutes.com/docs/zh-cn/vim-cn/ 31 | -------------------------------------------------------------------------------- /01~进程与处理器/进程管理/管理命令.md: -------------------------------------------------------------------------------- 1 | # ulimit 2 | 3 | `ulimint -a` 用来显示当前的各种用户进程限制,其语法为: 4 | 5 | ```sh 6 | ulimit [-aHS][-c ][-d <数据节区大小>][-f <文件大小>][-m <内存大小>][-n <文件数目>][-p <缓冲区大小>][-s <堆叠大小>][-t ][-u <程序数目>][-v <虚拟内存大小>] 7 | 8 | -H 设置硬件资源限制. 9 | -S 设置软件资源限制. 10 | -a 显示当前所有的资源限制. 11 | -c size:设置core文件的最大值.单位:blocks 12 | -d size:设置数据段的最大值.单位:kbytes 13 | -f size:设置创建文件的最大值.单位:blocks 14 | -l size:设置在内存中锁定进程的最大值.单位:kbytes 15 | -m size:设置可以使用的常驻内存的最大值.单位:kbytes 16 | -n size:设置内核可以同时打开的文件描述符的最大值.单位:n 17 | -p size:设置管道缓冲区的最大值.单位:kbytes 18 | -s size:设置堆栈的最大值.单位:kbytes 19 | -t size:设置CPU使用时间的最大上限.单位:seconds 20 | -v size:设置虚拟内存的最大值.单位:kbytes 21 | ``` 22 | -------------------------------------------------------------------------------- /02~存储/01~内存管理/物理内存分配与回收.md: -------------------------------------------------------------------------------- 1 | # 物理内存分配与回收 2 | 3 | # Swap 分区 4 | 5 | Linux 内核为了提高读写效率与速度,会将文件在内存中进行缓存,这部分内存就是 Cache Memory(缓存内存)。即使你的程序运行结束后,Cache Memory 也不会自动释放。这就会导致你在 Linux 系统中程序频繁读写文件后,你会发现可用物理内存变少。 6 | 7 | 当系统的物理内存不够用的时候,就需要将物理内存中的一部分空间释放出来,以供当前运行的程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间被临时保存到 Swap 空间中,等到那些程序要运行时,再从 Swap 分区中恢复保存的数据到内存中。这样,系统总是在物理内存不够时,才进行 Swap 交换。 8 | 9 | 系统的 Swap 分区大小设置多大才是最优,只能有一个统一的参考标准,具体还应该根据系统实际情况和内存的负荷综合考虑,像 ORACLE 的官方文档就推荐如下设置,这个是根据物理内存来做参考的。 10 | 11 | - 4G 以内的物理内存,SWAP 设置为内存的 2 倍。 12 | - 4 ~ 8G 的物理内存,SWAP 等于内存大小。 13 | - 8 ~ 64G 的物理内存,SWAP 设置为 8G。 14 | - 64 ~ 256G 物理内存,SWAP 设置为 16G。 15 | 16 | 我们可以使用 `swapon -s` 释放 Swap 分区,使用 swapoff 关闭交换分区,使用 swapon 启用交换分区,此时查看交换分区的使用情况,你会发现 used 为 0 了。 17 | -------------------------------------------------------------------------------- /02~存储/DMA.md: -------------------------------------------------------------------------------- 1 | # DMA 2 | 3 | CPU 操作外设时,将外设的数据读到内部寄存器中,再将数据传送至内存中,之所以还要讲数据送到内存,在于 CPU 内部寄存器数量很少,一般都是靠 RAM 来临时存储大量的代码和数据的。CPU 工作的核心就是一个 PC 指针,PC 指针指向什么地址,CPU 就会把相应地址处的二进制数据送至内部译码器进行译码后运行,RAM 是一个临时存放代码和数据的地方,CPU 要执行代码时,就要到内存(RAM)中去取指令。 4 | 5 | DMA:在现代操作系统中,外设有数据到来时,基本上都采用中断方式通知 CPU,操作系统响应中断,然后再从外设读取数据,这时,如果外设的数据比较频繁,那么是否每到一个数据都中断一次呢??这样 CPU 就非常频繁地被外调中断打断,操作系统在处理中断时要浪费一定时间,而且 CPU 读外部 IO 速度也很慢,这样的话,大量时间被用在了响应中断上,而去调度其它任务的时间减少,让人感觉系统响应速度不够,也会影响外设的数据传输速度(如果外设传输速度太快,操作系统就有可能丢失部分数据),由此引出 DMA 的机制: 6 | 7 | ![image](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/47349756-de961500-d6e6-11e8-84b9-6ebb1c9ef901.png) 8 | 9 | 外设直接将一块数据放在了 RAM 中,然后再产生一次中断,这样操作系统直接将内存中的那块数据传给想要获取这块数据的一个任务(或者放在内存的另一空闲部分),此时,系统就少了频繁响应外设中断的开销,也少了读取外设 IO 的时间开销(读取 RAM 比读取外设 IO 要快很多)。 10 | -------------------------------------------------------------------------------- /10~Shell 命令/用户权限/系统权限.md: -------------------------------------------------------------------------------- 1 | # IO 文件属性 2 | 3 | ![](https://uploadfiles.nowcoder.com/images/20160703/284361_1467545717226_794F6F23213EE88A2729AAA2CFD76847) 4 | 5 | ``` 6 | -rwxrw-r-- 1 aaa bbb 0 3月 4 11:21 ccc 7 | ``` 8 | 9 | 在此输出结果中,总共显示出七列内容: 10 | 11 | - 第一列为文件种类及权限。此列共有 10 个字符,其中第一个字符表示文件的种类。即,-表示是普通文件,d 表 示为目录,c 表示为字符设备,b 表示为块设备。而紧跟其后的 10 个字符,可以分为 3 块,每 3 个字符为一块,表示了此文件(目录)的属主、属组及 others 的权限。其中,r 表示 read,w 表示 write,x 表示 execute,-表示无权限。如,上图中的第一行,-rw-------,表示这是一个普通文件,文件的属主有读取和写入的权限,文件的属组无权限,others 无权限。 12 | 13 | - 第二列表示硬链接个数(文件每增加一个硬链接,数字会增加 1,默认从 1 开始,1 表示无硬链接文件,如果是一个目录,它的默认值应该是 2。目录是不能做硬链接的)。 14 | 15 | - 第三列为文件(目录)的属主。 16 | 17 | - 第四列为文件(目录)的属组。 18 | 19 | - 第五列为文件(目录)的大小,单为为字节。 20 | 21 | - 第六列为文件(目录)创建时间或最后一次访问时间,顺序为月、日、时间,如果该时间离现在过久,则直接显示年份。 22 | 23 | - 第七列为文件名。 24 | -------------------------------------------------------------------------------- /01~进程与处理器/系统调用/中断与异常.md: -------------------------------------------------------------------------------- 1 | # 中断 2 | 3 | 中断是计算机发展中一个重要的技术,它的出现很大程度上解放了 CPU,提高了 CPU 的执行效率。所谓中断,是指 CPU 在正常运行程序时,由于程序的预先安排或内外部事件,引起 CPU 中断正在运行的程序,而转到发生中断事件程序中。这些引起程序中断的事件称为中断源。 4 | 5 | 在中断出现之前,CPU 对 IO 采用的是轮询的方式进行服务,这使的 CPU 纠结在某一个 IO 上,一直在等待它的响应,如果它不响应,CPU 就在原地一直的等下去。这样就导致了其他 IO 口也在等待 CPU 的服务,如果某个 IO 出现了 important or emergency affairs,CPU 也抽不出身去响应这个 IO。 6 | 7 | # IO 中断 8 | 9 | IO 中断方式是以字节为单位,DMA 控制方式是以一个连续的数据块为单位,IO 通道控制方式是 DMA 控制方式的发展,是以一组数据块为单位的,即可以连续读取多个数据块。 10 | 11 | - 程序直接访问方式跟循环检测 IO 方式,应该是一个意思吧,是最古老的方式。CPU 和 IO 串行,每读一个字节(或字),CPU 都需要不断检测状态寄存 器的 busy 标志,当 busy=1 时,表示 IO 还没完成;当 busy=0 时,表示 IO 完成。此时读取一个字的过程才结束,接着读取下一个字。 12 | - 中断控制方式:循环检测先进些,IO 设备和 CPU 可以并行工作,只有在开始 IO 和结束 IO 时,才需要 CPU。但每次只能读取一个字。 13 | - DMA 方式:Direct Memory Access,直接存储器访问,比中断先进的地方是每次可以读取一个块,而不是一个字。 14 | - 通道方式:比 DMA 先进的地方是,每次可以处理多个块,而不只是一个块。 15 | -------------------------------------------------------------------------------- /01~进程与处理器/微处理器/指令集.md: -------------------------------------------------------------------------------- 1 | # 指令集架构 2 | 3 | 指令集架构 是计算机的抽象模型,在很多时候也被称作架构或者计算机架构,它其实是计算机软件和硬件之间的接口和桥梁;一个为特定指令集架构编写的应用程序能够运行在所有支持这种指令集架构的机器上,也就说如果当前应用程序支持 x86_64 的指令集,那么就可以运行在所有使用 x86_64 指令集的机器上,这其实就是分层的作用,每一个指令集架构都定义了支持的数据结构、主内存和寄存器、类似内存一致和地址模型的语义、支持的指令集和 IO 模型,它的引入其实就在软件和硬件之间引入了一个抽象层,让同一个二进制文件能够在不同版本的硬件上运行。 4 | 5 | 如果一个编程语言想要在所有的机器上运行,它就可以将中间代码转换成使用不同指令集架构的机器码,这可比为不同硬件单独移植要简单的太多了。在命令行中输入 uname -m 就能够获得当前机器上硬件的信息: 6 | 7 | ```sh 8 | $ uname -m 9 | x86_64 10 | ``` 11 | 12 | x86_64 是目前比较常见的指令集架构之一,除了 x86_64 之外,还包含其他类型的指令集架构,例如 amd64、arm64 以及 mips 等等,不同的处理器使用了大不相同的机器语言,所以很多编程语言为了在不同的机器上运行需要将源代码根据架构翻译成不同的机器代码。 13 | 14 | # 指令集分类 15 | 16 | 复杂指令集计算机(CISC)和精简指令集计算机(RISC)是目前的两种 CPU 区别,它们的在设计理念上会有一些不同,从名字我们就能看出来这两种不同的设计有什么区别,复杂指令集通过增加指令的数量减少需要执行的质量数,而精简指令集能使用更少的指令完成目标的计算任务;早期的 CPU 为了减少机器语言指令的数量使用复杂指令集完成计算任务,这两者之前的区别其实就是设计上的权衡。 17 | -------------------------------------------------------------------------------- /01~进程与处理器/README.md: -------------------------------------------------------------------------------- 1 | # 处理器 2 | 3 | 中央处理单元(CPU),简称处理器,是解释(或执行)存储在主存中指令的引擎。处理器的核心是一个字长的存储设备(或寄存器),称为程序计数器(PC)。在任何时刻,PC 都指向主存中的某条机器语言指令(即含有该条指令的地址)。 4 | 5 | 从系统通电开始,直到系统断电,处理器一直在不断地执行程序计数器指向的指令,再更新程序计数器,使其指向下一条指令。处理器看上去是按照一个非常简单的指令执行模型来操作的,这个模型是由指令集结构决定的。在这个模型中,指令按照严格的顺序执行,而执行一条指令包含执行一系列的步骤。处理器从程序计数器(PC)指向的存储器处读取指令,解释指令中的位,执行该指令指示的简单操作,然后更新 PC,使其指向下一条指令,而这条指令并不一定与存储器中刚刚执行的指令相邻。 6 | 7 | 这样的简单操作并不多,而且操作是围绕着主存、寄存器文件(register file)和算术/ 逻辑单元(ALU)进行的。寄存器文件是一个小的存储设备,由一些 1 字长的寄存器组成,每个寄存器都有唯一的名字。ALU 计算新的数据和地址值。下面列举一些简单操作的例子,CPU 在指令的要求下可能会执行以下操作: 8 | 9 | - 加载:把一个字节或者一个字从主存复制到寄存器,以覆盖寄存器原来的内容。 10 | - 存储:把一个字节或者一个字从寄存器复制到主存的某个位置,以覆盖这个位置上原来的内容。 11 | - 操作:把两个寄存器的内容复制到 ALU,ALU 对这两个字做算术操作,并将结果存放到一个寄存器中,以覆盖该寄存器中原来的内容。 12 | - 跳转:从指令本身中抽取一个字,并将这个字复制到程序计数器(PC)中,以覆盖 PC 中原来的值。 13 | 14 | ![](https://ww1.sinaimg.cn/large/007rAy9hgy1g0egpe5ntnj30q20akdgs.jpg) 15 | -------------------------------------------------------------------------------- /03~网络/网卡设备/README.md: -------------------------------------------------------------------------------- 1 | # 网卡设备 2 | 3 | 主流的数据中心基本上开始提供 10GbE 甚至 25GbE 的网络。为什么会变成这样?我们做一个简单的算术题就知道了。根据 Cisco 的文档介绍,一块千兆网卡的吞吐大概是: [1,000,000,000 b/s / (84 B * 8 b/B)] == 1,488,096 f/s (maximum rate)。那么万兆网卡的吞吐大概是它的十倍,也就是差不多每秒 1488 万帧,处理一个包的时间在百纳秒的级别,基本相当于一个 L2 Cache Miss 的时间。所以如何减小内核协议栈处理带来的内核-用户态频繁内存拷贝的开销,成为一个很重要的课题,这就是为什么现在很多高性能网络程序开始基于 DPDK 进行开发。 4 | 5 | # DPDK 6 | 7 | ![DPDK Flow Bifurcation](https://s1.ax1x.com/2020/06/07/tRu9Rf.md.png) 8 | 9 | 从上图可以看到,数据包直接从网卡到了 DPDK,绕过了操作系统的内核驱动、协议栈和 Socket Library。DPDK 内部维护了一个叫做 UIO Framework 的用户态驱动 (PMD),通过 ring queue 等技术实现内核到用户态的 zero-copy 数据交换,避免了 Syscall 和内核切换带来的 cache miss,而且在多核架构上通过多线程和绑核,极大提升了报文处理效率。如果你确定你的网络程序瓶颈在包处理效率上,不妨关注一下 DPDK。 10 | 11 | 另外 RDMA 对未来体系结构的影响也会很大,它会让一个分布式集群向一个超级 NUMA 的架构演进(它的通信延时/带宽已经跟现在 NUMA 架构中连接不同 socket node 的 QPI 的延时/带宽在一个量级),但是目前受限于成本和开发模型的变化,可能还需要等很长一段时间才能普及。 12 | -------------------------------------------------------------------------------- /10~Shell 命令/网络/网络请求.md: -------------------------------------------------------------------------------- 1 | # DNS 2 | 3 | DNS 解析检索与查询: 4 | 5 | ```sh 6 | $ dig ns alicdn.com 7 | ``` 8 | 9 | # nc 10 | 11 | 想要连接到某处: `nc [-options] hostname port[s][ports] …`,绑定端口等待连接: `nc -l -p port [-options][hostname] [port]`: 12 | 13 | ```sh 14 | -g gateway source-routing hop point[s], up to 8 15 | -G num source-routing pointer: 4, 8, 12, … 16 | -h 帮助信息 17 | -i secs 延时的间隔 18 | -l 监听模式,用于入站连接 19 | -n 指定数字的 IP 地址,不能用 hostname 20 | -o file 记录 16 进制的传输 21 | -p port 本地端口号 22 | -r 任意指定本地及远程端口 23 | -s addr 本地源地址 24 | -u UDP 模式 25 | -v 详细输出——用两个 26 | -v 可得到更详细的内容 27 | -w secs timeout 的时间 28 | -z 将输入输出关掉——用于扫描时,其中端口号可以指定一个或者用 lo-hi 式的指定范围。 29 | ``` 30 | 31 | 常见用法: 32 | 33 | ```sh 34 | # 端口扫描 35 | $ nc -v -w 2 192.168.2.34 -z 21-24 36 | 37 | # 监听 38 | $ nc -l 1234 39 | $ nc 192.168.2.34 1234 40 | 41 | # 文件拷贝,从 192.168.2.33 拷贝文件到 192.168.2.34 42 | $ nc -l 1234 > test.txt # 192.168.2.34 43 | $ nc 192.168.2.34 < test.txt # 192.168.2.33 44 | 45 | # 发送端 46 | $ cat a.txt | nc -l 3333 47 | # 接收端 48 | $ nc 192.168.0.3 3333 > a.txt 49 | ``` 50 | -------------------------------------------------------------------------------- /02~存储/01~内存管理/页式存储管理/mmap/README.md: -------------------------------------------------------------------------------- 1 | # mmap 2 | 3 | 数据库存储引擎要解决的主要问题之一是如何处理磁盘中大于可用内存的数据。从更高的层面来说,面向磁盘的存储引擎的主要目的是操作磁盘中的数据文件。但如果我们假设磁盘中的数据最终会大于可用内存,我们就不能简单地将整个数据文件加载到内存中,进行更改,然后再写回磁盘。这在计算机科学中并不是一个新问题。在 20 世纪 60 年代初开发操作系统时,就面临着类似的问题:如何运行存储在磁盘中的程序,而这些程序又大于可用的内存?1961 年,曼彻斯特的一个小组提出了解决这个问题的方法,并在 Atlas 计算机上实现。它被称为虚拟内存。虚拟内存给正在运行的程序一种错觉,认为它有足够大的内存,尽管事实上计算机没有足够的内存。 4 | 5 | 当一个程序在访问内存时,就是在访问虚拟内存。而也许程序要访问的数据其实并不在内存中,但这并不重要。操作系统会通过到磁盘上,把它假装成在磁盘上,然后放在那里,并替换掉一块旧的、不会被使用的内存。所以,数据库存储引擎解决大于内存问题的方法之一就是利用虚拟内存和内存映射文件的概念。在 Linux 中,我们可以通过使用系统调用 mmap 来实现这一用途,它可以让你直接将一个文件(无论多大的文件)映射到内存中。如果你的程序需要操作文件,只需要操作内存就可以了。操作系统会为你处理写入磁盘的事情。 6 | 7 | # mmap 简单示例 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "os" 14 | "fmt" 15 | "github.com/edsrzf/mmap-go" 16 | ) 17 | 18 | func main() { 19 | f, _ := os.OpenFile("./file", os.O_RDWR, 0644) 20 | defer f.Close() 21 | 22 | mmap, _ := mmap.Map(f, mmap.RDWR, 0 ) 23 | defer mmap.Unmap() 24 | fmt.Println(string(mmap)) 25 | 26 | mmap[0] = 'X' 27 | mmap.Flush() 28 | } 29 | ``` 30 | 31 | 美中不足的是,我们可以有一个更大的文件,而这个解决方案仍然有效。我们不必担心为了避免内存被填满而管理内存。 32 | -------------------------------------------------------------------------------- /02~存储/存储 IO/磁盘 IO/SSD.md: -------------------------------------------------------------------------------- 1 | # SSD 2 | 3 | 如果说云的出现是一种商业模式的变化的话,驱动这个商业革命的推手就是最近十年硬件的快速更新。比起 CPU,存储和网络设备的进化速度更加迅速。最近五年,SSD 的价格 (包括 PCIe 接口) 的成本持续下降,批量采购的话已经几乎达到和 HDD 接近的价格。 4 | 5 | ![Average Contract Price for PC-Client ](https://s1.ax1x.com/2020/06/07/tRmIgS.md.png) 6 | 7 | SSD 的普及,对于存储软件厂商的影响是深远的。 8 | 9 | 其一,是极大地缓解了 IO 瓶颈。对于数据库厂商来说,可以将更多的精力花在其他事情,而不是优化存储引擎上。最近两年发生了一些更大的变化,NVMe 正在成为主流,我们很早就在 Intel Optane 进行实验和投资,类似这样的非易失内存的技术,正在模糊内存和存储的界限,但是同时对开发者带来挑战也是存在的。举一个简单的例子,对于 Optane 这类的非易失内存,如果你希望能够完全利用它的性能优势,最好使用类似 PMDK 这类基于 Page cache Bypass 的 SDK 针对你的程序进行开发,这类 SDK 的核心思想是将 NVM 设备真正地当做内存使用。如果仅仅将 Optane 挂载成本地磁盘使用,其实很大程度上的瓶颈不一定出现在硬件本身的 IO 上。 10 | 11 | 下面这张图很有意思,来自 Intel 对于 Optane 的测试,我们可以看见在中间那一列,Storage with Optane SSD,随机读取的硬件延迟已经接近操作系统和文件系统带来的延迟,甚至 Linux VFS 本身会变成 CPU 瓶颈。其实背后的原因也很简单,过去由于 VFS 本身在 CPU 上的开销(比如锁)相比过去的 IO 来说太小了,但是现在这些新硬件本身的 IO 延迟已经低到让文件系统本身开销的比例不容忽视了。 12 | 13 | ![Intel 对于 Optane 的测试](https://s1.ax1x.com/2020/06/07/tRn9u4.png) 14 | 15 | 其二,这个变化影响了操作系统和文件系统本身。例如针对 Persistent Memory 设计新的文件系统,其中来自 UCSD 的 NVSL 实验室 (Non-Volatile Systems Laboratory) 的 NovaFS 就是一个很好的例子。简单来说是大量使用了无锁数据结构,减低 CPU 开销,NovaFS 的代码量很小很好读,有兴趣可以看看。另外 Intel 对 Persistent Memory 编程模型有很好的一篇文章,感兴趣的话可以从这里开始了解这些新变化。 16 | -------------------------------------------------------------------------------- /02~存储/存储 IO/磁盘 IO/数据存取.md: -------------------------------------------------------------------------------- 1 | # 磁盘的数据存取 2 | 3 | 当磁盘驱动器执行读/写功能时。盘片装在一个主轴上,并绕主轴高速旋转,当磁道在读/写头(磁头)下通过时,就可以进行数据的读/写了。一般磁盘分为固定头盘(磁头固定)和活动头盘。固定头盘的每一个磁道上都有独立的磁头,它是固定不动的,专门负责这一磁道上数据的读/写。活动头盘的磁头是可移动的。每一个盘面上只有一个磁头(磁头是双向的,因此正反盘面都能读写)。它可以从该面的一个磁道移动到另一个磁道。所有磁头都装 在同一个动臂上,因此不同盘面上的所有磁头都是同时移动的。当盘片绕主轴旋转的时候,磁头与旋转的盘片形成一个圆柱体。各个盘面上半径相同的磁道组成了一个圆柱面,我们称为柱面。因此,柱面的个数也就是盘面上的磁道数。 4 | 5 | 磁盘读取数据是以盘块(Block)为基本单位的。位于同一盘块中的所有数据都能被一次性全部读取出来。而磁盘 IO 代价主要花费在查找时间 Ts 上。因此我们应该尽量将相关信息存放在同一盘块,同一磁道中。或者至少放在同一柱面或相邻柱面上,以求在读/写信息时尽量减少磁头来回移动的次数,避免过多的查找时间 Ts。所以,在大规模数据存储方面,大量数据存储在外存磁盘中,而在外存磁盘中读取/写入块 (Block) 中某数据时,首先需要定位到磁盘中的某块,如何有效地查找磁盘中的数据,需要一种合理高效的外存数据结构。 6 | 7 | # 磁盘数据访问 8 | 9 | 磁盘上数据必须用一个三维地址唯一标示:柱面号、盘面号、块号 ( 磁道上的盘块 )。读/写磁盘上某一指定数据需要下面 3 个步骤: 10 | 11 | - (1) 首先移动臂根据柱面号使磁头移动到所需要的柱面上,这一过程被称为定位或查找。 12 | 13 | - (2) 如上图 11.3 中所示的 6 盘组示意图中,所有磁头都定位到了 10 个盘面的 10 条磁道上 ( 磁头都是双向的 )。这时根据盘面号来确定指定盘面上的磁道。 14 | 15 | - (3) 盘面确定以后,盘片开始旋转,将指定块号的磁道段移动至磁头下。经过上面三个步骤,指定数据的存储位置就被找到。这时就可以开始读 / 写操作了。 16 | 17 | 访问某一具体信息,由 3 部分时间组成: 18 | 19 | - 查找时间 (seek time) Ts: 完成上述步骤 (1) 所需要的时间。这部分时间代价最高,最大可达到 0.1s 左右。 20 | 21 | - 等待时间 (latency time) Tl: 完成上述步骤 (3) 所需要的时间。由于盘片绕主轴旋转速度很快,一般为 7200 转/分 ( 电脑硬盘的性能指标之一, 家用的普通硬盘的转速一般有 5400rpm( 笔记本 )、7200rpm 几种 )。因此一般旋转一圈大约 0.0083s。 22 | 23 | - 传输时间 (transmission time) Tt: 数据通过系统总线传送到内存的时间,一般传输一个字节 (byte) 大概 `0.02us=2*10^(-8)s` 24 | -------------------------------------------------------------------------------- /10~Shell 命令/Shell 编程/函数/README.md: -------------------------------------------------------------------------------- 1 | # Shell 函数 2 | 3 | 我们人类当然是一个聪明的物种。我们与他人合作,我们在共同的任务上相互依赖。例如,您依靠送牛奶员将牛奶装进牛奶瓶或纸箱中。此逻辑适用于包含 Shell 程序脚本的计算机程序。当脚本变得复杂时,您需要使用分而治之的技术。有时,Shell 脚本会变得复杂。为了避免使用大而复杂的脚本,请使用函数。您将大型脚本分为称为函数的小块/实体。函数使 Shell 脚本模块化并且易于使用。函数避免重复的代码。例如,is_root_user() 函数可以被各种 shell 脚本重用,以确定登录的用户是否是 root 用户。函数执行特定任务。例如,添加或删除用户帐户。使用的功能类似于普通命令。在其他高级编程语言中,功能也称为过程,方法,子例程或例程。 4 | 5 | 在 Shell 提示符下键入以下命令: 6 | 7 | ```sh 8 | hello() { echo 'Hello world!' ; } 9 | 10 | # 调用 hello 函数 11 | hello 12 | ``` 13 | 14 | 您可以将命令行参数传递给用户定义的函数。定义 hello 如下: 15 | 16 | ```sh 17 | hello() { echo "Hello $1, let us be a friend." ; } 18 | ``` 19 | 20 | 您可以使用 hello 函数并传递参数,如下所示: 21 | 22 | ```sh 23 | hello Vivek 24 | 25 | # Hello Vivek, let us be a friend. 26 | ``` 27 | 28 | {...} 中的一行函数必须以分号结尾。否则,您会在屏幕上看到错误: 29 | 30 | ```sh 31 | xrpm() { rpm2cpio "$1" | cpio -idmv; } 32 | ``` 33 | 34 | # 函数枚举 35 | 36 | 要显示定义的函数名称,请使用 declare 命令。在 shell 提示符下键入以下命令: 37 | 38 | ```sh 39 | declare -f 40 | declare -f | less 41 | declare -f functioName 42 | declare -f xrpm 43 | 44 | # Sample outputs 45 | command_not_found_handle () 46 | { 47 | if [ -x /usr/lib/command-not-found ]; then 48 | /usr/bin/python /usr/lib/command-not-found -- $1; 49 | return $?; 50 | else 51 | return 127; 52 | fi 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all 2 | * 3 | 4 | # Unignore all with extensions 5 | !*.* 6 | 7 | # Unignore all dirs 8 | !*/ 9 | 10 | .DS_Store 11 | 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | 71 | # next.js build output 72 | .next 73 | -------------------------------------------------------------------------------- /01~进程与处理器/进程管理/Meltdown.md: -------------------------------------------------------------------------------- 1 | ![](https://inews.co.uk/wp-content/uploads/2018/01/meltdownspectre-1376x1032.jpg) 2 | 3 | # Meltdown 简述 4 | 5 | 近日现代 CPU 的 Meltdown & Spectre 漏洞沸沸扬扬,最早是 Google 研究员发现[可以通过内存侧信道时序攻击来获取隐私数据](https://googleprojectzero.blogspot.de/2018/01/reading-privileged-memory-with-side.html),后续 [Chromium](https://www.chromium.org/Home/chromium-security/ssca), [Apple](https://support.apple.com/en-us/HT208394) 以及 [Mozilla](https://blog.mozilla.org/security/2018/01/03/mitigations-landing-new-class-timing-attack/) 都发文讨论了其对各个平台的影响与应对方案。网上讨论该漏洞的文章也很多,笔者个人感觉 [This is how Meltdown works](https://dev.to/isaacandsuch/how-meltdown-works-28j2), [Why Raspberry Pi Isn't Vulnerable to Spectre or Meltdown](http://t.cn/RH3DVKj) 讲解的不错,而本文则翻译自 [Meltdown in a nutshell](https://hackernoon.com/meltdown-in-a-nutshell-bda0b79f84a2) 这个简述;本文归纳于 [Linux 配置使用、内部原理与 Shell 编程](https://parg.co/UMI)系列。 6 | 7 | 对于如下伪代码: 8 | 9 | ```c 10 | x = read(memory_location_of_os_where_secret_lies) // 抛出异常 11 | y = arr[x * 4096] // 基于 x 读取本地内存 12 | ``` 13 | 14 | 应用程序会被编译或者解释为 CPU 指令,第一行对于敏感内存的读取最终会抛出异常,不过由于 CPU 的推测执行(Speculative Execution)优化,第二行代码会在 CPU 进行权限检测前执行,即 y 的值实际上已经被读取到了 CPU 缓存中。如果 x 的值是 `s`,那么内存 arr[`s` * 4096] 即会被 CPU 读取。CPU 发现进程无权读取后,即会清除 x 与 y 的值,不过 CPU 为了优化读取速度,会将本次的读取值缓存到 CPU 本地。攻击者即利用这一特性发起了旁路攻击,攻击者使用类似暴力破解的方式,遍历所有可能的 arr[x *4096] 地址;根据计时器来判断哪个地址的读取速度最快,即可以获取到 x 的值 `s`。依次类推可以获得 ‘e’, ‘c’,’r’,’e’,’t’ 等值。 15 | -------------------------------------------------------------------------------- /10~Shell 命令/01~快速开始/Unix 设计哲学.md: -------------------------------------------------------------------------------- 1 | # Unix 设计哲学 2 | 3 | ## 核心理念的起源 4 | 5 | Unix 管道的概念最初由道格·麦克罗伊(Doug McIlroy)在 1964 年提出:"当我们需要将消息从一个程序传递另一个程序时,我们需要一种类似水管法兰的拼接程序的方式,I/O 应该也按照这种方式进行"。这个水管的类比至今仍然适用,并发展成为现在所称的 Unix 哲学。 6 | 7 | ## Unix 的基本原则 8 | 9 | ### 英文原则 10 | 11 | - **Do one thing and do it well** - 编写专注于单一任务且能很好完成的程序,设计程序时考虑协同工作,使用文本流作为通用接口。 12 | - **Everything is file** - 通过将硬件视为文件来提供易用性和安全性。 13 | - **Small is beautiful** 14 | - **Store data and configuration in flat text files** - 文本文件是通用接口,易于创建、备份和迁移。 15 | - **Use shell scripts to increase leverage and portability** - 使用 shell 脚本在不同 Unix/Linux 系统间自动化常见任务。 16 | - **Chain programs together to complete complex task** - 使用管道和过滤器链接小型工具来完成复杂任务。 17 | - **Choose portability over efficiency** 18 | - **Keep it Simple, Stupid (KISS)** 19 | 20 | ### 中文诠释 21 | 22 | 1. 专注性:每个程序只做好一件事,需要新功能时创建新程序,而不是使现有程序复杂化。 23 | 2. 组合性:程序的输出应该能作为另一个程序的输入,避免无关信息和严格的数据格式。 24 | 3. 快速迭代:尽早尝试,快速构建,勇于重构。 25 | 4. 工具优先:优先使用工具来简化编程任务,即使需要专门开发工具。 26 | 27 | ## Unix 设计的关键特性 28 | 29 | ### 统一接口 30 | 31 | - 文件作为基本接口(文件描述符) 32 | - 所有资源都表现为字节序列 33 | - 统一接口使得不同组件易于连接 34 | - ASCII 文本作为常用数据格式 35 | 36 | ### 逻辑与布线分离 37 | 38 | - 标准输入(stdin)和标准输出(stdout)的设计 39 | - 程序无需关心数据来源和去向 40 | - 支持灵活的组件组合 41 | - 通过管道实现程序间通信 42 | 43 | ### 透明度和实验性 44 | 45 | - 输入文件通常不可变 46 | - 随时可以检查管道输出 47 | - 支持分段处理和调试 48 | - 便于实验和优化 49 | 50 | ## 现代意义 51 | 52 | 这种设计方法与现代软件开发理念高度吻合: 53 | 54 | - 自动化 55 | - 快速原型 56 | - 增量迭代 57 | - 模块化 58 | - 类似于现代敏捷开发和 DevOps 理念 59 | -------------------------------------------------------------------------------- /01~进程与处理器/进程与线程/02.用户线程与内核线程.md: -------------------------------------------------------------------------------- 1 | # Linux 中的线程 2 | 3 | 在 Linux 2.4 版以前,线程的实现和管理方式就是完全按照进程方式实现的;在 Linux 2.6 之前,内核并不支持线程的概念,仅通过轻量级进程(Lightweight Process)模拟线程;轻量级进程是建立在内核之上并由内核支持的用户线程,它是内核线程的高度抽象,每一个轻量级进程都与一个特定的内核线程关联。内核线程只能由内核管理并像普通进程一样被调度。这种模型最大的特点是线程调度由内核完成了,而其他线程操作(同步、取消)等都是核外的线程库(Linux Thread)函数完成的。 4 | 5 | 为了完全兼容 Posix 标准,Linux 2.6 首先对内核进行了改进,引入了线程组的概念(**仍然用轻量级进程表示线程**),有了这个概念就可以将一组线程组织称为一个进程,不过内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程;相反,线程仅仅被视为一个与其他进程(概念上应该是线程)共享某些资源的进程(概念上应该是线程)。在实现上主要的改变就是在 task_struct 中加入 tgid 字段,这个字段就是用于表示线程组 id 的字段。在用户线程库方面,也使用 NPTL 代替 Linux Thread,不同调度模型上仍然采用 `1 对 1` 模型。 6 | 7 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230522114838.png) 8 | 9 | 进程的实现是调用 fork 系统调用:`pid_t fork(void);`,线程的实现是调用 clone 系统调用:`int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ...)`。与标准 `fork()` 相比,线程带来的开销非常小,内核无需单独复制进程的内存空间或文件描写叙述符等等。这就节省了大量的 CPU 时间,使得线程创建比新进程创建快上十到一百倍,能够大量使用线程而无需太过于操心带来的 CPU 或内存不足。无论是 fork、vfork、kthread_create 最后都是要调用 do_fork,而 do_fork 就是根据不同的函数参数,对一个进程所需的资源进行分配。 10 | 11 | ## 内核线程 12 | 13 | 内核线程是由内核自己创建的线程,也叫做守护线程(Deamon),在终端上用命令 `ps -Al` 列出的所有进程中,名字以 k 开关以 d 结尾的往往都是内核线程,比如 kthreadd、kswapd 等。与用户线程相比,它们都由 `do_fork()` 创建,每个线程都有独立的 task_struct 和内核栈;也都参与调度,内核线程也有优先级,会被调度器平等地换入换出。二者的不同之处在于,内核线程只工作在内核态中;而用户线程则既可以运行在内核态(执行系统调用时),也可以运行在用户态;内核线程没有用户空间,所以对于一个内核线程来说,它的 0~3G 的内存空间是空白的,它的 `current->mm` 是空的,与内核使用同一张页表;而用户线程则可以看到完整的 0~4G 内存空间。 14 | 15 | 在 Linux 内核启动的最后阶段,系统会创建两个内核线程,一个是 init,一个是 kthreadd。其中 init 线程的作用是运行文件系统上的一系列”init”脚本,并启动 shell 进程,所以 init 线程称得上是系统中所有用户进程的祖先,它的 pid 是 1。kthreadd 线程是内核的守护线程,在内核正常工作时,它永远不退出,是一个死循环,它的 pid 是 2。 16 | -------------------------------------------------------------------------------- /10~Shell 命令/网络/路由与映射.md: -------------------------------------------------------------------------------- 1 | # iptables 2 | 3 | ```sh 4 | iptables -P INPUT ACCEPT 5 | iptables -P OUTPUT ACCEPT 6 | iptables -P FORWARD ACCEPT 7 | iptables -F 8 | ``` 9 | 10 | ## 端口映射 11 | 12 | 需要将外网访问本地 IP(192.168.75.5)的 80 端口转换为访问 192.168.75.3 的 8000 端口,这就需要用到 iptables 的端口映射,首先需要开启 Linux 的数据转发功能: 13 | 14 | ```sh 15 | # vi /etc/sysctl.conf,将net.ipv4.ip_forward=0更改为net.ipv4.ip_forward=1 16 | # sysctl -p //使数据转发功能生效 17 | ``` 18 | 19 | 然后更改 iptables,使之实现 nat 映射功能: 20 | 21 | ```sh 22 | # 将外网访问 192.168.75.5 的 80 端口转发到 192.168.75.3:8000 端口。 23 | $ iptables -t nat -A PREROUTING -d 192.168.75.5 -p tcp --dport 80 -j DNAT --to-destination 192.168.75.3:8000 24 | 25 | # 将 192.168.75.3 8000 端口将数据返回给客户端时,将源 ip 改为 192.168.75.5 26 | $ iptables -t nat -A POSTROUTING -d 192.168.75.3 -p tcp --dport 8000 -j SNAT 192.168.75.5 27 | ``` 28 | 29 | 查看 nat,可以使用命令:`iptables -t nat –lis`t 检查 nat 列表信息,结果如下图所示 30 | 31 | [![iptables](http://static.coolnull.com/wp-content/uploads/2014/09/iptables.jpg)](http://static.coolnull.com/wp-content/uploads/2014/09/iptables.jpg) 32 | 33 | ```sh 34 | # 将外网访问 80 端口的数据转发到 8080 端口 35 | $ iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8080 36 | 37 | # 将本机访问 80 端口的转发到本机 8080 38 | $ iptables -t nat -A OUTPUT -p tcp -d 127.0.0.1 --dport 80 -j DNAT --to 127.0.0.1:8080 39 | $ iptables -t nat -A OUTPUT -p tcp -d 192.168.4.177 --dport 80 -j DNAT --to 127.0.0.1:8080 40 | ``` 41 | 42 | 本地连接指的是在本机上,用 127.0.0.1 或者本机 IP 来访问本机的端口。本地连接的数据包不会通过网卡,而是由内核处理后直接发给本地进程。这种数据包在 iptables 中只经过 OUTPUT 链,而不会经过 PREROUTING 链。所以需要在 OUTPUT 链中进行 DNAT。除了对 127.0.0.1 之外,对本机 IP (即 192.168.4.177) 的访问也属于本地连接。 43 | -------------------------------------------------------------------------------- /03~网络/零拷贝/README.md: -------------------------------------------------------------------------------- 1 | # 零拷贝 2 | 3 | # 传统 IO 4 | 5 | 基于传统的 IO 方式,底层实际上通过调用 read()和 write()来实现。基本操作就是循环的从磁盘读入文件内容到缓冲区,再将缓冲区的内容发送到 Socket。但是由于 Linux 的 IO 操作默认是缓冲 I/O,在这两个系统调用之后,数据已经被复制了至少四次,并且几乎执行了同样次数的用户/内核空间的上下文切换。 6 | 7 | ```c 8 | while((n = read(diskfd, buf, BUF_SIZE)) > 0) 9 | write(sockfd, buf, n); 10 | ``` 11 | 12 | ![传统 Linux IO 流程示意图](https://pic.imgdb.cn/item/60544efe524f85ce290d4dbd.jpg) 13 | 14 | 整个过程发生了 4 次用户态和内核态的上下文切换和 4 次拷贝,具体流程如下: 15 | 16 | - read 系统调用导致上下文从用户模式切换到内核模式。第一个副本由 DMA 引擎执行,DMA 引擎从磁盘读取文件内容并将它们存储到内核地址空间缓冲区中。 17 | - 将数据从内核缓冲区复制到用户缓冲区,并且 read 系统调用返回。read 调用返回导致上下文从内核切换回用户模式。现在数据存储在用户地址空间缓冲区中。 18 | - write 系统调用导致上下文从用户模式切换到内核模式。执行第三次复制,以再次将数据放入内核地址空间缓冲区。这个时候,数据被放入一个不同的缓冲区,一个与 sockets 相关联的缓冲区。 19 | - 写系统调用返回,创建我们的第四个上下文切换。独立和异步地,当 DMA 引擎将数据从内核缓冲区传递到协议引擎时,发生第四次复制。 20 | 21 | 很多数据复制操作并不是真正需要的,可以消除一些复制操作以减少开销并提高性能,在此过程中,我们没有对文件内容做任何修改,那么在内核空间和用户空间来回拷贝数据无疑就是一种浪费,而零拷贝主要就是为了解决这种低效性。零拷贝主要的任务就是避免 CPU 将数据从一块存储拷贝到另外一块存储,主要就是利用各种零拷贝技术,避免让 CPU 做大量的数据拷贝任务,减少不必要的拷贝,或者让别的组件来做这一类简单的数据传输任务,让 CPU 解脱出来专注于别的任务。这样就可以让系统资源的利用更加有效。 22 | 23 | # COW 24 | 25 | 零拷贝技术都是减少数据在用户空间和内核空间拷贝技术实现的,但是有些时候,数据必须在用户空间和内核空间之间拷贝。这时候,我们只能针对数据在用户空间和内核空间拷贝的时机上下功夫了。Linux 通常利用写时复制(copy on write)来减少系统开销,这个技术又时常称作 COW。 26 | 27 | 如果多个程序同时访问同一块数据,那么每个程序都拥有指向这块数据的指针,在每个程序看来,自己都是独立拥有这块数据的,只有当程序需要对数据内容进行修改时,才会把数据内容拷贝到程序自己的应用空间里去,这时候,数据才成为该程序的私有数据。如果程序不需要对数据进行修改,那么永远都不需要拷贝数据到自己的应用空间里。这样就减少了数据的拷贝。 28 | 29 | # Links 30 | 31 | - https://www.linuxjournal.com/article/6345 32 | - https://www.jianshu.com/p/fad3339e3448 33 | - https://zhuanlan.zhihu.com/p/76640160 34 | - https://leokongwq.github.io/2017/01/12/linux-zero-copy.html 35 | -------------------------------------------------------------------------------- /01~进程与处理器/进程管理/COW.md: -------------------------------------------------------------------------------- 1 | # Copy on Write | 写时复制 2 | 3 | 在 Linux 程序中,`fork()` 会产生一个和父进程完全相同的子进程,但子进程在此后多会 exec 系统调用,出于效率考虑,Linux 中引入了写时复制技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。写时复制(Copy-on-write)是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。 4 | 5 | 也就是说,资源的复制只有在需要写入的时候才进行,在此之前,以只读方式共享。这种技术使地址空间上页的拷贝被推迟到实际发生写入的时候。有时共享页根本不会被写入,例如,`fork()` 后立即调用 `exec()`,就无须复制父进程的页了。`fork()` 的实际开销就是复制父进程的页表以及给子进程创建唯一的 PCB。这种优化可以避免拷贝大量根本就不会使用的数据(地址空间里常常包含数十兆的数据)。 6 | 7 | # fork 实现 8 | 9 | 传统的 fork()系统调用直接把所有的资源复制给新创建的进程。这种实现过于简单并且效率低下,因为它拷贝的数据也许并不共享,更糟的情况是,如果新进程打算立即执行一个新的映像,那么所有的拷贝都将前功尽弃。Linux 的 fork() 使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。在页根本不会被写入的情况下—举例来说,fork()后立即调用 exec()—它们就无需复制了。fork()的实际开销就是复制父进程的页表以及给子进程创建惟一的进程描述符。在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化可以避免拷贝大量根本就不会被使用的数据(地址空间里常常包含数十兆的数据)。由于 Unix 强调进程快速执行的能力,所以这个优化是很重要的。这里补充一点:Linux COW 与 exec 没有必然联系。 10 | 11 | 在 fork 之后 exec 之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果不是因为 exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为 exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。不过,fork 之后内核会通过将子进程放在队列的前面,以让子进程先执行,以免父进程执行导致写时复制,而后子进程执行 exec 系统调用,因无意义的复制而造成效率的下降。 12 | 13 | 实际上 COW 技术不仅仅在 Linux 进程上有应用,其他例如 C++的 String 在有的 IDE 环境下也支持 COW 技术,即例如: 14 | 15 | ```cpp 16 | string str1 = "hello world"; 17 | string str2 = str1; 18 | 19 | str1[1]='q'; 20 | str2[1]='w'; 21 | ``` 22 | 23 | 在开始的两个语句后,str1 和 str2 存放数据的地址是一样的,而在修改内容后,str1 的地址发生了变化,而 str2 的地址还是原来的,这就是 C++中的 COW 技术的应用,不过 VS2005 似乎已经不支持 COW。 24 | -------------------------------------------------------------------------------- /02~存储/存储 IO/磁盘 IO/README.md: -------------------------------------------------------------------------------- 1 | # Linux 磁盘 IO 2 | 3 | 磁盘是一个扁平的圆盘(与电唱机的唱片类似),盘面上有许多称为磁道的圆圈,数据就记录在这些磁道上。磁盘可以是单片的,也可以是由若干盘片组成的盘组,每一盘片上有两个面。如下图中所示的 6 片盘组为例,除去最顶端和最底端的外侧面不存储数据之外,一共有 10 个面可以用来保存信息。 4 | 5 | ![](http://hi.csdn.net/attachment/201106/7/8394323_13074405911zG7.jpg) 6 | 7 | ## IO 类别 8 | 9 | 不同应用通常具有不同的 IO 类型,了解应用的 IO 类型是为其设计解决方案、排错性能问题的首要工作。那 IO 类型通常包括哪些需要考虑的因素?我们今天就来谈一谈 IO 类型的几个重要方面。 10 | 11 | 读 vs. 写 12 | 13 | 应用程序的读写请求必须量化,了解他们之间的比列,因为读写对存储系统的资源消耗是不通的。了解读写比率直接关系到如何应用缓存、RAID类型等子系统的最佳实践。写通常需要比读更多的资源,SSD的写操作相对读更是慢得多。 14 | 15 | 顺序 vs. 随机 16 | 17 | 传统存储系统通常都是机械硬盘,因此整个系统设计为尽可能顺序化IO,减少由于磁盘寻道所带来的延迟。所以,顺序IO相对随机IO的性能会好很多。随机小IO消耗比顺序大IO更多的处理资源。随机小IO更在意系统处理IO的数量,即IOPS;而顺序大IO则更在意带宽,即MB/s。因此,如果系统承载了多种不同的应用,必须了解它们各自的需求,是对IOPS有要求,还是对带宽有要求。这往往需要在两种之间进行折衷考虑。闪盘是一个例外,它没有机械寻道操作,因此对随机小IO的处理是非常迅速的,由此是读操作。 18 | 19 | 大 IO vs. 小 IO 20 | 21 | 我们通常把<=16KB的IO认为是小IO,而>=32KB的IO认为是大IO。就单个IO来讲,大IO从微观的角度相比小IO会需要更多处理资源,不过对于智能存储系统来说,会尽可能把IO整理为顺序的,以单个操作执行,如此依赖,将多个小IO整理成单个大IO处理后,反而会更快。IO的大小依然取决于应用程序本身,了解IO的大小,影响到后期对缓存、RAID类型、LUN的一些属性的调优。 22 | 23 | 位置引用 24 | 25 | 数据的位置分布影响到后期对二级缓存或存储分层技术的应用,因为这些技术都会根据IO的位置分布来判断是否将IO放置到缓存或快速的层级。位置引用是指那些被频繁的存储位置,我们通常认为最新创建的数据以及最近被访问过的数据,它们周围的数据也同时被访问的可能性会比较大。因此,了解应用程序的IO位置特性,有助于应用正确的性能优化技术。 26 | 27 | 稳定 vs. 爆发 28 | 29 | IO数量在一天中的不同时段会有不同的表现。例如,早高峰时段的IO数量相比下班后的IO会多出许多。如果能准确预测和估计应用的IO在不同时间段的稳定性和爆发性,可以正确分配资源,提高资源利用率。在前期的设计阶段,就应该考虑系统是否能够处理IO高峰期。 30 | 31 | 多线程 vs. 单线程 32 | 33 | 多线程是实现并发操作的一种方式,同时也意味着对存储系统的资源消耗更多。这种高IOPS的请求方式,在有些情况下会造成磁盘繁忙,进而导致IO排队,增加了响应时间。因此,适度的调整线程数量,不仅可以实现并发,而且能在不拖累整个存储系统的情况下,达到最优的响应时间。 34 | 35 | ![image](https://user-images.githubusercontent.com/5803001/47896540-f882e500-dea8-11e8-947a-6fac0f755880.png) 36 | 37 | # Links 38 | 39 | - https://mp.weixin.qq.com/s/3mKxTH2pfXFpDvvJnDtgEQ 40 | -------------------------------------------------------------------------------- /02~存储/01~内存管理/页式存储管理/页结构.md: -------------------------------------------------------------------------------- 1 | # 页结构 2 | 3 | 请求分页式的页结构主要包含以下部分: 4 | 5 | - 页帧:被分成一页大小的逻辑地址 6 | - 页框:被分成一页大小的物理地址 7 | - 页表:页帧映射到页框的表格 8 | - 标志位:标志页帧是否已成功映射到页框,0 否 1 是 9 | 10 | 其中页表包含以下位: 11 | 12 | - 物理块号:指出该页在主存中的占用块(1 表示访问) 13 | - 状态位:指出该页是否已经调入主存(1 表示调入) 14 | - 访问字段:记录该页一段时间内被访问的次数或多久未被访问 15 | - 修改位:表示该页调入主存后是否被修改 16 | - 辅存地址:该页在磁盘上的地址 17 | 18 | 由于作业在磁盘上保留一份备份,若此次调入主存中后未被修改置换该页时不需要再将该页写回磁盘,减少系统开销;如果该页被修改,必须将该页写回磁盘上,保证信息的更新和完整。 19 | 20 | # 页表的实现 21 | 22 | 实现虚拟地址到物理地址转换最容易想到的方法是使用数组,对虚拟地址空间的每一个页,都分配一个数组项。但是有一个问题,考虑 IA32 体系结构下,页面大小为 4KB,整个虚拟地址空间为 4GB,则需要包含 1M 个页表项,这还只是一个进程,因为每个进程都有自己独立的页表。因此,系统所有的内存都来存放页表项恐怕都不够。 23 | 24 | 相像一下进程的虚拟地址空间,实际上大部分是空闲的,真正映射的区域几乎是汪洋大海中的小岛,因次我们可以考虑使用多级页表,可以减少页表内存使用量。实际上多级页表也是各种体系结构支持的,没有硬件支持,我们是没有办法实现页表转换的。 25 | 26 | 为了减少页表的大小并忽略未做实际映射的区域,计算机体系结构的设计都会靠虑将虚拟地址划分为多个部分。具体的体系结构划分方式不同,比如 ARM7 和 IA32 就有不同的划分。Linux 操作系统使用 4 级页表: 27 | 28 | ![Linear Address](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230430222257.png) 29 | 30 | 图中 CR3 保存着进程页目录 PGD 的地址,不同的进程有不同的页目录地址。进程切换时,操作系统负责把页目录地址装入 CR3 寄存器。地址翻译过程如下: 31 | 32 | - 对于给定的线性地址,根据线性地址的 bit22~bit31 作为页目录项索引值,在 CR3 所指向的页目录中找到一个页目录项。 33 | 34 | - 找到的页目录项对应着页表,根据线性地址的 bit12~bit21 作为页表项索引值,在页表中找到一个页表项。 35 | 36 | - 找到的页表项中包含着一个页面的地址,线性地址的 bit0~bit11 作为页内偏移值和找到的页确定线性地址对应的物理地址。 37 | 38 | 这个地址翻译过程完全是由硬件完成的。 39 | 40 | ## 页表转化失败 41 | 42 | 在地址转换过程中,有两种情况会导致失败发生。 43 | 44 | - 第一,要访问的地址不存在,这通常意味着由于编程错误访问了无效的虚拟地址,操作系统必须采取某种措施来处理这种情况,对于现代操作系统,发送一个段错误给程序;或者要访问的页面还没有被映射进来,此时操作系统要为这个线性地址分配相应的物理页面,并更新页表。 45 | 46 | - 第二,要查找的页不在物理内存中,比如页已经交换出物理内存。在这种情况下需要把页从磁盘交换回物理内存。 47 | 48 | CPU 的 Memory management unit(MMU)cache 了最近使用的页面映射。我们称之为 translation lookaside buffer(TLB)。TLB 是一个组相连的 cache。当一个虚拟地址需要转换成物理地址时,首先搜索 TLB。如果发现了匹配(TLB 命中),那么直接返回物理地址并访问。然而,如果没有匹配项(TLB miss),那么就要从页表中查找匹配项,如果存在也要把结果写回 TLB。 49 | -------------------------------------------------------------------------------- /01~进程与处理器/系统服务/服务与初始化.md: -------------------------------------------------------------------------------- 1 | # Linux 系统中的服务与初始化 2 | 3 | 自启动服务非常重要,例如 4 | 5 | (1)需要手动添加希望自启的服务,如安装 svn 后没有自动添加,就需要我们手动加入 6 | 7 | (2)安装某些程序后,自动加到自启动了,但我们不需要,需要手动移除 8 | 9 | (3)很多恶意程序都会把自己加入自启动,需要我们排查删除 10 | 11 | 运行级别的原理 12 | 13 | 涉及到自启动操作时,就会接触到 /etc/rc.d/rc[0-6].d 这 7 个目录 14 | 15 | 这几个目录代表着 linux 的 7 个运行级别 16 | 17 | 在 /etc/inittab 文件中指定了系统启动时默认进入哪个级别 18 | 19 | Linux 进入运行级别后,就会进入对应的 rcN.d 目录,按照文件名顺序检索目录下的文件 20 | 21 | rcN.d 目录下都是一些链接文件,这些链接文件都指向 /etc/init.d 目录下的 service 脚本文件,例如 22 | 23 | ![img](http://static.webhek.com/techug-res/uploads/2016/08/640.jpg) 24 | 25 | 命名规则为: 26 | K+nn+服务名,或 S+nn+服务名 27 | 28 | K – 系统将终止对应的服务 29 | 30 | S – 系统将启动对应的服务 31 | 32 | nn – 两位数字,执行顺序 33 | 34 | ![img](http://static.webhek.com/techug-res/uploads/2016/08/640-1.jpg) 35 | 7 个运行级别是什么? 36 | 37 | 0 – 停机 38 | 39 | 1 – 单用户模式,root 权限,用于系统维护,禁止远程登陆 40 | 2 – 多用户状态,没有 NFS 41 | 42 | 3 – 标准多用户模式,登陆后进入命令行模式 43 | 44 | 4 – 系统未使用,保留 45 | 46 | 5 – 多用户图形模式,登陆后进入图形 GUI 模式 47 | 48 | 6 – 重新启动 49 | 50 | 服务器一般都是命令行模式,所以默认运行级别为 3 51 | 52 | 如何添加自启程序? 53 | 54 | (1)/etc/init.d 目录中添加 55 | 56 | 以启动 SVN 为例 57 | 58 | 1)在 /etc/init.d 目录下创建启动服务的脚本 59 | 60 | vim /etc/init.d/svn 61 | 62 | 内容 63 | 64 | \#!/bin/bash 65 | svnserve -d -r /svn 仓库路径 66 | 67 | 设置执行权限 68 | 69 | chmod 755 /etc/init.d/svn 70 | 71 | 2)把这个脚本软链接到 /etc/rc.d/rc3.d 72 | 73 | ln -s /etc/init.d/svn /etc/rc.d/rc3.d/S101svn 74 | 75 | S 代表是要执行这个脚本,101 是执行顺序,通常要大于 60,这样可以保证基础服务都已经启动完成 76 | 77 | 重启 linux 测试 78 | 79 | (2)/etc/rc.local 文件中添加 80 | 81 | 直接修改 /etc/rc.local 82 | 83 | 该脚本是在系统初始化级别脚本运行之后再执行的,因此可以在里面添加想在系统启动之后执行的脚本 84 | 85 | (3)chkconfig 命令添加 86 | 87 | 如何禁止自启程序? 88 | 89 | (1)直接删除 /etc/rc.d/rcN.d 目录的目标链接文件 90 | 91 | (2)删除 /etc/rc.local 文件中定义的启动脚本 92 | 93 | (3)查看自启动服务列表,从中选择目标服务执行禁止操作 94 | 95 | chkconfig –list 96 | 97 | chkconfig –del 服务名 98 | -------------------------------------------------------------------------------- /10~Shell 命令/文本处理/awk.md: -------------------------------------------------------------------------------- 1 | # awk 2 | 3 | awk 是一种可以处理数据、产生格式化报表的语言。awk 的工作方式是读取数据文件,将每一行数据视为一条记录,每条记录以分隔符分成若干字段,然后输出。awk 常用的格式: 4 | 5 | 1. awk '样式' 文件,把符合样式的数据显示出来。 6 | 2. awk '{操作}' 文件,对每一行都执行{}中的操作。 7 | 3. awk '样式{操作}' 文件,对符合样式的数据进行括号里的操作。 8 | 9 | ```sh 10 | $ echo 'BEGIN' | awk '{print $0 "\nline one\nline two\nline three"}' 11 | BEGIN 12 | line one 13 | line two 14 | line three 15 | 16 | # 输出指定分割参数 17 | $ route -n | awk '/UG[ \t]/{print $2}' 18 | 19 | # 计算文件中的数值和 20 | $ awk '{s+=$1} END {printf "%.0f", s}' mydatafile 21 | # 显示含 La 的数据行 22 | awk '/La/' 1.log 23 | # 显示每一行的第1和第2个字段 24 | awk '{print $1, $2}' 1.log 25 | # 将含有 La 关键词的数据行的第 1 以及第 2 个字段显示出来 26 | awk '/La/{print $1, $2}' 1.log 27 | 28 | # EGIN 后紧跟的操作,在 awk 命令开始匹配第一行时执行,END 后面紧跟的操作在处理完后执行 29 | $ awk 'BEGIN {count=0}{count++} END{print count}' /etc/passwd 30 | $ awk -F ':' 'BEGIN {count=0;} {name[count] = $1;count++;}; END{for (i = 0; i < NR; i++) print i, name[i]}' /etc/passwd 31 | # 仅显示前 5 行 32 | $ awk -F : 'NR > 1 && NR <=5 {print $1}' /etc/passwd 33 | # 移除重复行 34 | $ awk '!visited[$0]++' your_file > deduplicated_file 35 | # 显示与 root 相关的用户 36 | $ awk -F : '/^root/{print $1, $2}' /etc/passwd 37 | ``` 38 | 39 | awk 也常用于与其他系统命令的协同操作: 40 | 41 | ```sh 42 | # NF 表示的是浏览记录的域的个数,$NF 表示的最后一个Field(列),即输出最后一个字段的内容 43 | $ free -m | grep buffers\/ | awk '{print $NF}' 44 | 45 | $ ps aux | awk '{print $2}' #获取所有进程PID 46 | ``` 47 | 48 | ## 内置变量 49 | 50 | ```sh 51 | ARGC 命令行参数个数 52 | ARGV 命令行参数排列 53 | ENVIRON 支持队列中系统环境变量的使用 54 | FILENAME awk浏览的文件名 55 | FNR 浏览文件的记录数 56 | FS 设置输入域分隔符,等价于命令行 -F选项 57 | NF 浏览记录的域的个数 58 | NR 已读的记录数 59 | OFS 输出域分隔符 60 | ORS 输出记录分隔符 61 | RS 控制记录分隔符 62 | ``` 63 | -------------------------------------------------------------------------------- /10~Shell 命令/README.md: -------------------------------------------------------------------------------- 1 | # Shell 命令指南 2 | 3 | ## Shell 简介 4 | 5 | Shell 是用户与 Linux 内核之间的接口程序,主要功能包括: 6 | 7 | - 命令解释器:解释用户输入的命令 8 | - 脚本执行器:执行 Shell 脚本 9 | - 系统调用接口:调用系统程序和应用程序 10 | 11 | ### 常见的 Shell 类型 12 | 13 | - **Bash** (Bourne Again Shell):Linux 默认 Shell,最常用 14 | - **Zsh**:基于 ksh 开发,功能强大,适合开发者使用 15 | - **Ksh** (KornShell):由 AT&T Bell lab 开发,兼容 Bash 16 | - **Tcsh**:整合 C Shell,提供扩展功能 17 | - **其他**:sh(已被 bash 取代)、csh(已被 tcsh 取代) 18 | 19 | > 注:Windows 用户可通过 Git Bash、WSL(Windows 10+) 等工具使用 Shell 20 | 21 | ## Shell 学习路线 22 | 23 | ### 1. 入门基础 24 | 25 | 1. **Shell 基础概念** 26 | 27 | - Shell 的本质与工作原理 28 | - 命令行基本操作 29 | - 脚本创建与执行 30 | 31 | 2. **核心语法** 32 | - 变量与数据类型 33 | - 运算符(算术、逻辑、字符串) 34 | - 控制结构(if-else、循环) 35 | 36 | ### 2. 进阶知识 37 | 38 | 1. **函数与模块化** 39 | 40 | - 函数定义与调用 41 | - 参数传递 42 | - 脚本模块化管理 43 | 44 | 2. **文件操作** 45 | 46 | - 文件读写与权限 47 | - 文本处理(grep、sed、awk) 48 | - 目录操作 49 | 50 | 3. **系统编程** 51 | - 进程管理 52 | - 系统监控 53 | - 网络配置 54 | 55 | ### 3. 高级特性 56 | 57 | 1. **进阶技术** 58 | 59 | - 正则表达式 60 | - 信号处理 61 | - 管道与重定向 62 | 63 | 2. **实用工具** 64 | - 常用命令(find、xargs、crontab) 65 | - 文本处理工具 66 | - 系统管理工具 67 | 68 | ### 4. 最佳实践 69 | 70 | 1. **编程规范** 71 | 72 | - 代码风格 73 | - 错误处理 74 | - 日志记录 75 | 76 | 2. **安全编程** 77 | 78 | - 输入验证 79 | - 权限控制 80 | - 安全隐患防范 81 | 82 | 3. **实战应用** 83 | - 系统维护脚本 84 | - 自动化部署 85 | - CI/CD 集成 86 | 87 | ## 学习建议 88 | 89 | 1. 🎯 **循序渐进** 90 | 91 | - 从基础命令开始 92 | - 逐步过渡到脚本编写 93 | - 最后尝试复杂应用 94 | 95 | 2. 💻 **实践为主** 96 | 97 | - 动手编写实用脚本 98 | - 阅读优秀开源项目 99 | - 解决实际问题 100 | 101 | 3. 🔄 **持续学习** 102 | - 关注新特性 103 | - 学习优秀工具 104 | - 培养安全意识 105 | 106 | > 记住:Shell 编程最重要的是实践,建议边学边做,多写实用脚本! 107 | -------------------------------------------------------------------------------- /02~存储/存储 IO/README.md: -------------------------------------------------------------------------------- 1 | # 存储与 IO 必知必会 2 | 3 | ![The Linux Storage Stack Diagram](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230622203509.png) 4 | 5 | 计算机的内存往往包括了 RAM 与 ROM,ROM 表示的是只读存储器,即:它只能读出信息,不能写入信息,计算机关闭电源后其内的信息仍旧保存,如计算机启动用的 BIOS 芯片。RAM 表示的是读写存储器,可其中的任一存储单元进行读或写操作,计算机关闭电源后其内的信息将不在保存,再次开机需要重新装入,通常用来存放操作系统,各种正在运行的软件、输入和输出数据、中间结果及与外存交换信息等,我们常说的内存主要是指 RAM。 6 | 7 | 所谓外存/辅存(Storage),狭义上是讲的硬盘;准确地说,是外部存储器(需要通过 IO 系统与之交换数据,全称为辅助存储设备)。 8 | 9 | 计算机硬件性能在过去十年间的发展普遍遵循摩尔定律,通用计算机的 CPU 主频早已超过 3GHz,内存也进入了普及 DDR4 的时代。然而传统硬盘虽然在存储容量上增长迅速,但是在读写性能上并无明显提升,同时 SSD 硬盘价格高昂,不能在短时间内完全替代传统硬盘。传统磁盘的 IO 读写速度成为了计算机系统性能提高的瓶颈,制约了计算机整体性能的发展。 10 | 11 | # 存储 IO 的耗时 12 | 13 | ### 计算机存储体系 14 | 15 | ![存储体系示意图](https://2836672763-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-LY_HB8UaEfE1efciC8V%2F-LY_K0SNgM4yb-lsVBlJ%2FScreen%20Shot%202019-02-13%20at%201.28.29%20PM.jpg?alt=media&token=8cd28260-ebb5-4729-8a41-732675a64afc) 16 | 17 | ![不同存储器的数据获取时间对照表](https://2836672763-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LMjQD5UezC9P8miypMG%2F-LY_HB8UaEfE1efciC8V%2F-LY_Kgs6xp4XVNA9n-FF%2FScreen%20Shot%202019-02-13%20at%201.31.21%20PM.jpg?alt=media&token=f4dade9f-4870-4c87-83bb-bd419e087ce1) 18 | 19 | ```sh 20 | L1 cache reference 0.5 ns 21 | 22 | Branch mispredict 5 ns 23 | 24 | L2 cache reference 7 ns 25 | 26 | Mutex lock/unlock 25 ns 27 | 28 | Main memory reference 100 ns 29 | 30 | Compress 1K bytes with Zippy 3,000 ns 31 | 32 | Send 2K bytes over 1 Gbps network 20,000 ns 33 | 34 | Read 1 MB sequentially from memory 250,000 ns 35 | 36 | Round trip within same datacenter 500,000 ns 37 | 38 | Disk seek 10,000,000 ns 39 | 40 | Read 1 MB sequentially from disk 20,000,000 ns 41 | 42 | Send packet CA->Netherlands->CA 150,000,000 ns 43 | ``` 44 | 45 | # Links 46 | 47 | - https://my.oschina.net/ericquan8/blog/1836953 48 | -------------------------------------------------------------------------------- /10~Shell 命令/磁盘文件/iostat.md: -------------------------------------------------------------------------------- 1 | # iostat 2 | 3 | ```sh 4 | $ iostat -x -d 2 5 | 6 | Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util 7 | vda 0.00 0.25 0.04 0.53 0.56 4.88 19.25 0.00 6.85 3.09 7.14 0.25 0.01 8 | ``` 9 | 10 | iostat 算是比较重要的查看块设备运行状态的工具,它数据的来源是 Linux 操作系统的/proc/diskstats。一般来说用法如下:`iostat -mtx 2`,即每 2 秒钟采集一组数据。假如我们对某块磁盘进行读写压测: 11 | 12 | ```sh 13 | $ fio --name=randwrite --rw=randwrite --bs=4k --size=20G --runtime=1200 --ioengine=libaio --iodepth=64 --numjobs=1 --rate_iops=5000 --filename=/dev/sdf --direct=1 --group_reporting 14 | ``` 15 | 16 | 使用 iostat 可以查看如下结果: 17 | 18 | ![](https://ww1.sinaimg.cn/large/007rAy9hgy1g2104el9uaj30u00fi40h.jpg) 19 | 20 | 上图中,%util,即为磁盘 I/O 利用率,同 CPU 利用率一样,这个值也可能超过 100%(存在并行 I/O);rkB/s 和 wkB/s 分别表示每秒从磁盘读取和写入的数据量,即吞吐量,单位为 KB;磁盘 I/O 处理时间的指标为 r_await 和 w_await 分别表示读/写请求处理完成的响应时间,svctm 表示处理 I/O 所需要的平均时间,该指标已被废弃,无实际意义。r/s + w/s 为 IOPS 指标,分别表示每秒发送给磁盘的读请求数和写请求数;aqu-sz 表示等待队列的长度。 21 | 22 | - rrqm/s : 每秒合并读操作的次数,块设备有相应的调度算法。如果两个 IO 发生在相邻的数据块时,他们可以合并成 1 个 IO。 23 | 24 | - wrqm/s: 每秒合并写操作的次数 25 | 26 | - r/s:每秒读操作的次数 27 | 28 | - w/s : 每秒写操作的次数 29 | 30 | - rMB/s :每秒读取的 MB 字节数 31 | 32 | - wMB/s: 每秒写入的 MB 字节数 33 | 34 | - avgrq-sz:每个 IO 的平均扇区数,即所有请求的平均大小,以扇区(512 字节)为单位。 35 | 36 | - avgqu-sz:平均为完成的 IO 请求数量,即平均意义山的请求队列长度,该值越大,表示排队等待处理的 io 越多。 37 | 38 | - await:平均每个 IO 所需要的时间,包括在队列等待的时间,也包括磁盘控制器处理本次请求的有效时间。 39 | 40 | - r_wait:每个读操作平均所需要的时间,不仅包括硬盘设备读操作的时间,也包括在内核队列中的时间。 41 | 42 | - w_wait: 每个写操平均所需要的时间,不仅包括硬盘设备写操作的时间,也包括在队列中等待的时间。 43 | 44 | - svctm:表面看是每个 IO 请求的服务时间,不包括等待时间,但是实际上,这个指标已经废弃。实际上,iostat 工具没有任何一输出项表示的是硬盘设备平均每次 IO 的时间。 45 | 46 | - %util:工作时间或者繁忙时间占总时间的百分比。 47 | 48 | 值得关注的是,avgrq-sz 这个值反应了用户的 IO 模式,即用户过来的 IO 是大 IO 还是小 IO。如果我们 fio 命令设置的 bs 为 4k,那么 sdc 的 avgrq-sz 总是 8,即`8个扇区 = 8*512(Byte)= 4KB`。 49 | -------------------------------------------------------------------------------- /10~Shell 命令/Shell 编程/语法基础/Shell 变量/变量.md: -------------------------------------------------------------------------------- 1 | # Shell 变量详解 2 | 3 | ## 变量类型 4 | 5 | Shell 中存在三种变量: 6 | 7 | 1. **局部变量** 8 | 9 | - 仅在当前 Shell 实例中有效 10 | - 其他 Shell 启动的程序无法访问 11 | 12 | 2. **环境变量** 13 | 14 | - 所有程序(包括 Shell 启动的程序)都能访问 15 | - 可在 Shell 脚本中定义 16 | - 某些程序依赖环境变量正常运行 17 | 18 | 3. **Shell 变量** 19 | - Shell 程序设置的特殊变量 20 | - 包含环境变量和局部变量 21 | - 用于确保 Shell 正常运行 22 | 23 | ## 变量操作基础 24 | 25 | ### 1. 变量定义与使用 26 | 27 | ```sh 28 | # 定义变量(注意:等号两边不能有空格) 29 | your_name="qinjx" 30 | 31 | # 使用变量 32 | echo $your_name 33 | echo ${your_name} # 使用花括号帮助解释器识别变量边界 34 | ``` 35 | 36 | ### 2. 变量赋值方式 37 | 38 | ```sh 39 | # 直接赋值 40 | name="John" 41 | 42 | # 命令执行结果赋值 43 | files=$(ls /etc) # 推荐方式 44 | files=`ls /etc` # 旧式语法 45 | ``` 46 | 47 | ### 3. 变量删除 48 | 49 | ```sh 50 | unset variable_name 51 | ``` 52 | 53 | ## 特殊变量操作 54 | 55 | ### 1. 只读变量 56 | 57 | ```sh 58 | myUrl="http://www.google.com" 59 | readonly myUrl # 设置为只读,值不能被改变 60 | ``` 61 | 62 | ### 2. 变量导出 63 | 64 | ```sh 65 | export backup="/nas10/mysql" # 导出变量到子进程 66 | export -p # 查看所有导出的变量 67 | ``` 68 | 69 | ### 3. 变量检测与默认值 70 | 71 | ```sh 72 | # 变量存在性检测 73 | ${varName?Error varName is not defined} # 变量未定义时报错 74 | ${varName:?Error varName is not defined or empty} # 变量未定义或为空时报错 75 | 76 | # 设置默认值 77 | echo ${var:-DefaultValue} # 仅使用默认值,不改变原变量 78 | echo ${var:=DefaultValue} # 使用默认值并赋值给变量 79 | ``` 80 | 81 | ## 常用系统变量 82 | 83 | | 变量名 | 说明 | 查看方式 | 84 | | -------- | --------------- | ---------------- | 85 | | HOME | 当前用户主目录 | `echo $HOME` | 86 | | PATH | 命令搜索路径 | `echo $PATH` | 87 | | SHELL | 当前 Shell 路径 | `echo $SHELL` | 88 | | HISTSIZE | 命令历史记录数 | `echo $HISTSIZE` | 89 | | LANG | 系统语言设置 | `echo $LANG` | 90 | | PS1 | 命令提示符设置 | `echo $PS1` | 91 | 92 | ## 最佳实践 93 | 94 | 1. 变量名使用有意义的描述性名称 95 | 2. 重要变量建议使用 readonly 保护 96 | 3. 关键变量建议做存在性检测 97 | 4. 需要跨进程使用的变量要用 export 导出 98 | 5. 使用花括号 `${}` 来明确变量边界 99 | -------------------------------------------------------------------------------- /03~网络/README.md: -------------------------------------------------------------------------------- 1 | # 网络 IO 2 | 3 | 在[虚拟存储](https://ng-tech.icu/books/DistributedSystem-Notes/#/?q=虚拟存储存储)一章中我们讨论了内核空间与用户空间的划分,即针对 32 位 Linux 操作系统而言,将最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF),供内核使用,称为内核空间,而将较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF),供各个进程使用,称为用户空间。 4 | 5 | ![读取网卡数据](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230430222322.png) 6 | 7 | 有了用户空间和内核空间,整个 Linux 内部结构可以分为三部分,从最底层到最上层依次是:硬件–>内核空间–>用户空间。我们都知道,为了 OS 的安全性等的考虑,进程是无法直接操作 IO 设备的,其必须通过系统调用请求内核来协助完成 IO 动作,而内核会为每个 IO 设备维护一个 Buffer。 8 | 9 | # 内核包处理(Kernel Packet Processing) 10 | 11 | 回到最简单的传统实现,网卡接收到一个数据包,并向 Linux 内核发送一个中断,指出存在应该处理的数据包。内核停止其他工作,将上下文切换到中断处理程序,处理数据包,然后切换回其正在执行的操作。 12 | 13 | ![传统的中断驱动](https://s2.ax1x.com/2019/11/24/MOUPOA.png) 14 | 15 | 这种上下文切换很慢,这在 90 年代对 10Mbit NIC 来说可能还不错,但是在 NIC 为 10G 且最大线路速率的现代服务器上,每秒可以带来大约 1500 万个数据包,而在具有 8 个核心的小型服务器上 这可能意味着内核每个内核每秒中断数百万次。 16 | 17 | 多年前,Linux 不再不断处理中断,而是添加了 NAPI,这是现代驱动程序用来提高高数据包速率性能的网络 API。在低速率下,内核仍然按照我们提到的方法接受来自 NIC 的中断。一旦有足够的数据包到达并超过阈值,它将禁用中断,而是开始轮询 NIC 并分批提取数据包。该处理在 “softirq” 或软件中断上下文中完成。这发生在系统调用和硬件中断的末尾,这是内核(而不是用户空间)已经在运行的时候。 18 | 19 | ![NAPI](https://s2.ax1x.com/2019/11/24/MOU3T0.png) 20 | 21 | 这快得多,但是带来了另一个问题。如果要处理的数据包如此之多,以至于我们花了所有的时间来处理来自 NIC 的数据包,但又没有时间让用户空间进程实际上耗尽那些队列(从 TCP 连接等读取),会发生什么?最终,队列将满,我们将开始丢弃数据包。为了使公平起见,内核将在给定 softirq 上下文中处理的数据包数量限制为一定的预算。超出预算后,它会唤醒一个称为 ksoftirqd 的单独线程(您将在每个内核的 ps 中看到其中一个),该线程将在正常的系统调用/中断路径之外处理这些 softirq。使用标准流程调度程序调度该线程,该进程已经尝试公平了。 22 | 23 | ![NAPI polling crossing threshold to schedule ksoftirqd](https://s2.ax1x.com/2019/11/24/MOU7h8.md.png) 24 | 25 | 纵观内核处理数据包的方式,我们可以肯定地有机会停止处理。如果两次 softirq 处理调用之间的时间增加,则在处理数据包之前,数据包可能会在 NIC RX 队列中停留一段时间。这可能是导致 CPU 内核死锁的原因,也可能是导致内核无法运行 softirqs 的缓慢原因。 26 | 27 | # Links 28 | 29 | - NGTE 扩展阅读:关于 IO 模型、IO 多路复用参阅《[Concurrent-Notes](https://github.com/wx-chevalier/Concurrent-Notes?q=)》;关于网络协议参阅《[Network-Notes](https://github.com/wx-chevalier/Network-Notes?q=)》 30 | 31 | - [2017~调整 Linux I/O 调度器优化系统性能](https://cubox.pro/c/qhBwRv): Linux I/O 调度器是 Linux 内核中的一个组成部分,用户可以通过调整这个调度器来优化系统性能。本文首先介绍 Linux I/O 调度器的结构,然后介绍如何根据不同的存储器来设置 Linux I/O 调度器从而达到优化系统性能。 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![License: CC BY-NC-SA 4.0](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg) ![](https://parg.co/bDm) 2 | 3 | # Linux 实践笔记 4 | 5 | 操作系统是管理计算机硬件与软件资源的计算机程序,同时也是计算机系统的内核与基石。计算机系统由硬件和软件两部分组成。操作系统 (OS,Operating System) 是配置在计算机硬件上的第一层软件,是对硬件系统的首次扩充。它在计算机系统中占据了特别重要的地位;而其它的诸如汇编程序、编译程序、数据库管理系统等系统软件,以及大量的应用软件,都将依赖于操作系统的支持,取得它的服务。以 Intel Pentium 系统产品系列的模型为例: 6 | 7 | ![image](https://user-images.githubusercontent.com/5803001/52262868-a8646480-2968-11e9-963e-c91128a6fe2c.png) 8 | 9 | 操作系统已成为现代计算机系统(大、中、小及微型机)、多处理机系统、计算机网络、多媒体系统以及嵌入式系统中都必须配置的、最重要的系统软件。从一般用户的观点,可把 OS 看做是用户与计算机硬件系统之间的接口;从资源管理的观点看,则可把 OS 视为计算机系统资源的管理者。另外,OS 实现了对计算机资源的抽象,隐藏了对硬件操作的细节,使用户能更方便地使用机器。 10 | 11 | # 操作系统的作用 12 | 13 | ## 用户与计算机硬件系统之间的接口 14 | 15 | OS 处于用户与计算机硬件系统之间,用户通过 OS 来使用计算机系统。或者说,用户在 OS 帮助下,能够方便、快捷、安全、可靠地操纵计算机硬件和运行自己的程序。 16 | 17 | 用户可以通过如下三种方式使用操作系统 18 | 19 | - 命令方式。这是指由 OS 提供了一组联机命令接口,以允许用户通过键盘输入有关命令来取得操作系统的服务,并控制用户程序的运行。 20 | - 系统调用方式。OS 提供了一组系统调用,用户可在自己的应用程序中通过相应的系统调用,来实现与操作系统的通信,并取得它的服务。 21 | - 图形、窗口方式。这是当前使用最为方便、最为广泛的接口,它允许用户通过屏幕上的窗口和图标来实现与操作系统的通信,并取得它的服务。 22 | 23 | ## 计算机系统资源的管理者 24 | 25 | 在一个计算机系统中,通常都含有各种各样的硬件和软件资源。归纳起来可将资源分为四类:处理器、存储器、IO 设备以及信息 ( 数据和程序 )。相应地,OS 的主要功能也正是针对这四类资源进行有效的管理,即:处理机管理,用于分配和控制处理机;存储器管理,主要负责内存的分配与回收;IO 设备管理,负责 IO 设备的分配与操纵;文件管理,负责文件的存取、共享和保护。可见,OS 的确是计算机系统资源的管理者。事实上,当今世界上广为流行的一个关于 OS 作用的观点,正是把 OS 作为计算机系统的资源管理者。 26 | 27 | ## 计算机资源的抽象 28 | 29 | 对于一个完全无软件的计算机系统(即裸机),它向用户提供的是实际硬件接口(物理接口),用户必须对物理接口的实现细节有充分的了解,并利用机器指令进行编程,因此该物理机器必定是难以使用的。为了方便用户使用 IO 设备,人们在裸机上覆盖上一层 IO 设备管理软件,由它来实现对 IO 设备操作的细节,并向上提供一组 IO 操作命令,如 Read 和 Write 命令,用户可利用它来进行数据输入或输出,而无需关心 IO 是如何实现的。此时用户所看到的机器将是一台比裸机功能更强、使用更方便的机器。这就是说,在裸机上铺设的 IO 软件隐藏了对 IO 设备操作的具体细节,向上提供了一组抽象的 IO 设备。 30 | 31 | # About 32 | 33 | ## Copyright & More | 延伸阅读 34 | 35 | 笔者所有文章遵循 [知识共享 署名-非商业性使用-禁止演绎 4.0 国际许可协议](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh),欢迎转载,尊重版权。您还可以前往 [NGTE Books](https://ng-tech.icu/books-gallery/) 主页浏览包含知识体系、编程语言、软件工程、模式与架构、Web 与大前端、服务端开发实践与工程架构、分布式基础架构、人工智能与深度学习、产品运营与创业等多类目的书籍列表: 36 | 37 | [![NGTE Books](https://s2.ax1x.com/2020/01/18/19uXtI.png)](https://ng-tech.icu/books-gallery/) 38 | -------------------------------------------------------------------------------- /02~存储/01~内存管理/页式存储管理/预调式管理/页面置换算法.md: -------------------------------------------------------------------------------- 1 | # Page-Replacement Algorithms | 页面置换算法 2 | 3 | # 页面置换策略 4 | 5 | 在请求分页式管理中,常见的页面置换策略包含了固定分配局部置换,可变分配局部置换,可变分配全局置换这三种方式。 6 | 7 | - 固定分配局部置换为每一个进程分配一定数目的主存物理块,在整个运行期间不在改变;缺点是难以为进程分配准确的内存数量。若太少,会频繁出现缺页中断,影响进程性能;若太多,会使内存驻留的进程数据减少,造成内存利用率下降。 8 | - 可变分配局部置换先分配一定数目的内存物理块,运行过程中频繁缺页中断就分配若干附加的物理块,缺页中断次数过少则缩小为该进程分配的物理块,控制缺页中断次数在一个合理的范围。 9 | - 可变分配全局置换是为每一进程分配一定数目的物理 块,保持一个空闲块队列,发生缺页中断则从该队列中取出一块,用完该队列中的物理块采用内存中选择页面进行置换(可能是系统中任一进程的页)。 10 | 11 | # OPT 最佳置换算法 12 | 13 | 淘汰的页面是以后永远不再使用或者是将来最长时间内不再被访问的页面。 14 | 15 | # FIFO 先进先出置换算法 16 | 17 | 先淘汰最近进入内存的页面(认为刚被调入的页面在最近的将来被访问的可能很大),淘汰在内存中驻留时间最长的页面。Belady 现象:在未给作业分配足够要求的页面数时,分配的物理块数增多,缺页中断次数反而增加;没有考虑程序执行的动态特征。 18 | 19 | ![image](https://user-images.githubusercontent.com/5803001/52262431-6d156600-2967-11e9-860b-951b5bf58c98.png) 20 | 21 | # LRU 最近最久未使用 22 | 23 | FIFO 置换算法性能之所以较差,是因为它所依据的条件是各个页面调入内存的时间,而页面调入的先后并不能反映页面的使用情况。最近最久未使用 (LRU) 的页面置换算法,是根据页面调入内存后的使用情况进行决策的。由于无法预测各页面将来的使用情况,只能利用“最近的过去”作为“最近的将来”的近似,因此,LRU 置换算法是选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最久未使用的页面予以淘汰。 24 | 25 | 利用 LRU 算法对上例进行页面置换的结果如下图所示。当进程第一次对页面 2 进行访问时,由于页面 7 是最近最久未被访问的,故将它置换出去。当进程第一次对页面 3 进行访问时,第 1 页成为最近最久未使用的页,将它换出。由图可以看出,前 5 个时间的图像与最佳置换算法时的相同,但这并非是必然的结果。因为,最佳置换算法是从 “ 向后看 ” 的观点出发的,即它是依据以后各页的使用情况;而 LRU 算法则是 “ 向前看 ” 的,即根据各页以前的使用情况来判断,而页面过去和未来的走向之间并无必然的联系。 26 | 27 | 在一个请求分页系统中,采用 LRU 页面置换算法,一个作业的页面走向为 1、2、3、4、2、1、5、6、2、1、2、3、7、6、3、2、1、2、3、6。当分配给该作业的物理块数为 5 时,在访问过程中所发生的缺页次数为 5 次。 28 | 29 | # CLOCK 时钟置换算法 30 | 31 | 当该页被访问时,由硬件将它的引用位信息置为 1;操作系统选择一个时间周期 T,每隔一个周期 T,将页表中所有页面的引用位信息置 0;这样,在时间周期 T 内,被访问过的页面的引用位为 1,而没有被访问过的页面的引用位仍为 0;当产生缺页中断时,可以从引用位为 0 的页面中选择一页调出,同时将所有页面的引用位信息全部重置 0。 32 | 33 | 淘汰一个页面时,如果该页面已被修改过,必须将它重新写回磁盘;但如果淘汰的是未被修改过的页面,就不需要写盘操作了,这样看来淘汰修改过的页面比淘汰未被修改过的页面开销要大 34 | (1)最近没有被引用,没有被修改(r=0,m=0) 35 | (2)最近被引用,没有被修改(r=1,m=0) 36 | (3)最近没有被引用,但被修改(r=0,m=1) 37 | (4)最近被引用过,也被修改过(r=1,m=1) 38 | 39 | 周期 T 的确定:T 太大,可能所有的引用位都变成 1,找不出最近最少使用的页面淘汰;T 太小,引用位为 0 的页面可能很多,而无法保证所选择的页面是最近最少使用的 40 | 41 | # LFU 最近最不常用置换算法 42 | 43 | 选择被访问次数最少的页面调出,即认为在过去的一段时间里被访问次数多的页面可能经常需要访问;周期 T 的确定:为每一页设置一个计数器,页面每次被访问后其对应的计数器加 1,每隔一定的时间周期 T,将所有计数器全部清零。 44 | -------------------------------------------------------------------------------- /02~存储/01~内存管理/虚拟存储管理.md: -------------------------------------------------------------------------------- 1 | # 虚拟存储管理 2 | 3 | 在[存储管理](https://parg.co/Z47)一节中我们介绍了计算机系统中常见的存储类型与管理方式,本章则是对于虚拟存储管理相关的内容进行详细介绍。 4 | 5 | 虚拟存储器是基于程序局部性原理上的一种假想的而不是物理存在的存储器,允许用户程序以逻辑地址来寻址,而不必考虑物理上可获得的内存大小。这种将物理空间和逻辑空间分开编址但又统一管理和使用的技术为用户编程提供了极大方便,它为每个进程提供了一个假象,即每个进程都在独占地使用主存。此时,用户作业空间称虚拟地址空间,其中的地址称虚地址。 6 | 7 | 虚拟存储器的容量由计算机的地址结构和辅助存储器的容量决定,建立在离散分配的存储管理方式的基础上,它允许将一个作业分多次调入内存。Linux 内存管理的设计充分利用了计算机系统所提供的虚拟存储技术,真正实现了虚拟存储器管理,其关注于程序编译链接后形成的地址空间管理、如何将虚地址转化为物理地址等方面。 8 | 9 | 从进程的视角来看,其虚拟地址空间最上面的区域是为操作系统中的代码和数据保留的,这对所有进程来说都是一样的。地址空间的底部区域存放用户进程定义的代码和数据。 10 | 11 | ![image](https://user-images.githubusercontent.com/5803001/52272019-52032000-2980-11e9-953c-89de286e5174.png) 12 | 13 | # 虚拟内存、内核空间和用户空间 14 | 15 | Linux 简化了分段机制,使得虚地址与线性地址总是一致的;系统将虚拟地址分为两部分:一部分专门给系统内核使用,另一部分给用户进程使用。对于 32 位的系统,虚拟地址范围是 0x00000000 ~ 0xFFFFFFFF,即最大虚拟内存为 2^32 Bytes = 4GB,系统将最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF)分配内核使用,此区域称作内核空间;另外将较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF),供各个进程使用,称为用户空间。 16 | 17 | ![内存地址与核空间](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230430222956.png) 18 | 19 | 因为每个进程可以通过系统调用进入内核,因此, Linux 内核空间由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有 4GB 的虚拟地址空间(也叫虚拟内存)。 20 | 21 | - 内核空间:系统内核使用的内存空间,当一个进程执行调用系统命令(例如 read, write)时,会进入内核代码的执行,进程此时的状态我们称之为内核态。 22 | - 用户空间:用户进程使用的内存空间,当一个进程执行用户自己的代码时,该进程此时的状态为用户态。 23 | 24 | 任意一个时刻,在一个 CPU 上只有一个进程在运行。所以对于此 CPU 来讲,在这时刻,整个系统只存在一个 4GB 的虚拟地址空间,这个虚拟地址空间是面向此进程的。当进程发生切换的时候,虚拟地址空间也随着切换。Linux 为每一个进程都建立其页表,将每个进程的虚拟地址空间根据自己的需要映射到物理地址空间上。既然某一时刻在某一 CPU 上只能有个进程在运行,那么当进程发生切换的时候,将页表也更换为相应进程的页表,这就可以实现每个进程都有自己的虚拟地址空间而互不影响。所以,在任意时刻对于一个 CPU 来说,只需要有当前进程的页表,就可以实现其虚拟地址到物理地址的转化。 25 | 26 | 虽然内核空间占据了每个虚拟空间中的最高 1GB,但映射到物理内存却总是从最低的地址(0x0000000)开始的,以方便在内核空间与物理内存之间建立起简单的线性映射关系。其中,3GB(0xC000000)就是物理地址与虚拟地址之间的位移量,在 Linux 代码中就叫做 `PAGE_OFFSET`。 27 | 28 | # 分页式管理 29 | 30 | Linux 采用了分页式虚拟存储系统,当作业被调度投入运行时,并不把作业的程序和数据全部装入主存,而仅仅装入立即使用的那些页面,至少要将作业的第一页信息装入主存,在执行过程中访问到不在主存的页面时,再把它们动态地装入。用得较多的分页式虚拟存储管理是请页式(Demand Paging),当需要执行某条指令或使用某个数据,而发现它们并不在主存时,产生一个缺页中断,系统从辅存中把该指令或数据所在的页面调入内存。 31 | 32 | 如前文介绍,每个进程都有 3G 的私有进程空间,所以系统的物理内存无法对这些地址空间进行一一映射,因此内核需要一种机制,把进程地址空间映射到物理内存上。当一个进程请求访问内存时,操作系统通过存储在内核中的进程页表把这个虚拟地址映射到物理地址,如果还没有为这个地址建立页表项,那么操作系统就为这个访问的地址建立页表项。最基本的映射单位是 Page,对应的是页表项 PTE。 33 | 34 | 页表是内存管理系统中的数据结构,用于向每个进程提供一致的虚拟地址空间,每个页表项保存的是虚拟地址到物理地址的映射以及一些管理标志。应用进程只能访问虚拟地址,内核必须借助页表和硬件把虚拟地址翻译为对物理地址的访问。页表项和物理地址是多对一的关系,即多个页表项可以对应一个物理页面,因而支持共享内存的实现(几个进程同时共享物理内存)。 35 | -------------------------------------------------------------------------------- /02~存储/存储 IO/磁盘 IO/块 IO 栈.md: -------------------------------------------------------------------------------- 1 | # 块设备 IO 栈 2 | 3 | 介绍块设备的 IO 栈之前,我们先来了解一下块 IO 栈的几个基本概念: 4 | 5 | - bio: bio 是通用块层 IO 请求的数据结构,表示上层提交的 IO 请求,一个 bio 包含多个 page,这些 page 必须对应磁盘上一段连续的空间。由于文件在磁盘上并不连续存放,文件 IO 提交到块设备之前,极有可能被拆成多个 bio 结构。 6 | 7 | - request: 表示块设备驱动层 I/O 请求,经由 I/O 调度层转换后的 I/O 请求,将会发到块设备驱动层进行处理。 8 | 9 | - request_queue: 维护块设备驱动层 I/O 请求的队列,所有的 request 都插入到该队列,每个磁盘设备都只有一个 queue(多个分区也只有一个)。 10 | 11 | 这 3 个结构的关系如下图示:一个 request_queue 中包含多个 request,每个 request 可能包含多个 bio,请求的合并就是根据各种原则将多个 bio 加入到同一个 requesst 中。 12 | 13 | ![块 IO 结构](https://s2.ax1x.com/2019/09/04/nEIaDS.png) 14 | 15 | # 请求处理方式 16 | 17 | ![块 IO 处理流程](https://s2.ax1x.com/2019/09/04/nEI0EQ.png) 18 | 19 | 如图所示是块设备的 I/O 栈,其中的红色文字表示关键 I/O 路径的函数。对于 I/O 的读写流程,逻辑比较复杂,这里以写流程简单描述如下: 20 | 21 | - 用户调用系统调用 write 写一个文件,会调到 sys_write 函数; 22 | 23 | - 经过 VFS 虚拟文件系统层,调用 vfs_write,如果是缓存写方式,则写入 page cache,然后就返回,后续就是刷脏页的流程;如果是 Direct I/O 的方式,就会走到 do_blockdev_direct_IO 的流程; 24 | 25 | - 如果操作的设备是逻辑设备如 LVM,MDRAID 设备等,会进入到对应内核模块的处理函数里进行一些处理,否则就直接构造 bio 请求,调用 submit_bio 往具体的块设备下发请求,submit_bio 函数通过 generic_make_request 转发 bio,generic_make_request 是一个循环,其通过每个块设备下注册的 q->make_request_fn 函数与块设备进行交互; 26 | 27 | - 请求下发到底层的块设备上,调用块设备请求处理函数`__make_request`进行处理,在这个函数中就会调用 blk_queue_bio,这个函数就是合并 bio 到 request 中,也就是 I/O 调度器的具体实现:如果几个 bio 要读写的区域是连续的,就合并到一个 request;否则就创建一个新的 request,把自己挂到这个 request 下。合并 bio 请求也是有限度的,如果合并后的请求超过阈值(在/sys/block/xxx/queue/max_sectors_kb 里设置),就不能再合并成一个 request 了,而会新分配一个 request; 28 | 29 | - 接下来的 I/O 操作就与具体的物理设备有关了,交由相应的块设备驱动程序进行处理,这里以 scsi 设备为例说明,queue 队列的处理函数`q->request_fn` 对应的 scsi 驱动的就是 `scsi_request_fn` 函数,将请求构造成 scsi 指令下发到 scsi 设备进行处理,处理完成后就会依次调用各层的回调函数进行完成状态的一些处理,最后返回给上层用户。 30 | 31 | # request-based 和 bio-based 32 | 33 | 在块设备的 I/O 处理流程中,会涉及到两种不同的处理方式: 34 | 35 | - request-based:这种处理方式下,会进行 bio 合并到 request(即 I/O 调度合并)的流程,最后才把请求下发到物理设备。目前使用的物理盘都是 request-based 的设备; 36 | 37 | - bio-based:在逻辑设备自己定义的 request 处理函数 make_request_fn 里进行处理,然后调用 generic_make_request 下发到底层设备。ramdisk 设备、大部分 Device Mapper 设备、virtio-blk 都是 bio-based; 38 | 39 | 下图从 Device Mapper 的角度来说明 request-based 和 bio-based 处理流程的区别。 40 | 41 | ![](https://s2.ax1x.com/2019/09/04/nVkBa4.png) 42 | 43 | 一个需要注意的地方是,Device mapper 目前只有 multipath 插件是 request-based 的,其他的如 linear,strip 都是 bio-based,所以如果是 linear DM 设备上创建的一个文件系统,对这个文件系统里的文件进行读写,采用缓存 I/O 时,即使刷脏页时是连续的请求,在 DM 设备上也不会进行合并,只会到底层的设备(如/dev/sdb)上才进行合并。 44 | -------------------------------------------------------------------------------- /02~存储/01~内存管理/页式存储管理/请页式管理/README.md: -------------------------------------------------------------------------------- 1 | # 请求分页式管理 2 | 3 | 根据调入方式的不同,虚拟分页式存储管理又分为请求分页式管理与预调入页式管理。预调入页式管理,根据某种算法动态预测进程最可能访问哪些页面,在使用前预先调入内存,尽量做到进程在访问页面之前已经预先调入该页,而且每次可以调入多个页面,以减小磁盘的 IO 次数。 4 | 5 | 用得较多的分页式虚拟存储管理是请页式(Demand Paging),当需要执行某条指令或使用某个数据,而发现它们并不在主存时,产生一个缺页中断,系统从辅存中把该指令或数据所在的页面调入内存。请求分页式存储管理是在页式存储管理的基础上,增加了请求分页功能和页面置换功能实现的虚拟存储系统。当需要执行某条指令而发现它不在内存时或当某条指令需要访问其他的数据或指令时,这些指令和数据不在内存,从而发生缺页中断,于是系统将外存中相应的页面调入内存。如果此时内存能容纳新页,则启动磁盘 IO 将其调入内存,如果内存已满,则通过页面置换功能将当前所需的页面调入。 6 | 7 | 请求调页也可看做一种动态内存分配技术,它把页面的分配推迟到不能再推迟为止,在内存总数保持不变的情况下,请求调页从总体上能使系统有更大的吞吐量。但是系统为此也要付出额外的开销,这是因为由请求调页所引发的每个“缺页”异常必须由内核处理,这将浪费 CPU 的周期。在大部分应用程序中,程序的局部性原理保证了一旦进程开始在一组页上运行,在接下来相当长的一段时间内它就会一直停留在这些页上而不去访问其他的页;这样就可以认为“缺页”异常是一种稀有事件。 8 | 9 | # 请页机制 10 | 11 | 当一个进程运行时,CPU 访问的地址是用户空间的虚地址。Linux 采用请页机制来节约物理内存,也就是说,它仅仅把当前要使用的用户空间中的少量页装入物理内存。当访问的虚存页面尚未装入物理内存时,处理器将向 Linux 报告一个页故障及其对应的故障原因。页故障的产生有以下三种原因: 12 | 13 | - 程序出现错误,例如,要访问的虚地址在 `PAGE_OFFSET(3GB)` 之外,则该地址无效,Linux 将向进程发送一个信号并终止进程的运行; 14 | - 虚地址有效,但其所对应的页当前不在物理内存中,即缺页异常,这时,操作系统必须从磁盘或交换文件(此页被换出)中将其装人物理内存。 15 | - 要访问的虚地址被写保护,即保护错误,这时,操作系统必须判断:如果是某个用户进程正在写当前进程的地址空间,则发送一个信号并终止进程的运行;如果错误发生在旧的共享页上时,则处理方法有所不同,也就是要对这一共享页进行复制,这就是曾经描述过的“写时复制”技术。 16 | 17 | ## 缺页中断 18 | 19 | ![image](https://user-images.githubusercontent.com/5803001/52261857-9cc36e80-2965-11e9-9933-6385a8ada933.png) 20 | 21 | 缺页中断与一般中断的区别: 22 | 23 | - 一般中断是在指令结束后接受中断请求并响应;缺页中断是在指令执行期间所要访问的指令或数据不在内存时产生和处理的 24 | - 一条指令在执行期间可能产生多次缺页中断 25 | 26 | ## 请求调页 27 | 28 | 当页从未被访问时则调用 `do_no_page()` 函数。有两种方法可以装入所缺的页,采用哪种方法取决于这个页是否与磁盘文件建立了映射关系。该函数通过检查虚存区描述符的 nopage 域来确定这一点,如果页与文件建立了映射关系,则 nopage 域就指向一个函数,该函数把所缺的页从磁盘装入到内存。因此,可能有以下两种情况。 29 | 30 | - nopage 域不为 NULL。在这种情况下,说明某个虚存区映射了一个磁盘文件,nopage 域指向从磁盘进行读入的函数。 31 | - nopage 域为 NULL。在这种情况下,虚存区没有映射磁盘文件,也就是说,它是个匿名映射。因此,dono_page()调用 `do_anonymous_page()` 函数获得一个新的页面。 32 | 33 | 对于`do_anonymous_page()` 函数,当处理写访问时,该函数调用 `_get_free_page` 分配一个新的页面,并把新页面填为 0。最后,把页表相应的表项置为新页面的物理地址,并把这个页面标记为可写和脏两个标志。 34 | 35 | 相反,当处理读访问时(所访问的虚存区可能是未初始化的数据段 BSS),因为进程正在对它进行第一次访问,因此页的内容是无关紧要的。给进程一个填充为 0 的页面要比给它个由其他进程填充了信息的旧页面更为安全。没有必要立即给进程分配一个填充为 0 的新页面,我们可以给它一个现有的称为“零页”的页,这样可以进一步推迟页面的分配。“零页”在内核初始化期间被静态分配,并存放在 `empty_zero_page` 变量中(一个有 1024 个长整数的数组,并用 0 填充),因此页表项被设为零 36 | 页的物理地址。由于“零页”被标记为不可写,如果进程试图写这个页,则写时复制机制被激活。当且仅 37 | 当在这个时候,进程才获得一个属于自已的页面并对它进行写。 38 | 39 | ## 地址变换 40 | 41 | ![image](https://user-images.githubusercontent.com/5803001/52261877-b5338900-2965-11e9-8bb1-e3854707a95e.png) 42 | -------------------------------------------------------------------------------- /03~网络/多路复用/select/select.md: -------------------------------------------------------------------------------- 1 | # select/poll 多路复用详解 2 | 3 | ## 1. 问题引入 4 | 5 | 让我们先看一个简单的服务器示例,来理解为什么需要多路复用: 6 | 7 | ```c 8 | while (1) { 9 | clin_len = sizeof(clin_addr); 10 | cfd = accept(lfd, (struct sockaddr *)&clin_addr, &clin_len); 11 | 12 | while (len = read(cfd, recvbuf, BUFSIZE)) { 13 | write(STDOUT_FILENO, recvbuf, len); 14 | if (strncasecmp(recvbuf, "stop", 4) == 0) { 15 | close(cfd); 16 | break; 17 | } 18 | } 19 | } 20 | ``` 21 | 22 | **存在的问题:** 23 | 24 | - 当多个客户端连接时,服务器会阻塞在第一个客户端 25 | - 直到第一个客户端发送"stop",才能处理下一个客户端的请求 26 | 27 | ## 2. select 介绍 28 | 29 | ### 2.1 函数原型 30 | 31 | ```c 32 | select(int nfds, fd_set *r, fd_set *w, fd_set *e, struct timeval *timeout) 33 | ``` 34 | 35 | ### 2.2 主要参数 36 | 37 | - `maxfdp1`: 描述符总数 38 | - `fd_set`: 描述符集合(bitmap 结构) 39 | - `timeout`: 等待超时时间 40 | - r, w, e: 分别表示读、写、异常事件集合 41 | 42 | ### 2.3 select 的局限性 43 | 44 | 1. bitmap 大小限制(通常为 1024) 45 | 2. 每次调用需要重新设置集合 46 | 3. 返回后需要遍历整个集合 47 | 4. 内核需要遍历所有 fd 检查事件 48 | 49 | ## 3. poll 改进 50 | 51 | ### 3.1 函数原型 52 | 53 | ```c 54 | poll(struct pollfd *fds, int nfds, int timeout) 55 | 56 | struct pollfd { 57 | int fd; 58 | short events; // 感兴趣的事件 59 | short revents; // 实际发生的事件 60 | } 61 | ``` 62 | 63 | ### 3.2 相比 select 的改进 64 | 65 | - 突破了文件描述符数量限制 66 | - 分离了感兴趣事件和实际发生事件 67 | 68 | ## 4. 多路复用实现示例 69 | 70 | 关键实现逻辑: 71 | 72 | ```c 73 | while (1) { 74 | // 1. 重置读集合 75 | read_set = read_set_init; 76 | 77 | // 2. 添加已连接的客户端fd 78 | for (i = 0; i < FD_SET_SIZE; ++i) { 79 | if (client[i] > 0) { 80 | FD_SET(client[i], &read_set); 81 | } 82 | } 83 | 84 | // 3. 等待事件发生 85 | retval = select(maxfd + 1, &read_set, NULL, NULL, NULL); 86 | 87 | // 4. 处理新连接 88 | if (FD_ISSET(lfd, &read_set)) { 89 | // 处理新客户端连接 90 | } 91 | 92 | // 5. 处理已连接客户端的数据 93 | for (i = 0; i < maxi; ++i) { 94 | if (client[i] < 0) continue; 95 | 96 | if (FD_ISSET(client[i], &read_set)) { 97 | // 处理客户端数据 98 | } 99 | } 100 | } 101 | ``` 102 | 103 | ## 5. 内核处理流程 104 | 105 | 1. 从用户空间复制 fd_set 到内核空间 106 | 2. 注册 pollwait 回调函数 107 | 3. 遍历所有 fd,调用对应的 poll 方法 108 | 4. 根据 poll 结果设置 fd_set 109 | 5. 必要时让进程睡眠等待事件 110 | 6. 将结果复制回用户空间 111 | -------------------------------------------------------------------------------- /03~网络/多路复用/README.md: -------------------------------------------------------------------------------- 1 | # IO Multiplexing | IO 多路复用 2 | 3 | IO 多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。select,poll,epoll 都是 IO 多路复用的机制。值得一提的是,epoll 仅对于 Pipe 或者 Socket 这样的读写阻塞型 IO 起作用,正常的文件描述符则是会立刻返回文件的内容,因此 epoll 等函数对普通的文件读写并无作用。 4 | 5 | 首先来看下可读事件与可写事件:当如下任一情况发生时,会产生套接字的可读事件: 6 | 7 | - 该套接字的接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的大小; 8 | - 该套接字的读半部关闭(也就是收到了 FIN),对这样的套接字的读操作将返回 0(也就是返回 EOF); 9 | - 该套接字是一个监听套接字且已完成的连接数不为 0; 10 | - 该套接字有错误待处理,对这样的套接字的读操作将返回-1。 11 | 12 | 当如下任一情况发生时,会产生套接字的可写事件: 13 | 14 | - 该套接字的发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的大小; 15 | - 该套接字的写半部关闭,继续写会产生 SIGPIPE 信号; 16 | - 非阻塞模式下,connect 返回之后,该套接字连接成功或失败; 17 | - 该套接字有错误待处理,对这样的套接字的写操作将返回-1。 18 | 19 | select,poll,epoll 本质上都是同步 IO,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步 IO 则无需自己负责进行读写,异步 IO 的实现会负责把数据从内核拷贝到用户空间。select 本身是轮询式、无状态的,每次调用都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大。epoll 则是触发式处理连接,维护的描述符数目不受到限制,而且性能不会随着描述符数目的增加而下降。 20 | 21 | | 方法 | 数量限制 | 连接处理 | 内存操作 | 22 | | ------ | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | 23 | | select | 描述符个数由内核中的 FD_SETSIZE 限制,仅为 1024;重新编译内核改变 FD_SETSIZE 的值,但是无法优化性能 | 每次调用 select 都会线性扫描所有描述符的状态,在 select 结束后,用户也要线性扫描 fd_set 数组才知道哪些描述符准备就绪(O(n)) | 每次调用 select 都要在用户空间和内核空间里进行内存复制 fd 描述符等信息 | 24 | | poll | 使用 pollfd 结构来存储 fd,突破了 select 中描述符数目的限制 | 类似于 select 扫描方式 | 需要将 pollfd 数组拷贝到内核空间,之后依次扫描 fd 的状态,整体复杂度依然是 O(n)的,在并发量大的情况下服务器性能会快速下降 | 25 | | epoll | 该模式下的 Socket 对应的 fd 列表由一个数组来保存,大小不限制(默认 4k) | 基于内核提供的反射模式,有活跃 Socket 时,内核访问该 Socket 的 callback,不需要遍历轮询 | epoll 在传递内核与用户空间的消息时使用了内存共享,而不是内存拷贝,这也使得 epoll 的效率比 poll 和 select 更高 | 26 | 27 | -------------------------------------------------------------------------------- /01~进程与处理器/进程与线程/01.用户态与内核态/01.用户态与内核态.md: -------------------------------------------------------------------------------- 1 | # Linux 中用户态与内核态 2 | 3 | 如下图所示,从宏观上来看,Linux 操作系统的体系架构分为用户态和内核态(或者用户空间和内核)。 4 | 5 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230430222749.png) 6 | 7 | 内核从本质上看是一种软件——控制计算机的硬件资源,并提供上层应用程序运行的环境。用户态即上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源,包括 CPU 资源、存储资源、IO 资源等。为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。 8 | 9 | 下图是对上图的一个细分结构,从这个图上可以更进一步对内核所做的事有一个“全景式”的印象。主要表现为:向下控制硬件资源,向内管理操作系统资源:包括进程的调度和管理、内存的管理、文件系统的管理、设备驱动程序的管理以及网络资源的管理,向上则向应用程序提供系统调用的接口。 10 | 11 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230430222809.png) 12 | 13 | 这种分层的架构极大地提高了资源管理的可扩展性和灵活性,而且方便用户对资源的调用和集中式的管理,带来一定的安全性。 14 | 15 | # 内核访问 16 | 17 | 用户态的应用程序可以通过三种方式来访问内核态的资源:系统调用、库函数以及 Shell 脚本。 18 | 19 | 我们可以把系统调用看成是一种不能再化简的操作(类似于原子操作,但是不同概念),有人把它比作一个汉字的一个“笔画”,而一个“汉字”就代表一个上层应用,我觉得这个比喻非常贴切。因此,有时候如果要实现一个完整的汉字(给某个变量分配内存空间),就必须调用很多的系统调用。如果从实现者(程序员)的角度来看,这势必会加重程序员的负担,良好的程序设计方法是:重视上层的业务逻辑操作,而尽可能避免底层复杂的实现细节。 20 | 21 | 库函数正是为了将程序员从复杂的细节中解脱出来而提出的一种有效方法。它实现对系统调用的封装,将简单的业务逻辑接口呈现给用户,方便用户调用,从这个角度上看,库函数就像是组成汉字的“偏旁”。这样的一种组成方式极大增强了程序设计的灵活性,对于简单的操作,我们可以直接调用系统调用来访问资源,如“人”,对于复杂操作,我们借助于库函数来实现,如“仁”。显然,这样的库函数依据不同的标准也可以有不同的实现版本,如 ISO C 标准库,POSIX 标准库等。 22 | 23 | Shell 是一个特殊的应用程序,俗称命令行,本质上是一个命令解释器,它下通系统调用,上通各种应用,通常充当着一种“胶水”的角色,来连接各个小功能程序,让不同程序能够以一个清晰的接口协同工作,从而增强各个程序的功能。 24 | 25 | 同时,Shell 是可编程的,它可以执行符合 Shell 语法的文本,这样的文本称为 Shell 脚本,通常短短的几行 Shell 脚本就可以实现一个非常大的功能,原因就是这些 Shell 语句通常都对系统调用做了一层封装。为了方便用户和系统交互,一般,一个 Shell 对应一个终端,终端是一个硬件设备,呈现给用户的是一个图形化窗口。我们可以通过这个窗口输入或者输出文本。这个文本直接传递给 Shell 进行分析解释,然后执行。 26 | 27 | # 用户态与内核态的切换 28 | 29 | 因为操作系统的资源是有限的,如果访问资源的操作过多,必然会消耗过多的资源,而且如果不对这些操作加以区分,很可能造成资源访问的冲突。 30 | 31 | 所以,为了减少有限资源的访问和使用冲突,Unix/Linux 的设计哲学之一就是:对不同的操作赋予不同的执行等级,就是所谓特权的概念。简单说就是有多大能力做多大的事,与系统相关的一些特别关键的操作必须由最高特权的程序来完成。Intel 的 X86 架构的 CPU 提供了 0 到 3 四个特权级,数字越小,特权越高。 32 | 33 | Linux 操作系统中主要采用了 0 和 3 两个特权级,分别对应的就是内核态和用户态。运行于用户态的进程可以执行的操作和访问的资源都会受到极大的限制,而运行在内核态的进程则可以执行任何操作并且在资源的使用上没有限制。 34 | 35 | 很多程序开始时运行于用户态,但在执行的过程中,一些操作需要在内核权限下才能执行,这就涉及到一个从用户态切换到内核态的过程。比如 C 函数库中的内存分配函数 malloc(),它具体是使用 sbrk() 系统调用来分配内存,当 malloc() 调用 sbrk() 的时候就涉及一次从用户态到内核态的切换,类似的函数还有 printf(),调用的是 wirte() 系统调用来输出字符串,等等。 36 | 37 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230503195334.png) 38 | 39 | 从用户态到内核态的切换,一般存在以下三种情况: 40 | 41 | - 当然就是系统调用。 42 | - 异常事件:当 CPU 正在执行运行在用户态的程序时,突然发生某些预先不可知的异常事件,这个时候就会触发从当前用户态执行的进程转向内核态执行相关的异常事件,典型的如缺页异常。 43 | - 外围设备的中断:当外围设备完成用户的请求操作后,会向 CPU 发出中断信号,此时,CPU 就会暂停执行下一条即将要执行的指令,转而去执行中断信号对应的处理程序,如果先前执行的指令是在用户态下,则自然就发生从用户态到内核态的转换。 44 | -------------------------------------------------------------------------------- /10~Shell 命令/文本处理/sed.md: -------------------------------------------------------------------------------- 1 | # sed 2 | 3 | sed 是一种非交互式的流编辑器,可动态编辑文件。所谓的非交互式是说,sed 和传统的文本编辑器不同,并非和使用者直接互动,sed 处理的对象是文件的数据流。sed 的工作模式是,比对每一行数据,若符合样式,就执行指定的操作。 4 | 5 | ```sh 6 | # 选项与参数 7 | -n:使用安静(silent)模式。在一般 sed 的用法中,所有来自 STDIN 的数据一般都会被列出到终端上。但如果加上 -n 参数后,则只有经过sed 特殊处理的那一行(或者动作)才会被列出来。 8 | -e:直接在命令列模式上进行 sed 的动作编辑; 9 | -f:直接将 sed 的动作写在一个文件内,-f filename 则可以运行 filename 内的 sed 动作; 10 | -r:sed 的动作支持的是延伸型正规表示法的语法。(默认是基础正规表示法语法) 11 | -i:直接修改读取的文件内容,而不是输出到终端。 12 | 13 | # 函数 14 | a:新增,a 的后面可以接字串,而这些字串会在新的一行出现(目前的下一行)~ 15 | c:取代,c 的后面可以接字串,这些字串可以取代 n1,n2 之间的行! 16 | d:删除,因为是删除啊,所以 d 后面通常不接任何咚咚; 17 | i:插入,i 的后面可以接字串,而这些字串会在新的一行出现(目前的上一行); 18 | p:列印,亦即将某个选择的数据印出。通常 p 会与参数 sed -n 一起运行~ 19 | s:取代,可以直接进行取代的工作哩!通常这个 s 的动作可以搭配正规表示法!例如 1,20s/old/new/g 就是啦! 20 | ``` 21 | 22 | 常用示例: 23 | 24 | ```sh 25 | # 在第一行和第二行间插入一行123Abc 26 | sed -i '2i 123Abc' 1.log 27 | # 在第二行和第三行间插入一行 123Abc 28 | sed -i '2a 123Abc' 1.log 29 | 30 | sed '1,4d' 1.log #删除1到4行数据,剩下的显示出来。d是sed的删除命令。这里的删除并不是修改了源文件 31 | 32 | # 删除最后一行 33 | sed '$d' 1.log 34 | # 删除匹配到包含'LA'字符行的数据,剩下的显示。#代表搜索 35 | sed '/LA/d' 1.log 36 | 37 | sed '/[0-9]\{3\}/d' 1.log #删除包含三位数字的行,注意{3}个数指定的大括号转义 38 | 39 | sed '/LA/!d' 1.log # 反选,把不含LA行的数据删除 40 | 41 | sed '/^$/d' 1.log #删除空白行 42 | 43 | # 如果想显示匹配到的呢? 44 | sed '/a/p' 1.log #由于默认sed也会显示不符合的数据行,所以要用-n,抑制这个操作 45 | sed -n '/a/p' 1.log 46 | 47 | 48 | # 替换字符,把a替换成A 49 | sed -n 's/a/A/p' 1.log #s是替换的命令,第一个#中的字符是搜索目标(a),第二个#是要替换的字符A 50 | 51 | # 上面的只会替换匹配到的第一个,如果我想所有替换呢 52 | sed -n 's/a/A/gp' 1.log # g 全局替换 53 | sed -n 's/a#gp' 1.log #删除所有的a 54 | sed -n 's/^...#gp' 1.log #删除每行的前三个字符 55 | sed -n 's/...$#gp' 1.log #删除每行结尾的三个字符 56 | sed -n 's/\(A\)/\1BC/gp' 1.log # 在A后面追加BC,\1表示搜索里面括号里的字符 57 | 58 | 59 | sed -n '/AAA/s/234/567/p' 1.log # 找到包含字符AAA这一行,并把其中的234替换成567 60 | sed -n '/AAA/,/BBB/s/234/567/p' 1.log # 找到包含字符AAA或者BBB的行,并把其中的234替换成567 61 | sed -n '1,4s/234/567/p' 1.log # 将1到4行中的234.替换成567 62 | cat 1.log | sed -e '3,$d' -e 's/A/a/g' # 删除3行以后的数据,并把剩余的数据替换A为a 63 | sed -i '1d' 1.log # 直接修改文件,删除第一行 64 | ``` 65 | 66 | 统计文件中的词频: 67 | 68 | ```sh 69 | sed 's/ /\n/g' "$@" | # convert to one word per line 70 | tr A-Z a-z | # map uppercase to lower case 71 | sed "s/[^a-z']//g" | # remove all characters except a-z and ' 72 | egrep -v '^$' | # remove empty lines 73 | sort | # place words in alphabetical order 74 | uniq -c | # use uniq to count how many times each word occurs 75 | sort -n # order words in frequency of occurrance 76 | ``` 77 | -------------------------------------------------------------------------------- /03~网络/多路复用/epoll/README.md: -------------------------------------------------------------------------------- 1 | > DocId: 1rOEBkl 2 | 3 | # epoll/kqueue:高性能 I/O 事件通知机制 4 | 5 | ## 概述 6 | 7 | epoll 是 Linux 下高性能的 I/O 事件通知机制,主要用于解决高并发网络服务器中的 I/O 复用问题。其特点是: 8 | 9 | - 能高效处理大量连接场景 10 | - 在任意时刻,真正进行读写操作的连接数量较少 11 | - 时间复杂度仅与活跃客户端数量相关,不会随着总连接数增加而性能下降 12 | 13 | ## 用户态与内核态 14 | 15 | 在理解 epoll 之前,需要先了解用户态和内核态的概念: 16 | 17 | 1. **用户态(User Space)** 18 | 19 | - 普通应用程序运行的环境 20 | - 权限受限,不能直接访问硬件资源 21 | - 需要通过系统调用与内核交互 22 | 23 | 2. **内核态(Kernel Space)** 24 | - 操作系统内核运行的环境 25 | - 具有最高权限,可以访问所有硬件资源 26 | - 负责管理系统资源和处理系统调用 27 | 28 | ## select/poll 的工作模式及不足 29 | 30 | ### 工作模式 31 | 32 | 以 select 为例: 33 | 34 | ```c 35 | // 用户态代码 36 | fd_set readfds; 37 | FD_ZERO(&readfds); 38 | FD_SET(sock1, &readfds); 39 | FD_SET(sock2, &readfds); 40 | FD_SET(sock3, &readfds); 41 | 42 | // 调用 select 43 | select(max_fd + 1, &readfds, NULL, NULL, NULL); 44 | ``` 45 | 46 | ### 主要问题 47 | 48 | 1. **重复拷贝问题** 49 | 50 | - 每次调用都需要将整个 fd 集合从用户态拷贝到内核态 51 | - 内核检查完成后,还需要将结果拷贝回用户态 52 | - 当 fd 数量很大时,这个拷贝开销很大 53 | - 即使只有一个 fd 就绪,也需要拷贝所有 fd 54 | 55 | 2. **无状态遍历问题** 56 | - 内核不会保存任何 fd 状态信息 57 | - 每次调用都需要重新遍历所有 fd 58 | - 导致 CPU 开销随着 fd 数量线性增长 59 | 60 | ## epoll 的改进方案 61 | 62 | ### 1. API 设计 63 | 64 | epoll 提供三个核心函数: 65 | 66 | - `epoll_create`:创建 epoll 句柄 67 | - `epoll_ctl`:注册要监听的事件类型 68 | - `epoll_wait`:等待事件产生 69 | 70 | ### 2. 核心优化 71 | 72 | 1. **一次拷贝** 73 | 74 | ```python 75 | # 创建 epoll 实例 76 | epoll = epoll_create() 77 | 78 | # 只在添加新连接时拷贝一次 79 | epoll_ctl(epoll, EPOLL_CTL_ADD, new_fd, events) 80 | 81 | while True: 82 | # 不需要拷贝 fd 集合,只返回就绪的 fd 83 | ready_fds = epoll_wait(epoll) 84 | for fd in ready_fds: 85 | handle_ready_fd(fd) 86 | ``` 87 | 88 | 2. **回调机制** 89 | 90 | - 为每个 fd 指定回调函数 91 | - 设备就绪时直接调用回调函数 92 | - 回调函数将就绪的 fd 加入就绪链表 93 | 94 | 3. **高效轮询** 95 | - 不需要遍历整个 fd 集合 96 | - 只需检查就绪链表是否为空 97 | - 显著减少 CPU 开销 98 | 99 | ### 性能优势 100 | 101 | 1. **CPU 效率更高** 102 | 103 | - select/poll:唤醒后需遍历所有 fd 104 | - epoll:唤醒后只需检查就绪链表 105 | - 时间复杂度从 O(n) 降低到 O(1) 106 | 107 | 2. **系统开销更小** 108 | - select/poll:每次调用都需要完整的 fd 拷贝和等待队列操作 109 | - epoll:一次性拷贝,一次性等待队列挂载 110 | 111 | ### 生动类比 112 | 113 | 可以通过以下场景来理解 select/poll 和 epoll 的区别: 114 | 115 | - **select/poll**: 116 | 117 | - 就像每次想知道朋友是否在线 118 | - 都要把整个通讯录复印一份给服务器检查 119 | - 服务器检查完再把结果返回给你 120 | 121 | - **epoll**: 122 | - 第一次把通讯录给服务器 123 | - 服务器记住这些联系人 124 | - 后续有人上线就主动通知你 125 | - 不需要重复传递通讯录 126 | 127 | ## 总结 128 | 129 | epoll 通过巧妙的设计解决了 select/poll 的主要问题,特别适合处理大量连接但活跃连接较少的场景(如网络服务器)。它的高效主要体现在: 130 | 131 | 1. 避免了重复的内存拷贝 132 | 2. 不需要遍历所有文件描述符 133 | 3. 采用事件驱动方式,只关注活跃连接 134 | 135 | 这些优化使得 epoll 在高并发场景下表现出色,成为 Linux 下网络编程的首选方案。 136 | -------------------------------------------------------------------------------- /10~Shell 命令/Shell 编程/语法基础/README.md: -------------------------------------------------------------------------------- 1 | # Shell 语法基础 2 | 3 | # Shebang 4 | 5 | #! 脚本中使用的语法表示在 UNIX/Linux 操作系统下执行的解释器。大多数 Linux shell 和 perl/python 脚本以以下行开头: 6 | 7 | ```sh 8 | #!/bin/bash 9 | #!/usr/bin/perl 10 | #!/usr/bin/python 11 | #!/usr/bin/python3 12 | #!/usr/bin/env bash 13 | ``` 14 | 15 | 脚本中所有的语句都会使用首行声明的解释器来进行执行,绝大部分的脚本都是以 `#!/bin/bash` 开始,这就保证了不管该脚本在何解释器中运行都会被 Bash 来执行;如果我们不添加任何的 Shebang 行,那么会默认使用 `/bin/sh` 来执行。Shebang 由 Dennis Ritchie 在 7 版 Unix 和 8 版之间在 Bell Laboratories 推出。然后,它也被添加到伯克利的 BSD 系列中 16 | 17 | ## /bin/sh 18 | 19 | sh 是系统的标准命令解释器。sh 的当前版本正在更改中,以符合 Shell 的 POSIX 1003.2 和 1003.2a 规范。典型的脚本如下: 20 | 21 | ```sh 22 | #! /bin/sh 23 | ### BEGIN INIT INFO 24 | # Provides: policykit 25 | # Required-Start: $local_fs 26 | # Required-Stop: $local_fs 27 | # Default-Start: 2 3 4 5 28 | # Default-Stop: 29 | # Short-Description: Create PolicyKit runtime directories 30 | # Description: Create directories which PolicyKit needs at runtime, 31 | # such as /var/run/PolicyKit 32 | ### END INIT INFO 33 | 34 | # Author: Martin Pitt 35 | 36 | case "$1" in 37 | start) 38 | mkdir -p /var/run/PolicyKit 39 | chown root:polkituser /var/run/PolicyKit 40 | chmod 770 /var/run/PolicyKit 41 | ;; 42 | stop|restart|force-reload) 43 | ;; 44 | *) 45 | echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2 46 | exit 3 47 | ;; 48 | esac 49 | 50 | : 51 | ``` 52 | 53 | ## /usr/bin/env bash 54 | 55 | `/usr/bin/env` 在修改后的环境中运行 bash 之类的程序。它使您的 bash 脚本具有可移植性。`#/usr/bin/env bash` 的优点是它将使用运行用户的 `$PATH` 变量中首先出现的 bash 可执行文件。 56 | 57 | ```sh 58 | #!/usr/bin/env bash 59 | # Purpose: Mount glusterfs at boot time 60 | # Must run as root 61 | # Author: Vivek Gite 62 | # -------------------------------------- 63 | p='gfs01:/gvol01' 64 | 65 | mount | grep -wq "^${p}" 66 | 67 | if [ $? -ne 0 ] 68 | then 69 | mount -t glusterfs "$p" /sharedwww/ 70 | fi 71 | ``` 72 | 73 | # 注释 74 | 75 | Shell 中以 # 表示单行注释: 76 | 77 | ```sh 78 | #!/bin/bash 79 | # A Simple Shell Script To Get Linux Network Information 80 | # Vivek Gite - 30/Aug/2009 81 | echo "Current date : $(date) @ $(hostname)" 82 | echo "Network configuration" 83 | /sbin/ifconfig 84 | ``` 85 | 86 | 以#开头的单词或行会导致该单词和该行上的所有剩余字符被忽略。这些行不是要执行 bash 的语句。实际上,bash 完全忽略了它们。这些注释称为注释。只是关于脚本的解释性文字。它使源代码更易于理解。这些说明适用于人类和其他系统管理员。它可以帮助其他系统管理员理解您的代码,逻辑,并可以帮助他们修改您编写的脚本。 87 | 88 | 多行注释的定义方式如下: 89 | 90 | ```sh 91 | #!/bin/bash 92 | echo "Adding new users to LDAP Server..." 93 | <max?max=current:max=max)) # ternary expression. 38 | ``` 39 | 40 | 使用 x 和 y 变量加两个数字。使用文本编辑器创建一个名为 add.sh 的 Shell 程序: 41 | 42 | ```sh 43 | #!/bin/bash 44 | x=5 45 | y=10 46 | ans=$(( x + y )) 47 | echo "$x + $y = $ans" 48 | ``` 49 | 50 | 带整数的数学运算符如下表所示: 51 | 52 | | Operator | Description | Example | Evaluates To | 53 | | -------- | --------------------------------------------- | ----------------------------------- | ------------ | 54 | | + | Addition | echo \$(( 20 + 5 )) | 25 | 55 | | - | Subtraction | echo \$(( 20 - 5 )) | 15 | 56 | | / | Division | echo \$(( 20 / 5 )) | 4 | 57 | | `*` | Multiplication | echo \$(( 20 `*` 5 )) | 100 | 58 | | % | Modulus | echo \$(( 20 % 3 )) | 2 | 59 | | ++ | post-increment (add variable value by 1) | x=5 echo $(( x++ )) echo $(( x++ )) | 5 6 | 60 | | -- | post-decrement (subtract variable value by 1) | x=5 echo \$(( x-- )) | 4 | 61 | | `**` | Exponentiation | x=2 y=3 echo \$(( x `**` y )) | 8 | 62 | 63 | 运算符按优先级顺序进行评估。这些级别按优先级从高到低的顺序列出(引用 bash 手册页中的内容): 64 | 65 | ```sh 66 | id++ id-- 67 | variable post-increment and post-decrement 68 | ++id --id 69 | variable pre-increment and pre-decrement 70 | - + unary minus and plus 71 | ! ~ logical and bitwise negation 72 | ** exponentiation 73 | * / % multiplication, division, remainder 74 | + - addition, subtraction 75 | << >> left and right bitwise shifts 76 | <= >= < > 77 | comparison 78 | == != equality and inequality 79 | & bitwise AND 80 | ^ bitwise exclusive OR 81 | | bitwise OR 82 | && logical AND 83 | || logical OR 84 | expr?expr:expr 85 | conditional operator 86 | = *= /= %= += -= <<= >>= &= ^= |= 87 | assignment 88 | expr1, expr2 89 | comma 90 | ``` 91 | -------------------------------------------------------------------------------- /02~存储/存储 IO/磁盘 IO/AIO.md: -------------------------------------------------------------------------------- 1 | # Asynchronous File IO on Linux 2 | 3 | # APIs 4 | 5 | ## aio_read() 6 | 7 | aio_read() is the function where we tell the system what file we want to read, the offset to begin the read, how many bytes to read, and where to put the bytes that are read. All of this information goes into a aiocb structure, which looks like this: 8 | 9 | - aio_fildes - file descriptor of the open file you want to read from 10 | - aio_offset - Offset where you want to begin reading 11 | aio_nbytes - Number of bytes to read 12 | aio_buf - Pointer to the buffer where the bytes read will be put 13 | 14 | There's plenty of other members inside this structure, but they aren't important to us right now. But it's still a good idea to zero out the structure before you use it. memset() would work fine for this. 15 | 16 | ## aio_error() 17 | 18 | aio_error() checks the current state of the IO request. Using this function you can find out of the request was successful or not. All you have to do is give it the address of the same aiocb structure that you gave aio_read(). The function returns 0 if the request completed successfully, EINPROGRESS if it's still working, or some other error code if an error occured. 19 | 20 | By the way, you may be wondering if this means you'll have to create a different aiocb for each request. Well, you do. It should be obvious that if you muck around with an aiocb while a request is currently being fulfilled, bad things can happen. It should also be obvious that the buffer you give the aiocb will need to remain in existance the whole time the request is being fulfilled. So don't give it a pointer to a stack array and then jump out of the function. Bad things. 21 | 22 | ## aio_return() 23 | 24 | aio_return() checks the result of an IO request once you find out the request has been finished. If the request succeeded, this function returns the number of bytes read. If it failed then the function returns -1. 25 | 26 | # 案例:代码实现 27 | 28 | ```c 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | 35 | using namespace std; 36 | 37 | const int SIZE_TO_READ = 100; 38 | 39 | int main() 40 | { 41 | // open the file 42 | int file = open("blah.txt", O_RDONLY, 0); 43 | 44 | if (file == -1) 45 | { 46 | cout << "Unable to open file!" << endl; 47 | return 1; 48 | } 49 | 50 | // create the buffer 51 | char* buffer = new char[SIZE_TO_READ]; 52 | 53 | // create the control block structure 54 | aiocb cb; 55 | 56 | memset(&cb, 0, sizeof(aiocb)); 57 | cb.aio_nbytes = SIZE_TO_READ; 58 | cb.aio_fildes = file; 59 | cb.aio_offset = 0; 60 | cb.aio_buf = buffer; 61 | 62 | // read! 63 | if (aio_read(&cb) == -1) 64 | { 65 | cout << "Unable to create request!" << endl; 66 | close(file); 67 | } 68 | 69 | cout << "Request enqueued!" << endl; 70 | 71 | // wait until the request has finished 72 | while(aio_error(&cb) == EINPROGRESS) 73 | { 74 | cout << "Working..." << endl; 75 | } 76 | 77 | // success? 78 | int numBytes = aio_return(&cb); 79 | 80 | if (numBytes != -1) 81 | cout << "Success!" << endl; 82 | else 83 | cout << "Error!" << endl; 84 | 85 | // now clean up 86 | delete[] buffer; 87 | close(file); 88 | 89 | return 0; 90 | } 91 | ``` 92 | -------------------------------------------------------------------------------- /10~Shell 命令/系统进程/进程查看.md: -------------------------------------------------------------------------------- 1 | # 进程查看 2 | 3 | 您需要使用 ps 命令,pstree 命令和 pgrep 命令来查看当前进程的快照。要查看当前进程,请使用 ps 命令: 4 | 5 | ```sh 6 | ps 7 | ps aux | less 8 | ps aux | grep "process-name" 9 | ps aux | grep "httpd" 10 | ps alx | grep "mysqld" 11 | ``` 12 | 13 | # pstree 14 | 15 | 要显示进程树,请使用 pstree 命令: 16 | 17 | ```sh 18 | $ pstree 19 | 20 | init─┬─acpid 21 | ├─apache2───6*[apache2] 22 | ├─atd 23 | ├─atop 24 | ├─avahi-daemon───avahi-daemon 25 | ├─bonobo-activati───{bonobo-activati} 26 | ├─console-kit-dae───63*[{console-kit-dae}] 27 | ├─cron 28 | ├─2*[dbus-daemon] 29 | ├─dbus-launch 30 | ├─dd 31 | ├─deluge───5*[{deluge}] 32 | ├─dhclient 33 | ├─dnsmasq 34 | ├─evince───{evince} 35 | ├─firefox───run-mozilla.sh───firefox-bin───27*[{firefox-bin}] 36 | ├─gconfd-2 37 | ├─gdm───gdm─┬─Xorg 38 | │ └─gnome-session─┬─gnome-panel 39 | │ ├─gpg-agent 40 | │ ├─metacity 41 | │ ├─nautilus 42 | │ ├─python 43 | │ ├─seahorse-agent 44 | │ ├─ssh-agent 45 | │ ├─tracker-applet 46 | │ ├─trackerd───2*[{trackerd}] 47 | │ ├─update-notifier 48 | │ └─{gnome-session} 49 | ├─gedit 50 | ├─6*[getty] 51 | ├─gnome-power-man 52 | ├─gnome-screensav 53 | ├─gnome-settings-───{gnome-settings-} 54 | ├─gnome-terminal─┬─bash───pstree 55 | │ ├─bash───ssltx───ssh 56 | │ ├─gnome-pty-helpe 57 | │ └─{gnome-terminal} 58 | ├─gvfs-fuse-daemo───3*[{gvfs-fuse-daemo}] 59 | ├─gvfs-gphoto2-vo 60 | ├─gvfs-hal-volume 61 | ├─gvfsd 62 | ├─gvfsd-burn 63 | ├─gvfsd-trash 64 | ├─hald───hald-runner─┬─hald-addon-acpi 65 | │ ├─hald-addon-cpuf 66 | │ ├─hald-addon-inpu 67 | │ └─hald-addon-stor 68 | ├─jsvc─┬─jsvc 69 | │ └─jsvc───39*[{jsvc}] 70 | ├─klogd 71 | ├─lighttpd───2*[php-cgi───4*[php-cgi]] 72 | ├─mixer_applet2───{mixer_applet2} 73 | ├─mount.ntfs 74 | ├─mysqld_safe─┬─logger 75 | │ └─mysqld───10*[{mysqld}] 76 | ├─netspeed_applet 77 | ├─ntpd 78 | ├─pppd───sh───pptpgw 79 | ├─pptpcm 80 | ├─pulseaudio─┬─gconf-helper 81 | │ └─2*[{pulseaudio}] 82 | ├─squid───squid───unlinkd 83 | ├─sshproxyd 84 | ├─syslogd 85 | ├─system-tools-ba 86 | ├─thunderbird───run-mozilla.sh───thunderbird-bin───10*[{thunderbird-bin}] 87 | ├─udevd 88 | ├─vmnet-bridge 89 | ├─2*[vmnet-dhcpd] 90 | ├─vmnet-natd 91 | ├─3*[vmnet-netifup] 92 | ├─winbindd───winbindd 93 | ├─workrave───{workrave} 94 | ├─workrave-applet 95 | └─wpa_supplicant 96 | ``` 97 | 98 | # pgrep 99 | 100 | pgrep 浏览当前正在运行的进程,并列出要与选择标准匹配的进程 ID。列出 vivek 用户拥有的名为 php-cgi AND 的进程。 101 | 102 | ```sh 103 | pgrep -u vivek php-cgi 104 | 105 | # To list the processes owned by vivek OR krish. 106 | pgrep -u vivek,krish 107 | ``` 108 | -------------------------------------------------------------------------------- /10~Shell 命令/Shell 编程/语法基础/表达式.md: -------------------------------------------------------------------------------- 1 | # Shell 中表达式 2 | 3 | # Curly braces 4 | 5 | 花括号({..})展开以创建模式,语法为: 6 | 7 | ```sh 8 | { pattern1, pattern2, patternN } 9 | text{ pattern1, pattern2, patternN } 10 | text1{ pattern1, pattern2, patternN }text2 11 | command something/{ pattern1, pattern2, patternN } 12 | ``` 13 | 14 | 典型用法如下: 15 | 16 | ```sh 17 | $ echo I like {tom,jerry} # I like tom jerry 18 | 19 | $ echo file{1,2,3}.txt # file1.txt file2.txt file3.txt 20 | 21 | $ echo file{1..5}.txt # file1.txt file2.txt file3.txt file4.txt file5.txt 22 | ``` 23 | 24 | 生成的文件名不需要存在。您还可以为括号内的每个模式运行命令。通常,您可以键入以下内容列出三个文件: 25 | 26 | ```sh 27 | $ ls /etc/{resolv.conf,hosts,passwd} 28 | $ rm -v hello.{sh,py,pl,c} 29 | ``` 30 | 31 | # Wildcards(通配符) 32 | 33 | Bash 支持如下的通配符: 34 | 35 | - `*` - Matches any string, including the null string 36 | 37 | - ? - Matches any single (one) character. 38 | 39 | - [...] - Matches any one of the enclosed characters. 40 | 41 | 要显示存储在 `/etc` 目录中的所有配置(.conf)文件,请输入: 42 | 43 | ``` 44 | ls /etc/*.conf 45 | ``` 46 | 47 | 要显示所有 C 项目头文件,请输入: 48 | 49 | ``` 50 | ls *.h 51 | ``` 52 | 53 | 要显示所有 C 项目.c 文件,请输入: 54 | 55 | ``` 56 | ls *.c 57 | ``` 58 | 59 | 您可以将通配符和花括号结合使用: 60 | 61 | ``` 62 | ls *.{c,h} 63 | ``` 64 | 65 | 样本输出: 66 | 67 | ``` 68 | f.c fo1.c fo1.h fo2.c fo2.h fo3.c fo3.h fo4.c fo4.h fo5.c fo5.h t.c 69 | ``` 70 | 71 | To list all png file (image1.png, image2.png...image7.png, imageX.png), enter: 72 | 73 | ``` 74 | ls image?.png 75 | ``` 76 | 77 | 要列出所有以字母 a 或 b 开头的文件配置文件,请输入: 78 | 79 | ``` 80 | ls /etc/[ab]*.conf 81 | ``` 82 | 83 | # 别名 84 | 85 | 别名不过是命令的快捷方式。 86 | 87 | ```sh 88 | alias name='command' 89 | alias name='command arg1 arg2' 90 | ``` 91 | 92 | 创建一个名为 c 的别名以清除终端屏幕,输入: 93 | 94 | ``` 95 | alias c='clear' 96 | ``` 97 | 98 | 要清除终端,请输入: 99 | 100 | ``` 101 | c 102 | ``` 103 | 104 | 创建一个名为 d 的别名以显示系统日期和时间,输入: 105 | 106 | ``` 107 | alias d='date' 108 | d 109 | ``` 110 | 111 | 样本输出: 112 | 113 | ``` 114 | Tue Oct 20 01:38:59 IST 2009 115 | ``` 116 | 117 | 使用 alias 命令创建并列出别名,然后使用 unalias 命令删除别名。语法为: 118 | 119 | ``` 120 | unalias alias-name 121 | unalias c 122 | unalias c d 123 | ``` 124 | 125 | 要列出当前定义的别名,请输入: 126 | 127 | ``` 128 | alias 129 | alias c='clear' 130 | alias d='date' 131 | ``` 132 | 133 | 如果需要取消取消对名为 d 的命令的使用,请输入: 134 | 135 | ``` 136 | unalias d 137 | alias 138 | ``` 139 | 140 | 如果给出了-a 选项,那么除去所有别名定义,输入: 141 | 142 | ``` 143 | unalias -a 144 | alias 145 | ``` 146 | 147 | Example ~/[.bashrc](https://bash.cyberciti.biz/guide/.bashrc) script: 148 | 149 | ``` 150 | # make sure bc start with standard math library 151 | alias bc='bc -l' 152 | # protect cp, mv, rm command with confirmation 153 | alias cp='cp -i' 154 | alias mv='mv -i' 155 | alias rm='rm -i' 156 | 157 | # Make sure dnstop only shows eth1 stats 158 | alias dnstop='dnstop -l 5 eth1' 159 | 160 | # Make grep pretty 161 | alias grep='grep --color' 162 | 163 | # ls command shortcuts 164 | alias l.='ls -d .* --color=tty' 165 | alias ll='ls -l --color=tty' 166 | alias ls='ls --color=tty' 167 | 168 | # Centos/RHEL server update 169 | alias update='yum update' 170 | alias updatey='yum -y update' 171 | # vi is vim 172 | alias vi='vim' 173 | 174 | # Make sure vnstat use eth1 by default 175 | alias vnstat='vnstat -i eth1' 176 | ``` 177 | -------------------------------------------------------------------------------- /10~Shell 命令/磁盘文件/创建与读写.md: -------------------------------------------------------------------------------- 1 | # 文件基础操作 2 | 3 | # 空文件创建 4 | 5 | 要创建空文件,请使用以下语法: 6 | 7 | ```sh 8 | > newfile.name 9 | ``` 10 | 11 | \> 操作员将输出重定向到文件。如果没有给出命令并且文件不存在,它将创建一个空文件。例如,创建一个名为 tarbackup.sh 的 shell 脚本: 12 | 13 | ```sh 14 | #!/bin/bash 15 | TAR=/bin/tar 16 | 17 | # SCSI tape device 18 | TAPE=/dev/st0 19 | 20 | # Backup dir names 21 | BDIRS="/www /home /etc /usr/local/mailboxes /phpjail /pythonjail /perlcgibin" 22 | 23 | # Logfile name 24 | ERRLOG=/tmp/tar.logfile.txt 25 | 26 | # Remove old log file and create the empty log file 27 | >$ERRLOG 28 | 29 | # Okay lets make a backup 30 | $TAR -cvf $TAPE $BDIRS 2>$ERRLOG 31 | ``` 32 | 33 | 注意,您还可以使用 touch 命令创建空文件: 34 | 35 | ```s 36 | touch /tmp/newtextfile 37 | ``` 38 | 39 | # 文件写入 40 | 41 | 您需要使用重定向符号 > 将数据发送到文件。例如,我的脚本./payment.py 在屏幕上生成的输出如下: 42 | 43 | ```s 44 | ./payment.py -a -t net >netrevenue.txt 45 | ``` 46 | 47 | 使用 >> 重定向符号,将附加到名为 netrevenue.txt 的文件中,输入: 48 | 49 | ```s 50 | ./payment.py -a -t net >>netrevenue.txt 51 | ``` 52 | 53 | 要禁止使用 > 运算符设置 noclobber 选项覆盖现有常规文件,如下所示: 54 | 55 | ```s 56 | echo "Test" > /tmp/test.txt 57 | set -C 58 | echo "Test 123" > /tmp/test.txt 59 | ``` 60 | 61 | 要使用 > 运算符 set noclobber 选项覆盖现有的常规文件,如下所示: 62 | 63 | ```s 64 | cat /tmp/test.txt 65 | set +C 66 | echo "Test 123" > /tmp/test.txt 67 | cat /tmp/test.txt 68 | ``` 69 | 70 | ## 先读后写 71 | 72 | 创建一个名为 fnames.txt 的文本文件: 73 | 74 | ``` 75 | vivek 76 | tom 77 | Jerry 78 | Ashish 79 | Babu 80 | ``` 81 | 82 | 现在,按如下所示运行 [tr 命令](https://bash.cyberciti.biz/guide/Tr_command) 将所有小写名称转换为大写,然后输入: 83 | 84 | ``` 85 | tr "[a-z]" "[A-Z]" < fnames.txt 86 | ``` 87 | 88 | 样本输出: 89 | 90 | ``` 91 | VIVEK 92 | TOM 93 | JERRY 94 | ASHISH 95 | BABU 96 | ``` 97 | 98 | 您可以将输出保存到名为 output.txt 的文件中,输入: 99 | 100 | ``` 101 | tr "[a-z]" "[A-Z]" < fnames.txt > output.txt 102 | cat output.txt 103 | ``` 104 | 105 | 注意,对于标准输入和标准输出,请不要使用相同的文件名。这将导致数据丢失,并且结果是不可预测的。要对存储在 output.txt 中的名称进行排序,请输入: 106 | 107 | ``` 108 | sort < output.txt 109 | ``` 110 | 111 | 最后,将所有已排序的命名存储到名为 sorted.txt 的文件中 112 | 113 | ```sh 114 | sort < output.txt > sorted.txt 115 | 116 | sort > sorted1.txt < output.txt 117 | ``` 118 | 119 | ## 指定输出文件的 fd 120 | 121 | 文件描述符 0、1 和 2 分别保留给 stdin,stdout 和 stderr。但是,bash shell 允许您将文件描述符分配给输入文件或输出文件。这样做是为了提高文件的读取和写入性能。这称为用户定义的文件描述符。您可以使用以下语法将文件描述符分配给输出文件: 122 | 123 | ```s 124 | exec fd> output.txt 125 | ``` 126 | 127 | 创建一个名为 fdwrite.sh 的 shell 脚本: 128 | 129 | ```s 130 | #!/bin/bash 131 | # Let us assign the file descriptor to file for output 132 | # fd # 3 is output file 133 | exec 3> /tmp/output.txt 134 | 135 | # Executes echo commands and # Send output to 136 | # the file descriptor (fd) # 3 i.e. write output to /tmp/output.txt 137 | echo "This is a test" >&3 138 | 139 | # Write date command output to fd # 3 140 | date >&3 141 | 142 | # Close fd # 3 143 | exec 3<&- 144 | ``` 145 | 146 | 要将文件描述符分配给输入文件,请使用以下语法: 147 | 148 | ```sh 149 | exec fd< input.txt 150 | ``` 151 | 152 | 创建一个名为 fdread.sh 的 shell 脚本: 153 | 154 | ```s 155 | #!/bin/bash 156 | # Let us assign the file descriptor to file for input 157 | # fd # 3 is Input file 158 | exec 3< /etc/resolv.conf 159 | 160 | # Executes cat commands and read input from 161 | # the file descriptor (fd) # 3 i.e. read input from /etc/resolv.conf file 162 | cat <&3 163 | 164 | # Close fd # 3 165 | exec 3<&- 166 | ``` 167 | -------------------------------------------------------------------------------- /03~网络/多路复用/epoll/epoll 函数使用.md: -------------------------------------------------------------------------------- 1 | # 函数分析 2 | 3 | ## int epoll_create(int size); 4 | 5 | 创建一个 epoll 的句柄,size 用来告诉内核这个监听的数目一共有多大。这个 参数不同于 select()中的第一个参数,给出最大监听的 fd+1 的值。需要注意的是,当创建好 epoll 句柄后,它就是会占用一个 fd 值,在 linux 下如果查看/proc/进程 id/fd/,是能够看到这个 fd 的,所以在使用完 epoll 后,必须调用 close()关闭,否则可能导致 fd 被 耗尽。 6 | 7 | ## int epoll_ctl(int epfd, int op, int fd, struct epoll_event \*event); 8 | 9 | epoll 的事件注册函数,它不同与 select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是 epoll_create()的返回值,第二个参数表示动作,用三个宏来表示: 10 | 11 | - EPOLL_CTL_ADD:注册新的 fd 到 epfd 中; 12 | - EPOLL_CTL_MOD:修改已经注册的 fd 的监听事件; 13 | - EPOLL_CTL_DEL:从 epfd 中删除一个 fd; 14 | 15 | 第三个参数是需要监听的 fd,第四个参数是告诉内核需要监听什么事,struct epoll_event 结构如下: 16 | 17 | ```c 18 | typedef union epoll_data { 19 | void *ptr; 20 | int fd; 21 | __uint32_t u32; 22 | __uint64_t u64; 23 | } epoll_data_t; 24 | 25 | struct epoll_event { 26 | __uint32_t events; /* Epoll events */ 27 | epoll_data_t data; /* User data variable */ 28 | }; 29 | ``` 30 | 31 | events 可以是以下几个宏的集合: 32 | 33 | - EPOLLIN:表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭); 34 | - EPOLLOUT:表示对应的文件描述符可以写; 35 | - EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来); 36 | - EPOLLERR:表示对应的文件描述符发生错误; 37 | - EPOLLHUP:表示对应的文件描述符被挂断; 38 | - EPOLLET: 将 EPOLL 设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。 39 | - EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里 40 | 41 | ### int epoll_wait(int epfd, struct epoll_event \* events, int maxevents, int timeout); 42 | 43 | 等 待事件的产生,类似于 select()调用。参数 events 用来从内核得到事件的集合,maxevents 告之内核这个 events 有多大,这个 maxevents 的值不能大于创建 epoll_create()时的 size,参数 timeout 是超时时间(毫秒,0 会立即返回,-1 将不确定,也有 说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回 0 表示已超时。 44 | 45 | ## 处理逻辑 46 | 47 | 使用 epoll 来实现服务端同时接受多客户端长连接数据时,的大体步骤如下:(1)使用 epoll_create 创建一个 epoll 的句柄,下例中我们命名为 epollfd。(2)使用 epoll_ctl 把服务端监听的描述符添加到 epollfd 指定的 epoll 内核事件表中,监听服务器端监听的描述符是否可读。(3)使用 epoll_wait 阻塞等待注册的服务端监听的描述符可读事件的发生。(4)当有新的客户端连接上服务端时,服务端监听的描述符可读,则 epoll_wait 返回,然后通过 accept 获取客户端描述符。(5)使用 epoll_ctl 把客户端描述符添加到 epollfd 指定的 epoll 内核事件表中,监听服务器端监听的描述符是否可读。(6)当客户端描述符有数据可读时,则触发 epoll_wait 返回,然后执行读取。 48 | 49 | 几乎所有的 epoll 模型编码都是基于以下模板: 50 | 51 | ```c 52 | for( ; ; ) 53 | { 54 | // 阻塞式等待事件 55 | nfds = epoll_wait(epfd,events,20,500); 56 | for(i=0;ifd; 76 | send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //发送数据 77 | ev.data.fd=sockfd; 78 | ev.events=EPOLLIN|EPOLLET; 79 | epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据 80 | } 81 | else 82 | { 83 | //其他的处理 84 | } 85 | } 86 | } 87 | ``` 88 | -------------------------------------------------------------------------------- /02~存储/01~内存管理/页式存储管理/预调式管理/页缓存.md: -------------------------------------------------------------------------------- 1 | # 预读与页缓存 2 | 3 | ## 磁盘预读 4 | 5 | 当一个数据被用到时,其附近的数据也通常会马上被使用。程序运行期间所需要的数据通常比较集中。由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),因此对于具有局部性的程序来说,预读可以提高 IO 效率。 6 | 7 | 预读的长度一般为页(Page)的整倍数。页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页得大小通常为 4k),主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,然后异常返回,程序继续运行。 8 | 9 | ## 页缓存 10 | 11 | 页缓存是 Linux 内核一种重要的磁盘高速缓存,它通过软件机制实现。但页缓存和硬件缓存的原理基本相同,将容量大而低速设备中的部分数据存放到容量小而快速的设备中,这样速度快的设备将作为低速设备的缓存,当访问低速设备中的数据时,可以直接从缓存中获取数据而不需再访问低速设备,从而节省了整体的访问时间。譬如当我们使用 `free -h` 命令查看系统的内存使用情况时,会发现已使用的内存总数(used)与可用内存总数(free)相加并不等于内存总数(total),这正是因为 OS 发现系统的物理内存有大量剩余时,为了提高 IO 的性能,就会使用多余的内存当做文件缓存。 12 | 13 | Linux 不会直接操作物理内存,而是建立一个虚拟地址(可以理解成跟物理内存相对应的映射),即在物理内存跟进程之间增加一个中间层。每个用户空间的进程都有自己的虚拟内存,每个进程都认为自己所有的物理内存,但虚拟内存只是逻辑上的内存,要想访问内存的数据,还得通过内存管理单元(MMU)查找页表,将虚拟内存映射成物理内存。如果映射的文件非常大,程序访问局部映射不到物理内存的虚拟内存时,产生缺页(Page Fault)中断,OS 需要读写磁盘文件的真实数据再加载到内存。 14 | 15 | ![image](https://user-images.githubusercontent.com/5803001/49648842-479cd680-fa62-11e8-96d4-3997faf92ec6.png) 16 | 17 | Linux 底层提供了 mmap 将一个程序指定的文件映射进虚拟内存(Virtual Memory),对文件的读写就变成了对内存的读写,能充分利用 Page Cache 的特性;不过,如果对文件进行随机读写,会使虚拟内存产生很多缺页中断。在大多数情况下,内核在读写磁盘时都会使用页缓存: 18 | 19 | - 内核在读文件时,首先在已有的页缓存中查找所读取的数据是否已经存在。如果该页缓存不存在,则一个新的页将被添加到高速缓存中,然后用从磁盘读取的数据填充它。如果当前物理内存足够空闲,那么该页将长期保留在高速缓存中,使得其他进程再使用该页中的数据时不再访问磁盘。 20 | 21 | - 写操作与读操作时类似,直接在页缓存中修改数据,但是页缓存中修改的数据(该页此时被称为 Dirty Page)并不是马上就被写入磁盘,而是延迟几秒钟,以防止进程对该页缓存中的数据再次修改。 22 | 23 | ## Direct IO 与 缓存 IO 24 | 25 | Direct I/O 绕过 Page Cache,而缓存 I/O 都是写到 Page Cache 里就表示写请求完成,然后由文件系统的刷脏页机制把数据刷到磁盘。因此,使用缓存 I/O,掉电时有可能 Page Cache 里的脏数据未刷到磁盘上,造成数据丢失。缓存 I/O 机制中,DMA 方式可以将数据直接从磁盘读到 Page Cache 中,或者将数据从 Page Cache 直接写回到磁盘上,而不能直接在应用程序地址空间和磁盘之间进行数据传输,这样的话,数据在传输过程中需要在应用程序地址空间和 Page Cache 之间进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。 26 | 27 | 而 Direct I/O 的优点就是通过减少操作系统内核缓冲区和应用程序地址空间的数据拷贝次数,降低了对文件读取和写入时所带来的 CPU 的使用以及内存带宽的占用,但是 Direct I/O 的读操作不能从 Page Cache 中获取数据,会直接从磁盘上读取,带来性能上的损失。一般 Direct I/O 与异步 I/O 结合起来使用提高性能,Direct I/O 要求用户态的缓冲区对齐,Direct I/O 一般用于需要自己管理缓存的应用如数据库系统。 28 | 29 | # 页缓存的设计 30 | 31 | 页缓存至少需要满足以下两种需求。首先,它必须可以快速定位含有给定数据的特定页。其次,由于页高速缓存中的数据来源不同,比如普通文件、块设备等,内核必须根据不同的数据来源来选择对页缓存的适当操作。内核通过抽象出 address_space 数据结构来满足上述两种设计需求。 32 | 33 | ## address_space 结构 34 | 35 | address_space 结构是页高速缓存机制中的核心数据结构,该结构并不是对某一个页高速缓存进行描述,而是以页高速缓存的所有者(owner)为单位,对其所拥有的缓存进行抽象描述。页高速缓存中每个页包含的数据肯定属于某个文件,该文件对应的 inode 对象就称为页高速缓存的所有者。 36 | 37 | 页缓存与文件系统和内存管理都有联系。每个 inode 结构中都嵌套一个 address_space 结构,即 inode 字段中的 i_data;同时 inode 中还有 i_maping 字段指向所嵌套 address_spaces 结构。而 address_space 结构通过 host 字段反指向页高速缓存的所有者。页缓存的本质就是一个物理页框,因此每个页描述符中通过 mmaping 和 index 两个字段与高速缓存进行关联。mmaping 指向页缓存所有者中的 address_space 对象。index 表示以页大小为单位的偏移量,该偏移量表示页框内数据在磁盘文件中的偏移量。 38 | 39 | address_space 结构中的 i_mmap 字段指向一个 radix 优先搜索树。该树将一个文件所有者中的所有页缓存组织在一起,这样可以快速搜索到指定的页缓存。内核中关于 radix 树有一套标准的使用方法,它不与特定的数据联系(与内核双联表类似),这样使得使用范围更加灵活。具体操作如下: 40 | 41 | - radix_tree_lookup(): 在 radix 树中对指定节点进行查找; 42 | - radix_tree_insert(): 在 radix 树中插入新节点; 43 | - radix_tree_delete(): 在 radix 树中删除指定节点; 44 | 45 | 此外,该结构中的 a_ops 字段指向 address_space_operations 结构,该结构是一个钩子函数集,它表明了对所有者的页进行操作的标准方法。比如 writepage 钩子函数表示将页中的数据写入到磁盘中,readpage 表示从磁盘文件中读数据到页中。通常,这些钩子函数将页缓存的所有者(inode)和访问物理设备的低级驱动程序关联起来。该函数集使得内核在上层使用统一的接口与页缓存进行交互,而底层则根据页缓存中数据的来源具体实现。通过上面的描述,可以看到 address_space 结构中的优先搜索树和钩子函数集解决了页高速缓存的两个主要设计需求。 46 | 47 | ## 内核对页缓存的操作函数 48 | 49 | 内核对页缓存的基本操作包含了在一个页缓存所形成的 radix 树中查找,增加和删除一个页缓存。基于 radix 的基本操作函数,页高速缓存的处理函数如下: 50 | 51 | - page_cache_alloc():分配一个新的页缓存; 52 | - find_get_page():在页高速缓存中查找指定页; 53 | - add_to_page_cache():把一个新页添加到页高速缓存; 54 | - remove_from_page_cache():将指定页从页高速缓存中移除; 55 | - read_cache_page():确保指定页在页高速缓存中包含最新的数据; 56 | -------------------------------------------------------------------------------- /02~存储/01~内存管理/高速缓存.md: -------------------------------------------------------------------------------- 1 | # 高速缓存 2 | 3 | # 程序访问的局部性 4 | 5 | 最早期的计算机,在执行一段程序时,都是把硬盘中的数据加载到内存,然后 CPU 从内存中取出代码和数据执行,在把计算结果写入内存,最终输出结果。随着程序运行越来越多,就发现一个规律:内存中某个地址被访问后,短时间内还有可能继续访问这块地址。内存中的某个地址被访问后,它相邻的内存单元被访问的概率也很大。 6 | 7 | 人们发现的这种规律被称为程序访问的局部性。程序访问的局部性包含 2 种: 8 | 9 | - 时间局部性:某个内存单元在较短时间内很可能被再次访问 10 | - 空间局部性:某个内存单元被访问后相邻的内存单元较短时间内很可能被访问 11 | 12 | 出现这种情况的原因很简单,因为程序是指令和数据组成的,指令在内存中按顺序存放且地址连续,如果运行一段循环程序或调用一个方法,又或者再程序中遍历一个数组,都有可能符合上面提到的局部性原理。那既然在执行程序时,内存的某些单元很可能会经常的访问或写入,那可否在 CPU 和内存之间,加一个缓存,CPU 在访问数据时,先看一下缓存中是否存在,如果有直接就读取缓存中的数据即可。如果缓存中不存在,再从内存中读取数据。 13 | 14 | 事实证明利用这种方式,程序的运行效率会提高 90%以上,这个缓存也叫做高速缓存 Cache。 15 | 16 | ## 缓存详解 17 | 18 | 缓存是 CPU 体系结构中最重要的元素之一。要编写高效的代码,开发人员需要了解其系统中的缓存如何工作。高速缓存是较慢的主系统内存的非常快的副本。高速缓存比主存储器要小得多,因为它与寄存器和处理器逻辑一起包含在处理器芯片内。从计算的角度来看,这是主要的房地产,并且其最大大小在经济和物理上都有限制。随着制造商发现越来越多的方法将越来越多的晶体管填充到芯片中,高速缓存的大小已大大增加,但是即使最大的高速缓存也只有几十兆字节,而不是主存储器的千兆字节或硬盘的数兆字节。 19 | 20 | 缓存由镜像主内存的小块组成。这些块的大小称为行大小,通常为 32 或 64 字节。在谈论缓存时,谈论行大小或缓存行是很常见的事,它指的是镜像主内存的一块。高速缓存只能以高速缓存行的倍数加载和存储内存。缓存具有自己的层次结构,通常称为 L1,L2 和 L3。L1 高速缓存是最快和最小的;L2 更大,更慢,L3 更大,更慢。 21 | 22 | L1 缓存通常进一步分为指令缓存和数据,在引入中继的基于哈佛 Mark-1 的计算机之后被称为“哈佛架构”。拆分缓存有助于减少流水线瓶颈,因为较早的流水线阶段倾向于引用指令缓存,而较后的阶段则倾向于数据缓存。除了减少对共享资源的争用之外,为指令提供单独的缓存还允许使用指令流性质的替代实现。它们是只读的,因此不需要昂贵的片上功能(例如多端口),也不需要处理子块读取操作,因为指令流通常使用更常规大小的访问。 23 | 24 | ![Cache Associativity](https://s2.ax1x.com/2020/01/27/1nssG6.png) 25 | 26 | 在正常操作期间,处理器会不断要求高速缓存检查高速缓存中是否存储了特定的地址,因此高速缓存需要某种方法来非常快速地查找其是否存在有效行。如果可以将给定地址缓存在缓存中的任何位置,则每次进行引用以确定命中或未命中时都需要搜索每个缓存行。为了保持快速搜索,这是在高速缓存硬件中并行完成的,但是对于合理大小的高速缓存而言,搜索每个条目通常过于昂贵。因此,可以通过限制特定地址必须驻留的位置来简化缓存。这是一个权衡;高速缓存显然比系统内存小得多,因此某些地址必须别名。如果两个彼此互为别名的地址一直在不断更新,则它们将争用缓存行。 27 | 28 | - 直接映射的缓存(Direct mapped caches)将允许缓存行仅存在于缓存中的单个条目中。这是最简单的在硬件中实现的方法,由于两个阴影地址必须共享同一条缓存行,因此无法避免混淆现象。 29 | 30 | - 完全关联高速缓存(Fully Associative caches)将允许高速缓存行存在于高速缓存的任何条目中。由于可以使用任何条目,因此可以避免别名问题。但是在硬件中实现非常昂贵,因为必须同时查找每个可能的位置以确定值是否在缓存中。 31 | 32 | - 集合关联缓存(Set Associative caches)是直接关联和完全关联缓存的混合,并且允许特定的缓存值存在于缓存内的某些行子集中。高速缓存被划分为偶数部分,称为方式,并且可以以任何方式定位特定地址。因此,n 路集关联缓存将允许在行大小为 set n 的总块 mod n 中的任何条目中存在一条缓存行。上图中“缓存关联性”显示了一个示例的 8 元素,4 路集关联缓存。在这种情况下,这两个地址具有四个可能的位置,这意味着在查找时仅必须搜索一半的缓存。方式越多,可能的位置就越多,混淆现象就越少,从而导致总体上更好的性能。 33 | 34 | 一旦缓存已满,处理器就需要清除一行来为新行腾出空间,处理器可以通过多种算法选择逐出哪条线。例如,最近最少使用(LRU)是一种算法,其中丢弃最旧的未使用的行以为新行腾出空间。当仅从高速缓存中读取数据时,无需确保与主内存的一致性。但是,当处理器开始写入高速缓存行时,它需要就如何更新底层主内存做出一些决定。 35 | 36 | - 直写式高速缓存(Write-through)将在处理器更新高速缓存时将更改直接写入主系统内存。这是较慢的,因为如我们所见,写入主存储器的过程较慢。 37 | 38 | - 回写缓存(Write-back)会将更改延迟写入 RAM,直到绝对必要为止。明显的优点是,写入缓存条目时需要较少的主存储器访问。已写入但未提交给内存的缓存行称为脏行。缺点是,当退出缓存条目时,它可能需要两次内存访问(一次写入脏数据主内存,另一次加载新数据)。 39 | 40 | 如果一个条目同时存在于较高级别和较低级别的高速缓存中,则我们说较高级别的高速缓存是 Inclusive;否则,如果具有一行的较高级别的高速缓存消除了具有该行的较低级别的高速缓存的可能性,我们说它是排他的(Exclusive)。 41 | 42 | ## 缓存寻址 43 | 44 | 到目前为止,我们还没有讨论过缓存如何确定给定地址是否驻留在缓存中。显然,高速缓存必须保留当前驻留在高速缓存行中的数据的目录。缓存目录和数据可能位于同一处理器上,但也可能是分开的,例如在具有核心 L3 目录的 POWER5 处理器的情况下,但是实际上访问数据需要遍历 L3 总线才能访问 核心内存。这样的安排可以促进更快的命中/未命中处理,而不会产生将整个缓存保留在内核中的其他成本。 45 | 46 | ![Cache tags](https://s2.ax1x.com/2020/01/27/1n67Dg.png) 47 | 48 | 需要并行检查标签以降低等待时间。更多的标记位(即,较少的设置关联性)需要更复杂的硬件来实现。另外,更多的集合关联性意味着更少的标签,但是处理器现在需要硬件来多路复用许多集合的输出,这也可能增加延迟。 49 | 50 | 为了快速确定地址是否位于缓存中,将其分为三个部分:标签,索引和偏移量。偏移位取决于高速缓存的行大小。例如,一个 32 字节的行大小将使用地址的最后 5 位(即 25)作为行的偏移量。索引是一个条目可能驻留的特定缓存行。例如,让我们考虑一个具有 256 个条目的缓存。如果这是一个直接映射的缓存,我们知道数据可能仅驻留在一条可能的行中,因此偏移量后的下一个 8 位(28)描述了要检查的行-0 到 255 之间。 51 | 52 | 现在,考虑相同的 256 元素高速缓存,但是分为两种方式。这意味着有两组 128 行,并且给定地址可以位于这两个组中的任何一个中。因此,仅需要 7 位作为索引即可偏移到 128 个条目的路径中。对于给定的高速缓存大小,随着方法数量的增加,由于每种方法都会变小,因此减少了作为索引所需的位数。高速缓存目录仍然需要检查高速缓存中存储的特定地址是否是它感兴趣的那个地址。因此,该地址的其余位是高速缓存目录对照传入的地址标记位进行检查以确定是否存在标记位。缓存命中与否。“缓存标签”中说明了这种关系。 53 | 54 | 当存在多种方式时,此检查必须在每种方式中并行进行,然后将其结果传递到多路复用器,该多路复用器输出最终的命中或未命中结果。如上所述,高速缓存的关联性越高,索引所需的位越少,而标记位则越多-到完全关联的高速缓存的极端(其中没有位用作索引位)。标签位的并行匹配是高速缓存设计的昂贵组件,并且通常是高速缓存可以增长多少行(即,多大)的限制因素。 55 | 56 | # Links 57 | 58 | - http://kaito-kidd.com/2018/08/23/computer-system-cpu-cache/ 59 | -------------------------------------------------------------------------------- /10~Shell 命令/Nushell/README.md: -------------------------------------------------------------------------------- 1 | # Nushell 2 | 3 | Nu 汲取了很多常见领域的灵感:传统 Shell 比如 Bash、基于对象的 Shell 比如 PowerShell、逐步类型化的语言比如 TypeScript、函数式编程、系统编程,等等。但是,Nu 并不试图成为万金油,而是把精力集中在做好这几件事上: 4 | 5 | - 作为一个具有现代感的灵活的跨平台 Shell; 6 | - 作为一种现代的编程语言,解决与数据有关的问题; 7 | - 给予清晰的错误信息和干净的 IDE 支持; 8 | 9 | 了解 Nu 能做什么的最简单的方法是从一些例子开始,所以让我们深入了解一下。 10 | 11 | 当你运行[`ls`](https://www.nushell.sh/commands/docs/ls.html)这样的命令时,你会注意到的第一件事是,你得到的不是一个文本块,而是一个结构化的表格: 12 | 13 | ```sh 14 | > ls 15 | ╭────┬───────────────────────┬──────┬───────────┬─────────────╮ 16 | │ # │ name │ type │ size │ modified │ 17 | ├────┼───────────────────────┼──────┼───────────┼─────────────┤ 18 | │ 0 │ 404.html │ file │ 429 B │ 3 days ago │ 19 | │ 1 │ CONTRIBUTING.md │ file │ 955 B │ 8 mins ago │ 20 | │ 2 │ Gemfile │ file │ 1.1 KiB │ 3 days ago │ 21 | │ 3 │ Gemfile.lock │ file │ 6.9 KiB │ 3 days ago │ 22 | │ 4 │ LICENSE │ file │ 1.1 KiB │ 3 days ago │ 23 | │ 5 │ README.md │ file │ 213 B │ 3 days ago │ 24 | ``` 25 | 26 | 该表不仅仅是以不同的方式显示目录,就像电子表格中的表一样,它还允许我们以更加互动的方式来处理数据。我们要做的第一件事是按大小对我们的表进行排序。要做到这一点,我们将从 ls 中获取输出,并将其输入到一个可以根据列的内容对表进行排序的命令中: 27 | 28 | ```sh 29 | > ls | sort-by size | reverse 30 | ╭────┬───────────────────────┬──────┬───────────┬─────────────╮ 31 | │ # │ name │ type │ size │ modified │ 32 | ├────┼───────────────────────┼──────┼───────────┼─────────────┤ 33 | │ 0 │ Gemfile.lock │ file │ 6.9 KiB │ 3 days ago │ 34 | │ 1 │ SUMMARY.md │ file │ 3.7 KiB │ 3 days ago │ 35 | │ 2 │ Gemfile │ file │ 1.1 KiB │ 3 days ago │ 36 | │ 3 │ LICENSE │ file │ 1.1 KiB │ 3 days ago │ 37 | │ 4 │ CONTRIBUTING.md │ file │ 955 B │ 9 mins ago │ 38 | │ 5 │ books.md │ file │ 687 B │ 3 days ago │ 39 | ... 40 | ``` 41 | 42 | 你可以看到,为了达到这个目的,我们没有向 ls 传递命令行参数。取而代之的是,我们使用了 Nu 提供的 sort-by 命令来对 ls 命令的输出进行排序。为了在顶部看到最大的文件,我们还使用了 reverse 命令。Nu 提供了许多可以对表进行操作的命令,例如,我们可以过滤 ls 表的内容,使其只显示超过 1 千字节的文件。 43 | 44 | ```sh 45 | > ls | where size > 1kb 46 | ╭───┬───────────────────┬──────┬─────────┬────────────╮ 47 | │ # │ name │ type │ size │ modified │ 48 | ├───┼───────────────────┼──────┼─────────┼────────────┤ 49 | │ 0 │ Gemfile │ file │ 1.1 KiB │ 3 days ago │ 50 | │ 1 │ Gemfile.lock │ file │ 6.9 KiB │ 3 days ago │ 51 | │ 2 │ LICENSE │ file │ 1.1 KiB │ 3 days ago │ 52 | │ 3 │ SUMMARY.md │ file │ 3.7 KiB │ 3 days ago │ 53 | ╰───┴───────────────────┴──────┴─────────┴────────────╯ 54 | ``` 55 | 56 | 就像在 Unix 哲学中一样,能够让命令相互对话给我们提供了在许多不同的组合中对命令进行混搭的方法。我们来看看一个不同的命令: 57 | 58 | ```sh 59 | > ps 60 | ╭─────┬──────┬──────────────────────┬─────────┬───────┬───────────┬──────────╮ 61 | │ # │ pid │ name │ status │ cpu │ mem │ virtual │ 62 | ├─────┼──────┼──────────────────────┼─────────┼───────┼───────────┼──────────┤ 63 | │ 0 │ 7570 │ nu │ Running │ 1.96 │ 23.2 MiB │ 32.8 GiB │ 64 | │ 1 │ 3533 │ remindd │ Sleep │ 0.00 │ 103.6 MiB │ 32.3 GiB │ 65 | │ 2 │ 3495 │ TVCacheExtension │ Sleep │ 0.00 │ 11.9 MiB │ 32.2 GiB │ 66 | │ 3 │ 3490 │ MusicCacheExtension │ Sleep │ 0.00 │ 12.9 MiB │ 32.2 GiB │ 67 | ... 68 | ``` 69 | 70 | 如果你使用过 Linux,你可能对 ps 命令很熟悉。通过它,我们可以得到一个当前系统正在运行的所有进程的列表,它们的状态是什么,以及它们的名字是什么,我们还可以看到这些进程的 CPU 负载。如果我们想显示那些正在活跃使用 CPU 的进程呢?就像我们之前对 ls 命令所做的那样,我们也可以利用 ps 命令返回给我们的表格来做到: 71 | 72 | ```sh 73 | > ps | where cpu > 5 74 | ╭───┬──────┬────────────────┬─────────┬────────┬───────────┬──────────╮ 75 | │ # │ pid │ name │ status │ cpu │ mem │ virtual │ 76 | ├───┼──────┼────────────────┼─────────┼────────┼───────────┼──────────┤ 77 | │ 0 │ 1583 │ Terminal │ Running │ 20.69 │ 127.8 MiB │ 33.0 GiB │ 78 | │ 1 │ 579 │ photoanalysisd │ Running │ 139.50 │ 99.9 MiB │ 32.3 GiB │ 79 | ╰───┴──────┴────────────────┴─────────┴────────┴───────────┴──────────╯ 80 | ``` 81 | -------------------------------------------------------------------------------- /10~Shell 命令/Shell 编程/函数/2024~Shell 函数完全指南.md: -------------------------------------------------------------------------------- 1 | 2 | # Shell 函数完全指南 3 | 4 | ## 1. 函数定义语法 5 | 6 | Shell 提供了三种定义函数的语法: 7 | 8 | ```bash 9 | # 1. POSIX 标准语法 10 | name() { 11 | commands 12 | } 13 | 14 | # 2. KSH 风格语法 15 | function name { 16 | commands 17 | } 18 | 19 | # 3. Bash 混合语法 20 | function name() { 21 | commands 22 | } 23 | ``` 24 | 25 | 所有语法都是等效的,但推荐使用 POSIX 标准语法,因为它具有最好的兼容性。 26 | 27 | ## 2. 函数参数 28 | 29 | ### 2.1 参数访问 30 | 31 | ```bash 32 | example_function() { 33 | echo "第一个参数: $1" 34 | echo "第二个参数: $2" 35 | echo "参数个数: $#" 36 | echo "所有参数: $@" 37 | echo "函数名: ${FUNCNAME[0]}" 38 | } 39 | 40 | # 调用函数 41 | example_function "arg1" "arg2" 42 | ``` 43 | 44 | ### 2.2 特殊参数变量 45 | 46 | - `$1, $2, ...` - 位置参数 47 | - `$#` - 参数个数 48 | - `$@` - 所有参数(作为独立的单词) 49 | - `$*` - 所有参数(作为单个字符串) 50 | - `$FUNCNAME` - 当前函数名 51 | 52 | ## 3. 变量作用域 53 | 54 | ### 3.1 局部变量 55 | 56 | ```bash 57 | my_function() { 58 | local local_var="局部变量" # 只在函数内可见 59 | global_var="全局变量" # 在整个脚本可见 60 | } 61 | ``` 62 | 63 | ### 3.2 全局变量 64 | 65 | ```bash 66 | # 全局变量定义 67 | GLOBAL_CONFIG="/etc/myapp.conf" 68 | 69 | config_reader() { 70 | echo "读取配置: $GLOBAL_CONFIG" 71 | } 72 | ``` 73 | 74 | ## 4. 函数返回值 75 | 76 | ### 4.1 使用 return 77 | 78 | ```bash 79 | is_number() { 80 | local num="$1" 81 | if [[ "$num" =~ ^[0-9]+$ ]]; then 82 | return 0 # 成功 83 | else 84 | return 1 # 失败 85 | fi 86 | } 87 | 88 | # 使用返回值 89 | is_number "123" 90 | if [ $? -eq 0 ]; then 91 | echo "是数字" 92 | fi 93 | ``` 94 | 95 | ### 4.2 使用输出捕获 96 | 97 | ```bash 98 | get_timestamp() { 99 | echo "$(date '+%Y-%m-%d %H:%M:%S')" 100 | } 101 | 102 | # 捕获输出 103 | current_time=$(get_timestamp) 104 | ``` 105 | 106 | ## 5. 实际应用案例 107 | 108 | ### 5.1 NAS 挂载函数 109 | 110 | ```bash 111 | mount_nas() { 112 | local NASMNT="/nas10" 113 | local NASSERVER="nas10.example.com" 114 | local NASUSER="admin" 115 | local NASPASSWORD="password" 116 | 117 | # 创建挂载点 118 | [ ! -d "$NASMNT" ] && mkdir -p "$NASMNT" 119 | 120 | # 检查是否已挂载 121 | if ! mount | grep -q "$NASMNT"; then 122 | mount -t cifs "//$NASSERVER/$NASUSER" \ 123 | -o username="$NASUSER",password="$NASPASSWORD" "$NASMNT" 124 | fi 125 | } 126 | 127 | umount_nas() { 128 | local NASMNT="/nas10" 129 | mount | grep -q "$NASMNT" && umount "$NASMNT" 130 | } 131 | ``` 132 | 133 | ### 5.2 文件类型检查 134 | 135 | ```bash 136 | check_file_type() { 137 | local file="$1" 138 | 139 | # 参数验证 140 | [ -z "$file" ] && { echo "错误: 未指定文件"; return 1; } 141 | 142 | # 检查文件类型 143 | [ -f "$file" ] && echo "$file 是普通文件" 144 | [ -d "$file" ] && echo "$file 是目录" 145 | [ -L "$file" ] && echo "$file 是符号链接" 146 | [ -x "$file" ] && echo "$file 是可执行文件" 147 | } 148 | ``` 149 | 150 | ## 6. 最佳实践 151 | 152 | ### 6.1 函数文档 153 | 154 | ```bash 155 | ####################################### 156 | # 函数描述 157 | # 参数: 158 | # $1 - 参数1的描述 159 | # $2 - 参数2的描述 160 | # 返回值: 161 | # 0 - 成功 162 | # 1 - 失败 163 | ####################################### 164 | function_name() { 165 | # 函数实现 166 | } 167 | ``` 168 | 169 | ### 6.2 参数验证 170 | 171 | ```bash 172 | process_file() { 173 | # 验证参数数量 174 | if [ $# -lt 1 ]; then 175 | echo "错误: 缺少参数" >&2 176 | return 1 177 | fi 178 | 179 | # 验证文件存在 180 | local file="$1" 181 | if [ ! -f "$file" ]; then 182 | echo "错误: 文件不存在: $file" >&2 183 | return 1 184 | fi 185 | } 186 | ``` 187 | 188 | ### 6.3 错误处理 189 | 190 | ```bash 191 | handle_error() { 192 | echo "错误发生在 ${FUNCNAME[1]}, 行号 $1" >&2 193 | exit 1 194 | } 195 | 196 | trap 'handle_error $LINENO' ERR 197 | ``` 198 | 199 | ## 7. 函数管理 200 | 201 | ### 7.1 函数移除 202 | 203 | ```bash 204 | # 删除函数定义 205 | unset -f function_name 206 | ``` 207 | 208 | ### 7.2 函数列表 209 | 210 | ```bash 211 | # 显示所有函数 212 | declare -F 213 | 214 | # 显示函数定义 215 | declare -f function_name 216 | ``` 217 | 218 | 通过遵循这些规范和最佳实践,可以编写出更加可靠和可维护的 Shell 函数。记住要始终进行适当的错误处理和参数验证,并为函数提供清晰的文档说明。 219 | -------------------------------------------------------------------------------- /03~网络/零拷贝/Linux 下的实现.md: -------------------------------------------------------------------------------- 1 | # mmap 与 write 2 | 3 | mmap 与 write 简单来说就是使用 mmap 替换了 read 与 write 中的 read 操作,减少了一次 CPU 的拷贝。mmap 主要实现方式是将读缓冲区的地址和用户缓冲区的地址进行映射,内核缓冲区和应用缓冲区共享,从而减少了从读缓冲区到用户缓冲区的一次 CPU 拷贝。 4 | 5 | ```c 6 | tmp_buf = mmap(file, len); 7 | write(socket, tmp_buf, len); 8 | ``` 9 | 10 | ![mmap 示意图](https://pic.imgdb.cn/item/60545141524f85ce290ef203.jpg) 11 | 12 | 整个过程发生了 4 次用户态和内核态的上下文切换和 3 次拷贝,具体流程如下: 13 | 14 | - 用户进程通过 mmap()方法向操作系统发起调用,上下文从用户态转向内核态 15 | - DMA 控制器把数据从硬盘中拷贝到读缓冲区 16 | - 上下文从内核态转为用户态,mmap 调用返回 17 | - 用户进程通过 write()方法发起调用,上下文从用户态转为内核态 18 | - CPU 将读缓冲区中数据拷贝到 socket 缓冲区 19 | - DMA 控制器把数据从 socket 缓冲区拷贝到网卡,上下文从内核态切换回用户态,write()返回 20 | 21 | 使用 mmap 替代 read 很明显减少了一次拷贝,当拷贝数据量很大时,无疑提升了效率。但是使用 mmap 会有一些隐藏的陷阱,例如,当你的程序 map 了一个文件,但是当这个文件被另一个进程截断(truncate)时, write 系统调用会因为访问非法地址而被 SIGBUS 信号终止。SIGBUS 信号默认会杀死你的进程并产生一个 coredump,最终可能导致服务器的终止。通常我们使用以下解决方案避免这种问题: 22 | 23 | - 为 SIGBUS 信号建立信号处理程序:当遇到 SIGBUS 信号时,信号处理程序简单地返回,write 系统调用在被中断之前会返回已经写入的字节数,并且 errno 会被设置成 success,但是这是一种糟糕的处理办法,因为你并没有解决问题的实质核心。 24 | - 使用文件租借锁:通常我们使用这种方法,在文件描述符上使用租借锁,我们为文件向内核申请一个租借锁,当其它进程想要截断这个文件时,内核会向我们发送一个实时的 RT_SIGNAL_LEASE 信号,告诉我们内核正在破坏你加持在文件上的读写锁。这样在程序访问非法内存并且被 SIGBUS 杀死之前,你的 write 系统调用会被中断。write 会返回已经写入的字节数,并且置 errno 为 success。 25 | 26 | 我们应该在 mmap 文件之前加锁,并且在操作完文件后解锁: 27 | 28 | ```cpp 29 | if(fcntl(diskfd, F_SETSIG, RT_SIGNAL_LEASE) == -1) { 30 | perror("kernel lease set signal"); 31 | return -1; 32 | } 33 | /* l_type can be F_RDLCK F_WRLCK 加锁*/ 34 | /* l_type can be F_UNLCK 解锁*/ 35 | if(fcntl(diskfd, F_SETLEASE, l_type)){ 36 | perror("kernel lease set type"); 37 | return -1; 38 | } 39 | ``` 40 | 41 | # sendfile 42 | 43 | 在内核版本 2.1 中,引入了 sendfile 系统调用,以简化网络上和两个本地文件之间的数据传输。相比 mmap 来说,sendfile 同样减少了一次 CPU 拷贝,而且还减少了 2 次上下文切换。使用如下: 44 | 45 | ```cpp 46 | #include 47 | 48 | ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); 49 | ``` 50 | 51 | 系统调用 sendfile()在代表输入文件的描述符 in_fd 和代表输出文件的描述符 out_fd 之间传送文件内容(字节)。描述符 out_fd 必须指向一个套接字,而 in_fd 指向的文件必须是可以 mmap 的。这些局限限制了 sendfile 的使用,使 sendfile 只能将数据从文件传递到套接字上,反之则不行。使用 sendfile 不仅减少了数据拷贝的次数,还减少了上下文切换,数据传送始终只发生在 kernel space。 52 | 53 | ![sendfile 示意图](https://pic.imgdb.cn/item/6054539b524f85ce29107de6.jpg) 54 | 55 | 整个过程发生了 2 次用户态和内核态的上下文切换和 3 次拷贝,具体流程如下: 56 | 57 | - 用户进程通过 sendfile()方法向操作系统发起调用,上下文从用户态转向内核态 58 | - DMA 控制器把数据从硬盘中拷贝到读缓冲区 59 | - CPU 将读缓冲区中数据拷贝到 socket 缓冲区 60 | - DMA 控制器把数据从 socket 缓冲区拷贝到网卡,上下文从内核态切换回用户态,sendfile 调用返回 61 | 62 | sendfile 方法 IO 数据对用户空间完全不可见,所以只能适用于完全不需要用户空间处理的情况,比如静态文件服务器。此外,在我们调用 sendfile 时,如果有其它进程截断了文件会发生什么呢?假设我们没有设置任何信号处理程序,sendfile 调用仅仅返回它在被中断之前已经传输的字节数,errno 会被置为 success。如果我们在调用 sendfile 之前给文件加了锁,sendfile 的行为仍然和之前相同,我们还会收到 RT_SIGNAL_LEASE 的信号。 63 | 64 | ## sendfile+DMA Scatter/Gather 65 | 66 | Linux2.4 内核版本之后对 sendfile 做了进一步优化,通过引入新的硬件支持,这个方式叫做 DMA Scatter/Gather 分散/收集功能。它将读缓冲区中的数据描述信息:内存地址和偏移量记录到 socket 缓冲区,由 DMA 根据这些将数据从读缓冲区拷贝到网卡,相比之前版本减少了一次 CPU 拷贝的过程。 67 | 68 | ![sendfile DMA Gather 示意图](https://pic.imgdb.cn/item/60545496524f85ce29110cfe.jpg) 69 | 70 | 整个过程发生了 2 次用户态和内核态的上下文切换和 2 次拷贝,其中更重要的是完全没有 CPU 拷贝,具体流程如下: 71 | 72 | - 用户进程通过 sendfile()方法向操作系统发起调用,上下文从用户态转向内核态 73 | - DMA 控制器利用 scatter 把数据从硬盘中拷贝到读缓冲区离散存储 74 | - CPU 把读缓冲区中的文件描述符和数据长度发送到 socket 缓冲区 75 | - DMA 控制器根据文件描述符和数据长度,使用 scatter/gather 把数据从内核缓冲区拷贝到网卡 76 | - sendfile()调用返回,上下文从内核态切换回用户态 77 | 78 | DMA gather 和 sendfile 一样数据对用户空间不可见,而且需要硬件支持,同时输入文件描述符只能是文件,但是过程中完全没有 CPU 拷贝过程,极大提升了性能。 79 | 80 | # splice 81 | 82 | sendfile 只适用于将数据从文件拷贝到套接字上,限定了它的使用范围。Linux 在 2.6.17 版本引入 splice 系统调用,用于在两个文件描述符中移动数据: 83 | 84 | ```cpp 85 | #define _GNU_SOURCE /* See feature_test_macros(7) */ 86 | #include 87 | ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags); 88 | ``` 89 | 90 | splice 调用在两个文件描述符之间移动数据,而不需要数据在内核空间和用户空间来回拷贝。他从 fd_in 拷贝 len 长度的数据到 fd_out,但是有一方必须是管道设备,这也是目前 splice 的一些局限性。flags 参数有以下几种取值: 91 | 92 | - **SPLICE_F_MOVE**:尝试去移动数据而不是拷贝数据。这仅仅是对内核的一个小提示:如果内核不能从 pipe 移动数据或者 pipe 的缓存不是一个整页面,仍然需要拷贝数据。Linux 最初的实现有些问题,所以从 2.6.21 开始这个选项不起作用,后面的 Linux 版本应该会实现。 93 | - **SPLICE_F_NONBLOCK**:splice 操作不会被阻塞。然而,如果文件描述符没有被设置为不可被阻塞方式的 IO,那么调用 splice 有可能仍然被阻塞。 94 | - **SPLICE_F_MORE**:后面的 splice 调用会有更多的数据。 95 | 96 | splice 调用利用了 Linux 提出的管道缓冲区机制,所以至少一个描述符要为管道。 97 | -------------------------------------------------------------------------------- /01~进程与处理器/进程与线程/01.用户态与内核态/99~参考资料/2021-从根上理解用户态与内核态.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://zhuanlan.zhihu.com/p/388057431) 2 | 3 | # 从根上理解用户态与内核态 4 | 5 | 本篇文章开始探秘用户态与内核态,虽然一般面试不会问这个,但搞清楚这块,对我们理解整个计算机系统是及其有意义的,这会让你在今后的学习中豁然开朗,你肯定会发出:“啊,原来如此的感叹!” 6 | 7 | ## 内容大纲 8 | 9 | ![img](https://pic3.zhimg.com/80/v2-a528a0c54f25bf78655a3f74a09b9606_1440w.webp) 10 | 11 | ## 小故事 12 | 13 | 张三是某科技公司的初级 Java 开发工程师(低权限),目前在 15 楼办公码代码,公司提供的资源仅有一套电脑(用户态),张三想着这一线的房价,倍感压力山大,于是给自己定下一个目标,一定要做技术总监,在一线扎根,奋斗 B 张三,奋斗 5 年终于当上了技术总监(高权限),之后张三搬到 30 楼,可以随时向资源部(系统调用)申请公司各种资源与获取公司的机密信息(内核态),所谓是走上人生巅峰。 14 | 15 | 通过这个故事,我们发现,低权限的资源范围较小,高权限的资源范围更大,所谓的「用户态与内核态只是不同权限的资源范围」。 16 | 17 | ## CPU 指令集权限 18 | 19 | 在说用户态与内核态之前,有必要说一下 `CPU 指令集`,指令集是 CPU 实现软件指挥硬件执行的媒介,具体来说每一条汇编语句都对应了一条 `CPU 指令`,而非常非常多的 `CPU 指令` 在一起,可以组成一个、甚至多个集合,指令的集合叫 `CPU 指令集`。 20 | 21 | 同时 `CPU 指令集` 有权限分级,大家试想,`CPU 指令集` 可以直接操作硬件的,要是因为指令操作的不规范`,造成的错误会影响整个计算机系统的。好比你写程序,因为对硬件操作不熟悉,导致操作系统内核、及其他所有正在运行的程序,都可能会因为操作失误而受到不可挽回的错误,最后只能重启计算机才行。 22 | 23 | 而对于硬件的操作是非常复杂的,参数众多,出问题的几率相当大,必须谨慎的进行操作,对开发人员来说是个艰巨的任务,还会增加负担,同时开发人员在这方面也不被信任,所以操作系统内核直接屏蔽开发人员对硬件操作的可能,都不让你碰到这些 `CPU 指令集`。 24 | 25 | ![img](https://pic3.zhimg.com/80/v2-506586ddee155b7c553e3d5cf660209e_1440w.webp) 26 | 27 | 针对上面的需求,硬件设备商直接提供硬件级别的支持,做法就是对 `CPU 指令集`设置了权限,不同级别权限能使用的 `CPU 指令集` 是有限的,以 Inter CPU 为例,Inter 把 `CPU 指令集` 操作的权限由高到低划为 4 级: 28 | 29 | - ring 0 30 | - ring 1 31 | - ring 2 32 | - ring 3 33 | 34 | 其中 ring 0 权限最高,可以使用所有 `CPU 指令集`,ring 3 权限最低,仅能使用常规 `CPU 指令集`,不能使用操作硬件资源的 `CPU 指令集`,比如 `I O` 读写、网卡访问、申请内存都不行,Linux 系统仅采用 ring 0 和 ring 3 这 2 个权限。 35 | 36 | ![img](https://pic3.zhimg.com/80/v2-7eb07a96e11e6da0cafd15edc82faa96_1440w.webp) 37 | 38 | 高情商 39 | 40 | - ring 0 被叫做内核态,完全在操作系统内核中运行 41 | - ring 3 被叫做用户态,在应用程序中运行 42 | 43 | 低情商 44 | 45 | - 执行内核空间的代码,具有 ring 0 保护级别,有对硬件的所有操作权限,可以执行所有`CPU 指令集`,访问任意地址的内存,在内核模式下的任何异常都是灾难性的,将会导致整台机器停机 46 | - 在用户模式下,具有 ring 3 保护级别,代码没有对硬件的直接控制权限,也不能直接访问地址的内存,程序是通过调用系统接口(System Call APIs)来达到访问硬件和内存,在这种保护模式下,即时程序发生崩溃也是可以恢复的,在电脑上大部分程序都是在,用户模式下运行的 47 | 48 | ## 用户态与内核态 49 | 50 | 通关了 CPU 指令集权限,现在再说用户态与内核态就十分简单了,用户态与内核态的概念就是 CPU 指令集权限的区别,进程中要读写 `I O`,必然会用到 ring 0 级别的 `CPU 指令集`,而此时 CPU 的指令集操作权限只有 ring 3,为了可以操作 ring 0 级别的 `CPU 指令集`,CPU 切换指令集操作权限级别为 ring 0,CPU 再执行相应的 ring 0 级别的 `CPU 指令集`(内核代码),执行的内核代码会使用当前进程的内核栈。 51 | 52 | PS:每个进程都有两个栈,分别是用户栈与内核栈,对应用户态与内核态的使用 53 | 54 | ## 用户态与内核态的空间 55 | 56 | 在内存资源上的使用,操作系统对用户态与内核态也做了限制,每个进程创建都会分配「虚拟空间地址」(不懂可以参考我的另一篇文章“15 分钟!一文帮小白搞懂操作系统之内存”),以 Linux32 位操作系统为例,它的寻址空间范围是 `4G`(2 的 32 次方),而操作系统会把虚拟控制地址划分为两部分,一部分为内核空间,另一部分为用户空间,高位的 `1G`(从虚拟地址 0xC0000000 到 0xFFFFFFFF)由内核使用,而低位的 `3G`(从虚拟地址 0x00000000 到 0xBFFFFFFF)由各个进程使用。 57 | 58 | ![img](https://pic4.zhimg.com/80/v2-1870a0008929b2a66e2462db14200213_1440w.webp) 59 | 60 | - 用户态:只能操作 `0-3G` 范围的低位虚拟空间地址 61 | - 内核态:`0-4G` 范围的虚拟空间地址都可以操作,尤其是对 `3-4G` 范围的高位虚拟空间地址必须由内核态去操作 62 | - 补充:`3G-4G` 部分大家是共享的(指所有进程的内核态逻辑地址是共享同一块内存地址),是内核态的地址空间,这里存放在整个内核的代码和所有的内核模块,以及内核所维护的数据 63 | 64 | 每个进程的 `4G` 虚拟空间地址,高位 `1G` 都是一样的,即内核空间。只有剩余的 `3G` 才归进程自己使用,换句话说就是,高位 `1G` 的内核空间是被所有进程共享的! 65 | 66 | 最后做个小结,我们通过指令集权限区分用户态和内核态,还限制了内存资源的使用,操作系统为用户态与内核态划分了两块内存空间,给它们对应的指令集使用 67 | 68 | ## 用户态与内核态的切换 69 | 70 | 相信大家都听过这样的话「用户态和内核态切换的开销大」,但是它的开销大在那里呢?简单点来说有下面几点 71 | 72 | - 保留用户态现场(上下文、寄存器、用户栈等) 73 | - 复制用户态参数,用户栈切到内核栈,进入内核态 74 | - 额外的检查(因为内核代码对用户不信任) 75 | - 执行内核态代码 76 | - 复制内核态代码执行结果,回到用户态 77 | - 恢复用户态现场(上下文、寄存器、用户栈等) 78 | 79 | 实际上操作系统会比上述的更复杂,这里只是个大概,我们可以发现一次切换经历了「用户态 -> 内核态 -> 用户态」。 80 | 81 | 用户态要主动切换到内核态,那必须要有入口才行,实际上内核态是提供了统一的入口,下面是 Linux 整体架构图 82 | 83 | ![img](https://pic1.zhimg.com/80/v2-62ff353fcfb90639ae2ec7aa5006b73c_1440w.webp) 84 | 85 | 从上图我们可以看出来通过系统调用将 Linux 整个体系分为用户态和内核态,为了使应用程序访问到内核的资源,如 CPU、内存、I/O,内核必须提供一组通用的访问接口,这些接口就叫系统调用。 86 | 87 | 库函数就是屏蔽这些复杂的底层实现细节,减轻程序员的负担,从而更加关注上层的逻辑实现,它对系统调用进行封装,提供简单的基本接口给程序员。 88 | 89 | Shell 顾名思义,就是外壳的意思,就好像把内核包裹起来的外壳,它是一种特殊的应用程序,俗称命令行。Shell 也是可编程的,它有标准的 Shell 语法,符合其语法的文本叫 Shell 脚本,很多人都会用 Shell 脚本实现一些常用的功能,可以提高工作效率。 90 | 91 | 最后来说说,什么情况会导致用户态到内核态切换 92 | 93 | - 系统调用:用户态进程主动切换到内核态的方式,用户态进程通过系统调用向操作系统申请资源完成工作,例如 fork()就是一个创建新进程的系统调用,系统调用的机制核心使用了操作系统为用户特别开放的一个中断来实现,如 Linux 的 int 80h 中断,也可以称为软中断 94 | - 异常:当 CPU 在执行用户态的进程时,发生了一些没有预知的异常,这时当前运行进程会切换到处理此异常的内核相关进程中,也就是切换到了内核态,如缺页异常 95 | - 中断:当 CPU 在执行用户态的进程时,外围设备完成用户请求的操作后,会向 CPU 发出相应的中断信号,这时 CPU 会暂停执行下一条即将要执行的指令,转到与中断信号对应的处理程序去执行,也就是切换到了内核态。如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后边的操作等。 96 | -------------------------------------------------------------------------------- /03~网络/Linux 网络 IO 模型/README.md: -------------------------------------------------------------------------------- 1 | # Linux 网络 IO 模型 2 | 3 | 对 Linux 系统而言,所有设备都是文件,其中包括磁盘、内存、网卡、键盘、显示器等等,对所有这些文件的访问都属于 IO。针对所有的 IO 对象,可以将 IO 分成三类:网络 IO、磁盘 IO 和内存 IO。而通常我们说的是前两种。 4 | 5 | 只有网络 IO 是能够单线程事件循环,文件 IO 暂时只能用线程池来模拟事件循环。 6 | 7 | 在整个请求过程中,IO 设备数据输入至内核 buffer 需要时间,而从内核 buffer 复制数据至进程 Buffer 也需要时间。因此根据在这两段时间内等待方式的不同,IO 动作可以分为以下五种模式: 8 | 9 | - 阻塞 IO (Blocking IO): 发起 IO 操作后阻塞当前线程直到 IO 结束,标准的同步 IO,如默认行为的 posix read 和 write。 10 | - 非阻塞 IO (Non-Blocking IO): 发起 IO 操作后不阻塞,用户可阻塞等待多个 IO 操作同时结束。non-blocking 也是一种同步 IO:“批量的同步”。如 linux 下的 poll,select, epoll,BSD 下的 kqueue。 11 | - IO 复用(IO Multiplexing): 发起 IO 操作后不阻塞,用户得递一个回调待 IO 结束后被调用。如 windows 下的 OVERLAPPED + IOCP。linux 的 native AIO 只对文件有效。 12 | - 信号驱动的 IO (Signal Driven IO) 13 | - 异步 IO (Asynchrnous IO) 14 | 15 | ![IO 模型](https://s3.ax1x.com/2021/02/28/6CWFr6.png) 16 | 17 | 前四个模型之间的主要区别是第一阶段,四个模型的第二阶段是一样的,过程受阻在调用 recvfrom 当数据从内核拷贝到用户缓冲区。然而,异步 IO 处理两个阶段,与前四个不同。 18 | 19 | linux 一般使用 non-blocking IO 提高 IO 并发度。当 IO 并发度很低时,non-blocking IO 不一定比 blocking IO 更高效,因为后者完全由内核负责,而 read/write 这类系统调用已高度优化,效率显然高于一般得多个线程协作的 non-blocking IO。但当 IO 并发度愈发提高时,blocking IO 阻塞一个线程的弊端便显露出来:内核得不停地在线程间切换才能完成有效的工作,一个 cpu core 上可能只做了一点点事情,就马上又换成了另一个线程,cpu cache 没得到充分利用,另外大量的线程会使得依赖 thread-local 加速的代码性能明显下降,如 tcmalloc,一旦 malloc 变慢,程序整体性能往往也会随之下降。而 non-blocking IO 一般由少量 event dispatching 线程和一些运行用户逻辑的 worker 线程组成,这些线程往往会被复用(换句话说调度工作转移到了用户态),event dispatching 和 worker 可以同时在不同的核运行(流水线化),内核不用频繁的切换就能完成有效的工作。线程总量也不用很多,所以对 thread-local 的使用也比较充分。这时候 non-blocking IO 就往往比 blocking IO 快了。不过 non-blocking IO 也有自己的问题,它需要调用更多系统调用,比如 epoll_ctl,由于 epoll 实现为一棵红黑树,epoll_ctl 并不是一个很快的操作,特别在多核环境下,依赖 epoll_ctl 的实现往往会面临棘手的扩展性问题。non-blocking 需要更大的缓冲,否则就会触发更多的事件而影响效率。non-blocking 还得解决不少多线程问题,代码比 blocking 复杂很多。 20 | 21 | ## 阻塞 IO (Blocking IO) 22 | 23 | 当用户进程调用了 recvfrom 这个系统调用,内核就开始了 IO 的第一个阶段:等待数据准备。对于 network io 来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的 UDP 包),这个时候内核就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当内核一直等到数据准备好了,它就会将数据从内核中拷贝到用户内存,然后内核返回结果,用户进程才解除 block 的状态,重新运行起来。所以,blocking IO 的特点就是在 IO 执行的两个阶段都被 block 了。(整个过程一直是阻塞的) 24 | 25 | ![Blocking IO](https://pic.imgdb.cn/item/60861c8cd1a9ae528f961d8a.png) 26 | 27 | ![Blocking IO 时序图](https://pic.imgdb.cn/item/60861c9dd1a9ae528f969830.png) 28 | 29 | ## 非阻塞 IO (Non-Blocking IO) 30 | 31 | linux 下,可以通过设置 socket 使其变为 non-blocking。当对一个 non-blocking socket 执行读操作时,流程是如下图所示: 32 | 33 | ![non-blocking](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230417210357.png) 34 | 35 | 当用户进程调用 recvfrom 时,系统不会阻塞用户进程,而是立刻返回一个 ewouldblock 错误,从用户进程角度讲,并不需要等待,而是马上就得到了一个结果(这个结果就是 ewouldblock)。用户进程判断标志是 ewouldblock 时,就知道数据还没准备好,于是它就可以去做其他的事了,于是它可以再次发送 recvfrom,一旦内核中的数据准备好了。并且又再次收到了用户进程的 system call,那么它马上就将数据拷贝到了用户内存,然后返回。当一个应用程序在一个循环里对一个非阻塞调用 recvfrom,我们称为轮询。应用程序不断轮询内核,看看是否已经准备好了某些操作。这通常是浪费 CPU 时间。 36 | 37 | ## IO 复用(IO Multiplexing) 38 | 39 | 我们都知道,select/epoll 的好处就在于单个 process 就可以同时处理多个网络连接的 IO。它的基本原理就是 select/epoll 这个 function 会不断的轮询所负责的所有 socket,当某个 socket 有数据到达了,就通知用户进程。它的流程如图: 40 | 41 | ![IO Multiplexing](https://pic.imgdb.cn/item/6077cf498322e6675c51f93e.png) 42 | 43 | Linux 提供 select/epoll,进程通过将一个或者多个 fd 传递给 select 或者 poll 系统调用,阻塞在 select 操作上,这样 select/poll 可以帮我们侦测多个 fd 是否处于就绪状态。select/poll 是顺序扫描 fd 是否就绪,而且支持的 fd 数量有限,因此它的使用受到一定的限制。Linux 还提供了一个 epoll 系统调用,epoll 使用基于事件驱动的方式代替顺序扫描,因此性能更高一些。 44 | 45 | ![epoll,进程通过将一个或者多个](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230417211906.png) 46 | 47 | IO 复用模型具体流程:用户进程调用了 select,那么整个进程会被 block,而同时,内核会“监视”所有 select 负责的 socket,当任何一个 socket 中的数据准备好了,select 就会返回。这个时候用户进程再调用 read 操作,将数据从内核拷贝到用户进程。这个图和 blocking IO 的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个 system call (select 和 recvfrom),而 blocking IO 只调用了一个 system call (recvfrom)。但是,用 select 的优势在于它可以同时处理多个 connection。 48 | 49 | ## 信号驱动的 IO (Signal Driven IO) 50 | 51 | 首先用户进程建立 SIGIO 信号处理程序,并通过系统调用 sigaction 执行一个信号处理函数,这时用户进程便可以做其他的事了,一旦数据准备好,系统便为该进程生成一个 SIGIO 信号,去通知它数据已经准备好了,于是用户进程便调用 recvfrom 把数据从内核拷贝出来,并返回结果。 52 | 53 | ![Signal Driven IO](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230417211922.png) 54 | 55 | ## 异步 IO 56 | 57 | 一般来说,这些函数通过告诉内核启动操作并在整个操作(包括内核的数据到缓冲区的副本)完成时通知我们。这个模型和前面的信号驱动 IO 模型的主要区别是,在信号驱动的 IO 中,内核告诉我们何时可以启动 IO 操作,但是异步 IO 时,内核告诉我们何时 IO 操作完成。 58 | 59 | ![异步 IO](https://s3.ax1x.com/2021/02/28/6CWyoF.png) 60 | 61 | 当用户进程向内核发起某个操作后,会立刻得到返回,并把所有的任务都交给内核去完成(包括将数据从内核拷贝到用户自己的缓冲区),内核完成之后,只需返回一个信号告诉用户进程已经完成就可以了。 62 | -------------------------------------------------------------------------------- /10~Shell 命令/命令执行/输入与输出.md: -------------------------------------------------------------------------------- 1 | # 用户键盘输入 2 | 3 | 您可以使用 read 命令接受键盘输入,并将输入值分配给用户定义的 shell 变量。 4 | 5 | ```sh 6 | $ read -p "Prompt" variable1 variable2 variableN 7 | ``` 8 | 9 | 简单的示例如下: 10 | 11 | ```sh 12 | #!/bin/bash 13 | read -p "Enter your name : " name 14 | echo "Hi, $name. Let us be friends!" 15 | 16 | Enter your name : Vivek Gite 17 | Hi, Vivek Gite. Let us be friends! 18 | 19 | # read three numbers and assigned them to 3 vars 20 | read -p "Enter number one : " n1 21 | read -p "Enter number two : " n2 22 | read -p "Enter number three : " n3 23 | 24 | # display back 3 numbers - punched by user. 25 | echo "Number1 - $n1" 26 | echo "Number2 - $n2" 27 | echo "Number3 - $n3" 28 | 29 | # A shell script to display the Internet domain name owner information (domain.sh): 30 | read -p "Enter the Internet domain name (e.g. nixcraft.com) : " domain_name 31 | whois $domain_name 32 | ``` 33 | 34 | 您可以使用 -t 选项使读取命令超时。如果未在 TIMEOUT 秒内读取完整的输入行,则会导致读取超时并返回失败。例如,如果在 10 秒内未提供任何输入,则程序将中止: 35 | 36 | ```sh 37 | read -t 10 -p "Enter the Internet domain name (e.g. nixcraft.com) : " domain_name 38 | whois $domain_name 39 | ``` 40 | 41 | -s 选项导致来自终端的输入不会显示在屏幕上。这对于密码处理很有用: 42 | 43 | ```sh 44 | #!/bin/bash 45 | read -s -p "Enter Password : " my_password 46 | echo 47 | echo "Your password - $my_password" 48 | ``` 49 | 50 | # echo & printf 51 | 52 | 在 Shell 中,echo 与 printf 都可以用来输出内容: 53 | 54 | ```sh 55 | $ echo $varName # not advisable unless you know what the variable contains 56 | $ echo "$varName" 57 | $ printf "%s\n" "$varName" 58 | ``` 59 | 60 | 使用 echo 命令显示一行文本或一个变量值。它不提供格式化选项。当您知道变量的内容不会引起问题时,这是显示简单输出的好命令。对于大多数用途,首选 printf。 61 | 62 | ```sh 63 | #!/bin/bash 64 | # Display welcome message, computer name and date 65 | echo "* Backup Shell Script *" 66 | echo 67 | echo "* Run time: $(date) @ $(hostname)" 68 | echo 69 | 70 | # Define variables 71 | BACKUP="/nas05" 72 | NOW=$(date +"%d-%m-%Y") 73 | 74 | # Let us start backup 75 | echo "* Dumping MySQL Database to $BACKUP/$NOW..." 76 | 77 | # Just sleep for 3 secs 78 | sleep 3 79 | 80 | # And we are done... 81 | echo 82 | echo "* Backup wrote to $BACKUP/$NOW/latest.tar.gz" 83 | ``` 84 | 85 | 您也可以使用通配符和 echo 命令打印文件名: 86 | 87 | ```sh 88 | cd /etc 89 | echo *.conf 90 | 91 | aatv.conf adduser.conf apg.conf argus.conf atool.conf brltty.conf ca-certificates.conf 92 | chkrootkit.conf cowpoke.conf cvs-cron.conf cvs-pserver.conf dconf.conf dconf-custom.conf 93 | debconf.conf deluser.conf 94 | .... 95 | ... 96 | .. 97 | wodim.conf wpa_supplicant.conf wvdial.conf xorg.conf 98 | ``` 99 | 100 | printf 命令格式并在屏幕上显示数据。但是,printf 不提供新行。您需要使用%指令提供格式字符串,并使用转义符以与 C printf() 函数最相似的方式来格式化数字和字符串参数。使用 printf 生成格式化的输出。 101 | 102 | ```s 103 | FORMAT controls the output as in C printf. Interpreted sequences are: 104 | 105 | \" double quote 106 | 107 | \NNN character with octal value NNN (1 to 3 digits) 108 | 109 | \\ backslash 110 | 111 | \a alert (BEL) 112 | 113 | \b backspace 114 | 115 | \c produce no further output 116 | 117 | \f form feed 118 | 119 | \n new line 120 | 121 | \r carriage return 122 | 123 | \t horizontal tab 124 | 125 | \v vertical tab 126 | 127 | \xHH byte with hexadecimal value HH (1 to 2 digits) 128 | 129 | \uHHHH Unicode (ISO/IEC 10646) character with hex value HHHH (4 digits) 130 | 131 | \UHHHHHHHH 132 | Unicode character with hex value HHHHHHHH (8 digits) 133 | 134 | %% a single % 135 | 136 | %b ARGUMENT as a string with '\' escapes interpreted, except that octal escapes are of the form 137 | \0 or \0NNN and all C format specifications ending with one of diouxXfeEgGcs, 138 | with ARGUMENTs converted to proper type first. Variable widths are handled. 139 | ``` 140 | 141 | printf 更详细的示例如下: 142 | 143 | ```sh 144 | vech="Car" 145 | printf "%s\n" $vech 146 | printf "%1s\n" $vech 147 | printf "%1.1s\n" $vech 148 | printf "%1.2s\n" $vech 149 | printf "%1.3s\n" $vech 150 | printf "%10.3s\n" $vech 151 | printf "%10.1s\n" $vech 152 | no=10 153 | printf "%d\n" $no 154 | big=5355765 155 | printf "%e\n" $big 156 | printf "%5.2e\n" $big 157 | sales=54245.22 158 | printf "%f\n" $sales 159 | printf "%.2f\n" $sales 160 | ``` 161 | -------------------------------------------------------------------------------- /10~Shell 命令/Shell 编程/函数/2024~Shell 函数调用详解.md: -------------------------------------------------------------------------------- 1 | # Shell 函数调用详解 2 | 3 | Shell 函数是一组可重复使用的命令集合,本文详细介绍 Shell 函数的定义、调用方式以及参数传递等内容。 4 | 5 | ## 函数定义与基本调用 6 | 7 | ### 基本语法 8 | 9 | Shell 函数有两种定义方式: 10 | 11 | ```sh 12 | # 方式1: 使用 function 关键字 13 | function function_name() { 14 | commands 15 | } 16 | 17 | # 方式2: 直接使用函数名 18 | function_name() { 19 | commands 20 | } 21 | ``` 22 | 23 | ### 函数参数处理 24 | 25 | 函数可以接收参数,在函数内部通过特殊变量访问: 26 | 27 | ```sh 28 | function example() { 29 | echo "第一个参数: $1" 30 | echo "第二个参数: $2" 31 | echo "参数个数: $#" 32 | echo "所有参数: $@" 33 | echo "所有参数: $*" # 与 $@ 类似,但处理方式略有不同 34 | 35 | # 使用 shift 移动参数 36 | shift 37 | echo "移动后的第一个参数: $1" 38 | } 39 | ``` 40 | 41 | ### 函数返回值 42 | 43 | Shell 函数可以通过两种方式返回值: 44 | 45 | ```sh 46 | # 方式1: return 语句(仅支持 0-255 的整数) 47 | function check_status() { 48 | [[ $1 -eq 0 ]] && return 0 || return 1 49 | } 50 | 51 | # 方式2: echo 输出(可返回任意字符串) 52 | function get_name() { 53 | echo "John Doe" 54 | } 55 | name=$(get_name) # 通过命令替换获取返回值 56 | ``` 57 | 58 | ## 全局函数库 59 | 60 | 您可以将常用函数存储在函数库文件中以便复用。 61 | 62 | ### 创建函数库 63 | 64 | 创建一个名为 `functions.sh` 的函数库文件: 65 | 66 | ```sh 67 | #!/bin/bash 68 | # functions.sh - 通用函数库 69 | 70 | # 常量定义 71 | declare -r TRUE=0 72 | declare -r FALSE=1 73 | 74 | # 字符串转小写函数 75 | function to_lower() { 76 | local str="$@" 77 | echo "${str,,}" 78 | } 79 | 80 | # 检查root用户 81 | function is_root() { 82 | [ $(id -u) -eq 0 ] && return $TRUE || return $FALSE 83 | } 84 | 85 | # 错误处理函数 86 | function die() { 87 | local message="$1" 88 | local code=${2:-1} 89 | echo "错误: $message" >&2 90 | exit "$code" 91 | } 92 | ``` 93 | 94 | ### 加载函数库 95 | 96 | 有两种方式加载函数库: 97 | 98 | ```sh 99 | # 方式1: 使用 . 命令 100 | . /path/to/functions.sh 101 | 102 | # 方式2: 使用 source 命令 103 | source /path/to/functions.sh [参数] 104 | ``` 105 | 106 | ### 使用示例 107 | 108 | ```sh 109 | #!/bin/bash 110 | # 加载函数库 111 | source ./functions.sh 112 | 113 | # 定义局部变量 114 | text="Hello World" 115 | 116 | # 调用函数库中的函数 117 | if is_root; then 118 | echo "当前用户是root" 119 | else 120 | echo "当前用户不是root" 121 | fi 122 | 123 | # 转换字符串为小写 124 | echo "原始文本: $text" 125 | echo "转换结果: $(to_lower "$text")" 126 | 127 | # 错误处理示例 128 | [ -f "config.txt" ] || die "配置文件不存在" 2 129 | ``` 130 | 131 | ## 递归函数 132 | 133 | Shell 支持函数递归调用。下面是计算阶乘的示例: 134 | 135 | ```sh 136 | function factorial() { 137 | local num=$1 138 | 139 | # 基本情况 140 | if [ $num -le 1 ]; then 141 | echo 1 142 | return 143 | fi 144 | 145 | # 递归调用 146 | local sub_result=$(factorial $(( num - 1 ))) 147 | echo $(( num * sub_result )) 148 | } 149 | 150 | # 使用示例 151 | result=$(factorial 5) 152 | echo "5的阶乘是: $result" 153 | ``` 154 | 155 | ## 后台函数调用 156 | 157 | 函数可以在后台执行,适用于需要并行处理的场景: 158 | 159 | ```sh 160 | # 定义进度显示函数 161 | function show_progress() { 162 | while true; do 163 | echo -n "." 164 | sleep 1 165 | done 166 | } 167 | 168 | # 定义主处理函数 169 | function main_process() { 170 | sleep 10 # 模拟耗时操作 171 | } 172 | 173 | # 启动进度显示(后台运行) 174 | show_progress & 175 | progress_pid=$! 176 | 177 | # 执行主要处理 178 | main_process 179 | 180 | # 完成后终止进度显示 181 | kill $progress_pid 182 | echo "完成!" 183 | ``` 184 | 185 | ## 最佳实践 186 | 187 | 1. **局部变量**:总是使用 local 声明函数内的变量 188 | 2. **参数验证**:检查必要的参数是否存在 189 | 3. **返回值**:合理使用返回值表示函数执行状态 190 | 4. **错误处理**:实现适当的错误处理机制 191 | 5. **文档注释**:为函数添加清晰的注释 192 | 193 | ```sh 194 | function process_file() { 195 | # 函数文档 196 | local usage="Usage: process_file [type]" 197 | 198 | # 参数验证 199 | [ $# -lt 1 ] && { echo "$usage"; return 1; } 200 | 201 | # 声明局部变量 202 | local file="$1" 203 | local type="${2:-default}" 204 | local status=0 205 | 206 | # 错误处理 207 | [ ! -f "$file" ] && { echo "Error: File not found"; return 1; } 208 | 209 | # 处理逻辑 210 | case "$type" in 211 | text) 212 | process_text "$file" 213 | status=$? 214 | ;; 215 | binary) 216 | process_binary "$file" 217 | status=$? 218 | ;; 219 | *) 220 | echo "Unknown type: $type" 221 | status=1 222 | ;; 223 | esac 224 | 225 | return $status 226 | } 227 | ``` 228 | 229 | 通过合理使用这些功能和最佳实践,可以编写出更加健壮和可维护的 Shell 脚本。 230 | -------------------------------------------------------------------------------- /10~Shell 命令/Shell 编程/语法基础/字符串.md: -------------------------------------------------------------------------------- 1 | # 字符串 2 | 3 | 字符串是 shell 编程中最常用最有用的数据类型,单引号里的任何字符都会原样输出,单引号字符串中的变量是无效的;双引号中则允许引入变量: 4 | 5 | ```sh 6 | # 使用双引号拼接 7 | greeting="hello, "$your_name" !" 8 | greeting_1="hello, ${your_name} !" 9 | 10 | # 使用单引号拼接 11 | greeting_2='hello, '$your_name' !' 12 | greeting_3='hello, ${your_name} !' # hello, ${your_name} ! 13 | ``` 14 | 15 | 字符串常见的操作如下: 16 | 17 | ```sh 18 | string="string" 19 | 20 | # 获取字符串长度 21 | echo ${#string} 22 | 23 | # 提取子字符串 24 | ${string:1:4} 25 | 26 | # 查找子字符串 27 | echo `expr index "$string" io` 28 | ``` 29 | 30 | # 引号 31 | 32 | 您的 Bash Shell 可以理解具有特殊含义的特殊字符。例如,`$var` 用于扩展变量值。Bash 扩展变量和通配符,例如: 33 | 34 | ```sh 35 | echo "$PATH" 36 | echo "$PS1" 37 | echo /etc/*.conf 38 | ``` 39 | 40 | 但是,有时您不希望使用变量或通配符。例如,不要打印 `$PATH` 的值,而只是在屏幕上将 `$PATH` 作为单词打印。您可以通过将特殊字符的含义用单引号引起来来启用或禁用。这对于在编写 Shell 脚本时抑制警告和错误消息也很有用。 41 | 42 | ```sh 43 | echo "Path is $PATH" ## $PATH will be expanded 44 | echo 'I want to print $PATH' ## PATH will not be expanded 45 | ``` 46 | 47 | 引号分为三种: 48 | 49 | | Quote type | Name | Meaning | Example (type at shell prompt) | 50 | | ---------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 51 | | " | The double quote | The double quote ( "quote" ) protects everything enclosed between two double quote marks except \$, ', " and \.Use the double quotes when you want only variables and command substitution. _ Variable - Yes _ Wildcards - No \* Command substitution - yes | The double quotes allowes to print the value of \$SHELL variable, disables the meaning of [wildcards](https://bash.cyberciti.biz/guide/Wildcards), and finally allows command substitution. `echo "$SHELL"echo "/etc/*.conf"echo "Today is $(date)"` | 52 | | ' | The single quote | The single quote ( 'quote' ) protects everything enclosed between two single quote marks. It is used to turn off the special meaning of all characters. _ Variable - No _ Wildcards - No \* Command substitution - No | The single quotes prevents displaying variable $SHELL value, disabled the meaning of [wildcards](https://bash.cyberciti.biz/guide/Wildcards) /etc/*.conf, and finally command substitution ($date) itself. `echo '$SHELL'echo '/etc/*.conf'echo 'Today is $(date)'` | 53 | | \ | The Backslash | Use backslash to change the special meaning of the characters or to escape special characters within the text such as quotation marks. | You can use \ before dollar sign to tell the shell to have no special meaning. Disable the meaning of the next character in $PATH (i.e. do not display value of $PATH variable): `echo "Path is \$PATH"echo "Path is $PATH"` | 54 | 55 | `\` 可以用来进行换行衔接: 56 | 57 | ```sh 58 | echo "A monkey-tailed boy named Goku is found by an old martial \ 59 | >arts expert who raises him as his grandson. One day Goku meets a \ 60 | >girl named Bulma and together they go on a quest to retrieve the seven Dragon Balls" 61 | 62 | # Purpose: clean /tmp/$domain ? 63 | check_temp_clean() { 64 | [ "$SERVER_MODE" = "daemon" ] || return 1 65 | [ "$SERVER_MODE" = "init" ] && return 0 66 | # note use of the backslash character to continue command on next line 67 | [ "$SERVER_MODE" = "clean" \ 68 | -a -e /usr/local/etc/nixcraft/lighttpd/disk_cache.init ] && return 0 69 | return 1 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /10~Shell 命令/命令执行/重定向.md: -------------------------------------------------------------------------------- 1 | # 重定向 2 | 3 | 几乎所有命令都会将输出产生到屏幕上或从键盘上获取输入,但是在 Linux 中,可以将输出发送到文件或从文件读取输入。每个 shell 命令都有其自己的输入和输出。在执行命令之前,可以使用由 Shell 解释的特殊符号来重定向其输入和输出。例如,将 date 命令的输出发送到文件而不是发送到屏幕。更改输入或输出的默认路径称为重定向。 4 | 5 | 在 Linux 中,所有内容都是文件。以上三个数字是标准 POSIX 编号,也称为文件描述符(FD)。每个 Linux 命令至少都打开上述流以与用户或其他系统程序对话。 6 | 7 | | Standard File | File Descriptor Number | Meaning | Example (type at shell prompt) | 8 | | ------------- | ---------------------- | ---------------------------------------------------------- | ------------------------------------------------- | 9 | | stdin | 0 | Read input from a file (the default is keyboard) | `cat < filename` | 10 | | stdout | 1 | Send data to a file (the default is screen). | `date > output.txtcat output.txt` | 11 | | stderr | 2 | Send all error messages to a file (the default is screen). | `rm /tmp/4815162342.txt 2>error.txtcat error.txt` | 12 | 13 | ## 标准输入 14 | 15 | 标准输入是默认输入法,所有命令都使用它来读取其输入。用零数字(0)表示。也称为 stdin。默认的标准输入是键盘。< 是输入重定向符号,语法为: 16 | 17 | ```sh 18 | $ command < filename 19 | 20 | $ cat < /etc/passwd 21 | ``` 22 | 23 | ![标准输入](https://s1.ax1x.com/2020/06/16/NFHI56.png) 24 | 25 | ## 标准输出 26 | 27 | 命令使用标准输出来写入(显示)其输出。默认为屏幕,用一个数字(1)表示。也称为标准输出。默认的标准输出是屏幕。> 是输出重定向符号,语法为: 28 | 29 | ```sh 30 | $ command > output.file.name 31 | 32 | $ ls > /tmp/output.txt 33 | ``` 34 | 35 | ![标准输出](https://s1.ax1x.com/2020/06/16/NFqG6g.png) 36 | 37 | 要简单地重定向输出,请使用以下语法: 38 | 39 | ``` 40 | command > /path/to/file 41 | /path/to/script.sh > output.txt 42 | ``` 43 | 44 | 例如,将 date 命令的输出发送到名为 now.txt 的文件: 45 | 46 | ``` 47 | date > now.txt 48 | ``` 49 | 50 | You can also use the > operator to print file, enter: 51 | 52 | ``` 53 | cat file.txt > /dev/lp0 54 | ``` 55 | 56 | OR 57 | 58 | ``` 59 | sudo bash -c "cat file.txt > /dev/lp0" 60 | ``` 61 | 62 | To make a usage listing of the directories in the /home partition, enter: 63 | 64 | ``` 65 | sudo bash -c "cd /home ; du -s *│ sort -rn >/tmp/usage" 66 | ``` 67 | 68 | You can also use the following syntax: 69 | 70 | ``` 71 | echo "Today is $(date)" 1>/tmp/now.txt 72 | ``` 73 | 74 | You can append the output to the same file using >> operator, enter: 75 | 76 | ``` 77 | date >> now.txt 78 | cat now.txt 79 | ``` 80 | 81 | You can also use the following syntax: 82 | 83 | ``` 84 | echo "Today is $(date)" 1>>/tmp/now.txt 85 | ``` 86 | 87 | ## 标准异常 88 | 89 | 标准错误是默认错误输出设备,用于写入所有系统错误消息。用两个数字(2)表示,也称为 stderr。默认的标准错误设备是屏幕或监视器,2> 是输入重定向符号,语法为: 90 | 91 | ```s 92 | $ command 2> errors.txt 93 | ``` 94 | 95 | 例如,将查找命令错误发送到名为 fileerrors.txt 的文件,以便以后可以查看错误,输入: 96 | 97 | ```sh 98 | find / -iname "*.conf" 2>fileerrors.txt 99 | cat fileerrors.txt 100 | ``` 101 | 102 | ![Error](https://s1.ax1x.com/2020/06/16/Nkj2X8.png) 103 | 104 | 要将标准错误重定向到名为 error.log 的文件中,请输入: 105 | 106 | ``` 107 | command-name 2>error.log 108 | ``` 109 | 110 | 在 /home 目录中找到所有 .profile 文件,并将错误记录到 /tmp/error 文件中,输入: 111 | 112 | ``` 113 | find /home -name .profile 2>/tmp/error 114 | ``` 115 | 116 | 样本输出: 117 | 118 | ```s 119 | /home/t2/.profile 120 | /home/vivek/ttt/skel/.profile 121 | ``` 122 | 123 | 要查看错误,请输入: 124 | 125 | ```s 126 | more /tmp/error 127 | ``` 128 | 129 | 样本输出: 130 | 131 | ```s 132 | find: `/home/vivek/.cpan/build/Acme-POE-Tree-1.01-qqmq77': Permission denied 133 | find: `/home/vivek/.cpan/build/Lchown-1.00-uOM4tb': Permission denied 134 | find: `/home/vivek/.cpan/build/IO-Tty-1.07-F9rDy3': Permission denied 135 | find: `/home/vivek/.cpan/build/POE-Test-Loops-1.002-9AjIro': Permission denied 136 | find: `/home/vivek/.cpan/build/POE-1.003-KwXVB1': Permission denied 137 | find: `/home/vivek/.cpan/build/Curses-1.27-ZLo169': Permission denied 138 | ``` 139 | 140 | 您可以将脚本错误重定向到名为 scripts.err 的日志文件: 141 | 142 | ```sh 143 | ./script.sh 2>scripts.err 144 | /path/to/example.pl 2>scripts.err 145 | ``` 146 | 147 | 您可以使用 >> 运算符将标准错误附加到 error.log 文件的末尾: 148 | 149 | ```sh 150 | command-name 2>>error.log 151 | ./script.sh 2>>error.log 152 | /path/to/example.pl 2>>error.log 153 | ``` 154 | 155 | 您可以使用以下语法将 stdout 和 stderr 都重定向到文件: 156 | 157 | ```sh 158 | command-name &>filename 159 | command-name >cmd.log 2>&1 160 | command-name >/dev/null 2>&1 161 | ``` 162 | 163 | 此语法通常用于 cron 作业: 164 | 165 | ```sh 166 | @hourly /scripts/backup/nas.backup >/dev/null 2>&1 167 | @hourly /scripts/backup/nas.backup &>/dev/null 168 | ``` 169 | -------------------------------------------------------------------------------- /01~进程与处理器/进程与线程/进程状态.md: -------------------------------------------------------------------------------- 1 | # 进程状态 2 | 3 | Linux 的进程状态包含了 R (TASK_RUNNING),可执行状态、S (TASK_INTERRUPTIBLE),可中断的睡眠状态、D (TASK_UNINTERRUPTIBLE),不可中断的睡眠状态、T (TASK_STOPPED or TASK_TRACED),暂停状态或跟踪状态、Z (TASK_DEAD – EXIT_ZOMBIE),退出状态,进程成为僵尸进程、X (TASK_DEAD – EXIT_DEAD),退出状态,进程即将被销毁。 4 | 5 | ![Linux Processes Life Cycle](https://s1.ax1x.com/2020/06/17/NAT5es.md.png) 6 | 7 | # 进程状态类别 8 | 9 | ## R (TASK_RUNNING),可执行状态 10 | 11 | 只有在该状态的进程才可能在 CPU 上运行。而同一时刻可能有多个进程处于可执行状态,这些进程的 task_struct 结构(进程控制块)被放入对应 CPU 的可执行队列中(一个进程最多只能出现在一个 CPU 的可执行队列中)。进程调度器的任务就是从各个 CPU 的可执行队列中分别选择一个进程在该 CPU 上运行。 12 | 13 | ## S (TASK_INTERRUPTIBLE),可中断的睡眠状态 14 | 15 | 处于这个状态的进程因为等待某某事件的发生(比如等待 socket 连接、等待信号量),而被挂起。这些进程的 task_struct 结构被放入对应事件的等待队列中。当这些事件发生时(由外部中断触发、或由其他进程触发),对应的等待队列中的一个或多个进程将被唤醒。通过 ps 命令我们会看到,一般情况下,进程列表中的绝大多数进程都处于 TASK_INTERRUPTIBLE 状态(除非机器的负载很高)。 16 | 17 | ## D (TASK_UNINTERRUPTIBLE),不可中断的睡眠状态 18 | 19 | 与 TASK_INTERRUPTIBLE 状态类似,进程处于睡眠状态,但是此刻进程是不可中断的。不可中断,指的并不是 CPU 不响应外部硬件的中断,而是指进程不响应异步信号。 20 | 21 | TASK_UNINTERRUPTIBLE 状态存在的意义就在于,内核的某些处理流程是不能被打断的。如果响应异步信号,程序的执行流程中就会被插入一段用于处理异步信号的流程(这个插入的流程可能只存在于内核态,也可能延伸到用户态)在进程对某些硬件进行操作时(比如进程调用 read 系统调用对某个设备文件进行读操作,而 read 系统调用最终执行到对应设备驱动的代码,并与对应的物理设备进行交互),可能需要使用 TASK_UNINTERRUPTIBLE 状态对进程进行保护,以避免进程与设备交互的过程被打断,造成设备陷入不可控的状态。这种情况下的 TASK_UNINTERRUPTIBLE 状态总是非常短暂的,通过 ps 命令基本上不可能捕捉到。 22 | 23 | ## T (TASK_STOPPED or TASK_TRACED),暂停状态或跟踪状态 24 | 25 | 向进程发送一个 SIGSTOP 信号,它就会因响应该信号而进入 TASK_STOPPED 状态(除非该进程本身处于 TASK_UNINTERRUPTIBLE 状态而不响应信号)。SIGSTOP 与 SIGKILL 信号一样,是非常强制的。不允许用户进程通过 signal 系列的系统调用重新设置对应的信号处理函数。向进程发送一个 SIGCONT 信号,可以让其从 TASK_STOPPED 状态恢复到 TASK_RUNNING 状态。 26 | 27 | 当进程正在被跟踪时,它处于 TASK_TRACED 这个特殊的状态。“正在被跟踪”指的是进程暂停下来,等待跟踪它的进程对它进行操作。比如在 gdb 中对被跟踪的进程下一个断点,进程在断点处停下来的时候就处于 TASK_TRACED 状态。而在其他时候,被跟踪的进程还是处于前面提到的那些状态。对于进程本身来说,TASK_STOPPED 和 TASK_TRACED 状态很类似,都是表示进程暂停下来。 28 | 29 | ## Z (TASK_DEAD – EXIT_ZOMBIE),退出状态,进程成为僵尸进程 30 | 31 | 进程在退出的过程中,处于 TASK_DEAD 状态。在这个退出过程中,进程占有的所有资源将被回收,除了 task_struct 结构(以及少数资源)以外。于是进程就只剩下 task_struct 这么个空壳,故称为僵尸。之所以保留 task_struct,是因为 task_struct 里面保存了进程的退出码、以及一些统计信息。而其父进程很可能会关心这些信息。比如在 shell 中,`$?` 变量就保存了最后一个退出的前台进程的退出码,而这个退出码往往被作为 if 语句的判断条件。 32 | 33 | 当然,内核也可以将这些信息保存在别的地方,而将 task_struct 结构释放掉,以节省一些空间。但是使用 task_struct 结构更为方便,因为在内核中已经建立了从 pid 到 task_struct 查找关系,还有进程间的父子关系。释放掉 task_struct,则需要建立一些新的数据结构,以便让父进程找到它的子进程的退出信息。 34 | 35 | 父进程可以通过 wait 系列的系统调用(如 wait4、waitid)来等待某个或某些子进程的退出,并获取它的退出信息。然后 wait 系列的系统调用会顺便将子进程的尸体(task_struct)也释放掉。子进程在退出的过程中,内核会给其父进程发送一个信号,通知父进程来“收尸”。这个信号默认是 SIGCHLD,但是在通过 clone 系统调用创建子进程时,可以设置这个信号。 36 | 37 | 当进程退出的时候,会将它的所有子进程都托管给别的进程(使之成为别的进程的子进程)。托管给谁呢?可能是退出进程所在进程组的下一个进程(如果存在的话),或者是 1 号进程。所以每个进程、每时每刻都有父进程存在。除非它是 1 号进程。1 号进程,pid 为 1 的进程,又称 init 进程。Linux 系统启动后,第一个被创建的用户态进程就是 init 进程。它有两项使命: 38 | 39 | - 执行系统初始化脚本,创建一系列的进程(它们都是 init 进程的子孙); 40 | - 在一个死循环中等待其子进程的退出事件,并调用 waitid 系统调用来完成“收尸”工作; 41 | 42 | init 进程不会被暂停、也不会被杀死(这是由内核来保证的)。它在等待子进程退出的过程中处于 TASK_INTERRUPTIBLE 状态,“收尸”过程中则处于 TASK_RUNNING 状态。 43 | 44 | ## X (TASK_DEAD – EXIT_DEAD),退出状态,进程即将被销毁 45 | 46 | 而进程在退出过程中也可能不会保留它的 task_struct。比如这个进程是多线程程序中被 detach 过的进程。或者父进程通过设置 SIGCHLD 信号的 handler 为 SIG_IGN,显式的忽略了 SIGCHLD 信号。(这是 posix 的规定,尽管子进程的退出信号可以被设置为 SIGCHLD 以外的其他信号。) 47 | 此时,进程将被置于 EXIT_DEAD 退出状态,这意味着接下来的代码立即就会将该进程彻底释放。所以 EXIT_DEAD 状态是非常短暂的,几乎不可能通过 ps 命令捕捉到。 48 | 49 | # 进程状态变迁 50 | 51 | ## 初始状态 52 | 53 | 进程是通过 fork 系列的系统调用(fork、clone、vfork)来创建的,内核(或内核模块)也可以通过 kernel_thread 函数创建内核进程。这些创建子进程的函数本质上都完成了相同的功能——将调用进程复制一份,得到子进程。(可以通过选项参数来决定各种资源是共享、还是私有。) 54 | 55 | 那么既然调用进程处于 TASK_RUNNING 状态(否则,它若不是正在运行,又怎么进行调用?),则子进程默认也处于 TASK_RUNNING 状态。另外,在系统调用调用 clone 和内核函数 kernel_thread 也接受 CLONE_STOPPED 选项,从而将子进程的初始状态置为 TASK_STOPPED。 56 | 57 | ## 调度状态 58 | 59 | 进程自创建以后,状态可能发生一系列的变化,直到进程退出。而尽管进程状态有好几种,但是进程状态的变迁却只有两个方向——从 TASK_RUNNING 状态变为非 TASK_RUNNING 状态、或者从非 TASK_RUNNING 状态变为 TASK_RUNNING 状态。也就是说,如果给一个 TASK_INTERRUPTIBLE 状态的进程发送 SIGKILL 信号,这个进程将先被唤醒(进入 TASK_RUNNING 状态),然后再响应 SIGKILL 信号而退出(变为 TASK_DEAD 状态)。并不会从 TASK_INTERRUPTIBLE 状态直接退出。进程从非 TASK_RUNNING 状态变为 TASK_RUNNING 状态,是由别的进程(也可能是中断处理程序)执行唤醒操作来实现的。执行唤醒的进程设置被唤醒进程的状态为 TASK_RUNNING,然后将其 task_struct 结构加入到某个 CPU 的可执行队列中。于是被唤醒的进程将有机会被调度执行。 60 | 61 | 而进程从 TASK_RUNNING 状态变为非 TASK_RUNNING 状态,则有两种途径: 62 | 63 | - 响应信号而进入 TASK_STOPED 状态、或 TASK_DEAD 状态; 64 | - 执行系统调用主动进入 TASK_INTERRUPTIBLE 状态(如 nanosleep 系统调用)、或 TASK_DEAD 状态(如 exit 系统调用);或由于执行系统调用需要的资源得不到满足,而进入 TASK_INTERRUPTIBLE 状态或 TASK_UNINTERRUPTIBLE 状态(如 select 系统调用)。 65 | 66 | 显然,这两种情况都只能发生在进程正在 CPU 上执行的情况下。 67 | 68 | # 僵尸进程 69 | 70 | 对于正常的使用情况,子进程的创建一般需要父进程通过系统调用 wait() 或者 waitpid() 来等待子进程结束,从而回收子进程的资源。除了这种方式外,还可以通过异步的方式来进行回收,这种方式的基础是子进程结束之后会向父进程发送 SIGCHLD 信号,基于此父进程注册一个 SIGCHLD 信号的处理函数来进行子进程的资源回收就可以了。 71 | 72 | 僵尸进程的最大危害是对资源的一种永久性占用,比如进程号,系统会有一个最大的进程数 n 的限制,也就意味一旦 1 到 n 进程号都被占用,系统将不能创建任何进程和线程(进程和线程对于 OS 而言,使用同一种数据结构来表示,task_struct)。这个时候对于用户的一个直观感受就是 shell 无法执行任何命令,这个原因是 shell 执行命令的本质是 fork。 73 | 74 | ## 孤儿进程 75 | 76 | 如果子进程先于父进程退出,并且父进程没有对子进程残留的资源进行回收的话将会产生僵尸进程。这里引申另外一种情况,父进程先于子进程退出的话,那么子进程的资源谁来回收呢? 77 | 78 | 父进程先于子进程退出,这个时候我们一般将还在运行的子进程称为孤儿进程,但是实际上孤儿进程并没有一个明确的定义,他的状态还是处于上面讨论的几种进程状态中。那么孤儿进程的资源谁来回收呢?类 Unix 系统针对这种情况会将这些孤儿进程的父进程置为 1 号进程也就是 systemd 进程,然后由 systemd 来对孤儿进程的资源进行回收。 79 | -------------------------------------------------------------------------------- /02~存储/01~内存管理/页式存储管理/请页式管理/Page Fault.md: -------------------------------------------------------------------------------- 1 | # Page Fault 2 | 3 | 上文中提及,当我们向操作系统申请内存时,操作系统并不是直接分配给我们物理内存,而是只标记当前进程拥有该段内存,当真正使用这段段内存时才会分配。这种延迟分配物理内存的方式就通过 page fault 机制来实现的。当我们访问一个内存地址时,如果该地址非法,或者我们对其没有访问权限,或者该地址对应的物理内存还未分配,cpu 都会生成一个 page fault,进而执行操作系统的 page fault handler。 4 | 5 | 这个 page fault handler 里会检查该 fault 产生的原因,如果是地址非法或没有权限,则会向当前进程发送一个 SIGSEGV signal,该 signal 默认会 kill 掉当前进程,并提示我们 segmentation fault 异常。如果是因为还未分配物理内存,操作系统会立即分配物理内存给当前进程,然后重试产生这个 page fault 的内存访问指令,一般情况下都可以正常向下执行。 6 | 7 | 下面我们来看下对应的内核源码: 8 | 9 | ```c 10 | // arch/x86/mm/fault.c 11 | dotraplinkage void notrace 12 | do_page_fault(struct pt_regs *regs, unsigned long error_code) 13 | { 14 | unsigned long address = read_cr2(); /* Get the faulting address */ 15 | ... 16 | __do_page_fault(regs, error_code, address); 17 | ... 18 | } 19 | NOKPROBE_SYMBOL(do_page_fault); 20 | ``` 21 | 22 | 该方法先从 cr2 寄存器中读出产生这个 page fault 的虚拟内存地址,然后再调用 `__do_page_fault` 方法。 23 | 24 | ```c 25 | // arch/x86/mm/fault.c 26 | static noinline void 27 | __do_page_fault(struct pt_regs *regs, unsigned long hw_error_code, 28 | unsigned long address) 29 | { 30 | ... 31 | /* Was the fault on kernel-controlled part of the address space? */ 32 | if (unlikely(fault_in_kernel_space(address))) 33 | do_kern_addr_fault(regs, hw_error_code, address); 34 | else 35 | do_user_addr_fault(regs, hw_error_code, address); 36 | } 37 | NOKPROBE_SYMBOL(__do_page_fault); 38 | ``` 39 | 40 | 该方法会检查该地址是属于 kernel space 还是 user space,如果是 user space,则会调用 do_user_addr_fault 方法。继续 do_user_addr_fault 方法: 41 | 42 | ```c 43 | // arch/x86/mm/fault.c 44 | static inline 45 | void do_user_addr_fault(struct pt_regs *regs, 46 | unsigned long hw_error_code, 47 | unsigned long address) 48 | { 49 | struct vm_area_struct *vma; 50 | struct task_struct *tsk; 51 | struct mm_struct *mm; 52 | ... 53 | tsk = current; 54 | mm = tsk->mm; 55 | ... 56 | vma = find_vma(mm, address); 57 | if (unlikely(!vma)) { 58 | bad_area(regs, hw_error_code, address); 59 | return; 60 | } 61 | if (likely(vma->vm_start <= address)) 62 | goto good_area; 63 | ... 64 | good_area: 65 | ... 66 | fault = handle_mm_fault(vma, address, flags); 67 | ... 68 | } 69 | NOKPROBE_SYMBOL(do_user_addr_fault); 70 | ``` 71 | 72 | 该方法会先从 mm 中找包含 address 的内存段,如果没有,则说明我们访问了一个非法地址,该方法进而会调用 bad_area 方法,向当前进程发送一个 SIGSEGV signal。如果找到了对应的内存段,则会调用 handle_mm_fault 方法继续处理。 73 | 74 | ```c 75 | // mm/memory.c 76 | vm_fault_t handle_mm_fault(struct vm_area_struct *vma, unsigned long address, 77 | unsigned int flags) 78 | { 79 | vm_fault_t ret; 80 | ... 81 | if (unlikely(is_vm_hugetlb_page(vma))) 82 | ... 83 | else 84 | ret = __handle_mm_fault(vma, address, flags); 85 | ... 86 | return ret; 87 | } 88 | EXPORT_SYMBOL_GPL(handle_mm_fault); 89 | ``` 90 | 91 | 该方法又调用了 `__handle_mm_fault` 方法: 92 | 93 | ```c 94 | // mm/memory.c 95 | static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma, 96 | unsigned long address, unsigned int flags) 97 | { 98 | struct vm_fault vmf = { 99 | .vma = vma, 100 | .address = address & PAGE_MASK, 101 | ... 102 | }; 103 | ... 104 | struct mm_struct *mm = vma->vm_mm; 105 | pgd_t *pgd; 106 | p4d_t *p4d; 107 | vm_fault_t ret; 108 | 109 | pgd = pgd_offset(mm, address); 110 | p4d = p4d_alloc(mm, pgd, address); 111 | ... 112 | vmf.pud = pud_alloc(mm, p4d, address); 113 | ... 114 | vmf.pmd = pmd_alloc(mm, vmf.pud, address); 115 | ... 116 | return handle_pte_fault(&vmf); 117 | } 118 | ``` 119 | 120 | 此时,vmf->pte 应该为 null。该方法通过 vma_is_anonymous 方法,判断 vmf->vma 对应的内存段是否是 anonymous 的,如果是,则调用 do_anonymous_page,如果不是,比如 mmap file 产生的 vma,则调用 do_fault。 121 | 122 | ```c 123 | // mm/memory.c 124 | static vm_fault_t do_anonymous_page(struct vm_fault *vmf) 125 | { 126 | struct vm_area_struct *vma = vmf->vma; 127 | ... 128 | struct page *page; 129 | ... 130 | pte_t entry; 131 | ... 132 | page = alloc_zeroed_user_highpage_movable(vma, vmf->address); 133 | ... 134 | entry = mk_pte(page, vma->vm_page_prot); 135 | ... 136 | set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry); 137 | ... 138 | return ret; 139 | ... 140 | } 141 | ``` 142 | 143 | 该方法先调用 alloc_zeroed_user_highpage_movable 分配一个新的 page,这个就是物理内存了。然后调用 mk_pte 方法,把 page 的地址信息等记录到 entry 里。最后,把这个 entry 写入到 vmf->pte 指向的内存中。这样在下次再访问这个 page 对应的虚拟内存地址时,page walk 就可以在 pte 中找到这个 page 了。 144 | -------------------------------------------------------------------------------- /10~Shell 命令/Shell 编程/函数/局部变量与返回.md: -------------------------------------------------------------------------------- 1 | # 局部变量与返回 2 | 3 | # 局部变量 4 | 5 | 默认情况下,所有变量都是全局变量。在函数中修改变量会在整个脚本中对其进行更改。这可能会导致问题。例如,创建一个名为 fvar.sh 的 shell 脚本: 6 | 7 | ```sh 8 | #!/bin/bash 9 | create_jail(){ 10 | d=$1 11 | echo "create_jail(): d is set to $d" 12 | } 13 | 14 | d=/apache.jail 15 | 16 | echo "Before calling create_jail d is set to $d" 17 | 18 | create_jail "/home/apache/jail" 19 | 20 | echo "After calling create_jail d is set to $d" 21 | 22 | # Sample outputs 23 | Before calling create_jail d is set to /apache.jail 24 | create_jail(): d is set to /home/apache/jail 25 | After calling create_jail d is set to /home/apache/jail 26 | ``` 27 | 28 | 您可以使用本地命令创建本地变量,语法为: 29 | 30 | ```sh 31 | local var=value 32 | local varName 33 | 34 | function name(){ 35 | local var=$1 36 | command1 on $var 37 | } 38 | ``` 39 | 40 | local 命令只能在功能内使用。它使变量名的可见范围仅限于该函数及其子函数。以下是上述脚本的更新版本: 41 | 42 | ```sh 43 | # global d variable 44 | d=/apache.jail 45 | 46 | # User defined function 47 | create_jail(){ 48 | # d is only visible to this fucntion 49 | local d=$1 50 | echo "create_jail(): d is set to $d" 51 | } 52 | 53 | echo "Before calling create_jail d is set to $d" 54 | 55 | create_jail "/home/apache/jail" 56 | 57 | echo "After calling create_jail d is set to $d" 58 | 59 | # Sample outputs 60 | Before calling create_jail d is set to /apache.jail 61 | create_jail(): d is set to /home/apache/jail 62 | After calling create_jail d is set to /apache.jail 63 | ``` 64 | 65 | 在以下示例中: 66 | 67 | - 声明命令用于创建称为 PASSWD_FILE 的常量变量。 68 | - 函数 die() 在所有其他函数之前定义。 69 | - 您可以从同一脚本或其他函数调用一个函数。例如,从 is_user_exist() 中调用 die()。 70 | - 所有函数变量都是局部的。这是一个好的编程习惯。 71 | 72 | ```sh 73 | #!/bin/bash 74 | # Make readonly variable i.e. constant variable 75 | declare -r PASSWD_FILE=/etc/passwd 76 | 77 | # 78 | # Purpose: Display message and die with given exit code 79 | # 80 | die(){ 81 | local message="$1" 82 | local exitCode=$2 83 | echo "$message" 84 | [ "$exitCode" == "" ] && exit 1 || exit $exitCode 85 | } 86 | 87 | # 88 | # Purpose: Find out if user exits or not 89 | # 90 | does_user_exist(){ 91 | local u=$1 92 | grep -qEw "^$u" $PASSWD_FILE && die "Username $u exists." 93 | } 94 | 95 | # 96 | # Purpose: Is script run by root? Else die.. 97 | # 98 | is_user_root(){ 99 | [ "$(id -u)" != "0" ] && die "You must be root to run this script" 2 100 | } 101 | 102 | # 103 | # Purpose: Display usage 104 | # 105 | usage(){ 106 | echo "Usage: $0 username" 107 | exit 2 108 | } 109 | 110 | 111 | [ $# -eq 0 ] && usage 112 | 113 | # invoke the function is_root_user 114 | is_user_root 115 | 116 | # call the function is_user_exist 117 | does_user_exist "$1" 118 | 119 | # display something on screen 120 | echo "Adding user $1 to database..." 121 | # just display command but do not add a user to system 122 | echo "/sbin/useradd -s /sbin/bash -m $1" 123 | ``` 124 | 125 | # 函数返回 126 | 127 | 在数学中,函数 ƒ 输入 x,然后返回输出 `f(x)`。在计算机中,shell 函数名称可以输入 \$1,然后将值(true 或 false)返回给脚本。换句话说,您可以从具有退出状态的函数中返回。return 命令使函数退出,返回值由 N 指定,语法为: 128 | 129 | ```s 130 | return N 131 | ``` 132 | 133 | 如果未指定 N,则返回状态为最后一条命令的状态。return 命令终止该功能。当返回值是最后执行的命令的返回值时,则不需要返回命令。 134 | 135 | ```sh 136 | #!/bin/bash 137 | # version 1.0 138 | 139 | # Purpose: Determine if current user is root or not 140 | is_root_user(){ 141 | [ $(id -u) -eq 0 ] 142 | } 143 | 144 | # invoke the function 145 | # make decision using conditional logical operators 146 | is_root_user && echo "You can run this script." || echo "You need to run this script as a root user." 147 | ``` 148 | 149 | 以下是同一脚本的更新版本。此版本使用称为 TRUE 和 FALSE 的声明命令创建常量变量。 150 | 151 | ```sh 152 | #!/bin/bash 153 | # version 2.0 154 | # define constants 155 | declare -r TRUE=0 156 | declare -r FALSE=1 157 | 158 | # Purpose: Determine if current user is root or not 159 | is_root_user(){ 160 | # root user has user id (UID) zero. 161 | [ $(id -u) -eq 0 ] && return $TRUE || return $FALSE 162 | } 163 | 164 | is_root_user && echo "You can run this script." || echo "You need to run this script as a root user." 165 | ``` 166 | 167 | 您不能从函数中返回单词或其他任何内容。但是,您可以使用 echo 或 printf 命令轻松将输出发送回脚本。 168 | 169 | ```sh 170 | #!/bin/bash 171 | # Variables 172 | domain="CyberCiti.BIz" 173 | out="" 174 | 175 | ################################################################## 176 | # Purpose: Converts a string to lower case 177 | # Arguments: 178 | # $@ -> String to convert to lower case 179 | ################################################################## 180 | function to_lower() 181 | { 182 | local str="$@" 183 | local output 184 | output=$(tr '[A-Z]' '[a-z]'<<<"${str}") 185 | echo $output 186 | } 187 | 188 | # invoke the to_lower() 189 | to_lower "This Is a TEST" 190 | 191 | # invoke to_lower() and store its result to $out variable 192 | out=$(to_lower ${domain}) 193 | 194 | # Display back the result from $out 195 | echo "Domain name : $out" 196 | ``` 197 | -------------------------------------------------------------------------------- /10~Shell 命令/命令执行/参数与返回.md: -------------------------------------------------------------------------------- 1 | # 脚本参数 2 | 3 | # 命令行参数 4 | 5 | 所有命令行参数(位置参数)都可以通过特殊的外壳变量 $1,$2,$3,...,$9 获得。 6 | 7 | ```sh 8 | #!/bin/bash 9 | echo "The script name : $0" 10 | echo "The value of the first argument to the script : $1" 11 | echo "The value of the second argument to the script : $2" 12 | echo "The value of the third argument to the script : $3" 13 | echo "The number of arguments passed to the script : $#" 14 | echo "The value of all command-line arguments (\$* version) : $*" 15 | echo "The value of all command-line arguments (\$@ version) : $@" 16 | 17 | # ./cmdargs.sh bmw ford toyota 18 | The script name : ./cmdargs.sh 19 | The value of the first argument to the script : bmw 20 | The value of the second argument to the script : ford 21 | The value of the third argument to the script : toyota 22 | The number of arguments passed to the script : 3 23 | The value of all command-line arguments ($* version) : bmw ford toyota 24 | The value of all command-line arguments ($@ version) : bmw ford toyota 25 | ``` 26 | 27 | 这里对 `$@` 与 `$*` 的异同再次进行阐述: 28 | 29 | - [`$@`](https://bash.cyberciti.biz/guide/$@) expanded as "$1" "$2" "$3" ... "$n" 30 | - [`$*`](https://bash.cyberciti.biz/guide/$*) expanded as "$1y$2y$3y...$n", where y is the value of [`$IFS`](https://bash.cyberciti.biz/guide/$IFS) variable i.e. "`$*`" is one long string and `$IFS` act as an separator or token delimiters. 31 | 32 | 相对完整的示例如下: 33 | 34 | ```sh 35 | if test $# = 1 36 | then 37 | start=1 38 | finish=$1 39 | elif test $# = 2 40 | then 41 | start=$1 42 | finish=$2 43 | else 44 | echo "Usage: $0 " 1>&2 45 | exit 1 46 | fi 47 | 48 | for argument in "$@" 49 | do 50 | if echo "$argument" | egrep -v '^-?[0-9]+$' >/dev/null 51 | then 52 | echo "$0: argument '$argument' is not an integer" 1>&2 53 | exit 1 54 | fi 55 | done 56 | 57 | number=$start 58 | while test $number -le $finish 59 | do 60 | echo $number 61 | number=`expr $number + 1` # or number=$(($number + 1)) 62 | done 63 | ``` 64 | 65 | ## 使用说明 66 | 67 | 您可以使用 if 命令来检查命令行参数。未通过必需的命令行选项时,许多 Linux 命令都会显示错误或使用情况信息。例如,尝试以下命令: 68 | 69 | ```sh 70 | gcc 71 | 72 | # gcc: no input files 73 | ``` 74 | 75 | 取决于用户输入的 shell 脚本必须:验证传递给它的参数数量。如果未将参数或输入传递给脚本,则显示错误或用法消息。您的 shell 脚本还可以使用 if 命令和 `$#` 特殊的 shell 变量参数来创建此类用法消息。创建一个名为 userlookup.sh 的外壳脚本: 76 | 77 | ```sh 78 | #!/bin/bash 79 | # A shell script to lookup usernames in /etc/passwd file 80 | # Written by: Vivek Gite 81 | # Last updated on: Sep/10/2003 82 | # ------------------------------------------------------- 83 | # Set vars 84 | user=$1 # first command line argument 85 | 86 | passwddb=/etc/passwd 87 | 88 | # Verify the type of input and number of values 89 | # Display an error message if the username (input) is not correct 90 | # Exit the shell script with a status of 1 using exit 1 command. 91 | [ $# -eq 0 ] && { echo "Usage: $0 username"; exit 1; } 92 | 93 | grep "^$user" $passwddb >/dev/null 94 | retval=$? # store exit status of grep 95 | 96 | # If grep found username, it sets exit status to zero 97 | # Use exit status to make the decision 98 | [ $retval -eq 0 ] && echo "$user found" || echo "$user not found" 99 | ``` 100 | 101 | # Shell 参数 102 | 103 | - All command line parameters or arguments can be accessed via $1, $2, $3,..., $9. 104 | - **[`$*`](https://bash.cyberciti.biz/guide/$*)** holds all command line parameters or arguments. 105 | - **[`$#`](https://bash.cyberciti.biz/guide/$)** holds the number of positional parameters. 106 | - **[`$-`](https://bash.cyberciti.biz/guide/$-)** holds flags supplied to the shell. 107 | - **[`$?`](https://bash.cyberciti.biz/guide/$%3F)** holds the return value set by the previously executed command. 108 | - **[`$$`](https://bash.cyberciti.biz/guide/$$)** holds the process number of the shell (current shell). 109 | - **[`$!`](https://bash.cyberciti.biz/guide/$!)** hold the process number of the last background command. 110 | - **[`$@`](https://bash.cyberciti.biz/guide/$@)** holds all command line parameters or arguments. 111 | 112 | # exit 113 | 114 | 每个 Linux 命令正常或异常终止时都会返回一个状态。您可以在 shell 脚本中使用退出状态的值来显示错误消息或采取某种措施。例如,如果 tar 命令不成功,它将返回一个代码,该代码告诉 Shell 脚本向 sysadmin 发送电子邮件。Shell 脚本或用户执行的每个 Linux 命令都具有退出状态。退出状态是整数。Linux 手册页统计了每个命令的退出状态。退出状态为 0 表示命令成功执行,没有任何错误。非零(1-255 值)的退出状态表示命令失败。您可以使用名为 `$` 的特殊 shell 变量。获取先前执行的命令的退出状态。要打印 `$` 变量使用 echo 命令: 115 | 116 | ```sh 117 | echo $? 118 | date # run date command 119 | echo $? # print exit status 120 | foobar123 # not a valid command 121 | echo $? # print exit status 122 | 123 | ls -l /tmp 124 | status=$? 125 | echo "ls command exit stats - $status" 126 | ``` 127 | 128 | 退出状态不仅限于 Shell 脚本。每次命令终止的 Shell 程序都会获得一个退出代码,以指示命令的成功或失败。因此,我们可以使用特定的 bash 变量 `$?` 来获取命令的退出状态。例如: 129 | 130 | ```sh 131 | $ ping -q -c 4 www.cyberciti.biz >/dev/null 132 | $ echo $? 133 | ``` 134 | 135 | 在此示例中,我们将仅看到最后一个命令(command3)的退出状态: 136 | 137 | ```sh 138 | command1 | command2 | command3 139 | ## will get the exit status of the last command in the pipeline ## 140 | echo $? 141 | ``` 142 | -------------------------------------------------------------------------------- /02~存储/00~存储分层/存储器的层次化结构.md: -------------------------------------------------------------------------------- 1 | # 存储器的层次化结构 2 | 3 | 计算机存储设备可被粗略分为内存储器(Main Memory)与外存储器(External Memory)两大类,内存存取速度快,但容量小,价格昂贵,而且不能长期保存数据,在不通电情况下数据会消失;外存储器存取速度相对较慢,却可以吃持久化存储。如果进行更加细致地划分,每个计算机系统中的存储设备都被组织成了一个存储器层次结构,在这个层次结构中,从上至下,设备变得访问速度越来越慢、容量越来越大,并且每字节的造价也越来越便宜。 4 | 5 | ![image](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230430222629.png) 6 | 7 | 最上层的是寄存器,存取时间极快,但容量小。其次是高速缓存,存取时间次之,容量比寄存器大一些。再往下就是我们常见的内存、硬盘,存取速度递减,但容量越来越大。CPU 在访问数据时,数据一般在相邻两层之间复制传送,且总是从慢速存储器复制到快速存储器,通过这种方式保证 CPU 的速度和存储器的速度相匹配。寄存器的存取时间在 1ns 级别,存储容量小于 1KB,可看做就是 L1 的高速缓存。L1 等高速缓存的存取时间在 2ns 级别,存储容量为 4MB;L1 是 L2 的高速缓存,L2 是 L3 的高速缓存,L3 是主存的高速缓存。主存的存取时间在 10ns 级别,存储容量目前在 GB 级别;主存又是磁盘的高速缓存。磁盘的存取速度在 10ms 级别,存储容量可达 TB 级别,在某些具有分布式文件系统的网络系统中,本地磁盘就是存储在其他系统中磁盘上的数据的高速缓存。 8 | 9 | ```log 10 | Latency Comparison Numbers (~2012) 11 | ---------------------------------- 12 | L1 cache reference 0.5 ns 13 | Branch mispredict 5 ns 14 | L2 cache reference 7 ns 14x L1 cache 15 | Mutex lock/unlock 25 ns 16 | Main memory reference 100 ns 20x L2 cache, 200x L1 cache 17 | Compress 1K bytes with Zippy 3,000 ns 3 us 18 | Send 1K bytes over 1 Gbps network 10,000 ns 10 us 19 | Read 4K randomly from SSD 150,000 ns 150 us ~1GB/sec SSD 20 | Read 1 MB sequentially from memory 250,000 ns 250 us 21 | Round trip within same datacenter 500,000 ns 500 us 22 | Read 1 MB sequentially from SSD 1,000,000 ns 1,000 us 1 ms ~1GB/sec SSD, 4X memory 23 | Disk seek 10,000,000 ns 10,000 us 10 ms 20x datacenter roundtrip 24 | Read 1 MB sequentially from disk 20,000,000 ns 20,000 us 20 ms 80x memory, 20X SSD 25 | Send packet CA->Netherlands->CA 150,000,000 ns 150,000 us 150 ms 26 | ``` 27 | 28 | CPU 仅可以直接从位于处理器芯片上的高速缓存中直接获取指令和数据。必须从主系统内存(随机存取内存或 RAM)中加载缓存。但是,RAM 仅在通电时才保留其内容,因此需要存储在更永久的存储器中。 29 | 30 | | Speed | Memory | Description | 31 | | ------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 32 | | Fastest | Cache | 高速缓存是实际上嵌入在 CPU 内部的内存。高速缓存内存非常快,通常只需要访问一个周期,但是由于它直接嵌入到 CPU 中,因此内存大小是有限制的。实际上,高速缓存有几个子级别(称为 L1,L2,L3),它们的速度都略有提高 | 33 | | | RAM | 处理器的所有指令和存储地址都必须来自 RAM。尽管 RAM 速度非常快,但是 CPU 仍需要花费大量时间来访问它(称为*latency*)。RAM 存储在与主板相连的单独的专用芯片中,这意味着它比高速缓存大得多 | 34 | | Slowest | Disk | 我们都熟悉软盘或 CDROM 上的软件,并将文件保存到硬盘上。我们还熟悉程序从硬盘加载所需的长时间-具有诸如旋转磁盘和移动磁头之类的物理机制意味着磁盘是最慢的存储形式。但它们也是迄今为止最大的存储形式 | 35 | 36 | 了解内存层次结构的重点是速度和大小之间的权衡-内存越快,内存越小: 37 | 38 | - 空间局部性(Spatial locality)表明,块内的数据可能会一起访问。 39 | - 时间位置(Temporal locality)表明最近使用的数据可能很快会再次使用。 40 | 41 | 这意味着,通过在实际中尽可能快地实现存储相关信息(空间)小块的快速可访问存储器(时间),可以获得好处。 42 | 43 | # 主存 44 | 45 | 主存是一个临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据。从物理上来说,主存是由一组动态随机存取存储器(DRAM)芯片组成的。从逻辑上来说,存储器是一个线性的字节数组,每个字节都有其唯一的地址(即数组索引),这些地址是从零开始的。一般来说,组成程序的每条机器指令都由不同数量的字节构成。 46 | 47 | ![](https://ww1.sinaimg.cn/large/007rAy9hly1g1k31pnn4vj30am06d0sl.jpg) 48 | 49 | 现代 DRAM 的结构和存取原理比较复杂,这里抽象出一个十分简单的存取模型来说明 DRAM 的工作原理。从抽象角度看,主存是一系列的存储单元组成的矩阵,每个存储单元存储固定大小的数据。每个存储单元有唯一的地址,现代主存的编址规则比较复杂,这里将其简化成一个二维地址:通过一个行地址和一个列地址可以唯一定位到一个存储单元。 50 | 51 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230430222649.png) 52 | 53 | 当系统需要读取主存时,则将地址信号放到地址总线上传给主存,主存读到地址信号后,解析信号并定位到指定存储单元,然后将此存储单元数据放到**数据总线**上,供其它部件读取。写主存的过程类似,系统将要写入单元地址和数据分别放在地址总线和数据总线上,主存读取两个总线的内容,做相应的写操作。这里可以看出,主存存取的时间仅与存取次数呈线性关系,因为不存在机械操作,两次存取的数据的“距离”不会对时间有任何影响,例如,先取 A0 再取 A1 和先取 A0 再取 D3 的时间消耗是一样的。 54 | 55 | # 寄存器与高速缓存 56 | 57 | 寄存器文件在层次结构中位于最顶部,也就是第 0 级或记为 L0。一个典型的寄存器文件只存储几百字节的信息,而主存里可存放几十亿字节。然而,处理器从寄存器文件中读数据的速度比从主存中读取几乎要快 100 倍。针对这种处理器与主存之间的差异,系统设计者采用了更小、更快的存储设备,即高速缓存存储器(简称高速缓存),作为暂时的集结区域,用来存放处理器近期可能会需要的信息。 58 | 59 | ![image](https://user-images.githubusercontent.com/5803001/52270789-eb303780-297c-11e9-938f-788ea6526bd3.png) 60 | 61 | L1 和 L2 高速缓存是用一种叫做静态随机访问存储器(SRAM)的硬件技术实现的。比较新的、处理能力更强大的系统甚至有三级高速缓存:L1、L2 和 L3。系统可以获得一个很大的存储器,同时访问速度也很快,原因是利用了高速缓存的局部性原理,即程序具有访问局部区域里的数据和代码的趋势。通过让高速缓存里存放可能经常访问的数据的方法,大部分的存储器操作都能在快速的高速缓存中完成。 62 | 63 | # 块设备与磁盘 64 | 65 | 块设备将信息存储在固定大小的块中,每个块都有自己的地址。对操作系统而言,块设备是以字符设备的外观展现的,例如 /dev/sda,虽然对这种字符设备可以按照字节为单位访问,但是实际上到块设备上却是以块为单位(最小 512byte,即一个扇区),这之间的转换是由操作系统来实现的。下面介绍几个块设备的基本概念: 66 | 67 | - 扇区:磁盘盘片上的扇形区域,逻辑化数据,方便管理磁盘空间,是硬件设备数据传送的基本单位,一般 512Byte; 68 | 69 | - 块:块是 VFS 和文件系统数据传送的基本单位,必须是扇区的整数倍,格式化文件系统时,可以指定块大小(一般 512,1024,2048,4096 字节); 70 | 71 | - 段:一个内存页或者内存页中的一部分,包含一些相邻磁盘扇区中的数据;磁盘的每个 IO 操作就是在磁盘与一些 RAM 单元之间相互传一些相邻扇区的内容,大多数情况下,磁盘控制器采用 DMA 方式进行数据传送。如果不同的段在 RAM 中相应的页框是连续的并且在磁盘上相应的数据块也是相邻的,就可以在通用块层合并它们,产生更大的内存区域,这个区域称为物理段。 72 | 73 | 通常情况下,我们是通过文件系统来访问块设备,也可以直接使用裸设备,通过指定偏移和大小来读写裸设备。常见的块存储设备就是物理磁盘,磁盘是一种直接存取的存储设备 (DASD);它是以存取时间变化不大为特征的。可以直接存取任何字符组,且容量大、速度较其它外存设备更快。在 Linux 系统下,还提供基于其他块设备之上的逻辑设备,如 Device Mapper,软 RAID 等。 74 | 75 | # Links 76 | 77 | - https://mp.weixin.qq.com/s/jz_sIFuzOTXHDOPi7ubMqw 78 | -------------------------------------------------------------------------------- /10~Shell 命令/用户权限/用户管理.md: -------------------------------------------------------------------------------- 1 | # 用户创建 2 | 3 | 我们可以使用如下命令快速创建新的用户: 4 | 5 | ```sh 6 | useradd $USERNAME 7 | mkdir /home/$USERNAME 8 | chown -R $USERNAME /home/$USERNAME 9 | chgrp -R class1 /home/$USERNAME 10 | ``` 11 | 12 | Linux 下的/etc/skel 目录往往不被人注意,其实此目录在新建用户时还是很有用的,灵活运用此目录可以节约一定的配置时间。 13 | skel 是 skeleton 的缩写,意为骨骼、框架。故此目录的作用是在建立新用户时,用于初始化用户根目录。系统会将此目录下的所有文件、目录都复制到新建用户 的根目录,并且将用户属主与用户组调整为与此根目录相同。所以可将用户配置文件预置到/etc/skel 目录下,比如说.bashrc、.profile 与.vimrc 等。 14 | 15 | 注:1.如果在新建用户时,没有自动建立用户根目录,则无法调用到此框架目录 2.如果不想以默认的/etc/skel 目录作为框架目录,可以在运行 useradd 命令时指定新的框架目录。例如: 16 | sudo useradd -d /home/chen -m -k /etc/my_skel chen 17 | 上述命令将新建用户 chen,设置用户根目录为/home/chen,并且此目录会自动建立;同时指定框架目录为/etc/my_skel3.如果不想在每次新建用户时,都重新指定新的框架目录,可以通过修改/etc/default/useradd 配置文件来改变默认的框架目录,方法如下: 18 | 查找 SKEL 变量的定义,如果此变量的定义已被注释掉,可以取消注释,然后修改其值: 19 | SKEL=/etc/my_skel 20 | 21 | 赋予 root 权限 22 | 方法一:修改 /etc/sudoers 文件,找到下面一行,把前面的注释(#)去掉 23 | 24 | ``` 25 | ## Allows people in group wheel to run all commands 26 | %wheel ALL=(ALL) ALL 27 | 然后修改用户,使其属于root组(wheel),命令如下: 28 | #usermod -g root tommy 29 | ``` 30 | 31 | 修改完毕,现在可以用 tommy 帐号登录,然后用命令 su –,即可获得 root 权限进行操作。 32 | 方法二:修改 /etc/sudoers 文件,找到下面一行,在 root 下面添加一行,如下所示: 33 | 34 | ``` 35 | ## Allow root to run any commands anywhere 36 | root ALL=(ALL) ALL 37 | tommy ALL=(ALL) ALL 38 | ``` 39 | 40 | 修改完毕,现在可以用 tommy 帐号登录,然后用命令 sudo –,即可获得 root 权限进行操作。 41 | 方法三:修改 /etc/passwd 文件,找到如下行,把用户 ID 修改为 0,如下所示: 42 | 43 | ``` 44 | tommy:x:0:33:tommy:/data/webroot:/bin/bash 45 | ``` 46 | 47 | 1、添加新的用户账号使用 useradd 命令, 48 | 添加用户账号就是在/etc/passwd 文件中为新用户增加一条记录,同时更新其他系统文件如/etc/shadow, /etc/group 等. 49 | Linux 提供了集成的系统管理工具 userconf,它可以用来对用户账号进行统一管理。 50 | 51 | 语法: 52 | useradd 选项 用户名 53 | 语义: 54 | -c comment 指定一段注释性描述。 55 | -d 目录 指定用户主目录,如果此目录不存在,则同时使用-m 选项,可以创建主目录。 56 | -g 用户组 指定用户所属的用户组。 57 | -G 用户组 用户组 指定用户所属的附加组。 58 | -s Shell 文件 指定用户的登录 Shell。 59 | -u 用户号 指定用户的用户号,如果同时有-o 选项,则可以重复使用其他用户的标识号。 60 | 用户名 指定新用户的登录名。 61 | 62 | 例 1: 63 | \$ useradd –d /usr/sam -m sam 64 | 释义: 65 | 此命令创建了一个用户 sam, 66 | 其中-d 和-m 选项用来为登录名 sam 产生一个主目录/usr/sam(/usr 为默认的用户主目录所在的父目录)。 67 | 68 | 例 2: 69 | \$ useradd -s /bin/sh -g group -G adm,root gem 70 | 释义: 71 | 此命令新建了一个用户 gem, 该用户的登录 Shell 是/bin/sh(有时要用/bin/bash), 72 | 它属于 group 用户组,同时又属于 adm 和 root 用户组,其中 group 用户组是其主组。 73 | 74 | 新建用户组可用命令: 75 | $ groupadd group 76 | $ groupadd adm 77 | 78 | 2、删除帐号 79 | 如果一个用户的账号不再使用,可以从系统中删除。 80 | 删除用户账号就是要将/etc/passwd 等系统文件中的该用户记录删除,必要时还删除用户的主目录。 81 | 语法: 82 | userdel 选项 用户名 83 | 选项: 84 | -r, 把用户的主目录一起删除。 85 | 86 | 例 1: 87 | \$ userdel -r sam 88 | 释义: 89 | 此命令删除用户 sam 在系统文件中(主要是/etc/passwd, /etc/shadow, /etc/group 等)的记录, 90 | 同时删除用户的主目录。 91 | 92 | 3、修改帐号 93 | 修改用户账号就是根据实际情况更改用户的有关属性,如用户号、主目录、用户组、登录 Shell 等。 94 | 修改已有用户的信息使用 usermod 命令. 95 | 语法: 96 | usermod 选项 用户名 97 | 选项: 98 | 包括-c, -d, -m, -g, -G, -s, -u 以及-o 等, 99 | 这些选项的意义与 useradd 命令中的选项一样,可以为用户指定新的资源值。 100 | 另外,有些系统可以使用如下选项: 101 | -l 新用户名 指定一个新的账号,即将原来的用户名改为新的用户名。 102 | 103 | 例如: 104 | \$ usermod -s /bin/ksh -d /home/z -g developer sam 105 | 释义: 106 | 此命令将用户 sam 的: 107 | 登录 Shell 修改为 ksh, 108 | 主目录改为/home/z, 109 | 用户组改为 developer. 110 | 4、给已有的用户增加工作组 111 | usermod -G groupname username 112 | 113 | 或者:gpasswd -a user group 114 | 115 | 5、用户口令的管理 116 | 用户管理的一项重要内容是用户口令的管理。 117 | 用户账号刚创建时没有口令,但是被系统锁定,无法使用,必须为其指定口令后才可以使用,即使是指定空口令。 118 | 指定和修改用户口令的 Shell 命令是 passwd。 119 | 超级用户可以为自己和其他用户指定口令,普通用户只能用它修改自己的口令。 120 | 语法: 121 | passwd 选项 用户名 122 | 选项: 123 | -l 锁定口令,即禁用账号。 124 | -u 口令解锁。 125 | -d 使账号无口令。 126 | -f 强迫用户下次登录时修改口令。 127 | 如果默认用户名,则修改当前用户的口令。 128 | 129 | 例如: 130 | 假设当前用户是 sam, 131 | 则下面的命令修改该用户自己的口令: 132 | \$ passwd 133 | Old password:**\*\*** 134 | New password:**\*\*\*** 135 | Re-enter new password:**\*\*\*** 136 | 137 | 如果是超级用户, 138 | 可以用下列形式指定任何用户的口令: 139 | \$passwd sam 140 | New password:**\*\*\*** 141 | Re-enter new password:**\*\*\*** 142 | 143 | 普通用户修改自己的口令时, 144 | passwd 命令会先询问原口令,验证后再要求用户输入两遍新口令, 145 | 如果两次输入的口令一致,则将这个口令指定给用户; 146 | 而超级用户为用户指定口令时,就不需要知道原口令。 147 | 148 | 为了系统安全起见,用户应该选择比较复杂的口令, 149 | 例如最好使用 8 位长的口令,口令中包含有大写、小写字母和数字,并且应该与姓名、生日等不相同。 150 | 151 | 例如 1: 152 | 为用户指定空口令时,执行下列形式的命令: 153 | \$passwd -d sam 154 | 释义: 155 | 此命令将用户 sam 的口令删除,这样用户 sam 下一次登录时,系统就不再询问口令。 156 | 157 | passwd 命令还可以用-l(lock)选项锁定某一用户,使其不能登录,例如: 158 | 例如 2: 159 | \$ passwd -l sam 160 | 161 | 二、Linux 系统用户组的管理 162 | 每个用户都有一个用户组,系统可以对一个用户组中的所有用户进行集中管理。 163 | 不同 Linux 系统对用户组的规定有所不同, 164 | 如 Linux 下的用户属于与它同名的用户组,这个用户组在创建用户时同时创建。 165 | 用户组的管理涉及用户组的添加、删除和修改。组的增加、删除和修改实际上就是对/etc/group 文件的更新。 166 | 167 | 1、增加一个新的用户组使用 groupadd 命令。 168 | 语法: 169 | groupadd 选项 用户组 170 | 选项: 171 | -g GID 指定新用户组的组标识号(GID)。 172 | -o 一般与-g 选项同时使用,表示新用户组的 GID 可以与系统已有用户组的 GID 相同。 173 | 174 | 例 1: 175 | \$ groupadd group1 176 | 释义: 177 | 此命令向系统中增加了一个新组 group1,新组的组标识号是在当前已有的最大组标识号的基础上加 1。 178 | 179 | 例 2: 180 | \$ groupadd -g 101 group2 181 | 释义: 182 | 此命令向系统中增加了一个新组 group2,同时指定新组的组标识号是 101。 183 | 184 | 2、如果要删除一个已有的用户组,使用 groupdel 命令. 185 | 语法: 186 | groupdel 用户组 187 | 188 | 例 1: 189 | \$ groupdel group1 190 | 释义: 191 | 此命令从系统中删除组 group1。 192 | 193 | 3.修改用户组的属性使用 groupmod 命令。 194 | 语法: 195 | groupmod 选项 用户组 196 | 选项: 197 | -g GID 为用户组指定新的组标识号。 198 | -o 与-g 选项同时使用,用户组的新 GID 可以与系统已有用户组的 GID 相同。 199 | -n 新用户组 将用户组的名字改为新名字 200 | 201 | 例 1: 202 | \$ groupmod -g 102 group2 203 | 释义: 204 | 此命令将组 group2 的组标识号修改为 102。 205 | 206 | 例 2: 207 | \$ groupmod –g 10000 -n group3 group2 208 | 释义: 209 | 此命令将组 group2 的标识号改为 10000,组名修改为 group3。 210 | 211 | 4.如果一个用户同时属于多个用户组,那么用户可以在用户组之间切换,以便具有其他用户组的权限。 212 | 用户可以在登录后,使用命令 newgrp 切换到其他用户组,这个命令的参数就是目的用户组。 213 | 214 | 例如: 215 | \$ newgrp root 216 | 释义: 217 | 这条命令将当前用户切换到 root 用户组,前提条件是 root 用户组确实是该用户的主组或附加组。 218 | 类似于用户账号的管理,用户组的管理也可以通过集成的系统管理工具来完成。 219 | http://www.libaqiang.com/?p=33001 220 | http://blog.chinaunix.net/uid-26000296-id-3496103.html 221 | -------------------------------------------------------------------------------- /02~存储/文件系统/文件/README.md: -------------------------------------------------------------------------------- 1 | # 文件 2 | 3 | 在 Linux、BSD 这样的类 Unix 系统中,万物皆文件(Everything is a file)。想象一下像文字处理程序这样的上下文中的文件,我们可以在此虚构的字处理文件上使用两个基本操作: 4 | 5 | - 读取它(来自字处理器的现有已保存数据)。 6 | - 向其写入(来自用户的新数据)。 7 | 8 | 在计算器中,我们与读写这样文件基础操作相关的元素还包括:屏幕、键盘、打印机、CD-ROM 等。 9 | 10 | ![抽象](https://s2.ax1x.com/2020/01/25/1eOoRS.png) 11 | 12 | 屏幕和打印机都像只写文件,但是信息不是作为位存储在磁盘上,而是显示为屏幕上的点或页面上的线。键盘就像一个只读文件,其数据来自用户提供的击键。CD-ROM 也类似,但并非直接来自用户,而是将数据直接存储在磁盘上。因此,文件的概念是数据接收器或数据源的良好抽象,它是一个可以附加到计算机的所有设备的出色抽象。这种实现是 UNIX 的强大功能,并且在整个平台的设计中显而易见。向程序员提供这种硬件抽象是操作系统的基本角色之一。可以说,抽象是支撑所有现代计算的主要概念,这可能并不过分。从设计现代用户界面到现代 CPU 的内部工作原理,没人能理解所有内容,更不用说自己构建所有内容了。对于程序员而言,抽象是允许我们进行协作和发明的通用语言。 13 | 14 | # 文件描述符 15 | 16 | UNIX 中每个正在运行的程序都以三个已经打开的文件开始: 17 | 18 | | Descriptive Name | Short Name | File Number | Description | 19 | | ---------------- | ---------- | ----------- | --------------------------- | 20 | | Standard In | stdin | 0 | Input from the keyboard | 21 | | Standard Out | stdout | 1 | Output to the console | 22 | | Standard Error | stderr | 2 | Error output to the console | 23 | 24 | ![Default Unix Files](https://s2.ax1x.com/2020/01/25/1eXtW8.png) 25 | 26 | 打开调用返回的值称为文件描述符,本质上是内核保存的打开文件数组的索引;换言之,文件描述符是通往内核底层硬件抽象的门户。 27 | 28 | ![File Descriptor](https://s2.ax1x.com/2020/01/25/1eXBes.md.png) 29 | 30 | 文件描述符是内核存储的文件描述符表的索引。内核响应打开调用而创建文件描述符,并将文件描述符与底层文件状对象的某种抽象相关联,这些对象是实际的硬件设备,文件系统或其他任何东西。因此,引用该文件描述符的进程的读取或写入调用将由内核路由到正确的位置,以最终做一些有用的事情。 31 | 32 | # 硬件文件抽象 33 | 34 | 从最低级别开始,操作系统需要程序员来创建设备驱动程序,以便能够与硬件设备进行通信。这些设备驱动程序就是那些遵循着 Linux 系统 API 的软件程序。 35 | 36 | ```c 37 | /** 38 | * virtio_driver - operations for a virtio I/O driver 39 | * @driver: underlying device driver (populate name and owner). 40 | * @id_table: the ids serviced by this driver. 41 | * @feature_table: an array of feature numbers supported by this driver. 42 | * @feature_table_size: number of entries in the feature table array. 43 | * @probe: the function to call when a device is found. Returns 0 or -errno. 44 | * @remove: the function to call when a device is removed. 45 | * @config_changed: optional function to call when the device configuration 46 | * changes; may be called in interrupt context. 47 | */ 48 | struct virtio_driver { 49 | struct device_driver driver; 50 | const struct virtio_device_id *id_table; 51 | const unsigned int *feature_table; 52 | unsigned int feature_table_size; 53 | int (*probe)(struct virtio_device *dev); 54 | void (*scan)(struct virtio_device *dev); 55 | void (*remove)(struct virtio_device *dev); 56 | void (*config_changed)(struct virtio_device *dev); 57 | #ifdef CONFIG_PM 58 | int (*freeze)(struct virtio_device *dev); 59 | int (*restore)(struct virtio_device *dev); 60 | #endif 61 | }; 62 | 63 | ``` 64 | 65 | 在上面的简化示例中,我们可以看到驱动程序提供了读写功能,以响应文件描述符上的类似操作而被调用。设备驱动程序知道如何将这些通用请求转换为针对特定设备的特定请求或命令。 66 | 67 | 为了提供对用户空间的抽象,内核通过通常称为设备层的方式提供文件接口。主机上的物理设备由特殊文件系统(例如 /dev)中的文件表示。在类似 UNIX 的系统中,所谓的设备节点具有主要编号和次要编号,它们允许内核将特定节点与其底层驱动程序相关联。 68 | 69 | ```s 70 | $ ls -l /dev/null /dev/zero /dev/tty 71 | crw-rw-rw- 1 root root 1, 3 Aug 26 13:12 /dev/null 72 | crw-rw-rw- 1 root root 5, 0 Sep 2 15:06 /dev/tty 73 | crw-rw-rw- 1 root root 1, 5 Aug 26 13:12 /dev/zero 74 | ``` 75 | 76 | 这将我们带到文件描述符,该文件描述符是用户空间用于与基础设备进行通信的句柄。从广义上讲,打开文件时发生的情况是内核正在使用路径信息将文件描述符与提供适当读写等 API 的内容进行映射。当此打开是针对设备的(上面的 / dev/sr0)时,打开的设备节点的主编号和次编号将提供内核找到正确的设备驱动程序并完成映射所需的信息。然后,内核将知道如何将进一步的调用(如读取)路由到设备驱动程序提供的基础功能。 77 | 78 | 尽管非设备文件之间存在更多的层,但其操作类似。这里的抽象是挂载点;挂载文件系统具有设置映射的双重目的,因此文件系统知道提供存储的底层设备,内核知道在该挂载点下打开的文件应定向到文件系统驱动程序。像设备驱动程序一样,文件系统被写入内核提供的特定通用文件系统 API。 79 | 80 | 实际上,实际上还有许多其他层使图片复杂化。例如,内核将竭尽全力在尽可能多的空闲内存中缓存来自磁盘的尽可能多的数据。这提供了许多速度优势。它还将尝试以最有效的方式组织设备访问;例如,即使没有按顺序到达请求,也要尝试命令磁盘访问以确保物理上靠在一起存储的数据被一起检索。此外,许多设备属于通用类,例如 USB 或 SCSI 设备,它们提供要写入的抽象层。因此,文件系统将直接穿过这些许多层,而不是直接写入设备。理解内核就是要理解这许多 API 是如何相互关联和共存的。 81 | 82 | # Shell 83 | 84 | Shell 是与操作系统进行交互的网关。无论是 bash,zsh,csh 还是许多其他 Shell 中的任何一个,它们基本上都只有一项主要任务-允许您执行程序(当我们谈论某些内部原理时,您将开始理解 Shell 实际是如何做到的)操作系统的版本)。但是,shell 所要做的不只是允许您简单地执行程序。它们具有强大的功能来重定向文件,允许您同时执行多个程序和编写完整程序的脚本。 85 | 86 | ## Redirection 87 | 88 | 很多时候我们并不希望输入输出的文件描述符指向默认的位置,譬如您可能希望将程序的所有输出捕获到磁盘上的文件中,或者让它从您之前准备的文件中读取命令;另一个有用的任务可能是希望将一个程序的输出传递给另一个程序的输入。 89 | 90 | | Name | Command | Description | Example | 91 | | ------------------ | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------- | 92 | | Redirect to a file | `> filename` | Take all output from standard out and place it into `filename`. Note using `>>` will append to the file, rather than overwrite it. | `ls > filename` | 93 | | Read from a file | < `filename` | Copy all data from the file to the standard input of the program | `echo < filename` | 94 | | Pipe | `program1 | program2` | Take everything from standard out of `program1` and pass it to standard input of `program2` | `ls | more` | 95 | 96 | ## pipe 的实现 97 | 98 | 对于 `ls | more` 这样的实现也是典型的抽象的应用案例,其文件描述符没有指向标准输出的文件描述符与某种底层设备(例如控制台,用于输出到终端)相关联,而是指向内核提供的内存缓冲区,这里就是管道。这里的技巧是,另一个进程可以将其标准输入与此同一个缓冲区的另一侧相关联,并有效地消耗另一个进程的输出。 99 | 100 | ![A pipe in action](https://s2.ax1x.com/2020/01/25/1ejQpT.md.png) 101 | 102 | 内核将对管道的写入存储起来,直到从另一侧进行的相应读取耗尽了缓冲区。这是一个非常强大的概念,并且是类 UNIX 操作系统中进程间通信或 IPC 的基本形式之一。管道不仅仅允许数据传输;它可以充当信令通道。如果进程读取空管道,则默认情况下它将阻塞或进入休眠状态,直到有可用数据。 103 | 104 | 因此,两个进程可以使用管道来传达已通过写一个字节数据采取了某些措施除了实际数据并不重要之外,管道中仅存在任何数据就可以发出消息例如,一个进程要求另一个进程打印文件,这将花费一些时间这两个进程可以在它们之间建立一个管道,其中请求进程对空管道进行读取如果为空,则该调用将阻塞,并且该过程不会继续一旦完成打印,其他进程便可以在管道中写入一条消息,从而有效地唤醒请求进程并发出工作已完成的信号。 105 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Linux-Series 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 34 | 38 | 40 | 45 | 46 |
47 | 64 | 97 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 143 | 144 | 145 | 146 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /01~进程与处理器/进程与线程/进程模型.md: -------------------------------------------------------------------------------- 1 | # 进程模型 2 | 3 | # Process Elements 4 | 5 | ![The Elements of a Process](https://s2.ax1x.com/2020/01/27/1nf0Cd.png) 6 | 7 | 进程 ID(或 PID)由操作系统分配,并且对于每个正在运行的进程都是唯一的。 8 | 9 | ## Process Hierarchy 10 | 11 | 尽管操作系统可以同时运行多个进程,但实际上它只能直接启动一个称为 init(初始缩写)进程的进程。这不是一个特别特殊的过程,除了它的 PID 始终为 0 并且将一直运行。所有其他过程都可以视为此初始过程的子过程。进程与其他进程一样都有一棵家谱。每个进程都有一个父进程,并且可以有许多同级,它们是同一父进程创建的进程。当然,子进程可以创造更多的子进程,依此类推。 12 | 13 | ```ps 14 | 1 init-+-apmd 15 | |-atd 16 | |-cron 17 | ... 18 | 5 |-dhclient 19 | |-firefox-bin-+-firefox-bin---2*[firefox-bin] 20 | | |-java_vm---java_vm---13*[java_vm] 21 | | `-swf_play 22 | ``` 23 | 24 | ## 文件描述符(File Descriptors) 25 | 26 | 我们之前了解了 stdin,stdout 和 stderr;给每个进程的默认文件。您将记住,这些文件始终具有相同的文件描述符号(分别为 0,1,2)。因此,内核为每个进程单独保存文件描述符。文件描述符也具有权限。例如,您可能能够从文件中读取但无法写入文件。打开文件后,操作系统会在文件描述符中保留对该文件的进程权限记录,并且不允许进程执行不应执行的任何操作。 27 | 28 | ## Registers 29 | 30 | 处理器实际上对寄存器中的值执行一般简单的操作。这些值被读取(或写入)到内存中,每个进程都分配有内核跟踪的内存。 31 | 32 | 因此,方程式的另一面是跟踪寄存器。当当前正在运行的进程放弃处理器以便其他进程可以运行时,它需要保存其当前状态。同样,当进程有更多时间在 CPU 上运行时,我们需要能够恢复此状态。为此,操作系统需要将 CPU 寄存器的副本存储到内存中。当该进程再次运行时,操作系统会将寄存器值从内存中复制回 CPU 寄存器中,而该进程将立即从中断处返回。 33 | 34 | ## 内核状态(Kernel State) 35 | 36 | 在内部,内核需要跟踪每个进程的许多元素。操作系统要跟踪的另一个重要元素是进程状态。如果该进程当前正在运行,则使其处于运行状态是有意义的。但是,如果进程请求从磁盘读取文件,则从内存层次结构中我们知道这可能会花费大量时间。该进程应该放弃其当前执行以允许另一个进程运行,但是内核不必让该进程再次运行,直到磁盘中的数据在内存中可用为止。因此,它可以将进程标记为磁盘等待(或类似),直到数据准备就绪为止。 37 | 38 | 一些过程比其他过程更重要,并且具有更高的优先级。请参阅下面有关调度程序的讨论。内核可以保留有关每个进程行为的统计信息,这有助于其决定进程的行为。例如,它主要是从磁盘读取还是主要是 CPU 密集型操作。 39 | 40 | # 内存(Memory) 41 | 42 | 所有程序代码以及变量和任何其他分配的存储都存储在该存储器中。内存的一部分可以在进程之间共享(称为共享内存)。在较早版本的操作系统中执行原始操作后,通常会看到称为系统五共享内存(SysV SHM)的信息。进程可以利用的另一个重要概念是将磁盘上的文件映射到内存。这意味着不必打开文件并使用诸如 `read()` 和 `write()` 之类的命令,文件看起来就像是其他任何类型的 RAM。映射区域具有诸如读取,写入和执行之类的权限,需要对其进行跟踪。众所周知,维护安全性和稳定性是操作系统的工作,因此它需要检查进程是否尝试写入只读区域并返回错误。 43 | 44 | 一个过程可以进一步分为代码和数据部分。程序代码和数据应分开保存,因为它们需要与操作系统不同的权限,并且分开可以促进代码共享(如您稍后所见)。操作系统需要授予程序代码读取和执行的权限,但通常不予写入。另一方面,数据(变量)需要读写权限,但不能执行。 45 | 46 | ## The Stack 47 | 48 | 进程的另一个非常重要的部分是称为栈的内存区域。这可以视为流程数据部分的一部分,并且与任何程序的执行都密切相关。栈是通用的数据结构,其工作方式与一堆板块完全相同。您可以推动一个元素(将一个元素放在一叠元素的顶部),然后成为顶部元素,或者您可以弹出一个元素(取下元素,露出前一个元素)。 49 | 50 | 栈是函数调用的基础。每次调用一个函数都会得到一个新的栈框架。这是一个内存区域,通常至少包含完成后要返回的地址,函数的输入参数和局部变量的空间。按照惯例,栈通常会变小。这意味着栈从内存中的高地址开始,然后逐渐降低。 51 | 52 | ![The Stack](https://s2.ax1x.com/2020/01/27/1nzrDS.png) 53 | 54 | 我们可以看到拥有栈如何带来函数的许多功能: 55 | 56 | - 每个函数都有其输入参数的副本。这是因为为每个函数分配了一个新的栈帧,其参数位于新的内存区域中。 57 | 58 | - 这就是为什么在函数内部定义的变量无法被其他函数看到的原因。全局变量(可以通过任何函数看到)都保存在数据存储器的单独区域中。 59 | 60 | - 这有助于递归调用。这意味着一个函数可以自由地再次调用自身,因为将为其所有局部变量创建一个新的栈框架。 61 | 62 | - 每个帧都包含要返回的地址。C 仅允许从函数返回单个值,因此按照惯例,该值将在指定的寄存器中而不是栈中返回给调用函数。 63 | 64 | - 由于每一帧都引用了它之前的那一帧,因此调试器可以向后“遍历”指针,跟随指针到达栈。由此可以生成栈跟踪,该跟踪向您显示调用该函数的所有函数。这对于调试非常有用。 65 | 66 | - 您可以看到函数的工作方式完全适合栈的性质。任何函数都可以调用任何其他函数,然后成为其他函数(放在栈顶)。最终,该函数将返回到调用它的函数(将自身移出栈)。 67 | 68 | - 栈的确会使调用函数变慢,因为必须将值移出寄存器并移入内存。有些体系结构允许参数直接在寄存器中传递。但是,为了保持每个函数获得每个参数的唯一副本的语义,寄存器必须旋转。 69 | 70 | - 您可能听说过术语栈溢出。这是通过传递伪造的值来入侵系统的一种常见方法。如果您是程序员,则可以接受对栈变量的任意输入(例如,从键盘或通过网络读取),则需要明确说明数据的大小。允许不检查任何数量的数据将仅覆盖内存。通常,这会导致崩溃,但是有些人意识到,如果函数写满了足以在栈帧的返回地址部分中放置特定值的内存,则函数完成时将返回而不是返回正确的位置(从),他们可以使其返回到刚发送的数据中。如果该数据包含会破坏系统的二进制可执行代码(例如,以 root 权限为用户启动终端),则说明您的计算机已受到威胁。发生这种情况是因为栈向下增长,但是数据是“向上”读取的(即从较低地址到较高地址)。有几种解决方法:首先,作为程序员,您必须确保始终检查要接收到的数据量。操作系统可以通过确保栈被标记为不可执行来帮助程序员避免这种情况。也就是说,即使恶意用户试图将某些代码传递到您的程序中,处理器也不会运行任何代码。现代体系结构和操作系统支持此功能。 71 | 72 | - 栈最终由编译器管理,因为它负责生成程序代码。对于操作系统来说,栈就像该进程的任何其他内存区域一样。 73 | 74 | 为了跟踪栈的当前增长,硬件将寄存器定义为栈指针。编译器(或编程器,在用汇编器编写时)使用该寄存器来跟踪栈的当前顶部。 75 | 76 | ```sh 77 | 1 $ cat sp.c 78 | void function(void) 79 | { 80 | int i = 100; 81 | 5 int j = 200; 82 | int k = 300; 83 | } 84 | 85 | $ gcc -fomit-frame-pointer -S sp.c 86 | 10 87 | $ cat sp.s 88 | .file "sp.c" 89 | .text 90 | .globl function 91 | 15 .type function, @function 92 | function: 93 | subl $16, %esp 94 | movl $100, 4(%esp) 95 | movl $200, 8(%esp) 96 | 20 movl $300, 12(%esp) 97 | addl $16, %esp 98 | ret 99 | .size function, .-function 100 | .ident "GCC: (GNU) 4.0.2 20050806 (prerelease) (Debian 4.0.1-4)" 101 | 25 .section .note.GNU-stack,"",@progbits 102 | 103 | ``` 104 | 105 | 上面我们展示了一个在栈上分配三个变量的简单函数。该反汇编说明了在 x86 体系结构上使用栈指针。首先,我们在栈上为局部变量分配一些空间。由于栈变小,我们从栈指针中保存的值中减去。值 16 是一个足以容纳我们的局部变量的值,但可能不完全是所需的大小(例如,对于 3 个 4 字节的 int 值,我们实际上只需要 12 个字节,而不是 16 个)就可以保持内存中栈的对齐 编译器要求的特定边界。 106 | 107 | 然后,我们将这些值移动到栈存储器中(并在实函数中使用它们)。最后,在返回父函数之前,我们通过将栈指针移回开始之前的位置来“弹出”栈中的值。 108 | 109 | ## The Heap 110 | 111 | 堆是由进程管理的用于动态分配内存的内存区域。这适用于在编译时不知道其内存要求的变量。堆的底部称为 brk,因此被称为对其进行修改的系统调用。通过使用 brk 调用向下扩展区域,进程可以请求内核分配更多的内存供其使用。 112 | 113 | 堆通常由 malloc 库调用管理。通过允许程序员简单地分配和释放(通过 free 调用)堆内存,这使程序员易于管理堆。malloc 可以使用诸如伙伴分配器之类的方案来管理用户的堆内存。malloc 在分配方面也可以更聪明,并且可以使用匿名 mmap 来获得额外的进程内存。在这里,不是将文件映射到进程内存,而是直接映射系统 RAM 的区域。这样可以更有效。由于正确管理内存的复杂性,对于任何现代程序来说,都有理由直接调用 brk 是非常罕见的。 114 | 115 | ## 内存布局 116 | 117 | ![Process memory layout](https://s2.ax1x.com/2020/01/27/1nz5uT.png) 118 | 119 | 如我们所见,一个进程分配了较小的内存区域,每个区域都有特定的用途。上面给出了内核如何在内存中安排进程的示例。从顶部开始,内核会在进程的顶部为其自身保留一些内存(我们通过虚拟内存了解如何在所有进程之间实际共享此内存)。在其下方是用于映射文件和库的空间。在堆栈的下面,在堆栈的下面。底部是程序映像,是从磁盘上的可执行文件加载的。在后面的章节中,我们将仔细研究加载数据的过程。 120 | 121 | # 进程上下文 122 | 123 | 进程是操作系统对一个正在运行的程序的一种抽象,在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。所谓的并发运行,则是说一个进程的指令和另一个进程的指令是交错执行的。无论是在单核还是多核系统中,可以通过处理器在进程间切换,来实现单个 CPU 看上去像是在并发地执行多个进程。操作系统实现这种交错执行的机制称为上下文切换。 124 | 125 | 操作系统保持跟踪进程运行所需的所有状态信息。这种状态,也就是上下文,它包括许多信息,例如 PC 和寄存器文件的当前值,以及主存的内容。在任何一个时刻,单处理器系统都只能执行一个进程的代码。当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文、恢复新进程的上下文,然后将控制权传递到新进程。新进程就会从上次停止的地方开始。 126 | 127 | ![image](https://user-images.githubusercontent.com/5803001/52271382-6fcf8580-297e-11e9-9161-de3d9461d5f4.png) 128 | 129 | 在《[Linux-Notes/虚拟存储管理器](https://github.com/wx-chevalier/Linux-Notes?q=)》一节中,我们介绍过它为每个进程提供了一个假象,即每个进程都在独占地使用主存。每个进程看到的是一致的存储器,称为虚拟地址空间。其虚拟地址空间最上面的区域是为操作系统中的代码和数据保留的,这对所有进程来说都是一样的;地址空间的底部区域存放用户进程定义的代码和数据。 130 | 131 | ![image](https://user-images.githubusercontent.com/5803001/52272019-52032000-2980-11e9-953c-89de286e5174.png) 132 | 133 | - 程序代码和数据,对于所有的进程来说,代码是从同一固定地址开始,直接按照可执行目标文件的内容初始化。 134 | 135 | - 堆,代码和数据区后紧随着的是运行时堆。代码和数据区是在进程一开始运行时就被规定了大小,与此不同,当调用如 malloc 和 free 这样的 C 标准库函数时,堆可以在运行时动态地扩展和收缩。 136 | 137 | - 共享库:大约在地址空间的中间部分是一块用来存放像 C 标准库和数学库这样共享库的代码和数据的区域。 138 | 139 | - 栈,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。 140 | 141 | - 内核虚拟存储器:内核总是驻留在内存中,是操作系统的一部分。地址空间顶部的区域是为内核保留的,不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数。 142 | -------------------------------------------------------------------------------- /10~Shell 命令/Shell 编程/交互式 Shell/菜单与对话框.md: -------------------------------------------------------------------------------- 1 | # 菜单与对话框 2 | 3 | 您每天都会使用某种通用应用程序菜单。菜单不过是由 Shell 脚本提供给用户的命令列表。例如,您可以编写菜单驱动的 Shell 脚本来获取终端信息。菜单驱动的 Shell 脚本充当“避免用户记住语法的常用命令的快捷方式”。通常,您需要键入说明或命令才能完成任务。命令输入可以在菜单的帮助下完成。 4 | 5 | ```sh 6 | #!/bin/bash 7 | # A menu driven shell script sample template 8 | ## ---------------------------------- 9 | # Step #1: Define variables 10 | # ---------------------------------- 11 | EDITOR=vim 12 | PASSWD=/etc/passwd 13 | RED='\033[0;41;30m' 14 | STD='\033[0;0;39m' 15 | 16 | # ---------------------------------- 17 | # Step #2: User defined function 18 | # ---------------------------------- 19 | pause(){ 20 | read -p "Press [Enter] key to continue..." fackEnterKey 21 | } 22 | 23 | one(){ 24 | echo "one() called" 25 | pause 26 | } 27 | 28 | # do something in two() 29 | two(){ 30 | echo "two() called" 31 | pause 32 | } 33 | 34 | # function to display menus 35 | show_menus() { 36 | clear 37 | echo "~~~~~~~~~~~~~~~~~~~~~" 38 | echo " M A I N - M E N U" 39 | echo "~~~~~~~~~~~~~~~~~~~~~" 40 | echo "1. Set Terminal" 41 | echo "2. Reset Terminal" 42 | echo "3. Exit" 43 | } 44 | # read input from the keyboard and take a action 45 | # invoke the one() when the user select 1 from the menu option. 46 | # invoke the two() when the user select 2 from the menu option. 47 | # Exit when user the user select 3 form the menu option. 48 | read_options(){ 49 | local choice 50 | read -p "Enter choice [ 1 - 3] " choice 51 | case $choice in 52 | 1) one ;; 53 | 2) two ;; 54 | 3) exit 0;; 55 | *) echo -e "${RED}Error...${STD}" && sleep 2 56 | esac 57 | } 58 | 59 | # ---------------------------------------------- 60 | # Step #3: Trap CTRL+C, CTRL+Z and quit singles 61 | # ---------------------------------------------- 62 | trap '' SIGINT SIGQUIT SIGTSTP 63 | 64 | # ----------------------------------- 65 | # Step #4: Main logic - infinite loop 66 | # ------------------------------------ 67 | while true 68 | do 69 | 70 | show_menus 71 | read_options 72 | done 73 | ``` 74 | 75 | ![Menu](https://s1.ax1x.com/2020/06/17/NE1usx.png) 76 | 77 | ## 案例:系统信息 78 | 79 | ```sh 80 | #!/bin/bash 81 | # grabsysinfo.sh - A simple menu driven shell script to to get information about your 82 | # Linux server / desktop. 83 | # Author: Vivek Gite 84 | # Date: 12/Sep/2007 85 | 86 | # Define variables 87 | LSB=/usr/bin/lsb_release 88 | 89 | # Purpose: Display pause prompt 90 | # $1-> Message (optional) 91 | function pause(){ 92 | local message="$@" 93 | [ -z $message ] && message="Press [Enter] key to continue..." 94 | read -p "$message" readEnterKey 95 | } 96 | 97 | # Purpose - Display a menu on screen 98 | function show_menu(){ 99 | date 100 | echo "---------------------------" 101 | echo " Main Menu" 102 | echo "---------------------------" 103 | echo "1. Operating system info" 104 | echo "2. Hostname and dns info" 105 | echo "3. Network info" 106 | echo "4. Who is online" 107 | echo "5. Last logged in users" 108 | echo "6. Free and used memory info" 109 | echo "7. exit" 110 | } 111 | 112 | # Purpose - Display header message 113 | # $1 - message 114 | function write_header(){ 115 | local h="$@" 116 | echo "---------------------------------------------------------------" 117 | echo " ${h}" 118 | echo "---------------------------------------------------------------" 119 | } 120 | 121 | # Purpose - Get info about your operating system 122 | function os_info(){ 123 | write_header " System information " 124 | echo "Operating system : $(uname)" 125 | [ -x $LSB ] && $LSB -a || echo "$LSB command is not insalled (set \$LSB variable)" 126 | #pause "Press [Enter] key to continue..." 127 | pause 128 | } 129 | 130 | # Purpose - Get info about host such as dns, IP, and hostname 131 | function host_info(){ 132 | local dnsips=$(sed -e '/^$/d' /etc/resolv.conf | awk '{if (tolower($1)=="nameserver") print $2}') 133 | write_header " Hostname and DNS information " 134 | echo "Hostname : $(hostname -s)" 135 | echo "DNS domain : $(hostname -d)" 136 | echo "Fully qualified domain name : $(hostname -f)" 137 | echo "Network address (IP) : $(hostname -i)" 138 | echo "DNS name servers (DNS IP) : ${dnsips}" 139 | pause 140 | } 141 | 142 | # Purpose - Network inferface and routing info 143 | function net_info(){ 144 | devices=$(netstat -i | cut -d" " -f1 | egrep -v "^Kernel|Iface|lo") 145 | write_header " Network information " 146 | echo "Total network interfaces found : $(wc -w <<<${devices})" 147 | 148 | echo "*** IP Addresses Information ***" 149 | ip -4 address show 150 | 151 | echo "***********************" 152 | echo "*** Network routing ***" 153 | echo "***********************" 154 | netstat -nr 155 | 156 | echo "**************************************" 157 | echo "*** Interface traffic information ***" 158 | echo "**************************************" 159 | netstat -i 160 | 161 | pause 162 | } 163 | 164 | # Purpose - Display a list of users currently logged on 165 | # display a list of receltly loggged in users 166 | function user_info(){ 167 | local cmd="$1" 168 | case "$cmd" in 169 | who) write_header " Who is online "; who -H; pause ;; 170 | last) write_header " List of last logged in users "; last ; pause ;; 171 | esac 172 | } 173 | 174 | # Purpose - Display used and free memory info 175 | function mem_info(){ 176 | write_header " Free and used memory " 177 | free -m 178 | 179 | echo "*********************************" 180 | echo "*** Virtual memory statistics ***" 181 | echo "*********************************" 182 | vmstat 183 | echo "***********************************" 184 | echo "*** Top 5 memory eating process ***" 185 | echo "***********************************" 186 | ps auxf | sort -nr -k 4 | head -5 187 | pause 188 | } 189 | # Purpose - Get input via the keyboard and make a decision using case..esac 190 | function read_input(){ 191 | local c 192 | read -p "Enter your choice [ 1 - 7 ] " c 193 | case $c in 194 | 1) os_info ;; 195 | 2) host_info ;; 196 | 3) net_info ;; 197 | 4) user_info "who" ;; 198 | 5) user_info "last" ;; 199 | 6) mem_info ;; 200 | 7) echo "Bye!"; exit 0 ;; 201 | *) 202 | echo "Please select between 1 to 7 choice only." 203 | pause 204 | esac 205 | } 206 | 207 | # ignore CTRL+C, CTRL+Z and quit singles using the trap 208 | trap '' SIGINT SIGQUIT SIGTSTP 209 | 210 | # main logic 211 | while true 212 | do 213 | clear 214 | show_menu # display memu 215 | read_input # wait for user input 216 | done 217 | ``` 218 | 219 | # Dialog 220 | 221 | ```sh 222 | $ sudo apt-get update 223 | $ sudo apt-get install dialog 224 | 225 | $ yum install dialog 226 | ``` 227 | 228 | dialog 命令允许您使用 Shell 脚本中的对话框显示各种问题或显示消息。使用对话框实用程序创建 TTY(终端)对话框。 229 | 230 | ```sh 231 | dialog --common-options --boxType "Text" Height Width --box-specific-option 232 | ``` 233 | -------------------------------------------------------------------------------- /10~Shell 命令/命令执行/README.md: -------------------------------------------------------------------------------- 1 | # Shell 命令执行基础 2 | 3 | # 执行环境 4 | 5 | Shell 首先检查命令是否是内部命令,不是的话再检查是否是一个应用程序,这里的应用程序可以是 Linux 本身的实用程序,比如 ls rm,然后 Shell 试着在搜索路径(`$PATH`)里寻找这些应用程序。搜索路径是一个能找到可执行程序的目录列表。如果你键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,将会显示一条错误信息。而如果命令被成功的找到的话,Shell 的内部命令或应用程序将被分解为系统调用并传给 Linux 内核。 6 | 7 | ## 环境变量 8 | 9 | 默认的 Bourne Shell 会从 `~/.profile` 文件中读取并且执行命令。而 Bash 会从 10 | 11 | ~/.profile is the place to put stuff that applies to your whole session, such as programs that you want to start when you log in (but not graphical programs, they go into a different file), and environment variable definitions. 12 | 13 | ~/.bashrc is the place to put stuff that applies only to bash itself, such as alias and function definitions, shell options, and prompt settings. (You could also put key bindings there, but for bash they normally go into ~/.inputrc.) 14 | 15 | ~/.bash_profile can be used instead of ~/.profile, but it is read by bash only, not by any other shell. (This is mostly a concern if you want your initialization files to work on multiple machines and your login shell isn't bash on all of them.) This is a logical place to include ~/.bashrc if the shell is interactive. I recommend the following contents in ~/.bash_profile: 16 | 17 | ```sh 18 | if [ -r ~/.profile ]; then . ~/.profile; fi 19 | 20 | case "$-" in *i*) if [ -r ~/.bashrc ]; then . ~/.bashrc; fi;; esac 21 | ``` 22 | 23 | ![](https://zwischenzugs.files.wordpress.com/2018/01/shell-startup-actual.png?w=840) 24 | 25 | ## 执行目录 26 | 27 | `cd` 命令可以切换工作路径,输入 `cd ~` 可以进入 home 目录,`..` 返回上一级目录。要访问你的 home 目录中的文件,可以使用前缀 `~`(例如 `~/.bashrc`)。 28 | 29 | 在 Shell 脚本里则用环境变量 `$HOME` 指代 home 目录的路径。而在 Bash 脚本中,同样可以使用 `cd` 命令切换工作目录,但是对于那些需要临时切换目录的情景,我们可以使用小括号进行控制: 30 | 31 | ```sh 32 | # do something in current dir 33 | (cd /some/other/dir && other-command) 34 | # continue in original dir 35 | ``` 36 | 37 | 在 Shell 脚本的首部,我们经常会定位到脚本所在的目录: 38 | 39 | ```sh 40 | #!/bin/bash 41 | cd "$(dirname "$0")" # Go to the script's directory 42 | ``` 43 | 44 | # 历史记录 45 | 46 | 最常用的历史记录检索方式就是使用 `history`: 47 | 48 | ```sh 49 | # 顺序查看 50 | $ history | more 51 | 1 2008-08-05 19:02:39 service network restart 52 | ... 53 | 54 | # 查看最新的命令 55 | $ history | tail -3 56 | ``` 57 | 58 | 反馈的命令记录中存在编号,我们可以根据编号来重复执行历史记录中的命令: 59 | 60 | ```sh 61 | $ !4 62 | cat /etc/redhat-release 63 | Fedora release 9 (Sulphur) 64 | ``` 65 | 66 | 使用 `history -c` 能够清除所有的历史记录,或者设置 HISTSIZE 环境变量以避免记录: 67 | 68 | ```sh 69 | $ export HISTSIZE=0 70 | $ history 71 | 72 | # [Note that history did not display anything] 73 | ``` 74 | 75 | `history` 命令往往只会记录用户交互式的命令内容,更详细的操作记录可以使用 `more /var/log/messages` 查看记录文件。 76 | 77 | # 输入辅助 78 | 79 | 使用 `ctrl-w` 删除最后一个单词,使用 `ctrl-u` 删除自光标处到行首的内容,使用 `ctrl-k` 删除自光标处到行末的内容;使用 `ctrl-b` 与 `ctrl-f` 按字母进行前后移动;使用 `ctrl-a` 将光标移动到行首,使用 `ctrl-e` 将光标移动到行末。 80 | 81 | 为了方便编辑长命令,我们可以设置自己的默认编辑器(系统默认是 Emacs),`export EDITOR=vim`,使用 `ctrl-x ctrl-e` 会打开一个编辑器来编辑当前输入的命令。 82 | 83 | 对于较长的命令,可以使用 `alias` 创建常用命令的快捷方式,譬如 `alias ll = 'ls -latr'` 创建新的名为 `ll` 的快捷方式。也可以使用 `{...}` 来进行命令简写: 84 | 85 | ```sh 86 | # 同时移动两个文件 87 | $ mv foo.{txt,pdf} some-dir 88 | # Copy 'filename' as 'filename.bak. 89 | $ cp filename{,.bak} 90 | 91 | # 会被扩展成 cp somefile somefile.bak 92 | $ cp somefile{,.bak} 93 | 94 | # 会被扩展成所有可能的组合,并创建一个目录树 95 | $ mkdir -p test-{a,b,c}/subtest-{1,2,3} 96 | ``` 97 | 98 | 我们也可以使用 Control+R 来进行交互式检索: 99 | 100 | ```sh 101 | $ [Press Ctrl+R from the command prompt, 102 | which will display the reverse-i-search prompt] 103 | (reverse-i-search)`red': cat /etc/redhat-release 104 | [Note: Press enter when you see your command, 105 | which will execute the command from the history] 106 | ``` 107 | 108 | # 命令连接 109 | 110 | 如果我们希望仅在前一个命令执行成功之后执行后一个命令,则需要使用 && 命令连接符: 111 | 112 | ```sh 113 | cd /my_folder && rm *.jar && svn co path to repo && mvn compile package install 114 | 115 | # 也可以写为多行模式 116 | cd /my_folder \ 117 | && rm *.jar \ 118 | && svn co path to repo \ 119 | && mvn compile package install 120 | ``` 121 | 122 | 如果我们希望能够无论前一个命令是否成功皆开始执行下一个命令,则可以使用 `;` 分隔符: 123 | 124 | ```sh 125 | cd /my_folder; rm *.jar; svn co path to repo; mvn compile package install 126 | ``` 127 | 128 | ## 管道 129 | 130 | - `|` 一种管道,其左方是一个命令的 STNOUT,将作为管道右方的另一个命令的 STDIN。例如:echo ‘test text’ | wc -l 131 | 132 | - `>` 大于号,作用是取一个命令 STDOUT 位于左方,并将其写入 / 覆写(overwrite)入右方的一个新文件。例如:ls > tmp.txt 133 | 134 | - `>>` 两个大于号,作用是取一个命令 STDOUT 位于左方,并将其追加到右方的一个新的或现有文件中。例如:date >> tmp.txt 135 | 136 | ## 参数切割 137 | 138 | 使用 `xargs` 将长列表参数切分为可用的短列表,常用的命令譬如: 139 | 140 | ```sh 141 | # 搜索名字中包含 Stock 的文件 142 | $ find . -name "*.java" | xargs grep "Stock" 143 | 144 | # 清除所有后缀名为 tmp 的临时文件 145 | $ find /tmp -name "*.tmp" | xargs rm 146 | ``` 147 | 148 | ## 后台运行 149 | 150 | 当用户注销 (logout) 或者网络断开时,终端会收到 HUP (hangup) 信号从而关闭其所有子进程;我们可以通过让进程忽略 HUP 信号,或者让进程运行在新的会话里从而成为不属于此终端的子进程来进行后台执行。 151 | 152 | ```sh 153 | # nohup 方式 154 | $ nohup ping www.ibm.com & 155 | 156 | # screen 方式,创建并且连接到新的屏幕 157 | # 创建新的伪终端 158 | $ screen -dmS Urumchi 159 | 160 | # 连接到当前伪终端 161 | $ screen -r Urumchi 162 | ``` 163 | 164 | # Tmux 165 | 166 | Tmux 是一个工具,用于在一个终端窗口中运行多个终端会话;还可以通过 Tmux 使终端会话运行于后台或是按需接入、断开会话。本部分是对于 [tmux shortcuts & cheatsheet](https://parg.co/UrT) 一文的总结提取 167 | 168 | ```sh 169 | # 启动新会话 170 | tmux 171 | 172 | # 指定新 Session 的名称,并创建 173 | tmux new -s myname 174 | 175 | # 列举出所有的 Session 176 | tmux ls 177 | 178 | # 附着到某个 Session 179 | tmux a # (or at, or attach) 180 | 181 | # 根据指定的名称附着到 Session 182 | tmux a -t myname 183 | 184 | # 关闭某个 Session 185 | tmux kill-session -t myname 186 | 187 | # 关闭全部 Session 188 | tmux ls | grep : | cut -d. -f1 | awk '{print substr($1, 0, length($1)-1)}' | xargs kill 189 | ``` 190 | 191 | 在 Tmux 中,使用 `ctrl + b` 前缀,然后可以使用如下命令 192 | 193 | ```sh 194 | # Sessions 195 | :new new session 196 | s list sessions 197 | $ name session 198 | [ view history 199 | d detach 200 | 201 | # Windows (tabs) 202 | c create window 203 | w list windows 204 | n next window 205 | p previous window 206 | f find window 207 | , name window 208 | & kill window 209 | 210 | # Panes (splits) 211 | % vertical split 212 | " horizontal split 213 | 214 | o swap panes 215 | q show pane numbers 216 | x kill pane 217 | + break pane into window (e.g. to select text by mouse to copy) 218 | - restore pane from window 219 | ⍽ space - toggle between layouts 220 | q (Show pane numbers, when the numbers show up type the key to goto that pane) 221 | { (Move the current pane left) 222 | } (Move the current pane right) 223 | z toggle pane zoom 224 | 225 | # Resizing Panes 226 | PREFIX : resize-pane -D (Resizes the current pane down) 227 | PREFIX : resize-pane -U (Resizes the current pane upward) 228 | PREFIX : resize-pane -L (Resizes the current pane left) 229 | PREFIX : resize-pane -R (Resizes the current pane right) 230 | PREFIX : resize-pane -D 20 (Resizes the current pane down by 20 cells) 231 | PREFIX : resize-pane -U 20 (Resizes the current pane upward by 20 cells) 232 | PREFIX : resize-pane -L 20 (Resizes the current pane left by 20 cells) 233 | PREFIX : resize-pane -R 20 (Resizes the current pane right by 20 cells) 234 | PREFIX : resize-pane -t 2 20 (Resizes the pane with the id of 2 down by 20 cells) 235 | PREFIX : resize-pane -t -L 20 (Resizes the pane with the id of 2 left by 20 cells) 236 | ``` 237 | -------------------------------------------------------------------------------- /10~Shell 命令/CentOS/README.md: -------------------------------------------------------------------------------- 1 | # CentOS 配置手册 2 | 3 | **常用命令:** 4 | 5 | ``` 6 | 查看所有网卡IP地址:ip addr 7 | 8 | 启动防火墙:systemctl start firewalld 9 | 10 | 停止防火墙:systemctl stop firewalld 11 | 12 | 禁止防火墙开机启动:systemctl disable firewalld 13 | 14 | 列出正在运行的服务状态:systemctl 15 | 16 | 启动一个服务:systemctl start postfix 17 | 18 | 关闭一个服务:systemctl stop postfix 19 | 20 | 重启一个服务:systemctl restart postfix 21 | 22 | 显示一个服务的状态:systemctl status postfix 23 | 24 | 在开机时启用一个服务:systemctl enable postfix 25 | 26 | 在开机时禁用一个服务:systemctl disable postfix 27 | 28 | 查看服务是否开机启动:systemctl is-enabled postfix.service;echo $? 29 | 30 | 查看已启动的服务列表:systemctl list-unit-files|grep enabled 31 | 32 | 查看运行级别:runlevel 33 | 34 | 设置系统默认启动运行级别3(命令行模式):ln -sf /lib/systemd/system/multi-user.target /etc/systemd/system/default.target 35 | 36 | 设置系统默认启动运行级别5(图形界面模式):ln -sf/lib/systemd/system/graphical.target/etc/systemd/system/default.target 37 | 38 | 在线用户:who 39 | 40 | 联网状态:netstat -a 41 | 42 | 后台执行的程序:ps -aux 43 | 44 | 查看Linux系统分区信息:fdisk -l 45 | 46 | 查看分区使用状况:df -lh 47 | 48 | 重启:init 6 或reboot 49 | 关机:init 0或shutdown 50 | ``` 51 | 52 | **使用 firewall 开放 Linux 端口**: 53 | 54 | 开启 80 端口(开放 10050 到 10060 端口): 55 | 56 | ``` 57 | firewall-cmd --zone=public --permanent --add-port=80/tcp 58 | firewall-cmd --permanent --zone=public --add-port=10050-10060/tcp 59 | ``` 60 | 61 | 重启防火墙: 62 | 63 | ``` 64 | firewall-cmd --reload 65 | ``` 66 | 67 | 命令含义: 68 | 69 | 命令含义: 70 | --zone #作用域 71 | 72 | 命令含义: 73 | --zone #作用域 74 | --add-port=80/tcp #添加端口,格式为:端口/通讯协议 75 | 76 | 命令含义: 77 | --zone #作用域 78 | --add-port=80/tcp #添加端口,格式为:端口/通讯协议 79 | --permanent #永久生效,没有此参数重启后失效 80 | 81 | 命令含义: 82 | --zone #作用域 83 | --add-port=80/tcp #添加端口,格式为:端口/通讯协议 84 | --permanent #永久生效,没有此参数重启后失效其它 firewall 可以用 firewall-cmd -h 查询 85 | 86 | **网卡操作命令(ip):** 87 | 88 | CentOS 7 用 ip 命令代替 ifconfig 命令: 89 | 90 | ``` 91 | ip [选项] 操作对象{link|addr|route...} 92 | # ip link show # 显示网络接口信息 93 | # ip link set eth0 upi # 开启网卡 94 | # ip link set eth0 down # 关闭网卡 95 | # ip link set eth0 promisc on # 开启网卡的混合模式 96 | # ip link set eth0 promisc offi # 关闭网卡的混个模式 97 | # ip link set eth0 txqueuelen 1200 # 设置网卡队列长度 98 | # ip link set eth0 mtu 1400 # 设置网卡最大传输单元 99 | # ip addr show # 显示网卡IP信息 100 | # ip addr add 192.168.0.1/24 dev eth0 # 设置eth0网卡IP地址192.168.0.1 101 | # ip addr del 192.168.0.1/24 dev eth0 # 删除eth0网卡IP地址 102 | # ip route list # 查看路由信息 103 | # ip route add 192.168.4.0/24 via 192.168.0.254 dev eth0 # 设置192.168.4.0网段的网关为192.168.0.254,数据走eth0接口 104 | # ip route add default via 192.168.0.254 dev eth0 # 设置默认网关为192.168.0.254 105 | # ip route del 192.168.4.0/24 # 删除192.168.4.0网段的网关 106 | # ip route del default # 删除默认路由 107 | ``` 108 | 109 | **DHCP:ip 地址释放和获取** 110 | 111 | ``` 112 | #dhclient -r #释放ip 113 | #dhclient #重新获取ip 114 | ``` 115 | 116 | (注:使用 ln -sf /lib/systemd/system/multi-user.target /etc/systemd/system/default.target 设置 VM 中 CentOS7 的运行级别为 3 之后,需要手动设置其 ip 地址,才能使用 XShell 连接) 117 | 118 | **Linux 下安装(卸载)KDE 和 GNOME:** 119 | 120 | 1.查看是否安装了桌面系统 121 | 122 | ``` 123 | yum grouplist 124 | yum grouplist |more ←如果输出太长,可以使用“|more”分页显示 125 | ``` 126 | 127 | 在 grouplist 的输出结果中的“InstalledGroups:”部分中, 128 | 129 | 在 grouplist 的输出结果中的“InstalledGroups:”部分中,如果你能找到“XWindow System”和“GNOME Desktop Environment 或 KDE (K DesktopEnvironment)或 XFCE-4.4”的话,证明你安装了桌面环境。 130 | 131 | 在 grouplist 的输出结果中的“InstalledGroups:”部分中,如果你能找到“XWindow System”和“GNOME Desktop Environment 或 KDE (K DesktopEnvironment)或 XFCE-4.4”的话,证明你安装了桌面环境。2.如果系统安装之前采用最小化安装,没有安装桌面,那么先安装桌面系统: 132 | 133 | ``` 134 | yum group install "X Window System" 135 | ``` 136 | 137 | 3.安装 GNOME 桌面环境 138 | 139 | ``` 140 | yum group install "Desktop" 141 | ``` 142 | 143 | 4.安装 KDE 桌面环境 144 | 145 | ``` 146 | yumgroupinstall "KDE Desktop" 147 | ``` 148 | 149 | 5.卸载 GNOME 桌面环境 150 | 151 | ``` 152 | yum group remove "GNOME Desktop Environment" 153 | ``` 154 | 155 | 6.卸载 KDE 桌面环境 156 | 157 | ``` 158 | yum group remove "KDE Desktop" 159 | ``` 160 | 161 | **从命令行界面切换到图形界面:** 162 | 163 | **从命令行界面切换到图形界面:** 164 | 方法 1:运行命令 165 | 166 | ``` 167 | startx 168 | ``` 169 | 170 | 需要先配置图形界面信息 171 | 172 | 需要先配置图形界面信息(old)方法 2:修改/etc/inittab 文件中的 173 | 174 | ``` 175 | id:3:initdefault,将3改为5,重新启动系统; 176 | ``` 177 | 178 | 方法 3:进入图形界面: 179 | 180 | ``` 181 | init 5 182 | ``` 183 | 184 | **从图形界面进入命令行界面:** 185 | 186 | ``` 187 | init 3 188 | ``` 189 | 190 | **开机默认文本界面:** 191 | 192 | ``` 193 | systemctl set-default multi-user.target 194 | ``` 195 | 196 | **开机默认图形界面:** 197 | 198 | ``` 199 | systemctl set-default graphical.target 200 | ``` 201 | 202 | shutdown 关机命令: 203 | 204 | ``` 205 | shutdown now # 立即关机 206 | shutdown +2 # 2 min 后关机 207 | shutdown 10:01 # 10:01关机 208 | shutdown +2 "The machine will shutdown" # 2min 后关机,并通知在线者 209 | ``` 210 | 211 | 真机环境中,在图形界面和文本界面间快捷键切换: 212 | 213 | ``` 214 | Ctrl+Alt+F(n), 其中F(n)为F1-F6,为6个控制台; 215 | Ctrl+ALT+F7; 216 | eg:CTRL+ALT+F1是进入文本界面,CTRL+ALT+F7才是图形界面。 217 | ``` 218 | 219 | **虚拟机静态 IP 设置及主机名设置绑定** 220 | 221 | 打开终端,root 权限下:vim /etc/sysconfig/network-scripts/ifcfg-enoXXXX, 222 | 223 | 在插入模式下: 224 | 225 | 在插入模式下:修改 226 | 227 | ``` 228 | BOOTPROTO=static 229 | ONBOOT=yes 230 | ``` 231 | 232 | 例如添加: 233 | 234 | ``` 235 | IPADDR0=192.168.145.130 236 | NETMASK=255.255.255.0 237 | GATEWAY0=192.168.145.1 238 | DNS1=8.8.8.8 239 | DNS2=8.8.4.4 240 | ``` 241 | 242 | hostname crs811 #设置主机名为 crs811 243 | 244 | ``` 245 | vi /etc/hosts #编辑配置文件 246 | 127.0.0.1 localhost www #修改localhost.localdomain为www 247 | ``` 248 | 249 | 重启网络: 250 | 251 | ``` 252 | systemctl restart network 253 | ``` 254 | 255 | **Centos7 默认没有 ifconfig 和 netstat** 256 | 257 | ifconfig 使用 ip addr 命令代替, 258 | 259 | ifconfig 使用 ip addr 命令代替,在 cenots6 下的 ss 命令可以代替 netstat,但是现在的 ss 和以前的完全是两样,还是得装上才行方便查看端口占用和 tcp 链接攻击等等。 260 | 261 | ifconfig 使用 ip addr 命令代替,在 cenots6 下的 ss 命令可以代替 netstat,但是现在的 ss 和以前的完全是两样,还是得装上才行方便查看端口占用和 tcp 链接攻击等等。 262 | Centos7 下把 net-tools 包装上就好了: 263 | 264 | ``` 265 | yum install net-tools 266 | ``` 267 | 268 | **CentOS7 中  php 默认 5.4, apache 默认 2.4,Mariadb 代替了 mysql** 269 | 270 | **CentOS7 dhcp 启动失败可能原因** 271 | 272 | 出现问题的可能有以下几个可能: 273 | 274 | 出现问题的可能有以下几个可能: 275 | \1. 配置文件有问题。 276 | 277 | 出现问题的可能有以下几个可能: 278 | \1. 配置文件有问题。 279 | 1.1 内容不符合语法结构,例如,少个分号; 280 | 281 | 出现问题的可能有以下几个可能: 282 | \1. 配置文件有问题。 283 | 1.1 内容不符合语法结构,例如,少个分号; 284 | 1.2 声明的子网和子网掩码不符合; 285 | 286 | 出现问题的可能有以下几个可能: 287 | \1. 配置文件有问题。 288 | 1.1 内容不符合语法结构,例如,少个分号; 289 | 1.2 声明的子网和子网掩码不符合; 290 | \2. 主机 IP 地址和声明的子网不在同一网段。 291 | 292 | 出现问题的可能有以下几个可能: 293 | \1. 配置文件有问题。 294 | 1.1 内容不符合语法结构,例如,少个分号; 295 | 1.2 声明的子网和子网掩码不符合; 296 | \2. 主机 IP 地址和声明的子网不在同一网段。 297 | \3. 主机没有配置静态 IP 地址。 298 | 299 | 出现问题的可能有以下几个可能: 300 | \1. 配置文件有问题。 301 | 1.1 内容不符合语法结构,例如,少个分号; 302 | 1.2 声明的子网和子网掩码不符合; 303 | \2. 主机 IP 地址和声明的子网不在同一网段。 304 | \3. 主机没有配置静态 IP 地址。 305 | \4. 配置文件路径出问题,比如在 RHEL6 以下的版本中,配置文件保存在了/etc/dhcpd.conf,但是在 rhel6 及以上版本中,却保存在了/etc/dhcp/dhcpd.conf。 306 | 307 | **Linux 课程(Cent OS)常用服务、工具和命令安装列表:** 308 | 309 | ``` 310 | 服务: 311 | 312 | DNS: #yum -y install bind-chroot 313 | 314 | DHCP: #yum -y install dhcp 315 | 316 | Samba: #yum -y install samba samba-client samba-common 317 | 318 | Web套装下载:https://www.apachefriends.org/index.html 319 | 320 | (安装命令:1 #chmod 751*.run 2 #./*.run或#sh *.run) 321 | 322 | 命令: 323 | 324 | dig - 查询域名解析: #yum install bind-utils 325 | 326 | wget - 下载文件命令: #yum install wget 327 | 328 | CentOS 6 之前常用网络命令安装: #yum install net-tools 329 | ``` 330 | -------------------------------------------------------------------------------- /03~网络/多路复用/epoll/99~参考资料/2020-深入浅出让你彻底理解 epoll.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://juejin.cn/post/6940453515353391140#comment) 2 | 3 | # 深入浅出让你彻底理解 epoll 4 | 5 | # 1.简介 6 | 7 | Epoll 是个很老的知识点,是后端工程师的经典必修课。这种知识具备的特点就是研究的人多,所以研究的趋势就会越来越深。当然分享的人也多,由于分享者水平参差不齐,也产生的大量错误理解。今天我再次分享 epoll,肯定不会列个表格,对比一下差异,那就太无聊了。我将从线程阻塞的原理,中断优化,网卡处理数据过程出发,深入的介绍 epoll 背后的原理,最后还会 diss 一些流行的观点。相信无论你是否已经熟悉 epoll,本文都会对你有价值。 8 | 9 | # 2.引言 10 | 11 | 正文开始前,先问大家几个问题。 12 | 13 | 1. epoll 性能到底有多高。很多文章介绍 epoll 可以轻松处理几十万个连接。而传统 IO 只能处理几百个连接,是不是说 epoll 的性能就是传统 IO 的千倍呢? 14 | 15 | 2. 很多文章把网络 IO 划分为阻塞,非阻塞,同步,异步。并表示:非阻塞的性能比阻塞性能好,异步的性能比同步性能好。 16 | 17 | - 如果说阻塞导致性能低,那传统 IO 为什么要阻塞呢? 18 | - epoll 是否需要阻塞呢? 19 | - Java 的 NIO 和 AIO 底层都是 epoll 实现的,这又怎么理解同步和异步的区别? 20 | 21 | 3. 都是 IO 多路复用。 22 | 23 | - 既生瑜何生亮,为什么会有 select,poll 和 epoll 呢? 24 | - 为什么 epoll 比 select 性能高? 25 | 26 | # 3.初识 epoll 27 | 28 | epoll 是 Linux 内核的可扩展 I/O 事件通知机制,其最大的特点就是性能优异。下图是 libevent(一个知名的异步事件处理软件库)对 select,poll,epoll ,kqueue 这几个 I/O 多路复用技术做的性能测试。 29 | 30 | ![Libevent Benchmark](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230503231114.png) 31 | 32 | 很多文章在描述 epoll 性能时都引用了这个基准测试,但少有文章能够清晰的解释这个测试结果。这是一个限制了 100 个活跃连接的基准测试,每个连接发生 1000 次读写操作为止。纵轴是请求的响应时间,横轴是持有的 socket 句柄数量。随着句柄数量的增加,epoll 和 kqueue 响应时间几乎无变化,而 poll 和 select 的响应时间却增长了非常多。 33 | 34 | 可以看出来,epoll 性能是很高的,并且随着监听的文件描述符的增加,epoll 的优势更加明显。不过,这里限制的 100 个连接很重要。epoll 在应对大量网络连接时,只有活跃连接很少的情况下才能表现的性能优异。换句话说,epoll 在处理大量非活跃的连接时性能才会表现的优异。如果 15000 个 socket 都是活跃的,epoll 和 select 其实差不了太多。 35 | 36 | 为什么 epoll 的高性能有这样的局限性?问题好像越来越多了,看来我们需要更深入的研究了。 37 | 38 | # 4.epoll 背后的原理 39 | 40 | ## 4.1 阻塞 41 | 42 | ### 4.1.1 为什么阻塞 43 | 44 | 我们以网卡接收数据举例,回顾一下之前我分享过的网卡接收数据的过程。 45 | 46 | ![网卡接收数据的过程](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230503231352.png) 47 | 48 | 为了方便理解,我尽量简化技术细节,可以把接收数据的过程分为 4 步: 49 | 50 | 1. NIC(网卡) 接收到数据,通过 DMA 方式写入内存(Ring Buffer 和 sk_buff)。 51 | 2. NIC 发出中断请求(IRQ),告诉内核有新的数据过来了。 52 | 3. Linux 内核响应中断,系统切换为内核态,处理 Interrupt Handler,从 RingBuffer 拿出一个 Packet,并处理协议栈,填充 Socket 并交给用户进程。 53 | 4. 系统切换为用户态,用户进程处理数据内容。 54 | 55 | 网卡何时接收到数据是依赖发送方和传输路径的,这个延迟通常都很高,是毫秒(ms)级别的。而应用程序处理数据是纳秒(ns)级别的。也就是说整个过程中,内核态等待数据,处理协议栈是个相对很慢的过程。这么长的时间里,用户态的进程是无事可做的,因此用到了“阻塞(挂起)”。 56 | 57 | ### 4.1.2 阻塞不占用 cpu 58 | 59 | 阻塞是进程调度的关键一环,指的是进程在等待某事件发生之前的等待状态。请看下表,在 Linux 中,进程状态大致有 7 种(在 include/linux/sched.h 中有更多状态): 60 | 61 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230503231452.png) 62 | 63 | 从说明中其实就可以发现,“可运行状态”会占用 CPU 资源,另外创建和销毁进程也需要占用 CPU 资源(内核)。重点是,当进程被"阻塞/挂起"时,是不会占用 CPU 资源的。换个角度来讲。为了支持多任务,Linux 实现了进程调度的功能(CPU 时间片的调度)。而这个时间片的切换,只会在“可运行状态”的进程间进行。因此“阻塞/挂起”的进程是不占用 CPU 资源的。 64 | 65 | 另外讲个知识点,为了方便时间片的调度,所有“可运行状态”状态的进程,会组成一个队列,就叫**工作队列**。 66 | 67 | ### 4.1.3 阻塞的恢复 68 | 69 | 内核当然可以很容易的修改一个进程的状态,问题是网络 IO 中,内核该修改那个进程的状态。 70 | 71 | ![网卡](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230503231757.png) 72 | 73 | socket 结构体,包含了两个重要数据:进程 ID 和端口号。进程 ID 存放的就是执行 connect,send,read 函数,被挂起的进程。在 socket 创建之初,端口号就被确定了下来,操作系统会维护一个端口号到 socket 的数据结构。 74 | 75 | 当网卡接收到数据时,数据中一定会带着端口号,内核就可以找到对应的 socket,并从中取得“挂起”进程的 ID。将进程的状态修改为“可运行状态”(加入到工作队列)。此时内核代码执行完毕,将控制权交还给用户态。通过正常的“CPU 时间片的调度”,用户进程得以处理数据。 76 | 77 | ### 4.1.4 进程模型 78 | 79 | 上面介绍的整个过程,基本就是 BIO(阻塞 IO)的基本原理了。用户进程都是独立的处理自己的业务,这其实是一种符合进程模型的处理方式。 80 | 81 | ## 4.2 上下文切换的优化 82 | 83 | 上面介绍的过程中,有两个地方会造成频繁的上下文切换,效率可能会很低。 84 | 85 | - 如果频繁的收到数据包,NIC 可能频繁发出中断请求(IRQ)。CPU 也许在用户态,也许在内核态,也许还在处理上一条数据的协议栈。但无论如何,CPU 都要尽快的响应中断。这么做实际上非常低效,造成了大量的上下文切换,也可能导致用户进程长时间无法获得数据。(即使是多核,每次协议栈都没有处理完,自然无法交给用户进程) 86 | 87 | - 每个 Packet 对应一个 socket,每个 socket 对应一个用户态的进程。这些用户态进程转为“可运行状态”,必然要引起进程间的上下文切换。 88 | 89 | ### 4.2.1 网卡驱动的 NAPI 机制 90 | 91 | 在 NIC 上,解决频繁 IRQ 的技术叫做 New API(NAPI) 。原理其实特别简单,把 Interrupt Handler 分为两部分。 92 | 93 | - 函数名为 napi_schedule,专门快速响应 IRQ,只记录必要信息,并在合适的时机发出软中断 softirq。 94 | - 函数名为 netrxaction,在另一个进程中执行,专门响应 napi_schedule 发出的软中断,批量的处理 RingBuffer 中的数据。 95 | 96 | 所以使用了 NAPI 的驱动,接收数据过程可以简化描述为: 97 | 98 | ![网卡接收数据简化](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230503232034.png) 99 | 100 | 1. NIC 接收到数据,通过 DMA 方式写入内存(Ring Buffer 和 sk_buff)。 101 | 2. NIC 发出中断请求(IRQ),告诉内核有新的数据过来了。 102 | 3. driver 的 napi_schedule 函数响应 IRQ,并在合适的时机发出软中断(NET_RX_SOFTIRQ) 103 | 4. driver 的 net_rx_action 函数响应软中断,从 Ring Buffer 中批量拉取收到的数据。并处理协议栈,填充 Socket 并交给用户进程。 104 | 5. 系统切换为用户态,多个用户进程切换为“可运行状态”,按 CPU 时间片调度,处理数据内容。 105 | 106 | 一句话概括就是:等着收到一批数据,再一次批量的处理数据。 107 | 108 | ### 4.2.2 单线程的 IO 多路复用 109 | 110 | 内核优化“进程间上下文切换”的技术叫的“IO 多路复用”,思路和 NAPI 是很接近的。每个 socket 不再阻塞读写它的进程,而是用一个专门的线程,批量的处理用户态数据,这样就减少了线程间的上下文切换。 111 | 112 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230504104135.png) 113 | 114 | 作为 IO 多路复用的一个实现,select 的原理也很简单。所有的 socket 统一保存执行 select 函数的(监视进程)进程 ID。任何一个 socket 接收了数据,都会唤醒“监视进程”。内核只要告诉“监视进程”,哪些 socket 已经就绪,监视进程就可以批量处理了。 115 | 116 | ## 4.3 IO 多路复用的进化 117 | 118 | ### 4.3.1 对比 epoll 与 select 119 | 120 | select,poll 和 epoll 都是“IO 多路复用”,那为什么还会有性能差距呢?篇幅限制,这里我们只简单对比 select 和 epoll 的基本原理差异。对于内核,同时处理的 socket 可能有很多,监视进程也可能有多个。所以监视进程每次“批量处理数据”,都需要告诉内核它“关心的 socket”。内核在唤醒监视进程时,就可以把“关心的 socket”中,就绪的 socket 传给监视进程。 121 | 122 | 换句话说,在执行系统调用 select 或 epoll_create 时,入参是“关心的 socket”,出参是“就绪的 socket”。而 select 与 epoll 的区别在于: 123 | 124 | - select (一次 O(n)查找) 125 | 126 | 1. 每次传给内核一个用户空间分配的 fd_set 用于表示“关心的 socket”。其结构(相当于 bitset)限制了只能保存 1024 个 socket。 127 | 2. 每次 socket 状态变化,内核利用 fd_set 查询 O(1),就能知道监视进程是否关心这个 socket。 128 | 3. 内核是复用了 fd_set 作为出参,返还给监视进程(所以每次 select 入参需要重置)。 129 | 130 | 然而监视进程必须遍历一遍 socket 数组 O(n),才知道哪些 socket 就绪了。 131 | 132 | - epoll (全是 O(1)查找) 133 | 134 | 1. 每次传给内核一个实例句柄。这个句柄是在内核分配的红黑树 rbr+双向链表 rdllist。只要句柄不变,内核就能复用上次计算的结果。 135 | 2. 每次 socket 状态变化,内核就可以快速从 rbr 查询 O(1),监视进程是否关心这个 socket。同时修改 rdllist,所以 rdllist 实际上是“就绪的 socket”的一个缓存。 136 | 3. 内核复制 rdllist 的一部分或者全部(LT 和 ET),到专门的 epoll_event 作为出参。 137 | 138 | 所以监视进程,可以直接一个个处理数据,无需再遍历确认。 139 | 140 | ![select 与 epoll 代码对比](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230505133423.png) 141 | 142 | 另外,epoll_create 底层实现,到底是不是红黑树,其实也不太重要(完全可以换成 hashtable)。重要的是 efd 是个指针,其数据结构完全可以对外透明的修改成任意其他数据结构。 143 | 144 | ### 4.3.2 API 发布的时间线 145 | 146 | 另外,我们再来看看网络 IO 中,各个 api 的发布时间线。就可以得到两个有意思的结论。 147 | 148 | - 1983,socket 发布在 Unix(4.2 BSD) 149 | - 1983,select 发布在 Unix(4.2 BSD) 150 | - 1994,Linux 的 1.0,已经支持 socket 和 select 151 | - 1997,poll 发布在 Linux 2.1.23 152 | - 2002,epoll 发布在 Linux 2.5.44 153 | 154 | 1、socket 和 select 是同时发布的。这说明了,select 不是用来代替传统 IO 的。这是两种不同的用法(或模型),适用于不同的场景。 155 | 156 | 2、select、poll 和 epoll,这三个“IO 多路复用 API”是相继发布的。这说明了,它们是 IO 多路复用的 3 个进化版本。因为 API 设计缺陷,无法在不改变 API 的前提下优化内部逻辑。所以用 poll 替代 select,再用 epoll 替代 poll。 157 | 158 | # 5.Diss 环节 159 | 160 | ## 5.1 关于 IO 模型的分类 161 | 162 | 关于阻塞,非阻塞,同步,异步的分类,这么分自然有其道理。但是在操作系统的角度来看这样分类,容易产生误解,并不好。 163 | 164 | ![IO 模型](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230505134238.png) 165 | 166 | ### 5.1.1 阻塞和非阻塞 167 | 168 | Linux 下所有的 IO 模型都是阻塞的,这是收发数据的基本原理导致的。阻塞用户线程是一种高效的方式。你当然可以写一个程序,socket 设置成非阻塞模式,在不使用监视器的情况下,依靠死循环完成一次 IO 操作。但是这样做的效率实在是太低了,完全没有实际意义。 169 | 170 | 换句话说,阻塞不是问题,运行才是问题,运行才会消耗 CPU。IO 多路复用不是减少了阻塞,是减少了运行。上下文切换才是问题,IO 多路复用,通过减少运行的进程,有效的减少了上下文切换。 171 | 172 | ### 5.1.2 同步和异步 173 | 174 | Linux 下所有的 IO 模型都是同步的。BIO 是同步的,select 同步的,poll 同步的,epoll 还是同步的。Java 提供的 AIO,也许可以称作“异步”的。但是 JVM 是运行在用户态的,Linux 没有提供任何的异步支持。因此 JVM 提供的异步支持,和你自己封装成“异步”的框架是没有本质区别的(你完全可以使用 BIO 封装成异步框架)。 175 | 176 | 所谓的“同步“和”异步”只是两种事件分发器(event dispatcher)或者说是两个设计模式(Reactor 和 Proactor)。都是运行在用户态的,两个设计模式能有多少性能差异呢? 177 | 178 | - Reactor 对应 java 的 NIO,也就是 Channel,Buffer 和 Selector 构成的核心的 API。 179 | - Proactor 对应 java 的 AIO,也就是 Async 组件和 Future 或 Callback 构成的核心的 API。 180 | 181 | ### 5.1.3 我的分类 182 | 183 | 我认为 IO 模型只分两类: 184 | 185 | - 更加符合程序员理解和使用的,进程模型; 186 | - 更加符合操作系统处理逻辑的,IO 多路复用模型。 187 | 188 | 对于“IO 多路复用”的事件分发,又分为两类:Reactor 和 Proactor。 189 | --------------------------------------------------------------------------------