0%

SSH配置

SSH基本概念

  • SSH是Secure Shell缩写,实现安全远程登录

​ SSH的安全性好,原因是其对数据进行加密,方式主要有两种:对称加密(密钥加密)和非对称加密(公钥加密)
​ 对称加密指加密解密使用的是同一套秘钥。Client端把密钥加密后发送给Server端,Server用同一套密钥解密。对称加密的加密强度比较高,很难破解。但是如果有一个Client端的密钥泄漏,那么整个系统的安全性就存在严重的漏洞。
​ 为了解决对称加密的漏洞,于是就产生了非对称加密。
​ 非对称加密有两个密钥:“公钥”和“私钥”。公钥加密后的密文,只能通过对应的私钥进行解密。想从公钥推理出私钥几乎不可能,所以非对称加密的安全性比较高。

  • SSH的加密原理中,使用了RSA非对称加密算法。

​ 整个过程:

​ (1)远程主机收到用户的登录请求,把自己的公钥发给用户。

​ (2)用户使用这个公钥,将登录密码加密后,发送回来。

​ (3)远程主机用自己的私钥,解密登录密码,如果密码正确,就同意用户登录。

  • 关于中间人攻击(Man-in-the-middle attack)

​ 中间人攻击的概念:如果有人冒充远程主机将伪造的公钥发给用户,用户很难辨别公钥真伪,用户 会和伪造主机通信而不是真正的主机。

​ 因为SSH不像https协议,SSH协议的公钥是没有证书中心(CA)公证的,是主机和用户之间自己签 发的,所有SSH从原理上无法彻底防止中间人攻击

  • SSH使用首次验证方式减少中间人攻击的概率:

    SSH首次连接会下载服务端的公钥,用户确认后公钥将被保存并信任。

    下次访问时客户端将会核对服务端发来的公钥和本地保存的是否相同,如果不同就发出中间人攻击的警告拒绝连接,除非用户手动清除已保存的公钥。

    所以,只要SSH首次连接没有中间人攻击,之后的SSH连接就无需担心中间人攻击

Ubuntu安装SSH服务

1
2
3
sudo apt install ssh -y
sudo service ssh start
sudo service ssh status

在系统重启后ssh service会自启动,不需要systemctl enable去配置自启动

Windows访问SSH服务

  • 使用win+R CMD验证SSH连接

    1
    ssh 远程主机用户名@远程主机IP
  • 使用putty,xshell等工具访问主机

参考:how2shout.com/how-to/how-to-login-into-ubuntu-using-ssh-from-windows-10-8-7

首次登陆会验证RSA公钥(1024位)的MD5 fingerprint(128位)

1
2
RSA key fingerprint is 98:2e:d7:e0:de:9f:ac:67:28:c2:42:2d:37:16:58:4d.
Are you sure you want to continue connecting (yes/no)?

当首次连接的公钥被接受以后,会保存在本地文件。下次再连接这台主机会跳过公钥警告,直接提示输入密码。如果以后的连接是中间人攻击,其公钥和本地的首次公钥不同,从而保证安全性。

  • 使用xftp, filezilla工具传输文件

和putty,xshell配置类似

  • 使用scp命令传输文件

在linux主机之间可以用scp传输文件和目录

1
2
3
4
5
6
#从远程cp到本地
scp username@ip_address:/home/username/filename .
#从本地cp到远程
scp filename username@ip_address:/home/username
#拷贝目录
scp -r source_dir username@ip_address:/home/username/target_dir

SSH远程开发

示例一:在SSH server和客户端建立后,可以使用VSCode和source insight等代码编辑工具改代码,用xftp传输代码到SSH Linux主机,用xshell远程编译。

示例二:VSCode安装SSH远程开发插件,可以直接远程SSH Linux主机完成代码编辑、编译,visualstudio.com/Remote Development using SSH

远程连接相关的Ubuntu配置

Ubuntu设置静态IP

在使用SSH和Samba连远程Ubuntu PC时,发现IP有时候会改变,因此需要配置Ubuntu PC为静态IP

1.ifconfig查看ethernet接口和当前IP

1
2
3
ubuntu@ubuntu-Z390-GAMING-X:~$ ifconfig
eno1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.52.4.71 netmask 255.255.255.0 broadcast 10.52.4.255

