select poll和epoll

select poll epoll都是IO多路復用機制。這裏的複用其實能夠理解爲複用的線程,即一個(或者較少的)線程完成多個IO的讀寫。這裏總結下這三個函數的區別。html

1 select

1.1 select原理分析

1 select的函數原型是node

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

使用的時候須要將fd_set從用戶空間copy到內核空間。select的使用方式相似以下socket

while true {
select(streams[])
for i in streams[] { //須要遍歷全部的fd_set
if i has data // 判斷是否有數據
read until unavailable
}
}

2 select的核心是do_select()。do_select首先會註冊回調函數__pollwait,__pollwait會在被調用的時候將當前進程添加到設備的等待隊列裏。ide

do_select會在一個for循環裏調用設備的f_op->poll。而該函數有兩個做用,一個是調用poll_wait()函數,一個是檢測設備當前狀態。而poll_wait會調用回調函數__pollwait,將當前進程加入到設備等待隊列裏。函數

設備本身實現了當有讀寫的時候會喚醒等待隊列裏的進程。若是當前沒有設備可讀寫,那麼do_select()就將當前進程睡眠。設備會在有讀寫的時候喚醒進程。喚醒後設備必須從新輪詢一遍全部的設備,調用poll來檢測設備當前的狀態以肯定哪些可寫可讀。.net

int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
{
	struct poll_wqueues table;
	poll_table *wait;
	poll_initwait(&table); // 註冊回調函數__pollwait
	wait = &table.pt;
	// …
    for (;;) {
		// …
		for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
			// …
			struct fd f;
			f = fdget(i);
			if (f.file) {
				const struct file_operations *f_op; // 重要
				f_op = f.file->f_op; // 重要
				mask = DEFAULT_POLLMASK;
				if (f_op->poll) {
					wait_key_set(wait, in, out,
						     bit, busy_flag);
					// 對每一個fd進行I/O事件檢測
					mask = (*f_op->poll)(f.file, wait); // 函數指針,每一個設備自定義本身的poll。每一個設備擁有一個struct file_operations結構體,這個結構體裏定義了各類用於操做設備的函數指針,具體怎麼操做是設備本身定義的
				}
				fdput(f);
				// …
			}
		}
		// 退出循環體
		if (retval || timed_out || signal_pending(current))
			break;
		// 沒有可讀寫,讓進程進入休眠
		if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,
				to, slack))
			timed_out = 1;
	}
}

3 file 結構線程

struct file {
	struct path		f_path;
	struct inode		*f_inode;	/* cached value */
	const struct file_operations	*f_op;

	// …

} __attribute__((aligned(4)));	/* lest something weird decides that 2 is OK */

4 file_operations結構指針

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iterate) (struct file *, struct dir_context *);
	// select()輪詢設備fd的操做函數, poll調用poll_wait, 而poll_wait會調用回調函數__pollwait, __pollwait將當前進程加到等待隊列裏
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	// …	
};

5 簡單總結來說,select會遍歷fd_set,調用f_op->poll(此poll非select/poll的poll),若是有可讀/寫的fd則返回可讀/寫的fd,若是沒有則在每一個fd的等待隊列中加入當前進程,當前進程進入睡眠。當有fd可讀/寫的時候會喚醒當前進程,當前進行從新遍歷fd_set,返回可讀/寫的全部fd。rest

6 從中也能夠看出select的幾大缺點:code

  1. 每次調用select,都須要把fd集合從用戶態拷貝到內核態,這個開銷在fd不少時會很大
  2. 同時每次調用select都須要在內核遍歷傳遞進來的全部fd,這個開銷在fd不少時也很大
  3. select支持的文件描述符數量過小了,默認是1024

2 poll

poll的實現原理和select相似,只是接口的方式不一樣。

3 epoll

1 epoll的函數原型是

int epoll_create(int size); // 建立一個epoll對象,通常epollfd = epoll_create()

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // (epoll_add/epoll_del的合體),往epoll對象中增長/刪除某一個流的某一個事件好比epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//有緩衝區內有數據時epoll_wait返回epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//緩衝區可寫入時epoll_wait返回

int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);  // 等待直到註冊的事件發生

epoll的使用方式相似以下:

while true {
active_stream[] = epoll_wait(epollfd) // 只返回可讀/寫的fd,而不是像select同樣,返回全部的fd
for i in active_stream[] {
read or write till unavailable
}
}

2 epoll_create。epoll會向內核註冊一個文件系統,調用epoll_create時:

  1. 就會在這個虛擬的epoll文件系統裏建立一個file結點;
  2. 而且在初始化的時候開闢epoll本身的內核cache,用於存儲epoll_ctl傳來的socket,這些socket以紅黑樹的方式組織放在cache裏,用於支持快速的查找、插入、刪除操做
  3. 還會再創建一個list鏈表,用於存儲準備就緒的事件

3 epoll_ctl。 當咱們執行epoll_ctl時,除了把socket放到epoll文件系統裏file對象對應的紅黑樹上以外,還會給內核中斷處理程序註冊一個回調函數,告訴內核,若是這個句柄的中斷到了,就把它放到準備就緒list鏈表裏。因此,當一個socket上有數據到了,內核在把網卡上的數據copy到內核中後就來把socket插入到list裏了

4 epoll_wait。 epoll_wait調用時,僅僅觀察這個list鏈表裏有沒有數據便可。有數據就返回,沒有數據就sleep

5 可見,

  1. select在醒着的時候要遍歷整個fd_set,而epoll只須要判斷一下list是否爲空就能夠了,這節約了大量的cpu時間;
  2. select,poll每次調用都要把fd集合從用戶態往內核態拷貝一次,而且要把current往設備等待隊列中掛一次,而epoll只要一次拷貝,並且把current往等待隊列上掛也只掛一次。這也能節省很多的開銷

參考

  1. http://janfan.cn/chinese/2015/01/05/select-poll-impl-inside-the-kernel.html
  2. http://www.javashuo.com/article/p-svnhixiy-b.html
  3. http://www.javashuo.com/article/p-txmndpyl-cm.html
  4. https://www.zhihu.com/question/20122137
相關文章
相關標籤/搜索