CS 之 书

关于各种代码编辑工具的使用技巧

关于 vscode 的各种使用技巧

问题描述: 在主机上更换系统为Arch OS,然后想要笔记本用Vsc(Visual Studio Code)的ssh远程连接功能连接该主机开发,但是不断尝试始终连接失败

解决方法:

  • 失败的原因,可以通过查看output窗口的输出来分析。可以点击view选项栏,点击output来拉出output窗口,如果你的output窗口是隐藏的话。
  • 注意到启动远程连接的时候,output信息中提示说远程主机的信息对不上,因为ssh连接建立的时候,会获取并记录远程主机的机器信息,并且与网络地址绑定存储在本地的~/.ssh/known_hosts文件中,重新与该网络地址(ip+端口)建立连接的时候会效验 远程主机提供的该信息是否与 之前记录的匹配, 如果不匹配则会发出警告,在控制台通过命令行连接的时候还会询问是否确认建立连接。所以笔者推测,是否之前建立连接的时候,因为OS不一样,生成的效验码不一样,然后被笔记本保存下来, 结果 现在新OS产生的效验码与 本地ssh保存的对不上,进而导致后续vsc ssh连接失败?
  • 笔者删除了本地的~/.ssh/known_hosts文件,重新启动vsc,重新与该网络地址建立ssh连接。因为删除了历史记录,相当于第一次建立连接,vsc询问是否continue,并提示是一个陌生的机器,笔者选择continue后成功连接上。
  • 所以另一种建立连接的成功方法可能是,先删除本地记录的主机信息,然后命令行建立连接记录上更新后的主机信息,可能这样子做之后vsc的ssh远程开发连接也能建立成功

vscode 插件下载失败问题 解决方案

问题的原因与解决方案

1. 关闭代理

ubuntu上使用vscode下载插件的时候出现XHR failed 原因:可能是网络不好.可能是之前开启了网络代理的原因.

解决方法:配置vscode不使用代理 打开Settings界面, 搜Proxy,选择关闭

2.离线下载插件

如果远程连接服务器进行开发,服务器网络受到魔法限制无法访问vscode插件仓库的时候,就需要通过离线方式下载插件。

去官网插件市场找到插件的.vsix文件,使用vscode提供的控制台code 工具载入.vsix文件

vscode无法监听文件变化的解决方法

  1. 可能是因为项目太大文件太多,监听钩子不够

解决方法

小林coding OS 篇 阅读笔记

Ref

  • https://xiaolincoding.com/os/

三. 操作系统结构

Ref

  • https://xiaolincoding.com/os/2_os_structure/linux_vs_windows.html

Note

1. windows内核和linux内核区别

windows闭源, linux开源

2. 什么是内核?

"作为应用连接外设的桥梁" 管理外设的中间层,让上层应用程序不需要直接与外设交互,而是可以通过内核提供的接口间接操作外设

3. 内核有哪些能力?

进程调度、内存管理、硬件管理、提供系统调用

4. 内核如何工作?

划分内存空间为用户空间,内核空间。 程序运行状态可以分为: 用户态、内核态。 用户态: 程序使用用户空间的时候 内核态: 程序使用内核空间的时候

交互机制: 用户态程序调用系统调用的时候,会产生一个中断, cpu中断用户程序的执行转而执行内核中断处理程序(进入内核态),处理完成后再主动触发中断, 让用户程序恢复执行

5. Linux的设计理念

Multitask(多任务),SMP(对称多处理), ELF(可执行文件链接格式),Monolithic Kernel(宏内核)

6. windows内核结构

windows7和windows10用的内核设计是混合型内核

7. 内核架构分类

宏内核、微内核、混合内核

内存管理

一. 为什么要有虚拟内存?

1. 为什么要有虚拟内存?

为应用程序开发提供统一的虚拟内存地址空间, 避免让应用程序处理直接操作物理内存可能存在的地址冲突问题。

虚拟地址: 程序使用的内存地址叫做虚拟内存地址 物理地址: 物理内存的地址叫做物理内存地址

单片机开发中程序是直接操作物理内存 linux中程序是操作虚拟内存

2. 虚拟内存工作的硬件依赖

虚拟地址通过cpu中的MMU转换为物理地址 MMU: 内存管理单元

3. 操作系统如何管理虚拟地址和物理地址之间的关系?

内存分段、内存分页

