Что делать
Отлично. Advanced debugging technologies показывают, что растёт какое-то говно с поинтерами
окей, у меня бесконечно растёт очередь ивентов. Подозреваю, что я просто по-ебаному веду себя со списком
Бляяя пиздец. Я же просто помечаю таску как неактивную, так? Выставляю стейту флаг EV_INACTIVE. А весь рофл в том, что я-то исключительно указателем на таску оперирую. Таким образом, когда таска вновь становится активной, она может заменить собой какую-то совершенно другую. При этом, старая, неактивная, которую мы ещё не успели заменить другой - тоже остаётся. А флаг снимается у обеих. Точнее, у неё одной, просто её уже две штуки. И чем дальше, тем хуже
Вот. 84a8 - это серверный сокет, означает, что новые подключения пришлёпали. Две таски двух подключений - 8358 и 8400. read error 0 означает EOF, т.е. что клиент закрыл подключение. Соответственно, NULLING означает, что я добавляю таску в очередь на освобождение. Проблема в том, что в то время, как я обрабатываю таски (это не логгируется), мне между делом прилетает ещё раз 8400. Соответственно, как только я получаю EOF из 8400 в активном пуле, я заменяю её первой таской из новых тасок. Ею оказывается она же. Соответственно, получаем уже EPIPE (поскольку мы со своей стороны тоже успели уже подключение закрыть). Мы не обращаем внимание и снова пихаем её в очередь на очищение. Таким образом, получаем double free.
Каким образом, зачем, почему еполл мне отправляет дубликат ивента - я в душе блять не чаю, это полный пиздец какой-то. Наверное, придётся костылить, и при EPIPE просто скипать таску. Ёбаный в рот.
Каким образом, зачем, почему еполл мне отправляет дубликат ивента - я в душе блять не чаю, это полный пиздец какой-то. Наверное, придётся костылить, и при EPIPE просто скипать таску. Ёбаный в рот.
https://stackoverflow.com/questions/4724137/epoll-wait-receives-socket-closed-twice-read-recv-returns-0
А вот и мой кейс. Отлично, значит, когда я дочитался до EAGAIN, и в это время сокет отключается - мне может прилететь в догонку ещё один ивент, с EPOLLRDHUP. Надо будет потом потыкать, как оно происходит. Потому что пока что я не понимаю алгоритм, по которому принимается решение выдать мне ивент повторно
А вот и мой кейс. Отлично, значит, когда я дочитался до EAGAIN, и в это время сокет отключается - мне может прилететь в догонку ещё один ивент, с EPOLLRDHUP. Надо будет потом потыкать, как оно происходит. Потому что пока что я не понимаю алгоритм, по которому принимается решение выдать мне ивент повторно
Stack Overflow
epoll_wait() receives socket closed twice (read()/recv() returns 0)
We have an application that uses epoll to listen and process http-connections. Sometimes epoll_wait() receives close event on fd twice in a "row". Meaning: epoll_wait() returns connection fd on which
Что делать
https://stackoverflow.com/questions/4724137/epoll-wait-receives-socket-closed-twice-read-recv-returns-0 А вот и мой кейс. Отлично, значит, когда я дочитался до EAGAIN, и в это время сокет отключается - мне может прилететь в догонку ещё один ивент, с EPOLLRDHUP.…
Я так и не понял, какие должны быть причины на появление этого ивента. Но у меня ещё была пара багов помимо этого, из-за чего даже добавлять EPOLLRDHUP в список интересов и специально пропускать такие ивенты особо не помогло. Решил пойти по другому пути
Теперь я просто объявлю общий интерфейс в ev.h, с функцией ev_run(), принимающей все нужные аргументы. Сама имплементация будет одна из нескольких C source файлов, где каждый файл отображает целевую операционную систему. Естественно, каждый будет в соответствующих гардах, чтобы под выбранную целевую платформу компилировалось только то, что нужно. Что, в целом, классическая схема, хоть и, как мне кажется, не самая надёжная.
Из минусов - больше нет штуки с разделением источника ивентов и самой логики ивентлупа. Правда, минус это с очень большой натяжкой, так как при портировании на kqueue я всё равно вынесу парочку общих функций (а-ля обработай один этот ивент - логика абсолютно агностична к платформе и окружению), да и всё равно, рано или поздно, придётся расширяться и на винду. А там iocp, который следует уже модели проактора, в отличии от реактора, как epoll и kqueue. Я пока до конца не разобрался, но ивенты я там буду получать совершенно иным образом. Соответственно, вынести исключительно атомарную логику (а не целый пласт, как это было раньше, где я полностью инкапсулировал весь ивентлуп) является потенциально выигрышным решением.
Теперь я просто объявлю общий интерфейс в ev.h, с функцией ev_run(), принимающей все нужные аргументы. Сама имплементация будет одна из нескольких C source файлов, где каждый файл отображает целевую операционную систему. Естественно, каждый будет в соответствующих гардах, чтобы под выбранную целевую платформу компилировалось только то, что нужно. Что, в целом, классическая схема, хоть и, как мне кажется, не самая надёжная.
Из минусов - больше нет штуки с разделением источника ивентов и самой логики ивентлупа. Правда, минус это с очень большой натяжкой, так как при портировании на kqueue я всё равно вынесу парочку общих функций (а-ля обработай один этот ивент - логика абсолютно агностична к платформе и окружению), да и всё равно, рано или поздно, придётся расширяться и на винду. А там iocp, который следует уже модели проактора, в отличии от реактора, как epoll и kqueue. Я пока до конца не разобрался, но ивенты я там буду получать совершенно иным образом. Соответственно, вынести исключительно атомарную логику (а не целый пласт, как это было раньше, где я полностью инкапсулировал весь ивентлуп) является потенциально выигрышным решением.
👍1
Предыдущий дизайн, с инкапсуляцией всего ивентлупа как самостоятельной единицы, вообще давал мало обещаний. Потому что необходимо прямо в логике производить операции - закрыть подключение, например. Тогда, когда я начну использовать io_uring, я не смогу его утилизировать по полной. Я нашёл выход - сделал массив тасок, которых надо отключить, и так возвращал источнику ивентов (в данном случае - обёртке над еполлом). Это усложняет реализацию, потому что нужно дополнительно выход за границы массива контролировать, и, в случае чего, прерывать ивентлуп для того, чтобы вернуть этот массив. Здесь и был злоебучий баг с double-free, который я починить не мог, что и, в том числе, надеюсь починить за счёт переписывания с нуля
Хотя, кстати, я логику ивентлупа всё равно никак не могу отвязать от платформы. Например, даже просто обработать одну таску - это всё равно надо будет прочитать что-то из сокета, либо же записать. Под линуксом я могу в это дело впилить юринг всё тот же, что я уже на darwin/bsd-based никак не портирую. Делать это на препроцессоре не хочу, ещё какие-то ухищрения в рантайме тоже. Получается, копипаста остаётся единственным решением. Но вариант откровенно хуйня: тогда, чтобы что-то пофиксить и/или поменять здесь, мне придётся менять это сразу во всех реализациях под каждую поддерживаемую платформу или семейство платформ. А реализации и сами по себе могут качественно отличаться!
В общем, задачка интересная. Пока поддерживать буду только линукс, чтобы хотя бы внутреннее апи сформировалось, а тогда уж можно и думать, как решать вопрос с портируемостью
В общем, задачка интересная. Пока поддерживать буду только линукс, чтобы хотя бы внутреннее апи сформировалось, а тогда уж можно и думать, как решать вопрос с портируемостью
Что делать
Сука шизоид что ему не нравится, какие нахуй скобки
Ага. Понятно. У нас же
struct epoll_event выглядит следующим образом. Вот в data я сую свою task. А data у нас - юнион. Вот оно и просило, чтобы я более явно это всё дело инициализировал. Ну и хуйняКстати, между делом наткнулся на какой-то старый баг гсс. В 2014 они не умели в zero-initializer вида
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=53119
struct myStruct my_var = { 0 };. А ещё, чтобы собрать С компилятор, нужны плюсы🗿https://gcc.gnu.org/bugzilla/show_bug.cgi?id=53119
Я переписал с нуля ивентлуп. Получилось проще, красивее и удобнее, но проблемы остались всё те же - опять растут арены, опять список с кэшем ивентов, который не должен превышать 16 вхождений, бесконечно разрастается. Ну, то есть, старые баги вернулись, только теперь ситуация сука ещё хуже. Зато код красивее, да.
🏆1
Арены я починил, я просто забывал при ошибке из сокета возвращать указатель на буфер. А вот ошибка с вечно раздувающимся массивом кэша ивентов, которую я ещё в старой имплементации отдебажить так и не смог, осталась. Кажется, сейчас будет интересно
Но, кстати, я нашёл какое-то сообщение в рассылке линукса на тему edge-triggered режима еполла. Там упоминалось в том числе и то, что практически все всё равно используют кэш ивентов. Потому что, как известно, edge-triggered режим обозначает как раз то, что будут приходить оповещения о сокетах только тогда, когда их состояние меняется. В данном случае, состояний два - сокет полностью вычитан/буфер записи забит, и тут в нём появляется что-то новое/появляется достаточно места в буфере для записи. То есть, получив условно единожды ивент о том, что такой-то сокет доступен на чтение, до тех пор, пока ты не дочитаешь до ошибки EAGAIN (данных больше не осталось, read buffer is exhausted) - снова тебе этот ивент не прилетит.
Собственно, держать где-то рядышком структуру данных, где все доступные для какой-либо из операций сокеты, и называется кэшем ивентов. А сказано было, что только так обычно и достигают highly efficient I/O. Значит, я на правильном пути
Но, кстати, я нашёл какое-то сообщение в рассылке линукса на тему edge-triggered режима еполла. Там упоминалось в том числе и то, что практически все всё равно используют кэш ивентов. Потому что, как известно, edge-triggered режим обозначает как раз то, что будут приходить оповещения о сокетах только тогда, когда их состояние меняется. В данном случае, состояний два - сокет полностью вычитан/буфер записи забит, и тут в нём появляется что-то новое/появляется достаточно места в буфере для записи. То есть, получив условно единожды ивент о том, что такой-то сокет доступен на чтение, до тех пор, пока ты не дочитаешь до ошибки EAGAIN (данных больше не осталось, read buffer is exhausted) - снова тебе этот ивент не прилетит.
Собственно, держать где-то рядышком структуру данных, где все доступные для какой-либо из операций сокеты, и называется кэшем ивентов. А сказано было, что только так обычно и достигают highly efficient I/O. Значит, я на правильном пути
А ведь мне ещё ой сколько работы по оптимизациям лупа предстоит) Как минимум, думаю, можно будет сделать небольшой "шедулер" - если ивентов достаточно мало, мы можем себе позволить несколько раз пройтись по кэшу ивентов, перед тем, как снова обратиться к еполлу за новыми.
Ещё, кстати, заметка о том, как я сейчас работаю с ним. Я обычно дёргаю
Ещё, возможно, мне стоит ограничить размер кэша, чтобы не было такого, что я трачу где-то секунду на то, чтобы по нему только пройтись и обработать, и всё это время другие клиенты будут ждать. Ну, то есть, они и так будут ждать, просто с ограниченным размером кэша, распределение будет немного более честным, и позволит дать хоть немного воздуха большему количеству подключений
epoll_wait() с таймаутом в 0 миллисекунд, чтобы получить все новые мгновенно. Это нужно для того, чтобы я не уходил в ожидание новых ивентов в то время, как у меня сидят ждут обработки таски в кэше. Но если кэш пустой, то, дабы не растрачивать попусту циклы, я выставляю таймаут в -1, чтобы проснуться только тогда, когда новые ивенты возникнут. Собственно, минимизация количества вызовов epoll_wait() будет моей первой задачей в попытках оптимизировать что-то, потому что, помимо него, я делаю только сисколлы для чтения и записи, что я уже сильно-то не оптимизирую (io_uring временно отложим). Ещё, возможно, мне стоит ограничить размер кэша, чтобы не было такого, что я трачу где-то секунду на то, чтобы по нему только пройтись и обработать, и всё это время другие клиенты будут ждать. Ну, то есть, они и так будут ждать, просто с ограниченным размером кэша, распределение будет немного более честным, и позволит дать хоть немного воздуха большему количеству подключений
Что делать
Арены я починил, я просто забывал при ошибке из сокета возвращать указатель на буфер. А вот ошибка с вечно раздувающимся массивом кэша ивентов, которую я ещё в старой имплементации отдебажить так и не смог, осталась. Кажется, сейчас будет интересно Но, кстати…
Но у меня есть стойкое ощущение, что я могу получить новый ивент по сокету даже без того, чтобы дочитаться до EAGAIN. иначе я не могу объяснить, почему у меня постоянно новые ивенты появляются, при том, что я не успеваю получить ошибку
Что делать
Но у меня есть стойкое ощущение, что я могу получить новый ивент по сокету даже без того, чтобы дочитаться до EAGAIN. иначе я не могу объяснить, почему у меня постоянно новые ивенты появляются, при том, что я не успеваю получить ошибку
Если так, то я в жопе. А выглядит всё пока именно так
Что делать
Если так, то я в жопе. А выглядит всё пока именно так
Первые EAGAIN всплывают только после того, как я получаю дубликатный ивент. Пиздец.
Хотя, на самом деле, у меня есть два варианта. Первое - у меня поломанный еполл (учитывая, что у меня твикнутое ядро от cachyos, то допущение валидно). Второе - чтобы не дрочить epoll_ctl с EPOLL_CTL_MOD каждый раз, когда я сменяю состояние таски с READ на WRITE и наоборот, я выставил изначально интересы
Ну так вот. Подозреваю, что мне просто ивент прилетает дважды, один раз - по поводу чтения, второй раз - по поводу записи. Тогда у меня в кэше оказывается две таски - точнее, два указателя на одну и ту же таску. Таким образом, обрабатывая таски в кэше, я обрабатываю одну и ту же как минимум дважды. Первый раз получаю данные, второй раз получаю EAGAIN. Ещё хорошо, если других ивентов нет - тогда я просто сую вместо дубликатной таски нулл. Поскольку я словил EAGAIN, при втором проходе получаю снова эту же таску. И дубликат, который сейчас равен нуллу, я заменяю на "новый" ивент. То есть, совершенно тем же, что и было там до этого. Цикл повторяется.
А если добавить ещё парочку подключений, которые тоже активно обмениваются сообщениями - ситуация совсем уже распиздос. Тут и с одним-то подключением кэш будет расти, хоть и медленно, а с 16 одновременными подключениями он улетает в небеса. А я откровенно хуй знает, что с этим делать...
Хотя, на самом деле, у меня есть два варианта. Первое - у меня поломанный еполл (учитывая, что у меня твикнутое ядро от cachyos, то допущение валидно). Второе - чтобы не дрочить epoll_ctl с EPOLL_CTL_MOD каждый раз, когда я сменяю состояние таски с READ на WRITE и наоборот, я выставил изначально интересы
EPOLLIN | EPOLLOUT | EPOLLRDHUP (ну, ладно, последнее - как костыль, чтобы такие ивенты просто скипать, это еполл сам по себе такую хуйню творит - см. линк на СО от 1 августа)Ну так вот. Подозреваю, что мне просто ивент прилетает дважды, один раз - по поводу чтения, второй раз - по поводу записи. Тогда у меня в кэше оказывается две таски - точнее, два указателя на одну и ту же таску. Таким образом, обрабатывая таски в кэше, я обрабатываю одну и ту же как минимум дважды. Первый раз получаю данные, второй раз получаю EAGAIN. Ещё хорошо, если других ивентов нет - тогда я просто сую вместо дубликатной таски нулл. Поскольку я словил EAGAIN, при втором проходе получаю снова эту же таску. И дубликат, который сейчас равен нуллу, я заменяю на "новый" ивент. То есть, совершенно тем же, что и было там до этого. Цикл повторяется.
А если добавить ещё парочку подключений, которые тоже активно обмениваются сообщениями - ситуация совсем уже распиздос. Тут и с одним-то подключением кэш будет расти, хоть и медленно, а с 16 одновременными подключениями он улетает в небеса. А я откровенно хуй знает, что с этим делать...
Наверное, стоит всё же перестать носом воротить, и начать смотреть, как остальные кэш ивентов оборудовали. Потому что я, по всей видимости, охуею разбираться, почему еполл - настолько ущербный и поломанный. Нет, ну реально, он отправляет EPOLLRDHUP ивенты даже тогда, когда ты их и не просил вовсе! Так ладно это. Он это делает избирательно! Не всегда! Он, сука, НЕКОНСИСТЕНТНЫЙ!!!
Надеюсь, что это я его просто не осилил, но это всё ещё какой-то пиздес.
Надеюсь, что это я его просто не осилил, но это всё ещё какой-то пиздес.
https://idea.popcount.org/2017-03-20-epoll-is-fundamentally-broken-22/
продолжение охуенной серии статей. Здесь, конечно, не описывается моя проблема, а только то, что, несмотря на заявления в мане, удалять сокет из еполла перед close() необходимо. А теперь осталось понять, КАКИМ ОБРАЗОМ МНЕ ДУБЛИРУЮЩИЕСЯ ИВЕНТЫ ПРИЛЕТАЮТ
продолжение охуенной серии статей. Здесь, конечно, не описывается моя проблема, а только то, что, несмотря на заявления в мане, удалять сокет из еполла перед close() необходимо. А теперь осталось понять, КАКИМ ОБРАЗОМ МНЕ ДУБЛИРУЮЩИЕСЯ ИВЕНТЫ ПРИЛЕТАЮТ
idea.popcount.org
Epoll is fundamentally broken 2/2 —
Idea of the day
Idea of the day