一: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没有大小的限制,也不需要轮询,也不用从用户跟内核直接拷贝,返回的都是可用的