select
函式實現原理分析
select需要驅動程式的支援,驅動程式實現fops內的poll函式。select通過每個裝置檔案對應的poll函式提供的資訊判斷當前是否有資源可用(如可讀或寫),如果有的話則返回可用資源的檔案描述符個數,沒有的話則睡眠,等待有資源變為可用時再被喚醒繼續執行。
下面我們分兩個過程來分析select:
1. select的睡眠過程
支援阻塞操作的裝置驅動通常會實現一組自身的等待佇列如讀/寫等待佇列用於支援上層(使用者層)所需的block或nonblock操作。當應用程式通過裝置驅動訪問該裝置時(預設為block操作),若該裝置當前沒有資料可讀或寫,則將該使用者程序插入到該裝置驅動對應的讀/寫等待佇列讓其睡眠一段時間,等到有資料可讀/寫時再將該程序喚醒。
select就是巧妙的利用等待佇列機制讓使用者程序適當在沒有資源可讀/寫時睡眠,有資源可讀/寫時喚醒。下面我們看看select睡眠的詳細過程。
select會迴圈遍歷它所監測的fd_set(一組檔案描述符(fd)的集合)內的所有檔案描述符對應的驅動程式的poll函式。驅動程式提供的poll函式首先會將呼叫select的使用者程序插入到該裝置驅動對應資源的等待佇列(如讀/寫等待佇列),然後返回乙個bitmask告訴select當前資源哪些可用。當select迴圈遍歷完所有fd_set內指定的檔案描述符對應的poll函式後,如果沒有乙個資源可用(即沒有乙個檔案可供操作),則select讓該程序睡眠,一直等到有資源可用為止,程序被喚醒(或者timeout)繼續往下執行。
下面分析一下**是如何實現的。
select的呼叫path如下:sys_select -> core_sys_select -> do_select
其中最重要的函式是do_select, 最主要的工作是在這裡, 前面兩個函式主要做一些準備工作。do_select定義如下:
int do_select(int n, fd_set_bits *fds, s64 *timeout)
for (j = 0; j 遍歷每個長字裡的每個位
int fput_needed;
if (i >= n)
break;
if (!(bit & all_bits))
continue;
file = fget_light(i, &fput_needed);
if (file)
if ((mask & pollout_set) && (out & bit))
if ((mask & pollex_set) && (ex & bit))
}cond_resched();
}//返回結果
if (res_in)
*rinp = res_in;
if (res_out)
*routp = res_out;
if (res_ex)
*rexp = res_ex;
}wait = null;
/* 到這裡遍歷結束。retval儲存了檢測到的可操作的檔案描述符的個數。如果有檔案可操作,則跳出for(;;)迴圈,直接返回。若沒有檔案可操作且timeout時間未到同時沒有收到signal,則執行schedule_timeout睡眠。睡眠時間長短由__timeout決定,一直等到該程序被喚醒。
那該程序是如何被喚醒的?被誰喚醒的呢?
我們看下面的select喚醒過程*/
if (retval || !*timeout || signal_pending(current))
break;
if(table.error)
if (*timeout
/* wait indefinitely */
__timeout = max_schedule_timeout;
} else if (unlikely(*timeout >= (s64)max_schedule_timeout - 1)) else
__timeout = schedule_timeout(__timeout);
if (*timeout >= 0)
*timeout = __timeout;
}__set_current_state(task_running);
poll_freewait(&table);
return retval;
}2. select的喚醒過程
前面介紹了select會迴圈遍歷它所監測的fd_set內的所有檔案描述符對應的驅動程式的poll函式。驅動程式提供的poll函式首先會將呼叫select的使用者程序插入到該裝置驅動對應資源的等待佇列(如讀/寫等待佇列),然後返回乙個bitmask告訴select當前資源哪些可用。
乙個典型的驅動程式poll函式實現如下:
(摘自《linux device drivers – thirdedition》page 165)
static unsigned int scull_p_poll(struct file *filp, poll_table *wait)
將使用者程序插入驅動的等待佇列是通過poll_wait做的。
poll_wait定義如下:
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
這裡的p->qproc在do_select內poll_initwait(&table)被初始化為__pollwait,如下:
void poll_initwait(struct poll_wqueues *pwq)
__pollwait定義如下:
/* add a new entry */
static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,
poll_table *p)
通過init_waitqueue_entry初始化乙個等待佇列項,這個等待佇列項關聯的程序即當前呼叫select的程序。然後將這個等待佇列項插入等待佇列wait_address。wait_address即在驅動poll函式內呼叫poll_wait(filp, &dev->inq, wait);時傳入的該驅動的&dev->inq或者&dev->outq等待佇列。
注: 關於等待佇列的工作原理可以參考下面這篇文件:
到這裡我們明白了select如何將當前程序插入所有所監測的fd_set關聯的驅動內的等待佇列,那程序究竟是何時讓出cpu進入睡眠狀態的呢?
進入睡眠狀態是在do_select內呼叫schedule_timeout(__timeout)實現的。當select遍歷完fd_set內的所有裝置檔案,發現沒有檔案可操作時(即retval=0),則呼叫schedule_timeout(__timeout)進入睡眠狀態。
喚醒該程序的過程通常是在所監測檔案的裝置驅動內實現的,驅動程式維護了針對自身資源讀寫的等待佇列。當裝置驅動發現自身資源變為可讀寫並且有程序睡眠在該資源的等待佇列上時,就會喚醒這個資源等待佇列上的程序。
舉個例子,比如核心的8250 uart driver:
uart是使用的tty層維護的兩個等待佇列, 分別對應於讀和寫: (uart是tty裝置的一種)
struct tty_struct
當uart裝置接收到資料,會呼叫tty_flip_buffer_push(tty);將收到的資料push到tty層的buffer。
然後檢視是否有程序睡眠的讀等待佇列上,如果有則喚醒該等待會列。
過程如下:
serial8250_interrupt -> serial8250_handle_port -> receive_chars -> tty_flip_buffer_push ->
flush_to_ldisc -> disc->receive_buf
在disc->receive_buf函式內:
if (waitqueue_active(&tty->read_wait)) //若有程序阻塞在read_wait上則喚醒
wake_up_interruptible(&tty->read_wait);
到這裡明白了select程序被喚醒的過程。由於該程序是阻塞在所有監測的檔案對應的裝置等待佇列上的,因此在timeout時間內,只要任意個裝置變為可操作,都會立即喚醒該程序,從而繼續往下執行。這就實現了select的當有乙個檔案描述符可操作時就立即喚醒執行的基本原理。
Select函式分析
main select等待3秒,3秒輪詢,要非阻塞就置0 char buffer 256 256位元組的接收緩衝區 假定已經建立udp連線,具體過程不寫,簡單,當然tcp也同理,主機ip和port都已經給定,要寫的檔案已經開啟 sock socket bind fp fopen while 1 en...
select函式及例項分析
select機制中提供了乙個資料結構 struct fd set 可以理解為乙個集合,實際上是乙個位圖,每乙個特定為來標誌相應大小檔案描述符,這個集合中存放的是檔案描述符 file descriptor 即檔案控制代碼 也就是點陣圖上的每一位都能與乙個開啟的檔案控制代碼 檔案描述符 建立聯絡,這個工...
golang的select實現原理剖析
select為golang提供了多路io復用機制,和其他io復用一樣,用於檢測是否有讀寫事件是否ready。本文將介紹一下golang的select的用法和實現原理。golang實現select的時候,實際上為每乙個case語句定義了乙個資料結構,select語句塊執行的時候,實際上可以模擬成對乙個...