分类目录归档:技术笔记

Bullet Journal for Notion 之快速入门

前些日子我的 BJ for Notion 辅助看板工具 —— BJ-PFD,专注于个人财务看板的小工具正式发布啦。再公布了预览之后得到许多热心网友的关注,我也真心的希望这个小工具能带给更多人价值。于是今天就带来了这篇《Bullet Journal for Notion 之快速入门》,目的在于快速配置,从注册 Notion 开始,带您快速看到效果。

获取 Notion 模版

BJ-PFD 的数据来源于我一年前就在制作并使用的一个 Notion Bullet Journal 模版,要想用起来这个小工具,首先就要获取我这份模版。

第一步:注册 Notion (有账号请跳过)

如果您此时还没有注册 Notion ,需要先注册一个 Notion 账号,前阵子 Notion 拿到了一笔不小的融资,现在对个人免费版已经没有区块和页面数量的限制,可以放心使用。

https://imagehost-cdn.frytea.com/images/2021/09/11/2021-09-11-3.18.584aacc5f18bf1c416.png

顺便一提,Notion 提供数据导出功能,可以轻松的将页面以 Markdown 格式导出,将数据库(表格)以 *.csv 格式导出。

注册账号很简答,只需 打开 Notion 官网,在这里输入您的邮箱:

https://imagehost-cdn.frytea.com/images/2021/09/11/2021-09-11-3.20.40ea053c87bd49cc6e.png

之后选择 Sign up 即可,之后您的邮箱会收到一封邮件,其中包含一个多段分割的验证码,回到一之前的页面输入这个验证码即可登陆。

https://imagehost-cdn.frytea.com/images/2021/09/11/2021-09-11-3.23.08315bd1c416b29428.png

如果您是新账号,进入 Notion 后会看到一个演示页面,至此,Notion 账号准备完毕。

https://imagehost-cdn.frytea.com/images/2021/09/11/2021-09-11-3.23.08315bd1c416b29428.png

今后想要登陆,一样的步骤,使用邮箱验证码即可,如果想要开启密码登录,在设置界面中设置密码即可。

第二步:获取模版

我已将制作好的 Bullet Journal 模版分享出来,并开启了克隆权限,您只需点击我的页面链接

Bullet Journal V1.0.0 模版链接:https://frytea-share.notion.site/Bullet-Journal-V1-0-0-c4dd075cdc2340129eb198daa48ebf5b

https://imagehost-cdn.frytea.com/images/2021/09/11/2021-09-11-3.34.04d5e2803319b3253b.png

打开您就可以看到这份模版啦,其中包括了存储各种内容的数据库,以及各种看板等,左上角就是使用频率最高的月度看板啦。

如何将该模版添加到您的 Notion 中呢?

https://imagehost-cdn.frytea.com/images/2021/09/11/2021-09-11-3.35.29095031aba185506b.png

方法很简单,看到右上角的 Duplicate 了吗?点击它,稍等片刻,即可在您的 Notion 工作区中找到这个模版啦。

https://imagehost-cdn.frytea.com/images/2021/09/11/2021-09-11-3.38.309c3570b7b0b12227.png

关于这个模版的使用方法,后面会慢慢介绍,请期待后面的详细使用指南。

请注意,该模版中的大部分内容您可以按照自己的喜好去定制,但需注意的是:ALL DB 中的数据库不可随意删除,其中的字段也不可以随意更名、删除!您可以增加自己需要的字段,或是将不想看到的字段隐藏。若任意删改数据库和字段将会导致您的模版无法驱动 BJ-PFD。

至此,您已经在自己的 Notion 账户中获取到这份模版,接下来就用这里的数据来填充一些 BJ-PFD 辅助看板工具吧。

配置 BJ-PFD

获取到了模版,接下来就来配置一下 BJ-PFD 把,按照这个流程走下来,您可以立刻看到最终效果。

第一步:注册 BJ-PFD

打开 BJ-PFD,点击 Get Started

BJ-PFD:https://bjpfd.frytea.com

如果您有账号可以直接登陆,没有账号请先注册,设置用户认证系统的原因见上一篇《概览》,在此不再赘述,相关数据均以加密的方式存储在云端,可以放心使用。

https://imagehost-cdn.frytea.com/images/2021/09/11/2021-09-11-3.46.4990a4f7f6ae62fcbb.png

如果没有账号,点击其中的 Sign Up:

https://imagehost-cdn.frytea.com/images/2021/09/11/2021-09-11-3.47.46834d2ee3ea4180df.png

填入相关信息注册即可,成功后会自动跳转到工具首页。

请记好您的密码,由于本人时间精力有限,暂不提供密码修改和找回功能。

第二步:填入配置参数

如果没有配置,打开界面所有组件都是空白的,同时设置界面会自动弹出。如果没有自动弹出,可以在右上角找到它。

https://imagehost-cdn.frytea.com/images/2021/09/11/2021-09-11-4.23.36775843e90423d0b6.png

