Linux网络编程 多路转接
五种IO模型,阻塞IO、非阻塞IO、多路复用IO、信号驱动IO以及异步IO。
前言
IO简单来说就是read 和 write,总体分为两步 1 等待数据就绪 2 从内核中的缓冲区拷贝数据到用户区,或从用户区拷贝数据到内核区。高效IO的本质其实就是减少等待数据就绪的时间。IO多路IO转接服务器也叫做多任务IO服务器,总体设计思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监听事件就绪。 主要使用有三种 select、poll、epoll
五种IO模型
阻塞IO
所有的套接字,默认都是阻塞IO,等待数据就绪。
非阻塞IO
如果未就绪,系统调用仍然会直接返回,返回EWOULDBLOCK错误码
信号驱动IO
内核将数据准备好的时,应用程序收到SIGIO信号,这种IO方式已经很少使用了。
多路转接IO
与阻塞式IO最本质的区别就是可以等待多个fd。
异步IO
由内核在数据拷贝完成时,通知应用程序(等待和拷贝都已完成)
fcntl 设置非阻塞
- 复制一个现有的描述符(cmd=F_DUPFD) .
- 获得/设置文件描述符标记(cmd=F_GETFD or F_SETFD).
- 获得/设置文件状态标记(cmd=F_GETFL or F_SETFL)
- 获得/设置异步I/O所有权(cmd=F_GETOWN or F_SETOWN)
- 获得/设置记录锁(cmd=F_GETLK,F_SETLK or F_SETLKW)
默认文件描述符都为阻塞式
SetNonBlock
void SetNonBlock(int fd) {
int fl = fcntl(fd,F_GETFL);
check(fl);
fcntl(fd,F_SETFL,fl | O_NONBLOCK);
}
select
select监视多个fd的状态变化,等待数据就绪。
select 底层是轮询检查数据是否就绪,因此如果存在大量连接那么服务器的响应时间就会大大下降。
函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
nfds: 监控的文件描述符集里最大文件描述符加1
readfds: 监控有读数据到达文件描述符集合,传入传出参数
writefds: 监控写数据到达文件描述符集合,传入传出参数
exceptfds: 监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
timeout: 定时阻塞监控时间,3种情况
1.NULL,阻塞等待
2.设置timeval,等待固定时间
3.设置timeval里时间均为0,立即返回,用来轮询
struct timeval {
long tv_sec;
long tv_usec;
};
fd_set : 这个结构就是整数数组,是一个位图,使用位图中对应的位来表示要监视的文件描述符.
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
返回值: 执行成功则返回文件描述词状态已改变的个数 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回 当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的值不可预测。 错误值可能为: EBADF文件描述词为无效的或该文件已关闭 EINTR此调用被信号所中断 EINVAL参数n为负值 ENOMEM核心内存不足
socket就绪条件
读就绪
- 内核中socket接收缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT,此时可以无阻塞的读该文件描述符,并且返回值大于0
- socket TCP通信中,对端关闭连接,此时对该socket读,则返回0
- 监听的socket上有新的连接请求;
- socket上有未处理的错误;
写就绪
- 内核中socket发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水位标记so_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0
- socket的写操作被关闭(close或者shutdown).对一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号
- socket使用非阻塞connect连接成功或失败之后;
- socket上有未读取的错误
异常就绪
select 缺点:
- 每次调用都需要重新设置fd_set
- 频繁的从用户态切换内核态进行拷贝,效率不高。
- 底层采用轮询机制,大量连接下效率很低。
- select 支持监听的fd有限。
epoll
poll解决了select的两个问题: 1 poll能等待的文件描述符没有限制 2 poll不需要在每次调用的时对所监听集合重新设置
函数原型
#include <sys/poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
nfds 监控数组中有多少文件描述符需要被监听
timeout 毫秒级等待
-1:阻塞等待
0:立即返回,不阻塞进程
>0:等待指定毫秒数,如当前系统时间精度不够毫秒,向上取值
struct pollfd {
int fd;
short events;
short revents;
};
POLLIN 普通或带外优先数据可读,即POLLRDNORM | POLLRDBAND
POLLRDNORM 数据可读
POLLRDBAND 优先级带数据可读
POLLPRI 高优先级可读数据
POLLOUT 普通或带外数据可写
POLLWRNORM 数据可写
POLLWRBAND 优先级带数据可写
POLLERR 发生错误
POLLHUP 发生挂起
POLLNVAL 描述字不是一个打开的文件
返回值 返回值小于0,表示出错; 返回值等于0,表示poll函数等待超时; 返回值大于0,表示poll由于监听的文件描述就绪.
epoll
epoll可以认为是改进版的poll,为了处理大量连接的情况,它可以显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。被公认为Linux2.6下性能最好的多路IO就绪通知方法。
epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户程序可以缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高程序效率。
函数原型
epoll_create
int epoll_create(int size);
创建一个epoll的句柄
自从linux2.6.8之后,size参数是被忽略的. 使用完毕,必须调用close()关闭句柄
epoll_ctl
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event) ;
向模型中添加fd或对应的事件
epoll的事件注册函数
第一个参数是epoll_create()的返回值(epoll的句柄). ·第二个参数表示动作,用三个宏来表示 EPOLL_CTL_ADD:注册新的fd到epfd中 EPOLL_CTL_MOD:修改已经注册的fd的监听事件; EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd 第四个参数:告知内核需要监听的事件
struct epoll_event {
__uint32_t events;
epoll_data_t data;
};
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的话,需要再次把这个socket加入到EPOLL队列里
epoll_wait
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
events: 用来存内核得到事件的集合,
maxevents: 告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,
timeout: 是超时时间
-1: 阻塞
0: 立即返回,非阻塞
>0: 指定毫秒
返回值: 成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1
epoll 原理
当某一进程调用epol_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用密切相关。
struct eventpo11{
....
struct rb_rootrbr;
struct list_head rdlist;
....
};
每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。 这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度). 而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法。 这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中.在epoll中,对于每一个事件,都会建立一个epitem结构体.
struct epitem{
struct rb_noderbn;
struct list_headrd11ink;
struct epo11_filefd ffd;
struct eventpo11 *ep;1/指向其所属的eventpoll对象
struct epo11_event event;
}
当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。 如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数掌返回给用户.这个操作的时间复杂度是O(1)。
epoll的优点
-
对比与select,在大量连接下只有少量连接响应的情况,epoll更加高效,直接从就绪队列读取,时间复杂度为O(1),select需要轮询检测 O(N)。 -
接口使用方便,虽然拆分成了三个国数但是反而使用起来更方便高效.不需要每次循环都设置关注的文件描述符,也做到输入输出参数分离开。 -
数据拷贝轻量,只在合适的时候调用EPoLl_CTL_ADD 将文件描述符结构拷贝到内核中,这个操作并不频繁(而select/poll都是每次循环都要进行拷贝) -
事件回调机制:避免使用遍历,而是使用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中,epoll_wait返回直接访问就绪队列就知道哪些文件描述符就绪.这个操作时间复杂度O(1).即使文件描述符数目很多,效率也不会受到影响. -
监听的socket没有数量限制。
epoll事件模型
EPOLL事件有两种模型:
LT:
epoll默认状态下就是LT工作模式
- 当epoll检测到socket上事件就绪的时候,可以不立刻进行处理.或者只处理一部分
- 如果一次只读取了部分数据,缓冲区还剩余数据,在第二次调用epoll_wait时epoll_wait仍然会立刻返回并通知socket读事件就绪
- 直到缓冲区上所有的数据都被处理完,epoll_wait 才不会立刻返回
- 支持阻塞读写和非阻塞读写
ET
epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免对一个文件描述符的IO操作阻塞,导致其他fd饿死。
- 非阻塞读写
- 只有当read或者write返回EAGAIN(非阻塞读,暂时无数据)时才需要挂起、等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。
ET的效率对比与LE更高,用户空间可以缓存IO状态,减少调用epoll_wait 的次数,但是会编程难度难度。
|