2.编辑Ubuntu的netplan配置文件/etc/netplan/*.yaml,用tab补全找到具体的yaml,制定静态IP和DNS

参考 Netplan network configuration tutorial for beginners

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
network:
version: 2
renderer: NetworkManager
ethernets:
eno1:
addresses:
- 10.52.4.71/24
nameservers:
addresses:
- 10.52.1.1
- 10.52.1.2
#gateway4: 10.52.0.1
routes:
- to: default
via: 10.52.0.1

以上IP和nameservers(DNS)是必须的,gateway4是网关,在ubuntu22被废弃(ubuntu22显示 ``gateway4 has been deprecated, use default routes instead.)使用routes配置网关,参考 netplan-gateway-has-been-deprecated。怎么获取这三个值,参考以下网络命令:

1
2
3
4
5
6
#查看所有
nmcli
#查看gateway
netstat -rn 或 route -n
#DNS配置文件
cat /etc/resolv.conf

例如nmcli输出如下,其中 inet4 10.52.4.71/24即当前IP/mask, route4 default via 10.52.0.1即默认网关,DNS configuration servers: 10.52.1.1 10.52.1.2即nameservers

eno1: connected to netplan-eno1
“Intel I219-V”
ethernet (e1000e), 18:C0:4D:1F:BA:B7, hw, mtu 1500
ip4 default
inet4 10.52.4.71/24
route4 10.52.4.0/24 metric 100
route4 10.52.0.1/32 metric 100
route4 default via 10.52.0.1 metric 100
inet6 fe80::1ac0:4dff:fe1f:bab7/64
route6 fe80::/64 metric 256

virbr0: connected (externally) to virbr0
“virbr0”
bridge, 52:54:00:13:EB:68, sw, mtu 1500
inet4 192.168.122.1/24
route4 169.254.0.0/16 metric 1000
route4 192.168.122.0/24 metric 0

lo: unmanaged
“lo”
loopback (unknown), 00:00:00:00:00:00, sw, mtu 65536

DNS configuration:
servers: 10.52.1.1 10.52.1.2
interface: eno1

3.生效

1
sudo netplan apply

ping确认网络正常:

ubuntu@ubuntu-Z390-GAMING-X:~$ ping www.bing.com
PING china.bing123.com (202.89.233.101) 56(84) bytes of data.
64 bytes from 202.89.233.101 (202.89.233.101): icmp_seq=1 ttl=117 time=27.1 ms
64 bytes from 202.89.233.101 (202.89.233.101): icmp_seq=2 ttl=117 time=27.2 ms
64 bytes from 202.89.233.101 (202.89.233.101): icmp_seq=3 ttl=117 time=27.2 ms

如果DNS server或gateway不符合当前网络状况,ping会失败,输出:

Name or service not known

Ubuntu禁止自动登出

自动登出会使SSH断开链接,按如下禁用

setting->Privacy->Screen->Automatic Screen Lock (OFF)

Samba基本概念

Samba是SMB protocol的应用程序实现,分为服务端和客户端;

Samba通常的使用场景:在同一局域网内的的Linux主机安装Samba服务,windows主机可以访问Linux Samba服务指定的共享目录。

在嵌入式开发中通常在windows 上编辑Samba共享目录下的代码,通过 Linux环境编译代码,而无需在两个主机间拷贝代码文件。

Ubuntu安装Samba服务

Ubuntu 20.04和22.04 版本,安装Samba服务参考:

www.how2shout.com/linux/how-to-install-samba-on-ubuntu-22-04-lts-jammy-linux
主要流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#install and run samba service
sudo apt install samba -y

#enable auto start samba service
sudo systemctl enable --now smbd

#firewall allow samba
sudo ufw allow samba

#add system user to sambashare group
sudo usermod -aG sambashare $USER

#set passwd for sambashare
sudo smbpasswd -a $USER

#check samba service is running
systemctl status smbd

#share the folder in ubuntu GUI checkbox
右键要共享的home文件夹properties -> local Network Share -> share this folder ->share name不能直接用用户名,可以用'用户名+Home'

显示无权共享:非root用户要共享/home,需要修改smb.conf:

1
2
3
sudo vim /etc/samba/smb.conf
在[global]新增usershare owner only = false
sudo systemctl restart smbd

Windows访问Samba共享目录

windows下可以在文件浏览器直接访问Linux主机ip查看共享的Linux目录

image-20230130110305978

首次windows访问共享目录有权限问题(不能写入),需要在Linux修改共享目录/home的权限:

1
sudo chmod 777 /home -R 

为了以后方便连接,可以创建网络位置,参考:6. Access the shared folder On Windows 11 or 10

如果一个主机有两个samba共享目录,windows不允许多重连接;

要更改连接目录,操作如下:

win10系统在搜索框搜索【凭据管理器】,然后删除已有的windows samba网络连接凭据

win+R CMD输入 net use * /del /y断开所有远程链接,包括samba网络连接

重新配置windows samba网络连接

重装Samba

Samba的配置文件位于/etc/samba/smb.conf,如果此文件被错误配置或者误删除,需要重装Samba,流程如下:

1
2
3
4
sudo apt-get remove samba --purge //删掉samba服务
sudo apt-get remove samba-common --purge //这一步是关键,只重装samba不会恢复smb.conf
sudo apt-get autoremove //删掉其他samba依赖库
sudo apt-get install samba //重装,包括samba和samba-common等

Samba使用示例

Samba最重要的特性是两个主机之间直接共享目录,不需要用户去拷贝文件。

在代码开发中,在windows主机的VSCode或其他编辑器直接打开Linux主机共享目录的代码,然后SSH远程Linux主机去编译。

Tree命令生成目录树

tree 命令的目录格式:TREE 【drive:】【path】【/F】【/A】

  • 可在cmd内输入(help tree 或 tree / ?)查看
  • /F 显示每个文件夹中文件的名称
  • /A 使用ASCII字符,而不使用拓展字符

示例一:只显示路径名不显示文件名

TREE 【drive:】【path】

image-20230130104934713

示例二:显示路径名和文件名

TREE 【drive:】【path】【/F】

image-20230130104835813

示例三:将目录树存入指定文件

TREE 【drive:】【path】 > 文件路径】

image-20230130105102054

修改工作区存储目录

VSCode会将每个工作区的一些配置、扩展、缓存等默认保存在C盘的AppData\Code\workspaceStorage,使用一段时间后数据能达到上十GB。

当C盘空间不足,用SpaceSniffer可以找到这些“数据垃圾”,但每隔一段时间清理也不是一劳永逸。

修改workspaceStorage存储路径到非系统盘:

1.首先选择VSCode在开始栏,状态栏,或桌面栏的快捷方式图标,常用哪个就修改哪个,右键属性:

添加启动的命令行选项,指定user-data-dir:

1
--user-data-dir "目标路径,例如F:\VSCodeWorkspaceStorage"

image-20221208120051137

2.转移已有的workspaceStorage.

修改完成后,将%AppData%\Code下的所有内容拷贝到设置的目录中; 也可以删除%AppData%\Code,但是需要重新配置VSCode。

常用快捷键

代码注释

以双斜杠//注释和取消注释:

1
2
3
方法一:
注释:ctrl + /
取消注释:ctrl + /
1
2
3
方法二:
注释:ctrl + k, ctrl + c
取消注释:ctrl + k, ctrl + u

以星号/**/注释和取消注释:

1
2
注释:shift + alt + a 
取消注释:shift + alt + a

更改快捷键

File->Preference->KeyboardShortCuts

例如可以把块注释/**/快捷键改成ctrl+Alt+/,和行注释ctrl+/达成统一:

选择recording keys,直接录入要修改的快捷键

image-20230220110133891

项目文件过滤

在项目的顶层目录中新建 .vscode 文件夹,在该文件夹下面新建 settings.json 文件

例如,对于Linux kernel项目,编译过的目录有大量编译输出文件(.o, .ko, .mod等),只想查看和搜索驱动目录下的源码,过滤示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"files.exclude": {
"**/*.cmd": true, //当前所有目录的所有以.cmd结尾的文件
"**/*.a": true,
"**/*.o": true,
"**/*.d": true,
"**/*.mod": true,
"**/*.mod.c": true,
"**/*.ko": true,

"[^drivers]*": true, //除了包含'd''r''i''v''e''r''s'目录以外的所有目录,近似等效于除了"drivers"文件夹以外的文件都被files.exclude
"[^include]*": true,
},

"search.exclude": {
"[^driver]*": true,
"[^include]*": true,
}
}

正则表达式参考 正则表达式排除字符

VSCode remote免密码登录(SSH密钥认证)

Windows端的VSCode remote如何配置参考Remote Development using SSH,Linux服务器配置好SSH服务后直接连接即可。

日常使用经常需要重启Linux服务端,需要重新输入密码登录;使用SSH密钥可以免密码登录。

SSH密钥登录的流程:

  • 在进行SSH连接之前,SSH客户端需要先生成自己的公钥私钥对,并将自己的公钥存放在SSH服务器上。

  • SSH客户端发送登录请求,SSH服务器就会根据请求中的用户名等信息在本地搜索客户端的公钥,并用这个公钥加密一个随机数发送给客户端。

  • 客户端使用自己的私钥对返回信息进行解密,并发送给服务器。

  • 服务器验证客户端解密的信息是否正确,如果正确则认证通过。

    image-20230822110009079

(1)Windows客户端生成ssh key

win+R -> ssh-keygen 生成密钥对,id_rsa.pub是公钥,id_rsa是私钥;

如果已经有ssh-key, 不需要重新生成;如果已有的key不能配置生效,参考如下方式生成重命名的ssh-key,后续流程一致。

image-20230822111509800

(2)Linux服务端生成ssh key

用xshell或samba拷贝windows端的C:\Users\用户名\.ssh\id_rsa.pub到Linux服务端的~/.ssh

拷贝到authorized_keys,并修改权限,否则Vscode remote不能访问。

1
2
3
cat id_rsa.pub >> authorized_keys
chmod 777 authorized_keys
service sshd restart

(3)配置VSCode remote

ssh配置文件C:\Users\用户名\.ssh\config

image-20230822104703184

添加IdentityFile字段,填写windows本机的id_rsa路径,注意没有.pub后缀

1
2
3
4
Host 10.52.4.63
HostName 10.52.4.63
User cursorhu
IdentityFile "C:\Users\thomas.hu\.ssh\id_rsa"

本文基于Qt官方示例 A Quick Start to Qt Designer, 实现自定义的slot函数,新增RGB色彩窗口显示色彩。

本文只记录项目过程中的注意事项,以及增量开发,其他部分参考Qt官方示例。

1.UI部分

  • 建立带UI的项目rgbSlider, 基于Qwidget生成默认自定义类名widget
  • 双击widget.ui进入UI编辑

UI 编辑模式下使用两种模式:widget编辑模式, slot/signal编辑模式

  1. widget编辑模式如下:使用水平、网格布局
    RGB数值控制部分,使用Label, spinBox和scrollBar三种控件,按先竖直,后水平排列
    RGB颜色显示部分,使用 graphicsView窗口

    注意调整布局的比例需要先选中,然后在layout属性调整

  2. slot/signal编辑模式
    直接拖拽起始控件和目标控件,设置控件的信号和槽

2.自定义槽

