I/O 多路复用技术是为了解决进程或线程阻塞到某个 I/O 系统调用而出现的技术,使进程不阻塞于某个特定的 I/O 系统调用。
在IO多路复用技术描述前,先讲解下同步,异步,阻塞,非阻塞的概念。
linux网络IO中涉及到的模型如下:
(1)阻塞式IO
(2)非阻塞式IO
(3)IO多路复用
(4)信号驱动IO
(5)异步IO
今天不谈信号驱动IO,略过..
在学习IO模型的时候,我们必须明确一个概念,处理 IO 的时候,阻塞和非阻塞都是同步 IO。
只有使用了特殊的 API 才是异步 IO,例如Linux网络中的AIO。
再看下POSIX对同步和异步这两个术语的定义:
通俗的理解下同步和异步
aio_read
时,用户不需要等待,只需要接收内核完成操作的通知,由内核来完成数据的读取。在知晓阻塞和非阻塞都是同步 IO后,阻塞和非阻塞就很好理解了
阻塞IO:由系统调用read,导致线程一直等待数据返回。
阻塞等待模型
非阻塞IO:系统调用read后立即返回一个状态,当数据达到内核缓冲区之前都是非阻塞的,即返回一个系统调用状态。
非阻塞等待模型
闪客的动图做的非常的形象,上述gif动图来源「低并发编程」
IO多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;
select 是操作系统提供的系统调用函数,select()用来等待文件描述词(普通文件、终端、伪终端、管道、FIFO、套接字及其他类型的字符型)状态的改变。是一个轮循函数,循环询问文件节点,可设置超时时间,超时时间到了就跳过代码继续往下执行。
通过select,我们可以把一个文件描述符的数组发给操作系统, 让操作系统去遍历,确定哪个文件描述符可以读写, 然后告诉我们去处理:
select原理
头文件
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
拥塞函数,拥塞等待文件描述符事件的到来
int select(int maxfdp
, fd_set *readset
, fd_set *writeset
, fd_set *exceptset
,struct timeval *timeout);
参数说明:
maxfdp:被监听的文件描述符的最大值,它比所有文件描述符集合中的文件描述符的最大值大1,因为文件描述符是从0开始计数的;
readfds、writefds、exceptset:分别指向可读、可写和异常等事件对应的描述符集合。
timeout:用于设置select函数的超时时间,即告诉内核select等待多长时间之后就放弃等待。timeout == NULL 表示等待无限长的时间,timeout == 0,select立即返回
struct timeval
{
long tv_sec; /*秒 */
long tv_usec; /*微秒 */
};
int FD_ZERO(int fd, fd_set *fdset); //一个 fd_set类型变量的所有位都设为 0
int FD_CLR(int fd, fd_set *fdset); //清除某个位时可以使用
int FD_SET(int fd, fd_set *fd_set); //设置变量的某个位置位
int FD_ISSET(int fd, fd_set *fdset); //测试某个位是否被置位
当声明了一个文件描述符集后,必须用FD_ZERO将所有位置零
调用 select函数,拥塞等待文件描述符事件的到来 ;如果超过设定的时间,则不再等待,继续往下执行
select返回后,用FD_ISSET测试给定位是否置位:
if(FD_ISSET(fd, &rset)
{
...
//do something
}
fd_set其实这是一个数组的宏定义,实际上是一long类型的数组,每一个数组元素都能与一打开的文件句柄(socket、文件、管道、设备等)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪个句柄可读。
整个 select 的流程图如下:
Demo1: 基于select的点对点通信
基于select的点对点通信
运行效果如下:
简易聊天室select版本
完整代码阅读全文转跳或者发送文末关键字..
Poll就是监控文件是否可读的一种机制,作用与select一样。
#include <poll.h>
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
参数说明
fds:是一个struct pollfd结构类型的数组,列出了我们需要poll()检查的文件描述符
typedef struct pollfd {
int fd; /* 需要被检测或选择的文件描述符*/
short events; /* 对文件描述符fd上感兴趣的事件 */
short revents; /* 文件描述符fd上当前实际发生的事件*/
} pollfd_t;
events:想要监听的事件
revents:实际上发生的事件
POLLIN
POLLOUT
POLLPRI
POLLRDHUB
POLLHUP
POLLERR
指定了fds中元素的个数,nfds_t为无符号整形
决定阻塞行为,一般如下:
#include <stdio.h>
#include <poll.h>
#include <string.h>
int main()
{
int timeout = 0;
char buf[1024];
struct pollfd fd_poll[1]; //设置只有一个事件
while(1){
fd_poll[0].fd = 0;
fd_poll[0].events = POLLIN;
fd_poll[0].revents = 0;
memset(buf, '\0', sizeof(buf));
switch( poll(fd_poll, 1, -1) ){
case 0:
perror("timeout!");
break;
case -1:
perror("poll");
break;
default:
{
if( fd_poll[0].revents & POLLIN )
{
gets(buf);
printf("buf : %s\n",buf);
}
}
break;
}
}
return 0;
}
makefile
tcp_poll:tcp_poll.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f tcp_poll
epoll没有对描述符数目的限制,它所支持的文件描述符上限是整个系统最大可以打开的文件数目,例如,在1GB内存的机器上,这个限制大概为10万左右。
epoll只有 epoll_create
、epoll_ctl
和 epoll_wait
这三个系统调用。
第一步,创建一个 epoll 句柄
第二步,向内核添加、修改或删除要监控的文件描述符。
第三步,发起了 select() 调用
epoll原理
其定义如下:
#include <sys/epoll.h>
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);
#include <sys/epoll.h>
int epoll_create(int size);
调用epoll_create方法创建一个epoll的句柄,使用完epoll后使用close函数进行关闭
#include <sys/epoll.h>
int epoll_ctl(int epfd //第一个参数epfd:epoll_create函数的返回值。
, int op //第二个参数events:表示动作类型。有三个宏来表示
, int fd //第三个参数fd:需要监听的fd。
, struct epoll_event *event);//第四个参数event:告诉内核需要监听什么事件。
op:
fd:需要注册监视对象文件描述符
// 感兴趣的事件和被触发的事件
struct epoll_event {
__uint32_t events; // Epoll events
epoll_data_t data; // User data variable
};
// 保存触发事件的某个文件描述符相关的数据
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
EPOLLIN:表示对应的文件描述符可读(包括对端Socket);
EPOLLOUT:表示对应的文件描述符可写;
EPOLLPRI:表示对应的文件描述符有紧急数据可读(带外数据);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET:将EPOLL设为边缘触发(Edge Triggered),这是相对于水平触发(Level Triggered)而言的.
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket,需要再次添加.
例如:
struct epoll_event ep_ev;
int accept_sock = accept(listen_sock,(struct sockaddr*)&remote,&len);
ep_ev.events = EPOLLIN | EPOLLET;
ep_ev.data.fd = accept_sock;
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,accept_sock,&ep_ev)
收集在epoll监控的事件中已经发生的事件
#include <sys/epoll.h>
int epoll_wait(int epfd //第一个参数epfd:epoll_create函数的返回值。
, struct epoll_event *events
, int maxevents
, int timeout); //超时时间(毫秒)
第一个参数epfd:epoll_create函数的返回值。
第二个参数events:是分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据赋值到这个event数组中,不会去帮助我们在用户态分配内存)
第三个参数maxevents:maxevents告诉内核这个events数组有多大,这个maxevents的值不能大于创建epoll_create时的size。
第四个参数:是超时时间(毫秒),如果函数调用成功,则返回对应IO上已准备好的文件描述符数目,如果返回0则表示已经超时。
基于epoll的简单回显服务器
浏览器输入:http://服务器ip:8000/例如,http://49.234.35.128:8000/
基于epoll的简易http服务器