Přeskočit na obsah
lurycz Blog programátora
Linux čtení 7 min

Nový kernel jsem vyzkoušel proti fyzickému disku bez restartu serveru

Cizí patch do ovladače sd bez hlavičky a bez stopy na internetu cílil na jádro, které nikdy neexistovalo. Popisuju, jak jsem ho posoudil, doportoval na 6.8 a vyzkoušel proti fyzickému disku v QEMU, aniž bych restartoval server.

Disk s 528bajtovými sektory nejde v Linuxu použít. Blokové vrstvě nesedí velikost bloku, která není mocninou dvou, takže zařízení skončí s nulovou velikostí a nic přes něj neproteče. Když se firmware odemknout nedaří, zbývá obejít ho v ovladači: číst a zapisovat celé 528bajtové sektory a hostu ukazovat prvních 512 bajtů z každého.

Někdo takový patch napsal a mně se dostal do ruky. Nedalo se ale poznat, kdo ho psal ani jestli ho někdy někdo přeložil. Tenhle text je o dvou věcech, které z toho vzešly a hodí se i jinde: jak posoudit cizí kernelový patch neznámého původu a jak nový kernel vyzkoušet proti fyzickému disku, aniž bych restartoval stroj, ve kterém ten disk sedí.

Proč jsem tu byl a co dalšího jsem na těch discích zkoušel, popisuju v článku Velikost sektoru zamkl výrobce disku, ne značka na štítku.

Co jsem dostal a co o tom šlo zjistit

Dva soubory. Unified diff proti drivers/scsi/sd.cdrivers/scsi/sd.h, 605 přidaných řádků a 6 odebraných, plus pythoní skript, který ten patch přenáší na jiné verze jádra. Cílová cesta v tom skriptu je patches/kernel/9999-wvg-sd-528-translation.patch, což je konvence Proxmoxu. To byla jediná stopa k prostředí, ze kterého to pochází.

Jinak nic. Patch nemá hlavičku, chybí Signed-off-by, autor, copyright i řádek SPDX. Jeho identifikátory jsem hledal na internetu a nenašel ani jeden, ani v gitu jádra, ani na LKML, ani na GitHubu. Časová razítka v diffu ukazují na únor a duben 2026.

Kód přitom působí kompetentně. Převody dělá přes DIV_ROUND_UP, ne bitovým posunem, což je u 528 nutnost, protože to mocnina dvojky není. Odkládací buffery bere z předalokovaného mempoolu, takže se v cestě I/O nic nealokuje. Má ošetřené chyby a stropy na velikost požadavku i hloubku fronty. A správně nuluje protection_type, protože těch 16 bajtů navíc není ochrana T10 PI, ale metadata diskového pole.

Dobře napsaný kód ale není důkaz, že se ten kód někdy překládal.

Kontext, který v jádře neexistuje

Aplikace na čistou 6.8 skončila takhle:

