ಪೂರ್ಣ-ವೈಶಿಷ್ಟ್ಯದ ಬೇರ್-C I/O ರಿಯಾಕ್ಟರ್

ಪೂರ್ಣ-ವೈಶಿಷ್ಟ್ಯದ ಬೇರ್-C I/O ರಿಯಾಕ್ಟರ್

ಪರಿಚಯ

I/O ರಿಯಾಕ್ಟರ್ (ಏಕ ಥ್ರೆಡ್ ಈವೆಂಟ್ ಲೂಪ್) ಹೆಚ್ಚಿನ ಲೋಡ್ ಸಾಫ್ಟ್‌ವೇರ್ ಬರೆಯಲು ಒಂದು ಮಾದರಿಯಾಗಿದೆ, ಇದನ್ನು ಅನೇಕ ಜನಪ್ರಿಯ ಪರಿಹಾರಗಳಲ್ಲಿ ಬಳಸಲಾಗುತ್ತದೆ:

ಈ ಲೇಖನದಲ್ಲಿ, ನಾವು I/O ರಿಯಾಕ್ಟರ್‌ನ ಒಳ ಮತ್ತು ಹೊರಗನ್ನು ನೋಡುತ್ತೇವೆ ಮತ್ತು ಅದು ಹೇಗೆ ಕಾರ್ಯನಿರ್ವಹಿಸುತ್ತದೆ, 200 ಕ್ಕಿಂತ ಕಡಿಮೆ ಕೋಡ್‌ಗಳಲ್ಲಿ ಅನುಷ್ಠಾನವನ್ನು ಬರೆಯಿರಿ ಮತ್ತು 40 ಮಿಲಿಯನ್ ವಿನಂತಿಗಳು/ನಿಮಿಷಕ್ಕಿಂತ ಸರಳವಾದ HTTP ಸರ್ವರ್ ಪ್ರಕ್ರಿಯೆಯನ್ನು ಮಾಡುತ್ತೇವೆ.

ಮುನ್ನುಡಿ

  • I/O ರಿಯಾಕ್ಟರ್‌ನ ಕಾರ್ಯನಿರ್ವಹಣೆಯನ್ನು ಅರ್ಥಮಾಡಿಕೊಳ್ಳಲು ಸಹಾಯ ಮಾಡಲು ಲೇಖನವನ್ನು ಬರೆಯಲಾಗಿದೆ ಮತ್ತು ಆದ್ದರಿಂದ ಅದನ್ನು ಬಳಸುವಾಗ ಅಪಾಯಗಳನ್ನು ಅರ್ಥಮಾಡಿಕೊಳ್ಳಿ.
  • ಲೇಖನವನ್ನು ಅರ್ಥಮಾಡಿಕೊಳ್ಳಲು ಮೂಲಭೂತ ಜ್ಞಾನದ ಅಗತ್ಯವಿದೆ. ಸಿ ಭಾಷೆ ಮತ್ತು ನೆಟ್ವರ್ಕ್ ಅಪ್ಲಿಕೇಶನ್ ಅಭಿವೃದ್ಧಿಯಲ್ಲಿ ಕೆಲವು ಅನುಭವ.
  • ಎಲ್ಲಾ ಕೋಡ್ ಅನ್ನು ಸಿ ಭಾಷೆಯಲ್ಲಿ ಕಟ್ಟುನಿಟ್ಟಾಗಿ ಬರೆಯಲಾಗಿದೆ (ಎಚ್ಚರಿಕೆ: ದೀರ್ಘ ಪಿಡಿಎಫ್) C11 ಮಾನದಂಡಕ್ಕೆ Linux ಗಾಗಿ ಮತ್ತು ಲಭ್ಯವಿದೆ GitHub.

ಇದು ಏಕೆ ಅಗತ್ಯ?

