Có an toàn để chia sẻ cùng một Epoll fd (không phải socket fd) trong một số chủ đề .
Vâng, nó là an toàn - giao diện epoll(7)
là thread-safe - nhưng bạn nên cẩn thận khi làm như vậy, ít nhất bạn nên sử dụng EPOLLET
(chế độ edge-kích hoạt, như trái ngược với mặc định cấp kích hoạt) để tránh đánh thức giả mạo trong các chủ đề khác. Điều này là do chế độ kích hoạt cấp sẽ đánh thức mọi chuỗi khi có sự kiện mới để xử lý. Vì chỉ có một luồng sẽ xử lý nó, điều này sẽ đánh thức hầu hết các luồng không cần thiết.
Nếu epfd chia sẻ được sử dụng mỗi thread sẽ phải vượt qua các sự kiện riêng mảng hoặc một mảng sự kiện chia sẻ để epoll_wait()
Có, bạn cần một mảng sự kiện riêng biệt trên mỗi thread, hoặc nếu không bạn sẽ có điều kiện chủng tộc và những điều khó chịu có thể xảy ra. Ví dụ, bạn có thể có một chuỗi vẫn đang lặp qua các sự kiện được trả về bởi epoll_wait(2)
và xử lý các yêu cầu khi đột nhiên một chuỗi khác gọi epoll_wait(2)
với cùng một mảng và sau đó các sự kiện bị ghi đè cùng lúc với chuỗi khác đang đọc chúng. Không tốt! Bạn hoàn toàn cần một mảng riêng biệt cho mỗi chuỗi.
Giả sử bạn có một mảng riêng biệt cho mỗi chuỗi, hoặc khả năng - chờ đợi trên cùng một fd epoll hoặc có foll epoll riêng biệt cho mỗi chuỗi - sẽ hoạt động tốt như nhau, nhưng lưu ý rằng ngữ nghĩa khác nhau. Với foll được chia sẻ chung trên toàn cầu, mỗi chuỗi chờ yêu cầu từ bất kỳ ứng dụng nào, vì tất cả khách hàng đều được thêm vào cùng một fd epoll. Với một foll epoll riêng biệt cho mỗi thread, sau đó mỗi thread về cơ bản chịu trách nhiệm cho một tập hợp con của các máy khách (những máy khách đã được chấp nhận bởi chủ đề đó).
Điều này có thể không liên quan đến hệ thống của bạn hoặc có thể tạo ra sự khác biệt lớn. Ví dụ, nó có thể xảy ra rằng một thread là không may, đủ để có được một nhóm người sử dụng năng lượng mà làm cho yêu cầu nặng và thường xuyên, để lại thread overworked, trong khi các chủ đề khác với khách hàng ít tích cực gần như nhàn rỗi. Nó sẽ không công bằng sao? Mặt khác, có thể bạn muốn chỉ có một số chủ đề giao dịch với một lớp người dùng cụ thể và trong trường hợp đó, có thể có ý nghĩa khi có các fds epoll khác nhau trên mỗi chuỗi. Như thường lệ, bạn cần phải xem xét cả hai khả năng, đánh giá thương mại, suy nghĩ về vấn đề cụ thể của bạn, và đưa ra quyết định.
Dưới đây là ví dụ sử dụng fd được chia sẻ chung trên toàn cầu.Ban đầu tôi không có ý định làm tất cả những điều này, nhưng một điều đã dẫn đến việc khác, và, tốt, thật vui và tôi nghĩ nó có thể giúp bạn bắt đầu. Đó là một máy chủ echo lắng nghe trên cổng 3000 và có một nhóm gồm 20 chủ đề sử dụng epoll để đồng thời chấp nhận các máy khách mới và phục vụ các yêu cầu.
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#define SERVERPORT 3000
#define SERVERBACKLOG 10
#define THREADSNO 20
#define EVENTS_BUFF_SZ 256
static int serversock;
static int epoll_fd;
static pthread_t threads[THREADSNO];
int accept_new_client(void) {
int clientsock;
struct sockaddr_in addr;
socklen_t addrlen = sizeof(addr);
if ((clientsock = accept(serversock, (struct sockaddr *) &addr, &addrlen)) < 0) {
return -1;
}
char ip_buff[INET_ADDRSTRLEN+1];
if (inet_ntop(AF_INET, &addr.sin_addr, ip_buff, sizeof(ip_buff)) == NULL) {
close(clientsock);
return -1;
}
printf("*** [%p] Client connected from %s:%" PRIu16 "\n", (void *) pthread_self(),
ip_buff, ntohs(addr.sin_port));
struct epoll_event epevent;
epevent.events = EPOLLIN | EPOLLET;
epevent.data.fd = clientsock;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, clientsock, &epevent) < 0) {
perror("epoll_ctl(2) failed attempting to add new client");
close(clientsock);
return -1;
}
return 0;
}
int handle_request(int clientfd) {
char readbuff[512];
struct sockaddr_in addr;
socklen_t addrlen = sizeof(addr);
ssize_t n;
if ((n = recv(clientfd, readbuff, sizeof(readbuff)-1, 0)) < 0) {
return -1;
}
if (n == 0) {
return 0;
}
readbuff[n] = '\0';
if (getpeername(clientfd, (struct sockaddr *) &addr, &addrlen) < 0) {
return -1;
}
char ip_buff[INET_ADDRSTRLEN+1];
if (inet_ntop(AF_INET, &addr.sin_addr, ip_buff, sizeof(ip_buff)) == NULL) {
return -1;
}
printf("*** [%p] [%s:%" PRIu16 "] -> server: %s", (void *) pthread_self(),
ip_buff, ntohs(addr.sin_port), readbuff);
ssize_t sent;
if ((sent = send(clientfd, readbuff, n, 0)) < 0) {
return -1;
}
readbuff[sent] = '\0';
printf("*** [%p] server -> [%s:%" PRIu16 "]: %s", (void *) pthread_self(),
ip_buff, ntohs(addr.sin_port), readbuff);
return 0;
}
void *worker_thr(void *args) {
struct epoll_event *events = malloc(sizeof(*events)*EVENTS_BUFF_SZ);
if (events == NULL) {
perror("malloc(3) failed when attempting to allocate events buffer");
pthread_exit(NULL);
}
int events_cnt;
while ((events_cnt = epoll_wait(epoll_fd, events, EVENTS_BUFF_SZ, -1)) > 0) {
int i;
for (i = 0; i < events_cnt; i++) {
assert(events[i].events & EPOLLIN);
if (events[i].data.fd == serversock) {
if (accept_new_client() == -1) {
fprintf(stderr, "Error accepting new client: %s\n",
strerror(errno));
}
} else {
if (handle_request(events[i].data.fd) == -1) {
fprintf(stderr, "Error handling request: %s\n",
strerror(errno));
}
}
}
}
if (events_cnt == 0) {
fprintf(stderr, "epoll_wait(2) returned 0, but timeout was not specified...?");
} else {
perror("epoll_wait(2) error");
}
free(events);
return NULL;
}
int main(void) {
if ((serversock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
perror("socket(2) failed");
exit(EXIT_FAILURE);
}
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(SERVERPORT);
serveraddr.sin_addr.s_addr = INADDR_ANY;
if (bind(serversock, (const struct sockaddr *) &serveraddr, sizeof(serveraddr)) < 0) {
perror("bind(2) failed");
exit(EXIT_FAILURE);
}
if (listen(serversock, SERVERBACKLOG) < 0) {
perror("listen(2) failed");
exit(EXIT_FAILURE);
}
if ((epoll_fd = epoll_create(1)) < 0) {
perror("epoll_create(2) failed");
exit(EXIT_FAILURE);
}
struct epoll_event epevent;
epevent.events = EPOLLIN | EPOLLET;
epevent.data.fd = serversock;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, serversock, &epevent) < 0) {
perror("epoll_ctl(2) failed on main server socket");
exit(EXIT_FAILURE);
}
int i;
for (i = 0; i < THREADSNO; i++) {
if (pthread_create(&threads[i], NULL, worker_thr, NULL) < 0) {
perror("pthread_create(3) failed");
exit(EXIT_FAILURE);
}
}
/* main thread also contributes as worker thread */
worker_thr(NULL);
return 0;
}
Một vài lưu ý:
main()
nên trở int
, không void
(như bạn thấy trong ví dụ của bạn)
- Luôn luôn đối phó với mã lỗi lại. Nó rất phổ biến để bỏ qua chúng và khi mọi thứ phá vỡ thật khó để biết những gì đã xảy ra.
- Mã giả định rằng không có yêu cầu nào lớn hơn 511 byte (được xem bởi kích thước bộ đệm trong
handle_request()
). Nếu một yêu cầu lớn hơn, có thể một số dữ liệu còn lại trong socket trong một thời gian rất dài, vì epoll_wait(2)
sẽ không báo cáo cho đến khi một sự kiện mới xảy ra trên bộ mô tả tệp đó (vì chúng tôi đang sử dụng EPOLLET
). Trong trường hợp xấu nhất, khách hàng có thể không bao giờ thực sự gửi bất kỳ dữ liệu mới nào và chờ một câu trả lời mãi mãi.
- Mã in số nhận dạng chuỗi cho mỗi yêu cầu giả định rằng
pthread_t
là loại con trỏ mờ. Thật vậy, pthread_t
là một loại con trỏ trong Linux, nhưng nó có thể là một loại số nguyên trong các nền tảng khác, vì vậy đây không phải là di động. Tuy nhiên, đó có lẽ không phải là một vấn đề, vì epoll là Linux cụ thể, do đó, mã không phải là di động anyway.
- Giả định rằng không có yêu cầu nào khác từ cùng một khách hàng đến khi chuỗi vẫn đang phục vụ yêu cầu từ khách hàng đó. Nếu yêu cầu mới đến trong thời gian chờ đợi và một chuỗi khác bắt đầu phân phối, chúng tôi có điều kiện chủng tộc và khách hàng sẽ không nhất thiết nhận được thông báo echo theo cùng thứ tự mà anh gửi cho họ (tuy nhiên,
write(2)
là nguyên tử, vì vậy trong khi trả lời có thể ra khỏi trật tự, họ sẽ không xen kẽ).
Cảm ơn câu trả lời toàn diện. Đó là sự giúp đỡ to lớn. – MiJo
@MiJo Vui vì tôi có thể giúp. Đó là một câu hỏi tuyệt vời :) –