graphicsView窗口预期效果是:只要调整RGB数值,自动显示对应的颜色
UI界面不能设置控件信号触发自定义槽,需要在代码中实现信号和槽的连接。

  1. 右键转到graphicsView窗口的槽函数,自定义为 Widget::on_rgbChanged()

    函数实现如下:
    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

    #include <QColor>

    #include <QPalette>


    void Widget::on_rgbChanged()

    {

    QPalette pal = QPalette();

    QColor color;

    //分别设置R,G,B,透明度

    color.setRgb(ui->spinBoxRed->value(), ui->spinBoxGreen->value(), ui->spinBoxBlue->value(), 255);

    //QPalette::Base

    //Used mostly as the background color for text entry widgets, It is usually white or another light color.

    pal.setColor(QPalette::Base, color);

    ui->graphicsView->setPalette(pal);

    }

在UI基础上使用控件对象的方法,只需要:

1
ui->控件名->控件的方法

注意setColor可以给不同图层上色,这里使用QPalette::Base,而不能是QPalette::WindowQPalette::Background

代码设置信号与槽, 注意,手动设置的代码要在ui->setupUi(this);的后面添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Widget::Widget(QWidget *parent) :

QWidget(parent),

ui(new Ui::Widget)

{

ui->setupUi(this);

connect(ui->spinBoxRed, SIGNAL(valueChanged(int)), this, SLOT(on_rgbChanged()));

connect(ui->spinBoxGreen, SIGNAL(valueChanged(int)), this, SLOT(on_rgbChanged()));

connect(ui->spinBoxBlue, SIGNAL(valueChanged(int)), this, SLOT(on_rgbChanged()));

}

3.测试效果

  • 拖动滑块,对应数值会更新,颜色同步更新
  • 修改数值,对应滑块更新,颜色更新

1. Linux MMC 框架现状

Linux MMC driver是支持包括SD卡,eMMC卡等等,属于MultiMediaCard设备和接口的驱动
其源码路径位于Kernel source code的drivers/mmc路径, 头文件位于include/linux/mmc
mmc源码分为core/host两层,是为了解耦:

  • 通用的SD/eMMC流程(core)
  • 具体的硬件操作流程(host),在此层又可分为通用的SDHCI框架和非SDHCI框架,各eMMC/SD host厂商实现最底层driver时,可以遵循SDHCI框架下的API, 间接实现core层定义的方法(driver称为operations), 也可以不遵循SDHCI框架,直接实现core层定义的方法。

本文重点关注mmc框架对SD卡驱动的支持

1.1 SD卡的类型概述

SD卡可以分为三种类型:
UHS-I, UHS-II, SD express

详细信息参考https://www.sdcard.org

  • Physical Layer Specification Ver.7.10 (从各层描述SD 7.0, SD 4.0, 以及更早版本SD的规范)
  • SD Host Controller Specification Ver7.0 (从host控制器角度,描述SD 7.0, SD 4.0, 以及更早版本SD的规范)
  • SD_Specifications_Part_1_UHS_II_Addendum(描述SD UHSII的附录规范)

UHS即Ultra High Speed, express也表示高速,这三代SD卡的读写速度是依次增加,参考下图:

  • UHSI:50~104MB/s
  • UHSII: 156~624MB/s
  • SD express: 985MB/s


1.2 Linux MMC框架对SD卡的支持

基本概念:只有mmc框架的core层支持某种SD模式,host层才能实现这种模式;如果core层都不支持,只能厂商自己开发core层,以patch补丁的方式发布。

core层对于上述三种SD模式的支持:

  • Linux kernel 5.11 以前,只支持UHS-I及其更低速度的legacy-SD模式
  • Linux kernel 5.11 开始,在core层添加了SD express的支持
  • 目前没有UHS-II的支持,只有提交待审核的,参考:lore.kernel.org/Jason Lai/patch

host层对于上述三种SD模式的支持:

  • UHS-I: 基本host目录的大多数SD厂商驱动都支持,很多符合sdhci框架
  • SD express: Realtek基于Linux kernel 5.11的core层API, 实现了 驱动的host底层部分,参考kernel的host/rtsx_pci_sdmmc.c, 其没有使用SDHCI框架。
  • UHS-II: 只有以patch方式实现的,参考# linux-uhs2-gl9755,其实现了core/host-sdhci/host vendor多个层次的UHS-II支持。

综上所述,本文参考uhs2-gl8755 patch,实现自己的SD UHSII driver。

2. 编译过程

本节描述编译mmc driver module和整个kernel的过程,同时描述中间踩的坑。

2.1 直接编译整个Kernel(带UHS-II Patch)

安装Linux Ubuntu 20版本,Ubuntu环境下载和解压待编译的整个Linux kernel 源码:# linux-uhs2-gl9755

注意:一定要在Linux环境下解压待编译源码,不能在windows下解压再拷到Linux编译,因为源码中有些大小写不同的同名文件,例如net/netfilter的很多头文件。windows不区分大小,解压时写会让你替换或重命名,这些同名文件的内容不一样,所以不能替换或重命名,强行替换会导致编译Linux报错找不到相关文件。

  1. 编译环境准备
    gcc/make等工具,都需要先安装build-essential等工具才能使用
    1
    sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev
  • 遇到的问题:

apt如果有依赖问题,建议apt手动安装,如果要特定版本,例如指定依赖libc6库版本为2.35-0ubuntu3,使用apt install libc6=2.35-0ubuntu3, 可以用apt policy libc6查看。

这里不建议sudo apt install aptitude(使用aptitude自动安装需要的依赖库版本),因为会导致make menuconfig出现<sys/types.h>找不到的问题,这个问题的原因是libc6-dev未安装,必须用apt安装libc6-dev解决此问题。

  1. 配置,编译和安装
1
2
3
4
5
cd linux-uhs2-gl9755-v3-patch #进入待编译Kernel源码
make menuconfig #配置内核,生成.config文件
make -j4 #以4线程编译内核,等同于make bzImage,make modules
make modules_install #安装各Driver模块
make install #安装内核(包括更新模块信息)

编译完成后会自动update-grub, 重启后选择编译好的kernel版本启动。

也可以设置默认启动的kernel,编辑/etc/default/grub的GRUB_DEFAULT="1>X", 其中1表示从advanced选项启动,X表示从哪个kernel启动(0 based),例如下图如果默认要从5.19启动,X设置为0,默认从5.8.0-rc4启动,X设置为6.

配置完毕必须要update-grub重启生效

  • 遇到的问题

    make有canonical-certs.pem证书问题:修改.config,取消证书要求:CONFIG_SYSTEM_TRUSTED_KEYS=””,CONFIG_SYSTEM_REVOCATION_KEYS=””

  1. 查看内核版本

    1
    2
    uname -r #查看当前运行的kernel版本
    cat Makefile #查看待编译kernel源码的内核版本

    以linux-uhs2-gl9755-v3-patch为例,其根目录Makefile如下,表示kernel源码版本为 5.8.0-rc4
    编译完成重启后应该选择5.8.0-rc4启动,进入桌面后用uname -r查看

    1
    2
    3
    4
    5
    VERSION = 5
    PATCHLEVEL = 8
    SUBLEVEL = 0
    EXTRAVERSION = -rc4
    NAME = Kleptomaniac Octopus
  2. 编译报错记录
    (1) 生成vmlinux Image时报错:

    1
    2
    Failed to generate BTF for vmlinux  
    Try to disable CONFIG_DEBUG_INFO_BTF

    修改Kernel源码根目录的.config文件,CONFIG_DEBUG_INFO_BTF=n 关闭此选项

​ (2) 编译完成,但运行新kernel时报错out of memory
解决办法:裁剪module大小,编译模块时使用 make INSTALL_MOD_STRIP=1 modules_install,.ko被编译时会缩减非必要的debug信息。

2.2 合并UHSII patch后再编译整个Kernel

官方kernel源码可以到kernel.org下载