ಇಂಟರ್ನೆಟ್‌ನ ಹೆಚ್ಚುತ್ತಿರುವ ಜನಪ್ರಿಯತೆಯೊಂದಿಗೆ, ವೆಬ್ ಸರ್ವರ್‌ಗಳು ಏಕಕಾಲದಲ್ಲಿ ಹೆಚ್ಚಿನ ಸಂಖ್ಯೆಯ ಸಂಪರ್ಕಗಳನ್ನು ನಿಭಾಯಿಸಲು ಪ್ರಾರಂಭಿಸಿದವು, ಮತ್ತು ಆದ್ದರಿಂದ ಎರಡು ವಿಧಾನಗಳನ್ನು ಪ್ರಯತ್ನಿಸಲಾಯಿತು: ಹೆಚ್ಚಿನ ಸಂಖ್ಯೆಯ OS ಥ್ರೆಡ್‌ಗಳಲ್ಲಿ I/O ಅನ್ನು ನಿರ್ಬಂಧಿಸುವುದು ಮತ್ತು I/O ಅನ್ನು ನಿರ್ಬಂಧಿಸದಿರುವುದು ಈವೆಂಟ್ ಅಧಿಸೂಚನೆ ವ್ಯವಸ್ಥೆ, ಇದನ್ನು "ಸಿಸ್ಟಮ್ ಸೆಲೆಕ್ಟರ್" ಎಂದೂ ಕರೆಯುತ್ತಾರೆ (ಎಪೋಲ್/kqueue/IOCP/ ಇತ್ಯಾದಿ).

ಪ್ರತಿ ಒಳಬರುವ ಸಂಪರ್ಕಕ್ಕಾಗಿ ಹೊಸ OS ಥ್ರೆಡ್ ಅನ್ನು ರಚಿಸುವುದನ್ನು ಮೊದಲ ವಿಧಾನವು ಒಳಗೊಂಡಿರುತ್ತದೆ. ಇದರ ಅನನುಕೂಲವೆಂದರೆ ಕಳಪೆ ಸ್ಕೇಲೆಬಿಲಿಟಿ: ಆಪರೇಟಿಂಗ್ ಸಿಸ್ಟಮ್ ಅನೇಕವನ್ನು ಕಾರ್ಯಗತಗೊಳಿಸಬೇಕಾಗುತ್ತದೆ ಸಂದರ್ಭ ಪರಿವರ್ತನೆಗಳು и ಸಿಸ್ಟಮ್ ಕರೆಗಳು. ಅವು ದುಬಾರಿ ಕಾರ್ಯಾಚರಣೆಗಳಾಗಿವೆ ಮತ್ತು ಪ್ರಭಾವಶಾಲಿ ಸಂಖ್ಯೆಯ ಸಂಪರ್ಕಗಳೊಂದಿಗೆ ಉಚಿತ RAM ನ ಕೊರತೆಗೆ ಕಾರಣವಾಗಬಹುದು.

ಮಾರ್ಪಡಿಸಿದ ಆವೃತ್ತಿ ಮುಖ್ಯಾಂಶಗಳು ಥ್ರೆಡ್ಗಳ ಸ್ಥಿರ ಸಂಖ್ಯೆ (ಥ್ರೆಡ್ ಪೂಲ್), ಇದರಿಂದಾಗಿ ಸಿಸ್ಟಮ್ ಅನ್ನು ಕಾರ್ಯಗತಗೊಳಿಸುವುದನ್ನು ತಡೆಯುತ್ತದೆ, ಆದರೆ ಅದೇ ಸಮಯದಲ್ಲಿ ಹೊಸ ಸಮಸ್ಯೆಯನ್ನು ಪರಿಚಯಿಸುತ್ತದೆ: ಥ್ರೆಡ್ ಪೂಲ್ ಅನ್ನು ಪ್ರಸ್ತುತ ದೀರ್ಘ ಓದುವ ಕಾರ್ಯಾಚರಣೆಗಳಿಂದ ನಿರ್ಬಂಧಿಸಿದ್ದರೆ, ಈಗಾಗಲೇ ಡೇಟಾವನ್ನು ಸ್ವೀಕರಿಸಲು ಸಾಧ್ಯವಾಗುವ ಇತರ ಸಾಕೆಟ್‌ಗಳಿಗೆ ಸಾಧ್ಯವಾಗುವುದಿಲ್ಲ ಹಾಗೆ ಮಾಡು.

