සම්පූර්ණ විශේෂාංග සහිත හිස්-C I/O ප්රතික්රියාකාරකය

සම්පූර්ණ විශේෂාංග සහිත හිස්-C I/O ප්රතික්රියාකාරකය

හැඳින්වීම

I/O ප්රතික්රියාකාරකය (තනි නූල් සහිත සිද්ධි ලූපය) යනු බොහෝ ජනප්‍රිය විසඳුම්වල භාවිතා වන අධි-බර මෘදුකාංග ලිවීමේ රටාවකි:

මෙම ලිපියෙන්, අපි I/O ප්‍රතික්‍රියාකාරකයක ඇතුළුවීම් සහ එය ක්‍රියා කරන ආකාරය දෙස බලමු, කේත පේළි 200කට වඩා අඩු ප්‍රමාණයකින් ක්‍රියාත්මක කිරීමක් ලියන්නෙමු, සහ විනාඩියකට මිලියන 40කට අධික HTTP සේවාදායක ක්‍රියාවලියක් සිදු කරන්නෙමු.

පෙරවදන

  • ලිපිය ලියා ඇත්තේ I/O ප්‍රතික්‍රියාකාරකයේ ක්‍රියාකාරිත්වය අවබෝධ කර ගැනීමට සහ එම නිසා එය භාවිතා කිරීමේදී ඇති විය හැකි අවදානම් තේරුම් ගැනීමටය.
  • ලිපිය තේරුම් ගැනීමට මූලික කරුණු පිළිබඳ දැනුමක් අවශ්ය වේ. සී භාෂාව සහ ජාල යෙදුම් සංවර්ධනය පිළිබඳ යම් අත්දැකීමක්.
  • සියලුම කේතය C භාෂාවෙන් ලියා ඇත්තේ (අවවාදයයි: දිගු PDF) C11 ප්‍රමිතියට Linux සඳහා සහ ලබා ගත හැක GitHub.

මෙය අවශ්‍ය වන්නේ ඇයි?

අන්තර්ජාලයේ වැඩිවන ජනප්‍රියතාවයත් සමඟ, වෙබ් සේවාදායකයන්ට එකවර සම්බන්ධතා විශාල සංඛ්‍යාවක් හැසිරවීමට අවශ්‍ය වීමට පටන් ගත් අතර, එබැවින් ප්‍රවේශයන් දෙකක් උත්සාහ කරන ලදී: OS නූල් විශාල සංඛ්‍යාවක I/O අවහිර කිරීම සහ I/O අවහිර නොකිරීම සිදුවීම් දැනුම්දීම් පද්ධතියක්, එය "පද්ධති තේරීම" ලෙසද හැඳින්වේ (epoll/kqueue/IOCP/ etc).

එන එක් එක් සම්බන්ධතාවය සඳහා නව OS නූල් නිර්මාණය කිරීම පළමු ප්‍රවේශය විය. එහි අවාසිය නම් දුර්වල පරිමාණය: මෙහෙයුම් පද්ධතියට බොහෝ දේ ක්රියාත්මක කිරීමට සිදුවනු ඇත සන්දර්භය සංක්රමණයන් и පද්ධති ඇමතුම්. Они являются дорогими операциями и могут привести к недостатку свободной ОЗУ при внушительном числе соединений.

නවීකරණය කරන ලද අනුවාදය ඉස්මතු කරයි ස්ථාවර නූල් සංඛ්යාව (නූල් සංචිතය), එමඟින් පද්ධතිය ක්‍රියාත්මක කිරීම නවතා දැමීම වළක්වයි, නමුත් ඒ සමඟම නව ගැටළුවක් හඳුන්වා දෙයි: දිගු කියවීම් මෙහෙයුම් මගින් දැනට නූල් සංචිතයක් අවහිර කර ඇත්නම්, දැනටමත් දත්ත ලබා ගැනීමට හැකි වෙනත් සොකට් වලට නොහැකි වනු ඇත. එසේ කරන්න.

දෙවන ප්රවේශය භාවිතා කරයි සිදුවීම් දැනුම්දීම් පද්ධතිය (පද්ධති තේරීම) OS මඟින් සපයනු ලැබේ. මෙම ලිපිය I/O මෙහෙයුම් සඳහා ඇති සූදානම පිළිබඳ ඇඟවීම් (සිදුවීම්, දැනුම්දීම්) මත පදනම්ව, වඩාත් පොදු ආකාරයේ පද්ධති තේරීම් ගැන සාකච්ඡා කරයි. ඒවා සම්පූර්ණ කිරීම පිළිබඳ දැනුම්දීම්. එහි භාවිතය පිළිබඳ සරල උදාහරණයක් පහත බ්ලොක් රූප සටහන මගින් නිරූපණය කළ හැක:

සම්පූර්ණ විශේෂාංග සහිත හිස්-C I/O ප්රතික්රියාකාරකය

