IO多路复用之select,poll,epoll分析

一:select

int select (int maxfd + 1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval * timeout);

参数解释:

maxfd+1:表示添加的所有fd的最大值加1,此处的有个注意点,select的最大fd是1024,此处的最大值也就是1025,说明一下为何要传入maxfd这个参数,首先readset,writeset,exceptset这几个参数都没有传入长度,内核轮训时不知道要轮训那些fd,为何需要加1,linux的fd是0开始,后面FD_SET,FD_ISSET 内部是用掩码的方式.

readset:表示需要监听可读的fd

writeset:表示需要监听可写的fd

exceptset:表示监听异常的fd

readset,writeset,exceptset如果不需要可以传入null,当这三个参数都传入Null时,只用

timeout时,可以实现一个定时器的功能

timeout:表示监听需要等待的时间,如若传入null,表示一直等待到有事件才返回,可以设置马上返回,就是将timeout里面的结构都设置为0

select介绍时还需要配套以下几个宏使用,1024的限定就是下面宏的数组决定的

fd_set fdset

FD_ZERO (&fdset); //初始化fdset,清空掩码位

FD_SET (1,&fdset);//设置fdset,将需要监听的fd置掩码位

FD_ISSET(fd,&fdset);//检测该fd是否可读写

看下这几个宏的实现

#define FD_SETSIZE      1024
typedef unsigned long   fd_mask;
#define NBBY    8               /* number of bits in a byte */
#define NFDBITS (sizeof(fd_mask) * NBBY)        /* bits per mask */
#define howmany(x, y)   (((x) + ((y) - 1)) / (y))
typedef struct fd_set {
        fd_mask fds_bits[howmany(FD_SETSIZE, NFDBITS)];
} fd_set;
#define _fdset_mask(n)  ((fd_mask)1 << ((n) % NFDBITS))
#define FD_SET(n, p)    ((p)->fds_bits[(n)/NFDBITS] |= _fdset_mask(n))
#define FD_CLR(n, p)    ((p)->fds_bits[(n)/NFDBITS] &= ~_fdset_mask(n))
#define FD_ISSET(n, p)  ((p)->fds_bits[(n)/NFDBITS] & _fdset_mask(n))
#define FD_COPY(f, t)   bcopy(f, t, sizeof(*(f)))
#define FD_ZERO(p)      bzero(p, sizeof(*(p)))

可以看出当传入的fd大于1024的时候会导致fds_bits数组越界

返回值:-1表示出错,0表示没有读写事件,大于0表示读写事件的个数

select的使用伪代码

while(1){

FD_ZERO (&fdset);//清空上一轮的状态

FD_SET(1,&fdset);//设置fdset,将需要监听的fd置掩码位

int res = select (maxfd + 1, &fdset, &fdset, &fdset, timeout)

switch(res)

{

case -1: exit(-1);break; //select错误,退出程序

case 0:break; //再次轮询,没有读写事件

default:

if(FD_ISSET(fd,&fds)) //测试sock是否可读,即是否网络上有数据

{

read 或者send

}

}}

从上面的程序可以看出,select的使用有1024的局限性,fd属于用户态的资源,调用时需要从用户态拷贝到内核态,当内核检测完成后需要从内核态拷贝到用户态,供用户检测,检测时也是需要遍历所有的fd调用FD_ISSET,判断该fd是否可读写

 

二:poll

int poll(struct pollfd fds[], nfds_t nfds, int timeout);

struct pollfd {

int fd; /*文件描述符*/

short events; /* 等待的需要测试事件 */

short revents; /* 实际发生了的事件,也就是返回结果,常用的读事件POLLIN,写事件POLLOUT */

};

第一个参数fds:表示一个需要监听的fd数组

第二参数 nfds:表示第一个参数传入的数组长度

第三个参数timeout:要监听的时间(0表示马上返回,-1表示一直等待,其他值为毫秒数)

返回值: 大于0表示数组fds中准备好读、写或出错状态的那些socket描述符的总数量,

等于0表示数组fds中没有任何socket描述符准备好读、写,或出错,

小于0表示poll函数调用失败,同时会自动设置全局变量errno,可以通过errno获取出错信息

poll使用的伪代码:

structpollfd fds[max_fd]; //定义一个fd结构

fds[n].fd = fdn; //赋值fd

fds[i].events = POLLIN; //设置需要监听读时间

while(1){

if (poll(fds, max_fd, timeout) <= 0) //对事件进行监听

{

printf("Poll error\n");

return 1;

}

for (i = 0; i< max_fd; i++)

{

if (fds[i].revents) //判断是否有读事件

{

read(fds[i].fd, buf, MAX_BUFFER_SIZE); //读取具体的数据内容

fds[i].revents =0; //清空标志位,等在下次触发

}

}

由上面的实现可以看出poll的实现也是需要轮询遍历查看那些事件触发外加fd从用户态拷贝到内核,内核到时后,将内核数据拷贝到用户态返回,对比select唯一解决的问题就是没有了fd的最大限制1024,此处的限制就是个类型的限制了

 

三:epoll

为了解决select与poll的轮询加拷贝fd的问题,后面推出了epoll,更加的高效,下面从四个维度来剖析epoll

1) epoll的数据结构

