命令

109k 词

password: ly123

hexo命令

clean:

1
hexo clean

生成网页

1
hexo g

部署:

1
hexo d

在本机上部署

1
hexo s

hexo添加对数学公式的支持

安装hexo-filter-mathjax和hexo-renderer-pandoc渲染器,

教程可以参考hexo-filter-mathjax项目主页:

https://github.com/next-theme/hexo-filter-mathjax

另外也可以参考下,里面有如何卸载原来的渲染器的过程,否则有冲突: https://blog.csdn.net/weixin_45511189/article/details/115798563

还有安装hexo-renderer-pandoc记得安装pandoc,不然会报错:

https://github.com/jgm/pandoc/releases

项目上有注意事项,仔细看格式,不然可能打印不出来

设置获取本机 ip 的别名

1
2
3
4
#通过ifconfig获取当前机器的ip地址
#alias getIp="ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | awk '\$0!=\"127.0.0.1\" {print \$0}'"
#如果想只获取一个 ip 地址请使用下面这条
alias getIp="ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | awk '\$0!=\"127.0.0.1\" {print \$0; exit}'"

docker 命令:

创建macvlan网络

为了能够让docker启动的容器作为家庭网络中的旁路由,因此需要创建macvlan网络。 其中 192.168.3.1为你局域网的网关,em1为你机器的网卡名称,这两个请根据实际情况修改。

  1. (可选)让docker监听ipv6。 编辑etc/docker/daemon.json文件
1
2
3
4
{  
 "ipv6": true,
 "fixed-cidr-v6": "2409:DA8:8001:7B22:200::/80"
}

重启docker

1
sudo systemctl restart docker
  1. 创建macvlan 没有ipv6的版本
1
2
3
4
5
6
docker network create -d macvlan \  
   --subnet=192.168.3.0/24 \
   --gateway=192.168.3.1 \
    -o parent=em1 \
    -o macvlan_mode=bridge macnet

我这边本地dockerinclash配置如下:

1
docker network create -d macvlan --subnet=192.168.1.0/24 --gateway=192.168.1.1 -o parent=eth0 -o macvlan_mode=bridge macnet

有ipv6的版本

1
2
3
4
5
6
7
docker network create -d macvlan --ipv6 \  
   --subnet=192.168.3.0/24 \
   --gateway=192.168.3.1 \
   --subnet=2409:DA8:8001:7B22:200::/80
   --gateway=2409:DA8:8001:7B22:200::1 \
    -o parent=em1 \
    -o macvlan_mode=bridge macnet

注意看含义,有的值需要变

查看网络:

1
docker network ls

制作docker镜像并创建容器

  1. 获取代码
1
2
3
git clone https://github.com/UntaggedRui/clashindocker
cd clashindocker
cp example.yml config.yml
  1. 更改地址 docker-compose.yml中的 ipv4_address为你的ip地址.
  2. 更改 config.yml中的 proxy-providerurl为你的机场订阅地址.
  3. 启动容器
1
docker compose up -d

如果遇到无法访问或者启动的问题,可以查看一下日志:

1
docker-compose logs -f

或者

在有项目的Dockerfile的目录运行

docker build -t TARGET_NAME .

TARGET_NAME为镜像名称和版本,可以自己起个名字

官方是:

1
docker build --tag name:tag . #如docker build . -t keynetworks/ztncui:latest

生成并运行容器

docker pull ghcr.io/raywangqvq/bilibili_tool_pro docker run -d --name="bili" -v /bili/Logs:/app/Logs -e Ray_DailyTaskConfig__Cron="0 15 * * *" -e Ray_LiveLotteryTaskConfig__Cron="0 22 * * *" -e Ray_UnfollowBatchedTaskConfig__Cron="0 6 1 * *" -e Ray_VipBigPointConfig__Cron="7 1 * * *" ghcr.io/raywangqvq/bilibili_tool_pro

查看实时日志

docker logs -f bili

删除容器

docker rm bili

进入容器

docker exec -it bili /bin/bash

查看一下缓存情况:

1
2
docker system df 

alt text

可以发现,存在大量使用 docker build 命令时产生的镜像缓存 (Build Cache) ,下面使用命令 prune 将其彻底清理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
docker system prune --help
# 输出
Flag shorthand -h has been deprecated, please use --help

Usage: docker system prune [OPTIONS]

Remove unused data

Options:
-a, --all Remove all unused images not just dangling ones
--filter filter Provide filter values (e.g. 'label=<key>=<value>')
-f, --force Do not prompt for confirmation
--volumes Prune volumes

docker国内镜像加速

对于国内加速镜像,直接设置镜像地址就好了: 编辑/etc/docker/daemon.json添加

1
2
3
{
"registry-mirrors": ["https://docker.mirrors.ustc.edu.cn"]
}

也可以用其它镜像比如阿里云 或者 daocloud 等等。

docker pull 拉取镜像时设置代理

docker 的代理设置是一件比较麻烦的事情,很容易出问题,但是弄懂了也还是比较清楚。

首先, docker pull 和 docker build/docker run 使用代理的方式不一样!

docker pull 的代理被 systemd 接管,所以需要设置 systemd…

sudo vim /etc/systemd/system/docker.service.d/http-proxy.conf

1
2
3
[Service]
Environment="HTTP_PROXY=http://127.0.0.1:8123"
Environment="HTTPS_PROXY=http://127.0.0.1:8123"

这里的127.0.0.1是直接用了本机的 http 代理,然后重启服务才能生效

1
2
sudo systemctl daemon-reload
sudo systemctl restart docker

可以通过sudo systemctl show --property=Environment docker看到设置的环境变量。

然后docker pull就会使用代理啦!

这里 HTTP 代理可以通过你的代理软件开出来,如果你的代理软件只能开出来 socks5 代理的话,你可以用 polipo 开一个 http 代理使用

build 镜像时使用代理

在 build 时添加 --build-arg 参数来设置生成镜像时使用的环境变量

1
docker build --build-arg http_proxy=http://172.17.0.1:8123 --build-arg https_proxy=http://172.17.0.1:8123 -t image_name .

这里需要注意的是 ip 地址用了默认虚拟网卡docker0 的地址172.17.0.1,因为build的时候network参数默认为bridge,即docker内部会使用这个网段,因为要访问到宿主机的代理只能这样访问; 另外,也可以设置参数--network=host 来直接和宿主机共用网络,就能直接使用127.0.0.1来访问到宿主机了

设置 docker 全局代理

设置方法参考官方文档: https://docs.docker.com/network/proxy/

注意新版和旧版本(17.07以前)设置方法不一样,比旧版更简单而且不需要重启服务,网上文章基本都是针对旧版本的,可能未来又变化了,所以多看官方文档~ 以及这种设置方法只对 build 和 run 的容易有用, docker pull 要按照上面的方法设置。

vim ~/.docker/config.json

1
2
3
4
5
6
7
8
9
10
11
{
"proxies":
{
"default":
{
"httpProxy": "http://172.17.0.1:8123",
"httpsProxy": "http://172.17.0.1:8123",
"noProxy": "localhost,127.0.0.1,.daocloud.io"
}
}
}

这样设置后,build和run都会将http_proxy和https_proxy ftp_proxy变量设置成对应的代理地址,如果你在容器里面不想使用这个代理了,需要export http_proxy= 和 export https_proxy= 将两个变量设置为空

注意:

仅支持 http https ftp 协议,不支持 socks5 协议(2022.3.24,未来不一定,官方文档为准),可以使用polipo创建一个http代理服务,参考https://neucrack.com/p/275

这里使用了172.17.0.1(docker 虚拟网卡地址), 而不是127.0.0.1, 这是因为这是从容器内部的角度来看的, 容器内部要使用代理,默认情况下只能访问这个虚拟网卡的地址, 127.0.0.1是容器内部, 如果代理在宿主机, 要使用 虚拟网卡的地址才能访问到.

这个文件一旦存在, docker就会使用这里面的代理, 包括创建的容器都会使用它。 所以不需要代理了, 需要关闭代理, 就是把文件重命名一下就好了, 这点用起来确实挺麻烦,也许未来会优化体验吧。

注意, 一个容器一旦生成, 这些环境变量(http_proxy https_proxy ftp_proxy no_proxy)就会被继承到容器中, 就算把config.json删除, 这个容器依然使用创建时的环境变量,可以手动在容器内重新设置这些环境变量, 这点也挺容易让人头疼的, 一定要注意。

比如 有个镜像叫 imageA, 现在设置~/.docker/config.json也就是有代理, 然后docker run创建容器 A, 然后再重命名config.json, 创建容器B, 然后会发现无论现在怎么修改config.json, 容器A B都会使用它被创建时的代理设置,即容器A使用代理, 容器B不使用代理

容器使用宿主机的代理的几种方法总结

和 build 时类似,只需要在 生成容器时(run -e http_proxy=...) 或者 容器中 设置环境变量http_proxy和https_proxy变量即可,同样需要注意容易使用的网络类型是bridge还是host,根据类别设置正确的代理 ip 地址

方法一: 直接在容器内使用(推荐

1
export ALL_PROXY='socks5://172.17.0.1:1080'

这里ip地址使用的是宿主机的ip

方法二: 与宿主机共享网络时直接在容器内使用 创建容器时使用--network=host参数

然后在docker内设置代理,比如全局代理

1
export ALL_PROXY='socks5://127.0.0.1:1080'

这样就可以使用宿主机的代理了

但是要注意, 如果是用--network=host,这样的话使用 -p 参数映射端口就没用了, 即所有端口都是开放和宿主机共享的

方法三: 映射代理端口后直接在容器内使用 docker run时带参数-p映射代理的端口到容器, 在容器里面使用即可,比如:

1
2
docker run  -p 1080:1080 .....
export ALL_PROXY='socks5://127.0.0.1:1080'

方法四: docker配置全局代理,以在生成容器时自动设置代理变量 设置 docker 全局代理,比如设置了http://172.17.0.1:8123, 容器内全都会走这个代理,除了配置文件中的白名单里面的地址以外

注意: 全局代理和 --network=host 参数只能用一个,不然就容易混乱

git配置和取消代理

1
2
3
4
5
6
7
8
9
10
git config --global http.proxy http://127.0.0.1:7890
git config --global https.proxy https://127.0.0.1:7890
或者
git config --global http.proxy http://localhost:7890
git config --global https.proxy https://localhost:7890
git config --global --unset http.proxy
git config --global --unset https.proxy

确认Mac上git的配置
git config --list

或者直接修改 ~/.gitconfig

仅为github.com设置socks5代理(推荐这种方式, 公司内网就不用设代理了, 多此一举): git config --global http.https://github.com.proxy socks5://127.0.0.1:1086 其中1086是socks5的监听端口, 这个可以配置的, 每个人不同, 在macOS上一般为1086. 设置完成后, ~/.gitconfig文件中会增加以下条目:

1
2
[http "https://github.com"]
proxy = socks5://127.0.0.1:1086
  1. ssh访问 需要修改~/.ssh/config文件, 没有的话新建一个. 同样仅为github.com设置代理:

    1
    2
    3
    Host github.com
    User git
    ProxyCommand nc -v -x 127.0.0.1:7890 %h %p

    如果是在Windows下, 则需要个性%home%.ssh, 其中内容类似于:

    1
    2
    3
    Host github.com
    User git
    ProxyCommand connect -S 127.0.0.1:7890 %h %p

    这里-S表示使用socks5代理, 如果是http代理则为-H. connect工具git自带, 在.

python模块化

Python init.py 作用详解

init.py 文件的作用是将文件夹变为一个Python模块,Python 中的每个模块的包中,都有__init__.py 文件。

通常__init__.py 文件为空,但是我们还可以为它增加其他的功能。我们在导入一个包时,实际上是导入了它的__init__.py文件。这样我们可以在__init__.py文件中批量导入我们所需要的模块,而不再需要一个一个的导入。

init.py中还有一个重要的变量,all, 它用来将模块全部导入。

1
2
3
4
5
6
7
8
9
10
# package
# __init__.py
import re
import urllib
import sys
import os

# a.py
import package
print(package.re, package.urllib, package.sys, package.os)

init.py中还有一个重要的变量,all, 它用来将模块全部导入。

1
2
3
4
5
# __init__.py
__all__ = ['os', 'sys', 're', 'urllib']

# a.py
from package import *

这时就会把注册在__init__.py文件中__all__列表中的模块和包导入到当前文件中来。

可以了解到,init.py主要控制包的导入行为。要想清楚理解__init__.py文件的作用,还需要详细了解一下import语句引用机制:

可以被import语句导入的对象是以下类型:

  • 模块文件(.py文件)
  • C或C++扩展(已编译为共享库或DLL文件)
  • 包(包含多个模块)
  • 内建模块(使用C编写并已链接到Python解释器中)

当导入模块时,解释器按照sys.path列表中的目录顺序来查找导入文件。

1
2
3
4
5
6
7
8
9
10
11
12
import sys
>>> print(sys.path)

# Linux:
['', '/usr/local/lib/python3.4',
'/usr/local/lib/python3.4/plat-sunos5',
'/usr/local/lib/python3.4/lib-tk',
'/usr/local/lib/python3.4/lib-dynload',
'/usr/local/lib/python3.4/site-packages']

# Windows:
['', 'C:\\WINDOWS\\system32\\python34.zip', 'C:\\Documents and Settings\\weizhong', 'C:\\Python34\\DLLs', 'C:\\Python34\\lib', 'C:\\Python34\\lib\\plat-win', 'C:\\Python34\\lib\\lib-tk', 'C:\\Python34\\Lib\\site-packages\\pythonwin', 'C:\\Python34', 'C:\\Python34\\lib\\site-packages', 'C:\\Python34\\lib\\site-packages\\win32', 'C:\\Python34\\lib\\site-packages\\win32\\lib', 'C:\\Python34\\lib\\site-packages\\wx-2.6-msw-unicode']

其中list第一个元素空字符串代表当前目录。

关于.pyc 文件 与 .pyo 文件

.py文件的汇编,只有在import语句执行时进行,当.py文件第一次被导入时,它会被汇编为字节代码,并将字节码写入同名的.pyc文件中。后来每次导入操作都会直接执行.pyc 文件(当.py文件的修改时间发生改变,这样会生成新的.pyc文件),在解释器使用-O选项时,将使用同名的.pyo文件,这个文件去掉了断言(assert)、断行号以及其他调试信息,体积更小,运行更快。(使用-OO选项,生成的.pyo文件会忽略文档信息)

导入模块

模块通常为单独的.py文件,可以用import直接引用,可以作为模块的文件类型有.py、.pyo、.pyc、.pyd、.so、.dll

在导入模块时,解释器做以下工作:

  1. 已导入模块的名称创建新的命名空间,通过该命名空间就可以访问导入模块的属性和方法。
  2. 在新创建的命名空间中执行源代码文件。
  3. 创建一个名为源代码文件的对象,该对象引用模块的名字空间,这样就可以通过这个对象访问模块中的函数及变量

import 语句可以在程序的任何位置使用,你可以在程序中多次导入同一个模块,但模块中的代码仅仅在该模块被首次导入时执行。后面的import语句只是简单的创建一个到模块名字空间的引用而已。

sys.modules字典中保存着所有被导入模块的模块名到模块对象的映射。

导入包

多个相关联的模块组成一个包,以便于维护和使用,同时能有限的避免命名空间的冲突。一般来说,包的结构可以是这样的:

1
2
3
4
5
6
7
package
|- subpackage1
|- __init__.py
|- a.py
|- subpackage2
|- __init__.py
|- b.py

有以下几种导入方式:

1
2
3
import subpackage1.a # 将模块subpackage.a导入全局命名空间,例如访问a中属性时用subpackage1.a.attr
from subpackage1 import a # 将模块a导入全局命名空间,例如访问a中属性时用a.attr_a
from subpackage.a import attr_a # 将模块a的属性直接导入到命名空间中,例如访问a中属性时直接用attr_a

使用from语句可以把模块直接导入当前命名空间,from语句并不引用导入对象的命名空间,而是将被导入对象直接引入当前命名空间。

需求 1

现在我有 models 文件夹作为一个模块,其目录结构如下: 1740550413252

现在我想实现一个 get_cls_net函数,定义如下,其中 OurNet 和CACMNet放在 Net.py,OurNet2放在 OurNet2.py,我要怎么组织起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def get_cls_net(config, **kwargs):
# 定义模型名字和模型类的映射关系
model_mapping = {
'CACMNet': CACMNet,
'OurNet': OurNet,
'OurNet2': OurNet2
}
model_name = config.MODEL.MODEL_NAME
# 根据模型名字从映射关系中获取对应的模型类
if model_name in model_mapping:
model_class = model_mapping[model_name]
# 实例化模型
model = model_class(config, **kwargs)
# 初始化模型权重
model.init_weights()
return model
else:
raise ValueError(f"Model {model_name} not found in the model mapping.")

解决方案:

好的,用户现在有一个问题,需要实现一个get_cls_net函数,根据不同的模型名称来实例化对应的类。他们的项目结构里,各个模型分布在不同的文件中,比如OurNet和CACMNet在Net.py,而OurNet2在OurNet2.py里。首先,我需要理解用户的需求,以及他们可能遇到的问题。

首先,用户提到models文件夹作为模块,里面有多个Python文件。他们想要组织这些模型类,使得get_cls_net函数能够正确导入这些类并创建实例。目前,这个函数中有一个模型名称到类的映射,但用户可能遇到的问题是,当这些类分布在不同的文件中时,如何正确导入它们,避免出现导入错误或者循环依赖的问题。

接下来,我需要分析用户给出的目录结构。models模块下有__init__.py,说明这是一个包。各个模型类分布在不同的文件中:CACMNet和OurNet在Net.py,OurNet2在OurNet2.py。用户希望在get_cls_net函数中通过模型名称动态获取对应的类。

可能的解决方案是,在__init__.py中集中导入这些类,这样当其他文件从models模块导入时,可以直接引用这些类,而不需要每个文件都单独导入。例如,在__init__.py中,使用from .Net import OurNet, CACMNet和from .OurNet2 import OurNet2,这样在get_cls_net函数中就可以通过models模块直接访问这些类。

然后,用户需要将get_cls_net函数放在哪里?用户可能希望将这个函数放在models模块的某个文件中,比如__init__.py或者单独的文件里。但根据常见的做法,可能会建议将函数放在__init__.py中,或者创建一个新的文件如model_factory.py来存放这个函数,这样可以保持代码的整洁。

不过,用户提供的示例代码中,get_cls_net函数需要访问各个模型类,所以需要确保这些类已经被正确导入到当前的命名空间中。如果这个函数被放在models模块的__init__.py中,那么当用户调用models.get_cls_net时,可以直接使用这些类。或者,用户可能希望将这个函数放在另一个文件中,比如model_factory.py,这时候需要确保该文件正确地从models模块导入所需的类。

另一个需要注意的问题是,如何避免循环导入。如果各个模型文件之间有相互依赖的情况,可能会导致导入错误。因此,集中导入在__init__.py中可以减少这类问题,因为每个模型文件只需要在__init__.py中被导入一次,其他地方通过models模块引用。

接下来,用户需要确保每个模型类在对应的文件中被正确定义。比如,Net.py中应该包含OurNet和CACMNet两个类的定义,而OurNet2.py中有OurNet2类。此外,每个类需要有一个接受config和**kwargs参数的构造函数,以及init_weights方法,这样才能在get_cls_net中被正确实例化和初始化。

然后,关于如何组织代码,可能的步骤是:

在models/init.py中导入所有模型类,使它们成为模块的顶级导出。 在models目录下的适当位置定义get_cls_net函数,比如在__init__.py中,或者单独的文件中。 确保在get_cls_net函数中能够正确引用这些类,比如通过from models import CACMNet, OurNet, OurNet2或者直接使用全局映射。

用户提供的示例代码中的model_mapping直接使用了类名,这意味着这些类必须在get_cls_net函数的定义范围内可用。因此,如果这个函数被放在models模块的__init__.py中,并且已经在__init__.py中导入了这些类,那么映射就可以正常工作。

总结解决方案:

在models/init.py中导入各个模型类,使得外部可以通过from models import ...来访问。 将get_cls_net函数放在models模块的适当位置,比如__init__.py或者一个单独的文件如model_factory.py。 在函数中建立模型名称到类的映射,使用导入的类。

可能的代码结构:

在models/init.py中:

from .Net import CACMNet, OurNet from .OurNet2 import OurNet2

def get_cls_net(config, **kwargs): model_mapping = { 'CACMNet': CACMNet, 'OurNet': OurNet, 'OurNet2': OurNet2 }

其余代码...

这样,当用户使用时,可以导入get_cls_net函数:

from models import get_cls_net

或者,如果函数放在单独的文件中,比如model_factory.py:

在models/model_factory.py中:

from . import CACMNet, OurNet, OurNet2

def get_cls_net(config, **kwargs): model_mapping = { ... }

然后在__init__.py中导入该函数:

from .model_factory import get_cls_net

这样用户同样可以从models模块直接导入。

此外,需要确保各个模型类的文件没有语法错误,比如Net.py中的类定义正确,参数正确。比如,每个模型类应该接受config作为参数,并且有init_weights方法。

最后,用户可能需要处理可能的错误,比如模型名称不存在时的异常处理,这部分在示例代码中已经处理了,所以只需要确保映射正确即可。

可能的错误点:

导入路径错误,导致类无法正确导入到__init__.py中。 模型类没有正确实现所需的构造函数或方法。 文件命名或路径问题,例如OurNet2.py是否正确命名,是否在models目录下。 如果用户将get_cls_net函数放在另一个文件中,比如attention.py或TCN.py中,需要确保该文件正确导入模型类。

测试步骤:

用户需要测试导入是否成功,可以尝试在Python中导入models模块,并检查是否有导入错误。例如:

python -c "from models import CACMNet, OurNet, OurNet2, get_cls_net"

如果没有错误,说明导入正确。然后测试get_cls_net函数能否正确创建实例。

综上,解决方案的核心在于正确组织__init__.py中的导入,并将get_cls_net函数放在合适的位置,确保映射中的类能够被正确引用。

根据当前目录结构和需求,建议按以下方式组织代码:

文件结构优化后的效果:

1
2
3
4
5
6
7
python
Copy Code
models/
├── init.py # 集中暴露所有模型类
├── Net.py # 包含CACMNet和OurNet
├── OurNet2.py # 包含OurNet2
└── ...其他文件

在models/init.py中写入:

1
2
3
4
5
6
7
8
9
10
11
from .Net import CACMNet, OurNet
from .OurNet2 import OurNet2all = ['CACMNet', 'OurNet', 'OurNet2', 'get_cls_net']def get_cls_net(config, **kwargs):
model_mapping = {
'CACMNet': CACMNet,
'OurNet': OurNet,
'OurNet2': OurNet2
} if (model_name := config.MODEL.MODEL_NAME) in model_mapping:
model = model_mapping[model_name](config, **kwargs)
model.init_weights()
return model
raise ValueError(f"Model {model_name} not found. Available: {list(model_mapping)}")

各模型文件示例(Net.py):

1
2
3
4
5
6
7
8
9
10
11
12
class CACMNet:
def __init__(self, config, **kwargs):
# 实现细节
pass

def init_weights(self):
# 权重初始化
pass

class OurNet:
# 类似CACMNet的结构
pass

使用方式:

1
from models import get_cls_net

假设config对象已包含MODEL.MODEL_NAME字段

1
model = get_cls_net(config, other_params=value)

优势说明:

集中式管理:所有模型类通过__init__.py统一暴露,调用时只需from models import ... 动态扩展:新增模型时只需在映射表添加条目 错误友好:清晰的错误提示和可用模型列表 权重初始化:强制所有模型类实现init_weights方法,保持接口统一

注意要点:

确保所有模型类都接收(config, **kwargs)参数 需要从项目中彻底清除pycache(建议添加.gitignore) 所有模型类文件需要保持无副作用代码(避免在模块层级写执行代码)

需求 2

如果 OurNet2.py 中要使用 Net.py 中的某些类,我要如何使用?比如说OurNet2.py中要使用 Net.py 中的  FeedForward类,我要如何 import

解决办法:

相对导入

1
2
3
4
5
6
7
# OurNet2.py
from .Net import FeedForward # 使用相对路径

class OurNet2:
def __init__(self):
self.ff = FeedForward()

解决conda activate虚拟环境后,pip安装的包没放在虚拟环境的问题

1750043198166

许多公式都有π和e,可能的原因有什么?

1750066936470
1750067008527
1750067028454

clash配置代理

要想让其他设备通过设置网关的方式将clash所在的设备设置为网关有三种方法:

开启ip转发功能。 mac: sudo sysctl -e net.inet.ip.forwarding=1 ubuntu: sudo sysctl -w net.ipv4.ip_forward=1

方法一:

安装官方提供的方法,将下面的内容添加到config.yml中,好像只支持linux,这个方法应该也相当于方法三,只不过是官方实现了,我后面才发现的:

1722848548632
1
2
3
4
5
6
# Enable the TPROXY listener
tproxy-port: 9898

iptables:
enable: true # default is false
inbound-interface: eth0 # detect the inbound interface, default is 'lo'

这种方式不能开tun模式,会报错如下:

1722848865964

方法二:

在config.yml开启tun模式.但是测试tun模式下我的该设备通过ddns配置的域名,域名能正确获取到公网ip,但是无法访问(我还没找到原因,网上没有相关的资料,感觉还是tun模式的实现原理的问题,或者说tun模式新增这个tun设备,在使用iptable将流量转发给这个设备再返回时出现了问题,唉不想研究,如果想研究我感觉按照下面的思路应该很容易发现问题出现的原因:看看打开tun模式运行了哪些iptables命令),所以我不打算用这种方式

1722426239320

方法三(建议使用方法一):

配置iptables规则,接管流量,好处是不影响通过域名访问该设备(有可能这台机器导致docker无法正常pull镜像(我一开始开启tun模式然后运行下面的 脚本遇到了,国内源也不行),唉,不想找解决办法了,反正我后面没遇到,我估计是当时我同时开启了tun模式和下面的脚本,iptables规则冲突了,可能流量被一边劫持了,遇到了应该只要开机别运行这个脚本然后重启电脑就好了,反正我当时是这么解决的)

config_iptable.sh

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
#在nat表中新建一个clash规则链
iptables -t nat -N CLASH
iptables -t nat -F CLASH
#排除环形地址与保留地址,匹配之后直接RETURN
iptables -t nat -A CLASH -d 0.0.0.0/8 -j RETURN
iptables -t nat -A CLASH -d 10.0.0.0/8 -j RETURN
iptables -t nat -A CLASH -d 127.0.0.0/8 -j RETURN
iptables -t nat -A CLASH -d 169.254.0.0/16 -j RETURN
iptables -t nat -A CLASH -d 172.16.0.0/12 -j RETURN
iptables -t nat -A CLASH -d 192.168.0.0/16 -j RETURN
iptables -t nat -A CLASH -d 224.0.0.0/4 -j RETURN
iptables -t nat -A CLASH -d 240.0.0.0/4 -j RETURN
#重定向tcp流量到本机7892端口
iptables -t nat -A CLASH -p tcp -j REDIRECT --to-port 7892
#拦截外部tcp数据并交给clash规则链处理
iptables -t nat -A PREROUTING -p tcp -j CLASH

#在nat表中新建一个clash_dns规则链
iptables -t nat -N CLASH_DNS
#清空clash_dns规则链
iptables -t nat -F CLASH_DNS
#重定向udp流量到本机1053端口
iptables -t nat -A CLASH_DNS -p udp -j REDIRECT --to-port 1053
iptables -t nat -A CLASH_DNS -p tcp -j REDIRECT --to-port 1053
#抓取本机产生的53端口流量交给clash_dns规则链处理
# iptables -t nat -I OUTPUT -p udp --dport 53 -j CLASH_DNS
#拦截外部upd的53端口流量交给clash_dns规则链处理
iptables -t nat -I PREROUTING -p udp --dport 53 -j CLASH_DNS
iptables -t nat -I PREROUTING -p tcp --dport 53 -j CLASH_DNS


1722426398285

docker pull不了image(--------------------------------好吧,当我没说,只是adguard-home安装不了)

1722428771818

配置代理,反正我试了运行clash的机器上上面两种方式都还要执行这个步骤

1
2
3
4
5
6
7
8
cat /etc/environment 
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"
export http_proxy="http://127.0.0.1:7890"
export https_proxy="http://127.0.0.1:7890"
export no_proxy="localhost, 127.0.0.1"

visudo:
Defaults env_keep+="http_proxy https_proxy no_proxy"

常见问题

1、clash勾选了仅内网还是被跑流量

1722592336269

Nginx Proxy Manager输入账号密码点击登录却遇到bad gateway

解决办法:

等一会就行了,之后就发现能登录了,等一下是因为在初始化安装些东西

1722855105546

看日志发现报错如下:

certbot --version The 'certbot_dns_cloudflare._internal.dns_cloudflare' plugin errored while loading: No module named 'CloudFlare'. You may need to remove or update this plugin. The Certbot log will contain the full error details and this should be reported to the plugin developer.

光盘刻录:

过程: 开始刻录->卸载光盘->读取光盘内分段数据->数据打包->刻录。 系统刻录光盘日志 ~/.cache/deepin/discburn/uburn/xxxx/log

xorriso -dev /dev/sr0 -map /src/ /dest/ -volid 卷标 -close off -commit -eject

关闭 ubuntu 的签名

1757145441540
1757145464484

时区设置:

date -R #查看当期时区 tzselect #设置时区 systemd-timeryncd.service服务的配置文件位于“/etc/systemd”路径下,文件名称为“timesyncd.conf”,可以设置ntp服务器(deepin有 /etc/systemd/timesyncd.conf.d/deepin.conf如果某个Linux发行版和deepin一样具有/etc/systemd/system.d/timesyncd.conf文件夹,那么systemd-timesyncd.service服务将优先选择这个文件夹下的.conf文件作为自己的优先配置文件,而原有的/etc/systemd/timesyncd.conf将作为备用的配置文件,优先级相对较低。 4.3、典型故障——无论怎样修改时区,Linux系统始终都显示零区的格林尼治时间 出现这样的情况一般是因为/usr/share/zoneinfo目录遭到严重破坏造成的,针对这样的故障,最简单粗暴的方式就是将找到一套同版本、正常运行的Linux发行版,将其/usr/share/zoneinfo目录复制出来,对故障Linux系统的相关目录进行替换。

4.4、典型故障——安全软件在启动时强制打开操作系统时间同步功能,后因安全软件服务端时间错误导致时钟错误同步 在信创领域,Linux杀毒软件经常被安装在Linux电脑上,用于病毒查杀和违规外联等功能。而主流的Linux杀毒软件都具有审计功能,审计会产生大量的日志纪录,这些纪录需要有一个时间对应,因此这些杀毒软件的服务端会自带时钟同步,并且会在Linux终端启动完成后强制打开和配置操作系统的时钟同步功能,如果杀毒软件服务端的本地时间时错误的,或者说同一个局域网内有多个存在一定时间差的NTP服务器,将可能会对Linux终端带来时间上的错误。

sudo trace-bpfcc -tUK __alloc_pages_nodemask

systemctl set-property name parameter=value

pactl list short modules pacmd list-modules

设置systemdlog_level: /etc/systemd/daemon.log

查看进程的fd: ls -l /proc/xxx/fd

1、通过以下步骤进行测试: (1)执行 mv /usr/bin/pulseaudio /usr/bin/pulseaudiobak, 重启让pulseaudio无法自启动 (2)命令行 执行 arecord -Dhw:0,1 -d 10 -f cd -t wav test.wav通过alsa进行录音(hw:0,1为指定录音的设备,arecord命令的使用可以参考网上资料) (3) 将生成的test.wav用audacity软件进行播放,查看是否有杂音。 此时依旧有杂音,基本可以确定是送上来的数据流有问题(中间音频驱动和alsa编解码的过程因为编解码过程都比较完善,一般不会出现问题) 2、此问题从现象上看就是将录制的音量提高到50%的时候,通过alsamixer查看此时Internal Mic boost开始有增强效果,导致噪音被放大。此问题在软件方面只能通过修改配置文件将Internal Mic boost设置为ignore来取消增强效果来减缓此问题,要根本解决还需要厂商进行适配。 https://blog.csdn.net/xiongtiancheng/article/details/80577478

SFTP vscode 设置

现在我们本地和远程均有一个文件夹“sftpFolder”,用VsCode打开本地文件夹“sftpFolder”,然后执行 ctrl+shift+p ,搜索 SFTP:Config ,回车后,会生成一个“.vscode/sftp.json”,这个就是配置文件。 同时,如下图左侧会多了一个“远程目录”。 alt text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"name": "al",
"host": "natsuu.705553939.xyz",
"port": 10222,
"protocol": "sftp",
"username": "al",
"privateKeyPath": "~/.ssh/id_rsa",
"remotePath": "/home/al/gitdep/DeepCropMapping/",
"uploadOnSave": true,
"useTempFile": false,
"openSsh": true,
"downloadOnOpen":true,
"ignore": [
"**/.vscode/**",
"**/.DS_Store"
],
"watcher": {
"files": "*",
"autoUpload": true,
"autoDelete": true
}
}

alt text

pip环境的转移和复制

python项目中一般都包含一个 requirements.txt 文件,用于记录所有依赖包及其精确的版本号。以便新环境的部署。

下面为在linux环境下“pip自动生成和安装requirements.txt”的步骤。

激活相应虚拟环境 conda env list # 查看所有的conda虚拟环境 source activate env_name 1 2 生成requirements.txt文件 pip freeze > requirements.txt 1 安装requirements.txt依赖 conda create -n env_name python=3.6 # 创建新的虚拟环境 source activate env_name # 激活新建的虚拟环境 pip install -r requirements.txt

conda环境的转移和复制

conda导出已有环境,环境会被保存在environment.yaml文件中。 conda env export > environment.yaml 1 当我们想再次创建该环境,或根据别人提供的.yaml文件复现环境时,就可以通过下面的命令来复现安装环境了。 conda env create -f environment.yaml 1 注:.yaml文件移植过来的环境只是安装了你原来环境里用conda install等命令直接安装的包,你用pip之类装的东西没有移植过来,需要你重新安装。

tensorboard

1
tensorboard --logdir logs

查看谁执行了某条命令:

重现bug,然后检查日志,sudo journalctl -b0 |grep SYSCALL 能看到有 date 命令调用 syscall 112 号 设置了系统时间。

sudo mv /usr/bin/date /usr/bin/date.real

编写文件 /usr/bin/date

1
2
3
4
5
6
7
8
9

#!/bin/sh
echo >> /tmp/log
pstree -p -A -l >> /tmp/log
echo pid: $$ >> /tmp/log
echo args: $@ >> /tmp/log
logger -f /tmp/log
/usr/bin/date.real "$@"

执行命令 sudo chmod +x /usr/bin/date

然后按照步骤重现bug,然后立即查看 /tmp/log 文件的内容。

比如看到文件底部有 pid: 12345, 然后去本文件上方找到 12345,看它的父进程是什么,我看到了一个父进程 AlarmThread,pid 为 3203。

然后切换到 root 用户,执行命令

1
2
3
4
5
6
7

cd /proc/3202

cat comm

readlink -f exe

看到结果是 AlarmThread 和 /opt/apps/cn.com.topsec.topdesk/files/bin/TDATX0,这就说明了是 cn.com.topsec.topdesk 软件包里的程序调用了 date 命令设置了时间。

quilt配置:

for where in ./ ../ ../../ ../../../ ../../../../ ../../../../../; do if [ -e {where}debian/patches ]; then export QUILT_PATCHES=debian/patches break fi done

wpa_cli:

wpa_cli -i wlan0 scan scan_results nmcli con show nmcli dev wifi connect ASXXX 查看连接情况wpa_cli -i wlan0 scan_results | grep ASXXX

查看电脑支不支持wpa:

gdb wpa 在这个函数里, wpa_supplicant_set_suites

然后p一下这个值。

wpa_s->drv_flags

gdb 他的电脑。

删除已连接的网络:

rm /etc/NetworkManager/systemd-connections/*** systemctl restart network-manager.service

跟踪signal_fd:

sudo trace-bpfcc -tK 'do_send_sig_info(int sig, struct siginfo info, struct task_struct p) "sig=%d,recv_pid=%d,send_pid=%d", sig,p->pid,info->si_pid'

trace-bpfcc指定头文件:加-I

trace-bpfcc: sudo trace-bpfcc -t 'do_filp_open(int dfd, struct filename *pathname) "name=%s", pathname->name' | grep "/dev/rtc"

sudo trace-bpfcc -tK 'btrfs_mount_root(void* f, int flags, char* name) "flags=0x%lx name=%s", flags, name'

sudo trace-bpfcc -t 'group_send_sig_info(int sig, void info, struct task_struct p)"sig=%d, task_pid=%d, task_name=%s",sig, p->pid, p->comm' >> 1.log

sudo dbus-monitor --system interface=org.freedesktop.login1.Manager path=/org/freedesktop/login1

316 trace-bpfcc -t 'generic_file_direct_write(struct kiocb *iocb) "func=0x%lx", iocb->ki_filp->f_mapping->a_ops->direct_IO' -p 28942

sudo trace-bpfcc -t 'group_send_sig_info(int sig, void info, struct task_struct p)"sig=%d, task_pid=%d, task_name=%s",sig, p->pid, p->comm' >> 1.log

mac os微信双开方法

open -n /Applications/WeChat.app/Contents/MacOS/WeChat

查找被隐藏的进程

1
2
sysdig -c topprocs_cpu # 该命令可以输出cpu占用的排行,经测试可以显示出被隐藏的进程
unhide proc # 自助搜索隐藏进程,linux系统中一切皆文件,proc目录下保存的就是所有正在运行程序的进程ID,即PID

以上两个命令需要额外安装,不是系统自带命令

1
2
3
4
#sysdig可用如下命令安装,curl是类似wget的下载命令,这里下载的是一个bash脚本,下载后再通过bash执行安装
curl -s https://s3.amazonaws.com/download.draios.com/stable/install-sysdig | bash
# unhide 可以通过如下命令进行安装
yum install unhide # 中毒的是Centos7系统,所以用的是yum
1
2
3
4
5
6
# 挖矿病毒都会有定时的网络发送信息,所以可用如下命令查看在病毒运行期间,是否存在异常IP地址
netstat -natp
# 知道这些后,我们可以用防火墙firewalld或iptables对这些IP进行封禁
iptables -I INPUT -s IP -j DROP
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="IP" reject'
# 其中IP字段替换为真实的黑客IP

/proc/目录

/proc/pid/cmdline 进程启动命令 /proc/pid/cwd 链接到进程当前工作目录 /proc/pid/environ 进程环境变量列表 /proc/pid/exe 链接到进程的执行命令文件 /proc/pid/fd 包含进程相关的所有的文件描述符 /proc/pid/maps 与进程相关的内存映射信息 /proc/pid/mem 指代进程持有的内存,不可读 /proc/pid/root 链接到进程的根目录 /proc/pid/stat 进程的状态 /proc/pid/statm 进程使用的内存的状态 /proc/pid/status 进程状态信息,比stat/statm更具可读性 /proc/self 链接到当前正在运行的进程

配置ssh免密登录

1
ssh-keygen -t rsa -b 4096

自动把公钥复制到虚拟机

1
2
ssh-copy-id ubuntu@192.168.105.10)

ssh设置别名

vim编辑~/.ssh/config文件

1
2
3
4
Host jhd
HostName 1xx.1xx.1xx.67
User root
IdentitiesOnly yes

SSH config

作者:Pavinberg 链接:https://zhuanlan.zhihu.com/p/716963359 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

SSH config 作用就是可以把 SSH 相关的信息都记录到一个配置文件,可以简化操作、节约时间。

SSH config 有一个系统级的,一个用户级的。一般普通用户只关注用户级的。文件路径为 ~/.ssh/config

基本写法

一般一个服务器写成一小段,形如:

1
2
3
4
5
6
Host Server1
Hostname 172.16.0.1
User zhangsan
Port 22
ServerAliveInterval 180
IdentityFile ~/.ssh/secret_key.pem

这段的含义为有一个服务器:

  1. 我们为它起了个名字叫 Server1
  2. 它的 IP 是 172.16.0.1(也可以填 Hostname)
  3. 我在上面的用户名是 zhangsan
  4. SSH 服务监听端口号为 22(即默认值,也可以不写这一行)
  5. ServerAliveInterval 180 表示在建立连接后,每 180 秒客户端会向服务器发送一个心跳,避免用户长时间没操作连接中断
  6. 最后一行表示使用一个专用的密钥,如果没有专用的密钥则删除该行即可。

登录这台服务器的话,输入:

1
$ ssh Server1

拷贝文件(反过来就是从服务器往本地下载文件):

1
$ scp /path/to/local/file Server1:/path/to/remote/

可以看到,这样的好处有:(1)简洁,不需要记忆 IP 地址、端口号。(2)可以保持连接。

配置免密也相同,输入以下命令并输入密码:

1
$ ssh-copy-id Server1

通配符

如果有一批服务器都是相同的配置,更是可以用通配符统一处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
Host Server*
User zhangsan
Port 22
ServerAliveInterval 180

Host Server1
Hostname 172.16.0.1

Host Server2
Hostname 172.16.0.2

Host Server3
Hostname 172.16.0.3

相信读者已经猜到其中的含义。第一段表示所有名字为 Server 开头的服务器,他们的用户名都是 zhangsan,端口都是 22,同时都有保持连接的心跳。然后下面列了 3 台服务器,我们只需要指定它们的 IP 地址。

多文件管理

如果需要管理非常多的服务器,全写到一个文件里会很乱很难维护,也不方便共享。事实上,~/.ssh/config 中支持引用其它文件。我一般习惯新建一个这样的配置 ~/.ssh/config-cluster-shanghai,在其中编写类似上文的内容。然后在 ~/.ssh/config 的开头加入如下一行即可:

1
Include config-cluster-shanghai

事实上这里也可以用通配符,比如:

1
Include config-*

这样 ~/.ssh/ 目录下的所有 config- 开头的文件都会被引用到。

跳板

很多集群需要跳板机才可登录,我们需要先登录跳板机,再从跳板机登录内部机器。这会引入两个麻烦,一是登录要两次,如果配置 SSH config 还需要在跳板机也配置一份儿;二是拷贝文件十分麻烦,要拷贝两次。

对此可以这样写配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Host Jumper
Hostname 1.2.3.4
User zhangsan

Host Server*
User zhangsan
ProxyJump Jumper
ServerAliveInterval 180

Host Server1
Hostname 172.16.0.1

Host Server2
Hostname 172.16.0.2

第一段为跳板机的登录方式,第二段中新增了一个 ProxyJump 字段,表示所有 Server 开头的服务器,在登录的时候都要从 Jumper 这个服务器跳转一下。这时候我们想登录 172.16.0.1,只需要直接输入:

1
2
$ ssh Server1
$ scp /path/to/local/file Server1:/path/to/remote/

注意一个细节是,这种配置下我们是直接从本地登录内部服务器,所以在配置免密时,是需要把本地的公钥放到内部服务器的。

SCP 服务器间拷贝文件

scp 的基本用法相信大家都会,上文也多次提到。但如果想在两台服务器之间拷贝文件,事实上是可以在本地执行 scp 的:

1
$ scp Server1:/path/to/file Server2:/path/to/file2

这个命令要求 Server1 可以直接访问 Server2。如果不满足这个条件,可以用本机转发,只需要增加一个参数 -3 表示用本地机器当转发机:

1
$ scp -3 Server1:/path/to/file Server2:/path/to/file2

批量操作 PSSH

如果有一组机器要做相同的操作,可以使用 PSSH 工具。

很遗憾的是,这个工具原作者已经不维护了,但 GitHub 上散落着原仓库的副本,同时也有大家新增的逻辑。

安装:

1
$ python3 -m pip install pssh

准备一个 iplist:

1
2
3
$ cat iplist
Server1
Server2

批量操作:

1
2
3
4
5
$ pssh -ih iplist echo Hello
[1] 14:27:50 [SUCCESS] Server1
Hello
[2] 14:27:50 [SUCCESS] Server2
Hello

-i 表示把 stdout 输出,否则只会显示命令执行是否成功。-h 表示 host 的列表,后接 iplist 这个文件名。pssh 默认会在命令超时后强制结束, 不希望超时,可以加一个 -t 0 表示超时时间为 0 秒,0 表示无穷大。

PSSH 也可以把每个服务器的结果输出到不同的目录、可以单独指定 Host 等等,大家可以参考 manpage:

1
$ man pssh

禁用root ssh登录

1
2
3
4
# 一方面是ssh远程登录没有禁用root的远程登录,此时需要在/etc/ssh/sshd_config中添加
PermitRootLogin no
# 保存后,对ssh服务进行重启
systemctl restart sshd

有条件的可以将服务器中ssh登录都设置为rsa免密登录,这样可以避免绝大多数问题

一般是通过尝试用户名和密码对服务器进行攻击,但是每台服务器中用户名都不同,所以很难攻击,但是root名称一般不会变,所以一般是先攻击这个名称。

vscode latex使用教程

1. 下载Visual Studio Code

Visual Studio Code (VScode)是一款集成开发环境(Integrated Development Environment,简称IDE) 。通过安装第三方扩展 LaTeX WorkshopVScode能够用于latex源文件管理与编辑。

打开下载好的 VSCode ,鼠标点击左边栏中的扩展/Extensions,在搜索框中输入workshop,安装James Yu的 LaTeX Workshop (点击Install即可安装)。安装好之后,打开latex源文件夹,即可看到红色框中的TEX标识,如下图所示。

1747647963604

通过点击左边栏管理器/Explorer,可以显示/隐藏文件管理器;在文件管理器中.tex文件是latex源文件,.bib文件是文献存储文件,如下图所示。

1747648016131

2. 下载MacTex

https://tug.org/mactex/mactex-download.html

MacTex是排版引擎Latex针对macOS系统的发型版,包含与TEX系统相关的各种程序、编辑与查看工具、常用宏包及文档、常用字体及多国语言支持。MacTeX相当于远处的水源,VScode则相当于水管,我们需要将水管与水源进行连接,才能从水管获取水源。因此,下载并安装MacTeX后,需要对VScode进行配置——水管与水源连接,才能编译.tex文件生成pdf文稿——获取水源。

1747721688405

选择Settings,在弹出的界面右上角,点击Open Settings图标,即红色箭头所指向,如下图所示。

3. VScode的latex配置

(1)打开配置文件

鼠标点击左下角⚙️(即红色框中齿轮图标),弹出菜单,如下图所示。

1747721735775

此时,settings.json文件将被打开,如下图所示。

(2)配置文件内容

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
{
// "latex.linter.enabled": false,
"latex-workshop.latex.autoBuild.run": "never",
"latex-workshop.showContextMenu": true,
"latex-workshop.intellisense.package.enabled": true,
"latex-workshop.message.error.show": false,
"latex-workshop.message.warning.show": false,
"latex-workshop.latex.tools": [
{
"name": "xelatex",
"command": "xelatex",
"args": [
"-synctex=1",
"-interaction=nonstopmode",
"-file-line-error",
"%DOCFILE%"
]
},
{
"name": "pdflatex",
"command": "pdflatex",
"args": [
"-synctex=1",
"-interaction=nonstopmode",
"-file-line-error",
"%DOCFILE%"
]
},
{
"name": "latexmk",
"command": "latexmk",
"args": [
"-synctex=1",
"-interaction=nonstopmode",
"-file-line-error",
"-pdf",
"-outdir=%OUTDIR%",
"%DOCFILE%"
]
},
{
"name": "bibtex",
"command": "bibtex",
"args": [
"%DOCFILE%"
]
}
],
"latex-workshop.latex.recipes": [
{
"name": "XeLaTeX",
"tools": [
"xelatex"
]
},
{
"name": "PDFLaTeX",
"tools": [
"pdflatex"
]
},
{
"name": "BibTeX",
"tools": [
"bibtex"
]
},
{
"name": "LaTeXmk",
"tools": [
"latexmk"
]
},
{
"name": "xelatex -> bibtex -> xelatex*2",
"tools": [
"xelatex",
"bibtex",
"xelatex",
"xelatex"
]
},
{
"name": "pdflatex -> bibtex -> pdflatex*2",
"tools": [
"pdflatex",
"bibtex",
"pdflatex",
"pdflatex"
]
},
],
"latex-workshop.latex.clean.fileTypes": [
"*.aux",
"*.bbl",
"*.blg",
"*.idx",
"*.ind",
"*.lof",
"*.lot",
"*.out",
"*.toc",
"*.acn",
"*.acr",
"*.alg",
"*.glg",
"*.glo",
"*.gls",
"*.ist",
"*.fls",
"*.log",
"*.fdb_latexmk"
],
"latex-workshop.latex.autoClean.run": "onFailed",
"latex-workshop.latex.recipe.default": "lastUsed",
"latex-workshop.view.pdf.internal.synctex.keybinding": "double-click",
"editor.unicodeHighlight.allowedLocales": {
"zh-hans": true,
"zh-hant": true
},
"[latex]": {

"editor.defaultFormatter": "James-Yu.latex-workshop"
}
}

(3)设置编译快捷键

鼠标点击左下角齿轮,选择Keyboard Shortcuts,在弹出的界面搜索栏中输入recipe,如下图所示。双击检索到的快捷键修改栏,同时按下option+command+R,编译快捷键则设置完成。

1747727977070
1748420721459
1748420587790

.aux

1748421203103

.blg

1748421685079

pnpm和npm以及yarn

pnpm和npm的区别是什么?

pnpm 和 npm 有以下一些区别:

  • 安装方式和存储结构:npm 会将依赖包安装到项目中的 node_modules 文件夹中,它为每个依赖包创建一个软链接指向该文件夹,可能存在大量软链接导致存储占用较大;而 pnpm 将依赖包安装到.pnpm 文件夹中,使用硬链接将依赖包链接到项目中,这样可以减少存储占用和安装时间。
  • 安装速度:npm 每个项目都需要安装和下载依赖包,速度相对较慢;pnpm 使用硬链接,相同的依赖包可以被多个项目共享,能够更快地安装依赖包,在大多数场景下其安装速度比 npm 快 2-3 倍。
  • 依赖管理:npm 采用嵌套或扁平化的依赖管理方式,可能存在幽灵依赖(未在 package.json 中定义但项目中可引用的依赖)和依赖分身(同一依赖的不同版本被重复安装)等问题;pnpm 引入了内容寻址存储策略,只有直接依赖会平铺在 node_modules 下,子依赖不会被提升,解决了幽灵依赖问题,且相同的依赖只会在全局 store 中安装一次,避免了依赖分身,其创建的 node_modules 是非扁平化结构。
  • 兼容性:npm 是默认的 JavaScript 包管理器,广泛使用于大多数项目;pnpm 是新兴的包管理工具,可以与 npm 兼容,并与多个 npm 生态系统工具协同工作,但可能存在与一些 npm 插件和工具的不兼容问题。
  • 磁盘空间利用:使用 npm/yarn 时,如果多个项目都依赖同一个包,该包可能会被多次安装,磁盘中存在多个相同代码的副本;而 pnpm 内部使用基于内容寻址的文件系统,不会重复安装同一个包,对于同一个包的不同版本,也会极大程度地复用之前版本的代码,能更高效地利用磁盘空间。

pnpm

pnpm 是一个快速、节省磁盘空间的包管理工具。

它具有以下一些特点和优势:

  • 高效的磁盘空间利用:通过硬链接和内容寻址存储,避免了多个项目中重复安装相同依赖的冗余存储。
  • 快速的安装速度:得益于其优化的依赖处理机制。

例如,当您有多个使用相同依赖版本的项目时,pnpm 可以共享这些依赖,而不是每个项目都单独安装一份。

安装pnpm

npm install -g pnpm

pnpm安装依赖

1
pnpm install

执行 pnpm install时,它会读取项目根目录下的 package.json文件来配置依赖包列表。

package.json是一个 JSON 格式的文件,用于描述项目的相关信息,包括项目名称、版本、描述以及项目所依赖的其他包及其版本等。

pnpm使用一个称为 pnpm-lock.yaml的文件来记录项目的依赖关系和版本,类似于 npmpackage-lock.json文件,它可以确保项目在不同环境中得到相同的依赖版本。

当执行 pnpm install命令时,如果存在 pnpm-lock.yaml文件,并且其中记录的依赖版本与 package.json中指定的兼容,pnpm将根据 pnpm-lock.yaml安装确切的依赖版本。如果 pnpm-lock.yaml不存在或与 package.json不兼容,pnpm将重新生成 pnpm-lock.yaml文件,并根据 package.json中的依赖信息安装相应的包。

在 macOS 系统中,执行 pnpm install 后,依赖包的默认安装位置是用户主目录下的 .pnpm 文件夹。

不过,你可以通过配置来修改依赖包的安装路径。例如,使用以下命令可以设置全局安装包的 bin 文件的目标目录、包元数据缓存的位置等:

1
2
3
4
pnpm config set global-bin-dir "你期望的全局 bin 文件目录"
pnpm config set cache-dir "你期望的缓存目录"
pnpm config set state-dir "你期望的 pnpm-state.json 文件目录"
pnpm config set global-dir "你期望的全局依赖目录"

需注意,修改安装路径后,需要重新打开终端以使更改生效。

另外,pnpm 还会创建一个 pnpm-lock.yaml 文件来记录项目的依赖关系和版本。这个文件类似于 npmpackage-lock.json,用于确保在不同环境中能够安装相同版本的依赖包。

如果你在安装过程中遇到问题或需要更详细的信息,可以参考pnpm 的官方文档

或者,你也可以参考如下步骤进行配置(假设你想将相关文件安装到“/users/xxx/my_pnpm”目录下):

  1. 终端运行 pnpmsetup 命令;
  2. 找到用户的主目录下的 .zshrc 文件(注意:该文件默认隐藏,可以用 cmd+shift+. 查看),初始内容可能类似于:
1
2
3
4
5
6
7
8
export pnpm_home="/users/xxx/library/pnpm"
case ":$path:" in
*):$pnpm_home:"*
);;
*)
export path="$pnpm_home/bin:$path"
;;
esac

在原来的基础上文件底部加上:

1
export path="/users/xxx/my_pnpm/bin:$path"

(请将“xxx”替换为自己的用户主目录)

  1. 终端运行 source ~/.zshrc 命令,就可以使用 pnpm 安装的全局依赖啦。

pnpm tauri dev

“pnpm tauri dev”的作用是启动 Tauri 应用的开发模式。

Tauri 是一个基于 Rust 实现的跨端桌面应用开发工具,应用的界面可以使用前端技术进行开发,如原生的 HTML、CSS、JS,或者使用 React、Vue 等前端框架。

执行“pnpm tauri dev”命令后,它首先会执行“pnpm dev”,启动一个 Vite 脚手架来运行前端页面。经过一段时间的编译(大概 3 分钟左右),Rust 后端程序也会启动,随后会自动弹出 Tauri 应用

在实际使用中,为了更方便开发,可以在“package.json”的“scripts”里加入一行 "tauri:dev":"tauri dev",之后就可以使用 pnpm run tauri:dev来启动开发窗口。

该命令中的“tauri dev”具体做的事情可能包括启动 Tauri 应用的后端服务、监测项目中的文件变化并自动重新编译和加载等,以便开发者能够实时看到代码修改后的效果,方便进行前端界面和 Rust 后端逻辑的开发调试工作。

b

如果需要打包后进行调试,请在以下命令后面加上 --debug

“pnpm tauri build”命令用于构建 Tauri 应用。它会将你的前端资源(如 HTML、CSS、JavaScript 等)与 Rust 代码一起嵌入到一个单独的二进制文件中。

第一次运行该命令时,需要一些时间来收集 Rust 包并构建所有内容,但在随后的运行中,它只需重新构建应用程序代码,速度会快很多。

二进制文件本身将位于 src-tauri/target/release/(应用程序名称),而安装程序将位于 src-tauri/target/release/bundle/

然而,在新创建的项目中第一次打包时,可能会报“error: you must change the bundle identifier in tauri.conf.json > tauri > bundle > identifier. the default value com.tauri.dev is not allowed as it must be unique across applications.”错误。这是因为第一次打包需要修改 src-tauri/tauri.conf.json 文件里的配置项“identifier”,将其值由“com.tauri.dev”改成“com.tauri.build”,然后再运行打包命令。

如果你需要为特定的操作系统或架构进行构建,例如 32 位或 arm64,可以添加相应的 --target 标志。例如,要编译 32 位 Windows 应用,可以使用 tauri build --target i686-pc-windows-msvc;要编译 arm64 应用,需要先安装额外的构建工具(如在 Visual Studio Installer 中安装“C++ arm64 构建工具”),然后使用 rustup target add aarch64-pc-windows-msvc 添加目标,并执行 tauri build --target aarch64-pc-windows-msvc --bundle nsis(注意,只有 nsis 目标支持 arm64 目标,且安装程序本身仍是 x86 的,将在 arm 机器上通过模拟运行,而应用本身是原生的 arm64 二进制)。

对于其他操作系统的详细构建信息和注意事项,可以参考 Tauri 的官方文档(https://tauri.app/ )以获取更准确和最新的指导。

pnpm命令:

1
pnpm store path #查看 pnpm 整体的缓存路径
1722067669618
1
pnpm store prune #清理 pnpm 的缓存

tauri

tauri init 命令(或者说npm run tauri init)将生成 src-tauri 文件夹。 传统上,Tauri 应用会将其核心相关的文件放置于此文件夹中。 让我们快速过一下此文件夹中的内容:

  • Cargo.toml Cargo 的清单文件。 您可以声明您应用所依赖的 Rust 包和应用的元数据等等。 要查看所有可修改的值,请参阅 Cargo 清单格式

  • tauri.conf.json 此文件可让您自定义 Tauri 应用的各方各面,包括应用名称到允许的 API 列表。 请参阅 Tauri 的 API 配置来深入了解每个支持的选项。

  • src/main.rs 这是你的 Rust 程序的入口,也是我们启动 Tauri 的地方。 您可以发现它由两个部分组成: src/main.rs

    1
    2
    3
    4
    5
    6
    7
    #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

    fnmain(){
    tauri::Builder::default()
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
    }

    The line beginning with the cfg! 宏所开始的一行仅有一个目的:关闭构建好的应用在 Windows 上运行时一般会出现的控制台窗口。 若您是 Windows 用户,您可以试试去掉这行看看会发生什么。 main 函数是您程序的入口点,也是运行时调用的第一个函数。

  • icons 您可能想为自己的应用整一个漂亮的图标! 为了帮助您快速开发,我们为您提供了一套默认图标。 您应该在发布前把这些图标换成您自己的图标。 您可以在 Tauri 的图标功能指南中了解有关多种图标格式的信息。

就是这样! 现在您可以在您的终端中运行接下来的命令来开始您的应用程序的开发构建:

1
npm run tauri dev

应用预览 alt text

npm

npmNode Package Manager ,是 Node.js 的默认包管理工具。

它允许开发者轻松地安装、共享、分发和管理代码模块(包)。

npm 拥有庞大的包仓库,涵盖了各种各样的功能和库。

例如,您可以使用 npm install express 命令来安装 Express 这个用于创建 Web 应用的框架。

许多前端和后端的项目都依赖 npm 来管理项目所需的各种依赖包,以提高开发效率和代码的可维护性。

如何解决npm ERR! request to https://registry.npm.taobao.org/pnpm failed, reason: certificate has expired

原因:需注意原淘宝 npm 镜像站的域名(https://registry.npm.taobao.org)已不再使用,于 2022 年 05 月 31 日零时起停止服务

解决方法:

1
npm config set registry https://registry.npmmirror.com

npm cache clean --force清理的是哪里的缓存?

npm cache clean --force 清理的是 npm 本地缓存。

在大多数系统中,npm 缓存默认存储在以下路径:

  • Windows:%appdata%-cache
  • macOS 和 Linux:~/.npm

使用该命令会强制删除所有已有的缓存文件,包括已下载的依赖包和相关数据等。

需要注意的是,执行此操作后,npm 将需要重新下载所有依赖,这可能会增加安装时间。因此,在执行该命令之前,需确保了解其后果,并且确实需要清理缓存。例如,当怀疑缓存中的包损坏或过时,导致安装问题;或者需要确保安装的是包的最新版本;又或者缓存占用了大量磁盘空间时,可以考虑使用该命令。

另外,如果只是想查看 npm 缓存的位置,可以使用命令 npm cache dir或者 npm config list --json。对于 yarn,可以使用命令 yarn cache dir查看缓存位置。

npm fund

npm fund是 npm 包管理工具中的一个命令。它的作用是列出项目中正在寻找资金的依赖包及其对应的捐赠平台和链接。

当运行 npm install安装依赖包后,使用 npm fund命令可以查看相关信息。在 package.json文件中,维护者可以添加一个“funding”字段,用于指定捐赠平台的元数据和链接,如 patreonopencollectivegithub sponsorslicensezero或其他支付网站的 url

运行 npm fund后,它会在用户的默认浏览器中打开指定的捐赠服务链接,以便进行信用卡捐赠等操作。

例如,运行 npm fund可能会得到类似以下的输出:

1
2
3
4
5
6
7
8
package-name@version
+--dependency1@version1
| +--type:opencollective
| `--url:https://opencollective.com/dependency1
+--dependency2@version2
| +--type:github sponsors
| `--url:https://github.com/sponsors/dependency2
...

这表示 package-name这个包及其依赖项 dependency1dependency2正在寻找资金支持,dependency1的捐赠平台是 opencollective,链接为 https://opencollective.com/dependency1dependency2的捐赠平台是 github sponsors,链接为 https://github.com/sponsors/dependency2

通过 npm fund命令,用户可以了解到所使用的开源包是否接受捐赠以及如何进行捐赠,以支持开源项目的发展。

需要注意的是,并非所有的包都会在 package.json中设置“funding”字段,只有那些明确声明了的包才会在 npm fund命令的输出中显示相关的捐赠信息。同时,捐赠是完全自愿的行为,用户可以根据自己的意愿和能力决定是否进行捐赠。

npm list

检查某个模块是否安装成功

1
npm list hexo-filter-mathjax

npm项目 package.json

scripts字段

scripts 字段是一个对象,用于定义各种可执行的脚本命令。

这些脚本命令可以通过在终端中运行 npm run <script_name> 来执行。

例如,如果 scripts 字段中有以下定义:

1
2
3
4
"scripts": {
"start": "node server.js",
"build": "webpack --config webpack.config.js"
}

那么,您可以在终端中使用 npm run start 来启动 node server.js ,使用 npm run build 来执行 webpack --config webpack.config.js

scripts 字段的主要用途包括:

  1. 定义项目的开发流程:比如启动开发服务器、进行代码编译、运行测试等。
  2. 简化复杂的命令:将一系列复杂的命令组合成一个简单易记的脚本名称。
  3. 保持命令的一致性:无论在哪个开发环境中,只要项目的 package.json 相同,运行 npm run <script_name> 的效果就是一致的。
  4. 方便团队协作:团队成员可以通过相同的脚本命令进行项目的开发、构建和部署等操作。

总之,scripts 字段为项目提供了一种方便、可复用和标准化的方式来执行各种命令,提高了开发效率和项目的可维护性。

yarn

Yarn 是一个 JavaScript 包管理工具。

它的主要目的是为了提供更可靠、高效和安全的依赖管理体验。

Yarn 具有以下一些特点和优势:

1. 确定性:通过创建 yarn.lock 文件来锁定每个依赖项的版本,确保在不同的环境中安装的依赖版本一致,从而提高项目的可重复性。

2. 并行安装:能够同时下载多个依赖包,大大提高了安装速度。

3. 离线模式:一旦依赖被下载过,再次安装时,如果缓存中存在,就可以离线安装,无需重新从网络获取。

4. 安全检查:在安装依赖时,会对依赖包的完整性进行检查,确保其没有被篡改。

例如,当您在一个大型的前端项目中,使用 Yarn 可以快速、稳定地安装各种前端库和框架,如 ReactVue 等。同时,团队协作时,通过 yarn.lock 文件可以保证每个成员的开发环境中依赖版本的一致性。

webview

webview是什么?

WebView 是一个能在移动应用或桌面应用中嵌入网页内容的组件。

它允许开发者在原生应用的界面中直接显示网页,实现了原生应用和网页内容的融合。

WebView 的主要作用包括:

  1. 混合开发:通过在原生应用中嵌入 WebView,开发者可以将部分界面或功能以网页的形式实现,这样既能利用原生应用的性能和功能,又能借助网页开发的便捷性和快速迭代能力。
  2. 展示动态内容:比如新闻应用可以使用 WebView 来显示从服务器获取的实时新闻页面。
  3. 跨平台:在不同的平台(如 Android、iOS 等)上,WebView 提供了一种相对统一的方式来展示网页内容,有助于减少跨平台开发的差异。
  4. 节省开发成本:对于一些相对简单且经常变化的功能,使用 WebView 加载网页可以避免为每个平台单独开发原生代码。

例如,一些电商应用可能会在商品详情页面使用 WebView 来展示从服务器动态获取的商品介绍和评论,而应用的框架和核心功能则由原生代码实现。

不同的平台对 WebView 的实现和特性可能会有所不同,但总体功能和用途是相似的。在开发过程中,需要注意 WebView 的性能优化、缓存管理、与原生代码的交互等方面的问题。

Rust

cargo

Cargo 是 Rust 编程语言的包管理和构建工具。

它主要用于以下几个方面:

  1. 依赖管理:可以方便地声明项目所依赖的其他 Rust 库,并自动下载和管理它们的版本。
  2. 项目构建:处理项目的编译、链接等构建过程,支持多种编译选项和配置。
  3. 测试运行:能够轻松运行项目中的测试用例。
  4. 发布管理:帮助开发者将项目发布到 Rust 的包仓库(crates.io)。

使用 Cargo 可以极大地提高 Rust 项目开发的效率和便利性。

例如,当您创建一个新的 Rust 项目时,只需要运行 cargo new project_name 命令,Cargo 就会自动为您创建项目的基本结构,包括 Cargo.toml 配置文件(用于管理依赖和项目设置)和 src 目录(存放源代码)。

如果您想在项目中添加一个依赖库,只需要在 Cargo.toml 文件中添加相应的依赖信息,然后运行 cargo buildCargo 就会自动下载并集成该依赖库。

总之,Cargo 是 Rust 生态系统中不可或缺的一部分,为 Rust 开发者提供了强大而便捷的工具支持。

Cargo 功能

Cargo 除了创建工程以外还具备构建(build)工程、运行(run)工程等一系列功能,构建和运行分别对应以下命令:

  • cargo new <project-name>:创建一个新的 Rust 项目。
  • cargo build:编译当前项目。
  • cargo run:编译并运行当前项目。
  • cargo check:检查当前项目的语法和类型错误。
  • cargo test:运行当前项目的单元测试。
  • cargo update:更新 Cargo.toml 中指定的依赖项到最新版本。
  • cargo --help:查看 Cargo 的帮助信息。
  • cargo publish:将 Rust 项目发布到 crates.io。
  • cargo clean:清理构建过程中生成的临时文件和目录。
  • cargo clippy: 类似eslint,lint工具检查代码可以优化的地方
  • cargo fmt: 类似go fmt,代码格式化
  • cargo tree: 查看第三方库的版本和依赖关系
  • cargo bench: 运行benchmark(基准测试,性能测试)
  • cargo udeps(第三方): 检查项目中未使用的依赖

另外 cargo build/run --release 使用 release 编译会比默认的 debug 编译性能提升 10 倍以上,但是 release 缺点是编译速度较慢,而且不会显示 panic backtrace 的具体行号

创建一个最简单的rust项目

1
2
3
4
cargo new greeting 
cd ./greeting
cargo build
cargo run

编译rust代码

1
rustc runoob.rs   # 编译 runoob.rs 文件

编译后会生成 runoob 可执行文件

Rust 输出文字的方式主要有两种:println!()print!()

1
2
3
println!("a is {}, a again is {}", a, a);
println!("a is {0}, a again is {0}", a); //在 {} 之间可以放一个数字,它将把之后的可变参数当作一个数组来访问,下标从 0 开始。
//如果要输出 { 或 } 怎么办呢?格式字符串中通过 {{ 和 }} 分别转义代表 { 和 }。但是其他常用转义字符与 C 语言里的转义字符一样,都是反斜杠开头的形式。

在 VSCode 中配置 Rust 工程

Cargo 是一个不错的构建工具,如果使 VSCode 与它相配合那么 VSCode 将会是一个十分便捷的开发环境。

在上一章中我们建立了 greeting 工程,现在我们用 VSCode 打开 greeting 文件夹( 注意不是 runoob-greeting )。

打开 greeting 之后,在里面新建一个新的文件夹 .vscode (注意 vscode 前面的点,如果有这个文件夹就不需要新建了)。在新建的 .vscode 文件夹里新建两个文件 tasks.json 和 launch.json,文件内容如下:

tasks.json

1
2
3
4
5
6
7
8
9
10
11
{ 
    "version":"2.0.0",
    "tasks":[
        {
            "label":"build",
            "type":"shell",
            "command":"cargo",
            "args":["build"]
        }
    ]
}

launch.json(mac os)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"version": "0.2.0",
"configurations": [
{
"name": "(lldb) 启动",
"type": "cppdbg",
"preLaunchTask": "build",
"request": "launch",
"program": "${workspaceFolder}/target/debug/${workspaceFolderBasename}",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "lldb"
}
]
}

launch.json(linux)

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug",
"type": "gdb",
"preLaunchTask": "build",
"request": "launch",
"target": "${workspaceFolder}/target/debug/${workspaceFolderBasename}",
"cwd": "${workspaceFolder}"
}
]
}

然后点击 VSCode 左栏的 "运行"。

如果你使用的是 MSVC 选择 "(Windows) 启动"。

如果使用的是 MinGW 且安装了 GDB 选择"(gdb)启动",gdb 启动前请注意填写 launch.json 中的 "miDebuggerPath"。 alt text

程序就会开始调试运行了。运行输出将出现在"调试控制台"中: alt text

rust声明变量:

1
let a = 123;

注意:

Rust 语言不允许精度有损失的自动数据类型转换

在语言层面尽量少的让变量的值可以改变。所以 a 的值不可变。但这不意味着 a 不是"变量"(英文中的 variable),官方文档称 a 这种变量为"不可变变量"。

如果我们编写的程序的一部分在假设值永远不会改变的情况下运行,而我们代码的另一部分在改变该值,那么代码的第一部分可能就不会按照设计的意图去运转。由于这种原因造成的错误很难在事后找到。这是 Rust 语言设计这种机制的原因。

当然,使变量变得"可变"(mutable)只需一个 mut 关键字。

1
2
let mut a =123;
a =456;

变量的值可以"重新绑定",但在"重新绑定"以前不能私自被改变,这样可以确保在每一次"绑定"之后的区域里编译器可以充分的推理程序逻辑

1
2
let a = 123;   // 可以编译,但可能有警告,因为该变量没有被使用
let a = 456;

Rust函数:

1
2
3
4
5
6
7
8
9
fn <函数名> ( <参数> ) <函数体>
fn main() {
another_function(5, 6);
}

fn another_function(x: i32, y: i32) {
println!("x 的值为 : {}", x);
println!("y 的值为 : {}", y);
}

Rust 中可以在一个用 {} 包括的块里编写一个较为复杂的表达式:

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let x = 5;

let y = {
let x = 3;
x + 1
};

println!("x 的值为 : {}", x);
println!("y 的值为 : {}", y);
}

注意:x + 1 之后没有分号,否则它将变成一条语句!

这种表达式块是一个合法的函数体。而且在 Rust 中,函数定义可以嵌套:

实例

1
2
3
4
5
6
fn main() {
fn five() -> i32 {
5
}
println!("five() 的值为: {}", five());
}

Rust 函数声明返回值类型的方式:在参数声明之后用 -> 来声明函数返回值的类型(不是 : )。

在函数体中,随时都可以以 return 关键字结束函数运行并返回一个类型合适的值。这也是最接近大多数开发者经验的做法:

1
2
3
fn add(a: i32, b: i32) -> i32 {
return a + b;
}

但是 Rust 不支持自动返回值类型判断!如果没有明确声明函数返回值的类型,函数将被认为是"纯过程",不允许产生返回值,return 后面不能有返回值表达式。这样做的目的是为了让公开的函数能够形成可见的公报。

注意: 函数体表达式并不能等同于函数体,它不能使用 return** 关键字。**

Rust 条件语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() { 
let a = 12;
let b;
if a > 0 {
b = 1;
}
else if a < 0 {
b = -1;
}
else {
b = 0;
}
println!("b is {}", b);
}

if <condition> { block 1 } else { block 2 }

示例:

1
2
3
4
5
fn main() { 
let a = 3;
let number = if a > 0 { 1 } else { -1 };
println!("number 为 {}", number);
}

Rust 循环

rust while循环
1
2
3
4
5
6
7
8
fn main() {
let mut number = 1;
while number != 4 {
println!("{}", number);
number += 1;
}
println!("EXIT");
}

在 C 语言中 for 循环使用for (i = 0; i < 10; i++)三元语句控制循环,但是 Rust 中没有这种用法,需要用 while 循环来代替:

1
2
3
4
5
let mut i = 0; 
while i < 10 {
// 循环体
i += 1;
}
rust for循环
1
2
3
4
5
6
fn main() {
let a = [10, 20, 30, 40, 50];
for i in a.iter() {
println!("值为 : {}", i);
}
}
rust loop 循环(相当于while(true))
1
2
3
4
5
6
7
8
9
10
11
12
fn main() { 
let s = ['R', 'U', 'N', 'O', 'O', 'B'];
let mut i = 0;
loop {
let ch = s[i];
if ch == 'O' {
break;
}
println!("\'{}\'", ch);
i += 1;
}
}

loop 循环可以通过 break 关键字类似于 return 一样使整个循环退出并给予外部一个返回值。这是一个十分巧妙的设计,因为 loop 这样的循环常被用来当作查找工具使用,如果找到了某个东西当然要将这个结果交出去:

1
2
3
4
5
6
7
8
9
10
11
12
fn main() { 
let s = ['R', 'U', 'N', 'O', 'O', 'B'];
let mut i = 0;
let location = loop {
let ch = s[i];
if ch == 'O' {
break i;
}
i += 1;
};
println!(" \'O\' 的索引为 {}", location);
}

运行结果:

1
'O' 的索引为 3

Rust 迭代器

使用 iter() 方法创建借用迭代器:

1
2
let vec = vec![1,2,3,4,5];
let iter = vec.iter();

使用 iter_mut() 方法创建可变借用迭代器:

1
2
let mut vec = vec![1,2,3,4,5];
let iter_mut = vec.iter_mut();

使用 into_iter() 方法创建获取所有权的迭代器:

1
2
let vec = vec![1,2,3,4,5];
let into_iter = vec.into_iter();

Rust 的迭代器提供了丰富的方法来处理集合中的元素,其中一些常见的方法包括:

  • map():对每个元素应用给定的转换函数。
  • filter():根据给定的条件过滤集合中的元素。
  • fold():对集合中的元素进行累积处理。
  • skip():跳过指定数量的元素。
  • take():获取指定数量的元素。
  • enumerate():为每个元素提供索引。
  • ......

使用 map() 方法对每个元素进行转换:

1
2
let vec = vec![1,2,3,4,5];
let squared_vec:Vec<i32>= vec.iter().map(|x| x * x).collect();

使用 filter() 方法根据条件过滤元素:

1
2
let vec = vec![1,2,3,4,5];
let filtered_vec:Vec<i32>= vec.into_iter().filter(|&x| x %2==0).collect();
使用 for 循环遍历迭代器

Rust 提供了 for 循环语法来遍历迭代器中的元素,是一种更加简洁和直观的遍历方式。

Rust 的 for 循环底层实际上是使用迭代器的。

1
2
3
4
let vec = vec![1,2,3,4,5];
for&num in vec.iter(){
println!("{}", num);
}

在这个循环中,vec.iter() 返回一个迭代器,for 循环遍历这个迭代器,并将每个元素赋值给 num 变量,然后执行循环体中的代码。

消费迭代器

使用迭代器直到它被完全消耗。

实例

1
2
3
4
5
let arr = vec![1, 2, 3];
let mut iter = arr.into_iter();
while let Some(val) = iter.next() {
println!("{}", val);
}

Rust 闭包

Rust 中的闭包是一种匿名函数,它们可以捕获并存储其环境中的变量。

闭包允许在其定义的作用域之外访问变量,并且可以在需要时将其移动或借用给闭包。

闭包在 Rust 中被广泛应用于函数式编程、并发编程和事件驱动编程等领域。

闭包在 Rust 中非常有用,因为它们提供了一种简洁的方式来编写和使用函数。

以下是 Rust 闭包的一些关键特性和用法:

闭包的语法声明:

1
|参数...|{表达式}

参数可以有类型注解,也可以省略,Rust 编译器会根据上下文推断它们。

1
let add_one =|x: i32| x +1;

闭包的参数和返回值: 闭包可以有零个或多个参数,并且可以返回一个值。

1
let calculate =|a, b, c| a * b + c;

闭包的调用: 闭包可以像函数一样被调用。

1
let result = calculate(1,2,3);
使用实例
匿名函数

闭包在 Rust 中类似于匿名函数,可以在代码中以 {} 语法块的形式定义,使用 || 符号来表示参数列表,实例如下:

1
2
let add =|a, b| a + b;
println!("{}", add(2,3));// 输出: 5

在这个示例中,add 是一个闭包,接受两个参数 a 和 b,返回它们的和。

捕获外部变量

闭包可以捕获周围环境中的变量,这意味着它可以访问定义闭包时所在作用域中的变量。例如:

1
2
3
let x =5;
let square =|num| num * x;
println!("{}", square(3));// 输出: 15

以上代码中,闭包 square 捕获了外部变量 x,并在闭包体中使用了它。

移动与借用

闭包可以通过 move 关键字获取外部变量的所有权,或者通过借用的方式获取外部变量的引用。例如:

借用变量: 默认情况下,闭包会借用它捕获的环境中的变量,这意味着闭包可以使用这些变量,但不能改变它们的所有权。这种情况下,闭包和外部作用域都可以使用这些变量。例如:

实例

1
2
3
4
let x = 10;
let add_x = |y| x + y;
println!("{}", add_x(5)); // 输出 15
println!("{}", x); // 仍然可以使用 x

获取所有权: 通过在闭包前添加 move 关键字,闭包会获取它捕获的环境变量的所有权。这意味着这些变量的所有权会从外部作用域转移到闭包内部,外部作用域将无法再使用这些变量。例如:

1
2
3
4
let s = String::from("hello");
let print_s = move || println!("{}", s);
print_s(); // 输出 "hello"
// println!("{}", s); // 这行代码将会报错,因为 s 的所有权已经被转移给了闭包

Rust 所有权

计算机程序必须在运行时管理它们所使用的内存资源。

大多数的编程语言都有管理内存的功能:

C/C++ 这样的语言主要通过手动方式管理内存,开发者需要手动的申请和释放内存资源。但为了提高开发效率,只要不影响程序功能的实现,许多开发者没有及时释放内存的习惯。所以手动管理内存的方式常常造成资源浪费。

Java 语言编写的程序在虚拟机(JVM)中运行,JVM 具备自动回收内存资源的功能。但这种方式常常会降低运行时效率,所以 JVM 会尽可能少的回收资源,这样也会使程序占用较大的内存资源。

所有权对大多数开发者而言是一个新颖的概念,它是 Rust 语言为高效使用内存而设计的语法机制。所有权概念是为了让 Rust 在编译阶段更有效地分析内存资源的有用性以实现内存管理而诞生的概念。

所有权规则

所有权有以下三条规则:

  • Rust 中的每个值都有一个变量,称为其所有者。
  • 一次只能有一个所有者。
  • 当所有者不在程序运行范围时,该值将被删除。

这三条规则是所有权概念的基础。

接下来将介绍与所有权概念有关的概念。

Rust 中没有调用 free 函数来释放字符串 s 的资源(我知道这样在 C 语言中是不正确的写法,因为 "runoob" 不在堆中,这里假设它在)。Rust 之所以没有明示释放的步骤是因为在变量范围结束的时候,Rust 编译器自动添加了调用释放资源函数的步骤。

这种机制看似很简单了:它不过是帮助程序员在适当的地方添加了一个释放资源的函数调用而已。但这种简单的机制可以有效地解决一个史上最令程序员头疼的编程问题。

变量与数据交互的方式

变量与数据交互方式主要有移动(Move)和克隆(Clone)两种:

移动

多个变量可以在 Rust 中以不同的方式与相同的数据交互:

1
2
let x =5;
let y = x;

这个程序将值 5 绑定到变量 x,然后将 x 的值复制并赋值给变量 y。现在栈中将有两个值 5。此情况中的数据是"基本数据"类型的数据,不需要存储到堆中,仅在栈中的数据的"移动"方式是直接复制,这不会花费更长的时间或更多的存储空间。"基本数据"类型有这些:

  • 所有整数类型,例如 i32 、 u32 、 i64 等。
  • 布尔类型 bool,值为 true 或 false 。
  • 所有浮点类型,f32 和 f64。
  • 字符类型 char。
  • 仅包含以上类型数据的元组(Tuples)。

但如果发生交互的数据在堆中就是另外一种情况:

1
2
let s1 =String::from("hello");
let s2 = s1;

第一步产生一个 String 对象,值为 "hello"。其中 "hello" 可以认为是类似于长度不确定的数据,需要在堆中存储。

第二步的情况略有不同( 这不是完全真的,仅用来对比参考 ): alt text

如图所示:两个 String 对象在栈中,每个 String 对象都有一个指针指向堆中的 "hello" 字符串。在给 s2 赋值时,只有栈中的数据被复制了,堆中的字符串依然还是原来的字符串。

前面我们说过,当变量超出范围时,Rust 自动调用释放资源函数并清理该变量的堆内存。但是 s1 和 s2 都被释放的话堆区中的 "hello" 被释放两次,这是不被系统允许的。为了确保安全,在给 s2 赋值时 s1 已经无效了。没错,在把 s1 的值赋给 s2 以后 s1 将不可以再被使用。下面这段程序是错的:

1
2
3
let s1 =String::from("hello");
let s2 = s1;
println!("{}, world!", s1);// 错误!s1 已经失效

所以实际情况是: alt text

s1 名存实亡。

克隆

Rust会尽可能地降低程序的运行成本,所以默认情况下,长度较大的数据存放在堆中,且采用移动的方式进行数据交互。但如果需要将数据单纯的复制一份以供他用,可以使用数据的第二种交互方式——克隆。

实例

1
2
3
4
5
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
}

运行结果:

1
s1 = hello, s2 = hello

这里是真的将堆中的 "hello" 复制了一份,所以 s1 和 s2 都分别绑定了一个值,释放的时候也会被当作两个资源。

当然,克隆仅在需要复制的情况下使用,毕竟复制数据会花费更多的时间。

涉及函数的所有权机制

对于变量来说这是最复杂的情况了。

如果将一个变量当作函数的参数传给其他函数,怎样安全的处理所有权呢?

下面这段程序描述了这种情况下所有权机制的运行原理:

实例

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
fn main() {
let s = String::from("hello");
// s 被声明有效

takes_ownership(s);
// s 的值被当作参数传入函数
// 所以可以当作 s 已经被移动,从这里开始已经无效

let x = 5;
// x 被声明有效

makes_copy(x);
// x 的值被当作参数传入函数
// 但 x 是基本类型,依然有效
// 在这里依然可以使用 x 却不能使用 s

} // 函数结束, x 无效, 然后是 s. 但 s 已被移动, 所以不用被释放


fn takes_ownership(some_string: String) {
// 一个 String 参数 some_string 传入,有效
println!("{}", some_string);
} // 函数结束, 参数 some_string 在这里释放

fn makes_copy(some_integer: i32) {
// 一个 i32 参数 some_integer 传入,有效
println!("{}", some_integer);
} // 函数结束, 参数 some_integer 是基本类型, 无需释放

如果将变量当作参数传入函数,那么它和移动的效果是一样的。

函数返回值的所有权机制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn main() {
let s1 = gives_ownership();
// gives_ownership 移动它的返回值到 s1

let s2 = String::from("hello");
// s2 被声明有效

let s3 = takes_and_gives_back(s2);
// s2 被当作参数移动, s3 获得返回值所有权
} // s3 无效被释放, s2 被移动, s1 无效被释放.

fn gives_ownership() -> String {
let some_string = String::from("hello");
// some_string 被声明有效

return some_string;
// some_string 被当作返回值移动出函数
}

fn takes_and_gives_back(a_string: String) -> String {
// a_string 被声明有效

a_string // a_string 被当作返回值移出函数
}

被当作函数返回值的变量所有权将会被移动出函数并返回到调用函数的地方,而不会直接被无效释放。

引用与租借

引用(Reference)是 C++ 开发者较为熟悉的概念。

如果你熟悉指针的概念,你可以把它看作一种指针。

实质上"引用"是变量的间接访问方式。

实例

1
2
3
4
5
fn main() {
let s1 = String::from("hello");
let s2 = &s1;
println!("s1 is {}, s2 is {}", s1, s2);
}

运行结果:

1
s1 is hello, s2 is hello

& 运算符可以取变量的"引用"。

当一个变量的值被引用时,变量本身不会被认定无效。因为"引用"并没有在栈中复制变量的值: alt text

引用不会获得值的所有权。

引用只能租借(Borrow)值的所有权。

引用本身也是一个类型并具有一个值,这个值记录的是别的值所在的位置,但引用不具有所指值的所有权:

1
2
3
4
5
6
fn main() {
let s1 = String::from("hello");
let s2 = &s1;
let s3 = s1;
println!("{}", s2);
}

这段程序不正确:因为 s2 租借的 s1 已经将所有权移动到 s3,所以 s2 将无法继续租借使用 s1 的所有权。如果需要使用 s2 使用该值,必须重新租借:

1
2
3
4
5
6
7
fn main() {
let s1 = String::from("hello");
let mut s2 = &s1;
let s3 = s1;
s2 = &s3; // 重新从 s3 租借所有权
println!("{}", s2);
}

这段程序是正确的。

既然引用不具有所有权,即使它租借了所有权,它也只享有使用权(这跟租房子是一个道理)。

如果尝试利用租借来的权利来修改数据会被阻止:

1
2
3
4
5
6
7
fn main() {
let s1 = String::from("run");
let s2 = &s1;
println!("{}", s2);
s2.push_str("oob"); // 错误,禁止修改租借的值
println!("{}", s2);
}

这段程序中 s2 尝试修改 s1 的值被阻止,租借的所有权不能修改所有者的值。

当然,也存在一种可变的租借方式,就像你租一个房子,如果物业规定房主可以修改房子结构,房主在租借时也在合同中声明赋予你这种权利,你是可以重新装修房子的:

1
2
3
4
5
6
7
8
9
10
fn main() {
let mut s1 = String::from("run");
// s1 是可变的

let s2 = &mut s1;
// s2 是可变的引用

s2.push_str("oob");
println!("{}", s2);
}

这段程序就没有问题了。我们用 &mut 修饰可变的引用类型。

可变引用与不可变引用相比除了权限不同以外,可变引用不允许多重引用,但不可变引用可以:

1
2
3
4
5
6
let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);

这段程序不正确,因为多重可变引用了 s。

Rust 对可变引用的这种设计主要出于对并发状态下发生数据访问碰撞的考虑,在编译阶段就避免了这种事情的发生。

由于发生数据访问碰撞的必要条件之一是数据被至少一个使用者写且同时被至少一个其他使用者读或写,所以在一个值被可变引用时不允许再次被任何引用。

垂悬引用(Dangling References)

这是一个换了个名字的概念,如果放在有指针概念的编程语言里它就指的是那种没有实际指向一个真正能访问的数据的指针(注意,不一定是空指针,还有可能是已经释放的资源)。它们就像失去悬挂物体的绳子,所以叫"垂悬引用"。

"垂悬引用"在 Rust 语言里不允许出现,如果有,编译器会发现它。

下面是一个垂悬的典型案例:

1
2
3
4
5
6
7
8
9
fn main() {
let reference_to_nothing = dangle();
}

fn dangle() -> &String {
let s = String::from("hello");

&s
}

很显然,伴随着 dangle 函数的结束,其局部变量的值本身没有被当作返回值,被释放了。但它的引用却被返回,这个引用所指向的值已经不能确定的存在,故不允许其出现。

Rust Slice(切片)类型(不难)

切片(Slice)是对数据值的部分引用。

切片这个名字往往出现在生物课上,我们做样本玻片的时候要从生物体上获取切片,以供在显微镜上观察。在 Rust 中,切片的意思大致也是这样,只不过它从数据取材引用。

字符串切片

最简单、最常用的数据切片类型是字符串切片(String Slice)。

1
2
3
4
5
6
7
8
fn main() {
let s = String::from("broadcast");

let part1 = &s[0..5];
let part2 = &s[5..9];

println!("{}={}+{}", s, part1, part2);
}
alt text

上图解释了字符串切片的原理(注:Rust 中的字符串类型实质上记录了字符在内存中的起始位置和其长度,我们暂时了解到这一点)。

使用 .. 表示范围的语法在循环章节中出现过。x..y 表示 [x, y) 的数学含义。.. 两边可以没有运算数:

1
2
3
..y 等价于0..y
x..等价于位置 x 到数据结束
..等价于位置0到结束

注意: 到目前为止,尽量不要在字符串中使用非英文字符,因为编码的问题。具体原因会在"字符串"章节叙述。

被切片引用的字符串禁止更改其值:

1
2
3
4
5
6
fn main() {
let mut s = String::from("runoob");
let slice = &s[0..3];
s.push_str("yes!"); // 错误
println!("slice = {}", slice);
}

这段程序不正确。

s 被部分引用,禁止更改其值。

实际上,到目前为止你一定疑惑为什么每一次使用字符串都要这样写String::from("runoob") ,直接写 "runoob" 不行吗?

事已至此我们必须分辨这两者概念的区别了。在 Rust 中有两种常用的字符串类型:str 和 String。

str 是 Rust 核心语言类型,就是本章一直在讲的字符串切片(String Slice),常常以引用的形式出现(&str)。

凡是用双引号包括的字符串常量整体的类型性质都是 &str :

1
let s = "hello";

这里的 s 就是一个 &str 类型的变量。

String 类型是 Rust 标准公共库提供的一种数据类型,它的功能更完善——它支持字符串的追加、清空等实用的操作。String 和 str 除了同样拥有一个字符开始位置属性和一个字符串长度属性以外还有一个容量(capacity)属性。

String 和 str 都支持切片,切片的结果是 &str 类型的数据。

注意:切片结果必须是引用类型,但开发者必须自己明示这一点:

1
let slice = &s[0..3];

有一个快速的办法可以将 String 转换成 &str:

1
2
let s1 =String::from("hello");
let s2 =&s1[..];
非字符串切片

除了字符串以外,其他一些线性数据结构也支持切片操作,例如数组:

1
2
3
4
5
6
7
fn main() {
let arr = [1, 3, 5, 7, 9];
let part = &arr[0..3];
for i in part.iter() {
println!("{}", i);
}
}

运行结果:

1
2
3
1
3
5

Rust 结构体

结构体定义

这是一个结构体定义:

1
2
3
4
5
6
structSite{
domain:String,
name:String,
nation:String,
found: u32
}

注意:如果你常用 C/C++,请记住在 Rust 里 struct 语句仅用来定义,不能声明实例,结尾不需要 ; 符号,而且每个字段定义之后用 , 分隔。

结构体实例

Rust 很多地方受 JavaScript 影响,在实例化结构体的时候用 JSON 对象的 key: value 语法来实现定义:

实例

1
2
3
4
5
6
let runoob = Site {
domain: String::from("www.runoob.com"),
name: String::from("RUNOOB"),
nation: String::from("China"),
found: 2013
};

如果你不了解 JSON 对象,你可以不用管它,记住格式就可以了:

1
2
3
4
结构体类名 {
字段名 : 字段值,
...
}

这样的好处是不仅使程序更加直观,还不需要按照定义的顺序来输入成员的值。

如果正在实例化的结构体有字段名称和现存变量名称一样的,可以简化书写:

1
2
3
4
5
6
7
8
let domain = String::from("www.runoob.com");
let name = String::from("RUNOOB");
let runoob = Site {
domain, // 等同于 domain : domain,
name, // 等同于 name : name,
nation: String::from("China"),
traffic: 2013
};

有这样一种情况:你想要新建一个结构体的实例,其中大部分属性需要被设置成与现存的一个结构体属性一样,仅需更改其中的一两个字段的值,可以使用结构体更新语法:

1
2
3
4
5
let site = Site {
    domain: String::from("www.runoob.com"),
    name: String::from("RUNOOB"),
    ..runoob
};

注意:..runoob 后面不可以有逗号。这种语法不允许一成不变的复制另一个结构体实例,意思就是说至少重新设定一个字段的值才能引用其他实例的值。

元组结构体

有一种更简单的定义和使用结构体的方式: 元组结构体

元组结构体是一种形式是元组的结构体。

与元组的区别是它有名字和固定的类型格式。它存在的意义是为了处理那些需要定义类型(经常使用)又不想太复杂的简单数据:

1
2
3
4
5
structColor(u8, u8, u8);
structPoint(f64, f64);

let black =Color(0,0,0);
let origin =Point(0.0,0.0);

"颜色"和"点坐标"是常用的两种数据类型,但如果实例化时写个大括号再写上两个名字就为了可读性牺牲了便捷性,Rust 不会遗留这个问题。元组结构体对象的使用方式和元组一样,通过 . 和下标来进行访问:

1
2
3
4
5
6
fn main() {
struct Color(u8, u8, u8);
struct Point(f64, f64); let black = Color(0, 0, 0);
let origin = Point(0.0, 0.0); println!("black = ({}, {}, {})", black.0, black.1, black.2);
println!("origin = ({}, {})", origin.0, origin.1);
}
结构体所有权

结构体必须掌握字段值所有权,因为结构体失效的时候会释放所有字段。

这就是为什么本章的案例中使用了 String 类型而不使用 &str 的原因。

但这不意味着结构体中不定义引用型字段,这需要通过"生命周期"机制来实现。

但现在还难以说明"生命周期"概念,所以只能在后面章节说明。

输出结构体

调试中,完整地显示出一个结构体实例是非常有用的。但如果我们手动的书写一个格式会非常的不方便。所以 Rust 提供了一个方便地输出一整个结构体的方法:

实例

1
2
3
4
5
6
7
8
9
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let rect1 = Rectangle { width: 30, height: 50 }; println!("rect1 is {:?}", rect1);
}

如第一行所示:一定要导入调试库 #[derive(Debug)] ,之后在 println 和 print 宏中就可以用 {:?} 占位符输出一整个结构体:

1
rect1 isRectangle{ width:30, height:50}

如果属性较多的话可以使用另一个占位符 {:#?}

输出结果:

1
2
3
4
rect1 isRectangle{
width:30,
height:50
}
结构体方法

方法(Method)和函数(Function)类似,只不过它是用来操作结构体实例的。

Rust 语言不是面向对象的,从它所有权机制的创新可以看出这一点。但是面向对象的珍贵思想可以在 Rust 实现。

结构体方法的第一个参数必须是 &self,不需声明类型,因为 self 不是一种风格而是关键字。

计算一个矩形的面积:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Rectangle {
width: u32,
height: u32,
}

impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}

fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!("rect1's area is {}", rect1.area());
}

请注意,在调用结构体方法的时候不需要填写 self ,这是出于对使用方便性的考虑。

一个多参数的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct Rectangle {
width: u32,
height: u32,
}

impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}

fn wider(&self, rect: &Rectangle) -> bool {
self.width > rect.width
}
}

fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
let rect2 = Rectangle { width: 40, height: 20 };

println!("{}", rect1.wider(&rect2));
}
结构体关联函数

之所以"结构体方法"不叫"结构体函数"是因为"函数"这个名字留给了这种函数:它在 impl 块中却没有 &self 参数。

这种函数不依赖实例,但是使用它需要声明是在哪个 impl 块中的。

一直使用的 String::from 函数就是一个"关联函数"。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

impl Rectangle {
fn create(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
}

fn main() {
let rect = Rectangle::create(30, 50);
println!("{:?}", rect);
}

贴士: 结构体 impl 块可以写几次,效果相当于它们内容的拼接!

单元结构体

结构体可以只作为一种象征而无需任何成员:

1
structUnitStruct;

我们称这种没有身体的结构体为单元结构体(Unit Struct)。

Rust 枚举类

枚举类在 Rust 中并不像其他编程语言中的概念那样简单,但依然可以十分简单的使用:

实例

1
2
3
4
5
6
7
8
9
10
#[derive(Debug)]

enum Book {
Papery, Electronic
}

fn main() {
let book = Book::Papery;
println!("{:?}", book);
}

运行结果:

1
Papery
match 语法

枚举的目的是对某一类事物的分类,分类的目的是为了对不同的情况进行描述。基于这个原理,往往枚举类最终都会被分支结构处理(许多语言中的 switch )。 switch 语法很经典,但在 Rust 中并不支持,很多语言摒弃 switch 的原因都是因为 switch 容易存在因忘记添加 break 而产生的串接运行问题,Java 和 C# 这类语言通过安全检查杜绝这种情况出现。

Rust 通过 match 语句来实现分支结构。先认识一下如何用 match 处理枚举类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
enum Book {
Papery {index: u32},
Electronic {url: String},
}

let book = Book::Papery{index: 1001};
let ebook = Book::Electronic{url: String::from("url...")};

match book {
Book::Papery { index } => {
println!("Papery book {}", index);
},
Book::Electronic { url } => {
println!("E-book {}", url);
}
}
}

运行结果:

1
Papery book 1001

match 块也可以当作函数表达式来对待,它也是可以有返回值的:

1
2
3
4
5
match 枚举类实例{
分类1=>返回值表达式,
分类2=>返回值表达式,
...
}

但是所有返回值表达式的类型必须一样!

如果把枚举类附加属性定义成元组,在 match 块中需要临时指定一个名字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum Book {
Papery(u32),
Electronic {url: String},
}
let book = Book::Papery(1001);

match book {
Book::Papery(i) => {
println!("{}", i);
},
Book::Electronic { url } => {
println!("{}", url);
}
}

match 除了能够对枚举类进行分支选择以外,还可以对整数、浮点数、字符和字符串切片引用(&str)类型的数据进行分支选择。其中,浮点数类型被分支选择虽然合法,但不推荐这样使用,因为精度问题可能会导致分支错误。

对非枚举类进行分支选择时必须注意处理例外情况,即使在例外情况下没有任何要做的事 . 例外情况用下划线 **_** 表示:

1
2
3
4
5
6
7
fn main() {
let t = "abc";
match t {
"abc" => println!("Yes"),
_ => {},
}
}
Option 枚举类

Option 是 Rust 标准库中的枚举类,这个类用于填补 Rust 不支持 null 引用的空白。

许多语言支持 null 的存在(C/C++、Java),这样很方便,但也制造了极大的问题,null 的发明者也承认这一点,"一个方便的想法造成累计 10 亿美元的损失"。

null 经常在开发者把一切都当作不是 null 的时候给予程序致命一击:毕竟只要出现一个这样的错误,程序的运行就要彻底终止。

为了解决这个问题,很多语言默认不允许 null,但在语言层面支持 null 的出现(常在类型前面用 ? 符号修饰)。

Java 默认支持 null,但可以通过 @NotNull 注解限制出现 null,这是一种应付的办法。

Rust 在语言层面彻底不允许空值 null 的存在,但无奈null 可以高效地解决少量的问题,所以 Rust 引入了 Option 枚举类

1
2
3
4
enum Option<T> {
Some(T),
None,
}

如果你想定义一个可以为空值的类,你可以这样:

1
let opt =Option::Some("Hello");

如果你想针对 opt 执行某些操作,你必须先判断它是否是 Option::None

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let opt = Option::Some("Hello");
match opt {
Option::Some(something) => {
println!("{}", something);
},
Option::None => {
println!("opt is nothing");
}
}
}

如果你的变量刚开始是空值,你体谅一下编译器,它怎么知道值不为空的时候变量是什么类型的呢?

所以初始值为空的 Option 必须明确类型:

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let opt: Option<&str> = Option::None;
match opt {
Option::Some(something) => {
println!("{}", something);
},
Option::None => {
println!("opt is nothing");
}
}
}

这种设计会让空值编程变得不容易,但这正是构建一个稳定高效的系统所需要的。由于 Option 是 Rust 编译器默认引入的,在使用时可以省略 Option:: 直接写 None 或者 Some()。

Option 是一种特殊的枚举类,它可以含值分支选择:

1
2
3
4
5
6
7
fn main() {
let t = Some(64);
match t {
Some(64) => println!("Yes"),
_ => println!("No"),
}
}
if let 语法
1
2
3
4
5
let i = 0;
match i {
0 => println!("zero"),
_ => {},
}

这段程序的目的是判断 i 是否是数字 0,如果是就打印 zero。

现在用 if let 语法缩短这段代码:

1
2
3
4
let i = 0;
if let 0 = i {
println!("zero");
}

if let 语法格式如下:

1
2
3
if let 匹配值 = 源变量 {
语句块
}

可以在之后添加一个 else 块来处理例外情况。

if let 语法可以认为是只区分两种情况的 match 语句的"语法糖"(语法糖指的是某种语法的原理相同的便捷替代品)。

对于枚举类依然适用:

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
enum Book {
Papery(u32),
Electronic(String)
}
let book = Book::Electronic(String::from("url"));
if let Book::Papery(index) = book {
println!("Papery {}", index);
} else {
println!("Not papery book");
}
}

Rust 组织管理

Rust 中有三个重要的组织概念:箱、包、模块。

箱(Crate)

"箱"是二进制程序文件或者库文件,存在于"包"中。

"箱"是树状结构的,它的树根是编译器开始运行时编译的源文件所编译的程序。

注意:"二进制程序文件"不一定是"二进制可执行文件",只能确定是是包含目标机器语言的文件,文件格式随编译环境的不同而不同。

包(Package)

当我们使用 Cargo 执行 new 命令创建 Rust 工程时,工程目录下会建立一个 Cargo.toml 文件。工程的实质就是一个包,包必须由一个 Cargo.toml 文件来管理,该文件描述了包的基本信息以及依赖项。

一个包最多包含一个库"箱",可以包含任意数量的二进制"箱",但是至少包含一个"箱"(不管是库还是二进制"箱")。

当使用 cargo new 命令创建完包之后,src 目录下会生成一个 main.rs 源文件,Cargo 默认这个文件为二进制箱的根,编译之后的二进制箱将与包名相同。

模块(Module)

对于一个软件工程来说,我们往往按照所使用的编程语言的组织规范来进行组织,组织模块的主要结构往往是树。Java 组织功能模块的主要单位是类,而 JavaScript 组织模块的主要方式是 function。

这些先进的语言的组织单位可以层层包含,就像文件系统的目录结构一样。Rust 中的组织单位是模块(Module)。

1
2
3
4
5
6
7
8
9
10
11
mod nation {
mod government {
fn govern() {}
}
mod congress {
fn legislate() {}
}
mod court {
fn judicial() {}
}
}

这是一段描述法治国家的程序:国家(nation)包括政府(government)、议会(congress)和法院(court),分别有行政、立法和司法的功能。我们可以把它转换成树状结构:

1
2
3
4
5
6
7
nation
├── government
│ └── govern
├── congress
│ └── legislate
└── court
└── judicial

在文件系统中,目录结构往往以斜杠在路径字符串中表示对象的位置,Rust 中的路径分隔符是 ::

路径分为绝对路径和相对路径。绝对路径从 crate 关键字开始描述。相对路径从 self 或 super 关键字或一个标识符开始描述。例如:

1
crate::nation::government::govern();

是描述 govern 函数的绝对路径,相对路径可以表示为:

1
nation::government::govern();

现在你可以尝试在一个源程序里定义类似的模块结构并在主函数中使用路径。

如果你这样做,你一定会发现它不正确的地方:government 模块和其中的函数都是私有(private)的,你不被允许访问它们。

访问权限

Rust 中有两种简单的访问权:公共(public)和私有(private)。

默认情况下,如果不加修饰符,模块中的成员访问权将是私有的。

如果想使用公共权限,需要使用 pub 关键字。

对于私有的模块,只有在与其平级的位置或下级的位置才能访问,不能从其外部访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mod nation {
pub mod government {
pub fn govern() {}
}

mod congress {
pub fn legislate() {}
}

mod court {
fn judicial() {
super::congress::legislate();
}
}
}

fn main() {
nation::government::govern();
}

这段程序是能通过编译的。请注意观察 court 模块中 super 的访问方法。

如果模块中定义了结构体,结构体除了其本身是私有的以外,其字段也默认是私有的。所以如果想使用模块中的结构体以及其字段,需要 pub 声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mod back_of_house {
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}

impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
pub fn eat_at_restaurant() {
let mut meal = back_of_house::Breakfast::summer("Rye");
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
}
fn main() {
eat_at_restaurant()
}

枚举类枚举项可以内含字段,但不具备类似的性质:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mod SomeModule {
pub enum Person {
King {
name: String
},
Queen
}
}

fn main() {
let person = SomeModule::Person::King{
name: String::from("Blue")
};
match person {
SomeModule::Person::King {name} => {
println!("{}", name);
}
_ => {}
}
}

运行结果:

1
Blue
难以发现的模块

使用过 Java 的开发者在编程时往往非常讨厌最外层的 class 块——它的名字与文件名一模一样,因为它就表示文件容器,尽管它很繁琐但我们不得不写一遍来强调"这个类是文件所包含的类"。

不过这样有一些好处:起码它让开发者明明白白的意识到了类包装的存在,而且可以明确的描述类的继承关系。

在 Rust 中,模块就像是 Java 中的类包装,但是文件一开头就可以写一个主函数,这该如何解释呢?

每一个 Rust 文件的内容都是一个"难以发现"的模块。

让我们用两个文件来揭示这一点:

main.rs 文件

1
2
3
4
5
6
7
// main.rs
mod second_module;

fn main() {
println!("This is the main module.");
println!("{}", second_module::message());
}

second_module.rs 文件

1
2
3
4
// second_module.rs
pub fn message() -> String {
String::from("This is the 2nd module.")
}

运行结果:

1
2
Thisis the main module.
Thisis the 2ndmodule.
use 关键字

use 关键字能够将模块标识符引入当前作用域:

1
2
3
4
5
6
7
8
9
10
11
mod nation {
pub mod government {
pub fn govern() {}
}
}

use crate::nation::government::govern;

fn main() {
govern();
}

这段程序能够通过编译。

因为 use 关键字把 govern 标识符导入到了当前的模块下,可以直接使用。

这样就解决了局部模块路径过长的问题。

当然,有些情况下存在两个相同的名称,且同样需要导入,我们可以使用 as 关键字为标识符添加别名:

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mod nation {
pub mod government {
pub fn govern() {}
}
pub fn govern() {}
}

use crate::nation::government::govern;
use crate::nation::govern as nation_govern;

fn main() {
nation_govern();
govern();
}

这里有两个 govern 函数,一个是 nation 下的,一个是 government 下的,我们用 as 将 nation 下的取别名 nation_govern。两个名称可以同时使用。

use 关键字可以与 pub 关键字配合使用:

1
2
3
4
5
6
7
8
9
10
mod nation {
pub mod government {
pub fn govern() {}
}
pub use government::govern;
}

fn main() {
nation::govern();
}
引用标准库

Rust 官方标准库字典:https://doc.rust-lang.org/stable/std/all.html

在学习了本章的概念之后,我们可以轻松的导入系统库来方便的开发程序了:

1
2
3
4
5
use std::f64::consts::PI;

fn main() {
println!("{}", (PI / 2.0).sin());
}

所有的系统库模块都是被默认导入的,所以在使用的时候只需要使用 use 关键字简化路径就可以方便的使用了。

Rust 错误处理

Rust 有一套独特的处理异常情况的机制,它并不像其它语言中的 try 机制那样简单。

首先,程序中一般会出现两种错误:可恢复错误和不可恢复错误

可恢复错误的典型案例是文件访问错误,如果访问一个文件失败,有可能是因为它正在被占用,是正常的,我们可以通过等待来解决。

但还有一种错误是由编程中无法解决的逻辑错误导致的,例如访问数组末尾以外的位置。

大多数编程语言不区分这两种错误,并用 Exception (异常)类来表示错误。在 Rust 中没有 Exception

对于可恢复错误用 Result<T, E> 类来处理,对于不可恢复错误使用 panic! 宏来处理。

不可恢复错误

本章以前没有专门介绍 Rust 宏的语法,但已经使用过了 println! 宏,因为这些宏的使用较为简单,所以暂时不需要彻底掌握它,我们可以用同样的方法先学会使用 panic! 宏的使用方法。

1
2
3
4
fn main() {
panic!("error occured");
println!("Hello, Rust");
}

运行结果:

1
2
thread 'main' panicked at 'error occured', src\main.rs:3:5
note: run with`RUST_BACKTRACE=1` environment variable to display a backtrace.

很显然,程序并不能如约运行到 println!("Hello, Rust") ,而是在 panic! 宏被调用时停止了运行。

不可恢复的错误一定会导致程序受到致命的打击而终止运行。

让我们注视错误输出的两行:

  • 第一行输出了 panic! 宏调用的位置以及其输出的错误信息。
  • 第二行是一句提示,翻译成中文就是"通过 RUST_BACKTRACE=1 环境变量运行以显示回溯"。接下来我们将介绍回溯(backtrace)。

紧接着刚才的例子,我们在 VSCode 中新建一个终端: alt text

在新建的终端里设置环境变量(不同的终端方法不同,这里介绍两种主要的方法):

如果在 Windows 7 及以上的 Windows 系统版本中,默认使用的终端命令行是 Powershell,请使用以下命令:

1
$env:RUST_BACKTRACE=1; cargo run

如果你使用的是 Linux 或 macOS 等 UNIX 系统,一般情况下默认使用的是 bash 命令行,请使用以下命令:

1
RUST_BACKTRACE=1 cargo run

然后,你会看到以下文字:

1
2
3
4
5
6
thread 'main' panicked at 'error occured', src\main.rs:3:5
stack backtrace:
...
11: greeting::main
at .\src\main.rs:3
...

回溯是不可恢复错误的另一种处理方式,它会展开运行的栈并输出所有的信息,然后程序依然会退出。上面的省略号省略了大量的输出信息,我们可以找到我们编写的 panic! 宏触发的错误。

可恢复的错误

此概念十分类似于 Java 编程语言中的异常。实际上在 C 语言中我们就常常将函数返回值设置成整数来表达函数遇到的错误,在 Rust 中通过 Result<T, E> 枚举类作返回值来进行异常表达:

1
2
3
4
enumResult<T, E>{
Ok(T),
Err(E),
}

在 Rust 标准库中可能产生异常的函数的返回值都是 Result 类型的。例如:当我们尝试打开一个文件时:

1
2
3
4
5
6
7
8
9
10
11
12
13
use std::fs::File;

fn main() {
let f = File::open("hello.txt");
match f {
Ok(file) => {
println!("File opened successfully.");
},
Err(err) => {
println!("Failed to open the file.");
}
}
}

如果 hello.txt 文件不存在,会打印 "Failed to open the file."

当然,我们在枚举类章节讲到的 if let 语法可以简化 match 语法块:

1
2
3
4
5
6
7
8
9
10
use std::fs::File;

fn main() {
let f = File::open("hello.txt");
if let Ok(file) = f {
println!("File opened successfully.");
} else {
println!("Failed to open the file.");
}
}

如果想使一个可恢复错误按不可恢复错误处理,Result 类提供了两个办法:unwrap() 和 expect(message: &str) :

1
2
3
4
5
6
use std::fs::File;

fn main() {
let f1 = File::open("hello.txt").unwrap();
let f2 = File::open("hello.txt").expect("Failed to open.");
}

这段程序相当于在 Result 为 Err 时调用 panic! 宏。两者的区别在于 expect 能够向 panic! 宏发送一段指定的错误信息。

可恢复的错误的传递

之前所讲的是接收到错误的处理方式,但是如果我们自己编写一个函数在遇到错误时想传递出去怎么办呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
fn f(i: i32) -> Result<i32, bool> {
if i >= 0 { Ok(i) }
else { Err(false) }
}

fn main() {
let r = f(10000);
if let Ok(v) = r {
println!("Ok: f(-1) = {}", v);
} else {
println!("Err");
}
}

运行结果:

1
Ok: f(-1)=10000

这段程序中函数 f 是错误的根源,现在我们再写一个传递错误的函数 g :

1
2
3
4
5
6
7
fn g(i: i32) -> Result<i32, bool> {
let t = f(i);
return match t {
Ok(i) => Ok(i),
Err(b) => Err(b)
};
}

函数 g 传递了函数 f 可能出现的错误(这里的 g 只是一个简单的例子,实际上传递错误的函数一般还包含很多其它操作)。

这样写有些冗长,Rust 中可以在 Result 对象后添加 ? 操作符将同类的 Err 直接传递出去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn f(i: i32) -> Result<i32, bool> {
if i >= 0 { Ok(i) }
else { Err(false) }
}

fn g(i: i32) -> Result<i32, bool> {
let t = f(i)?;
Ok(t) // 因为确定 t 不是 Err, t 在这里已经是 i32 类型
}

fn main() {
let r = g(10000);
if let Ok(v) = r {
println!("Ok: g(10000) = {}", v);
} else {
println!("Err");
}
}

运行结果:

1
Ok: g(10000)=10000

? 符的实际作用是将 Result 类非异常的值直接取出,如果有异常就将异常 Result 返回出去。所以,? 符仅用于返回值类型为 Result<T, E> 的函数,其中 E 类型必须和 ? 所处理的 Result 的 E 类型一致。

kind 方法

到此为止,Rust 似乎没有像 try 块一样可以令任何位置发生的同类异常都直接得到相同的解决的语法,但这样并不意味着 Rust 实现不了:我们完全可以把 try 块在独立的函数中实现,将所有的异常都传递出去解决。实际上这才是一个分化良好的程序应当遵循的编程方法:应该注重独立功能的完整性。

但是这样需要判断 Result 的 Err 类型,获取 Err 类型的函数是 kind()。

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
use std::io;
use std::io::Read;
use std::fs::File;

fn read_text_from_file(path: &str) -> Result<String, io::Error> {
let mut f = File::open(path)?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}

fn main() {
let str_file = read_text_from_file("hello.txt");
match str_file {
Ok(s) => println!("{}", s),
Err(e) => {
match e.kind() {
io::ErrorKind::NotFound => {
println!("No such file");
},
_ => {
println!("Cannot read the file");
}
}
}
}
}

运行结果:

1
No such file

Rust 泛型与特性

泛型是一个编程语言不可或缺的机制。

C++ 语言中用"模板"来实现泛型,而 C 语言中没有泛型的机制,这也导致 C 语言难以构建类型复杂的工程。

泛型机制是编程语言用于表达类型抽象的机制,一般用于功能确定、数据类型待定的类,如链表、映射表等。

在函数中定义泛型

这是一个对整型数字选择排序的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn max(array: &[i32]) -> i32 {
let mut max_index = 0;
let mut i = 1;
while i < array.len() {
if array[i] > array[max_index] {
max_index = i;
}
i += 1;
}
array[max_index]
}

fn main() {
let a = [2, 4, 6, 3, 1];
println!("max = {}", max(&a));
}

这是一个简单的取最大值程序,可以用于处理 i32 数字类型的数据,但无法用于 f64 类型的数据。通过使用泛型我们可以使这个函数可以利用到各个类型中去。但实际上并不是所有的数据类型都可以比大小,所以接下来一段代码并不是用来运行的,而是用来描述一下函数泛型的语法格式:

1
2
3
4
5
6
7
8
9
10
11
fn max<T>(array: &[T]) -> T {
let mut max_index = 0;
let mut i = 1;
while i < array.len() {
if array[i] > array[max_index] {
max_index = i;
}
i += 1;
}
array[max_index]
}
结构体与枚举类中的泛型

在之前我们学习的 Option 和 Result 枚举类就是泛型的。

Rust 中的结构体和枚举类都可以实现泛型机制。

1
2
3
4
struct Point<T> {
    x: T,
    y: T
}

这是一个点坐标结构体,T 表示描述点坐标的数字类型。我们可以这样使用:

1
2
let p1 = Point {x: 1, y: 2};
let p2 = Point {x: 1.0, y: 2.0};

使用时并没有声明类型,这里使用的是自动类型机制,但不允许出现类型不匹配的情况如下:

1
let p = Point {x: 1, y: 2.0};

x 与 1 绑定时就已经将 T 设定为 i32,所以不允许再出现 f64 的类型。如果我们想让 x 与 y 用不同的数据类型表示,可以使用两个泛型标识符:

1
2
3
4
struct Point<T1, T2> {
    x: T1,
    y: T2
}

在枚举类中表示泛型的方法诸如 Option 和 Result:

1
2
3
4
5
6
7
8
9
enum Option<T> {
Some(T),
None,
}

enum Result<T, E> {
Ok(T),
Err(E),
}

结构体与枚举类都可以定义方法,那么方法也应该实现泛型的机制,否则泛型的类将无法被有效的方法操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Point<T> {
x: T,
y: T,
}

impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}

fn main() {
let p = Point { x: 1, y: 2 };
println!("p.x = {}", p.x());
}

运行结果:

1
p.x =1

注意,impl 关键字的后方必须有 <T>,因为它后面的 T 是以之为榜样的。但我们也可以为其中的一种泛型添加方法:

1
2
3
4
5
impl Point<f64>{
fn x(&self)-> f64 {
self.x
}
}

impl 块本身的泛型并没有阻碍其内部方法具有泛型的能力:

1
2
3
4
5
6
7
8
impl<T, U>Point<T, U>{
fn mixup<V, W>(self, other:Point<V, W>)->Point<T, W>{
Point{
x:self.x,
y: other.y,
}
}
}

方法 mixup 将一个 Point<T, U> 点的 x 与 Point<V, W> 点的 y 融合成一个类型为 Point<T, W> 的新点。

特性(trait)

特性(trait)概念接近于 Java 中的接口(Interface),但两者不完全相同。特性与接口相同的地方在于它们都是一种行为规范,可以用于标识哪些类有哪些方法。

特性在 Rust 中用 trait 表示:

1
2
3
trait Descriptive{
fn describe(&self)->String;
}

Descriptive 规定了实现者必需有 describe(&self) -> String 方法。

我们用它实现一个结构体:

1
2
3
4
5
6
7
8
9
10
struct Person {
name: String,
age: u8
}

impl Descriptive for Person {
fn describe(&self) -> String {
format!("{} {}", self.name, self.age)
}
}

格式是:

1
impl <特性名> for <所实现的类型名>

Rust 同一个类可以实现多个特性,每个 impl 块只能实现一个。

默认特性

这是特性与接口的不同点:接口只能规范方法而不能定义方法,但特性可以定义方法作为默认方法,因为是"默认",所以对象既可以重新定义方法,也可以不重新定义方法使用默认的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
trait Descriptive {
fn describe(&self) -> String {
String::from("[Object]")
}
}

struct Person {
name: String,
age: u8
}

impl Descriptive for Person {
fn describe(&self) -> String {
format!("{} {}", self.name, self.age)
}
}

fn main() {
let cali = Person {
name: String::from("Cali"),
age: 24
};
println!("{}", cali.describe());
}

运行结果:

1
Cali 24

如果我们将 impl Descriptive for Person 块中的内容去掉,那么运行结果就是:

1
[Object]
特性做参数

很多情况下我们需要传递一个函数做参数,例如回调函数、设置按钮事件等。在 Java 中函数必须以接口实现的类实例来传递,在 Rust 中可以通过传递特性参数来实现:

1
2
3
fn output(object: impl Descriptive){
println!("{}",object.describe());
}

任何实现了 Descriptive 特性的对象都可以作为这个函数的参数,这个函数没必要了解传入对象有没有其他属性或方法,只需要了解它一定有 Descriptive 特性规范的方法就可以了。当然,此函数内也无法使用其他的属性与方法。

特性参数还可以用这种等效语法实现:

1
2
3
fn output<T:Descriptive>(object: T){
println!("{}",object.describe());
}

这是一种风格类似泛型的语法糖,这种语法糖在有多个参数类型均是特性的情况下十分实用:

1
2
3
4
fn output_two<T:Descriptive>(arg1: T, arg2: T){
println!("{}", arg1.describe());
println!("{}", arg2.describe());
}

特性作类型表示时如果涉及多个特性,可以用 + 符号表示,例如:

1
2
fn notify(item: impl Summary+Display)
fn notify<T:Summary+Display>(item: T)

注意: 仅用于表示类型的时候,并不意味着可以在 impl 块中使用。

复杂的实现关系可以使用 where 关键字简化,例如:

1
fn some_function<T:Display+Clone, U:Clone+Debug>(t: T, u: U)

可以简化成:

1
2
3
fn some_function<T, U>(t: T, u: U)-> i32
where T:Display+Clone,
U:Clone+Debug

在了解这个语法之后,泛型章节中的"取最大值"案例就可以真正实现了:

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
trait Comparable {
fn compare(&self, object: &Self) -> i8;
}

fn max<T: Comparable>(array: &[T]) -> &T {
let mut max_index = 0;
let mut i = 1;
while i < array.len() {
if array[i].compare(&array[max_index]) > 0 {
max_index = i;
}
i += 1;
}
&array[max_index]
}

impl Comparable for f64 {
fn compare(&self, object: &f64) -> i8 {
if &self > &object { 1 }
else if &self == &object { 0 }
else { -1 }
}
}

fn main() {
let arr = [1.0, 3.0, 5.0, 4.0, 2.0];
println!("maximum of arr is {}", max(&arr));
}

运行结果:

1
maximum of arr is5

Tip: 由于需要声明 compare 函数的第二参数必须与实现该特性的类型相同,所以 Self (注意大小写)关键字就代表了当前类型(不是实例)本身。

特性做返回值

特性做返回值格式如下:

1
2
3
4
5
6
fn person() -> impl Descriptive {
Person {
name: String::from("Cali"),
age: 24
}
}

但是有一点,特性做返回值只接受实现了该特性的对象做返回值且在同一个函数中所有可能的返回值类型必须完全一样。比如结构体 A 与结构体 B 都实现了特性 Trait,下面这个函数就是错误的:

1
2
3
4
5
6
7
fn some_function(bool bl) -> impl Descriptive {
if bl {
return A {};
} else {
return B {};
}
}
有条件实现方法

impl 功能十分强大,我们可以用它实现类的方法。但对于泛型类来说,有时我们需要区分一下它所属的泛型已经实现的方法来决定它接下来该实现的方法:

1
2
3
4
5
struct A<T> {}

impl<T: B + C> A<T> {
fn d(&self) {}
}

这段代码声明了 A <T> 类型必须在 T 已经实现 B 和 C 特性的前提下才能有效实现此 impl 块。

Rust 生命周期

Rust 生命周期机制是与所有权机制同等重要的资源管理机制。

之所以引入这个概念主要是应对复杂类型系统中资源管理的问题。

引用是对待复杂类型时必不可少的机制,毕竟复杂类型的数据不能被处理器轻易地复制和计算。

但引用往往导致极其复杂的资源管理问题,首先认识一下垂悬引用:

1
2
3
4
5
6
7
8
9
10
{
let r;

{
let x = 5;
r = &x;
}

println!("r: {}", r);
}

这段代码是不会通过 Rust 编译器的,原因是 r 所引用的值已经在使用之前被释放。

alt text

上图中的绿色范围 'a 表示 r 的生命周期,蓝色范围 'b 表示 x 的生命周期。很显然,'b 比 'a 小得多,引用必须在值的生命周期以内才有效。

一直以来我们都在结构体中使用 String 而不用 &str,我们用一个案例解释原因:

1
2
3
4
5
6
7
fn longer(s1: &str, s2: &str) -> &str {
if s2.len() > s1.len() {
s2
} else {
s1
}
}

longer 函数取 s1 和 s2 两个字符串切片中较长的一个返回其引用值。但只这段代码不会通过编译,原因是返回值引用可能会返回过期的引用:

1
2
3
4
5
6
7
8
9
fn main() {
let r;
{
let s1 = "rust";
let s2 = "ecmascript";
r = longer(s1, s2);
}
println!("{} is longer", r);
}

这段程序中虽然经过了比较,但 r 被使用的时候源值 s1 和 s2 都已经失效了。当然我们可以把 r 的使用移到 s1 和 s2 的生命周期范围以内防止这种错误的发生,但对于函数来说,它并不能知道自己以外的地方是什么情况,它为了保障自己传递出去的值是正常的,必选所有权原则消除一切危险,所以 longer 函数并不能通过编译。

注: &str和String类型的区别如下:

1
2
3
4
5
6
7
8
9
let a = "hello world";
let b = "OK";
let mut s = String::from("Hello Rust");
println!("{}", s.capacity()); // prints 12
s.push_str("Here I come!");
println!("{}", s.len()); // prints 24
let s = "Hello, Rust!";
println!("{}", s.capacity()); // compile error: no method named `capacity` found for type `&str`
println!("{}", s.len()); // prints 12
  • 上面的a, b 是&str,不是String,&str更像一个固定的数组,String像一个可变的数组。
  • String保留了一个len()和capacity(),但str只有一个len()。
  • &str 是 str的一个的borrowed 类型,可以称为一个字符串切片,一个不可变的string。

如果只想要一个字符串的只读视图,或者&str作为一个函数的参数,那就首选&str。如果想拥有所有权,想修改字符串那就用String吧。

生命周期注释

生命周期注释是描述引用生命周期的办法。

虽然这样并不能够改变引用的生命周期,但可以在合适的地方声明两个引用的生命周期一致。

生命周期注释用单引号开头,跟着一个小写字母单词:

1
2
3
&i32        // 常规引用
&'a i32 // 含有生命周期注释的引用
&'a mut i32 // 可变型含有生命周期注释的引用

让我们用生命周期注释改造 longer 函数:

1
2
3
4
5
6
7
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s2.len() > s1.len() {
s2
} else {
s1
}
}

我们需要用泛型声明来规范生命周期的名称,随后函数返回值的生命周期将与两个参数的生命周期一致,所以在调用时可以这样写:

1
2
3
4
5
6
7
8
9
fn main() {
let r;
{
let s1 = "rust";
let s2 = "ecmascript";
r = longer(s1, s2);
println!("{} is longer", r);
}
}

以上两段程序结合的运行结果:

1
ecmascript is longer

注意: 别忘记了自动类型判断的原则。

结构体中使用字符串切片引用

这是之前留下的疑问,在此解答:

1
2
3
4
5
6
7
8
9
fn main() {
struct Str<'a> {
content: &'a str
}
let s = Str {
content: "string_slice"
};
println!("s.content = {}", s.content);
}

运行结果:

1
s.content = string_slice

如果对结构体 Str 有方法定义:

1
2
3
4
5
impl<'a> Str<'a> {
fn get_content(&self) -> &str {
self.content
}
}

这里返回值并没有生命周期注释,但是加上也无妨。这是一个历史问题,早期 Rust 不支持生命周期自动判断,所有的生命周期必须严格声明,但主流稳定版本的 Rust 已经支持了这个功能。

静态生命周期

生命周期注释有一个特别的:'static 。所有用双引号包括的字符串常量所代表的精确数据类型都是 &'static str ,'static 所表示的生命周期从程序运行开始到程序运行结束。

泛型、特性与生命周期协同作战
1
2
3
4
5
6
7
8
9
10
11
12
use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
where T: Display
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}

这段程序出自 Rust 圣经,是一个同时使用了泛型、特性、生命周期机制的程序,不强求,可以体验,毕竟早晚用得到!

Rust 面向对象

面向对象的编程语言通常实现了数据的封装与继承并能基于数据调用方法。

Rust 不是面向对象的编程语言,但这些功能都得以实现。

封装

封装就是对外显示的策略,在 Rust 中可以通过模块的机制来实现最外层的封装,并且每一个 Rust 文件都可以看作一个模块,模块内的元素可以通过 pub 关键字对外明示。这一点在"组织管理"章节详细叙述过。

"类"往往是面向对象的编程语言中常用到的概念。"类"封装的是数据,是对同一类数据实体以及其处理方法的抽象。在 Rust 中,我们可以使用结构体或枚举类来实现类的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pub struct ClassName {
pub field: Type,
}

pub impl ClassName {
fn some_method(&self) {
// 方法函数体
}
}

pub enum EnumName {
A,
B,
}

pub impl EnumName {
fn some_method(&self) {

}
}

下面建造一个完整的类:

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
second.rs
pub struct ClassName {
field: i32,
}

impl ClassName {
pub fn new(value: i32) -> ClassName {
ClassName {
field: value
}
}

pub fn public_method(&self) {
println!("from public method");
self.private_method();
}

fn private_method(&self) {
println!("from private method");
}
}
main.rs
mod second;
use second::ClassName;

fn main() {
let object = ClassName::new(1024);
object.public_method();
}
继承

几乎其他的面向对象的编程语言都可以实现"继承",并用"extend"词语来描述这个动作。

继承是多态(Polymorphism)思想的实现,多态指的是编程语言可以处理多种类型数据的代码。在 Rust 中,通过特性(trait)实现多态。有关特性的细节已在"特性"章节给出。但是特性无法实现属性的继承,只能实现类似于"接口"的功能,所以想继承一个类的方法最好在"子类"中定义"父类"的实例。

总结地说,Rust 没有提供跟继承有关的语法糖,也没有官方的继承手段(完全等同于 Java 中的类的继承),但灵活的语法依然可以实现相关的功能。

Rust 宏

Rust 宏(Macros)是一种在编译时生成代码的强大工具,它允许你在编写代码时创建自定义语法扩展。

宏(Macro)是一种在代码中进行元编程(Metaprogramming)的技术,它允许在编译时生成代码,宏可以帮助简化代码,提高代码的可读性和可维护性,同时允许开发者在编译时执行一些代码生成的操作。

宏在 Rust 中有两种类型:声明式宏(Declarative Macros)过程宏(Procedural Macros)

本文主要介绍声明式宏。

宏的定义

在 Rust 中,使用 macro_rules! 关键字来定义声明式宏。

1
2
3
4
5
6
7
macro_rules! my_macro {
// 模式匹配和展开
($arg:expr)=>{
// 生成的代码
// 使用 $arg 来代替匹配到的表达式
};
}

声明式宏使用 macro_rules! 关键字进行定义,它们被称为 "macro_rules" 宏。这种宏的定义是基于模式匹配的,可以匹配代码的结构并根据匹配的模式生成相应的代码。这样的宏在不引入新的语法结构的情况下,可以用来简化一些通用的代码模式。

下面是一个简单的宏定义的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 宏的定义
macro_rules! greet {
// 模式匹配
($name:expr) => {
// 宏的展开
println!("Hello, {}!", $name);
};
}

fn main() {
// 调用宏
greet!("World");
}

说明

  • 模式匹配: 宏通过模式匹配来匹配传递给宏的代码片段,模式是宏规则的左侧部分,用于捕获不同的代码结构。
  • 规则: 宏规则是一组由 $ 引导的模式和相应的展开代码,规则由分号分隔。
  • 宏的展开: 当宏被调用时,匹配的模式将被替换为相应的展开代码,展开代码是宏规则的右侧部分。
实例

下面是一个更复杂的例子,演示了如何使用宏创建一个简单的 vec! 宏,以便更方便地创建 Vec:

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
// 宏的定义
macro_rules! vec {
// 基本情况,空的情况
() => {
Vec::new()
};

// 递归情况,带有元素的情况
($($element:expr),+ $(,)?) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($element);
)+
temp_vec
}
};
}

fn main() {
// 调用宏
let my_vec = vec![1, 2, 3];
println!("{:?}", my_vec); // 输出: [1, 2, 3]

let empty_vec = vec![];
println!("{:?}", empty_vec); // 输出: []
}

在这个例子中,vec! 宏使用了模式匹配,以及 element:expr),+ $(,)?) 这样的语法来捕获传递给宏的元素,并用它们创建一个 Vec。

注意,$(,)?) 用于处理末尾的逗号,使得在不同的使用情境下都能正常工作。

($($element:expr),+ $(,)?) 是 Rust 声明宏中用于模式匹配的一部分。它的具体含义如下:

  • $($element:expr):这是一个重复匹配的部分。其中 $element:expr 表示匹配一个表达式,并将其绑定到元变量 $element 上。$() 表示可以匹配零个或多个这样的表达式。
  • ,+:表示在匹配了前面的表达式之后,紧接着可以有一个或多个逗号。
  • $(,)?):这里的 $(,)? 表示可选的单个逗号。其中 ? 表示该部分是可选的,即可以有逗号,也可以没有逗号。

总的来说,($($element:expr),+ $(,)?) 用于匹配一个或多个表达式,这些表达式之间可以用逗号分隔,并且最后可能有一个可选的逗号。

过程宏(Procedural Macros)

过程宏是一种更为灵活和强大的宏,允许在编译时通过自定义代码生成过程来操作抽象语法树(AST)。过程宏在功能上更接近于函数,但是它们在编写和使用上更加复杂。

过程宏的类型:

  • 派生宏(Derive Macros) :用于自动实现trait(比如 CopyDebug)的宏。
  • 属性宏(Attribute Macros) :用于在声明上附加额外的元数据,如 #[derive(Debug)]

过程宏的实现通常需要使用 proc_macro 库提供的功能,例如 TokenStream 和 TokenTree,以便更直接地操纵源代码。

Rust 智能指针

智能指针(Smart pointers)是一种在 Rust 中常见的数据结构,它们提供了额外的功能和安全性保证,以帮助管理内存和数据。

在 Rust 中,智能指针是一种封装了对动态分配内存的所有权和生命周期管理的数据类型。

智能指针通常封装了一个原始指针,并提供了一些额外的功能,比如引用计数、所有权转移、生命周期管理等。

在 Rust 中,标准库提供了几种常见的智能指针类型,例如 Box、Rc、Arc 和 RefCell。

智能指针的使用场景:

  • 当需要在堆上分配内存时,使用 Box<T>
  • 当需要多处共享所有权时,使用 Rc<T>Arc<T>
  • 当需要内部可变性时,使用 RefCell<T>
  • 当需要线程安全的共享所有权时,使用 Arc<T>
  • 当需要互斥访问数据时,使用 Mutex<T>
  • 当需要读取-写入访问数据时,使用 RwLock<T>
  • 当需要解决循环引用问题时,使用 Weak<T>
Box <T> 智能指针

Box <T> 是 Rust 中最简单的智能指针之一,它允许在堆上分配一块内存,并将值存储在这个内存中。

由于 Rust 的所有权规则,使用 Box 可以在堆上创建具有已知大小的数据。

1
2
let b = Box::new(5);
println!("b = {}", b);
Rc <T> 智能指针

Rc <T>(引用计数指针)允许多个所有者共享数据,它使用引用计数来跟踪数据的所有者数量,并在所有者数量为零时释放数据。

Rc <T> 适用于单线程环境下的数据共享。

1
2
3
4
use std::rc::Rc;

let data = Rc::new(5);
let data_clone = Rc::clone(&data);
Arc <T> 智能指针

Arc <T>(原子引用计数指针)与 Rc <T> 类似,但是可以安全地在多线程环境中共享数据,因为它使用原子操作来更新引用计数。

1
2
3
4
use std::sync::Arc;

let data = Arc::new(5);
let data_clone = Arc::clone(&data);
RefCell <T> 智能指针

RefCell <T> 允许在运行时检查借用规则,它使用内部可变性来提供了一种安全的内部可变性模式,允许在不可变引用的情况下修改数据。

但是,RefCell <T> 只能用于单线程环境。

1
2
3
4
5
use std::cell::RefCell;

let data = RefCell::new(5);
let mut borrowed_data = data.borrow_mut();
*borrowed_data = 10;
Mutex <T> 智能指针

Mutex <T> 是一个互斥锁,它保证了在任何时刻只有一个线程可以访问 Mutex 内部的数据。

1
2
3
4
use std::sync::Mutex;

let m = Mutex::new(5);
let mut data = m.lock().unwrap();
RwLock <T> 智能指针

RwLock <T> 是一种读取-写入锁,允许多个读取者同时访问数据,但在写入时是排他的。

1
2
3
4
use std::sync::RwLock;

let lock = RwLock::new(5);
let read_guard = lock.read().unwrap();
Weak <T> 智能指针

Weak <T> 是 Rc <T> 的非拥有智能指针,它不增加引用计数,用于解决循环引用问题。

1
2
3
4
use std::rc::{Rc, Weak};

let five = Rc::new(5);
let weak_five = Rc::downgrade(&five);
智能指针的生命周期管理

智能指针可以帮助管理数据的生命周期,当智能指针被销毁时,它们会自动释放内存,从而避免了内存泄漏和野指针的问题。

此外,智能指针还允许在创建时指定特定的析构函数,以实现自定义的资源管理。

实例

下面是一个简单的 Rust 智能指针完整实例,该示例使用 Rc <T> 智能指针实现了一个简单的引用计数功能,并演示了多个所有者共享数据的情况。

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
// 引入所需的依赖库
use std::rc::Rc;

// 定义一个结构体,用于存储数据
#[derive(Debug)]
struct Data {
value: i32,
}

// 主函数
fn main() {
// 创建一个 Rc 智能指针,共享数据
let data = Rc::new(Data { value: 5 });

// 克隆 Rc 智能指针,增加数据的引用计数
let data_clone1 = Rc::clone(&data);
let data_clone2 = Rc::clone(&data);

// 输出数据的值和引用计数
println!("Data value: {}", data.value);
println!("Reference count: {}", Rc::strong_count(&data));

// 打印克隆后的 Rc 智能指针
println!("Data clone 1: {:?}", data_clone1);
println!("Data clone 2: {:?}", data_clone2);
}

以上代码中,我们首先定义了一个 Data 结构体,用于存储一个整数值。然后在 main 函数中创建了一个 Rc<Data> 智能指针,用于共享数据。接着通过 Rc::clone 方法克隆了两个智能指针,增加了数据的引用计数。最后打印了数据的值、引用计数和克隆后的智能指针。

运行该程序,可以看到输出了数据的值和引用计数,以及克隆后的智能指针。由于 Rc 智能指针使用引用计数来跟踪数据的所有者数量,因此在每次克隆时,数据的引用计数会增加,当所有者数量为零时,数据会被自动释放。

输出结果如下:

1
2
3
4
Data value:5
Reference count:3
Data clone 1:Data{ value:5}
Data clone 2:Data{ value:5}
总结

Rust 的智能指针提供了一种安全和自动化的方式来管理内存和共享所有权。

智能指针是 Rust 中非常重要的一种数据结构,它们提供了一种安全、灵活和方便的内存管理方式,帮助程序员避免了常见的内存安全问题,提高了代码的可靠性和可维护性。

智能指针是 Rust 安全性模型的重要组成部分,允许开发者编写低级代码而不必担心内存安全问题。

通过智能指针,Rust 既保持了 C 语言的控制能力,又避免了其风险。

Rust 并发编程

安全高效的处理并发是 Rust 诞生的目的之一,主要解决的是服务器高负载承受能力。

并发(concurrent)的概念是指程序不同的部分独立执行,这与并行(parallel)的概念容易混淆,并行强调的是"同时执行"。

并发往往会造成并行。

本章讲述与并发相关的编程概念和细节。

线程

线程(thread)是一个程序中独立运行的一个部分。

线程不同于进程(process)的地方是线程是程序以内的概念,程序往往是在一个进程中执行的。

在有操作系统的环境中进程往往被交替地调度得以执行,线程则在进程以内由程序进行调度。

由于线程并发很有可能出现并行的情况,所以在并行中可能遇到的死锁、延宕错误常出现于含有并发机制的程序。

为了解决这些问题,很多其它语言(如 Java、C#)采用特殊的运行时(runtime)软件来协调资源,但这样无疑极大地降低了程序的执行效率。

C/C++ 语言在操作系统的最底层也支持多线程,且语言本身以及其编译器不具备侦察和避免并行错误的能力,这对于开发者来说压力很大,开发者需要花费大量的精力避免发生错误。

Rust 不依靠运行时环境,这一点像 C/C++ 一样。

但 Rust 在语言本身就设计了包括所有权机制在内的手段来尽可能地把最常见的错误消灭在编译阶段,这一点其他语言不具备。

但这不意味着我们编程的时候可以不小心,迄今为止由于并发造成的问题还没有在公共范围内得到完全解决,仍有可能出现错误,并发编程时要尽量小心!

Rust 中通过 std::thread::spawn 函数创建新线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use std::thread;
use std::time::Duration;

fn spawn_function() {
for i in 0..5 {
println!("spawned thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
}

fn main() {
thread::spawn(spawn_function);

for i in 0..3 {
println!("main thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
}

运行结果:

1
2
3
4
5
6
main thread print0
spawned thread print0
main thread print1
spawned thread print1
main thread print2
spawned thread print2

这个结果在某些情况下顺序有可能变化,但总体上是这样打印出来的。

此程序有一个子线程,目的是打印 5 行文字,主线程打印三行文字,但很显然随着主线程的结束,spawn 线程也随之结束了,并没有完成所有打印。

std::thread::spawn 函数的参数是一个无参函数,但上述写法不是推荐的写法,我们可以使用闭包(closures)来传递函数作为参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use std::thread;
use std::time::Duration;

fn main() {
thread::spawn(|| {
for i in 0..5 {
println!("spawned thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
});

for i in 0..3 {
println!("main thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
}

闭包是可以保存进变量或作为参数传递给其他函数的匿名函数。闭包相当于 Rust 中的 Lambda 表达式,格式如下:

1
2
3
|参数1,参数2,...|->返回值类型{
// 函数体
}

例如:

1
2
3
4
5
6
fn main() {
let inc = |num: i32| -> i32 {
num + 1
};
println!("inc(5) = {}", inc(5));
}

运行结果:

1
inc(5)=6

闭包可以省略类型声明使用 Rust 自动类型判断机制:

1
2
3
4
5
6
fn main() {
let inc = |num| {
num + 1
};
println!("inc(5) = {}", inc(5));
}

结果没有变化。

join 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use std::thread;
use std::time::Duration;

fn main() {
let handle = thread::spawn(|| {
for i in 0..5 {
println!("spawned thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
});

for i in 0..3 {
println!("main thread print {}", i);
thread::sleep(Duration::from_millis(1));
}

handle.join().unwrap();
}

运行结果:

1
2
3
4
5
6
7
8
main thread print0 
spawned thread print0
spawned thread print1
main thread print1
spawned thread print2
main thread print2
spawned thread print3
spawned thread print4

join 方法可以使子线程运行结束后再停止运行程序。

move 强制所有权迁移

这是一个经常遇到的情况:

1
2
3
4
5
6
7
8
9
10
11
use std::thread;

fn main() {
let s = "hello";

let handle = thread::spawn(|| {
println!("{}", s);
});

handle.join().unwrap();
}

在子线程中尝试使用当前函数的资源,这一定是错误的!因为所有权机制禁止这种危险情况的产生,它将破坏所有权机制销毁资源的一定性。我们可以使用闭包的 move 关键字来处理:

1
2
3
4
5
6
7
8
9
10
11
use std::thread;

fn main() {
let s = "hello";

let handle = thread::spawn(move || {
println!("{}", s);
});

handle.join().unwrap();
}
消息传递

Rust 中一个实现消息传递并发的主要工具是通道(channel),通道有两部分组成,一个发送者(transmitter)和一个接收者(receiver)。

std::sync::mpsc 包含了消息传递的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::thread;
use std::sync::mpsc;

fn main() {
let (tx, rx) = mpsc::channel();

thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});

let received = rx.recv().unwrap();
println!("Got: {}", received);
}

运行结果:

1
Got: hi

子线程获得了主线程的发送者 tx,并调用了它的 send 方法发送了一个字符串,然后主线程就通过对应的接收者 rx 接收到了。

Rust 异步编程 async/await

在现代编程中,异步编程变得越来越重要,因为它允许程序在等待 I/O 操作(如文件读写、网络通信等)时不被阻塞,从而提高性能和响应性。

异步编程是一种在 Rust 中处理非阻塞操作的方式,允许程序在执行长时间的 I/O 操作时不被阻塞,而是在等待的同时可以执行其他任务。

Rust 提供了多种工具和库来实现异步编程,包括 asyncawait 关键字、futures 和异步运行时(如 tokio、async-std 等),以及其他辅助工具。

  • Future :Future 是 Rust 中表示异步操作的抽象。它是一个可能还没有完成的计算,将来某个时刻会返回一个值或一个错误。
  • async/awaitasync 关键字用于定义一个异步函数,它返回一个 Future。await 关键字用于暂停当前 Future 的执行,直到它完成。
实例

以下实例展示了如何使用 async 和 await 关键字编写一个异步函数,以及如何在异步函数中执行异步任务并等待其完成。

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
// 引入所需的依赖库
use tokio;

// 异步函数,模拟异步任务
async fn async_task() -> u32 {
// 模拟异步操作,等待 1 秒钟
tokio::time::delay_for(std::time::Duration::from_secs(1)).await;
// 返回结果
42
}

// 异步任务执行函数
async fn execute_async_task() {
// 调用异步任务,并等待其完成
let result = async_task().await;
// 输出结果
println!("Async task result: {}", result);
}

// 主函数
#[tokio::main]
async fn main() {
println!("Start executing async task...");
// 调用异步任务执行函数,并等待其完成
execute_async_task().await;
println!("Async task completed!");
}

以上代码中,我们首先定义了一个异步函数 async_task(),该函数模拟了一个异步操作,使用 tokio::time::delay_for() 方法来等待 1 秒钟,然后返回结果 42。接着定义了一个异步任务执行函数 execute_async_task(),在其中调用了异步函数,并使用 await 关键字等待异步任务的完成。最后在 main 函数中使用 tokio::main 宏来运行异步任务执行函数,并等待其完成。

运行该程序,可以看到程序输出了开始执行异步任务的提示,然后等待了 1 秒钟后输出了异步任务的结果,并最终输出了异步任务完成的提示:

1
2
3
Start executing async task...
Async task result:42
Async task completed!

这个例子演示了 Rust 中使用 asyncawait 关键字编写异步函数,以及如何在异步函数中执行异步任务并等待其完成。

以下实例使用 tokio 库执行异步 HTTP 请求,并输出响应结果:

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
// 引入所需的依赖库
use std::error::Error;
use tokio::runtime::Runtime;
use reqwest::get;

// 异步函数,用于执行 HTTP GET 请求并返回响应结果
async fn fetch_url(url: &str) -> Result<String, Box<dyn Error>> {
// 使用 reqwest 发起异步 HTTP GET 请求
let response = get(url).await?;
let body = response.text().await?;
Ok(body)
}

// 异步任务执行函数
async fn execute_async_task() -> Result<(), Box<dyn Error>> {
// 发起异步 HTTP 请求
let url = "https://jsonplaceholder.typicode.com/posts/1";
let result = fetch_url(url).await?;
// 输出响应结果
println!("Response: {}", result);
Ok(())
}

// 主函数
fn main() {
// 创建异步运行时
let rt = Runtime::new().unwrap();
// 在异步运行时中执行异步任务
let result = rt.block_on(execute_async_task());
// 处理异步任务执行结果
match result {
Ok(_) => println!("Async task executed successfully!"),
Err(e) => eprintln!("Error: {}", e),
}
}

以上代码中,我们首先引入了 tokio 和 reqwest 库,分别用于执行异步任务和进行 HTTP 请求。然后定义了一个异步函数 fetch_url,用于执行异步的 HTTP GET 请求,并返回响应结果。

接着定义了一个异步任务执行函数 execute_async_task,该函数在其中发起了异步 HTTP 请求,并输出响应结果。

最后,在 main 函数中创建了一个 tokio 异步运行时,并在其中执行了异步任务,处理了异步任务的执行结果。

运行该程序,可以看到输出了异步 HTTP 请求的响应结果,实例中请求了 JSONPlaceholder 的一个帖子数据,并打印了其内容。

异步编程说明
async 关键字

async 关键字用于定义异步函数,即返回 Future 或 impl Future 类型的函数。异步函数执行时会返回一个未完成的 Future 对象,它表示一个尚未完成的计算或操作。

异步函数可以包含 await 表达式,用于等待其他异步操作的完成。

1
2
3
async fn hello() -> String {
"Hello, world!".to_string()
}
await 关键字

await 关键字用于等待异步操作的完成,并获取其结果。

await 表达式只能在异步函数或异步块中使用,它会暂停当前的异步函数执行,等待被等待的 Future 完成,然后继续执行后续的代码。

1
2
3
4
async fn print_hello() {
let result = hello().await;
println!("{}", result);
}
异步函数返回值

异步函数的返回值类型通常是 impl Future<Output = T>,其中 T 是异步操作的结果类型。由于异步函数的返回值是一个 Future,因此可以使用 .await 来等待异步操作的完成,并获取其结果。

1
2
3
async fn add(a: i32, b: i32) -> i32 {
a + b
}
异步块

除了定义异步函数外,Rust 还提供了异步块的语法,可以在同步代码中使用异步操作。异步块由 async { } 构成,其中可以包含异步函数调用和 await 表达式。

1
2
3
4
5
async {
let result1 = hello().await;
let result2 = add(1, 2).await;
println!("Result: {}, {}", result1, result2);
};
异步任务执行

在 Rust 中,异步任务通常需要在执行上下文中运行,可以使用 tokio::mainasync-stdtask::block_onfutures::executor::block_on 等函数来执行异步任务。这些函数会接受一个异步函数或异步块,并在当前线程或执行环境中执行它。

1
2
3
4
5
use async_std::task;

fn main() {
task::block_on(print_hello());
}
错误处理

await 后面跟一个 ? 操作符可以传播错误。如果 await 的 Future 完成时返回了一个错误,那么这个错误会被传播到调用者。

1
2
3
4
async fn my_async_function() -> Result<(), MyError> {
some_async_operation().await?;
// 如果 some_async_operation 出错,错误会被传播
}
异步 trait 方法

Rust 允许为 trait 定义异步方法。这使得你可以为不同类型的对象定义异步操作。

1
2
3
4
5
6
7
8
9
trait MyAsyncTrait {
async fn async_method(&self) -> Result<(), MyError>;
}

impl MyAsyncTrait for MyType {
async fn async_method(&self) -> Result<(), MyError> {
// 异步逻辑
}
}
异步上下文

在 Rust 中,异步代码通常在异步运行时(如 Tokio 或 async-std)中执行。这些运行时提供了调度和执行异步任务的机制。

1
2
3
4
#[tokio::main]
async fn main() {
some_async_operation().await;
}

以上代码中,#[tokio::main] 属性宏将 main 函数包装在一个异步运行时中。

异步宏

Rust 提供了一些异步宏,如 tokio::spawn,用于在异步运行时中启动新的异步任务。

1
2
3
4
5
6
7
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
// 异步逻辑
});
handle.await.unwrap();
}
异步 I/O

Rust 的标准库提供了异步 I/O 操作,如 tokio::fs::Fileasync_std::fs::File

1
2
3
4
5
6
7
8
9
10
11
use tokio::fs::File;
use tokio::io::{self, AsyncReadExt};

#[tokio::main]
async fn main() -> io::Result<()> {
let mut file = File::open("file.txt").await?;
let mut contents = String::new();
file.read_to_string(&mut contents).await?;
println!("Contents: {}", contents);
Ok(())
}
异步通道

Rust 的一些异步运行时提供了异步通道(如 tokio::sync::mpsc),允许在异步任务之间传递消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use tokio::sync::mpsc;
use tokio::spawn;

#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(32);

let child = spawn(async move {
let response = "Hello, world!".to_string();
tx.send(response).await.unwrap();
});

let response = rx.recv().await.unwrap();
println!("Received: {}", response);

child.await.unwrap();
}

.vscode 中tasks.json和launch.json的作用是什么?

.vscode 文件夹中:

tasks.json 的作用

tasks.json 用于定义在 Visual Studio Code 中可以执行的任务。这些任务可以是编译代码、运行测试、执行脚本等各种自定义的操作。

例如,如果您正在开发一个使用特定编程语言的项目,您可以配置一个编译任务,指定编译器的命令和参数,以便在 VS Code 中轻松地编译您的代码。

再比如,您可能有一个需要在特定目录下运行的脚本,也可以在 tasks.json 中定义这个任务。

launch.json 的作用

launch.json 主要用于配置调试(Debug)相关的设置。

它允许您定义如何启动和调试您的应用程序或脚本。您可以指定调试的类型(例如 Node.js、Python、Java 等)、启动的文件、传递的参数、环境变量等。

例如,如果您正在开发一个 Node.js 应用程序,您可以在 launch.json 中配置如何启动服务器,并在调试过程中设置断点、查看变量值等。

以下是一个简单的示例:

tasks.json 中定义一个简单的 JavaScript 编译任务:

1
2
3
4
5
6
7
8
9
10
{
"version": "2.0.0",
"tasks": [
{
"label": "Compile JavaScript",
"type": "shell",
"command": "babel src -d dist"
}
]
}

launch.json 中定义一个 Node.js 调试配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Node.js",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/app.js",
"stopOnEntry": false,
"args": [],
"cwd": "${workspaceFolder}",
"preLaunchTask": "Compile JavaScript"
}
]
}

在上述示例中,preLaunchTask 字段指定在调试之前先执行 Compile JavaScript 这个任务。

bpfcc在5.10内核上不可用解决:

需要重新编译bcc,参考这个文档 https://github.com/realwujing/ebpf-learning/blob/1d4dc60cef79a16354520fbda4f44b7013275e47/BCC.md

1)安装依赖 sudo apt-get -y install bison build-essential cmake flex git libedit-dev llvm-13-dev libclang-13-dev python zlib1g-dev libelf-dev libfl-dev python3-distutils

依赖里面 llvm-13-dev libclang-13-dev 没有,可以添加debian源,然后再下。 添加debian源方法: cat /etc/debian_version 确定debian版本,10对应buster,百度搜索debian 清华源 ,选择buster

2)下载代码: git clone https://github.com/iovisor/bcc.git git checkout -b v0.6.1 3)编译: 在执行git clone的目录写一个脚本 #!/bin/bash set -ex mkdir bcc/build cd bcc/build cmake .. make sudo make install cmake -DPYTHON_CMD=python3 .. # build python3 binding pushd src/python/ make sudo make install popd 然后执行 4)设置环境变量 需要先sudo -i echo "export PATH=$PATH:/usr/share/bcc/tools" >> /etc/profile sudo visudo后追加/usr/share/bcc/tools到secure_path后面 source /etc/profile

5)修改python默认指向python3 sudo update-alternatives --install /usr/bin/python python /usr/bin/python2 100 sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 150 sudo update-alternatives --config python

编译出来的程序是trace; 直接 sudo trace运行,有时候需要重启

curl

curl 是一个用于通过各种网络协议(如 HTTP、FTP 等)获取数据的工具

1723693919066
1
2
3
4
5
curl -O https://example.com/file.txt #-O 选项表示使用服务器上的文件名来保存下载的文件
curl -o custom_name.txt https://example.com/file.txt #-o 选项指定自定义的文件名来保存下载的文件
curl --proxy 127.0.0.1:7890 www.google.com
curl -s https://install.zerotier.com/ | sudo bash #curl读取并执行脚本。-s, --silent Silent mode
curl -sfL https://get.k3s.io | sh - #-s 表示静默模式,不显示进度或错误信息(除非是严重错误)。-f 表示如果服务器返回错误,curl 直接报错退出。-L 表示如果服务器返回的是重定向的响应,curl 会跟随重定向。

内核编译:

284 ls arch/ 285 ls arch/arm64/configs/ 286 make merge_kirin990_armpc_defconfig 287 sudo apt install bison 288 make merge_kirin990_armpc_defconfig 289 sudo apt install flex 290 make merge_kirin990_armpc_defconfig 291 make menuconfig 292 lscpu 294 make bindeb-pkg -j10 编出来后安装linux-headerXXX.deb linux-image-xxx.deb

sudo apt install bison flex cp /boot/config-4.19.0-amd64-desktop arch/x86/configs/icbc_x86_defconfig make icbc_x86_defconfig make menuconfig 不需要 lscpu vim scripts/package/Makefile 注释掉deepin-apigail-generate的2行

查看回调函数具体调用的函数名 在Linux内核代码里面,很多结构体里面都有一些函数指针。在调试过程中,经常不知道该指针具体调用的是哪一个函数,那么有下面两种方法可以进行查看: 方法一:打印堆栈信息 在使用函数指针的后面,调用dump_stack()函数。这个函数会把堆栈信息打印出来,通过堆栈信息,就可以知道函数指针具体调用的是哪一个函数。 方法二:使用lookup_symbol_name函数 函数原型:int lookup_symbol_name(unsigned long addr, char *symname) 说明: addr参数-传入函数指针的值 symname参数-保存函数名字的首地址 注:要使用lookup_symbol_name函数,需要包含#include <linux/kallsyms.h>头文件 Oops: 0002 [#1] SMP 0002这个是 OOPS 信息的错误码

bit 0 0 means no page found, 1 means a protection fault bit 1 0 means read, 1 means write bit 2 0 means kernel, 1 means user-mode 位0 0 表示找不到页面,1表示保护故障 bit 1 0表示已读,1表示已写 位2 0 表示内核,1表示用户模式

[#1] — this value is the number of times the Oops occurred. Multiple Oops can be triggered as a cascading effect of the first one. 这个值是 Oops 发生的次数, 多个 Oops 可以级联效应触发

CPU: 0 PID: 1313 Comm: kworker/u4:0 Tainted: G D 3.14.52-1.1.1_ga #40 表示发生在CPU 0,当前运行的进程是1313 进程是kworker/u4:0 Tainted标识为:G 内核版本:3.14.52-1.1.1_ga 操作系统#40 ———————————————— 版权声明:本文为CSDN博主「monkey_lqd」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/a3121772305/article/details/116885768

内核中函数指针用的很多,在debug 的时候能直接打印出一个函数指针对应的函数就会很方便。

打印裸指针(raw pointer)用 %p,%p除了可以用来打印指针外还可以打印其它的信息

%pF可打印函数指针的函数名和偏移地址,%pf只打印函数指针的函数名,不打印偏移地址。

printk("%pf %pF", ptr, ptr) will print:

module_start module_start+0x0/0x62 [hello]

但是为了支持这个功能你需要开启CONFIG_KALLSYMS 选项

打印函数栈 用户态 打印函数栈 使用backtrace()相关函数来达到输出函数栈的目的,man backtrace查看详细的参数,返回值等信息 简单说下这3个函数的用法:

int backtrace(void **buffer, int size); backtrace()把函数栈中的函数地址写到buffer数组,buffer数组的成员即void *类型。size表示把栈顶size个函数地址搞出来,如果想把函数栈完全搞出来,确保buffer数组和size足够大。

char backtrace_symbols(void const buffer, int size); void backtrace_symbols_fd(void const buffer, int size, int fd); backtrace_symbols()或者backtrace_symbols_fd()与上边的backtrace()配合使用,起一个翻译作用,即把函数地址翻译为函数名(还有函数的偏移和返回地址)。backtrace_symbols()把buffer数组中的size个元素翻译为函数名,把字符串数组保存在返回值char 中,自带了malloc功能,用完需把返回值free,见测试例。backtrace_symbols_fd()前两个参数和前者一样,但是把结果字符串数组输出到fd中,如fd=1,即输出到屏幕 由函数指针得到函数名 根据以上可以看出,backtrace_symbols和backtrace_symbols_fd可以用来将函数指针转换为函数名 内核态 打印函数栈 使用dump_stack()函数。直接在需要查看函数栈的位置加入这一句,然后dmesg中就可以看到函数栈了。

查找分区的uuid grub-probe -t fs_uuid /boot

bash :local a=0 声明一个局部变量 判断一个命令是否存在:if ! command -v fsck >/dev/null 2>&1;then 将命令结果保存到指定目录logsave -a -s root_fstype" "ROOT" ]; then $?用于查找最后执行的命令的返回值

$$ Shell本身的PID(ProcessID) ? 最后运行的命令的结束代码(返回值) 使 所有参数列表。如"$"用「"」括起来的情况、以"2 … @ 所有参数列表。如"1" "n" 的形式输出所有参数。 You can't use 'macro parameter character #' in math mode# 添加到Shell的参数个数 0 Shell本身的文件名 n 添加到Shell的各参数值。2是第2参数…。

获取当前终端 read -r console rest </proc/consoles if [ "You can't use 'macro parameter character #' in math mode{console}" = "tty0" ]; then # Need to choose a specific VT console="tty1" fi 获取所有sh(应该也可以用其他程序)的pid集合 pids=(pidof sh) 获取当前pid self_pid= $$ 如何判断是否在real environment还是在initramfs中:看看dde-dock存不存在 ## vscode快捷键 ### 展开和折叠代码 #### macOS 在 macOS 上,你可以使用以下快捷键来折叠和展开代码: * **折叠当前级别** :`Command` + `Option` + `[` * **展开当前级别** :`Command` + `Option` + `]` * **折叠所有代码** :`Command` + `K` 然后 `Command` + `0` * **展开所有代码** :`Command` + `K` 然后 `Command` + `J` #### Windows 在 Windows 上,你可以使用以下快捷键来折叠和展开代码: **折叠当前级别** :`Ctrl` + `Shift` + `[` **展开当前级别** :`Ctrl` + `Shift` + `]` **折叠所有代码** :`Ctrl` + `K` 然后 `Ctrl` + `0` **展开所有代码** :`Ctrl` + `K` 然后 `Ctrl` + `J` ### 打开命令窗口 F1或者command + shift + p ![1721828678402](命令/1721828678402.png) ### 切换页面 `ctrl+1~9` ### 在 Mac 终端中通过输入 `code.`来打开 VS Code 要在 Mac 终端中通过输入 `code.`来打开 VS Code,可以按照以下步骤进行设置: 方法一: 1. 打开 VS Code。 2. 使用快捷键 `command + shift + p`打开命令窗口。 3. 在命令窗口中输入 `shell command`,找到并点击提示中的 `shell command: install ‘code’ command in path`,运行该命令。 这种方法简单快捷,但设置的命令在电脑重启后可能会失效。 ### 代码编辑 6)在当前行下方插入一行: Ctrl + Enter(mac 是 command) 7)在当前行上方插入一行: Ctrl + Shift + Enter(mac 是 command) ## chrome快捷键 ### 切换页面 `cmd+1~9` `ctrl+tab 或ctrl+shift+tab或ctrl+pgup或ctrl+pagedown` ## quake 3算法 计算 `float y = 1 / sqrt (x) ;` **参考视频:https://www.bilibili.com/video/BV1ym421g7tz/?buvid=Z2448B2CF12E9AC1456080ED1C48B091E8F7&from_spmid=main.my-history.0.0&is_story_h5=false&mid=FPr3cOaw3%2Fuu5BD4j4onuw%3D%3D&p=1&plat_id=116&share_from=ugc&share_medium=iphone&share_plat=ios&share_session_id=371B5190-2E4C-4290-A7BB-93C1B390D45B&share_source=COPY&share_tag=s_i&spmid=united.player-video-detail.0.0&timestamp=1721631862&unique_k=lASYA5Q&up_id=1263732318&vd_source=69d49a91e3d96bad6e2f1ea1eb1f6c22** ![1721706464469](命令/1721706464469.png) **注意这个算法要求number非负数,非0,且取得是近似值**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
float Q_rsqrt( float number )
{
long i;//占32位
float ×2, y;//均占32位
const float threehalfs = 1.5F;//1.5 also 32位

X2 = number * 0.5F;
y = number;
i = * ( long * ) &у;// evil floating point bit hack通过强制类型转换将浮点数 y 的地址当作长整型的地址来获取其底层的位表示
i = 0x5f3759df - ( i >> 1 );// what the fuck?
y = * (float * ) &i;//又通过强制类型转换将长整型 i 的位表示转换回浮点数赋值给 y
y = y * ( threehalfs - ( x2 * y * y ) );// 1st iteration
// y = y * ( threehalfs - ( x2 * y * y ) );// 2nd iteration, can be removed
return y;
}

上面的 i = * ( long * ) &у实际就等于M+2^23*E,其中M和E是y这个浮点数用IEEE754表示法的尾数和指数(实际把32位数写出来再看很好理解)

log2(y)=1721707887167

至于上面这个公式实际就是计算log2(y)的近似值,推导过程见视频,其中上面的mu是一个0到1的未确定的值,但会使得约等于的两边取得最小的误差

所以经过 i = * ( long * ) &у后,i会约等于log2(y)

而log(1/(y^1/2))=-1/2log2(y)=-(y>>1)

1721717289233
1721717410864

y = * (float * ) &i;//又通过强制类型转换将长整型 i 的位表示转换回浮点数赋值给 y这一步就不讲了,跟上面的步骤反过来就是了

y = y * ( threehalfs - ( x2 * y * y ) );// 1st iteration

经过上面的步骤,我们得到一个不错的近似值,但同时也引入了一些误差项,得益于牛顿迭代法,可以从一个相当不错的近似值出发,得到一个相当精确的近似结果

牛顿迭代法(Newton's Method)

牛顿迭代法是一种用于求解方程根的数值方法。

它的基本思想是通过不断地用切线来逼近函数的根。

假设我们要求解方程 f(x) = 0 的根,首先我们需要选取一个初始的近似值 x0

然后,通过以下公式进行迭代计算:

x(n + 1) = x(n) - f(x(n)) / f'(x(n))

其中 f'(x) 是函数 f(x) 的导数。

每次迭代都会使新的近似值更接近方程的根。

例如,要求解方程 x^2 - 5 = 0 ,其导数为 2x

假设初始近似值 x0 = 2 ,则:

第一次迭代:x1 = 2 - (2^2 - 5) / (2 * 2) = 2.25

第二次迭代:x2 = 2.25 - (2.25^2 - 5) / (2 * 2.25) ≈ 2.2361

以此类推,不断迭代,逐渐逼近方程的根。

牛顿迭代法的优点是收敛速度较快,但它也有一些局限性。比如,要求函数在根的附近具有较好的性质,且导数的计算要准确和方便。

在实际应用中,牛顿迭代法常用于求解非线性方程、计算函数的零点、优化问题等领域。

1721717907153
1721718035706
1721718082505
1721718125314

对于 y = y * (threehalfs - (x2 * y * y)) 这一行,它是牛顿迭代法的应用。

1721719272279
1721719295174
1721719321991

Whether in the real environment

real_env=1 if [ -f /bin/dde-dock ]; then real_env=0 LOG_FILE=/tmp/$PROJ.log fi

shell log: log_info() { echo "$*" >&2 echo "TUI: LOG_FILE }

trap用法: trap clean_boot EXIT 起因 项目中的升级脚本可能耗时很长,在这段时间内,脚本没有任何输出的,这带给市场部署人员的感觉就是脚本好像卡住了。通常情况下,部署人员都会直接CTRL+C停掉升级脚本,这会导致升级失败,最终需要开发人员介入去修复环境。

可以通过输出升级进度的方式提示部署人员升级正在进行中,但进度也可能在一段时间不动,而且无法避免意外终止升级的情况,此时可以使用Shell的内建命令trap来忽略SIGINT这些信号,保证升级不会中断。 trap [-lp] [[arg] sigspec ...] arg可以是shell命令或者自定义函数 sigspec可以是以下的一个或多个 定义在<signal.h>中的信号名或者数值。信号名的大小写不敏感,SIG这个前缀也是可选的。以下的命令的效果都是一样的 trap "echo 123" SIGINT trap "echo 123" INT trap "echo 123" 2 trap "echo 123" int trap "echo 123" Int EXIT:在shell退出前执行trap设置的命令,也可以指定为0 RETURN:在函数返回时,或者.和source执行其他脚本返回时,执行trap设置的命令 DEBUG:在任何命令执行前执行trap设置的命令,但对于函数仅在函数的第一条命令前执行一次

shell中判断语句if中-z 和 -n

[ -a FILE ] 如果 FILE 存在则为真。

[ -b FILE ] 如果 FILE 存在且是一个块特殊文件则为真。

[ -c FILE ] 如果 FILE 存在且是一个字特殊文件则为真。

[ -d FILE ] 如果 FILE 存在且是一个目录则为真。

[ -e FILE ] 如果 FILE 存在则为真。

[ -f FILE ] 如果 FILE 存在且是一个普通文件则为真。

[ -g FILE ] 如果 FILE 存在且已经设置了SGID则为真。

[ -h FILE ] 如果 FILE 存在且是一个符号连接则为真。

[ -k FILE ] 如果 FILE 存在且已经设置了粘制位则为真。

[ -p FILE ] 如果 FILE 存在且是一个名字管道(F如果O)则为真。

[ -r FILE ] 如果 FILE 存在且是可读的则为真。

[ -s FILE ] 如果 FILE 存在且大小不为0则为真。

[ -t FD ] 如果文件描述符 FD 打开且指向一个终端则为真。

[ -u FILE ] 如果 FILE 存在且设置了SUID (set user ID)则为真。

[ -w FILE ] 如果 FILE 如果 FILE 存在且是可写的则为真。

[ -x FILE ] 如果 FILE 存在且是可执行的则为真。

[ -O FILE ] 如果 FILE 存在且属有效用户ID则为真。

[ -G FILE ] 如果 FILE 存在且属有效用户组则为真。

[ -L FILE ] 如果 FILE 存在且是一个符号连接则为真。

[ -N FILE ] 如果 FILE 存在 and has been mod如果ied since it was last read则为真。

[ -S FILE ] 如果 FILE 存在且是一个套接字则为真。

[ FILE1 -nt FILE2 ] 如果 FILE1 has been changed more recently than FILE2, or 如果 FILE1 exists and FILE2 does not则为真。

[ FILE1 -ot FILE2 ] 如果 FILE1 比 FILE2 要老, 或者 FILE2 存在且 FILE1 不存在则为真。

[ FILE1 -ef FILE2 ] 如果 FILE1 和 FILE2 指向相同的设备和节点号则为真。

[ -o OPTIONNAME ] 如果 shell选项 “OPTIONNAME” 开启则为真。

[ -z STRING ] “STRING” 的长度为零则为真。

[ -n STRING ] or [ STRING ] “STRING” 的长度为非零 non-zero则为真。

[ STRING1 == STRING2 ] 如果2个字符串相同。 “=” may be used instead of “==” for strict POSIX compliance则为真。

[ STRING1 != STRING2 ] 如果字符串不相等则为真。

[ STRING1 < STRING2 ] 如果 “STRING1” sorts before “STRING2” lexicographically in the current locale则为真。

[ STRING1 > STRING2 ] 如果 “STRING1” sorts after “STRING2” lexicographically in the current locale则为真。

[ ARG1 OP ARG2 ] “OP” is one of -eq, -ne, -lt, -le, -gt or -ge. These arithmetic binary operators return true if “ARG1” is equal to, not equal to, less than, less than or equal to, greater than, or greater than or equal to “ARG2”, respectively. “ARG1” and “ARG2” are integers.

cat /proc/net/netlink| grep sockid 这里面的基本都是内核传的socket sudo netstat -x | grep sockid sudo netstat -xp| grep proc cat /proc/net/unix | grep sockid

给u盘改名: sudo mlabel -i /dev/sda ::LL (label是名字)

例子:

sudo mlabel -i /dev/sda ::recovery

格式化U盘:

sudo mkfs.btrfs -f /dev/sdc

下载wpa最上游源码:

git clone git://w1.fi/srv/git/hostap.git

使用wpa开热点:

ctrl_interface=/var/run/wpa_supplicant update_config=1 ap_scan=2

network={ ssid="A-WPA3-TEST" mode=2 key_mgmt=SAE proto=RSN pairwise=CCMP psk="12345678" sae_password="12345678" ieee80211w=2 } wpa_supplicant -Dnl80211 -iwlan0 -c wpa_supplicant.conf

删除已连接的网络:

删除/etc/NetworkManager/system-connections/下,然后重启NetworkManager

vim快捷键

这几个常用:

向上翻半页`ctrl+u`
  • 向后翻半页:ctrl+d
  • 向后翻整页:ctrl+f
  • 向前翻整页:ctrl+b

突出显示字符: set hls

1
2
3
4
5
6
7
8
9
:set number 
# 或者简写为
:set nu


# 关闭行号显示
:set nonumber
# 或者简写为
:set nonu

vim快速跳转

一.按单词跳转(可以跨行跳转)

w <---> b 跳转到下个/上个单词的第一个字符处

e <---> ge 跳转到下个/上个单词的最后一个字符处

二.行内跳转

0 跳转到光标所在行的行首

$ 跳转到光标所在行的行尾

n| 跳转到光标所在行的指定列

三.行内查找并跳转

f 命令移动到光标右边的指定字符上,例如,"fm",会把移动到光标右边的第一个'm'字符上

F 命令则反方向查找,也就是移动到光标左边的指定字符上

t 命令和”f“命令的区别在于,它移动到光标右边的指定字符之前

T 命令是”t“命令的反向版本,它移动到光标右边的指定字符之后

; 命令重复前一次输入的f, t, F, T命令,

, 命令会反方向重复前一次输入的f, t, F, T命令

可以在这组命令前面使用数字,表示倍数。例如,"6fm"表示移动到光标右边的第6个'm'字符上

四、 跳转指定行

比如想跳转到文本的第12行,可以执行如下命令:

(1)12gg / 12G(建议使用这种,因为ngg/nG输入以后不需要按下回车键,而输入:n之后还要按下回车键才会跳转)

(2):12

(3)打开文件时输入vim +12 filename

五、往上或往下跳n行

5k :往上跳 5 行

5j: 往下跳 5 行

vim替换模式

使用大写 R命令,将进入替换模式(屏幕底部显示“--REPLACE--”)。此时新输入的文本将直接替代/覆盖已经存在的内容,直至点击ESC键返回常规模式。 alt text

使用小写 r命令,将进入单字符替换模式,此时新输入的字符将替代光标之下的当前字符,然后自动返回到常规模式。

通过在r命令中增加数字前缀,可以一次性替换多个字符。例如,将光标定位到“||”字符处,然后执行 2r&命令,可以将其替换为“&&”。

Vim中启用鼠标:

set mouse =a #这将在所有模式下启用鼠标。如果你只想在特定模式下启用鼠标,可以将上述配置中的“a”替换为对应的模式。各模式的区别如下:

1
2
3
4
5
6
7
8
9
10
11
12
Enable the use of the mouse.  Works for most terminals (xterm, Win32
win32-mouse, QNX pterm, *BSD console with sysmouse and Linux console
with gpm). For using the mouse in the GUI, see gui-mouse. The
mouse can be enabled for different modes:
n Normal mode and Terminal modes
v Visual mode
i Insert mode
c Command-line mode
h all previous modes when editing a help file
a all previous modes
r for hit-enter and more-prompt prompt

在Vim中,鼠标可以用于启动各种模式。当你将鼠标光标移动到屏幕边缘时,Vim将自动切换到相应的模式。例如,将光标移动到屏幕底部边缘将切换到Normal模式,移动到屏幕左侧边缘将切换到Visual模式,移动到屏幕右侧边缘将切换到Insert模式。 除了在屏幕边缘切换模式外,你还可以使用鼠标右键菜单来切换模式。在Normal模式下,将鼠标光标移动到屏幕边缘并单击右键,将弹出一个菜单,你可以从中选择要切换到的模式。

需要注意的是,启用鼠标后可能会影响Vim的操作体验。因为在Vim中,键盘快捷键是主要的操作方式,而鼠标操作可能会打破这种习惯。此外,Vim中的许多功能是通过键盘快捷键实现的,启用鼠标后可能会与这些快捷键产生冲突。因此,如果你是一个习惯使用键盘快捷键的Vim用户,建议慎重考虑是否启用鼠标。

查看vim是否支持剪切板

vim --version| grep "clipboard" 不支持的话安装: sudo apt install vim-gtk

  1. 使用快捷键# "+y和"+p的输入挺麻烦的,我们直接为他们创建个快捷键吧

vim创建快捷键的原理可以看 jalused的博客

我使用的快捷键如下

" 支持在Visual模式下,通过C-y复制到系统剪切板 vnoremap <C-y> "+y " 支持在normal模式下,通过C-p粘贴系统剪切板 nnoremap <C-p> "*p 将上面的内容copy到~/.vimrc中即可生效,效果就是:

在vim的visual模式下选中了要复制的内容,再用Ctrl + y复制到系统剪切板,直接去其他窗口中Ctrl + v粘贴 在其他窗口中选择之后(不一定要Ctrl + c),可直接在vim中用Ctrl + p来粘贴 "语法高亮度显示 syntax on "下面两行在进行编写代码时,在格式对起上很有用; "第一行,vim使用自动对起,也就是把当前行的对起格式应用到下一行; "第二行,依据上面的对起格式,智能的选择对起方式,对于类似C语言编 "写上很有用 set autoindent set smartindent "在编辑过程中,在右下角显示光标位置的状态行 set ruler

cpio格式 initrd 的解压与压缩:

https://blog.csdn.net/feisang/article/details/6121269 https://blog.sina.com.cn/s/blog_6795385f0100kdb7.html

加载initrd的过程: http://blog.chinaunix.net/uid-29073321-id-5570250.html

第二部分:initrd、initramfs

一、 initrd出现的背景 在早期的linux系统中,一般只有硬盘或者软盘被用来作为linux根文件系统的存储设备,因此也就很容易把这些设备的驱动程序集成到内核中。但是现在的嵌入式系统中可能将根文件系统保存到各种存储设备上,包括scsi、sata,u-disk等等。因此把这些设备的驱动代码全部编译到内核中显然就不是很方便。 在内核模块自动加载机制udev中,我们看到利用udevd可以实现内核模块的自动加载,因此我们希望如果存储根文件系统的存储设备的驱动程序也能够实现自动加载,那就好了。但是这里有一个矛盾,udevd是一个可执行文件,在根文件系统被挂载前,是不可能执行udevd的,但是如果udevd没有启动,那就无法自动加载存储根文件系统设备的驱动程序,同时也无法在/dev目录下建立相应的设备节点。 为了解决这一矛盾,于是出现了基于ramdisk的initrd( bootloader initialized RAM disk )。Initrd是一个被压缩过的小型根目录,这个目录中包含了启动阶段中必须的驱动模块,可执行文件和启动脚本,也包括上面提到的udevd(实现udev机制的demon)。当系统启动的时候,bootloader会把initrd文件读到内存中,然后把initrd文件在内存中的起始地址和大小传递给内核。内核在启动初始化过程中会解压缩initrd文件,然后将解压后的initrd挂载为根目录,然后执行根目录中的/init脚本(cpio格式的initrd为/init,而image格式的initrd<也称老式块设备的initrd或传统的文件镜像格式的initrd>为/initrc),您就可以在这个脚本中运行initrd文件系统中的udevd,让它来自动加载realfs(真实文件系统)存放设备的驱动程序以及在/dev目录下建立必要的设备节点。在udevd自动加载磁盘驱动程序之后,就可以mount真正的根目录,并切换到这个根目录中来。 这里只是个简单的描述,后面慢慢分析吧。

5、noinitrd (仅当内核配置了选项 CONFIG_BLK_DEV_RAM和CONFIG_BLK_DEV_INITRD)现在的内核都可以支持initrd了,引导进程首先装载内核和一个初始化的ramdisk,然后内核将initrd转换成普通的ramdisk,也就是读写模式的根文件系统设备。然后linuxrc执行,然后装载真正的根文件系统,之后ramdisk被卸载,最后执行启动序列,比如/sbin/init。

选项noinitrd告诉内核不执行上面的步骤,即使内核编译了initrd,而是把initrd的数据写到 /dev/initrd,只是这是一个一次性的设备。

Debian Linux 内核手册:

https://kernel-team.pages.debian.net/kernel-handbook/index.html

142347 【4.19自测】【桌面专业版】【1050update2】【第一轮】【PGV】【Kirin990】【DDE】双屏仅单屏显示,切换tty后再切换账户登录,a屏显示登录页面,b屏显示tty界面: 设置主屏only时候 xorg切换tty会调用10+次 drm_mode_setcrtc,调用栈基本一样 32.36100 1326 1326 Xorg drm_mode_setcrtc drm_mode_setcrtc+0x0 kernel drm_ioctl+0x1c0 kernel $x+0x50 kernel do_vfs_ioctl+0xa4 kernel ksys_ioctl+0x78 kernel __arm64_sys_ioctl+0x1c kernel el0_svc_common+0x90 kernel el0_svc_handler+0x9c kernel el0_svc+0x8 kernel GI_ioctl+0xc [libc-2.28.so] drmModeSetCrtc+0x98 [libdrm.so.2.4.0] [unknown] [amdgpu_drv.so] [unknown] [Xorg] xf86DisableUnusedFunctions+0xac [Xorg] [unknown] [Xorg] RRCrtcSet+0x22c [Xorg] ProcRRSetCrtcConfig+0x21c [Xorg] [unknown] [Xorg] [unknown] [Xorg] __libc_start_main+0xe4 [libc-2.28.so] [unknown] [Xorg] 华为机器使用的wayland,切换tty不会调用drm_mode_setcrtc,此函数用于关闭显示器. 在其他使用wayland的机器上也是如此,并且有同样的现象。说明此问题是wayland的问题。

Linux 下常用的程序调试器 gdb 是什么原理?

linux系统上有一个非常厉害的系统调用: ptrace() ,在linux上的声明是这样子的

1
extern long int ptrace (enum __ptrace_request __request, ...) __THROW;
  • long ptrace(request, pid_t pid, void addr, void data)
    • request: 宏定义,比如说PTRACE_TRACEME, PTRACE_PEEKDATA等,表示一个动作
    • pid就是一个进程的进程号
    • addr是地址
    • data是一个数据,写入动作的话,就是将这个数据写入到这个地址上,如果是读出,那么会读到这个地址上去。

他可以接管一个子进程的执行,就是跟踪一个进程,linux上的进程产生差不多是这样子:父进程先fork()一遍产生一个几乎一摸一样的自己,然后再excel()一个新进程,这时候就会将fork出来的进程替换成要执行的进程。使用ptrace()这个命令就可以跟踪这个新产生的进程。并且进行调试。代码差不多是这样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <sys/ptrace.h>
#include <unistd.h>
#include <sched.h>

int main(int argc, char* argv[]) {
if (argc < 2) {
return -1;
}

std::string program_name { argv[1] };
pid_t pid = fork();

// 子进程
if (pid == 0) {
// 设置跟踪状态
ptrace(PTRACE_TRACEME, 0, nullptr, nullptr);
execl(program_name.c_str(), program_name.c_str(), nullptr);
}
// 父进程
if (pid > 0) {
// 进行调试等操作
}
}

断点的原理

emm.我觉得断点的原理跟修改代码差不多,使用ptrace()这个无敌的函数调用可以对函数的汇编代码进行一定的修改,在x86上有一条汇编指令,叫做 ** INT 3(** 这里的INT 不是 int,而是interrupt的缩写),当出现这个指令的时候,程序就会发生中断,也就是trap了,然后父进程就可以捕获这个信号,子进程就会中断在那里,等待下一步的命令,也就是说当gdb进行打断点的时候,就是将一条汇编代码修改一下,将其修改成INT 3的机器码,而INT 3的机器码是多少呢?是0xcc, 所以说当打断点的时候,就会将原来的汇编代码对应的机器码中的机器码指令换成0xcc,当程序执行到一步的时候,就会触发断点,等到调试,那如何跳出这个断点?,将要被替换的机器码指令保存下来就可以了,等到执行的时候将其重新替换上去就可以了(相当于恢复现场)。

ptrace一次是四个字节的修改,但是0xcc是一个字节,(八位),你就需要修改四字节的低位进行处理,然后替换的时候就是将其替换到低位(也就是末位),替换到低位是因为大部分机器是小端存储,等到一执行到这里的时候就会立马发生中断,等到恢复的时候,不仅要恢复现场(将修改的数值修改到原来的位置上),并且要将寄存器的PC指针 - 1(pc寄存器向上递增的,这里要将寄存器的数值 -1 就是0xcc之前的那个位置,然后继续执行,执行到下一个断点)

打断点的本质上是在机器码上进行打断点的,那么如何知道机器码和你写的代码之间的关系呢,有一个调试信息文件,也就是dwarf文件,dwarf上会记录上一些相关的信息,如函数的low_pc()和high_pc(),(low_pc() 就是最低的那个地址,是函数开头,可以通过objdump这个工具反汇编得到的信息和dwarf文件上的信息对比一下,刚好是对应上的)函数名所在的机器码指令在哪,都可以找到,所以说在gdb上就可以直接进行在函数名上打断点了。

ptrace()能干的事情有很多,具体可以查一查手册qwq,其他原理如bt(gdb打印出函数调用)等,涉及到了函数栈帧的调用,保存在寄存器上,ptrace()也可以直接看寄存器上保存的数值,然后读取出来,做一些有用的事情。

重新安装英伟达驱动:

1、清除英伟达显卡驱动安装包: dpkg -l |grep nvidia |awk '{print $2}' |xargs sudo apt-get purge -y 2、看仓库有什么版的包: apt policy nvidia-driver 3、安装英伟达的驱动包: apt install nvidia-driver

nvidia开启drm的设置:

企业微信截图_16752202244564

问题分析:该机型同时存在两种显示驱动,一个是BIOS提供的,一个是amd驱动提供,默认控制终端显示采用bios提供的那个驱动,而那个驱动偏偏是有问题的, 如果在grub中添加一个参数:fbcon=map:1 强行让amdgpu驱动来显示控制终端就可以切换tty了

getenforce 看selinux是否打开

http://pkg.loongnix.cn/loongnix/pool/main 龙芯的源码在这个仓库地址里找。

debian官网查找类似代码的网站,可以用来参考其他包是怎么写代码的:

https://codesearch.debian.net/

获取机器的机型:sudo dmidecode -s system-family

通过dsc下载代码:

http://pkg.loongnix.cn/loongnix/pool/main/o/openjdk-8/openjdk-8_8u352-b08-lnd.8.1.12.dsc

dpkg-source -x openjdk-8_8u352-b08-lnd.8.1.12.dsc

龙芯仓库 http://pkg.loongnix.cn/loongnix/pool/main/

龙芯mips仓库http://ftp.loongnix.cn/os/loongnix/20/mips64el/pool/main/

dbus-send:

dbus-send --print-reply --session --dest=com.deepin.daemon.InputDevices /com/deepin/daemon/InputDevice/Wacom org.freedesktop.DBus.Properties.Get string:com.deepin.daemon.InputDevice.Wacom string:Exis

dbus-send --print-reply --system --type=method_call --dest=org.freedesktop.DBus / org.freedesktop.DBus.ReloadConfig

获取当前的bssid:

1
2
3
4
5
6
7
8
9
10
dbus-send --system --dest=fi.w1.wpa_supplicant1 --print-reply /fi/w1/wpa_supplicant1/Interfaces/1 org.freedesktop.DBus.Properties.Get string:fi.w1.wpa_supplicant1.Interface string:CurrentBSS

method return time=1685433175.012823 sender=:1.7 -> destination=:1.861 serial=631 reply_serial=2
variant object path "/fi/w1/wpa_supplicant1/Interfaces/1/BSSs/22"

dbus-send --system --dest=fi.w1.wpa_supplicant1 --print-reply /fi/w1/wpa_supplicant1/Interfaces/1/BSSs/22 org.freedesktop.DBus.Properties.Get string:fi.w1.wpa_supplicant1.BSS string:BSSID
method return time=1685434152.265572 sender=:1.7 -> destination=:1.1099 serial=643 reply_serial=2
variant array of bytes [
38 eb 47 bc eb d1
]

通过 D-Bus 访问接口,跟d-feet打印的类似:

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
$ gdbus introspect --system --dest org.freedesktop.hostname1 --object-path /org/freedesktop/hostname1
node /org/freedesktop/hostname1 {
interface org.freedesktop.hostname1 {
methods:
SetHostname(in s name,
in b user_interaction);
SetStaticHostname(in s name,
in b user_interaction);
SetPrettyHostname(in s name,
in b user_interaction);
SetIconName(in s name,
in b user_interaction);
SetChassis(in s name,
in b user_interaction);
signals:
properties:
readonly s Hostname = 'dhcp-192-168-47-11';
readonly s StaticHostname = 'lennarts-computer';
readonly s PrettyHostname = 'Lennart's Computer';
readonly s IconName = 'computer-laptop';
readonly s Chassis = 'laptop';
};
interface org.freedesktop.DBus.Properties {
};
interface org.freedesktop.DBus.Introspectable {
};
interface org.freedesktop.DBus.Peer {
};
};
image-20230419101036631

一种打印非以'\0'结尾的字符串的方法:

1
2
3
/* Print only 'nbytes' of 'buf', as it doesn't contain a terminating
null byte ('\0'). */
printf("'%s' points to '%.*s'\n", argv[1], (int) nbytes, buf);

Libinput:

查看libinput事件:

sudo libinput debug-events

查看按的键:

sudo libinput debug-events --show-keycodes

libevdev中libevdev-events.c文件对应的命令是什么?怎么使用

libevdev-events.c 文件是 libevdev 库中的一个示例程序,用于演示如何使用 libevdev 库处理输入事件。该文件并不是一个可执行程序,而是一个源代码文件,需要通过编译生成可执行程序。

你可以通过以下步骤编译 libevdev-events.c 文件:

  1. 确保已经安装了 libevdev 库及其头文件,以及编译工具链(如 gcc)。

  2. 下载 libevdev 源代码,并切换到对应版本的代码目录。

  3. 进入 ./test 目录,该目录下包含了 libevdev 库的一些示例程序。

  4. 使用以下命令编译 libevdev-events.c 文件:

    1
    2
    Copy code
    gcc -Wall -g -o libevdev-events libevdev-events.c -levdev

    上述命令中,-Wall 参数用于开启所有警告信息,-g 参数用于开启调试信息,-o 参数用于指定生成的可执行文件名为 libevdev-events-levdev 参数用于链接 libevdev 库。

  5. 执行编译生成的可执行文件 libevdev-events,并传递输入设备文件名作为参数,如:

    1
    2
    bashCopy code
    ./libevdev-events /dev/input/event0

    上述命令中,/dev/input/event0 是输入设备文件名,你需要将其替换为你想要监控的输入设备文件名。

执行上述命令后,libevdev-events 程序会打印出输入设备上产生的所有事件信息。你可以通过修改 libevdev-events.c 文件中的代码,来了解如何使用 libevdev 库处理输入事件。

如何确定/dev/input/eventx呢?比如我想知道触摸板设备对应哪个event(实际上没这么麻烦,直接使用libinput debug-events然后移动鼠标就行了)

您可以使用 evtest命令来列出可用的输入设备及其对应的event。下面是一些基本的使用方法:

  1. 安装evtest:在大多数Linux发行版上,您可以使用以下命令安装evtest:

    1
    2
    arduinoCopy code
    sudo apt-get install evtest

    或者

    1
    2
    Copy code
    sudo yum install evtest
  2. 列出可用的输入设备:运行以下命令以列出可用的输入设备:

    1
    2
    cssCopy code
    evtest --enumerate

    该命令将列出所有的输入设备及其对应的event,例如:

    1
    2
    3
    4
    cssCopy code/dev/input/event0    Lid Switch
    /dev/input/event1 Power Button
    /dev/input/event2 AT Translated Set 2 keyboard
    /dev/input/event3 SynPS/2 Synaptics TouchPad
  3. 测试输入设备:您可以使用以下命令来测试某个输入设备,例如触摸板设备:

    1
    2
    bashCopy code
    evtest /dev/input/event3

    该命令将打印出输入设备发出的事件,例如:

    1
    2
    3
    4
    luaCopy codeEvent: time 1618822168.785292, type 3 (EV_ABS), code 0 (ABS_X), value 3097
    Event: time 1618822168.785292, type 3 (EV_ABS), code 1 (ABS_Y), value 1509
    Event: time 1618822168.785292, type 3 (EV_ABS), code 24 (ABS_PRESSURE), value 0
    Event: time 1618822168.785292, -------------- SYN_REPORT ------------

    这将帮助您确定触摸板设备对应的event号码。

ldconfig:

/*ldconfig 命令的用途主要是在默认搜寻目录 /lib 和 /usr/lib 以及动态库配置文件 /etc/ld.so.conf 内所列的目录下,

搜索出可共享的动态链接库(格式如 lib.so),进而创建出动态链接器(ld.so 或 ld-linux.so)所需的缓存文件。

缓存文件默认为 /etc/ld.so.cache,此文件保存已排好序的动态链接库名字列表,为了让动态链接库为系统所共享,

需运行动态链接库的管理命令 ldconfig 更新动态链接库的缓存文件,此执行程序存放在 /sbin 目录下。

ldconfig 通常在系统启动时运行,而当用户安装了一个新的动态链接库时,就需要手工运行这个命令。*/

一个重要的bug:https://pms.uniontech.com/zentao/bug-view-101762.html

复现问题,查看cat /proc/bus/input/devices >input.txt ,查看文件触摸板设备Touch为event2 远程cat /dev/input/event2发现是有数据的。执行 sudo libinput debug-events --show-keycode 命令无数据,sudo libinput debug-events有数据。

这个问题目前分析的应该是比较全面了。分析结论如下:

1、休眠同时触发合盖动作,lid event节点会上报合盖事件,同时休眠。

2、kwin调用libinput,在接收到合盖事件后,主动禁用触摸设备,这一操作是libinput内部操作,主要的实现机制是关闭对应event的fd。在kwin场景下是通过调用systemd的releaseDevice来达到关闭触摸设备的目的。

3、开盖后,唤醒设备,此时系统并不会有开盖的事件上报。

4、因为此时kwin并没有收到开盖事件,所以触摸设备仍然处于禁用状态。

5、按下键盘,此时仍然不会收到开盖事件,这一点可以通过监听sudo cat /dev/input/eventX设备节点可以确认,而且通过sudo evtest也可以确认确实没有监听到开盖事件,同时也可以查到此时开合盖的状态仍然处于合盖状态。

img

6、按下按键后,libinput虽然发现设备仍然处于合盖状态,但是根据libinput的机制,认为触发键盘按键时,设备应该实际处于开盖状态。所以libinput主动上报了一个开盖事件出来。(通过sudo libinput debug-events --show-keycodes可以监听到这个事件)。

img

上面说的2、5、6三点均为libinput的机制。官方文档地址如下

https://wayland.freedesktop.org/libinput/doc/1.8.1/switches.html

简单翻译

在可用的场景下,libinput会监听提供盖子开关的设备。evdev事件代码EV_SW SW_LID提供为LIBINPUT_SWITCH_LID。如果带有盖子开关的设备有触摸板设备,则当盖子在逻辑上关闭时,该设备将被禁用。这是为了避免触控板和闭合的盖子可能引起的鬼触。当盖子打开时,触摸板会自动重新启用。

这种盖子开关的处理对用户来说是透明的,不发送通知,设备在任何时候都显示为启用。

在某些设备上,设备的盖子状态并不总是反映物理状态,即使当盖子物理上是打开的,盖子状态也可能报告为关闭。Libinput使用一些启发式方法来检测用户输入(特别是输入),以重新启用这些设备上的触摸板。

该问题的根本原因还是在于开合盖的状态和实际物理状态不一致导致的。如果想要彻底解决该问题,还是需要能够解决这个状态异常的情况。

问题复现时进行开盖合盖测试程序测试

img

发生问题时开盖合盖简直监测:

img

当问题复现时驱动层无开盖信息。

复现场景为待机按键后马上合盖,合盖后进入待机流程。此时待机未完成进入待机保存现场(带有合盖待机状态),处于待机未完成。当打开笔记本盖子时继续完成待机,(待机相关信息保存在硬盘,无法监测到开盖事件。)相关处理指示灭掉灯表示此时已经完成保存待机状态。待机状态设备状态此是不通电驱动层的无法感知开盖,系统层无法修改,只能应用层规避此问题。

休眠             合盖                              开盖            半休眠状态信息保存硬盘是掉电状态驱动无法获取开盖事件

运行状态---------》休眠到一半-------》带有合盖状态休眠保存待机状态---------》恢复带有合盖状态休眠保存待机状态再次执行未完成的待机--—》开机问题复现

进程休眠或者待机收到为同一信号无法做区分,需要加入新的唤醒信号或者做标识,来让上层做分析处理。

下层处理风险太大,按现在逻辑分析按键时间会模拟开盖事件,只是滑动触摸板没有模拟开盖事件。无硬件反馈下层是无法感知,即使修改会产生很大问题。libinput是可以感应到滑动触摸板,做个开盖模拟操作就可以了。

查看kwin相关代码:libinput/device.cpp +526

Device::Device(libinput_device device, QObject parent)类中有相关处理

cat /dev/input/event 事件,触摸板有滑动事件,但是异常出问题libinput被拦截。替换麒麟内核问题依然复现不是内核原因,不是在用户内部服务修改,不然登陆界面复现问题,比较大的概率在libinput。

安装双系统:

第一个系统分区[注意顺序]:

efi 300M

swap 8G[和原有运行内存相当]

/boot 2G

/ 根分区100G

第二个系统

在第一个系统的分区基础上进行

/boot 2G

/

华为机型判断

一、华为机型判断 KLV中有: HUAWEI QINGYUN L420 KLVV-W5821 PGV中有: PGUV W515 KLU中有: L410 KLVU PGW中有: PGUW W525 机型判断3种方式 1.可以通过机器上的铭牌查看 2.进入系统后通过cat /etc/hw_version查看 3.通过dmidecode -s system-product-name查看

二、klu和klua机型区分 hwfirmware -v的结果小于等于2.0.0.53是klu,其他是klua,如:结果是2.0.1.7就是klua

三、pgv和pgv C版本机器区分 拆开机箱看显卡芯片,在风扇边上的是小芯片就是pgv机器,是大芯片就是pgv c版本机器

四、pgv使用ti显卡芯片还是龙讯显卡芯片区分 dmesg | grep lt9711a结果出现lt9711a is onboard就是龙讯显卡芯片的pgv机器 dmesg | grep sn65dsix6结果出现sn65dsix6 is onboard就是ti显卡芯片的pgv机器

五、查看目前安装的系统华为版本 通过cat /etc/hw_version查看

六、查看目前安装的系统统信版本 通过cat /etc/os-version查看,主要看MinorVersion=的结果,如MinorVersion=1022是1022版本

systemd-analyze是如何统计开机时间的

使用systemd-analyze可以得到以下结果: Startup finished in 22.440s (firmware) + 2.471s (loader) + 40.688s (kernel) + 8.427s (userspace) = 1min 14.028s graphical.target reached after 8.420s in userspace 我们可以清晰的看到固件,grub,内核所耗费的时间,但systemd启动的时机在fireware,loader和kernel之后,它是如何统计这些启动阶段所耗的时间的呢。

firmware和loader

通过分析systemd代码,systemd统计fireware和loader的时间是通过读取acpi fpdt表和efi变量来完成的。具体的代码如下。

1
2
3
4
5
6
7
8
int boot_timestamps(const dual_timestamp *n, dual_timestamp *firmware, dual_timestamp *loader) {
r = acpi_get_boot_usec(&x, &y);
if (r < 0) {
r = efi_loader_get_boot_usec(&x, &y);
if (r < 0)
return r;
}
}

其中acpi_get_boot_usec主要是解析acpi fpdt表的内容。大致实现如下。在高版本的内核中已经实现了acpi表的解析,不再需要在用户态来解析

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
int acpi_get_boot_usec(usec_t *loader_start, usec_t *loader_exit) {
struct acpi_table_header *tbl;
struct acpi_fpdt_header *rec;
struct acpi_fpdt_boot_header hbrec;
struct acpi_fpdt_boot brec;

r = read_full_file("/sys/firmware/acpi/tables/FPDT", &buf, &l);

/* find Firmware Basic Boot Performance Pointer Record */
for (rec = (struct acpi_fpdt_header *)(buf + sizeof(struct acpi_table_header));
(char *)rec length)) {
if (rec->length type != ACPI_FPDT_TYPE_BOOT)
continue;
if (rec->length != sizeof(struct acpi_fpdt_header))
continue;

ptr = rec->ptr;
break;
}

/* read Firmware Basic Boot Performance Data Record */
fd = open("/dev/mem", O_CLOEXEC|O_RDONLY);
l = pread(fd, &hbrec, sizeof(struct acpi_fpdt_boot_header), ptr);
l = pread(fd, &brec, sizeof(struct acpi_fpdt_boot), ptr + sizeof(struct acpi_fpdt_boot_header));
return 0;
}

efi_loader_get_boot_usec函数主要是读取LoaderTimeInitUSec和LoaderTimeExecUSec两个efi变量,在此不再描述。

如果acpi表中数据有误,systemd-analyze则无法显示firmware和loader的时间。

kernel

systemd统计kernel耗费时间的方式是记录自身启动时间的monotonic time。但实际上并不是内核启动的时间,它包含了kernel+initrd+systemd加载自身的时间。具体如何分析这三部分的时间需要分析内核和initrd的日志。 systemd统计内核启动时间的方式如下:

1
2
3
4
5
6
int main(int argc, char *argv[]) {

dual_timestamp initrd_timestamp = DUAL_TIMESTAMP_NULL, userspace_timestamp = DUAL_TIMESTAMP_NULL, kernel_timestamp = DUAL_TIMESTAMP_NULL;
/* Take timestamps early on */
dual_timestamp_from_monotonic(&kernel_timestamp, 0);
}

CMake基础学习

https://docsin.uniontech.com/?p=20713

Mount:

sudo mount -o remount,rw /dev/sdd2 重新以可写方式挂载boot分区

mount -i 打印的信息就是cat /proc/self/mountinfo的信息

GDB打印所有堆栈:

thread apply all bt

玲珑包工具:

https://linglong.dev/guide/ll-cli/introduction.html

trace-cmd:

ftrace: https://www.modb.pro/db/105914 trace-cmd: https://www.modb.pro/db/125882

查看trace-cmd是否可用:

mount | grep tracefs(好像没啥用,我虚拟机完全没打印东西)

查看有哪些追踪器可用:

trace-cmd list -t

启用你的第一个追踪器:

trace-cmd start -p function

trace-cmd record -l ext4_* -p function_graph

trace-cmd record -p function_graph

trace-cmd report

-P 选项传递PID

/sys/kernel/tracing

apparmor官方文档:

https://gitlab.com/apparmor/apparmor/-/wikis/Documentation

scsi设备扫描:

https://zhuanlan.zhihu.com/p/61608889

休眠的实现:

将"mem", "standby", "freeze" and "disk"写入/sys/power/state

参考:https://www.kernel.org/doc/Documentation/power/states.txt

内核部分的代码:关键词Syncing filesystems..和done.

image-20230316161646504

openssl 调试:

\1. 编译 /config --debug --prefix=/usr/ \2. 安装 make install_sw 会把openssl库安装到/usr/lib64

\3. /etc/ld.so.conf.d/libc.conf添加 /usr/lib64

\4. 如果openssl有问题,会导致系统无法运行,此时用live镜像或者 initramfs 删除 /root/lib64下的openssl库

/etc/profile是由base-file生成的

双rtc问题(参考190897):

1、分析确认设备只有一个物理时钟芯片,但从内核看到两个rtc设备节点(efi-rtc和ds1307) 2、两个设备中读取出来的时间存在一致的问题。

img

初步分析为 问题1: 一个物理时钟芯片,在内核中存在两个rtc设备问题

双rtc的问题的根源是固件那边同时实现了物理rtc到efi-rtc的映射实现的同时,在固件传给内核的acpi表上没有屏蔽掉物理rtc芯片设备,从而导致内核这边注册了两次rtc设备。

这个问题可以从固件方面修改,需要固件那边屏蔽acpi上面的物理设备的传递。或者内核这边通过内核启动参数,只注册efi-rtc忽略其他rtc设备。 或者内核通过custome acpi表,修改和屏蔽一个rtc设备。

img

问题2:设备中读取出来的时间存在不一致的问题 通过修改bios中时间,在系统下读内核注册的rtc时间发现。

固件中修改时间后,rtc0 (efi-rtc)中的年份时间与预期一致。 但rtc1中(ds1307) 中的年份与预期不一致。

img
img

因内核第二次从rtc中读出年份解析错误,而导致系统时间变成了 2106(这个是系统显示时间上限问题)

这个问题初步确认为内核中rtc1的驱动读取时间时,出现解析年份异常。导致出现了2124年,这个问题是否是双rtc问题导致不确定(需要设备进一步排除)。

结论,以上问题非系统问题,建议转内核或厂商固件处理。

Shutdown/reboot

对于目前使用systemd的linux发行版,reboot,shutdown,halt等命令是指向systemctl的软链接,执行reboot相当于执行 systemctl reboot,systemctl reboot 会切换到 reboot.target.

img

注意,如果应用程序希望重启系统, 那么不应该直接启动此目标,而应该使用 systemctl reboot 命令(有可能还需要加上 --no-block 选项)或者直接调用 systemd-logind.service(8) 的 org.freedesktop.login1.Manager.Reboot D-Bus 方法。

为了保持与SysV的兼容性, runlevel6.target 是此单元的一个别名(软连接)。

img

conflicts with all system services:指那些定义了Conflicts=shutdown.target 和 Before=shutdown.target 依赖关系(除非明确设置了 DefaultDependencies=no ,否则 service 单元将会自动添加这些依赖)的服务,这些服务在shutdown.target运行之前会停止。

实际执行过程从上到下,以reboot为例:

systemd-reboot.service:
img
我们的systemd-reboot.service是这样写的:
image-20230322103814108
image-20230322103941482

\1. 停止和shutdown.target、umount.target冲突的服务。

\2. shutdown.target、umount.target

\3. various low-level services

\4. final.target

\5. systemd-reboot.service

该服务执行的命令行:ExecStart=/bin/systemctl --force reboot,这条命令会调用systemd-shutdown,它将以简单而强大的方式卸载任何剩余的文件系统,杀死任何剩余的进程并释放任何其他剩余的资源,而不再考虑任何服务或单元概念。一般这是最后执行的服务。

\6. reboot.target

目标单元的功能仅仅是通过依赖关系将一组单元汇聚在一起, 形成一个同步点,并给这个同步点取一个众所周知的名称, 以便用作启动目标或其他单元的依赖。对于shutdown.target、umount.target、final.target、reboot.target这些目标单元,其组内的单元(.wants/、.requires/)实际的启动顺序取决于单元自身的依赖关系。

systemd常规启动流程:

当成功挂载了"root="内核引导选项指定的根文件系统之后,内核将启动由"init="内核引导选项指定的init程序, 从这个时间点开始,即进入了"常规启动流程": 检测硬件设备并加载驱动、挂载必要的文件系统、启动所有必要的服务,等等。对于 systemd(1) 系统来说,上述"init程序"就是 systemd 进程, 而整个"常规启动流程"也以几个特殊的 target 单元(详见 systemd.target(5)) 作为节点,被划分为几个阶段性步骤。 在每个阶段性步骤内部,任务是高度并行的, 所以无法准确预测同一阶段内单元的先后顺序, 但是不同阶段之间的先后顺序总是固定的。

当启动系统时, systemd 将会以 default.target 为启动目标, 借助单元之间环环相扣的依赖关系,即可完成"常规启动流程"。 default.target 通常只是一个指向 graphical.target(图形界面) 或 multi-user.target(文本控制台) 的软连接。 为了强制启动流程的规范性以及提高单元的并行性, 预先定义了一些具有特定含义的 target 单元。 详见 systemd.special(7) 手册。

下面的图表解释了 这些具有特定含义的 target 单元之间的依赖关系 以及各自在启动流程中的位置。 图中的箭头表示了单元之间的依赖关系与先后顺序, 整个图表按照自上而下的时间顺序执行。

image-20230322102843189

斜体标识的目标单元经常被用作启动目标。 有两种方法可以指定启动目标: (1)使用 systemd.unit= 内核引导选项(参见 systemd(1)); (2)使用 default.target 软连接。

因为 timers.target 以异步方式包含在 basic.target 中, 所以 timer 单元可以依赖于在 basic.target 之后才启动的服务。

initrd(Initial RAM Disk) 启动流程

在initrd内部, 也可以将 systemd 用作 init程序(由"rdinit="内核引导选项指定)。

此时 initrd.target 将是默认目标。 initrd内部启动流程的上半部分与前一小节 basic.target 之前的部分完全相同。 随后的启动流程将以 initrd.target 为目标(如下图所示)。 在挂载任何文件系统之前(也就是在启动 local-fs-pre.target 之前), systemd-hibernate-resume@.service 将会首先完成启动, 以检查系统是要从先前的休眠状态中恢复, 还是要执行常规的启动流程。 在检查完成之前,将不会挂载任何文件系统。 当根文件系统设备可用时,将到达 initd-root-device.target 目标。 如果成功的将根文件系统挂载到 /sysroot 目录,那么 sysroot.mount 单元将被启动,然后进一步到达 initrd-root-fs.target 目标。 initrd-parse-etc.service 将会分析 /sysroot/etc/fstab 文件以挂载 /usr (若需要) 与带有 x-initrd.mount 标记的挂载点。 所有这些挂载点都将被挂载到 /sysroot 之下,然后流程到达 initrd-fs.target 目标。再接下来 initrd-cleanup.service 将会使用 systemctl --no-block isolate 命令启动 initrd-switch-root.target 目标。 因为 isolate 表示立即停止所有在新的目标单元中不需要的进程, 所以此动作实际上是为接下来切换根目录做预先的准备(也就是清理环境)。 最后,启动 initrd-switch-root.service 服务,将系统的根目录切换至 /sysroot 目录。

image-20230322103231804

关机流程

systemd 系统在关机时同样遵循固定的流程, 具体如下图所示:

image-20230322103311681

斜体标识的目标单元经常被用作关机目标。

注意, systemd-halt.service(8), systemd-reboot.service, systemd-poweroff.service, systemd-kexec.service 会将系统与 systemd(PID=1) 带入关机流程的第二个阶段(由 systemd-shutdown 执行), 也就是不再考虑任何服务与单元等概念, 只用一种简单粗暴的方式卸载所有文件系统、杀死所有进程、释放所有资源。 一般来说,在第一阶段结束时,常规应用都已终止、常规资源都已释放,第二阶段只是一个兜底的安全网, 那些在第一阶段基于单元的关闭流程中(见前文) 未能结束的进程将被强制终止、未能释放的资源将被强制释放。

出现D状态程序后打印内核堆栈:

如果出现D状态程序后,开两个终端,一个先执行dmesg -w,另一个再执行echo w > /proc/sysrq-trigger, 在dmesg里打印内核的调用栈,

/etc/sysctl.conf file:

1
2
3
4
5
6
7
kernel.sysrq=1

下面是抓程序堆栈(没试过):
文管卡住的时候,
1. 查询文管 PID
2. gcore $PID 生成 core.xxx 文件
3. gdb dde-file-manager core.xxxx

休眠变重启问题:

系统安装中最后一次执行的update-initramfs动作是在chroot环境内,在chroot环境中执行是不会生成initrd/conf/conf.d/zz-resume-auto文件,该文件用于记录休眠唤醒时的起点,必须在真机上执行update-initramfs时才会生成

该问题使用临时方案进行解决:将安装配置(用户、时区等)放在系统安装之后

https://gerrit.uniontech.com/plugins/gitiles/system-devel/iso-build-deepin/+/ae9276819d28e1e7dd63fd25950f026fe6331798

该问题根因:安装器的配置流程放在了系统安装之前,V20上是安装完成之后才开始配置的

wpa 打开log日志:

image-20230323093436435

虚拟机搭建:

1
sudo apt-get install libvirt0 libvirt-daemon libvirt-daemon-system qemu virt-manager bridge-utils libvirt-clients python-libvirt qemu-efi uml-utilities virtinst qemu-system

设置网络桥接

首先:要停止网络服务
1
systemctl stop network-manager.service
其次:修改网络配置文件
1
sudo vim /etc/network/interfaces
手动设置ip

请将以下代码复制到编辑的文件中,注意:IP等设置自己按照需要修改。不想手动设置ip可跳过这一步,使用下面自动获取ip的方法

auto lo

iface lo inet loopback

auto eth0

iface eth0 inet manual

auto br0

iface br0 inet static

address 192.168.200.130

network 192.168.200.0

netmask 255.255.255.0

broadcast 192.168.200.255

gateway 192.168.200.1

dns-nameservers 8.8.8.8

bridge_ports eth0

bridge_stp off

bridge_fd 0

bridge_maxwait 0

自动从DHCP获取ip

请将一下代码复制到编辑的文件中

auto lo

iface lo inet loopback

#auto eth0

#iface eth0 inet manual

auto br0

iface br0 inet dhcp

bridge_ports eth0

bridge_stp off

bridge_fd 0

最后:重新启动网络服务便可
1
systemctl start network-manager.service
启用虚拟网络(每次启动虚拟机报network相关错误就执行命令)
1
sudo virsh net-start default

X11获取设备信息:

image-20230324152658407
image-20230324152721451
image-20230324152537663
image-20230324152549668
image-20230324152559927
image-20230324152609252
image-20230324152622996

这里上面的xcb_connection_t 里的fd应该是/tmp/.X11-unix/x:

image-20230324153059779
image-20230324153336357
image-20230324152958599
image-20230324152829036
image-20230324153538211

systemctl is-enabled 返回 indirect 是什么意思?

当你运行 systemctl is-enabled <unit>命令来检查指定的systemd服务单元的启用状态时,可能会得到"indirect"的结果。这表示指定的单元并没有直接启用,而是被其他服务单元所依赖而间接启用。

换句话说,如果一个服务单元A依赖于另一个服务单元B,并且B已经启用,那么A也会被间接地启用,即使它本身没有直接被启用。在这种情况下,当你运行 systemctl is-enabled A时,你会得到"indirect"的输出结果。

需要注意的是,间接启用并不意味着服务单元A可以独立地启动或停止。如果依赖于A的其他服务单元也被启用,那么A将保持运行状态。

systemctl enable A.service后,开机启动后并没有启动A.service,只是起了A.socket,这是为什么?

当你运行 systemctl enable A.service命令来启用一个systemd服务单元时,它将创建一个符号链接,指向启用的服务单元文件(通常是位于 /etc/systemd/system/usr/lib/systemd/system目录下的 .service文件)到 /etc/systemd/system目录下的一个目标文件(通常是 /etc/systemd/system/multi-user.target.wants目录下的符号链接)。这将导致服务单元在系统启动时自动启动。

然而,在某些情况下,systemd服务单元本身并不是实际需要自动启动的程序,而是一种被动的服务单元(例如,一个.socket文件),它监听一个端口并等待传入的连接请求。这种情况下,systemd会自动创建一个与服务单元文件同名的.socket文件,并将其链接到启用的服务单元文件(即创建一个 A.socket -> A.service的符号链接)。

因此,在你运行 systemctl enable A.service命令后,如果A.service是一个被动的服务单元,那么systemd将自动创建一个与之同名的.socket文件,并将其链接到A.service文件。在系统启动时,systemd会启动这个.socket文件并等待传入的连接请求,而不是直接启动A.service文件。如果你想要启动A.service,你可以手动运行 systemctl start A.service命令来启动它。

怎么判断service是一个被动的服务单元?

一般来说,可以通过查看systemd服务单元文件的内容来判断它是否是一个被动的服务单元。

被动的服务单元通常是 .socket.path类型的服务单元。.socket服务单元用于监听一个网络端口或一个文件系统中的套接字文件,等待传入的连接请求。.path服务单元用于监视一个文件系统中的路径,当指定的文件或目录发生变化时触发服务单元的启动。

而普通的服务单元则通常是 .service类型的服务单元,它是一种主动的服务单元,通常用于执行某些操作或运行某些程序。

因此,如果一个systemd服务单元文件的名称以 .socket.path结尾,那么它很可能是一个被动的服务单元。另外,你还可以查看服务单元文件的内容,查看其中的 [Socket][Path]部分是否存在,以确认服务单元是否是被动的服务单元。

这个服务是被动服务单元吗 ? 从哪个地方可以看出?

[Unit] Description=PC/SC Smart Card Daemon Requires=pcscd.socket Documentation=man:pcscd(8)

[Service] ExecStart=/usr/sbin/pcscd --foreground --auto-exit ExecReload=/usr/sbin/pcscd --hotplug

[Install] Also=pcscd.socket

根据提供的systemd服务单元文件的内容,可以看出这是一个被动服务单元。原因如下:

  • 该服务单元文件的名称是 pcscd.service,而不是 .socket.path类型的服务单元文件。
  • [Unit]部分,它的描述(Description)是“PC/SC Smart Card Daemon”,而不是“PC/SC Smart Card Socket”或“PC/SC Smart Card Path”等名称,这进一步说明它是一个服务而不是一个socket或path服务单元。
  • [Unit]部分,它有一个 Requires=pcscd.socket的依赖项,这意味着它依赖于一个 .socket类型的服务单元,这通常是被动服务单元
  • [Install]部分,它有一个Also=pcscd.socket的指令,这意味着如果该服务单元被启用,也会同时启用与之同名的 .socket服务单元,这也是被动服务单元的标志之一

综上所述,根据该服务单元文件的内容,可以判断它是一个被动服务单元,用于启动 pcscd.socket服务单元,监听一个网络端口,等待传入的连接请求

systemctl enable 此服务会做什么? enable之后开机对此服务会做什么

systemctl enable命令用于将systemd服务单元设置为开机自动启动。具体来说,当你运行 systemctl enable service_name命令来启用一个systemd服务单元时,它会在 /etc/systemd/system目录下创建一个符号链接,指向该服务单元文件(通常是位于 /etc/systemd/system/usr/lib/systemd/system目录下的 .service文件)到 /etc/systemd/system/multi-user.target.wants目录下的一个目标文件(也是一个符号链接)。这将导致服务单元在系统启动时自动启动。

因此,当你运行 systemctl enable service_name命令来启用一个服务单元时,在系统下次启动时,systemd将自动启动该服务单元,以确保服务在系统启动后可用。这对于一些关键的系统服务,如网络服务、系统日志服务等非常重要,因为这些服务需要在系统启动时启动,以确保系统的正常运行。

总之,systemctl enable命令的作用是将指定的systemd服务单元设置为开机自动启动,以确保服务在系统启动时自动启动并运行。

[Unit] Description=PC/SC Smart Card Daemon Requires=pcscd.socket Documentation=man:pcscd(8)

[Service] ExecStart=/usr/sbin/pcscd --foreground --auto-exit ExecReload=/usr/sbin/pcscd --hotplug

[Install] Also=pcscd.socket

systemctl enable上面这个service会做什么? enable之后开机此服务会启动吗

这个服务单元的作用是启动 PC/SC 智能卡守护进程(PC/SC Smart Card Daemon),并将其设置为开机自动启动。

具体来说,在 [Service] 部分中,指定了 PC/SC 智能卡守护进程的启动命令,即 /usr/sbin/pcscd --foreground --auto-exit--foreground参数指定该服务以前台模式启动(即不以守护进程的方式运行),而 --auto-exit 参数指定当没有智能卡连接到系统时,服务将自动退出。

[Install] 部分中,使用了 Also=pcscd.socket 来指示 systemd 同时启用 pcscd.socket socket 服务单元。这表示当 pcscd.service 服务单元启动时,同时也会启动 pcscd.socket 服务单元。pcscd.socket 负责监听 PC/SC 客户端的连接请求,因此 pcscd.service 是一个被动的服务单元,它依赖于 pcscd.socket 服务单元的启动,以接受客户端连接请求。

当你运行 systemctl enable pcscd.service 命令启用此服务单元时,它将在 /etc/systemd/system/multi-user.target.wants 目录下创建一个符号链接,指向该服务单元文件。这将导致 pcscd.service 服务单元在系统启动时自动启动,并同时启动 pcscd.socket 服务单元。

因此,如果你启用了 pcscd.service 服务单元,则在下次系统启动时,该服务将自动启动,并以前台模式运行 PC/SC 智能卡守护进程,同时启动 pcscd.socket 服务单元,以便监听 PC/SC 客户端的连接请求。

audit审计:

https://access.redhat.com/documentation/zh-cn/red_hat_enterprise_linux/7/html/security_guide/sec-defining_audit_rules_and_controls

1、查看进程被谁杀死:

sudo apt install auditd systemctl enable auditd.service systemctl restart auditd.service 然后通过auditctrl添加规则: auditctl -a exit,always -F arch=b64 -S kill -F a1=9(好像auditctl -a exit,always -S kill就行了,-F参数表示额外匹配,这里选择匹配系统调用的第一个参数,参数内容是要监控进程的PID(这里要用16进制)。) 启动然后kill掉Python程序; 查看日志,即可发现kill发起的程序和用户;

sudo ausearch -sc kill

日志如下:

time->Thu Feb 24 04:00:08 2022

type=PROCTITLE msg=audit(1645675208.403:201): proctitle="htop"

type=OBJ_PID msg=audit(1645675208.403:201): opid=40099 oauid=1016 ouid=1016 oses=12951 obj==unconfined ocomm="nvtop"

type=SYSCALL msg=audit(1645675208.403:201): arch=c000003e syscall=62 success=yes exit=0 a0=9ca3 a1=9 a2=c1 a3=8 items=0 ppid=45939 pid=40129 auid=1016 uid=1016 gid=1016 euid=1016 suid=1016 fsuid=1016 egid=1016 sgid=1016 fsgid=1016 tty=pts4 ses=18035 comm="htop" exe="/usr/bin/htop" subj==unconfined key=(null)

type=OBJ_PID 那一列是杀进程的命令(ocomm="nvtop" )

type=SYSCALL 那一列是kill的调用者详情和系统命名码(syscall=62 )

2、查看文件被谁删除

在/etc/audit/rules.d/audit.rules加上以下两行: -a always,exit -F arch=b32 -S unlink -S unlinkat -S rename -S renameat -S rmdir -k delete -a always,exit -F arch=b64 -S unlink -S unlinkat -S rename -S renameat -S rmdir -k delete

然后重启auditd服务:

systemctl restart auditd

检查规则是否生效:

# auditctl -l -a always,exit -F arch=b32 -S unlink,rename,rmdir,unlinkat,renameat -F key=delete -a always,exit -F arch=b64 -S rename,rmdir,unlink,unlinkat,renameat -F key=delete

然后检查/var/log/audit/audit.log即可看到什么文件被什么程序删除

3、查看谁使用了某个文件

service auditd start

auditctl -w /proc/sys/vm/drop_caches -p rwxa (-w表示使用watch功能, 要watch的文件为 /proc/sys/vm/drop_caches, 观察的事件为文件的读写执行。通过man auditctl 可以知道, audit是通过内核里面hook open系统调用实现的该功能。, -W是删除)

ausearch -f /proc/sys/vm/drop_caches

1.查看信息: 输入以下程序:

dmesg | egrep -i -B100 'killed process' 可以输出最近killed的信息。

2.设定kill优先度: (1)完全关掉oom(不建议): sysctl vm.overcommit_memory=2 echo “vm.overcommit_memory=2” >> /etc/sysctl.conf (2)出现 Out of memory之后重启: sysctl vm.panic_on_oom=1 sysctl kernel.panic=X echo “vm.panic_on_oom=1” >> /etc/sysctl.conf echo “kernel.panic=X” >> /etc/sysctl.conf (3)优先考虑kill这个进程: sudo echo 10>/proc/[PID]/oom_adj (4)尽量不去kill这个进程: sudo echo -15>/proc/[PID]/oom_adj (5)不会kill这个进程: sudo echo -17>/proc/[PID]/oom_adj oom_adj的值在-16 到 +15之间,值越高被kill的优先度越高。当该值为-17时,系统将不会杀死指定pid的进程,而-16~15则会使得进程的/proc/[pid]/oom_adj值呈指数()形式递增,即它们被杀掉的可能性呈指数递增。针对init(进程号为1)这个进程,无论该值设为多少都不会被杀。

3.若拒绝访问: 命令改为:

bash -c "echo '10' | tee /proc/[PID]/oom_adj" bash -c "echo '-15' | tee /proc/[PID]/oom_adj" bash -c "echo '-17' | tee /proc/[PID]/oom_adj"

1.手动修改/etc/shadow中的用户密码

/etc/shadow文件

第一字段:用户名(也被称为登录名),在/etc/shadow中,用户名和/etc/passwd 是相同的,这样就把passwd 和shadow中用的用户记录联系在一起;这个字段是非空的; 第二字段:密码(已被加密),这个字段是非空的; 第三字段:上次修改口令的时间;这个时间是从1970年01月01日算起到最近一次修改口令的时间间隔(天数),您可以通过passwd 来修改用户的密码,然后查看/etc/shadow中此字段的变化; 第四字段:两次修改口令间隔最少的天数;如果这个字段的值为空,帐号永久可用;

第五字段:两次修改口令间隔最多的天数;如果这个字段的值为空,帐号永久可用;

第六字段:提前多少天警告用户口令将过期;如果这个字段的值为空,帐号永久可用;

第七字段:在口令过期之后多少天禁用此用户;如果这个字段的值为空,帐号永久可用;

第八字段:用户过期日期;此字段指定了用户作废的天数(从1970年的1月1日开始的天数),如果这个字段的值为空,帐号永久可用; 第九字段:保留字段,目前为空,以备将来发展之用;

/etc/shadow中格式如下 #testaccount:acQMceF9$1SaCpG2qiKKA3eGolU4Fp0:13402:0:99999:7::: acQMceF9$1SaCpG2qiKKA3eGolU4Fp0段为加密后的密码,表示采用的是md5加密,acQMceF9$是简单的字符串,1SaCpG2qiKKA3eGolU4Fp0为加密后的密码

只要删除 acQMceF9$1SaCpG2qiKKA3eGolU4Fp0 它后,就删除了密码

2.linux忘记登陆密码修改/etc/passwd也可以

很简单的一个技巧,给大家介绍一下 在这个界面 按任意键 按 e键(编辑命令之前启动) 选择第二项 在按e键(修改选定的命令在启动) 输入single (注意空格)进入单用户模式 选择b 启动 输入 vi /etc/passwd root❌0:0:root:/root:/bin/bash 光标移至x下面按delete 键 删除它 输入:x! 输入reboot重启 重启后你会发现 没让你输密码,破译成功

原理解释: 在 /etc/passwd 的文件里 保存着用户的信息文件 root❌0:0:Administrator:/root:/bin/bash

在这一句中

root就是用户名,

x是密码标志,只是说明密码的存放位置,具体呢是放在/etc/passwd的 至于密码别想了 全 是密文保存,看不懂的

0 用户id号

0 组id号

administrator估计是我在装系统的时候,原来的名字没改(虚拟机默认用户名),是用户说 明的意思,

root 用户的家目录

/bin/bash 记录着用户登陆后所拥有的权限,即所拥有的shell

那么我们把密码标示删掉之后,自然就不会有问你密码的对话框了

当然

GRUB这个引导装载程序是可以设密码的

不过 设密码 我们可以光启 ,用安装光盘进入安全模式,在把密码清除

光启 是可以设BIOS密码的,

BIOS 密码是可以拆机箱 扣电池的

那这么说来 传说中的linux岂不是很不安全,通常所说的安全是基于网络的 ,意思是连接互联网后,对方通过网络途径入侵你的linux计算机是很困难的,这种破译的方法只是以防万一,在万一你忘记密码的时候使用的,至于物理安全,那就看你怎么保护了.

gdb调试宏定义并显示宏定义的值

https://blog.csdn.net/zhangjs0322/article/details/39666889

编译时需添加-gdwarf-2和-g3两个参数。

加了-g3的参数后,gcc编译的时候,会将扩展的debug 信息编译进二进制文件里面,包括宏定义信息。

Linux ACL(access control list)访问控制权限列表

在普通权限中,用户对文件只有三种身份,就是属主、属组和其他人;每种用户身份拥有读(read)、写(write)和执行(execute)三种权限。但是在实际工作中,这三种身份实在是不够用,我们举个例子来看看。

图 1 ACL权限简介

img

图 1 的根目录中有一个 /project 目录,这是班级的项目目录。班级中的每个学员都可以访问和修改这个目录,老师也需要对这个目录拥有访问和修改权限,其他班级的学员当然不能访问这个目录。需要怎么规划这个目录的权限呢?应该这样:老师使用 root 用户,作为这个目录的属主,权限为 rwx;班级所有的学员都加入 tgroup 组,使 tgroup 组作为 /project 目录的属组,权限是 rwx;其他人的权限设定为 0。这样这个目录的权限就可以符合我们的项目开发要求了。

有一天,班里来了一位试听的学员 st,她必须能够访问 /project 目录,所以必须对这个目录拥有 r 和 x 权限;但是她又没有学习过以前的课程,所以不能赋予她 w 权限,怕她改错了目录中的内容,所以学员 st 的权限就是 r-x。可是如何分配她的身份呢?变为属主?当然不行,要不 root 该放哪里?加入 tgroup 组?也不行,因为 tgroup 组的权限是 rwx,而我们要求学员 st 的权限是 r-x。如果把其他人的权限改为 r-x 呢?这样一来,其他班级的所有学员都可以访问 /project 目录了。

当出现这种情况时,普通权限中的三种身份就不够用了。ACL 权限就是为了解决这个问题的。在使用 ACL 权限给用户 st 陚予权限时,st 既不是 /project 目录的属主,也不是属组,仅仅赋予用户 st 针对此目录的 r-x 权限。这有些类似于 Windows 系统中分配权限的方式,单独指定用户并单独分配权限,这样就解决了用户身份不足的问题。

ACL是Access Control List(访问控制列表)的缩写,不过在Linux系统中,ACL用于设定用户针对文件的权限,而不是在交换路由器中用来控制数据访问的功能(类似于防火墙)。

开启ACL权限在 CentOS 6.x 系统中 ACL 权限默认是开启的,不需要手工开启。不过,如果你的操作系统不是 CentOS 6.x,那该如何查看 ACL 权限是否开启了呢?可以这样查看:

[root@localhost ~]# mount

/dev/sda1 on /boot type ext4 (rw)

/dev/sda3 on I type ext4 (rw)

…省略部分输出…

#使用mount命令可以看到系统中已经挂载的分区,但是并没有看到ACL权限的设置

[root@localhost ~]# dumpe2fs -h /dev/sda3

#dumpe2fs是查询指定分区文件系统详细信息的命令

…省略部分输出…

Default mount options: user_xattr acl

…省略部分输出…其中,dumpe2fs 命令可选的选项及其含义如下:

-h:仅显示超级块中的信息,而不显示磁盘块组的详细信息;

使用 mount 命令可以査看到系统中已经挂载的分区,而使用 dumpe2fs 命令可以査看到这个分区文件系统的详细信息。大家可以看到,我们的 ACL 权限是 /dev/sda3 分区的默认挂载选项,所以不需要手工挂载。

不过我的 Linux 系统如果没有默认挂载,则可以手工挂载吗?当然可以,执行如下命令:

[root@localhost ~]# mount -o remount, acl /

#重新挂载根分区,并加入ACL权限使用 mount 命令重新挂载,并加入 ACL 权限。不过使用此命令是临时生效的。要想永久生效,需要修改 /etc/fstab 文件,命令如下:

[root@localhost ~]#vi /etc/fstab

UUID=c2ca6f57-b15c-43ea-bca0-f239083d8bd2 /ext4 defaults, acl 1 1

#加入ACL权限

[root@localhost ~]# mount -o remount /

#重新挂载文件系统或重启系统,使修改生效在你需要开启 ACL 权限的分区行上(也就是说 ACL 权限针对的是分区),手工在 defaults 后面加入",acl"即可永久在此分区中开启 ACL 权限。

ACL权限设置

1) ACL权限管理命令我们知道了 ACL 权限的作用,也知道了如何开启 ACL 权限,接下来学习如何査看和设定 ACL 权限。命令如下:

[root@localhost ~]# getfacle 文件名

#查看ACL权限

[root@localhost ~]# setfacl 选项 文件名

#设定ACL权限选项:

-m:设定 ACL 权限。如果是给予用户 ACL 权限,则使用"u:用户名:权限"格式赋予;如果是给予组 ACL 权限,则使用"g:组名:权限" 格式赋予;

-x:删除指定的 ACL 权限;

-b:删除所有的 ACL 权限;

-d:设定默认 ACL 权限。只对目录生效,指目录中新建立的文件拥有此默认权限;

-k:删除默认 ACL 权限;

-R:递归设定 ACL 权限。指设定的 ACL 权限会对目录下的所有子文件生效;

2) 给用户和用户组添加ACL权限举个例子,就来看看图 1 中的权限怎么分配。我们要求 root 是 /project 目录的属主,权限是 rwx;tgroup 是此目录的属组,tgroup 组中拥有班级学员 zhangsan 和 lisi,权限是 rwx;其他人的权限是 0。这时,试听学员 st 来了,她的权限是 r-x。我们来看具体的分配命令。

[root@localhost ~]# useradd zhangsan

[root@localhost ~]# useradd lisi

[root@localhost ~]# useradd st

[root@localhost ~]# groupadd tgroup

#添加需要试验的用户和用户组,省略设定密码的过程

[root@localhost ~]# mkdir /project #建立需要分配权限的目录

[root@localhost ~]# chown root:tgroup /project/

#改变/project目录的属主和属组

[root@localhost ~]# chmod 770 /project/

#指定/project目录的权限

[root@localhost ~]# ll -d /project/

drwxrwx--- 2 root tgroup 4096 1月19 04:21 /project/

#查看一下权限,已经符合要求了

#这时st学员来试听了,如何给她分配权限

[root@localhost ~]# setfacl -m u:st:rx /project/

#给用户st赋予r-x权限,使用"u:用户名:权限" 格式

[root@localhost /]# cd /

[root@localhost /]# ll -d project/

drwxrwx---+ 3 root tgroup 4096 1月19 05:20 project/

#使用ls-l査询时会发现,在权限位后面多了一个"+",表示此目录拥有ACL权限

[root@localhost /]# getfacl project

#查看/prpject目录的ACL权限

#file: project <-文件名

#owner: root <-文件的属主

#group: tgroup <-文件的属组

user::rwx <-用户名栏是空的,说明是属主的权限

user:st:r-x <-用户st的权限

group::rwx <-组名栏是空的,说明是属组的权限

mask::rwx <-mask权限

other::--- <-其他人的权限大家可以看到,st 用户既不是 /prpject 目录的属主、属组,也不是其他人,我们单独给 st 用户分配了 r-x 权限。这样分配权限太方便了,完全不用先辛苦地规划用户身份了。

我想给用户组赋予 ACL 权限可以吗?当然可以,命令如下:

[root@localhost /]# groupadd tgroup2

#添加测试组

[root@localhost /]# setfacl -m g:tgroup2:rwx project/

#为组tgroup2纷配ACL权限,使用"g:组名:权限"格式

[root@localhost /]# ll -d project/

drwxrwx---+ 2 root tgroup 4096 1月19 04:21 project/

#属组并没有更改

[root@localhost /]# getfacl project/

#file: project/

#owner: root

#group: tgroup

user::rwx

user:st:r-x

group::rwx

group:tgroup2:rwx <-用户组tgroup2拥有了rwx权限

mask::rwx

other::--

3) 最大有效权限maskmask 是用来指定最大有效权限的。mask 的默认权限是 rwx,如果我给 st 用户赋予了 r-x 的 ACL 权限,mj 需要和 mask 的 rwx 权限"相与"才能得到 st 的真正权限,也就是 r-x "相与"rwxtj 出的值是 r-x,所以 st 用户拥有 r-x 权限。

如果把 mask 的权限改为 r--,和 st 用户的权限相与,也就是 r--"相与"r-x 得出的值是 r--,st 用户的权限就会变为只读。大家可以这么理解:用户和用户组所设定的权限必须在 mask 权限设定的范围之内才能生效,mask权限就是最大有效权限。

不过我们一般不更改 mask 权限,只要给予 mask 最大权限 rwx,那么任何权限和 mask 权限相与,得出的值都是权限本身。也就是说,我们通过给用户和用户组直接赋予权限,就可以生效,这样做更直观。

补充:逻辑与运算的运算符是"and"。可以理解为生活中所说的"并且"。也就是相与的两个值都为真,结果才为真;有一个值为假,与的结果就为假。比如 A 相与 B,结果入表 2 所示。

A B and

那么两个权限相与和上面的结果类似,我们以读(r)权限为例,结果如表 3 所示。

A B and
r r r
r - -
- r -
- - -

所以,"rwx"相与"r-x",结果是"r-x";"r--"相与"r-x",结果是"r--"。

修改最大有效权限的命令如下:

[root@localhost /]# setfacl -m m:rx project/

#设定mask权限为r-x,使用"m:权限"格式

[root@localhost /]# getfacl project/

#file:project/

#owner:root

#group:tgroup

user::rwx

group::rwx #effective:r-x

mask::r-x

#mask权限变为r-x

other::--

4) 默认ACL权限和递归ACL权限我们已经给 /project 目录设定了 ACL 权限,那么,在这个目录中新建一些子文件和子目录,这些文件是否会继承父目录的 ACL 权限呢?我们试试吧。

[root@localhost /]# cd /project/

[root@localhost prq'ect]# touch abc

[root@localhost prq'ect]# mkdir d1

#在/project目录中新建了abc文件和d1目录

[root@localhost project]#ll

总用量4

-rw-r--r-- 1 root root 01月19 05:20 abc

drwxr-xr-x 2 root root 4096 1月19 05:20 d1

#这两个新建立的文件权限位后面并没有"+",表示它们没有继承ACL权限子文件 abc 和子目录 d1 因为是后建立的,所以并没有继承父目录的 ACL 权限。当然,我们可以手工给这两个文件分配 ACL 权限,但是如果在目录中再新建文件,都要手工指定,则显得过于麻烦。这时就需要用到默认 ACL 权限。

默认 ACL 权限的作用是:如果给父目录设定了默认 ACL 权限,那么父目录中所有新建的子文件都会继承父目录的 ACL 权限。默认 ACL 权限只对目录生效。命令如下:

[root@localhost /]# setfacl -m d:u:st:rx /project/

#使用"d:u:用户名:权限"格式设定默认ACL权限

[root@localhost project]# getfacl project/

# file: project/

# owner: root

# group: tgroup

user:: rwx

user:st:r-x

group::rwx

group:tgroup2:rwx

mask::rwx

other::--

default:user::rwx <-多出了default字段

default:user:st:r-x

default:group::rwx

default😷:rwx

default:other::--

[root@localhost /]# cd project/

[root@localhost project]# touch bcd

[root@localhost project]# mkdir d2

#新建子文件和子目录

[root@localhost project]# ll 总用量8

-rw-r--r-- 1 root root 01月19 05:20 abc

-rw-rw----+ 1 root root 01月19 05:33 bcd

drwxr-xr-x 2 root root 4096 1月19 05:20 d1

drwxrwx---+ 2 root root 4096 1月19 05:33 d2

#新建的bcd和d2已经继承了父目录的ACL权限大家发现了吗?原先的 abc 和 d1 还是没有 ACL 权限,因为默认 ACL 权限是针对新建立的文件生效的。

再说说递归 ACL 权限。递归是指父目录在设定 ACL 权限时,所有的子文件和子目录也会拥有相同的 ACL 权限。

[root@localhost project]# setfacl -m u:st:rx -R/project/

#-R递归

[root@localhost project]# ll

总用量8

-rw-r-xr--+ 1 root root 01月19 05:20 abc

-rw-rwx--+ 1 root root 01月19 05:33 bcd

drwxr-xr-x+ 2 root root 4096 1月19 05:20 d1

drwxrwx--+ 2 root root 4096 1月19 05:33 d2

#abc和d1也拥有了ACL权限总结一下:默认 ACL 权限指的是针对父目录中新建立的文件和目录会继承父目录的 ACL 权限,格式是"setfacl-m d:u:用户名:权限 文件名";递归 ACL 权限指的是针对父目录中已经存在的所有子文件和子目录继承父目录的 ACL 权限,格式是"setfacl-m u:用户名: 权限 -R 文件名"。

5) 删除ACL权限我们来看看怎么删除 ACL 权限,命令如下:

删除指定的ACL权限:

[root@localhost /]# setfacl -x u:st /project/

#删除指定用户和用户组的ACL权限

[root@localhost /]# getfacl project/

# file:project/

# owner: root

# group: tgroup

user::rwx

group::rwx

group:tgroup2:rwx

mask::rwx

other::--

#st用户的权限已被删除

删除所有ACL权限:

[root@localhost /]# setfacl -b project/

#会删除文件的所有ACL权限

[root@localhost /]# getfacl project/

#file: project/

#owner: root

# group: tgroup

user::rwx

group::rwx

other::--

#所有ACL权限已被删除

丁昊的笔记:

丁昊 2-3 20:16:37 https://note.youdao.com/s/Pc3xVfF3 PXE安装

丁昊 2-3 20:20:54 https://note.youdao.com/s/FuARzknP

丁昊 2-3 20:21:18 https://note.youdao.com/s/EREk5WMt

丁昊 2-3 20:21:35 https://note.youdao.com/s/3CazyEHa

tmux教程

新建会话

tmux new -s <session-name>

分离会话(退出当前 Tmux 窗口,但是会话和里面的进程仍然在后台运行)

在 Tmux 窗口中,按下 Ctrl+b d或者输入 tmux detach命令,就会将当前会话与窗口分离。

1
2

$ tmux detach

上面命令执行后,就会退出当前 Tmux 窗口,但是会话和里面的进程仍然在后台运行。

tmux ls命令可以查看当前所有的 Tmux 会话。

1
2
3
4

$ tmux ls
# or
$ tmux list-session

杀死会话

tmux kill-session命令用于杀死某个会话。

1
2
3
4
5
6

# 使用会话编号
$ tmux kill-session -t 0

# 使用会话名称
$ tmux kill-session -t <session-name>

切换会话

tmux switch命令用于切换会话。

1
2
3
4
5
6

# 使用会话编号
$ tmux switch -t 0

# 使用会话名称
$ tmux switch -t <session-name>

重命名会话

tmux rename-session命令用于重命名会话。

1
2

$ tmux rename-session -t 0 <new-name>

上面命令将0号会话重命名。

关闭所有会话:

1
tmux ls | grep : | cut -d. -f1 | awk '{print substr($1, 0, length($1)-1)}' | xargs kill

会话快捷键

下面是一些会话相关的快捷键。

  • Ctrl+b d:分离当前会话。
  • Ctrl+b s:列出所有会话。
  • Ctrl+b $:重命名当前会话。
  • Ctrl+b %:划分左右两个窗格。 Ctrl+b ":划分上下两个窗格。 Ctrl+b <arrow key>:光标切换到其他窗格。 Ctrl+b ;:光标切换到上一个窗格。 Ctrl+b o:光标切换到下一个窗格。 Ctrl+b {:当前窗格与上一个窗格交换位置。 Ctrl+b }:当前窗格与下一个窗格交换位置。 Ctrl+b Ctrl+o:所有窗格向前移动一个位置,第一个窗格变成最后一个窗格。 Ctrl+b Alt+o:所有窗格向后移动一个位置,最后一个窗格变成第一个窗格。 Ctrl+b x:关闭当前窗格。 Ctrl+b !:将当前窗格拆分为一个独立窗口。 Ctrl+b z:当前窗格全屏显示,再使用一次会变回原来大小。 Ctrl+b Ctrl+<arrow key>:按箭头方向调整窗格大小。 Ctrl+b q:显示窗格编号。

窗格滚动快捷键

1
2
Ctrl+b pageUp:向上滚动窗格。
Ctrl+b pageDown:向下滚动窗格。

进入窗格滚动模式后,使用鼠标滚轮可上下移动光标。 此模式下,输入快捷键 q 即可退出滚动模式。

tmux文本复制模式

Ctrl+b [ 进入复制模式,按 q退出
Ctrl+b ] 粘贴复制模式中复制的文本
  1. 按下三个按键进入文本复制模式 CTRL + b + [
  2. 按下 空格 按键进入文本开始复制的起始位置
  3. 上下左右键选择要复制的文本
  4. 选中了要复制的文本后,按下 Enter 按键,将文本保存到tmux的buffer中
  5. 按下三个按键将文本粘贴到某处 CTRL + b + ]

将上面缓冲区内的内容自动复制到系统剪切板

macos

将下面的内容添加到~/.tmux.conf(这个方法是我自己想的,因为 -X copy-pipe-and-cancel 是 Tmux 中 bind-key 命令的一个参数选项,它的作用是在执行复制操作时,将所选内容通过管道传递给外部命令(在这里是 pbcopy),并在复制完成后取消复制模式。)

1
2
set-window-option -g mode-keys vi
bind -T copy-mode-vi Enter send-keys -X copy-pipe-and-cancel "pbcopy"

而macOS自带了 pbcopy 和 pbpaste 命令,用于与剪贴板进行交互。要将命令的输出复制到剪贴板,可以使用管道 | 将命令的输出传递给 pbcopy。例如:

1
your-command | pbcopy

这将把命令的输出复制到剪贴板,您可以随后使用粘贴操作(通常是Command + V)将其粘贴到其他地方。

使用echo和重定向:

1
2
echo "Hello, World" | pbcopy

这将 “Hello, World” 复制到剪贴板。

在 Vi 模式启用的情况下,可以辅助一些别的快捷键高效工作。

例如,可以使用 w 键逐词移动,使用 b 键逐词回退。使用 f 键加上任意字符跳转到当前行第一次出现该字符的位置,使用 F 键达到相反的效果。

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
vi             emacs        功能
^ M-m 反缩进
Escape C-g 清除选定内容
Enter M-w 复制选定内容
j Down 光标下移
h Left 光标左移
l Right 光标右移
L 光标移到尾行
M M-r 光标移到中间行
H M-R 光标移到首行
k Up 光标上移
d C-u 删除整行
D C-k 删除到行末
$ C-e 移到行尾
: g 前往指定行
C-d M-Down 向下滚动半屏
C-u M-Up 向上滚动半屏
C-f Page down 下一页
w M-f 下一个词
p C-y 粘贴
C-b Page up 上一页
b M-b 上一个词
q Escape 退出
C-Down or J C-Down 向下翻
C-Up or K C-Up 向下翻
n n 继续搜索
? C-r 向前搜索
/ C-s 向后搜索
0 C-a 移到行首
Space C-Space 开始选中
C-t 字符调序

linux

将下面的内容添加到~/.tmux.conf(这个我还没测试,不过我看没有问题,至于为什么mac不用xsel和xclip命令,是因为会报错“xsel: Can't open display: (null) : Connection refused”)

1
2
3
4
set-window-option -g mode-keys vi
bind -T copy-mode-vi Enter send-keys -X copy-pipe-and-cancel "xsel -i --clipboard"
或者
bind -T copy-mode-vi Enter send-keys -X copy-pipe-and-cancel "xclip -i -f -selection primary | xclip -i -selection clipboard"

Ubuntu Tmux 启用鼠标滚动

在Ubuntu上使用Tmux是一件非常舒服的事,但有时使用鼠标滚轮时,和平时使用终端的习惯不怎么一致,因此可以设置启用鼠标滚轮。 具体方式: 按完前缀ctrl+B后,再按冒号:进入命令行模式, 输入以下命令:

set -g mouse on 1 就启用了鼠标滚轮,可以通过鼠标直接选择不同的窗口,也可以上下直接翻页。

Tip 但在以上设置下,会发现无法用中键向 tmux 中复制文本,也无法将 tmux 中选择好的文本中键复制到系统其他应用程序中。 这里有一个 trick,那就是在 tmux 中不论选择还是复制时,都按住 Shift 键,你会发现熟悉的中键又回来了 ? 此外,还可以使用 Shift+Insert 快捷键将系统剪切板中的内容输入 tmux 中。 相对于 tmux 原生的选择模式(不加 shift 键),使用系统选择有个缺陷,即当一行内存在多个面板时,无法选择单个面板中的内容,这时就必须使用 tmux 自带的复制粘贴系统了。

允许用鼠标选择面板、窗口,并允许手动修改面板的大小

1
2
3
4
set -g mouse-resize-pane on
set -g mouse-select-pane on
set -g mouse-select-window on

qemu虚拟机的配置文件:

在/etc/libvirt目录下,可以修改虚拟机路径等

修改好了好需要执行systemctl restart libvirtd.service

my_log_info:

void my_log_info(const char fmt,...) { FILE g_log_FP = fopen("/tmp/1.log","a"); if (g_log_FP != 0) { va_list ap; va_start(ap, fmt); vfprintf(g_log_FP, fmt, ap); va_end(ap); //fflush(g_log_FP); fclose(g_log_FP); } }

udev读block块设备容量读的是/sys/devices/pci0000:00/0000:00:0a.0/virtio3/block/vdb/size 这种接口文件

C语言打印函数堆栈方法

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
#include <stdio.h>
#include <execinfo.h>
#include <dlfcn.h>
#define STACK_SIZE 32

static void printStack(void)
{

void *trace[STACK_SIZE];
size_t size = backtrace(trace, STACK_SIZE);
char **symbols = (char **)backtrace_symbols(trace,size);
size_t i = 0;
for(; i<size; i++)
{
printf("%ld--->%s\n", i, symbols[i]); //注意这个得改
}
return;
}
static void printStack(void)
{

void *trace[STACK_SIZE];
size_t size = backtrace(trace, STACK_SIZE);
char **symbols = (char **)backtrace_symbols(trace,size);
size_t i = 0;
for(; i<size; i++)
{
log_error("ly---%ld--->%s\n", i, symbols[i]); //注意这个得改
}
return;
}
static void printStack(void)
{

void *bt[50];
int bt_size;
char **bt_syms;
int i;

bt_size = backtrace(bt, 50);
bt_syms = backtrace_symbols(bt, bt_size);
for (i = 0; i < bt_size; i++) {
Dl_info info;
if (dladdr(bt[i], &info) && info.dli_sname) {
log_error("ly---%d--%s", i, info.dli_sname);
} else {
log_error("ly---%d--%s\n", i,bt_syms[i]);
}
}
free(bt_syms);
}

这个错误提示是缺少动态链接库 libdl.so.2,可以尝试LDFLAGS加上 -ldl 参数来链接该库(改完之后得删掉原来的二进制文件)

#include <execinfo.h> #include <dlfcn.h>

static void print_backtrace(void) { void *bt[50]; int bt_size; char **bt_syms; int i;

bt_size = backtrace(bt, 50);
bt_syms = backtrace_symbols(bt, bt_size);
for (i = 0; i < bt_size; i++) {
    Dl_info info;
    if (dladdr(bt[i], &info) && info.dli_sname) {
        printf("%s\n", info.dli_sname);
    } else {
        printf("???\n");
    }
}
free(bt_syms);

}

内网仓库:

http://pools.uniontech.com/desktop-professional/pool/main/p/python-netaddr/

blkid -l -t "UUID=9cd72106-5e81-4860-a8e1-8285bc74a5ac" -o device

首先读/run/blkid/blkid.tab缓存文件,如果能找到直接返回

否则通过realpath(/dev/disk/by-uuid/af467362-9f82-4219-808f-fb23ca11a4ea)找到对应设备

image-20230403181751780

realpath实际就是找/dev/disk/by-uuid/af467362-9f82-4219-808f-fb23ca11a4ea的软链接

image-20230403181959316
image-20230403181616823

/usr/lib/klibc/bin/fstype原理:

搞一个静态数组然后遍历它,如果ip->identify能识别独处的 超级块,则返回对应的ip->name

image-20230403184655148

这里针对"ext4"进行讨论:

image-20230403184849483

查看某个版本号对应的包的所以已安装包:

dpkg -l | grep 2:2.1.0-5+deb10u2 | cut -d ' ' -f 3|cut -d ":" -f 1 | xargs

或dpkg -l | grep 2:2.1.0-5+deb10u2 | cut -d ' ' -f 3|cut -d ":" -f 1 | awk '{printf("%s ", $0)} END {printf("")}'

查看dpkg安装过程的详细信息:

dpkg -D333

报这个错的解决方法(PCIe Bus Error):

001f50367ab88be4530f8ac6919d9c26

pcie 物理链路层报错,应该是硬件出问题了

解决方法:启动参数加pcie_aspm=off

开启coredump:

建议卸载systemd-coredump使用内核自带的core机制(systemd-coredump不会生成systemd的core文件)

ulimit -c unlimited

(测试发现在普通用户下开启coredump和在root下开启coredump 时,在不同用户下执行ulimit -c以及coredumpctl list显示的值不一样,如下面所示,所以最好在所有用户下都执行下)

image-20230406134935521

如果进程已经在运行,可以设置指定pid的limit

  1. 临时修改进程 prlimit --core=unlimited -p $PID

注:上面的配置重启会失效,所以要永久启用请参考下面的:

2、永久修改

对于普通用户进程来说,编辑/etc/profile最后加上ulimit -c unlimited就行了,但是测试对root不生效。

image-20230406135145550

注:实测对root用户,用户名替换为*未生效,所以最好是显示指定为root

最好改成这样image-20230418165303527

1
2
3
4
5
6
7
8
*               soft    core            unlimited
* hard core unlimited
root soft core unlimited
root hard core unlimited
message+ soft core unlimited
message+ hard core unlimited
message soft core unlimited
message hard core unlimited

这个文件是由pam_limits模块来读的:

#define _PAM_LOGFILE "/var/run/pam-debug.log"

其他dbus-daemon服务等实测要修改这个地方:

image-20230508112416923

设置格式:

1
2
3
echo "/tmp/core-%e-%p-%t" >/proc/sys/kernel/core_pattern
echo "/home/uos/systemd-coredumps/core-%e-%p-%t" >/proc/sys/kernel/core_pattern
echo "/var/log/systemd-coredumps/core-%e-%p-%t" >/proc/sys/kernel/core_pattern

这样更改只是临时的,要想永久生效可以通过更改配置文件:

1
2
3
vim /etc/sysctl.conf
添加一行:kernel.core_pattern=/home/uos/systemd-coredumps/core-%e-%p-%t
kernel.core_pattern=/var/log/systemd-coredumps/core-%e-%p-%t

对于dbus-daemon:

1
2
编辑 /etc/sysctl.conf 文件来设置 /proc/sys/fs/suid_dumpable 参数。在该文件中添加以下行:
fs.suid_dumpable = 1

如果您已经设置了 ulimit -c unlimited,并且仍然没有生成core文件,可能是因为以下原因之一:

  1. 内核可能已被配置为禁用生成core文件。您可以检查 /proc/sys/kernel/core_pattern文件的内容,以确定是否已禁用生成core文件。如果它是空的或包含“|/bin/false”之类的命令,则生成core文件已被禁用。您可以使用以下命令将其重置为默认值:

    1
    2
    perlCopy code
    echo "/var/core/core-%e-%p-%t.core" > /proc/sys/kernel/core_pattern
  2. 如果 /var/core目录不存在或不可写,则core文件可能无法写入。您可以尝试手动创建该目录,并确保该目录对当前用户可写。

  3. 如果您正在运行Systemd,可能需要将 LimitCORE=infinity添加到 /etc/systemd/system.conf或相关服务单元中。这将确保系统可以生成无限制大小的core文件。

  4. 另外,如果dbus-daemon在使用journald作为日志记录系统,则它可能会导致core文件无法生成。您可以尝试将其配置为使用其他日志记录系统,如syslog。

如果您已经检查了以上问题,并且仍然无法生成core文件,请尝试重新启动系统,然后再次测试dbus-daemon崩溃是否可以生成core文件。

查看某个运行中的进程对应的limit配置:

1
cat /proc/xxx/limits
image-20230418163539455

使用内核机制生成coredump的过程:

1、堆栈如下(do_coredump):

image-20230419105156862

查看lz4的core文件:

1-先解压:lz4 -d sss.lz4 sss 2-查看:gdb 程序名 -c sss

gvfs:

GVfs 是 GIO(GLib 中可用的库)的用户空间虚拟文件系统实现。GVfs 带有一组后端,包括垃圾支持、SFTP、SMB、HTTP、DAV 和许多其他后端。GVfs 还包含用于实现卷监视器和持久元数据存储的 GIO 模块。还有 FUSE 支持,为不使用 GIO 的应用程序提供对 GVfs 文件系统的有限访问。

报这个问题的代码逻辑:

image-20230406145744572

首先借助gdb可以知道代码路径是在这里:

image-20230406151216996

这个gvfsd的作用:

image-20230406151534579

这里是有on_name_lost打印的日志,条件为connection==NULL

image-20230406145835453

显然是g_bus_own_name失败时执行的on_name_lost

image-20230406151314396

g_bus_own_name(代码在glib2.0中)

image-20230406151715089
image-20230406152046796
image-20230406151835335
image-20230406153412509
1
2
3
4
5
g_bus_own_name{client->name_lost_handler = name_lost_handler;
{g_bus_get(connection_get_cb){g_task_new{task->callback = callback
{connection = get_uninitialized_connection
{g_async_initable_init_async(反正这里会执行connection_get_cb),g_async_initable_init_async中初始化的结果由g_bus_get_finish返回(这是代码的注释)
connection_get_cb中,当client->connection返回为NULL时,执行call_lost_handler,进而打印该错误日志
image-20230406145725352

g_bus_get_finish返回的 是res->result.pointer

通过syslog过滤程序退出的日志(可用来研究关机重启超时问题):

img

切换java:

sudo update-alternatives --config java

切换iptable:

sudo update-alternatives --set iptables /usr/sbin/iptables-legacy

当然用上面那个也行

xtables-nft — iptables using nftables kernel api

DESCRIPTION xtables-nft are versions of iptables that use the nftables API. This is a set of tools to help the system administrator mi‐ grate the ruleset from iptables(8), ip6tables(8), arptables(8), and ebtables(8) to nftables(8).

xtables-legacy — iptables using old getsockopt/setsockopt-based kernel api

DESCRIPTION xtables-legacy are the original versions of iptables that use old getsockopt/setsockopt-based kernel interface. This kernel interface has some limitations, therefore iptables can also be used with the newer nf_tables based API. See xtables-nft(8) for information about the xtables-nft variants of iptables.

iptables

iptables命令 是Linux上常用的防火墙软件,是netfilter项目的一部分。可以直接配置,也可以通过许多前端和图形界面配置。

https://wangchujiang.com/linux-command/c/iptables.html

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
-t, --table table 对指定的表 table 进行操作, table 必须是 raw, nat,filter,mangle 中的一个。如果不指定此选项,默认的是 filter 表。

# 通用匹配:源地址目标地址的匹配
-p:指定要匹配的数据包协议类型;
-s, --source [!] address[/mask] :把指定的一个/一组地址作为源地址,按此规则进行过滤。当后面没有 mask 时,address 是一个地址,比如:192.168.1.1;当 mask 指定时,可以表示一组范围内的地址,比如:192.168.1.0/255.255.255.0。
-d, --destination [!] address[/mask] :地址格式同上,但这里是指定地址为目的地址,按此进行过滤。
-i, --in-interface [!] <网络接口name> :指定数据包的来自来自网络接口,比如最常见的 eth0 。注意:它只对 INPUT,FORWARD,PREROUTING 这三个链起作用。如果没有指定此选项, 说明可以来自任何一个网络接口。同前面类似,"!" 表示取反。
-o, --out-interface [!] <网络接口name> :指定数据包出去的网络接口。只对 OUTPUT,FORWARD,POSTROUTING 三个链起作用。

# 查看管理命令
-L, --list [chain] 列出链 chain 上面的所有规则,如果没有指定链,列出表上所有链的所有规则。

# 规则管理命令
-A, --append chain rule-specification 在指定链 chain 的末尾插入指定的规则,也就是说,这条规则会被放到最后,最后才会被执行。规则是由后面的匹配来指定。
-I, --insert chain [rulenum] rule-specification 在链 chain 中的指定位置插入一条或多条规则。如果指定的规则号是1,则在链的头部插入。这也是默认的情况,如果没有指定规则号。
-D, --delete chain rule-specification -D, --delete chain rulenum 在指定的链 chain 中删除一个或多个指定规则。
-R num:Replays替换/修改第几条规则

# 链管理命令(这都是立即生效的)
-P, --policy chain target :为指定的链 chain 设置策略 target。注意,只有内置的链才允许有策略,用户自定义的是不允许的。
-F, --flush [chain] 清空指定链 chain 上面的所有规则。如果没有指定链,清空该表上所有链的所有规则。
-N, --new-chain chain 用指定的名字创建一个新的链。
-X, --delete-chain [chain] :删除指定的链,这个链必须没有被其它任何规则引用,而且这条上必须没有任何规则。如果没有指定链名,则会删除该表中所有非内置的链。
-E, --rename-chain old-chain new-chain :用指定的新名字去重命名指定的链。这并不会对链内部造成任何影响。
-Z, --zero [chain] :把指定链,或者表中的所有链上的所有计数器清零。

-j, --jump target <指定目标> :即满足某条件时该执行什么样的动作。target 可以是内置的目标,比如 ACCEPT,也可以是用户自定义的链。
-h:显示帮助信息;

基本参数

参数 作用
-P 设置默认策略:iptables -P INPUT (DROP
-F 清空规则链
-L 查看规则链
-A 在规则链的末尾加入新规则
-I num 在规则链的头部加入新规则
-D num 删除某一条规则
-s 匹配来源地址IP/MASK,加叹号"!"表示除这个IP外。
-d 匹配目标地址
-i 网卡名称 匹配从这块网卡流入的数据
-o 网卡名称 匹配从这块网卡流出的数据
-p 匹配协议,如tcp,udp,icmp
--dport num 匹配目标端口号
--sport num 匹配来源端口号

命令选项输入顺序

1
iptables -t 表名 <-A/I/D/R> 规则链名 [规则号] <-i/o 网卡名> -p 协议名 <-s 源IP/源子网> --sport 源端口 <-d 目标IP/目标子网> --dport 目标端口 -j 动作

NAT 表

该表应该只用于不同数据包上的 NAT(网络地址转换)。换句话说,它应该只用于转换数据包的源字段或目标字段。

过滤表

过滤表主要用于过滤数据包。我们可以匹配数据包并以我们想要的任何方式过滤它们。这是我们实际对数据包采取行动并查看它们包含的内容并根据其内容丢弃或接受它们的地方。当然我们也可以做事前过滤;然而,这个特定的表是设计过滤的地方。

filter 的 FORWARD 链只被转发的数据包(来自网络和出去到网络的数据包)遍历,即你的计算机就像一个路由器,而 nat 的 PREROUTING 链被两个转发的数据包遍历目的是本地主机的数据包和数据包。

您应该仅使用 nat 的 PREROUTING 来更改数据包的目标地址,而过滤器的 FORWARD 仅用于过滤(丢弃/接受数据包)。

工作机制

规则链名包括(也被称为五个钩子函数(hook functions)):

  • INPUT链 :处理输入数据包。
  • OUTPUT链 :处理输出数据包。
  • FORWARD链 :处理转发数据包。
  • PREROUTING链 :用于目标地址转换(DNAT)。
  • POSTOUTING链 :用于源地址转换(SNAT)。

防火墙的策略

防火墙策略一般分为两种,一种叫 策略,一种叫 策略,通策略,默认门是关着的,必须要定义谁能进。堵策略则是,大门是洞开的,但是你必须有身份认证,否则不能进。所以我们要定义,让进来的进来,让出去的出去,所以通,是要全通,而堵,则是要选择。当我们定义的策略的时候,要分别定义多条功能,其中:定义数据包中允许或者不允许的策略,filter过滤的功能,而定义地址转换的功能的则是nat选项。为了让这些功能交替工作,我们制定出了“表”这个定义,来定义、区分各种不同的工作功能和处理方式。

我们现在用的比较多个功能有3个:

  1. filter 定义允许或者不允许的,只能做在3个链上:INPUT ,FORWARD ,OUTPUT
  2. nat 定义地址转换的,也只能做在3个链上:PREROUTING ,OUTPUT ,POSTROUTING
  3. mangle功能:修改报文原数据,是5个链都可以做:PREROUTING,INPUT,FORWARD,OUTPUT,POSTROUTING

我们修改报文原数据就是来修改TTL的。能够实现将数据包的元数据拆开,在里面做标记/修改内容的。而防火墙标记,其实就是靠mangle来实现的。

小扩展:

  • 对于filter来讲一般只能做在3个链上:INPUT ,FORWARD ,OUTPUT
  • 对于nat来讲一般也只能做在3个链上:PREROUTING ,OUTPUT ,POSTROUTING
  • 而mangle则是5个链都可以做:PREROUTING,INPUT,FORWARD,OUTPUT,POSTROUTING

iptables/netfilter(这款软件)是工作在用户空间的,它可以让规则进行生效的,本身不是一种服务,而且规则是立即生效的。而我们iptables现在被做成了一个服务,可以进行启动,停止的。启动,则将规则直接生效,停止,则将规则撤销。

iptables还支持自己定义链。但是自己定义的链,必须是跟某种特定的链关联起来的。在一个关卡设定,指定当有数据的时候专门去找某个特定的链来处理,当那个链处理完之后,再返回。接着在特定的链中继续检查。

注意:规则的次序非常关键,谁的规则越严格,应该放的越靠前,而检查规则的时候,是按照从上往下的方式进行检查的。

表名包括:

  • raw :高级功能,如:网址过滤。
  • mangle :数据包修改(QOS),用于实现服务质量。
  • nat :地址转换,用于网关路由器。
  • filter :包过滤,用于防火墙规则。

动作包括:

  • ACCEPT :接收数据包。
  • DROP :丢弃数据包。
  • REDIRECT :重定向、映射、透明代理。
  • SNAT :源地址转换。
  • DNAT :目标地址转换。
  • MASQUERADE :IP伪装(NAT),用于ADSL。
  • LOG :日志记录。
  • SEMARK : 添加SEMARK标记以供网域内强制访问控制(MAC)
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
                             ┏╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┓
┌───────────────┐ ┃ Network ┃
│ table: filter │ ┗━━━━━━━┳━━━━━━━┛
│ chain: INPUT │◀────┐ │
└───────┬───────┘ │ ▼
│ │ ┌───────────────────┐
┌ ▼ ┐ │ │ table: nat │
│local process│ │ │ chain: PREROUTING │
└ ┘ │ └─────────┬─────────┘
│ │ │
▼ │ ▼ ┌─────────────────┐
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅ │ ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅ │table: nat │
Routing decision └───── outing decision ─────▶│chain: PREROUTING│
┅┅┅┅┅┅┅┅┅┳┅┅┅┅┅┅┅┅┅ ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅ └────────┬────────┘
│ │
▼ │
┌───────────────┐ │
│ table: nat │ ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅ │
│ chain: OUTPUT │ ┌─────▶ outing decision ◀──────────────┘
└───────┬───────┘ │ ┅┅┅┅┅┅┅┅┳┅┅┅┅┅┅┅┅
│ │ │
▼ │ ▼
┌───────────────┐ │ ┌────────────────────┐
│ table: filter │ │ │ chain: POSTROUTING │
│ chain: OUTPUT ├────┘ └──────────┬─────────┘
└───────────────┘ │

┏╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┓
┃ Network ┃
┗━━━━━━━━━━━━━━━┛

实例

清空当前的所有规则和计数

1
2
3
iptables -F  # 清空所有的防火墙规则
iptables -X # 删除用户自定义的空链
iptables -Z # 清空计数

配置允许ssh端口连接

1
2
iptables -A INPUT -s 192.168.1.0/24 -p tcp --dport 22 -j ACCEPT
# 22为你的ssh端口, -s 192.168.1.0/24表示允许这个网段的机器来连接,其它网段的ip地址是登陆不了你的机器的。 -j ACCEPT表示接受这样的请求

允许本地回环地址可以正常使用

1
2
3
iptables -A INPUT -i lo -j ACCEPT
#本地圆环地址就是那个127.0.0.1,是本机上使用的,它进与出都设置为允许
iptables -A OUTPUT -o lo -j ACCEPT

设置默认的规则

1
2
3
iptables -P INPUT DROP # 配置默认的不让进
iptables -P FORWARD DROP # 默认的不允许转发
iptables -P OUTPUT ACCEPT # 默认的可以出去

配置白名单

1
2
3
iptables -A INPUT -p all -s 192.168.1.0/24 -j ACCEPT  # 允许机房内网机器可以访问
iptables -A INPUT -p all -s 192.168.140.0/24 -j ACCEPT # 允许机房内网机器可以访问
iptables -A INPUT -p tcp -s 183.121.3.7 --dport 3380 -j ACCEPT # 允许183.121.3.7访问本机的3380端口

开启相应的服务端口

1
2
3
iptables -A INPUT -p tcp --dport 80 -j ACCEPT # 开启80端口,因为web对外都是这个端口
iptables -A INPUT -p icmp --icmp-type 8 -j ACCEPT # 允许被ping
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT # 已经建立的连接得让它进来

保存规则到配置文件中

1
2
3
cp /etc/sysconfig/iptables /etc/sysconfig/iptables.bak # 任何改动之前先备份,请保持这一优秀的习惯
iptables-save > /etc/sysconfig/iptables
cat /etc/sysconfig/iptables

列出已设置的规则

iptables -L [-t 表名] [链名]

  • 四个表名 rawnatfiltermangle
  • 五个规则链名 INPUTOUTPUTFORWARDPREROUTINGPOSTROUTING
  • filter表包含 INPUTOUTPUTFORWARD三个规则链
1
2
3
4
5
6
iptables -L -t nat                  # 列出 nat 上面的所有规则
# ^ -t 参数指定,必须是 raw, nat,filter,mangle 中的一个
iptables -L -t nat --line-numbers # 规则带编号
iptables -L INPUT

iptables -L -nv # 查看,这个列表看起来更详细

清除已有规则

1
2
3
4
iptables -F INPUT  # 清空指定链 INPUT 上面的所有规则
iptables -X INPUT # 删除指定的链,这个链必须没有被其它任何规则引用,而且这条上必须没有任何规则。
# 如果没有指定链名,则会删除该表中所有非内置的链。
iptables -Z INPUT # 把指定链,或者表中的所有链上的所有计数器清零。

删除已添加的规则

1
2
# 添加一条规则
iptables -A INPUT -s 192.168.1.5 -j DROP

将所有iptables以序号标记显示,执行:

1
iptables -L -n --line-numbers

比如要删除INPUT里序号为8的规则,执行:

1
iptables -D INPUT 8

开放指定的端口

1
2
3
4
5
6
7
8
9
iptables -A INPUT -s 127.0.0.1 -d 127.0.0.1 -j ACCEPT               #允许本地回环接口(即运行本机访问本机)
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT #允许已建立的或相关连的通行
iptables -A OUTPUT -j ACCEPT #允许所有本机向外的访问
iptables -A INPUT -p tcp --dport 22 -j ACCEPT #允许访问22端口
iptables -A INPUT -p tcp --dport 80 -j ACCEPT #允许访问80端口
iptables -A INPUT -p tcp --dport 21 -j ACCEPT #允许ftp服务的21端口
iptables -A INPUT -p tcp --dport 20 -j ACCEPT #允许FTP服务的20端口
iptables -A INPUT -j reject #禁止其他未允许的规则访问
iptables -A FORWARD -j REJECT #禁止其他未允许的规则访问

屏蔽IP

1
2
3
4
5
iptables -A INPUT -p tcp -m tcp -s 192.168.0.8 -j DROP  # 屏蔽恶意主机(比如,192.168.0.8
iptables -I INPUT -s 123.45.6.7 -j DROP #屏蔽单个IP的命令
iptables -I INPUT -s 123.0.0.0/8 -j DROP #封整个段即从123.0.0.1到123.255.255.254的命令
iptables -I INPUT -s 124.45.0.0/16 -j DROP #封IP段即从123.45.0.1到123.45.255.254的命令
iptables -I INPUT -s 123.45.6.0/24 -j DROP #封IP段即从123.45.6.1到123.45.6.254的命令是

指定数据包出去的网络接口

只对 OUTPUT,FORWARD,POSTROUTING 三个链起作用。

1
iptables -A FORWARD -o eth0

查看已添加的规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
iptables -L -n -v
Chain INPUT (policy DROP 48106 packets, 2690K bytes)
pkts bytes target prot opt in out source destination
5075 589K ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0
191K 90M ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
1499K 133M ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:80
4364K 6351M ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
6256 327K ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination

Chain OUTPUT (policy ACCEPT 3382K packets, 1819M bytes)
pkts bytes target prot opt in out source destination
5075 589K ACCEPT all -- * lo 0.0.0.0/0 0.0.0.0/0

启动网络转发规则

公网 210.14.67.7让内网 192.168.188.0/24上网

1
iptables -t nat -A POSTROUTING -s 192.168.188.0/24 -j SNAT --to-source 210.14.67.127

端口映射

本机的 2222 端口映射到内网 虚拟机的22 端口

1
iptables -t nat -A PREROUTING -d 210.14.67.127 -p tcp --dport 2222  -j DNAT --to-dest 192.168.188.115:22

字符串匹配

比如,我们要过滤所有TCP连接中的字符串 test,一旦出现它我们就终止这个连接,我们可以这么做:

1
2
3
4
5
6
7
8
9
10
11
12
iptables -A INPUT -p tcp -m string --algo kmp --string "test" -j REJECT --reject-with tcp-reset
iptables -L

# Chain INPUT (policy ACCEPT)
# target prot opt source destination
# REJECT tcp -- anywhere anywhere STRING match "test" ALGO name kmp TO 65535 reject-with tcp-reset
#
# Chain FORWARD (policy ACCEPT)
# target prot opt source destination
#
# Chain OUTPUT (policy ACCEPT)
# target prot opt source destination

阻止Windows蠕虫的攻击

1
iptables -I INPUT -j DROP -p tcp -s 0.0.0.0/0 -m string --algo kmp --string "cmd.exe"

防止SYN洪水攻击

1
iptables -A INPUT -p tcp --syn -m limit --limit 5/second -j ACCEPT

添加SECMARK记录

1
2
iptables -t mangle -A INPUT -p tcp --src 192.168.1.2 --dport 443 -j SECMARK --selctx system_u:object_r:myauth_packet_t
# 向从 192.168.1.2:443 以TCP方式发出到本机的包添加MAC安全上下文 system_u:object_r:myauth_packet_t

查看iptables 加入规则的流程大概是这样的:

1、执行命令后,iptabls通过一个netlink socket像内核发送获取rules的命令,内核返回一个rules list,iptables保存到一个cache_list中, 然后iptables再将新的规则加进去放到h->obj_list中,然后再通过构建一个msg再通过socket发送给内核,如果有错误再保存給 h->err_list.

更多实例

用iptables搭建一套强大的安全防护盾 http://www.imooc.com/learn/389

iptables: linux 下应用层防火墙工具

iptables 5链: 对应 Hook point netfilter: linux 操作系统核心层内部的一个数据包处理模块 Hook point: 数据包在 netfilter 中的挂载点; PRE_ROUTING / INPUT / OUTPUT / FORWARD / POST_ROUTING

iptables & netfilter img

iptables 4表5链 img

iptables rules img

  • 4表

filter: 访问控制 / 规则匹配 nat: 地址转发 mangle / raw

  • 规则

数据访问控制: ACCEPT / DROP / REJECT 数据包改写(nat -> 地址转换): snat / dnat 信息记录: log

使用场景实例

  • 场景一

开放 tcp 10-22/80 端口 开放 icmp 其他未被允许的端口禁止访问

存在的问题: 本机无法访问本机; 本机无法访问其他主机

  • 场景二

ftp: 默认被动模式(服务器产生随机端口告诉客户端, 客户端主动连接这个端口拉取数据) vsftpd: 使 ftp 支持主动模式(客户端产生随机端口通知服务器, 服务器主动连接这个端口发送数据)

  • 场景三

允许外网访问: web http -> 80/tcp; https -> 443/tcp mail smtp -> 25/tcp; smtps -> 465/tcp pop3 -> 110/tcp; pop3s -> 995/tcp imap -> 143/tcp

内部使用: file nfs -> 123/udp samba -> 137/138/139/445/tcp ftp -> 20/21/tcp remote ssh -> 22/tcp sql mysql -> 3306/tcp oracle -> 1521/tcp

  • 场景四

nat 转发

  • 场景五

防CC攻击

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
iptables -L -F -A -D # list flush append delete
# 场景一
iptables -I INPUT -p tcp --dport 80 -j ACCEPT # 允许 tcp 80 端口
iptables -I INPUT -p tcp --dport 10:22 -j ACCEPT # 允许 tcp 10-22 端口
iptables -I INPUT -p icmp -j ACCEPT # 允许 icmp
iptables -A INPUT -j REJECT # 添加一条规则, 不允许所有

# 优化场景一
iptables -I INPUT -i lo -j ACCEPT # 允许本机访问
iptables -I INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT # 允许访问外网
iptables -I INPUT -p tcp --dport 80 -s 10.10.188.233 -j ACCEPT # 只允许固定ip访问80

# 场景二
vi /etc/vsftpd/vsftpd.conf # 使用 vsftpd 开启 ftp 主动模式
port_enable=yes
connect_from_port_20=YES
iptables -I INPUT -p tcp --dport 21 -j ACCEPT

vi /etc/vsftpd/vsftpd.conf # 建议使用 ftp 被动模式
pasv_min_port=50000
pasv_max_port=60000
iptables -I INPUT -p tcp --dport 21 -j ACCEPT
iptables -I INPUT -p tcp --dport 50000:60000 -j ACCEPT

# 还可以使用 iptables 模块追踪来自动开发对应的端口

# 场景三
iptables -I INPUT -i lo -j ACCEPT # 允许本机访问
iptables -I INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT # 允许访问外网
iptables -I INPUT -s 10.10.155.0/24 -j ACCEPT # 允许内网访问
iptables -I INPUT -p tcp -m multiport --dports 80,1723 -j ACCEPT # 允许端口, 80 -> http, 1723 -> vpn
iptables -A INPUT -j REJECT # 添加一条规则, 不允许所有

iptables-save # 保存设置到配置文件

# 场景四
iptables -t nat -L # 查看 nat 配置

iptables -t nat -A POST_ROUTING -s 10.10.177.0/24 -j SNAT --to 10.10.188.232 # SNAT
vi /etc/sysconfig/network # 配置网关

iptables -t nat -A POST_ROUTING -d 10.10.188.232 -p tcp --dport 80 -j DNAT --to 10.10.177.232:80 # DNAT

#场景五
iptables -I INPUT -p tcp --syn --dport 80 -m connlimit --connlimit-above 100 -j REJECT # 限制并发连接访问数
iptables -I INPUT -m limit --limit 3/hour --limit-burst 10 -j ACCEPT # limit模块; --limit-burst 默认为5

iptables-legacy 与 iptables-nft 的关系与区别

区别

  • iptables-legacy 调用的是 iptables kernel API,iptables-nft 调用的是 nftables kernel API,所以它们的规则在不同内核模块中管理,nft 命令只读取 nftables 内核模块中的规则,所以 nft list ruleset 只能看到 iptables-nft 配置的规则。

共同点

  • 都兼容 iptables 命令配置规则的语法。
  • 匹配报文逻辑共用的同一份代码(xtables match),nft 配置的规则才会用 nftables match。

与 iptables 命令的关系

  • 通常在支持 nftables 的发行版中才会有 iptables-legacy 与 iptables-nft,而 iptables 命令本身是个软链,用户可以选择在两这者之间切换。
  • iptables-legacy 等同于不支持 nftables 发行版的 iptables 命令,即传统的 iptables。
  • 较新发行版往往将 iptables 软链到 iptables-nft。

最佳实践

  • 当系统支持 iptables-nft 时,可以取代 iptables-legacy。
  • 不要两者混用,避免混淆。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+--------------+     +--------------+     +--------------+
| iptables | | iptables | | nft | USER
| legacy | | nft | | (nftables) | SPACE
+--------------+ +--------------+ +--------------+
| | |
====== | ===== KERNEL API ======= | ======= | =====================
| | |
+--------------+ +--------------+
| iptables | | nftables | KERNEL
| API | | API | SPACE
+--------------+ +--------------+
| | |
| | |
+--------------+ | | +--------------+
| xtables |--------+ +-----| nftables |
| match | | match |
+--------------+ +--------------+

安装配置阶段进入tty配置用户以及修改默认启动程序的操作步骤 1 安装完成后不要重启 2 切tty2 crtl+alt+F2 3 lsblk -f 查看roota的设备节点名,本例中是sdd3 4 sudo mkdir /target 5 cd /target 6 sudo mount /dev/sdd3 /target 将roota挂载到新建的target 7 sudo chroot /target 8 passwd root 修改root密码

切wayland :

1、下载dde-dconfig-editor

2、修改 /usr/share/dsg/configs/org.deepin.dde.lightdm-deepin-greeter/org.deepin.dde.lightdm-deepin-greeter.json

image-20230406174135280

解压live系统:

image-20230406173901628
image-20230406173935523

简单来说就3点: \1. 在live系统中更新httpfs2并执行update-initramfs -u更新initrd \2. 更新的initrd文件拷贝出来覆盖/var/www/html/iso/live/initrd.lz \3. 重新压缩filesystem.squashfs并覆盖/var/www/html/iso/live/下的同名文件

查询socket:

netstat -nalp

sudo netstat -nalp | grep "/run/systemd/private"

调试使用某个so文件的进程:

如果你想调试调用libinput库的进程,首先需要找到正在调用libinput的进程。可以使用命令 lsofgrep 来查找正在使用libinput库的进程,例如:

1
2
perlCopy code
sudo lsof -n | grep libinput

该命令会列出所有正在使用libinput库的进程及其对应的PID和文件描述符。接下来,使用GDB调试这个进程的话,可以使用以下命令:

1
2
cssCopy code
sudo gdb -p <pid>

其中 <pid> 是你要调试的进程的PID。这会启动GDB并附加到指定的进程上。

通过上面的过程可以发现控制鼠标、触摸板等是被Xorg调用的

#0 0x00007f033060c168 in tp_palm_detect (tp=0x3374be0, t=0x33751a0, time=1085585244) at ../src/evdev-mt-touchpad.c:1043 #1 0x00007f033060d7f2 in tp_process_state (tp=0x3374be0, time=1085585244) at ../src/evdev-mt-touchpad.c:1789 #2 0x00007f033060dbde in tp_handle_state (tp=0x3374be0, time=1085585244) at ../src/evdev-mt-touchpad.c:1915 #3 0x00007f033060dcf3 in tp_interface_process (dispatch=0x3374be0, device=0x336f8c0, e=0x7f02f37fcca0, time=1085585244) at ../src/evdev-mt-touchpad.c:1967 #4 0x00007f03305fe2dc istn evdev_process_event (device=0x336f8c0, e=0x7f02f37fcca0) at ../src/evdev.c:873 #5 0x00007f03305fe321 in evdev_device_dispatch_one (device=0x336f8c0, ev=0x7f02f37fcca0) at ../src/evdev.c:881 #6 0x00007f03305fe506 in evdev_device_dispatch (data=0x336f8c0) at ../src/evdev.c:940 #7 0x00007f03305f82bc in libinput_dispatch (libinput=0x32f9b80) at ../src/libinput.c:2070 #8 0x00007f033066ea18 in () at /usr/lib/xorg/modules/input/libinput_drv.so #9 0x000000000059b913 in () #10 0x000000000059df01 in () #11 0x000000000059b75e in () #12 0x00007f0361147fa3 in start_thread (arg=<optimized out>) at pthread_create.c:486 #13 0x00007f036107663f in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

使用手掌控制触摸板会进入这里然后goto out

image-20230417110045540

#0 0x00007f033060b02f in tp_process_absolute (tp=0x3374be0, e=0x7f02f37fcca0, time=2035521967) at ../src/evdev-mt-touchpad.c:509 #1 0x00007f033060dc93 in tp_interface_process (dispatch=0x3374be0, device=0x336f8c0, e=0x7f02f37fcca0, time=2035521967) at ../src/evdev-mt-touchpad.c:1956 #2 0x00007f03305fe2dc in evdev_process_event (device=0x336f8c0, e=0x7f02f37fcca0) at ../src/evdev.c:873 #3 0x00007f03305fe321 in evdev_device_dispatch_one (device=0x336f8c0, ev=0x7f02f37fcca0) at ../src/evdev.c:881 #4 0x00007f03305fe506 in evdev_device_dispatch (data=0x336f8c0) at ../src/evdev.c:940 #5 0x00007f03305f82bc in libinput_dispatch (libinput=0x32f9b80) at ../src/libinput.c:2070 #6 0x00007f033066ea18 in () at /usr/lib/xorg/modules/input/libinput_drv.so #7 0x000000000059b913 in () #8 0x000000000059df01 in () #9 0x000000000059b75e in () #10 0x00007f0361147fa3 in start_thread (arg=<optimized out>) at pthread_create.c:486 #11 0x00007f036107663f in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

libinput防误触配置:

image-20230417151650132

但是我看代码里默认是开启的,应该不需要开

image-20230417151757687

禁用域管:

systemctl disable system-udcp-daemon.service 和 systemctl --user disable session-udcp-daemon.service

获取是否是笔记本:

image-20230417165322081

代码来自 libdframeworkdbus2库

1
2
3
4
5
static inline const char* strempty(const char *s) {

return s ?: "";

}

service文件拒绝手动启动:

放在【Unit】下

image-20230419102130173

systemd service 依赖:

一、自动依赖

1、隐含依赖

下列依赖关系是自动隐含的:

设置了 Type=dbus 的服务会自动添加 Requires=dbus.socket 与 After=dbus.socket 依赖。

基于套接字启动的服务会自动添加对关联的 .socket 单元的 After= 依赖。 服务单元还会为所有在 Sockets= 中列出的 .socket 单元自动添加 Wants= 与 After= 依赖。

还有一些 其他依赖关系是由 systemd.exec(5) 与 systemd.resource-control(5) 中的某些资源限制选项自动隐含添加的。

2、默认依赖

除非明确设置了 DefaultDependencies=no ,否则 service 单元将会自动添加下列依赖关系:

Requires=sysinit.target, After=sysinit.target, After=basic.target, Conflicts=shutdown.target, Before=shutdown.target 。 这样可以确保普通的服务单元: (1)在基础系统启动完毕之后才开始启动,(2)在关闭系统之前先被干净的停止。 只有那些需要在系统启动的早期就必须启动的服务, 以及那些必须在关机动作的结尾才能停止的服务才需要设置 DefaultDependencies=no 。

从同一个模版实例化出来的所有服务单元(单元名称中带有 "@" 字符), 默认全部属于与模版同名的同一个 slice 单元(参见 systemd.slice(5))。 该同名 slice 一般在系统关机时,与所有模版实例一起停止。 如果你不希望像上面这样,那么可以在模版单元中明确设置 DefaultDependencies=no , 并且:要么在该模版文件中明确定义特定的 slice 单元(同样也要明确设置 DefaultDependencies=no)、 要么在该模版文件中明确设置 Slice=system.slice (或其他合适的 slice)。 参见 systemd.resource-control(5) 手册。

systemd service中的Before= 和After=代表的含义是什么

1
2
3
4
5
6
7
8
9
10
11
12
13
在systemd服务中,Before= 和After=用于定义服务之间的启动顺序。

Before=用于指定一个服务应该在哪些服务之前启动。例如:

Before=nginx.service httpd.service
这将确保在启动nginx.service和httpd.service之前,该服务将被启动。

相反,After=用于指定服务应该在哪些服务之后启动。例如:

After=network.target syslog.target
这将确保在network.target和syslog.target服务之后,该服务将被启动。

需要注意的是,在同一级别的依赖关系中,Before和After并没有严格的先后顺序,因为systemd不保证在同一级别的依赖关系中按照任何特定的顺序启动服务。相反,它使用一种称为“启动、启用或激活控制”的机制来管理依赖性。

为什么systemd-machine-id-commit.service中明明设置了Before=sysinit.target shutdown.target,但是systemctl list-dependences却显示systemd-machine-id-commit.service依赖sysinit.target

在 Systemd 中,服务的依赖关系是根据单元文件中声明的 Before 和 After 关键字来计算的。如果一个服务 A 声明了 Before=B,那么在启动或停止服务 B 之前,服务 A 将被启动或停止。

然而,在显示服务的依赖关系时,Systemd 还会考虑单元文件中的其他信息,比如必需的配置文件和其他服务的启动状态等。这可能会导致某些服务与它们声明的依赖关系不一致,因为这些附加信息可能会修改启动顺序。

对于你提到的 systemd-machine-id-commit.service,尽管它声明了 Before=sysinit.target shutdown.target,但它实际上需要在 sysinit.target 启动后才能运行,因为它需要读取 /etc/machine-id 文件并将其写入 /var/lib/dbus/machine-id 文件,这些文件都由 sysinit.target 提供。因此,Systemd 在计算依赖关系时将 systemd-machine-id-commit.service 视为依赖于 sysinit.target,即使 Before 关键字声明了相反的顺序。

image-20230420164920843

总之,单元文件中的 Before 和 After 关键字只是服务之间启动顺序的一部分,还有其他因素可能影响系统中服务的实际启动顺序。

before After和Wantedby的区别是什么:

Before 和 After 关键字用于定义服务之间的启动顺序,而 WantedBy 关键字用于指定 systemd 启动该服务的目标。

Before 和 After 关键字可以在系统中建立服务之间的启动顺序依赖关系。例如,如果一个服务 A 声明了 Before=B,那么在启动或停止服务 B 之前,服务 A 将被启动或停止。相反地,如果一个服务 A 声明了 After=B,则服务 B 将在启动或停止服务 A 之后启动或停止。

WantedBy 关键字用于指定 systemd 启动该服务的目标。这通常是一个 target 单元,如 multi-user.target 或 graphical.target。例如,如果一个服务声明了 WantedBy=multi-user.target,则该服务将在系统进入 multi-user.target 目标时启动。

因此,Before 和 After 关键字用于控制服务之间的启动顺序,而 WantedBy 关键字则指定 systemd 启动该服务的目标。这些关键字可以一起使用来创建复杂的启动依赖关系和目标层次结构。

systemd 单元的dependencies[UNIT_REFERENCED_BY]和dependencies[UNIT_REFERENCES]的区别是什么?

1
dependencies[UNIT_REFERENCES] 属性用于记录一个单元所依赖的其他单元,而 dependencies[UNIT_REFERENCED_BY] 属性用于记录依赖该单元的其他单元。

systemctl set-default(关闭和开启图形化界面):

image-20230424101610834

/tmp目录被清空实现

image-20230424102102208

把这一行注释掉/tmp下的内容就不会删除

image-20230424102117921
image-20230803071713337

kms激活码:

kms.uniontech.com:8900:nqYvXZXdNPKNn335

sd_bus_add_match_async 函数

sd_bus_add_match_async 函数的作用?“sd_bus_add_match_async(track->bus, &n->slot, match, on_name_owner_changed, NULL, track);”这个的含义是什么?

sd_bus_add_match_async函数用于向DBus系统总线注册一个过滤规则,当满足该规则的消息到达总线时,DBus将会通知应用程序。该函数的原型如下:

1
int sd_bus_add_match_async(sd_bus *bus, sd_bus_slot **slot, const char *match, sd_bus_message_handler_t callback, void *userdata, sd_bus_slot **ret_slot);
  • bus参数指向DBus系统总线。
  • slot参数是一个指向 sd_bus_slot类型指针的指针,用于返回DBus总线的插槽。
  • match参数是一个DBus过滤规则,用于匹配总线上的消息。具体规则可以参考DBus文档中的描述。
  • callback参数是当DBus总线上有符合过滤规则的消息到达时应该调用的回调函数。
  • userdata参数是传递给回调函数的用户数据指针。
  • ret_slot参数是一个指向 sd_bus_slot类型指针的指针,用于返回DBus总线的插槽。

所以,sd_bus_add_match_async(track->bus, &n->slot, match, on_name_owner_changed, NULL, track);的含义是:向DBus系统总线注册一个过滤规则(match),当DBus总线上有符合该规则的消息到达时,调用 on_name_owner_changed函数,回调函数的用户数据指针为 track,并返回DBus总线的插槽。

session-c1-device-226-0代表什么意思:

image-20230428162455823
image-20230428161449272
1
2
"4月 28 13:44:50 uos-PC systemd[1]: systemd-logind.service: Got notification message from PID 1239 (FDSTORE=1, FDNAME=session-4-device-226-0)
4月 28 13:44:50 uos-PC systemd[1]: systemd-logind.service: Added fd 123 (session-4-device-226-0) to fd store." 这个日志是什么意思

这个日志是系统的 systemd-logind.service 服务收到了一个来自进程 ID 为 1239 的进程的通知消息,该消息的内容是将一个名为 session-4-device-226-0 的文件描述符添加到文件描述符存储中。

具体来说,这个日志包含了以下两条系统日志信息:

第一条信息: systemd-logind.service: Got notification message from PID 1239 (FDSTORE=1, FDNAME=session-4-device-226-0)。其中:

  • systemd-logind.service 是系统中用于控制用户登录和登出的服务。
  • Got notification message 表示该服务收到了一个通知消息。
  • PID 1239 表示发送该通知消息的进程 ID 是 1239。
  • (FDSTORE=1, FDNAME=session-4-device-226-0) 表示通知消息的内容,其中 FDSTORE=1 表示将该文件描述符存储,FDNAME=session-4-device-226-0 表示文件描述符名为 session-4-device-226-0

第二条信息:systemd-logind.service: Added fd 123 (session-4-device-226-0) to fd store.。其中:

  • Added fd 123 表示成功将文件描述符添加到文件描述符存储中,并获得了一个文件描述符编号 123。
  • (session-4-device-226-0) 表示被添加的文件描述符的名字。

综上,这个日志记录了系统中一个进程向 systemd-logind.service 发送了一个消息,通知该服务将一个名为 session-4-device-226-0 的文件描述符存储到系统的文件描述符存储中。

查看deb包dbg调试信息:

image-20230428174919715

判断一个包是否已安装的代码:

原理是遍历/var/lib/dpkg/info/packagename.list

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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
    ret = get_dpkg_file_count(packagename, &num_pkg_files,&package_dir_list); //获取应用相关的所有文件

if (ret < 0)

{

g_list_foreach(package_dir_list, (GFunc)free_glist_enum_memory, NULL);

g_list_free(package_dir_list);

return ret;

}

// 如果获取到与包相关的文件数为0,则认为该包名不正确,返回参数异常错误

SPM_LOG_ERR("#package:%s,num_pkg_files:%d", packagename, num_pkg_files);

if (num_pkg_files <= 0)

{

ret = -EINVAL;

g_list_foreach(package_dir_list, (GFunc)free_glist_enum_memory, NULL);

g_list_free(package_dir_list);

return ret;

}



static int get_dpkg_file_count(const char *packagename, int *num_pkg_files,GList **package_dir_list)

{

char info_file_name[MAX_PACKAGENAME_LEN] = {0}; //.list文件

FILE *fp;

char *buf = NULL;

buf = (char *)malloc(MAX_LINE_LEN);

if (buf == NULL)

{

SPM_LOG_ERR("NOMEMORY:%m");

return SPM_RET_NOMEMORY;

}

memset(buf, 0, MAX_LINE_LEN);

char *buf_long = NULL;

int readsize = MAX_LINE_LEN;

int len = 0;

int attr = 0;

struct stat sbuf;

int ret = 0;

char script_file_name[MAX_PACKAGENAME_LEN]; // dpkg/info目录中的脚本文件

int len_suffix_array = (sizeof(aux_files_suffix) / sizeof(aux_files_suffix[0]));

snprintf(info_file_name, MAX_PACKAGENAME_LEN, "%s/%s.%s", PKG_INFO_PATH, packagename, LISTFILE);

fp = fopen(info_file_name, "r");

if (fp == NULL)

{

ret = -errno;

free(buf);

buf = NULL;

SPM_LOG_ERR("%s,failed :%m", info_file_name);

return ret;

}



while (fgets(&buf[attr], readsize - len, fp) != NULL)

{

if (!newline_terminated(&buf[attr], readsize - len))

{

len = readsize;

attr = strlen(buf);

readsize = readsize * 2;

buf_long = (char *)malloc(readsize);

if (buf_long == NULL)

{

free(buf);

fclose(fp);

SPM_LOG_ERR("NOMEMORY:%m");

return SPM_RET_NOMEMORY;

}

memset(buf_long, 0, readsize);

strncpy(buf_long, buf, len);

free(buf);

buf = NULL;

buf = buf_long;

}

else

{

char *find = strchr(buf, '\n'); //末尾的"\n"

if (find)

{

*find = '\0'; //替换

}

ret = stat(buf, &sbuf);

if (ret < 0) //针对文件或目录不存在的容错处理

{

continue;

}

// 2022-11-09 添加 对目录设置保护

if (!S_ISDIR(sbuf.st_mode) || check_package_dirname_and_addtolist(buf, packagename, package_dir_list))

{

*num_pkg_files = *num_pkg_files + 1;

}



len = 0;

attr = 0;

readsize = MAX_LINE_LEN;

free(buf);

buf = NULL;

buf = (char *)malloc(readsize);

if (buf == NULL)

{

fclose(fp);

SPM_LOG_ERR("NOMEMORY:%m");

return SPM_RET_NOMEMORY;

}

memset(buf, 0, readsize);

}

}

fclose(fp);

free(buf);

buf = NULL;



for (int i = 0; i < len_suffix_array; i++)

{

snprintf(script_file_name, MAX_PACKAGENAME_LEN, "%s/%s.%s", PKG_INFO_PATH, packagename, aux_files_suffix[i]);

if (access(script_file_name, F_OK) != -1)

{

*num_pkg_files = *num_pkg_files + 1;

}

}

return 0;

}

\#define PKG_INFO_PATH "/var/lib/dpkg/info"

\#define LISTFILE "list"
// 判断一行是否读取完整
static int newline_terminated(char *buf, size_t buflen)
{
size_t len = strlen(buf);
if (len == buflen - 1 && buf[len - 1] != '\r' &&
buf[len - 1] != '\n')
return 0;
return 1;
}

shell替换软件包版本号的shell命令

dpkg -l|grep 2.28.23-deepin1|cut -d ' ' -f 3|cut -d ':' -f 1|sed 's/(.*)/\1=2.28.21-deepin1/g' | tr '' ' '

echo -e "aaaa" | sed 's/(.*)/\1=xx/g' | tr '' ' '

``解释:

\1. echo -e "aaaa\nbbbb":使用 echo 命令输出两行文本,aaaabbbb。 2 sed 's/\(.*\)/\1=xx/g':使用 sed 命令在每个单词后面添加 =xx。 \3. tr '\n' ' ':使用 tr 命令将换行符替换为空格。

运行此命令后,您将得到以下输出:

``aaaa=xx bbbb=xx

usbip设置u盘重定向:

1、可复现问题的机型:

任何两台x86或arm架构uos机器

2、设备详细信息(启动器-设备管理器-右键-导出):

如果事件单中已提供,可备注事件单中已提供。

3、复现问题的镜像链接:

105x公版镜像

4、安装第3条中的镜像后,客户另外自行安装的应用列表(如果事定制版应用,请提供下载链接):

从系统仓库安装usbip工具

5、故障发生时正在运行的应用列表(系统监视器-程序进程截屏):

eg.

6、详细的复现步骤:

uos1和uos2需要安装usbip工具,分别加载驱动,设置网卡延迟。

1、系统仓库安装usbip sudo apt install usbip

2、执行sudo modprobe vhci_hcd和sudo modprobe usbip-host

3、执行sudo tc qdisc add dev enp2s0 root netem delay 30ms

备注:云厂商适配适配场景,主机和虚拟机环境一般会存在网络延迟,所有本地复现时需要将网络延迟设置30ms左右

Uos1

1、系统仓库安装usbip sudo apt install usbip

2、执行sudo modprobe vhci_hcd和sudo modprobe usbip-host

3、执行sudo tc qdisc add dev 网卡名称1 root netem delay 30ms

4、物理设备端插入USB设备

5、物理设备端执行sudo usbip list -l 查询USB设备的busid

6、物理设备端执行sudo usbip bind -b <busid> 进行绑定

7、物理设备端执行sudo usbipd -d -4 开启服务

Uos2

1、系统仓库安装usbip sudo apt install usbip

2、执行sudo modprobe vhci_hcd和sudo modprobe usbip-host

3、执行sudo tc qdisc add dev 网卡名称2 root netem delay 30ms

4、虚拟设备端执行sudo usbip attach -r ** -b <busid> 进行连接

5、打开文件管理,可以查看插入uos1的u盘数据

虚拟设备执行usbip detach -p 0进行断开连接

物理设备端执行usbip unbind -b <busid>进行解绑

调试dde-daemon:

pkill dde-session-dae ;/usr/lib/deepin-daaemon/dde-session-daemon

或者

killall dde-session-daemon;DDE_DEBUG_LEVEL=debug /usr/lib/deepin-daemon/dde-session-daemon

打补丁

git clone / 如果目录上级没有源码文件 debuild -us -uc 然后取消 mv 代码目录 代码目录-版本号 dh_make --createorig -s 产生orig源码文件/ 这些有源代码不需要 dpkg-source -b ./ 或 dpkg-source --before-build . 应用补丁 ln -s debian/patches/ patches quilt new 补丁名 例如add-uos-support.patch quilt add 要修改的文件 例如Makefile 修改文件 quilt refresh 补丁就生成出来了 在debian/patches目录下 cd ../../ 退到代码根目录 dch -i 改changelog git add debian/changelog git add debian/patches/series git add debian/patches/生成的补丁 git commit 这里可以 debuild -b 编译进行测试;或者 dpkg-source --after-build . dpkg-source --before-build . 测试补丁应用 最后 git reset --hard git clean -df 将临时文件清除 git review -r origin uos提交,最后面是要提交的分支名

debian目录不打patch,直接修改

quilt rename -P 给补丁改名字 quilt remove files 和add相反 必须先add 再改

git撤销amend:

首先使用 git reflog 命令查看操作记录:

1
2
3
4
5
6
7
8
$ git reflog
c1c1b21 HEAD@{0}: commit (amend): add blank line to index.html
9ff821d HEAD@{1}: commit: add blank line to index.html
b078331 HEAD@{2}: commit: no more commit!
b86e902 HEAD@{3}: commit: so many commit
77e6ce9 HEAD@{4}: commit: this is another commit
ccde039 HEAD@{5}: commit: this is a commit
a49dcf4 HEAD@{6}: clone: from ssh://liux@xxx.xx.xx.xxx:29418/git_test.git

看到 amend 操作之前的最后一个操作就是 HEAD@{1}. 现在可以用 git reset 将当前分支的 HEAD 指向 HEAD@{1}, 即可达到撤销 amend 的目的:

1
2
3
4
5
6
7
8
9
10
$ git reset --soft HEAD@{1}
$ git status
On branch master
Your branch is ahead of 'origin/master' by 5 commits.
(use "git push" to publish your local commits)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified: index.html

找socket对应的文件:

ls -l /proc/2546/fd/12 -> socket:[52845]

cat /proc/net/unix | grep 52845 -> /run/user/1000/systemd/private

cat /proc/net/netlink| grep sockid

lightdm到startdde的整个流程:

lightdm启动greeter:

日志在/var/log/lightdm/lightdm.log

greeter 16:55:29创建会话(/var/log/lightdm/seat0-greater.log )

img

kwin 16:55:33启动(~/.kwin.log)

img

startdde 16:55:50启动(journalctl /usr/bin/lightdm -b0)

img

代码:

lightdm->/usr/bin/deepin-greeter->/usr/share/dde-session-shell/greeters.d/x/*或者/usr/share/dde-session-shell/greeters.d/wayland/launch-kwin-wayland->/usr/bin/kwin_wayland --drm --no-lockscreent /usr/share/dde-session-shell/greeters.d/wayland/lightdm-deepin-greeter-wayland

打印枚举值:

enum tp_gesture_state {

GESTURE_STATE_NONE,

GESTURE_STATE_UNKNOWN,

GESTURE_STATE_SCROLL,

GESTURE_STATE_PINCH,

GESTURE_STATE_SWIPE,

GESTURE_STATE_TAP,

};

#define CASE_RETURN_STRING(a) case a: return #a

static inline const char*

gesture_state_to_str(enum tp_gesture_state state)

{

switch (state) {

CASE_RETURN_STRING(GESTURE_STATE_NONE);

CASE_RETURN_STRING(GESTURE_STATE_UNKNOWN);

CASE_RETURN_STRING(GESTURE_STATE_SCROLL);

CASE_RETURN_STRING(GESTURE_STATE_PINCH);

CASE_RETURN_STRING(GESTURE_STATE_SWIPE);

CASE_RETURN_STRING(GESTURE_STATE_TAP);

}

return NULL;

}

编译kwin遇到如下问题:

解决:

1
echo -e "override_dh_shlibdeps:\n\tdh_shlibdeps --dpkg-shlibdeps-params=--ignore-missing-info"|tee -a debian/rules

gdb set substitute-path命令用法

set substitute-path 是 GDB 中的一个命令,用于设置源代码路径的替换规则。它可以帮助 GDB 在调试过程中正确地找到源代码文件。

该命令的语法如下:

1
2
phpCopy code
set substitute-path <原始路径> <替换路径>

其中,<原始路径> 是要被替换的源代码路径,<替换路径> 是替换后的路径。

例如,假设源代码路径为 /home/user/project/src,而实际代码所在的路径是 /mnt/external_drive/project/src,你可以使用以下命令进行路径替换:

1
2
bashCopy code
set substitute-path /home/user/project/src /mnt/external_drive/project/src

这样,当 GDB 在调试过程中遇到源代码文件路径为 /home/user/project/src/foo.c 时,会自动将其替换为 /mnt/external_drive/project/src/foo.c

你可以使用 show substitute-path 命令来查看当前设置的替换规则。如果需要取消替换规则,可以使用 unset substitute-path 命令。

请注意,set substitute-path 命令通常在调试会话开始之前使用,因为它会影响到后续的源代码查找和断点设置。

查看一个文件夹占了哪个磁盘的 空间以及磁盘所占空间

1
2
df -h /path/to/folder
df -h /dev/sda

查看一个文件夹下各文件所占空间大小,只算一层

1
du -sh *

查看journal某次启动的日志:

journalctl --directory logsall/logs/var/log/journal/75ac2718a7824e04aca173da6d3c0277/ --list-boots

journalctl --directory logsall/logs/var/log/journal/75ac2718a7824e04aca173da6d3c0277/ --list-boots | grep "2023-07-11 01:14:"

journalctl --directory logsall/logs/var/log/journal/75ac2718a7824e04aca173da6d3c0277/ -b -90

vim logsall/logs/var/log/deepin/dde-dconfig-daemon/dde-dconfig-daemon.log

开启dbus日志:

dbus-damon里有很多调试log是 通过_dbus_verbose 打印的,这个打印必须打开DBUS_ENABLE_VERBOSE_MODE 才会打印。 而这个宏在 debian/rules 通过参数--enable-verbose-mode打开,dbus默认有个debug_configure_flags打开了这个宏,但是没有编译到包里,是在 dbus/debian/build-debug/bus/目录下。 必须用这个编译出来的debug版本,加上 DBUS_VERBOSE=1参数才能打开verbose log,log是直接fprintf(stderr方式打印,所以要用nofork方式启动。 log非常多,为了不影响正常业务,可以单独的起一个dbus-daemon,然后对要调试的程序,通过环境变量 DBUS_SYSTEM_BUS_ADDRESS 或者DBUS_SESSION_BUS_ADDRESS指定地址的方式 让调试程序将dbus消息发到我们这个debug版本上,系统其他的进程还是发到原来的dbus-daemon上,这样对正常业务没有影响。

golang

运行

go run hellow.go

常见问题

The "dlv" command is not available. Run "go install -v github.com/go-delve/delve/cmd/dlv@latest" to install.

dlv是Go语言的调试工具Delve的命令行工具。

如何用BPFtrace跟踪ioctl调用

要使用 BPFtrace 跟踪 ioctl 调用,你可以编写一个 BPFtrace 脚本,使用 tracepoint:syscalls:sys_enter_ioctl 事件来捕获 ioctl 系统调用的进入。

以下是一个简单的示例脚本:

1
2
3
4
5
6
#!/usr/bin/bpftrace

tracepoint:syscalls:sys_enter_ioctl
{
printf("ioctl called: fd=%d, request=%lx\n", arg1, arg2);
}

保存脚本为 ioctl_trace.bt,然后在终端中运行以下命令启动 BPFtrace:

1
2
bashCopy code
sudo bpftrace ioctl_trace.bt

这将以超级用户权限运行 BPFtrace,并开始跟踪 ioctl 调用。每当有进程调用 ioctl 时,将打印相应的信息,包括文件描述符和请求参数。

请注意,运行 BPFtrace 需要具有适当的权限。在某些系统上,可能需要以超级用户身份运行 BPFtrace。另外,BPFtrace 的可用性和功能可能会因操作系统和内核版本的不同而有所差异。请确保你的系统满足 BPFtrace 的要求,并在运行脚本之前进行适当的测试和验证。

tracepoint:syscalls:sys_enter_ioctl这个tracepoint是什么意思?

这个tracepoint定义了一个名为 syscalls:sys_enter_ioctl的跟踪事件。在内核中,tracepoint是用于跟踪和观察特定代码路径或系统调用的工具。

该tracepoint用于跟踪 ioctl系统调用的进入点。当应用程序调用 ioctl函数时,该tracepoint会被触发,并执行其中的代码块。代码块中的内容会被打印到控制台上,显示相关的信息,如文件描述符(fd)和ioctl请求(request)。

通过定义和使用tracepoint,可以在内核中注入自定义的代码逻辑,用于跟踪和监视系统的运行时行为。这对于调试、性能分析和问题排查非常有用。通过tracepoint,您可以观察特定的系统调用、函数调用或事件,并在它们发生时执行自定义的操作。

tracepoint和uprobe的区别?

Tracepoint和Uprobe都是Linux内核中的跟踪工具,但它们的实现和使用方式有所不同。

Tracepoint是内核中预定义的静态跟踪事件,用于跟踪和监视特定的代码路径、系统调用或事件。Tracepoint在内核代码中显式定义,并在运行时触发相应的事件。可以使用trace-cmd等工具捕获和分析这些事件,以了解系统的行为和性能。

Uprobe(User Probe)是一种动态的用户空间探测工具,用于在用户空间代码中插入探针。Uprobe允许您在用户程序的指定位置设置探针,当程序执行到该位置时,探针会触发并执行相应的动作。通过Uprobe,您可以监视和分析用户空间代码的执行,收集关键指标和信息。

关键区别如下:

  1. Tracepoint是内核中静态定义的事件,而Uprobe是在用户空间动态设置的探针。
  2. Tracepoint用于跟踪和监视内核中的代码路径和事件,而Uprobe用于跟踪和监视用户空间程序的执行。
  3. Tracepoint由内核开发人员定义和维护,而Uprobe由用户或开发人员在运行时设置和使用。
  4. Tracepoint的触发是由内核事件驱动的,而Uprobe的触发是由用户空间程序的执行驱动的。

总的来说,Tracepoint更适用于内核级别的跟踪和分析,而Uprobe更适用于用户空间程序的动态监测和调试。

objdump -tT /lib/x86_64-linux-gnu/libc.so.6 | grep nptl_setxid

objdump -tT /lib/x86_64-linux-gnu/libc.so.6 | grep setgid

bpftrace -e 'kprobe:__sys_setresgid { @[ustack] = count(); }'

bpftrace -e 'kprobe:__sys_setresgid { printf("%s",stack) }'

bpftrace -e 'kprobe:__sys_setresgid { printf("%s",ustack) }'

bpftrace -e 'kprobe:__sys_setresgid { printf("%s",ustack) }'

pkexec提权

image-20230719134624308

dconfig

image-20230721134554563

显卡驱动位置:

image-20230725115356234

drm调试等级设定:

image-20230725115602949

查看某个服务为什么退出:

stopping %s ....

gdb -p 1

b job_add_to_gc_queue 即可,放心,调的次数不算太多

获取USB设备的idVendor和idProduct信息。以下是几种常用的方法:

  1. 使用lsusb命令: 在终端中运行 lsusb命令,它会列出当前连接到计算机的所有USB设备及其相应的idVendor和idProduct信息。示例输出如下:

    1
    2
    3
    4
    yamlCopy codeBus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
    Bus 001 Device 003: ID 0c45:670c Microdia
    Bus 001 Device 002: ID 0bda:0129 Realtek Semiconductor Corp. RTS5129 Card Reader Controller
    Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

    在这个输出中,"0c45"表示idVendor,"670c"表示idProduct。

  2. 使用udevadm命令: 在终端中运行 udevadm info -a -n /dev/usbX(将"usbX"替换为您要检查的USB设备路径,如"/dev/usb1")。该命令将显示有关USB设备的详细信息,包括idVendor和idProduct。

  3. 使用dmesg命令: 在终端中运行 dmesg命令,它会显示Linux内核日志,其中包含有关连接的USB设备的信息,包括idVendor和idProduct。

检查二进制程序是否存在内存泄露

valgrind --leak-check=summary df

查看安装器的日志:

cat /var/log/deepin-installer.log | base64 -d

禁用 SELinux 的强制执行

将启动参数 enforcing=0 添加到启动选项中,可以将 SELinux 安全政策设置为 "Permissive" 模式,从而禁用 SELinux 的强制执行

  1. Permissive 模式下的 SELinux 仅记录问题,但不阻止任何操作:在 Permissive 模式下,SELinux 仍会分析和评估进程的安全上下文,但不会阻止违反规则的操作。相反,它会生成警告和错误日志来记录安全问题,以帮助系统管理员进行故障排除和调试。
  2. Permissive 模式允许你查看 SELinux 引起的潜在问题:通过将 SELinux 设置为 Permissive 模式,你可以查看与 SELinux 相关的潜在问题,例如哪些操作违反了 SELinux 的规则或策略。这有助于确定哪些过程可能会受到 SELinux 的限制,并可以帮助你识别和调整相应的 SELinux 配置。

其他方法:

  1. 打开终端以进入命令行界面。

  2. 以 root 用户或具有 sudo 权限的用户身份执行以下命令以编辑 SELinux 配置文件

    1
    selinux.conf

    1
    sudo vi /etc/selinux/config
  3. 找到

    1
    SELINUX

    行,并将其值修改为

    1
    permissive

    1
    disabled

    。请注意,这将根据你的需求来决定如何配置 SELinux。

    • 如果将值设置为 permissive,SELinux 将记录异常但不阻止任何操作。
    • 如果将值设置为 disabled,SELinux 将完全禁用。
  4. 添加下面的行,以启用调试日志:

    1
    2
    SELINUX_LOGGING=all
    SELINUX_DEBUG=1

    这将开启 SELinux 的调试日志记录功能。

  5. 保存并关闭文件,退出编辑器。

  6. 重新启动系统或重新加载 SELinux 配置,使更改生效。

开启wine应用日志:

/opt/deepinwine/tools/log.sh <Wine 应用包名> +pid,+tid,+timestamp,+seh,+process,+module

arecord录制音频:

arecord -vv -f dat -d 120 -D pulse -r 48000 -c 2 1.wav

arecord -Dhw:0,1 -d 10 -f cd -t wav test.wav

此问题出现的原因是厂商主板设计适配声卡方面有缺陷。 1、通过以下步骤进行测试: (1)执行 mv /usr/bin/pulseaudio /usr/bin/pulseaudiobak, 重启让pulseaudio无法自启动 (2)命令行 执行 arecord -Dhw:0,1 -d 10 -f cd -t wav test.wav通过alsa进行录音(hw:0,1为指定录音的设备,arecord命令的使用可以参考网上资料) (3) 将生成的test.wav用audacity软件进行播放,查看是否有杂音。 此时依旧有杂音,基本可以确定是送上来的数据流有问题(中间音频驱动和alsa编解码的过程因为编解码过程都比较完善,一般不会出现问题) 2、此问题从现象上看就是将录制的音量提高到50%的时候,通过alsamixer查看此时Internal Mic boost开始有增强效果,导致噪音被放大。此问题在软件方面只能通过修改配置文件将Internal Mic boost设置为ignore来取消增强效果来减缓此问题,要根本解决还需要厂商进行适配。

pulseaudio为什么不能以系统级运行:

https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/WhatIsWrongWithSystemWide/

image-20230809110641829

i2c总线:

i2c_designware FTI20001:01: timeout waiting for bus ready

你提到的错误信息 "i2c_designware FTI20001:01: timeout waiting for bus ready" 是关于 I2C 总线通信的错误。I2C 是一种常用的串行通信协议,用于连接各种外设和传感器。这个错误通常表示在等待 I2C 总线准备好时发生了超时。

这个错误可能由多种原因引起,以下是一些常见的可能性和解决方法:

  1. 硬件连接问题:确保你的硬件连接是正确的,包括正确连接了 SDA(数据线)和 SCL(时钟线),以及正确设置了总线上的电源和地线。
  2. 电源问题:确保你的设备以及所连接的外设都有足够的电源供应,并且电源稳定。
  3. 信号干扰:I2C 总线对信号的稳定性要求较高,如果有其他干扰源,可能会导致通信问题。确保周围环境的电磁干扰较小。
  4. 外设故障:如果你连接了外设到 I2C 总线上,外设本身可能出现故障,导致通信问题。
  5. 总线速率设置:检查你的代码或配置文件中设置的 I2C 总线速率是否正确。如果速率设置不正确,可能会导致通信超时。
  6. 驱动程序问题:检查操作系统内核中的 I2C 驱动程序是否正常加载,并且是否适用于你的硬件。
  7. 硬件故障:如果一切都正确设置而仍然出现问题,可能是硬件本身出现了故障。

解决问题的步骤可能因你的具体硬件和情况而异。首先,确保硬件连接正确,然后检查电源和信号干扰等可能性。如果问题仍然存在,可以查阅硬件供应商的文档,或者查看操作系统的日志以获取更多信息。

wget下载:

1
2
3
4
5
6
wget -O ~/.config/clash/config.yaml "订阅链接"

wget -e use_proxy=yes -e http_proxy=http://127.0.0.1:9093 -e https_proxy=http://127.0.0.1:9093 https://github.com/Fndroid/clash_for_windows_pkg/releases/download/0.20.31/Clash.for.Windows-0.20.31-x64-linux.tar.gz
带身份验证:
wget -e use_proxy=yes -e http_proxy=http://username:password@proxy.example.com:8080 URL

DSDT 表

DSDT 表是电脑硬件设备在 ACPI(高级配置和电源接口) 中的描述。ACPI 是一种高级电源管理和配置标准,旨在提供操作系统与计算机硬件之间的通信接口,以实现电源管理、硬件配置和设备控制等功能。

DSDT 是 "Differentiated System Description Table" 的缩写,它包含了一份以AML(ACPI机器语言)编写的二进制数据,描述了主板上各种硬件设备的层次结构、功能、资源配置和控制方法。这些硬件设备可以包括 CPU、内存、外部设备(如 USB、声卡、网卡等)、电源管理以及其他扩展功能。

操作系统在引导过程中会加载 DSDT 表,并根据其中的信息来实现对硬件设备的正确配置和管理。例如,操作系统可以根据 DSDT 表中的信息来决定如何正确地使用电源管理特性,如休眠、唤醒以及调整 CPU 频率等。

在某些情况下,硬件制造商可能提供了不完整或不准确的 DSDT 表,这可能导致一些硬件功能在操作系统中无法正常工作,或者导致电源管理等方面的问题。为了解决这些问题,技术爱好者和操作系统开发人员可能会尝试自行编写或修改 DSDT 表,以使其适配特定的硬件配置和操作系统需求。

需要注意的是,修改 DSDT 表需要深入了解 ACPI 和操作系统内核的工作原理,因此这通常是一个较为高级的任务,适用于对操作系统和硬件有深入了解的开发人员。

修改 DSDT 表

修改 DSDT 表需要一些高级的技术知识和工具,以及对 ACPI 和操作系统内核的深入了解。以下是大致的步骤,但请注意这是一个复杂的任务,可能需要在特定的场景下进行,例如解决硬件兼容性问题。

  1. 了解 ACPI 和 DSDT 表: 在开始之前,确保您对 ACPI 和 DSDT 表的基本概念有一定的了解。阅读 ACPI 规范和相关文档可以帮助您理解 DSDT 表的结构和作用。
  2. 获取 DSDT 表: 您可以从操作系统中获取当前系统使用的 DSDT 表。在 Linux 系统中,您可以使用 acpidump 工具来导出 DSDT 表。
  3. 使用 AML 编辑器: DSDT 表是使用 AML(ACPI Machine Language)编写的。您需要使用 AML 编辑器来编辑二进制数据。常用的 AML 编辑器包括 iASL 和 SSDTTime。
  4. 解码和分析 DSDT 表: 使用 AML 编辑器将 DSDT 表解码成易于阅读的源代码。这将使您能够分析 DSDT 表的内容,了解其中包含的硬件设备、资源配置和控制方法。
  5. 修改 DSDT 表: 根据您的需求,您可以在源代码中修改设备描述、资源配置、控制方法等。这需要深入了解 ACPI 语法和设备的工作原理。
  6. 编译和编译错误修复: 编辑完成后,您需要将修改后的源代码编译回二进制格式。AML 编辑器通常会提供编译工具。在此过程中可能会出现编译错误,您需要修复这些错误,确保编译通过。
  7. 替换操作系统中的 DSDT 表: 一旦您编辑和编译好了 DSDT 表,您需要将修改后的 DSDT 表替换操作系统中的原始 DSDT 表。这可能涉及到操作系统的内核配置和加载过程。
  8. 测试和验证: 修改 DSDT 表可能会影响硬件的行为和操作系统的稳定性。在进行实际部署之前,务必进行充分的测试和验证,确保修改不会导致系统出现问题。

请注意,修改 DSDT 表是一项高风险的任务,可能会导致系统无法启动或出现其他严重问题。如果您没有足够的经验和知识,建议不要随意修改 DSDT 表。在特定的情况下,您也可以考虑与操作系统社区和硬件制造商合作,以获得更准确和可靠的解决方案。

替换操作系统中的 DSDT 表:

  1. 备份原始 DSDT 表: 在进行替换之前,务必备份原始的 DSDT 表,以防止出现问题时能够恢复系统。您可以将原始 DSDT 表保存到一个安全的位置。
  2. 编辑和编译 DSDT 表: 使用 AML 编辑器进行编辑和编译您的修改后的 DSDT 表。
  3. 将修改后的 DSDT 表嵌入到内核: 您需要将修改后的 DSDT 表嵌入到操作系统内核中,以便内核能够在启动时使用它。这可能涉及到编译操作系统内核时的一些配置选项。
  4. 更新操作系统引导选项: 您可能需要更新操作系统的引导选项,以确保内核能够加载您修改后的 DSDT 表。这可能涉及到引导加载程序的配置文件(如 GRUB 或 UEFI 引导管理器)。
  5. 重启系统: 在完成上述步骤后,重启计算机以应用修改后的 DSDT 表。
  6. 验证和测试: 在系统重新启动后,务必进行充分的测试和验证,确保修改后的 DSDT 表不会导致系统出现问题。

请注意,上述步骤可能因操作系统和硬件而有所不同。修改 DSDT 表是一个风险较高的操作,可能会导致系统无法启动或出现其他严重问题。在进行操作之前,请务必充分了解操作系统和硬件的工作原理,最好在实验环境中进行测试。

如果您不确定如何操作或缺乏相关经验,请考虑寻求专业人士的帮助,或在相关的技术社区寻求建议。

比较文件或路径是否相同(抄systemd的代码):

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
// 比较两个文件是否相同
int files_same(const char *filea, const char *fileb, int flags) {
struct stat a, b;

assert(filea);
assert(fileb);

if (fstatat(AT_FDCWD, filea, &a, flags) < 0)
return -errno;

if (fstatat(AT_FDCWD, fileb, &b, flags) < 0)
return -errno;

return a.st_dev == b.st_dev &&
a.st_ino == b.st_ino;
}
// 实现了对两个路径字符串的"排序比较"
int path_compare(const char *a, const char *b) {
int d;

assert(a);
assert(b);

/* A relative path and an absolute path must not compare as equal.
* Which one is sorted before the other does not really matter.
* Here a relative path is ordered before an absolute path. */
d = (a[0] == '/') - (b[0] == '/');
if (d != 0)
return d;

for (;;) {
size_t j, k;

// 将路径指针 a 和 b 前进到下一个非 / 字符。
a += strspn(a, "/");
b += strspn(b, "/");

if (*a == 0 && *b == 0)
return 0;

/* Order prefixes first: "/foo" before "/foo/bar" */
if (*a == 0)
return -1;
if (*b == 0)
return 1;
// 这两行计算当前路径段的长度,即下一个 / 之前的字符数。
j = strcspn(a, "/");
k = strcspn(b, "/");

/* Alphabetical sort: "/foo/aaa" before "/foo/b" */
// 这行使用 memcmp 函数比较两个路径段的字节内容,最多比较 j 和 k 个字节。
d = memcmp(a, b, MIN(j, k));
if (d != 0)
return (d > 0) - (d < 0); /* sign of d */

/* Sort "/foo/a" before "/foo/aaa" */
d = (j > k) - (j < k); /* sign of (j - k) */
if (d != 0)
return d;
// 这两行将路径指针 a 和 b 前进到下一个 / 之后的位置,以准备比较下一个路径段。
a += j;
b += k;
}
}
//比较路径是否相同
bool path_equal(const char *a, const char *b) {
return path_compare(a, b) == 0;
}
bool path_equal_or_files_same(const char *a, const char *b, int flags) {
return path_equal(a, b) || files_same(a, b, flags) > 0;
}
//判断是不是绝对路径
bool path_is_absolute(const char *p) {
return p[0] == '/';
}

213277 grub菜单显示风格问题

  • 主要原因: theme.txt为空.导致显示的实际是以图形界面显示字符菜单
  • 有关theme.txt, 并没有控制菜单是否有边框的选项.https://www.gnu.org/software/grub/manual/grub/html_node/Theme-file-format.html

213181 安装时使用lvm分区第二系统无法正确引导

此问题关键在/etc/grub.d/30_os-prober:292, 这里是在第二系统中尝试生成menu entry中包含root=的那一行, 其中${LPARAMS}就是root=参数

$LPARAMS获取主要通过linux-boot-prober, 这是一个shell脚本, 在46行会遍历所有/usr/lib/linux-boot-probes/mounted目录下的脚本并尝试搜索grub.cfg, LPARAMS其实就是linux那一行的运行参数

经过以上分析, 我们知道新生成的grub.cfg与原有的grub.cfg是有关联的. 那么问题的根因也就明了了.

华为适配grub相关问题

主要在/etc/grub.d/00_header.in, 30_uefi-firmware.in 一般的arm设备是不支持背景图片的,华为除外.grub识别华为设备是通过dmidecode 一些调试手段: 其实grub.cfg可以看作是在grub命令行中的一个脚本, 其中所有条目都可以作为命令行在grub命令行中执行. 举例来说:

1
2
3
4
5
terminal_output gfxterm 设置图形界面
terminal_output console 设置纯字符界面显示
background_image /grub/themes/deepin/background.jpg 设置背景图片
videoinfo 查看支持分辨率(有时候可能不准)
loadfont ($root)/boot/grub/font.pf2 载入字体, 有时候字符界面出现异常字符可能会和字符集有关

systemd引导阶段出现问题的故障排查

  1. 启用debug-shell, 此功能一般默认已被屏蔽, 参考https://gerrit.uniontech.com/c/base/systemd/+/23900

    241.9-1+dde

    image-20230816152845195
    image-20230816152925334
  2. systemd.confirm_spawn=1逐步调试 以212431为例进行展开. 此问题最终定位是由于laptop_mode引起, 我们可以尝试查看一下这个项目的代码, 全部由shell组成.

问题关键点在于1234行, 此处由于使用了&符号使得所有脚本同时执行. 我们可以去掉这个符号, 方便我们调试 需要注意的是, 启动阶段虽然开启了debug-shell, 但是由于启动的早期阶段本地磁盘并未挂载, 修改脚本操作需要在磁盘挂载后进行

200217

一个安全问题. 实际上只是因为合法文件损坏导致. 与其他文件格式相比, TIFF格式没有CRC校验和字段. 这里可以提出两个问题. CRC校验的内存占用进一步延伸出md5sum的内存占用是否比TIFF那种直接载入整个文件消耗要小

locale-gen 命令

代码在glibc中

locale-gen 命令主要的工作是根据 /etc/locale.gen 文件中的配置生成所需的本地化数据文件。这些数据文件包括区域设置相关的语言、字符集、货币符号等信息,供系统中的应用程序使用。

在执行 locale-gen 命令时,它会做以下事情:

  1. 读取 /etc/locale.gen 文件,该文件包含了一系列的区域设置配置行,每行表示一种区域设置。

    image-20230817141603929
  2. 对于每个配置行,如果未被注释掉,locale-gen 将根据配置生成对应的本地化数据文件。

    image-20230817141634381
  3. 生成的本地化数据文件通常保存在 /usr/lib/locale 或类似的目录中,不同发行版可能有所不同。

  4. 在生成数据文件后,locale-gen 命令可能会更新一些系统相关的缓存,以便系统中的应用程序能够快速访问这些本地化数据。

查看某个进程占用了哪个端口

sudo netstat -tlnp | grep 端口号

修改init进程

可以通过修改grub参数修改init进程,如init=/bin/bash这样的参数

systemd的inhibit机制:

image-20230821143614643

学习下lightdm获取给自己发送信号进程的机制:

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
static int signal_pipe[2];

if (pipe (signal_pipe) != 0)
g_critical ("Failed to create signal pipe");
fcntl (signal_pipe[0], F_SETFD, FD_CLOEXEC);
fcntl (signal_pipe[1], F_SETFD, FD_CLOEXEC);
g_io_add_watch (g_io_channel_unix_new (signal_pipe[0]), G_IO_IN, handle_signal, NULL);
action.sa_sigaction = signal_cb;
sigemptyset (&action.sa_mask);
action.sa_flags = SA_SIGINFO | SA_RESTART;
sigaction (SIGTERM, &action, NULL);
sigaction (SIGINT, &action, NULL);
sigaction (SIGUSR1, &action, NULL);
sigaction (SIGUSR2, &action, NULL);

static void
signal_cb (int signum, siginfo_t *info, void *data)
{
/* Check if we are from a forked process that hasn't updated the signal handlers or execed.
If so, then we should just quit */
if (getpid () != signal_pid)
_exit (EXIT_SUCCESS);

/* Write signal to main thread, if something goes wrong just close the pipe so it is detected on the other end */
if (write (signal_pipe[1], &info->si_signo, sizeof (int)) < 0 ||
write (signal_pipe[1], &info->si_pid, sizeof (pid_t)) < 0)
close (signal_pipe[1]);
}



static gboolean
handle_signal (GIOChannel *source, GIOCondition condition, gpointer data)
{
errno = 0;
int signo;
pid_t pid;
if (read (signal_pipe[0], &signo, sizeof (int)) != sizeof (int) ||
read (signal_pipe[0], &pid, sizeof (pid_t)) != sizeof (pid_t))
{
g_warning ("Error reading from signal pipe: %s", strerror (errno));
return FALSE;
}

g_debug ("Got signal %d from process %d", signo, pid);

Process *process = g_hash_table_lookup (processes, GINT_TO_POINTER (pid));
if (process == NULL)
process = process_get_current ();
if (process)
g_signal_emit (process, signals[GOT_SIGNAL], 0, signo);

return TRUE;
}

开启和关闭安全管控:

image-20230823100312504

设置允许任意应用安装:

两种方法:

image-20230823100448905
image-20230823100410141

kill进程可以这么写函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int kill_and_sigcont(pid_t pid, int sig) {
int r;

r = kill(pid, sig) < 0 ? -errno : 0;

/* If this worked, also send SIGCONT, unless we already just sent a SIGCONT, or SIGKILL was sent which isn't
* affected by a process being suspended anyway.
如果这有效,也发送 SIGCONT,除非我们刚刚发送了 SIGCONT,或者发送了 SIGKILL,而该 SIGKILL 无论如何都不受进程挂起的影响。*/

// SIGTERM信号会通知进程终止,但进程并不一定会立即终止。进程可能会忽略SIGTERM信号,或者它可能会终止,
// 但需要一些时间。发送SIGCONT信号可以确保进程在收到SIGTERM信号后立即终止,即使进程正在暂停状态。
if (r >= 0 && !IN_SET(sig, SIGCONT, SIGKILL))
(void) kill(pid, SIGCONT);

return r;
}
留言