IO模型
同步阻塞IO (blocking IO) 也叫做BIO
用户线程通过系统调用read发起IO读操作,由用户空间转到内核空间。内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成read操作。
整个读取过程,用户线程是被阻塞的,导致在发起IO请求时,不能做任何事情,对CPU资源利用不够
同步非阻塞IO (Non-blocking IO) 也叫做NIO
同步非阻塞IO是在同步阻塞IO的基础上,将socket设置为NONBLOCK。这样做用户线程可以在发起IO请求后可以立即返回。
由于socket是非阻塞的方式,因此用户线程发起IO请求时立即返回。但并未读取到任何数据,用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。
不断的轮询,重复请求,会消耗大量的cpu资源
IO多路复用
IO多路复用是一种同步IO模型,实现一个线程可以监控多个文件句柄。一旦某个文件句柄就绪,就能通知应用程序进行相应的IO操作。没有文件句柄时就会发生阻塞,交出cpu。多路指多个文件句柄,复用指同一个线程
一句话解释:单线程或单进程同时监控若干个文件描述符是否可以执行IO操作的能力
IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。
用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。
这里与BIO类似,等待数据到达的时间都是会阻塞的。但是好处在于一个线程可以设置多个监听socket,不断的调用select函数,从而激活不同的socket,达到在同一个线程内同时处理多个IO请求的目的
使用Reactor设计模式的多路复用
通过Reactor的方式,可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。
由于select是阻塞的,而用户线程异步,因此也称为异步阻塞模型
select函数接口、使用示例
接口
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
| #include <sys/select.h> #include <sys/time.h>
#define FD_SETSIZE 1024 #define NFDBITS (8 * sizeof(unsigned long)) #define __FDSET_LONGS (FD_SETSIZE/NFDBITS)
typedef struct { unsigned long fds_bits[__FDSET_LONGS]; } fd_set;
int select( int max_fd, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout )
FD_ZERO(int fd, fd_set* fds) FD_SET(int fd, fd_set* fds) FD_ISSET(int fd, fd_set* fds) FD_CLR(int fd, fd_set* fds)
|
示例
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
| int main() {
fd_set read_fs, write_fs; struct timeval timeout; int max = 0;
FD_ZERO(&read_fs); FD_ZERO(&write_fs);
int nfds = 0; while (1) { nfds = select(max + 1, &read_fd, &write_fd, NULL, &timeout); for (int i = 0; i <= max && nfds; ++i) { if (i == listenfd) { --nfds;
FD_SET(i, &read_fd); } if (FD_ISSET(i, &read_fd)) { --nfds; } if (FD_ISSET(i, &write_fd)) { --nfds; } } } }
|
缺点
- 单个进程所打开的FD是有限制的,通过FD_SETSIZE设置,默认1024。==因为select使用描述字集,典型地是一个整数数组,其中每个整数中的每一位对应一个文件描述字。==
- 每次调用select,都需要把fd_set(文件描述符标记的bitmap)从用户态拷贝到内核态,这个开销在fd很多时会很大。这个fd_set由于会改变,所以不可重用。
- 唤醒的时候由于进程不知道是哪个 fd 已经就绪,需要进行一次遍历,采用轮询的方法,效率较低(高并发时)
for (int i = 0; i <= max && nfds; ++i)
poll函数
功能与select类似
接口
1 2 3 4 5 6 7 8 9 10 11
| #include <poll.h>
struct pollfd { int fd; short events; short revents; };
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
|
示例
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_POLLFD_LEN 4096
int main() {
int nfds = 0; pollfd fds[MAX_POLLFD_LEN]; memset(fds, 0, sizeof(fds)); fds[0].fd = listenfd; fds[0].events = POLLRDNORM; int max = 0; int timeout = 0;
int current_size = max; while (1) { nfds = poll(fds, max+1, timeout); if (fds[0].revents & POLLRDNORM) { connfd = accept(listenfd); } for (int i = 1; i < max; ++i) { if (fds[i].revents & POLLRDNORM) { sockfd = fds[i].fd if ((n = read(sockfd, buf, MAXLINE)) <= 0) { if (n == 0) { close(sockfd); fds[i].fd = -1; } } else { } if (--nfds <= 0) { break; } } } } }
|
如果有就绪的poll是会改变其结构体pollfd内的events置成内核监听事件
优点
- 相比select 文件描述符的数量已不再受限制
- 可以修改fds[i].fd文件描述符 和 fds[i].revents = 0 实现数组的复用
缺点
- 每次调用poll,都需要把pollfd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 对fd集合扫描时仍然是线性扫描,采用轮询的方法,效率较低(高并发时)
for (int i = 1; i < max; ++i)
epoll函数
接口
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
| #include <sys/epoll.h>
struct eventpoll { struct rb_root rbr; struct list_head rdlist; };
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
struct epoll_event { __uint32_t events; epoll_data_t data; };
|
示例
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
| int main(int argc, char* argv[]) {
epfd=epoll_create(256); epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); while(1) { nfds = epoll_wait(epfd,events,20,0); for(i=0;i<nfds;++i) { if(events[i].data.fd==listenfd) { connfd = accept(listenfd); epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); } else if (events[i].events&EPOLLIN) { read(sockfd, BUF, MAXLINE); epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); } else if(events[i].events&EPOLLOUT) { write(sockfd, BUF, n); epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); } } } return 0; }
|
epolln内部存了一个列表,所以不需要遍历所有的fd了,只需要遍历这个列表就行了for(i=0;i<nfds;++i)
异步IO
异步IO模型中,用户线程直接使用内核提供的异步IO API发起read请求,且发起后立即返回,继续执行用户线程代码。不过此时用户线程已经将调用的AsynchronousOperation和CompletionHandler注册到内核,然后操作系统开启独立的内核线程去处理IO操作。当read请求的数据到达时,由内核负责读取socket中的数据,并写入用户指定的缓冲区中。最后内核将read的数据和用户线程注册的CompletionHandler分发给内部Proactor,Proactor将IO完成的信息通知给用户线程(一般通过调用用户线程注册的完成事件处理函数),完成异步IO。
在多路复用中,用户线程是收到Reactor的通知,再去拷贝数据,而在异步IO中,内核会直接读取socket中的数据,拷贝到指定缓冲区,再通知用户。