4. 什么是内存分段? (https://xiaolincoding.com/os/3_memory/vmem.html#%E5%86%85%E5%AD%98%E5%88%86%E6%AE%B5)

虚拟内存地址=段选择子+段内偏移,根据段选择子中的段号在段表项中找到对应段描述符(含有段基址和段界限),段内偏移加上段基址就是物理地址,同时能够判断段内偏移是否越界

优点: 不存在内部内存碎片,分配连续的内存空间 缺点: 存在外部内存碎片,内存交换效率低(比如段可能很大,一次换入/换出耗时较长)

5. 什么是内存分页? 什么是简单分页? 为什么要有多级页表?

分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。 简单分页是指只用1级页表来管理虚拟地址和物理地址之间的映射关系。 多级页表是为了节省内存空间而设计的。因为简单分页需要为每个虚拟页分配一个物理页框,如果虚拟地址空间很大,可能会浪费大量内存。多级页表用比较小的1级页表来覆盖所有虚拟页,后面的n级(n>=2)页表只在需要的时候才分配,节省了内存空间。

关键词: 页表、页号、页内偏移、多级页表

6. TLB(Translation Lookaside Buffer) 作用是什么?

TLB 是一个硬件缓存,用于存储最近使用的虚拟地址到物理地址的映射,以加速地址转换过程。

7. 什么是段页式内存管理?

先分段再分页,虚拟地址=段号+页号+页内偏移

8. 为什么linux要把所有段的基地址设为0?

关键词: 逻辑地址、线性地址(虚拟地址)、物理地址、Intel处理器发展史

9. linux的虚拟地址空间是如何分布的?

每个程序的内存空间中,从高到低分别是: 内核地址、栈地址、文件映射地址、堆地址、BSS地址、数据段、代码段

每个程序虚拟内存中的内核地址,关联的都是相同的物理内存。

10. 虚拟内存有什么作用?

  • 降低应用程序管理内存复杂性: 每个程序有独立的虚拟内存空间,避免开发者处理物理内存地址冲突问题
  • 提供更大的内存地址空间: 支持进程使用大于物理内存的内存空间
  • 提供更好的内存访问安全性: 比如通过页表项中的权限位来限制进程对内存的访问

11. 什么是内存交换?

内存交换是指把不常用的内存页换出到磁盘上,以释放内存空间。linux中使用swap分区来实现内存交换。

12. 什么是缺页中断? 缺页中断是内存访问的哪一步触发的?

在程序试图访问一个没有物理内存映射的虚拟内存页时,会触发缺页中断。缺页中断发生在虚拟地址转换为物理地址的过程中。 CPU尝试访问内存,MMU查询页表发现该虚拟页没有对应物理页映射,于是触发一个缺页中断(一个硬件中断),操作系统捕获到这个中断, 为这个中断处理程序分配物理页,并更新页表,使得虚拟页映射到新分配的物理页。之后,CPU可以继续执行原来的指令,访问到正确的物理内存。

二. malloc是如何分配内存的?

1. 内核空间和用户空间比例

32位系统中, 总空间4G, 内核空间1G, 用户空间3G 64位系统中, 内核空间和用户空间都是128T

2. malloc是系统调用吗? 底层是什么?

malloc是c库函数,底层调用brk()或mmap()系统调用来分配内存。

  • brk(): 调整堆顶指针的位置,用于分配小块内存
  • mmap(): 映射文件或匿名内存到进程的虚拟地址空间,用于分配大块内存

这里内存的大块小块的划分根据不同glibc的实现而不同, 一般阈值是128kB

3. malloc分配的是物理内存吗?

不,是虚拟内存

4. malloc(1)会分配多大内存?

不是1字节,而是预分配更大的空间做内存池待后面使用。

5. 如何查看进程的内存分配情况?

cat /proc/$pid/maps 查看进程的内存映射情况

6. free释放内存,会归还给操作系统吗?

使用brk()分配的内存,free后不会归还,而是缓存待下次使用; 使用mmap()分配的内存,free后会归还。

最终进程结束的时候,不管之前归没归还的资源都会回收

7. 为什么不全部使用mmap来分配内存?

使用brk()分配内存的内存会放到malloc的内存池中缓存使用,后续再分配的时候可能就不需要再走系统调用,减少系统调用次数。另外,之前分配的虚拟内存地址在页表中的物理地址映射可能还存在,复用之前分配的虚拟内存地址的话,还可能减少缺页中断重新映射物理地址的次数

8. 为什么不全部使用brk()分配内存?

brk分配的内存free的时候不会被释放而是被malloc的内存池管理等待复用,比较容易造成内存碎片,如果要分配大块内存的话很可能无法复用内存碎片的空间,而是需要继续增长brk分配的内存,所以对于大块的内存,使用mmap分配比较好,

9. free只传入一个内存地址,如何知道要释放多大内存?

把分配的内存块大小存放在了内存块地址前面的16字节中

10. malloc内存分配器是如何实现的?

https://mp.weixin.qq.com/s/Flt85kKbDEn_XD83mtYxUA?token=1646973705&lang=zh_CN

三. 内存满了,会发生什么?

1. 内核分配的过程是怎么样的?

malloc分配虚拟内存,虚拟内存实际访问的时候如果发现未映射到物理内存,则触发缺页中断,开始分配物理内存。 如果物理内存充足,则直接分配。如果物理内存不足,则唤醒后台内存回收线程kswapd进行后台内存回收(异步),如果回收速度跟不上进程分配内存的速度,则启动直接内存回收(同步), 如果还是没有足够物理内存,则启动OOMOOM会持续杀死物理内存占用较高的进程,直到释放足够的内存。

2. 哪些内存可以回收?

文件页:

  • 干净页: 直接回收
  • 脏页: 先写回磁盘再回收 匿名页: 通过Swap机制回收

3. 回收内存会带来什么性能影响? 如何优化?

回收内存会造成一些磁盘I/O操作,降低系统性能,其中文件页回收比匿名页回收往往性能影响更大。

优化方法:

  • 设置/proc/sys/vm/swappiness选项,调整文件页和匿名页的回收倾向
  • 尽早触发kswapd线程开启异步回收:通过设置min_free_kbytes

4. SMP架构是什么?SMP架构有什么问题?

SMP: 对称多处理器架构。多CPU平等共享系统资源。也称为UMA(Uniform Memory Access)架构

缺点: 总线带宽压力随着核数增大。

5. NUMA架构如何解决SMP架构总线带宽压力过大的问题?

CPU分组,每组CPU用一个Node表示。 "每个 Node 有自己独立的资源,包括内存、IO 等" 当Node的内存不足的时候,有多种回收模式可以选择, 通过/proc/sys/vm/zone_reclaim_mode控制:

  • 0(默认): 先去其他Node找空闲内存
  • 1: 只回收本地
  • 2: 只回收本地, 可写回脏页
  • 4:

注意: 不同的模式是可以组合使用的

6. 如何保护一个进程不被OOM杀掉?

设置/proc/[pid]/oom_score_adj参数, 取值范围:-1000~1000,越小越不容易被杀掉

四. 4G物理内存上,申请8G内存会怎么样?

32位操作系统上: 8G操过了理论上用户态虚拟内存空间上限3G,会申请失败

64位操作系统上: 8G小于物理内存上限128T,可以申请成功。 但是访问虚拟内存的时候,根据是否开启swap机制会有不同结果:

  • 不开启swap机制: 使用的时候,会因物理空间不足,触发OOM,导致进程被杀掉
  • 开启swap机制: 使用的时候,可以通过swap来回收内存,该书中测试是可以正常访问;书中把分配的虚拟内存增大到64G的时候,使用过程中进程还是被OOM杀掉了,因为系统多次回收内存还是不能够获取足够的内存。

总结:

  1. 如果分配的虚拟内存空间操作系统的用户态虚拟内存空间上限(32位操作系统3G)会申请失败
  2. 分配成功后,使用时如果没有开启swap机制,会因为物理空间不足触发OOM,导致进程被杀掉
  3. 如果开启了swap机制,使用超过物理内存的虚拟内存空间时,会通过swap机制回收内存,如果回收能够获取足够的内存,那么就能够正常使用;如果多次回收内存还是不能够获取足够的内存,那么就会触发OOM,导致进程被杀掉

五. 如何避免预读失效和缓存污染的问题?

普通的LRU缓存淘汰算法存在两个问题: 预读失效: 预先读取的页后续没有被使用,却被缓存了且淘汰了热点页 缓存污染: 某些一次性读取的页面后续不再使用,却占用了缓存空间,导致热点页被淘汰

解决预读失效: 通过改进LRU算法,给予访问的页和预读的页不同的优先级。 linux的LRU实现使用两个链表:active list和inactive list,预读的页先加入inactive list头部,只有访问过的页才能加入active list中; MySQL Innodb使用一个链表,链表划分为两个部分young和old,预读的页先插入old头部,只有访问过的页才加入young中。

解决缓存污染: 提高优先级改变的阈值,避免仅仅因为访问一次就改变优先级。 linux的LRU实现中在预读内存页被访问第二次的时候,才从inactive list移到active list中; MySQL Innodb,不仅预读的内存页要访问到第二次,还要判断两次时间间隔是否超过1s, 超过了才会从old移到young中。

什么是预读? 利用空间局部性原理,为了提高性能,操作系统在缓存某个要访问的页的时候,会预先读取并缓存后续的若干页。

六. 深入理解Linux虚拟内存管理

1. 什么是虚拟内存地址?

一个用于关联到物理地址的数据。

32位虚拟内存地址=页目录项(10)+页表项(10)+页内偏移(12)

每个虚拟内存地址用于定位一个虚拟内存空间中一个特定的字节。

2. 为什么要使用虚拟内存地址访问内存?

  • 提供进程隔离: 每个进程都有独立的虚拟内存空间,避免了不同进程直接使用物理地址需要处理的地址冲突的问题。
  • 统一开发者视图,简化开发难度: 每个进程都有独立的虚拟内存空间,避免了开发者需要处理物理内存地址冲突的问题,而且使用连续的大块的虚拟内存空间的时候,不需要考虑底层的物理内存空间是否是连续的问题。把内存管理的复杂性交给操作系统的内存管理模块处理。
  • 支持内存超售: 虚拟内存允许系统分配比物理内存更多的内存
  • 提高安全性: 通过页表项中的权限位来限制进程对内存的访问

3. 进程虚拟内存空间布局

  • https://xiaolincoding.com/os/3_memory/linux_mem.html#_3-%E8%BF%9B%E7%A8%8B%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98%E7%A9%BA%E9%97%B4

4. Linux进程虚拟内存空间

七. 深入理解linux物理内存管理

笔者认为最好的回答。

一句话: 大端序高字节低地址, 小端序高字节高地址

在默认 从 左到右地址 增大 的情况下。

大端序是 每个字节 从左到右书写. 小端序是 每个字节 从右到左书写.

比如一个单词 abc,三个字符{a,b,c}分别对应16进制数字:0x61,0x62,0x63 把它作为一个数据,对他来说a、b相对于c是高字节,b、c相对于a来说是低字节

大端序为: 0x61,0x62,0x63 小端序为: 0x63,0x62,0x61

工程开发相关

Dockerfile命令

参考资料

Overview

  • 什么是Dockerfile
  • 如何编写Dockerfile
  • Q&A

一. 什么是Dockerfile

Dockerfile 是 组织 Dockerfile命令的文件, Dockerfile命令用来指导docker build 构建docker 镜像的过程,一般Dockerfile命名为Dockerfile,也有命名为dockerfile的。 一般使用方式如:docker build . -t <target-image-name> 其中. 指示开始构建过程的时候以当前路径为初始构建环境路径,-t <target-image-name指定构建的镜像的名字,比如hello:1.0,比如test,镜像名可能包括版本号

二. 如何编写Dockerfile

1. Dockerfile命令语法格式

2. Dockerfile的结构:

一个Dockerfile至少包含一个FROM命令,一般放在最开头,用来指定镜像的基底,如 FROM ubuntu:latest 指定该镜像的构建基于ubuntu:latest镜像 一般会包含一个CMD命令指定从镜像启动容器的时候执行的程序,如CMD ["echo","Hello World!"] 所以可以编写一个简单的镜像的Dockerfile如下:

FROM ubuntu:latest

CMD ["echo","Hello World!"]

三. Q&A

1. Dockerfile中是否能够使用环境变量? 是否能够使用外部设定的环境变量?

2. Dockerfile中COPY命令使用的参数中是否参数名能够使用环境变量参与组成?

3. Dockerfile中的RUN命令和CMD命令的区别是什么?

4. Dockerfile中

Ref

  • ubuntu24上安装: https://www.sysgeek.cn/install-docker-ubuntu/

Q&A

  1. 问题docker ps 失败, 原因可能是 用户不在docker用户组中,使用如下命令把用户加入用户组 sudo usermod -aG docker $USER 如果使用了该命令还没有解决,可能是docker用户组不存在,cat /etc/group |grep docker确认 尝试如下操作:
sudo groupadd docker
sudo gpasswd -a $USER docker
newgrp docker
  1. 通过 registry 服务分发
  2. 通过打包传输解压镜像文件分发
    docker save -o myimage.tar myimage:tag
    # 从压缩文件恢复镜像
    docker load -i myimage.tar
    

docker compose 使用

Overview

  • 参考资料

  • 简介

  • 常用命令和基础概念

  • docker-compose.yml 与 .env 语法

  • Q&A

一.参考资料

二.简介

docker compose 是一种 命令行工具,能够用来管理多容器的docker应用的配置与执行。

一种获取方式是通过给 docker 加入插件的方式获取,这种方式能够使用docker compose ... 形式命令调用。新版本的docker下载后自带该插件

另一种获取docker compose的方式是下载单独的docker-compose 软件,往往用于老版本。

三.常用命令与基础概念

  1. 配置文件:

    docker compose 使用docker-compose.yml文件配置要创建的容器以及网络,通过.env 文件 配置 docker-compose.yml 中用到的环境变量

    这两个文件要在同一个路径下

  2. 启动与关闭

    docker compose up -d #启动

    docker compose down #关闭
    这两个命令要在配置文件同一路径下输入

  3. 查看是否已经根据配置启动了容器们

docker compose ps

四.docker-compose.yml 与.env 语法

(一)..env 语法

<env> = <value>  
# 使用"#" 作为注释符
# <env> 为一个 字符字面量,可以称环境变量名,在该语句中 与<value>绑定
# <value> 一般为一个数 或者为一个字符串
# 可以在docker-compose.yaml中使用${<env>}的方式引用<value>绑定的值

(二).docker-compose.yml语法

示例:

version: '3'
services:
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASS}
  • 声明docker compose 的文件格式版本

    version : "3"

  • 定义服务

    services:
        <服务名>:
            image: <镜像名>
            environment:
                <容器内环境变量名>:<变量值>
    

