张东轩的博客

合抱之木,生于毫末;九层之台,起于累土;千里之行,始于足下。

0%

网络编程中的I/O多路复用 - Select & Poll

在上篇文章中已经介绍过I/O多路复用和其他IO模型的区别:网络编程中的I/O多路复用 - I/O Models

实现I/O多路复用的方式主要有selectpoll两种方式

Select()

下面是使用select实现的I/O多路复用,Server开启了一个线程用来对所有的socket描述符进行select,下面是一个使用selec()实现I/O多路复用的例子:

对应的代码如下

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
static int g_svfd;
std::vector<int> g_fds;
pthread_t select_thread;

void select_servermsg(void) {
pthread_attr_t attr;
(void)pthread_attr_init(&attr);
pthread_create(&select_thread, &attr, server_select, NULL);
pthread_attr_destroy(&attr);
}

int accept_client_conn(void) {
int cs = accept(g_svfd, NULL, NULL);
if (cs < 0) {
perror("accept");
return -1;
}
g_fds.push_back(cs);
printf("Server Accept acc:%d\n", cs);
return cs;
}

void read_data_from_socket(int sockfd) {
char recv_data[1024] = {0};
long rc = recv(sockfd, recv_data, sizeof(recv_data), 0);
if (rc <= 0) {
printf("Server Read Failed cs:%ld\n", rc);
return;
}
printf("Server Socket acc:%d revc:%s \n", sockfd, recv_data);
}

void mg_add_to_set(int sock, fd_set *set, int *max_fd) {
if (sock != INVALID_SOCKET && sock < (int) FD_SETSIZE) {
FD_SET(sock, set);
if (*max_fd == INVALID_SOCKET || sock > *max_fd) {
*max_fd = sock;
}
}
}

void *server_select(void *udata) {
g_selecting = true;
while (g_selecting) {
int milli = 1500;
struct timeval tv;
tv.tv_sec = milli / 1000;
tv.tv_usec = (milli % 1000) * 1000;

fd_set read_set, err_set;
FD_ZERO(&read_set);
FD_ZERO(&err_set);
// https://stackoverflow.com/questions/36415680/setting-up-select-and-write-fds-in-c
int max_fd = INVALID_SOCKET;

for (auto sock = g_fds.begin(); sock != g_fds.end(); sock++) {
mg_add_to_set(*sock, &read_set, &max_fd);
mg_add_to_set(*sock, &err_set, &max_fd);
}

int num_selected = select((int) max_fd + 1, &read_set, NULL, &err_set, &tv);
if (num_selected <= 0) {
continue;
}

unsigned long size = g_fds.size();
for (int i = 0; i < size; i++) {
int sock = g_fds[i];
if (FD_ISSET(sock, &read_set)) {
if (sock == g_svfd) {
accept_client_conn();
} else {
read_data_from_socket(sock);
}
continue;
}

if (FD_ISSET(sock, &err_set)) {
printf("select:%d error\n", sock);
continue;
}
}
}
return NULL;
}

上面select使用的FD_SETFD_ISSET定义如下:

1
2
3
4
5
6
7
#ifndef FD_ISSET
#define FD_ISSET(n, p) __DARWIN_FD_ISSET(n, p)
#endif /* FD_ISSET */

#ifndef FD_SET
#define FD_SET(n, p) __DARWIN_FD_SET(n, p)
#endif /* FD_SET */

在iOS系统中,上面的__DARWIN_FD_ISSET__DARWIN_FD_SET定义在_fd_def.h中,文件内容如下:

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
#define __DARWIN_FD_SETSIZE     1024
#define __DARWIN_NBBY 8 /* bits in a byte */
#define __DARWIN_NFDBITS (sizeof(__int32_t) * __DARWIN_NBBY) /* bits per mask */
#define __DARWIN_howmany(x, y) ((((x) % (y)) == 0) ? ((x) / (y)) : (((x) / (y)) + 1)) /* # y's == x bits? */

typedef struct fd_set {
__int32_t fds_bits[__DARWIN_howmany(__DARWIN_FD_SETSIZE, __DARWIN_NFDBITS)];
} fd_set;