接下来就来介绍如何填写这些参数。

  • Notion Token

BJ-PFD 是基于 Notion 公开 API 实现的,因此要想让 BJ-PFD 读取您账本中的数据并可视化,需要配置一个权限。

首先需要创建一个 integration ,在您 BJ 模版所在的工作空间,按照图示的顺序打开 integration 的管理面板。

https://imagehost-cdn.frytea.com/images/2021/09/11/2021-09-11-4.26.20a59e9295ed9227c3.png

点击最中间的 Create new integration 。

https://imagehost-cdn.frytea.com/images/2021/09/11/2021-09-11-4.29.346746050488b6912c.png

填写一下名称,点击 Submit 即可。

https://imagehost-cdn.frytea.com/images/2021/09/11/2021-09-11-4.30.213d41d39156036287.png

之后在这里将生成的 Token 拷出来,如果为隐藏状态,点击一下 Show。

https://imagehost-cdn.frytea.com/images/2021/09/11/2021-09-11-4.31.47-1c786a5b79b6f0f99.png

这就获取到了 Notion Token,回去填到 BJ-PFD 对应表格中即可。

都这里还没完,该 integration 还没有权限访问您的 Bullet Journal,还需要给它一个权限。

https://imagehost-cdn.frytea.com/images/2021/09/11/2021-09-11-4.34.368c92d5a82e66a8f5.png

再次回到您的 Bullet Journal 页面,点击右上角的 Share ,选择 Invite ,选中您刚刚创建的 integration 之后 invite即可,此时再点击 Share 可以看到您创建的 integration 就有了访问您 Bullet Journal 的权限。BJ-PFD就是通过这个来访问您的数据并可视化的。

https://imagehost-cdn.frytea.com/images/2021/09/11/2021-09-11-4.38.0847ccae4349ae0492.png

到这里 Token 配置完毕了,还需要指定一下特定的几个数据库 ID。

  • Notion 「BJPFD-账本-DB」 ID

回到您的 Bullet Journal 主界面,找到 BJPFD-账本-DB 这个项目,右键拷贝链接。

https://imagehost-cdn.frytea.com/images/2021/09/11/2021-09-11-4.39.457642aadd88886a36.png

在主界面选中可以,也可以在左侧快速导航中拷贝。

拷出来的链接可以放在任意一个文本编辑框中,其结构大概是这样:

https://www.notion.so/xxxxxxxxxxxxxxxxxxx?v=yyyyyyyyyyyyyyyyyy

您需要拷贝其中 xxxxxxxxxxxxxxxxxxx 的部分,这就是该数据库的 ID 了,将它拷入 BJ-PFD 中 Notion 「BJPFD-账本-DB」 ID 中即可。

剩下的 Notion 「BJPFD-账户-DB」 ID、Notion 「BJPFD-投资账本-DB」 ID、Notion 「BJPFD-投资账户-DB」 ID、Notion 「BJPFD-预算-DB」 ID 按照类似的方法配置即可,需要将对应 DB 的ID拷入,最后点击 Save。窗口会自动刷新,从您配置的 Notion 数据库中获取数据。

待加载完毕,就可以看到数据啦!

https://imagehost-cdn.frytea.com/images/2021/09/11/2021-09-11-4.46.4589553b3ba17f16e4.png

这些图表和 Bullet Journal 中是如何对应的,这还需要您一点点来探索。

至此,Bullet Journal 和 BJ-PFD 的快速部署就完成啦,至于这个模版如何使用,我会整理另外的文档详细说明,尽请期待!

Windows Terminal 快速快捷键指南

  • Ctrl + Shift + T 打开默认配置文件的新标签。
  • Ctrl + Shift + N 其他指定配置文件新标签。
  • Alt + Shift + D 复制并拆分窗格。
  • Ctrl + Alt + N 切换打开的控制台
  • Alt + Shift+ 复制垂直窗格
  • Alt + Shift + - 复制水平窗格
  • 按住 Alt 键,然后使用光标键在键盘上的 活动窗格之间切换
  • Alt + Shift 并使用光标键相应地调整窗格的大小。
  • Alt + Shift + W 或输入终端的标准退出命令(通常是 exit )来关闭活动窗格或选项卡。
  • Ctrl + +Ctrl + - 来调整活动终端的文本大小 or 按住 Ctrl 并滚动鼠标滚轮。
  • Ctrl + ⬆ or ⬇ or Page Up or Page Down滚动视窗
  • Ctrl + Shift + F 打开搜索框

参考文献

Bullet Journal for Notion 之概览

最近几乎花费全部业余时间制作了一个 BJ for Notion 辅助看板工具,配合我做的 Bj 模版使用。本文主要就是做一个预览,具体的使用方法后面慢慢出文介绍。话不多说,直接看效果吧!

工具官网:https://bjpfd.frytea.com/

https://imagehost-cdn.frytea.com/images/2021/09/10/2021-09-10-12.08.3841e4f126607696d4.png

注册登陆

