0%

为 Linux 用户登录添加谷歌二次验证器保护

Google Authenticator 使用一次性密码(One-time Passcodes)(OTP)进行两步验证。iOS、Android 和 Blackberry 上都提供了 OTP 生成器应用。两步验证的机制集成在Linux的 PAM 系统中。

安装软件包

使用包管理器安装 libpam-google-authenticator 软件包,或从这里下载源码自行编译。
我使用pacman包安装软件包:sudo pacman -S libpam-google-authenticator
如果你用过Google Authenticator二次验证器的话你肯定知道,添加密钥的方式有两种:一种是扫描二维码添加密钥,另外一种是手动输入密钥。
若要扫码添加,则需要在终端生成配置二维码,你需要额外安装 qrencode 软件包。不想扫码的话你可以不用安装,这个软件包是一个可选项。

设置插入式验证模块

注意⚠️:若通过 SSH 进行 Google Authenticator 的所有配置,在完成所有配置并测试正常之前,请勿关闭 SSH 会话,否则可能会无法登录。此外,最好在激活 PAM 之前生成密钥文件,关于密钥文件在下方会有介绍。

通常情况下你仅仅需要在远程 SSH 登陆的时候才需要开启二次验证,对应的PAM的配置在文件 /etc/pam.d/sshd ,你可以使用vim或你熟悉的文本编辑器在非注释行的行首添加:
auth required pam_google_authenticator.so

sudoedit /etc/pam.d/sshd 编辑后的内容如下

/etc/pam.d/sshd
1
2
3
4
5
auth      required  pam_google_authenticator.so
auth include system-remote-login
account include system-remote-login
password include system-remote-login
session include system-remote-login

这样将会首先询问二次验证码,验证成功后才会询问密码,二者都要成功验证后才允许登录。
交换 pam_google_authenticator.sosystem-remote-login 两行会改变验证顺序。

若要二者任一正确即可登陆的话则,将配置信息修改为 auth sufficient pam_google_authenticator.so。此时会先询问二次验证码,若正确则直接登录到系统,否则验证密码,两者任一正确即可登录系统。

还需要在文件/etc/ssh/sshd_config内开启质疑-应答认证:
修改配置项为:ChallengeResponseAuthentication yes
并重载sshd服务:systemctl reload sshd

注意⚠️:如果当前你设置使用密钥登陆并禁止密码登录, OpenSSH 会忽略如上所有的配置,若想使用密钥登陆同时开启二次验证的话你需要几个额外步骤:
添加或修改/etc/ssh/sshd_config配置文件 :

  • 配置项 KbdInteractiveAuthentication yes
  • 配置项 ChallengeResponseAuthentication yes
  • 配置项 AuthenticationMethods publickey,keyboard-interactive:pam

默认的 PAM 认证规则中包含密码认证,既然只用密钥和二次验证码登陆系统,则需要修改 sshd 的 PAM 规则,编辑/etc/pam.d/sshd文件,关闭密码验证:

/etc/pam.d/sshd
1
2
3
4
5
6
7
#%PAM-1.0
#auth required pam_securetty.so #disable remote root
auth required pam_google_authenticator.so
#auth include system-remote-login # 在行首添加注释符,关闭密码登录规则。
account include system-remote-login
password include system-remote-login
session include system-remote-login

在修改完如上配置后需要重载sshd服务,sudo systemctl reload sshd
放心,我们目前为止所有的操作和添加的规则仅针对 SSH 远程登录有效,到目前位置还没有结束,你还需要最后一步,为用户生成密钥。

生成密钥文件

提示✅:可选安装 qrencode 软件包以在屏幕上生成可以扫描的二维码。扫描二维码以自动配置两次验证器。

每一个想要使用两次验证的用户需要在其用户目录生成一个密钥文件,使用命令 google-authenticator 来完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
$ google-authenticator                                                                                    ─╯

Do you want authentication tokens to be time-based (y/n) y
Warning: pasting the following URL into your browser exposes the OTP secret to Google:

Your new secret key is: J4T4C4HYT2KIA52WGSDJEOLM2I (验证器配置密钥)
Enter code from app (-1 to skip): 269371 (输入验证器生成的验证码)
Code confirmed
Your emergency scratch codes are: (紧急备用令牌码)
93394730
14394073
33491911
86112157
22174783

Do you want me to update your "/home/username/.google_authenticator" file? (y/n) y
(是否重新生成登录配置文件?)

Do you want to disallow multiple uses of the same authentication
token? This restricts you to one login about every 30s, but it increases
your chances to notice or even prevent man-in-the-middle attacks (y/n) y
(是否拒绝多次重复使用相同的令牌?这将限制你每30s仅能登录一次,但会提醒/阻止中间人攻击。)

By default, a new token is generated every 30 seconds by the mobile app.
In order to compensate for possible time-skew between the client and the server,
we allow an extra token before and after the current time. This allows for a
time skew of up to 30 seconds between authentication server and client. If you
experience problems with poor time synchronization, you can increase the window
from its default size of 3 permitted codes (one previous code, the current
code, the next code) to 17 permitted codes (the 8 previous codes, the current
code, and the 8 next codes). This will permit for a time skew of up to 4 minutes
between client and server.
Do you want to do so? (y/n) n
(是否将验证码有效窗口时间由1分30秒增加到约4分钟?这将缓解时间同步问题。)

If the computer that you are logging into isn't hardened against brute-force
login attempts, you can enable rate-limiting for the authentication module.
By default, this limits attackers to no more than 3 login attempts every 30s.
Do you want to enable rate-limiting? (y/n) y
(是否启用此模块的登录频率限制,登录者将会被限制为最多在30秒内登录3次。)

建议将备用令牌码保存在安全的地方(打印出来并放在一个安全的位置),因为当丢失手机(即你的两步验证器)或其他原因不能使用两步验证器时,只能使用备用令牌码登录。它们同时也被保存在~/.google_authenticator,你可以在登录后随时查阅。

设置二次验证器

在你的手机上安装两步验证器软件。例如:
Google Authenticator Google应用商店 / IOS.
在软件中创建一个新验证,输入密钥(如例子中的’J4T4C4HYT2KIA52WGSDJEOLM2I’)或扫描二维码来导入密钥,并依照屏幕提示输入验证码。

软件现在应该会显示一个每30秒更新的验证码。

测试验证

在完成测试前请不要断开目前已经的 SSH 连接!如果配置出错或者无法登陆还可以有补救的机会,否则失联的痛楚就要一人默默感受了!
现在请开启一个全新的终端,尝试 SSH 连接到完成了上述配置的主机,如果你配置了密码和二次验证,则下面的显示与你会有略微差别。
总之,尝试输入你的身份登录信息和二次验证码,若可以登陆系统,那恭喜你🎉!教程到此结束,下面的使一些其他玩法,若感兴趣请继续了解!

1
2
3
ssh username@你的服务器地址
Enter passphrase for key '/Users/username/.ssh/id_rsa': <输入密钥密码>
(username@你的服务器地址) Verification code: <输入二次验证码>

其他配置

用于桌面登陆

谷歌两次认证插件可以同时用于控制台与 GNOME 桌面登录。
只需要在文件 /etc/pam.d/login/etc/pam.d/gdm-password 内加入:
auth required pam_google_authenticator.so

存储位置

如果想要改变密钥存储位置,请使用 --secret 参数:

google-authenticator --secret="/PATH_FOLDER/USERNAME"
然后更改/etc/pam.d/sshd内的路径配置:

/etc/pam.d/sshd
1
auth required pam_google_authenticator.so user=root secret=/PATH_FOLDER/${USER}

user=root 用于强制PAM使用root用户权限来搜索文件。
另外请注意,密钥文件的所有者是root,生成文件的用户只能读取文件(chmod: 400):

chown root.root /PATH_FILE/SECRET_KEY_FILES
chmod 400 /PATH_FILE/SECRET_KEY_FILES


硬盘

硬盘发展简史

1956 年 9 月,世界上诞生了第一台磁盘存储设备 IBM 350 RAMAC(Random Access Method of Accounting and Control),没错,又是 IBM,这就是蓝色巨人的魅力,在几十年前已经为科技领航。此设备用磁头来读写数据,用盘片来存储数据,以后的硬盘都是以这样的模式发展。这个磁头可以直接移动 到盘片上的任何一块存储单元,从而实现了随机存储。虽然它的总容量只有 5MB,但是限于当时的制造 工艺水平,一共使用了 50 个直径为 24 英寸的盘片,摞起来的体积相当于冰箱那么大。
1968 年由 IBM 首次提出了“温彻斯特/Winchester”的技术,这可是个了不起的技术。“温彻斯特”技术的精髓是:“镀磁盘片在密封空间中高速自转,磁头悬浮在盘片上方,固定在磁头臂上沿盘片径向移动”。
磁头不与盘片接触也不应该接触,这是最容易想象的,如果磁头与盘片接触,在高速转动下摩擦,什么材料都会磨损,数据自然就丢啦。另外,盘片自转速度是存取数据速度的关键,如果磁头与盘片接触, 受摩擦力的影响,想快也快不了。
一个可行的方案是让磁头在盘片上方“悬浮”,与盘片保持非常近的距离, 类似咱们物理实验中的气垫导轨。盘片高速旋转会产生气流,磁头在这种气流下像飞碟一样悬浮,这样就保证了不会与盘片有摩擦。磁头被固定在磁头臂上,它能沿盘片径向移动,由于磁头和盘片各自的运动,再加上如此近的距离,所以哪怕一点灰尘都会造成磁盘的损伤。于是,磁头、盘片被密封在了一个盒子里。今天的硬盘依然是这样的结构。

世界上第一块基于 “温彻斯特”技术的硬盘,在 1973 年诞生, 它就是所有硬盘的源头IBM 3340,容量60MB,由两个30MB的存储单元拼合而成。从此硬盘技术的发 展便有了成形的结构基础。所以,今天的硬盘也称为温盘,在一般的可编程中断控制器上连接硬盘的引脚 都标有温彻斯特硬盘呢。
硬盘的随机存取是靠磁头臂不断移动实现 的,磁头臂移动到目标位置的时间称为寻道时间,如果存储的数据不连续,这一块那一片的,磁头就得不断调整 位置,这是机械式硬盘不可避免的,这便是硬盘的瓶颈所在,所以一般的硬盘都将寻道时间作为重要参数。

在众多竞争对象当中,也有一款顽强地活了下来,它就是 SSD 固态硬盘,人家也有几十年的历史了。 SSD 固态硬盘为我们带来了全新的解决方案,看起来就像大块的内存条。

存在即合理,尽管温彻斯特硬盘为了提速,内部加了很多缓存,应用了各种优化寻道的方法,甚至接口已经变成了串口,但主流磁盘转速还是 7200 转,速度却提升有限。于是 固态硬盘的优势开始显山露水,其最大的优点是速度快,但缺点也明显,容量低,价格也很贵。主要还是运用在要求存储速度较快的生产环境中如数据库系统。当然现在随着固态硬盘价格越来越便宜,也进入了咱们老百姓的家中。

硬盘的工作原理

为了讲清楚硬盘的工作原理,书的作者用画图板花了 1 个小时画了这张示意图,希望对大家有所帮助。