合并UHSII patch,仅涉及到mmc模块的代码,如果差异不大可以将linux-uhs2-gl9755-v3-patch的drivers/mmc和include头文件直接拷到待编译kernel的drivers/mmc和include/linux/mmc。

如果是手动合并UHS-II patch,需要考虑以下部分:

  • 源码,包括drivers/mmc和include/linux/mmc
  • Makefile, 包括drivers/mmc/core和drivers/mmc/host
  • Kconfig, 包括drivers/mmc,及其子目录core和host

具体合并方法参考《Linux设备驱动开发详解》
合并完后,Kernel编译流程和上节相同

2.3 单独编译MMC模块

一般的驱动开发,都是可以单独编译成module模块,然后用rmmod和insmod替换原系统的模块

但是UHS-II patch涉及到mmc/core层的改动,而core是build-in的,不能作为模块编译,因此只能编译整个kernel。以后如果只修改host层的代码,可以将mmc/host单独编译为module后安装。

待编译kernel目录是~/linux-5.8-rc4

1
2
3
4
5
6
7
8
#编译模块,"M="指定待编译源码,编译完拷贝.ko到"-C"指定的目录,此目录为系统存放模块的目录
sudo make -C /lib/modules/`uname -r`/build M=~/linux-5.8-rc4/drivers/mmc

#安装模块
make -C /lib/modules/`uname -r`/build M=~/linux-5.8-rc4/drivers/mmc modules_install

#清除模块,包括.o和.ko文件
sudo make -C /lib/modules/`uname -r`/build M=~/linux-5.8-rc4/drivers/mmc clean

参考:# Building External Modules

注意,make xxx modules_install是不能让模块自动加载的,只是安装到了/lib/modules位置。使用modinfo查看模块信息,似乎是使用了/lib/modules下的,但没有实际加载和生效。
要加载模块,两种方法:

  1. rmmod/insmod 手动替换, 参考下一节
  2. make modules_install 之后再 make install,更新整个kernel, 此后外部模块才会被内核自动加载(通常使用这种方式)

2.4 手动替换MMC模块

2.4.1 UHS-II相关模块的依赖关系

可以从mmc/host的Kconfig得知依赖:

1
2
3
4
5
6
7
8
config MMC_SDHCI_PCI
tristate "SDHCI support on PCI bus"
depends on MMC_SDHCI && PCI
select MMC_SDHCI_UHS2

config MMC_SDHCI_UHS2
tristate "UHS2 support on SDHCI controller"
depends on MMC_SDHCI

使用lsmod可以得知module依赖关系,如下图,sdhci_uhs2被sdhci_pci引用1次, sdhci被sdhci_uhs2和sdhci_pci引用2次

modinfo可以得知已加载module的.ko路径

2.4.2 手动卸载和装载module

卸载和装载都要按依赖顺序处理,shell脚本如下.

1
2
3
4
5
6
7
8
9
#!/bin/bash

sudo rmmod sdhci_pci
sudo rmmod sdhci_uhs2
sudo rmmod sdhci

sudo insmod /lib/modules/`uname -r`/build/drivers/mmc/host/sdhci.ko
sudo insmod /lib/modules/`uname -r`/build/drivers/mmc/host/sdhci-uhs2.ko
sudo insmod /lib/modules/`uname -r`/build/drivers/mmc/host/sdhci-pci.ko

3. 调试过程

3.1 调试工具

  1. printk
    printk是很常用的driver调试手段,配合dmesg查看kernel log可以定位常见问题。
    printk如何开启不同打印级别,参考# Message logging with printk

例如,使用dmesg -n 6开启KERN_INFO级别,然后在driver中添加pr_info()作为info打印, 在dmesg中查看打印log。

注意KERN_DEBUG比较特殊,不仅要dmesg -n 7开启, 还需要在driver module的makefile添加Debug CFLAGS, 有两种方法:

1
2
3
4
5
#该Makefile相关模块全部启用debug
EXTRA_CFLAGS += -DDEBUG

#指定模块启用debug
CFLAGS-xxx-mmc += -DDEBUG

示例:使用pr_info(“enter %s\n”, __FUNCTION__); 打印函数调用流程

  1. dmesg
    示例参考 # How to use the dmesg Command on Linux
    比较常用的有:
    1
    2
    3
    4
    5
    sudo dmesg
    sudo dmesg -c
    sudo dmesg | head -100
    sudo dmesg | tail
    sudo dmesg | xxx

3.vscode
vscode比vim/gedit更方便直接改代码,用.deb安装容易失败,推荐命令行安装方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
#更新相关microsoft源
wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg

sudo install -o root -g root -m 644 packages.microsoft.gpg /etc/apt/trusted.gpg.d/

sudo sh -c 'echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/trusted.gpg.d/packages.microsoft.gpg] \
https://packages.microsoft.com/repos/code stable main" > /etc/apt/sources.list.d/vscode.list'

rm -f packages.microsoft.gpg

#安装
sudo apt update
sudo apt install code