六.Q&A

1. wsl中使用在wsl中下载的linux版本docker,使用docker compose up -d的时候报错http: invalid Host header

原因:未知,怀疑是 docker 本身的问题,

发生问题的版本Docker Compose version v2.17.2 ,Docker version 20.10.24, build 297e128

解决方法,参考Docker-compose ERROR [internal] booting buildkit, http: invalid Host header - Stack Overflow

更新docker版本sudo snap refresh docker --channel=latest/edge , 成功解决!

一.镜像,容器,DockerFile的关系

镜像是对容器配置的描述.DockerFile是对镜像配置的描述

docker能够通过镜像启动容器.

docker通过dockerfile配置结合本地资源和基础镜像 编译出 新镜像.

二.容器与 shell,与虚拟机的区别

容器的功能就像是一个独立的机器,但是实际上它只是独立地拥有一套虚拟的文件系统,一套独立的环境变量:

  • 与shell的区别:除了独立环境变量外,容器还虚拟了一套文件系统

  • 与虚拟机的区别:虚拟机不仅仅模拟了文件系统,而是模拟了整个操作系统,包括内存管理,处理器调度等.

一.如何拉取镜像

docker pull <image-label>

if with no version tag,use latest in default

二.如何从镜像启动容器,如何停止容器

(1). 启动容器

