select多路複用 源碼剖析

簡單說一下5種IO

  • 阻塞IO: 一直等待知道數據到來。
  • 非阻塞IO: 直接返回有沒有數據,沒有就直接返回錯誤。
  • IO複用:將多個IO,放在一起,一個個輪詢。
  • 信號驅動:設置一個信號,當有IO的信號的時候告訴我。
  • 異步IO:直接丟給別人做。

可以去看看這個博客
select是IO複用的一種。
函數原型如下。

int select (int __nfds, fd_set *__restrict __readfds,
		   fd_set *__restrict __writefds,
		   fd_set *__restrict __exceptfds,
		   struct timeval *__restrict __timeout);

參數也很容易懂.

  • nfds: 最大的文件描述符+1
  • readfds:讀的描述符集。
  • writefds:寫的描述符集。
  • exceptfds:異常描述符集。
  • timeout:超時時間。

直接看看結構體是啥。

/* fd_set for select and pselect.  */
typedef long int __fd_mask;//我的64位環境
//簡單來講 就是個位圖。
typedef struct
  {
    /* XPG4.2 requires this member name.  Otherwise avoid the name
       from the global namespace.  */
#ifdef __USE_XOPEN
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
    __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
  } fd_set;

測試下多少位。

int main(int argc, char* argv[])
{
    printf("%d\n",sizeof(fd_set)*8);
    return 0;
}
//輸出 1024  這個地方告訴我們最大能設置爲1024,聽說有方法可以改,這個我們不去關心了。

看看時間結構體,也很簡單

struct timeval
  {
    __time_t tv_sec;		/* Seconds.  秒*/ 
    __suseconds_t tv_usec;	/* Microseconds. 毫秒 */
  };

幾個常用的操作

/* Access macros for `fd_set'.  */
#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)//清0
//宏定義實現也簡單
#ifndef FD_SET
#define FD_SET(n, p)    (__XFDS_BITS(p, ((n)/NFDBITS)) |= ((fd_mask)1 << ((n) % NFDBITS)))// 或上去,置1
#endif
#ifndef FD_CLR
#define FD_CLR(n, p)    (__XFDS_BITS((p), ((n)/NFDBITS)) &= ~((fd_mask)1 << ((n) % NFDBITS))) //與上取反的  清0
#endif
#ifndef FD_ISSET
#define FD_ISSET(n, p)  ((__XFDS_BITS((p), ((n)/NFDBITS))) & ((fd_mask)1 << ((n) % NFDBITS)))//與上不取反的獲取標誌位
#endif
#ifndef FD_ZERO
#define FD_ZERO(p)      bzero((char *)(p), sizeof(*(p)))//這個有點不合格啊竟然調用函數,清0函數。    我還以爲是自己異或自己。
#endif

看我大致是啥我們就去看內核代碼。系統調用select
我在linux2.6裏面找到的是SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp, fd_set __user *, exp, struct timeval __user *, tvp)這個東西,在fs/select.c裏面,有的是sys_select這個函數,不知道是內核版本問題還是啥。反正實現都是一樣的將就着看。

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))//轉換成timespec 這個
			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()

後面如果有些函數不知道在哪,可以用grep搜索一下,或者裝個vscode直接全局搜索。
大部分都在include/linux/poll.h不同內核版本可能路徑不一樣。

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;
	/*
	typedef struct {
	unsigned long *in, *out, *ex;
	unsigned long *res_in, *res_out, *res_ex;
	} fd_set_bits;//用來指向描述符的指針
	*/
	void *bits;
	int ret, max_fds;
	unsigned int size;
	struct fdtable *fdt;
	/*
	struct fdtable {
	unsigned int max_fds;
	struct file __rcu **fd;       //current fd array 
	fd_set *close_on_exec;
	fd_set *open_fds;
	struct rcu_head rcu;
	struct fdtable *next;
	};	
	
	*/
	/* 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();//rcu 鎖
	fdt = files_fdtable(current->files);//這個函數看裏面的內容是讀取文件描述符
	//current 是當前進程,沒猜錯的話應該是把所有打開的文件描述符讀出來
	max_fds = fdt->max_fds;//設置下最大fd
	rcu_read_unlock();//刪除鎖
	if (n > max_fds)//n 和最大打開的文件取個最小值
		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. //我們需要 6個 位圖來處理
	 */
	size = FDS_BYTES(n);//這個應該是判斷需要多少個位
	/* 這個函數在 include/linux/poll.h 裏面
//How many longwords for "nr" bits?
#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))
	*/
	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;
	/*  這回函數就是複製了一遍 ,所以說 select 每次都會拷貝一份,所以非常慢。
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;
}
	*/
	zero_fd_set(n, fds.res_in);
	zero_fd_set(n, fds.res_out);
	zero_fd_set(n, fds.res_ex);