左边的主轴上有两张盘片,其实不止2张,这里示意性的画了两张。盘片固定在主轴上随主轴告诉转动,目前主流个人电脑硬盘上的转速是7200转/分钟。每个盘片分上下两面,每面都存储数据,每个盘面都由一个磁头来读取数据,所以一个盘面上对应2个磁头。
由于盘面与磁头是一一对应的关系,故用磁头号来表示盘面。磁头编号从上到下以 0 开始计数,所以用磁头 0 代表第 一个盘面。磁头不会自己在盘片上移动,它需要被固定在右 边的磁头臂上,在磁头臂的带动下,沿着盘片边缘向圆心的 方向来回摆动,注意,摆动的轨迹是个弧,并不是绝对径向地直来直去。一方面这是因为磁头臂是由步进电机驱动的, 磁头臂一端是步进电机主轴,另一端是磁头。步进电机每次都会转动一个角度,所以带动磁头臂在“画圆”,而磁头位 于磁头臂的另一端,所以也跟着呈钟摆运动,运动轨迹是个弧线,并不是直线。所以,图中磁头臂中标注了“类似于”径向运动,这就是“类似”的原因。
另一方面,磁头读取数据也不需要做直来直去的移动,能否找到数据,只跟它最终落点有关,和中间路径形状是没关系的。所以,一方面盘片的自转,另一方面磁头的摆动,这两种动作的合成,使磁头能够读取盘片任意位置的数据。

说完了硬盘内部各部件的运动,再说下存储逻辑。盘片表面是用于存储数据的磁性介质,为了更有效管理磁盘,这些磁性介质也被“格式化”成易于管理的格局,即将整个盘面划分为多个同心环,以圆心画扇形,扇形与每个同心环相交的弧状区域作为最基本的数据存储单元。
这个同心环就称为磁道,而同心环上的弧状区域是扇形的一部分,故称之为扇区,它作为我们向硬盘存储数据的最基本单位,大小是 512 字节
我们写入的数据最终是写进了磁道上的扇区中。注意啦,我上面说的磁道是个环,不是线,很多教科 书上介绍磁道时都简单画了个圆圈,这容易让人误解磁道是条线,线上可无法存储数据,“环”是有横截 面的,数据就存储这些“面积”中。磁头臂带动磁头在盘片上方移动,就是在找磁道的位置,盘片高速自转,就是在磁道内定位扇区。看图,配合着想像力理解一下。
磁道的编号和磁头一样也是从 0 开始的。相同编号的磁道组成的管状区域就称为柱面。图中, 两个盘片上编号相同的磁道,它们之间用灰色直线连接起来的部分,很像柱子的弧形表面,所以称柱面。 如果盘片非常多的话,“柱面”就显得非常形象了。

CPU与外设通信—IO接口#南桥-输入输出控制中心中提到了,CPU只会通过IO接口与外设通信,那么硬盘的IO接口就是硬盘控制器。硬盘控制器同硬盘的关系,如同显卡和显示器一样,它们都是专门驱动外部设备的模块电路,CPU只同他们说话,由它们将CPU的话转译给外部设备,这是它们的共同点。不过,不同的是显卡和显示适配器是分开的,因为显示器太大了,不能塞到机箱里,但是现在的硬盘和硬盘控制器是在一起的,为什么用但是这个词呢,是因为很早之前硬盘控制器和硬盘也是分开的,不过业界内几个大佬合作开发出的新的接口,这才将硬盘和硬盘控制器整合到一起,这种接口便称为集成设备电路(Integrated Drive Electronics,IDE)。
随着 IDE 接口标准的影响力越来越广泛,全球标准化协议将此接口使用的技术规范归纳成为全球硬盘标准,这样就产生了 ATA(Advanced Technology Attachment)。不过由于 IDE 这个名字已经叫开了,所以大家依然习惯称硬盘为 IDE 硬盘。
计算机发展非常快,新老交替的现象层出不穷,前几年刚出道的硬盘串行接口(Serial ATA,SATA),由于其是串行,所以之前的 ATA 接口只好称为并行 ATA,即(Parallel ATA,PATA)。以前一般的主机只支持 4 个并口硬盘,但自从出现串口硬盘后, 情况就变了,支持多少块硬盘,取决于主板的能力。有的主板同时兼容这两种接口,如图所示。
!


这两种线缆完全不同,左边 PATA 接口的线缆也称为 IDE 线,一个 IDE 线上可以挂两块硬盘,一个是主盘(Master),一个是从盘(Slave)。一个主板支持这样的 4 块 IDE(PATA)硬盘,所以主板上提供两个 IDE 插槽。这两个接口也是以 0 为起始编号的,一个称为 IDE0,另一个称为 IDE1。不过按 ATA 的 说法,这两个插槽称为通道,IDE0 叫作 Primary 通道,IDE1 叫作 Secondary 通道。即使主板上安装的是 SATA 硬盘,它也兼容 PATA 的编程接口,向上兼容是计算机能源源不断向前发展的根基。所以,后面给出的端口号也将按照 PATA 这两个通道来分组给出。
这里所说的主盘 master、从盘 slave 别和 Primary 通道、Secondary 通道搞混了,通道是 channel,不是 disk,每个通道上分别有主盘和从盘。

硬盘控制器端口

接下来的内容是关于如何控制硬盘了。让硬盘工作,我们需要通过读写硬盘控制器的端口,端口的概念在此重复一下,端口就是位于IO控制器上的寄存器,此处的端口是指硬盘控制器上的寄存器。
下面图标中列出了部分端口,对于我们今后的应用,这几个端口足够了。

端口可以被分为两组,Command Block registers和Control Block registers。Command Block registers 用于向硬盘驱动器写入命令字或者从硬盘控制器获得硬盘状态,Control Block registers 用于控制硬盘工作状态。在 Control Block registers 组中的寄存器已经精减了,重点介绍 Command Block registers 组中的寄存器。

端口是按照通道给出的,不要误以为端口是直接针对某块硬盘的,不是这样的,一个通道上的主、从两块硬盘都用这些端口号。要想操作某通道上的某块硬盘,需要单独指定。上面的表格中就又一个叫 device 的寄存器,顾名思义,指的就是驱动器设备,也就是和硬盘相关。不过此寄存器是 8 位的,一个通道上就两块硬盘,指定哪一个硬盘只用 1 位就够了,寄存器资源宝贵不能浪费, 所以此寄存器是个杂项,很多设置都需集中在此寄存器中了,其中的第 4 位,便是指定通道上的主或从硬 盘,0 为主盘,1 为从盘。
端口用途在读硬盘和写硬盘时还是有点区别的,比如拿Primary通道上的0x1F1端口来说,读操作时, 若读取失败,里面存储的是失败状态信息,所以称为 error 寄存器,并且 0x1F2 端口中存储未读的扇区数。 写操作时就变成了 feauture 寄存器,此寄存器用于写命令的参数。
这么做的目的还是因为节省寄存器的成本…前辈们为了节省资源真的是用尽了奇技淫巧。

寻址方式 CHS 和 LBA

硬盘中的扇区在物理上是用“柱面-磁头-扇区”来定位的(Cylinder Head Sector),简称为 CHS。

但每次我们要事先算出扇区是在哪个盘面,哪个柱面上,这太麻烦了,这对于磁头来说很直观,它就是根据这 些信息来定位扇区的。可是咱们还是希望有一套对人来说较直观的寻址方法,我们希望磁盘中扇区从 0 开 始依次递增编号,不用考虑扇区所在的物理结构。其实我在描述需求时已经说出了 LBA 的定义,这是一 种逻辑上为扇区址的方法,全称为逻辑块地址(Logical Block Address)。

LBA有两种模式:

  • LBA28模式:LBA28,用 28 位比特来描述一个扇区的地址。最大寻址范围是 2 的 28 次方等 于 268435456 个扇区,每个扇区是 512 字节,最大支持 128GB。我们这里为图简单,采用 LBA28 模式。
  • LBA48模式:由于 128GB 已经不能满足日益增长的存储需求,硬盘越来越大了,得有相匹配的寻址方法与之配套,于 是要介绍的另外一种是 LBA48,用 48 位比特来描述一个扇区的地址,最大可寻址范围是 2 的 48 次方, 等于 281474976710656 个扇区,乘以 512 字节后,最大支持 131072TB,即 128PB。

硬盘中各寄存器的作用

按照上表硬盘_硬盘控制器主要端口寄存器来逐一介绍下寄存器的作用。

data 寄存器在名字上我们就知道它是负责管理数据的,它相当于数据的门,数据能进,也能出,所以其作用是读取或写入数据。数据的读写还是越快越好,所以此寄存器较其他寄存器宽一些,16 位(已经很不错了,表中其他寄存器都是 8 位的)。在读硬盘时,硬盘准备好的数据后,硬盘控制器将其放在内部的缓冲区中,不断读此寄存器便是读出缓冲区中的全部数据。在写硬盘时,我们要把数据源源不断地输送到此端口, 数据便被存入缓冲区里,硬盘控制器发现这个缓冲区中有数据了,便将此处的数据写入相应的扇区中。

读硬盘时,端口 0x171 或 0x1F1 的寄存器名字叫 Error 寄存器,只在读取硬盘失败时有用,里面才会记录失败的信息,尚未读取的扇区数在Sector count寄存器中。在写硬盘时,此寄存器有了别的用途,所以有了新的名字,叫 Feature 寄存器。有些命令需要指定额外参数,这些参数就写在 Fea ture 寄存器中。 强调一下,error 和 feature 这两个名字指的是同一个寄存器,只是因为不同环境下有不同的用途,为了区别这两种用途,所以在相应环境下有不同的名字。这两个寄存器都是 8 位宽度。

Sector count 寄存器用来指定待读取或待写入的扇区数。硬盘每完成一个扇区,就会将此寄存器的值 减 1,所以如果中间失败了,此寄存器中的值便是尚未完成的扇区。这是 8 位寄存器,最大值为 255,若 指定为 0,则表示要操作 256 个扇区。