docker run [-it] [--name <container-name>] [-d] <image-name>

参数说明:

  • -d :后台运行容器中命令,启动容器后回到主进程,容器独立运行

(2). 停止容器

docker stop <container-name>|<container-id>

停止后的容器内容信息不会被删除,可以再次启动:

docker start <container-name>|<container-id>

三.如何进入容器

  1. 启动时交互式进入容器 docker exec -it <image-name> <bash-path>

    默认使用/bin/bash

    这种方式进入容器一旦通过exit之类的方式登出,则容器将终止

  2. 交互式进入运行中的容器

    docker exec -it <container-name> <bash-path>

    用这种方式可以多次进入一个容器,在容器中开始的主进程的工作结束前容器都不会因为登出而结束

四.如何查看容器信息,镜像信息

a. 查看所有运行中的容器: docker ps

b. 查看所有未清除的容器: docker ps -a

c. 查看某个容器具体信息 : docker inspect <container-name/id>

d. 查看镜像信息 : docker images

e. 查看指定容器的内存、硬盘、网络、cpu等资源的使用情况

# 使用docker stats

f. 查看指定容器占据的硬盘大小

# 把容器commit成镜像
# 然后查看镜像大小

五.删除镜像,删除容器

  1. 删除镜像: docker rmi <image-name[:version-tag]>

    注意:要成功删除镜像,需要删除使用到该镜像的所有容器先. 其次,删除镜像有时候需要指定具体的tag,如果删除了指定tag的某个镜像, 比如删除了redis:1.0,但是如果该镜像有其他的名字比如redis:1,实际上底层是同一个镜像的话,该镜像的内容并不会从docker的存储区种删除. 只有一个镜像失去了它所有的指向方式(所有标签)的时候才会被实际删除

  2. 删除容器: docker rm <container-name>/<container-id>

    注意:容器要先stop,之后才能删除

六.使用docker build编译Dockerfile为镜像

docker build -t <image-name> .

这里面的.表示以当前路径为打包开始的路径,则docker build执行打包镜像操作的时候,使用的相对地址就是就是用. 指定的基地址 来确定绝对地址

编译镜像的过程中往往需要下下载软件,很多软件下载时往往需要科学上网才能在合理时间内下载成功

七. 把容器commit成镜像

docker commit <container-name> <image-name>

提示词

docker有哪些网络模式

反馈

Docker提供了多种网络模式,用于配置容器之间和容器与外部网络之间的通信。以下是Docker中常用的网络模式:

  1. 桥接模式(Bridge):桥接模式是Docker默认的网络模式。在桥接模式下,Docker会创建一个虚拟的网络桥接接口(bridge),并将容器连接到这个桥接接口上。每个容器都会分配一个IP地址,并可以通过容器名称或IP地址相互通信。

  2. 主机模式(Host):主机模式下,容器与宿主机共享网络命名空间。容器将直接使用宿主机的网络接口,不进行网络地址转换(NAT),也不会创建额外的网络接口。这种模式下容器的网络性能会更好,但容器之间的端口冲突可能会出现。

  3. 无网络模式(None):无网络模式下,容器没有网络接口,完全与外部网络隔离。这种模式适用于那些不需要网络连接的容器,比如批处理任务或与外部网络无关的服务。

  4. 容器模式(Container):容器模式允许多个容器共享一个网络命名空间。这意味着这些容器可以使用相同的网络配置,包括IP地址和端口。这种模式适用于需要共享网络栈的容器,比如微服务架构中的多个容器。

  5. 自定义网络模式(Custom):除了上述内置的网络模式,Docker还提供了自定义网络模式的功能。使用自定义网络模式,可以创建具有自定义网络配置的网络,包括子网、网关和DNS等。这种模式适用于复杂的网络拓扑和多容器应用程序的部署。