මෙම ප්රවේශයන් අතර වෙනස පහත පරිදි වේ:

  • I/O මෙහෙයුම් අවහිර කිරීම අත්හිටුවනවා පරිශීලක ප්රවාහය තුරුOS එක හරියට වෙනකම් defragments එන IP පැකට් බයිට් ප්‍රවාහයට (TCP, දත්ත ලැබීම) හෝ පසුව යැවීම සඳහා අභ්‍යන්තර ලිවීමේ බෆරවල ප්‍රමාණවත් ඉඩක් නොතිබෙනු ඇත. ජාතික හැඳුනුම්පත (දත්ත යැවීම).
  • පද්ධති තේරීම කාලයත් එක්ක OS බව වැඩසටහනට දැනුම් දෙයි දැනටමත් defragmented IP packets (TCP, data reception) හෝ අභ්‍යන්තර ලිවීමේ බෆරවල ප්‍රමාණවත් ඉඩක් දැනටමත් ලබා ගත හැකිය (දත්ත යැවීම).

සාරාංශගත කිරීම සඳහා, එක් එක් I/O සඳහා OS ත්‍රෙඩ් එකක් වෙන් කිරීම පරිගණක බලය නාස්තියකි, මන්ද යථාර්ථයේ දී නූල් ප්‍රයෝජනවත් කාර්යයක් නොකරන බැවිනි (එබැවින් මෙම යෙදුම "මෘදුකාංග බාධාව"). Системный селектор решает эту проблему, позволяя пользовательской программе расходовать ресурсы ЦПУ значительно экономнее.

I/O ප්රතික්රියාකාරක ආකෘතිය

I/O ප්‍රතික්‍රියාකාරකය පද්ධති තේරීම්කාරකය සහ පරිශීලක කේතය අතර ස්ථරයක් ලෙස ක්‍රියා කරයි. එහි ක්‍රියාකාරිත්වයේ මූලධර්මය පහත දැක්වෙන බ්ලොක් රූප සටහන මගින් විස්තර කෙරේ:

සම්පූර්ණ විශේෂාංග සහිත හිස්-C I/O ප්රතික්රියාකාරකය

  • සිදුවීමක් යනු කිසියම් සොකට් එකකට අවහිර නොවන I/O මෙහෙයුමක් සිදු කළ හැකි බවට දැනුම් දීමක් බව මම ඔබට මතක් කරමි.
  • Обработчик событий — это функция, вызываемая I/O реактором при получении события, которая далее совершает неблокирующую I/O операцию.

I/O ප්‍රතික්‍රියාකාරකය නිර්වචනය අනුව තනි නූල් එකක් බව සැලකිල්ලට ගැනීම වැදගත් වේ, නමුත් සංකල්පය බහු-නූල් පරිසරයක නූල් 1: 1 ප්‍රතික්‍රියාකාරක අනුපාතයකින් භාවිතා කිරීමට බාධාවක් නැත, එමඟින් සියලුම CPU හරයන් ප්‍රතිචක්‍රීකරණය කරයි.

Реализация

අපි පොදු අතුරු මුහුණත ගොනුවක තබමු reactor.h, සහ ක්රියාත්මක කිරීම - තුළ reactor.c. reactor.h පහත නිවේදන වලින් සමන්විත වනු ඇත:

reactor.h හි ප්රකාශන පෙන්වන්න

typedef struct reactor Reactor;

/*
 * Указатель на функцию, которая будет вызываться I/O реактором при поступлении
 * события от системного селектора.
 */
typedef void (*Callback)(void *arg, int fd, uint32_t events);

/*
 * Возвращает `NULL` в случае ошибки, не-`NULL` указатель на `Reactor` в
 * противном случае.
 */
Reactor *reactor_new(void);

/*
 * Освобождает системный селектор, все зарегистрированные сокеты в данный момент
 * времени и сам I/O реактор.
 *
 * Следующие функции возвращают -1 в случае ошибки, 0 в случае успеха.
 */
int reactor_destroy(Reactor *reactor);

int reactor_register(const Reactor *reactor, int fd, uint32_t interest,
                     Callback callback, void *callback_arg);
int reactor_deregister(const Reactor *reactor, int fd);
int reactor_reregister(const Reactor *reactor, int fd, uint32_t interest,
                       Callback callback, void *callback_arg);

/*
 * Запускает цикл событий с тайм-аутом `timeout`.
 *
 * Эта функция передаст управление вызывающему коду если отведённое время вышло
 * или/и при отсутствии зарегистрированных сокетов.
 */
int reactor_run(const Reactor *reactor, time_t timeout);

I/O ප්රතික්රියාකාරක ව්යුහය සමන්විත වේ ගොනු විස්තරකය තේරීම්කරු epoll и හැෂ් වගු GHashTable, එය එක් එක් සොකට් සිතියම්ගත කරයි CallbackData (සිදුවීම් හසුරුවෙකුගේ ව්‍යුහය සහ ඒ සඳහා පරිශීලක තර්කයක්).

ප්‍රතික්‍රියාකාරකය සහ ඇමතුම් දත්ත පෙන්වන්න

struct reactor {
    int epoll_fd;
    GHashTable *table; // (int, CallbackData)
};

typedef struct {
    Callback callback;
    void *arg;
} CallbackData;

අපි හැසිරවීමේ හැකියාව සබල කර ඇති බව කරුණාවෙන් සලකන්න අසම්පූර්ණ වර්ගය по указателю. В reactor.h අපි ව්යුහය ප්රකාශ කරමු reactor, සහ ඇතුළත reactor.c අපි එය නිර්වචනය කරන්නෙමු, එමඟින් පරිශීලකයා එහි ක්ෂේත්‍ර පැහැදිලිව වෙනස් කිරීමෙන් වළක්වන්නෙමු. මෙය මෝස්තර වලින් එකකි දත්ත සැඟවීම, එය සී අර්ථ ශාස්ත්‍රයට සංක්ෂිප්තව ගැලපේ.

කාර්යයන් reactor_register, reactor_deregister и reactor_reregister обновляют список интересующих сокетов и соответствующих обработчиков событий в системном селекторе и в хеш-таблице.

ලියාපදිංචි කිරීමේ කාර්යයන් පෙන්වන්න

#define REACTOR_CTL(reactor, op, fd, interest)                                 
    if (epoll_ctl(reactor->epoll_fd, op, fd,                                   
                  &(struct epoll_event){.events = interest,                    
                                        .data = {.fd = fd}}) == -1) {          
        perror("epoll_ctl");                                                   
        return -1;                                                             
    }

int reactor_register(const Reactor *reactor, int fd, uint32_t interest,
                     Callback callback, void *callback_arg) {
    REACTOR_CTL(reactor, EPOLL_CTL_ADD, fd, interest)
    g_hash_table_insert(reactor->table, int_in_heap(fd),
                        callback_data_new(callback, callback_arg));
    return 0;
}

int reactor_deregister(const Reactor *reactor, int fd) {
    REACTOR_CTL(reactor, EPOLL_CTL_DEL, fd, 0)
    g_hash_table_remove(reactor->table, &fd);
    return 0;
}

int reactor_reregister(const Reactor *reactor, int fd, uint32_t interest,
                       Callback callback, void *callback_arg) {
    REACTOR_CTL(reactor, EPOLL_CTL_MOD, fd, interest)
    g_hash_table_insert(reactor->table, int_in_heap(fd),
                        callback_data_new(callback, callback_arg));
    return 0;
}

I/O ප්‍රතික්‍රියාකාරකය විස්තරකය සමඟ සිදුවීමට බාධා කළ පසු fd, එය එය සමත් වන අනුරූප සිදුවීම් හසුරුවන්නා අමතයි fd, ටිකක් මාස්ක් ජනනය කරන ලද සිදුවීම් සහ පරිශීලක දර්ශකය void.

Reactor_run() ශ්‍රිතය පෙන්වන්න

int reactor_run(const Reactor *reactor, time_t timeout) {
    int result;
    struct epoll_event *events;
    if ((events = calloc(MAX_EVENTS, sizeof(*events))) == NULL)
        abort();

    time_t start = time(NULL);

    while (true) {
        time_t passed = time(NULL) - start;
        int nfds =
            epoll_wait(reactor->epoll_fd, events, MAX_EVENTS, timeout - passed);

        switch (nfds) {
        // Ошибка
        case -1:
            perror("epoll_wait");
            result = -1;
            goto cleanup;
        // Время вышло
        case 0:
            result = 0;
            goto cleanup;
        // Успешная операция
        default:
            // Вызвать обработчиков событий
            for (int i = 0; i < nfds; i++) {
                int fd = events[i].data.fd;

                CallbackData *callback =
                    g_hash_table_lookup(reactor->table, &fd);
                callback->callback(callback->arg, fd, events[i].events);
            }
        }
    }

cleanup:
    free(events);
    return result;
}

සාරාංශ කිරීම සඳහා, පරිශීලක කේතයේ ඇති ශ්‍රිත ඇමතුම් දාමය පහත පෝරමය ගනී:

සම්පූර්ණ විශේෂාංග සහිත හිස්-C I/O ප්රතික්රියාකාරකය

Однопоточный сервер

I/O ප්‍රතික්‍රියාකාරකය අධික බරක් යටතේ පරීක්ෂා කිරීම සඳහා, අපි රූපයක් සහිත ඕනෑම ඉල්ලීමකට ප්‍රතිචාර දක්වන සරල HTTP වෙබ් සේවාදායකයක් ලියන්නෙමු.

HTTP ප්‍රොටෝකෝලය වෙත ඉක්මන් සඳහනක්

HTTP - මෙය ප්රොටෝකෝලයයි යෙදුම් මට්ටම, මූලික වශයෙන් සේවාදායක-බ්‍රවුසර අන්තර්ක්‍රියා සඳහා භාවිතා වේ.

HTTP පහසුවෙන් භාවිතා කළ හැක ප්රවාහනය ප්රොටෝකෝලය TCP, නිශ්චිත ආකෘතියකින් පණිවිඩ යැවීම සහ ලැබීම පිරිවිතර.

