Я работаю системным программистом в компании КриптоПро. Нередко мои задачи связаны с ошибками, которые лежат на самом нижнем уровне современных операционных систем, под которые мы пишем ПО. Я хочу поведать тебе, Хабр, об одной из таких ошибок и о том, как я жаловался на неё разработчикам.
Я отвечаю за поддержку одной из наших библиотек с C-интерфейсом, написанной на C и C++. Мой коллега из другого отдела сообщил, что его нагрузочный тест нашей библиотеки на C# в Linux выдаёт ошибку в хитром сценарии: нужно иметь два процесса по пять потоков, делающих некоторые идентичные вызовы. Если процесс один, а потоков много, то проблема не проявляется. Если процессов два, но в каждом по одному потоку, то проблема не проявляется. Путём просмотра исходников нагрузочного теста и логов работы библиотеки удалось перенести проблему в маленький юнит-тест на C++ с использованием нашего API.
Вскрылось очень интересное. Ошибку возвращала функция lockf, отвечающая за взятие общесистемной блокировки на открытый файл. Функция в общих чертах работает так: если один процесс заблокировал файл с помощью lockf, то другие процессы при попытке заблокировать его с помощью lockf ждут его освобождения первым процессом. Все остальные операции с файлом по-прежнему доступны другим процессам, то есть эта блокировка не навязывается процессам, которые не пытались взять блокировку явным вызовом lockf.
errno было равно 35, что означало код EDEADLK с описанием «Resource deadlock avoided». Я полез в маны.
Allocating a system resource would have resulted in a deadlock situation. The system does not guarantee that it will notice all such situations. This error means you got lucky and the system noticed; it might just hang.
Система говорит нам: если она сделает то, что мы попросили, то произойдёт взаимная блокировка, система этого не хочет, поэтому просто вернёт ошибку. Но что это значило в моём случае?
Для воспроизведения ошибки требовалось минимум два процесса минимум по два потока в каждом. Первый поток первого процесса успешно взял лок на файл А. Первый поток второго процесса успешно взял лок на файл Б. Второй поток первого процесса хотел взять лок на файл Б, но не смог – лок принадлежит второму процессу. Он ждёт освобождения лока на файл Б. Второй поток второго процесса хотел взять лок на файл А, но не смог – лок принадлежит первому процессу. Ожидания освобождения не будет, lockf немедленно вернёт ошибку, ведь с точки зрения детектора взаимных блокировок в ядре Linux настал момент бить в набат!
На первый взгляд, тут как будто бы происходит хрестоматийный deadlock из учебников по параллельному программированию. Или всё же нет? Предположим, второй поток второго процесса стал бы ждать освобождения лока на файл А. Встали бы намертво все четыре потока обоих процессов? Почти наверняка нет. Обычно планирование потоков и процессов происходит так, что каждый из них получит управление через какое-то время. Тогда первый поток первого процесса мог бы продолжить исполнение и отпустить лок на файл А, тем самым дав возможность взять лок второму потоку второго процесса. Или же первый поток второго процесса продолжит исполнение, отпустит лок на файл Б, тем самым дав возможность взять лок второму потоку первого процесса. На уровне процессов взаимная блокировка как будто бы есть, а на уровне потоков её нет.
Такое поведение меня удивило. Я написал маленький юнит-тест на C с использованием только libc, и он подтвердил, что почти наверняка изначальная ошибка вызывалась ровно этим. Код представлен ниже. Компиляция: gcc -o edeadlk ./edeadlk.c -lpthread. Запуск: ./edeadlk a b в первом эмуляторе терминала, ./edeadlk a b во втором эмуляторе терминала.
Исходный код
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<errno.h>
#include<pthread.h>
#include<stddef.h>
#include<stdint.h>
#define DIE(x)\
{\
fprintf(stderr, "Assertion failed: " #x " file: %s, line:%d, errno:%d ", __FILE__, __LINE__, errno); \
perror(". Error:");\
fflush(stdout);\
abort();\
}
#define ASS(x) if (!(x)) DIE(x)
#define ASS1(x) ASS((x) != -1)
#define ASS0(x) ASS((x) == 0)
void * deadlocker(void *arg)
{
int fd = (int)(ptrdiff_t)arg;
for (;;) {
ASS1( lockf(fd, F_LOCK, 1) );
ASS1( lockf(fd, F_ULOCK, 1) );
}
return NULL;
}
int main(int argc, char * argv[])
{
int fd1, fd2;
ASS( argc >= 3 );
ASS1( fd1 = creat(argv[1], 0660) );
ASS1( fd2 = creat(argv[2], 0660) );
void * thrv;
pthread_t thr1, thr2;
ASS0( pthread_create(&thr1, NULL, deadlocker, (void *)(ptrdiff_t)fd2) );
ASS0( pthread_create(&thr2, NULL, deadlocker, (void *)(ptrdiff_t)fd1) );
ASS0( pthread_join(thr1, &thrv) );
ASS0( pthread_join(thr2, &thrv) );
return 0;
}
Я погрузился в документацию. С точки зрения libc, функция lockf работает на уровне процессов («Locks are associated with processes»), она ничего не знает про потоки, которые могут разрешить какие-то противоречия. Если процесс 1 взял лок А и ждёт лок Б, а процесс 2 наоборот, то тормозим немедленно.
The specified region is being locked by another process. But that process is waiting to lock a region which the current process has locked, so waiting for the lock would result in deadlock. The system does not guarantee that it will detect all such conditions, but it lets you know if it notices one.
POSIX тоже утверждает, что поведение своего рода правильное:
A potential for deadlock occurs if the threads of a process controlling a locked section are blocked by accessing a locked section of another process. If the system detects that deadlock would occur, lockf() shall fail with an [EDEADLK] error.
strace не показывал мне вызовов lockf, потому что lockf в glibc реализована через fcntl: «On Linux, lockf() is just an interface on top of fcntl(2) locking». Я нашёл также такие слова:
When placing locks with F_SETLKW, the kernel detects deadlocks, whereby two or more processes have their lock requests mutually blocked by locks held by the other processes. For example, suppose process A holds a write lock on byte 100 of a file, and process B holds a write lock on byte 200. If each process then attempts to lock the byte already locked by the other process using F_SETLKW, then, without deadlock detection, both processes would remain blocked indefinitely. When the kernel detects such deadlocks, it causes one of the blocking lock requests to immediately fail with the error EDEADLK; an application that encounters such an error should release some of its locks to allow other applications to proceed before attempting regain the locks that it requires. Circular deadlocks involving more than two processes are also detected. Note, however, that there are limitations to the kernel's deadlock-detection algorithm; see BUGS.
Так-так-так, а что там?
The deadlock-detection algorithm employed by the kernel when dealing with F_SETLKW requests can yield both false negatives (failures to detect deadlocks, leaving a set of deadlocked processes blocked indefinitely) and false positives (EDEADLK errors when there is no deadlock). For example, the kernel limits the lock depth of its dependency search to 10 steps, meaning that circular deadlock chains that exceed that size will not be detected. In addition, the kernel may falsely indicate a deadlock when two or more processes created using the clone(2) CLONE_FILES flag place locks that appear (to the kernel) to conflict.
Нехорошо. Похоже, тут мы наступили на грабли, и теперь требовалось как-то преодолеть найденный фатальный недостаток lockf. Я прочитал про целый вагон разных примитивов синхронизации в *nix, и самым надёжным решением в моей конкретной ситуации мне показалось использование очень похожей функции flock вместо lockf. Она не совсем идентична lockf, между ними есть тонкие различия, но кажется, они в целом схожи. К счастью, flock не имеет глубоко под капотом такой хитрой логики по детектированию взаимных блокировок: «flock() does not detect deadlock». Я проверил, что эти изменения лечат маленький юнит-тест на C для libc, маленький юнит-тест на C++ для нашей библиотеки, нагрузочный тест на C# для нашей библиотеки, закоммитился, проверил ночные тесты и обрадовался.
Казалось бы, тут и сказочке конец, но я же хороший парень, надо же пожаловаться на проблему мейнтейнерам Linux! Для выполнения моей рабочей задачи этого не требовалось, но хотелось получить подтверждение результатов моего исследования от авторитетных людей, а ещё сделать доброе дело.
Хорошо, первым делом наверное пойдём в баг-трекер ядра Linux. Что мы там видим?
Please use your distribution's bug tracking tools
This bugzilla is for reporting bugs against upstream Linux kernels.
If you did not compile your own kernel from scratch, you are probably in the wrong place.
Please use the following links to report a bug to your distribution instead:
Ubuntu | Fedora | Arch | Mint | Debian | Red Hat | OpenSUSE | SUSE
Моя проблема воспроизводилась на ряде старых ядер и в разных дистрибутивах, и я был на 99% уверен, что вижу её в коде последней ревизии. Ядро из исходников я никогда не собирал, ради такого повода даже попробовал это сделать, но за 20 минут не смог и потерял мотивацию. Окей…
Пожаловался в баг-трекер Ubuntu. За неделю никакого ответа не получил. Окей…
Пожаловался в рассылку LKML, главную рассылку разработчиков ядра Linux. За неделю никакого ответа не получил. Окей…
Случайно наткнулся на файл MAINTAINERS в исходниках ядра, который говорил, что жаловаться надо не в LKML, а в более узкоспециализированную рассылку. У меня была жалоба на функцию lockf и на код в файле fs/locks.c. Посмотрим.
FILE LOCKING (flock() and fcntl()/lockf())
M: Jeff Layton jlayton@kernel.org
L: linux-fsdevel@vger.kernel.org
S: Maintained
F: fs/fcntl.c
F: fs/locks.c
F: include/linux/fcntl.h
F: include/uapi/linux/fcntl.h
Пожаловался в рассылку linux-fsdevel. За неделю никакого ответа не получил. Окей…
Если честно, мне это уже порядком поднадоело. Жалуешься-жалуешься, на блюдечке подносишь юнит-тест, умоляешь сказать хотя бы, баг это или такая особенная фича, правильно ли я всё понял, а в ответ просто гробовая тишина. Печалит это всё. Однако я уже столько времени и сил вложил в сие предприятие, что хотелось довести его до какого-то ощутимого результата. И я пошёл ещё дальше.
Я набрался наглости и написал лично человеку, который отвечал за этот участок кода – Jeff Layton. И оказалось, что так надо было поступить с самого начала!! Всего через пару часов Джефф очень дружелюбно, конструктивно и подробно ответил на все мои вопросы, и я наконец успокоился. Наша переписка проходила всё в той же рассылке linux-fsdevel. По словам моего собеседника, в Linux:
поведение lockf (и fcntl с POSIX-локами) кривое, но менять его не будут из соображений обратной совместимости
использовать flock или OFD locks вместо lockf – нормальная идея
хорошо бы поправить man-страницу, где подчеркнуть, что lockf вообще не стоит использовать в многопоточных программах
Для реализации последнего пункта во мне уже не хватило любви к Open Source, и я остановился.
А потом опять замотивировался и написал этот пост.
Комментарии (26)
tmk826
27.03.2022 02:53+5Файл MAINTAINERS для того и существует чтобы знать кому посылать. Там ёще есть скрипт 'scripts/get_maintainer.pl' который показывает кто отвечает за какой участок кода. Надо послать человеку помеченному как М и в лист. Разработчики ядра получают сотни майлов в день. Многие из них, как Джефф (кстати отличный парень) зарегистрированы во многих листах. Обычно надо подождать две недели и написать снова.
posthedgehog Автор
27.03.2022 09:38+2Спасибо за пояснение!
Возможно, я плохо искал, но где можно найти информацию о том, куда/кому жаловаться на баги и где/кому задавать вопросы? Или это передаётся из уст в уста, как сейчас? Или нужно логическим путём понять, что жалоба на баг или вопрос -- это как недопатч, и потому если с патчами надо обращаться так-то, то и с репортами/вопросами нужно обращаться аналогично?
Из описания в MAINTAINERS следует:
Descriptions of section entries and preferred order
...
B: URI for where to file bugs. A web-page with detailed bug
filing info, a direct bug tracker link, or a mailto: URI.
C: URI for chat protocol, server and channel where developers
usually hang out, for example irc://server/channel.Но в секции "FILE LOCKING (flock() and fcntl()/lockf())" нет ни B:, ни C:.
tmk826
27.03.2022 11:28+5Есть документ как сообщать об ошибках:
https://github.com/torvalds/linux/blob/master/Documentation/admin-guide/reporting-issues.rst
Но о нём, конечно, тоже заранее откуда-то знать.
slonopotamus
27.03.2022 12:36-4Итого, вы долбили письмами кучу людей при том что никакого бага нет, а есть специфицированное POSIX поведение. Что вам в результате и подтвердили.
vanxant
27.03.2022 19:46+3Ну как минимум было бы неплохо пропатчить документацию.
slonopotamus
27.03.2022 20:04-2Пропачить в какую сторону? Документация документирует то что принято в стандарте POSIX. Если автор недоволен принятым стандартом, он имеет полное право пойти и попытаться протолкнуть изменение в следующей версии. Очевидно что его никто не примет, потому что это ломает обратную совместимость, и максимум на что можно надеяться - это добавление новой функции, семантика которой учитывает существование потоков. Но ни багтрекер убунты, ни письма в личку товарищу Jeff Layton не являются хоть сколько-нибудь работающим способом внесения изменений в POSIX.
vanxant
27.03.2022 22:24+4В документации нужно прямо отобразить, что данная функция не работает с многопоточкой. Чтобы там вначале большими буквами было написано НЕ ИСПОЛЬЗОВАТЬ С МНОГОПОТОЧКОЙ, ЕСЛИ У ВАС МНОГОПОТОЧКА СМ ...
Собственно, автору статьи мейнтейнер именно это и сказал - пропатчи, мол, друг, соответствующий man page.
posthedgehog Автор
27.03.2022 22:30+5С вашего позволения тезисно перескажу пост, чтобы более чётко артикулировать свою точку зрения.
Согласно POSIX, функция lockf должна возвращать ошибку, когда система замечает дедлок.
Linux реализует такой детектор дедлоков. И в документации, и в коде написано, что он может давать как false positive-ы, так и false negative-ы.
Я обнаружил случай, строго говоря, ещё одного false positive-а со стороны детектора дедлоков, не упомянутый в документации. Он касался распространённого сценария использования потоков в коде.
Я пожаловался на этот случай мейнтейнерам соответствующего кода.
У меня возникла смелая мысль, что раз детектор обладает такими интересными свойствами, то было бы хорошо по крайней мере поправить man, чтобы сказать: он плохо дружит с тредами.
На правку POSIX не претендую. Он здесь вообще почти не при делах.
Надеюсь, я ответил на ваш вопрос.
amarao
27.03.2022 12:41+6Багтрекер убунты мёртв. Вероятность, что они отреагируют на баг стремится к нулю. Багтрекер дебиана - живее всех живых.
Я недавно репортил баг в netdev (простыня из altnames ломала netlink), мне ответили в течение нескольких дней, причём патчем.
А в целом, багрепорт в opensource (даже с unit-тестом для гарантированого воспроизведения) - это пол-дела. Хороший багрепорт идёт с патчем.
apro
27.03.2022 18:44+1Багтрекер дебиана - живее всех живых
Сарказм? У них же вроде нет багтрекера, только список рассылки?
amarao
27.03.2022 19:09apro
27.03.2022 22:56По указанной ссылке как раз написано что чтобы отправить баг или добавить информацию к уже существующему багу нужно отправить email или напрямую или через спец. программу которая вызовет почтовый клиент с правильным шаблоном и больше способов общения не предусмотрено. Или я что-то упустил?
amarao
28.03.2022 14:54+2Да, отправка бага через почту или через reportbug (через ту же почту). Но баг-трекер-то от этого не исчезает. У багов есть номера, статус, трекинг по версиям и т.д.
raandom
28.03.2022 11:26+1В 2020-м в трекере Убунты мне ответили за два дня: https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1875667
Сейчас на глаз там такая же скорость ответов:
https://bugs.launchpad.net/ubuntu/+source/linux/+bugs?field.searchtext=&field.status%3Alist=CONFIRMED
Ради интереса закину репорт на ошибку в ядре 4.9 в Debian, чтобы сравнить скорость.
sena
28.03.2022 06:58+1У меня отвалился звук через hdmi на интеловской видеокарте под Дебианом. Последнее работающее ядро 4.19, а 5+ уже не работает. Куда и как лучше слать репорт? В Дебиан послал в августе 2021, ни ответа, ни привета.
edo1h
28.03.2022 07:36+2а что за known solution? ИМХО странно упоминать что есть решение, но не давать ссылку
sena
28.03.2022 12:12Это опечатка, имелось в виду что нет известных решений.
BD9
28.03.2022 22:01+1Решений много разных: duckduckgo.com/?q=Intel+Xeon+E3-1200+linux+hdmi+audio.
Необязательно ограничиваться одной сборкой Linux.
Главный по звуку в Linux есть Takashi Iwai github.com/tiwai, работает в SUSE. Ставите/грузите LiveUSB openSUSE и (если будет ошибка) можете получить ответ от него в bugzilla.opensuse.org.
LevOrdabesov
29.03.2022 11:03+3За статью спасибо, но читать не разделённую ничем смесь разных языков неудобно даже чисто эстетически.
Хоть форматирование сделайте на выдержки из документации на английском, в виде цитат м.б…posthedgehog Автор
29.03.2022 12:07+2Отличная идея! Благодарю, переделал. Кажется, стало намного лучше.
samoreklam
А я год назад, всем инфу разослал о проблеме в модуле сетевого устройства.
Написал стать на хабре, с описанием проблем.
Спустя год, ничего не изменилось. Как проблема была, так и осталась.
fshp
Вы наверное забыли патч приложить.
samoreklam
Так и было)