通过选择适当的网络模式,可以实现容器之间的通信和与外部网络的连接。不同的网络模式适用于不同的应用场景,根据实际需求选择合适的网络模式可以提供更好的网络性能、安全性和灵活性。

宿主机和容器通信的几种方式

  • 容器可以通过宿主机的IP地址和端口号进行通信
  • 容器中,可以使用特殊的DNS名host.docker.internal来访问宿主机。例如,如果你的应用程序在宿主机的 8080 端口上运行,你可以从容器内部使用 http://host.docker.internal:8080 来访问它。
  • 容器可以通过连接到宿主机的网络来访问宿主机。例如,使用--network=host标志启动容器,容器将连接到宿主机的网络,可以通过宿主机的IP地址和端口号进行通信。

搭建与使用 本地DockerRegistry

参考

  1. official guide

Case

#!/bin/bash

# 本脚本用于搭建一个本地的Docker Registry并展示如何使用它。

# 设置变量
REGISTRY_NAME="local-registry"
REGISTRY_PORT="5000"
CONTAINER_INNER_REGISTRY_PORT="5000"
# 指定镜像存储位置
STORAGE_PATH="/path/to/local/registry/storage"
IMAGE_NAME="my-image"
LOCAL_IMAGE_TAG="localhost:${REGISTRY_PORT}/${IMAGE_NAME}"

# 启动 Docker Registry 容器
echo "启动 Docker Registry..."
docker run -d -p "${REGISTRY_PORT}:${CONTAINER_INNER_REGISTRY_PORT}" --restart=always --name "${REGISTRY_NAME}" \
  -v "${STORAGE_PATH}:/var/lib/registry" \
  registry:2

# 等待几秒确保 Docker Registry 启动
sleep 3

# 检查 Registry 是否成功运行
echo "检查 Docker Registry 状态..."
curl -X GET http://localhost:${REGISTRY_PORT}/v2/_catalog

# 标记本地镜像以便推送到本地 Registry
echo "标记镜像为本地 Registry 格式..."
docker tag "${IMAGE_NAME}" "${LOCAL_IMAGE_TAG}"

# 推送镜像到本地 Registry
echo "推送镜像到本地 Registry..."
docker push "${LOCAL_IMAGE_TAG}"

# 从本地 Registry 拉取镜像
echo "从本地 Registry 拉取镜像..."
docker pull "${LOCAL_IMAGE_TAG}"

# 脚本结束
echo "本地 Docker Registry 配置完成。"

坑点 1: 为一个用户安装了 podman 后,其他用户无法使用

环境:

PRETTY_NAME="Ubuntu 24.04 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04 LTS (Noble Numbat)"

安装方式: apt

在某个用户 u 安装了 podman 之后,切换到用户 gitea 执行如下命令报错:

gitea@VM-8-17-ubuntu:~$ podman --version
ERRO[0000] XDG_RUNTIME_DIR directory "/run/user/1003" is not owned by the current user

检查发现 1003 是 用户 u 的 uid, 而 用户 gitea 的 uid 是 1004.

解决方法: 暂无

WSL(Windows Subsystem for Linux)

一. 参考资料

  1. GPT

  2. Winux之路-WSL 2的使用及填坑 - 知乎 (zhihu.com)

  3. 史上最全的WSL安装教程 - 知乎 (zhihu.com)

二. 简介

WSL(Windows Subsystem for Linux)是Windows的一个功能,允许在Windows计算机上运行Linux环境,而无需使用虚拟机或双引导。WSL旨在为希望同时使用Windows和Linux的开发人员提供无缝高效的体验。

WSL有两个主要版本:WSL 1和WSL 2。WSL 2是默认的发行版类型,它使用虚拟化技术在轻量级实用工具虚拟机(VM)中运行Linux内核。通过WSL 2运行的Linux发行版将共享同一网络命名空间、设备树、CPU/内核/内存/交换空间等,但有自己的PID命名空间、装载命名空间、用户命名空间等[1]

WSL的优势包括:

  • 资源消耗较少:相较于虚拟机,WSL作为系统层的一部分,消耗更少的资源,并且与系统更紧密地集成[2]

  • 高度兼容性:WSL提供了对Linux内核的支持,使得绝大多数在完整Linux系统中可用的功能和工具都可以在WSL中使用,如Bash shell、命令行工具、编程语言解释器等[2]

  • 与Windows应用程序的互操作性:WSL允许用户在同一组文件上使用Windows应用程序和Linux工具,方便开发人员进行跨平台开发[2]

目前,WSL支持多个Linux发行版,包括Ubuntu、Kali Linux、openSUSE等,用户可以在Microsoft Store中下载并安装自己喜欢的发行版[3]

总之,WSL为开发人员提供了在Windows环境中运行Linux的便利性和灵活性,使得开发人员可以同时享受Windows和Linux的优势。

三. 使用

  1. windows上win+s,搜索"功能",找到"启动或关闭windows功能“, 选择 开启适用于linux的windows子系统 功能以及虚拟机平台 功能

  2. 下载或者更新wsl,使用 wsl --install 命令,下载更新wsl, 使用wsl --set-default-version <Version> 命令切换版本到wsl2,

  3. 查看能够下载使用的发行版wsl --list --online

  4. 下载并启动发行版: wsl --install -d <发行版本> ,常用的发行版比如

  5. 查看当前可用linux子系统以及该系统使用的wsl版本: wsl -l -v

  6. 删除某个wsl启动的linux子系统: wsl --unregister <发行版名称>

  7. 设计wsl开机自启动:

    wsl本身不会开机自启动,但是启动速度快,

    尤其是正在不需要开多个子系统以及本身机器资源丰富的情况下,不需要设置开机自启动

  8. 使用指定用户进入指定wsl子系统:wsl -d <子系统发行版名称> --user <用户名>

  9. wsl中 使用 sudo hwclock -s 命令同步时间。有的命令需要时间同步才能执行成功,比如apt-get update

