Linux Kernel SelectPollEpoll源码深度历险(一)---- 探索Select底层运行原理

Linux Kernel SelectPollEpoll源码深度历险(一)---- 探索Select底层运行原理,第1张

文章目录 前引Linux Kernel Select/Poll/Epoll源码深度历险(一)---- 探索Select底层运行原理1、先得到Linux Kernel Source Code2、开始历险 顺藤摸瓜探索Select1、初得用户态 Select调用代码表层2、 欲求无果用户态调用 直接转向内核调用Select入口3、 仔细查看core_sys_select select核心实现1、先从fd_set结构体入手2、看看fd_set 相关的宏定义3、转回core_sys_select 开始分析(上)4、转回core_sys_select 开始分析(下) 4、原来do_select才是真正实现功能的核心函数(内容非常多 稍复杂)1、仔细分析do_select 核心代码(上)2、仔细分析do_select 核心代码(中)3、仔细分析do_select 核心代码(下) 3、最后浅浅总结一下


前引

鉴于之前我自己看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 再换算为32Unsigned 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 如果pNULL 则什么都不做

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这个写完去吃午饭了

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/yw/926289.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-16
下一篇 2022-05-16

发表评论

登录后才能评论

评论列表(0条)

保存