3.2 UHSII调试

  1. 模块加载初始化过程中dmesg显示直接dump
    基本是空指针问题,例如:

    • 只编译UHSII host 模块,而不编译kernel的core层,insmod host模块时就会dump, 因为core层相关API不存在。
    • 获取相关数据结构方法不对导致空指针
      例如获取slot要使用:
      1
      2
      3
      4
      5
      6
        struct sdhci_pci_slot *slot = sdhci_priv(host);

      static inline void *sdhci_priv(struct sdhci_host *host){
      return host->private;
      }

      而host->private实际指向sdhci_host结构体的最后定义的如下0长度数组
      1
      unsigned long private[] ____cacheline_aligned;
      参考:# Explanation on private variable in c struct
      基本含义是可以获取结构体外部的数据,而host指针本身确实属于slot结构体sdhci_pci_slot的一部分,所以host->private能访问到slot。
  2. 贴一段dmesg log,包含UHSII初始化过程直到最后一步GO_DORMANT fail
    具体流程参考UHSII spec: SD_Specifications_Part_1_UHS_II_Addendum

    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
    [  522.171631] sdhci_uhs2 [sdhci_uhs2_do_detect_init()]: sdhci_uhs2_do_detect_init: begin UHS2 init.
    [ 522.171632] enter sdhci_pci_o2_pre_detect_init.
    [ 522.171632] exit sdhci_pci_o2_pre_detect_init.
    [ 522.171835] sdhci_uhs2 [sdhci_uhs2_interface_detect()]: mmc0: UHS2 Lane synchronized in UHS2 mode, PHY is initialized.
    [ 522.171855] uhs2_cmd_assemble: uhs2_cmd: header=0x80 arg=0x292
    [ 522.171856] uhs2_cmd_assemble: payload_len=4 packet_len=8 resp_len=6
    [ 522.171856] [uhs2_dev_init()]: Begin DEVICE_INIT, header=0x80, arg=0x292, payload=0x8808.
    [ 522.171856] [uhs2_dev_init()]: Sending DEVICE_INIT. Count = 0
    [ 522.171858] mmc0: starting CMD0 arg 00000000 flags 00000000
    [ 522.171865] sdhci_uhs2 [sdhci_uhs2_send_command()]: 0x800 is set to UHS2 CMD register.
    [ 522.171874] mmc0: sdhci: IRQ status 0x00000001
    [ 522.171885] mmc0: req done (CMD0): 0: 00000000 00000000 00000000 00000000
    [ 522.171887] uhs2_cmd_assemble: uhs2_cmd: header=0x80 arg=0x292
    [ 522.171887] uhs2_cmd_assemble: payload_len=4 packet_len=8 resp_len=6
    [ 522.171888] [uhs2_dev_init()]: Begin DEVICE_INIT, header=0x80, arg=0x292, payload=0x8808.
    [ 522.171888] [uhs2_dev_init()]: Sending DEVICE_INIT. Count = 1
    [ 522.171888] mmc0: starting CMD0 arg 00000000 flags 00000000
    [ 522.171894] sdhci_uhs2 [sdhci_uhs2_send_command()]: 0x800 is set to UHS2 CMD register.
    [ 522.188184] mmc0: sdhci: IRQ status 0x00000001
    [ 522.188205] mmc0: req done (CMD0): 0: 00000000 00000000 00000000 00000000
    [ 522.188254] [uhs2_dev_init()]: CF is set, device is initialized!
    [ 522.188257] [uhs2_enum()]: Begin ENUMERATE, header=0x80, arg=0x392, payload=0xf0.
    [ 522.188260] uhs2_cmd_assemble: uhs2_cmd: header=0x80 arg=0x392
    [ 522.188262] uhs2_cmd_assemble: payload_len=4 packet_len=8 resp_len=8
    [ 522.188266] mmc0: starting CMD0 arg 00000000 flags 00000000
    [ 522.188277] sdhci_uhs2 [sdhci_uhs2_send_command()]: 0x800 is set to UHS2 CMD register.
    [ 522.188290] mmc0: sdhci: IRQ status 0x00000001
    [ 522.188308] mmc0: req done (CMD0): 0: 00000000 00000000 00000000 00000000
    [ 522.188318] [uhs2_enum()]: id_f = 6, id_l = 6.
    [ 522.188320] [uhs2_enum()]: Enumerate Cmd Completed. No. of Devices connected = 1
    [ 522.188322] [uhs2_config_read()]: INQUIRY_CFG: read Generic Caps.
    [ 522.188324] [uhs2_config_read()]: Begin INQUIRY_CFG, header=0x86, arg=0x10.
    [ 522.188326] uhs2_cmd_assemble: uhs2_cmd: header=0x86 arg=0x10
    [ 522.188328] uhs2_cmd_assemble: payload_len=0 packet_len=4 resp_len=0
    [ 522.188331] mmc0: starting CMD0 arg 00000000 flags 00000000
    [ 522.188342] sdhci_uhs2 [sdhci_uhs2_send_command()]: 0x400 is set to UHS2 CMD register.
    [ 522.188363] mmc0: sdhci: IRQ status 0x00000001
    [ 522.188392] mmc0: req done (CMD0): 0: 00010100 00000000 00000000 00000000
    [ 522.188398] [uhs2_config_read()]: Device Generic Caps (0-31) is: 0x10100.
    [ 522.188399] [uhs2_config_read()]: INQUIRY_CFG: read PHY Caps.
    [ 522.188401] [uhs2_config_read()]: Begin INQUIRY_CFG, header=0x86, arg=0x220.
    [ 522.188404] uhs2_cmd_assemble: uhs2_cmd: header=0x86 arg=0x220
    [ 522.188410] uhs2_cmd_assemble: payload_len=0 packet_len=4 resp_len=0
    [ 522.188415] mmc0: starting CMD0 arg 00000000 flags 00000000
    [ 522.188427] sdhci_uhs2 [sdhci_uhs2_send_command()]: 0x400 is set to UHS2 CMD register.
    [ 522.188447] mmc0: sdhci: IRQ status 0x00000001
    [ 522.188476] mmc0: req done (CMD0): 0: 00008000 00000080 00000000 00000000
    [ 522.188482] [uhs2_config_read()]: Device PHY Caps (0-31) is: 0x8000.
    [ 522.188484] [uhs2_config_read()]: Device PHY Caps (32-63) is: 0x80.
    [ 522.188487] [uhs2_config_read()]: INQUIRY_CFG: read LINK-TRAN Caps.
    [ 522.188492] [uhs2_config_read()]: Begin INQUIRY_CFG, header=0x86, arg=0x420.
    [ 522.188499] uhs2_cmd_assemble: uhs2_cmd: header=0x86 arg=0x420
    [ 522.188504] uhs2_cmd_assemble: payload_len=0 packet_len=4 resp_len=0
    [ 522.188507] mmc0: starting CMD0 arg 00000000 flags 00000000
    [ 522.188516] sdhci_uhs2 [sdhci_uhs2_send_command()]: 0x400 is set to UHS2 CMD register.
    [ 522.188554] mmc0: sdhci: IRQ status 0x00000001
    [ 522.188582] mmc0: req done (CMD0): 0: 20024000 00000000 00000000 00000000
    [ 522.188601] [uhs2_config_read()]: Device LINK-TRAN Caps (0-31) is: 0x20024000.
    [ 522.188604] [uhs2_config_read()]: Device LINK-TRAN Caps (32-63) is: 0x0.
    [ 522.188605] [uhs2_config_write()]: SET_COMMON_CFG: write Generic Settings.
    [ 522.188607] [uhs2_config_write()]: Both Host and device support 2L-HD.
    [ 522.188609] [uhs2_config_write()]: Begin SET_COMMON_CFG, header=0x86, arg=0x8a0
    [ 522.188611] [uhs2_config_write()]: UHS2 write Generic Settings 00000000 00000000
    [ 522.188613] [uhs2_config_write()]: flags=00000005 dev_prop.n_lanes_set=0 host_caps.n_lanes_set=0
    [ 522.188615] uhs2_cmd_assemble: uhs2_cmd: header=0x86 arg=0x8a0
    [ 522.188618] uhs2_cmd_assemble: payload_len=8 packet_len=12 resp_len=0
    [ 522.188620] mmc0: starting CMD0 arg 00000000 flags 00000000
    [ 522.188632] sdhci_uhs2 [sdhci_uhs2_send_command()]: 0xc00 is set to UHS2 CMD register.
    [ 522.188650] mmc0: sdhci: IRQ status 0x00000001
    [ 522.188678] mmc0: req done (CMD0): 0: 00000000 00000000 00000000 00000000
    [ 522.188684] [uhs2_config_write()]: SET_COMMON_CFG: PHY Settings.
    [ 522.188686] [uhs2_config_write()]: set dev_prop.speed_range_set to SPEED_B
    [ 522.188689] [uhs2_config_write()]: UHS2 SET PHY Settings 40000000 04000000
    [ 522.188691] [uhs2_config_write()]: host->flags=00000015 dev_prop.speed_range_set=1
    [ 522.188693] [uhs2_config_write()]: dev_prop.n_lss_sync_set=4 host_caps.n_lss_sync_set=4
    [ 522.188694] [uhs2_config_write()]: dev_prop.n_lss_dir_set=0 host_caps.n_lss_dir_set=8
    [ 522.188696] [uhs2_config_write()]: Begin SET_COMMON_CFG header=0x86 arg=0xaa0
    [ 522.188698] [uhs2_config_write()]: payload[0]=0x40000000 payload[1]=0x4000000
    [ 522.188700] uhs2_cmd_assemble: uhs2_cmd: header=0x86 arg=0xaa0
    [ 522.188703] uhs2_cmd_assemble: payload_len=8 packet_len=12 resp_len=4
    [ 522.188705] mmc0: starting CMD0 arg 00000000 flags 00000000
    [ 522.188715] sdhci_uhs2 [sdhci_uhs2_send_command()]: 0xc00 is set to UHS2 CMD register.
    [ 522.188730] mmc0: sdhci: IRQ status 0x00000001
    [ 522.188741] mmc0: req done (CMD0): 0: 00000000 00000000 00000000 00000000
    [ 522.188746] [uhs2_config_write()]: SET_COMMON_CFG: LINK-TRAN Settings.
    [ 522.188748] [uhs2_config_write()]: Begin SET_COMMON_CFG header=0x86 arg=0xca0
    [ 522.188750] [uhs2_config_write()]: payload[0]=0x80320 payload[1]=0x1000000
    [ 522.188752] uhs2_cmd_assemble: uhs2_cmd: header=0x86 arg=0xca0
    [ 522.188754] uhs2_cmd_assemble: payload_len=8 packet_len=12 resp_len=0
    [ 522.188756] mmc0: starting CMD0 arg 00000000 flags 00000000
    [ 522.188766] sdhci_uhs2 [sdhci_uhs2_send_command()]: 0xc00 is set to UHS2 CMD register.
    [ 522.188780] mmc0: sdhci: IRQ status 0x00000001
    [ 522.188808] mmc0: req done (CMD0): 0: 00000000 00000000 00000000 00000000
    [ 522.188813] [uhs2_config_write()]: SET_COMMON_CFG: Set Config Completion.
    [ 522.188815] [uhs2_config_write()]: Begin SET_COMMON_CFG, header=0x86, arg=0x8a0, payload[0] = 0x0.
    [ 522.188817] uhs2_cmd_assemble: uhs2_cmd: header=0x86 arg=0x8a0
    [ 522.188819] uhs2_cmd_assemble: payload_len=8 packet_len=12 resp_len=5
    [ 522.188821] mmc0: starting CMD0 arg 00000000 flags 00000000
    [ 522.188831] sdhci_uhs2 [sdhci_uhs2_send_command()]: 0xc00 is set to UHS2 CMD register.
    [ 522.188842] mmc0: sdhci: IRQ status 0x00000001
    [ 522.188855] mmc0: req done (CMD0): 0: 00000000 00000000 00000000 00000000
    [ 522.188862] sdhci_uhs2 [sdhci_uhs2_do_set_reg()]: Begin sdhci_uhs2_set_reg, act 0.
    [ 522.201612] sdhci_uhs2 [sdhci_uhs2_do_set_reg()]: Begin sdhci_uhs2_set_reg, act 3.
    [ 522.201614] sdhci_uhs2 [sdhci_uhs2_do_set_reg()]: Begin sdhci_uhs2_set_reg, act 2.
    [ 522.201616] [uhs2_go_dormant()]: Begin GO_DORMANT_STATE, header=0x86, arg=0x192, payload=0x0.
    [ 522.201617] uhs2_cmd_assemble: uhs2_cmd: header=0x86 arg=0x192
    [ 522.201618] uhs2_cmd_assemble: payload_len=4 packet_len=8 resp_len=0
    [ 522.201619] mmc0: starting CMD0 arg 00000000 flags 00000000
    [ 522.201626] sdhci_uhs2 [sdhci_uhs2_send_command()]: 0x8c0 is set to UHS2 CMD register.
    [ 522.218633] mmc0: sdhci: IRQ status 0x00008000
    [ 522.218636] sdhci_uhs2 [sdhci_uhs2_irq()]: *** mmc0 got UHS2 interrupt: 0x00010000
    [ 522.218651] enter sdhci_pci_o2_reset.
    [ 522.218652] enter o2_uhs2_reset_sd_tran.
    [ 522.218652] exit o2_uhs2_reset_sd_tran.
    [ 522.218654] enter sdhci_pci_o2_reset.
    [ 522.218654] enter o2_uhs2_reset_sd_tran.
    [ 522.218654] exit o2_uhs2_reset_sd_tran.
    [ 522.218659] mmc0: req done (CMD0): -110: 00000000 00000000 00000000 00000000
    [ 522.218666] mmc0: uhs2_go_dormant: UHS2 CMD send fail, err= 0xffffff92!
    [ 522.218668] mmc0: uhs2_change_speed: UHS2 GO_DORMANT_STATE fail, err= 0xfffffffb!
    [ 522.218669] mmc0: UHS2 uhs2_change_speed() fail!

    含义是UHSII初始化接近完成,切换到高速的RangeB时,GO_DORMANT_STATE命令未完成,超时。
    解决办法:先绕过RangeB模式,使用RangA(较低速度的UHSII模式),为此要从一开始就上报host不支持RangeB。
    修改mmc/host/sdhci-uhs2.c中的上报host能力(capability)的speed_range为不支持RangeB

    1
    2
    3
    //mmc->uhs2_caps.speed_range =(caps_phy & SDHCI_UHS2_HOST_CAPS_PHY_RANGE_MASK) >> SDHCI_UHS2_HOST_CAPS_PHY_RANGE_SHIFT;

    mmc->uhs2_caps.speed_range = 0; //Range-A

    重新编译安装module后,UHSII初始化正常,读写正常。

