理解select,poll,epoll实现分析
mark 引用:http://janfan.cn/chinese/2015/01/05/select-poll-impl-inside-the-kernel.html 文章
select()/poll() 的内核实现
05 Jan 2015
同时对多个文件设备进行I/O事件监听的时候(I/O multiplexing),我们经常会用到系统调用函数select()
poll()
,甚至是为大规模成百上千个文件设备进行并发读写而设计的epoll()
。
I/O multiplexing: When an application needs to handle multiple I/O descriptors at the same time, and I/O on any one descriptor can result in blocking. E.g. file and socket descriptors, multiple socket descriptors
一旦某些文件设备准备好了,可以读写了,或者是我们自己设置的timeout时间到了,这些函数就会返回,根据返回结果主程序继续运行。
用了这些函数有什么好处? 我们自己本来就可以实现这种I/O Multiplexing啊,比如说:
- 创建多个进程或线程来监听
- Non-blocking读写监听的轮询(polling)
- 异步I/O(Asynchronous I/O)与Unix Signal事件触发
想要和我们自己的实现手段做比较,那么首先我们就得知道这些函数在背后是怎么实现的。 本文以Linux(v3.9-rc8)源码为例,探索select()
poll()
的内核实现。
select()
源码概述
首先看看select()
函数的函数原型,具体用法请自行输入命令行$ man 2 select
查阅吧 : )
int select(int nfds,
fd_set *restrict readfds,
fd_set *restrict writefds,
fd_set *restrict errorfds,
struct timeval *restrict timeout);
下文将按照这个结构来讲解select()
在Linux的实现机制。
select()
内核入口do_select()
的循环体struct file_operations
设备驱动的操作函数scull
驱动实例poll_wait
与设备的等待队列- 其它相关细节
- 最后
好,让我们开始吧 : )
select()
内核入口
我们首先把目光放到文件fs/select.c
文件上。
SYSCALL_DEFINE5(select, int, n,
fd_set __user *, inp,
fd_set __user *, outp,
fd_set __user *, exp,
struct timeval __user *, tvp)
{
// …
ret = core_sys_select(n, inp, outp, exp, to);
ret = poll_select_copy_remaining(&end_time, tvp, 1, ret);
return ret;
}
int core_sys_select(int n,
fd_set __user *inp,
fd_set __user *outp,
fd_set __user *exp,
struct timespec *end_time)
{
fd_set_bits fds;
// …
if ((ret = get_fd_set(n, inp, fds.in)) ||
(ret = get_fd_set(n, outp, fds.out)) ||
(ret = get_fd_set(n, exp, fds.ex)))
goto out;
zero_fd_set(n, fds.res_in);
zero_fd_set(n, fds.res_out);
zero_fd_set(n, fds.res_ex);
// …
ret = do_select(n, &fds, end_time);
// …
}
很好,我们找到了一个宏定义的select()
函数的入口,继续深入,可以看到其中最重要的就是do_select()
这个内核函数。
do_select()
的循环体
do_select()
实质上是一个大的循环体,对每一个主程序要求监听的设备fd(File Descriptor)做一次struct file_operations
结构体里的poll
操作。
int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
{
// …
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);
}
fdput(f);
// …
}
}
// 退出循环体
if (retval || timed_out || signal_pending(current))
break;
// 进入休眠
if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,
to, slack))
timed_out = 1;
}
}
(*f_op->poll)
会返回当前设备fd的状态(比如是否可读可写),根据这个状态,do_select()
接着做出不同的动作
- 如果设备fd的状态与主程序的感兴趣的I/O事件匹配,则记录下来,
do_select()
退出循环体,并把结果返回给上层主程序。 - 如果不匹配,
do_select()
发现timeout已经到了或者进程有signal信号打断,也会退出循环,只是返回空的结果给上层应用。
但如果do_select()
发现当前没有事件发生,又还没到timeout,更没signal打扰,内核会在这个循环体里面永远地轮询下去吗?
select()
把全部fd检测一轮之后如果没有可用I/O事件,会让当前进程去休眠一段时间,等待fd设备或定时器来唤醒自己,然后再继续循环体看看哪些fd可用,以此提高效率。
int poll_schedule_timeout(struct poll_wqueues *pwq, int state,
ktime_t *expires, unsigned long slack)
{
int rc = -EINTR;
// 休眠
set_current_state(state);
if (!pwq->triggered)
rc = schedule_hrtimeout_range(expires, slack, HRTIMER_MODE_ABS);
__set_current_state(TASK_RUNNING);
/*
* Prepare for the next iteration.
*
* The following set_mb() serves two purposes. First, it's
* the counterpart rmb of the wmb in pollwake() such that data
* written before wake up is always visible after wake up.
* Second, the full barrier guarantees that triggered clearing
* doesn't pass event check of the next iteration. Note that
* this problem doesn't exist for the first iteration as
* add_wait_queue() has full barrier semantics.
*/
set_mb(pwq->triggered, 0);
return rc;
}
EXPORT_SYMBOL(poll_schedule_timeout);
struct file_operations
设备驱动的操作函数
设备发现I/O事件时会唤醒主程序进程? 每个设备fd的等待队列在哪?我们什么时候把当前进程添加到它们的等待队列里去了?
mask = (*f_op->poll)(f.file, wait);
就是上面这行代码干的好事。 不过在此之前,我们得先了解一下系统内核与文件设备的驱动程序之间耦合框架的设计。
上文对每个设备的操作f_op->poll
,是一个针对每个文件设备特定的内核函数,区别于我们平时用的系统调用poll()
。 并且,这个操作是select()
poll()
epoll()
背后实现的共同基础。
Support for any of these calls requires support from the device driver. This support (for all three calls,
select()
poll()
andepoll()
) is provided through the driver’s poll method.
Linux的设计很灵活,它并不知道每个具体的文件设备是怎么操作的(怎么打开,怎么读写),但内核让每个设备拥有一个struct file_operations
结构体,这个结构体里定义了各种用于操作设备的函数指针,指向操作每个文件设备的驱动程序实现的具体操作函数,即设备驱动的回调函数(callback)。
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 */
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的操作函数
unsigned int (*poll) (struct file *, struct poll_table_struct *);
// …
};
这个f_op->poll
对文件设备做了什么事情呢? 一是调用poll_wait()
函数(在include/linux/poll.h
文件); 二是检测文件设备的当前状态。
unsigned int (*poll) (struct file *filp, struct poll_table_struct *pwait);
The device method is in charge of these two steps:
- Call
poll_wait()
on one or more wait queues that could indicate a change in the poll status. If no file descriptors are currently available for I/O, the kernel causes the process to wait on the wait queues for all file descriptors passed to the system call.- Return a bit mask describing the operations (if any) that could be immediately performed without blocking.
或者来看另一个版本的说法:
For every file descriptor, it calls that fd’s
poll()
method, which will add the caller to that fd’s wait queue, and return which events (readable, writeable, exception) currently apply to that fd.
下一节里我们会结合驱动实例程序来理解。
scull
驱动实例
由于Linux设备驱动的耦合设计,对设备的操作函数都是驱动程序自定义的,我们必须要结合一个具体的实例来看看,才能知道f_op->poll
里面弄得是什么鬼。
在这里我们以Linux Device Drivers, Third Edition一书中的例子——scull
设备的驱动程序为例。
scull
(Simple Character Utility for Loading Localities). scull is a char driver that acts on a memory area as though it were a device.
scull
设备不同于硬件设备,它是模拟出来的一块内存,因此对它的读写更快速更自由,内存支持你顺着读倒着读点着读怎么读都可以。 我们以书中“管道”(pipe)式,即FIFO的读写驱动程序为例。
首先是scull_pipe
的结构体,注意wait_queue_head_t
这个队列类型,它就是用来记录等待设备I/O事件的进程的。
struct scull_pipe {
wait_queue_head_t inq, outq; /* read and write queues */
char *buffer, *end; /* begin of buf, end of buf */
int buffersize; /* used in pointer arithmetic */
char *rp, *wp; /* where to read, where to write */
int nreaders, nwriters; /* number of openings for r/w */
struct fasync_struct *async_queue; /* asynchronous readers */
struct mutex mutex; /* mutual exclusion semaphore */
struct cdev cdev; /* Char device structure */
};
scull
设备的轮询操作函数scull_p_poll
,驱动模块加载后,这个函数就被挂到(*poll)
函数指针上去了。
我们可以看到它的确是返回了当前设备的I/O状态,并且调用了内核的poll_wait()
函数,这里注意,它把自己的wait_queue_head_t
队列也当作参数传进去了。
static unsigned int scull_p_poll(struct file *filp, poll_table *wait)
{
struct scull_pipe *dev = filp->private_data;
unsigned int mask = 0;
/*
* The buffer is circular; it is considered full
* if "wp" is right behind "rp" and empty if the
* two are equal.
*/
mutex_lock(&dev->mutex);
poll_wait(filp, &dev->inq, wait);
poll_wait(filp, &dev->outq, wait);
if (dev->rp != dev->wp)
mask |= POLLIN | POLLRDNORM; /* readable */
if (spacefree(dev))
mask |= POLLOUT | POLLWRNORM; /* writable */
mutex_unlock(&dev->mutex);
return mask;
}
当scull
有数据写入时,它会把wait_queue_head_t
队列里等待的进程给唤醒。
static ssize_t scull_p_write(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
// …
/* Make sure there's space to write */
// …
/* ok, space is there, accept something */
// …
/* finally, awake any reader */
wake_up_interruptible(&dev->inq); /* blocked in read() and select() */
// …
}
可是wait_queue_head_t
队列里的进程是什么时候装进去的? 肯定是poll_wait
搞的鬼! 我们又得回到该死的Linux内核去了。
poll_wait
与设备的等待队列
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
if (p && p->_qproc && wait_address)
p->_qproc(filp, wait_address, p);
}
/*
* Do not touch the structure directly, use the access functions
* poll_does_not_wait() and poll_requested_events() instead.
*/
typedef struct poll_table_struct {
poll_queue_proc _qproc;
unsigned long _key;
} poll_table;
/*
* structures and helpers for f_op->poll implementations
*/
typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);
可以看到,poll_wait()
其实就是只是直接调用了struct poll_table_struct
结构里绑定的函数指针。 我们得找到struct poll_table_struct
初始化的地方。
The
poll_table
structure is just a wrapper around a function that builds the actual data structure. That structure, forpoll
andselect
, is a linked list of memory pages containingpoll_table_entry
structures.
struct poll_table_struct
里的函数指针,是在do_select()
初始化的。
int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
{
struct poll_wqueues table;
poll_table *wait;
poll_initwait(&table);
wait = &table.pt;
// …
}
void poll_initwait(struct poll_wqueues *pwq)
{
// 初始化poll_table里的函数指针
init_poll_funcptr(&pwq->pt, __pollwait);
pwq->polling_task = current;
pwq->triggered = 0;
pwq->error = 0;
pwq->table = NULL;
pwq->inline_index = 0;
}
EXPORT_SYMBOL(poll_initwait);
static inline void init_poll_funcptr(poll_table *pt, poll_queue_proc qproc)
{
pt->_qproc = qproc;
pt->_key = ~0UL; /* all events enabled */
}
我们现在终于知道,__pollwait()
函数,就是poll_wait()
幕后的真凶。
add_wait_queue()
把当前进程添加到设备的等待队列wait_queue_head_t
中去。
/* Add a new entry */
static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,
poll_table *p)
{
struct poll_wqueues *pwq = container_of(p, struct poll_wqueues, pt);
struct poll_table_entry *entry = poll_get_entry(pwq);
if (!entry)
return;
entry->filp = get_file(filp);
entry->wait_address = wait_address;
entry->key = p->_key;
init_waitqueue_func_entry(&entry->wait, pollwake);
entry->wait.private = pwq;
// 把当前进程装到设备的等待队列
add_wait_queue(wait_address, &entry->wait);
}
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
{
unsigned long flags;
wait->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&q->lock, flags);
__add_wait_queue(q, wait);
spin_unlock_irqrestore(&q->lock, flags);
}
EXPORT_SYMBOL(add_wait_queue);
static inline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t *new)
{
list_add(&new->task_list, &head->task_list);
}
/**
* Insert a new element after the given list head. The new element does not
* need to be initialised as empty list.
* The list changes from:
* head → some element → ...
* to
* head → new element → older element → ...
*
* Example:
* struct foo *newfoo = malloc(...);
* list_add(&newfoo->entry, &bar->list_of_foos);
*
* @param entry The new element to prepend to the list.
* @param head The existing list.
*/
static inline void
list_add(struct list_head *entry, struct list_head *head)
{
__list_add(entry, head, head->next);
}
其它相关细节
fd_set
实质上是一个unsigned long
数组,里面的每一个long
整值的每一位都代表一个文件,其中置为1的位表示用户要求监听的文件。 可以看到,select()
能同时监听的fd好少,只有1024个。
#define __FD_SETSIZE 1024
typedef struct {
unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;
typedef __kernel_fd_set fd_set;
- 所谓的文件描述符fd (File Descriptor),大家也知道它其实只是一个表意的整数值,更深入地说,它是每个进程的file数组的下标。
struct fd {
struct file *file;
unsigned int flags;
};
select()
系统调用会创建一个poll_wqueues
结构体,用来记录相关I/O设备的等待队列;当select()
退出循环体返回时,它要把当前进程从全部等待队列中移除——这些设备再也不用着去唤醒当前队列了。
The call to
poll_wait
sometimes also adds the process to the given wait queue. The whole structure must be maintained by the kernel so that the process can be removed from all of those queues before poll or select returns.
/*
* Structures and helpers for select/poll syscall
*/
struct poll_wqueues {
poll_table pt;
struct poll_table_page *table;
struct task_struct *polling_task;
int triggered;
int error;
int inline_index;
struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];
};
struct poll_table_entry {
struct file *filp;
unsigned long key;
wait_queue_t wait;
wait_queue_head_t *wait_address;
};
wait_queue_head_t
就是一个进程(task)的队列。
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
select()
与epoll()
的比较
- select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用
epoll_wait
不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。- epoll所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以
cat /proc/sys/fs/file-max
察看,一般来说这个数目和系统内存关系很大。
更具体的比较可以参见这篇文章。
最后
非常艰难的,我们终于来到了这里(T^T)
总结一下select()
的大概流程(poll
同理,只是用于存放fd的数据结构不同而已)。
- 先把全部fd扫一遍
- 如果发现有可用的fd,跳到5
- 如果没有,当前进程去睡觉xx秒
- xx秒后自己醒了,或者状态变化的fd唤醒了自己,跳到1
- 结束循环体,返回
我相信,你肯定还没懂,这代码实在是乱得一逼,被我剪辑之后再是乱得没法看了(叹气)。 所以看官请务必亲自去看Linux源码,在这里我已经给出了大致的方向,等你看完源码回来,这篇文章你肯定也就明白了。 当然别忘了下面的参考资料,它们可帮大忙了 :P
主要参考资料
理解select,poll,epoll实现分析的更多相关文章
- 理解 select poll epoll
举例说明:老师收学生作业,相当于应用层调用I/O操作. 1.老师逐个收学生作业,学生没有做完,只能阻塞等待,收了之后,再去收下一个学生的作业.这显然存在性能问题. 2.怎么解决上面的问题? 老师找个班 ...
- select, poll, epoll的实现分析
select, poll, epoll都是Linux上的IO多路复用机制.知其然知其所以然,为了更好地理解其底层实现,这几天我阅读了这三个系统调用的源码. 以下源代码摘自Linux4.4.0内核. 预 ...
- 多进程、协程、事件驱动及select poll epoll
目录 -多线程使用场景 -多进程 --简单的一个多进程例子 --进程间数据的交互实现方法 ---通过Queues和Pipe可以实现进程间数据的传递,但是不能实现数据的共享 ---Queues ---P ...
- Java IO 学习(二)select/poll/epoll
如上文所说,select/poll/epoll本质上都是同步阻塞的,但是由于实现了IO多路复用,在处理聊天室这种需要处理大量长连接但是每个连接上数据事件较少的场景时,相比最原始的为每个连接新开一个线程 ...
- Linux内核中网络数据包的接收-第二部分 select/poll/epoll
和前面文章的第一部分一样,这些文字是为了帮别人或者自己理清思路的.而不是所谓的源代码分析.想分析源代码的,还是直接debug源代码最好,看不论什么文档以及书都是下策. 因此这类帮人理清思路的文章尽可能 ...
- 【原创】Linux select/poll机制原理分析
前言 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 1. 概述 Linux系统 ...
- Linux下select&poll&epoll的实现原理(一)
最近简单看了一把 linux-3.10.25 kernel中select/poll/epoll这个几个IO事件检测API的实现.此处做一些记录.其基本的原理是相同的,流程如下 先依次调用fd对应的st ...
- 转一贴,今天实在写累了,也看累了--【Python异步非阻塞IO多路复用Select/Poll/Epoll使用】
下面这篇,原理理解了, 再结合 这一周来的心得体会,整个框架就差不多了... http://www.haiyun.me/archives/1056.html 有许多封装好的异步非阻塞IO多路复用框架, ...
- select.poll,epoll的区别与应用
先讲讲同步I/O的五大模型 阻塞式I/O, 非阻塞式I/O, I/O复用,信号驱动I/O(SIGIO),异步I/O模型 而select/poll/epoll属于I/O复用模型 select函数 该函数 ...
- select poll epoll三者之间的比较
一.概述 说到Linux下的IO复用,系统提供了三个系统调用,分别是select poll epoll.那么这三者之间有什么不同呢,什么时候使用三个之间的其中一个呢? 下面,我将从系统调用原型来分析其 ...
随机推荐
- 解决maven项目 maven install失败 错误 Failed to execute goal org.apache.maven.plugins
1.Maven构建失败 Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin: 2.3.4 :compile ( ...
- ARKit从入门到精通(2)-ARKit工作原理及流程介绍
转载:http://blog.csdn.net/u013263917/article/details/73038519 1.1-写在前面的话 1.2-ARKit与SceneKit的关系 1.3-ARK ...
- 基于jQuery和CSS3超酷Material Design风格滑动菜单导航特效
分享一款效果非常炫酷的谷歌 Material Design 风格jQuery和CSS3滑动选项卡特效.该选项卡特效集合了扁平风格设计和按钮点击波特效.是一款设计的非常不错的Material Desig ...
- 为什么说Thunderbird是最好的桌面RSS阅读器
也许现在再讨论RSS阅读器似乎已经过时了,毕竟随着社交网络服务的发展,通过一个带有大众评分能力的社交网络(比如reddit),相比RSS的固定订阅而言,也许你能更快地在你所关心的话题上更快地获得新的资 ...
- flume 多chanel配置
#配置文 a1.sources= r1 a1.sinks= k1 k2 a1.channels= c1 c2 #Describe/configure the source a1.sources.r1. ...
- Spark Streaming 执行流程
Spark Streaming 是基于spark的流式批处理引擎,其基本原理是把输入数据以某一时间间隔批量的处理,当批处理间隔缩短到秒级时,便可以用于处理实时数据流. 本节描述了Spark Strea ...
- [Linux]Shell的运算符和特殊变量
说起Shell脚本,免不了用变量.特别是对于这种一堆符号表示变量的语言来说,你不了解一下相关变量的本意,根本无从下手.譬如写个循环遍历,$#就起了好大作用.所以还是有必要记录一下,也是对学习的一个笔记 ...
- C# DIctionary:集合已修改,可能无法执行枚举操作
C#中直接对集合Dictionary进行遍历并修改其中的值,会报错,如下代码就会报错:集合已修改;可能无法执行枚举操作.代码如下 public void ForeachDic() { Dictiona ...
- Mysql5.7主主互备安装配置
一.安装说明 ======================================================================================= 环境: ...
- 6、Qt Meta Object system 学习
原文地址:http://blog.csdn.net/ilvu999/article/details/8049908 使用 meta object system 继承自 QOject 类定义中添加 Q_ ...