分类 Linux 下的文章

苦逼兼职运维的第六年,已经无力吐槽Ubuntu了= =

症状:在nmtui里激活网卡,提示Could not activate connection: XXX because device is strictly unmanaged.

懒人解决方案:

sudo su -c 'echo ",except:type:ethernet" >> /usr/lib/NetworkManager/conf.d/10-globally-managed-devices.conf'
sudo service network-manager restart

原因大致是Ubuntu的NetworkManager默认将以太网卡设置为了不管理模式,因而无法通过nmtui对以太网卡进行开关操作,即使配置了网络设置也不管用。而上面这段except则是反向操作,告诉NetworkManager不要不管理以太网卡。

咕果上翻了半天没找到合适的解决方案,感谢这位知乎网友:https://zhuanlan.zhihu.com/p/514966896

需求是在gem5里跑SPEC程序。作为一个指令级模拟器,允许在不单独运行操作系统的情况下运行指定架构的程序本身就已经是一个巨大的工程了,而它所付出的代价则是运行效率低下和不支持动态链接(作者似乎说了在x86模式下支持加载动态库,但我没测试)。

于是对应的解决方案是,静态链接。

然而,在如今这个内外存充足、处理器足够快的时代,静态链接的用途可以说是已经被大幅削减了,如果要依赖外部程序的话,没有什么是附加一个动态库解决不了的。一定要说离不开静态链接的,可能也就只有在仍处于系统底层的内核了。对于只能识别汇编指令的处理器来说,它不知道该去哪里找对应的依赖实现,因而程序必须一字一句指导处理器执行。

好在仍然有部分C语言程序离不开静态编译,因而现有的操作系统还是提供了gcc的静态包。而fortran就不一样了,天生作为计算工具的语言往往在操作系统中执行,没必要搞静态链接,但凡一个库函数被调用了两次都是一种空间的浪费——或许是基于这种思想,几乎所有的Linux发行版都从安装源里删去了gfortran的静态依赖库,仿佛多的这几百KB的空间对服务器也是种累赘。

然而还是有人关注这个问题:在CentOS论坛中,有人解决方案。作为开源操作系统,保留源码可以说是一个非常好的文明了,而红帽系的系统甚至还提供了rpmbuild工具用来实现快捷的,从源码包到二进制包的编译功能,大致分为两步:

首先找到源码rpm包(这里推荐pkgs.org和rpmfind.net,格式工整,资料齐全)并解压:

rpm -i gcc-8.3.1-5.1.el8.src.rpm

然后用rpmbuild按照配置文件编译即可:

rpmbuild -bb ./rpmbuild/SPECS/gcc.spec

然后编译过程中可能会提示缺包缺文件,通过yum/dnf安装即可,缺失的文件也都是可以通过包管理器补上的

以经典的/lib/libc.so.6/usr/lib/libc.so为例,这两个文件是x86架构下的glibc库文件,在amd64系统上是默认不装的,但是因为rpmbuild需要,可以通过分别glibc.i686glibc-devel.i686补上。

补完缺失的依赖之后就可以快乐泡茶等编译啦~

鸽了五年来填坑了

随着人年龄越来越大,逐渐对U盘的数量不再那么感兴趣,反而觉得一串串的U盘变成了随身携带的累赘,生怕一不小心就弄丢了。

几年前就试图给自己整个活,弄一个支持Windows+Linux+MacOS的三合一安装启动盘,无奈学识尚浅,花了几周都没搞定。

最近正好手头有装服务器的需求,心中的整活之魂又燃起来了,花了一下午时间总算把坑填好。

一 明确需求

由于通常情况下不论哪个系统都要求使用一个完整的U盘(尤其是MBR时期,刻盘是最方便的MBR更新方式),所以常规情况下换个系统就要重刻一张盘。

因而本项目的基本需求就是一个U盘能够在不修改引导的情况下完成三类系统的安装,且最好同时支持兼容(后文简称为MBR)+UEFI模式。

二 设计方案

三种不同的系统安装方法有所不同,虽然W/L/M三家都支持原装启动盘启动,但是Windows的官方启动盘功能实在是少得可怜,远比不上*nix两家的shell好用,所以大部分情况下会使用PE+WinNTSetup的方法,附加工具多,可定制化程度高。因而在整体方案上,使用PE工具建立Windows安装环境,Linux、MacOS则原盘启动安装。

好在MBR/UEFI的启动方式不同,一个是读扇区,一个是读文件系统,因而两者具有共存的余地——实际上现在的绝大部分启动盘都是同时支持两种模式的。

由于MacOS很早就抛弃了MBR启动,所以MBR启动不做MacOS支持(其实也不难,加个Chameleon iso引导就行);其他方式能支持则尽可能支持:

Windows Linux MacOS
MBR ×
UEFI