第一次进入需要注册登陆,请记好您的邮箱和密码。设置登陆的目的在于 加密 记录您的 Notion Token 及各个数据库的 ID ,我想如果每次进入都要输入这一堆乱码一定会很煎熬吧。

https://imagehost-cdn.frytea.com/images/2021/09/10/2021-09-10-12.08.04bf48948ee2781528.png

在这里提供一个演示用的账户,您可以登陆查看效果,数据来自 BJ 模版虚拟数据。

  • 账号: public@frytea.com
  • 密码: public

登陆后请勿修改其中的任何内容!(不点击设置中的 Save 即可)

工具配置

演示账户请自觉忽略该步骤,若需配置请自行注册账户。

如果您是注册后第一次登陆该工具,登陆成功后会弹出设置页面,您也可以在右上角找到它。

https://imagehost-cdn.frytea.com/images/2021/09/10/2021-09-10-12.10.536ff63630e083c389.png

在这里填入您Notion页面的各项数据即可,具体请查看后面的的 《快速入门》。

效果展示

当一切配置妥当,您大概就可以看到 BJ 数据库中的数据被填充进该工具的各个表格中。

https://imagehost-cdn.frytea.com/images/2021/09/10/2021-09-10-12.12.5671df52d28519e322.png

在首页,您可以看到您的账户总额、投资总额、消费统计等情况,在下方的「资产总况」中可以看到您各个账户的分布情况。

https://imagehost-cdn.frytea.com/images/2021/09/10/2021-09-10-12.15.21c233efc2eec9ff9c.png

再向下看,可以看到「资产瀑布图」,这里统计了您选中年份的月度资产汇总情况,红色和绿色区别出您资产的增减,资产累计情况一目了然。

https://imagehost-cdn.frytea.com/images/2021/09/10/2021-09-10-12.17.013b3283c8874b7bf3.png

再向下,就是一个按月为单位的消费统计图,该统计图展示了您各个月份预算及实际花销的对比图,同样是红绿区分出消费和预算,红色的区域就是您超出预算的部分,提醒您要小心消费咯!

https://imagehost-cdn.frytea.com/images/2021/09/10/2021-09-10-12.18.301d34e1c9b791f1b0.png

习惯的养成源于日常的点滴,如果想控制消费就需要从每一天去努力,接下来就是一个「日消费趋势图」,该趋势图统计出您选定月份按日统计的消费情况,要是那一天太过“突出”记得去 Notion 看看钱都花到哪里去了吧!

https://imagehost-cdn.frytea.com/images/2021/09/10/2021-09-10-12.20.12fbb5284e99462acc.png

在最后,是一个年度消费统计图。人们总是高估一天的价值,低估半年、一年的价值。不信就来看看这幅年度消费统计图吧。

总结

滴水石穿,资产的积累源自于每一天。小时候家人告诉我:钱是硬通货。虽说义务教育告诉我们不要太看中钱,但钱是做许多事情的基础。就个人来说,资本的积累不仅仅是让自己过的更好,也是为这个世界创造更大价值的必进之路。只有自己过得好了,才有更大的力气帮助别人。

正是本着这一想法,毕业后我就开始使用 Notion 来记账,刚开始借鉴了几个外国友人和国人的模版,但总感觉太简单,不能符合我的需求。于是干脆融会贯通,总结出自己的一套模版,直接将记账、复盘、负反馈、目标管理、愿望清单等功能囊括其中。在做记账时发现 Notion 虽强大,但是少了些统计图表,很难直观看出消费情况。

基于以上原因,正巧自己想要做一个小项目,干脆拿这个练手,以 Notion 最近刚刚公开测试的 APi 接口为基础,边学边用 React 前端框架,借鉴了许多开源项目后做出了您看到的这个工具。

我认为软件产品应该尽可能简单、专注、实用、稳定、人性化。从设计开发这个工具之初我就秉持这些理念,现在终于到了交卷的时刻了。

希望这个小工具你能喜欢,也希望与您交流看法,下面是我的联系方式:

最后,我将会在近期整理出该工具及 BJ 模版的快速上手教程及各类指南,待文档汇总齐全后我会将其汇总到一个总的 Wiki 中,敬请期待。

Libpcap 落地包转发及性能调优

近期接到一个需求,需要使用 libpcap 从某网卡抓包发送到另一张网卡,关于 libpcap 的使用方法在这里不再赘述,网上有很多教程,本文最后会给出一个示例程序。这里记录一个转发效率性能调优的方法。

在写好程序后,发现 Ping 的延时很高,优化了一个参数后得到了极大的改善了。

https://imagehost-cdn.frytea.com/images/2021/03/17/_16159857327845a539b65d50119ba.png

发现是自己在使用这个函数打开网络设备时的超时时间设定不合理导致的:

