مڪمل نمايان بيئر-سي I/O ريڪٽر

مڪمل نمايان بيئر-سي I/O ريڪٽر

تعارف

I/O ريڪٽر (اڪيلو موضوع واقعو لوپ) اعلي لوڊ سافٽ ويئر لکڻ لاء هڪ نمونو آهي، ڪيترن ئي مشهور حلن ۾ استعمال ڪيو ويو آهي:

هن آرٽيڪل ۾، اسان هڪ I/O ري ايڪٽر جي اندر ۽ ٻاهران ڏسنداسين ۽ اهو ڪيئن ڪم ڪري ٿو، ڪوڊ جي 200 کان گهٽ لائينن ۾ هڪ عملدرآمد لکندو، ۽ 40 ملين درخواستن/منٽ کان وڌيڪ هڪ سادي HTTP سرور پروسيس ٺاهيو.

اڳوڻي

  • آرٽيڪل I/O ري ايڪٽر جي ڪم کي سمجهڻ ۾ مدد ڏيڻ لاءِ لکيو ويو آهي، ۽ تنهن ڪري ان کي استعمال ڪرڻ وقت خطرن کي سمجھو.
  • مضمون کي سمجھڻ لاءِ بنيادي ڄاڻ جي ضرورت آھي. سي ٻولي ۽ نيٽ ورڪ ايپليڪيشن ڊولپمينٽ ۾ ڪجهه تجربو.
  • سمورو ڪوڊ سختي سان سي ٻولي ۾ لکيل آهي (احتياط: ڊگهو PDF) C11 معيار تائين لينڪس لاءِ ۽ دستياب تي GitHub.

هي ضروري آهي؟

انٽرنيٽ جي وڌندڙ مقبوليت سان، ويب سرورز کي هڪ ئي وقت وڏي تعداد ۾ ڪنيڪشن سنڀالڻ جي ضرورت محسوس ٿيڻ لڳي، ۽ ان ڪري ٻه طريقا آزمايا ويا: وڏي تعداد ۾ OS ٿريڊز تي I/O کي بلاڪ ڪرڻ ۽ غير بلاڪ ڪرڻ I/O سان ميلاپ ۾. ھڪڙو واقعو نوٽيفڪيشن سسٽم، پڻ سڏيو ويندو آھي "سسٽم چونڊيندڙ" (ايپل/قطار/IOCP/etc).

پهريون طريقو شامل آهي هر ايندڙ ڪنيڪشن لاءِ هڪ نئون OS ٿريڊ ٺاهڻ. ان جو نقصان غريب scalability آهي: آپريٽنگ سسٽم ڪيترن ئي لاڳو ڪرڻو پوندو حوالن جي منتقلي и سسٽم ڪالون. اهي قيمتي آپريشن آهن ۽ ڪنيڪشن جي هڪ متاثر کن تعداد سان مفت رام جي کوٽ سبب ٿي سگهن ٿيون.

تبديل ٿيل نسخو نمايان ڪري ٿو سلسلن جو مقرر تعداد (ٿريڊ پول)، ان ڪري سسٽم کي عمل کي ختم ڪرڻ کان روڪي ٿو، پر ساڳئي وقت هڪ نئون مسئلو پيش ڪري ٿو: جيڪڏهن هڪ ٿريڊ پول في الحال بند ٿيل آهي ڊگهي پڙهڻ واري عملن جي ڪري، پوءِ ٻيا ساکٽ جيڪي اڳ ۾ ئي ڊيٽا حاصل ڪرڻ جي قابل نه هوندا. ائين ڪرڻ.

