Un volum continuă să fie montat, dar o citire directă întoarce EIO. În loc să căutăm periodic expresii fragile în mesajele kernelului, vom primi evenimentul prin interfața fanotify, îl vom decoda într-un proces userspace și îl vom păstra într-un journal separat.
Laboratorul folosește un sistem de fișiere ext4 creat într-o imagine locală de 512 MiB. Device Mapper introduce temporar o eroare de citire numai pe acest dispozitiv de test. Niciun disc real nu este corupt, iar tabela liniară este restaurată automat chiar dacă testul eșuează.
La final rulează trei componente legate între ele:
fserror-monitor, un program C care cereFAN_FS_ERRORși decodează înregistrărileERRORșiFID;fserror-monitor.service, pornit sub un utilizator dedicat, cu singura capabilitate necesară pentru marca fanotify la nivel de filesystem;fserror-health.timer, care verifică periodic dacă monitorul a rămas activ.
Evenimentele se citesc cu journalctl --namespace=fserror. Rezultatul verificabil este un mesaj cu eroarea brută, numărul errno, contorul de evenimente și identificatorul obiectului furnizat de kernel.
fserror transportă problema, iar fanotify stabilește contractul cu procesul
fserror nu este un program pe care îl instalăm și nici un daemon care analizează dmesg. Este infrastructură internă a kernelului, construită în jurul structurii super_block, prin care codul VFS și sistemele de fișiere pot raporta erori de metadata sau de I/O către fsnotify.
În Linux 7.0, această cale a fost standardizată pentru raportarea generică a erorilor de filesystem și de fișier. Linux 7.2, folosit aici, include infrastructura și suportul relevant pentru ext4. Acoperirea depinde totuși de sistemul de fișiere și de locul din kernel care raportează eroarea: FAN_FS_ERROR nu promite că orice eroare produsă de orice driver va deveni automat eveniment fanotify.
Fluxul laboratorului este:
dd cu O_DIRECT
|
v
Device Mapper: target "error"
|
v
operația de citire primește -EIO
|
v
fserror raportează eroarea pentru super_block/inode
|
v
fsnotify construiește FAN_FS_ERROR
|
v
fanotify queue
|
v
fserror-monitor citește ERROR + FID
|
v
journal namespace "fserror"
O notificare nu înlocuiește rezultatul operației. Aplicația care a făcut citirea trebuie să verifice în continuare codul său de retur. Aceeași separare contează și în aplicațiile descrise în ghidul despre io_uring și monitorizarea aplicațiilor I/O-intensive: completarea unei operații spune ce s-a întâmplat acelei cereri, în timp ce FAN_FS_ERROR informează un monitor despre o problemă observată în filesystem.
Evenimentul fanotify conține o structură fanotify_event_metadata, urmată de înregistrări cu lungime variabilă. Pentru cazul nostru sunt relevante:
FAN_EVENT_INFO_TYPE_ERROR, cu eroarea șierror_count;FAN_EVENT_INFO_TYPE_FID, cufsidși un file handle;FAN_Q_OVERFLOW, dacă procesul nu a golit coada suficient de repede.
error_count poate arăta că mai multe erori echivalente au fost reunite. Nu este un număr de sectoare defecte și nu trebuie interpretat ca rezultat al unei verificări de integritate.
Fixăm laboratorul la Fedora 44, kernel 7.2 și un volum ext4 izolat
Configurația de referință este Fedora Server 44 x86-64, cu kernelul Fedora 7.2.11-200.fc44, systemd 259 și minimum 2 GiB RAM. Sunt necesari aproximativ 700 MiB liberi: 512 MiB pentru imagine, plus journal și fișierele proiectului. Compilarea și testarea durează aproximativ 30–45 de minute.
Laboratorul trebuie rulat într-o mașină virtuală sau pe un sistem de test. Scriptul verifică numele Device Mapper înainte de orice modificare, dar comenzile losetup, dmsetup, mount și mkfs.ext4 cer privilegii administrative.
Verifică mediul înainte de a crea proiectul:
grep -E '^(NAME|VERSION_ID)=' /etc/os-release
uname -r
uname -m
systemd --version | head -n 1
gcc --version | head -n 1
dmsetup version
mkfs.ext4 -V 2>&1 | head -n 1
findmnt --version
df -h /var/lib
Un exemplu compatibil este:
NAME="Fedora Linux"
VERSION_ID="44"
7.2.11-200.fc44.x86_64
x86_64
systemd 259
gcc (GCC) 16.x
Library version: 1.02.x
mke2fs 1.47.x
findmnt from util-linux 2.x
Versiunile secundare ale GCC, LVM2, e2fsprogs și util-linux pot fi mai noi după actualizările Fedora. Cerințele rigide sunt kernel 7.0 sau mai nou pentru noua infrastructură generică și un filesystem care emite evenimentele urmărite. Pașii au fost fixați la kernel 7.2 și ext4.
Instalează pachetele într-un singur pas:
sudo dnf install -y gcc glibc-headers kernel-headers
device-mapper e2fsprogs util-linux systemd
Proiectul va avea această structură:
/opt/fserror-lab/
├── src/
│ └── fserror-monitor.c # consumatorul fanotify pentru FAN_FS_ERROR
└── scripts/
├── lab-setup.sh # creează imaginea, mapperul și volumul ext4
├── inject-io-error.sh # comută temporar mapperul pe targetul error
└── healthcheck.sh # verifică serviciul și marca activă
/etc/systemd/system/
├── fserror-monitor.service # procesul persistent de monitorizare
├── fserror-health.service # verificarea periodică
└── fserror-health.timer # programarea verificării
/etc/systemd/journald@fserror.conf.d/
└── retention.conf # retenția journalului izolat
Pasul 1 — Compilăm consumatorul care decodează ERROR și FID
Programul trebuie să ceară explicit FAN_REPORT_FID. Documentația kernelului precizează această condiție pentru FAN_FS_ERROR. Folosim o marcă FAN_MARK_FILESYSTEM, nu o simplă supraveghere a directorului.
Creează directoarele fără a suprascrie un proiect existent:
sudo test ! -e /opt/fserror-lab
sudo install -d -m 0755 /opt/fserror-lab/src
sudo install -d -m 0755 /opt/fserror-lab/scripts
Fișierul /opt/fserror-lab/src/fserror-monitor.c:
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/fanotify.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
static volatile sig_atomic_t running = 1;
static void stop_monitor(int signo)
{
(void)signo;
running = 0;
}
static void print_timestamp(void)
{
struct timespec ts;
struct tm tm;
char stamp[32];
if (clock_gettime(CLOCK_REALTIME, &ts) == -1 ||
gmtime_r(&ts.tv_sec, &tm) == NULL) {
fputs("time=unknown ", stdout);
return;
}
if (strftime(stamp, sizeof(stamp), "%Y-%m-%dT%H:%M:%S", &tm) == 0) {
fputs("time=unknown ", stdout);
return;
}
printf("time=%s.%03ldZ ", stamp, ts.tv_nsec / 1000000L);
}
static void print_fid(const struct fanotify_event_info_fid *fid)
{
const struct file_handle *handle;
unsigned int i;
handle = (const struct file_handle *)fid->handle;
printf("fsid=%08x:%08x handle_type=%d handle_bytes=%u handle=",
(unsigned int)fid->fsid.val[0],
(unsigned int)fid->fsid.val[1],
handle->handle_type,
handle->handle_bytes);
for (i = 0; i < handle->handle_bytes; i++)
printf("%02x", handle->f_handle[i]);
}
static int process_event(const struct fanotify_event_metadata *metadata)
{
const char *base = (const char *)metadata;
const struct fanotify_event_info_error *error_info = NULL;
const struct fanotify_event_info_fid *fid_info = NULL;
size_t offset = FAN_EVENT_METADATA_LEN;
if (metadata->vers != FANOTIFY_METADATA_VERSION) {
fprintf(stderr,
"unsupported fanotify metadata version: got=%u expected=%un",
metadata->vers, FANOTIFY_METADATA_VERSION);
return -1;
}
if (metadata->mask & FAN_Q_OVERFLOW) {
print_timestamp();
puts("QUEUE_OVERFLOW events_may_be_missing=yes");
fflush(stdout);
return 0;
}
if (!(metadata->mask & FAN_FS_ERROR))
return 0;
while (offset + sizeof(struct fanotify_event_info_header) <=
metadata->event_len) {
const struct fanotify_event_info_header *header =
(const struct fanotify_event_info_header *)(base + offset);
if (header->len < sizeof(*header) ||
offset + header->len > metadata->event_len) {
fprintf(stderr,
"invalid fanotify information record: offset=%zu len=%u event_len=%un",
offset, header->len, metadata->event_len);
return -1;
}
if (header->info_type == FAN_EVENT_INFO_TYPE_ERROR)
error_info =
(const struct fanotify_event_info_error *)header;
else if (header->info_type == FAN_EVENT_INFO_TYPE_FID)
fid_info = (const struct fanotify_event_info_fid *)header;
offset += header->len;
}
print_timestamp();
if (error_info != NULL) {
int raw_error = error_info->error;
int error_number = raw_error < 0 ? -raw_error : raw_error;
printf("FS_ERROR raw=%d errno=%d message="%s" count=%u ",
raw_error,
error_number,
strerror(error_number),
error_info->error_count);
} else {
fputs("FS_ERROR error_record=missing ", stdout);
}
if (fid_info != NULL)
print_fid(fid_info);
else
fputs("fid=missing", stdout);
putchar('n');
fflush(stdout);
return 0;
}
int main(int argc, char **argv)
{
int fanotify_fd;
char buffer[8192] __attribute__((aligned(8)));
if (argc != 2) {
fprintf(stderr, "usage: %s MOUNTPOINTn", argv[0]);
return EXIT_FAILURE;
}
signal(SIGTERM, stop_monitor);
signal(SIGINT, stop_monitor);
fanotify_fd = fanotify_init(FAN_CLASS_NOTIF | FAN_REPORT_FID,
O_RDONLY | O_LARGEFILE | O_CLOEXEC);
if (fanotify_fd == -1) {
perror("fanotify_init");
return EXIT_FAILURE;
}
if (fanotify_mark(fanotify_fd,
FAN_MARK_ADD | FAN_MARK_FILESYSTEM,
FAN_FS_ERROR,
AT_FDCWD,
argv[1]) == -1) {
perror("fanotify_mark");
close(fanotify_fd);
return EXIT_FAILURE;
}
printf("READY mount=%s mask=FAN_FS_ERROR report=FAN_REPORT_FIDn",
argv[1]);
fflush(stdout);
while (running) {
ssize_t bytes_read = read(fanotify_fd, buffer, sizeof(buffer));
ssize_t remaining;
struct fanotify_event_metadata *metadata;
if (bytes_read == -1) {
if (errno == EINTR)
continue;
perror("read");
close(fanotify_fd);
return EXIT_FAILURE;
}
if (bytes_read == 0)
continue;
remaining = bytes_read;
metadata = (struct fanotify_event_metadata *)buffer;
while (FAN_EVENT_OK(metadata, remaining)) {
if (process_event(metadata) == -1) {
close(fanotify_fd);
return EXIT_FAILURE;
}
metadata = FAN_EVENT_NEXT(metadata, remaining);
}
}
close(fanotify_fd);
return EXIT_SUCCESS;
}
Compilează și instalează binarul:
sudo gcc -std=c17 -O2 -Wall -Wextra -Wpedantic
-o /usr/local/sbin/fserror-monitor
/opt/fserror-lab/src/fserror-monitor.c
sudo chown root:root /usr/local/sbin/fserror-monitor
sudo chmod 0755 /usr/local/sbin/fserror-monitor
/usr/local/sbin/fserror-monitor 2>&1 || true
Opțiunile -Wall, -Wextra și -Wpedantic activează verificări utile la compilare. Programul validează atât versiunea metadatelor fanotify, cât și lungimea fiecărei înregistrări, pentru a nu traversa date în afara evenimentului.
Trebuie să vezi:
usage: /usr/local/sbin/fserror-monitor MOUNTPOINT
Punct de control: dacă structura fanotify_event_info_error nu este declarată, verifică dacă kernel-headers și antetele glibc provin din Fedora 44; nu înlocui manual structurile ABI.
Pasul 2 — Construim volumul ext4 peste un mapper reversibil
Scriptul creează o imagine, atașează un loop device, construiește un target Device Mapper liniar și scrie un fișier de 64 MiB. Volumul este remontat read-only înaintea testului, astfel încât injectarea să nu întrerupă scrieri de jurnal ext4.
Fișierul /opt/fserror-lab/scripts/lab-setup.sh:
#!/usr/bin/env bash
set -euo pipefail
STATE_DIR=/var/lib/fserror-lab
IMAGE="$STATE_DIR/disk.img"
STATE_FILE="$STATE_DIR/state.env"
MOUNTPOINT=/srv/fserror-lab
MAPPER_NAME=fserror-lab
MAPPER_DEV="/dev/mapper/$MAPPER_NAME"
LOOP_DEV=
CREATED_MAPPER=0
MOUNTED=0
cleanup_on_error() {
local status=$?
if (( status == 0 )); then
return
fi
if (( MOUNTED == 1 )); then
umount "$MOUNTPOINT" || true
fi
if (( CREATED_MAPPER == 1 )); then
dmsetup remove "$MAPPER_NAME" || true
fi
if [[ -n "$LOOP_DEV" ]]; then
losetup -d "$LOOP_DEV" || true
fi
exit "$status"
}
trap cleanup_on_error EXIT
if [[ $EUID -ne 0 ]]; then
echo "ERROR: rulează scriptul prin sudo" >&2
exit 1
fi
if dmsetup info "$MAPPER_NAME" >/dev/null 2>&1; then
echo "ERROR: mapperul $MAPPER_NAME există deja" >&2
exit 1
fi
if mountpoint -q "$MOUNTPOINT"; then
echo "ERROR: $MOUNTPOINT este deja un punct de montare" >&2
exit 1
fi
if [[ -e "$IMAGE" || -e "$STATE_FILE" ]]; then
echo "ERROR: starea laboratorului există deja în $STATE_DIR" >&2
exit 1
fi
install -d -m 0700 "$STATE_DIR"
install -d -m 0755 "$MOUNTPOINT"
truncate -s 512M "$IMAGE"
LOOP_DEV=$(losetup --find --show "$IMAGE")
SECTORS=$(blockdev --getsz "$LOOP_DEV")
printf '0 %s linear %s 0n' "$SECTORS" "$LOOP_DEV" |
dmsetup create "$MAPPER_NAME"
CREATED_MAPPER=1
mkfs.ext4 -q -F -L FSERROR_LAB "$MAPPER_DEV"
mount "$MAPPER_DEV" "$MOUNTPOINT"
MOUNTED=1
dd if=/dev/urandom of="$MOUNTPOINT/probe.bin"
bs=1M count=64 conv=fsync status=progress
sync
umount "$MOUNTPOINT"
MOUNTED=0
mount -o ro "$MAPPER_DEV" "$MOUNTPOINT"
MOUNTED=1
cat >"$STATE_FILE" <<EOF
LOOP_DEV=$LOOP_DEV
SECTORS=$SECTORS
MAPPER_NAME=$MAPPER_NAME
MOUNTPOINT=$MOUNTPOINT
EOF
chmod 0600 "$STATE_FILE"
findmnt --noheadings --output SOURCE,FSTYPE,OPTIONS "$MOUNTPOINT"
dmsetup table "$MAPPER_NAME"
stat --format='probe=%n bytes=%s' "$MOUNTPOINT/probe.bin"
trap - EXIT
echo "READY: laboratorul ext4 este montat read-only"
Activează și execută scriptul:
sudo chmod 0750 /opt/fserror-lab/scripts/lab-setup.sh
sudo /opt/fserror-lab/scripts/lab-setup.sh
blockdev --getsz întoarce dimensiunea în sectoare de 512 octeți, exact unitatea cerută de tabela Device Mapper. Tabela linear expune întreaga imagine fără transformări.
Trebuie să vezi un rezultat de această formă; numărul loop device-ului poate diferi:
/dev/mapper/fserror-lab ext4 ro,relatime
0 1048576 linear /dev/loop0 0
probe=/srv/fserror-lab/probe.bin bytes=67108864
READY: laboratorul ext4 este montat read-only
Punct de control: findmnt trebuie să arate ext4 și opțiunea ro; nu continua dacă sursa este un disc real sau dacă tabela nu începe cu linear.
Pasul 3 — Pornim monitorul cu o capabilitate delimitată și journal propriu
Marca FAN_MARK_FILESYSTEM împreună cu raportarea FID necesită privilegii. Procesul nu rulează cu UID 0: primește CAP_SYS_ADMIN prin systemd. Aceasta rămâne o capabilitate foarte puternică, motiv pentru care binarul este deținut de root, nu este editabil de utilizatorul serviciului și rulează cu protecții suplimentare.
Creează utilizatorul:
sudo useradd --system --home-dir / --shell /usr/sbin/nologin
fserror-mon 2>/dev/null || true
Fișierul /etc/systemd/system/fserror-monitor.service:
[Unit]
Description=Monitor FAN_FS_ERROR pentru volumul de laborator
After=local-fs.target
RequiresMountsFor=/srv/fserror-lab
ConditionPathIsMountPoint=/srv/fserror-lab
[Service]
Type=simple
User=fserror-mon
Group=fserror-mon
ExecStart=/usr/local/sbin/fserror-monitor /srv/fserror-lab
Restart=on-failure
RestartSec=3s
AmbientCapabilities=CAP_SYS_ADMIN
CapabilityBoundingSet=CAP_SYS_ADMIN
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictRealtime=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
SystemCallArchitectures=native
LogNamespace=fserror
[Install]
WantedBy=multi-user.target
Un journal namespace separă logurile monitorului de journalul principal și permite o politică proprie de retenție. Fișierul /etc/systemd/journald@fserror.conf.d/retention.conf:
[Journal]
Storage=persistent
Compress=yes
SystemMaxUse=256M
MaxRetentionSec=14day
Înainte de creare, verifică să nu existe configurații cu același nume:
sudo test ! -e /etc/systemd/system/fserror-monitor.service
sudo test ! -e /etc/systemd/journald@fserror.conf.d/retention.conf
sudo install -d -m 0755 /etc/systemd/journald@fserror.conf.d
Salvează fișierele de mai sus, apoi pornește serviciul:
sudo systemctl daemon-reload
sudo systemctl enable --now fserror-monitor.service
sudo systemctl is-active fserror-monitor.service
sudo journalctl --namespace=fserror
-u fserror-monitor.service --no-pager -n 10
Trebuie să vezi:
active
READY mount=/srv/fserror-lab mask=FAN_FS_ERROR report=FAN_REPORT_FID
ProtectSystem=strict face arborele de fișiere read-only pentru proces, iar MemoryDenyWriteExecute=yes împiedică mapările simultan inscriptibile și executabile. LogNamespace=fserror trimite ieșirea serviciului către instanța sa de journald.
Punct de control: dacă lipsește mesajul READY, citește imediat systemctl status fserror-monitor.service și journalul namespace-ului înainte de a injecta eroarea.
Pasul 4 — Comutăm controlat numai volumul de laborator pe EIO
Avertisment: următorul script suspendă temporar dispozitivul /dev/mapper/fserror-lab. Nu modifica MAPPER_NAME pentru a indica un volum de producție. Scriptul acceptă numai numele și punctul de montare salvate de etapa de creare.
Fișierul /opt/fserror-lab/scripts/inject-io-error.sh:
#!/usr/bin/env bash
set -euo pipefail
STATE_FILE=/var/lib/fserror-lab/state.env
SWITCHED=0
if [[ $EUID -ne 0 ]]; then
echo "ERROR: rulează scriptul prin sudo" >&2
exit 1
fi
if [[ ! -r "$STATE_FILE" ]]; then
echo "ERROR: lipsește $STATE_FILE" >&2
exit 1
fi
source "$STATE_FILE"
if [[ "$MAPPER_NAME" != "fserror-lab" ||
"$MOUNTPOINT" != "/srv/fserror-lab" ]]; then
echo "ERROR: starea laboratorului nu corespunde țintei permise" >&2
exit 1
fi
CURRENT_TABLE=$(dmsetup table "$MAPPER_NAME")
EXPECTED_TABLE="0 $SECTORS linear $LOOP_DEV 0"
if [[ "$CURRENT_TABLE" != "$EXPECTED_TABLE" ]]; then
echo "ERROR: tabela mapperului nu este cea liniară a laboratorului" >&2
echo "GĂSIT: $CURRENT_TABLE" >&2
exit 1
fi
restore_linear() {
local status=$?
if (( SWITCHED == 1 )); then
dmsetup suspend "$MAPPER_NAME" || true
printf '0 %s linear %s 0n' "$SECTORS" "$LOOP_DEV" |
dmsetup load "$MAPPER_NAME" || true
dmsetup resume "$MAPPER_NAME" || true
fi
exit "$status"
}
trap restore_linear EXIT INT TERM
dmsetup suspend "$MAPPER_NAME"
printf '0 %s errorn' "$SECTORS" | dmsetup load "$MAPPER_NAME"
dmsetup resume "$MAPPER_NAME"
SWITCHED=1
if dd if="$MOUNTPOINT/probe.bin" of=/dev/null
bs=1M iflag=direct status=none; then
echo "ERROR: citirea a reușit, deși targetul error era activ" >&2
exit 1
else
echo "EXPECTED: citirea directă a primit o eroare I/O"
fi
sleep 2
dmsetup suspend "$MAPPER_NAME"
printf '0 %s linear %s 0n' "$SECTORS" "$LOOP_DEV" |
dmsetup load "$MAPPER_NAME"
dmsetup resume "$MAPPER_NAME"
SWITCHED=0
dmsetup table "$MAPPER_NAME"
dd if="$MOUNTPOINT/probe.bin" of=/dev/null
bs=1M iflag=direct status=none
trap - EXIT
echo "RESTORED: tabela liniară și citirea directă funcționează"
Rulează testul:
sudo chmod 0750 /opt/fserror-lab/scripts/inject-io-error.sh
sudo /opt/fserror-lab/scripts/inject-io-error.sh
iflag=direct cere O_DIRECT, astfel încât verificarea să nu fie satisfăcută doar din page cache. Targetul error răspunde cu eroare tuturor operațiilor adresate lui. Capcana trap încearcă restaurarea tabelei liniare la orice ieșire ulterioară comutării.
Ieșirea așteptată include eroarea dd; numărul de octeți la care apare poate varia:
dd: error reading '/srv/fserror-lab/probe.bin': Input/output error
EXPECTED: citirea directă a primit o eroare I/O
0 1048576 linear /dev/loop0 0
RESTORED: tabela liniară și citirea directă funcționează
Punct de control: indiferent de rezultatul testului, rulează sudo dmsetup table fserror-lab; primul target trebuie să fie din nou linear, nu error.
Pasul 5 — Verificăm traseul complet și instalăm supravegherea periodică
Citește numai evenimentele monitorului apărute în ultimele cinci minute:
sudo journalctl --namespace=fserror
-u fserror-monitor.service
--since '-5 minutes' --no-pager
Exemplul de rezultat arată astfel; timestampul, fsid, handle-ul și contorul pot diferi:
READY mount=/srv/fserror-lab mask=FAN_FS_ERROR report=FAN_REPORT_FID
time=2026-10-05T12:34:56.123Z FS_ERROR raw=-5 errno=5 message="Input/output error" count=1 fsid=4f31a2c0:9018be77 handle_type=1 handle_bytes=12 handle=7a0000000000000000000000
Acesta este testul de capăt: dd a primit eroarea operației, fserror a propagat raportarea către fsnotify, fanotify a livrat evenimentul, iar serviciul l-a decodat în userspace. Un câmp fid=missing poate apărea pentru erori care nu pot fi asociate unui obiect identificabil; eroarea rămâne totuși relevantă la nivel de filesystem.
Pentru verificarea periodică, fișierul /opt/fserror-lab/scripts/healthcheck.sh:
#!/usr/bin/env bash
set -euo pipefail
SERVICE=fserror-monitor.service
MOUNTPOINT=/srv/fserror-lab
if ! mountpoint -q "$MOUNTPOINT"; then
echo "HEALTH status=failed reason=mountpoint_missing path=$MOUNTPOINT"
exit 1
fi
if ! systemctl is-active --quiet "$SERVICE"; then
echo "HEALTH status=failed reason=service_inactive unit=$SERVICE"
exit 1
fi
MAIN_PID=$(systemctl show "$SERVICE" --property=MainPID --value)
if [[ -z "$MAIN_PID" || "$MAIN_PID" == "0" ]]; then
echo "HEALTH status=failed reason=main_pid_missing unit=$SERVICE"
exit 1
fi
echo "HEALTH status=ok unit=$SERVICE pid=$MAIN_PID mount=$MOUNTPOINT"
Fișierul /etc/systemd/system/fserror-health.service:
[Unit]
Description=Verifică monitorul FAN_FS_ERROR
After=fserror-monitor.service
[Service]
Type=oneshot
ExecStart=/opt/fserror-lab/scripts/healthcheck.sh
LogNamespace=fserror
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
Fișierul /etc/systemd/system/fserror-health.timer:
[Unit]
Description=Verificare periodică pentru fserror-monitor
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
Persistent=true
Unit=fserror-health.service
[Install]
WantedBy=timers.target
Activează verificarea:
sudo chmod 0750 /opt/fserror-lab/scripts/healthcheck.sh
sudo systemctl daemon-reload
sudo systemctl enable --now fserror-health.timer
sudo systemctl start fserror-health.service
sudo systemctl list-timers fserror-health.timer --no-pager
sudo journalctl --namespace=fserror
-u fserror-health.service --no-pager -n 5
Trebuie să vezi un mesaj asemănător:
HEALTH status=ok unit=fserror-monitor.service pid=1842 mount=/srv/fserror-lab
Timerul confirmă existența mountpointului, starea serviciului și un PID principal nenul. Nu confirmă că dispozitivul este sănătos; aceasta cere instrumentele sistemului de fișiere și analiza operațiilor care au eșuat.
Pentru investigații temporare, eBPF poate adăuga context despre call path, dar nu înlocuiește ABI-ul fanotify și trebuie controlat atent. Într-un mediu cu politici stricte, ghidul despre verificarea programelor eBPF semnate oferă modelul de guvernanță potrivit.
Șase mesaje care localizează defectul laboratorului
fanotify_init: Operation not permitted
Procesul nu are privilegiul cerut de grupul fanotify cu FID. Confirmă că serviciul păstrează capabilitatea:
sudo systemctl show fserror-monitor.service
-p User -p AmbientCapabilities -p CapabilityBoundingSet
sudo systemctl restart fserror-monitor.service
Nu rezolva problema transformând binarul în setuid root.
fanotify_mark: Invalid argument
Kernelul activ sau combinația de filesystem și mască nu acceptă marca solicitată. Confirmă versiunea, mountpointul și tipul filesystemului:
uname -r
findmnt -no TARGET,FSTYPE,OPTIONS /srv/fserror-lab
grep -w FAN_FS_ERROR /usr/include/linux/fanotify.h
Pe acest laborator trebuie să apară kernel 7.2, ext4 și constanta din antet.
fanotify_mark: No such device
Mountpointul nu este asociat unui filesystem care poate fi marcat prin fsnotify sau serviciul a pornit după demontare. Refă montarea și repornește unitatea:
sudo findmnt /srv/fserror-lab
sudo systemctl restart fserror-monitor.service
dd: error reading '/srv/fserror-lab/probe.bin': Input/output error
În interiorul inject-io-error.sh, acesta este rezultatul intenționat, nu defectul testului. Devine o problemă dacă persistă după mesajul RESTORED.
sudo dmsetup table fserror-lab
sudo dd if=/srv/fserror-lab/probe.bin of=/dev/null
bs=1M iflag=direct status=none
Tabela trebuie să conțină linear.
ERROR: tabela mapperului nu este cea liniară a laboratorului
Scriptul a refuzat să schimbe o configurație neașteptată. Nu forța executarea. Inspectează starea salvată și tabela activă:
sudo cat /var/lib/fserror-lab/state.env
sudo dmsetup table fserror-lab
sudo losetup --list
Dacă tabela este error, reconstruiește manual tabela linear numai cu valorile din state.env, apoi reia testul.
Eroarea apare în dd, dar journalul nu conține FS_ERROR
Mai întâi confirmă că monitorul afișase READY înaintea injectării și că citești namespace-ul corect:
sudo systemctl is-active fserror-monitor.service
sudo journalctl --namespace=fserror
-u fserror-monitor.service --no-pager
Dacă serviciul este activ, kernelul sau call path-ul testat nu a emis notificarea. FAN_FS_ERROR nu este o copie universală a tuturor erorilor bloc. Verifică versiunea kernelului și suportul filesystemului în documentația acelei versiuni; nu declara monitorizarea funcțională doar pentru că dd a întors EIO.
Demontăm întâi filesystemul, apoi mapperul și loop device-ul
Avertisment: ordinea contează. Încercarea de a elimina mapperul cât timp ext4 este montat produce Device or resource busy.
Oprește componentele și citește starea înainte de ștergere:
sudo systemctl disable --now fserror-health.timer
sudo systemctl stop fserror-health.service 2>/dev/null || true
sudo systemctl disable --now fserror-monitor.service
sudo bash -c '
set -euo pipefail
source /var/lib/fserror-lab/state.env
if dmsetup table "$MAPPER_NAME" | grep -qw error; then
dmsetup suspend "$MAPPER_NAME"
printf "0 %s linear %s 0n" "$SECTORS" "$LOOP_DEV" |
dmsetup load "$MAPPER_NAME"
dmsetup resume "$MAPPER_NAME"
fi
umount "$MOUNTPOINT"
dmsetup remove "$MAPPER_NAME"
losetup -d "$LOOP_DEV"
'
Șterge numai fișierele create de laborator:
sudo rm -f /etc/systemd/system/fserror-monitor.service
sudo rm -f /etc/systemd/system/fserror-health.service
sudo rm -f /etc/systemd/system/fserror-health.timer
sudo rm -f /usr/local/sbin/fserror-monitor
sudo rm -rf /etc/systemd/journald@fserror.conf.d
sudo rm -rf /var/lib/fserror-lab
sudo rm -rf /opt/fserror-lab
sudo rmdir /srv/fserror-lab 2>/dev/null || true
sudo userdel fserror-mon 2>/dev/null || true
sudo systemctl daemon-reload
sudo systemctl reset-failed
Namespace-ul persistent poate păstra loguri sub /var/log/journal. Nu șterge manual directoare journal după nume presupuse; verifică mai întâi cu journalctl --namespace=fserror --disk-usage și aplică politica operațională a sistemului.
Lista de acceptanță pentru monitorul FAN_FS_ERROR
- ☐ Kernelul activ este 7.2.x sau o versiune ulterioară validată.
- ☐
/srv/fserror-labeste ext4, read-only și provine din/dev/mapper/fserror-lab. - ☐
dmsetup table fserror-labarată targetullinearînainte și după test. - ☐ Binarul este deținut de root și are modul
0755. - ☐ Serviciul rulează ca
fserror-mon, nu ca root. - ☐ Journalul namespace-ului conține mesajul
READY. - ☐ Testul controlat produce
Input/output errorîndd. - ☐ Același test produce un eveniment
FS_ERRORcuerrno=5. - ☐ Citirea directă reușește după restaurarea tabelei.
- ☐
fserror-health.timereste activ și emiteHEALTH status=ok.
Trei extinderi care păstrează semnalul verificabil
Prima extindere este înlocuirea mesajului text cu un obiect JSON scris de monitor, păstrând câmpurile errno, error_count, fsid și handle-ul. Un colector local poate valida schema înainte de expediere.
A doua este asocierea file handle-ului cu inventarul intern al volumului. open_by_handle_at() cere privilegii și poate eșua dacă obiectul nu mai există, deci rezoluția trebuie tratată drept informație suplimentară, nu condiție pentru înregistrarea erorii.
A treia este un test separat pe XFS, folosind altă imagine și același consumator fanotify. Nu amesteca rezultatele ext4 și XFS: sistemele de fișiere nu raportează neapărat aceleași clase de erori din aceleași locuri.
Întrebări frecvente
Monitorul confirmă că o scriere sau o citire a reușit?
Nu. Aplicația trebuie să verifice rezultatul propriei operații. FAN_FS_ERROR notifică monitorul că filesystemul a observat o problemă, fără să înlocuiască semantica apelului I/O.
De ce nu folosim doar dmesg sau expresii regulate peste journalul kernelului?
Mesajele text diferă între subsisteme și versiuni. fanotify oferă un eveniment structurat cu eroare, contor și, când este disponibil, un identificator FID.
Poate serviciul să ruleze fără CAP_SYS_ADMIN?
Nu în configurația acestui proiect. Marca la nivel de filesystem și raportarea FID cer privilegiul respectiv, motiv pentru care capabilitatea este acordată unui serviciu dedicat și delimitată prin systemd.
Testul poate afecta discul sistemului?
Scriptul operează numai asupra mapperului fserror-lab, creat peste o imagine de 512 MiB, și refuză o tabelă neașteptată. Totuși, laboratorul trebuie executat într-o mașină de test, iar numele țintei nu trebuie schimbat către un volum real.

















































