分类 操作系统 下的文章

最近看内存冷热页面替换方向的论文的时候,看到有篇论文 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值就会开启这个功能,允许当大页内存池的固定内存用完时弹性借用普通内存池的内存(未经测试)。

  1. 配置hosts

    vi /etc/hosts
  2. 关闭防火墙、SELinux

    systemctl stop firewalld    # ubuntu换成ufw
    setenforce 0
  3. 装docker

(暂略)

  1. 安装cephadm
    wget https://github.com/ceph/ceph/blob/v16.2.7/src/cephadm/cephadm
    chmod +x cephadm
    ./cephadm add-repo --version 15.2.7    # 指定ceph版本
    ./cephadm install ceph-common  ceph    # 客户端软件包
    ./cephadm install                      # 把cephadm装到系统环境中

其实cephadm也可以用包管理器装。似乎cephadm的版本和要部署的ceph集群版本可以不一致。

  1. 创建本机最小集群

    mkdir -p /etc/ceph
    cephadm bootstrap --mon-ip <ip>

    其中<ip>替换成本机在ceph集群中欲配置的ip地址,这里不能用主机名。

  2. 配置集群免密登录
    ssh-copy-id -f -i /etc/ceph/ceph.pub <user>@<host>

<user><host>替换成要加入集群的机器名称。

  1. 进入ceph容器命令行
    cephadm shell

这一步的目的可以理解为隔离环境,也就是说允许在本机不安装ceph包的情况下,运行ceph相关的管理命令,可以保证本地环境不被污染。注意退出本shell之后所有临时生成的文件会被删除,但是命令记录似乎会被保留。

  1. 将节点添加进集群(在其它节点上部署docker容器)
    # 在cephadm shell中执行
    ceph orch host add <hostname> <host-ip>    # --labels _admin 以作为管理员节点

<hostname><host-ip>替换成要添加的节点的hosts名称和ip。

  1. 部署ceph mon
    # 在cephadm shell中执行
    # ceph config set mon public_network XXX.XXX.XXX.XXX/XX    # 配置网段,不清楚是否为必须
    ceph orch apply mon <host1>,<host2>,...

使用服务器的hosts名称指定要部署mon的节点,用逗号(不加空格)分开。

  1. 部署ceph osd

osd要求使用裸盘,任何建立了文件系统的盘都不能用作osd。

将集群中所有节点的裸盘用作osd:

# 在cephadm shell中执行
ceph orch apply osd --all-available-devices

如果要详细指定仅使用满足某些特定条件的盘,可以参考https://docs.ceph.com/en/quincy/cephadm/services/osd/#drivegroups,用yml定制过滤器,并用--dry-run检查过滤器写的是否符合预期。

  1. 创建CephFS
    # 在cephadm shell中执行
    ceph fs volume create default

这样会创建一个名为default的CephFS,默认的副本数应该是3。

改成1的方法:

ceph config set mon mon_allow_pool_size_one true
ceph osd pool set cephfs.default.data size 1 --yes-i-really-mean-it

一些翻车后的小技巧:

删除集群后,释放osd占用的裸盘:

ceph-volume lvm zap /dev/sdX --destroy

CephFS创错了,删除提示配置不允许删除:

# 在cephadm shell中执行
ceph config set mon mon_allow_pool_delete true

从集群中删除节点:

ceph orch host rm <host> --force

若是离线了还要加上--offline参数。

查看集群中所有ceph服务的运行状态:

ceph orch ps

一台机器的所有服务都炸了,或者没删除服务就卸载ceph了:

# 在cephadm shell中执行
ceph orch daemon redeploy <service>.<host>

比如ceph orch daemon redeploy mon.ceph01将会重新配置ceph01节点上的mon服务容器。

