网络异步IO操作-select模型使用

文章目的:

       在写在篇文章之前,select模型里面的为什么每次都给描述符集赋值呢?由于当时我学习的时候听信了网络上有些文章说明的什么描述符集FD_CLR把其他的也清空的话;以至于只是稍微的看了看,没有深究里面的道理,现在反过来再次看时发现不是那么回事;于是我抱着怀疑自己、怀疑一切的精神深入的研究下select的工作机制。

基础知识

1)  描述符集fd_set:

fd_set是select机制提供的一种数据结构,实际上就是一个long类型的数组。

定义头文件目录:

./include/linux/types.h./include/linux/posix_types.h

可能有人会问为啥是两个头文件呢?首先说明的是fd_set是__kernel_fd_set的typedef定义,这个包含在types.h中;具体的定义在posix_types.h里面。

结构体定义:

types.h定义:

typedef __kernel_fd_set		fd_set;

Posix_types.h定义:

#undef __NFDBITS
#define __NFDBITS	(8 * sizeof(unsigned long))

#undef __FD_SETSIZE
#define __FD_SETSIZE	1024

#undef __FDSET_LONGS
#define __FDSET_LONGS	(__FD_SETSIZE/__NFDBITS)

#undef __FDELT
#define	__FDELT(d)	((d) / __NFDBITS)

#undef __FDMASK
#define	__FDMASK(d)	(1UL << ((d) % __NFDBITS))

typedef struct {
	unsigned long fds_bits [__FDSET_LONGS];
} __kernel_fd_set;

定义分析:

这个linux版本是3.0的,内部定义了一个__NFDBITS宏,数组为8*sizeof(long)即是32;宏__FD_SETSIZE为 1024;数组大小的宏__FDSET_LONGS为1024/(8*4)即是32。

其实经过这一系列的转化后我们发现其实这个数组就是为了1024bit,为什么这么定义呢?因为select模型里面操作的就是这些描述符集的位,select通过置位描述符集的相应位表示此描述符有变化(可读、可写、异常)

 

上面讲述了fd_set的数据结构,现在看看对这个fd_set的操作。

分析:对于一个数据结构的操作主要包括哪些部分呢?根据我们以前学习的数据结构来看主要有创建、清空(特殊的删除操作)、添加、删除、读取、插入等操作;那么针对fd_set数据结构包括添加(FD_SET)、清空(FD_ZERO)、删除(FD_CLR)、检测(FD_ISSET)。与之前定义fd_set一样这个也是进行了一层封装,分别定义在两个文件中。这些操作都定义在相应的arch下面,但是我在查ARM的时发现它是定义在./include/linux/time.h   ./include/asm-generic/posix_types.h

time.h:

#define NFDBITS			__NFDBITS

#define FD_SETSIZE		__FD_SETSIZE
#define FD_SET(fd,fdsetp)	__FD_SET(fd,fdsetp)
#define FD_CLR(fd,fdsetp)	__FD_CLR(fd,fdsetp)
#define FD_ISSET(fd,fdsetp)	__FD_ISSET(fd,fdsetp)
#define FD_ZERO(fdsetp)		__FD_ZERO(fdsetp)

posix_types.h:

#ifdef __KERNEL__

#undef __FD_SET
static inline void __FD_SET(unsigned long __fd, __kernel_fd_set *__fdsetp)
{
	unsigned long __tmp = __fd / __NFDBITS;
	unsigned long __rem = __fd % __NFDBITS;
	__fdsetp->fds_bits[__tmp] |= (1UL<<__rem);
}

#undef __FD_CLR
static inline void __FD_CLR(unsigned long __fd, __kernel_fd_set *__fdsetp)
{
	unsigned long __tmp = __fd / __NFDBITS;
	unsigned long __rem = __fd % __NFDBITS;
	__fdsetp->fds_bits[__tmp] &= ~(1UL<<__rem);
}

#undef __FD_ISSET
static inline int __FD_ISSET(unsigned long __fd, const __kernel_fd_set *__p)
{
	unsigned long __tmp = __fd / __NFDBITS;
	unsigned long __rem = __fd % __NFDBITS;
	return (__p->fds_bits[__tmp] & (1UL<<__rem)) != 0;
}

/*
 * This will unroll the loop for the normal constant case (8 ints,
 * for a 256-bit fd_set)
 */