/* This inline avoids argument side-effect issues with FD_ISSET() */
int __darwin_fd_isset(int _fd, const struct fd_set *_p) {
return _p->fds_bits[(unsigned long)_fd / __DARWIN_NFDBITS] & ((__int32_t)(((unsigned long)1) << ((unsigned long)_fd % __DARWIN_NFDBITS)));
}

void __darwin_fd_set(int _fd, struct fd_set *const _p){
(_p->fds_bits[(unsigned long)_fd / __DARWIN_NFDBITS] |= ((__int32_t)(((unsigned long)1) << ((unsigned long)_fd % __DARWIN_NFDBITS))));
}

void __darwin_fd_clr(int _fd, struct fd_set *const _p) {
(_p->fds_bits[(unsigned long)_fd / __DARWIN_NFDBITS] &= ~((__int32_t)(((unsigned long)1) << ((unsigned long)_fd % __DARWIN_NFDBITS))));
}

#define __DARWIN_FD_SET(n, p) __darwin_fd_set((n), (p))
#define __DARWIN_FD_CLR(n, p) __darwin_fd_clr((n), (p))
#define __DARWIN_FD_ISSET(n, p) __darwin_fd_isset((n), (p))

select采用的是位掩码的模型,其使用的fds_bits是一个有32个int32值的数组,一共有32 * 32 = 1024 bit位,数组中的每一位代表一个文件句柄的掩码,这个fds_bits只能支持fd < 1024的情况,当socket fd > 1024,就会出现无法处理的情况。

poll()

虽然selectpoll都可以实现I/O的多路复用,但是大家更推崇poll(),因为poll是通过传入自定义数组pollfd的形式,对socket描述符并没有1024的限制。下面是使用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
//  https://www.ibm.com/docs/en/i/7.4?topic=designs-using-poll-instead-select
void *server_poll(void *udata) {
g_polling = true;
while (g_polling) {
int32_t maxsock = 0;
unsigned long fd_size = g_fds.size() * sizeof(pollfd);
struct pollfd *pollfds = (struct pollfd *)malloc(fd_size);

for (auto sock = g_fds.begin(); sock != g_fds.end(); sock++) {
pollfds[maxsock].fd = *sock;
pollfds[maxsock].events = POLLIN;
++maxsock;
}

int rc = poll(pollfds, maxsock, 10);
if (rc < 0) {
continue;;
}

for (int j = 0; j < maxsock; j++) {
if (pollfds[j].revents & POLLIN) {
int socket = pollfds[j] .fd;
if (socket == g_svfd) {
accept_client_conn();
} else {
read_data_from_socket(socket);
}
}
}
}
return NULL;
}

void poll_servermsg(void) {
pthread_attr_t attr;
(void)pthread_attr_init(&attr);
pthread_create(&poll_thread, &attr, server_poll, NULL);
pthread_attr_destroy(&attr);
}

the difference of select and poll

这里总结一下select和poll的不同:
1、select使用的是定长数组,而poll是通过用户自定义数组长度的形式(pollfd[]);
2、select只支持最大fd < 1024,如果单个进程的文件句柄数超过1024,select就不能用了。poll在接口上无限制,考虑到每次都要拷贝到内核,一般文件句柄多的情况下建议用epoll;
3、select由于使用的是位运算,所以select需要分别设置read/write/error fds的掩码。而poll是通过设置数据结构中fd和event参数来实现read/write,比如读为POLLIN,写为POLLOUT,出错为POLLERR;
4、select中fd_set是被内核和用户共同修改的,所以要么每次FD_CLR再FD_SET,要么备份一份memcpy进去。而poll中用户修改的是events,系统修改的是revents。所以参考muduo的代码,都不需要自己去清除revents,从而使得代码更加简洁;
5、select的timeout为NULL时表示无限等待,否则是指定的超时目标时间;poll的timeout为-1表示无限等待。所以有用select来实现usleep的;

源码:https://github.com/zhangdongxuan/CodeSlice/tree/master/network/MultiplexingIO/ServerTest