四. Q&A

A. 如何在wsl中 为什么监听777端口提示"bind: permission denied"?如何解决?

  1. wsl2中开启服务监听777端口 提示"bind: permission denied"

    在WSL2中,当您尝试监听TCP端口时,出现"bind: permission denied"错误通常是由于权限限制导致的。WSL2默认情况下会限制非特权用户对低于1024的端口进行绑定。

    要解决这个问题,您可以尝试以下方法:

    • 使用高于1024的端口:尝试将要监听的端口更改为大于1024的端口号,例如7777或8888。

    • 使用sudo或管理员权限:在命令前加上sudo或者以管理员身份运行WSL2终端。

  2. 修改WSL2的配置文件(仅限高级用户):您可以修改WSL2的配置文件,以允许非特权用户绑定低于1024的端口。打开/etc/wsl.conf文件(如果不存在则创建),添加以下内容:

    [network]
    generateResolvConf = false
    bindNonprivilegedPort = true
    保存文件并重启WSL2。
    

    请注意,修改WSL2的配置文件需要管理员权限。如果您不熟悉这方面的操作,请谨慎进行,并确保了解所做更改的后果

  3. 低于1024的端口有什么特别意义?

    在计算机网络中,1024以下的端口号被称为"特权端口"(privileged ports)或"系统端口"(well-known ports)。这些端口号在操作系统中具有特殊的意义,并且通常需要具有管理员或root权限才能绑定或使用。
    
    特权端口用于一些常见的网络服务和协议,例如Web服务器(端口号80)、Secure Shell(SSH,端口号22)、域名系统(DNS,端口号53)等。这些端口号在操作系统中已经被预留,以便于标准化网络通信。
    
    默认情况下,WSL2和许多其他操作系统会限制非特权用户对特权端口的绑定和使用,这是出于安全考虑。这样可以防止普通用户意外或故意占用或干扰这些关键服务。
    
    因此,当尝试在WSL2中绑定低于1024的端口时,可能会出现"bind: permission denied"错误,除非您以管理员身份运行WSL2或修改WSL2配置以允许非特权用户绑定这些端口。
    

B. wsl中突然无法访问网络是为什么? 如何解决?

在wsl使用过程中,笔者发现突然所有外部网络都无法访问了,如下尝试ping github.com无法ping通

❯ ping github.com
ping: connect: Network is unreachable

原因不明,重启wsl后解决

小狗钱钱

Quick Intro

这是一本可爱的书,书里面的一些知识很稚嫩,但是一些观点很有参考意义。 这本书可以用作幼儿财商的培养。

Note

1. 十个愿望

写下十个为什么想要自己有钱的愿望,并且挑出其中重点的三个。而且之后应该每天都看一遍。

这个作法是为了让自己思考得出金钱对自己的意义,并确定最重要的目标。

2. 梦想储蓄罐

任务: 做个"梦想储蓄罐",上面要有跟梦想相关的图片等信息。

目的:人们把这种行为称作‘视觉化’。成功的人之所以成功,就是因为他们一直梦想着自己成功的那一天,不停地想象着自己实现了理想时的情形。当然,人不能停留在梦想里。

学习就是认识新观念和新想法的过程。假如人们始终以同一种思维方式来考虑问题的话,那么始终只会得到同样的结果。因为我对你讲述的许多内容是你以前从未接触过的,所以我建议你,在你还没有做之前,不要轻易下结论。没有想象力的人是很难成就大事的。我们对一件事投入的精力越多,成功的可能性也越大。可是大多数人把精力放在自己并不喜欢的事情上,而不去想象自己希望得到的东西。

3. 保持自信

要求做到两点:

  • 考察自己,考察身边的机会,同时保持自信,勇敢行动抓住机会。
  • 编写成功日记,记录自己做成功的事情

这一章讲了个小孩成为百万富翁的作用,告诉读者自信对于赚钱的作用。

不要总是把心思留在那些自己不知道、能做和拥有的东西上,而是多把心思考察自己、考察周围。

其实从达瑞把精力集中在他知道、能做和拥有的东西上的那一天起,他的成功就已经拉开了序幕。这一决定使得一个孩子完全有能力挣到比成人更多的钱,因为成人
经常把一生的时间都用来考虑他们不知道、不能做或没有的东西上。

而编写成功日记,记录自己做成功的事情,就是一种增强自己自信的手段。

4. 从自己的兴趣出发

从自己的兴趣触发寻找赚钱机会

如果一件事情是自己乐意做、能做好,而且别人不乐意做、做没那么好且想要做好的,那就是个自己提供服务来赚钱的机会了。

5. 72 小时约定

很简单。当你决定做一件事情的时候,你必须在 72 小时之内完 成,否则你很可能永远不会再做了。

6. 避免借债

书中提出两个观点:

  1. 合理规划还贷分期。

    • 将扣除将扣除生活费后剩下的钱的一半存起来,剩下的一半用于支付消费贷款,而不是生活费以外的钱都用于还贷。
    • 最好根本不申请消费贷款。
    • 如果每期还贷设置过高的话,可能导致需要额外借贷

    笔者点评: 其实不一定要一半一半,设置合适即可,而且具体怎样更合适还要看当时社会背景。 如果存钱到银行的利息远远小于未偿还贷款产生的利息呢?

  2. 提前储蓄,避免借债(笔者认可)

7. 不要杀死你的鹅

自己的本金是用来产生复利的鹅。

不要轻易消费自己的本金。要注意储蓄和投资。

CNCSMonster's language leaning notes

Rust Quiz