#undef __FD_ZERO
static inline void __FD_ZERO(__kernel_fd_set *__p)
{
	unsigned long *__tmp = __p->fds_bits;
	int __i;

	if (__builtin_constant_p(__FDSET_LONGS)) {
		switch (__FDSET_LONGS) {
		case 16:
			__tmp[ 0] = 0; __tmp[ 1] = 0;
			__tmp[ 2] = 0; __tmp[ 3] = 0;
			__tmp[ 4] = 0; __tmp[ 5] = 0;
			__tmp[ 6] = 0; __tmp[ 7] = 0;
			__tmp[ 8] = 0; __tmp[ 9] = 0;
			__tmp[10] = 0; __tmp[11] = 0;
			__tmp[12] = 0; __tmp[13] = 0;
			__tmp[14] = 0; __tmp[15] = 0;
			return;

		case 8:
			__tmp[ 0] = 0; __tmp[ 1] = 0;
			__tmp[ 2] = 0; __tmp[ 3] = 0;
			__tmp[ 4] = 0; __tmp[ 5] = 0;
			__tmp[ 6] = 0; __tmp[ 7] = 0;
			return;

		case 4:
			__tmp[ 0] = 0; __tmp[ 1] = 0;
			__tmp[ 2] = 0; __tmp[ 3] = 0;
			return;
		}
	}
	__i = __FDSET_LONGS;
	while (__i) {
		__i--;
		*__tmp = 0;
		__tmp++;
	}
}

从上面的代码我们分析:

__FD_SET:这个函数的目的是把fd对应的位置1。

__FD_CLR:这个函数的目的是把fd对应的位清零。

__FD_ISSET:这个函数的目的是检测FD_ISSET是否为1。

我们暂且不看__FD_ZERO,从以上三个函数我们是不是有疑问呢?我们在使用这些函数时先是添加(__FD_SET),然后是检测(__FD_ISSET);如果我们中间的select没有对添加的描述符做相应的处理,那么现象时什么呢?那就是我们的没添加一个描述符都会把认为这个描述符改变了,我类个去,大家还记得我上面写的这篇文章的目的吧?网上说这个描述符经过select后是被FD_CLR清空的描述符集的,现在看来这个理论是扯淡啊!首先我们从逻辑上看FD_CLR是在FD_ISSET后调用的;再从代码上FD_CLR只是清空其中的一位的一个操作,打死它,它也不会操作到整个描述符集!

       至此我的疑问已经没有了,我已经理解了肯定是select函数改变的描述符集,就需要看看select的具体源码了。下面先给fd_set基础知识扫个尾,简单介绍下对于fd_set的清零操作(__FD_ZERO):

从上面的代码看:

__FD_ZERO:就是把定义的描述符数组中所有为清零。

代码分析:

从上面的代码看,首先获取数组指针,然后数组大小,然后逐个清零。我咋一看这段代码时我第一印象是为啥不用memset呢?接着我就明白了因为他赋值的是long型的;但是为啥不用循环逐个赋值呢?没有理解原因,可能是因为内核代码这样写代码执行的快,又或者是这只是个人编写代码的习惯而已,我想多了,哈哈,我就是这么较真啊!

Select函数:

Select是系统调用,是内核实现的函数。

函数原型:

int select(int n,fd_set* readfds,fd_set*writefds,fd_set *exceptfds,struct timeval *timeout);

函数源码:包含在./fs/select.c

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

源码分析:

       从源码看,首先是把超时等待的数据结构拷贝到内核里面;然后就是我们看到的core_sys_select函数,把我们定义的描述符集传到这个函数时,我们疑问将在这个函数解开,下面看看代码:

/*
 * We can actually return ERESTARTSYS instead of EINTR, but I'd
 * like to be certain this leads to no problems. So I return
 * EINTR just for safety.
 *
 * Update: ERESTARTSYS breaks at least the xview clock binary, so
 * I'm trying ERESTARTNOHAND which restart only when you want to.
 */
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;
}

前面是把最大的描述符加1赋值到n,由此换算出来需要多少bit即size的大小;接着定义6个这样大小的空间 (这里面又做了部分处理,首先如果数据大小不大于我们预先定义的2048的六分之一就不需要分配空间,直接用bits指向我们定义的stack_fds,否者就申请分配) ;把用户空间的数据拷贝到内核态保存fds结构体中,清空fds结构体的res_xx;然后进行处理(内部就是内核具体实现把res_xx成员中的相应位置位)经过这个过程后res_xx就保存的是对应描述符的置位;然后把这个res_xx又赋给了用户空间。至此我的猜测全部验证通过。

欢迎各位大神批评指正!


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值