【Select】Linux Select源码

在网络编程中,经常用到selec系统调用来判断套接字上是否存在数据可读,或者能否向一个套接字写入数据。其原型为:

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

Note: select,poll和epoll监听的fd一定是需要实现poll函数,例如ext3,xfs这些文件系统都没有实现poll函数,所以这些文件系统中被打开的文件是不能使用select,poll和epoll函数。

想要彻底理解清楚select的原理,首先需要清楚一些数据结构

#define __FD_SETSIZE	1024
typedef __kernel_fd_set		fd_set;
//fd_set其实就是一个unsigned long数组,默认数组长度是1024/8*8=16,每一个bit位都代表一个fd是否被监听
typedef struct {
	unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;

Linux为了让用户方便的操作fd_set,提供了一系列的macro  

FD_CLR( s, *set)    //从set中删除句柄s;
FD_ISSET( s, *set)  //检查句柄s是否存在与set中;
FD_SET( s, *set )   //把句柄s添加到set中;
FD_ZERO( *set )     //把set队列初始为空.
#define __FD_SET(fd, fdsetp)   (((fd_set *)(fdsetp))->fds_bits[(fd) >> 6] |= (1<<((fd) & 63)))             //设置对应的bit
#define __FD_CLR(fd, fdsetp)   (((fd_set *)(fdsetp))->fds_bits[(fd) >> 6] &= ~(1<<((fd) & 63)))            //清除对应的bit
#define __FD_ISSET(fd, fdsetp)   ((((fd_set *)(fdsetp))->fds_bits[(fd) >> 6] & (1<<((fd) & 63))) != 0)     //判断对应的bit是否为1
#define __FD_ZERO(fdsetp)   (memset (fdsetp, 0, sizeof (*(fd_set *)(fdsetp))))                             //重置bitmap

我们先举个比较简单的demo,根据这个demo来分析select这个系统调用

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
	fd_set rd;
	struct timeval tv;
	int err;
	

	FD_ZERO(&rd);
    //设置标准输入的fd到fd_set
	FD_SET(0,&rd);
	
	tv.tv_sec = 5;
	tv.tv_usec = 0;
	err = select(1,&rd,NULL,NULL,&tv);
	
	if(err == 0) //超时
	{
		printf("select time out!\n");
	}
	else if(err == -1)  //失败
	{
		printf("fail to select!\n");
	}
	else  //成功
	{
		printf("data is available!\n");
	}

	
	return 0;
}

select系统调用的源码在select.c中

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

   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 */
    /*预先分配固定长度的数组,这个数组最后用来存in_set,out_set,ex_set,res_in_set,res_out_set,res_ex_set。
但是如果预分配的内存不足以存放,会重新使用kmalloc来分配*/
	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;
    /*如果6个位图的大小没有超过预分配的,就使用预分配的数组,否则重新分配内存空间*/
	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;
    /*将用户态传递过来的三个fd_set赋值给fds.in,fds.out和fds.ex*/
	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;
    /*将三个response fd_set置为0,等待后面操作进行相关置位*/
	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;
	}

  /*将response fd_set返回给用户态,从这里可以知道,在select()返回后,
只有对应fd有读事件或写事件发生的时候,相关的bit位才会被置1,
其他bit位都是0,所以每次select返回后,都要在用户态程序中重新设置fd_set,即监听哪些fd*/
	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;
}

do_select

  

int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
{
	ktime_t expire, *to = NULL;
    /*poll_wqueues table这个变量在下面的三个for循环,
每次调用poll的时候,都会从table->inline_entries中获取一个entry,
来记录当前等待队列的对头和wait,并将wait挂在等待队列上*/
	struct poll_wqueues table;
	poll_table *wait;
	int retval, i, timed_out = 0;
	unsigned long slack = 0;

	rcu_read_lock();
    /*返回设置的fd中最大值+1,同时会检查设置的fd对应的文件是否有打开,
因为在用户态设置的fd并不一定是打开的,如果检查到有设置的fd对应的文件
没有打开,那么select就会直接返回错误,同时这里返回max(fd)+1,
有人会说n就是最大的fd+1么,但是用户态的用户不一定传下来的就是max(fd)+1*/
	retval = max_select_fd(n, fds);
	rcu_read_unlock();

	if (retval < 0)
		return retval;
	n = retval;
    /*这里就是设置poll_wait调用的回调函数,其实还是有必要看下
poll_wqueues结构体的*/
	poll_initwait(&table);
    /*wait会传递poll(),在poll函数中,调用poll_wait(),
poll_wait()主要就是执行上面设置的回调函数*/
	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;

		inp = fds->in; outp = fds->out; exp = fds->ex;
		rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
        /*这层循环,每次拿出来一个unsigned long,前面介绍过,最多也就
16个unsigned long*/
		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;

			in = *inp++; out = *outp++; ex = *exp++;
            /*如果这次循环取出来unsigned long,都没有在in,out,ex中
设置bit位,那么直接跳过,取下一个unsigned long*/ 
			all_bits = in | out | ex;
			if (all_bits == 0) {
				i += BITS_PER_LONG;
				continue;
			}
            /*这层循环是把每一个unsigned long的每一位取出来,然后查看是否有监听,如果有监听,就调用对应文件的poll()方法*/
			for (j = 0; j < BITS_PER_LONG; ++j, ++i, bit <<= 1) {
				struct fd f;
				if (i >= n)
					break;
                /*检查当前的这一bit是否有文件监听,如果没有则跳过*/
				if (!(bit & all_bits))
					continue;
				f = fdget(i);
				if (f.file) {
					const struct file_operations *f_op;
					f_op = f.file->f_op;
					mask = DEFAULT_POLLMASK;
					if (f_op && f_op->poll) {
                        /*由于in,out,ex只要有一个fd_set对应的bit位置为1,就会执行poll(),但是我们不知道这个fd到底需要监听什么类型的事件,
这里就是利用wait_key_set来设置监听什么类型的事件*/
						wait_key_set(wait, in, out, bit);
                        /*执行对应文件的poll()*/
						mask = (*f_op->poll)(f.file, wait);
					}
					fdput(f);
                    /*如果执行的poll函数返回有对应的事件的mask,就会将其在res bitmap中置位,同时将retval计数器累加*/
					if ((mask & POLLIN_SET) && (in & bit)) {
						res_in |= bit;
						retval++;
                        /*当检查到一个fd对应有事件发生,就把回调函数kill掉,因为没有必要再
                         设置了,在循环完,就会直接退出,select并不会睡眠了,也减少了后边
                          free的开销*/
						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;
					}
				}
			}
			if (res_in)
				*rinp = res_in;
			if (res_out)
				*routp = res_out;
			if (res_ex)
				*rexp = res_ex;
            /*每次执行完一个unsigned long的轮询,就调用这个函数,让更高优先级的进程得到运行的机会*/
			cond_resched();
		}
    /*这里把回调函数置为null,因为如果不置null,在下次执行poll的时候,
依然会执行_poll_wait,把waiter挂在到等待队列上,那样就会出现同样
一个fd,挂载2次waiter到同一个等待队列上*/
		wait->_qproc = NULL;
        /*如果retval不为0说明已经产生了对应的事件,或者超时了,或者有信号需要处理,则会退出当前的循环*/
		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 (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,
					   to, slack))
			timed_out = 1;
	}

    /*这个函数也很重要,在最后返回的时候,需要将挂载在等待队列里waiter给移除掉*/
	poll_freewait(&table);

	return retval;
}