ඉල්ලීම් ආකෘතිය

<КОМАНДА> <URI> <ВЕРСИЯ HTTP>CRLF
<ЗАГОЛОВОК 1>CRLF
<ЗАГОЛОВОК 2>CRLF
<ЗАГОЛОВОК N>CRLF CRLF
<ДАННЫЕ>

  • CRLF අක්ෂර දෙකක අනුපිළිවෙලකි: r и n, ඉල්ලීමේ පළමු පේළිය, ශීර්ෂක සහ දත්ත වෙන් කිරීම.
  • <КОМАНДА> - එකක් CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. බ්රවුසරය අපගේ සේවාදායකයට විධානයක් එවනු ඇත GET, තේරුම "ගොනුවේ අන්තර්ගතය මට එවන්න."
  • <URI> - yнифицированный идентификатор ресурса. උදාහරණයක් ලෙස, URI = නම් /index.html, එවිට සේවාදායකයා වෙබ් අඩවියේ ප්‍රධාන පිටුව ඉල්ලා සිටී.
  • <ВЕРСИЯ HTTP> - ආකෘතියේ HTTP ප්රොටෝකෝලයේ අනුවාදය HTTP/X.Y. Наиболее часто используемая версия на сегодняшний день — HTTP/1.1.
  • <ЗАГОЛОВОК N> — это пара ключ-значение в формате <КЛЮЧ>: <ЗНАЧЕНИЕ>, වැඩිදුර විශ්ලේෂණය සඳහා සේවාදායකය වෙත යවනු ලැබේ.
  • <ДАННЫЕ> - මෙහෙයුම සිදු කිරීමට සේවාදායකයට අවශ්‍ය දත්ත. බොහෝ විට එය සරලයි JSON හෝ වෙනත් ආකෘතියක්.

ප්‍රතිචාර ආකෘතිය

<ВЕРСИЯ HTTP> <КОД СТАТУСА> <ОПИСАНИЕ СТАТУСА>CRLF
<ЗАГОЛОВОК 1>CRLF
<ЗАГОЛОВОК 2>CRLF
<ЗАГОЛОВОК N>CRLF CRLF
<ДАННЫЕ>

  • <КОД СТАТУСА> — это число, представляющее собой результат операции. Наш сервер будет всегда возвращать статус 200 (успешная операция).
  • <ОПИСАНИЕ СТАТУСА> - තත්ව කේතයේ තන්තු නිරූපණය. තත්ව කේතය 200 සඳහා මෙය වේ OK.
  • <ЗАГОЛОВОК N> - ඉල්ලීමෙහි ඇති එකම ආකෘතියේ ශීර්ෂකය. අපි මාතෘකා ආපසු දෙන්නෙමු Content-Length (ගොනු ප්රමාණය) සහ Content-Type: text/html (ආපසු දත්ත වර්ගය).
  • <ДАННЫЕ> - පරිශීලකයා විසින් ඉල්ලා සිටින දත්ත. අපගේ නඩුවේදී, රූපය තුළට යන මාර්ගය මෙයයි HTML.

ගොනුව http_server.c (තනි නූල් සේවාදායකය) ගොනුව ඇතුළත් වේ common.h, පහත සඳහන් ශ්‍රිත මූලාකෘති අඩංගු වේ:

Показать прототипы функций в common.h

/*
 * Обработчик событий, который вызовется после того, как сокет будет
 * готов принять новое соединение.
 */
static void on_accept(void *arg, int fd, uint32_t events);

/*
 * Обработчик событий, который вызовется после того, как сокет будет
 * готов отправить HTTP ответ.
 */
static void on_send(void *arg, int fd, uint32_t events);

/*
 * Обработчик событий, который вызовется после того, как сокет будет
 * готов принять часть HTTP запроса.
 */
static void on_recv(void *arg, int fd, uint32_t events);

/*
 * Переводит входящее соединение в неблокирующий режим.
 */
static void set_nonblocking(int fd);

/*
 * Печатает переданные аргументы в stderr и выходит из процесса с
 * кодом `EXIT_FAILURE`.
 */
static noreturn void fail(const char *format, ...);

/*
 * Возвращает файловый дескриптор сокета, способного принимать новые
 * TCP соединения.
 */
static int new_server(bool reuse_port);

Также описан функциональный макрос SAFE_CALL() සහ කාර්යය අර්ථ දක්වා ඇත fail(). සාර්ව ප්‍රකාශනයේ අගය දෝෂය සමඟ සංසන්දනය කරයි, සහ කොන්දේසිය සත්‍ය නම්, ශ්‍රිතය අමතන්න fail():

