I/O多路复用之select
select的功能可以用一句话来描述:
实现基于I/O多路复用的异步并发编程。
在具体讲解select之前我们先看看常规的阻塞socket编程方式,以服务端为例:
转自:
http://my.oschina.net/pathenon/blog/64219
对于这种方式,最大的问题在哪里呢?accept和recev的阻塞调用!下面以两种场景为例,来说明相比这种情况,select是如何做到异步I/O多路复用的高效性。
第一种场景:
server除了要对外响应client的服务外,还要能够接受标准输入的命令来进行管理。
假如使用上述阻塞方式,在单线程中,accept调用和read调用必定有先后顺序,而它们都是阻塞的。比如先调用accept,后调用 read,那么如果没有客户请求时,服务器会一直阻塞在accept,没有机会调用read,也就不能响应标准输入的命令。
1 | int fd_stdin = open(...); |
2 | int fd_socket = socket(...); |
3 | bind(...); |
4 | listen(...); |
5 | while (1){ |
6 | accept(...); |
7 | read(...); |
8 | } |
而如果使用select,先注册分别由socket和open创建的文件描述符,然后进入select调用。当其中任何一个文件描述符的状态发生改变时,就可以进行相应的处理。
01 | int fd_stdin = open(...); |
02 | int fd_socket = socket(...); |
03 | bind(...); |
04 | listen(...); |
05 | fd_set fs; |
06 | while (1){ |
07 | FD_ZERO(fs...); |
08 | FD_SET(fd_stdin, fs); |
09 | FD_SET(fd_socket, fs); |
10 | select(); |
11 | if (FD_ISSET(fd_socket...)) |
12 | accept(...); |
13 | if (FD_ISSET(fd_stdin...)) |
14 | read(...); |
15 | } |
第二种场景:
server要对外提供大量的client请求服务。
假如使用阻塞方式,在单线程中,由于accept和recev都是阻塞式的,那么当一个client被服务器accept后,它可能在send发送消息时阻塞,因此服务器就会阻塞在recev调用。即时此时有其他的client进行connect,也无法进行响应。
1 | int fd_socket = socket(...); |
2 | bind(...); |
3 | listen(...); |
4 | while (1){ |
5 | accept(...); |
6 | revev(...); |
7 | } |
而如果使用select,在服务器端先注册由socket创建的文件描述符,然后进入select调用。只有当由socket创建的文件描述符的状态发生改变时,才执行accept操作,并把得到的client的文件描述符进行注册,再次进入select调用。当select检查到有文件描述符的状态改变时,如果是server的socket创建的文件描述符,则执行accept操作,否则执行recev操作。当请求的client数目比较多时, select明显能够提高并发性。
01 | int fd_socket = socket(...); |
02 | bind(...); |
03 | listen(...); |
04 | fd_set fs; |
05 | while (1){ |
06 | FD_ZERO(fs...); |
07 | FD_SET(fd_socket, fs); |
08 | set fs with the file descriptor fd_accept got from accept; |
09 | select(); |
10 | if (FD_ISSET(fd_socket...)){ |
11 | accept(...); |
12 | record the file descriptor; |
13 | } |
14 | |
15 | if (FD_ISSET(fd_accept...)) |
16 | recv(...); |
17 | } |
说完了select相比阻塞调用的好处,我们也简单说说它的
限制和不足。
(1)select在查找状态改变的文件描述符时,是对描述符链表进行遍历操作,因此对效率有较大影响。
(2)select在默认情况下,支持的最大文件描述符个数为1024。当然,可以通过修改linux的socket内核进行修改。
PS:本博客的主要用途是记录在准备找工作过程中对以前所学知识的复习笔记。由于时间关系,大部分文章都没有进入深入的细节讲解。忘海涵!
I/O多路复用之epoll
在上一章,我们对select进行了大致的描述,知道了它相对传统的阻塞式服务提高了并发度,但是它也由于轮询而导致效率底下。本文对epoll进行讲解,相比select它的并发度更高,现代高负载服务器很多都采用这种模型。
在讲解epoll的具体用法之前,我们先看看采用
epoll模型主要用到的三个函数以及一个数据结构。
epoll中三个主要的函数:
(1)int epoll_create(int size);
功能 :生成一个epoll专用的文件描述符。
参数 :size:在该epoll fd上关注的最大socket fd数。
返回值:生成的文件描述符。
(2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能 :控制某个epoll文件描述符上的事件,可以注册事件,修改事件,删除事件。
参数 :epfd :由 epoll_create 生成的epoll专用的文件描述符;
op :EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修 改、EPOLL_CTL_DEL 删除;
fd :关联的文件描述符;
event:指向epoll_event的指针;
返回值:0:成功;
-1:失败;
(3)int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout);
功能 :轮询I/O事件的发生。
参数 :epfd :由 epoll_create 生成的epoll专用的文件描述符;
events :用于回传待处理事件的数组;
maxevents:每次能处理的事件数;
timeout :等待I/O事件发生的超时值;-1相当于阻塞,0相当于非阻塞;
返回值:>=0 :返回发生事件数;
-1 :错误;
epoll中的主要数据结构:
01 | typedef union epoll_data { |
02 | void *ptr; |
03 | int fd; |
04 | __uint32_t u32; |
05 | __uint64_t u64; |
06 | } epoll_data_t; |
07 |
08 | struct epoll_event { |
09 | __uint32_t events; /* Epoll events */ |
10 | epoll_data_t data; /* User data variable */ |
11 | }; |
其中,events的类型有:
EPOLLIN :文件描述符可以读;
EPOLLOUT:文件描述符可以写;
EPOLLPRI:文件描述符有紧急的数据可读;
EPOLLERR:文件描述符发生错误;
EPOLLHUP:文件描述符被挂断;
EPOLLET :文件描述符有事件发生;
epoll的使用还是很简单的,请看下面一个简单的采用epoll提供并发服务的服务端程序(注:为了简洁,都没有进行错误处理,实际使用时,一定要记住进行错误处理。):
01 | #include <errno.h> |
02 | #include <string.h> |
03 | #include <sys/types.h> |
04 | #include <netinet/in.h> |
05 | #include <sys/socket.h> |
06 | #include <sys/wait.h> |
07 | #include <unistd.h> |
08 | #include <arpa/inet.h> |
09 | #include <sys/epoll.h> |
10 | #include <sys/time.h> |
11 |
12 | #define MAXBUF 1024 |
13 | #define MAX_EPOLL_SIZE 10000 |
14 | #define SERVICE_PORT 8888 |
15 |
16 |
17 | int main( int argc, char **argv) |
18 | { |
19 | int server_fd, new_fd; |
20 | struct sockaddr_in server_addr, client_addr; |
21 |
22 | struct epoll_event ev; |
23 | struct epoll_event events[MAX_EPOLL_SIZE]; |
24 |
25 | socklen_t len = sizeof ( struct sockaddr_in); |
26 | server_fd = socket(AF_INET, SOCK_STREAM, 0); |
27 |
28 | bzero(&server_addr, sizeof (server_addr)); |
29 | server_addr.sin_family = AF_INET; |
30 | server_addr.sin_port = htons(SERVICE_PORT); |
31 | server_addr.sin_addr.s_addr = INADDR_ANY; |
32 |
33 | bind(server_fd, ( struct sockaddr *) &server_addr, sizeof ( struct sockaddr)); |
34 | listen(server_fd, 1000); |
35 |
36 | //create epoll fd, and register the server listening fd |
37 | int epoll_fd = epoll_create(MAX_EPOLL_SIZE); |
38 | ev.events = EPOLLIN | EPOLLET; |
39 | ev.data.fd = server_fd; |
40 | epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev); |
41 |
42 | int active_fd_count = 1; |
43 | while (1) |
44 | { |
45 | //wait for some events to happen |
46 | int event_active_fd_count = epoll_wait(epoll_fd, events, active_fd_count, -1); |
47 |
48 | // process all events |
49 | for ( int i = 0; i < event_active_fd_count; ++i) |
50 | { |
51 | if (events[i].data.fd == server_fd) |
52 | { |
53 | new_fd = accept(server_fd, ( struct sockaddr *) &client_addr,&len); |
54 |
55 | //register new fd to epoll |
56 | ev.events = EPOLLIN | EPOLLET; |
57 | ev.data.fd = new_fd; |
58 | epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_fd, &ev); |
59 | active_fd_count++; |
60 | } |
61 | else |
62 | { |
63 | handle message on events[i].data.fd |
64 | if (client close the connection) |
65 | { |
66 | epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd,&ev); |
67 | active_fd_count--; |
68 | } |
69 | } |
70 | } |
71 | } |
72 | close(server_fd); |
73 | return 0; |
74 | } |
讲完epoll的常规使用方法,这里需要注意的是epoll有两种工作方式:
(1)ET:Edge Triggered,边缘触发。仅当状态发生变化时才会通知,需要细致的处理每个请求,否则容易发生丢失事件的情况。只支持非阻塞的socket。
(2)LT:Level Triggered,水平触发(默认工作方式)。只要还有没有处理的事件就会一直通知,因此不用担心事件丢失的情况。效率会低于ET触发,尤其在大并发,大流量的情况下。支持阻塞和非阻塞的socket。
最后讲讲
为什么epoll会比select高效,主要从三方面来进行论述。
(1)elect对描述符状态的改变是通过轮询来进行查找的;而epoll是当描述符状态发生改变时主动进行通知内核,这就是所谓的Reactor事件处理机制。可以用“好莱坞原则”进行描述:不要打电话给我们,我们会打电话通知你。相比之下,select的机制就好比面试结束后不停给面试官打电话询问面试结果。效率孰高孰低,可见一 斑。
(2)select的文件描述符是使用链表进行组织的;而epoll是使用红黑树这一高效数据结构组织的。
(3)select从内核到用户空间传递文件描述符上发送的信息是使用内存复制的方式进行的;而epoll是采用共享内存的方式。