notice

可以注意到中间漏了些题目,比如 7, 这是因为这个题目已经过时了,已经被移除出 Rust Quiz

quick intro

Rust Quiz is a collection of questions designed to test your knowledge of Rust. It covers various topics, including ownership, lifetimes, and concurrency.

Ref

Note

Q1

值得一提的是,这里介绍了一种 Rust 编译器处理语法的细节。 虽然 Rust 中一切皆表达式,但 Rust 遇到 {} 的时候,会把块处理成 stmt,不会当成后面的二元运算符的左表达式。 比如 {} && true,Rust 会理解成 {}&&true 两个 stmt,而不是 {} && true 一个 expr 类型的 stmt. 也就是这里 && 并没有被当成一个二元运算符,而是当成了连续使用的两个 & 一元运算符。

如果想要把一个 block 当作二元运算符的一个算子表达式,可以用括号括起来,比如 ({} && true),这样 Rust 就会把 {} 当成一个 expr,把 ({} && true) 整体当成一个 expr 类型的 stmt.

Q2

该题目涉及的知识点和 Q1 类似,Rust 编译器在处理语法的时候会把 {} 当成一个 stmt,而不是一个 expr。所以 {} & S(4) 会被 Rust 编译器理解成两个 stmt: {}&S(4),其中 & 没有被理解成 BitAnd 运算符,而是理解成了一元的取引用运算符。

Q3

Rust 中用 const 定义的全局常量,在局部作用域中取&mut 的时候,实际是创建了一个具有该常量值的临时变量,并且&mut 指向该临时变量。 在局部作用域中对 const 定义的全局常量的字段进行修改的时候,会创建一个具有该常量值的临时变量,并不会实际修改该全局常量的值,也就是后面再访问该全局常量时,仍然是原来的值。

Q4

Rust 中使用 b"xx" 语法创建 byte string,也就是类型为 &'static [u8;<n>] 的类型表示,这里面 <n> 代指对应的字符串中字符数量,每个字符看作 ascii 字符。 Rust 中 .. 可以用作表示匹配剩余内容,也可以用于表示一个 RangeFull 类型的 range 所以 b"062"[..][1] 指向的是 [b'0', b'6', b'2'] 中的第一个元素 b'6',对应 ascll 值为 54.

Q6

Rust 中 assiggnment 语句本身的值是 (),也就是 unit 类型。 所以 let x = y = 1; 等价于 let x = (y = 1);,也就是先执行 y = 1,然后该语句的值是 (),再将 () 赋值给 x。 也就是 let x = y = 1; 等价于 y = 1; let x = ();

Q8

Rust 中声明宏匹配标点符号的时候要考虑空格,有时缺乏空格的情况会按照优先左结合组成标点符号的规律匹配,所以有时有没有空格的匹配结构一致。比如声明宏中 ==>== > 是等价的,都被匹配 ==> 两个标点符号。

Rust 中过程宏的 api 更加灵活,能够准确地处理空格的使用。比如能够区别 ==>== >

Q9

Rust 的声明宏的匹配机制中,不同类型的宏的片段指示符(fragment specifiers)可以分为两种,一种会让被捕获的成员不透明,另一种不会。 透明的片段指示符包括:

  • ident
  • lifetime
  • tt

Q10

考察 Rust 中的 method lookup. Rust 中类型的自有方法和类型实现的 trait 方法同名的情况下:

  1. 方法的接收器不同时, &self&mut self 优先
  2. 方法的接收器相同时, 类型的自有方法比 trait 方法优先

本题目为特殊情况,对于 dyn Trait 类型,存在 dyn Trait 的自有方法与该 Trait 提供的方法同名的情况下,当前没有办法调用 到该 dyn Trait 类型的自有方法。

Q11

考察 Rust 中泛型参数的 early bound 和 late bound

对于生命周期泛型参数, 存在 early bound 和 late bound 两种情况。 如果是 late bound 的生命周期泛型参数,则不能够显式地指定生命周期泛型参数。 其中 fn f<'a>()'a 就是 late bound 的生命周期泛型参数; 其中 fn g<'a:'a>()'a 就是 early bound 的生命周期泛型参数

Q12

考察 Drop 的顺序

Q13

考察 Rust 中 0 大小类型(ZST)的数组中每个元素的地址都是一样的。 ZST 在 Rust 中是一种特殊的抽象,属于编译时才存在的概念。 编译后程序不会为 ZST 的值开辟空间。

Q14

Rust 中的 impl 语句实现的位置不重要,整个程序的 scope 中都可以访问。

Q15

考察 Rust 中整数类型推导的规则。 对于 Rust 中的整数类型的变量,如果它调用了 trait 方法。 首先查询该整数类型本身是否实现了该 trait,比如查询 i32,该题目中查询到没有,然后查询其他整数类型,查询到 u32 满足该 trait,所以推导该变量为 u32 类型。 如果所有整数类型 T 都没有实现该 trait,则会查询整数类型的 &T 类型是否是实现了该 trait,优先从 &i32 开始。

ps, 这个材料中推荐的 stack overflow 的回答看起来很晦涩,笔者直接放弃。笔者觉得自己这个理解更直接。

经过实验,发现:

  1. 如果直接整数类型 T 对 trait 的实现存在多个,但是不包含 i32 的实现的话,则编译器会推导整数类型为 i32,然后报错 trait 未实现
  2. 如果有多个直接整数类型 T 对 trait 的实现,且包含 i32 的实现的话,则正常编译,并且编译器推导该变量为 i32 类型。
  3. 如果只是存在一个整数类型对该 trait 的实现,则编译器推导该变量为该整数类型。
  4. 用 T 代指整数类型,如果没有直接整数类型实现该 trait, 但是有且仅有一个 &T 整数类型的引用类型实现该 trait, 则编译器推导该变量为 T 类型
  5. 如果存在多个&T 对该 trait 的实现,且不包含 &i32 的实现,则编译器推导该变量为 i32 类型,然后报错 trait 未实现

Q16