#define SAFE_CALL(call, error)                                                 
    do {                                                                       
        if ((call) == error) {                                                   
            fail("%s", #call);                                                 
        }                                                                      
    } while (false)

උත්සවය fail() සම්මත වූ තර්ක ටර්මිනලයට මුද්‍රණය කරයි (වැනි printf()) සහ කේතය සමඟ වැඩසටහන අවසන් කරයි EXIT_FAILURE:

static noreturn void fail(const char *format, ...) {
    va_list args;
    va_start(args, format);
    vfprintf(stderr, format, args);
    va_end(args);
    fprintf(stderr, ": %sn", strerror(errno));
    exit(EXIT_FAILURE);
}

උත්සවය new_server() возвращает файловый дескриптор «серверного» сокета, созданного системными вызовами socket(), bind() и listen() සහ අවහිර නොවන ආකාරයෙන් පැමිණෙන සම්බන්ධතා පිළිගැනීමට හැකියාව ඇත.

new_server() ශ්‍රිතය පෙන්වන්න

static int new_server(bool reuse_port) {
    int fd;
    SAFE_CALL((fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP)),
              -1);

    if (reuse_port) {
        SAFE_CALL(
            setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &(int){1}, sizeof(int)),
            -1);
    }

    struct sockaddr_in addr = {.sin_family = AF_INET,
                               .sin_port = htons(SERVER_PORT),
                               .sin_addr = {.s_addr = inet_addr(SERVER_IPV4)},
                               .sin_zero = {0}};

    SAFE_CALL(bind(fd, (struct sockaddr *)&addr, sizeof(addr)), -1);
    SAFE_CALL(listen(fd, SERVER_BACKLOG), -1);
    return fd;
}

  • සොකට් එක මුලින් නිර්මාණය කර ඇත්තේ ධජය භාවිතයෙන් අවහිර නොවන ආකාරයෙන් බව සලකන්න SOCK_NONBLOCK, чтобы в функции on_accept() (තවත් කියවන්න) පද්ධති ඇමතුම accept() නූල් ක්රියාත්මක කිරීම නතර කළේ නැත.
  • නම් reuse_port සමානයි true, එවිට මෙම කාර්යය විකල්පය සමඟ සොකට් වින්යාස කරනු ඇත SO_REUSEPORT හරහා setsockopt()බහු-නූල් පරිසරයක් තුළ එකම වරාය භාවිතා කිරීමට ("බහු නූල් සේවාදායකය" කොටස බලන්න).

සිදුවීම් හසුරුවන්නා on_accept() මෙහෙයුම් පද්ධතිය සිදුවීමක් උත්පාදනය කිරීමෙන් පසුව කැඳවනු ලැබේ EPOLLIN, මෙම අවස්ථාවෙහිදී අදහස් වන්නේ නව සම්බන්ධතාවය පිළිගත හැකි බවයි. on_accept() නව සම්බන්ධතාවයක් පිළිගනී, එය අවහිර නොවන මාදිලියට මාරු කර සිදුවීම් හසුරුවන්නෙකු සමඟ ලියාපදිංචි වේ on_recv() I/O ප්රතික්රියාකාරකයක.

on_accept() ශ්‍රිතය පෙන්වන්න

static void on_accept(void *arg, int fd, uint32_t events) {
    int incoming_conn;
    SAFE_CALL((incoming_conn = accept(fd, NULL, NULL)), -1);
    set_nonblocking(incoming_conn);
    SAFE_CALL(reactor_register(reactor, incoming_conn, EPOLLIN, on_recv,
                               request_buffer_new()),
              -1);
}

සිදුවීම් හසුරුවන්නා on_recv() මෙහෙයුම් පද්ධතිය සිදුවීමක් උත්පාදනය කිරීමෙන් පසුව කැඳවනු ලැබේ EPOLLIN, මෙම නඩුවේ අර්ථය සම්බන්ධතාවය ලියාපදිංචි කර ඇති බවයි on_accept(), දත්ත ලබා ගැනීමට සූදානම්.

on_recv() HTTP ඉල්ලීම සම්පුර්ණයෙන්ම ලැබෙන තුරු සම්බන්ධතාවයෙන් දත්ත කියවයි, පසුව එය හසුරුවන්නෙකු ලියාපදිංචි කරයි on_send() HTTP ප්‍රතිචාරයක් යැවීමට. සේවාදායකයා සම්බන්ධතාවය බිඳ දැමුවහොත්, සොකට් එක ලියාපදිංචි කර වසා දමා ඇත close().

on_recv() ශ්‍රිතය පෙන්වන්න

static void on_recv(void *arg, int fd, uint32_t events) {
    RequestBuffer *buffer = arg;

    // Принимаем входные данные до тех пор, что recv возвратит 0 или ошибку
    ssize_t nread;
    while ((nread = recv(fd, buffer->data + buffer->size,
                         REQUEST_BUFFER_CAPACITY - buffer->size, 0)) > 0)
        buffer->size += nread;

    // Клиент оборвал соединение
    if (nread == 0) {
        SAFE_CALL(reactor_deregister(reactor, fd), -1);
        SAFE_CALL(close(fd), -1);
        request_buffer_destroy(buffer);
        return;
    }

    // read вернул ошибку, отличную от ошибки, при которой вызов заблокирует
    // поток
    if (errno != EAGAIN && errno != EWOULDBLOCK) {
        request_buffer_destroy(buffer);
        fail("read");
    }

    // Получен полный HTTP запрос от клиента. Теперь регистрируем обработчика
    // событий для отправки данных
    if (request_buffer_is_complete(buffer)) {
        request_buffer_clear(buffer);
        SAFE_CALL(reactor_reregister(reactor, fd, EPOLLOUT, on_send, buffer),
                  -1);
    }
}