函数名称: pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms, char *ebuf)
函数功能:获得用于捕获网络数据包的数据包捕获描述字。
参数说明:device参数为指定打开的网络设备名。snaplen参数定义捕获数据的最大字节数。promisc指定是否将网络接口置于混杂模式。toms参数指*定超时时间(毫秒)。ebuf参数则仅在pcapopen_live()函数出错返回NULL时用于传递错误消息。

我将其中的第四个参数,由之前的 1000 改为 1 ,性能得到极大改善。下面给出示例程序:

/*************************************************************************
        > File Name : pcap_example.c
        > Author : TL Song
        > EMail : songtianlun@frytea.com
        > Created Time : Thu Feb 18 18:57:20 2021
     ************************************************************************/
    #include <stdio.h>
    #include <string.h>
    #include <pthread.h>
    #include <pcap.h>

    #define RECV_DEVICE      "ens3"
    #define RECV_DEVICE_SEND "docker0"
    #define RECV_FILTER      "arp or icmp"

    #define dPrint(fmt, ...) do{fprintf(stderr, "[%s:%d] " fmt "\r\n", __FUNCTION__, __LINE__, ##__VA_ARGS__);}while(0)

    int main()
    {
        char err_buf[PCAP_ERRBUF_SIZE];
        struct bpf_program fp_recv;      /* The compiled filter expression */
        char filter_recv[] = RECV_FILTER;  /* The filter expression (filter 53 port)*/
        pcap_t *handle_recv;
        pcap_t *handle_recv_send;
        bpf_u_int32 mask_recv;       /* The netmask of our sniffing device */
        bpf_u_int32 net_recv;        /* The IP of our sniffing device */
        u_char *pkt_data = NULL;
        int rst;
        struct pcap_pkthdr header;
        struct pcap_pkthdr *p_header = &header;

        printf("Recv Device: %s\n", RECV_DEVICE);

        /*Step1, Open the session in promiscuous mode*/
        handle_recv = pcap_open_live(RECV_DEVICE, BUFSIZ, 1, 1, err_buf);
        if (handle_recv == NULL) {
            fprintf(stderr, "Couldn't open device %s: %s\n", RECV_DEVICE, err_buf);
            return 0;
        }

        handle_recv_send = pcap_open_live(RECV_DEVICE_SEND, BUFSIZ, 1, 1, err_buf);
        if (handle_recv_send == NULL) {
            printf("Couldn't open device %s: %s\n", RECV_DEVICE_SEND, err_buf);
            return 0;
        }

        /*Step2, get network mask*/
        if (pcap_lookupnet(RECV_DEVICE, &net_recv, &mask_recv, err_buf) == -1) {
            fprintf(stderr, "Can't get netmask for device %s\n", RECV_DEVICE);
            net_recv = 0;
            mask_recv = 0;
        }

        /* Step3, Compile and apply the filter */
        if (pcap_compile(handle_recv, &fp_recv, filter_recv, 0, net_recv) == -1) {
            fprintf(stderr, "Couldn't parse filter %s: %s\n", filter_recv, pcap_geterr(handle_recv));
            return 0;
        }
        if (pcap_setfilter(handle_recv, &fp_recv) == -1) {
            fprintf(stderr, "Couldn't install filter %s: %s\n", filter_recv, pcap_geterr(handle_recv));
            return 0;
        }

        /* Step4, get frame. */
        while(1){
            pkt_data = (unsigned char * )pcap_next(handle_recv, p_header);
            rst = pcap_sendpacket(handle_recv_send, pkt_data, p_header->caplen);
            printf("Send to %s ret : %d\n", RECV_DEVICE_SEND, rst);
        }

        /* Step5, cleanup */
        pcap_freecode(&fp_recv);
        pcap_close(handle_recv);
        pcap_close(handle_recv_send);
        dPrint("Capture complete.");
        return 0;
    }

参考文献

一键在 vs code online 中打开任意 github 仓库

