试图了解select与poll函数
前言
在前面一篇文章提到的文本回射服务器,每新来一个客户都需要新建子进程,每次套接字来数据都需要切换进程,而且,系统对进程描述符是有限的,而且需要处理僵尸进程,捕捉信号。如何让一个进程处理多个套接字描述符,这个时候就需要IO复用技术。
而对于客户端来说:当服务器进程被杀死时,服务器虽然给客户端发送了 FIN,但此时客户正阻塞于标准输入中,它将看不见这个EOF,而只有当它试图从套接字读取数据时才能知道服务器已终止。这时可能已经过了很多时间了。而如何解决这样的情况,就需要一种预先告知内核的能力,使得,内核一旦发现一个或多个条件已就绪(读入已准备好被读取,或者描述符已能承担更多的输出),它就通知进程,这个能力,称为I/O多路复用。
IO多路复用
多线程模型下的CS模型
io复用的CS模型
I/O复用模型:
使用IO复用,程序将不再阻塞在具体的I/O系统调用上,而是阻塞在select或者poll上。(新型的epoll还没学不敢乱说)。下面是I/O复用模型。
select
select函数允许进程指示内核等待多个事件的任何一个发生,或者过了指定一段时间后返回。事件如:
- 描述符集中的一个或多个准备好读
2)描述符集中的一个或多个准备好写
3)描述符集中的某些描述符存在异常条件待处理
4)过了一段时间
函数原型
# include <sys/select.h>
# include <sys/time.h>
int select(int maxfdpl, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
返回值:若有就绪描述符则返回就绪描述符数目, 若出错返回 -1
参数介绍:
fd_set :是一个数据结构,用来存储描述符集,原型是一个long类型的数组。其中每一个数组元素的每一位都能与一打开的文件句柄(不管是socket句柄,还是其他文件或命名管道或设备句柄)建立联系。如:第一元素对应于描述符0-31
readset: 用来检查可读性的一组文件描述字。
writeset: 用来检查可写性的一组文件描述字。
exceptset: 用来检查是否有异常条件出现的文件描述字
maxfdpl:一个整型数据,为描述符集中的(fd)最大值+1,用来指定待测试的描述符个数。
timeout:是一个struct timeval
用来设置对单个描述符的等待时间。
struct timeval{
long tv_sec; //秒
long tv_usec; //微秒
}
有三种用法:
1)设为空指针永远等待下去,
2)等待一段固定时间,在有一个描述符准备好I/O时返回,但是不超过timeout指定的时间。
3)根本不等待,置为0
相关宏
fd_set rset;
void FD_ZERO(fd_Set *rset);/*清空rset每一位,用来初始化*/
void FD_SET(int fd, fd_set *fdset);/*将fd添加到rset组*/
void FD_CLR(int fd, fd_set *fdset);/*将fd移除组,是FD_sET()的逆向操作*/
void FD_ISSET(int fd, fd_set *fdset);/*fd的是否就绪?*/
FD_SETSIZE 1024 /*现大多数系统对select的最大监听描述符量*/
描述符就绪条件
当一个描述符就绪,将计入select的返回值内。所以应该对描述符何时处于就绪有所了解。
使用流程
1)声明一个fd_set结构 rset,
2)FD_ZERO(&rset)清空rset
组,初始化该结构
3)FD_SET(fd,&rset)将待检查的描述符添加到组
4)调用select();
5)FD_ISSET();检查相关描述符是否就绪。
需要注意的是: select函数的第一个参数是所有你关心的描述符fd中的最大值+1;
另外就是select中的三个参数:readset,writeset,exceptset都是值——结果参数,当select返回时,未就绪的描述符对应的位都会被清0。为此每次重新调用select都需要把关心的描述符重新置1,下面的例子中有个方便的方法就是建立两个描述符集结构allse
t和rset
,其中allset
保存所有关心的描述符,当select返回后,只需要将rset = allset
即可。
使用了select的文本回射客户/服务器
客户端:
#include "mfs.h"
#include <iostream>
using namespace std;
void str_cli(FILE *fp, int sockfd){
int maxfdp1, stdineof;
fd_set rset;
char buf[MAXLINE];
int n;
stdineof = 0;
FD_ZERO(&rset);
for(; ;){
if (!stdineof)
FD_SET(fileno(fp), &rset);
FD_SET(sockfd,&rset);
maxfdp1 = max(fileno(fp), sockfd)+1;
select(maxfdp1, &rset, NULL, NULL, NULL);
if(FD_ISSET(sockfd, &rset)){
if( (n = read(sockfd, buf,MAXLINE)) == 0){
if(stdineof == 1)
return; /*normal termination*/
else {
perror("str_cli: server terminated prematurely");
exit(1);
}
}
buf[n]='\0';
cout<<"receive: "<<buf<<endl;
}
if(FD_ISSET(fileno(fp), &rset)){
if( (n = read(fileno(fp), buf, MAXLINE)) == 0){
stdineof = 1;
shutdown(sockfd, SHUT_WR);/*send FIN*/
FD_CLR(fileno(fp), &rset);/*turn off*/
continue;
}
writen(sockfd, buf, n);
}
}
}
int main (int argc, char **argv){
int sockfd;
struct sockaddr_in servaddr;
if(argc != 2){
perror ("usage: tcpcli<IPaddress>");
exit(1);
}
sockfd = socket(AF_INET,SOCK_STREAM,0);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port =htons(2020);
inet_pton(AF_INET,argv[1],&servaddr.sin_addr);
if(connect(sockfd, (SA *)&servaddr, sizeof(servaddr) ))
cout << "connect error"<< endl;
str_cli(stdin, sockfd);
exit(0);
}
服务器
#include "mfs.h"
#include <iostream>
using namespace std;
int main(){
int i,maxi, maxfd, listenfd, connfd, sockfd;
int nready, client[FD_SETSIZE];
ssize_t n;
fd_set rset,allset;
char buf[MAXLINE];
socklen_t clilen;
struct sockaddr_in cliaddr,servaddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(2020);
bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
listen(listenfd, LISTENQ);
maxfd = listenfd;
maxi = -1;
for(i = 0; i < FD_SETSIZE; i++){
client[i] = -1;
}
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
for(; ;){
rset = allset;
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if(FD_ISSET(listenfd , &rset)){/*新客户连接*/
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (SA *)&cliaddr, &clilen);
for(i = 0; i < FD_SETSIZE; i++){
if(client[i] < 0){
client[i] = connfd;/*保存描述符*/
break;
}
}
if(i == FD_SETSIZE){
cout<< "too many clients"<<endl;
continue;
}
printf("connection from %s, port %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, buf, sizeof(buf) ),ntohs(cliaddr.sin_port));
FD_SET(connfd, &allset);
if(connfd > maxfd){
maxfd = connfd;
}
if(i > maxi)
maxi = i;
if(--nready <= 0)
continue;
}
for(i = 0; i <= maxi; i++){
if((sockfd = client[i]) < 0)
continue;
if(FD_ISSET(sockfd, &rset)){
bzero(buf,sizeof(buf));
if( (n = read(sockfd, buf, MAXLINE)) == 0){
close(sockfd);
FD_CLR(sockfd,&allset);
client[i] = -1;
}
else {
buf[n]='\0';
cout<< buf << endl;
writen(sockfd, buf, n);
}if(--nready <= 0)
break;
}
}
}
}
poll函数
函数原型
#include <poll.h>
int poll(struct pollfd *fdarray, unsigned long nfds,int timeout);
/*返回:若有就绪描述符则为其数目,若超时则为0,若出错则为-1*/
第一个参数是指向一个结构数组的第一个元素的指针,每个元素都是一个pollfd结构,用来指定测试某个给定描述符fd的条件。
poll原理上与select差不多,只是选用了不同的数据结构struct pollfd
。该数据结构完全存储了描述符的fd,所以不需要额外存储每个描述符的编号。另外这个数据结构不需要反复重置关心的描述符相关位。因为该结构声明了存储参数和结果的两个成员不需要。
struct pollfd{
int fd; /*待检查的描述符*/
short events; /*关心的事件,事件发生poll返回*/
short revents;/*发生的事件,事件在revents上按位存储所以后面检测要用位运算,*/
}
第二个参数nfds
是pollfd数组中最大下标值加1。与select不同,select中的maxfp1
是指最大描述符编号+1。
第三个参数是poll返回前等待多长时间,单位是毫秒。
可能的取值:
取值 | 说明 |
---|---|
负值(-1) | 永远等待 |
0 | 立即返回 |
正值 | 等待指定数目的毫秒值 |
相关事件
poll识别三类数据:普通、优先级带 和 高优先级。
POLLLIN可被定义为POLLRDNORM和POLLRDBAND的逻辑或,POLLOUT等同于POLLWRNORM,
之所以出现这么多常值,是因为历史遗留问题,为了向后兼容,某些古老的常值也留下来了,用的
时候挑合适的就好不用纠结。
普通数据:
所有正规TCP数据和所有UDP数据都被认为是普通数据
当TCP连接读半部关闭(如接受了FIN),也是普通数据随后读操作返回 0
TCP连接存在错误,既可以认为是普通数据也可以是错误(POLLERR)。在监听套接字上有新连接既可以是普通数据也可以是优先级数据,大多数实现看成普通数据。
高优先级数据(High priority):带外数据,传输层协议使用带外数据(out-of-band,OOB)来发送一些重要的数据,如果通信一方有重要的数据需要通知对方时,协议能够将这些数据快速地发送到对方。为了发送这些数据,协议一般不使用与普通数据相同的通道,而是使用另外的通道。linux系统的套接字机制支持低层协议发送和接受带外数据。
优先级带数据(priority band)。
啥高优先级数据优先级带数据,实在找不到资料,如果有知道的朋友,麻烦告诉我一下。反正普通数据就挺常用的。
实例
把上面的select服务端改成poll服务
#include "mfs.h"
#include <iostream>
using namespace std;
int main()
{
int i, maxi, listenfd, connfd, sockfd;
socklen_t clilen;
int nready;
ssize_t n;
char buff[MAXLINE];
struct sockaddr_in servaddr, cliaddr;
struct pollfd client[OPEN_MAX];
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(2020);
bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
listen(listenfd, LISTENQ);
client[0].fd = listenfd;
client[0].events = POLLRDNORM;
for (i = 1; i < OPEN_MAX; ++i) /*初始化其他空位*/
client[i].fd = -1;
maxi = 0; /*client 里的最大下标*/
for (;;)
{
nready = poll(client, maxi + 1, -1);
if (client[0].revents & POLLRDNORM)
{
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (SA *)&cliaddr, &clilen);
for (i = 1; i < OPEN_MAX; ++i)
{
if (client[i].fd < 0)
{
client[i].fd = connfd;
client[i].events = POLLRDNORM;
printf("connection from %s, port %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)), ntohs(cliaddr.sin_port));
break;
}
}
if (i == OPEN_MAX)
{
cout << "too many client" << endl;
continue;
}
if (i > maxi)
maxi = i;
if (--nready <= 0)
continue;
}
for (i = 1; i <= maxi; i++)
{ /*检查所有描述符是否有数据*/
if ((sockfd = client[i].fd) < 0)
continue;
if (client[i].revents & (POLLRDNORM | POLLERR))
{
cout << "new data" << endl;
if ((n = read(sockfd, buff, MAXLINE)) < 0)
{
if (errno == ECONNRESET)
{
/*connection reset by client*/
close(sockfd);
client[i].fd = -1;
}
else
{
perror("read error");
exit(1);
}
}
else if (n == 0)
{
close(sockfd);
}
else
{
buff[n] = '\0';
cout << buff << endl;
writen(sockfd, buff, n);
}
if (--nready <= 0)
break;
}
}
}
}
运行结果:
总结
与select对比:
1、poll的描述符上限没有特别上限,主要取决你对pollfd数组的定义以及系统对该进程的描述符打开上限,select上限是 1024。
2、select与poll用的数据结构不同,select采用一个长整型数组fd_set
,每一个描述符,对应其中一位(bit)而poll使用pollfd
储存整个描述符编号。
3、select的fd_set
每次都会重置没有事件发生的描述符,而pollfd
不会。
4、select事件限制为微秒单位,poll为毫秒。
select和poll的优缺点
优点:避免了多进程或者多线程需要的进/线程切换开销。以及避免阻塞在单一I/O上,而错过其他描述符信息。
缺点:
1、每次都需要从用户空间复制描述符集到内核。
2、过多描述符时,每次遍历描述符集,服务速度会明显下降。
据说在epoll上解决了上面的问题。下次再学学看。。。。。
epoll
epoll可以参考select、poll、epoll之间的区别(搜狗面试)写得很好,也没必要再抄一遍吧。
补充水平触发与边缘触发:.
水平触发level trigger LT(状态达到)
当被监控的文件描述符上有可读写事件发生时,会通知用户程序去读写,如果用户一次读写没取完数据,他会一直通知用户,如果这个描述符是用户不关心的,它每次都返回通知用户,则会导致用户对于关心的描述符的处理效率降低。
复用型IO中的select和poll都是使用的水平触发方式。
2.边缘触发edge trigger ET(状态改变)
当被监控的文件描述符上有可读写事件发生时,会通知用户程序去读写,它只会通知用户进程一次,这需要用户一次把内容读取玩,相对于水平触发,效率更高。如果用户一次没有读完数据,再次请求时,不会立即返回,需要等待下一次的新的数据到来时才会返回,这次返回的内容包括上次未取完的数据。
信号驱动型IO使用的是边缘触发方式。
epoll既支持水平触发也支持边缘触发,默认是水平触发。
3.比较
水平触发是状态达到后,可以多次取数据。这种模式下要注意多次读写的情况下,效率和资源利用率情况。
边缘触发是状态改变一次,取一次数据。这种模式下读写数据要注意一次是否能读写完成。
epoll与select对比
假设现在有1024个fd ; select 和epoll 都同时维护他, 假设这些fd 都是活跃的, 这种情况下,select一次扫描 可以扫描1024个fd,空闲的fd很少,
但是epoll 就有可能不一样了, epoll 是先注册等待回调, 有可能出现1024次回调;
这样的情况下, 要是说epoll 效率比select 高-----这就不好说了!!!!!!!!
如果select 和epoll 同时维护1024个fd ,但是每次只有一个fd有事件,这种情况下 select 每次都会扫描所有的fd, 对比于epoll 每次只有一个fd 回调。 select 做了很多无用功, 此时应该epoll的效率高吧!!
或者在短连接多的时候, 一个连接使用epoll 会触发epoll_ctrl_add/del 两次系统调用,但是select 只有一次扫描 ,此时 也许select 效率性能更高。
高并发,且任一时间只有少数socket是活跃的。如果在并发量低,socket都比较活跃的情况下,select就不见得比epoll慢了
参考:
《unix网络编程卷1》
为什么人们总是认为epoll 效率比select高!!!!!!l)