සිදුවීම් හසුරුවන්නා on_send() මෙහෙයුම් පද්ධතිය සිදුවීමක් උත්පාදනය කිරීමෙන් පසුව කැඳවනු ලැබේ EPOLLOUT, එනම් සම්බන්ධතාවය ලියාපදිංචි වී ඇති බවයි on_recv(), готово к отправке данных. Эта функция отправляет HTTP ответ, содержащий HTML с изображением, клиенту, а затем меняет обработчик событий снова на on_recv().

on_send() ශ්‍රිතය පෙන්වන්න

static void on_send(void *arg, int fd, uint32_t events) {
    const char *content = "<img "
                          "src="https://habrastorage.org/webt/oh/wl/23/"
                          "ohwl23va3b-dioerobq_mbx4xaw.jpeg">";
    char response[1024];
    sprintf(response,
            "HTTP/1.1 200 OK" CRLF "Content-Length: %zd" CRLF "Content-Type: "
            "text/html" DOUBLE_CRLF "%s",
            strlen(content), content);

    SAFE_CALL(send(fd, response, strlen(response), 0), -1);
    SAFE_CALL(reactor_reregister(reactor, fd, EPOLLIN, on_recv, arg), -1);
}

අවසාන වශයෙන්, ගොනුවේ http_server.c, ක්රියාකාරිත්වය තුළ main() мы создаём I/O реактор посредством reactor_new(), සේවාදායක සොකට් එකක් සාදා එය ලියාපදිංචි කරන්න, භාවිතා කර ප්රතික්රියාකාරකය ආරම්භ කරන්න reactor_run() හරියටම විනාඩියකට, පසුව අපි සම්පත් මුදා හැර වැඩසටහනෙන් ඉවත් වන්නෙමු.

http_server.c පෙන්වන්න

#include "reactor.h"

static Reactor *reactor;

#include "common.h"

int main(void) {
    SAFE_CALL((reactor = reactor_new()), NULL);
    SAFE_CALL(
        reactor_register(reactor, new_server(false), EPOLLIN, on_accept, NULL),
        -1);
    SAFE_CALL(reactor_run(reactor, SERVER_TIMEOUT_MILLIS), -1);
    SAFE_CALL(reactor_destroy(reactor), -1);
}

සෑම දෙයක්ම අපේක්ෂිත පරිදි ක්‍රියාත්මක වන බව පරීක්ෂා කර බලමු. සම්පාදනය කිරීම (chmod a+x compile.sh && ./compile.sh ව්‍යාපෘති මූලයේ) සහ ස්වයං-ලිඛිත සේවාදායකය දියත් කරන්න, විවෘත කරන්න http://127.0.0.1:18470 බ්රවුසරයේ සහ අපි බලාපොරොත්තු වූ දේ බලන්න:

සම්පූර්ණ විශේෂාංග සහිත හිස්-C I/O ප්රතික්රියාකාරකය

කාර්ය සාධනය මැනීම

මගේ කාර් පිරිවිතර පෙන්වන්න

$ screenfetch
 MMMMMMMMMMMMMMMMMMMMMMMMMmds+.        OS: Mint 19.1 tessa
 MMm----::-://////////////oymNMd+`     Kernel: x86_64 Linux 4.15.0-20-generic
 MMd      /++                -sNMd:    Uptime: 2h 34m
 MMNso/`  dMM    `.::-. .-::.` .hMN:   Packages: 2217
 ddddMMh  dMM   :hNMNMNhNMNMNh: `NMm   Shell: bash 4.4.20
     NMm  dMM  .NMN/-+MMM+-/NMN` dMM   Resolution: 1920x1080
     NMm  dMM  -MMm  `MMM   dMM. dMM   DE: Cinnamon 4.0.10
     NMm  dMM  -MMm  `MMM   dMM. dMM   WM: Muffin
     NMm  dMM  .mmd  `mmm   yMM. dMM   WM Theme: Mint-Y-Dark (Mint-Y)
     NMm  dMM`  ..`   ...   ydm. dMM   GTK Theme: Mint-Y [GTK2/3]
     hMM- +MMd/-------...-:sdds  dMM   Icon Theme: Mint-Y
     -NMm- :hNMNNNmdddddddddy/`  dMM   Font: Noto Sans 9
      -dMNs-``-::::-------.``    dMM   CPU: Intel Core i7-6700 @ 8x 4GHz [52.0°C]
       `/dMNmy+/:-------------:/yMMM   GPU: NV136
          ./ydNMMMMMMMMMMMMMMMMMMMMM   RAM: 2544MiB / 7926MiB
             .MMMMMMMMMMMMMMMMMMM

