遵义市汇川区建设厅网站,做车品的网站,工业设计网站哪个最,wordpress加印章插件文章目录 理解select函数select函数的功能和调用顺序设置文件描述符设置监视范围及超时select函数调用示例 优于select的epoll基于select的I/O复用速度慢实现epoll时必要的函数和结构体epoll_createepoll_ctlepoll_wait基于epoll的服务器端 边缘触发和水平触发 理解select函数 … 文章目录 理解select函数select函数的功能和调用顺序设置文件描述符设置监视范围及超时select函数调用示例 优于select的epoll基于select的I/O复用速度慢实现epoll时必要的函数和结构体epoll_createepoll_ctlepoll_wait基于epoll的服务器端 边缘触发和水平触发 理解select函数
select函数的功能和调用顺序
使用select函数可以将多个文件描述符集中到一起统一监视
是否存在套接字接收数据无需阻塞传输数据的套接字有哪些哪些套接字发生了异常
select函数的调用方法和顺序
设置文件描述符指定监视范围设置超时
↓ 调用select函数 ↓ 查看调用结果
设置文件描述符
利用select函数可以同时监视多个文件描述符监视文件门描述符也可以视为监视套接字首先需要将要监视的文件描述符集中到一起。集中时也要按照监视项接收、传输、异常进行区分
FD_ZERO(fd_set * fdset) 将fd_set变量都初始化为0FD_SET(int fd,fd_set *fdset) 在参数fdset指向的变量注册文件描述符fd的信息FD_CLR(int fd, fd_set * fdset) 从参数fdset指向的变量中清楚文件描述符fd的信息FD_ISSET(int fd , fd_set * fdset)若参数fdset指向的变量中包含文件描述符fd的信息则返回 真 int main(void)
{fd_set set;FD_ZERO(set); 0 0 0 0 ....FD_SET(1,set); 0 1 0 0 ....FD_SET(2,set); 0 1 1 0 ....FD_CLR(2,set); 0 1 0 0 ....
}设置监视范围及超时
#include sys/select.h
#include sys/time.hint select(int maxfd, fd_set * readset,fd_set *writeset,fd_set exceptset,const struct timeval * timeout);成功返回大于 0 的值失败返回 - 1maxfd 监视文件描述符的数量readset 将所有关注是否存在待读取数据的文件描述符注册到fd_set型变量并传递到其地址值writeset 将所有关注是否可传无阻塞数据的文件描述符注册到fd_set型变量并传递到其地址值exceptset 将所有关注是否发生异常的文件描述符注册到fd_set型变量并传递其地址值timeout 调用select函数后为防止陷入无限阻塞的状态传递超时time - out消息文件描述符的监视范围与select函数的第一个参数有关select要求通过第一个参数传递监视对象文件描述符的数量
select函数的超时时间与select函数的最后一个参数有关其中timeval结构体定义为
struct timeval
{long tv_sec; //秒long tv_usec; //微秒
}select函数只有在监视的文件描述符发生变化时才返回如果未发生变化就会进到阻塞状态。指定超时时间就是为了这种情况的发生通过上述结构体变量将秒数填入tv_sec成员微秒数填入tv_usec成员将结构体的地址值传递到select函数的最后一个参数不想设置超时时间直接传递NULL。
select函数调用示例
#include stdio.h
#include stdlib.h
#include string.h
#include unistd.h
#include arpa/inet.h
#include sys/socket.h
#include sys/time.h
#include sys/select.h#define BUF_SIZE 100
void error_handling(char *buf);int main(int argc, char *argv[])
{int serv_sock, clnt_sock;struct sockaddr_in serv_adr, clnt_adr;struct timeval timeout;fd_set reads, cpy_reads;socklen_t adr_sz;int fd_max, str_len, fd_num, i;char buf[BUF_SIZE];if(argc!2) {printf(Usage : %s port\n, argv[0]);exit(1);}serv_socksocket(PF_INET, SOCK_STREAM, 0);memset(serv_adr, 0, sizeof(serv_adr));serv_adr.sin_familyAF_INET;serv_adr.sin_addr.s_addrhtonl(INADDR_ANY);serv_adr.sin_porthtons(atoi(argv[1]));if(bind(serv_sock, (struct sockaddr*) serv_adr, sizeof(serv_adr))-1)error_handling(bind() error);if(listen(serv_sock, 5)-1)error_handling(listen() error);FD_ZERO(reads);FD_SET(serv_sock, reads);fd_maxserv_sock;while(1){cpy_readsreads;timeout.tv_sec5;timeout.tv_usec5000;if((fd_numselect(fd_max1, cpy_reads, 0, 0, timeout))-1)break;if(fd_num0)continue;for(i0; ifd_max1; i){if(FD_ISSET(i, cpy_reads)){if(iserv_sock) // connection request!{adr_sz sizeof(clnt_adr);clnt_sock accept(serv_sock, (struct sockaddr*)clnt_adr, adr_sz);FD_SET(clnt_sock, reads);if(fd_maxclnt_sock)fd_maxclnt_sock;printf(connected client: %d \n, clnt_sock);}else // read message!{str_lenread(i, buf, BUF_SIZE);if(str_len0) // close request!{FD_CLR(i, reads);close(i);printf(closed client: %d \n, i);}else{write(i, buf, str_len); // echo!}}}}}close(serv_sock);return 0;
}void error_handling(char *buf)
{fputs(buf, stderr);fputc(\n, stderr);exit(1);
}优于select的epoll
epoll 在内核里使用「红黑树」来关注进程所有待检测的 Socket红黑树是个高效的数据结构增删改一般时间复杂度是 O(logn)通过对这棵黑红树的管理不需要像 select/poll 在每次操作时都传入整个 Socket 集合减少了内核和用户空间大量的数据拷贝和内存分配。
epoll 使用事件驱动的机制内核里维护了一个「链表」来记录就绪事件只将有事件发生的 Socket 集合传递给应用程序不需要像 select/poll 那样轮询扫描整个集合包含有和无事件的 Socket 大大提高了检测的效率。
基于select的I/O复用速度慢
调用select函数后常见的针对所有文件描述符的循环语句每次调用select时都需要向该函数传递监视对象信息
相比于selectepoll最大的好处在于它不会随着监听fd数目的增长而降低效率。
因为在内核中的select实现中它是采用轮询来处理的轮询的fd数目越多自然耗时越多。
实现epoll时必要的函数和结构体
epoll_create: 创建保存epoll文件描述符的空间epoll_ctl: 向空间注册并注销文件描述符epoll_wait: 等待文件描述符发生变化
为添加和删除监视对象文件描述符select方式中需要FD_SET、FD_CLR函数但是在epoll中都是通过epoll_ctl函数请求操作系统完成
select方式中调用select等待文件描述符的变化而epoll调用epoll_wait函数。
select方式中通过fd_set变量查看监视对象的状态变化而epoll_wait方式通过结构体epoll_event将发生变化的文件描述符集中一起 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;events可以是以下几个宏的集合
EPOLLIN 表示对应的文件描述符可以读包括对端SOCKET正常关闭EPOLLOUT表示对应的文件描述符可以写EPOLLPRI表示对应的文件描述符有紧急的数据可读这里应该表示有带外数据到来EPOLLERR表示对应的文件描述符发生错误EPOLLHUP表示对应的文件描述符被挂断EPOLLET 将EPOLL设为边缘触发(Edge Triggered)模式这是相对于水平触发(Level Triggered)来说的。EPOLLONESHOT只监听一次事件当监听完这次事件之后如果还监听这个socket的话再次把这个socket加入到EPOLL队列
epoll_create
#include sys/epoll.hint epoll_create(int size);成功返回epoll文件描述符失败返回 - 1创建一个epoll的描述符size用来告诉内核这个监听数目一共多大此参数不同于select()中的第一个参数给出最大监听的fd1的值
当创建好epoll描述符后它就是会占用一个fd值所以在使用完epoll后必须调用close()关闭否则可能导致fd被耗尽。
epoll_ctl
#include sys/epoll.hint epoll_ctl(int epfd, int op, int fd, struct epoll_event * event);成功返回0失败时返回-1epfd 用于注册监视对象的epoll例程的文件描述符op 用于指定监视对象的添加、删除、更改操作 ↓EPOLL_CTL_ADD注册新的fd到epfd中EPOLL_CTL_MOD修改已经注册的fd的监听事件EPOLL_CTL_DEL从epfd中删除一个fdfd 需要注册的监视对象文件描述符event 监视对象的事件类型epoll_wait
#include sys/epoll.hint epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);成功返回时间的文件描述符失败返回-1epfd 时间发生监视范围的epoll例程的文件描述符events 保存时间的文件描述符集合的结构体地址值 (缓冲需要动态分配)maxevents 第二个参数可以保存的最大事件数timeout 以毫秒为单位传递-1一直等待发送事件。基于epoll的服务器端
#include stdio.h
#include stdlib.h
#include string.h
#include unistd.h
#include arpa/inet.h
#include sys/socket.h
#include sys/epoll.h#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *buf);int main(int argc, char *argv[])
{int serv_sock, clnt_sock;struct sockaddr_in serv_adr, clnt_adr;socklen_t adr_sz;int str_len, i;char buf[BUF_SIZE];struct epoll_event *ep_events;struct epoll_event event;int epfd, event_cnt;if(argc!2) {printf(Usage : %s port\n, argv[0]);exit(1);}serv_socksocket(PF_INET, SOCK_STREAM, 0);memset(serv_adr, 0, sizeof(serv_adr));serv_adr.sin_familyAF_INET;serv_adr.sin_addr.s_addrhtonl(INADDR_ANY);serv_adr.sin_porthtons(atoi(argv[1]));if(bind(serv_sock, (struct sockaddr*) serv_adr, sizeof(serv_adr))-1)error_handling(bind() error);if(listen(serv_sock, 5)-1)error_handling(listen() error);epfdepoll_create(EPOLL_SIZE);ep_eventsmalloc(sizeof(struct epoll_event)*EPOLL_SIZE);event.eventsEPOLLIN;event.data.fdserv_sock; epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, event);while(1){event_cntepoll_wait(epfd, ep_events, EPOLL_SIZE, -1);if(event_cnt-1){puts(epoll_wait() error);break;}for(i0; ievent_cnt; i){if(ep_events[i].data.fdserv_sock){adr_szsizeof(clnt_adr);clnt_sockaccept(serv_sock, (struct sockaddr*)clnt_adr, adr_sz);event.eventsEPOLLIN;event.data.fdclnt_sock;epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, event);printf(connected client: %d \n, clnt_sock);}else{str_lenread(ep_events[i].data.fd, buf, BUF_SIZE);if(str_len0) // close request!{epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);close(ep_events[i].data.fd);printf(closed client: %d \n, ep_events[i].data.fd);}else{write(ep_events[i].data.fd, buf, str_len); // echo!}}}}close(serv_sock);close(epfd);return 0;
}void error_handling(char *buf)
{fputs(buf, stderr);fputc(\n, stderr);exit(1);
}
边缘触发和水平触发
epoll 支持两种事件触发模式分别是边缘触发edge-triggeredET和 水平触发level-triggeredLT。
这两个术语还挺抽象的其实它们的区别还是很好理解的。
使用边缘触发模式时当被监控的 Socket 描述符上有可读事件发生时服务器端只会从 epoll_wait 中苏醒一次即使进程没有调用 read 函数从内核读取数据也依然只苏醒一次因此我们程序要保证一次性将内核缓冲区的数据读取完使用水平触发模式时当被监控的 Socket 上有可读事件发生时服务器端不断地从 epoll_wait 中苏醒直到内核缓冲区数据被 read 函数读完才结束目的是告诉我们有数据需要读取
举个例子你的快递被放到了一个快递箱里如果快递箱只会通过短信通知你一次即使你一直没有去取它也不会再发送第二条短信提醒你这个方式就是边缘触发如果快递箱发现你的快递没有被取出它就会不停地发短信通知你直到你取出了快递它才消停这个就是水平触发的方式。
这就是两者的区别水平触发的意思是只要满足事件的条件比如内核中有数据需要读就一直不断地把这个事件传递给用户而边缘触发的意思是只有第一次满足条件的时候才触发之后就不会再传递同样的事件了。
如果使用水平触发模式当内核通知文件描述符可读写时接下来还可以继续去检测它的状态看它是否依然可读或可写。所以在收到通知后没必要一次执行尽可能多的读写操作。
如果使用边缘触发模式I/O 事件发生时只会通知一次而且我们不知道到底能读写多少数据所以在收到通知后应尽可能地读写数据以免错失读写的机会。因此我们会循环从文件描述符读写数据那么如果文件描述符是阻塞的没有数据可读写时进程会阻塞在读写函数那里程序就没办法继续往下执行。所以边缘触发模式一般和非阻塞 I/O 搭配使用程序会一直执行 I/O 操作直到系统调用如 read 和 write返回错误错误类型为 EAGAIN 或 EWOULDBLOCK。
一般来说边缘触发的效率比水平触发的效率要高因为边缘触发可以减少 epoll_wait 的系统调用次数系统调用也是有一定的开销的的毕竟也存在上下文的切换。
select/poll 只有水平触发模式epoll 默认的触发模式是水平触发但是可以根据应用场景设置为边缘触发模式。
参考资料
https://xiaolincoding.com/ 更多资料尽在 GitHub 欢迎各位读者去Star
⭐学术交流群Q 754410389 持续更新中~~~