之前有大佬开发过一个项目 [github1s](https://github.com/conwnet/github1s) ,利用 GitHub action ,仅需在任意 github 仓库在 github 后面加上 1s 即可在一个在线的 VS code 中打开这个项目。

https://imagehost-cdn.frytea.com/images/2021/09/03/2021-09-03-5.51.4487ec1eade813be27.png

就在前不久,Github 官方发布了类似的功能,进一步简化了这个过程,仅需在仓库的 web 页面,按下 . 键,没错就是键盘上那个句号,github 就会打开一个在线的 VS code 并开启该仓库,您就可以更方便的浏览这个仓库了。

https://imagehost-cdn.frytea.com/images/2021/09/03/2021-09-03-5.54.000522906cfa16b653.png

两个方式原理类似,都是跳转到另一个网址,之后使用该路径中的地址获取到仓库代码并显示,不得不说,这个功能真的是用起来太爽了,各位好好使用。

参考文献

虚拟机 img 镜像密码修改

本文介绍使用 libguestfs-tools 修改镜像文件密码的方法。

步骤

# 环境
# CentOS Linux release 7.9.2009 (AltArch)
# 鲲鹏 ARM 服务器

第一步:检查并修改qemu访问虚拟机镜像的权限

修改 qemu 配置文件 /etc/libvirt/qemu.conf,将 user = "root"group = "root" 注释取消,并重启 libvirtd 或重启宿主机。

$ vim /etc/libvirt/qemu.conf
- #user = "root"
- #group = "root"
+ user = "root"
+ group = "root"

systemctl restart libvirtd

第二步:安装工具

# 安装libguestfs-tools
$ yum install libguestfs-tools 
$ systemctl start libvirtd

第三步:修改密码

# 以下两组命令貌似均可,但是实测我的环境仅第二个命令可用

$ virt-customize -a CentOS-7-x86_64-GenericCloud.qcow2 --root-password password:xxx
$ virt-sysprep --root-password password:123456 -a *.qcow2

# 若以下错误
# cannot access storage file (as uid:107, gid:107)permission denied
# 说明您第一步没有做,给一下权限再尝试

# 示例
[root@compute-arm-01 stl]$ virt-sysprep --root-password password:123456 -a bionic-server-cloudimg-arm64.img 
[   0.0] Examining the guest ...
[   5.4] Performing "abrt-data" ...
[   5.4] Performing "backup-files" ...
[   5.8] Performing "bash-history" ...
[   5.9] Performing "blkid-tab" ...
[   5.9] Performing "crash-data" ...
[   6.0] Performing "cron-spool" ...
[   6.0] Performing "dhcp-client-state" ...
[   6.0] Performing "dhcp-server-state" ...
[   6.0] Performing "dovecot-data" ...
[   6.0] Performing "logfiles" ...
[   6.1] Performing "machine-id" ...
[   6.2] Performing "mail-spool" ...
[   6.2] Performing "net-hostname" ...
[   6.2] Performing "net-hwaddr" ...
[   6.3] Performing "pacct-log" ...
[   6.4] Performing "package-manager-cache" ...
[   6.5] Performing "pam-data" ...
[   6.5] Performing "passwd-backups" ...
[   6.5] Performing "puppet-data-log" ...
[   6.6] Performing "rh-subscription-manager" ...
[   6.6] Performing "rhn-systemid" ...
[   6.7] Performing "rpm-db" ...
[   6.8] Performing "samba-db-log" ...
[   6.8] Performing "script" ...
[   6.8] Performing "smolt-uuid" ...
[   6.9] Performing "ssh-hostkeys" ...
[   6.9] Performing "ssh-userdir" ...
[   6.9] Performing "sssd-db-log" ...
[   7.0] Performing "tmp-files" ...
[   7.0] Performing "udev-persistent-net" ...
[   7.1] Performing "utmp" ...
[   7.1] Performing "yum-uuid" ...
[   7.1] Performing "customize" ...
[   7.2] Setting a random seed
virt-sysprep: warning: random seed could not be set for this type of guest
[   7.3] Setting the machine ID in /etc/machine-id
[   7.3] Setting passwords
[   8.7] Performing "lvm-uuids" ...

参考文献

GDB 调试 QEMU 源码跟踪 QMP 协议执行

接上文,通过跟踪 libvirt 的源码,找到 virsh domblkinfo 最终是使用 QMP 协议从 QEMU 获取到关键字为 query-block 的数据,其中带有 wr_highest_offset 字段,该字段被 libvirt 认定为 磁盘利用率中 Allocation 值的来源。

今天就尝试在 QEMU 中找到获取 wr_highest_offset 字段的方法。

环境准备

  • QEMU 4.0
  • Centos
  • 鲲鹏 ARM

首先需要编译 QEMU 加入函数表,重新编译 QEMU在其中加入该字段即可,编译方法可以参考源码目录:

./configure --enable-debug

跟踪前需要定位到 QEMU 中填充该字段的函数,首先在源码中全局搜索 wr_highest_offset ,最终确定 block/qapi.c 文件中的 bdrv_query_bds_stats 函数最有可能是填充该字段的位置,下面就来跟踪这个函数的走向吧。

跟踪记录

一个虚拟机在宿主机中表现为一个 QEMU 的进程,在这里仅保留一个虚拟机,查询该虚拟机状态时 libvirt 回使用 unix socket 的方式发往该进程监听的 unix socket 服务。因此跟踪该虚拟机所在进程即可。

# ps -aux | grep qemu
qemu     2185346  0.6  0.5 3562240 333440 ?      Sl   10:05   2:20 /usr/bin/qemu-system-aarch64 -name guest=instance-000001bb,...imestamp=on
root     2472547  0.0  0.0 110784  2496 pts/3    S+   16:03   0:00 grep --color=auto qemu

GDB 开始跟踪:

gdb qemu-system-aarch64 2185346

在之前找到的目标函数处打上断点:

(gdb) b bdrv_query_bds_stats

之后 c 继续执行,尝试查询一下磁盘状态。

$ virsh domblkinfo 25 vda --human

Breakpoint 1, bdrv_query_bds_stats (bs=0x3b549940, blk_level=true) at /root/stl/qemu-4.0.0/block/qapi.c:509
509         BlockStats *s = NULL;

会发现终端卡住了,此时 gdb 中断了进程,说明我们找对函数了,下面我们继续追踪吧。

发现这个函数是在 qmp_query_blockstats 中被调用多次,最终得出结果。

544     }
(gdb) n
qmp_query_blockstats (has_query_nodes=false, query_nodes=false, errp=0xffffe3963110) at /root/stl/qemu-4.0.0/block/qapi.c:609
609                 s->has_device = true;
(gdb) p s->stats->wr_highest_offset 
$3 = 3072