අපි තනි නූල් සේවාදායකයක කාර්ය සාධනය මැන බලමු. අපි පර්යන්ත දෙකක් විවෘත කරමු: එකකින් අපි ධාවනය කරන්නෙමු ./http_server, в другом — රැක්. මිනිත්තුවකට පසු, පහත සඳහන් සංඛ්‍යාලේඛන දෙවන පර්යන්තයේ පෙන්වනු ඇත:

$ wrk -c100 -d1m -t8 http://127.0.0.1:18470 -H "Host: 127.0.0.1:18470" -H "Accept-Language: en-US,en;q=0.5" -H "Connection: keep-alive"
Running 1m test @ http://127.0.0.1:18470
  8 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   493.52us   76.70us  17.31ms   89.57%
    Req/Sec    24.37k     1.81k   29.34k    68.13%
  11657769 requests in 1.00m, 1.60GB read
Requests/sec: 193974.70
Transfer/sec:     27.19MB

අපගේ තනි නූල් සේවාදායකයට සම්බන්ධතා 11කින් ආරම්භ වන විනාඩියකට මිලියන 100කට අධික ඉල්ලීම් සැකසීමට හැකි විය. නරක ප්රතිඵලයක් නොවේ, නමුත් එය වැඩිදියුණු කළ හැකිද?

බහු නූල් සේවාදායකය

ඉහත සඳහන් කළ පරිදි, I/O ප්‍රතික්‍රියාකාරකය වෙනම නූල්වලින් සෑදිය හැකි අතර එමඟින් සියලුම CPU මධ්‍යයන් භාවිතා කරයි. අපි මෙම ප්රවේශය ප්රායෝගිකව ක්රියාත්මක කරමු:

http_server_multithreaded.c පෙන්වන්න

#include "reactor.h"

static Reactor *reactor;
#pragma omp threadprivate(reactor)

#include "common.h"

int main(void) {
#pragma omp parallel
    {
        SAFE_CALL((reactor = reactor_new()), NULL);
        SAFE_CALL(reactor_register(reactor, new_server(true), EPOLLIN,
                                   on_accept, NULL),
                  -1);
        SAFE_CALL(reactor_run(reactor, SERVER_TIMEOUT_MILLIS), -1);
        SAFE_CALL(reactor_destroy(reactor), -1);
    }
}

දැන් හැම ත්‍රෙඩ් එකක්ම ඔහුගේම අයිතියි ප්රතික්රියාකාරකය:

static Reactor *reactor;
#pragma omp threadprivate(reactor)

කාර්යය තර්කය බව කරුණාවෙන් සලකන්න new_server() අනුග්‍රහය true. මෙයින් අදහස් කරන්නේ අපි විකල්පය සේවාදායක සොකට් එකට පවරන බවයි SO_REUSEPORTබහු-නූල් පරිසරයක් තුළ එය භාවිතා කිරීමට. ඔබට වැඩි විස්තර කියවිය හැකිය මෙහි.

දෙවන ධාවනය

දැන් අපි බහු-නූල් සේවාදායකයක කාර්ය සාධනය මැන බලමු:

$ wrk -c100 -d1m -t8 http://127.0.0.1:18470 -H "Host: 127.0.0.1:18470" -H "Accept-Language: en-US,en;q=0.5" -H "Connection: keep-alive"
Running 1m test @ http://127.0.0.1:18470
  8 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.14ms    2.53ms  40.73ms   89.98%
    Req/Sec    79.98k    18.07k  154.64k    78.65%
  38208400 requests in 1.00m, 5.23GB read
Requests/sec: 635876.41
Transfer/sec:     89.14MB

මිනිත්තු 1 කින් සකසන ලද ඉල්ලීම් ගණන ~3.28 ගුණයකින් වැඩි විය! නමුත් අපට වටයේ අංකයට වඩා මිලියන XNUMX ක් අඩුයි, එබැවින් අපි එය නිවැරදි කිරීමට උත්සාහ කරමු.

Сперва посмотрим на статистику, сгенерированную පරිපූර්ණ:

$ sudo perf stat -B -e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,branches,branch-misses,cache-misses ./http_server_multithreaded

 Performance counter stats for './http_server_multithreaded':

     242446,314933      task-clock (msec)         #    4,000 CPUs utilized          
         1 813 074      context-switches          #    0,007 M/sec                  
             4 689      cpu-migrations            #    0,019 K/sec                  
               254      page-faults               #    0,001 K/sec                  
   895 324 830 170      cycles                    #    3,693 GHz                    
   621 378 066 808      instructions              #    0,69  insn per cycle         
   119 926 709 370      branches                  #  494,653 M/sec                  
     3 227 095 669      branch-misses             #    2,69% of all branches        
           808 664      cache-misses                                                

      60,604330670 seconds time elapsed

CPU සම්බන්ධය භාවිතා කිරීම, සමග සම්පාදනය -march=native, PGO, පහර ගණන වැඩි වීම හැඹිලිය, වැඩි කරන්න MAX_EVENTS සහ භාවිතා කරන්න EPOLLET කාර්ය සාධනයේ සැලකිය යුතු වැඩි වීමක් ලබා දුන්නේ නැත. නමුත් ඔබ එකවර සම්බන්ධතා ගණන වැඩි කළහොත් කුමක් සිදුවේද?