/*
static inline
void zero_fd_set(unsigned long nr, unsigned long *fdset)
{
	memset(fdset, 0, FDS_BYTES(nr));
}
*/
	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;
/*複製回去
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;
}
*/
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;
	struct poll_wqueues table;
	/*
//Structures and helpers for select/poll syscall  
幫助調用select和poll,select和poll原理都是一樣的
struct poll_wqueues {
	poll_table pt;
	struct poll_table_page *table;
	struct task_struct *polling_task;//當前用戶環境 PCB
	int triggered;// 當前用戶進程被喚醒後置成1,以免該進程接着進睡眠
	int error;// 錯誤碼
	int inline_index;// 數組inline_entries的引用下標
	struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];
};
*/
	poll_table *wait;
	/*
 structures and helpers for f_op->poll implementations //幫助調用 poll
typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);

typedef struct poll_table_struct {
	poll_queue_proc qproc;
	unsigned long key;
} poll_table;
	*/
	int retval, i, timed_out = 0;
	unsigned long slack = 0;

	rcu_read_lock();
	//根據已經設置好的fd位圖檢查用戶打開的fd, 要求對應fd必須打開, 並且返回最大的fd。
	retval = max_select_fd(n, fds);
	rcu_read_unlock();

	if (retval < 0)
		return retval;
	n = retval;
    /* 一些重要的初始化:
       poll_wqueues.poll_table.qproc函數指針初始化,
       該函數是驅動程序中poll函數(fop->poll)實現中必須要調用的poll_wait()中使用的函數。  */
	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 = 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;

		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;//如果所有位 0 直接跳過
			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)) //這一位爲0 就直接跳過
					continue;
				file = fget_light(i, &fput_needed);//當做打開文件
				if (file) {
					f_op = file->f_op;
					mask = DEFAULT_POLLMASK;
					if (f_op && f_op->poll) {//如果存在poll 函數
						wait_key_set(wait, in, out, bit);
						/*
static inline void wait_key_set(poll_table *wait, unsigned long in,unsigned long out, unsigned long bit)
{
	if (wait) {				//設置 poll_table 的標誌位
		wait->key = POLLEX_SET;
		if (in & bit)
			wait->key |= POLLIN_SET;
		if (out & bit)
			wait->key |= POLLOUT_SET;
	}
}

						*/
						mask = (*f_op->poll)(file, wait);//調用poll
					}
					fput_light(file, fput_needed);//關閉文件
					if ((mask & POLLIN_SET) && (in & bit)) {//更具poll獲取到的mask 更新返回值
						res_in |= bit;
						retval++;
						wait = NULL;//置空,意味着如果 一旦又一次mask 成功,就再也不會掛再了
					}
					if ((mask & POLLOUT_SET) && (out & bit)) {
						res_out |= bit;
						retval++;
						wait = NULL;
					}
					if ((mask & POLLEX_SET) && (ex & bit)) {
						res_ex |= bit;
						retval++;
						wait = NULL;
					}
				}
			}
			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.
		 * 如果這是第一個循環,並且給出了超時,那麼我們將轉換爲ktime_t,並將to指針設置爲到期值。
		 */
		if (end_time && !to) {
			expire = timespec_to_ktime(*end_time);
			to = &expire;
		}

		if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,
					   to, slack))//這個地方很有意思,這個調度只有 當時間到了,或者其他事件到達的時候喚醒    也就是兩種情況,一種是超時了,一種 是poll 裏面有東西準備好了喚醒了。
			timed_out = 1;//如果是超時了 就是 timeout` 返回1
	}

	poll_freewait(&table);

	return retval;
}

