首先,每个客户端连接在Linux系统下,都有一个文件描述符fd与之对应,文件描述符有一个编号,不同的编号表示不同的连接。
1、select系统调用
select系统调用有一个重要参数,为fd文件描述符集合,即你要监听哪些文件描述符(哪些连接),这个文件描述符集合rset用一个bitmap位图表示,位图大小为1024,即最多只能监听1024个客户端连接。
当发起系统调用时,会将rset拷贝到内核态,然后内核态监听有没有数据可以处理,监听的所有文件描述符都没有数据的话会一直阻塞,直到有数据时,将有数据的fd索引置一,然后返回给用户态
Select缺点:
-
位图大小默认1024,有上限。
-
每次都需要创建一个文件描述符位图并拷贝到内核态。
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
-
nfds:要检测的文件描述符数量,最大文件描述符加1。
-
readfds:指定了被读监控的文件描述符集;
-
writefds:指定了被写监控的文件描述符集;
-
exceptfds:指定了被例外条件监控的文件描述符集;
-
timeout:超时时间。
readfds是个长度为1024的bitmap。我们都知道fd文件描述符有一个序号,
如果现在我监听3,6,8号的fd,那么位图就是:
...10100100
那么select的具体流程是什么呢?
1、应用程序创建socket,生成文件描述符,并生成bitmap,使用hash的方式将bitmap的对应位置置一。
2、执行系统调用,将bitmap拷贝至内核空间,根据bitmap遍历对应的文件描述符,一旦有事件产生就返回。
3、用户程序遍历文件描述符,处理请求。
4、应用程序不停的调用select即可。
select模型已经很不错了,但是依然有不足的地方:
-
bitmap位图上限是1024,所以能监控的fd最多也就这么多。
-
fset位图不可重用,每次赋值全部清零,状态全部丢失。
-
fset位图需要不断的进行用户空间到内核空间的拷贝。
-
每次查找时间复杂度都是O(n)。
说句实话,如果没有更好的选择方案,这都不是问题。
2、Poll系统调用
Poll工作原理与Select基本相同,不同的只是将位图数组改成数组,也有资料说是链表,没有了最大连接数1024的限制,依然有fd集合的拷贝和O(n)的遍历过程。
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
这个系统调用的
-
fds:存放需要被检测状态的套接字描述符,与select不同(select在调用之后会清空这个数组),每当调用这个数组,系统不会清空这个数组,而是存放revents状态变化描述符变量,这样才做起来很方便。
-
nfds:用于标记数组fd中struct pollfd结构元素的总数量。
-
timeout:是超时时间。
-
返回值大于零表示成功,返回满足条件的文件描述符的个数
返回值等于零,表示超时。
返回值等于-1 发生错误,比如描述符不合法,接受到中断信号,内存不足
被检测的套接字使用结构体封装,如下:
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
pollfd
-
fd 文件描述符
-
events 请求的事件
-
revents 返回的事件
事件的类型比如:
-
pollin表示文件有数据来、文件描述符可读
-
pollout表示文件可写
-
pollerr表示错误发生
poll的优势:
1、大量的 fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
2、 可重用
3、Epoll系统调用
为解决fd集合拷贝的问题,epoll采用用户态和内核态共享epoll_fds集合。当调用epoll_wait系统调用时,内核态会去检查有哪些fd有事件,检查完毕后会将共享的epoll_fds集合重排序,将有事件的fd放在前面,并返回有事件的fd个数。
客户端收到返回的个数,就不需要全部遍历,而是直接处理fd。
1、int epoll_create(int size);
#注意:size参数只是告诉内核这个 epoll对象会处理的事件大致数目,而不是能够处理的事件的最大个数。在 Linux最新的一些内核版本的实现中,#这个 size参数没有任何意义。
2、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
#epoll的事件注册函数,epoll_ctl向 epoll对象中添加、修改或者删除感兴趣的事件,返回0表示成功,否则返回–1,此时需要根据errno错误码##判断错误类型。
#它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
3、int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epoll_wait方法返回的事件必然是通过 epoll_ctl添加到 epoll中的。
#第一个参数是epoll_create()的返回值,
#第二个参数表示动作,用三个宏来表示:
#EPOLL_CTL_ADD:注册新的fd到epfd中;
#EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
#EPOLL_CTL_DEL:从epfd中删除一个fd;
#第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事
处理流程大致如下:
#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排在前边
-
没有靠背开销,共享内存。
-
o(1)复杂度。
本文介绍了Linux系统调用在Nio中的应用,包括select、poll和epoll三种方式。select系统调用受限于1024个文件描述符,存在位图拷贝和O(n)遍历的问题;poll虽然解决了最大连接数限制,但仍存在遍历开销;epoll通过用户态和内核态共享fd集合,降低了拷贝开销,实现了O(1)复杂度的效率提升。
4111

被折叠的 条评论
为什么被折叠?



