Linux中国
1.73K subscribers
777 photos
6.34K links
Linux 中国官方 Telegram 频道
Download Telegram
页面缓存、内存和文件之间的那些事

Media上一篇文章中我们学习了内核怎么为一个用户进程 管理虚拟内存,而没有提及文件和 I/O。这一篇文章我们将专门去讲这个重要的主题 —— 页面缓存。文件和内存之间的关系常常很不好去理解,而它们对系统性能的影响却是非常大的。在面对文件时,有两个很重要的问题需要操作系统去解决。第一个是相对内存而言,慢的让人发狂的硬盘驱动器,尤其是磁盘寻道。第二个是需要将文件内容一次性地加载到物理内存中,以便程序间共享文件内容。如果你在 Windows 中使用 进程浏览器 去查看它的进程,你将会看到每个进程中加载了大约 ~15MB 的公共 DLL。我的 Windows 机器上现在大约运行着 100 个进程,因此,如果不共享的话,仅这些公共的 DLL 就要使用高达 ~1.5 GB 的物理内存。如果是那样的话,那就太糟糕了。同样的,几乎所有的 Linux 进程都需要 ld.so 和 libc,加上其它的公共库,它们占用的内存数量也不是一个小数目。幸运的是,这两个问题都用一个办法解决了:页面缓存 —— 保存在内存中的页面大小的文件块。为了用图去说明页面缓存,我捏造出一个名为 render 的 Linux 程序,它打开了文件 scene.dat,并且一次读取 512 字节,并将文件内容存储到一个分配到堆中的块上。第一次读取的过程如下:Reading and the page cacherender 请求 scene.dat 从位移 0 开始的 512 字节。内核搜寻页面缓存中 scene.dat 的 4kb 块,以满足该请求。假设该数据没有缓存。内核分配页面帧,初始化 I/O 请求,将 scend.dat 从位移 0 开始的 4kb 复制到分配的页面帧。内核从页面缓存复制请求的 512 字节到用户缓冲区,系统调用 read() 结束。读取完 12KB 的文件内容以后,render 程序的堆和相关的页面帧如下图所示:Non-mapped file read它看起来很简单,其实这一过程做了很多的事情。首先,虽然这个程序使用了普通的读取(read)调用,但是,已经有三个 4KB 的页面帧将文件 scene.dat 的一部分内容保存在了页面缓存中。虽然有时让人觉得很惊奇,但是,普通的文件 I/O 就是这样通过页面缓存来进行的。在 x86 架构的 Linux 中,内核将文件认为是一系列的 4KB 大小的块。如果你从文件中读取单个字节,包含这个字节的整个 4KB 块将被从磁盘中读入到页面缓存中。这是可以理解的,因为磁盘通常是持续吞吐的,并且程序一般也不会从磁盘区域仅仅读取几个字节。页面缓存知道文件中的每个 4KB 块的位置,在上图中用 #0#1 等等来描述。Windows 使用 256KB 大小的视图view,类似于 Linux 的页面缓存中的页面page。不幸的是,在一个普通的文件读取中,内核必须拷贝页面缓存中的内容到用户缓冲区中,它不仅花费 CPU 时间和影响 CPU 缓存在复制数据时也浪费物理内存。如前面的图示,scene.dat 的内存被存储了两次,并且,程序中的每个实例都用另外的时间去存储内容。我们虽然解决了从磁盘中读取文件缓慢的问题,但是在其它的方面带来了更痛苦的问题。内存映射文件是解决这种痛苦的一个方法:Mapped file read当你使用文件映射时,内核直接在页面缓存上映射你的程序的虚拟页面。这样可以显著提升性能:Windows 系统编程 报告指出,在相关的普通文件读取上运行时性能提升多达 30% ,在 Unix 环境中的高级编程 的报告中,文件映射在 Linux 和 Solaris 也有类似的效果。这取决于你的应用程序类型的不同,通过使用文件映射,可以节约大量的物理内存。对高性能的追求是永恒不变的目标,测量是很重要的事情,内存映射应该是程序员始终要使用的工具。这个 API 提供了非常好用的实现方式,它允许你在内存中按字节去访问一个文件,而不需要为了这种好处而牺牲代码可读性。在一个类 Unix 的系统中,可以使用 mmap 查看你的 地址空间,在 Windows 中,可以使用 CreateFileMapping,或者在高级编程语言中还有更多的可用封装。当你映射一个文件内容时,它并不是一次性将全部内容都映射到内存中,而是通过 页面故障 来按需映射的。在 获取 需要的文件内容的页面帧后,页面故障句柄 映射你的虚拟页面 到页面缓存上。如果一开始文件内容没有缓存,这还将涉及到磁盘 I/O。现在出现一个突发的状况,假设我们的 render 程序的最后一个实例退出了。在页面缓存中保存着 scene.dat 内容的页面要立刻释放掉吗?人们通常会如此考虑,但是,那样做并不是个好主意。你应该想到,我们经常在一个程序中创建一个文件,退出程序,然后,在第二个程序去使用这个文件。页面缓存正好可以处理这种情况。如果考虑更多的情况,内核为什么要清除页面缓存的内容?请记住,磁盘读取的速度要慢于内存 5 个数量级,因此,命中一个页面缓存是一件有非常大收益的事情。因此,只要有足够大的物理内存,缓存就应该保持全满。并且,这一原则适用于所有的进程。如果你现在运行 render 一周后, scene.dat 的内容还在缓存中,那么应该恭喜你!这就是什么内核缓存越来越大,直至达到最大限制的原因。它并不是因为操作系统设计的太“垃圾”而浪费你的内存,其实这是一个非常好的行为,因为,释放物理内存才是一种“浪费”。(LCTT 译注:释放物理内存会导致页面缓存被清除,下次运行程序需要的相关数据,需要再次从磁盘上进行读取,会“浪费” CPU 和 I/O 资源)最好的做法是尽可能多的使用缓存。由于页面缓存架构的原因,当程序调用 write() 时,字节只是被简单地拷贝到页面缓存中,并将这个页面标记为“脏”页面。磁盘 I/O 通常并不会立即发生,因此,你的程序并不会被阻塞在等待磁盘写入上。副作用是,如果这时候发生了电脑死机,你的写入将不会完成,因此,对于至关重要的文件,像数据库事务日志,要求必须进行 fsync()(仍然还需要去担心磁盘控制器的缓存失败问题),另一方面,读取将被你的程序阻塞,直到数据可用为止。内核采取预加载的方式来缓解这个矛盾,它一般提前预读取几个页面并将它加载到页面缓存中,以备你后来的读取。在你计划进行一个顺序或者随机读取时(请查看 madvise()readahead()Windows 缓存提示 ),你可以通过提示hint帮助内核去调整这个预加载行为。Linux 会对内存映射的文件进行 预读取,但是我不确定 Windows 的行为。当然,在 Linux 中它可能会使用 O_DIRECT 跳过预读取,或者,在 Windows 中使用 NO_BUFFERING 去跳过预读,一些数据库软件就经常这么做。一个文件映射可以是私有的,也可以是共享的。当然,这只是针对内存中内容的更新而言:在一个私有的内存映射上,更新并不会提交到磁盘或者被其它进程可见,然而,共享的内存映射,则正好相反,它的任何更新都会提交到磁盘上,并且对其它的进程可见。内核使用写时复制copy on write(CoW)机制,这是通过页面表条目page table entry(PTE)来实现这种私有的映射。在下面的例子中,render 和另一个被称为 render3d 的程序都私有映射到 scene.dat 上。然后 render 去写入映射的文件的虚拟内存区域:The Copy-On-Write mechanism两个程序私有地映射 scene.dat,内核误导它们并将它们映射到页面缓存,但是使该页面表条目只读。render 试图写入到映射 scene.dat 的虚拟页面,处理器发生页面故障。内核分配页面帧,复制 scene.dat 的第二块内容到其中,并映射故障的页面到新的页面帧。继续执行。程序就当做什么都没发生。上面展示的只读页面表条目并不意味着映射是只读的,它只是内核的一个用于共享物理内存的技巧,直到尽可能的最后一刻之前。你可以认为“私有”一词用的有点不太恰当,你只需要记住,这个“私有”仅用于更新的情况。这种设计的重要性在于,要想看到被映射的文件的变化,其它程序只能读取它的虚拟页面。一旦“写时复制”发生,从其它地方是看不到这种变化的。但是,内核并不能保证这种行为,因为它是在 x86 中实现的,从 API 的角度来看,这是有意义的。相比之下,一个共享的映射只是将它简单地映射到页面缓存上。更新会被所有的进程看到并被写入到磁盘上。最终,如果上面的映射是只读的,页面故障将触发一个内存段失败而不是写到一个副本。动态加载库是通过文件映射融入到你的程序的地址空间中的。这没有什么可奇怪的,它通过普通的 API 为你提供与私有文件映射相同的效果。下面的示例展示了映射文件的 render 程序的两个实例运行的地址空间的一部分,以及物理内存,尝试将我们看到的许多概念综合到一起。Mapping virtual memory to physical memory这是内存架构系列的第三部分的结论。我希望这个系列文章对你有帮助,对理解操作系统的这些主题提供一个很好的思维模型。via:https://manybutfinite.com/post/page-cache-the-affair-between-memory-and-files/作者:Gustavo Duarte 译者:qhwdw 校对:wxy本文由 LCTT 原创编译,Linux中国 荣誉推出Media

