第六章 休眠
在第五章并发与竞态中,介绍了一个当进程需要的资源被其他进程占用,而不得不等待该资源的情形。这里,要说的则是即便进程得到资源之后,由于资源本身的一些问题而不能预期的完成功能,必须休眠一段时间,直到它的要求得到满足。两种情形的区别是显而易见的,不过在使用上,实在是有点相似。其实最近学的一些东西都是这样,信号量,自旋锁....不管他们的内在区别的话,使用时无外乎就是初始化,然后在一个地方申请,另一个地方反馈。
先对休眠有一个基础的认识先。就是说,进程确实是在运行,需要的东西也有给它了,但是,给它的东西却不满足它的要求,于是乎它还是做不了事。怎么办呢,就只有让它等了,当前进程也就休眠了。进入休眠的时候会把它对资源的要求提出来,一旦这个要求得到满足,休眠的进程就会立刻被唤醒。
然后是关于休眠的两个需要注意的地方。首先,进入休眠之后,当前进程就算是失去了对操作系统的控制,休眠多久是不确定的,所以肯定不能放在原子上下文。宁外,之前也强调过的,在使用自旋锁时一定不能休眠,而信号量方法是允许休眠的。第二个就是在休眠被唤醒时,程序一定要再次检查要求是否得到满足,这个一点都不多余。
操作系统的知识告诉我,以上说的起始就是进程的阻塞了。操作系统会有一个等待队列来管理阻塞的进程,要实现休眠的第一步也就是构建一个等待队列头了。如下:
1 DECLARE_WAIT_QUEUE_HEAD(name);//静态
2 wait_queue_head_t my_queue;
3 init_waitqueue_head(&my_queue);//动态
下面的方法让进程进入休眠:
1 wait_event(queue, condition)
2 wait_event_interruptible(queue, condition)//可中断
3 wait_event_timeout(queue, condition, timeout)
4 wait_event_interruptible_timeout(queue, condition, timeout)
Queue也就是之前构建的等待队列头,condition也就是进程提出的要求了,这是一个布尔表达式,当表达式为真时,休眠的进程就会立刻被唤醒。
唤醒用以下函数,这个就没什么好讲的了:
1 void wake_up(wait_queue_head_t *queue);
2 void wake_up_interruptible(wait_queue_head_t *queue);
之前的scull项目本身是没有支持休眠的,但是scull源码下的pipe.c文件给我们提供了一个休眠的实例。这是驱动程序跟之前的scull很是不同,它用到了缓冲区来读写数据。这样才会出现读写时阻塞的情况。读数据时缓冲区为空阻塞,写数据时缓冲区已满阻塞。Scullpiep的设备结构如下:
1 struct scull_pipe {
2 wait_queue_head_t inq, outq; /* read and write queues */
3 char *buffer, *end; /* begin of buf, end of buf */
4 int buffersize; /* used in pointer arithmetic */
5 char *rp, *wp; /* where to read, where to write */
6 int nreaders, nwriters; /* number of openings for r/w */
7 struct fasync_struct *async_queue; /* asynchronous readers */
8 struct semaphore sem; /* mutual exclusion semaphore */
9 struct cdev cdev; /* Char device structure */
10 };
新增了缓冲区。
读取的代码如下:
1 static ssize_t scull_p_read (struct file *filp, char __user *buf, size_t count,
2 loff_t *f_pos)
3 {
4 struct scull_pipe *dev = filp->private_data;
5
6 if (down_interruptible(&dev->sem))
7 return -ERESTARTSYS;
8
9 while (dev->rp == dev->wp) { /* nothing to read */
10 up(&dev->sem); /* release the lock */
11 if (filp->f_flags & O_NONBLOCK)
12 return -EAGAIN;
13 PDEBUG("\"%s\" reading: going to sleep\n", current->comm);
14 if (wait_event_interruptible(dev->inq, (dev->rp != dev->wp)))
15 return -ERESTARTSYS; /* signal: tell the fs layer to handle it */
16 /* otherwise loop, but first reacquire the lock */
17 if (down_interruptible(&dev->sem))
18 return -ERESTARTSYS;
19 }
20 /* ok, data is there, return something */
21 if (dev->wp > dev->rp)
22 count = min(count, (size_t)(dev->wp - dev->rp));
23 else /* the write pointer has wrapped, return data up to dev->end */
24 count = min(count, (size_t)(dev->end - dev->rp));
25 if (copy_to_user(buf, dev->rp, count)) {
26 up (&dev->sem);
27 return -EFAULT;
28 }
29 dev->rp += count;
30 if (dev->rp == dev->end)
31 dev->rp = dev->buffer; /* wrapped */
32 up (&dev->sem);
33
34 /* finally, awake any writers and return */
35 wake_up_interruptible(&dev->outq);
36 PDEBUG("\"%s\" did read %li bytes\n",current->comm, (long)count);
37 return count;
38 }
关键在于while循环的内容。dev->rp == dev->wp表示缓冲区为空,这时进程不能顺利的读数据,必须休眠。因为这里使用了信号量来管理并发进程,当前进程休眠时必须先释放信号量,以便其他进程运行。wait_event_interruptible(dev->inq, (dev->rp != dev->wp))就是进入休眠了,被唤醒之后,就接着运行之后的代码,第一步也就是重新获取信号量了。这里也并没有直接退出while循环,循环条件仍需要判断,之前就说过,这个对要求是否得到满足的再次判断是不多余的。后面的一些读取操作以前的没太大区别。
接下来的重点就在于在哪里唤醒这个休眠中的进程了。其实很简单,在write方法完成的时候,执行以下代码就行了:
1 * finally, awake any reader */
2 wake_up_interruptible(&dev->inq); /* blocked in read() and select() */
整体的模式还是相当的清晰的,自行脑补。