ٻيو طريقو استعمال ڪري ٿو واقعي جي نوٽيفڪيشن سسٽم (سسٽم چونڊيندڙ) OS پاران مهيا ڪيل. هي آرٽيڪل سڀ کان عام قسم جي سسٽم چونڊيندڙ تي بحث ڪري ٿو، خبردارين (واقعات، نوٽيفڪيشن) جي بنياد تي I/O عملن جي تياري بابت، بلڪه ان جي مڪمل ٿيڻ بابت اطلاع. ان جي استعمال جو هڪ آسان مثال هيٺ ڏنل بلاڪ ڊراگرام جي نمائندگي ڪري سگهجي ٿو:

مڪمل نمايان بيئر-سي I/O ريڪٽر

انهن طريقن جي وچ ۾ فرق هن ريت آهي:

  • I/O عملن کي بلاڪ ڪرڻ معطل ڪرڻ استعمال ڪندڙ جي وهڪري جيستائينجيستائين OS صحيح نه آهي خراب ڪرڻ اچڻ وارو IP پيڪٽس بائيٽ وهڪرو (ٽي پي، ڊيٽا وصول ڪري رهيو آهي) يا پوءِ ذريعي موڪلڻ لاءِ اندروني لکڻ جي بفرن ۾ ڪافي جڳهه موجود نه هوندي اين آء (ڊيٽا موڪلڻ).
  • سسٽم چونڊيندڙ اضافي وقت پروگرام کي اطلاع ڏئي ٿو ته OS اڳ ۾ ئي defragmented IP packets (TCP، ڊيٽا استقبال) يا اندروني لکڻ جي بفرن ۾ ڪافي جاء اڳ ۾ ئي دستياب (ڊيٽا موڪلڻ).

ان کي خلاصو ڪرڻ لاءِ، هر I/O لاءِ هڪ OS ٿريڊ محفوظ ڪرڻ ڪمپيوٽر جي طاقت جو ضايع آهي، ڇاڪاڻ ته حقيقت ۾، ٿريڊ مفيد ڪم نه ڪري رهيا آهن (تنهنڪري اصطلاح "سافٽ ويئر مداخلت"). سسٽم چونڊيندڙ هن مسئلي کي حل ڪري ٿو، صارف پروگرام کي سي پي يو وسيلن کي وڌيڪ اقتصادي طور تي استعمال ڪرڻ جي اجازت ڏئي ٿو.

I/O ريڪٽر ماڊل

I/O ريڪٽر سسٽم چونڊيندڙ ۽ يوزر ڪوڊ جي وچ ۾ هڪ پرت جي طور تي ڪم ڪري ٿو. ان جي آپريشن جو اصول هيٺ ڏنل بلاڪ ڊراگرام طرفان بيان ڪيو ويو آهي:

مڪمل نمايان بيئر-سي I/O ريڪٽر

  • مون کي توهان کي ياد ڏيارڻ ڏيو ته هڪ واقعو هڪ نوٽيفڪيشن آهي ته هڪ خاص ساکٽ هڪ غير بلاڪنگ I/O آپريشن ڪرڻ جي قابل آهي.
  • هڪ ايونٽ هينڊلر هڪ فنڪشن آهي جنهن کي I/O ري ايڪٽر سڏيو ويندو آهي جڏهن هڪ واقعو موصول ٿئي ٿو، جيڪو پوءِ غير بلاڪ ڪرڻ وارو I/O آپريشن انجام ڏئي ٿو.

اهو نوٽ ڪرڻ ضروري آهي ته I/O ري ايڪٽر تعريف جي لحاظ کان سنگل ٿريڊ آهي، پر 1 ٿريڊ: 1 ري ايڪٽر جي تناسب سان ملٽي ٿريڊ واري ماحول ۾ استعمال ٿيڻ کان تصور کي روڪڻ جي ڪا به شيءِ ناهي، اهڙي طرح سڀني سي پي يو ڪور کي ريسائڪل ڪري ٿو.

عمل