接下来,我会拿一个比较简单的字符设备poll函数来举例,说明poll函数调用的过程。

/*mem设备描述结构体*/
struct mem_dev 
{ 
  char *data; 
  unsigned long size; 
  wait_queue_head_t inq; 
};

static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
  unsigned long p = *ppos;
  unsigned int count = size;
  int ret = 0;
  struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
  
  /*分析和获取有效的写长度*/
  if (p >= MEMDEV_SIZE)
    return 0;
  if (count > MEMDEV_SIZE - p)
    count = MEMDEV_SIZE - p;

  /*从用户空间写入数据*/
  if (copy_from_user(dev->data + p, buf, count))
    ret = - EFAULT;
  else
  {
    *ppos += count;
    ret = count;
    
    printk(KERN_INFO "written %d bytes(s) from %d\n", count, p);
  }
  
  have_data = true; /* 有新的数据可读 */
    
    /* 唤醒读进程 */
    wake_up(&(dev->inq));

  return ret;
}


unsigned int mem_poll(struct file *filp, poll_table *wait)
{
    struct mem_dev *dev = filp->private_data; 
    unsigned int mask = 0;
    
    /*执行对应的wait->_qproc*/
    poll_wait(filp, &dev->inq, wait);

    /*判断当前是否有数据可读,如果有,返回对应的mask*/
    if (have_data) mask |= POLLIN | POLLRDNORM; /* readable */

    return mask;
}

poll_wait

static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
    /*判断回调函数是否存在以及等待队列是否存在,如果存在执行回调函数,对应select的回调函数是
     __pollwait*/
	if (p && p->_qproc && wait_address)
		p->_qproc(filp, wait_address, p);
}

__pollwait

struct poll_table_entry {
	struct file *filp;
	unsigned long key;
	wait_queue_t wait;
	wait_queue_head_t *wait_address;
};

/*
 * 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];
};


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);
    /*获取一个poll_table_entry,如果inline_entries已经使用完,则会另外分配空间*/
	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;
    /*将wait挂在等待队列上*/
	add_wait_queue(wait_address, &entry->wait);
}

所以在使用select的时候,如果没有指定timeout的话,在执行do_select函数的时候,会主动poll所有监听的fd,如果在第一次poll的时候,没有任何事件发生,那么用户态进程会睡眠,在有新的事件到来的时候,一般在write函数中使用wake_up()来唤醒等待队列上所有的waiter,即执行waiter的唤醒函数(pollwake),睡眠的进程会被唤醒,然后接着执行for循环,接着主动调用poll函数,来查看是否有对应的事件产生。所以可以看出来,select函数是一直在轮询,即使对应fd上产生了事件,将进程环境后,依然要再主动调用poll函数,来查看产生了什么事件。

#define wake_up(x)			__wake_up(x, TASK_NORMAL, 1, NULL)

void __wake_up(wait_queue_head_t *q, unsigned int mode,
			int nr_exclusive, void *key)
{
	unsigned long flags;

	spin_lock_irqsave(&q->lock, flags);
	__wake_up_common(q, mode, nr_exclusive, 0, key);
	spin_unlock_irqrestore(&q->lock, flags);
}


static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
			int nr_exclusive, int wake_flags, void *key)
{
	wait_queue_t *curr, *next;

	list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
		unsigned flags = curr->flags;

        /*在__pollwait中设置了func为pollwake*/
		if (curr->func(curr, mode, wake_flags, key) &&
				(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
			break;
	}
}


static int pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
	struct poll_table_entry *entry;

	entry = container_of(wait, struct poll_table_entry, wait);
	if (key && !((unsigned long)key & entry->key))
		return 0;
    /*唤醒用户态进程*/
	return __pollwake(wait, mode, sync, key);
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值