via https://linux.cn/article-9528-1.html?utm_source=rss&utm_medium=rss
用 PGP 保护代码完整性(二):生成你的主密钥

在本文中,我们将展示如何生成和保护你的 PGP 主密钥。Media

via https://linux.cn/article-9529-1.html
让我们做个简单的解释器(三)

识别出记号流中的词组的过程就叫做 解析。解释器或者编译器执行这个任务的部分叫做 解析器。解析也称为 语法分析,并且解析器这个名字很合适,你猜的对,就是 语法分析器。Media

via https://linux.cn/article-9521-1.html
如何解决 “mount.nfs: Stale file handle”错误

了解如何解决 Linux 平台上的 mount.nfs: Stale file handle 错误。这个 NFS 错误可以在客户端或者服务端解决。Media

via https://linux.cn/article-9530-1.html
如何解决 “mount.nfs: Stale file handle”错误

了解如何解决 Linux 平台上的 mount.nfs: Stale file handle 错误。这个 NFS 错误可以在客户端或者服务端解决。Media当你在你的环境中使用网络文件系统时,你一定不时看到 mount.nfs:Stale file handle 错误。此错误表示 NFS 共享无法挂载,因为自上次配置后有些东西已经更改。无论是你重启 NFS 服务器或某些 NFS 进程未在客户端或服务器上运行,或者共享未在服务器上正确输出,这些都可能是导致这个错误的原因。此外,当这个错误发生在先前挂载的 NFS 共享上时,它会令人不快。因为这意味着配置部分是正确的,因为是以前挂载的。在这种情况下,可以尝试下面的命令:确保 NFS 服务在客户端和服务器上运行良好。
# service nfs statusrpc.svcgssd is stoppedrpc.mountd (pid 11993) is running...nfsd (pid 12009 12008 12007 12006 12005 12004 12003 12002) is running...rpc.rquotad (pid 11988) is running...
如果 NFS 共享目前挂载在客户端上,则强制卸载它并尝试在 NFS 客户端上重新挂载它。通过 df 命令检查它是否正确挂载,并更改其中的目录。
# umount -f /mydata_nfs# mount -t nfs server:/nfs_share /mydata_nfs#df -k------ output clipped -----server:/nfs_share 41943040 892928 41050112 3% /mydata_nfs
在上面的挂载命令中,服务器可以是 NFS 服务器的 IP 或主机名。如果你在强制取消挂载时遇到像下面错误:
# umount -f /mydata_nfsumount2: Device or resource busyumount: /mydata_nfs: device is busyumount2: Device or resource busyumount: /mydata_nfs: device is busy
然后你可以用 lsof 命令来检查哪个进程或用户正在使用该挂载点,如下所示:
# lsof |grep mydata_nfslsof: WARNING: can't stat() nfs file system /mydata_nfs Output information may be incomplete.su 3327 root cwd unknown /mydata_nfs/dir (stat: Stale NFS file handle)bash 3484 grid cwd unknown /mydata_nfs/MYDB (stat: Stale NFS file handle)bash 20092 oracle11 cwd unknown /mydata_nfs/MPRP (stat: Stale NFS file handle)bash 25040 oracle11 cwd unknown /mydata_nfs/MUYR (stat: Stale NFS file handle)
如果你在上面的示例中看到共有 4 个 PID 正在使用该挂载点上的某些文件。尝试杀死它们以释放挂载点。完成后,你将能够正确卸载它。有时 mount 命令会有相同的错误。接着使用下面的命令在客户端重启 NFS 服务后挂载。
# service nfs restartShutting down NFS daemon: [ OK ]Shutting down NFS mountd: [ OK ]Shutting down NFS quotas: [ OK ]Shutting down RPC idmapd: [ OK ]Starting NFS services: [ OK ]Starting NFS quotas: [ OK ]Starting NFS mountd: [ OK ]Starting NFS daemon: [ OK ]
另请阅读:如何在 HPUX 中逐步重启 NFS即使这没有解决你的问题,最后一步是在 NFS 服务器上重启服务。警告!这将断开从该 NFS 服务器输出的所有 NFS 共享。所有客户端将看到挂载点断开。这一步将 99% 解决你的问题。如果没有,请务必检查 NFS 配置,提供你修改的配置并发布你启动时看到的错误。上面文章中的输出来自 RHEL6.3 服务器。请将你的评论发送给我们。via: https://kerneltalks.com/troubleshooting/resolve-mount-nfs-stale-file-handle-error/作者:KernelTalks 译者:geekpi 校对:wxy本文由 LCTT 原创编译,Linux中国 荣誉推出Media