事实上此GO_DORMANT fail issue的根本原因是兼容性问题:
UHSII初始化流程中,SD host侧对lane speed的配置最好在卡处在dormant状态下进行,host侧提高速度(从Range-A提高到RangeB)以后,卡侧在退出dormant状态时重新配置速度,和host速度匹配。
如果host侧修改lane speed时间点错误,有的SD卡来不及反应,不能同步速度,所以GO_DORMANT fail;而有的SD 卡性能好,随时同步host侧的速度,没有此issue。

另外有的Issue和硬件特性相关,例如上电需要等待一定时间以后,才能启动UHSII设备初始化,这个等待时间取决于SD host厂商的硬件特性。

Python使用正则表达式示例

Python的正则表达式比较全面的教程,参考# Python RegEx

使用背景:芯片ATE测试中,不同ATE平台的测试模式文件格式有不同,需要匹配字符串并按特定转换
转换前:

Pattern “pll_dll_100m_test” {
waveform_start:
W pll_dll_100m_wft;

//Enter PLL/DLL Mode
V {pll_dll_100m_group = 0 0 1 0 0 1 1 0 X X ;}
V {pll_dll_100m_group = 0 1 1 0 0 1 1 0 X X ;}
V {pll_dll_100m_group = 0 0 1 0 0 1 1 0 X X ;}

转后后:

//Enter PLL/DLL Mode
V {pll_dll_100m_group = 0 0 1 0 0 1 1 0 X X ;} W pll_dll_100m_wft;
V {pll_dll_100m_group = 0 1 1 0 0 1 1 0 X X ;}

规则:将以“W_xxx”的字符串放到下一个以“V_xxx”的字符串后面

利用python正则匹配,配合读取文件到字符串数组,实现如下转换:

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
import os.path
import re

infile_name = input("Please input the name of file in current directory to convert: ")
name_flag = infile_name.find('.')
if name_flag == -1:
print("file name error, need input the suffix of file name")
input("Please press Enter key to exit")
exit(0)
else:
if os.path.isfile(infile_name):
outfile_name = infile_name[0:name_flag] + "_updated" + infile_name[name_flag:]
else:
print("no such file!")
input("Please press Enter key to exit")
exit(0)

infile = open(infile_name, "r",encoding='utf-8')
outfile = open(outfile_name, "w",encoding='utf-8')

lines = infile.readlines()
infile.close()
flag = 0

for index in range(len(lines)):
str_obj = re.match('[\s]*W[\s].*', lines[index]) #match the "W ..."
if str_obj != None:
flag = 1
temp_index = index
temp_str = str_obj.group()
else:
str_obj = re.match('[\s]*V[\s].*', lines[index]) #match the "V ..."
if str_obj != None:
if flag == 1:
lines[temp_index] = '\n' #clear last "W ..."
lines[index] = str_obj.group() + ' ' + temp_str + '\n' #add the "W ..." from "V ..." end
flag = 0
outfile.writelines(lines)
outfile.close()
print("outputfile is " + outfile_name)
input("Please press Enter key to exit")

这里的W和V前面加了额外的匹配项:[\s]*,是因为文件存在不可见的回车换行等引起,如果不加匹配不到

0.背景

写技术笔记并发布博客,通常有以下方式:

  • 第三方平台CSDN/cnblogs,最简单但是最不可控,例如我写过一篇如何使用shadow-sock,直接被删掉,各种广告也是技术洁癖症不能忍的。
  • 使用云服务器自建Leanote博客系统,我用过几年,最大缺点是服务器续费太贵,且文章数据存在数据库很难导出,优点是自建的Leanote写作发布一体化体验极好。
  • 使用github+picgo+hexo, github作为图床和博客的云服务,picgo用于建立图床通道,hexo用于发布博客。缺点是github访问慢,用开源CDN可以很好解决;优点是全部免费,数据可移植(图片链接都在github图床),可长期使用(只要github不倒闭不锁区)

最终我选择github+picgo+hexo方案。
为什么不用gitee: gitee上传图片限制1M, github有25M。有了CDN, github的速度也不是问题

现在只需要确定Markdown编辑器

VSCode加Markdown插件试过,体验并不完美
Typora是很不错的选择,除了收费
Obsidian免费且优雅:

  • 支持动态渲染,即写出的Markdown语句自动显示预览
  • 支持各种快捷键,无需手动输入Markdown语法格式(Markdown 基本语法)。比如标题,链接,列表,引用,可以设置标准的Markdown快捷键。
  • 支持关联PicGo实现粘贴图片即上传到云端图床,这一点对于写作体验和文章的可迁移性很重要
  • 特色的Zettelkasten笔记管理方法,本文不描述这部分,参考玩转 Obsidian 01:打造知识循环利器

1. 搭建可移植的Markdown写作环境

Markdown文档本身是可移植的,但是其图片资源不是, 因为图片不是以二进制嵌入文档,而只是个图片地址的链接。

如果只是在本地写写Markdown,完全不需要考虑图片的可移植,只需要存本地固定路径即可。
如果想在本地写Markdown,且这个文档拿到别的机器,或者放到博客论坛发布,别人都可以看到图片,就要一个云端的图片存储服务,即图床。

本节讲如何实现一个“一处写作,到处可见”的Markdown写作环境,并优化工具设置,让写作顺滑流程

1.1 Github+PicGo搭建图床

完整流程参考PicGo + GitHub 搭建个人图床工具

流程包含以下部分:

  • 一个public的Github仓库,用来存放文章链接的图片,注意单仓库最大容量1GB。
  • 一个personal access token,用于PicGo免密访问github实现自动上传图片
  • 一个图片上传工具:PicGo及其插件,用它上连Github图床仓库,下连本地markdown编辑器,实现“图片粘贴即上传”
  • 一个CDN(推荐,很好用):使用开源CDN jsdelivr加速Github访问,避免因访问速度造成图片上传失败

最终我的配置如下:

单元测试:
手动上传本地图片,如果上传成功,且图片可预览,则功能正常

遇到的坑:
仓库名必须填<用户名><仓库名>,不是完整的http或git仓库链接!否则上传图片error404

PicGo支持log调试,参考:
PicGo上传图片到GitHub总是失败的特殊解决办法

2022/5/17更新:
最近发现jsdelivr在大陆挂了,那么只能放弃使用CDN, 直接使用github图床的raw链接.

将Picgo默认图床路径从jsdelivr的
https://cdn.jsdelivr.net/gh/账户名/图床仓库名@master
改为:
https://raw.githubusercontent.com/账户名/图床仓库名/master

注意master前是@还是反斜杠 ‘’/‘’.

对于已发布的博客的图床链接,直接用VSCode全局查找替换以上前缀即可,后面的图片id是不变的。

如何删除Github图床的图片:
PicGo默认的Github图床不能通过相册删除github仓库的图片,需要使用第三方插件github-plus,建议作为默认图床工具,支持相册删除同步到github仓库:

1.2 Obsidian链接图床

流程参考# 在Obsidian中使用图床实现“一次上传、多次搬运”省心又省力

重点部分:配置自动上传插件。写Markdown文章时粘贴的图片自动上传到Github+PicGo图床,无需手动上传和写链接

可以看到,此插件相当于一个PicGo客户端,而本地运行的PicGo Server已开启端口36677的监听。

单元测试:
以上配置好后,Obsidian新建一个Markdown文档,粘贴图片会显示updating… ,成功后可在github图床仓库看到该图片,且Markdown本地预览也可以看到。

遇到的坑:
一开始上传的图片,github可预览,Obsidian预览失败
在PicGo客户端查看刚才上传图片预览也不正常,PicGo客户端手动上传一张后才正常预览。
此后Obsidian再粘贴上传图片,预览正常。
可能是PicGo首次链接Obsidian插件的bug。

1.3 优化Obsidian写作体验

1.3.1 Markdown快捷键

很多Markdown编辑器支持各种快捷键,写作时不需要手动输入Markdown语法,例如一个ctrl+h形成标题,一个ctrl+k形成代码格式。
Obsidian默认快捷键不完整,需要补全优化
我之前用的Leanote写博客,Obsidian也快捷键设置如下。

提升标题 Ctrl + H (header)
有序列表 Ctrl + O (Ordered)
无序列表 Ctrl + U (Un-Ordered)
插入链接 Ctrl + L (Link)
插入代码 Ctrl + K (K = Code, 因为ctrl+C用于粘贴)
加粗 Ctrl + B (Bold)
斜体 Ctrl + I (Italian体)
引用 Ctrl + Q (Quote)
插入图片 Ctrl + G (Graph, 有了“粘贴即上传”,这个实际上用不上)

其他我基本不用的就没设置,例如表格,删除线,分割线等等。
如果经常用表格,推荐 Advanced Tables插件。
读者也可以参考VS Code的Markdown插件设置。

1.3.2 实时预览和标题大纲

实时预览是你写一句Markdown,自动转换成预览格式,而不显示Markdown源码。这样不需要开两个窗口,一个写源码一个看预览。在设置->编辑器里开启。

标题大纲是侧边栏显示文章标题列表,就像word的导航窗口一样。在设置-核心插件开启。

1.4 typora设置自动上传

2. 搭建hexo博客发布环境

2.1 环境安装

整体流程参考:# 个人博客第5篇——安装node.js和Hexo
官方文档参考:hexo.io

简单总结一下:

  • hexo是一个基于nodeJS的博客框架,nodeJS提供JavaScript实现后端服务的能力。
  • hexo可以将本地写好的Markdown文本和图片资源,以JavaScript网页资源的方式打包。hexo可以在本机运行服务,通过浏览器可以在网页访问。
  • 如果要使博客外网可访问,需要一个云服务存放hexo生成的博客,我们使用github仓库作为博客云端。前面已经搭建了github图床仓库,因此图片并不需要放到此hexo仓库。
  • 使用git实现hexo博客部署到github仓库

遇到的坑:
如果你以前用过hexo,之后nodeJS升级了版本,一定不要用以前的hexo博客目录,必须新建博客目录,重新hexo init安装相关hexo插件。否则nodeJS和hexo插件可能不兼容导致部署失败。

hexo插件版本是根据nodeJS版本自动安装的,配置文件是package.json。以nodeJS 16.3为例,自动生成的依赖版本如下:

{ "name": "hexo-site", "version": "0.0.0", "private": true, "scripts": { "build": "hexo generate", "clean": "hexo clean", "deploy": "hexo deploy", "server": "hexo server" }, "hexo": { "version": "6.0.0" }, "dependencies": { "hexo": "^6.0.0", "hexo-deployer-git": "^3.0.0", "hexo-generator-archive": "^1.0.0", "hexo-generator-category": "^1.0.0", "hexo-generator-index": "^2.0.0", "hexo-generator-tag": "^1.0.0", "hexo-renderer-ejs": "^2.0.0", "hexo-renderer-marked": "^5.0.0", "hexo-renderer-stylus": "^2.0.0", "hexo-server": "^3.0.0", "hexo-theme-landscape": "^0.0.3" } }

博客的github仓库,主题等关键配置都在hexo init对应目录下的_config.yml
我修改了几个关键配置如下:
博客框架的语言一定要配,否则默认德语阿拉伯语…

title: ThinkNotes
subtitle: Simple is not easy
author: Cursorhu
language: zh-CN

Github部署配置

url: https://cursorhu.github.io
deploy:
type: git
repo: https://github.com/cursorhu/cursorhu.github.io
branch: master

2.2 发布博客

hexo new命令可以从0新建markdown写博客,一般不用这么麻烦。

我们直接将Obsidian写好的Markdown(图片资源是图床链接)放到hexo博客目录的post文件夹,例如我的本地目录是:

F:\HexoBlog\source_posts

在Markdown文件前添加hexo博客特有的头,说明博客的标题时间,分类等属性。这个头如果用hexo new命令会自动生成,可以先new一个然后拷过来。

title: 博客文章名
date: 2022-02-28 15:04:18
tags: markdown
categories: markdown

最后使用git生成网页文件并发布到github博客仓库, 注意不是用node命令行!
在hexo博客目录下运行:

1
2
3
hexo clean   #清除缓存文件 db.json 和已生成的静态文件 public
hexo g #生成网站静态文件到默认设置的 public 文件夹(hexo generate 的缩写)
hexo d #自动生成网站静态文件,并部署到设定的仓库(hexo deploy 的缩写)

2.3 博客主题

博客主题(theme)取决于不同人喜好,github有大量的hexo主题,按需配置
推荐几个主流的:
Next
hexo-theme-material

以Next主题为例,安装:

$ cd hexo博客目录
$ git clone https://github.com/theme-next/hexo-theme-next themes/next

已我的配置为例,就会在F:\HexoBlog\themes\next看到next主题的源码和配置文件config.yml

Next配置可以自定义各种设置,例如子主题:

scheme: Muse
#scheme: Mist
#scheme: Pisces
#scheme: Gemini

我们前面已经用jsdelivr开源CDN为PicGo上传图片加速,可以配置Next主题的网页静态文件的相关库也用jsdelivr加速。最好用到才开启,例如motion enable, 其依赖库velocity就可以设置jsdelivr加速:

motion:
enable: true
velocity: //cdn.jsdelivr.net/npm/velocity-animate@1/velocity.min.js
velocity_ui: //cdn.jsdelivr.net/npm/velocity-animate@1/velocity.ui.min.js

注意这些配置文件也是Markdown格式,# 注释一定要带空格

hexo要使用next, 在hexo配置文件设置:

theme: next

hexo clean, hexo g, hexo s 跑本地服务,看一下效果:

最后hexo clean, g, d 命令三连部署到github

2.4 速度优化

参考# Hexo 网站访问速度优化

1.MD5简介

MD5的全称是Message-Digest Algorithm 5(信息摘要算法),经MD2、MD3和MD4发展而来。
所谓信息摘要,就是包含数据关键特性,能(唯一)识别原数据的关键信息。

MD5也称为单向散列算法,这是从其实现方式命名,因为:

  • MD5能对大量数据,进行哈希映射,输出固定长度(128bit)的数据,输出数据也称为原数据的信息摘要。
  • 不能由摘要推测出原数据,即MD5算法是单向的,当加密来用的话,只能加密不能解密。

MD5的特点:

  • 固定长度:输入任意长度的信息,经过MD5处理,输出总是128位的信息。
  • 唯一性:不同的输入得到的不同的结果;同样的输入一定得到相同的结果。
  • 不可逆:根据128位的输出结果不可能反推出输入的信息。

2.MD5的应用

1、防止被篡改:
1)比如A和B发送一个电子文档,发送前,A先自己计算出数据的MD5输出结果a。
然后在B收到电子文档后,B计算得到一个MD5的输出结果b。
如果a与b一样就代表传输中途数据未被篡改。
2)比如A提供文件下载,为了防止不法分子在文件中添加木马,伪装成A的文件。A可以在网站上公布由安装文件得到的MD5输出结果。
要下载文件的人只需要下载后,验证MD5是否和A一致,如果不一致,就是被其他人修改过。