ಎರಡನೆಯ ವಿಧಾನವು ಬಳಸುತ್ತದೆ ಈವೆಂಟ್ ಅಧಿಸೂಚನೆ ವ್ಯವಸ್ಥೆ (ಸಿಸ್ಟಮ್ ಸೆಲೆಕ್ಟರ್) OS ನಿಂದ ಒದಗಿಸಲಾಗಿದೆ. ಈ ಲೇಖನವು I/O ಕಾರ್ಯಾಚರಣೆಗಳಿಗೆ ಸನ್ನದ್ಧತೆಯ ಕುರಿತು ಎಚ್ಚರಿಕೆಗಳನ್ನು (ಈವೆಂಟ್‌ಗಳು, ಅಧಿಸೂಚನೆಗಳು) ಆಧರಿಸಿ, ಸಿಸ್ಟಮ್ ಸೆಲೆಕ್ಟರ್‌ನ ಸಾಮಾನ್ಯ ಪ್ರಕಾರವನ್ನು ಚರ್ಚಿಸುತ್ತದೆ. ಅವುಗಳ ಪೂರ್ಣಗೊಂಡ ಬಗ್ಗೆ ಸೂಚನೆಗಳು. ಅದರ ಬಳಕೆಯ ಸರಳೀಕೃತ ಉದಾಹರಣೆಯನ್ನು ಈ ಕೆಳಗಿನ ಬ್ಲಾಕ್ ರೇಖಾಚಿತ್ರದಿಂದ ಪ್ರತಿನಿಧಿಸಬಹುದು:

ಪೂರ್ಣ-ವೈಶಿಷ್ಟ್ಯದ ಬೇರ್-C I/O ರಿಯಾಕ್ಟರ್

ಈ ವಿಧಾನಗಳ ನಡುವಿನ ವ್ಯತ್ಯಾಸವು ಈ ಕೆಳಗಿನಂತಿರುತ್ತದೆ:

  • I/O ಕಾರ್ಯಾಚರಣೆಗಳನ್ನು ನಿರ್ಬಂಧಿಸುವುದು ಅಮಾನತುಗೊಳಿಸು ಬಳಕೆದಾರರ ಹರಿವು ತನಕOS ಸರಿಯಾಗಿ ಆಗುವವರೆಗೆ ಡಿಫ್ರಾಗ್ಮೆಂಟ್ಸ್ ಒಳಬರುವ ಐಪಿ ಪ್ಯಾಕೆಟ್‌ಗಳು ಬೈಟ್ ಸ್ಟ್ರೀಮ್ ಮಾಡಲು (TCP, ಡೇಟಾವನ್ನು ಸ್ವೀಕರಿಸುವುದು) ಅಥವಾ ನಂತರದ ಮೂಲಕ ಕಳುಹಿಸಲು ಆಂತರಿಕ ಬರಹ ಬಫರ್‌ಗಳಲ್ಲಿ ಸಾಕಷ್ಟು ಸ್ಥಳಾವಕಾಶ ಲಭ್ಯವಿರುವುದಿಲ್ಲ ಯಾವುದೂ (ಡೇಟಾ ಕಳುಹಿಸಲಾಗುತ್ತಿದೆ).
  • ಸಿಸ್ಟಮ್ ಸೆಲೆಕ್ಟರ್ ಹೆಚ್ಚುವರಿ ಸಮಯ OS ಎಂದು ಪ್ರೋಗ್ರಾಂಗೆ ತಿಳಿಸುತ್ತದೆ ಈಗಾಗಲೇ ಡಿಫ್ರಾಗ್ಮೆಂಟೆಡ್ ಐಪಿ ಪ್ಯಾಕೆಟ್‌ಗಳು (TCP, ಡೇಟಾ ರಿಸೆಪ್ಷನ್) ಅಥವಾ ಆಂತರಿಕ ಬರಹ ಬಫರ್‌ಗಳಲ್ಲಿ ಸಾಕಷ್ಟು ಸ್ಥಳಾವಕಾಶ ಈಗಾಗಲೇ ಲಭ್ಯವಿದೆ (ಡೇಟಾ ಕಳುಹಿಸಲಾಗುತ್ತಿದೆ).

ಸಂಕ್ಷಿಪ್ತವಾಗಿ ಹೇಳುವುದಾದರೆ, ಪ್ರತಿ 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 ರಿಯಾಕ್ಟರ್ ರಚನೆಯು ಒಳಗೊಂಡಿದೆ ಫೈಲ್ ಡಿಸ್ಕ್ರಿಪ್ಟರ್ ಆಯ್ಕೆಗಾರ ಎಪೋಲ್ и ಹ್ಯಾಶ್ ಕೋಷ್ಟಕಗಳು GHashTable, ಇದು ಪ್ರತಿ ಸಾಕೆಟ್ ಅನ್ನು ನಕ್ಷೆ ಮಾಡುತ್ತದೆ CallbackData (ಈವೆಂಟ್ ಹ್ಯಾಂಡ್ಲರ್‌ನ ರಚನೆ ಮತ್ತು ಅದಕ್ಕೆ ಬಳಕೆದಾರರ ವಾದ).

ರಿಯಾಕ್ಟರ್ ಮತ್ತು ಕಾಲ್ಬ್ಯಾಕ್ ಡೇಟಾ ತೋರಿಸಿ

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

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

ನಿಭಾಯಿಸುವ ಸಾಮರ್ಥ್ಯವನ್ನು ನಾವು ಸಕ್ರಿಯಗೊಳಿಸಿದ್ದೇವೆ ಎಂಬುದನ್ನು ದಯವಿಟ್ಟು ಗಮನಿಸಿ ಅಪೂರ್ಣ ಪ್ರಕಾರ ಸೂಚ್ಯಂಕದ ಪ್ರಕಾರ. IN 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.

ರಿಯಾಕ್ಟರ್_ರನ್() ಕಾರ್ಯವನ್ನು ತೋರಿಸಿ

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> - ಏಕರೂಪದ ಸಂಪನ್ಮೂಲ ಗುರುತಿಸುವಿಕೆ. ಉದಾಹರಣೆಗೆ, 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 (ರಿಟರ್ನ್ ಡೇಟಾ ಪ್ರಕಾರ).
  • <ДАННЫЕ> - ಬಳಕೆದಾರರು ವಿನಂತಿಸಿದ ಡೇಟಾ. ನಮ್ಮ ಸಂದರ್ಭದಲ್ಲಿ, ಇದು ಚಿತ್ರದ ಮಾರ್ಗವಾಗಿದೆ ಎಚ್ಟಿಎಮ್ಎಲ್.

ಕಡತ 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() 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(), ಡೇಟಾವನ್ನು ಕಳುಹಿಸಲು ಸಿದ್ಧವಾಗಿದೆ. ಈ ಕಾರ್ಯವು HTML ಅನ್ನು ಹೊಂದಿರುವ HTTP ಪ್ರತಿಕ್ರಿಯೆಯನ್ನು ಕ್ಲೈಂಟ್‌ಗೆ ಚಿತ್ರದೊಂದಿಗೆ ಕಳುಹಿಸುತ್ತದೆ ಮತ್ತು ನಂತರ ಈವೆಂಟ್ ಹ್ಯಾಂಡ್ಲರ್ ಅನ್ನು ಬದಲಾಯಿಸುತ್ತದೆ 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, ಪಿಜಿಒ, ಹಿಟ್‌ಗಳ ಸಂಖ್ಯೆಯಲ್ಲಿ ಹೆಚ್ಚಳ ಸಂಗ್ರಹ, ಹೆಚ್ಚಳ 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 ನಿಮಿಷದಲ್ಲಿ ಸಂಸ್ಕರಿಸಿದ ವಿನಂತಿಗಳ ಸಂಖ್ಯೆಯ ಅವಲಂಬನೆಯನ್ನು ತೋರಿಸುವ ಆಸಕ್ತಿದಾಯಕ ಗ್ರಾಫ್:

ಪೂರ್ಣ-ವೈಶಿಷ್ಟ್ಯದ ಬೇರ್-C I/O ರಿಯಾಕ್ಟರ್

