Как прочитать [неблокирующий] файловый дескриптор файла, к которому добавлен (также известный как tail -f)?

На самом деле я использую libev; но под капотом это использует epoll (я только на linux). Когда я добавляю наблюдателя для чтения файла, и все данные были прочитаны, я получаю обратный вызов, что есть данные для чтения, но read(2) возвращает 0 (EOF). В этот момент я должен остановить наблюдателя, иначе он будет продолжать говорить мне, что есть что почитать. Однако, если я остановлю наблюдателя, а затем какой-то другой процесс добавит данные в этот файл, я никогда его не увижу.

Каков правильный способ получить уведомление о том, что в файле есть дополнительные/добавленные данные, которые можно прочитать, когда я уже прочитал до конца?

Я бы предпочел ответ с точки зрения libev, но более низкий уровень тоже подойдет (тогда я, вероятно, смогу перевести это на то, как это сделать с libev).


person Carlo Wood    schedule 25.07.2018    source источник
comment
Срабатывание по фронту или по уровню? В любом случае, похоже, что вы можете использовать inotify для мониторинга файла на наличие изменений, прежде чем выдавать другое чтение.   -  person jxh    schedule 26.07.2018
comment
libev поддерживает только запуск по уровню; но да, я в настоящее время изучаю возможность добавления inotify fd.   -  person Carlo Wood    schedule 26.07.2018


Ответы (1)


По какой-то причине очень часто люди думают, что создание неблокирующего fd или вызов poll/select/.. имеет другое поведение для файлов по сравнению с другими типами описаний файлов, но неблокирующее поведение и поведение готовности к вводу/выводу по сути, то же самое для всех типов описаний файлов: ядро ​​​​немедленно вернется из чтения/записи и т. д., если результат известен, и в этом случае будет сигнализировать о готовности ввода-вывода. Когда сокет имеет условие EOF, select будет сигнализировать о том, что сокет готов к чтению, и вы получите 0 (для EOF). То же самое происходит с файлами — если вы находитесь в конце файла, ядро ​​​​немедленно вернется из чтения и вернет 0, чтобы сигнализировать EOF.

Важным отличием является то, что файлы могут изменять содержимое в случайных местах и ​​могут быть расширены. Каналы и сокеты не являются произвольным доступом и не могут быть добавлены после закрытия. Таким образом, несмотря на то, что поведение является последовательным, часто это не то, что требуется, а именно ожидание изменения файла каким-либо образом.

Конфликт в умах многих людей заключается просто в том, что они хотят, чтобы им сообщали «когда появятся новые данные», но если вы немного подумаете об этом, вы поймете, что простое пробуждение не будет адекватным интерфейсом для этого, поскольку вы у вас нет возможности узнать, почему вы проснулись и что изменилось.

POSIX не имеет интерфейса для этого, кроме регулярного опроса fd или файла (и в случае случайных изменений, регулярного чтения всего файла!). Некоторые операционные системы имеют интерфейс для выполнения чего-то подобного (kqueue в BSD, inotify в GNU/Linux), но они также не идеально подходят друг другу (например, inotify не может просматривать fd). для изменений он будет отслеживать путь для изменений).

Самое близкое, что вы можете сделать с libev, это использовать ev_stat наблюдателя. Он ведет себя так, как если бы вы регулярно stat() устанавливали путь и вызывали обратный вызов наблюдателя при каждом изменении данных статистики. При переносе он делает именно это: он регулярно вызывает stat, но в некоторых операционных системах (в настоящее время только inotify в GNU/Linux, так как kqueue не имеет правильной семантики для этого) он может использовать другие механизмы для ускорения этого в некоторых случаях, хотя он будет возвращаться к обычному stat опросу везде, например, когда файл находится в сетевой файловой системе, где inotify не может видеть удаленные изменения.

Чтобы ответить на ваш вопрос: если у вас есть путь, вы можете использовать наблюдателя ev_stat для отслеживания изменений статистических данных, таких как изменения размера / mtime и т. д. Сделать это правильно может быть немного сложно (см. документацию libev, особенно часть о разрешении времени статистики: http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#code_ev_stat_code_did_the_file_attri), и вы должны помните, что при этом отслеживается путь, а не файловый дескриптор, поэтому вам может потребоваться регулярно сравнивать устройство/инод вашего файлового дескриптора и отслеживаемый путь, чтобы убедиться, что вы все еще открыть нужный файл.

Это по-прежнему не говорит вам, какая часть файла изменилась.

В качестве альтернативы, поскольку вы, по-видимому, хотите только читать добавленные данные, вы можете просто регулярно read() файл (в обратном вызове ev_timer) и избавиться от всех сложностей и проблем настройки ev_stat наблюдателя (не забывая также сравнить путь stat с вашими статданными fd, чтобы увидеть, открыт ли у вас еще нужный файл, в зависимости от того, может ли файл, который вы читаете, быть переименован или заменен.Иногда программы также обрезают файлы, что вы также можете обнаружить, увидев уменьшение размера между stat звонки).

По сути, это то, что делают старые реализации tail -f, в то время как новые могут, например, получать подсказки (только) от inotify, как это делают ev_stat наблюдатели.

Все это непросто, и детали зависят от ваших знаний о том, как именно изменяется файл, но это лучшее, что вы можете сделать.

person Remember Monica    schedule 25.07.2018
comment
Спасибо за развернутый и очень полный ответ. Я подозревал, что inotify был (единственным) способом, потому что опрос через регулярные промежутки времени для меня не вариант; Я пишу высокопроизводительную библиотеку, управляемую событиями, и опросы просто табу. Причина, по которой я столкнулся с этим, заключается в том, что библиотека работает по принципу «запусти и забудь», в то время как реальное действие выполняется в пуле потоков. Так что теперь может случиться так, что в одном потоке вы открываете файл, записываете в него, а затем открываете другой файл и читаете из предыдущего файла, а затем говорите библиотеке очистить, как только все «сделано». - person Carlo Wood; 26.07.2018
comment
Однако, поскольку все обрабатывается асинхронно, чтение может начаться еще до того, как вы начнете писать, или, по крайней мере, прочитать до конца, прежде чем вы закончите писать. Поэтому для таких случаев мне нужен входной файл, который будет ждать дополнительных данных (inotify), пока не произойдет определенное пользователем событие, которое сообщит ему, что он может прекратить чтение. Это не самая важная часть библиотеки, но я просто хочу ее поддержать ;). - person Carlo Wood; 26.07.2018
comment
Я изменил заголовок, добавив [неблокирующий] в квадратных скобках. Спасибо. - person Carlo Wood; 26.07.2018
comment
Конечно, имейте в виду, однако, что inotify является асинхронным и не может сказать вам, какова текущая ситуация, и что обычно он видит только локальные модификации и доступы, поэтому периодический опрос может быть разумным. Если вы знаете, что это не проблема в вашей среде, то, конечно, вы можете полностью положиться на inotify. - person Remember Monica; 27.07.2018