sd.c   10 ze 13 hunku sedi, 3 selhaly (#1, #9, #10)
sd.h    3 ze 3 hunku sedi

Deset hunků prošlo hlavně proto, že patch toleruje posun. Zajímavé byly ty tři, které neprošly. Hunk 9 očekává v kontextových řádcích tohle:

	if (!scsi_device_online(sdp))
		goto out;

	lim = kmalloc(sizeof(*lim), GFP_KERNEL);
	if (!lim)
		goto out;

A hunk 10 pak sahá na lim->max_dev_sectors, tedy přes ukazatel. Stáhl jsem si originály sd.c pro tagy v6.8 až v6.14 a spočítal výskyty:

Jádrolim = kmalloclim->max_dev_sectorslim.max_dev_sectorsstruct queue_limits lim;
6.8 až 6.100000
6.11 až 6.140014

Do 6.10 včetně tam žádné queue_limits nejsou vůbec. Od 6.11 už ano, ale jako lokální proměnná na zásobníku, ke které se přistupuje tečkou, ne šipkou, a nikde se nealokuje. Patch tedy cílí na strom, který v upstreamu nikdy nebyl.

Přiložený rebase skript to nezachránil, protože spadl hned na první kontrole:

ERROR: sd_revalidate_disk: expected one function definition, found 0

Dohromady to znamená kód, který nikdy neprošel překladem. Kdyby ho autor zkompiloval, na tenhle rozpor by narazil při prvním pokusu. Není to důkaz, že je patch špatný. Je to důkaz, že je neověřený, a to jsou dvě různé věci.

Doportování na 6.8

Tři místa a tři různé opravy:

MístoCo bylo potřeba
sd_disable_advanced_block_ops()brala lim a sahala na lim->max_discard_sectors; přepsáno na blk_queue_max_discard_sectors(q, 0)blk_queue_max_write_zeroes_sectors(q, 0), upraveno i volající místo
hunk 9volání sd_528_limit_queue_depth() přesunuto před buffer = kmalloc(SD_BUF_SIZE, ...)
hunk 10lim->max_dev_sectors na q->limits.max_dev_sectors, totéž u max_segments

Past, na kterou jsem naletěl dvakrát

Skript, který ty tři hunky dopisuje, si musí umět ověřit, že už doběhl. Napsal jsem to takhle:

if "sd_528_effective_max_sectors" in text:
    return "uz aplikovano"

Vypadá to nevinně a je to špatně. Ten symbol totiž do souboru přichází už s hunkem 1, který prošel obyčejným patchem o krok dřív. Kontrola tedy hlásila hotovo i ve chvíli, kdy hunk 10 hotový nebyl, a já pak dlouho hledal, proč se překlad chová, jako by mé úpravy vůbec nebyly v souboru. Napodruhé jsem na to naletěl znovu, protože jsem tu podmínku opravil jinde a tady zapomněl.

Správně se kontroluje symbol, který přijde jedině s tím hunkem, který zrovna aplikuju. U hunku 10 je to emu_cap. Obecné pravidlo z toho je krátké: u skriptovaného patchování musí být příznak „už aplikováno“ vázaný na tu jednu změnu, ne na cokoli, co s ní přišlo do souboru.

Testovat nový kernel na stroji, který se nesmí restartovat

Ovladač sd je v tom systému zabudovaný napevno, ne jako modul, takže ho nejde vyměnit za běhu. A hostitel se restartovat nesměl. To vypadá jako slepá ulička, ale není: fyzický disk se dá do virtuálky pustit celý.

qemu-system-x86_64 -enable-kvm -m 2048 -smp 2 -nographic -no-reboot \
  -kernel /usr/src/k/linux-6.8/arch/x86/boot/bzImage \
  -initrd /tmp/qinitrd.gz \
  -append "console=ttyS0 sd_mod.emulate_512_from_fat_sectors=1" \
  -device virtio-scsi-pci,id=scsi0 \
  -drive file=/dev/sg2,if=none,id=d0,format=raw \
  -device scsi-generic,drive=d0,bus=scsi0.0

Podstatné je scsi-generic. Příkazy SCSI z virtuálky jdou rovnou na fyzické zařízení, takže testovaný ovladač mluví se skutečným diskem, ne s emulací. Hostitelské jádro do toho nemluví a běží dál. Initramfs stačí postavit z busyboxu, není v něm potřeba nic než shell a pár nástrojů.

Tenhle postup se hodí i mimo tenhle případ. Kdykoli potřebujete zkusit ovladač proti konkrétnímu kusu hardwaru na stroji, který nemůžete odstavit, je to nejlevnější cesta.

Výsledky

Nejdřív kontrolní běh s vypnutou emulací, aby bylo vidět, že měřím to, co si myslím:

emulate_512_from_fat_sectors=0
    sda: velikost=0        physical_bs=4224     dd -> 0 records

emulate_512_from_fat_sectors=1
    sd 0:0:0:0: [sda] Attached SCSI disk
    sda: 3030911576 bloku  logical_bs=512  physical_bs=512  ->  1445 GB
    dd -> 1+0 records in/out

Pak zápis a zpětné čtení na třech místech, která se od sebe liší tím, jak padnou do vnitřního dělení:

TestVýsledek
zarovnaný zápis 32 kB na LBA 1000otisk sedí
nezarovnaný zápis 17 sektorů na LBA 2049sedí
1 MB přes hranici 64kB chunku na LBA 10240otisk sedí

Otisky sedící samy se sebou ale nedokazují tolik, kolik to vypadá. Kdyby emulace ukládala data soustavně o kus vedle, čtení touž cestou by se s ní zmýlilo stejně. Proto ten poslední krok, který mě na celé práci potěšil nejvíc: data zapsaná ve virtuálce přes emulované 512bajtové zařízení jsem přečetl z hostitele nativně jako 528bajtový sektor.

sg_raw -r 528 -o out.bin /dev/sg2 28 00 00 00 03 e8 00 00 01 00

emulace v QEMU:    3b 0f a4 56 af f2 0e 24 c8 c9 87 4b a2 e2 6f 29
nativne na disku:  3b 0f a4 56 af f2 0e 24 c8 c9 87 4b a2 e2 6f 29
                   25 24 02 a8 c3 21 4d d3 96 c4 7f 4b b9 50 4a 53
metadata[512:528]: same nuly

Bajty jsou fyzicky na správném místě ve správných sektorech a 16 bajtů metadat je vynulovaných. Disk po všech testech hlásí zdraví v pořádku, nula vadných sektorů a velikost bloku beze změny 528 bajtů.

Slepá ulička: shim v userspace

Než jsem se pustil do jádra, zkusil jsem to jednodušeji. Plugin pro nbdkit, který totéž přepočítávání dělá nad síťovým blokovým zařízením, se napíše za odpoledne a nepotřebuje překládat kernel.

První verze běžela 4,7 MB/s. Přepočítávala bajt po bajtu v Pythonu, což byla hloupost, kterou jsem si měl uvědomit dřív, než jsem to spustil. Po přepsání na práci s řezy vyskočila na 118 MB/s. To už je použitelné číslo, jenže nativní přístup k témuž disku dává kolem 390 MB/s, takže by se za pohodlí platilo dvěma třetinami výkonu. Plus vrstva navíc v cestě dat, kterou by bylo potřeba provozovat a hlídat. Zamítl jsem to.

Co to stojí a co jsem neudělal

Cena emulace je 16 bajtů z každého sektoru: 1,55 TB místo 1,60 TB na disk, tedy kolem 388 GB ze všech osmi dohromady.

Co se tím naopak nezmění, je rychlost linku – a stojí za to říct proč, protože se to nabízí jako první nápad. Řadič nabízí 12 Gb/s (maximum_linkrate hlásí 12.0 Gbit), ale disk se domluví na 6 a tam zůstane. Ptal jsem se přímo jeho: stránka 19h/01h „Phy control and discover“ vrací u obou phy v bajtu 33 hodnotu 0xaa, tedy hardwarové maximum 6 Gb/s. Strop nesedí v kabeláži ani v řadiči, ale ve firmwaru disku, a jádro s ním nezmůže nic.

Druhý port by nepomohl taky ne, i když disky jsou dual-port. V SAS se rychlost vyjednává na každé phy zvlášť, takže druhá linka přidá redundanci a součet propustnosti – ne vyšší rychlost jednoho spoje. Rozepsal jsem to zvlášť v rozboru rychlosti linku.

A hlavně: ostrá data na tom zatím nemám a mít nebudu, dokud neproběhne pořádné testování. Testy výše byly krátké a záměrně mířené na hranice dělení, ne na výdrž. Než na tom něco poběží, chce to fio s verifikací přes několik hodin, badblocks -w na celý disk, test se souborovým systémem včetně kontroly po odpojení a odpověď na otázku, co udělá výpadek napájení uprostřed zápisu. Šest set řádků v cestě DMA se neprojeví hláškou, ale tichým poškozením dat.

Emulace navíc zahazuje metadata, ze kterých si diskové pole počítá vlastní kontrolu integrity. Disk pak už pole konzistentní nepřipadá. Pro mě to nevadí, používám ho jako obyčejné blokové zařízení, ale zpátky do pole by ten disk už neměl co dělat.

Kód a podklady

Všechno, na čem tenhle text stojí, je ve veřejném repozitáři st1600fm0013-528b-sectors – skripty, zálohy firmwaru i rozbory, které se do článku nevešly.

Zdroje

Dál k tématu

  • Linux

    Velikost sektoru zamkl výrobce disku, ne značka na štítku

    Osm SAS SSD z vyřazeného pole hlásí v Linuxu nulovou velikost. Rozebral jsem firmware, našel sedm bajtů, které za to můžou, a doportoval patch do jádra. Pak jsem pustil jednu nedestruktivní sondu a zjistil, že u jiného disku stačí jeden příkaz.

  • Linux

    Zámek velikosti sektoru sedí v datovém záznamu, ne v kódu firmwaru

    Osm SAS SSD z vyřazeného pole hlásí v Linuxu nulovou velikost. Než jsem začal obcházet firmware v ovladači, rozebral jsem tři dumpy SPI flash a našel místo, které o velikosti sektoru rozhoduje. Je to datový záznam, ne kus kódu.

  • Linux

    Linux load average – Chápeme hodnoty příkazu uptime

    Řešili jste někdy co vlastně znamenají 3 čísla na konci příkazu uptime ? load average: 0,10, 0,09, 0,11 Tato čísla obecně vyjadřují průměrné zatížení linuxového systému za období 1, 5 a 15 minut a nezahrnují žádné procesy, čekající doby i/o wait na discích nebo síti.…

  • Linux

    Test rychlosti internetu (speedtest) z linuxového bashe

    Řešili jste někdy otázku, jak změřit rychlost na linuxovém serveru bez GUI ? Dříve jsem to řešil stahováním 1GB souboru , ale to se pro větší rychlosti nehodí hlavně kvůli nedostatečné šířce pásma cílového serveru, tudíž přesné změření rychlosti nepřipadá v úvahu. Proto jsem…

Diskuse

Zatím tu nikdo nediskutuje.

Napsat příspěvek

Diskutovat můžete i bez účtu. S registrací se ale příspěvek zveřejní hned a nemusíte pokaždé vyplňovat jméno. Účet už máte? Přihlaste se.

Nezveřejňuje se, slouží jen k ověření.

Podporuje zápis Texy: **tučně**, *kurzíva*, odrážky, odkazy.