关于IO多路复用函数(select、poll、epoll)的一些理解
注意:本文部分参考自网络
一、阻塞现象
- 什么是阻塞现象?
当一个数据流中再也没有数据,read的时候,或者,我们的流中写满了数据在去write的时候,即无数据可读和无空间可写的尴尬现象,称为阻塞现象
-
阻塞等待,非阻塞忙轮询
比如这个例子,送快递与收快递的例子
- 阻塞等待相当于快递员在送快递的过程中,客户可以空出大脑睡大觉(不占用CPU的宝贵时间片)
- 非阻塞忙轮询:指的是快递员在送快递的过程中客户每分钟催一次,占用了快递员的时间(占用了CPU,系统资源)
二、 阻塞死等待的缺点
如果快递同一时刻到达,但是同一时刻只能签收并验货一份快递,因此在签收的时候,便借不到其他快递员的电话(多线程或者多进程)
三、解决阻塞死等待的方法
1、非阻塞、忙轮询
2. 方法2:select与poll
select 代收员 比较懒,她只会告诉你快递到了,但是是谁到的,你需要挨个快递员问一遍。每次调用select前都要重新初始化描述符集,将fd从用户态拷贝到内核态,每次调用select后,都需要将fd从内核态拷贝到用户态;poll基本上也相同,只不过poll没有最大文件描述符限制,poll解决了select重复初始化的问题。但仍旧是轮寻排查
3、方法3:epoll
-
什么是epoll
- 是IO多路转接技术
- 只需关心活跃的连接,无需遍历全部的描述符集合
- 能够处理大量的连接请求(系统可以打开的文件数目)
-
epoll API
/** * @param size 告诉内核监听的数目 * * @returns 返回一个epoll句柄(即一个文件描述符) */ int epoll_create(int size); /** * @param epfd 用epoll_create所创建的epoll句柄 * @param op 表示对epoll监控描述符控制的动作 * * EPOLL_CTL_ADD(注册新的fd到epfd) * EPOLL_CTL_MOD(修改已经注册的fd的监听事件) * EPOLL_CTL_DEL(epfd删除一个fd) * * @param fd 需要监听的文件描述符 * @param event 告诉内核需要监听的事件 * * @returns 成功返回0,失败返回-1, errno查看错误信息 */ int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); struct epoll_event { __uint32_t events; /* epoll 事件 */ epoll_data_t data; /* 用户传递的数据 */ } /* * events : {EPOLLIN, EPOLLOUT, EPOLLPRI, EPOLLHUP, EPOLLET, EPOLLONESHOT} */ typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event new_event; new_event.events = EPOLLIN | EPOLLOUT; new_event.data.fd = 5; epoll_ctl(epfd, EPOLL_CTL_ADD, 5, &new_event); /** * * @param epfd 用epoll_create所创建的epoll句柄 * @param event 从内核得到的事件集合 * @param maxevents 告知内核这个events有多大, * 注意: 值 不能大于创建epoll_create()时的size. * @param timeout 超时时间 * -1: 永久阻塞 * 0: 立即返回,非阻塞 * >0: 指定微秒 * * @returns 成功: 有多少文件描述符就绪,时间到时返回0 * 失败: -1, errno 查看错误 */ int epoll_wait(int epfd, struct epoll_event *event, int maxevents, int timeout); struct epoll_event my_event[1000]; int event_cnt = epoll_wait(epfd, my_event, 1000, -1);
-
epoll编程框架
//创建 epoll int epfd = epoll_crete(1000); //将 listen_fd 添加进 epoll 中 epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd,&listen_event); while (1) { //阻塞等待 epoll 中 的fd 触发 int active_cnt = epoll_wait(epfd, events, 1000, -1); for (i = 0 ; i < active_cnt; i++) { if (evnets[i].data.fd == listen_fd) { //accept. 并且将新accept 的fd 加进epoll中. } else if (events[i].events & EPOLLIN) { //对此fd 进行读操作 } else if (events[i].events & EPOLLOUT) { //对此fd 进行写操作 } } }
-
触发模式
-
水平触发
特点是:如果用户在监听epoll事件,当内核有事件的时候,会拷贝给用户态,但是如果用户只处理了一次,那么剩下的没有处理的会在下次epoll——wait再次返回该事件
后果是:如果用户永远不处理这个事件就会导致每次都会有该事件从内核到用户的拷贝,耗费性能,但是水平触发相对安全,最起码事件不会丢掉,除非用户处理完毕
-
边沿触发
跟水平粗发想法,当内核有时间到达,只会通知用户一次,如果用户不处理,以后将不会通知。这样减少了拷贝过程,增加了性能,但是 相对来说,如果用户马虎忘记处理,将会产生事件丢的情况
-