1、系统调用分成低速系统调用和其他系统调用两类。低速系统调用是可能会使进程永远阻塞的一类调用调用,他们包含:
#include
#include
#include
#include
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; (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;
// get_fd_set仅仅调用copy_from_user从用户空间拷贝了fd_set
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;
从用户空间复制到内核空间
memset(fdset, 0, nr);
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; 返回就绪的文件描述符的个数
struct poll_table_entry {
struct file * filp;
wait_queue_t wait;
wait_queue_head_t * wait_address;
};
struct poll_table_page {
struct poll_table_page * next;
struct poll_table_entry * entry;
struct poll_table_entry entries[0];
};
/*
* Structures and helpers for sys_poll/sys_poll
*/
struct poll_wqueues {
poll_table pt;
struct poll_table_page * table;
int error;
int inline_index;
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)
{
init_poll_funcptr(&pwq->pt, __pollwait);
pwq->error = 0;
pwq->table = NULL;
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循环,正常情况下一直要到监视中的某个已打开文件中
有了输入或满足了其它等待条件,或者指定的睡眠等待时问已经到期,或者当前进程接收到了信号时
才会结束。
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;
// 先取出当前循环周期中的32个文件描述符对应的bitmaps
in = *inp++; out = *outp++; ex = *exp++;
all_bits = in | out | ex;
if (all_bits == 0) {
i += __NFDBITS;//每32个文件描述符一个循环,正好一个long型数
continue;
}
如果三个位图之一中的某一位为1,就对相应的已打开文件作一次询问
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结构指针,并增加 并把询问的结果汇集到fds所指的fd_ set bits数据结构中。一趟扫描下来以后,就检查一< 下上述的条件是否已经满足,或出了错。如果没有就通过< 进入睡眠,到被唤醒时再 在下一轮循环中作另一次扫描。就这样,除第一次以外,以后都是在进程被唤醒时才执行一遍循环,所 以从本质上讲是一种do-while循环。
if (file) {
f_op = file->f_op;
mask = DEFAULT_POLLMASK;
if (f_op && f_op->poll) 在这里循环调用所监测的<内的所有文件描述符对应的驱动程序的函数 mask = (*f_op->poll)(file, retval ? NULL : wait); /*
调用驱动程序中的poll函数,以evdev驱动中的
evdev_poll()为例该函数会调用函数poll_wait(file, &evdev->wait, wait),继续调用__pollwait()回调来分配一个poll_table_entry结构体,该结构体有一个内嵌的等待队列项,设置好wake时调用的回调函数后将其添加到驱动程序中的等待队列头中。
*/ // 释放file结构指针,实际就是减小他的一个引用
计数字段f_count。
fput_light(file, fput_needed);
if ((mask & POLLIN_SET) && (in & bit)) {
res_in |= bit; 如果是这个描述符可读将这个位置位
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 (3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll) (4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。 l (5)__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。< (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函数初始化填充的)。