2、防止暴露明文:
基本上存储用户密码的场景,都用到MD5加密明文。
1)例如网站服务器在其数据库存储用户的密码,都是存储用户密码的MD5值。就算不法分子得用户密码的MD5值,也无法知道用户的密码。
2)在UNIX、Linux系统中,用户密码就是以MD5(或其它类似的算法)经加密后存储在文件系统中。当用户登录的时候,系统把用户输入的密码计算成MD5值,然后再去和保存在文件系统中的MD5值进行比较,进而确定输入的密码是否正确。通过这样的步骤,系统在并不知道用户密码的明码的情况下就可以确定用户登录系统的合法性。这不但可以避免用户的密码被具有系统管理员权限的用户知道,而且还在一定程度上增加了密码被破解的难度。

3、防止抵赖(数字签名):
这需要一个存储MD5值的第三方认证机构。例如A写了一个文件,认证机构对此文件用MD5算法产生摘要信息并做好记录。若以后A说这文件不是他写的,权威机构只需对此文件重新产生摘要信息,然后跟记录在册的摘要信息进行比对,相同的话,就证明是A写的了。这就是所谓的“数字签名”。

3.MD5算法实现

对MD5算法简要的叙述可以为:
MD5以512位分组来处理输入的信息,且每一分组又被划分为16个32位子分组,经过了一系列的处理后,算法的输出由四个32位分组组成,将这四个32位分组级联后将生成一个128位散列值。