352 සමගාමී සම්බන්ධතා සඳහා සංඛ්‍යාලේඛන:

$ wrk -c352 -d1m -t8 http://127.0.0.1:18470 -H "Host: 127.0.0.1:18470" -H "Accept-Language: en-US,en;q=0.5" -H "Connection: keep-alive"
Running 1m test @ http://127.0.0.1:18470
  8 threads and 352 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     2.12ms    3.79ms  68.23ms   87.49%
    Req/Sec    83.78k    12.69k  169.81k    83.59%
  40006142 requests in 1.00m, 5.48GB read
Requests/sec: 665789.26
Transfer/sec:     93.34MB

අපේක්ෂිත ප්‍රති result ලය ලබා ගත් අතර, එය සමඟ සම්බන්ධතා ගණන මත මිනිත්තු 1 කින් සැකසූ ඉල්ලීම් ගණන රඳා පවතින බව පෙන්වන සිත්ගන්නා ප්‍රස්ථාරයක්:

සම්පූර්ණ විශේෂාංග සහිත හිස්-C I/O ප්රතික්රියාකාරකය

සම්බන්ධතා සිය ගණනකට පසුව, සේවාදායකයන් දෙකම සඳහා සැකසූ ඉල්ලීම් ගණන තියුනු ලෙස පහත වැටෙන බව අපට පෙනේ (බහු නූල් අනුවාදයේ මෙය වඩාත් කැපී පෙනේ). මෙය Linux TCP/IP තොග ක්‍රියාවට නැංවීමට සම්බන්ධද? ප්‍රස්ථාරයේ මෙම හැසිරීම සහ බහු-නූල් සහ තනි-නූල් විකල්ප සඳහා ප්‍රශස්තකරණයන් පිළිබඳ ඔබේ උපකල්පන ලිවීමට නිදහස් වන්න.

කොහොමද සටහන් කර ඇත අදහස් දැක්වීමේදී, මෙම කාර්ය සාධන පරීක්ෂණය සැබෑ බර යටතේ I/O ප්‍රතික්‍රියාකාරකයේ හැසිරීම නොපෙන්වයි, මන්ද සේවාදායකය සෑම විටම පාහේ දත්ත සමුදාය සමඟ අන්තර් ක්‍රියා කරයි, ලොග් ප්‍රතිදානය කරයි, ගුප්තකේතනය භාවිතා කරයි TLS ආදිය, එහි ප්රතිඵලයක් ලෙස බර ඒකාකාර නොවන (ගතික) බවට පත් වේ. I/O ප්‍රොක්ටරය පිළිබඳ ලිපියේ තෙවන පාර්ශවීය සංරචක සමඟ පරීක්ෂණ සිදු කරනු ලැබේ.

I/O ප්රතික්රියාකාරකයේ අවාසි

I/O ප්‍රතික්‍රියාකාරකය එහි අඩුපාඩු නොමැතිව නොවන බව ඔබ තේරුම් ගත යුතුය, එනම්:

  • Пользоваться I/O реактором в многопоточной среде несколько сложнее, т.к. придётся вручную управлять потоками.
  • ප්රායෝගිකව පෙන්නුම් කරන්නේ බොහෝ අවස්ථාවලදී පැටවීම ඒකාකාර නොවන අතර, එය එක් නූල් ලොග් වීමට හේතු විය හැකි අතර තවත් කාර්යයක් සමඟ කාර්යබහුල වේ.
  • එක් සිදුවීම් හසුරුවන්නෙකු නූල් අවහිර කරන්නේ නම්, පද්ධති තේරීම්කාරකය ද අවහිර කරනු ඇත, එය සොයා ගැනීමට අපහසු දෝෂ වලට තුඩු දිය හැකිය.

මෙම ගැටළු විසඳයි I/O ප්‍රොක්ටර්, බොහෝ විට නූල් පොකුණකට බර ඒකාකාරව බෙදා හරින උපලේඛකයක් ඇති අතර වඩාත් පහසු API ද ඇත. අපි ඒ ගැන පසුව, මගේ අනෙක් ලිපියෙන් කතා කරමු.

නිගමනය

න්‍යායේ සිට කෙලින්ම profiler exhaust වෙත අපගේ ගමන අවසන් වී ඇත්තේ මෙතැනින්ය.

විවිධ මට්ටම්වල පහසුව සහ වේගය සමඟ ජාල මෘදුකාංග ලිවීම සඳහා තවත් බොහෝ සමාන රසවත් ප්‍රවේශයන් ඇති නිසා ඔබ මේ ගැන වාසය නොකළ යුතුය. රසවත්, මගේ මතය අනුව, සබැඳි පහත දක්වා ඇත.

නැවත හමුවෙමු!

සිත්ගන්නා ව්යාපෘති

මා තවත් කුමක් කියවිය යුතුද?

මූලාශ්රය: www.habr.com

අදහස් එක් කරන්න