下面主要就是跟着源码来看了,本文主要是讲了如何使用 GDB 跟踪 QEMU 源码,若有疑问欢迎留言。

参考文献

GDB 调试 libvirt 源码之 domblkinfo 命令源码跟踪记

最近发现环境中 KVM 虚拟机磁盘利用率查不准,使用 virsh 命令查看磁盘使用情况得到如下结果:

# virsh domblkinfo 20 vda --human
Capacity:       2.000 GiB
Allocation:     2.000 GiB
Physical:       2.000 GiB

显然是有问题的,正常的数值三个应该不通,进入系统查看磁盘使用率也仅有 2% 左右,因此试图通过检查源码的方式查看是否正确。

  • libvirt: 5.6.0
  • os: Centos

跟踪记录

首先找到 libvirtd 的 PID:

ps -aux | grep libvirtd
root      1907  0.0  0.0 1385796 25116 ?       Ssl  Aug26   5:22 /usr/sbin/libvirtd --timeout 120

使用 GDB 开始跟踪他:

gdb libvirtd 1907

首先在源码中全局搜索 domblkinfo 关键字,找到该命令的执行函数: tools/virsh-domain-monitor.c→cmdDomblkinfo

分析源码找到获取信息的函数 src/libvirt-domain.c -> virDomainGetBlockInfo

if (virDomainGetBlockInfo(dom, device, &info, 0) < 0)
    goto cleanup;

if (!cmdDomblkinfoGet(ctl, &info, &cap, &alloc, &phy, human))
    goto cleanup;
vshPrint(ctl, "%-15s %s\n", _("Capacity:"), cap);
vshPrint(ctl, "%-15s %s\n", _("Allocation:"), alloc);
vshPrint(ctl, "%-15s %s\n", _("Physical:"), phy);

这其中的 info 包含了所需信息,看一下填充该字段的 virDomainGetBlockInfo 函数实现,用 GDB 跟一下它吧.

跟踪 src/libvirt-domain.c -> virDomainGetBlockInfo

先打个断点:

(gdb) break virDomainGetBlockInfo
Breakpoint 1 at 0x7f4d4394a760: file libvirt-domain.c, line 6094.

再打开一个终端,执行一下命令:

[root@compute-01 ~]# virsh list
 Id   Name                State
-----------------------------------
 2    instance-000001b6   running
 3    instance-000001b8   running
 4    instance-000001b9   running

[root@compute-01 ~]# virsh domblkinfo 4 vda

此时会发现终端卡住了,看一下 GDB 已经将程序中断,单步调试看一下:

[Switching to Thread 0x7f4d32ef0700 (LWP 1918)]

Breakpoint 1, virDomainGetBlockInfo (domain=domain@entry=0x7f4cfc00aeb0, disk=0x7f4cfc00cc60 "vda", 
    info=info@entry=0x7f4d32eefac0, flags=0) at libvirt-domain.c:6094