第一步、填充:如果输入信息的长度(bit)对512求余的结果不等于448,就需要填充使得对512求余的结果等于448。填充的方法是填充一个1和n个0。填充完后,信息的长度就为N512+448(bit);
第二步、记录信息长度:用64位来存储填充前信息长度。这64位加在第一步结果的后面,这样信息长度就变为N
512+448+64=(N+1)*512位。
第三步、装入标准的幻数(四个整数):标准的幻数(物理顺序)是(A=(01234567) 16 ,B=(89ABCDEF) 16 ,C=(FEDCBA98) 16 ,D=(76543210) 16 )。如果在程序中定义应该考虑大小端(A=0X67452301L,B=0XEFCDAB89L,C=0X98BADCFEL,D=0X10325476L)。
第四步、四轮循环运算:循环的次数是分组的个数(N+1)。 最终由4个32bit数据拼成128bit的结果。

image-20221212143002209

4.MD5的安全性

普遍认为MD5是很安全,因为哈希散列是强抗碰撞的:已知原数据和其MD5值,想通过枚举找到一个相同MD5值的数据,基本不可能。如果暴力破解MD5,其运算时间是不可想象的。

但是实际应用上,如果把用户密码仅仅MD5处理后再存储到数据库,其实是很不安全的。因为用户的密码的固有特征,让枚举集合变小了许多,原因:

  • 用户密码是比较短的一般8位左右。
  • 很多用户的密码有规律,例如使用生日,手机号码,或者使用常用数字组合,或某个英文单词。
  • 许多用户的常用密码只有一个,也就是说,泄漏了微信的密码,也很有可能QQ,支付宝密码也泄漏了

如果把常用的密码先MD5处理后存储结果,然后再跟用户的MD5结果匹配,这时就有较大概率“碰撞”,得到明文。这种预先存储的MD5表称为rainbow-table。
因此MD5作为“信息摘要”的用途多一些,作为加密,还需要配合其他的算法(例如AES等几种公钥算法),或者“加盐”。
安全性比较好的网站,都会用一种叫做 “加盐”(salt)的方式来存储密码:

  • 先将用户输入的密码进行一次MD5(或其它哈希算法)加密。
  • 将得到的MD5值前后加上一些只有管理员自己知道的随机串,再进行一次MD5加密。
  • 这个随机串中可以包括某些固定的串,也可以包括用户名(用来保证每个用户加密使用的密钥都不一样)。
  • 在管理员和用户的两个salt没有泄露的情况下,黑客拿到加密串,就几乎不可能推算出原始的密码是什么了。

5.MD5库

在涉及到文件传输的场景,通常用MD5校验文件的一致性,openSSL库提供MD5计算函数:

MD5_Update

1
2
3
int MD5_Init(MD5_CTX *c);
int MD5_Update(MD5_CTX *c, const void *data, unsigned long len);
int MD5_Final(unsigned char *md, MD5_CTX *c);