簡單說一下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
隊列,裏面含有回調函數,然後掛載到fd
的poll()
函數,每次poll,返回一個mask
,再根據mask
設置對應狀態也就是前面的res_in,res_out,resrx
。最後阻塞進程,等待時間到或者wake
喚醒。
最核心的三個問題已經全部出來了
- 最大隻能檢測
1024
個df - 在第一次所有監聽都沒有事件時,調用 select 都需要把進程掛到所有監聽的文件描述符一次。有事件到來時,不知道是哪些文件描述符有數據可以讀寫,需要把所有的文件描述符都輪詢一遍才能知道。
- 需要拷貝
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
具體幹了啥可能是每個設備都不一樣。
給兩個圖理解一下。
這圖聽說可以放大。
博客主要參考兩位神仙大佬: