• select poll和epoll


    select poll epoll都是IO多路复用机制。这里的复用其实可以理解为复用的线程,即一个(或者较少的)线程完成多个IO的读写。这里总结下这三个函数的区别。

    1 select

    1.1 select原理分析

    1 select的函数原型是

    int select(int nfds, 
    	    fd_set *restrict readfds, 
    	    fd_set *restrict writefds, 
    	    fd_set *restrict errorfds, 
    	    struct timeval *restrict timeout);
    

    使用的时候需要将fd_set从用户空间copy到内核空间。select的使用方式类似如下

    while true {
    select(streams[])
    for i in streams[] { //需要遍历所有的fd_set
    if i has data // 判断是否有数据
    read until unavailable
    }
    }
    

    2 select的核心是do_select()。do_select首先会注册回调函数__pollwait,__pollwait会在被调用的时候将当前进程添加到设备的等待队列里。

    do_select会在一个for循环里调用设备的f_op->poll。而该函数有两个作用,一个是调用poll_wait()函数,一个是检测设备当前状态。而poll_wait会调用回调函数__pollwait,将当前进程加入到设备等待队列里。

    设备自己实现了当有读写的时候会唤醒等待队列里的进程。如果当前没有设备可读写,那么do_select()就将当前进程睡眠。设备会在有读写的时候唤醒进程。唤醒后设备必须重新轮询一遍所有的设备,调用poll来检测设备当前的状态以确定哪些可写可读。

    int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
    {
    	struct poll_wqueues table;
    	poll_table *wait;
    	poll_initwait(&table); // 注册回调函数__pollwait
    	wait = &table.pt;
    	// …
        for (;;) {
    		// …
    		for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
    			// …
    			struct fd f;
    			f = fdget(i);
    			if (f.file) {
    				const struct file_operations *f_op; // 重要
    				f_op = f.file->f_op; // 重要
    				mask = DEFAULT_POLLMASK;
    				if (f_op->poll) {
    					wait_key_set(wait, in, out,
    						     bit, busy_flag);
    					// 对每个fd进行I/O事件检测
    					mask = (*f_op->poll)(f.file, wait); // 函数指针,每个设备自定义自己的poll。每个设备拥有一个struct file_operations结构体,这个结构体里定义了各种用于操作设备的函数指针,具体怎么操作是设备自己定义的
    				}
    				fdput(f);
    				// …
    			}
    		}
    		// 退出循环体
    		if (retval || timed_out || signal_pending(current))
    			break;
    		// 没有可读写,让进程进入休眠
    		if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,
    				to, slack))
    			timed_out = 1;
    	}
    }
    

    3 file 结构

    struct file {
    	struct path		f_path;
    	struct inode		*f_inode;	/* cached value */
    	const struct file_operations	*f_op;
    
    	// …
    
    } __attribute__((aligned(4)));	/* lest something weird decides that 2 is OK */
    

    4 file_operations结构

    struct file_operations {
    	struct module *owner;
    	loff_t (*llseek) (struct file *, loff_t, int);
    	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    	ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    	int (*iterate) (struct file *, struct dir_context *);
    	// select()轮询设备fd的操作函数, poll调用poll_wait, 而poll_wait会调用回调函数__pollwait, __pollwait将当前进程加到等待队列里
    	unsigned int (*poll) (struct file *, struct poll_table_struct *);
    	// …	
    };
    

    5 简单总结来讲,select会遍历fd_set,调用f_op->poll(此poll非select/poll的poll),如果有可读/写的fd则返回可读/写的fd,如果没有则在每个fd的等待队列中加入当前进程,当前进程进入睡眠。当有fd可读/写的时候会唤醒当前进程,当前进行重新遍历fd_set,返回可读/写的所有fd。

    6 从中也可以看出select的几大缺点:

    1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
    2. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
    3. select支持的文件描述符数量太小了,默认是1024

    2 poll

    poll的实现原理和select类似,只是接口的方式不同。

    3 epoll

    1 epoll的函数原型是

    int epoll_create(int size); // 创建一个epoll对象,一般epollfd = epoll_create()
    
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // (epoll_add/epoll_del的合体),往epoll对象中增加/删除某一个流的某一个事件比如epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//有缓冲区内有数据时epoll_wait返回epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//缓冲区可写入时epoll_wait返回
    
    int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);  // 等待直到注册的事件发生
    

    epoll的使用方式类似如下:

    while true {
    active_stream[] = epoll_wait(epollfd) // 只返回可读/写的fd,而不是像select一样,返回所有的fd
    for i in active_stream[] {
    read or write till unavailable
    }
    }
    

    2 epoll_create。epoll会向内核注册一个文件系统,调用epoll_create时:

    1. 就会在这个虚拟的epoll文件系统里创建一个file结点;
    2. 并且在初始化的时候开辟epoll自己的内核cache,用于存储epoll_ctl传来的socket,这些socket以红黑树的方式组织放在cache里,用于支持快速的查找、插入、删除操作
    3. 还会再建立一个list链表,用于存储准备就绪的事件

    3 epoll_ctl。 当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到list里了

    4 epoll_wait。 epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep

    5 可见,

    1. select在醒着的时候要遍历整个fd_set,而epoll只需要判断一下list是否为空就可以了,这节约了大量的cpu时间;
    2. select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次。这也能节省不少的开销

    参考

    1. http://janfan.cn/chinese/2015/01/05/select-poll-impl-inside-the-kernel.html
    2. https://blog.csdn.net/wangfeng2500/article/details/9127421
    3. https://www.cnblogs.com/Anker/p/3265058.html
    4. https://www.zhihu.com/question/20122137
  • 相关阅读:
    oracle多个单引号的处理
    oracle 存储过程 动态sql语句
    Python内置方法的时间复杂度
    链表和数组的区别
    python enumerate用法总结
    Python 特殊语法:filter、map、reduce、lambda
    Pandas中DateFrame修改列名
    机器学习通用框架
    Python文件处理之文件写入方式与写缓存(三)
    转载: scikit-learn学习之回归分析
  • 原文地址:https://www.cnblogs.com/set-cookie/p/9685707.html
Copyright © 2020-2023  润新知