前引
鉴于之前我自己看LevelDB
源码中间看了半天未果 而且自己也看不动了
这段时间自己又在准备 在刷大量面经 看到面试题里面经常会问Select/Poll/Epoll
的实现原理和区别
我一直认为 源码之前 了无秘密
在没有看到源码和自己去深度探索过一些东西的运行流程的时候 所有的东西都只不过是粗略了解概念罢了
秉着所有东西好好准备的原则 以及 赎罪我之前看LevelDB
源码没有看完的那段时间 我们的Select/Poll/Epoll
就以源码剖析的角度来看看 究竟是内核是怎么支持实现的IO复用
这几篇我是无论如何都要写完的 不会就中途弃稿删除的
那我们下面就一步步来吧
Linux Kernel Select/Poll/Epoll源码深度历险(一)---- 探索Select底层运行原理
1、先得到Linux Kernel Source Code
首先就是我们需要得到Linux Kernel Soucr Code
没有源码我们怎么来看底层实现呢
有的人就要说了 啊 你不是之前安了虚拟机的吗 直接看你安装的Ubuntu
找找源码就可以了吗 对的 刚开始我也是这样想的
但是之后我认真的搜索了一下 发现找半天都没有找到Select.c
搜索了半天也只找到了其头文件select.h
也罢 我们直接去下载一个内核源码就好了 下面给出两个连接 一个是目前让仍然保持迭代 Linux
之父以及Linux
官方所维持的最新版本链接
第二个链接就是我们之后要用的 较早版本的Linux Kernel 2.6.28
版本的
因为在Linux kernel 2.6
版本以上 内核就支持了Epoll
我所写的High-Performance-WebServer
也是必须要求内核版本在2.6
以上才能支持(主要是由于IO复用函数我默认就选择的是Epoll
内核必须支持Epoll
)
第二个链接直接就是下载链接 直接下载即可
torvalds linux kernel soucr code Github链接
linux-2.6.28.6.tar.bz2 下载链接
在下载好之后 我们在linux
上面解压 最后就得到了我们的源码了
那我们的源码历险就要开始了
2、开始历险 顺藤摸瓜探索Select
1、初得用户态 Select调用代码表层
这里其实有点头疼的就是 我没找到Select
的用户层代码
这是最让人头疼的 但其实man
中也告诉了我们用户层select
代码用法 这也算天无绝人之路了
看看man select
定义的文件我找了半天 终于找到了 应该用户态调用的文件是在GNU C Library
里面
这个这是定义文件
函数实现的部分我还是想再找找看 至少让我看下用户态到内核态的那个调用语句嘛
找了半天没找到 算了 我们还是直接去看内核调用SELECT
的入口就好了
2、 欲求无果用户态调用 直接转向内核调用Select入口
这个时候 我们直接来到内核调用SELECT
入口就好了
毕竟用户态的函数没找到 反正是需要转到这里来的
文件是在fs/select.c
中
搜索了一下select
得到了 我们把它拿出来看看
代码如下 我们初步看一看
SYSCALL_DEFINE5
这个5
是五个参数的意思 目前这个代码是在内核态运行的 也应该是由int 0x80
软中断进入的
首先先判断了 是否有tvp
指针 这里的tvp
我们判断一下就是用户态的第五个参数struct timeval*
参数 看看是否放入了 如果设置为NULL
就直接跳过了 没设置的话则会调用copy_from_user
这个函数调用不用去看 都知道是讲用户态的这个结构体 拷贝到目前的tv
这个目前内核太态的局部变量里面
下面这个就是设置 超时时间timeout
我们的主要目的不是来看这个的 这个就暂时略过 反正源函数也在这个相同的文件中
到下面了 这个两个函数应该是主要部分 先调用了core_sys_select
我猜测这个应该是select
的核心实现函数 最后的poll_select_copy_remaining
是和我们的第五个参数超时有关的 那我们就直接来到core_sys_select
SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp,
fd_set __user *, exp, struct timeval __user *, tvp)
{
struct timespec end_time, *to = NULL;
struct timeval tv;
int ret;
if (tvp) {
if (copy_from_user(&tv, tvp, sizeof(tv)))
return -EFAULT;
to = &end_time;
if (poll_select_set_timeout(to,
tv.tv_sec + (tv.tv_usec / USEC_PER_SEC),
(tv.tv_usec % USEC_PER_SEC) * NSEC_PER_USEC))
return -EINVAL;
}
ret = core_sys_select(n, inp, outp, exp, to);
ret = poll_select_copy_remaining(&end_time, tvp, 1, ret);
return ret;
}
3、 仔细查看core_sys_select select核心实现
直到刚刚 我才基本上理清楚了Select
的系统调用是怎么样的
当然 过程中我打开了相当多的文件 有些函数的话 我们看名称也就明白了什么意思 刚刚觉得 如果我没有理清楚代码就来写的话 则是一种不负责任的表现 从头开始理清了 这样子自己写博客的时候 也知道该怎么下手 该怎么写才合理 上面其实也都算是小打小闹 那我们下面也算是正式开始了
1、先从fd_set结构体入手
既然要搞懂Select
系统调用 至少要把最关键的FD_SET
也就是我们传入的参数结构给搞懂吧 这也是我找了半天才找到的文件
那我们下面走起
linux/types.h
中的文件 找了半天结果发现这里只是改了一下名字fd_set
在内部定义是__kerenl_fd_set
那我们再找找__kerenl_fd_set
我们根据上面包含的头文件 找到了
linux/posix_types.h
fd_set
的庐山真面目出来了
这里我们就好好的来分析出来东西了
如果面试题里面有 为什么Select
的文件描述符默认最大只有1024
的话 这里我们根据源码就可以做出回答了
但是我们不急着来回答 先来分析一下吧
这里 其实fd_set
本质就是以unsigned long
的数组 以数据结构的角度来看 其实也就是位图 这个位图我们根据上面的宏定义来看 其实也就发现 这里的1024
其实在位图中 也就是1024
位而已 换算为字节数 不过也就是128
再换算为32
个Unsigned long
为什么只有1024
:由于Linux
内核宏定义设置FD_SET
位图最多1024位 但可以修改宏定义使其突破监控文件描述符最大只有1024
ok 在看完FD_SET
后 也许有的人也很好奇FD_CLR
FD_SET
FD_ZERO
这里我也去找了找相应的头文件
2、看看fd_set 相关的宏定义
由于我在我的Ubuntu
里面找到了 所以就看看我虚拟机里面的关于这个定义 肯定是一样的
x86_64-linux-gnu/sys/select.h
x86_64-linux-gnu/bits/select.h
代码很简单 其实也就是位图 *** 作而已 没什么大不了的 很容易就看懂了
为了方便看代码 我这里就再贴一份 就不多写了 很简单
/* We don't use `memset' because this would require a prototype and
the array isn't too big. */
# define __FD_ZERO(set) \
do { \
unsigned int __i; \
fd_set *__arr = (set); \
for (__i = 0; __i < sizeof (fd_set) / sizeof (__fd_mask); ++__i) \
__FDS_BITS (__arr)[__i] = 0; \
} while (0)
#define __FD_SET(d, set) \
((void) (__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d)))
#define __FD_CLR(d, set) \
((void) (__FDS_BITS (set)[__FD_ELT (d)] &= ~__FD_MASK (d)))
#define __FD_ISSET(d, set) \
((__FDS_BITS (set)[__FD_ELT (d)] & __FD_MASK (d)) != 0)
3、转回core_sys_select 开始分析(上)
上面没有贴出来这个函数的源代码 这里就贴一下
下面我们就开始一部分一部分开始分析了
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;
void *bits;
int ret, max_fds;
unsigned int size;
struct fdtable *fdt;
/* Allocate small arguments on the stack to save memory and be faster */
long stack_fds[SELECT_STACK_ALLOC/sizeof(long)];
ret = -EINVAL;
if (n < 0)
goto out_nofds;
/* max_fds can increase, so grab it once to avoid race */
rcu_read_lock();
fdt = files_fdtable(current->files);
max_fds = fdt->max_fds;
rcu_read_unlock();
if (n > max_fds)
n = max_fds;
/*
* We need 6 bitmaps (in/out/ex for both incoming and outgoing),
* since we used fdset we need to allocate memory in units of
* long-words.
*/
size = FDS_BYTES(n);
bits = stack_fds;
if (size > sizeof(stack_fds) / 6) {
/* Not enough space in on-stack array; must use kmalloc */
ret = -ENOMEM;
bits = kmalloc(6 * size, GFP_KERNEL);
if (!bits)
goto out_nofds;
}
fds.in = bits;
fds.out = bits + size;
fds.ex = bits + 2*size;
fds.res_in = bits + 3*size;
fds.res_out = bits + 4*size;
fds.res_ex = bits + 5*size;
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);
if (ret < 0)
goto out;
if (!ret) {
ret = -ERESTARTNOHAND;
if (signal_pending(current))
goto out;
ret = 0;
}
if (set_fd_set(n, inp, fds.res_in) ||
set_fd_set(n, outp, fds.res_out) ||
set_fd_set(n, exp, fds.res_ex))
ret = -EFAULT;
out:
if (bits != stack_fds)
kfree(bits);
out_nofds:
return ret;
}
下面截取了上半部分
首先说说上面的变量吧fd_set_bits
是位图的结合体 因为我们传入了3个位图(fd_set
) 它在遍历的时候 也需要三个位图来记录哪些是有相应的 所以这个结构体中 就有6个变量指针 分别是3个我们的inp
outp
exp
还有3个是我们之后要返回的rinp
routp
rexp
结构如下
typedef struct {
unsigned long *in, *out, *ex;
unsigned long *res_in, *res_out, *res_ex;
} fd_set_bits;
然后就是bits
我们的位图存储需要从用户态存回内核态 我们这里为了高效一点 就采用的开栈的方式stack_fds
来存储我们的位图(包括复制用户态的位图 和 我们需要返回的位图) 后面就可以看到我们的位图存储了
struct fdtable
的话 就是我们对于文件描述符相关的结构体了 我们这里不用了解其他的 只需要直到可以得到目前进程打开的最大文件描述符是多少即可
上面基本上就介绍完了 到后面的rcu_read_lock
这个函数我去看了看 在这里的意义 应该就是给我们的进程拍了个快照 我们得到了目前进程最大打开的文件描述符 作用就是与我们传进来的监视最大文件描述符做比较而已
那我们对于这部分代码也就介绍完了
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;
void *bits;
int ret, max_fds;
unsigned int size;
struct fdtable *fdt;
/* Allocate small arguments on the stack to save memory and be faster */
long stack_fds[SELECT_STACK_ALLOC/sizeof(long)];
ret = -EINVAL;
if (n < 0)
goto out_nofds;
/* max_fds can increase, so grab it once to avoid race */
rcu_read_lock();
fdt = files_fdtable(current->files);
max_fds = fdt->max_fds;
rcu_read_unlock();
if (n > max_fds)
n = max_fds;
......
4、转回core_sys_select 开始分析(下)
那我们书接上回 继续来分析
这里的话 其实就是在为我们的位图存储做工作了
一是检测我们上面的开栈空间是否足够 不足的话 就要调用内核的kmalloc
来分配空间 二是讲之前的6个位图指针指向分配好的位置 三是复制我们的用户态的位图 内存从用户态分配到内核态get_fd_set
就是在做这个工作 成功了返回0 把后面的3个我们之后要返回 也就是要用的存储位图清零后 就要进入真正的核心函数do_select
了
其实do_select
才是真正的核心工作函数 这里只不过是在为后面做准备工作而已 我们可以提前看一下后面的函数do_select
返回后 我们又调用了set_fd_set
其实也就是把我们的 准备ret
的位图 又重新复制回了我们输入参数的用户态所在内存而已
最后再返回 我们目前有多少文件描述符引起响应的个数 结束了调用
那我们最后来看看do_select
Select
函数的深度历险 我们基本上也就走完了 走着
.........
/*
* We need 6 bitmaps (in/out/ex for both incoming and outgoing),
* since we used fdset we need to allocate memory in units of
* long-words.
*/
size = FDS_BYTES(n);
bits = stack_fds;
if (size > sizeof(stack_fds) / 6) {
/* Not enough space in on-stack array; must use kmalloc */
ret = -ENOMEM;
bits = kmalloc(6 * size, GFP_KERNEL);
if (!bits)
goto out_nofds;
}
fds.in = bits;
fds.out = bits + size;
fds.ex = bits + 2*size;
fds.res_in = bits + 3*size;
fds.res_out = bits + 4*size;
fds.res_ex = bits + 5*size;
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);
if (ret < 0)
goto out;
if (!ret) {
ret = -ERESTARTNOHAND;
if (signal_pending(current))
goto out;
ret = 0;
}
if (set_fd_set(n, inp, fds.res_in) ||
set_fd_set(n, outp, fds.res_out) ||
set_fd_set(n, exp, fds.res_ex))
ret = -EFAULT;
out:
if (bits != stack_fds)
kfree(bits);
out_nofds:
return ret;
}
上面代码用到的函数的补充
static inline
int get_fd_set(unsigned long nr, void __user *ufdset, unsigned long *fdset)
{
nr = FDS_BYTES(nr);
if (ufdset)
return copy_from_user(fdset, ufdset, nr) ? -EFAULT : 0;
memset(fdset, 0, nr);
return 0;
}
static inline unsigned long __must_check
set_fd_set(unsigned long nr, void __user *ufdset, unsigned long *fdset)
{
if (ufdset)
return __copy_to_user(ufdset, fdset, FDS_BYTES(nr));
return 0;
}
4、原来do_select才是真正实现功能的核心函数(内容非常多 稍复杂)
1、仔细分析do_select 核心代码(上)
还是老样子 这里先把代码全列出来了 后面一部分一部分来
int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
{
ktime_t expire, *to = NULL;
struct poll_wqueues table;
poll_table *wait;
int retval, i, timed_out = 0;
unsigned long slack = 0;
rcu_read_lock();
retval = max_select_fd(n, fds);
rcu_read_unlock();
if (retval < 0)
return retval;
n = retval;
poll_initwait(&table);
wait = &table.pt;
if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {
wait = NULL;
timed_out = 1;
}
if (end_time && !timed_out)
slack = estimate_accuracy(end_time);
retval = 0;
for (;;) {
unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
set_current_state(TASK_INTERRUPTIBLE);
inp = fds->in; outp = fds->out; exp = fds->ex;
rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
unsigned long in, out, ex, all_bits, bit = 1, mask, j;
unsigned long res_in = 0, res_out = 0, res_ex = 0;
const struct file_operations *f_op = NULL;
struct file *file = NULL;
in = *inp++; out = *outp++; ex = *exp++;
all_bits = in | out | ex;
if (all_bits == 0) {
i += __NFDBITS;
continue;
}
for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {
int fput_needed;
if (i >= n)
break;
if (!(bit & all_bits))
continue;
file = fget_light(i, &fput_needed);
if (file) {
f_op = file->f_op;
mask = DEFAULT_POLLMASK;
if (f_op && f_op->poll)
mask = (*f_op->poll)(file, retval ? NULL : wait);
fput_light(file, fput_needed);
if ((mask & POLLIN_SET) && (in & bit)) {
res_in |= bit;
retval++;
}
if ((mask & POLLOUT_SET) && (out & bit)) {
res_out |= bit;
retval++;
}
if ((mask & POLLEX_SET) && (ex & bit)) {
res_ex |= bit;
retval++;
}
}
}
if (res_in)
*rinp = res_in;
if (res_out)
*routp = res_out;
if (res_ex)
*rexp = res_ex;
cond_resched();
}
wait = NULL;
if (retval || timed_out || signal_pending(current))
break;
if (table.error) {
retval = table.error;
break;
}
/*
* If this is the first loop and we have a timeout
* given, then we convert to ktime_t and set the to
* pointer to the expiry value.
*/
if (end_time && !to) {
expire = timespec_to_ktime(*end_time);
to = &expire;
}
if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS))
timed_out = 1;
}
__set_current_state(TASK_RUNNING);
poll_freewait(&table);
return retval;
}
先看一下下面的变量吧
poll_wqueues
是我们的核心的数据结构poll_wqueues
里面最核心的就是一个函数指针(也就是对于驱动设备调用各自版本的poll
后 向设备等待队列注册需要的回调函数) 这个回调函数的作用是 将我们的current
(也就是我们的进程的task_struct
指针)注册进我们的各自的fd
中的等待队列 当设备也就是各个fd
如果完成了读入数据 或者准备好了写入时 就会回去遍历之前我们在设备等待队列中正在等待的进程 发送信号 我们的do_select
在第一遍遍历后 只是把我们把current
结构体 向我们的各个fd
的等待队列中注册了而已 如果注册的时候 返回发现就已经有可用的时候 在第一次循环的时候 就可以直接退出了 如果没有的话 就休眠 直到被注册的等待队列唤醒
这里一下子跨度有点大 不着急 我们一点点来分析 因为设计的文件有点多 这里刚开始我也没有理解 去看了非常多的资料后才明白的 一点点来
先看看下面的结构体poll_wqueues
struct poll_wqueues {
poll_table pt; // 驱动注册 回调函数 __pollwait
struct poll_table_page * table; // 如果下面的inline_entries不够 就会需要
int error;
int inline_index; // 记录下面的目前使用过的下标
struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];
};
poll_table
就是我们的回调函数指针 定义如下
typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);
typedef struct poll_table_struct {
poll_queue_proc qproc;
} poll_table;
回调函数的内部实现待会调用的时候我们再贴出来 这个回调函数只会被各个不同设备内部实现的poll
调用 我们往下看
取max_select_fd
看函数名称就知道什么意思了 看看遍历最大文件描述符
往下面看poll_initwait(&table)
这个函数主要实现就是达成一个目的 把我们的__pollwait
(回调函数的内部实现) 函数指针赋值到我们的table
里面 最下面贴出来那个函数
剩下的都是一些无关紧要的 *** 作 我们往下面看 内容还非常多
int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
{
ktime_t expire, *to = NULL;
struct poll_wqueues table;
poll_table *wait;
int retval, i, timed_out = 0;
unsigned long slack = 0;
rcu_read_lock();
retval = max_select_fd(n, fds);
rcu_read_unlock();
if (retval < 0)
return retval;
n = retval;
poll_initwait(&table);
wait = &table.pt;
if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {
wait = NULL;
timed_out = 1;
}
if (end_time && !timed_out)
slack = estimate_accuracy(end_time);
.......
补充函数
static struct poll_table_entry *poll_get_entry(poll_table *_p)
{
struct poll_wqueues *p = container_of(_p, struct poll_wqueues, pt);
struct poll_table_page *table = p->table;
if (p->inline_index < N_INLINE_POLL_ENTRIES)
return p->inline_entries + p->inline_index++;
if (!table || POLL_TABLE_FULL(table)) {
struct poll_table_page *new_table;
new_table = (struct poll_table_page *) __get_free_page(GFP_KERNEL);
if (!new_table) {
p->error = -ENOMEM;
__set_current_state(TASK_RUNNING);
return NULL;
}
new_table->entry = new_table->entries;
new_table->next = table;
p->table = new_table;
table = new_table;
}
return table->entry++;
}
/* Add a new entry */
static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,
poll_table *p)
{
struct poll_table_entry *entry = poll_get_entry(p);
if (!entry)
return;
get_file(filp);
entry->filp = filp;
entry->wait_address = wait_address;
init_waitqueue_entry(&entry->wait, current);
add_wait_queue(wait_address, &entry->wait);
}
补充上面相关的结构体
struct poll_table_struct;
/*
* structures and helpers for f_op->poll implementations
*/
typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);
typedef struct poll_table_struct {
poll_queue_proc qproc;
} poll_table;
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
if (p && wait_address)
p->qproc(filp, wait_address, p);
}
static inline void init_poll_funcptr(poll_table *pt, poll_queue_proc qproc)
{
pt->qproc = qproc;
}
struct poll_table_entry {
struct file * filp;
wait_queue_t wait;
wait_queue_head_t * wait_address;
};
/*
* Structures and helpers for sys_poll/sys_poll
*/
struct poll_wqueues {
poll_table pt;
struct poll_table_page * table;
int error;
int inline_index;
struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];
};
2、仔细分析do_select 核心代码(中)
精彩的来了嗷 进入主要循环了
我们终于进入主要循环了
我们首先先把retval
置零 这个也就是我们的要返回的值 这个值返回的是我们目前活跃的文件描述符个数(一个文件描述符如果读、写、异常都活跃 则记录为3)
进入循环
先调用set_current_state(TASK_INTERRUPTIBLE);
这个是为了我们第一次循环后向每个需要的fd等待队列中注册后 进入休眠 这里的休眠函数应该就是前面的set_current_state
应该是这个函数 但对于第一次循环开始之前 怎么唤醒 我也不太清楚… 所以这里如果有知道的读者 可以在下面留一下言 感谢
然后进入外循环 外循环控制总遍历文件描述符个数 内循环遍历一个unsigned long
位数这么多文件描述符(32个一次)
对于一些位 (读、写、异常)并集都为0 直接略过 如果并集后 位出现了1 则需要去检查注册了
外层循环都是一些无关紧要的 *** 作了 看看内层
主要 *** 作是*f_op->poll
这个就是调用各个设备 因为每个fd
都对应着相应的设备 可能是tcp
中的文件描述符 可能是keyboard
的IO设备 这就是去调用各个驱动自己实现的了 但在第一次循环的时候 我们WAIT
值不为NULL
后面再循环的话 则就已经被赋值了NULL
了 因为注册只需要注册一次 可看下面的函数 这里的函数指针就是p
如果p
为NULL
则什么都不做
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
if (p && wait_address)
p->qproc(filp, wait_address, p);
}
那不妨我们就直接转去一些典型设备实现的Poll
去看看
那我们进入下一部分了
retval = 0;
for (;;) {
unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
set_current_state(TASK_INTERRUPTIBLE);
inp = fds->in; outp = fds->out; exp = fds->ex;
rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
unsigned long in, out, ex, all_bits, bit = 1, mask, j;
unsigned long res_in = 0, res_out = 0, res_ex = 0;
const struct file_operations *f_op = NULL;
struct file *file = NULL;
in = *inp++; out = *outp++; ex = *exp++;
all_bits = in | out | ex;
if (all_bits == 0) {
i += __NFDBITS;
continue;
}
for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {
int fput_needed;
if (i >= n)
break;
if (!(bit & all_bits))
continue;
file = fget_light(i, &fput_needed);
if (file) {
f_op = file->f_op;
mask = DEFAULT_POLLMASK;
if (f_op && f_op->poll)
mask = (*f_op->poll)(file, retval ? NULL : wait);
fput_light(file, fput_needed);
if ((mask & POLLIN_SET) && (in & bit)) {
res_in |= bit;
retval++;
}
if ((mask & POLLOUT_SET) && (out & bit)) {
res_out |= bit;
retval++;
}
if ((mask & POLLEX_SET) && (ex & bit)) {
res_ex |= bit;
retval++;
}
}
}
if (res_in)
*rinp = res_in;
if (res_out)
*routp = res_out;
if (res_ex)
*rexp = res_ex;
cond_resched();
}
wait = NULL;
if (retval || timed_out || signal_pending(current))
break;
if (table.error) {
retval = table.error;
break;
}
/*
* If this is the first loop and we have a timeout
* given, then we convert to ktime_t and set the to
* pointer to the expiry value.
*/
if (end_time && !to) {
expire = timespec_to_ktime(*end_time);
to = &expire;
}
if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS))
timed_out = 1;
}
__set_current_state(TASK_RUNNING);
poll_freewait(&table);
return retval;
}
3、仔细分析do_select 核心代码(下)
下面的代码是来自
net/ipv4/tcp
中的 我们选取了tcp_poll
来看看
可以看到第五行poll_wait
这里就是调用的我们的回调函数 我们回调函数的声明是在上面已经贴出来了 而且可以看看poll_wait
传入的第二个参数sk->sk_sleep
这里就是每个fd
在这里的就是socket
的设备等待队列
我们调用poll_wait
就是调用了回调函数
至于poll_wait
后面的 就是对socket
状态进行判断 然后返回mask
值而已
我们转到poll_wait
去看看
下面就是回调函数的代码 上面的poll_get_entry
已经在上面给出来了 关于等待队列 大家感兴趣的可以去看一下wait.c
wait.h
之前我实现过一个tiny_os
相对这部分看起来就好理解一点
核心的就是看倒数init_waitqueue_entry
add_wait_queue
注意我们的第二个参数wait_address
其实看tcp_poll
就可以发现是传入的sk->sk_sleep
也就是socket
的设备等待队列
static void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p)
{
struct poll_table_entry *entry = poll_get_entry§;
if (!entry)
return;
get_file(filp);
entry->filp = filp;
entry->wait_address = wait_address;
init_waitqueue_entry(&entry->wait, current);
add_wait_queue(wait_address, &entry->wait);
}
unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
{
unsigned int mask;
struct sock *sk = sock->sk;
struct tcp_sock *tp = tcp_sk(sk);
poll_wait(file, sk->sk_sleep, wait);
if (sk->sk_state == TCP_LISTEN)
return inet_csk_listen_poll(sk);
mask = 0;
if (sk->sk_err)
mask = POLLERR;
if (sk->sk_shutdown == SHUTDOWN_MASK || sk->sk_state == TCP_CLOSE)
mask |= POLLHUP;
if (sk->sk_shutdown & RCV_SHUTDOWN)
mask |= POLLIN | POLLRDNORM | POLLRDHUP;
/* Connected? */
if ((1 << sk->sk_state) & ~(TCPF_SYN_SENT | TCPF_SYN_RECV)) {
int target = sock_rcvlowat(sk, 0, INT_MAX);
if (tp->urg_seq == tp->copied_seq &&
!sock_flag(sk, SOCK_URGINLINE) &&
tp->urg_data)
target--;
if (tp->rcv_nxt - tp->copied_seq >= target)
mask |= POLLIN | POLLRDNORM;
if (!(sk->sk_shutdown & SEND_SHUTDOWN)) {
if (sk_stream_wspace(sk) >= sk_stream_min_wspace(sk)) {
mask |= POLLOUT | POLLWRNORM;
} else { /* send SIGIO later */
set_bit(SOCK_ASYNC_NOSPACE,
&sk->sk_socket->flags);
set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
/* Race breaker. If space is freed after
* wspace test but before the flags are set,
* IO signal will be lost.
*/
if (sk_stream_wspace(sk) >= sk_stream_min_wspace(sk))
mask |= POLLOUT | POLLWRNORM;
}
}
if (tp->urg_data & TCP_URG_VALID)
mask |= POLLPRI;
}
return mask;
}
3、最后浅浅总结一下
基本上 上面就已经把比较难懂 而且核心的地方已经写了
至于最后的什么 另外开了三个位图 将另外三个位图复制到用户态传入参数的内存区 这些就不介绍了
这里其实如果搞得很清楚的话 poll
分析起来也容易得多 因为poll
中这个部分也是用了相同的函数
简单总结一下
Select
传入了三个SET
队列 (读、写、异常) 本质其实也就是一个1024
位的unsigned long
数组而已
do_select
中 利用回调函数 我们将current
注册到每个fd
对应的等待队列中 再次进入休眠 等待信号把我们唤醒
一旦出现一个描述符是活跃的 在遍历完后 立马退出循环 将内核中另外开空间的三个位图复制到用户态 也就是我们传入参数的内存区 结束调用
也就是这个样子的 Select
也算是分析完了
后面还有Poll
Epoll
我估计今天应该是可以弄完的 毕竟这里还是花了很多功夫来分析的
这三篇会以系列来发布 到时候也会把三个博客以总结的形式发出来
看能不能把poll
这个写完去吃午饭了
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)