ಒಂದೆರಡು ನೂರು ಸಂಪರ್ಕಗಳ ನಂತರ, ಎರಡೂ ಸರ್ವರ್‌ಗಳಿಗೆ ಸಂಸ್ಕರಿಸಿದ ವಿನಂತಿಗಳ ಸಂಖ್ಯೆಯು ತೀವ್ರವಾಗಿ ಇಳಿಯುತ್ತದೆ ಎಂದು ನಾವು ನೋಡುತ್ತೇವೆ (ಬಹು-ಥ್ರೆಡ್ ಆವೃತ್ತಿಯಲ್ಲಿ ಇದು ಹೆಚ್ಚು ಗಮನಾರ್ಹವಾಗಿದೆ). ಇದು Linux TCP/IP ಸ್ಟಾಕ್ ಅನುಷ್ಠಾನಕ್ಕೆ ಸಂಬಂಧಿಸಿದೆಯೇ? ಕಾಮೆಂಟ್‌ಗಳಲ್ಲಿ ಬಹು-ಥ್ರೆಡ್ ಮತ್ತು ಏಕ-ಥ್ರೆಡ್ ಆಯ್ಕೆಗಳಿಗಾಗಿ ಗ್ರಾಫ್‌ನ ಈ ನಡವಳಿಕೆ ಮತ್ತು ಆಪ್ಟಿಮೈಸೇಶನ್‌ಗಳ ಕುರಿತು ನಿಮ್ಮ ಊಹೆಗಳನ್ನು ಬರೆಯಲು ಹಿಂಜರಿಯಬೇಡಿ.

ಹೇಗೆ ಗಮನಿಸಿದರು ಕಾಮೆಂಟ್‌ಗಳಲ್ಲಿ, ಈ ಕಾರ್ಯಕ್ಷಮತೆ ಪರೀಕ್ಷೆಯು ನೈಜ ಲೋಡ್‌ಗಳ ಅಡಿಯಲ್ಲಿ I/O ರಿಯಾಕ್ಟರ್‌ನ ನಡವಳಿಕೆಯನ್ನು ತೋರಿಸುವುದಿಲ್ಲ, ಏಕೆಂದರೆ ಸರ್ವರ್ ಯಾವಾಗಲೂ ಡೇಟಾಬೇಸ್‌ನೊಂದಿಗೆ ಸಂವಹನ ನಡೆಸುತ್ತದೆ, ಲಾಗ್‌ಗಳನ್ನು ಉತ್ಪಾದಿಸುತ್ತದೆ, ಜೊತೆಗೆ ಕ್ರಿಪ್ಟೋಗ್ರಫಿಯನ್ನು ಬಳಸುತ್ತದೆ ಟಿಎಲ್ಎಸ್ ಇತ್ಯಾದಿ, ಇದರ ಪರಿಣಾಮವಾಗಿ ಲೋಡ್ ಏಕರೂಪವಲ್ಲದ (ಡೈನಾಮಿಕ್) ಆಗುತ್ತದೆ. I/O ಪ್ರೊಯಾಕ್ಟರ್ ಕುರಿತು ಲೇಖನದಲ್ಲಿ ಮೂರನೇ ವ್ಯಕ್ತಿಯ ಘಟಕಗಳೊಂದಿಗೆ ಪರೀಕ್ಷೆಗಳನ್ನು ಕೈಗೊಳ್ಳಲಾಗುತ್ತದೆ.

I/O ರಿಯಾಕ್ಟರ್‌ನ ಅನಾನುಕೂಲಗಳು

