как select() получает уведомление о том, что fd становится "готовым"?

Я не знаю, почему мне сложно найти это, но я смотрю на какой-то Linux-код, в котором мы используем select(), ожидая в файловом дескрипторе, чтобы сообщить об этом. На странице man выберите:

select() and pselect() allow a program to monitor multiple file descriptors,
waiting until one or more of the file descriptors become "ready" for some
class of I/O operation 

Итак, это здорово... Я вызываю select на каком-то дескрипторе, даю ему некоторое время ожидания и начинаю ждать выхода индикатора. Как дескриптор файла (или владелец дескриптора) сообщает, что он "готов", чтобы оператор select() возвращался?

Ответы

Ответ 1

Сообщается, что он готов к возврату.

select ожидает событий, которые обычно находятся вне вашего программного управления. По сути, вызывая select, ваша программа говорит: "Мне нечего делать до тех пор, пока..., пожалуйста, приостановите мой процесс".

Условие, которое вы указываете, представляет собой набор событий, любой из которых пробудит вас.

Например, если вы загружаете что-то, ваш цикл должен будет ждать появления новых данных, когда произойдет перерыв в передаче, или пользователь может прерывать, что и делает select.

При наличии нескольких загрузок данные, поступающие по любому из подключений, активируют активность в вашей программе (вам нужно записать данные на диск), поэтому вы должны указать список всех подключений для загрузки к select в списке файловых дескрипторов для просмотра "чтения".

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

Общая идея заключается в том, что ваша программа становится управляемой событиями, то есть она реагирует на внешние события из общего цикла сообщений, а не выполняет последовательные операции. Вы сообщаете ядру "это набор событий, для которых я хочу что-то сделать", и ядро ​​дает вам набор событий, которые произошли. Это довольно распространено для двух событий, происходящих одновременно; например, подтверждение TCP было включено в пакет данных, это может сделать одно и то же fd как читаемым (данные доступны), так и записываться (подтвержденные данные были удалены из буфера отправки), поэтому вы должны быть готовы обрабатывать все события перед вызовом select снова.

Одна из самых тонких точек в том, что select в принципе дает вам обещание, что один вызов read или write не будет блокироваться, не гарантируя самого вызова. Например, если один байт буферного пространства доступен, вы можете попытаться записать 10 байт, и ядро ​​вернется и скажет: "Я написал 1 байт", поэтому вы также должны быть готовы обработать этот случай. Типичный подход заключается в том, чтобы иметь буферные данные, которые должны быть записаны в этот fd, и пока он не является пустым, fd добавляется в набор записи, а "записываемое" событие обрабатывается путем попытки записать все данные, находящиеся в буфере. Если после этого буфер пуст, то штраф, если нет, просто подождите снова "writeable".

"Исключительный" набор используется редко - он используется для протоколов, которые имеют внеполосные данные, где возможно блокирование передачи данных, в то время как другие данные должны пройти. Если ваша программа в настоящее время не может принимать данные из "читаемого" файлового дескриптора (например, вы загружаете, а диск заполнен), вы не хотите включать дескриптор в "читаемый" набор, потому что вы не можете обработать событие и select будет немедленно возвращаться, если вызывается снова. Если приемник включает fd в "исключительный" набор, и отправитель просит свой стек IP отправить пакет с "срочными" данными, тогда приемник проснулся и может решить отказаться от необработанных данных и повторно синхронизировать с отправителем, Протокол telnet использует это, например, для обработки Ctrl-C. Если вы не разрабатываете протокол, требующий такой функции, вы можете легко оставить это без вреда.

Пример с обязательным кодом:

#include <sys/types.h>
#include <sys/select.h>

#include <unistd.h>

#include <stdbool.h>

static inline int max(int lhs, int rhs) {
    if(lhs > rhs)
        return lhs;
    else
        return rhs;
}

void copy(int from, int to) {
    char buffer[10];
    int readp = 0;
    int writep = 0;
    bool eof = false;
    for(;;) {
        fd_set readfds, writefds;
        FD_ZERO(&readfds);
        FD_ZERO(&writefds);

        int ravail, wavail;
        if(readp < writep) {
            ravail = writep - readp - 1;
            wavail = sizeof buffer - writep;
        }
        else {
            ravail = sizeof buffer - readp;
            wavail = readp - writep;
        }

        if(!eof && ravail)
            FD_SET(from, &readfds);
        if(wavail)
            FD_SET(to, &writefds);
        else if(eof)
            break;
        int rc = select(max(from,to)+1, &readfds, &writefds, NULL, NULL);
        if(rc == -1)
            break;
        if(FD_ISSET(from, &readfds))
        {
            ssize_t nread = read(from, &buffer[readp], ravail);
            if(nread < 1)
                eof = true;
            readp = readp + nread;
        }
        if(FD_ISSET(to, &writefds))
        {
            ssize_t nwritten = write(to, &buffer[writep], wavail);
            if(nwritten < 1)
                break;
            writep = writep + nwritten;
        }
        if(readp == sizeof buffer && writep != 0)
            readp = 0;
        if(writep == sizeof buffer)
            writep = 0;
    }
}

Мы пытаемся прочитать, если у нас есть свободное место в буфере, и на стороне чтения не было конца файла или ошибки, и мы пытаемся написать, если у нас есть данные в буфере; если конец файла достигнут и буфер пуст, то мы закончили.

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

Ответ 2

На той же странице:

При выходе, наборы изменены, чтобы указать, какие файловые дескрипторы фактически изменили статус.

Итак, используйте FD_ISSET() на наборах, переданных для выбора, чтобы определить, какие FD были готовы.