簡單來講 do_select 循環調用了poll來設置標誌位,每次一次循環完判斷一下有沒有超時,如果有結果,超時或者有信號處理了,就直接返回。
大致流程我們已經全看明白了。
一開始系統調用select,將時間複製到了內核並轉換成了內核裏面識別的時間,然後調用了core_sys_select,當返回的時候說明處理完畢了,最後修改了一下時間。

core_sys_select 在系統裏面開闢了真正的文件描述符空間,先將select 傳進來的文件描述符拷貝了一份,然後又設置了三個表示結果的描述符res_in,res_out,resrx,然後調用do_select,等do_select返回,然後將res_in,res_out,resrx 重新寫回 ,用戶空間。

do_select首先檢查了文件描述符對應的文件是否打開,然後初始化了poll_wqueues隊列,裏面含有回調函數,然後掛載到fdpoll() 函數,每次poll,返回一個mask,再根據mask設置對應狀態也就是前面的res_in,res_out,resrx。最後阻塞進程,等待時間到或者wake喚醒。

最核心的三個問題已經全部出來了

  1. 最大隻能檢測 1024 個df
  2. 在第一次所有監聽都沒有事件時,調用 select 都需要把進程掛到所有監聽的文件描述符一次。有事件到來時,不知道是哪些文件描述符有數據可以讀寫,需要把所有的文件描述符都輪詢一遍才能知道。
  3. 需要拷貝bitmap。寫回去又要拷貝一次。

如果不糾結poll()函數是怎麼個過程就不用繼續看了。看到這基本上就已經可以了。

static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,
		       poll_table *p);

void poll_initwait(struct poll_wqueues *pwq)
{
	init_poll_funcptr(&pwq->pt, __pollwait);//在這個地方我們初始化了一個函數指針
	pwq->polling_task = current;
	pwq->triggered = 0;
	pwq->error = 0;
	pwq->table = NULL;
	pwq->inline_index = 0;
}


/* Add a new entry */
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);
	//這個地方是一個很巧妙的宏定義,獲取到的pwq裏面的pt 會使 p
	struct poll_table_entry *entry = poll_get_entry(pwq);
	/*
	struct poll_table_entry {
       struct file     *filp;                 // 指向特定fd對應的file結構體;
       unsigned long   key;                   // 等待特定fd對應硬件設備的事件掩碼,如POLLIN、 POLLOUT、POLLERR;
       wait_queue_t    wait;                  // 代表調用select()的應用進程,等待在fd對應設備的特定事件 (讀或者寫)的等待隊列頭上,的等待隊列項;
       wait_queue_head_t   *wait_address;     // 設備驅動程序中特定事件的等待隊列頭(該fd執行fop->poll,需要等待時在哪等,所以叫等待地址);
};
*/
	if (!entry)
		return;
	get_file(filp);
	entry->filp = filp;
	entry->wait_address = wait_address;
	//不設置這個 key  可能會因爲你需要的是POLLIN,卻因爲POLLOUT 喚醒;額
	entry->key = p->key;
	//後面這幾個應該是給驅動用的,驅動需要知道要喚醒什麼。如果一旦有了喚醒條件就會喚醒當前進程。
	init_waitqueue_func_entry(&entry->wait, pollwake); 
	
	entry->wait.private = pwq;
	add_wait_queue(wait_address, &entry->wait);
}

wait = &table.pt
mask = (*f_op->poll)(file, wait); //這個地方 通過wait 成功把__pollwait掛到了對應file 上面。
//linux/fs.h
struct file_operations {
	...
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	...
}
// include/linux/poll.h
static inline void init_poll_funcptr(poll_table *pt, poll_queue_proc qproc)
{
    pt->_qproc = qproc;
    pt->_key   = ~0UL; /* all events enabled */
}
 
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
	if (p && p->_qproc && wait_address)
		p->_qproc(filp, wait_address, p);
}
 
typedef struct poll_table_struct {
	poll_queue_proc _qproc;
	unsigned long _key;
} poll_table;

找了半天沒有找到poll 具體幹了啥可能是每個設備都不一樣。

給兩個圖理解一下。

在這裏插入圖片描述
這圖聽說可以放大。
在這裏插入圖片描述

博客主要參考兩位神仙大佬:

  1. 第一位
  2. 第二位
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章