上一篇文章介绍了我购买并配置Intel NUC13Pro的过程。在使用过程中，我发现有时候开机会找不到启动盘，有时候系统会突然卡死。本文将记述我逐步排查问题，并最终更换SSD的过程。SSD掉盘的三种形式其实在最开始的一周，这块金士顿的SSD并没有表现出太多的问题。偶尔在打开IDEA的时候会卡顿，有时候会直接导致系统卡死。我一开始以为是Swap的原因，因为Swap给的太小了，我平时工作需要开40多个Chrome标签，还有2个左右的IDEA，整体不够用。后来紧急从京东下单了一条DDR4 32GB的内存条，变成了8+32的配置，内存就赋予了很多，现在基本上日常占用在20GB左右，因此也就不着急升级到32+32了。但这个时候系统卡顿的情况还是会偶尔发生，但不是很频繁，我就没在意。第一次系统卡死发生后我就开启了Sysrq，这样我就可以通过REISUB来安全的重启系统了。但后来我发现了两个现象：首先就是卡死后重启有机率找不到SSD，表现形式就是BIOS提示A bootable drive has not been detected，进入bios后发现NVMe槽是空的，和没插盘是一样的。其次就是我注意到在按下Alt+PrtScr+R之后，输出会被强制切换到控制台，这个时候有机会能看到NVMe相关的报错，主要有三条：第一条的具体报错记不清了，大概就是Btrfs进行IO操作的时候因为超时失败了第二条也记不清了，大意是NVMe没有及时响应系统的请求，内核尝试reset控制器第三条则是一个Abort status: 0x371但大部分情况都能在重启后解决，于是我就没有当回事。直到周五晚上我打开IDEA，系统突然崩溃——和卡死不同，鼠标和UI都是有响应的，但任何从硬盘读数据的行为都会被阻塞（事后才知道，这个时候盘已经掉了，ui还能响应全靠内存的缓存）。于是我便重启了，重启之后看到控制台询问我的luks密码，我就按了电源键关机，睡觉去了。可等到周六我再开机，那盘就再也没出现过了。当时我想可能是因为金手指接触不良，我拆开擦擦就是了。结果拆开再装回去，还是不识别。我就很奇怪，于是就保持后盖打开，装好SSD，再开机——识别了。好的，那我们把后盖合上，然后再开机——系统找不到盘。我：？？？此时我心中有了一些假设，因为刚才拆下来擦金手指的时候我就觉得这块盘的做工极差——像是三星和西数的SSD，你轻微施力掰它，它的PCB是比较硬的，这种手感的回馈就是告诉你不要掰；而金士顿这块硬盘的PCB很薄，也很软，你轻微施力掰它，它就随着你用力而弯曲，没有太多的抵抗。而NUC为了节省空间，它的SSD并不是贴在主板上的，反而是悬空的。在垂直方向，先是网卡在主板上，然后是SSD摞在网卡上。所以我推测会不会是背盖上的散热条把SSD压弯了，导致M.2接口接触不良呢？针对这个假设，我做了一个实验：不装背盖：可以检测到盘装上背盖，但不拧螺丝：可以检测到盘装上背盖，用手旋紧螺丝：有机率可以检测到盘。如果遇到掉盘，拆下背盖后重新安放、拧紧装上背盖，用螺丝刀拧紧螺丝：我试了好几次，就是检测不到盘虽然不能确定就是散热条把SSD压弯了导致接触不良，但从结果来看，螺丝的松紧程度确实能够影响盘能不能被识别到。因此我想到了一个很简易的办法：把纸叠起来，垫在ssd底下。因为叠起来的纸有弹性，所以可以确保把SSD压在散热条上，这样就可以保证它处于正确的位置了。经过这样一番折腾，我成功地解决了PCB太软的问题。但这还并不是问题的全部。周六中午我在进行网络冲浪的时候，突然发现浏览器崩溃了，而且没有崩溃报告，同时所有需要从硬盘读数据的操作都卡住了。我很自然地按下Alt+PrtScr+R，看到了先前说的几条报错信息。于是我就去网络上搜索了一下，大部分结果都指向一个叫做APST的东西。这东西全称Autonomous Power State Transition，中文叫自动电源状态转换。这个功能的主要目的就是在你不使用或轻度使用NVMe盘时自动缩减功耗，从而为移动设备争取一些续航。但这种缩减功耗带来的就是响应延迟的上升，而这里我确实在控制台上看到一个timeout，所以我觉得有戏。根据ArchWiki)所说，Linux内核对于Power State最大延迟设置是25ms，任何超过25ms的硬盘都应该通过内核参数手动调整这个值，来避免Linux误认为NVMe盘出问题了。于是我通过sudo nvme id-ctrl /dev/nvme0n1查看了一下我这个盘的延迟：ps 0 : mp:5.00W operational enlat:0 exlat:0 rrt:0 rrl:0 rwt:0 rwl:0 idle_power:- active_power:- active_power_workload:- ps 1 : mp:3.50W operational enlat:0 exlat:200 rrt:1 rrl:1 rwt:0 rwl:0 idle_power:- active_power:- active_power_workload:- ps 2 : mp:2.50W operational enlat:0 exlat:1000 rrt:2 rrl:2 rwt:0 rwl:0 idle_power:- active_power:- active_power_workload:- ps 3 : mp:1.50W non-operational enlat:5000 exlat:5000 rrt:3 rrl:3 rwt:3 rwl:3 idle_power:- active_power:- active_power_workload:- ps 4 : mp:1.50W non-operational enlat:20000 exlat:70000 rrt:4 rrl:4 rwt:4 rwl:4 idle_power:- active_power:- active_power_workload:-总延迟是enlat + exlat，这么来看，如果SSD进入了4号电源状态，那么总共需要90ms才能响应系统的指令，这远超过Linux内核默认的25毫秒。所以我在YaST中添加了内核参数nvme_core.default_ps_max_latency_us=100000，我打出富裕量，给了100ms的超时。此时我以为这些问题都被解决了，可以高枕无忧了。但随后的事情让我直接去京东下单了新的SSD。故事发生在周六下午。我在为接下来的五一假期做准备，我计划带着这个NUC和一个便携屏幕出游。这个NUC比笔记本要轻大约一斤多，而这块便携屏幕不用的时候还可以给Switch用，我觉得甚是方便。于是便开始从家里的NAS拷贝一些要看的电影、动漫之类的。就在拷贝途中，整个系统突然崩溃了，那我能怎么办？重启呗。结果重启之后就找不到盘了。我这一想，纸也垫了，内核参数也加了，怎么就还是有问题呢？而且这次掉盘持续时间很长：无论怎么开机，就是提示找不到。我只能又拆开背盖。拆开背盖之后用手一摸SSD，我本意是想可能还是接触不良，我就伸手押着SSD头部，没想到我瞄的特别准，一手指头就按到主控上去了，当时我就后悔了——烫啊，真他妈烫啊。凉了差不多十分钟，盖上盖子再开机，SSD又像个好人似的了。于是我便继续复制影视作品，没想到，梅开二度，又来一次。这下我可就犯难了：垫上纸可能会导致SSD过热，毕竟纸基本不导热，而且更像是给SSD加了个褥子。可是不加纸吧，这动不动就掉盘，我真怕数据丢了。每次强制重启之后我都要用btrfs scrub检查一下数据和元数据，幸好没有出错。但是老这样不是办法啊，看了一眼时间，我还有机会，现在下单明天（周日）就能到。于是开始考虑购买哪一款SSD。首先排除三星，不是它不好——我笔记本的两条SSD都是三星的，我给亲戚组电脑也推荐的三星，我用的很满意——但就是太贵了。我这个月为了NUC和望远镜已经花了不少钱了，我想控制一下预算。但是又得避免因为控制预算而买到类似这个金士顿的情况，于是左思右想，买了2TB的西数SN580。虽然不是顶级产品线，比三星差了不老少，但是我从一些测评来看，它至少稳定，不会翻车，保修也靠谱，三年900TB写入，最主要的是2TB卖1000块钱，隔壁三星1千连1TB都不给你，还要什么自行车啊。周日上午硬盘如期而至，下面的问题就是怎么在不重装系统的情况下迁移并扩容硬盘。迁移启动盘如果是数据盘那就好办了，直接复制粘贴。但是系统盘的话有启动分区，还有好多安装的配置，如果重装的话就太麻烦了。我安装系统时使用了LUKS加密，并且开启了LVM，主文件系统是Btrfs。由于这次是从500GB到2TB的“升级”，所以相对来说还是容易一些。计划大体分两部：使用CloneZilla迁移数据，然后再想怎么扩容。ClonezillaClonezilla一直是一个我很喜欢的工具。它和dd不一样，clonezilla能够感知文件系统。对于没有全盘加密的分区，它可以只保留实际有数据的部分。对于有加密的分区，新版本还可以先解密再按照分区抓取。不过我这里的目的更像是1:1复制整个硬盘，所以我没有选择解密。整个盘500G，实际使用不到80GB，最终经过压缩，占用的数据量只有120GB左右。不过这个是新盘，如果用过一段时间的话，没被使用的部分会留下难以压缩的脏数据，这个到时候就麻烦了。这次好在我有可以用来存储的移动硬盘，因为移动硬盘也是固态，所以抓取很快就结束了。在抓取的过程中，我根据以前的经验，让SSD尽可能处在最佳状态：打开背盖、吹风扇。抓下来数据之后，保存下来就可以往新的SSD里还原了。还原的过程不需要任何额外的配置，由于分区大小不一样，我选择按照原始分区大小还原，之后由我手动来扩容。还原之后重启，看到屏幕上询问我LUKS密码的字样，我心头的石头就放下了一半。进到系统进行了简单的测试，确认文件系统没有任何错误之后可以进行扩容了。扩容本节的命令如果没有特殊说明，则都是在root权限下运行。扩容阶段我使用的是openSUSE TW的Gnome LiveCD，它里面包含了一个叫做GParted的图形工具，可以用来分区，比YaST的分区工具要好用。在分区工具里，我发现我的硬盘上竟然有4个分区：1个Linux的启动分区，1个Windows EFI分区（16MB），1个LVM数据卷，还有1个不知道是做什么的大小只有500MB的分区。虽然我在安装系统的时候指示安装器把Windows分区删掉，可能安装器以为我要Dual Boot，就没删干净。不过没关系，这里我们可以把他们删掉，这样一来，nvme0n1p1是Linux的启动分区，nvme0n1p3是LVM数据卷。这个数据卷前后都有空闲空间，虽然前面的空闲空间只有16MB，那也不能闲着。默认情况下LVM分区是没有解密挂载的，所以我们可以直接开始操作：在GParted里右键LVM卷选择Move，然后通过滑块调整，让前面的空闲空间变为0MB，然后把大小拉到最大。这样除了100MB的启动分区，剩下的就全都是LVM的天下了。保存结果后GParted需要将原来的数据复制过去，就是说前面的16MB不能空着，得让数据整体往前移动16MB。好在NVMe固态硬盘很快，没有十分钟就复制完了。接下来就是让LVM扩容。我在网上看到一些资料声称要给LUKS扩容。我查询了一些资料，发现LUKS是不需要扩容的，它本质是一种映射：你访问一个块，它把数据解密之后给你，它并不关心这个分区有多大，它只关心在数据写入磁盘之前进行加密、从磁盘读出来之后解密。由于先前我们使用GParted进行了位移操作，所以LVM肯定是没有挂载的。挂载LVM时只能往后延伸分区，并不能位移。如果你挂载了LVM，那么你可以通过关闭再打开的方式让LUKS设备刷新它的大小，或者通过如下命令让LUKS知晓你对分区的变更：cryptsetup resize <name>这里的name是解密挂载时的名称，如果你是让GParted挂载的，那么名字通常是<partition>_crypt，比如我的分区是nvme0n1p3，那么我解密挂载的设备就是nvme0n1p3_crypt。当LUKS能够检测到新的硬盘大小之后，我们就可以扩容LVM了。这里涉及到LVM的三个概念：PV，也称Physical Volume，即物理卷。物理卷是在磁盘中实际存储数据的分区，比如我们的nvme0n1p3VG，也称Volume Group，即卷组。一个卷组类似一个存储池，它将多个物理卷抽象成一个块设备，形成一个逻辑上的硬盘LV，也称Logical Volume，即逻辑卷。一个卷组可以分配出多个逻辑卷。逻辑卷就像是虚拟硬盘上的分区，这个也是我们实际给文件系统用的分区这样就看出来LVM的好处了：通过将多个物理设备抽象成逻辑概念，我们可以保持分区不动的情况下更换或调整物理设备。那么从这个结构中也能看出来，物理卷是最基础的一层，我们应该先扩容它：pvresize /dev/mapper/nvme0n1p3_crypt在没有任何多余选项的情况下，pvresize命令会让/dev/mapper/nvme0n1p3_crypt这个物理卷扩展到整个分区。如果你不知道这个路径该用什么，可以使用命令pvs查询所有可用的物理卷。通常情况下只有一个，因为livecd不会用lvm的。调整完成后再看pvs的输出结果，PSize变成1.82TB就算是成功了。接下来调整逻辑卷。我想调整两个，一个是Swap，从原来的2GB调整成8GB，然后是root，让它占满余下的分区。由于Btrfs可以缩小，所以这里我就让它先占满了。如果你使用的文件系统不支持缩小，那么扩容的时候最好谨慎一些，等不够用了给。不然等日后多了新的需求，却发现硬盘没地方了，那就有点尴尬了。首先来调整swap分区：lvresize -L+6G /dev/system/swap这里的意思是将/dev/system/swap的大小增加6GB。这个路径是怎么来的呢？使用lvs可以查询到，我的卷组名字叫system，swap分区的逻辑卷名字就叫swap，所以这个路径就是这样了。目前我看tab自动补全的效果还是不错的，如果实在不会写，可以让自动补全碰碰运气。现在分区的大小是变大了，可是上面的文件系统还没有扩容。好在swap分区不存在任何实际的数据，直接格式化就好了：mkswap /dev/system/swap这个时候注意检查一下原来系统的fstab，我这里发现fstab使用的是路径，也就是说无论分区的uuid怎么变，只要最后被挂载到/dev/system/swap的就是交换分区。如果你的fstab还在使用uuid的话，那这里要记录一下mkswap输出的新分区的uuid，之后你要自己更新fstab，不然系统是找不到swap分区的，可能会在启动的时候引发内核panic。接下来就可以扩容root分区了：lvresize -l+100%FREE /dev/system/root这里的小写字母L是通过比例来调整分区大小。显而易见，我这条指令让root分区的大小占满剩余空间。完成后查询lvs，确保分区大小如我所愿。最后就可以扩容btrfs了。我很喜欢btrfs的一个点就是它的好多操作就可以在线进行，并且交互简单明了：mkdir /mnt/root mount /dev/system/root /mnt/root这样一来我们就把原来的根分区挂载到/mnt/root路径了。然后使用btrfs进行在线扩容：btrfs filesystem resize max /mnt/root这样扩容就完成了。这时就可以修改fstab了（如果需要的话），路径是/mnt/root/etc/fstab。除此之外，还可以顺手检查一下数据和元数据是否正确：btrfs scrub start /mnt/root btrfs scrub status /mnt/root第一条命令启动scrub，第二条命令查询scrub的状态。当第二条命令返回的状态为finish，并且没有error时就万事大吉了。这个时候我们就可以卸载分区了，卸载之后还可以顺手检查一下btrfs的结构完整性：umount /mnt/root btrfs check /dev/system/root我本以为无事发生，结果走到7/7的时候出错了，说qgroup读不出来。这好办，qgroup是和quota（配额）有关的，虽然我现在不用，但是可以先修好：mount /dev/system/root /mnt/root btrfs quota disable /mnt/root btrfs quota enable /mnt/root # 执行完成后要等一会儿，开启配额后需要rescan umount /mnt/root这样一通操作之后再去做check，7/7全部通过，非常好。最后我们需要退出livecd，进入原本的系统看看是否一切正常。进系统进入系统之后除了检查数据在不在，还可以根据ArchWiki的简易配置一下Power State超时。这是新硬盘的电源状态：ps 0 : mp:4.80W operational enlat:0 exlat:0 rrt:0 rrl:0 rwt:0 rwl:0 idle_power:0.3000W active_power:4.80W active_power_workload:80K 128KiB SW ps 1 : mp:3.50W operational enlat:0 exlat:0 rrt:0 rrl:0 rwt:0 rwl:0 idle_power:0.3000W active_power:3.00W active_power_workload:80K 128KiB SW ps 2 : mp:2.40W operational enlat:0 exlat:0 rrt:0 rrl:0 rwt:0 rwl:0 idle_power:0.3000W active_power:2.00W active_power_workload:80K 128KiB SW ps 3 : mp:0.0150W non-operational enlat:1500 exlat:2500 rrt:3 rrl:3 rwt:3 rwl:3 idle_power:0.0150W active_power:- active_power_workload:- ps 4 : mp:0.0050W non-operational enlat:10000 exlat:6000 rrt:4 rrl:4 rwt:4 rwl:4 idle_power:0.0050W active_power:- active_power_workload:- ps 5 : mp:0.0033W non-operational enlat:176000 exlat:25000 rrt:5 rrl:5 rwt:5 rwl:5 idle_power:0.0033W active_power:- active_power_workload:-真不是我黑金士顿。金士顿那个p3和p4延迟差距巨大，可是功耗还都是1.5W。人家西数的蓝盘，在P3就降到15mW了，虽然P5的延迟比200ms还高，可是人家功率小啊，人家能干到3.3mW。金士顿，呸。除了配置内核参数之外，我还进行了一些简单的测试，比如持续拷贝大文件之类的。主要是给这块盘上点压力，看看会不会掉盘。目前为止也都很稳定。至此这一套换盘的操作也算是打完收工了。后日谈虽然这一次有惊无险的解决了问题，但时候回想起来，从原来的SSD里抓数据是最悬的——如果抓到一半盘突然暴毙，那我就只能重新装系统，重新配环境了，这得耽误多少事啊。这件事情你要说生气吧，倒也不至于，毕竟解决了。但是你说心平气和吧，我又不是很平和。其中我最想不明白的一点就是，Intel NUC一直以来溢价就比较高，我花五千多块钱买个NUC，去隔壁零刻能买俩。那我都花了这么多钱了，为什么不能给我配个好点的SSD呢？再后来我就想：你既然都知道它溢价严重，那你不想想它为什么溢价？你花了钱就得给你应得的配置，那还叫溢价吗？你猜为什么京东上裸机一直缺货？文章结尾再劝诫一下大家吧：买存储相关的设备，别碰金士顿。我这个OEM的盘是金士顿NV2系列的，500GB，当时得亏没买1TB的，不然肠子得悔青了。后来有一个国际友人得知此事后跟我说，Tom's Hardware有一篇关于这个硬盘的测评，最后的结论是：在大多数情况下，金士顿 NV2 根本不是我们可以推荐的硬盘，因为它可能配备的硬件范围让购买者风险太大。你可以买到优秀的 SM2269XT 控制器，也可以买到平庸的 SM2267XT 控制器。你可以购买 BiCS5 TLC 或速度更慢的 QLC。这些产品都没有 DRAM，而且在所有这些产品中，称这款产品为 PCIe 4.0 硬盘可能有失公允。在测试中，该硬盘即使在空闲时也会发热，而且耗电量相当大，因此对于包括笔记本电脑在内的许多机器来说，它并不是理想之选。它的优势在于其令人惊叹的价格，这使得它除了作为主硬盘外，或许还能用于其他用途。除此之外，你可能并不希望掷骰子来赌运气。最好从最坏的角度看：这是一款经济型硬盘，旨在以低廉的价格增加更多 NVMe 级存储空间。如果您使用过 A400 或 NV1，那么您就会知道 NV2 的预期效果。许多主板都配备了自己的 M.2 散热器或屏蔽罩，这也有助于缓解这款硬盘可能存在的发热问题，从而减轻其缺点之一。我们的样品都使用了 SMI SM2267XT 控制器，它在 TLC 或 QLC 上的表现都不尽如人意。无论是哪种情况，闪存的性能其实都不差。只是市场上还有很多更好的选择。价格确实是您在考虑 NV2 时必须考虑的最终因素。如果它的价格与 SATA 替代品相比具有竞争力，那么作为游戏硬盘也许还不错，但这意味着你需要更高的容量。在容量较低的情况下，它只是一种廉价升级旧机器的方式——但我们没有对这些 SKU 进行评测。这时候我想到了我买的SN580，正好也有一篇测评，结论是这么说的：蓝盘 SN580 是西部数据推出的一款相对低成本的产品，它不冒任何风险，但却能提供令人满意的体验。它汲取了 SN570 和 SN770 的基因，是一款介于两者之间的硬盘，其核心是一款出色的经济型固态硬盘。虽然称其为 PCIe 4.0 硬盘有些夸张，但它确实比 SN570 性能更强、缓存更好，而且没有踩到 SN770 的脚趾。在大多数情况下，它将以更低的价格带来与 SN770 相当的体验，尽管这表明 SN770 最终会被取代。SN580 拥有相当不错的保修服务，软件支持也不差。这些都不是主要的优势，但加上西部数据成熟的设计，它是一款非常可靠的硬盘。无论您的用途如何，它都是一个不错的选择。西部数据在硬盘硬件上确实有过更新换代的历史，比如 SN550 和 SATA 蓝盘，但从测试和推出的情况来看，SN580 是一款值得信赖的经济型硬盘。它并不是我们见过的最令人兴奋的产品，但这并不意味着它不好或没有用武之地。我们已经测试过这个级别的许多优秀硬盘，即 4-5GBps 范围内的无 DRAM PCIe 4.0 硬盘，它们提供的出色体验甚至可以与高端硬盘媲美。但 2TB 硬盘可能是个例外，许多 2TB 硬盘都换成了 QLC，不过 SN770、SN570 和现在的 SN580 并不适用。如果您正在寻找一款预算充足的 2TB TLC 硬盘，那么 SN580 将是您的不二之选。与 SN570 相比，SN580 在性能上的小幅提升和缓存设计上的改进是一大优势，这将带来更好、更高效的使用体验。SN580 的真正竞争对手是 SN770，以及以 Predator GM7 为代表的更高端无 DRAM 4.0 产品。虽然 SN580 有西部数据的名字，但后者可能会以相当的成本提供更好的体验。就 SN770 而言，它仍然是一款出色的硬盘，但可能需要及时更新。这样一来，SN580 就成了一个安全、全面的选择，没有任何新意或令人兴奋的功能。它只是填补了阵容中的一个位置，在这个位置上，它可以碾压金士顿 NV2 等不太理想的经济型 PCIe 4.0 硬盘。剩下的就是价格了。看起来不错。-全文完。本片撰写于Linux上。-
之前写过一篇帮朋友配置Intel NUC的文章，没想到今天我也有自己的NUC了。本篇记述我为这个NUC安装和配置系统的全过程。硬件选取本来一开始是没打算买主机的，因为我家这边夏天经常停电，不过不搭配UPS使用的话，频繁的突然断电早晚会把文件系统弄坏。但这次下定决心买NUC，主要还是因为发工资了，并且工作用的电脑一直没到，开虚拟机笔记本又过热，笔记本一过热电池就鼓包，活生生变成一个安全隐患。所以想来想去，不如买一个永除后患。说起mini主机，我第一个想到的就是Intel NUC。现在这个赛道，Intel NUC已经卖给华硕了，国产品牌也有不少后起之秀，比如零刻，价格便宜，性能更好。不过我买都买了，也就不多说什么了。我觉得Intel虽然把NUC卖给了华硕，但两家毕竟都是大厂，最基本的保障还是有的。而且我买NUC的目的主要是运行Linux，虽然零刻那边价格便宜，但是不知道Linux支持怎么样。如果到时候又得关安全启动，又得调着调那，最后还有一堆东西不兼容，那就麻烦了。好消息：Intel NUC用到现在，安全启动不用关，什么也不用调，装出来的系统直接就能用，也没发现有什么硬件不兼容的问题。特别好。这里要点名批评雷蛇：都什么年代了，还在BIOS里偷工减料。我买你们家的笔记本，在上面跑个Linux怎么了？且不说Linux支持有多差，甚至哟肚饿发行版本跑都跑不起来，电池还老鼓包。什么玩意儿啊，呸。至于型号，我买的是NUC13ANHi7，就是i7厚款。京东买的，自带16GB内存和500GB的NVMe固态。本来想买裸机然后回来自己插，这样的话钱不会差太多，但东西更好。可是裸机京东没货了，迫不得已只能买了个16GB内存的版本。好在对于Linux来说，500GB的固态算是够用，目前只是工作使用，未来打算将除了打游戏之外的需求都搬到NUC上，不过那是之后的事情了。内存只有16GB，对于Linux本身还是够用的，不过大家从我之前的文章也看出来了，我是写Java的，那IDEA开起来，Gradle daemon后台一跑，再来几个数据库的Docker，还有Chrome，开他十几个标签，直接给16GB的内存干满了——浏览器用掉了一半的内存，Java用掉了剩下的一半。数据库和Linux系统只能在swap中夹缝求生。所以下个月发了工资第一件事就是升级一下内存，直接干到64GB拉满。在存储方面我也考虑了一下，原本打算做个Btrfs的RAID 1，但是后来发现有点亏。虽然说我这个NUC能插三个盘：一个NVMe固态，尺寸是2280，一个M.2 B key的SATA固态（NGFF），但尺寸是2242，还有一个就是2.5英寸的SATA硬盘位。第一个槽位的NVMe暂且好办——买来的时候已经插上了，暂时不需要更换。虽然是金士顿的，但总不能刚到手就暴毙吧。第二个NGFF的就不好办了——我目前没找到哪个大牌子还在做这个协议的硬盘。淘宝上能找到的全都是杂牌子，2TB的硬盘能卖到726，这价格我真的有点不敢碰。最后SATA的盘位也好办，京东上随便找个三星镁光之类的冲就完了。所以归根结底，我觉得Intel NUC这个2242的B key槽位就很鸡肋，哪怕你整一个2242的NVMe呢？所以之后升级的话我可能就要考虑放弃这个槽位，只扩容SATA盘位了。如果硬要扩展的话也不是不行。所有数据都放在主盘上，然后可以重建的数据放在SATA盘上，比如BT下载之类的，总之就是丢了可以在下回来的。至于2242那个盘，那就只能存放一些本地有备份的，比如有些电影想拿出来再看一遍，那就放到这个盘上，万一崩盘了也不会造成任何麻烦。至于性能，我这个型号的NUC用的CPU是13代I7 1360P。从跑分来看这个CPU甚至要比我现在笔记本的i7 10875H还要高。不过实际使用来看，可能是内存不够吃，浏览器反而有卡顿的感觉。不过以现代CPU的性能来看，只要不是土豆，只要你不跑高性能运算，大部分情况下，包括开发，都是够用的。瓶颈和限制都在外部：比如电源功率上不去，散热跟不上，内存频率低/不够用。。。反正我觉得Intel NUC对我来说是够用的。那么决定好硬件之后，京东下单从上海调货，花费5299，后来想起来自己没有USB鼠标，只有键盘，又跑去戴尔那边买了个20块钱的光学鼠标。装系统一如既往地，我选择了openSUSE。这次也一样，在Leap和Tumbleweed之间纠结了一下，最后选择了风滚草。由于买的是带硬盘的配置，所以硬盘上带了个Windows 11，但是我不想折腾这个东西，但也不想放过这个机会，所以为了以备不时之需，我选用CloneZilla搞了个整盘克隆。如果之后有需要的话可以通过这个克隆文件把Windows 11还原回去。克隆完毕之后我又去华硕的官网搜索我这个NUC的型号，找到对应的BIOS更新。一开始我还担心这玩意儿别是非得进Windows才能更新吧，后来我看提供了一个CAP文件。开机按F7就可以直接从U盘里读这个文件进行BIOS更新。体验极佳，这里要点名表扬Intel。一切都准备妥当之后，烧写openSUSE的镜像，保持安全启动打开（当然还顺手跳了一下散热设置，然风扇转的更激进一些），开始安装。有些老生常谈的项目就不多说了，什么插根网线调成DHCP、配置主机名之类的，这些就略过了。桌面我选的GNOME，抽象，但是好用。至于分区加密，我这次选上了启用LVM。这次读了wiki才知道，如果你不启用LVM就开启加密，那开机一次要输入好几次密码才能解锁全部分区。如果启用了LVM，那么每次开机只需要输入一次密码，就能够解锁全部分区了。非常好。关于这个LVM还得多说几句。LVM全称叫做Linux逻辑卷管理。它是从硬件层面，也就是块设备的层面，将不同的硬盘统一进行抽象，这样当你配置全盘加密或者软件RAID的时候就不用触及Linux的fstab了，只需要在LVM里修改设备就好了。不过这里我还在想，LVM的功能是不是会和Btrfs重复？因为Btrfs也能提供类似软件RAID的功能，同时也不需要触及fstab，只要把硬盘加进存储池里就好了。后来我的老板给出了一个不同的见解：他说LVM从块设备级别提供了更灵活的硬盘管理方式。你可以使用LVM创建一个大的存储池，然后将上面的逻辑卷格式化成不同的分区，然后挂载；而Btrfs只能是Btrfs。假如你需要再加一个swap分区，如果有LVM的话，你只需要新增一块硬盘，然后在上面划分一个逻辑卷，mkswap就可以了。但Btrfs就做不到，因为它只是一个文件系统。所以如果后续不出意外的话，大家可能会看到一篇我写LVM扩容的文章。至于文件系统的配置，主文件系统当然是Btrfs，它的快照还是比较好用的。使用独立的交换分区，但大小只有2GB，一开始我寻思这16GB的内存还能不够吃？意思意思得了。现在看来，还真不够。如果后续不打算加内存的话，交换分区还是得大一点。虽然这个后期可以调，但是调就意味着你得先缩小一个分区，然后把这个分区的容量加给swap。有些文件系统不支持缩小，那就麻烦了。最后就是其他的杂项了，什么开启安全启动、CPU缓解策略（这个本来想禁用的，后来想想，为了工作的信息安全，还是开开吧）、防火墙、ssh什么的就不多说了。并不是什么很重要的事，全看个人需求，你开了也罢，关了也好，看着来。配置系统完事之后就是配置系统了。从哪里开始呢？从开机开始。开机问题因为目前我的NUC独占一个外接显示器，而我的笔记本在支架上。我目前使用的蓝牙键鼠可以配对多个设备，并且可以切换。所以我就没把键盘再拿出来，怪麻烦的。但这样有个问题：我的键盘是蓝牙的，但蓝牙到系统启动之后才有，可是要启动系统就得先输入密码解锁LUKS。可是要输入密码，就得现有蓝牙，要有蓝牙就得先进系统，要进系统就得解锁LUKS，要解锁LUKS就得……看到问题了吧？我这个蓝牙键盘不能解锁全盘加密。也就是说这个NUC只要关了，我再开就不行了，因为输不了密码。怎么办呢？我咨询了群友，群友说有一种很高级的蓝牙适配器，能够记住曾经配对过的键盘，并把它作为普通键盘呈现。我虽然没有听过这种蓝牙适配器，不过我推断这应该是以前的老黄历了。你想啊，这都2024年了，怎么会有这么好心的蓝牙适配器？我灵机一动，想了个土办法：把装系统用的薄膜键盘放在旁边备着。等需要开机输密码了就插上，不用的时候就拔了搁一边。反正是之前买东西送的，个儿不大，也不占地方。不过这个键盘不知道咋回事，输密码的时候自动开启Fn，导致键盘右边的字母全都变成小键盘的数字了。刚开始装完系统重启后输入密码，直接给我干到GRUB的启动界面了，我一想，坏了，别是我安装的时候输错密码了吧。就当我想输入reboot的时候，看到屏幕上写了个reb66t，我一看，啊？左思右想，原来是Fn开了，那输密码的时候按着Fn就行了。其实这个问题我还有一个解决方案，就是利用TPM。使用fde-tools可以把LUKS的加密密钥封进TPM里，开机就可以自动解密了。就算之后因为系统更新导致解密失败，也可以手动输入密码，然后重新密封密钥。可是正当我准备跟着Wiki走的时候却发现，pcr-oracle这个包更新了，更新之后改了命令行，导致fde-tools对它调用失败。我寻思这种破坏式更新不是应该提升大版本号吗？哎，没办法，人家的项目，人家爱干啥干啥，我也管不了。不过我看4月19日fde tools的维护者已经把新的patch提交到openSUSE的工厂了。估计下次风滚草打包的时候就可以用了吧。如果之后可以了，我会在另写一篇文章的。上网问题众所周知，国内的网络质量一直一言难尽。而要想痛快地使用Linux，一个好的网络连接必不可少。之前在虚拟机里的时候我是用tailscale，我专门组了一台德国的服务器来帮我做流量中转。可是放到NUC上就不行了。不使用出口代理就能和节点UDP直连；可是只要一使用出口代理，节点立马不可达，只能用中继。中继就慢了啊，能跑到500KB每秒都算烧高香了。我一想这不行，还得是专业的软件做专业的事。软件就不介绍了，我相信各位读者各有神通。这里我介绍介绍怎么给Linux配置代理。我使用的是GNOME桌面，所以很自然地，就可以通过GNOME的设置来配置。其次就是修改/etc/sysconfig/proxy，这里面提供了一个比较方便的系统级代理，大概是SUSE才有的？当然，如果你用的不是SUSE，那传统的环境变量http_proxy和https_proxy也是一种办法。如果以上两个还不行，我们还可以借助第三方软件，一个叫做proxychains的软件（软件包proxychains-ng），它可以利用库注入来劫持应用的网络请求，让它强行通过代理。比如proxychains4 rclone copy ...，这样就可以让rclone在自身不吃代理的情况下，让它发出的流量走代理。当然了，各个软件都有自己的代理配置方法，比如Gradle、Git这些。关于Git要说一句，如果你使用ssh的话，那轻易是没法走代理的，如果使用http或者https克隆和推送，那可以直接走http和https代理。最后如果还是不行的话，那就只能祭出最终的大杀器了：软路由以旁路路由的身份提供透明代理。使用的是NekoRay，这个软件在openSUSE上有个很奇怪的问题，就是默认情况下找不到openssl。由于新版本的openssl和1.1版本不兼容，所以需要手动安装openssl 1.1的包，不然更新订阅的时候就会提示TLS握手失败。GNOME托盘图标处理完网络就可以开始说正事了。不知道GNOME这帮人怎么想的，把托盘图标给弄没了。这一个桌面管理器做的越来越抽象。好看到是好看，可是太抽象了容易耽误事儿。没有托盘图标，不说别的，就JetBrains那个ToolBox，开机自启之后就是运行在托盘里。没有托盘图标，你叫我怎么升级和更新IDE？好在并不是我一个人嫌弃没有托盘图标。就连Ubuntu都嫌弃，所以他们搞了一个插件，叫AppIndicator and KStatusNotifierItem Support。它的主要功能之一就是把托盘图标给整回来了。中文输入法GNOME倒是自带几种中文输入法，但我觉得都不好用。所以我选了Rime。由于GNOME上继承的输入法框架是iBus，你当然可以用fcitx，但是还是集成的用起来好一些吧。所以要安装的包有两个：rime和ibus-rime，安装之后记得先重启一下，不然刚装完进去GNOME的输入法设置是找不到Rime的。在GNOME里启用Rime之后，还需要一些配置。比如把候选字从竖着的调成横着的，把繁体中文调成简体中文。要调整候选字方向，在~/.config/ibus/rime/ibus_rime.custom.yaml文件中这样写：patch: "style/horizontal": true要调整候选字的个数，在~/.config/ibus/rime/default.custom.yaml里这样写：patch: "menu/page_size": 7配置文件调整完成之后，可以去托盘那里找到输入法的选项，选择部署即可读取最新的配置文件。要调整简体中文，在输入法激活时按F4，选择“朙月拼音·简体字”即可。备份如一开始所说，由于存储接口的不同，组软件RAID 1并不会带来特别大的好处，反而可能会导致性能较差的硬盘拖慢整个RAID的性能。但是没有RAID来提供冗余性，那该怎么保证数据的安全性呢？我想到了我之前一直在用的一个备份软件：Duplicati。这个软件支持多种后端存储，可以通过增量备份的形式保存不同时间的备份。我之前一直使用这个软件来备份我的Windows笔记本。刚好，这个软件也支持Linux。下载RPM包安装后发现缺少依赖，经过一番搜索，只需要手动安装gtk-sharp就好了。但是问题又来了：有些要备份的数据需要root权限才能读取，这该怎么办呢？搜索了半天，最后发现其实Duplicati的源码中包含了一个Service，但不知何故这个service文件并没有进到安装包里，我们手动配置一个就好了（/etc/systemd/system/duplicati.service）：[Unit] Description=Duplicati web-server After=network.target [Service] Nice=19 IOSchedulingClass=idle EnvironmentFile=-/etc/sysconfig/duplicati ExecStart=/usr/bin/duplicati-server Restart=on-failure [Install] WantedBy=multi-user.target配置完成后重载systemd并启用服务：sudo systemctl daemon-reload sudo systemctl enable --now duplicati由于Duplicati运行在root权限，因此建议只将webui暴露给localhost。之后打开浏览器，访问http://127.1:8200即可打开网页端了。关于存储后端，我使用的是BackBlaze的B2，我单独创建了一个桶用来存备份的东西，也不需要额外配置其他的什么锁和加密之类的。加密的部分我们使用Duplicati在上传之前完成即可。关于备份的内容，我目前备份了如下文件夹：/etc：系统的所有自定义配置/home：用户数据/opt：没有使用包管理器安装的软件/root：根用户的数据/srv：和当前服务器有关的特定数据，虽然我这不是服务器，但也备份上了/usr/local/bin/：用户自行安装的软件/usr/local/sbin：用户自行安装的特权软件/var/log：系统日志，以备系统崩溃时使用/var/opt：给opt下的软件用的var文件夹/var/spool：未完成的打印队列等/var/crash：崩溃日志/var/container-data：我自己偏好的容器数据文件夹其中关于/var是最难搞的，因为它里面有些数据值得备份，有些不值得，你得自己慢慢择。备份频率是每两小时备份一次，备份的保留策略我选的智能，因为我也不知道我要什么，所以就全自动了。目前来看价格上应该不会特别贵。B2存储每月每TB价格6美元，赠送2倍还是3倍月均存储的下载流量，不过我暂时用不上。这种能够随时存取的级别，这个价格就不算贵了，隔壁AWS差不多要20多美元一个月。我这块盘一共就500G，就算占满了，里面的数据天天变，还能变出10TB不成？我觉得成本上来看问题不大。结束语至此就算是全部都配置完了。至于后续的使用就全看个人了。像是我就安装了JetBrains ToolBox来管理写代码用的IDE，后来也安装了Docker，而且新版本的Docker也支持给cli挂代理了。所以整体还是比较顺利地，跟着官方文档走就可以了，也没有坑，因此这里也就不再详细介绍了。今天写完文章看了一眼，新快照出来了，fde-tools也更新了，新版本兼容了新的pcr-oracle。本来想跟着流程走一走，但没想到它要LUKS2，这玩意儿还是实验性功能。我一想，这也实验那也实验，万一哪天盘炸了，那我也就实验了。所以最终决定还是不要乱搞了，开机插键盘输入个密码也挺好。之后我打算买个小的UPS，断电了就直接进挂起，只有内存需要供电，那应该也能撑好久呢。除非是什么大的系统更新，不然应该不会需要频繁的重启输入密码。回想文章开头，说买NUC主要还是为了减轻笔记本的压力，不让电池过热鼓包。但是当我今天发现笔记本的触摸板按不下去的时候，我就知道，这电池又他妈鼓包了。操。至此之前欠下的债就算是还完了。虽然没有正式约定过每月一更，但是我还是希望保持这个更新频率。现在有了工作那肯定是得优先保证工作。如果像之前那样托更了的话，我会试图在几个月之内补上的。如果没补上的话，那就是真的鸽了。--全文完--
这篇文章完全是意料之外的。起因是正在上研究生的大学同学突然拿着一个txt后缀的C代码问我怎么sendto怎么返回参数错误。经过一阵奇妙的发展，最终变成了我用一下午的时间简单入门了一下Linux的套接字多播。本文简单记述了这个过程。背景这里简单介绍一下背景吧。所谓的套接字，就是Socket，它是一个用C语言开发的抽象层，它主要作为计算机进程间通讯的端点，通过抽象，它隐藏了底层通讯的细节，而提供了一个统一的交互界面。套接字根据传输层协议主要分为两类，一类是packets，底层是TCP支持的，用于两个进程间（相对）可靠的一对一通信；另一类是datagrams，底层是UDP支持的，用于不那么可靠的通讯，可以一对一，也可以一对多。说起多播，它介于单播和广播之间。单播其实就是一对一。而广播是向网络中的全体无差别发送。多播，有时候也称为组播，它是向网络中特定一组目标发送数据。多播的用处比较广泛，假如说你想向许多人分发数据，例如直播影像。如果采用单播，那么来一个人你就得建立一个套接字给他发送数据，人多了就得同一份数据向好多个套接字里写入。如果采用广播，那么你的数据会无差别发送给网络中的所有主机，如果别人不想要你的数据，那处理这种数据（哪怕仅仅是简单的拒绝1）就变成了一种负担。采用多播，你可以指定一个多播地址，数据只需要向套接字中写入一次，所有加入了这个地址的主机都会收到这个消息；而没有订阅的主机则不会收到消息2。在这里我们并不以此构建什么应用，只是简单的写一个收发程序来验证这个事情。目标编写一个C程序，使用如下命令行启动：# Sender ./multicast s 172.16.71.138 224.0.0.1 9000 # Receiver ./multicast r 224.0.0.1 9000作为发送者启动时，程序将向多播套接字中写入数据；作为接收者启动时，程序将从多播套接字中读取数据。写入的数据由用户输入。实现有一说一，我从大一之后就没再写过C，一直写Java比较多。如果各位觉得这C代码总有一股子咖啡味儿，还请多担待。建立项目虽然是C语言，但是作为一个Java程序员，多少先得弄个项目。CLion默认采用CMake管理项目，那话不多说，配置文件CMakeLists.txt的内容如下：cmake_minimum_required(VERSION 3.22) project(LinuxNetwork C) set(CMAKE_C_STANDARD 11) add_executable(multicast src/multicast/main.c src/multicast/sender.c src/multicast/receiver.c src/multicast/receiver.h src/multicast/sender.h src/multicast/common.h) 这里要求CMake的最低版本号其实没什么讲究，只因为我自己用的版本是3.22，所以就用了这个。使用的标准是C11。这里为了项目规整，又为了给日后这个同学再来问我问题留出余地，这次的代码我放到了src/multicast文件夹中。等之后他要是还有别的问题，我就可以把之后的代码放到其他文件夹中，从而避免了频繁创建工程。其实写代码的时候顺序是自底向上（先完成底层收发，再写命令行的部分），但是写完代码需要写文章讲解的时候，就变成自顶向下了（先说命令行，再说底层细节），所以实际写代码时候的顺序应该是按文章介绍顺序反着来。项目结构这里一共有这么几个文件：main.c：主函数，负责解析命令行参数，根据参数调用对应的过程sender.c：作为发送者的实现sender.h：发送者的头文件，用于在主函数中引用receiver.c：作为接收者的实现receiver.h：接收者的头文件，用于在主函数中引用common.h：共用的定义，例如消息大小头文件三个头文件起到了类似接口的做用：先定义函数签名，以便其他人能够在代码还没有被写出来时继续各自的开发工作。sender.h：#ifndef LINUXNETWORK_SENDER_H #define LINUXNETWORK_SENDER_H void sender_run(char *local_if, char *multicast_ip, char *port_str); #endif //LINUXNETWORK_SENDER_H 发送者的实现主要有三个参数：本地网络接口（多播从哪个网口发）、多播IP和端口（往哪个多播发）。当用户没有指定本地网络接口的时候，可以将local_if设定为NULL，以便使用默认的设置。receiver.h：#ifndef LINUXNETWORK_RECEIVER_H #define LINUXNETWORK_RECEIVER_H void receiver_run(char *ip, char *port_str); #endif //LINUXNETWORK_RECEIVER_H 接收者的实现则相对简单一些：只需要告知接收哪个多播即可。common.h：#ifndef LINUXNETWORK_COMMON_H #define LINUXNETWORK_COMMON_H #define MAX_MESSAGE_LEN 1024 #endif //LINUXNETWORK_COMMON_H 最简单的就是共用定义，就是一个消息大小，类似于缓冲区大小。主函数主函数其实挺简单的：#include <stdio.h> #include <stdlib.h> #include <string.h> #include "receiver.h" #include "sender.h" int main(int argc, char *args[]) { if (strlen(args[1]) == 1) { if (args[1][0] == 'r') { if (argc == 4) { // command r mc_ip mc_port receiver_run(args[2], args[3]); exit(0); } else if (argc == 5) { // command r lo_if(ignored) mc_ip mc_port receiver_run(args[3], args[4]); exit(0); } } else if (args[1][0] == 's') { if (argc == 4) { // command s mc_ip mc_port sender_run(NULL, args[2], args[3]); exit(0); } else if (argc == 5) { // command s lo_if mc_ip mc_port sender_run(args[2], args[3], args[4]); exit(0); } } } fprintf(stderr, "Usage: %s type [local_interface] multicast_ip multicast_port\n", args[0]); fprintf(stderr, "\ttype: 'r' for receiver, 's' for sender\n"); fprintf(stderr, "\tlocal_interface: set outgoing interface\n"); fprintf(stderr, "\tmulticast_ip: sent to this multicast ip\n"); fprintf(stderr, "\tmulticast_port: sent to this port\n"); exit(1); } 真的挺简单的：检查第一个参数：如果是r，就调用接收者的过程；如果是s，就调用发送者的过程。调用成功之后退出程序。如果没有进入任何过程，那么打印帮助信息，然后退出。发送者创建UDP套接字既然是多播套接字，就得首先有套接字： int socket_def = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (socket_def == -1) { perror("Failed to create socket"); exit(1); }其中socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)调用就指定了我们的套接字是IPv4（AF_INET）、套接字类型是datagrams（SOCK_DGRAM），使用的传输层协议是UDP（IPPROTO_UDP，这个参数也可以写0让系统自动匹配）。如果返回了-1，则说明出现错误，使用perror函数将errno解析成人类可读错误消息，然后退出程序。设定套接字多播参数接下来对套接字进行设定： { // Target to local network only unsigned char multicast_ttl = 1; if (setsockopt( socket_def, IPPROTO_IP, IP_MULTICAST_TTL, (void *) &multicast_ttl, sizeof(multicast_ttl)) < 0) { perror("Failed to set socket option"); close(socket_def); exit(1); } } { // set outgoing interface if (local_if != NULL) { struct in_addr localInterface; localInterface.s_addr = inet_addr(local_if); if (setsockopt(socket_def, IPPROTO_IP, IP_MULTICAST_IF, (void *) &localInterface, sizeof(localInterface)) < 0) { perror("Failed to set outgoing interface"); exit(1); } } }这里用代码块的做用是限制参数的作用域，避免后面的程序意外用到这些参数。第一个参数是设定多播的TTL。将TTL设定为1可以保证多播报文不会被转发出本地网络。第二个参数就是设定本地网络接口。由于多播是传输层协议的一部分，因此这些配置的level都是IPPROTO_IP。创建多播地址接下来需要告诉套接字，我们要往哪多播： struct sockaddr_in multicast_addr; memset((char *) &multicast_addr, 0, sizeof(multicast_addr)); multicast_addr.sin_family = AF_INET; multicast_addr.sin_addr.s_addr = inet_addr(multicast_ip); multicast_addr.sin_port = htons(strtol(port_str, NULL, 10));其实就是初始化了一个sockaddr_in结构体，这个结构体是sockaddr的IPv4变体，专门用于存放IPv4的数据。如果是IPv6的话，就要用sockaddr_in6，另外代码的其他部分也要跟着变。由于这里只是简单的实验，v4就足够用了。首先我们要把结构体清空，避免内存中的脏数据引发预期之外的行为。这里我们为多播地址设置了三个参数：它是IPv4地址（AF_INET），它的多播IP是multicast_ip，他的端口号是port_str。准备妥当之后，我们就可以发数据了。发送数据发送数据其实就是一个循环： char message[MAX_MESSAGE_LEN]; memset(message, 0, sizeof(message)); size_t data_len; printf("Type your message, one per line:\n> "); while (fgets(message, MAX_MESSAGE_LEN, stdin) != NULL) { data_len = strlen(message); printf("Sending %zu bytes\n", data_len); ssize_t val = sendto(socket_def, (void *) message, data_len, 0, (struct sockaddr *) &multicast_addr, sizeof(multicast_addr)); if (val < 0) { perror("Failed to send message"); close(socket_def); exit(1); } if (val != data_len) { fprintf(stderr, "Should send %zu bytes, but actually send %zu", data_len, val); perror("Returned byte count doesn't match sent count"); close(socket_def); exit(1); } printf("> "); memset(message, 0, sizeof(message)); }这里为了好看，每行输入数据的时候都会有一个这样的前缀：> 这是消息，前面的尖括号是前缀，有点Linux命令行的感觉通过fgets，我们从标准输入中按行读取字符串，这个字符串是带换行符的，我们需要这个换行符和数据一起发送，因为一会儿接收端也要按行读取数据。这部分关于初始化和清空内存就不多说了（memset）。这里的关键就是sendto调用：这个调用将给定缓冲区（message）中的前data_len个字节写入到套接字socket_def中，目标地址是multicast_addr。其中那个0对应消息传输类型标志，这里用默认的即可，也就是什么标志都没有。这个函数如果发送成功，返回发送成功的字节数；如果失败则返回-1，并把错误代码放到errno里，我们可以通过perror来访问。这里还额外验证了发送的数据量：如果实际发送的数据量和预期的不同，我们将其认定为一种错误。当然，我们的程序不可能一直卡死在这里。当标准输入流被关闭（Ctrl+D）时，这个循环就会退出。但是程序并不能随之退出，我们最好处理一下关闭并释放资源的代码。退出退出就是告诉关闭套接字，然后程序退出，占用的内存也随之归还系统。 close(socket_def);接收者有了发送者，我们看看怎么接收这些多播报文。创建套接字同样地，我们也得先创建一个套接字，和发送者一模一样的套接字： int socket_def = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (socket_def == -1) { perror("Failed to create socket"); exit(1); }允许套接字重用这里我们可以设定允许套接字重用。也就是说，我们这个程序监听了给定的多播地址，别的程序还可以监听同样的地址，这样就避免了排他性地独占资源。（毕竟是个只读操作，没有必要独占。） { // set reuse, so we can share the same datagrams across multiple instances of this application int reuse = 1; if (setsockopt(socket_def, SOL_SOCKET, SO_REUSEADDR, (void *) &reuse, sizeof(reuse)) < 0) { perror("Failed to set reuse"); close(socket_def); exit(1); } }这里可以看到，允许地址重用是针对套接字设定的（它的level是SOL_SOCKET）。绑定监听地址我们的套接字需要一个入站连接才能接收到别人的消息。 struct sockaddr_in listen_addr; memset((char *) &listen_addr, 0, sizeof(listen_addr)); listen_addr.sin_family = AF_INET; listen_addr.sin_addr.s_addr = inet_addr(ip); listen_addr.sin_port = htons(strtol(port_str, NULL, 10)); if (bind(socket_def, (struct sockaddr *) &listen_addr, sizeof(listen_addr)) < 0) { perror("Failed to bind incoming port"); close(socket_def); exit(1); }这里和之前创建多播地址一样，但额外地，我们要监听这个端口，因此需要把这个端口和套接字绑定在一起（bind）。值得一提的是，在我们的系统中，没有一个网络接口的IP是我们想监听的多播地址，但Linux并没有因此而报错（如果你尝试把多播地址换成诸如192.168.1.1这类地址，bind会报错Cannot assign requested address，因为Linux不知道改绑定到哪个网卡）。这是因为Linux知道这是一个多播地址，操作系统收到发给这个地址的报文后，将其转发给监听在此地址的应用程序即可。如果需要过滤多播报文的来源网络接口，那么就需要在下面的结构体中设置（即控制哪个网络接口加入多播组）。创建并设置多播请求那么我们如何告诉套接字接收多播报文呢？我们需要一个ip_mreq结构体。如果是v6的话则需要ipv6_mreq。 struct ip_mreq multicast_req; memset((char *) &multicast_req, 0, sizeof(multicast_req)); multicast_req.imr_interface.s_addr = INADDR_ANY; multicast_req.imr_multiaddr.s_addr = inet_addr(ip); if (setsockopt(socket_def, IPPROTO_IP, IP_ADD_MEMBERSHIP, (void *) &multicast_req, sizeof(multicast_req)) < 0) { perror("Failed to add multicast group"); close(socket_def); exit(1); }这里我们设置了两个参数：imr_interface设置为0.0.0.0，意为接收来自任何网络接口的多播报文（即所有网络接口都加入这个多播组）；imr_multiaddr则指定了我们所在的多播组（也就是多播IP，由于主机按照“订阅”的多播IP被分组，因此multicast有时也被译为「组播」）。创建完结构体，通过套接字设定告诉网络接口我们加入了这个多播组，也就是让网卡订阅了来自这个组的消息，这样网卡收到对应组的消息时就会转发给操作系统，我们作为应用程序才能收到相应的数据。读取并打印数据现在万事俱备，只差读数据了。毫无意外，这也是个循环： char message[MAX_MESSAGE_LEN]; while (1) { memset(message, 0, sizeof(message)); struct sockaddr_in source_addr; size_t source_addr_len = sizeof(source_addr); ssize_t received_len = recvfrom(socket_def, message, MAX_MESSAGE_LEN, 0, (struct sockaddr *) &source_addr, (socklen_t *) &source_addr_len); if (received_len < 0) { perror("Receiving error"); break; } printf("Received %zu bytes from %s:%d: ", received_len, inet_ntoa(source_addr.sin_addr), source_addr.sin_port); printf("%s", message); }这里主要通过recvfrom调用来读取数据：从socket_def代表的套接字中读取最多MAX_MESSAGE_LEN个字节，写入到缓冲区message里，源地址保存到source_addr里面。这里的0和上面sendto的0一样，是标志。这里打印信息的效果是：Received 7 bytes from 172.16.71.138:39077: 123123可以看到，通过UDP报文我们可以获取到源地址和源端口。（什么？你说123123只有6个字节？换行符也占一个啊）退出这里的退出其实就不是特别明显了，因为这个循环的退出依赖于读取出错。但是UDP报文没有状态一说，因此更谈不上什么断开连接这种操作。即便所有发送者都关闭了程序，对于接收端来说还是在等待下一个报文的到达。但是有没有的，还是得假一下： if (setsockopt(socket_def, IPPROTO_IP, IP_DROP_MEMBERSHIP, (void *) &multicast_req, sizeof(multicast_req)) < 0) { perror("Failed to exit multicast group"); close(socket_def); exit(1); } close(socket_def);一开始我们告诉网卡加入多播组，退出的时候就得对应地告诉网卡退出多播组。不必担心直接退出程序会将多播组“滞留”在网卡上：这个多播组是通过套接字设定的，而套接字对接的是操作系统。套接字关闭后，操作系统会进行善后工作，如果没有其他进程需要这个多播组了，那么操作系统会告诉网卡退出多播组。效果展示写完代码之后，构建一下，把构建产物复制到两个虚拟机上，每台虚拟机各运行一个发送者、一个接收者和一个监听在其他地址的接收者。可以看到，发送者发出的消息，可以被所有加入了多播组的主机收到；监听在其他多播地址的接收者，即便他们监听了同一端口号，由于多播组不通，他们并不会接收到我们的消息。吐槽最后要吐槽一下我的这个同学。他这个C语言可能是跟师娘学的。以及他用的CentOS7，自带的cmake版本是2.18；我用的是3.22。我整个工程都打包发给他了，他经过一番尝试之后得出结论：我不禁感叹，这就是我校研究生的水平。可叹啊。-全文完- 这是由于网卡并不知道这些数据是否是有用的，因此只能在收到消息时都中断递交收到的数据，让软件（操作系统）进行判断是否转发给应用程序。但这种情况下，即便不是发给本主机的数据也要占用CPU资源进行处理，性能影响较大。 ↩ 严格来说，网卡还是会收到数据，但这一次网卡会发现这不是发给本主机的数据，因此不需要触发中断，直接丢弃就好。硬件上的自动丢弃不占用CPU资源，因此性能较好。 ↩
【歪门邪道】修复小米MIX2在MIUI EU版上无法开启加密的问题
最近觉得在K20Pro上运行MIUI EU版体验不错，既得到了原版硬件驱动的支持，由得到了谷歌全家桶的体验（距离原生还是差很远，但原生与兼容性不可兼得，只能舍原生而找兼容性）。于是今天将原来的Mix2也换成了MIUI EU，在同步设置的时候发现没有开全盘加密，于是便开启。尝试了两次全都卡住了，经过一系列搜索，最终发现解决之道。准备工作刷机嘛，手机上什么TWRP之类的第三方Recovery就不用说了，其次要准备好Mix2的MIUI EU，我使用的稳定版，版本是V12.0.1.0.PDECNXM_v12-9。之后还要准备谷歌的platform-tools，主要是其中的adb。最后再来一个文本编辑器就齐活了。流程首先四清或全清，总之就是要干干净净的，然后通过卡刷刷入MIUI EU的ROM。然后不要重启进系统，而是直接重启到第三方Recovery中。在第三方Recovery中挂在System分区，打开ADB，拿到系统的fstab，该文件在Linux中指导内核挂载分区。安卓9使用如下命令：adb pull /system/vendor/etc/fstab.qcom根据安卓版本的设备的不同，该文件的位置和后缀名可能不同，需要自行寻找，有可能需要挂在Vendor分区。执行完命令之后手机中的fstab.qcom就会上传到电脑中。将其中的encryptable=ice修改成fileencryption=ice，其他的不要改动，尤其留意修改时使用的符号或空格等，修改不对可能导致手机无法启动。并且该方法只在第一次启动系统前有效。修改完成后使用如下命令将修改下载回手机：adb push fstab.qcom /system/vendor/etc/fstab.qcom完成后重启手机，进入系统。并在初始化设置中设置密码，随后手机将启动开启全盘加密。初次启动系统耗时较长，因为要加密全屏，请耐心等待。如果超过一小时还未启动，则考虑重新开始流程，应该是修改过程出了问题。验证系统设置中显示手机已经加密重启到第三方Recovery之后需要输入密码才能查看文件内容原理之前提到过，fstab是现代Linux系统中指导内核挂载磁盘分区的重要文件。以我们刚刚修改的为例：#<src> <mnt_point> <type> <mnt_flags and options> <fs_mgr_flags> /dev/block/bootdevice/by-name/userdata /data ext4 noatime,nosuid,nodev,barrier=1,noauto_da_alloc wait,check,reservedsize=128M,fileencryption=ice,quota根据注释，第一个参数指定了挂载设备的名称，在Linux等PC机上，挂载的对象通常是/dev/sda/sda1之类的，而这里/dev/block/bootdevice/by-name/userdata则表示手机中实际存储用户数据的分区。第二个参数表示挂载到文件系统中的哪个位置：/data，即挂在后我们通过/data就能访问到原来位于/dev/block/bootdevice/by-name/userdata上面的数据。第三个参数表示该文件系统是什么格式的，这里是ext4，是一种常见的Linux下的格式，类似于Windows上的NTFS或者Fat32。第四个参数指定了内核以何种方式挂载。例如其中的noatime告诉内核，挂载后不要在访问文件的时候更新文件的访问时间，一方面加快了访存速度，另一方面也减少了写入次数，提高了存储器的寿命。而诸如nodev和nosuid则是为了提高安全性。而第五个参数则是指定安卓的文件系统管理器如何挂载磁盘。例如quota，是允许文件系统管理器针对该分区启用统计数据。其他参数姑且不谈，本文中起到关键作用的fileencryption正是出现在此列。在安卓5的时代，最先引入的是全盘加密，但是全盘加密有一个问题，就是手机启动后，用户未输入密码前，手机的全部数据都是加密的，用户只能拨打紧急电话或者输入密码解锁。如果用过水货HTC的话，或许会对一个白底界面有印象：输入密码解锁手机，或者拨打紧急电话。如果密码输入错误超过30次，那么手机将会自动清空数据。文中被修改的encryptable标志则是在那个时候引入的。这种全盘加密带来了诸多不便，首当其冲的便是闹钟功能，因为用户没有解密，所以闹钟在手机关闭后完全不起作用。于是为了解决这个问题，谷歌引入了文件级加密：在启用了直接启动特性的设备上，系统可以直接进入界面，这使得诸如闹钟之类的应用可以发挥作用。这种特性的基础便是文件级加密，它与全盘加密的区别在于，文件级加密的基本单位是文件，在逻辑上仍然可以识别出独立的文件，而不知道密码的情况下无法知晓文件内容；而全盘加密则是以盘块或盘块簇为单位，在存储器级别进行加密，如果不解密，则根本无从知晓其上的文件系统，就更不要说识别文件了。而fileencryption正是在这个时候被引入的，根据谷歌官方的介绍，用法如下：fileencryption=[加密模式]，小米指定的是ice，而谷歌官方解释如下：如果不使用内嵌加密硬件，则对于大多数设备推荐设置为 fileencryption=aes-256-xts。如果使用内嵌加密硬件，则对于大多数设备推荐设置为 fileencryption=aes-256-xts:aes-256-cts:inlinecrypt_optimized。在没有采用任何形式的 AES 加速的设备上，可以设置 fileencryption=adiantum，从而用 Adiantum代替 AES。在搭载 Android 10 或更低版本的设备上，也可以使用 fileencryption=ice 来指定使用 FSCRYPT_MODE_PRIVATE 文件内容加密模式。Android 通用内核未实现该模式，但供应商可使用自定义内核补丁程序实现该模式。该模式生成的磁盘格式因供应商而异。在搭载 Android 11 或更高版本的设备上，不允许再使用该模式，而必须使用标准加密格式。我在想，除了小米自己实现的，如果使用标准加密格式，对于速度有什么影响呢。由于对于小米Mix2的硬件信息所知甚少，因此我实际上有刷了4次系统来分别测试速度，每次对应上面的一种加密。软件使用CPDT Benchmark。测试数据如下：加密方法4MB顺序写入4MB顺序读取4KB随机写4KB随机读无加密156.86MB/s506.11MB/s19.15MB/s13.62MB/sice156.50MB/s425.07MB/s19.24MB/s12.92MB/s其中使用aes-256-xts:aes-256-cts:inlinecrypt_optimized和adiantum后无法启动系统，大概率是因为内核不支持。而aes-256-xts倒是能启动进入系统，但是卡在了登录谷歌账号上，只要进入到登陆界面，Google Play就崩溃，无法继续配置，重启后进不去系统。看起来MIUI用的内核比较精简，只有厂商自己实现的加密算法。值得一提的是，Mix2使用的是安卓9，或许更新的安卓版本会支持。于是只测试了厂商实现的算法，与不使用加密算法的区别，从结果来看影响似乎不是很大。并且随着测试次数的增加，我猜测可能是与缓存机制有关，对于同一文件的读写测试，在数值上区别越来越小，甚至还有加密后的速度超过不加密的情况。看起来只要手机不是特别差（好歹Mix2用的也是835呢），文件级加密完全没有问题，而出于安全考虑，我的手机开了文件级加密，而电脑的C盘也开了BitLocker，这样一来即便丢了，也不会造成数据泄露。-全文完-
说出来你可能不信，是Arch先动的手。我想对于双显卡（Intel+Nvidia）的笔记本，可能显卡驱动是最要命的部分。一开始就什么都不管，一路安装，就能看到Plasma，而稍微一动显卡驱动再一重启，系统在启动之后就卡住了，一直是黑屏。所以我觉得。。。如果笔记本上有两块显卡而我又没法在Arch中将其发挥到极致，那么这于我而言有些不爽。[...]
我就当真是不服这个Arch，在昨天弄好桌面环境之后，今天又把系统删了重装Arch，按照官方的wiki已经安装好了命令行。主要问题在于这个图形界面，而真电脑又不像虚拟机那样可以搞快照，不行就恢复，因此必须要谨慎行事。还好关于KDE的Plasma在Arch的wiki上有教程，因此，如果Arch还是不行的话，那我就认了，就用命令行，谁怕谁2333[...]
几经折腾修好了原来被电源取走性命的笔记本，按照惯例，除了常用机器安装windows之外，剩余的机器我都是安装成Linux的，之前的三星R518在跑不动win10之后我换成了ElementaryOS，用的很好，这次我打算尝试一下其他不一样的linux。[...]