Публикуя перевод первой статьи из цикла материалов о реализации
Функция
Объявление
В этом фрагменте кода функция
После этого
Если функции удалось выделить объём памяти, достаточный для
После этого
Структура
Сущность
Член
Для того чтобы облегчить восстановление в
Затем
После этого
Давайте рассмотрим пример, в котором используется реализация
Функция
Функция
А что представляет собой
Что же
В случае с
Сначала
После этого
После этого
А вот тут у меня возникает один вопрос. Почему
Я, правда, не могу точно ответить на этот вопрос. Насколько я могу судить, если только некто не собирается использовать экземпляры
Хорошо тут то, что неясности вокруг
Помните то, о чём мы говорили в предыдущем разделе? Речь шла о том, что
Полагаю, ещё стоит обратить внимание на то, что механизм возобновления работы процессов, применяемый в
Когда именно осуществляется возобновление работы
В
Когда же будут вызываться
На этом мы завершаем второй материал из цикла статей о реализации
Пользовались ли вы epoll?
epoll
, мы провели опрос, посвящённый целесообразности перевода продолжения цикла. Более 90% участников опроса высказались за перевод остальных статей. Поэтому сегодня мы публикуем перевод второго материала из этого цикла.Функция ep_insert()
Функция
ep_insert()
— это одна из самых важных функций в реализации epoll
. Понимание того, как она работает, чрезвычайно важно для того чтобы разобраться в том, как именно epoll
получает сведения о новых событиях от файлов, за которыми наблюдает.Объявление
ep_insert()
можно найти в строке 1267 файла fs/eventpoll.c
. Рассмотрим некоторые фрагменты кода этой функции:user_watches = atomic_long_read(&ep->user->epoll_watches);
if (unlikely(user_watches >= max_user_watches))
return -ENOSPC;
В этом фрагменте кода функция
ep_insert()
сначала проверяет, не превышает ли общее количество файлов, за которым наблюдает текущий пользователь, значения, заданного в /proc/sys/fs/epoll/max_user_watches
. Если user_watches >= max_user_watches
, то функция немедленно прекращает работу с errno
, установленным в ENOSPC
.После этого
ep_insert()
выделяет память, пользуясь механизмом управления памятью slab ядра Linux:if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
return -ENOMEM;
Если функции удалось выделить объём памяти, достаточный для
struct epitem
, будет выполнен следующий процесс инициализации:/* Инициализация ... */
INIT_LIST_HEAD(&epi->rdllink);
INIT_LIST_HEAD(&epi->fllink);
INIT_LIST_HEAD(&epi->pwqlist);
epi->ep = ep;
ep_set_ffd(&epi->ffd, tfile, fd);
epi->event = *event;
epi->nwait = 0;
epi->next = EP_UNACTIVE_PTR;
После этого
ep_insert()
попытается зарегистрировать коллбэк в файловом дескрипторе. Но прежде чем мы сможем об этом поговорить, нам нужно познакомиться с некоторыми важными структурами данных.Структура
poll_table
— это важная сущность, используемая реализацией poll()
VFS. (Понимаю, что в этом можно запутаться, но тут мне хотелось бы объяснить, что функция poll()
, которую я тут упомянул, представляет собой реализацию файловой операции poll()
, а не системный вызов poll()
). Она объявлена в include/linux/poll.h
:typedef struct poll_table_struct {
poll_queue_proc _qproc;
unsigned long _key;
} poll_table;
Сущность
poll_queue_proc
представляет тип функции-коллбэка, который выглядит так:typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);
Член
_key
таблицы poll_table
, на самом деле, является не тем, чем он может показаться на первый взгляд. А именно, несмотря на имя, наводящее на мысль о некоем «ключе», в _key
, на самом деле, хранятся маски интересующих нас событий. В реализации epoll
_key
устанавливается в ~0
(дополнение до 0). Это значит, что epoll
стремится получать сведения о событиях любых видов. В этом есть смысл, так как приложения пользовательского пространства могут в любое время менять маску событий с помощью epoll_ctl()
, принимая все события от VFS, а затем фильтруя их в реализации epoll
, что упрощает работу.Для того чтобы облегчить восстановление в
poll_queue_proc
исходной структуры epitem
, epoll
использует простую структуру, называемую ep_pqueue
, которая служит обёрткой для poll_table
с указателем на соответствующую структуру epitem
(файл fs/eventpoll.c
, строка 243):/* Структура-обёртка, используемая в очереди */
struct ep_pqueue {
poll_table pt;
struct epitem *epi;
};
Затем
ep_insert()
инициализирует struct ep_pqueue
. Следующий код сначала записывает в член epi
структуры ep_pqueue
указатель на структуру epitem
, соответствующую файлу, который мы пытаемся добавить, а затем записывает ep_ptable_queue_proc()
в член _qproc
структуры ep_pqueue
, а в _key
записывает ~0
./* Инициализация таблицы с использованием коллбэка */
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
После этого
ep_insert()
вызовет ep_item_poll(epi, &epq.pt);
, что приведёт к вызову реализации poll()
, связанной с файлом.Давайте рассмотрим пример, в котором используется реализация
poll()
TCP-стека Linux, и разберёмся с тем, что именно эта реализация делает с poll_table
.Функция
tcp_poll()
— это реализация poll()
для TCP-сокетов. Её код можно найти в файле net/ipv4/tcp.c
, в строке 436. Вот фрагмент этого кода:unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
{
unsigned int mask;
struct sock *sk = sock->sk;
const struct tcp_sock *tp = tcp_sk(sk);
sock_rps_record_flow(sk);
sock_poll_wait(file, sk_sleep(sk), wait);
// код опущен
}
Функция
tcp_poll()
вызывает sock_poll_wait()
, передавая, в качестве второго аргумента, sk_sleep(sk)
, а в качестве третьего — wait
(это — ранее переданная функции tcp_poll()
таблица poll_table
).А что представляет собой
sk_sleep()
? Как оказывается, это — всего лишь геттер, предназначенный для доступа к очереди ожидания событий для конкретной структуры sock
(файл include/net/sock.h
, строка 1685):static inline wait_queue_head_t *sk_sleep(struct sock *sk)
{
BUILD_BUG_ON(offsetof(struct socket_wq, wait) != 0);
return &rcu_dereference_raw(sk->sk_wq)->wait;
}
Что же
sock_poll_wait()
собирается делать с очередью ожидания событий? Оказывается, что эта функция выполнит некую простую проверку и потом вызовет poll_wait()
с передачей тех же самых параметров. Затем функция poll_wait()
вызовет заданный нами коллбэк и передаст ему очередь ожидания событий (файл include/linux/poll.h
, строка 42):static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
if (p && p->_qproc && wait_address)
p->_qproc(filp, wait_address, p);
}
В случае с
epoll
сущность _qproc
будет представлять собой функцию ep_ptable_queue_proc()
, объявленную в файле fs/eventpoll.c
, в строке 1091./*
* Это - коллбэк, используемый для включения нашей очереди ожидания в
* состав списков процессов целевого файла, работу которых надо возобновить.
*/
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
poll_table *pt)
{
struct epitem *epi = ep_item_from_epqueue(pt);
struct eppoll_entry *pwq;
if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {
init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
pwq->whead = whead;
pwq->base = epi;
add_wait_queue(whead, &pwq->wait);
list_add_tail(&pwq->llink, &epi->pwqlist);
epi->nwait++;
} else {
/* Нам нужно сообщить о возникновении ошибки */
epi->nwait = -1;
}
}
Сначала
ep_ptable_queue_proc()
пытается восстановить структуру epitem
, которая соответствует файлу из очереди ожидания, с которым мы работаем. Так как epoll
использует структуру-обёртку ep_pqueue
, восстановление epitem
из указателя poll_table
представлено простой операцией с указателями.После этого
ep_ptable_queue_proc()
просто выделяет столько памяти, сколько нужно для struct eppoll_entry
. Эта структура работает как «связующее звено» между очередью ожидания файла, за которым ведётся наблюдение, и соответствующей структурой epitem
для этого файла. Для epoll
чрезвычайно важно знать о том, где находится голова очереди ожидания для файла, за которым ведётся наблюдение. В противном случае epoll
не сможет позже отменить регистрацию в очереди ожидания. Структура eppoll_entry
, кроме того, включает в себя очередь ожидания (pwq->wait
) с функцией возобновления работы процесса, представленной ep_poll_callback()
. Возможно, pwq->wait
— это самая важная часть во всей реализации epoll
, так как эту сущность используют для решения следующих задач:- Мониторинг событий, происходящих с конкретным файлом, за которым осуществляется наблюдение.
- Возобновление работы других процессов в том случае, если возникает такая необходимость.
После этого
ep_ptable_queue_proc()
присоединит pwq->wait
к очереди ожидания целевого файла (whead
). Функция, кроме того, добавит struct eppoll_entry
в связный список из struct epitem
(epi->pwqlist
) и инкрементирует значение epi->nwait
, представляющее собой длину списка epi->pwqlist
.А вот тут у меня возникает один вопрос. Почему
epoll
нужно использовать связный список для хранения структуры eppoll_entry
внутри структуры epitem
одного файла? Не нужен ли epitem
лишь один элемент eppoll_entry
?Я, правда, не могу точно ответить на этот вопрос. Насколько я могу судить, если только некто не собирается использовать экземпляры
epoll
в каких-нибудь безумных циклах, список epi->pwqlist
будет содержать лишь один элемент struct eppoll_entry
, а epi->nwait
для большинства файлов, скорее всего, будет равняться 1
.Хорошо тут то, что неясности вокруг
epi->pwqlist
никак не отражаются на том, о чём я буду говорить ниже. А именно, речь пойдёт о том, как Linux уведомляет экземпляры epoll
о событиях, происходящих с файлами, за которыми осуществляется наблюдение.Помните то, о чём мы говорили в предыдущем разделе? Речь шла о том, что
epoll
присоединяет wait_queue_t
к списку ожидания целевого файла (к wait_queue_head_t
). Несмотря на то, что wait_queue_t
чаще всего используется как механизм возобновления работы процессов, это, по сути, просто структура, хранящая указатель на функцию, которая будет вызвана тогда, когда Linux решит возобновить работу процессов из очереди wait_queue_t
, прикреплённую к wait_queue_head_t
. В этой функции epoll
может принять решение о том, что делать с сигналом возобновления работы, но у epoll
нет необходимости возобновлять работу какого-либо процесса! Как можно будет увидеть позже, обычно при вызове ep_poll_callback()
возобновления работы чего-либо не происходит.Полагаю, ещё стоит обратить внимание на то, что механизм возобновления работы процессов, применяемый в
poll()
, полностью зависит от реализации. В случае с файлами TCP-сокетов голова очереди ожидания — это член sk_wq
, сохранённый в структуре sock
. Это, кроме того, объясняет необходимость использования коллбэка ep_ptable_queue_proc()
для работы с очередью ожидания. Так как в реализациях очереди для разных файлов голова очереди может оказаться в совершенно разных местах, у нас нет способа обнаружить нужное нам значение wait_queue_head_t
без использования коллбэка.Когда именно осуществляется возобновление работы
sk_wq
в структуре sock
? Как оказалось, система сокетов Linux следует тем же «OO»-принципам проектирования, что и VFS. Структура sock
объявляет следующие хуки в строке 2312 файла net/core/sock.c
:void sock_init_data(struct socket *sock, struct sock *sk)
{
// код опущен...
sk->sk_data_ready = sock_def_readable;
sk->sk_write_space = sock_def_write_space;
// код опущен...
}
В
sock_def_readable()
и sock_def_write_space()
осуществляется вызов wake_up_interruptible_sync_poll()
для (struct sock)->sk_wq
с целью выполнения функции-коллбэка, возобновляющей работу процесса.Когда же будут вызываться
sk->sk_data_ready()
и sk->sk_write_space()
? Это зависит от реализации. Рассмотрим, в качестве примера, TCP-сокеты. Функция sk->sk_data_ready()
будет вызвана во второй половине обработчика прерывания в том случае, когда TCP-подключение завершит процедуру трёхстороннего рукопожатия, или когда для некоего TCP-сокета будет получен буфер. Функция sk->sk_write_space()
будет вызвана при изменении состояния буфера с full
на available
. Если помнить об этом при разборе следующих тем, особенно той, что посвящена срабатыванию по фронту, эти темы будут выглядеть интереснее.Итоги
На этом мы завершаем второй материал из цикла статей о реализации
epoll
. В следующий раз поговорим о том, чем именно занимается epoll
в коллбэке, зарегистрированном в очереди возобновления выполнения процессов сокета.Пользовались ли вы epoll?
Zekori
epoll мощный механизм, но не совсем полноценный из-за отсутствия механизма получения ассоциированого с fd — epoll_event. Приходится использовать костыли в виде деревьев и т.п., что влечёт дополнительные расходы на поддержание и очистку ресурсов
z3apa3a
С poll() или WaitForMultipleObjects() не путаете? В epoll_event как раз есть epoll_data и можно обойтись без деревьев. Скорей не хватает возможности poll'ить семафоры.
DistortNeo
Нет. Речь о том, что можно назначить epoll_data конкретному дескриптору при вызове epoll_ctl(), но нельзя узнать текущий статус и epoll_data по конретному дескриптору.
Типичный сценарий:
epoll_ctl(..., EPOLL_CTL_ADD, ...)
.epoll_ctl(..., EPOLL_CTL_DEL, ...)
, после чего удалить объект.Так вот, было бы удобно, чтобы при удалении дескриптора из epoll возвращалось старое значение epoll_data.
Другой сценарий: для одного и то же дескриптора постоянно добавляются и удаляются события, связанные с чтением и записью. При этом оба они разделяют одно и то же значение epoll_data. То есть приходится локально хранить и анализовать маску событий. Проблема решается дублированием дескрипторов и использованием одного только на чтение, а другого — только на запись. Но это расточительство.