清理集群删除失败时留下的残渣(相当于移除机器上与ceph相关的所有信息,包括配置文件、数据

# 删除服务
for i in `systemctl | grep ceph | awk '{print $1}' | grep -P 'service|target'`; do systemctl stop $i; done
rm -rf /etc/systemd/system/ceph*
systemctl daemon-reload

# 删除配置文件
rm -rf /var/lib/ceph
rm -f /etc/ceph/*

# 然后还可能需要zap掉创建的osd,参见上文

虽然没怎么用到,但是插眼:https://www.cnblogs.com/oloroso/p/14647299.html

最近新换了个电脑,宏碁的暗影骑士擎,属于比较少见的2NVME+1SATA配置的大存储型号了。俗言人尽其才、物尽其用,宝贵的SATA接口怎么能干放着不利用起来,于是就把老电脑上的1TB汰渍拆下来,拿过来继续当系统盘用。然而intel总喜欢搞些幺蛾子,当年阻碍6/7代U用Windows 7的XHCI、这次的主角Intel RST,都花了我老大功夫才解决问题。

按道理来说,把硬盘从老电脑上一拔、往新电脑上一插、开机键一按,系统就应该噌噌地跑起来才对。然而,事与愿违,启动SATA盘上的系统的时候,不是黑屏,就是出现INACCESSIBLE_BOOT_DEVICE蓝屏。到巨硬官网可以了解到,这个问题通常是因为系统分区所在硬盘的存储驱动配置异常导致的,最为常见的一种情况就是没装驱动。回到电脑自带的Windows 11系统一看,SATA硬盘的控制器名称不是标准SATA AHCI控制器,而是两个Intel RST VMD开头的控制器,我就知道是intel又整新活了。既然已经明确了问题,那就好办了:缺少驱动,就安装驱动。

然而,事情并没有想象中的那么简单:诸如网卡声卡一类的驱动,不装也可以进系统,大不了不用这些功能;显卡驱动的话,不装也有兼容模式,可以在低效率的情况下仍然保持正常工作,又不是不能用。这两种情况都可以在进入系统之后再补驱动。然而,intel家的VMD就没有这种待遇了:不装驱动,连数据都读不出来。读不出系统数据,就进不了系统,更不用谈补驱动了。

然而,在寻找解决方案的时候,无意间发现了官方论坛的一个讨论贴,里面提到BIOS里竟然有隐藏选项可以禁用VMD控制器,转而使用标准的SATA控制器。该隐藏选项需要在Main选项卡(即第二个)中按组合键Ctrl+S召唤出来(稍微按久一点,最好至少1s左右,否则不会显示出来),禁用后重启就能够正常进入系统了。

折腾了这么久,终于可以让自己的系统在新电脑上启动了= =


你以为这就结束了?

作为富有探索精神的咱,可不会因为一点小小进展就满足,最终目标可是在开启VMD的情况下正常进入系统啊。

找了好久,终于在StackOverflow上看到有人有类似的需求,心想StackOverflow上的老哥向来是靠谱的,于是决定拿来一试,嘿,还真他娘的管用。

其原理是利用DISM强制向系统添加驱动,比在系统内用dpinstpnputil一类工具的效果不知道高到哪里去了。命令如下:

dism /Image:C:\ /Add-Driver /Driver:D:\ /Recurse

其中C盘替换成要装驱动的系统分区,D盘及其路径替换成驱动所在分区,如果不需要在子文件夹中搜索驱动的话可以去掉/Recurse选项。DISM命令可以跨系统使用,Windows 11给Windows 10装、Windows 10给Windows 8装,甚至x64系统给x86系统装都是没有问题的,只要保证驱动与目标系统是兼容的就行。输完命令,敲下回车后,程序会给出驱动的安装情况,不出意外的话VMD驱动会出现在其中。

此时重启,再进入要安装驱动的系统时,不会再蓝屏,而是进入类似SysPrep的驱动配置阶段,小圆点转个几圈过后,熟悉的锁屏界面又出现在了眼前。一个字,爽!

为什么要禁止呢?因为一方面,连接的是已知网络,登陆地址都快能背下来了;另一方面,我并未将常用浏览器设置为默认浏览器,所以每次一联网都要弹个新窗口出来,看着不爽= =

方法是修改注册表项

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NlaSvc\Parameters\Internet\EnableActiveProbing

设置为0就可以禁止自动弹出了。

而修改

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NlaSvc\Parameters\Internet\ActiveWebProbeHost

则可以在启用自动弹出登陆页面的时候,修改Windows用于检测连通性的网址。