Волей-неволей, я оказался в другой стране. Февральский Тбилиси встретил ветрами, доброжелательными людьми, безденежьем и необходимостью (после 6-летнего перерыва) в очередной раз вкатываться в IT. И я подписался админить первый подвернувшийся проект.
Основой проекта был сбор данных, который состоял из двух частей. Сначала куча бинарников сливала данные в один текстовый файл, который в процессе распухал до сотен гигабайт. Время от времени аналитик, (когда ему была необходима очередная порция данных, запускал его на разборку, скармливая его ещё одному бинарнику (назовём его "parser"). Случалось это иногда раз в неделю, а иногда и раз в месяц.
Сама система была разработана давно уволившемся программистом и, на удивление, уже лет 5+ работала без обслуживания и, при этом, без сбоев. Картину дополняли бардак в документации (если её таковой можно было назвать) и полное отсутствие исходников парсера. Но для админских задач они, как бы, не требовались. До поры.
Уже в конце первой недели от аналитика пришла просьба: "Файл очень большой, разбирается иногда пару дней, а работает парсер абсолютно молчаливо, ничего не выдавая на экран, и, поэтому, нельзя ли отобразить процент выполнения. Желательно видный через progress, так как через него отслеживаются и другие задачи".
Упомянутая выше документация заканчивалась одной интересной строчкой: "парсинг переведён в однопоточный режим из-за "гонок"". Из этой, не до конца понятной фразы, я вынес для себя, что раз парсинг идёт однопоточно, то нет никаких сложностей извлечь данные из "/proc". Поэтому: "Да без проблем," — ответил я."
Парсер запущен, данные из файла успешно разлетаются по табличкам, и мы сначала глянем PID процесса читающего файл (пусть это будет "/tmp/testfile.txt"").
$ lsof /tmp/testfile.txt
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
parser 123456 root 3r REG 8,3 6888896 111222 /tmp/testfile.txt
Не обращайте внимание на размер — это просто тестовый файл.
Пид известен, сейчас посмотрим смещение, завернём это всё в пятистрочный скриптик и дело с концом, но:
$ progress -p 123456 -m
[123456] parser /tmp/testfile.txt
0.0% (0 / 6.6 MiB) 0/s
Позиция в файле вообще не меняется. Скрипт успешно разбирает весь файл до конца, но в /proc всё по нулям:
$ cat /proc/123456/fdinfo/3
pos: 0
flags: 0100000
mnt_id: 63
ino: 111222
Озабоченно почесав затылок я запустил gdb в надежде увидеть прогресс в отладчике, здесь
3 — это наш файловый дескриптор
1 — SEEK_CUR (указатель смещения)
$ gdb -p 123456
(gdb) p lseek(3, 0, 1)
$1 = 0
Но он также был нулевой — указатель не продвигается дальше начала файла. Действительно, никто же не обещал, что для чтения используется системный вызов read, который в отличие от fread, pread, preadv и прочих ожидаемо изменяет позицию смещения.
Хорошо бы убедиться глянув код, однако исходников нет, но попробуем сделать хотя бы трассировку:
$ ./bin/parser & strace -p $!
[2] 123456
strace: Process 123456 attached
restart_syscall(<... resuming interrupted read ...>) = 0
pread64(3, "4\n12775\n12776\n12777\n12778\n12779\n"..., 65536, 65536) = 65536
openat(AT_FDCWD, "/proc/123456/fdinfo/3", O_RDONLY) = 4
newfstatat(4, "", {st_mode=S_IFREG|0444, st_size=0, ...}, AT_EMPTY_PATH) = 0
pread64(3, "697\n23698\n23699\n23700\n23701\n2370"..., 65536, 131072) = 65536
openat(AT_FDCWD, "/proc/123456/fdinfo/3", O_RDONLY) = 4
newfstatat(4, "", {st_mode=S_IFREG|0444, st_size=0, ...}, AT_EMPTY_PATH) = 0
pread64(3, "34620\n34621\n34622\n34623\n34624\n34"..., 65536, 196608) = 65536
openat(AT_FDCWD, "/proc/123456/fdinfo/3", O_RDONLY) = 4
newfstatat(4, "", {st_mode=S_IFREG|0444, st_size=0, ...}, AT_EMPTY_PATH) = 0
detached
<detached ...>
Ага, файл читается с помощью pread64 (возможно, как наследие многопоточного режима), а когда используется данный системный вызов, смещение в файле не меняется, потому что pread64 выполняет чтение с определенной позиции, не изменяя текущий указатель файла.
Соответственно, смещение файла, в /proc/[pid]/fdinfo/[fd], остается неизменно нулевым и получить его с помощью progress (который как раз использует /proc) не представляется возможным. Или нет?
Последовательный рост позиции смещения дополнительно нас утвержадает в мысли, что чтение идёт однопоточно и, возможно, для наших целей было бы достаточно просто отфильтровать вывод strace:
$ ./bin/parser & strace -p $! 2>&1 | grep -F pread
[2] 123456
pread64(3, "4\n12775\n12776\n12777\n12778\n12779\n"..., 65536, 65536) = 65536
pread64(3, "697\n23698\n23699\n23700\n23701\n2370"..., 65536, 131072) = 65536
pread64(3, "34620\n34621\n34622\n34623\n34624\n34"..., 65536, 196608) = 65536
pread64(3, "2\n45543\n45544\n45545\n45546\n45547\n"..., 65536, 262144) = 65536
^C
, где уже видна текущая позиция.
Но мы заморочимся больше и получим тот же результат используя системный вызов ptrace, приостанавливая поток и извлекая данные смещения из регистра r10, со значения которого должно начаться чтение в файле. Это позволяет читать данные из любого места в файле, не изменяя текущую позицию указателя.
...
while (1) {
if (ptrace(PTRACE_SYSCALL, pid, NULL, NULL) == -1) {
perror("ptrace syscall");
return 1;
}
waitpid(pid, &status, 0);
if (WIFEXITED(status)) break;
if (ptrace(PTRACE_GETREGS, pid, NULL, ®s) == -1) {
perror("ptrace getregs");
return 1;
}
if (regs.orig_rax == SYS_pread64) {
long long fd = (long long)regs.rdi; // Файловый дескриптор
long long bytes_to_read = (long long)regs.rdx; // Количество байт для чтения
long long offset = (long long)regs.r10; // Искомое смещение
}
// Продолжаем процесс и ждём выхода из системного вызова
if (ptrace(PTRACE_SYSCALL, pid, NULL, NULL) == -1) {
perror("ptrace syscall");
return 1;
}
waitpid(pid, &status, 0);
if (WIFEXITED(status)) break;
// Захватываем результат после выполнения системного вызова
if (ptrace(PTRACE_GETREGS, pid, NULL, ®s) == -1) {
perror("ptrace getregs");
return 1;
}
if (regs.orig_rax == SYS_pread64) {
long long result = (long long)regs.rax;
if (result > 0) total_read += result;
}
}
...
$ ./bin/parser & ./bin/file_pos_ptrace $!
[1] 123456
pread64 called (before syscall)
FD: 3, Offset: 0, Bytes to read: 65536
pread64 returned, result: 65536, Progress: 0.95%
pread64 called (before syscall)
FD: 3, Offset: 65536, Bytes to read: 65536
pread64 returned, result: 65536, Progress: 1.90%
pread64 called (before syscall)
FD: 3, Offset: 131072, Bytes to read: 65536
pread64 returned, result: 65536, Progress: 2.85%
^C
Вроде проблема решена и прогресс обработки виден. Но что-то гложет. Ведь договаривались на мониторинг через progress.
pread64 для этих целей нас не устраивает, но почему бы нам не заменить его связкой lseek и read, самостоятельно устанавливая нужный offset. Ведь замещение функций, за мои годы вне IT, никто не отменял.
#define _GNU_SOURCE
#include <dlfcn.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset) {
static ssize_t (*real_pread)(int, void *, size_t, off_t) = NULL;
if (!real_pread) real_pread = dlsym(RTLD_NEXT, "pread");
// Установка смещения
if (lseek(fd, offset, SEEK_SET) == (off_t)-1) return -1;
// Чтение данных с использованием read
return read(fd, buf, count);
}
Создадим нашу библиотечку
$ gcc -shared -fPIC -o ./bin/libreplace_pread.so ./src/replace_pread.c -ldl
Замещаем оригинальный pread и
$ LD_PRELOAD=./bin/libreplace_pread.so ./bin/parser & progress -m -p $!
[123456] parser /tmp/testfile.txt
6.7% (448 KiB / 6.6 MiB) 64.0 KiB/s remaining 0:01:38
получаем желаемое.
В заключение
Возможный недостаток метода заключается в том, что связка lseek и read совершает два системных вызова вместо одного, что влечёт дополнительный overhead, и это, при больших объёмах, может привести большему времени работы. Но в данном случае, даже на синтетических тестах, разница была в рамках статистической погрешности. В реальной же работе, где основные задержки были связаны с записью в базы данных, это стало вообще несущественным.
PS1:
На этом месте должна была появиться совсем другая статья, но на четвёртый день написания я понял, что всё, о чем я пишу, уже придумано до меня. А полученный инвайт остался "неотработанным", поэтому этой байкой отдаю долги.
При этом по-прежнему негодую и призываю к ответу Boomburum: почему для написания первой статьи недоступно сохранение в черновики!?
PS2:
Благодарности:
Виктории aka Exosphere за инвайт
Георгию aka amarao за бесценные статьи о Linux и твёрдую позицию
ValdikSS за давнее знакомство с LD_PRELOAD и работающий YouTube