三 大致步骤

由于MBR模式和UEFI模式在引导方式上有区别,因而要采用不同的方式对待。由于需要支持MBR模式,因而整体上U盘还是需要保持MBR格式的。

对于UEFI启动比较简单,首先到网上找一份efi shell,放到efi分区(也就是U盘物理地址上第一个分区)。然后启动进入efi shell之后,直接用命令行执行要uefi启动的系统引导即可。

由于只有MacOS本身可以正常操作其文件系统,通常要为MacOS的安装盘单独准备一个分区,然后在MacOS里用磁盘工具恢复分区。(对,手头需要先有一个能用的MacOS,不论是白的还是黑的)

MBR模式的稍微麻烦一点,我这里是基于大白菜魔改的。大白菜的多级菜单引导模式是基于grldr做的,也就是大名鼎鼎的grub4dos,因而需要通过修改菜单,让grub4dos来引导Linux系统的启动流程。

需要准备一个工具fbinsttool,用来读写大白菜的隐藏文件系统。大白菜专门预留了一个文件用来给我们制作自定义的启动菜单,在(ud)/IDBC/GRUB/DIY.LIST位置,在里面照抄现有启动项即可实现自定义启动项的添加。

以我配置的Rocky Linux 9为例:

title 【01】 Install Rocky Linux 9 Minimal 
# find只支持定位文件,不支持文件夹!
find --set-root --ignore-cd --ignore-floppies /RPM_GPG_KEY_ROCKY_9
kernel /ISOLINUX/VMLINUZ inst.stage2=hd:LABEL=Rocky_9_0-x86_64-dvd quiet
initrd /ISOLINUX/INITRD.IMG
boot

需要注意的是,这里最好用dd直接把光盘镜像的分区写入到U盘,不然后面的Anaconda安装可能找不到本地安装源。然后偷懒使用光盘内特有的文件来定位该分区(因为grub4dos不支持用分区名定位)。然后照常加载kernel和initrd即可完成启动。需要注意的是inst.stage2参数,该参数用于指导安装器Anaconda的加载,要是填错了或者没填会导致启动失败。(在光盘里这俩文件在不同位置出现了几次,估计是为了支持不同的启动方式吧)

最近看内存冷热页面替换方向的论文的时候,看到有篇论文 Transparent Hardware Management of Stacked DRAM as Part of Memory 提出了一个有趣的想法,即以段为单位监测内存访问的热点情况,并为每个段分配固定大小的高速缓存。虽然这个方法具有一定的局限性,即没有考虑到段本身的冷热程度,在效率上并不能达到最优,但它成功唤起了我对于内存管理的思考。

根据计算机组成原理,通常认为内存管理是段页式的,其中段式管理提供逻辑上的内存划分(同一段内存具有相同的语义,即同属一个应用程序,或者同为代码/数据),页式管理提供物理上的内存划分(操作系统级别的最小内存管理单位,如分配/虚拟内存的换入换出等)。

然而,事与愿违,当试图在网上搜索的时候,发现大家都在说,x86和x64的操作系统放弃了使用段寄存器,而intel甚至在x64指令集中要求,将段寄存器置为0才是合理的使用方式。

那么事实为何如此呢?根据从网络上收集到的信息来看,可能主要与指令集设计者想甩掉段式管理这个历史包袱有关。早期处理器存在的问题是,处理器的寄存器宽度只有16位,而寻址范围的宽度可以上升到20位,为了应对处理器无法直接寻址超过寄存器宽度的内存地址的问题,加入了段寄存器,通过段寄存器移位的方式,对于数据段/代码段等段寄存器,扩充了程序可使用的内存地址空间。然而寄存器(指令集)大小是指数增长的,光是从16位升级到32位,就让内存花了数十年的时间才重新扩充满整个地址空间,推动64位指令集的刚需化发展。也就是说,从32位指令集开始,内存地址空间的扩张速度就已经远远追不上寻址空间的发展速度了——从这个角度来看的话,既然段式管理解决的是内存空间大于寻址空间的问题,那么在内存空间远小于寻址空间的情况下,段式内存管理似乎就没有存在的必要了(参考StackOverflow)。而从32位指令集开始,段寄存器中的使用方式已经不是之前的基地址移位了,而是变成了段表项,转换成了通过查表的方式取得段的基地址。

于是,一方面为了保障向前兼容性,另一方面为甩开包袱做好准备,指令集设计者和操作系统设计者在这个问题上默契地采用了同一种方法来应对这个问题:不取消段寄存器,但是要求所有的段的基地址都从0x00000000开始。反正指令集的寻址空间足够大,那么只要我将整个地址空间都视作一个段,不就相当于架空了段寄存器的作用,进而消除了段式管理在操作系统中的应用么?