Rust 中没有 --++ 这样的一元自增/自减运算符。 所以 --x 其实相当于 -(-x),也就是先对 x 取负,然后再对结果取负。

Q17

考察 Rust 中的运算符,Rust 中没有自增/自减运算符。 所以若干个 - 或者若干个 + 会相当于用右结合的方式运算。

Q18

Rust 语法中的成员搜索规则是先搜索成员方法,找不到成员方法再搜索成员变量。

Q19

Rust 中 let _ = s; 这个语句不会 move 变量 s, 也就是 s 如果有 Drop::drop 实现的话,该函数不会在这一行后立刻触发。

Q22

Rust 中 1. 整体表示一个浮点数。 Rust 中宏指示符会把合法数字表示当成一个 token,对应 tt 片段指示符。

Q23

考察 Rust 中 method lookup 的规则。 自由方法和实现的 trait 方法同名的情况下, 如果接收器相同,优先使用接收器为 &self 的方法 如果接收器不同, 优先使用自有方法。

Q24

可以把宏的卫生性理解成一种对局部变量的着色。 对于宏中直接使用的外部局部变量的名称,指向的是宏定义处有定义的局部变量。 至于常量,Rust 中把局部常量当作 items 而不是局部变量,宏使用的外部局部常量的名称指向调用宏时外部的局部常量。

Q25

考察 Rust 的 desconstructing 语法,以及 drop 的时机。 drop 的时机:

  1. 当一个值被构造出来后没有持有者的时候,会直接 drop;
  2. 当值的持有者超出作用域的时候,drop.

let S = f()S 不是一个变量名, 而是一个 destructuring 语法中用到的模式,指向类型 struct S, 这里表示一个不会失败的模式解构,并且绑定新的变量。

Q26

考察迭代器的延迟计算

Q27

&dyn Trait 类型作为参数类型或者参数类型为 T,约束 T:Trait 时参数的行为都是一样的,只能够访问到 Trait 中定义的方法 Rust 中 super trait 不等于 c++ 中的继承,而只是说明一个 trait 要实现必须先实现了某些 trait.

Q28

同 Q25 一样,考察 Drop 的时机

Q29

type M = (i32); 等价于 type M = i32; Rust 中 一个元素的 tuple 需要结尾加上 , 来表示, 比如 (T,),以与 (T) 区分开来。

Rust 中对于无后缀的整数数字,往往优先认为是 i32 类型的整数。

Q30

考察 Rust 中的方法查找规则。 以及考察 Rust 标准库中的 Rc 实现了 Clone trait 这个知识点。

Q31

考察 Rust 中变量的方法查找顺序。

trait Or {
    fn f(self);
}

struct T;

impl Or for &T {
    fn f(self) {
        print!("1");
    }
}

impl Or for &&&&T {
    fn f(self) {
        print!("2");
    }
}

fn main() {
    let t = T;
    let wt = &T; // &T
    let wwt = &&T; // &&T, &&&T, &mut &&T , &T
    let wwwt = &&&T; // &&&T, &&&&T
    let wwwwt = &&&&T; // &&&&T
    let wwwwwt = &&&&&T; // &&&&&T,&&&&&&T, &mut &&&&&T, &&&&T
    t.f(); // 1
    wt.f(); // 1
    wwt.f(); // 1
    wwwt.f(); // 2
    wwwwt.f(); // 2
    wwwwwt.f(); // 2
}

Q32

考察 Rust 的 match 语法 xx|yy if bb => {} {..} 这种模式, 相当于

xx if bb => {..},
yy if bb => {..}

Q33 (Notice!!)

Rust 中 || 优先和后面的表达式组成闭包 所以 || .. .method() 用括号和空格标上顺序应该为 (|| ..) . method() 如果想要表达调用为 .. 实现的 method 方法,应该给 .. 加上括号如下: || ((..).method())

Q34

Rust 中函数指针的大小是固定的,是 usize 大小,需要用来存储函数的地址。 但是 Rust 中函数名本身作为值使用时,它本身的类型不是对应函数指针类型,而是与其函数定义本身绑定的一个类型,这个类型特定指向该函数的实现,所以不需要存储函数的地址,所以大小为 0.

举例说明

fn main(){
    fn f() {}
    let a : fn() = f; // a的类型是函数指针类型, 大小为usize的大小
    let b = f; // b的类型是函数f的类型(fn f()), 大小为0
    println!(
        "{:?} {:?}",
        std::mem::size_of_val(&a),
        std::mem::size_of_val(&b)
    );
    // 输出: 8 0
}

Q35

考察 Rust 中声明宏的卫生性规则。 为了避免声明宏中新定义的变量与外部同作用域下变量命名冲突,即使是同名变量,宏中定义的变量会被当成不同名字的变量处理。

不过如果变量命名是通过宏参数传入的,那么宏中定义的变量会与外部同作用域下变量命名相同。

#![allow(unused)]
fn main() {
macro_rules! x {
    ($n:expr) => {
        let a = X($n);
    };
    ($id:ident,$n:expr) => {
        let $id = X($n);
    };
}

struct X(u64);

impl Drop for X {
    fn drop(&mut self) {
        print!("{}", self.0);
    }
}

/// 输出: 121
#[test]
fn t1() {
    let a = X(1);
    // x!(2);
    x!(a, 2);
    print!("{}", a.0);
}

/// 输出: 221
#[test]
fn t2() {
    let a = X(1);
    x!(a, 2);
    print!("{}", a.0);
}
}

Q36

考察 Rust 中闭包的本质。 闭包相当于建立了一个具有闭包对应函数签名的自有方法的结构体。 该闭包捕获的外部变量,相当于结构体的字段。 如果闭包捕获的外部变量的类型都是实现了 Copy trait,则该闭包相当的结构体也相当于 实现了 Copy trait

Q37

考察 Rust 中的临时生命周期延长。 当一个临时变量的引用被 let/const/static 语句中使用的时候,这个临时变量的生命周期会延长到该语句所在的作用域结束。

文学