Archive

Posts Tagged ‘pthreads’

2 слова о серверах

September 16th, 2011 No comments

Сегодня я хочу поделиться технологией создания масштабируемого сервера на основе linux pthreads и epoll.

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

 

Теперь подробнее:

создаём слушающий сокет, причём настоятельно рекомендую использовать опции SO_REUSEADDR и TCP_NODELAY. Да, ещё рекомендую явно указать линжер в нули.

После этого – время создавать потоки. Каждому создаваемому потоку – передаём параметром сокет созданный ранее.

 

pthread_create(&th[t_cnt], &ta,(void*(*)(void *)) server_thread,(void *)&sock)

server_thread – это указатель на функцию потока
sock – это и есть указатель на серверный сокет созданный в начале.

 

Вот как-то так.
Теперь самое главное – функция потока обрабатывающая соединения:

 
void *server_thread(void *fd) {
int *sock = (int *)fd;
int localSock = *sock;

Так преобразовываем поинтер в сокет. Ну, на всякий случай, если кто не знает.

 

struct epoll_event ev, *events;
ep_fd = epoll_create(num_sockets_per_sender);
events = (struct epoll_event *)malloc(sizeof(struct epoll_event) * epoll_room_in_receiver);
ev.events = EPOLLIN | EPOLLERR | EPOLLHUP | EPOLLET;
ev.data.fd = localSock;
epoll_ctl(ep_fd, EPOLL_CTL_ADD,localSock, &ev);

 

Создаём структуру для еполла. Вообще принцип epoll – прост. В структуру записываются сокеты, на которых отслеживаются изменения. Т.е. если в какой-то сокет прошла запись клиентом – ядро (epoll обслуживается ядром Linux) – подаёт сигнал, который вытаскивает программу из ожидания в функции epoll_wait.

Вот как-то так:

while (1) {
int num_fds = epoll_wait(ep_fd, events, num_sockets_per_sender, -1);
.........
};

 

Сиё работает крайне просто, и является абсолютно thread-safe.
Все потоки которые вы создали, заваливаются на epoll_wait, и ждут когда в ep_fd появятся изменения – когда подключится новый клиент, или напишет чего-нибудь кто-то из уже подключенных клиентов.

Ну а дальше – совсем просто. Как только что-то появляется (в количестве в пределах 3 параметра функции epoll_wait) – функция возвращает количество евентов, ну и структуру с сокетами в которых произошли эвенты, и описания происшедших эвентов.

Т.е. например:

if (events.events & (EPOLLHUP | EPOLLERR)) {
fputs("epoll: EPOLLERR", stderr);
close(events.data.fd);
continue;
};

 

В общем-то просто и понятно.
В этом цикле и должна происходить обработка соединений – чтение, запись, закрытие сокетов.

Самое важное в этом процессе – добавлять новые сокеты в структуру epoll.

 

if (events.data.fd == localSock) {
struct sockaddr remote_addr;
socklen_t addr_size = sizeof(remote_addr);
 
int connection = accept(localSock, &remote_addr, &addr_size);
if (connection == -1) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
serr("accept");
}
continue;
};
fcntl(connection, F_SETFL,O_NONBLOCK | fcntl(connection, F_GETFL, 0));
ev.data.fd = connection;
epoll_ctl(ep_fd, EPOLL_CTL_ADD, connection, &ev);
continue;
};

 

Идея проста – если событие произошло на серверном сокете – это означает только одно – появился новый клиент. Т.е. надо принимать коннект и добавлять в структуру epoll для дальнейшего мониторинга.

Ну и двигаясь к концу – пара общих советов, которые вы и так наверняка знаете:
При работе с еполлом – используйте не блокирующиеся сокеты. Это показано выше, но обращу отдельное внимание.

fcntl(connection, F_SETFL,O_NONBLOCK | fcntl(connection, F_GETFL, 0));

При работе с многопоточными приложениями, не надо бояться использовать потоки. В некоторых случаях потоки вообще единственно приемлемое решение.

Мутексы. Мутексы это основа многопоточности. Однако, злоупотребляя мутексами – можно свести на ноль все преимущества многопоточной системы.

Да, два слова о производительности:
Сервер, кусочки кода которого я тут приводил, продемонстрировал примерно вот такие результаты: при 900 обращениях в секунду, полная обработка 1 соединения занимает примерно 27 миллисекунд. Со всей математикой, регекспами, обсчётами, массивами и даже поиском по огромным бинарным деревьям.
Сервер – Dual Quad Core Xeon 2.4Ghz, x64 Debian, 24GB ram.

Из дополнительных параметров компиляции – только -msse3 (только если у вас современный Intel), -O3 (общий набор оптимизации компилятора g++).