参考资料:https://deepwiki.com/qinguoyi/TinyWebServer

功能梳理

select

  • 使用 线性表 来表示文件描述符集合
  • 文件描述符 在 用户态 被加入文件描述符集合中
  • 用户态到内核态的拷贝
  • 遍历
  • 只支持 LT 模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>

int some_task()
{
int sockfd = g_sockfd;

fd_set rfds;
struct timeval tv;
int error = 0;

while(1) {
tv.tv_sec = 5;
tv.tv_usec = 0;

FD_ZERO(&rfds);
FD_SET(sockfd, &rfds);

error = select(sockfd + 1, &rfds, NULL, NULL, &tv);
if (error == 0) { /* timeout */
continue;
}
else if (error < 0) {
if(error == EINTR) { /* interrupted */
continue;
}
else { /* other error */
break;
}
}

if(FD_ISSET(sockfd, &rfds)) {
some_recv(sockfd);
}
}

return error;
}

poll

  • 使用 链表 来表示文件描述符集合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void time_poll(void)
{
struct pollfd *pfds;
nfds_t nfds;
int ready;
TIMER *timer;

FOREACH(timer, &g_timer_head, list)
{
nfds++;
}

pfds = calloc(nfds, sizeof(struct pollfd));
if (pfds == NULL)
err(EXIT_FAILURE, "malloc");

for (nfds_t j = 0; j < nfds; j++) {
pfds[j].fd = timer->fd;
pfds[j].events = POLLIN;
}

while(1)
{
ready = poll(pfds, nfds, -1);
if (ready > 0) {
time_poll_handle(pfds, nfds)
}
else if (ready < 0) {
if(errno == EINTR) {
continue;
}
else {
return;
}
}
else {
continue;
}
}
}

epoll

  • 使用 红黑树 来表示文件描述符集合,同时维护一个 ready list 用来保存已经就绪的事件

  • 文件描述符集合 维护在 内核态

  • 支持 LT 和 ET 模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;

/* Code to set up listening socket, 'listen_sock',
(socket(), bind(), listen()) omitted. */

epollfd = epoll_create1(0);
if (epollfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}

ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}

for (;;) {
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}

for (n = 0; n < nfds; ++n) {
if (events[n].data.fd == listen_sock) {
conn_sock = accept(listen_sock,
(struct sockaddr *) &addr, &addrlen);
if (conn_sock == -1) {
perror("accept");
exit(EXIT_FAILURE);
}
setnonblocking(conn_sock);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
&ev) == -1) {
perror("epoll_ctl: conn_sock");
exit(EXIT_FAILURE);
}
} else {
do_use_fd(events[n].data.fd);
}
}
}

总结

  • 当监测的 fd 数量较少,且各个 fd 都很活跃的情况下,建议使用 select 和 poll

  • 当监测的 fd 数量较多,且单位时间内仅部分 fd 活跃的情况下,建议使用 epoll

LT 和 ET

LT (水平触发)

ET (边缘触发)

在使用 ET 模式时,必须要保证该文件描述符是非阻塞的(确保在没有数据可读时,该文件描述符不会一直阻塞);并且每次调用 readwrite 的时候都必须等到它们返回 EWOULDBLOCK(确保所有数据都已读完或写完)。

Reactor 和 Proactor

服务器程序通常需要处理三类事件:I/O事件,信号及定时事件。

有两种事件处理模式:

  • Reactor 模式:要求主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生(可读、可写),若有,则立即通知工作线程(逻辑单元),将 socket 可读可写事件放入请求队列,交给工作线程处理。

  • Proactor 模式:将所有的I/O操作都交给主线程和内核来处理(进行读、写),工作线程仅负责处理逻辑,如主线程读完成后 users[sockfd].read(),选择一个工作线程来处理客户请求 pool->append(users + sockfd)

通常使用同步 I/O 模型(如 epoll_wait)实现 Reactor,使用异步 I/O(如 aio_read 和 aio_write )实现 Proactor。

本项目使用同步 I/O 模拟的 Proactor 事件处理模式。

同步 I/O 和 异步 I/O

同步(阻塞) I/O: 在一个线程中,CPU执行代码的速度极快,然而,一旦遇到IO操作,如读写文件、发送网络数据时,就需要等待 IO 操作完成,才能继续进行下一步操作。

异步(非阻塞) I/O:当代码需要执行一个耗时的 IO 操作时,它只发出 IO 指令,并不等待 IO 结果,然后就去执行其他代码了。一段时间后,当 IO 返回结果时,再通知 CPU 进行处理。

Asan