via https://linux.cn/article-9530-1.html?utm_source=rss&utm_medium=rss
Linux 中的 5 个 SSH 别名例子

我们可以用 定义在你的 .bashrc 文件里的别名 或函数来大幅度缩减花在命令行界面(CLI)的时间。但这不是最佳解决之道。最佳办法是在 ssh 配置文件中使用 SSH 别名 。Media

via https://linux.cn/article-9531-1.html
区块链不适用的若干场景

这三个问题可以帮你避开不实宣传。Media

via https://linux.cn/article-9532-1.html
使用 Graylog 和 Prometheus 监视 Kubernetes 集群

在本文中,我将使用 Graylog (用于日志)和 Prometheus (用于指标)去打造一个 Kubernetes 集群的监视解决方案。Media

via https://linux.cn/article-9534-1.html
UMStor Hadapter:大数据与对象存储的柳暗花明

HDFS 仍旧是“计算存储融合”阵营的定海神针,不过,我们也在 Hadapter 上看到了“计算存储分离”的新未来。Media

via https://linux.cn/article-9535-1.html
怎样用 parted 管理硬盘分区

parted 是一个操作硬盘分区的程序。它支持多种分区表类型,包括 MS-DOS 和 GPT。Media

via https://linux.cn/article-9536-1.html
深度学习战争:Facebook 支持的 PyTorch 与 Google 的 TensorFlow