epoll底层使用的是红黑树加链表实现的,红黑树用来保存fd,链表用来保存就绪fd

2)tcp/ip如何通知到epoll对应的Io就绪

当网卡收到数据包时,会将数据包拷贝到内核的socket缓冲区,这个时候epoll监听了对应的soket缓冲区,知道有对应的事件,这个时候就能回调返回

3)epoll涉及的API,epoll_create, epoll_ctl, epoll_wait、close

int epoll_create(int maxfds):

创建一个epoll,结束使用close关闭

在Linux2.6.8开始该参数已经不起作用了,以前的版本可用设置为系统打开的最大文件句柄数

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

该函数用于控制某个epoll文件描述符上的事件,可以注册事件,修改事件,删除事件。

参数:

epfd:由 epoll_create 生成的epoll专用的文件描述符;

op:要进行的操作例如注册事件,取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修 改、EPOLL_CTL_DEL 删除

fd:关联的文件描述符,socket描述符或者文件描述符

event:指向epoll_event的指针;

struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
} __attribute__ ((__packed__));

在该结构中表明要监听事件类型,默认是水平触发LT

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

第1个参数 epfd是 epoll的描述符.

第2个参数 events则是分配好的 epoll_event结构体数组,epoll将会把发生的事件复制到 events数组中(events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存。内核这种做法效率很高)。

第3个参数 maxevents表示本次可以返回的最大事件数目,通常 maxevents参数与预分配的events数组的大小是相等的。

第4个参数 timeout表示在没有检测到事件发生时最多等待的时间(单位为毫秒),如果 timeout为0,则表示 epoll_wait在 rdllist链表中为空,立刻返回,不会等待,-1表示一直等待直到有事件发生

返回值:如返回0表示已超时。如果返回–1,则表示出现错误,需要检查 errno错误码判断错误类型,大于0表示准备好的事件个数

close:关闭epoll

4)epoll的触发模式ET/LT

ET边缘触发是指socket缓冲区的数据从无到有就会触发,这个时候往往遇到这样一种场景,将socket设置为阻塞模式,阻塞模式是不能跟边缘触发结合使用的,因为边缘触发读取数据需要将所有数据读取完,直到读取到 EAGAIN错误才结束,如若设置Wie阻塞模式,就会一直等待,直到读取到数据,会导致线程阻塞,线程阻塞不是我们想看到的场景

LT:水平触发是指只要socket缓冲区有数据,就通知你

 

epoll的使用代码

int epfd = epoll_create(EPOLL_SIZE);

struct epoll_event ev;

struct epoll_event events[20];

nfds = epoll_wait(epfd, events, 20, 500);

{

for (n = 0; n < nfds; ++n) {

if (events[n].data.fd == listener) {

//如果是主socket的事件的话,则表示

//有新连接进入了,进行新连接的处理。

client = accept(listener, (structsockaddr *)&local, &addrlen);

if (client < 0) {

perror("accept");

continue;

}

setnonblocking(client); //将新连接置于非阻塞模式

ev.events = EPOLLIN | EPOLLET; //并且将新连接也加入EPOLL的监听队列。

//注意,这里的参数EPOLLIN|EPOLLET并没有设置对写socket的监听,

//如果有写操作的话,这个时候epoll是不会返回事件的,如果要对写操作

//也监听的话,应该是EPOLLIN|EPOLLOUT|EPOLLET

ev.data.fd = client;

if (epoll_ctl(epfd, EPOLL_CTL_ADD, client, &ev) < 0) {

//设置好event之后,将这个新的event通过epoll_ctl加入到epoll的监听队列里面,

//这里用EPOLL_CTL_ADD来加一个新的epoll事件,通过EPOLL_CTL_DEL来减少一个

//epoll事件,通过EPOLL_CTL_MOD来改变一个事件的监听方式。

fprintf(stderr, "epollsetinsertionerror:fd=%d", client);

return -1;

}

}

else if(event[n].events & EPOLLIN)

{

//如果是已经连接的用户,并且收到数据,

//那么进行读入

int sockfd_r;

if ((sockfd_r = event[n].data.fd) < 0)

continue;

read(sockfd_r, buffer, MAXSIZE);

//修改sockfd_r上要处理的事件为EPOLLOUT

ev.data.fd = sockfd_r;

ev.events = EPOLLOUT | EPOLLET;

epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd_r, &ev)

}

else if(event[n].events & EPOLLOUT)

{

//如果有数据发送

int sockfd_w = events[n].data.fd;

write(sockfd_w, buffer, sizeof(buffer));

//修改sockfd_w上要处理的事件为EPOLLIN

ev.data.fd = sockfd_w;

ev.events = EPOLLIN | EPOLLET;

epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd_w, &ev)

}

do_use_fd(events[n].data.fd);

}

}

 

从上面可以看到fd没有大小的限制,也不需要轮询,也不用从用户跟内核直接拷贝,返回的都是可用的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值