假如我们设计了一个程序,该程序从标准输入接收数据输入,然后通过套接字发送出去,同时,改程序也通过套接字接收对方发送的数据流。
我们可以调用某些语言提供的API等待标准输入,但是一旦这样做,就没有办法在套接字有数据的时候读出数据;我们也可以使用某些API等待套接字有数据返回,但是这样做,也没有办法在标准输入有数据的情况下,读入数据并发送给对方。
I/O多路复用意味着我们可以同时监视多个fd上的I/O,并在其中任何一个就绪时读取,而非阻塞I/O意味着在读取fd时,如果没有可用的,则立即返回一个错误,而不是等待/阻塞直到数据可用。
IO模型 | 相对性能 | 关键思路 | 操作系统 | Java支持情况 |
---|---|---|---|---|
select | 较高 | Reactor | windows/Linux | 支持,Linux操作系统的 kernels 2.4内核版本之前,默认使用select;而目前windows下对同步IO的支持,都是select模型 |
poll | 较高 | Reactor | Linux | Linux下的JAVA NIO框架,Linux kernels 2.6内核版本之前使用poll进行支持。也是使用的Reactor模式 |
epoll | 高 | Reactor/Preactor | Linux | Linux kernels 2.6内核版本及以后使用epoll进行支持;Linux kernels 2.6内核版本之前使用poll进行支持;另外一定注意,由于Linux下没有Windows下的IOCP技术提供真正的 异步IO 支持,所以Linux下使用epoll模拟异步IO |
kqueue | 较高 | Preactor | Linux | 目前Java不支持 |
select函数就是一种常见的I/O多路复用技术,使用select函数,通知内核挂起进程,当一个或多个I/O事件发生后,将控制权返还给应用程序,由应用程序进行I/O事件的处理。
缺点: 支持的文件描述符个数是有限的,在Linux系统中,select的默认最大值为1024.
有一个is_read(int fd)
函数,来判断fd是否准备就绪。
//当fd 准备就绪时返回true
bool is_ready(int fd);
struct fd_info {
int fd;
bool ready;
};
//fds:文件描述符集合
//max_fd:文件描述符个数
int select(set<fd_info> fds, int max_fd) {
int ready_cnt = 0;
while (ready_cnt == 0) {
//遍历fd集合
for (int i = 0; i < max_fd; i++) {
//判断fd是否就绪
if (is_ready(i)) {
auto it = fds.find(i);
//修改fd状态
it->ready = true;
ready_cnt++;
}
}
}
//返回就绪的文件描述符个数
return ready_cnt;
}
set<fd_info> fds;
while (1) {
// 每次调用select前都需要重新初始化fds集合
fds.clear();
fds.inert({.fd = 1})
fds.inert({.fd = 100})
int ready_cnt = select(fds, /*max_fd=*/100 + 1);
assert(ready_cnt > 0);
for (int i = 0; i < fds.size(); i++) {
if (fds[i].ready) {
// Use fds[i].fd
}
}
}
poll是另一种在各种Unix系统上被广泛支持的I/O多路复用技术,虽然名声没有select那么响,但能力一点不比select差,而且因为可以突破select文件描述符的个数限制,在高并发下及其占优势。当需要控制的fd不超过1000的情况下,则使用select也够了。
// 当fd 准备就绪时返回true
bool is_ready(int fd);
struct fd_info {
int fd;
bool ready;
};
//fd_info* fds:fd集合指针
//nfds:fd个数
int poll(struct fd_info* fds, int nfds) {
int ready_cnt = 0;
while(ready_cnt == 0) {
for (int i = 0; i < nfds; i++) {
if (is_ready(fds[i])) {
fds[i].ready = true;
ready_cnt++;
} else {
fds[i] = false;
}
}
}
return ready_cnt;
}
// 只需要初始化一次
fd_info fds[2];
fds[0].fd = 1;
fds[1].fd = 100;
int nfds = 2;
while (1) {
int ready_cnt = poll(fds, nfds);
assert(ready_cnt > 0);
for (int i = 0; i < nfds; i++) {
if (fds[i].ready) {
// Use fds[i].fd
}
}
}
事件触发机制
add_monitor
是用来触发外部的线程来监控all_fds
将就绪的fd添加到read_fds
中。
// Start monitoring fds in `all_fds` and constantly adds ready ones to
// `ready_fds`.
void add_monitor(const vector<int>& all_fds, vector<int>& ready_fds);
struct fd_info {
int fd;
bool ready;
};
struct epoll_info {
vector<int> all_fds;
vector<int> ready_fds;
};
map<int, epoll_info> epoll_info_by_epoll_id;
// 创建epoll实例,并返回他的id
int epoll_create() {
return epoll_info_by_epoll_fd.size();
}
// 向epoll中添加监控fd
void epoll_add(int epoll_id, int fd) {
epoll_info_by_epoll_id[epoll_id].push_back(fd);
}
// 等待直到至少有一个fd就绪,返回就绪的fd个数
// Afte the function returns, the first `ready_cnt` of `ready_fds` contain
// ready fds. The rest can be ignored.
int epoll_wait(int epoll_id, struct fd_info* ready_fds) {
int ready_cnt = 0;
struct epoll_info info = epoll_info_by_epoll_id[epoll_id];
add_monitor(info.allfds, info.ready_fds);
while (ready_cnt == 0) {
ready_cnt = ready_fds.size();
for (int i = 0; i < ready_cnt; i++) {
ready_fds[i].fd = ready_fds[i];
ready_fds[i].ready = true;
}
}
return ready_cnt;
}
epoll
提供了3个api;epoll
比poll
具有更好的性能。换句话说,即使两个算法的复杂度都是O(n),在现实中,n=3和n=10000可能关系很大。int epoll_id = epoll_create();
epoll_add(epoll_id, 1);
epoll_add(epoll_id, 100);
while (1) {
struct fd_info fds[2];
int ready_cnt = epoll_wait(epoll_id, fds);
assert(ready_cnt > 0);
for (int i = 0; i < ready_cnt; i++) {
// Use fds[i].fd
}
}
通过以上的伪代码就可以看出来三者的区别:
poll
却没有;