在这篇文章中,我们将 PyTorch 与 TensorFlow 进行不同方面的比较。Media

via https://linux.cn/article-9533-1.html
SQL 入门

使用 SQL 构建一个关系数据库比你想的更容易。Media

via https://linux.cn/article-9537-1.html
完全指南:在 Linux 中如何打印和管理打印机

本教程将介绍在 Linux 中如何使用 CUPS 去打印。Media

via https://linux.cn/article-9538-1.html
高级 SSH 速查表

所有人都知道 SSH 是一种远程登录工具,然而它也有许多其他用途。Media

via https://linux.cn/article-9540-1.html
高级 SSH 速查表

Media所有人都知道 SSH 是一种远程登录工具,然而它也有许多其他用途。创建一个 SOCKS 代理来浏览网页(也就是翻墙啦):
ssh -D <port> <remote_host>
设置 localhost:<port> 作为你浏览器的代理连接一个堡垒机后的 Windows RDP 主机:
ssh -L <port>:<target_host>:3389 <bastion_server> 
让你的 RDP 客户端连接到 localhost:<port>在不使用 VNC 端口的情况下,连接远程 VNC 主机:
ssh -L 5901:localhost:5901 <remote_host> 
让你的 VNC 客户端连接到 localhost:5901按照这个思路,你可以映射任意端口:LDAP (389)、631 (CUPS)、8080 (替代的 HTTP),等等。产生一个新的 SSH 密钥对:
ssh-keygen
更新密钥对的密码:
ssh-keygen -p
把公钥复制到远程主机上:
ssh-copy-id -i <identity file> <remote_host>
SSH 有一堆命令行选项,但是如果有一些是你经常使用的,你可以为它们在 SSH 配置文件 (${HOME}/.ssh/config) 里创建一个入口。比如:
host myhouse User itsme HostName house.example.com
那么你就可以输入 ssh myhouse 来代替 ssh itsme@house.example.com。以下是常用的命令行选项和他们的配置文件写法。一些是常用的简化写法。请查看 ssh(1) 和 ssh_config(5) 的手册页来获取详尽信息。命令行配置文件描述-l <login name>User <login name>远程主机的登录用户名。-i <identity file>IdentityFile <identity file>指定要使用的鉴权文件(SSH 密码对)。-p <remote port>Port <remote port>远程 SSH 守护进程监听的端口号。 (默认为 22)-CCompression <yes,no>压缩往来信息。 (默认为 no)-D <port>DynamicForward <port>把本地端口的报文转发到远程主机。-XForwardX11 <yes,no>把 X11 的图像数据转发到远程主机的端口. (默认为 no)-AForwardAgent <yes,no>把授权代理的报文转发给远程主机。如果你使用第三方主机登录,这个功能将很有用。 (默认为 no)-4(仅使用 IPv4)
-6 (仅使用 IPv6)AddressFamily <any,inet4,inet6>指定仅使用 IPv4 或者 IPv6。-L <local port>:<target host>:<target port>LocalForward <local port>:<target host>:<target port>把本地主机指定端口的报文转发到远程主机的某个端口。opensource.com Twitter @opensourceway | facebook.com/opensourceway | IRC: #opensource.com on Freenode作者简介:Ben Cotton 是业余的气象学家和职业的高性能计算工程师。Ben 是微软 Azure 的产品营销经理,专注于高性能计算。他是一个 Fedora 用户和贡献者,共同创立了一个当地的开放源码群,并且是开源促进会的成员和保护自由软件的支持者。通过以下方式联系他 Twitter (@FunnelFiasco) 或者 FunnelFiasco.com.via: https://opensource.com/sites/default/files/gated-content/cheat_sheet_ssh_v03.pdf作者:BEN COTTON 译者:kennethXia 校对:wxy本文由 LCTT 原创编译,Linux中国 荣誉推出Media

