linux系统的网络IO(select/epoll)

参考文献

https://blog.csdn.net/davidsguo008/article/details/73556811
https://www.cnblogs.com/ccsccs/articles/4224253.html

概述

linux提供了select,poll,epoll等机制。

select

select需要使用两个system call (select 和 recvfrom)。select就是巧妙的利用等待队列机制让用户进程适当在没有资源可读/写时睡眠,有资源可读/写时唤醒。下面我们看看select睡眠的详细过程。select会循环遍历它所监测的fd_set(一组文件描述符(fd)的集合)内的所有文件描述符对应的驱动程序的poll函数。
驱动程序提供的poll函数首先会将调用select的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列),然后返回一个bitmask告诉select当前资源哪些可用。当select循环遍历完所有fd_set内指定的文件描述符对应的poll函数后,如果没有一个资源可用(即没有一个文件可供操作),则select让该进程睡眠,一直等到有资源可用为止,进程被唤醒(或者timeout)继续往下执行。

驱动程序提供的poll函数首先会将调用select的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列),然后返回一个bitmask告诉select当前资源哪些可用。驱动程序维护了针对自身资源读写的等待队列。当设备驱动发现自身资源变为可读写并且有进程睡眠在该资源的等待队列上时,就会唤醒这个资源等待队列上的进程。

函数原型如下:

//sys/select.h   
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);

select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有异常),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。

从 select函数返回后,内核告诉我们一下信息:

  • 对我们的要求已经做好准备的描述符的个数
  • 对于三种条件哪些描述符已经做好准备.(读,写,异常)

在这里插入图片描述

  1. 使用copy_from_user从用户空间拷贝fd_set到内核空间
  2. 注册回调函数__pollwait
  3. 遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)
  4. 以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。
  5. __pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。
  6. poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。
  7. 如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。
  8. 把fd_set从内核空间拷贝到用户空间。

下面代码展示了如何使用select来从网络上接收数据写入本地文件。

main()
{
	int sock; int fd;
	fd_set fds;
	struct timeval timeout={0,3}; //select等待3微秒,3微秒轮询,要非阻塞就置0
	char buffer[256]={0}; //256字节的接收缓冲区
/* 假定已经建立UDP连接,具体过程不写,简单,当然TCP也同理,主机ip和port都已经给定,要写的文件已经打开
sock=socket(...);
bind(...);
fd=open(...); */
	while{
		FD_ZERO(&fds); //每次循环都要清空集合,否则不能检测描述符变化
		FD_SET(sock,&fds); //添加描述符
		FD_SET(fd,&fds); //同上
		timeout.tv_sec=3;
		timeout.tv_usec=0;//select函数会不断修改timeout的值,所以每次循环都应该重新赋值[windows不受此影响]
		maxfdp=sock>fd?sock+1:fd+1; //描述符最大值加1
		switch(select(maxfdp,&fds,&fds,NULL,&timeout)) //select使用
		{
			case -1: exit(-1;break; //select错误,退出程序
			case 0:break; //再次轮询
			default:
			if(FD_ISSET(sock,&fds)) //测试sock是否可读,即是否网络上有数据
			{
				recvfrom(sock,buffer,256,.....);//接受网络数据
				if(FD_ISSET(fd,&fds)) //测试文件是否可写
				write(fd,buffer...);//写入文件
				buffer清空;
			}// end if break;
		}// end switch
	}//end while
}//end main

select有如下缺点

  • 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在linux内核头文件中,有这样的定义:#define __FD_SETSIZE 1024)
  • 内核 / 用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;
  • select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
  • 应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。

poll

和select一样,如果事件未发生,则等待事件发生,放弃cpu,直到被唤醒。

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

typedef struct pollfd {
        int fd;                               /* 需要被检测或选择的文件描述符*/
        short events;                   /* 对文件描述符fd上感兴趣的事件 */
        short revents;                  /* 文件描述符fd上当前实际发生的事件*/
} pollfd_t;
//常见的events有POLLIN/POLLRDNORM(可读)、POLLOUT/POLLWRNORM(可写)、POLLERR(出错)。


可以看到,poll用一个pollfd链表代替了select中的三个参数。此外,他没有最大监视文件的限制。

从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

epoll(内核>=2.6)

分成了三个系统调用

  1. 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
  2. 调用epoll_ctl向epoll对象中添加这100万个连接的套接字
  3. 调用epoll_wait收集发生的事件的连接

epoll实现机理的三大核心是:mmap、红黑树、链表。

  • mmap将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址(不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射映射到物理地址),使得这块物理内存对内核和对用户均可见,减少用户态和内核态之间的数据交换。内核可以直接看到epoll监听的句柄,效率高。下面的红黑树和链表,都存储在这样的内存中。
  • epoll在实现上采用红黑树去存储所有套接字,当添加或者删除一个套接字时(epoll_ctl),都在红黑树上去处理,红黑树本身插入和删除性能比较好,时间复杂度O(logN)。
  • 红黑树中每插入一个事件,该事件都会与相应的设备(网卡)驱动程序建立回调关系,当相应的事件发生后,就会调用这个回调函数(ep_poll_callback), 这个回调函数负责就所把这个事件添加到rdllist这个双向链表中。一旦有事件发生,epoll就会将该事件添加到双向链表中。那么当我们调用epoll_wait时,epoll_wait只需要检查rdlist双向链表中是否有存在注册的事件,效率非常可观。

epoll 支持两种模式,Edge Triggered(简称 ET) 和 Level Triggered(简称 LT)。JAVA NIO便采用了LT模式,NETTY自己实现的EPOLL。JAVA库则使用ET模式。

这里写图片描述

操作系统启动时,就会初始化epoll系统。主要工作是第一为epoll机制创造其专用的文件系统(调用epollcreate就是在这个文件系统中创建文件),第二就是为其准备一个专用的告诉cache区。我们的文件句柄被以红黑树的形式存放在这里。

每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在上述红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。
而所有添加到epoll对象中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。


struct epitem{
    struct rb_node  rbn;//红黑树节点
    struct list_head    rdllink;//双向链表节点
    struct epoll_filefd  ffd;  //事件句柄信息
    struct eventpoll *ep;    //指向其所属的eventpoll对象
    struct epoll_event event; //期待发生的事件类型

总结

select最大的问题是,在知道有IO事件发生后,却不知道具体是哪一个事件。必须无差别轮训所有句柄,才能拿到所需要的返回值。这也是epoll后来出现的意义。他们本质上都是IO多路复用的思路,只不过是在改良。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值