I/O ರಿಯಾಕ್ಟರ್ ಅದರ ನ್ಯೂನತೆಗಳಿಲ್ಲ ಎಂದು ನೀವು ಅರ್ಥಮಾಡಿಕೊಳ್ಳಬೇಕು, ಅವುಗಳೆಂದರೆ:

  • ಬಹು-ಥ್ರೆಡ್ ಪರಿಸರದಲ್ಲಿ I/O ರಿಯಾಕ್ಟರ್ ಅನ್ನು ಬಳಸುವುದು ಸ್ವಲ್ಪ ಹೆಚ್ಚು ಕಷ್ಟಕರವಾಗಿದೆ, ಏಕೆಂದರೆ ನೀವು ಹಸ್ತಚಾಲಿತವಾಗಿ ಹರಿವುಗಳನ್ನು ನಿರ್ವಹಿಸಬೇಕಾಗುತ್ತದೆ.
  • ಹೆಚ್ಚಿನ ಸಂದರ್ಭಗಳಲ್ಲಿ ಲೋಡ್ ಏಕರೂಪವಾಗಿರುವುದಿಲ್ಲ ಎಂದು ಅಭ್ಯಾಸವು ತೋರಿಸುತ್ತದೆ, ಇದು ಒಂದು ಥ್ರೆಡ್ ಲಾಗಿಂಗ್ಗೆ ಕಾರಣವಾಗಬಹುದು ಮತ್ತು ಇನ್ನೊಂದು ಕೆಲಸದಲ್ಲಿ ನಿರತವಾಗಿದೆ.
  • ಒಂದು ಈವೆಂಟ್ ಹ್ಯಾಂಡ್ಲರ್ ಥ್ರೆಡ್ ಅನ್ನು ನಿರ್ಬಂಧಿಸಿದರೆ, ಸಿಸ್ಟಮ್ ಸೆಲೆಕ್ಟರ್ ಸ್ವತಃ ನಿರ್ಬಂಧಿಸುತ್ತದೆ, ಇದು ಹುಡುಕಲು ಕಷ್ಟಕರವಾದ ದೋಷಗಳಿಗೆ ಕಾರಣವಾಗಬಹುದು.

ಈ ಸಮಸ್ಯೆಗಳನ್ನು ಪರಿಹರಿಸುತ್ತದೆ I/O ಪ್ರವರ್ತಕ, ಇದು ಸಾಮಾನ್ಯವಾಗಿ ಥ್ರೆಡ್‌ಗಳ ಪೂಲ್‌ಗೆ ಲೋಡ್ ಅನ್ನು ಸಮವಾಗಿ ವಿತರಿಸುವ ಶೆಡ್ಯೂಲರ್ ಅನ್ನು ಹೊಂದಿದೆ ಮತ್ತು ಹೆಚ್ಚು ಅನುಕೂಲಕರ API ಅನ್ನು ಸಹ ಹೊಂದಿದೆ. ನಾವು ಅದರ ಬಗ್ಗೆ ನಂತರ ಮಾತನಾಡುತ್ತೇವೆ, ನನ್ನ ಇತರ ಲೇಖನದಲ್ಲಿ.

ತೀರ್ಮಾನಕ್ಕೆ

ಇಲ್ಲಿಯೇ ಸಿದ್ಧಾಂತದಿಂದ ನೇರವಾಗಿ ಪ್ರೊಫೈಲರ್ ಎಕ್ಸಾಸ್ಟ್‌ಗೆ ನಮ್ಮ ಪ್ರಯಾಣವು ಕೊನೆಗೊಂಡಿದೆ.

ನೀವು ಇದರ ಮೇಲೆ ವಾಸಿಸಬಾರದು, ಏಕೆಂದರೆ ವಿವಿಧ ಹಂತದ ಅನುಕೂಲತೆ ಮತ್ತು ವೇಗದೊಂದಿಗೆ ನೆಟ್ವರ್ಕ್ ಸಾಫ್ಟ್ವೇರ್ ಅನ್ನು ಬರೆಯಲು ಹಲವು ಸಮಾನವಾದ ಆಸಕ್ತಿದಾಯಕ ವಿಧಾನಗಳಿವೆ. ಆಸಕ್ತಿದಾಯಕ, ನನ್ನ ಅಭಿಪ್ರಾಯದಲ್ಲಿ, ಲಿಂಕ್ಗಳನ್ನು ಕೆಳಗೆ ನೀಡಲಾಗಿದೆ.

!

ಆಸಕ್ತಿದಾಯಕ ಯೋಜನೆಗಳು

ಇನ್ನೇನು ಓದಬೇಕು?

ಮೂಲ: www.habr.com

ಕಾಮೆಂಟ್ ಅನ್ನು ಸೇರಿಸಿ