6094    {
(gdb) n
6097        VIR_DOMAIN_DEBUG(domain, "info=%p, flags=0x%x", info, flags);
(gdb) n
6094    {
(gdb) n
6097        VIR_DOMAIN_DEBUG(domain, "info=%p, flags=0x%x", info, flags);
(gdb) n
6099        virResetLastError();
(gdb) n
6101        if (info)
(gdb) n
6102            memset(info, 0, sizeof(*info));
(gdb) n
6104        virCheckDomainReturn(domain, -1);
(gdb) n
6105        virCheckNonEmptyStringArgGoto(disk, error);
(gdb) n
6106        virCheckNonNullArgGoto(info, error);
(gdb) n
6110        if (conn->driver->domainGetBlockInfo) {
(gdb) n
6112            ret = conn->driver->domainGetBlockInfo(domain, disk, info, flags);
(gdb) s
qemuDomainGetBlockInfo (dom=0x7f4cfc00aeb0, path=0x7f4cfc00cc60 "vda", info=0x7f4d32eefac0, flags=0)
    at qemu/qemu_driver.c:12413
12413   {

发现在 6112 行跳到了另一个函数,继续跟踪它.

跟踪 src/qemu/qemu_driver.c -> qemuDomainGetBlockInfo

(gdb) n
12421       virCheckFlags(0, -1);
(gdb) n
12413   {
(gdb) n
12414       virQEMUDriverPtr driver = dom->conn->privateData;
(gdb) n
12421       virCheckFlags(0, -1);
(gdb) n
12419       qemuBlockStatsPtr entry = NULL;
(gdb) n
12414       virQEMUDriverPtr driver = dom->conn->privateData;
(gdb) n
12421       virCheckFlags(0, -1);
(gdb) n
12423       if (!(vm = qemuDomObjFromDomain(dom)))
(gdb) n
12426       cfg = virQEMUDriverGetConfig(driver);
(gdb) n
12428       if (virDomainGetBlockInfoEnsureACL(dom->conn, vm->def) < 0)
(gdb) n
12431       if (qemuDomainObjBeginJob(driver, vm, QEMU_JOB_QUERY) < 0)
(gdb) n
12434       if (!(disk = virDomainDiskByName(vm->def, path, false))) {
(gdb) n
12440       if (virStorageSourceIsEmpty(disk->src)) {
(gdb) n
12448       if (!virDomainObjIsActive(vm)) {
(gdb) n
12460       if (qemuDomainBlocksStatsGather(driver, vm, path, true, &entry) < 0)
(gdb) n
12463       if (!entry->wr_highest_offset_valid) {
(gdb) n
12466           if (virStorageSourceGetActualType(disk->src) == VIR_STORAGE_TYPE_FILE &&
(gdb) n
12468               info->allocation = entry->physical;
(gdb) n
12466           if (virStorageSourceGetActualType(disk->src) == VIR_STORAGE_TYPE_FILE &&
(gdb) p info->allocation
$2 = 0
(gdb) n
12470               info->allocation = entry->wr_highest_offset;
(gdb) n
12484       if (entry->physical == 0 || info->allocation == 0 ||
(gdb) p info->allocation
$3 = 32870912
(gdb) p entry->wr_highest_offset
$4 = 32870912

至此,我们知道了 info -> allocation 的值来自 entry->wr_highest_offset ,接下来查看源码, entry->wr_highest_offset 的值应该是在这里被赋予的:

if (qemuDomainBlocksStatsGather(driver, vm, path, true, &entry) < 0)
  goto endjob;

下面将断点打在 qemuDomainBlocksStatsGather 看一下其中的 entry->wr_highest_offset 是在哪里被赋值.

跟踪 src/qemu/qemu_driver.c -> qemuDomainBlocksStatsGather

将之前的断点删除,打上新的断点

(gdb) info breakpoints 
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00007f4d4394a760 in virDomainGetBlockInfo at libvirt-domain.c:6094
        breakpoint already hit 1 time
(gdb) delete 1
(gdb) break qemuDomainBlocksStatsGather
Breakpoint 2 at 0x7f4d208e3700: file qemu/qemu_driver.c, line 11427.

之后在 GDB continue ,之后一直按回车,直到程序正常运行了,再执行一下获取磁盘信息的命令,继续跟踪。

Breakpoint 1, qemuDomainBlocksStatsGather (driver=driver@entry=0x7f4d180f99b0, vm=0x7f4d100b8890, 
    path=path@entry=0x7f4d0c00ae50 "vda", capacity=capacity@entry=true, retstats=retstats@entry=0x7f4d336f0980)
    at qemu/qemu_driver.c:11427
11427   {
(gdb) n
11428       qemuDomainObjPrivatePtr priv = vm->privateData;
(gdb) 
11429       bool blockdev = virQEMUCapsGet(priv->qemuCaps, QEMU_CAPS_BLOCKDEV);
(gdb) 
11439       if (*path) {
(gdb) 
11440           if (!(disk = virDomainDiskByName(vm->def, path, false))) {
(gdb) 
11445           if (blockdev) {
(gdb) 
11448               if (!disk->info.alias) {
(gdb) 
11458       qemuDomainObjEnterMonitor(driver, vm);
(gdb) 
11459       nstats = qemuMonitorGetAllBlockStatsInfo(priv->mon, &blockstats, false);
(gdb) 
11461       if (capacity && nstats >= 0) {
(gdb) 
11465               rc = qemuMonitorBlockStatsUpdateCapacity(priv->mon, blockstats, false);
(gdb) 
11468       if (qemuDomainObjExitMonitor(driver, vm) < 0 || nstats < 0 || rc < 0)
(gdb) 
11471       if (VIR_ALLOC(*retstats) < 0)
(gdb) 
11474       if (entryname) {
(gdb) 
11475           if (!(stats = virHashLookup(blockstats, entryname))) {
(gdb) 
11481           **retstats = *stats;
(gdb) p stats
$12 = (qemuBlockStats *) 0x7f4d0c001000
(gdb) p *stats
$13 = {rd_req = 712, rd_bytes = 17435136, wr_req = 130, wr_bytes = 418816, rd_total_times = 527027278, 
  wr_total_times = 86798718, flush_req = 20, flush_total_times = 94396427, capacity = 2147483648, 
  physical = 2147483648, wr_highest_offset = 32870912, wr_highest_offset_valid = true, write_threshold = 0}
(gdb) c
Continuing.

分析这一调用过程,发现我们跟踪的 restats 变量来自 stats,而该值在这一行被填充:

11475           if (!(stats = virHashLookup(blockstats, entryname))) {

值来自哈希表查询结果,从 blockstats 中查询 entryname ,而该哈希表在这两行被赋值:

11459       nstats = qemuMonitorGetAllBlockStatsInfo(priv->mon, &blockstats, false);
11465       rc = qemuMonitorBlockStatsUpdateCapacity(priv->mon, blockstats, false);

之后就可以跟踪源码了,经过一番探索,发现他们最终都调用了同一个函数来从 QEMU 获取设备信息,即 src/qemu/qemu_monitor_json.c -> qemuMonitorJSONQueryBlock ,看一下它的函数实现:

static virJSONValuePtr
qemuMonitorJSONQueryBlock(qemuMonitorPtr mon)
{
    virJSONValuePtr cmd;
    virJSONValuePtr reply = NULL;
    virJSONValuePtr devices = NULL;

    if (!(cmd = qemuMonitorJSONMakeCommand("query-block", NULL)))
        return NULL;

    if (qemuMonitorJSONCommand(mon, cmd, &reply) < 0 ||
        qemuMonitorJSONCheckReply(cmd, reply, VIR_JSON_TYPE_ARRAY) < 0)
        goto cleanup;

    devices = virJSONValueObjectStealArray(reply, "return");

 cleanup:
    virJSONValueFree(cmd);
    virJSONValueFree(reply);
    return devices;
}

继续探索会发现 libvirt 在这里调用了 QEMU 提供的 QMP 协议,其中的查询关键词为 query-block ,返回的结果中含有 wr_highest_offset 字段。

最终得到一张 libvirt 查询磁盘使用情况的调用栈示意图:

https://imagehost-cdn.frytea.com/images/2021/09/02/domblkinfoac4ecdcf5caa1926.png

如果继续探索,可能就需要去跟踪 QEMU 源码了,下篇文章见。

参考文献

附件

libvirt-domblkinfo-命令源码调用栈 .xmind

解决 Clash for windows 端口为 0 导致无法使用

今天更新完 Windows 重启后发现上不了网了,检查 clash for windows 发现监听端口为 0

https://imagehost-cdn.frytea.com/images/2021/08/30/20210830095030627b1fa801f19241.png

这就不正常了,检查了一下 `C:\Users\

\.config\clash\logs` 的日志,发现这行报错: “`perl level=error msg=”Start Mixed(http and socks) server error: listen tcp 127.0.0.1:7890: bind: An attempt was made to access a socket in a way forbidden by its access permissions.” “` 貌似是端口无法被正常绑定,网上找了一下原因,发现遇到该问题的人不少,大致这样解决: `CMD` 执行这行指令 `netsh int ipv4 show dynamicport tcp` 发现起始端口变成了1024。 管理员身份运行 CMD 执行这些命令: “`bash # 这两条命令来自博客 https://blog.csdn.net/tian2342/article/details/108934646 netsh int ipv4 set dynamicport tcp start=49152 num=16383 确定。 netsh int ipv4 set dynamicport udp start=49152 num=16383 确定。 # 这条命令来自 https://github.com/Fndroid/clash_for_windows_pkg/issues/671 netsh int ipv4 set dynamic tcp start=49152 num=16384 “` 然后检查结果 “`bash netsh int ipv4 show dynamicport tcp “` 端口正常后**重启计算机,恢复正常**。 ## 参考文献 – [WIN10更新后端口显示为0的解决方法 #671](https://github.com/Fndroid/clash_for_windows_pkg/issues/671) – [关于Windows端口没被占用提示An attempt was made to access a socket in a way forbidden by its access permissions](https://blog.csdn.net/tian2342/article/details/108934646) – [Clash端口显示为0的解决方法](https://www.cnblogs.com/anyview/p/15056008.html)

Perl 程序后台执行示例

最近阅读 PVE 源码发现一处源码这样使用了 fork() 方法:

$spid = fork();
    if (!defined ($spid)) {
        die "can't put server into background - fork failed";
    } elsif ($spid) { # parent
        exit (0);
    }

自己写示例发现这种方法可以使程序进入后台执行状态,大概原理是 fork 子进程,退出主进程,使得程序被 1 号父进程接管,在终端表现则是进入了后台执行状态

以下是实例代码:

#!/usr/bin/perl

sub mainThread() {
    print "---------- Main Thread! ------------\n";
    $spid = fork();
    if (!defined ($spid)) {
        die "can't put server into background - fork failed";
    } elsif ($spid) { # parent
        exit (0);
    }
    for(;;)
    {
        print "Hello, world in main thread!\n";
        sleep 1;
    }
}

mainThread();

看下进程状态:

https://imagehost-cdn.frytea.com/images/2021/08/26/_1629948977368e83558ffb3dfcdb2.png

退出程序则是指定 PID 即可:

$ kill -9 3300

参考文献