via https://linux.cn/article-9540-1.html?utm_source=rss&utm_medium=rss
如何创建一个 Docker 镜像

在这篇文章中,我们将学习创建 Docker 镜像的基本知识。Media

via https://linux.cn/article-9541-1.html
如何创建一个 Docker 镜像

Media前面的文章 中,我们学习了在 Linux、macOS、以及 Windows 上如何使用 Docker 的基础知识。在这篇文章中,我们将学习创建 Docker 镜像的基本知识。我们可以在 DockerHub 上得到可用于你自己的项目的预构建镜像,并且也可以将你自己的镜像发布到这里。我们使用预构建镜像得到一个基本的 Linux 子系统,因为,从头开始构建需要大量的工作。你可以使用 Alpine( Docker 版使用的官方版本)、Ubuntu、BusyBox、或者 scratch。在我们的示例中,我将使用 Ubuntu。在我们开始构建镜像之前,让我们先“容器化”它们!我的意思是,为你的所有 Docker 镜像创建目录,这样你就可以维护不同的项目和阶段,并保持它们彼此隔离。
$ mkdir dockerprojectscd dockerprojects
现在,在 dockerprojects 目录中,你可以使用自己喜欢的文本编辑器去创建一个 Dockerfile 文件;我喜欢使用 nano,它对新手来说很容易上手。
$ nano Dockerfile
然后添加这样的一行内容:
FROM Ubuntu
Media使用 Ctrl+Exit 然后选择 Y 去保存它。现在开始创建你的新镜像,然后给它起一个名字(在刚才的目录中运行如下的命令):
$ docker build -t dockp .
(注意命令后面的圆点)这样就创建成功了,因此,你将看到如下内容:
Sending build context to Docker daemon 2.048kBStep 1/1 : FROM ubuntu---> 2a4cca5ac898Successfully built 2a4cca5ac898Successfully tagged dockp:latest
现在去运行和测试一下你的镜像:
$ docker run -it Ubuntu
你将看到 root 提示符:
root@c06fcd6af0e8:/#
这意味着在 Linux、Windows、或者 macOS 中你可以运行一个最小的 Ubuntu 了。你可以运行所有的 Ubuntu 原生命令或者 CLI 实用程序。Media我们来查看一下在你的目录下你拥有的所有 Docker 镜像:
$docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEdockp latest 2a4cca5ac898 1 hour ago 111MBubuntu latest 2a4cca5ac898 1 hour ago 111MBhello-world latest f2a91732366c 8 weeks ago 1.85kB
你可以看到共有三个镜像:dockp、Ubuntu、和 hello-world, hello-world 是我在几周前创建的,这一系列的前面的文章就是在它下面工作的。构建一个完整的 LAMP 栈可能是一个挑战,因此,我们使用 Dockerfile 去创建一个简单的 Apache 服务器镜像。从本质上说,Dockerfile 是安装所有需要的包、配置、以及拷贝文件的一套指令。在这个案例中,它是安装配置 Apache 和 Nginx。你也可以在 DockerHub 上去创建一个帐户,然后在构建镜像之前登入到你的帐户,在这个案例中,你需要从 DockerHub 上拉取一些东西。从命令行中登入 DockerHub,运行如下所求的命令:
$ docker login
在登入时输入你的用户名和密码。接下来,为这个 Docker 项目,在目录中创建一个 Apache 目录:
$ mkdir apache
在 Apache 目录中创建 Dockerfile 文件:
$ nano Dockerfile
然后,粘贴下列内容:
FROM ubuntuMAINTAINER Kimbro Staken version: 0.1RUN apt-get update && apt-get install -y apache2 && apt-get clean && rm -rf /var/lib/apt/lists/*ENV APACHE_RUN_USER www-dataENV APACHE_RUN_GROUP www-dataENV APACHE_LOG_DIR /var/log/apache2EXPOSE 80CMD ["/usr/sbin/apache2", "-D", "FOREGROUND"]
然后,构建镜像:
docker build -t apache .
(注意命令尾部的空格和圆点)这将花费一些时间,然后你将看到如下的构建成功的消息:
Successfully built e7083fd898c7Successfully tagged ng:latestSwapnil:apache swapnil$
现在,我们来运行一下这个服务器:
$ docker run -d apachea189a4db0f7c245dd6c934ef7164f3ddde09e1f3018b5b90350df8be85c8dc98
发现了吗,你的容器镜像已经运行了。可以运行如下的命令来检查所有运行的容器:
$ docker psCONTAINER ID IMAGE COMMAND CREATEDa189a4db0f7 apache "/usr/sbin/apache2ctl" 10 seconds ago
你可以使用 docker kill 命令来杀死容器:
$docker kill a189a4db0f7
正如你所见,这个 “镜像” 它已经永久存在于你的目录中了,而不论运行与否。现在你可以根据你的需要创建很多的镜像,并且可以从这些镜像中繁衍出来更多的镜像。这就是如何去创建镜像和运行容器。想学习更多内容,你可以打开你的浏览器,然后找到更多的关于如何构建像 LAMP 栈这样的完整的 Docker 镜像的文档。这里有一个帮你实现它的 Dockerfile 文件。在下一篇文章中,我将演示如何推送一个镜像到 DockerHub。你可以通过来自 Linux 基金会和 edX 的 “介绍 Linux” 免费课程来学习更多的知识。via: https://www.linux.com/blog/learn/intro-to-linux/2018/1/how-create-docker-image作者:SWAPNIL BHARTIYA 译者:qhwdw 校对:wxy本文由 LCTT 原创编译,Linux中国 荣誉推出Media

via https://linux.cn/article-9541-1.html?utm_source=rss&utm_medium=rss
为初学者提供的 uniq 命令教程及示例

该命令会帮助你轻松地从文件中找到重复的行。它不仅用于查找重复项,而且我们还可以使用它来删除重复项,显示重复项的出现次数,只显示重复的行,只显示唯一的行等。Media

via https://linux.cn/article-9542-1.html
AI 和机器学习中暗含的算法偏见

我们又能通过开源社区做些什么?Media

via https://linux.cn/article-9543-1.html
在树莓派上运行 DOS 系统

不同的 CPU 架构意味着在树莓派上运行 DOS 并非唾手可得,但其实也没多麻烦。Media

via https://linux.cn/article-9544-1.html
如何在 Windows 10 上开启 WSL 之旅

WSL 可以让你访问 Windows 上的 Linux Bash shell。Media

via https://linux.cn/article-9545-1.html