刚刚在[寻址方式 CHS 和 LBA](#寻址方式 CHS 和 LBA)中介绍完了 LBA,现在可以说 LBA 寄存器了,这里有 LBA low、LBA mid、LBA high 三个,它们三个 都是 8 位宽度的。LBA low 寄存器用来存储 28 位地址的第 0~7 位,LBA mid 寄存器用来存储第 8~15 位, LBA high 寄存器存储第 16~23 位。哎?三个 8 位的加起来才 24 位,连 LBA28 都不够,咱们怎么用呢?
有问题就有解决方案,这就引出了下一个寄存器,device 寄存器。

device 寄存器是个杂项,它的宽度是 8 位。在此寄存器的低 4 位用来存储 LBA 地址 的第 24~27 位。结合上面的三个 LBA 寄存器。第 4 位用来指定通道上的主盘或从盘,0 代表主盘,1 代 表从盘。第 6 位用来设置是否启用 LBA 方式,1 代表启用 LBA 模式,0 代表启用 CHS 模式。另外的两位: 第 5 位和第 7 位是固定为 1 的,称为 MBS 位,大家不用关注啦。

在读硬盘时,端口 0x1F7 或 0x177 的寄存器名称是 Status,它是 8 位宽度的寄存器,用来给出硬盘的 状态信息。第 0 位是 ERR 位,如果此位为 1,表示命令出错了,具体原因可见 error 寄存器。第 3 位是 data request 位,如果此位为 1,表示硬盘已经把数据准备好了,主机现在可以把数据读出来。第 6 位是 DRDY, 表示硬盘就绪,此位是在对硬盘诊断时用的,表示硬盘检测正常,可以继续执行一些命令。第 7 位是 BSY 位,表示硬盘是否繁忙,如果为 1 表示硬盘正忙着,此寄存器中的其他位都无效。另外的 4 位暂不关注。

在写硬盘时,端口 0x1F7 或 0x177 的寄存器名称是 command,和上面说过的 error 和 feature 寄存器情况 一样,只是用途变了,所以换了个名字表示新的用途,它和 status 寄存器是同一个。此寄存器用来存储让硬 盘执行的命令,只要把命令写进此寄存器,硬盘就开始工作了。在咱们的系统中,主要使用了三个命令:

  • identify: 0xEC,即硬盘识别。
  • read sector:0x20,即读扇区。
  • write sector:0x30,即写扇区。

在此只列出了本书需要的指令,大家若对此感兴趣,还是去看 ATA 手册,您懂的,里面内容丰富详实,相信大家一定会一饱眼福。
总结下寄存器 error、feature 和 status、command,大家可以这样来助记:这两组都是同一寄存器(也就是同一端口)多个用途,对同一端口写操作时,硬盘控制器认为这是个命令,对同一端口读操作时,硬盘控制器认为是想获得状态。

常用的硬盘操作方法

硬盘中的指令很多,各指令的用法也不同。有的指令直接往 command 寄存器中写就行了,有的还要 在 feature 寄存器中写入参数,最权威的方法还是要去参考 ATA 手册。由于本书中用到的都是简单的指令, 所以对此抽象出一些公共的步骤仅供参考之用。
不管是读硬盘,还是写硬盘,都不是一个指令就完事的。相关寄存器都需要设置。要是读硬盘,得告 诉读哪个扇区,读几个扇区,用哪种模式对扇区寻址,LBA?CHS?写硬盘也一样,写哪个,写几个,还要设置操作的是哪个通道的哪个硬盘……讲了这么多寄存器,心想,我到底先设置哪个寄存器呢?有没有个一般硬盘操作的基本顺序呢?其实最主要的顺序就是 command 寄存器一定得是最后写,因为一旦 command 寄存器被写入后,硬盘就开始干活啦,它才不管其他寄存器中的值对不对,一律拿来就用,有问题的话报错就好啦。其他寄存器顺序不是很重要。

  1. 先选择通道,往该通道的 sector count 寄存器中写入待操作的扇区数。
  2. 往该通道上的三个 LBA 寄存器写入扇区起始地址的低 24 位。
  3. 往 device 寄存器中写入 LBA 地址的 24~27 位,并置第 6 位为 1,使其为 LBA 模式,设置第 4位,选择操作的硬盘(master 硬盘或 slave 硬盘)。
  4. 往该通道上的 command 寄存器写入操作命令。
  5. 读取该通道上的 status 寄存器,判断硬盘工作是否完成。
  6. 如果以上步骤是读硬盘,进入下一个步骤。否则,完工。
  7. 将硬盘数据读出。

硬盘工作完成后,它已经准备好了数据,咱们该怎么获取呢?一般常用的数据传送方式如下。

  • 无条件传送方式。
  • 查询传送方式。
  • 中断传送方式
  • 直接存储器存取方式(DMA)
  • I/O处理机传送方式

对于上面的第 1 种“无条件传送方式”,应用此方式的数据源设备一定是随时准备好了数据,CPU 随时取随时拿都没问题,如寄存器、内存就是类似这样的设备,CPU 取数据时不用提前打招呼。

第 2 种“查询传送方式”,也称为程序 I/O、PIO(Programming Input/Output Model),是指传输之前, 由程序先去检测设备的状态。数据源设备在一定的条件下才能传送数据,这类设备通常是低速设备,比 CPU 慢很多。CPU 需要数据时,先检查该设备的状态,如果状态为“准备好了可以发送”,CPU 再去获取数据。硬盘有 status 寄存器,里面保存了工作状态,所以对硬盘可以用此方式来获取数据。

第 3 种“中断传送方式”,也称为中断驱动 I/O。上面提到的“查询传送方式”有这样的缺陷,由于 CPU 需要不断查询设备状态,所以意味着只有最后一刻的查询才是有意义的,之前的查询都是发生在数据尚未准备好的时间段里,所以说效率不高,仅对于不要求速度的系统可以采用。可以改进的地方是如果数据源设备 将数据准备好后再通知 CPU 来取,这样效率就高了。通知 CPU 可以采用中断的方式,当数据源设备准备好数据后,它通过发中断来通知 CPU 来拿数据,这样避免了 CPU 花在查询上的时间,效率较高。

第 4 种“直接存储器存取方式(DMA)”。在中断传送方式中,虽然极大地提高了 CPU 的利用率,但 通过中断方式来通知 CPU,CPU 就要通过压栈来保护现场,还要执行传输指令,最后还要恢复现场。似乎有同学说此方式已经很爽了,你还想怎样?哈哈,其实更爽的是一点都不要浪费 CPU 资源,不让 CPU 参与传输,完全由数据源设备和内存直接传输。CPU 直接到内存中拿数据就好了。这就是此方式中“直接”的意思。不过 DMA 是由硬件实现的,不是软件概念,所以需要 DMA 控制器才行。

第 5 种“I/O 处理机传送方式”。不知大家发现了没有,在说上面每一种的时候都把它们各自说得特别好,似乎完美不可替代了,但该公司一出新产品,就开始自曝曾经无与伦比的老一代产品的问题以突显现在产品更胜一筹。DMA 已经借助其他硬件了,CPU 已经很轻松了,难道还有更爽的方式?
是啊,DMA 方式中 CPU 还嫌爽的不够,毕竟数据输入之后或输出之前还是有一部分工作要由 CPU 来完成的,如数据交换、组合、校验等。 如果 DMA 控制器再强大一点,把这些工作帮 CPU 做了就好了。也是哦,既然为了解放 CPU,都已经引用一个硬件(DMA)了,干脆一不做二不休,再引入一个硬件吧。于是,I/O 处理机诞生啦,听名字就知道它专门用于处理 IO,并且它其实是一种处理器,只不过用的是另一套擅长 IO 的指令系统,随时可以处理数据。有了 I/O 处理机的帮忙,CPU 甚至可以不知道有传输这回事,这下 CPU 才真正爽到家啦。同样, 这也是需要单独的硬件来支持。

综上所述,硬盘不符合第 1 种方法,因为它需要在某种条件下才能传输。第 4 种和第 5 种需要单独 的硬件支持,先不说我们的 bochs 能否模拟这两种硬件,单独学习这两类硬件的操作方法就很头疼,大家有兴趣的话还是先放一放,以后再琢磨吧。所以在我们的系统中,我们用了第 2、3 这两种(查询传送、中断传送)软件传输方式。

让MBR使用硬盘

我们的MBR从BIOS手中接过了接力棒,但是MBR只有512字节,这么小的空间着实干不了大事情。从MBR接棒的那时候我们就知道还会继续交接的,但是下一棒要交给谁?

接下来在之前的MBR基础上,做一些改进,让它能够读写硬盘!

我们的MBR受限于512字节大小,在那么小的空间中,没法为内核做好准备环境,更没法将内核成功加载到内存并运行。所以要在另一个程序中万层初始化环境及加载内核的任务,这个程序叫 loader ,即加载器。
问题来了,loader在哪里?如果跳过去执行?这就是新改版MBR的使命,简而言之就是负责把loader加载到内存,并将接力棒交给它。
由于MBR占据了硬盘的第0扇区(以LBA逻辑寻址方式,扇区从0开始编号,若以物理CHS方式,扇区则从1开始编号),第一扇区是空闲的可以用,但和MBR离的太近了不是很放心,所以把loader放在第2扇区。MBR从第2扇区中给它读出来。读出来放到哪里呢?原则上是找个空闲地方就行了,在表[[实模式下的内存布局.png]]中查看下,只要在“用途”列中注明“可用区域”的地方都可以用。 0x500~0x7BFF 和 0x7E00~9FBFF 这两段内存区域都可以。

首先,loader中要定义一些数据结构(如[[GDT全局描述符表]],不懂没关系,后面会说),这些数据结构将来的内核还是要继续使用的,所以loader加载到内存后不能被覆盖。
其次,随着咱们不断添加功能,内核必然越来越大,其所在的内存地址也会向越来越高的地方发展,难免会超过可用区域的上限,先尽量把loader放在内存低处,多留出一些空间给内核。 所以,将loader的加载地址选为0x900。为什么不是0x500,这个多省空间?还是预留出一定空间把,彼此隔开远一点心里才踏实,不差这点空间了~这完全是个人偏好,大家随意!
按照上面所说的规划,接下来的代码就是改头换面的新款MBR,代码量增长到126行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
;主引导程序
;------------------------------------------------------------

%include "boot.inc"
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800
mov gs,ax

;清屏,利用0x06号功能,上卷全部行,即可清屏
;------------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;------------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为 0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值

mov ax,0600h
mov bx,0700h
mov cx,0 ;左上角(0,0)
mov dx,184fh ;右下角(80,25)
;因为 VGA文本模式中,一行只能容纳80个字符,共25行。
;下标从0开始,所以0x18=24,0x4f=79
int 10h ;触发中断

; 输出字符串:MBR
mov byte [gs:0x00],'1'
mov byte [gs:0x01],0xA4
;A 表示绿色背景闪烁,4 表示前景色为红色

mov byte [gs:0x00],' '
mov byte [gs:0x01],0xA4

mov byte [gs:0x00],'M'
mov byte [gs:0x01],0xA4

mov byte [gs:0x00],'B'
mov byte [gs:0x01],0xA4

mov byte [gs:0x00],'R'
mov byte [gs:0x01],0xA4

;------------------------------------------------------------
mov eax,LOADER_START_SECTOR ;起始扇区lba地址
mov bx,LOADER_BASE_ADDR ;写入的地址
mov cx,1 ;读入的扇区数
call rd_disk_m_16 ;以下读取程序的起始部分(一个扇区)

jmp LOADER_BASE_ADDR

;-------------------------------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;-------------------------------------------------------------------------------
; eax=LBA扇区号
; bx=将数据写入的内存地址
; cx=读入的扇区数
mov esi,eax ;备份eax
mov di,cx ;备份cx
;读写硬盘:
;第1步:设置要读取的扇区数
mov dx,0x1f2 ;0x1f2 Primar通道 扇区数端口
mov al,cl
out dx,al

mov eax,esi ;恢复 ax
;第2步: 将LBA地址存入0x1f3 - 0x1f6

;LBA地址 7~0 位写入端口 0x1f3
mov dx,0x1f3
out dx,al

;LBA地址 15~8位写入端口 0x1f4
mov cl,8
shr eax,cl ;右移cl寄存器中的位数
mov dx,0x1f4
out dx,al

;LBA地址 23~16位写入端口 0x1f5
shr eax,cl ;不需要重新为cl赋值
mov dx,0x1f5
out dx,al

shr eax,cl
and al,0x0f ;lba第 24~27 位
or al,0xe0 ;设置MBS位和LBA模式以及使用0主盘
mov dx,0x1f6 ;设置device端口号
out dx,al ;

;第3步:向0x1f7端口写入读指令,0x20
mov dx,0x1f7
mov al,0x20 ;读扇区
out dx,al ;写入指令到command端口的时刻,硬盘根据上面的参数开始读扇区

;第4步:检查硬盘状态
.not_ready:
;同一端口,写时表示写入命令字,读时表示读入硬盘状态
nop ;nop指令表示空操作,仅为了增加一些延迟,相当于小sleep
in al,dx
and al,0x88 ;只关注第4位和第8位
;第 4 位为 1 表示硬盘控制器已准备好数据传输
;第 7 位为 1 表示硬盘忙
cmp al,0x08
jnz .not_ready

;第5步:从0x1f0中读取数据
mov ax,di
mov dx,256 ;因为data寄存器是16位的
mul dx ;所以共需 di*512/2 次,所以 di*256
mov cx,ax ;di为要读取的扇区数,一个扇区有512字节,每次读入一个字节
mov dx,0x1f0
.go_on_read:
in ax,dx
mov [bx],ax
add bx,2
loop .go_on_read
ret

times 510-($-$$) db 0

db 0x55,0xaa

程序最开始的%include "boot.inc",这个%include 是 nasm 编译器中的预处理指令,意思是让编译器在 编译之前把 boot.inc 文件包含进来。任何编译器都应该有 include 之类的能够包含其他文件的预处理指令, 不要认为底层的汇编语言就应该简陋到一穷二白,这和语言是没关系的,是编译器为了开发人员方便管理代码,应该加的。
boot.inc 的内容很简单,目前就两句话,文件内容如下。

1
2
3
;-------------	 loader和kernel   ----------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

boot.inc 是我们的配置文件,我们目前关于加载器的配置信息就写在里面,今后还会在此添加更多的配置信息。大家看到的这两句也是预处理命令,是 nasm 提供的宏,和 C 语言中的宏是一回事。只不过 nasm 中的语法是:宏名 equ 值,而 C 语言中的宏是由#define 指令来实现的。所以 LOADER_BASE_ADDR 和 LOADER_START_SECTOR 是两个宏名。
函数名 rd_disk_m_16 的意思是“在 16 位模式下读硬盘”,此函数是咱们本节的重点。
第64行的“mov esi,eax”是把eax中的值先备份到esi中。因为al在out指令中会被用到,这会影 响到eax的低8位。
第 65 行是备份读取的扇区数到 di 寄存器,di 寄存器是 16 位的,和 cx 大小一致。cx 的值会在读取数 据时用到,所以在此提前备份。
第 67~70 行,按照咱们操作硬盘的约定,先选定一个通道,再往 sector count 寄存器中写扇区数。往端口中写入数据用 out 指令,注意 out 指令中 dx 寄存器是用来存储端口号的。先查看咱们 bochs 配置文件关于硬盘的配置部分如下:

1
2
3
# 硬盘设置
ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
ata0-master: type=disk, path="hd60M.img", mode=flat

咱们的虚拟硬盘属于 ata0,是 Primary 通道,所以其 sector count 寄存器是由 0x1f2 端口来访问的。顺便再看第二行的 ata0-master,path=”hd60M.img”,这说明 hd60M.img 是主盘。
第74~95行是将LBA地址写入三个LBA 寄存器和device寄存器的低4位。端口0x1f3是寄存器LBA low,端口 0x1f4 是寄存器 LBA mid,端口 0x1f5 是寄存器 LBA high。shr 指令是逻辑右移指令,这里主要 通过此指令置换出地址的相应部分,写入相应的LBA寄存器。第93行的“or al,0xe0”,用了or“或” 指令和 0xe0 做或运算,拼出 device 寄存器的值。高 4 位为 e,即高 4 位的二进制表示为 1110,其第 5 位 和第 7 位固定为 1,第 6 位为 1 表示启用 LBA。大家可以参考注释。
第 97~100 行便是写入命令啦,因为我们这里是读操作,所以读扇区的命令是 0x20。通过 out 指令写 入 command 端口 0x1f7 后,硬盘就开始工作了。
第 102~109 行检测 status 寄存器的 BSY 位。由于 status 寄存器依然是 0x1f7 端口,所以不需要再为 dx 重新赋值。
105 行的 nop 表示空操作,即什么也不做,只是为了增加延迟,相当于 sleep 了一小下,目 的是减少打扰硬盘的工作。对同一端口在读写两种操作时有不同的用途,在读硬盘时,此端口中的值是硬 盘的工作状态。
第 106 行是将 Status 寄存器的值读入到 al 寄存器,通过第 107 行的 and“与”操作,保留 第 4 位和第 7 位,第 4 位若为 1,表示数据已经准备好,可以传输了。若第 7 位为 1,表示硬盘现在正忙着。只要判断第 4 位是否为 1 就好了,用第 108 行的 cmp 指令和 0x08 做减法运算,判断第 4 位是否为 1。
cmp 指令并不改变操作数的值,只是根据结果去设置标志位,从而咱们根据标志位反着去判断结果。cmp 指令会影响的标志位有 ZF、CF、PF 等,这里咱们借助 ZF 位来判断 cmp 的结果。于是用第 109 行的 jnz .not_ready 来判断结果是否不等于 0,即若等于 0,则 status 寄存器的第 4 位为 1,这表示可以读数据了。 若不等于 0,说明 status 寄存器的第 4 位为 0,表示硬盘正忙(此时 status 寄存器第 7 位肯定为 1)。.not_ready 是个标号,于是跳回去继续判断硬盘状态,直到硬盘把数据准备好才跳出这个循环。
第 111~122 行是从硬盘取数据的过程。由于 data 寄存器是 16 位,即每次 in 操作只读入 2 字节,根据读入的数据总量(扇区数*512 字节)来求得执行 in 指令的次数。这里的乘法用 mul 指令,在实模式下, mul 指令可以做 8 位乘法和 16 位乘法,格式是:mul 操作数。操作数可以是寄存器或内存。乘法运算至少要有两个数参与才行,这里的操作数只是一个乘数,被乘数隐含在 al 或 ax 寄存器中(mul 指令被设计 成这样的,由于历史原因产生很多奇怪的用法,习惯就好啦)。如果操作数是 8 位,被乘数就是 al 寄存器的值,乘积就是 16 位,位于 ax 寄存器。如果操作数是 16 位,被乘数就是 ax 寄存器的值,乘积就是 32 位,积的高 16 位在 dx 寄存器,积的低 16 位在 ax 寄存器。
虽然我们进行的是 16 位的乘法,其结果是 32 位,但由于我知道这两个乘数 ax 的值和 dx 的值都不大,ax 的实际的值其实是 1,乘出来的这个结果,其高位是 0,所以在第 115 行的“mov cx, ax”我们只将这个结果的低 16 位移入 cx 作为循环读取的次数。此处用 8 位乘法不合适,因为 256 超过了 8 位寄存器表示的范围。
在第 118~122 行通过循环来将数据写入 bx 寄存器指向的内存,每读入 2 个字节,bx 所指的地 址便+2。值得注意的是由于在实模式下偏移地址为 16 位,所以用 bx 只会访问到 0~FFFFh 的偏移。待写 入的地址超过 bx 的范围时,从硬盘上读出的数据会把 0x0000~0xffff 的覆盖,所以此处加载的程序不能 超过 64KB,即 2 的 16 次方等于 65536。由于本 mbr 是用来加载 loader 的,所以 loader.bin 要小于 64KB 才行。这一点大可以放心,我们最终的 loader 不超过 2KB,将来的内核也不会超过 70KB。
也许有同学会说,把 bx 改为 ebx 行吗?也不行,在实模式下,CPU 依然会用 16 位偏移地址。这是实 模式下访问内存的规定与缺陷,还记得那个“段基址+段内偏移地址”吗?段内偏移地址正因为是 16 位, 只能访问 64KB 的段空间,所以才将段基址乘以 16 来突破这 64KB,从而实现访问低调 1MB 空间的。
第 123 行返回指令 ret,它用来从函数中返回。如果我们没有定义函数,就不需要它了。函数和一般 代码相比,就是在被调用时,CPU 会将返回地址压到栈中,所以在函数体中,要用 ret 指令将栈中的返回 地址重新加载到程序计数器中,如 cs:ip,这样程序便恢复到之前的执行顺序了。
执行完第 123 行后,程序便回到了第 55 行,这是个跳转的指令。个人觉得,jmp 指令和 call 指令是必不 可少的,jmp 表示一去不回头,call 表示去了还回来。各有各的用途。这里是 MBR 交出接力棒的一刻,采用 jmp 是唯一合适的选择。Jmp 的操作数是 LOADER_BASE_ADDR,即 0x900,这是要跳到内核加载器的节奏。 MBR 到此结束了使命,顺序完成了第二棒的拼接。

接下来的工作是编译,本次的编译较之前相比,多加了一个参数 -I。此参数的意思还是参见 nasm 帮 助,nasm –h 回车,找到-I 的说明。
-I<path> adds a pathname to the include file path
大概意思是添加一个包含文件的路径,其实就是添加个库目录。为了目录整洁一些,我在 boot 目录下建立了一个子目录 include,并把 boot.inc 放到了 include 目录下。nasm 要用-I 指定库目录,所以在 boot 目录下输入: nasm -I include/ -o mbr.bin mbr.S 回车。
接下来用 dd 命令将 mbr.bin 写入虚拟硬盘
dd if=./mbr.bin of="./hd60M.img" bs=512 count=1 conv=notrunc

实现内核加载器

这一节的内容并不长,因为在进入[[保护模式 | 保护模式]]之前,我们能做的不多,loader 是要经过实模式到保护模式的过渡,并最终在保护模式下加载内核。本节只实现一个简单的 loader,本 loader 只在实模式下工作,等学习了保护模式后,我们再来个真格的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
;---------------loader程序开始----------
; 输出背景色绿色,前景色红色,并且跳动的字符串"1 MBR"
mov byte [gs:0x00],'2'
mov byte [gs:0x01],0xA4
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4
;A表示绿色背景闪烁,4表示前景色为红色

mov byte [gs:0x04],'L'
mov byte [gs:0x05],0xA4

mov byte [gs:0x06],'O'
mov byte [gs:0x07],0xA4

mov byte [gs:0x08],'A'
mov byte [gs:0x09],0xA4

mov byte [gs:0x0a],'D'
mov byte [gs:0x0b],0xA4

mov byte [gs:0x0c],'E'
mov byte [gs:0x0d],0xA4

mov byte [gs:0x0e],'R'
mov byte [gs:0x0f],0xA4

jmp $ ;死循环使程序停留在这里

本 loader 程序第 2 行代码用到了 LOADER_BASE_ADDR,所以在第 1 行中把 boot.inc 包含进来了, 其值是 0x900。
编译后,将loader.bin写入硬盘第二个分区,第 0 个扇区是 MBR,第 1 个扇区是空的未使用,原因如前所述,纯粹个人喜好。
dd if=./loader.bin of=./hd60M.img bs=512 count=1 seek=2 conv=notrunc
接下来用bochs开始验证,如果程序正确的话,MBR会跳转到loader.bin去运行,屏幕上会显示”2 LOADER”并且闪烁!效果如下图所示。

腾讯云/轻量云 安装ArchLinux的个人记录

本文仅用于个人学习使用,过阵子再以教程的形式重新发布出来。如果你现在完全按这篇文章操作不一定会成功。

请同时搭配记录我的联想M720Q Arch Sway窗口管理器的安装和配置过程食用。

准备工作

下载镜像

wget https://mirrors.tuna.tsinghua.edu.cn/archlinux/iso/2021.08.01/archlinux-2021.08.01-x86_64.iso

编辑/boot/grub/grub.cfg文件,添加下列内容

1
2
3
4
5
6
7
8
9
10
set timeout=60
menuentry 'ArchISO' --class iso {
set isofile=/archlinux-2021.08.01-x86_64.iso
loopback loop0 $isofile
#archisolabel设置archiso文件驻留的文件系统标签。
#img_dev指明archiso文件驻留的设备
#img_loop是archiso文件在img_dev里的绝对位置
linux (loop0)/arch/boot/x86_64/vmlinuz-linux archisolabel=ARCH20210801 img_dev=/dev/vda1 img_loop=$isofile
initrd (loop0)/arch/boot/x86_64/initramfs-linux.img
}

Reboot 选择ArchISO引导项

安装

替换文件

mkdir mnt

mount -o rw /dev/vda1 mnt

mount -o rw,remount mnt

rm -rf mnt/*

pacstrap ./mnt base linux linux-firmware networkmanager sudo openssh

genfstab -U mnt >> mnt/etc/fstab

vim mnt/etc/fstab 将ro修改为rw可读写

切换到Arch

arch-chroot mnt

安装一些可能接下来会用到的包

pacman -S vim bash-completion

剩下还有一些步骤参考记录我的联想M720Q Arch Sway窗口管理器的安装和配置过程

开机自启动一些服务

systemctl enable NetworkManager sshd

如果没有启动NetworkManagersshd这两个服务的话,下次启动时没有网络并且无法ssh远程访问。

添加用户并设置密码

username替换为你要创建的用户名。

useradd -m username

passwd username

配置sudo

usermod -aG wheel username

编辑/etc/sudoers文件允许wheel组中用户使用sudo提权到root级别权限。

一定要将公钥添加到用户家目录的.ssh/authorized_keys文件内,否则重启后只能通过VNC访问这台机器进行后续配置。

安装grub引导
pacman -S grub efibootmgr
grub-install –target=i386-pc /dev/vda
生成grub2配置文件
grub-mkconfig -o /boot/grub/grub.cfg

reboot
重启后将使用本机ssh远程这台Arch轻量云实例,所以需要你在重启前配置好sshd服务并且将本机公钥传入到刚刚创建的用户的家目录

记录一次引导失败插曲:
忘记生成grub2配置文件,好在腾讯轻量云提供了远程VNC。
按照下面的命令,手动引导系统。

1
2
3
4
grub> set root=(hd0,msdos1)
grub> linux /boot/vmliunz-linux root=/dev/vda1
grub> initrd /boot/initramfs-linux.img
grub> boot

进入系统后再生成grub2配置文件grub-mkconfig -o /boot/grub/grub.cfg

后续配置

安装一些常用的包

pacman -S vim bash-completion

设置时区时间

timedatectl set-timezone Asia/Shanghai

hwclock --systohc

设置使用的区域

编辑/etc/locale.gen然后移除需要的 地区 前的注释符号 #

接着执行locale-gen以生成locale信息

编辑/etc/locate.conf配置文件,没有则新建:

文本内输入: LANG=en_US.UTF-8

设置全局区域环境为美国英语环境。

设置主机名

hostnamectl set-hostname 主机名

开启BBR

Kernel 4.9 及以后已经合并了 BBR 的代码,BBR有可能默认安装。
使用modinfo tcp_bbr查看BBR内核模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
filename:       /lib/modules/5.13.7-arch1-1/kernel/net/ipv4/tcp_bbr.ko.zst
description: TCP BBR (Bottleneck Bandwidth and RTT)
license: Dual BSD/GPL
author: Soheil Hassas Yeganeh <soheil@google.com>
author: Yuchung Cheng <ycheng@google.com>
author: Neal Cardwell <ncardwell@google.com>
author: Van Jacobson <vanj@google.com>
srcversion: 3ADF6435C60E99C5F186F19
depends:
retpoline: Y
intree: Y
name: tcp_bbr
vermagic: 5.13.7-arch1-1 SMP preempt mod_unload
sig_id: PKCS#7
signer: Build time autogenerated kernel key

查看是否开启BBR:
sysctl net.ipv4.tcp_congestion_control

开启BBR:

sudo sysctl net.ipv4.tcp_congestion_control=bbr

1
2
3
4
5
6
7
8
$ sysctl net.ipv4.tcp_congestion_control
net.ipv4.tcp_congestion_control = cubic
$ sudo modprobe tcp_bbr
$ lsmod | grep tcp_bbr
tcp_bbr 20480 0
$ sudo sysctl net.ipv4.tcp_congestion_control=bbr
net.ipv4.tcp_congestion_control = bbr

开启后效果可以立即看到,如果想在重启后依然使用BBR,则需要写入到文件系统:

echo "tcp_bbr" > /etc/modules-load.d/80-bbr.conf

echo "net.ipv4.tcp_congestion_control=bbr" > /etc/sysctl.d/80-bbr.conf

记录我的联想M720Q Arch Sway窗口管理器的安装和配置过程

安装Arch Linux基本过程纪录

下载并烧录镜像

本次下载2021.5.1日的镜像,写这篇文章的时候是六一儿童节,但是并没有看到这个月的镜像。好耶,都去过六一了!

应该是最后一次用东软维云的镜像站咯!下载Arch 2021.5.1 ISO走起。

引导系统

引导Arch ISO前在BIOS中关闭安全引导,借着还有Windows的时候,更新了一下BIOS到2021年三月份的版本。

链接到互联网

在 Arch ISO Live中安装了iwd来管理无线网络,因为本次安装过程只有无线网络,那么用iwctl工具来配置本次安装过程中使用的无线网络。

进入iwctl交互模式: iwctl

罗列设备: device list,在列表中可以找到wlan0无线设备,记下来他的名字,接下来会使用到这个名称。

扫描无线网络: station wlan0 scan

罗列已发现的网络 station wlan0 get-networks

连接到无线网络 station wlan0 connect SSID

删除多余的引导

使用efibootmgr删除Windows引导。

efibootmgr -b 000X -B

更新系统时间

使用timedatectl更新时间,开启ntp。确保系统时间是准确的: timedatectl set-ntp true

建立硬盘分区

使用fdisk工具对磁盘进行分区,我要将整个磁盘都空间都划分给Arch使用。

fdisk /dev/nvme0n1

  • ESP分区 1G
  • SWAP分区 8G
  • 根文件存储 剩余空间

格式化磁盘文件系统

将ESP被格式化成,ESP分区格式化成FAT32分区格式: mkfs.vfat -F32 /dev/nvme0n1p1

创建SWAP文件系统:

mkswap /dev/nvme0n1p2

swapon /dev/nvme0n1p2

创建根目录文件系统ext4: mkfs.ext4 /dev/nvme0n1p3

挂载分区

挂载根分区目录: mount /dev/nvme0n1p3 /mnt

创建EFI目录并进行挂载:

mkdir /mnt/efi

mount /dev/nvme0n1p1 /mnt/efi

选择镜像源

文件 /etc/pacman.d/mirrorlist 定义了软件包会从哪个镜像源下载。在 LiveCD 启动的系统上,在连接到因特网后,reflector 会通过选择 20 个最新同步的 HTTPS 镜像并按下载速率对其进行排序来更新镜像列表。

在列表中越前的镜像在下载软件包时有越高的优先权。您或许想检查一下文件,看看是否满意。如果不满意,可以相应的修改 /etc/pacman.d/mirrorlist 文件,并将地理位置最近的镜像源挪到文件的头部,同时也应该考虑一些其他标准。

这个文件接下来还会被 pacstrap 拷贝到新系统里,所以请确保设置正确。

安装系统软件包

pacstrap /mnt base linux linux-firmware base-devel networkmanager

写入引导

用以下命令生成 fstab 文件 (用 -U 或 -L 选项设置UUID 或卷标):

genfstab -U /mnt >> /mnt/etc/fstab

强烈建议在执行完以上命令后,后检查一下生成的 /mnt/etc/fstab 文件是否正确。

切换根目录

Chroot更当前进程及其子进程的可见根路径。变更后,程序无法访问可见根目录外文件和命令。

arch-chroot /mnt

安装一些常用软件包

pacman -S vim bash-completion

设置时区时间

timedatectl set-timezone Asia/Shanghai

hwclock --systohc

设置使用的区域

编辑/etc/locale.gen然后移除需要的 地区 前的注释符号 #

接着执行 locale-gen 以生成 locale 信息

编辑/etc/locate.conf:

LANG=en_US.UTF-8

设置全局区域环境为美国英语环境。

稍后用户可以编辑自己的~/.config/locale.conf文件来设置使用的语言和特定的区域格式。如:

  • LANG
  • LANGUAGE
  • LC_ADDRESS
  • LC_COLLATE
  • LC_CTYPE
  • LC_IDENTIFICATION
  • LC_MEASUREMENT
  • LC_MESSAGES
  • LC_MONETARY
  • LC_NAME
  • LC_NUMERIC
  • LC_PAPER
  • LC_TELEPHONE
  • LC_TIME

LANG:默认的区域设置
这个变量的值会覆盖掉所有未设置的 LC_* 变量的值。

相关详情访问: Arch Wiki - Locate(简体中文)

设置主机名

hostnamectl set-hostname Archxxxxxx

设置用户及ROOT密码

设置root用户密码: passwd

添加普通用户并设置密码:

useradd -m username

passwd username

将用户添加到管理员组(wheel): usermod -aG wheel username

编辑/etc/sudoers文件允许wheel组中用户使用sudo提权到root级别权限。

安装引导程序

安装引导是安装的最后一步,使用GRUB配置本机引导,首先安装grubefibootmgr两个包。

pacman -S grub efibootmgr

然后按照下列步骤安装 GRUB:

挂载 EFI 系统分区,在本节之后的内容里,挂载点为/efi

选择一个启动引导器标识,这里叫做 GRUB。这将在 esp/EFI/ 中创建一个与标识同名的目录来储存 EFI 二进制文件,而且这个名字还会在 UEFI 启动菜单中表示 GRUB 启动项。

执行下面的命令来将 GRUB EFI 应用 grubx64.efi 安装到 esp/EFI/GRUB/,并将其模块安装到 /boot/grub/x86_64-efi/。

grub-install --target=x86_64-efi --efi-directory=/efi --bootloader-id=GRUB

生成GRUB配置文件: grub-mkconfig -o /boot/grub/grub.cfg

安装后的一些系统服务配置

使用NetworkManager管理本机网络

刚才在安装过程中使用pacstrap安装软件包时就已经将networkmanager软件包安装好了,接下来开启NetworkManager服务,并使用nmcli配置连接无线网络。

systemctl enable --now NetworkManager

罗列当前可用的无线网络: nmcli device wifi list

连接到SSID_NAME无线网络
nmcli device wifi connect SSID_NAME ifname wlp3s0 password <mypassword> name XXXXX

若要交互式的输入密码,可以加-a --ask参数,避免在终端内输入密码留下痕迹。

nmcli device wifi connect SSID_NAME ifname wlp3s0 name XXXXX

此时缺省password参数,会在终端内进行交互式输入密码。

PPPoE / DSL support

若要使用PPPoE拨号,需要rp-pppoe软件包后,才可以用nmcli连接。

pacman -S rp-pppoe

nmcli connection add type pppoe pppoe.username "myusername" pppoe.password "mypassword" ifname "enp1s0f1" con-name "NDR" autoconnect yes

nmcli connection up NDR

配置EAP PEAP认证无线网络

校内的无线网络使用EAP认证,通过查阅资料使用nmcli添加EAP无线网络方法如下:

nmcli connection add type wifi con-name my-CUCC ifname wlp3s0 ssid CUCC ipv4.method auto 802-1x.eap peap 802-1x.phase2-auth mschapv2 802-1x.identity yourusername 802-1x.password yourpassword wifi-sec.key-mgmt wpa-eap autoconnect yes

桌面窗口服务

使用Wayland图形显示服务,sway平铺窗口管理器。

常见的环境配置文件所在地址

对于特定用户,还可以在 /.bashrc、/.xinitrc 或 ~/.xprofile 中设置自己的用户环境。不同之处在于:

.bashrc:每次使用终端登录时读取并运用里面的设置。

.xinitrc:每次使用 startx 或 SLiM 启动 X 界面时读取并运用里面的设置。

.xprofile:每次使用 GDM 等显示管理器登录时读取并运用里面的设置。

安装wayland和Sway以及依赖的一些包

pacman -S wayland sway

pacman -S alacritty swayidle

有关使用Wayland中的常见问题详见Arch Wiki - Wayland

配置Sway平铺窗口管理器

配置在登录系统后自动启动sway,以及配置Sway的程序启动器,任务栏,和各种优化。

配置tty登录后自动运行sway

vim ~/.bash_profile

启动Sway时,设置环境变量,其中包含让firefox使用wayland的标记变量以及QT环境等变量信息。

vim ~/.local/bin/sway

1
2
3
4
5
6
7
8
#!bin/sh
set -a
[ -f $HOME/.config/sway/env ] && . "$HOME/.config/sway/env"
set +a

if [ -z $DISPLAY ] && [ "$(tty)" = "/dev/tty1" ]; then
exec sway
fi

编辑环境变量文件:

1
2
MOZ_ENABLE_WAYLAND=1
QT_QPA_PLATFORM=wayland
  • MOZ_ENABLE_WAYLAND=1 : 火狐浏览器使用Wayland图形窗口服务标记
  • QT_QPA_PLATFORM=wayland : 在Wayland环境下使用Qt5需要安装qt5-wayland包,并设置QT_QPA_PLATFORM=wayland环境变量。

使用wofi作为程序启动坞

安装wofi: yay -S wofi

修改Sway配置文件

set $menu dmenu_path | wofi --show drun -i | xargs swaymsg exec --

rofi-emoji

使用dex自动启动程序-Fcitx5(自启动fcitx5)

fcitx5等一些应用会创建XDG Autostart启动配置,通常以.desktop文件拓展名结尾。像fcitx5等一些程序包在安装时会在/etc/xdg/目录下生成配置文件,但是只有部分的窗口管理器支持xdg启动。在Sway中目前没有看到原生对它的支持,所以就需要一些其它工具辅助执行。

那么若要在进入桌面环境后自动启动fcitx5,则可以使用dex工具。它可以自动执行/etc/xdg/autostart目录下的.desktop程序启动配置文件。

安装dex: yay -S dex

在Sway配置文件中配置自启动dex:

~/.config/sway/config 文件末尾处添加: exec_always "dex -a"

之后在Sway启动后,会执行dex工具,dex工具又回去执行哪些在XDG自动启动目录下,但是在Sway中不会自动执行的程序启动配置文件.desktop,此时fcitx5已经可以自动启动了。

安装中文字体

推荐安装以下常用开源中文字体:

  • wqy-microhei
  • wqy-microhei-lite
  • wqy-bitmapfont
  • wqy-zenhei
  • noto-fonts-cjk

yay -S wqy-microhei wqy-microhei-lite wqy-bitmapfont wqy-zenhei noto-fonts-cjk

更新字体缓存: fc-cache -fv

关于更多本土化中文字体设置详见

Arch Wiki Simplified Chinese

锁屏工具swaylock

我使用的是swaylock-effects

创建锁屏执行脚本文件: sudo vim /usr/bin/lock-screen

1
2
3
4
5
6
#!/bin/bash
#
swaylock --screenshots --clock --indicator --indicator-radius 100 \
--indicator-thickness 7 --effect-blur 7x5 --effect-vignette 0.5:0.5 \
--ring-color bb00cc --key-hl-color 880033 --line-color 00000000 \
--inside-color 00000088 --separator-color 00000000 --grace 2 --fade-in 0.2

赋予全体用户执行权限: chmod u+x /usr/bin/lock-screen

接下里配置Sway配置文件: vim .config/sway/config

1
2
3
4
5
6
7
8
9
10
11
12
### Idle configuration
#
# Example configuration:
#
exec swayidle -w \
timeout 300 'lock-screen &' \
timeout 500 'swaymsg "output * dpms off"' \
resume 'swaymsg "output * dpms on"' \
before-sleep 'lock-screen &'
# This will lock your screen after 300 seconds of inactivity, then turn off
# your displays after another 200 seconds, and turn your screens back on when
# resumed. It will also lock your screen before your computer goes to sleep.

上面的部分配置为Sway默认配置,简单修改为上面的配置即可,修改好配置文件好需要重启Sway后才可以生效,因为使用的是exec,所以重新加载配置文件还不能生效,如果想要查看效果。可以: killall -9 sway ,之后重新登录系统等待查看效果。

延时工具swayidle配置

swayidle我用来和swaylock-effects搭配使用,用于在一定时间内无操作后自动执行脚本或程序,所以可以用来解决一定时间哪无操作自动锁屏的需求。

zsh

安装zsh: yay -S zsh

更改用户默认Shell: usermod --shell /usr/bin/zsh username

要注意bash_profile等文件是否有内容,如果有的话需要迁移为zsh相关的文件。

初始化默认配置文件:

1
2
3
git clone git://github.com/robbyrussell/oh-my-zsh.git ~/.oh-my-zsh
cp ~/.oh-my-zsh/templates/zshrc.zsh-template ~/.zshrc
source .zshrc

常用软件包

这里会记录一些我常用的软件包

  • git
  • vim
  • bash-completion bash自动补全
  • rp-pppoe 对PPPoE的支持
  • openssh openssh 需要开启服务
  • rclone 挂载云盘
  • yay 使用yay管理AUR源
  • proxychains-ng 终端代理,可配置代理链
  • v2ray-core 请珍惜眼前所拥有的一切
  • ntfs-3g 挂载ntfs设备
  • qt5-wayland 在Wayland下支持Qt5,需要配置环境变量

开启archlinuxcn

我使用清华大学archlinuxcn源

使用方法:

/etc/pacman.conf 文件末尾添加以下两行,并安装 archlinuxcn-keyring 包导入 GPG key。

1
2
[archlinuxcn]
Server = https://mirrors.tuna.tsinghua.edu.cn/archlinuxcn/$arch

安装fcitx5-rime中文输入法

安装以下包:yay -S fcitx5 fcitx5-qt fcitx5-gtk fcitx5-configtool fcitx5-rime qt5-wayland

若在Wayland环境下使用Qt5需要安装qt5-wayland包,并设置QT_QPA_PLATFORM=wayland环境变量

yay安装

1
2
3
git clone https://aur.archlinux.org/yay.git
cd yay
makepkg -si

或者在开启了archlinuxcn源后直接通过源安装。

pacman -S yay

让yay通过proxychains使用代理

go版本的yay不能使用proxychains进行代理,所以需要替换安装以下的软件包:

1
2
yay -S gcc-go (代替go)
yay -S yay (or yay-git)

重新安装yay后还需要更改proxychains的配置文件:将52行附近的proxy_dns前使用#注释#proxy_dns

Edge浏览器

从AUR源中安装微软Edge浏览器: yay -S microsoft-edge-dev-bin

在Wayland环境下,还需要对Edge浏览器的启动脚本做些手脚才能正式使用。

vim /usr/bin/microsoft-edge-dev

在exec指令中添加两个参数:

-enable-features=UseOzonePlatform

-ozone-platform=wayland

目前没办法在EDGE浏览器中使用输入法,待解决

安装Wine

Wine目前原生不支持wayland,需要Xwayland支持。不过经过搜索后发现Wine的Git仓库中存在了wayland分支,并且在持续开发,截止到目前已经支持wayland环境与Wine运行的应用共享剪贴板。

接下来尝试使用wayland的分支尝试编译运行。

emmmm失败了,错误提示找不到显示驱动,先不折腾它,接下来试试Xwayland吧。

安装Xwayland

yay -S xorg-xwayland

安装wine

Wine可通过开启Multilib仓库来安装wine包及依赖。

vim /etc/pacman.conf

1
2
[multilib]
Include = /etc/pacman.d/mirrorlist

yay -Sy

yay -S insatll wine

Jenkins Linux 安装

在官网下载Jenkins

下载当前最新的稳定版Jenkins保存到指定目录。

安装包下载

wget -O ./jenkins.war http://mirrors.jenkins.io/war-stable/latest/jenkins.war

配置Jenkins的JAVA运行环境

Kenkins需要Java虚拟机才可以运行,在运行Jenkins前先配置JAVA虚拟机环境。

下载 Java JRE Linux X64

Java JRE安装包下载

wget -O ./jre-8u281-linux-x64.tar https://javadl.oracle.com/webapps/download/AutoDL?BundleId=244058_89d678f2be164786b292527658ca1605

安装Java JRE

将下载好的Java JRE安装包移动你要安装的目录里,我想要安装在/usr/java目录下。

(ROOT)创建安装目录sudo mkdir -p /usr/java

(ROOT)移动安装包sudo mv ./jre-8u281-linux-x64.tar /usr/java

(ROOT)解压 sudo tar -zxvf /usr/java/jre-8u281-linux-x64.tar

查看/usr/java目录你会发现除了安装包外新存在一个名字与jre1.8.0_281差不多相同的目录(这取决于你下载的JavaJRE版本)

那么现在你的Java JRE安装在了/usr/java/jre1.8.0_281内,现在可以将/usr/java/目录下的Java JRE安装包删除掉以节省空间

接下来配置环境变量,请根据自己Linux发行版的规则变通的配置/etc/profile

/etc/profile文件开始处填写:

1
2
3
4
export JAVA_HOME=/usr/java/jre1.8.0_281/
export JRE_HOME=/usr/java/jre1.8.0_281/
export CLASS_PATH=.:$JAVA_HOME/lib/rt.jar:$JRE_HOME/lib
export JAVA_PATH=$JAVA_HOME/bin:$JRE_HOME/bin

之后找到PATH,在PATH末尾填写上${JAVA_PATH}。请根据自己的实际情况填写,否则可能会覆盖之前的环境变量哦!

我的环境变量配置如下:

1
2
3
4
5
6
7
8
9
10
11
export JAVA_HOME=/usr/java/jre1.8.0_281/
export JRE_HOME=/usr/java/jre1.8.0_281/
export CLASS_PATH=.:$JAVA_HOME/lib/rt.jar:$JRE_HOME/lib
export JAVA_PATH=$JAVA_HOME/bin:$JRE_HOME/bin

if [ "`id -u`" -eq 0 ]; then
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${JAVA_PATH}"
else
PATH="/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:${JAVA_PATH}"
fi
export PATH

激活环境变量 source /etc/profile

查看配置是否成功则执行java -version,出现版本信息则配置成功:

1
2
3
java version "1.8.0_281"
Java(TM) SE Runtime Environment (build 1.8.0_281-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, mixed mode)

初始化Jenkins

准备工作已经结束,接下来开始进行初始化Jenkins!

回到下载Jenkins的目录,执行java -jar jenkins.war --httpPort=8080命令。这里的8080是Web界面的端口号,你可以修改成自己需要的端口号,注意防火墙要放行该TCP端口。

首次运行Jenkins,首次运行Jenkins时会在当前用户家目录下生成一些配置文件~/.jenkins

访问你的域名或IP地址加上端口号后,你可以看到这样的界面。

01-first-interface-2021-02-06-14-31-08

此时Jenkins会下载一些资源,需要等待5分钟左右。如果时间过长,结束掉现在运行的Jenkins,先需要更改国内下载源来加快速度:

1
2
3
4
5
6
7
8
9
10
11
cd ~/.jenkins
vim hudson.model.UpdateCenter.xml

# 将<url>标签内的网址改为清华源

# 将下面的内容
<url>https://updates.jenkins.io/update-center.json</url>
# 修改为

<url>https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json</url>

之后回到Jenkins软件包的目录下重新执行java -jar jenkins.war --httpPort=8080

稍后页面会让你输入管理员密码解锁安装Jenkins。管理员密码可以在运行Jenkins的终端上找到,也可以根据页面提示的文件中找到。

Jenkins安装解锁密码

输入密码后下一步会安装基本常用插件,选择左侧的选项开始安装即可。

Jenkins安装常用插件

此时需要一段时间等待……

如果出现插件安装失败,不用担心,很可能是Jenkins版本不支持当前下载的最新插件,稍后进入到Jenkins管理页面更新Jenkins版本即可。

Jenkins安装插件部分插件安装失败

接下来根据你的个人需求配置管理员用户名密码等设置。

配置开机自动启动 systemd 守护进程

以下内容使用于以war包形式部署Jenkins的方式来配置systemd守护进程服务,其它方式安装的Jenkins需要你自己变通。

以下内容参考linux(centos8):用systemctl管理war包形式的jenkins(java 14 / jenkins 2.257)这篇文章。

创建管理脚本

vim jenkins-control.sh创建新文本文件,将以下内容写入到文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash
#

source /etc/profile

pid=`ps -ef | grep jenkins.war | grep -v 'grep'| awk '{print $2}'| wc -l`
if [ "$1" = "start" ];then
if [ $pid -gt 0 ];then
echo 'jenkins is running...'
else
nohup $JAVA_HOME/bin/java -jar /home/hackinsss/Jenkins/jenkins.war --enable-future-java --httpPort=8088 2>&1 &
fi
elif [ "$1" = "stop" ];then
exec ps -ef | grep jenkins | grep -v grep | awk '{print $2}'| xargs kill -9
echo 'jenkins is stop..'
else
echo "Please input like this:"./jenkins.sh start" or "./jenkins stop""
fi

为脚本文件赋予执行权限 chmod u+x jenkins-control.sh

启动Jenkins ./jenkins-control.sh start

关闭Jenkins ./jenkins-control.sh stop

上面两个操作没有问题,能正确开启或关闭Jenkins的话则脚本运行没有问题,开始编辑systemd服务文件。

创建systemd服务文件

sudo vim /etc/systemd/system/jenkins.service

修改运行Jenkins的用户和脚本路径地址为你自己的。不推荐使用root权限运行Jenkins

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Unit]
Description=Jenkins
After=network.target

[Service]
Type=forking
User=使用该用户运行Jenkins
Group=使用该用户组运行Jenkins
ExecStart=/home/hackinsss/Jenkins/jenkins-control.sh start
ExecReload=
ExecStop=/home/hackinsss/Jenkins/jenkins-control.sh stop
PrivateTmp=true

[Install]
WantedBy=multi-user.target

sudo systemctl daemon-reload

设置开机自动运行Jenkins sudo systemctl enable jenkins

运行Jenkins sudo systemctl start jenkins

停止运行Jenkins sudo systemctl start jenkins

安装Jenkins流程记录到此结束。

有些精品好课真的是百看不厌、教师精心准备、学习氛围也好。不过有些已购买的课程却只能在线观看…现在讲究的是一个环保、节能。如果能将已购买的课程离线下载好,那么会为各大平台节省多少带宽费用当然还有数据包在这世间重复的传输所浪费的电力资源。

本着环保、节能、减排的目的,开始了尝试对腾讯课堂网页版中自己购买了课程的视频回放进行下载。

然后顺便又看了下钉钉的回放。

github直达项目:https://github.com/yeefire/cloud-class-replay

腾讯课堂

由于没有使用客户端,在网页上观看。使用浏览器中的开发者工具来寻找请求。

tencent_class_safari_devnet-2020-12-28-00-30-43

可以看到请求了很多视频片段,文件拓展名为ts,GET请求。于是直接复制求请求连接然后丢在浏览器地址栏播放,结果不行,是加密的。

因为看到了ts,那么八九不离十使用的是M3U文件存储分段多媒体信息。

ts是日本高清摄像机拍摄下进行的封装格式文件,全称为MPEG2-TS。

M3U8是Unicode版本的M3U,用UTF-8编码。”M3U”和”M3U8”文件都是苹果公司使用的HTTP Live Streaming格式的基础,这种格式可以在iPhone和Macbook等设备播放。

寻找M3U

在请求中搜索m3u,出现了几个m3u8拓展文件,选择资源文件最大的那个m3u8文件,获取cURL请求或者其他方式将其下载到本地方便进一步分析。

现在找到了m3u文件,我们可以获取到这节课的所有分段视频了。

tencent_class_safari_search_m3u8-2020-12-28-00-48-19

分析腾讯课堂M3U文件

已经下载好了m3u8拓展文件,接下来打开文件进行分析!

可以看到腾讯课堂的每个分段视频是使用AES-128进行加密的,好在下载到的m3u8文件里给出了解密密钥的地址以及偏移量。

这下我们有了密钥和偏移量还有分段视频的请求参数(还不知道HTTP请求路径)

tencent_class_m3u8_file-2020-12-28-01-06-24

tencent_class_m3u8_check-2020-12-28-01-11-59

整理总结

现在可以尝试下载一个小的视频片段,不过目前还没有请求视频片段的完整路径,只有一个个的请求参数。这个好办,再回到浏览器中播放回放视频,观察浏览器开发者工具中的网络请求动态,找到’vxxxxxx.ts’请求,并查看获取该视频片段的完整HTTP请求路径。

发现和m3u8的请求路径与其相似。

m3u: https://xxxxxxxxxx.vod2.myqcloud.com/xxxxxxxxxxxxxx/b4e0xxxxxxxxxxxxxx7/drm/voddrm.xxxxxxxxxxxxx

ts: https://xxxxxxxxxx.vod2.myqcloud.com/xxxxxxxxxxxxxx/b4e0xxxxxxxxxxxxxx7/drm/v.fxxxx.ts?start=195027344&end=195666559&type=mpegtsxxxxxxxxxxxxx

在最后出现/斜杠位置前的所有请求路径都相同。并且斜杠后的请求参数正是m3u文件中的一个个分段视频的请求参数,看来仅仅需要简单的拼接就可以将这些分段视频下载好了。

那么现在我们有了全部分段视频的下载请求地址、解密算法、解密密钥及偏移量。有了这些就可以尝试下载分段视频并进行解密和合并了。

先尝试解密一个分段视频试试看:

1
2
3
4
5
with aiofiles.open(m3u8_encrypt_file, mode='rb') as f:
f = f.read()
content_video_part = AES.new(key, AES.MODE_CBC, iv).decrypt(f)
with aiofiles.open(dest_decrypt_file, mode='wb') as f:
f.write(content_video_part)

可以正常播放,没有问题。接下来下载全部的分段视频并解密,最后重新整合为一个mp4格式视频文件。剩下的交给脚本处理了!

腾讯课堂回放下载脚本

脚本使用异步进行请求下载分段视频和解密视频,尽可能的以最快的速度下载好全部的分段视频。

如果下载期间遇到网络波动,脚本可以自动重试下载。

若脚本意外停止,可以继续追加下载,不必全部重新开始下载分段视频。

使用方法:

  • 先安装依赖模块 pip3 install pycrypto m3u8 aiofiles requests_async
  • 命令行执行 python3 tencent_class_m3u8.py 这节课的名称 这节课的M3U文件请求地址(网址或者本地路径都可以)

例如: python3 tencent_class_m3u8.py 【Python进阶】Python-上午 https://1dada217.vod2.myqcloud.com/fdadadada3kmdkfsxxxxxxxxxxxxxxxxx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
from Crypto.Cipher import AES
import requests_async as requests
import aiofiles
import m3u8
import os, sys
import asyncio

class_video_name = sys.argv[1]
m3u8_file_uri = sys.argv[2]
prefix_request_url = f'{m3u8_file_uri.rsplit("/", 1)[0]}/'


async def download_m3u8_video(index: int, suffix_url: str):
if not os.path.exists(f'{class_video_name}/downloads/{index}.ts'):
i = 0
while i < 3:
try:
download_video_ts = await requests.get(url=prefix_request_url + suffix_url, timeout=30)
with open(f'{class_video_name}/downloads/{index}.ts', "wb") as ts:
ts.write(download_video_ts.content)
print(f'[{class_video_name}]——已下载第 {index} 个片段/ 共 {len(playlist.files)} 个片段')
return
except requests.exceptions.RequestException:
print(f'[{class_video_name}]——下载超时,正在重新下载第 {index} 个片段/ 共 {len(playlist.files)} 个片段')
await asyncio.sleep(i)
i += 1


async def download_m3u8_all():
if not os.path.exists(class_video_name + '/downloads'):
os.makedirs(class_video_name + '/downloads')
download_async_list = [asyncio.create_task(download_m3u8_video(i, video_suffix_url))
for i, video_suffix_url in enumerate(playlist.files, 1)]
await asyncio.wait(download_async_list)

download_encrypt_list = [uri for uri in os.listdir(f'{class_video_name}/downloads') if uri[0] != '.']
if len(download_encrypt_list) == len(playlist.files): # 判断是否有漏下的分段视频没有下载
print(f'[{class_video_name}]——视频全部下载完成')
return download_encrypt_list
else: # 有部分视频在三次重试后依旧没有下载成功
print(f'[{class_video_name}]——下载过程中出现问题,正在重试...')
return await download_m3u8_all()


async def decrypt_m3u8_video(m3u8_encrypt_file_uri: str, key: bytes, iv: bytes):
decrypt_name = f'{m3u8_encrypt_file_uri.split("/")[-1].split(".")[0]}'
dest_decrypt_uri = f'{class_video_name}/decryption/{decrypt_name}.de.ts'
if not os.path.exists(dest_decrypt_uri):
async with aiofiles.open(m3u8_encrypt_file_uri, mode='rb') as f:
f = await f.read()
content_video_part = AES.new(key, AES.MODE_CBC, iv).decrypt(f)
async with aiofiles.open(dest_decrypt_uri, mode='wb') as f:
await f.write(content_video_part)
print(f'[{class_video_name}]——已解密第 {decrypt_name} 个片段/ 共 {len(playlist.files)} 个片段')


async def decrypt_m3u8_all():
if not os.path.exists(class_video_name + '/decryption'):
os.makedirs(class_video_name + '/decryption')
key = await requests.get(playlist.keys[0].uri)
key = key.content
iv = bytes(playlist.keys[0].iv, 'UTF-8')[:16]
decrypt_m3u8_list = [asyncio.create_task(decrypt_m3u8_video(f'{class_video_name}/downloads/{uri}', key, iv))
for uri in os.listdir(f'{class_video_name}/downloads') if uri[0] != '.'] # 忽略隐藏文件
await asyncio.wait(decrypt_m3u8_list)
print(f'[{class_video_name}]——视频全部解密完成')


def merge_m3u8_all():
download_decrypt_list = [uri for uri in os.listdir(f'{class_video_name}/decryption') if uri[0] != '.']
download_encrypt_list = [uri for uri in os.listdir(f'{class_video_name}/downloads') if uri[0] != '.']
if len(download_decrypt_list) != len(download_encrypt_list): # 判断是否有漏下的分段视频没有下载
print('解密分段视频出现问题,可能是受限于类Unix系统文件句柄数量限制导致脚本不能获取足够的文件句柄。\n '
'如果你是 Linux 或 Macos 请尝试在运行本脚本的终端内执行 "ulimit -n 5120" 命令,以解除255(Macos)/1024(Linux)数量限制')
return
with open(f'{class_video_name}/{class_video_name}.mp4', 'ab') as final_file:
print(f'[{class_video_name}]——开始拼接解密后的分段视频')
temp_file_uri_list = os.listdir(f'{class_video_name}/decryption')
temp_file_uri_list.sort(key=lambda x: int(x[:-6]))
for uri in temp_file_uri_list:
if uri[0] == '.': continue # 忽略隐藏文件
with open(f'{class_video_name}/decryption/{uri}', 'rb') as temp_file:
final_file.write(temp_file.read()) # 将ts格式分段视频追加到完整视频文件中
print(f'[{class_video_name}]——合成视频成功')


if __name__ == '__main__':
playlist = m3u8.load(m3u8_file_uri, verify_ssl=False)
del playlist.files[0] # 第一个文件为视频密钥,忽略这个文件。
asyncio.run(download_m3u8_all())
asyncio.run(decrypt_m3u8_all())
merge_m3u8_all()
print(f'[{class_video_name}]——视频文件:{os.getcwd()}/{class_video_name}/{class_video_name}.mp4')

钉钉

钉钉回放下载更简单,之后将腾讯课堂回放的脚本稍作删减就可以用于钉钉回放下载。

Ceph块设备 对RBD块设备操作LVM创建PV时报错

当映射好了RBD映像中后,要在其上创建LVM逻辑卷,在执行pvcreate时出错。报错如下:

1
2
3
[root@ceph-master ceph]# pvcreate /dev/rbd0
/dev/sdd: open failed: No medium found
Device /dev/rbd0 excluded by a filter.

可以看到执行创建PV时被过滤器拦截掉了,这是因为默认情况下LVM不支持rbd设备类型,那么在LVM过滤器配置中手动添加RBD类型即可。

调试模式查看详细信息:

1
2
3
4
5
# pvcreate -vvvv /dev/rbd0 &> /tmp/out
# less /tmp/out
....
#filters/filter-type.c:27 /dev/rbd0: Skipping: Unrecognised LVM device type 252
....

查看设备类型ID

cat /proc/devices

可以找到rbd设备类型ID编号为252,记住它后接下来在LVM过滤器配置文件中添加它。

修改LVM过滤器配置文件

vim /etc/lvm/lvm.conf

找到types参数,将rbd和252修改为如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
# Configuration section devices.
# How LVM uses block devices.
devices {
...
# Configuration option devices/types.
# List of additional acceptable block device types.
# These are of device type names from /proc/devices, followed by the
# maximum number of partitions.
#
# Example
types = [ "rbd", 252 ]
#
# This configuration option is advanced.
# This configuration option does not have a default value defined.
...

现在尝试重新添加rbd设备作为PV:

pvcreate /dev/rbd0

现在可以成功在RBD块设备上执行创建PV操作了。

Ceph 映射RBD块设备

前提准备

你需要有一个运行着的Ceph集群,并且已创建好了一个Pool池,以此来创建新的RBD映像。

Pool池名称:rbd

Namespace命名空间名称:42team

要创建的RBD镜像名:42team.dev.yeefire.com.100G.img

新建块设备

创建块设备首先要创建Pool池,关于Pool池的创建如果你还不知道的话可以先看看这篇文章:OSD与Pool池的常见操作及管理

创建Pool池:ceph osd pool create rbd 16 16

新创建的Pool池如果要用于RBD映像存储的话最好先对其初始化并对这个Pool池设置rbd应用:

  • 初始化Pool池用于RBD存储:rbd pool init rbd
  • 为rbd池设置app应用,并标记rbd应用:ceph osd pool application enable rbd rbd

创建命名空间(非必要操作)

命名空间的存在是方便对一个存储池Pool进行更细化的用户访问控制,这样可以少创建一些存储池,使用池中命名空间来对用户进行隔离。

如果你想了解如何使用cephx认证配合命名空间对用户限制访问池中资源,请阅读:Ceph用户管理

为rbd池创建名为42team的命名空间:namespace create -p rbd --namespace 42team

创建RBD映像

到这一步为止,你已经做了如下操作:

  • 有一个正常状态的Ceph存储集群
  • 创建了一个名为rbd的存储池
  • 将rbd存储池的application应用设置为rbd
  • 可选:(为rbd池创建名为42team的命名空间)
  • 可选:(创建一个cephx认证用户,并将这个用户的osd能力限制在rbd池中的42team命名空间内)

接下来开始创建一个RBD映像

rbd create -p rbd --namespace 42team --size 100G 42team.dev.yeefire.com.100G.img

查看创建的rbd映像

rbd ls -p rbd --namespace 42team

映射RBD映像到内核模块

用 rbd 把映像名映射为内核模块。必须指定映像名、存储池名、和用户名。若 RBD 内核模块尚未加载, rbd 命令会自动加载。

例如要将rbd池中42team命名空间的42team.dev.yeefire.com.100G.imgRBD映像映射到本机:

rbd device map -p rbd --namespace 42team --image 42team.dev.yeefire.com.100G.img

至此挂载RBD映像成功,如果要查看本机已挂载的RBD映像可以执行:

rbd device ls

2020年9月26日前端部门Vue考核

注意事项

  • 将答案写在Microsoft Word文档中,注意标明题号书写格式
  • 本试卷为后端部门Python基础知识考核,考核时间为90分钟,可以在考试60分钟后提前交卷,交卷需与监考学长示意,并将试卷按照部门-年级-姓名-Vue前端考核,例:前端-19-马帅帅-Vue前端考核进行命名(注意保留文件拓展名)
  • 只有在作答实践题、简答题时才允许使用代码编辑器、IDE工具等,在作答选择题时不允许使用任何代码编辑器。
  • 作答实践题时需要将代码及运行结果截图都贴到作答文档中。
  • 考试期间不允许通过搜索引擎、聊天软件、询问学长等方式获取及交流考核试题、答案。任何传递答案的作弊行为都会被立即请出社团!
  • 提交试题后不允许再次修改,提前交卷的成员可以在自己的座位上使用互联网自行学习,同样需要保持考核纪律。
  • 考试期间如遇到个人紧急情况可以与监考学长示意,考核时间照常计算不予延长
  • 本次考核为了检测大家假期及最近的学习成果,不论最后考核结果如何,都应该在本次考核中展现出自己全部实力。

考核试题

  1. 什么是mvvm?(6)

  2. 当前有两个数据

1
2
val1 = "测试"
val2 = "通过"

页面默认显示 测试
当点击测试两字时改变值为 通过
html代码:

1
2
3
<div id="自拟">
<h1>显示数据的位置</h1>
</div>

请使用vue实现此功能(10)

  1. 用于监听DOM的指令的是?(7)

A:v-on B:v-model C:v-bind D:v-html

  1. 现有以下代码(7)
1
2
3
<div id="app">
<input :value="show">
</div>

其中:value是以下哪种代码的缩写?

A:v-model:value B:v-bind:value C:v-show:value D:v-if:value

  1. 简述v-if 与v-show的区别。(10)

  2. 目前有如下数据(20)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// gender 0是女1是男
// 在vue组件内:
data:{
users:[
{
id:1,
name:"张三",
gender:0
},
{
id:2,
name:"李四",
gender:1
},
{
id:3,
name:"Foo",
gender:1
},
{
id:2,
name:"Bar",
gender:0
},
]
}

要求:以尽可能美观的方式将数据展示在html中

ps:html表单

1
2
3
4
5
<table>
<tr>
<th></th>
<tr>
</table>
  1. 目前有一个内容为0-10的数组,在页面上用列表(li)渲染出来,要求不显示奇数。(20)

  2. 在页面上实现一个简单的加法计算器,要求使用vue的计算属性,无需点击实时显示计算结果。(20)

  3. 加分题:

使用ajax发送一段post请求将以下数据以Json的形式提交到localhost:5000

1
2
3
4
formData = {
id:1,
newName:"更新后的值"
}

2020年9月26日后端部门Python考核

注意事项

  • 将答案写在Microsoft Word文档中,注意标明题号书写格式
  • 本试卷为后端部门Python基础知识考核,考核时间为90分钟,可以在考试60分钟后提前交卷,交卷需与监考学长示意,并将试卷按照部门-年级-姓名-Python后端考核,例:后端-19-马飞飞-Python后端考核进行命名(注意保留文件拓展名)
  • 只有在作答实践题、简答题时才允许使用代码编辑器、IDE工具等,在作答选择题时不允许使用任何代码编辑器。
  • 作答实践题时需要将代码及运行结果截图都贴到作答文档中。
  • 考试期间不允许通过搜索引擎、聊天软件、询问学长等方式获取及交流考核试题、答案。任何传递答案的作弊行为都会被立即请出社团!
  • 提交试题后不允许再次修改,提前交卷的成员可以在自己的座位上使用互联网自行学习,同样需要保持考核纪律。
  • 考试期间如遇到个人紧急情况可以与监考学长示意,考核时间照常计算不予延长
  • 本次考核为了检测大家假期及最近的学习成果,不论最后考核结果如何,都应该在本次考核中展现出自己全部实力。

考核试题

选择题

第一题

Python3中,单行注释应该用以下哪种()

A.<!-要注释的代码行->

B.'''要注释的代码行'''

C.//要注释的代码行

D.#要注释的代码航

第二题

下面四个数据类型中Python3不支持的是()

A.list

B.char

C.bool

D.int

第三题

关于Python3中的变量,下列说法正确的是()

A.定义变量时必须要先将变量赋予初始值

B.定义变量时无需制定数据类型

C.变量定义后可以通过free来释放不再使用的变量

D.变量定义后不能在进行更改

第四题

计算机中信息处理和信息储存用()

A.十六进制代码

B.ASCII代码

C.十进制代码

D.二进制代码

第五题

Python的源程序通常是通过()方式运行

A.编译运行

B.边编译边运行

C.解析运行

D.直接执行

第六题

下面关于Python3中的异常,说法正确的是()

A.程序在运行中如果抛出异常则会立即终止运行

B.可以使用raise来捕获异常

C.可以使用raise来自动解决异常

D.程序如果抛出异常,不一定会终止程序

第七题

下列哪个表示的是Python字典()

A.temp=['key','value']

B.temp={'key':'value'}

C.temp=('key','value')

D.temp=[('key','value')]

第八题

下列哪个说法是错误的()

A.空字符串的布尔值是False

B.空列表的布尔值是False

C.值为0的任何数字对象的布尔值是False

D.除了字典类型外,其他类型标准对象都可以进行布尔值测试

第九题

以下代码运行的每行结果依次是什么?()

1
2
3
4
5
for i in range(2):
print i
i=3
for i inrange(4,6):
print i

A.1,2,3,4,5,6

B.0,1,2,3,4,5,6

C.1,4,5

D.0,1,4,5

E.1,3,4,5

F.0,1,3,4,5

第十题

如果love=”42Team社团成立于2003年,下个月即将迎来小组17岁的生日”,下面输出错误的是()

A.print(love[:])

输出:42Team社团成立于2003年,下个月即将迎来小组17岁的生日

B.print(love[1:16])

输出:42Team社团成立于2003年

C.print(love[-15:])

输出:下个月即将迎来小组17岁的生日

D.print(love[:-16])

输出:42Team社团成立于2003年

简答题

第十一题

如何判断一个变量是否为字符串?

第十二题

怎样实现int类型和字符串类型的互相转换?如果将一个包含字母的字符串转换为int类型时会发生什么?

第十三题

如何定义一个函数,请你描述以下定义函数的过程或者直接使用代码来定义一个函数。

实践题

第十四题

用一行代码实现从1加到100之和,并打印到控制台。

第十五题

有下列一个列表,现在需要将这个列表中的负数,将其变为这个负数的绝对值,并使用一个新列表来存储这些数。

1
2
random_list=[-4,6,0,-3,1,-0]
new_list=[]

第十六题

现在有下面一个字典,要求遍历所有的键值对,并按照键-值这样的输出方式一行一行的输出。

1
2
3
4
5
6
7
8
9
10
user = {
'uid': 123321123,
'uuid': '6a2d1b42fca911eabf678243dce30801',
'username': None,
'student_id': '12223221',
'phone_number': '13131333321',
'email': '42team@dnui.edu.cn',
'ban': False,
'login_count': 0
}

第十七题

请完善并修改下面的flask框架代码,要求将上一题中使用的字典转换为JSON格式并返回到前端页面上。(提示:别忘记了要安装模块)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from flask import Flask
from flask import jsonify

app = Flask(__name__)

user = {
'uid": '123321123',
'uuid': '6a2d1b42fca911eabf678243dce30801'
'username': None,
'student_id": 12223221',
'phone_number': '13131333321',
'email': '42team@dnui.edu.cn',
'ban': False,
'login_count': 0
}

@app.route('/json',methods=['GET'])
def hello_world():
return 'How to return JSON?'


if __name__ == '__main__':
app.run()

最终要求效果:

20200925224815-2020-09-25