1、Linux的I/O模式
网络I/O,会涉及两个系统对象,用户空间的进程调用IO和内核空间的内核系统,每次进行I/O都要进行两个阶段,当进行read操作时,两个阶段为:
- 等待内核准备数据就绪
- 将数据从内核拷贝到进程
正因为这两个阶段,linux系统产生了五种IO方式:
- 阻塞I/O
- 非阻塞I/O
- I/O多路复用
- 异步I/O
- 信号驱动IO(少见、不常用)
1.1 阻塞I/O(blocking I/O)
在linux系统中,默认情况下所有socket都是阻塞的,典型的read操作流程:
流程中可以看出,blocking I/O 的特点是在I/O的两个阶段(等待数据和拷贝数据)都是被阻塞了。
1.2 非阻塞I/O(non-blocking I/O)
linux系统中,socket可以设置为非阻塞,当一个non-blocking socket进行读操作时,流程如下:
从上面的流程中可以看出,当我们read数据时,如果kernel中数据还没有准备完成,read也会立即返回一个error,而不会阻塞我们的进程。当kernel数据准备完成,当我们read时,kernel会拷贝数据到我们的进程内存中,read会成功读取数据。
非阻塞recv函数返回值代表不同含义:
- recv() 返回值 > 0,读取数据成功,返回值为读取数据得长度;
- recv() 返回值 = -1 && error = EAGAIN,表示kernel数据没有准备完,read没有读到数据,正常返回。
- recv() 返回值 = -1 && error != EAGAIN,表示socket故障,无法正常通信,具体故障可以参考error值。
1.3 IO多路复用(IO multiplexing)
多路复用需要把socket设置为non-blocking状态,基本流程如下:
I/O多路复用的特点是通过一种机制让一个进程可以同事监控多个socket状态,这些socket集合中任意一个socket进入读写就绪状态,该状态就会被返回。实现多路复用常用的有select、poll以及epoll。
1.4 异步I/O(Asynchronous I/O)
Linux 下的 asynchronous I/O 用在磁盘 I/O 读写操作,不用于网络 I/O,从内核 2.6 版本才开始引入。先看一下它的流程
2、I/O多路复用之 select、poll、epoll
select、poll、epoll都是I/O多路复用机制,它们都需要在读写事件就绪后自己负债读写。
2.1 select 流程和API
从上述流程来看,使用select函数进行I/O请求和阻塞I/O模型没有太大区别,甚至还增加了监视socket,以及额外的socket函数的额外操作,效率更差了。但是使用select最大的优势是我们可以在一个线程中同时处理多个socket的I/O请求。我们可以添加多个socket,然后不断通过select函数读取发生事件的socket,即可达到同一个线程内同时处理多个I/O请求的目的(I/O多路复用)。
select的API:
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
FD_ZERO(fd_set* fds); //一个 fd_set类型变量的所有位都设为 0
FD_SET(int fd, fd_set* fds); //设置变量的某个位置
FD_ISSET(int fd, fd_set* fds); //测试某个位置是否被置位
FD_CLR(int fd, fd_set* fds); //清除某个位时可以使用
//maxfds:被监听socket的总数,它比所有socket集合中的最大socket大 1,因为socket是从 0 开始的。
//rfds、wfds、efds分别指向可读、可写、异常事件对应的集合。timeout用于设置select函数的超时时间。
int select(int maxfds, fd_set *rfds, fd_set *wfds, fd_set *efds,struct timeval *timeout);
struct timeval
{
long tv_sec; //秒
long tv_usec; //微秒
}
select函数的返回值:超时返回:= 0; 失败返回: = -1; 成功返回: > 0。
fd_set的理解:fd_set查看源码可以发现fd_set为长度为1024个bit位数组,fd_set中的每个bit都对应一个fd,最多支持1024个fd。select返回的时真个fd_set,需要我们遍历整个数组才只能知道发生了哪些变化。
2.2 poll 流程和API
poll 就是把 select 中的 fd_set 数组换成了链表,其他和 select 没什么不同。
poll的API
#include <sys/poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:列出了我们需要poll()来检查的文件描述符们。该参数为结构体数组,每个数组元素都是一个struct pollfd结构。可以传递多个结构体,指示 poll() 监视多个文件描述符。
nfds:表示fds结构体数组的长度,简单说,就是向 poll 申请的事件检测的个数。
在 select 里面,文件描述符的个数已经随着 fd_set 的实现而固定,没有办法对此进行配置
而在 poll 函数里,我们可以控制 pollfd 结构的数组大小,这意味着我们可以突破原来 select 函数最大描述符的限制
timeout:描述了 poll 的行为,单位是毫秒
如果是一个 <0 的数,表示在有事件发生之前永远等待;
如果是 0,表示不阻塞进程,立即返回;
如果是一个 >0 的数,表示 poll 调用方等待指定的毫秒数后返回。
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
2.3 epoll 流程和API
epoll是基于事件驱动的I/O模式,它没有fd个数的限制,epoll对fd采用红黑树的结构进行管理,并且该红黑树是放在内核中的。所以epoll模式的优点有:
- 支持进程打开fd的数据理论没有上限
- I/O效率不会随fd个数增加而线性下降
- 使用mmap加速内核和用户的数据交互
epoll的有两种工作模式
- LT(level triggered 水平触发,默认工作模式,支持block和non-blocking),当fd就绪,内核会一直通知你fd就绪,可以进行I/O操作了,如果你不做任何操作,内核会连续通知你。
- ET(edge triggered 边缘触发,高速工作模式,只支持non-blocking模式),当fd就绪,内核只会通知你一次,如果你不处理该事件,内核都不会为该事件通知第二遍,当有新的数据来到时,内核会才会再次发出就绪通知。
epoll的API:
#include <sys/epoll.h>
/* 创建一个epoll的句柄,size用来告诉内核需要监听的数目一共有多大。当创建好epoll句柄后,
它就是会占用一个fd值,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。*/
int epoll_create(int size);
/*epoll的事件注册函数*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*等待事件的到来,如果检测到事件,就将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组*/
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll的事件注册函数epoll_ctl,第一个参数是epoll_create的返回值,第二个参数表示动作,使用下面三个宏来表示:
EPOLL_CTL_ADD //注册新的fd到epfd中;
EPOLL_CTL_MOD //修改已经注册的fd的监听事件;
EPOLL_CTL_DEL //从epfd中删除一个fd;
typedef union epoll_data
{
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event
{
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
EPOLLIN //表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT //表示对应的文件描述符可以写;
EPOLLPRI //表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR //表示对应的文件描述符发生错误;
EPOLLHUP //表示对应的文件描述符被挂断;
EPOLLET //将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT//只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
2.4 总结3种多路复用
系统调用 | select | poll | epoll |
---|---|---|---|
事件集合 | 用户通多3个参数分别传入感兴趣的可读、可写以及异常事件,内核通过对这些参数的在线修改来反馈其中的就绪事件。这使得用户每次调用select都要重置这3个参数。 | 统一处理所有事件类型,因此只需要一个事件集参数。用户通过pollfd.events传入感兴趣的事件,内核通过修复pollfd.revents反馈其中就绪的事件。 | 内核通过一个事件表直接管理用户感兴趣的所有事件。因此每次调用epoll_wait时,无需反复传入用户感兴趣的事件。epoll_wait系统调用的参数events近用了反馈就绪的事件。 |
时间负债度 | O(n) | O(n) | O(1) |
支持最多fd数据量 | 系统默认1024 | 理论没有上限 | 理论没有上限 |
工作模式 | LT | LT | LT/ET |
内核实现 | 采用轮询方式检测I/O就绪事件 | 采用轮询方式检测I/O就绪事件 | 采用回调方式检测就绪事件 |
3、代码实例之 select、poll、epoll
3.1 select 服务demo
#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/select.h>
#include <netinet/in.h>
int init_svr(int port) {
int svr_fd = -1;
struct sockaddr_in svr_addr;
svr_fd = socket(AF_INET, SOCK_STREAM, 0);
svr_addr.sin_family = AF_INET;
svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
svr_addr.sin_port = htons(port);
if (-1 == bind(svr_fd, (struct sockaddr *)&svr_addr, sizeof (svr_addr))){
close(svr_fd);
printf("bind error:%d", errno);
return -1;
}
if (-1 == listen(svr_fd, 100)){
close(svr_fd);
printf("listen error:%d", errno);
return -1;
}
return svr_fd;
}
int main() {
const int buf_len = 1024;
int svr_fd = init_svr(9999);
fd_set rfds;
FD_ZERO(&rfds);
while(true) {
FD_SET(svr_fd, &rfds);
struct timeval tv;//每次while循环,tv都会被清空
tv.tv_sec = 3;
tv.tv_usec = 0;
switch (select(FD_SETSIZE, &rfds, nullptr, nullptr, &tv)) {
case -1:
printf("select error:%d\n", errno);
break;
case 0:
printf("select time out\n");
break;
default:
for (int fd = 0; fd < FD_SETSIZE; ++fd) {
if (FD_ISSET(fd, &rfds)) {
if (fd == svr_fd){
struct sockaddr clt_addr;
int addr_len = sizeof(clt_addr);
int clt_fd = accept(svr_fd, (struct sockaddr *) &clt_addr,reinterpret_cast<socklen_t *>(&addr_len));
int flags = fcntl(clt_fd, F_GETFL, 0);
flags |= O_NONBLOCK;
fcntl(clt_fd, F_SETFL, flags);
FD_SET(clt_fd, &rfds);
printf("svr rcv client :%d\n", clt_fd);
} else {
char buf[buf_len] = {0};
int ret = recv(fd, buf, buf_len, 0);
if (0 == ret) {
FD_CLR(fd,&rfds);
close(fd);
}else if (ret > 0){
printf("client rcv data len:%d\n", ret);
FD_SET(fd, &rfds);
}
}
}
}
break;
}
}
return 0;
}
3.2 poll 服务demo
#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <poll.h>
int init_svr(int port) {
int svr_fd = -1;
struct sockaddr_in svr_addr;
svr_fd = socket(AF_INET, SOCK_STREAM, 0);
svr_addr.sin_family = AF_INET;
svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
svr_addr.sin_port = htons(port);
if (-1 == bind(svr_fd, (struct sockaddr *)&svr_addr, sizeof (svr_addr))){
close(svr_fd);
printf("bind error:%d", errno);
return -1;
}
if (-1 == listen(svr_fd, 100)){
close(svr_fd);
printf("listen error:%d", errno);
return -1;
}
return svr_fd;
}
int main() {
const int buf_len = 1024;
int svr_fd = init_svr(9999);
struct pollfd pfd[1024];
int max_size = sizeof (pfd)/sizeof (pfd[0]);
for (int i = 0; i <max_size; ++i) {
pfd[i].fd = -1;
}
pfd[0].fd = svr_fd;
pfd[0].events = POLLIN;
int nfds = 1;
while (true){
switch (poll(pfd, nfds, 1000*3)) {
case -1:
printf("poll error:%d\n", errno);
break;
case 0:
printf("poll time out\n");
break;
default:
if (pfd[0].revents & POLLIN){
struct sockaddr_in clt_addr;
socklen_t len = sizeof(clt_addr);
int clt_fd = accept(svr_fd,(struct sockaddr*)&clt_addr,&len);
printf("accept finish %d\n",clt_fd);
int index = 0;
for (; index < max_size; ++index) {
if (pfd[index].fd < 0){
pfd[index].fd = clt_fd;
break;
}
}
if (max_size == index){
close(clt_fd);
printf("to many client\n");
}
pfd[index].events = POLLIN;
nfds = nfds>index+1?nfds:index+1;
}
for (int i = 1; i < nfds; ++i) {
if (pfd[i].revents & POLLIN){
char buf[buf_len]={0};
int ret = recv(pfd[i].fd, buf, sizeof(buf)-1, 0);
if (0 == ret){
printf("client close:%d\n", pfd[i].fd);
close(pfd[i].fd);
pfd[i].fd = -1;
} else if (ret > 0){
printf("client rcv size:%d\n", ret);
}
}
if (pfd[i].revents & POLLOUT){
//send data
}
}
break;
}
}
return 0;
}
3.3 epoll 服务demo
#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
int init_svr(int port) {
int svr_fd = -1;
struct sockaddr_in svr_addr;
svr_fd = socket(AF_INET, SOCK_STREAM, 0);
svr_addr.sin_family = AF_INET;
svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
svr_addr.sin_port = htons(port);
if (-1 == bind(svr_fd, (struct sockaddr *)&svr_addr, sizeof (svr_addr))){
close(svr_fd);
printf("bind error:%d", errno);
return -1;
}
if (-1 == listen(svr_fd, 100)){
close(svr_fd);
printf("listen error:%d", errno);
return -1;
}
return svr_fd;
}
int main() {
const int buf_len = 1024;
int svr_fd = init_svr(9999);
int epfd = epoll_create(1);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = svr_fd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, svr_fd, &ev) < 0){
printf("epoll_ctrl error:%d\n", errno);
close(svr_fd);
return -1;
}
const int ev_nums = 128;
struct epoll_event evs[ev_nums];
while (true){
switch (epoll_wait(epfd, evs, ev_nums, 3 * 1000)) {
case -1:
printf("epoll_wait error:%d\n", errno);
break;
case 0:
printf("epoll_wait time out\n");
break;
default:
for (int i = 0; i < ev_nums; ++i) {
struct sockaddr_in clt_addr;
int addr_len = sizeof (clt_addr);
if (evs[i].data.fd == svr_fd && evs[i].events & EPOLLIN){
int clt_fd = accept(svr_fd, (struct sockaddr *) &clt_addr,reinterpret_cast<socklen_t *>(&addr_len));
ev.data.fd = clt_fd;
ev.events = EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_ADD,clt_fd,&ev);
printf("recv client:%d\n", clt_fd);
} else if (evs[i].data.fd != svr_fd && evs[i].events & EPOLLIN){
char buf[buf_len]={0};
int ret = recv(evs[i].data.fd, buf, sizeof(buf)-1, 0);
if (0 == ret){
printf("client close:%d\n", evs[i].data.fd);
close(evs[i].data.fd);
evs[i].data.fd = -1;
epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, nullptr);
} else if (ret > 0){
printf("client rcv size:%d\n", ret);
ev.data.fd = evs[i].data.fd;
ev.events = EPOLLOUT | EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_MOD,evs[i].data.fd,&ev);
}
}else if (evs[i].data.fd != svr_fd && evs[i].events & EPOLLOUT){
//send data
}
}
break;
}
}
return 0;
}