linux网络编程-select/poll/epoll
本文的主要内容:
- select函数
- poll函数
- epoll函数
原理
**高并发的程序一般使用同步非阻塞模式,而不是多线程+同步阻塞模式.**这是因为如果采用多线程,server需要自己监听客户端的连接请求。此时可以采用多路I/O转接服务器。也就是利用内核监听客户端请求。当有请求连接的时候server再去建立连接。但是连接建立好之后还需要等待用户数据,把这个部分也可以交给内核。避免了服务器的阻塞,服务器可以去做其他事情。
select函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval timeout)
- 第一个参数:所监听的所有文件描述符中,最大的文件描述符+1
- 第2/3/4个参数:fd_set是文件描述符的集合,是一个bitmap。所监听的文件描述符“可读/可写/异常”事件。
- 第5个参数:设定监听的时长。
- 返回值:成功:返回监听的所有集合中,满足条件的总数;失败返回-1.
假如读:1,2 写:2,3,4 异常3 其中1;2,3;1,2监听成功,返回总数5.
四个函数
如何将文件描述符加入到fd_set呢?有下面四个函数:
void FD_ZERO(fd_set *set);
将set清空为0void FD_CLR(int fd, fd_set *set);
将fd在对应的set中清0void FD_SET(int fd, fd_set *set);
将fd对应置1void FD_ISSET(int fd, fd_set *set);
判断fd是否在set中
使用
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fd1,&readfds);
n = select();----->返回总数
for(;n;)
{
//判断fd是否在readfds/writefds/exceptfds中
FD_ISSET(fd1,&readfds);
}
select函数的缺点
- 同时监听的文件描述符最大为1024
- 当满足监听条件的文件描述符比较少的时候,判断是哪个文件描述符需要遍历所有的文件描述符集合。比如文件描述符有1024个,select返回值为2,此时就要从0到1023依次遍历,判断哪个满足条件。有时会自定义一个数组,把监听的文件描述符放在数组中,最后只需要遍历这个数组就可以。但是这需要用户自己定义。
- 监听集合和满足监听条件的集合是一个集合,如下图,监听4个文件描述符,最后返回lfd和fd3是满足条件的。因此每次修改的时候将原有集合保存。
服务器源码实现
client数组存放所有被监听过的文件描述符
allset用来暂存,rset存放读事件集合。
假设此时有个c5请求相应,同时fd2和fd4请求发送数据。首先在Line43的while(1)循环中,调用select,返回此时请求总个数3.然后读新的请求c5,把fd5放入client中。此时判断nready不为0,所以进入while下面的for循环中,表示读取数据。读数据时先判断是哪个请求,读完之后清除对应标记。然后再处理下一个请求。下面是模拟请求的一个过程。
poll函数
优缺点
相比select函数的优点:
- 突破1024的限制(ulimit -a查看)
- 监听、返回集合的分离
- 搜索的范围变小
可以使用cat命令查看一个进程可以打开的socket描述符上限。
cat /proc/sys/fs/file-max
如果有需要可以修改配置文件:
sudo vi /etc/security/limits.conf
* soft nofile 65536
* hard nofile 100000
缺点:
监听1000个,返回3个,还是要依次遍历1000个来判断是哪个文件描述符。
函数原型
int poll(struct pollfd *fds,nfds_t nfds, int timeout);
struct pollfd
{
int fd;
short events;
short revents;
};
- fds表示数组的首地址
- nfds表示数组长度
- timeout是时间,单位毫秒,-1表示永久等待,0表示不等待
- events有三种类型:POLLIN/POLLOUT/POLLERR
- revents的类型由函数自己定义
使用
struct pollfd fds[5000];
fds[0].fd=listenfd;
fds[0].events=POLLIN;
poll(fds,5,-1);
epoll
优点:
- 能够修改描述符上限
- 监听少量文件描述符时能够返回对应的描述符,不用检索所有被监听的描述符
API
epoll_create()
int epoll_create(int size);
输入参数:需要多大的空间,调用calloc,需要监听多少文件描述符,便于创建红黑树。
返回参数:返回一颗红黑树的树根
epoll_ctl()
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
op: 操作:包括EPOLL_CTL_ADD/MOD/DEL
fd:对哪个文件描述符进行操作
struct epoll_event
{
unint32_t events;--EPOLLIN/OUT/ERR
epoll_data_t data;
}
typedef union epoll_data
{
void *ptr;--泛型指针,用于传回调函数的指针
int fd;
unint32_t u32;
unint64_t u64;
}epoll_data_t;
epoll_wait
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout)
events:是一个传出参数,传出满足监听所有数组的首地址。
maxevents:数组容量
timeout:超时时间
返回:成功返回有多少个文件描述符就绪
使用
int epfd = epoll(10); --epfd(句柄)
struct epoll_event events;
events.events = EPOLLIN;
events.data.fd = lfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&events);
struct epoll_event evt[100];
epoll_wait(epfd, evt, 100, 100);
下面这幅图是一个epoll使用的例子。
触发方式 epoll_ET/LT
epoll有两种触发方式,边沿触发和水平触发。分别对应高低电平转换和维持不变的时候。ET比LT效率要高,能够减少epoll_wait调用次数。从而提高效率。
水平模式是服务器读完所有的数据。默认方式是水平触发。
使用:
event = EPOLLIN|EPOLLET;
假如一个客户端要传1000B的数据,epoll监听到了,报告给了服务器程序,程序只读了500B,那么epoll下面是否应该触发呢?如果触发,就是水平触发。如果不触发,就是边沿触发。举个例子,假如小明上学,老师见到小明一次就提醒他一次要写作业,这个叫水平触发;而老师最后放学的时候只说一次,放学要写作业,写不写是小明的事情,这个就是边沿触发。
但是边沿触发模式下,加入客户端一直向服务器端发送数据,但是服务器端又只读很少,可能会造成缓冲区的累积。但是这种方法应用场景也可以是,加入客户端发送5000B的数据,其中50B可以供服务器判断是否需要,假如不需要那么把缓冲区清空一下就可以了,不用将全部数据读入。
非阻塞IO
边沿触发,利用while循环读数据,设置非阻塞模式。fcntl(O_NONBLOCK);
还有一种情况,服务器每次读500B才返回,但是客户端只发了200B,假如是水平出发那么服务器就会阻塞。但是服务器阻塞的时候没办法调用epoll进行监听,所以会产生死锁。非阻塞IO用fnctl函数。