📄 ch06s02.html
字号:
if (filp->f_flags & O_NONBLOCK) return -EAGAIN; PDEBUG("\"%s\" reading: going to sleep\n", current->comm); if (wait_event_interruptible(dev->inq, (dev->rp != dev->wp))) return -ERESTARTSYS; /* signal: tell the fs layer to handle it */ /* otherwise loop, but first reacquire the lock */ if (down_interruptible(&dev->sem)) return -ERESTARTSYS; } /* ok, data is there, return something */ if (dev->wp > dev->rp) count = min(count, (size_t)(dev->wp - dev->rp)); else /* the write pointer has wrapped, return data up to dev->end */ count = min(count, (size_t)(dev->end - dev->rp)); if (copy_to_user(buf, dev->rp, count)) { up (&dev->sem); return -EFAULT; } dev->rp += count; if (dev->rp == dev->end) dev->rp = dev->buffer; /* wrapped */ up (&dev->sem); /* finally, awake any writers and return */ wake_up_interruptible(&dev->outq); PDEBUG("\"%s\" did read %li bytes\n",current->comm, (long)count); return count;}</pre><p>如同你可见的, 我们在代码中留有一些 PDEBUG 语句. 当你编译这个驱动, 你可使能消息机制来易于跟随不同进程间的交互.</p><p>让我们仔细看看 scull_p_read 如何处理对数据的等待. 这个 while 循环在持有设备旗标下测试这个缓冲. 如果有数据在那里, 我们知道我们可立刻返回给用户, 不必睡眠, 因此整个循环被跳过. 相反, 如果这个缓冲是空的, 我们必须睡眠. 但是在我们可做这个之前, 我们必须丢掉设备旗标; 如果我们要持有它而睡眠, 就不会有写者有机会唤醒我们. 一旦这个确保被丢掉, 我们做一个快速检查来看是否用户已请求非阻塞 I/O, 并且如果是这样就返回. 否则, 是时间调用 wait_event_interruptible.</p><p>一旦我们过了这个调用, 某些东东已经唤醒了我们, 但是我们不知道是什么. 一个可能是进程接收到了一个信号. 包含 wait_event_interruptible 调用的这个 if 语句检查这种情况. 这个语句保证了正确的和被期望的对信号的反应, 它可能负责唤醒这个进程(因为我们处于一个可中断的睡眠). 如果一个信号已经到达并且它没有被这个进程阻塞, 正确的做法是让内核的上层处理这个事件. 到此, 这个驱动返回 -ERESTARTSYS 到调用者; 这个值被虚拟文件系统(VFS)在内部使用, 它或者重启系统调用或者返回 -EINTR 给用户空间. 我们使用相同类型的检查来处理信号, 给每个读和写实现.</p><p>但是, 即便没有一个信号, 我们还是不确切知道有数据在那里为获取. 其他人也可能已经在等待数据, 并且它们可能赢得竞争并且首先得到数据. 因此我们必须再次获取设备旗标; 只有这时我们才可以测试读缓冲(在 while 循环中)并且真正知道我们可以返回缓冲中的数据给用户. 全部这个代码的最终结果是, 当我们从 while 循环中退出时, 我们知道旗标被获得并且缓冲中有数据我们可以用.</p><p>仅仅为了完整, 我们要注意, scull_p_read 可以在另一个地方睡眠, 在我们获得设备旗标之后: 对 copy_to_user 的调用. 如果 scull 当在内核和用户空间之间拷贝数据时睡眠, 它在持有设备旗标中睡眠. 在这种情况下持有旗标是合理的因为它不能死锁系统(我们知道内核将进行拷贝到用户空间并且在不加锁进程中的同一个旗标下唤醒我们), 并且因为重要的是设备内存数组在驱动睡眠时不改变.</p></div><div class="sect2" lang="zh-cn"><div class="titlepage"><div><div><h3 class="title"><a name="AdvancedSleeping.sect2"></a>6.2.5. 高级睡眠</h3></div></div></div><p>许多驱动能够满足它们的睡眠要求, 使用至今我们已涉及到的函数. 但是, 有时需要深入理解 Linux 等待队列机制如何工作. 复杂的加锁或者性能需要可强制一个驱动来使用低层函数来影响一个睡眠. 在本节, 我们在低层看而理解在一个进程睡眠时发生了什么.</p><div class="sect3" lang="zh-cn"><div class="titlepage"><div><div><h4 class="title"><a name="Howaprocesssleeps.sect3"></a>6.2.5.1. 一个进程如何睡眠</h4></div></div></div><p>如果我们深入 <linux/wait.h>, 你见到在 wait_queue_head_t 类型后面的数据结构是非常简单的; 它包含一个自旋锁和一个链表. 这个链表是一个等待队列入口, 它被声明做 wait_queue_t. 这个结构包含关于睡眠进程的信息和它想怎样被唤醒.</p><p>使一个进程睡眠的第一步常常是分配和初始化一个 wait_queue_t 结构, 随后将其添加到正确的等待队列. 当所有东西都就位了, 负责唤醒工作的人就可以找到正确的进程.</p><p>下一步是设置进程的状态来标志它为睡眠. 在 <linux/sched.h> 中定义有几个任务状态. TASK_RUNNING 意思是进程能够运行, 尽管不必在任何特定的时刻在处理器上运行. 有 2 个状态指示一个进程是在睡眠: TASK_INTERRUPTIBLE 和 TASK_UNTINTERRUPTIBLE; 当然, 它们对应 2 类的睡眠. 其他的状态正常地和驱动编写者无关.</p><p>在 2.6 内核, 对于驱动代码通常不需要直接操作进程状态. 但是, 如果你需要这样做, 使用的代码是:</p><pre class="programlisting">void set_current_state(int new_state); </pre><p>在老的代码中, 你常常见到如此的东西:</p><pre class="programlisting">current->state = TASK_INTERRUPTIBLE; </pre><p>但是象这样直接改变 current 是不鼓励的; 当数据结构改变时这样的代码会轻易地失效. 但是, 上面的代码确实展示了自己改变一个进程的当前状态不能使其睡眠. 通过改变 current 状态, 你已改变了调度器对待进程的方式, 但是你还未让出处理器.</p><p>放弃处理器是最后一步, 但是要首先做一件事: 你必须先检查你在睡眠的条件. 做这个检查失败会引入一个竞争条件; 如果在你忙于上面的这个过程并且有其他的线程刚刚试图唤醒你, 如果这个条件变为真会发生什么? 你可能错过唤醒并且睡眠超过你预想的时间. 因此, 在睡眠的代码下面, 典型地你会见到下面的代码:</p><pre class="programlisting">if (!condition) schedule();</pre><p>通过在设置了进程状态后检查我们的条件, 我们涵盖了所有的可能的事件进展. 如果我们在等待的条件已经在设置进程状态之前到来, 我们在这个检查中注意到并且不真正地睡眠. 如果之后发生了唤醒, 进程被置为可运行的不管是否我们已真正进入睡眠.</p><p>调用 schedule , 当然, 是引用调度器和让出 CPU 的方式. 无论何时你调用这个函数, 你是在告诉内核来考虑应当运行哪个进程并且转换控制到那个进程, 如果必要. 因此你从不知道在 schedule 返回到你的代码会是多长时间.</p><p>在 if 测试和可能的调用 schedule (并从其返回)之后, 有些清理工作要做. 因为这个代码不再想睡眠, 它必须保证任务状态被重置为 TASK_RUNNING. 如果代码只是从 schedule 返回, 这一步是不必要的; 那个函数不会返回直到进程处于可运行态. 如果由于不再需要睡眠而对 schedule 的调用被跳过, 进程状态将不正确. 还有必要从等待队列中去除这个进程, 否则它可能被多次唤醒.</p></div><div class="sect3" lang="zh-cn"><div class="titlepage"><div><div><h4 class="title"><a name="Manualsleeps.sect3"></a>6.2.5.2. 手动睡眠</h4></div></div></div><p>在 Linux 内核的之前的版本, 正式的睡眠要求程序员手动处理所有上面的步骤. 它是一个繁琐的过程, 包含相当多的易出错的样板式的代码. 程序员如果愿意还是可能用那种方式手动睡眠; <linux/sched.h> 包含了所有需要的定义, 以及围绕例子的内核源码. 但是, 有一个更容易的方式.</p><p>第一步是创建和初始化一个等待队列. 这常常由这个宏定义完成:</p><pre class="programlisting">DEFINE_WAIT(my_wait); </pre><p>其中, name 是等待队列入口项的名子. 你可用 2 步来做:</p><pre class="programlisting">wait_queue_t my_wait;init_wait(&my_wait);</pre><p>但是常常更容易的做法是放一个 DEFINE_WAIT 行在循环的顶部, 来实现你的睡眠.</p><p>下一步是添加你的等待队列入口到队列, 并且设置进程状态. 2 个任务都由这个函数处理:</p><pre class="programlisting">void prepare_to_wait(wait_queue_head_t *queue, wait_queue_t *wait, int state); </pre><p>这里, queue 和 wait 分别地是等待队列头和进程入口. state 是进程的新状态; 它应当或者是 TASK_INTERRUPTIBLE(给可中断的睡眠, 这常常是你所要的)或者 TASK_UNINTERRUPTIBLE(给不可中断睡眠).</p><p>在调用 prepare_to_wait 之后, 进程可调用 schedule -- 在它已检查确认它仍然需要等待之后. 一旦 schedule 返回, 就到了清理时间. 这个任务, 也, 被一个特殊的函数处理:</p><pre class="programlisting">void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait); </pre><p>之后, 你的代码可测试它的状态并且看是否它需要再次等待.</p><p>我们早该需要一个例子了. 之前我们看了 给 scullpipe 的 read 方法, 它使用 wait_event. 同一个驱动中的 write 方法使用 prepare_to_wait 和 finish_wait 来实现它的等待. 正常地, 你不会在一个驱动中象这样混用各种方法, 但是我们这样作是为了能够展示 2 种处理睡眠的方式.</p><p>为完整起见, 首先, 我们看 write 方法本身:</p><pre class="programlisting">/* How much space is free? */static int spacefree(struct scull_pipe *dev){ if (dev->rp == dev->wp) return dev->buffersize - 1; return ((dev->rp + dev->buffersize - dev->wp) % dev->buffersize) - 1;}static ssize_t scull_p_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos){ struct scull_pipe *dev = filp->private_data; int result; if (down_interruptible(&dev->sem)) return -ERESTARTSYS; /* Make sure there's space to write */ result = scull_getwritespace(dev, filp); if (result) return result; /* scull_getwritespace called up(&dev->sem) */ /* ok, space is there, accept something */ count = min(count, (size_t)spacefree(dev)); if (dev->wp >= dev->rp) count = min(count, (size_t)(dev->end - dev->wp)); /* to end-of-buf */ else /* the write pointer has wrapped, fill up to rp-1 */ count = min(count, (size_t)(dev->rp - dev->wp - 1)); PDEBUG("Going to accept %li bytes to %p from %p\n", (long)count, dev->wp, buf); if (copy_from_user(dev->wp, buf, count)) { up (&dev->sem); return -EFAULT; } dev->wp += count; if (dev->wp == dev->end) dev->wp = dev->buffer; /* wrapped */ up(&dev->sem); /* finally, awake any reader */ wake_up_interruptible(&dev->inq); /* blocked in read() and select() */ /* and signal asynchronous readers, explained late in chapter 5 */ if (dev->async_queue) kill_fasync(&dev->async_queue, SIGIO, POLL_IN); PDEBUG("\"%s\" did write %li bytes\n",current->comm, (long)count); return count;}</pre>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -