select原理

    打算看一看linux底层源码,之所去看这个还是在看libso源码的的时候,libso在用户态实现一个协程的epoll,觉得这个实现方法可能是借鉴内核的方法,打算看看内核的实现方式,然后对看看libso的实现。

参开文献

Linux系统调用SYSCALL_DEFINE详解

一、寻找源码

    说真的,一开始想从GNU的lib库中一路寻找到系统调用,再然后看内核源码,说实话太难找了,最后决定直接看linux的系统调用,至于GNU在用户太有作了什么,后面在说。不过找这个linux内核源码也是怪费尽的,一开始直接想在自己的ubuntu系统里找的,奈何找起来确实太费尽了,最后查了自己的linux版本,然后下载了内核源码。

    查看自己的linux版本

cat /proc/version

就可以得到自己的linux版本

Linux version 5.15.0-102-generic

这是我的linux版本。

    然后去中科大的镜像源寻找自己对应的linux版本(也可以取linux官网,或者国内其他镜像源)中科大镜像源。下载成功解压就行了。

二、select源码

2.1 SYSCALL_DEFINE5

    select.c文件在linux-5.15.102/fs下,这里我下在的是5.15.0-102版本,所以解压完是那个名字,总之源码在/fs文件夹下。

SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp,
		fd_set __user *, exp, struct __kernel_old_timeval __user *, tvp)
{
	return kern_select(n, inp, outp, exp, tvp);
}

这个是select系统调用的入口,如果对SYSCALL_DEFINE5迷惑,可以先看看Linux系统调用SYSCALL_DEFINE详解

2.2 kern_select

static int kern_select(int n, fd_set __user *inp, fd_set __user *outp,
		       fd_set __user *exp, struct __kernel_old_timeval __user *tvp)
{
	struct timespec64 end_time, *to = NULL;
	struct __kernel_old_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);
	return poll_select_finish(&end_time, tvp, PT_TIMEVAL, ret);
}

这里有3个数据结构,分别看看

struct timespec64 {
	time64_t	tv_sec;			/* seconds */
	long		tv_nsec;		/* nanoseconds */
};

第二个参数中__user是一个宏,具体可以bing一下,具体不是很能理解这个机理。

// linux/types.h
typedef __kernel_fd_set		fd_set;


// uapi/linux/posix_types.h
#define __FD_SETSIZE	1024
typedef struct {
	unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;

     这里构建一个unsigned long的数组,数组大小是16(在64位计算机下),而这个是位图,也就是能表示1024个二进制位,所以说select最大只能放1024个文件描述符。但是在man select下有这么一句话:The  Linux kernel allows file descriptor sets of arbitrary size, determining the length of the sets to be checked from the value of nfds.  However, in the glibc implementation, the fd_set type is fixed in size.难道glic和linux内核的select不一样???

// uapi/asm-generic/posix_types.h
typedef long		__kernel_long_t;

// uapi/linux/time_types.h
struct __kernel_old_timeval {
	__kernel_long_t tv_sec;
	__kernel_long_t tv_usec;
};

woc真的难找啊!!!!构建了三个时间结构,然后调用core_sys_select函数。

2.3 core_sys_select

int core_sys_select(int n, fd_set __user *inp, fd_set __user *outp,
			   fd_set __user *exp, struct timespec64 *end_time)
{
	fd_set_bits fds;
	void *bits;
	int ret, max_fds;
	size_t size, alloc_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;
		if (size > (SIZE_MAX / 6))
			goto out_nofds;

		alloc_size = 6 * size;
		bits = kvmalloc(alloc_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)
		kvfree(bits);
out_nofds:
	return ret;
}

首先,

// linux-5.15.102/fs/select.c
typedef struct {
	unsigned long *in, *out, *ex;
	unsigned long *res_in, *res_out, *res_ex;
} fd_set_bits;

嘿嘿,找到一个快速定位符号的好办法,就是在github上找一个linux内核,然后在github界面进入一个.c文件,然后搜索符号,就可以在linux这个文件夹下搜索符号。

大概就这样

struct fdtable {
	unsigned int max_fds;
	struct file __rcu **fd;      /* current fd array */
	unsigned long *close_on_exec;
	unsigned long *open_fds;
	unsigned long *full_fds_bits;
	struct rcu_head rcu;
};
// linux/poll.h
#define FRONTEND_STACK_ALLOC	256
#define SELECT_STACK_ALLOC	FRONTEND_STACK_ALLOC
// select.c
#define FDS_BITPERLONG	(8*sizeof(long))
#define FDS_LONGS(nr)	(((nr)+FDS_BITPERLONG-1)/FDS_BITPERLONG)
#define FDS_BYTES(nr)	(FDS_LONGS(nr)*sizeof(long))
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;
}

 这个函数的作用是如果用户set不为空,就将用户的数据拷贝到内核区,否则就将内核数据区设置为0。