知乎有一文对于在现代操作系统中的管理设计做了实际验证,通过各种骚操作印证了这个事实。不过文中有一处写的不够清楚,在Linux的gdt_table那里,GDT是如何从一串不规则的值映射到像0x00x1F这类连续0/1的数字的。这个实际上是因为GDT中的变量不是连续存放的,可能是出于硬件设计对数据做了切分,按照OSDev上的Segment Descriptor做一些简单的运算后,就能验证实际上这些变量都是具有正确的含义的。

看完这些信息后,总感觉我之前对段式内存管理的理解有些不太正确。以前一直以为,段式内存管理像malloc一样,要多少分多少,属于一种与软件相关的逻辑定义;现在才发现,段式管理更多的是一种硬件资源管理方式,段的大小似乎是由体系结构所决定的,也没有软件那种随用随分、即时释放的灵活性。

最后总结一下,由于现在寻址空间远大于内存空间,段页式内存管理中的段式管理逐渐失去其作用,被软硬件所淘汰,主流操作系统中主要依靠页式内存管理和多级页表(应用于虚拟内存)来管理内存空间。

同学跑程序要用大页来提升性能,然后发现在机房中的某些机器上能跑起来,而另外一些不行,然后就找到我这debug来了。表现出来的症状是,调用shmget生成共享内存id时直接报ENOMEM问题。

回顾一下计算机基础知识,页是定长、连续的内存物理结构,而大页区别于普通页最大的区别就是其大小,于是首先想到的是在内存分配上是否会因为没有足够长的连续空间而出现即使内存数量足够、但是分配不出连续大空间的问题,解决该问题最直接的方法就是重启。

然而,重启过后,问题仍然存在,遂排除连续内存空间不够的可能性(然而如果是数十/百M的大页应该也不算大,内存不至于那么碎)。而后怀疑是程序本身的问题,于是把shmget操作从程序中抄出来,单独用一段代码执行:

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <wait.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/shm.h>

int main(int argc, char *argv[]) {

    int status;
    int segment_id;

    const int SEGMENT_SIZE = 0x1234;

    segment_id = shmget (IPC_PRIVATE, SEGMENT_SIZE,
                        IPC_CREAT | IPC_EXCL | 0666 | SHM_HUGETLB);

    printf("segment_id=%d\n", segment_id);
    perror("");

    shmctl(segment_id, IPC_RMID, 0);
    exit(EXIT_SUCCESS);
}

由于我们只需要验证共享内存是否可以创建,而不是是否可以使用,因而后面的shmat/shmdt都省去了。运行后发现,ENOMEM错误仍然存在。

然后思路又回到大页这方面,猜测是不是SHM_HUGETLB参数导致的共享内存分配失败,编译执行发现程序运行正常,然后就想到了用SHM_HUGETLB作为关键字上网冲浪。然后在StackOverflow上发现有人遇到了相似的问题,说是因为总大页数达到了系统设置的上限。然后顺着问题中的思路检查机器,用sysctl -a发现了问题:对比两台分别可否运行程序的机器,发现有个叫vm.nr_hugepages的参数,看起来和大页的数量有关,在不能运行同学程序的机器上,该参数值为0,而可以运行的机器上参数值为16384。

然后就去网上搜索这个参数到底是什么含义。经过艰难摸索,在官方文档里找到了描述:

/proc/sys/vm/nr_hugepages indicates the current number of “persistent” huge pages in the kernel’s huge page pool. “Persistent” huge pages will be returned to the huge page pool when freed by a task. A user with root privileges can dynamically allocate more or free some persistent huge pages by increasing or decreasing the value of nr_hugepages.

大意是说大页是有单独的内存池的,而这个nr_hugepages可以通过控制允许创建的大页数目来控制大页内存池的大小,因而合理推测当该内存池大小为0的时候,等价于实际上禁用了大页功能。于是通过使用sysctl命令,临时设置大大页内存池的大小:

sysctl vm.nr_hugepages=1

测试后发现问题解决。

然后继续往下阅读文档发现,似乎还有个选项,允许程序运行时动态调整大页内存池的大小,而不至于“撞墙”:

/proc/sys/vm/nr_overcommit_hugepages specifies how large the pool of huge pages can grow, if more huge pages than /proc/sys/vm/nr_hugepages are requested by applications. Writing any non-zero value into this file indicates that the hugetlb subsystem is allowed to try to obtain that number of “surplus” huge pages from the kernel’s normal page pool, when the persistent huge page pool is exhausted. As these surplus huge pages become unused, they are freed back to the kernel’s normal page pool.

大意是说往/proc/sys/vm/nr_overcommit_hugepages里面写任意一个非0值就会开启这个功能,允许当大页内存池的固定内存用完时弹性借用普通内存池的内存(未经测试)。