1、系统调用分成低速系统调用和其他系统调用两类。低速系统调用是可能会使进程永远阻塞的一类调用调用,他们包含:
#include <stdio.h> #include <errno.h> #include <fcntl.h> #include <unistd.h> char buf[50000]; int main(void){ int ntow,nw; char *ptr; ntow = read(STDIN_FILENO,buf,sizeof(buf)); fprintf(stderr, "read %d bytes.\n",ntow); int flags = fcntl(STDOUT_FILENO, F_GETFL,0); fcntl(STDOUT_FILENO,F_SETFL,flags|O_NONBLOCK); ptr = buf; while(ntow > 0){ errno = 0; nw = write(STDOUT_FILENO,ptr,ntow); fprintf(stderr,"nwrite = %d, errno = %d\n",nw, errno); if(nw > 0){ ptr += nw; ntow -= nw; } } fcntl(STDOUT_FILENO,F_SETFL,flags|~O_NONBLOCK); }
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; 由于timerval数据结构都在用户空间,所以要通过copy_from_user把数据从用户空间复制到内核空间中 to = &end_time;<span lang="EN-US" style="color: rgb(51, 51, 51); // 得到timespec格式的未来超时时间< 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); /*如果有超时值, 并拷贝离超时时刻还剩的时间到用户空间的timeval中*/< // 返回就绪的文件描述符的个数
(1)使用copy_from_user从用户空间拷贝timer到内核空间
(2)core_sys_select ------>do_select
3、poll_select_copy_remaining
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 */ 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);// RCU ref, 获取当前进程的文件描述符表 max_fds = fdt->max_fds; 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个bitmaps */ size = FDS_BYTES(n); bits = stack_fds;//分配的内存 // 以一个文件描述符占一bit来计算,传递进来的这些fd_set需要用掉多少个字 if (size > sizeof(stack_fds) / 6) { // 除6,为什么?因为每个文件描述符需要6个bitmaps /* Not enough space in on-stack array; must use kmalloc */ ret = -ENOMEM; bits = kmalloc(6 * size, GFP_KERNEL);// stack中分配的太小,直接kmalloc 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; <span style="white-space:pre"> // get_fd_set仅仅调用copy_from_user从用户空间拷贝了fd_set</span> 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))) copy到内核空间 goto out;
/* * We do a VERIFY_WRITE here even though we are only reading this time: * we'll write to it eventually.. * * Use "unsigned long" accesses to let user-mode fd_set's be long-aligned. */ 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; 从用户空间复制到内核空间 <span style="white-space:pre"> </span>memset(fdset, 0, nr); <span style="white-space:pre"> </span>return 0; }
zero_fd_set(n, fds.res_in);// 对这些存放返回状态的字段清0 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; } // 把结果集,拷贝回用户空间 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)) 写回到用户空间 copy_to_user ret = -EFAULT; out: if (bits != stack_fds) kfree(bits); out_nofds: return ret;<span style="white-space:pre"> </span>返回就绪的文件描述符的个数<span style="white-space:pre"> </span>
</pre><pre code_snippet_id="1555746" snippet_file_name="blog_20160114_5_5606235" name="code" class="cpp">
struct poll_table_entry { <span style="white-space:pre"> </span>struct file * filp; <span style="white-space:pre"> </span>wait_queue_t wait; <span style="white-space:pre"> </span>wait_queue_head_t * wait_address; }; struct poll_table_page { <span style="white-space:pre"> </span>struct poll_table_page * next; <span style="white-space:pre"> </span>struct poll_table_entry * entry; <span style="white-space:pre"> </span>struct poll_table_entry entries[0]; }; /* * Structures and helpers for sys_poll/sys_poll */ struct poll_wqueues { <span style="white-space:pre"> </span>poll_table pt; <span style="white-space:pre"> </span>struct poll_table_page * table; <span style="white-space:pre"> </span>int error; <span style="white-space:pre"> </span>int inline_index; <span style="white-space:pre"> </span>struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES]; };
当一个进程要进入睡眠,而想要某个设备的驱动程序在设备的状态发生变化时将其唤醒,就得要准备好一个wait_queue_ t数据结构,并将这个数据结构挂入目标设备的某个等待队列中。在等待对象单一时一般都把wait_queue_ t数据结构建立在堆栈中。可是,在有多个等待对象时就不能那样了。另一方面,在有多个等待对象、从而有多个wait_queue t数据结构时,要有个既有效、又灵活,便于扩充的方法将这些wait_queue_ t结构管理起来,上面这些数据结构就正是为此而设计的。这里的poll_ table_ entry数据结构既是对wait queue_ t的扩充,又是对它的“包装”。此外,poll_ table_page结构中数组entries []的下标为。,表示该数组的大小可以动态地确定。实际使用时总是分配一个页面,页面中能容纳儿个poll_ table_ entry结构,这个数组就是多大。使用中指针 entry总是指向entries []中的第一个空闲的poll_ table_ entry结构,根据需要动态地分配entries []中的表项。一个页面 用完了,就再分配一个,通过指针next连成一条单链。函数do_ select()中定义了一个局部的poll_ table数据结构table } poll_initwait(&table);先对其进行初始化
void poll_initwait(struct poll_wqueues *pwq) { <span style="white-space:pre"> </span>init_poll_funcptr(&pwq->pt, __pollwait); <span style="white-space:pre"> </span>pwq->error = 0; <span style="white-space:pre"> </span>pwq->table = NULL; <span style="white-space:pre"> </span>pwq->inline_index = 0;初始化还未分配任何页面 }
int do_select(int n, fd_set_bits *fds, struct timespec *end_time) {fds为fd_set_bits指针,结构中有6个位图,其中前3个为要求位图; ktime_t expire, *to = NULL; struct poll_wqueues table; poll_table *wait; int retval, i, timed_out = 0; unsigned long slack = 0; rcu_read_lock(); retval = max_select_fd(n, fds);通过max_ select fd()根据这3个位图计算出本次操作所涉及最大的已打开文件号是什么, rcu_read_unlock();所有号码高于这个数值的已打开文件都与本次操作无关。 if (retval < 0) return retval; n = retval; poll_initwait(&table); wait = &table.pt; if (end_time && !end_time->tv_sec && !end_time->tv_nsec) { wait = NULL; // 如果系统调用带进来的超时时间为0,那么设置 timed_out = 1,表示不阻塞,直接返回。 timed_out = 1; } if (end_time && !timed_out)// 超时时间转换 slack = estimate_accuracy(end_time); retval = 0; CPU就进入了一个无穷for循环,正常情况下一直要到监视中的某个已打开文件中 <span style="white-space:pre"> </span>有了输入或满足了其它等待条件,或者指定的睡眠等待时问已经到期,或者当前进程接收到了信号时 <span style="white-space:pre"> </span>才会结束。 for (;;) { unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp; set_current_state(TASK_INTERRUPTIBLE); 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; <span style="white-space:pre"> </span> // 先取出当前循环周期中的32个文件描述符对应的bitmaps</span> in = *inp++; out = *outp++; ex = *exp++; all_bits = in | out | ex; if (all_bits == 0) { i += __NFDBITS;//每32个文件描述符一个循环,正好一个long型数 continue; } <span style="white-space:pre"> </span>如果三个位图之一中的某一位为1,就对相应的已打开文件作一次询问</span> for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) { int fput_needed; if (i >= n) break; if (!(bit & all_bits)) continue; file = fget_light(i, &fput_needed);// 得到file结构指针,并增加<span style="white-space:pre"> </span>并把询问的结果汇集到fds所指的fd_ set bits数据结构中。一趟扫描下来以后,就检查一<<span style="white-space:pre"> </span>下上述的条件是否已经满足,或出了错。如果没有就通过<<span style="white-space:pre"> </span>进入睡眠,到被唤醒时再 在下一轮循环中作另一次扫描。就这样,除第一次以外,以后都是在进程被唤醒时才执行一遍循环,所 以从本质上讲是一种do-while循环。<span style="white-space:pre"> </span> if (file) { f_op = file->f_op; mask = DEFAULT_POLLMASK; if (f_op && f_op->poll)<span style="white-space:pre"> 在这里循环调用所监测的<内的所有文件描述符对应的驱动程序的</函数 <span style="white-space:pre"> </span>mask = (*f_op->poll)(file, retval ? NULL : wait);<span style="white-space:pre"> </span> /* 调用驱动程序中的poll函数,以evdev驱动中的 evdev_poll()为例该函数会调用函数poll_wait(file, &evdev->wait, wait),继续调用__pollwait()回调来分配一个poll_table_entry结构体,该结构体有一个内嵌的等待队列项,设置好wake时调用的回调函数后将其添加到驱动程序中的等待队列头中。 */ // 释放file结构指针,实际就是减小他的一个引用 计数字段f_count。 </span> fput_light(file, fput_needed); if ((mask & POLLIN_SET) && (in & bit)) { res_in |= bit;<span style="white-space:pre"> </span>如果是这个描述符可读将这个位置位 retval++;//返回描述符个数加1 } if ((mask & POLLOUT_SET) && (out & bit)) { res_out |= bit; retval++; } if ((mask & POLLEX_SET) && (ex & bit)) { res_ex |= bit; retval++; } } } if (res_in) *rinp = res_in; if (res_out) *routp = res_out; if (res_ex) *rexp = res_ex; 这里的目是为了增加一个抢占点。 在支持抢占式调度的内核中(定义了CONFIG_PREEMPT),cond_resched是空操作。 cond_resched();自动放弃cpu给高级任务使用 } wait = NULL;/ 后续有用,避免重复执行__pollwait() 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. */ /*跳出这个大循环的条件有: 有设备就绪或有异常(retval!=0), 超时(timed_out = 1), 或者有中止信号出现*/ if (end_time && !to) { expire = timespec_to_ktime(*end_time); to = &expire; } // 第一次循环中,当前用户进程从这里进入休眠, // 上面传下来的超时时间只是为了用在睡眠超时这里而已 // 超时,poll_schedule_timeout()返回0;被唤醒时返回-EINTR if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS)) timed_out = 1; /* 超时后,将其设置成1,方便后面退出循环返回到上层 */ } __set_current_state(TASK_RUNNING); poll_freewait(&table); return retval; } 那么,在什么情况下会被唤醒呢?首先,受到询问的已打开文件的设备驱动程序会把当前进程通 过一个wait_queue_ t数据结构,从而poll_ table_ entry数据结构,挂入其唤醒队列,使得该设备的中断 服务程序在接收到输入时就会唤醒这个进程。其次,如果指定了时问限制,则当时问到点时也会唤醒 这个进程,这是因为进程在进入睡眠时都指定了需要继续睡眠的时问。最后,如果进程接收到了信号 也会被唤醒。 显然,这里的关键在于对具体已打开文件,即设备的询问。从代码中可以看出,这是通过具体 file operations数据结构中提供的函数指针poll进行的。我们以前都把注意力集中在open, read, write 等更为常用的操作上,有意忽略了这个函数指针,现在要回过来关注这个操作了。另一方面,阅读poll 操作的代码在某种意义上也是对有关内容的一次复习。 <(2)注册回调函数__pollwait<span style="white-space:pre"> </span> (3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)<span style="white-space:pre"> </span> (4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。 l<span style="white-space:pre"> </span>(5)__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。<<span style="white-space:pre"> </span> (6)poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。 (7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。 (8)把fd_set从内核空间拷贝到用户空间。(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大(3)select支持的文件描述符数量太小了,默认是1024上层要能使用select()和poll()系统调用来监测某个设备文件描述符,那么就必须实现这个设备驱动程序中struct file_operation结构体的poll函数,为什么?因为这两个系统调用最终都会调用驱动程序中的poll函数来初始化一个等待队列项, 然后将其加入到驱动程序中的等待队列头,这样就可以在硬件可读写的时候wake up这个等待队列头,然后等待(可以是多个)同一个硬件设备可读写事件的进程都将被唤醒。(这个等待队列头可以包含多个等待队列项,这些不同的等待队列项是由不同的应用程序调用select或者poll来监测同一个硬件设备的时候调用file_operation的poll函数初始化填充的)。</p>