static inline
void zero_fd_set(unsigned long nr, unsigned long *fdset)
{
	memset(fdset, 0, FDS_BYTES(nr));
}

这个函数作用是将给定的地址内存清零。

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;
}

这个是将内核数据拷贝到用户数据。

2.3.1 函数流程

1. 创造内核需要的数据。

  •  fd_set_bits,包括需要监视的读文件、写文件、异常文件和输出的可读文件、可写文件和产生异常的文件。
  • bits指针,bitmap用于描述上面六个文件集合。
  • size:用于描述目前进程所打开的最大文件。alloc_size:需要重新分配的大小。
  • stack_fds[SELECT_STACK_ALLOC/sizeof(long)] = stack_fds[256/sizeof(long)],就是在栈上分配了256字节内存(在linux源文件中有这么一句话:Allocate small arguments on the stack to save memory and be faster)。

2. 获取当前进程的最大文件描述符

rcu_read_lock();
fdt = files_fdtable(current->files);
max_fds = fdt->max_fds;
rcu_read_unlock();
if (n > max_fds)
	n = max_fds;

加lcu读锁,current是linux内核定义的一个宏,用于获取当前进程。

#define get_current() (current_thread_info()->task) //获取当前进程的线程task
#define current get_current() //表示当前进程

n是输入值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1(目前来看这个是即使不严格+1似乎也没什么事,只要不是<0就行)。

3. 构建bitmap

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;
	if (size > (SIZE_MAX / 6))
		goto out_nofds;

	alloc_size = 6 * size;
	bits = kvmalloc(alloc_size, GFP_KERNEL);
	if (!bits)
		goto out_nofds;
}

根据输入的n确定size,FDS_BYTES函数是对n/sizeof(long)向上取整。n是文件描述的最大值,这了就是对当前进程文件构建bitmap,如果size大于在栈上开辟的内存,则需要另开辟内存(kvmalloc),如果大于最限制,就报错。

4. 初始化fds指针

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;

 5. 初始化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);

6. select循环

ret = do_select(n, &fds, end_time);

 7. 拷贝输出结果

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;

2.4 do_select()

static int do_select(int n, fd_set_bits *fds, struct timespec64 *end_time)
{
	ktime_t expire, *to = NULL;
	struct poll_wqueues table;
	poll_table *wait;
	int retval, i, timed_out = 0;
	u64 slack = 0;
	__poll_t busy_flag = net_busy_loop_on() ? POLL_BUSY_LOOP : 0;
	unsigned long busy_start = 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->_qproc = NULL;
		timed_out = 1;
	}

	if (end_time && !timed_out)
		slack = select_estimate_accuracy(end_time);

	retval = 0;
	for (;;) {
		unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
		bool can_busy_loop = false;

		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, j;
			unsigned long res_in = 0, res_out = 0, res_ex = 0;
			__poll_t mask;

			in = *inp++; out = *outp++; ex = *exp++;
			all_bits = in | out | ex;
			if (all_bits == 0) {
				i += BITS_PER_LONG;
				continue;
			}

			for (j = 0; j < BITS_PER_LONG; ++j, ++i, bit <<= 1) {
				struct fd f;
				if (i >= n)
					break;
				if (!(bit & all_bits))
					continue;
				mask = EPOLLNVAL;
				f = fdget(i);
				if (f.file) {
					wait_key_set(wait, in, out, bit,
						     busy_flag);
					mask = vfs_poll(f.file, wait);

					fdput(f);
				}
				if ((mask & POLLIN_SET) && (in & bit)) {
					res_in |= bit;
					retval++;
					wait->_qproc = NULL;
				}
				if ((mask & POLLOUT_SET) && (out & bit)) {
					res_out |= bit;
					retval++;
					wait->_qproc = NULL;
				}
				if ((mask & POLLEX_SET) && (ex & bit)) {
					res_ex |= bit;
					retval++;
					wait->_qproc = NULL;
				}
				/* got something, stop busy polling */
				if (retval) {
					can_busy_loop = false;
					busy_flag = 0;

				/*
				 * only remember a returned
				 * POLL_BUSY_LOOP if we asked for it
				 */
				} else if (busy_flag & mask)
					can_busy_loop = true;

			}
			if (res_in)
				*rinp = res_in;
			if (res_out)
				*routp = res_out;
			if (res_ex)
				*rexp = res_ex;
			cond_resched();
		}
		wait->_qproc = NULL;
		if (retval || timed_out || signal_pending(current))
			break;
		if (table.error) {
			retval = table.error;
			break;
		}

		/* only if found POLL_BUSY_LOOP sockets && not out of time */
		if (can_busy_loop && !need_resched()) {
			if (!busy_start) {
				busy_start = busy_loop_current_time();
				continue;
			}
			if (!busy_loop_timeout(busy_start))
				continue;
		}
		busy_flag = 0;

		/*
		 * 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 = timespec64_to_ktime(*end_time);
			to = &expire;
		}

		if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,
					   to, slack))
			timed_out = 1;
	}

	poll_freewait(&table);

	return retval;
}
typedef s64			int64_t;
typedef s64	ktime_t;
// linux/poll.h
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];
};


// include/linux/pool.h
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_t _key;
} poll_table;



// fs/select.c
struct poll_table_page {
	struct poll_table_page * next;
	struct poll_table_entry * entry;
	struct poll_table_entry entries[];
};


// include/linux/sched.h task_struct


// include/linux/pool.h
typedef struct wait_queue_entry wait_queue_entry_t;
typedef int (*wait_queue_func_t)(struct wait_queue_entry *wq_entry, unsigned mode, int flags, void *key);

struct wait_queue_entry {
	unsigned int		flags;
	void			*private;
	wait_queue_func_t	func;
	struct list_head	entry;
};


struct poll_table_entry {
	struct file *filp;
	__poll_t key;
	wait_queue_entry_t wait;
	wait_queue_head_t *wait_address;
};
typedef u64			uint64_t;

2.4.1 函数流程

1. 参数初始化

ktime_t expire, *to = NULL;
struct poll_wqueues table;
poll_table *wait;
int retval, i, timed_out = 0;
u64 slack = 0;
__poll_t busy_flag = net_busy_loop_on() ? POLL_BUSY_LOOP : 0;
unsigned long busy_start = 0;

2. 判断合法性

rcu_read_lock();
retval = max_select_fd(n, fds);
rcu_read_unlock();

if (retval < 0)
	return retval;
n = retval;

3. 初始化struct poll_wqueues(poll 等待队列)

poll_initwait(&table);
wait = &table.pt;


这里可以先不管等待队列,实际上和这个似乎也没有什么关系。

4. 时间参数

if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {
	wait->_qproc = NULL;
	timed_out = 1;
}

if (end_time && !timed_out)
	slack = select_estimate_accuracy(end_time);

在select中有struct timeval*timeout这一项,当这一项为NULL时,select处于阻塞状态,若不为NULL,但时间为0,则非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;若大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回,返回参数同第二种。

    在内部实现是,首先,如果时间存在,且时间为0,将_qproc置NULL(就是刚才初始化的__poll_wait函数指针变成NULL),并将time_out标志置1。

    其次,判断时间如果为不为NULL且time_out=0,则计算距离timeout的微秒数

5. select循环

for (;;) {
	unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
	bool can_busy_loop = false;

	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){...}
}

    首先是最外层和循环,显然,这是个死循环,一直在等待事件的发生。在进入判断事件是否发生之前,首先进行参数的初始化。

for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
	unsigned long in, out, ex, all_bits, bit = 1, j;
	unsigned long res_in = 0, res_out = 0, res_ex = 0;
	__poll_t mask;

	in = *inp++; out = *outp++; ex = *exp++;
	all_bits = in | out | ex;
	if (all_bits == 0) {
		i += BITS_PER_LONG;
		continue;
	}

	for (j = 0; j < BITS_PER_LONG; ++j, ++i, bit <<= 1) {...}

    n是前面描述当前进程开启的最大文件描述符。首先判断当前集合中有需要判断的文件吗,如果没有,则直接跳到下64 bit。

    在然后是最内层循环

for (j = 0; j < BITS_PER_LONG; ++j, ++i, bit <<= 1) {
	struct fd f;
	if (i >= n)
		break;
	if (!(bit & all_bits))
		continue;
	mask = EPOLLNVAL;
	f = fdget(i);
	if (f.file) {
		wait_key_set(wait, in, out, bit,
		    busy_flag);
		mask = vfs_poll(f.file, wait);

		fdput(f);
	}
	if ((mask & POLLIN_SET) && (in & bit)) {
		res_in |= bit;
		retval++;
		wait->_qproc = NULL;
	}
	if ((mask & POLLOUT_SET) && (out & bit)) {
		res_out |= bit;
		retval++;
		wait->_qproc = NULL;
	}
	if ((mask & POLLEX_SET) && (ex & bit)) {
		res_ex |= bit;
		retval++;
		wait->_qproc = NULL;
	}
	/* got something, stop busy polling */
	if (retval) {
		can_busy_loop = false;
		busy_flag = 0;

	/*
	* only remember a returned
	* POLL_BUSY_LOOP if we asked for it
	*/
	} else if (busy_flag & mask)
		can_busy_loop = true;

	}

    这里的代码细节就看不懂了,大致意思是获取i的文件,然后调用文件系统的poll方法(这里看起来select内核并没有做什么,而是调用文件系统提供的poll方法,如果文件系统没有提供,就返回default)

static inline __poll_t vfs_poll(struct file *file, struct poll_table_struct *pt)
{
	// 如果我们没有在驱动层注册相应的poll函数,则返回DEFAULT_POLLMASK(内核认为这个可能性不大,毕竟既然应用层有poll的需求,驱动层就应该提供相应的poll函数)
	if (unlikely(!file->f_op->poll))
		return DEFAULT_POLLMASK;
	
	// 调用驱动层的poll函数(一般被命名为xxx_poll),返回值就是xxx_poll的返回值
	return file->f_op->poll(file, pt);
}

fdput是解除对文件描述的引用(有点像智能指针)。在后面三个if就是判断有没有文件就绪。

    然后内层循环结束后

for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
    ...

	for (j = 0; j < BITS_PER_LONG; ++j, ++i, bit <<= 1) {...}

	if (res_in)
		*rinp = res_in;
	if (res_out)
		*routp = res_out;
	if (res_ex)
		*rexp = res_ex;
	cond_resched();
}
    

三个if写入结果。然后让出cpu(不知道为什么,反正大概是让出cpu)。

for (;;) {
    ...

    for (j = 0; j < BITS_PER_LONG; ++j, ++i, bit <<= 1) {...}

	wait->_qproc = NULL;
	if (retval || timed_out || signal_pending(current))
		break;
	if (table.error) {
		retval = table.error;
		break;
	}

	/* only if found POLL_BUSY_LOOP sockets && not out of time */
	if (can_busy_loop && !need_resched()) {
		if (!busy_start) {
			busy_start = busy_loop_current_time();
			continue;
		}
		if (!busy_loop_timeout(busy_start))
			continue;
	}
	busy_flag = 0;

	/*
	 * 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 = timespec64_to_ktime(*end_time);
		to = &expire;
	}

	if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,
				   to, slack))
		timed_out = 1;
}

检查retval,timeout等参数,如果有事件发生,或者超时,就结束循环,返回。(还记前面时间为0,其实就是检查一遍,有事件就返回事件数,没有就返回0)。后面的if其实就不太明白是在做什么。最后的if则是将自己阻塞,和poll中的是一样的。

    最后释放poll_freewait。返回retval。

最后select约束真的限制在1024个文件吗?见select 1024限制

2.5 释放空间

    最后,通过poll_freewait和kvfree释放空间。

三、总结

    select和poll几乎差不多,select底层也是通过文件系统的poll方法构建等待实体。

  1. 首先调用core_sys_select,select会有三个文件集合,包括读集合、写集合和异常集合,select会先构建一个6集合的bitmap结构体,包括输入的三个集合和需要输出的三个集合。这个bitmap并没有限制在1024大小,struct结构体只是6个long long指针指向bitmap,而bitmap大小则是根据输入的文件数量开辟的bitmap。
  2. 然后调用do_select,遍历所有的文件描述符,判断其是否在监听的三个集合中,如果在就通过文件描述符获得文件,通过虚拟文件系统的poll方法构造等待实体。
  3. 最后,和poll一样,将自己挂起等待事件发生。

    最后,还有一点奇怪的东西,就是关于poll和select性能的问题,poll相比于select突破了1024的限制,但是select本来就可以不受1024限制,而从源码上看,poll构造链表,频繁的构造poll_wait_entrty,开辟内存页也很麻烦,而相比于select,通过bitmap只需要很小的内存。但是select需要遍历所有的文件描述符,虽然通过一些方法避免了遍历,但是循环肯定是要if判断的,所以poll真的回比select好吗??

  • 12
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值