اسان عوامي انٽرفيس کي فائل ۾ رکون ٿا 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 ريڪٽر جي جوڙجڪ تي مشتمل آهي فائل بيان ڪندڙ چونڊيندڙ ايپل и hash ٽيبل GHashTable، جيڪو هر ساکٽ ڏانهن نقشو ٺاهي ٿو CallbackData (هڪ واقعو سنڀاليندڙ جي جوڙجڪ ۽ ان لاءِ استعمال ڪندڙ دليل).

ڏيکاريو ريڪٽر ۽ ڪال بيڪ ڊيٽا

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

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

مهرباني ڪري نوٽ ڪريو ته اسان سنڀالڻ جي صلاحيت کي چالو ڪيو آهي نامڪمل قسم انڊيڪس جي مطابق. IN reactor.h اسان ساخت جو اعلان ڪريون ٿا reactor، ۽ ۾ reactor.c اسان ان جي وضاحت ڪريون ٿا، ان ڪري صارف کي ان جي فيلڊ کي واضح طور تي تبديل ڪرڻ کان روڪيو. هي نمونن مان هڪ آهي ڊيٽا لڪائڻ، جيڪو مختصر طور تي C semantics ۾ اچي ٿو.

ڪارڪن 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;
}

اختصار ڪرڻ لاءِ، يوزر ڪوڊ ۾ فنڪشن ڪالز جو سلسلو ھيٺ ڏنل فارم وٺي ويندو:

مڪمل نمايان بيئر-سي I/O ريڪٽر

سنگل سلسلي وارو سرور

I/O ري ايڪٽر کي وڌيڪ لوڊ هيٺ جانچڻ لاءِ، اسان لکنداسين هڪ سادو HTTP ويب سرور جيڪو تصوير سان ڪنهن به درخواست جو جواب ڏيندو.

HTTP پروٽوڪول ڏانهن تڪڙو حوالو

HTTP - هي پروٽوڪول آهي درخواست جي سطح, بنيادي طور تي استعمال ڪيو سرور-براؤزر رابطي لاء.

HTTP آساني سان استعمال ڪري سگھجي ٿو ٽرانسپورٽ پروٽوڪول ٽي پي, پيغام موڪلڻ ۽ وصول ڪرڻ مخصوص فارميٽ ۾ وضاحت.

درخواست فارميٽ

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

  • CRLF ٻن اکرن جو هڪ سلسلو آهي: r и n, درخواست جي پهرين لائن کي الڳ ڪرڻ، هيڊر ۽ ڊيٽا.
  • <КОМАНДА> - مان هڪ CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. برائوزر اسان جي سرور ڏانهن هڪ حڪم موڪليندو GET، مطلب ته "مون کي فائل جو مواد موڪليو."
  • <URI> - يونيفارم وسيلن جي سڃاڻپ ڪندڙ. مثال طور، جيڪڏهن 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، جنهن ۾ هيٺين فنڪشن پروٽوٽائپ شامل آهن:

ڏيکاريو فنڪشن prototypes 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() سڏيو ويندو آهي OS کان پوء هڪ واقعو ٺاهي ٿو 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() سڏيو ويندو آهي OS کان پوء هڪ واقعو ٺاهي ٿو 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() سڏيو ويندو آهي OS کان پوء هڪ واقعو ٺاهي ٿو 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 برائوزر ۾ ۽ ڏسو ته اسان ڇا توقع ڪئي:

مڪمل نمايان بيئر-سي 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 ري ايڪٽر الڳ ٿريڊن ۾ ٺاهي سگھجن ٿا، ان ڪري سڀ سي پي يو ڪور استعمال ڪري سگهجن ٿا. اچو ته هن طريقي کي عملي طور تي رکون:

ڏيکاريو 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, پي، هٽن جي تعداد ۾ اضافو ڪيش، واڌارو 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

مطلوب نتيجو حاصل ڪيو ويو، ۽ ان سان گڏ هڪ دلچسپ گراف ڏيکاري ٿو پروسيس ٿيل درخواستن جي تعداد جي انحصار کي 1 منٽ ۾ ڪنيڪشن جي تعداد تي:

مڪمل نمايان بيئر-سي I/O ريڪٽر

اسان ڏسون ٿا ته ٻه سؤ ڪنيڪشن کان پوء، ٻنهي سرورن لاء پروسيس ٿيل درخواستن جو تعداد تيزيء سان گهٽجي ٿو (گهڻن موضوعن واري ورزن ۾ اهو وڌيڪ قابل ذڪر آهي). ڇا اهو لينڪس TCP/IP اسٽيڪ عمل درآمد سان لاڳاپيل آهي؟ گراف جي هن رويي بابت پنهنجا مفروضا لکڻ لاءِ آزاد ٿيو ۽ تبصرن ۾ ملٽي ٿريڊ ۽ سنگل ٿريڊ آپشنز لاءِ اصلاح.

ڪيئن نوٽ ڪيو تبصرن ۾، هي پرفارمنس ٽيسٽ حقيقي لوڊ هيٺ I/O ريڪٽر جي رويي کي نه ڏيکاريندو آهي، ڇاڪاڻ ته تقريبا هميشه سرور ڊيٽابيس سان رابطو ڪري ٿو، لاگ آئوٽ ڪري ٿو، ڪرپٽوگرافي استعمال ڪري ٿو. TLS وغيره، جنهن جي نتيجي ۾ لوڊ غير يونيفارم (متحرڪ) ٿي ويندو آهي. ٽئين پارٽي جي اجزاء سان گڏ ٽيسٽ ڪيا ويندا مضمون ۾ I/O پروڪٽر بابت.

I/O ري ايڪٽر جا نقصان

توهان کي سمجهڻ جي ضرورت آهي ته I/O ري ايڪٽر ان جي خرابين کان سواء ناهي، يعني:

  • گھڻن موضوعن واري ماحول ۾ I/O ريڪٽر استعمال ڪرڻ ڪجھ وڌيڪ ڏکيو آھي، ڇاڪاڻ ته توهان کي دستي طور تي وهڪري کي منظم ڪرڻو پوندو.
  • مشق ڏيکاري ٿي ته اڪثر ڪيسن ۾ لوڊ غير يونيفارم هوندو آهي، جنهن ڪري هڪ ٿريڊ لاگنگ ٿي سگهي ٿو جڏهن ته ٻيو ڪم ۾ مصروف آهي.
  • جيڪڏهن هڪ ايونٽ هينڊلر هڪ سلسلي کي بلاڪ ڪري ٿو، سسٽم چونڊيندڙ پاڻ کي به بلاڪ ڪري ڇڏيندو، جنهن جي ڪري مشڪل سان ڳولڻ وارا بگ پيدا ٿي سگهن ٿا.

انهن مسئلن کي حل ڪري ٿو I/O پروڪٽر, جنهن ۾ اڪثر هڪ شيڊيولر هوندو آهي جيڪو لوڊ کي سلسلن جي تلاءَ ۾ ورهائي ٿو، ۽ ان ۾ وڌيڪ آسان API پڻ آهي. اسان ان بابت بعد ۾ ڳالهائينداسين، منهنجي ٻئي مضمون ۾.

ٿڪل

هي اهو آهي جتي اسان جو سفر نظريي کان سڌو پروفائلر جي نڪرڻ ۾ اچي چڪو آهي.

توھان کي ھن تي نه رھڻ گھرجي، ڇو ته نيٽ ورڪ سافٽ ويئر لکڻ لاءِ ٻيا به ڪيترائي دلچسپ طريقا آھن مختلف سطحن جي سهولت ۽ رفتار سان. دلچسپ، منهنجي خيال ۾، لنڪ هيٺ ڏنل آهن.

DO новых встреч!

دلچسپ منصوبا

مون کي ٻيو ڇا پڙهڻ گهرجي؟

جو ذريعو: www.habr.com

تبصرو شامل ڪريو