您的位置:首页 > Web前端

LDD3源码分析之阻塞型I/O

2012-06-08 15:34 281 查看


LDD3源码分析之阻塞型I/O

分类: LDD3源码分析2012-03-26
15:00 87人阅读 评论(0) 收藏 举报

作者:刘昊昱

博客:http://blog.csdn.net/liuhaoyutz

编译环境:Ubuntu 10.10

内核版本:2.6.32-38-generic-pae

LDD3源码路径:examples/scull/pipe.c examples/scull/main.c

本文分析LDD3第六章介绍的scullpipe设备是如何实现阻塞I/O的。另外,我发现scullpipe的实现代码有一个问题,在文章的最后,对这个问题进行了说明,并给出了修正代码。

一、scullpipe设备实现阻塞I/O分析

scuoopipe设备被实现为具有类似管道特性,用以演示如何实现阻塞型和非阻塞型I/O。实际的设备驱动中,这通常通过硬件中断的方式实现:当等待事件发生时,硬件发出一个中断,然后在中断处理程序中,驱动程序会唤醒等待进程。作为一个演示程序,scullpipe没有利用中断,而是选择使用另一个进程来产生数据并唤醒读取进程,类似的,利用读取进程来唤醒等待缓冲区可用的写入进程。

scullpipe的主体实现在examples/scull/pipe.c中,但是也利用了examples/scull/main.c中的一些代码。我们分析scullpipe的起点在main.c中,首先看main.c中的如下代码:

[cpp] view
plaincopy

648 /* Initialize each device. */

649 for (i = 0; i < scull_nr_devs; i++) {

650 scull_devices[i].quantum = scull_quantum;

651 scull_devices[i].qset = scull_qset;

652 init_MUTEX(&scull_devices[i].sem);

653 scull_setup_cdev(&scull_devices[i], i);

654 }

655

656 /* At this point call the init function for any friend device */

657 dev = MKDEV(scull_major, scull_minor + scull_nr_devs);

658 dev += scull_p_init(dev);

659 dev += scull_access_init(dev);

649 - 654行,我们在分析第三章中的scull设备时已经分析过了,用for循环初始化scull0 - scull3,但当时657
- 659行我们跳过没有分析。

现在来看657行,scull_major和scull_minor的默认值都是0,scull_nr_devs的默认值是4。dev变量在618行定义,它是dev_t类型,注意dev_t类型用来保存设备编号,包括主设备号和次设备号。所以,657行,我们通过MKDEV宏得到一个主设备号为0,次设备号为4的设备编号保存在dev中。这里之所以让次设备号为4,是因为前面已经注册了scull0
- scull3,它们的主设备号均为系统分配值,次设备号分别是0,1,2,3。虽然我们还没有看后面的代码,但我们由此可以推测出作者的意图,要创建scullpipe0
- scullpipe3,其主设备号均为系统分配值,次设备号分别为4,5,6,7。

658行是我们要分析的重点。而659行以后才会涉及,在本文中不分析659行。注意,658行,把657行生成的设备编号dev做为参数传递给scull_p_init函数,现在看这个函数的实现,在examples/scull/pipe.c文件中:

[cpp] view
plaincopy

343/*

344 * Initialize the pipe devs; return how many we did.

345 */

346int scull_p_init(dev_t firstdev)

347{

348 int i, result;

349

350 result = register_chrdev_region(firstdev, scull_p_nr_devs, "scullp");

351 if (result < 0) {

352 printk(KERN_NOTICE "Unable to get scullp region, error %d\n", result);

353 return 0;

354 }

355 scull_p_devno = firstdev;

356 scull_p_devices = kmalloc(scull_p_nr_devs * sizeof(struct scull_pipe), GFP_KERNEL);

357 if (scull_p_devices == NULL) {

358 unregister_chrdev_region(firstdev, scull_p_nr_devs);

359 return 0;

360 }

361 memset(scull_p_devices, 0, scull_p_nr_devs * sizeof(struct scull_pipe));

362 for (i = 0; i < scull_p_nr_devs; i++) {

363 init_waitqueue_head(&(scull_p_devices[i].inq));

364 init_waitqueue_head(&(scull_p_devices[i].outq));

365 init_MUTEX(&scull_p_devices[i].sem);

366 scull_p_setup_cdev(scull_p_devices + i, i);

367 }

368#ifdef SCULL_DEBUG

369 create_proc_read_entry("scullpipe", 0, NULL, scull_read_p_mem, NULL);

370#endif

371 return scull_p_nr_devs;

372}

350行,调用register_chrdev_region函数申请从firstdev开始的scull_p_nr_devs个设备编号,firstdev是scull_p_init函数的形参,所以firstdev是前面main.c中658行传的设备编号。而scull_p_nr_devs的默认值为4.

355行,scull_p_devno = firstdev; scull_p_devno用于记录第一个scullpipe设备的设备编号。

356行,为scullpipe0 - scullpipe3的设备结构体分配内存空间。scullpipe设备由scull_pipe结构体表示,其定义如下:

[html] view
plaincopy

33struct scull_pipe {

34 wait_queue_head_t inq, outq; /* read and write queues */

35 char *buffer, *end; /* begin of buf, end of buf */

36 int buffersize; /* used in pointer arithmetic */

37 char *rp, *wp; /* where to read, where to write */

38 int nreaders, nwriters; /* number of openings for r/w */

39 struct fasync_struct *async_queue; /* asynchronous readers */

40 struct semaphore sem; /* mutual exclusion semaphore */

41 struct cdev cdev; /* Char device structure */

42};

361行,将scull_pipe结构体数组的内容清0。

362 - 367行,初始化scullpipe设备对应的scull_pipe结构体。每次for循环初始化一个scullpipe设备。

363 - 364行,初始化两个等待队列头scull_p_devices[i].inq和scull_p_devices[i].outq,这两个等待队列头分别代表休眠在scullpipe设备上的等待读取和等待写入的进程。

365行初始化信号量scull_p_devices[i].sem。

366行,调用scull_p_setup_cdev函数完成进一步的初始化。下面看这个函数的实现:

[cpp] view
plaincopy

326/*

327 * Set up a cdev entry.

328 */

329static void scull_p_setup_cdev(struct scull_pipe *dev, int index)

330{

331 int err, devno = scull_p_devno + index;

332

333 cdev_init(&dev->cdev, &scull_pipe_fops);

334 dev->cdev.owner = THIS_MODULE;

335 err = cdev_add (&dev->cdev, devno, 1);

336 /* Fail gracefully if need be */

337 if (err)

338 printk(KERN_NOTICE "Error %d adding scullpipe%d", err, index);

339}

333行,调用cdev_init函数初始化dev->cdev,指定设备操作函数集是scull_pipe_fops。

335行,调用cdev_add函数将dev->cdev注册到系统中, devno是对应的设备编号。

至此,scullpipe设备就初始化完毕并且注册到系统中。

再回到scull_p_init函数,368 - 370行,注册了scullpipe的/proc接口。因为本文主要分析阻塞型I/O,所以不讨论这个接口。但需要注意的是,scullpipe的/proc接口实现与scull的/proc接口实现不一样,主要是start参数相关的问题,有兴趣可以仔细研究一下。

371行,返回注册的scullpipe设备个数。

下面我们来看scullpipe的read函数,即scull_p_read函数,其代码如下所示:

[cpp] view
plaincopy

118static ssize_t scull_p_read (struct file *filp, char __user *buf, size_t count,

119 loff_t *f_pos)

120{

121 struct scull_pipe *dev = filp->private_data;

122

123 if (down_interruptible(&dev->sem))

124 return -ERESTARTSYS;

125

126 while (dev->rp == dev->wp) { /* nothing to read */

127 up(&dev->sem); /* release the lock */

128 if (filp->f_flags & O_NONBLOCK)

129 return -EAGAIN;

130 PDEBUG("\"%s\" reading: going to sleep\n", current->comm);

131 if (wait_event_interruptible(dev->inq, (dev->rp != dev->wp)))

132 return -ERESTARTSYS; /* signal: tell the fs layer to handle it */

133 /* otherwise loop, but first reacquire the lock */

134 if (down_interruptible(&dev->sem))

135 return -ERESTARTSYS;

136 }

137 /* ok, data is there, return something */

138 if (dev->wp > dev->rp)

139 count = min(count, (size_t)(dev->wp - dev->rp));

140 else /* the write pointer has wrapped, return data up to dev->end */

141 count = min(count, (size_t)(dev->end - dev->rp));

142 if (copy_to_user(buf, dev->rp, count)) {

143 up (&dev->sem);

144 return -EFAULT;

145 }

146 dev->rp += count;

147 if (dev->rp == dev->end)

148 dev->rp = dev->buffer; /* wrapped */

149 up (&dev->sem);

150

151 /* finally, awake any writers and return */

152 wake_up_interruptible(&dev->outq);

153 PDEBUG("\"%s\" did read %li bytes\n",current->comm, (long)count);

154 return count;

155}

126行,当读指针dev->rp与写指针 dev->wp相等时,说明缓冲区中没有数据可读。这种情况下,要根据用户指定的标志位决定是进入休眠等待还是直接返回。

127行,进入休眠之前,要将已经获得的锁释放掉。

128 - 129行,如果用户空间的read函数指定了O_NONBLOCK标志,则不阻塞进程,直接退出。

131 - 132行,调用wait_event_interruptible函数休眠在dev->inq等待队列上,注意被唤醒的条件是(dev->rp != dev->wp),即缓冲区中有数据可读。由这一句也可以看出,用户空间的read进程对应的等待队列是dev->inq。另外,wait_event_interruptible的返回值有两种,返回0表示缓冲区中有数据可读,被唤醒。返回非0值表示休眠被某个信号中断,这时,132行直接返回-ERESTARTSYS。

134 - 135行,休眠被唤醒后,首先重新获得互斥锁,然后返回到while循环开始处判断是否有数据可读。

138 - 141行,确认有数据可读后,退出while循环,根据读写指针的位置位及count值,决定要读多少数据。

142行,读数据到用户空间。

146 - 148行,更新读指针的位置。

149行,释放互斥锁。

152行,读取结束后,就有缓冲区空间可以进行写操作了。所以唤醒所有休眠在dev->outq上的写进程,唤醒函数是wake_up_interruptible。

154行,返回读取的字节数。

注意,scull_p_read在142行调用copy_to_user将数据拷贝到用户空间过程中,可能会休眠。此时,虽然进程拥有互斥锁,但这种情况下拥有互斥锁休眠是可以接受的,因为我们知道内核会把数据复制到用户空间并唤醒我们,同时不会试图锁上同一个信号量。

下面我们来看scullpipe的write函数,即scull_p_write函数,其代码如下所示:

[cpp] view
plaincopy

188static ssize_t scull_p_write(struct file *filp, const char __user *buf, size_t count,

189 loff_t *f_pos)

190{

191 struct scull_pipe *dev = filp->private_data;

192 int result;

193

194 if (down_interruptible(&dev->sem))

195 return -ERESTARTSYS;

196

197 /* Make sure there's space to write */

198 result = scull_getwritespace(dev, filp);

199 if (result)

200 return result; /* scull_getwritespace called up(&dev->sem) */

201

202 /* ok, space is there, accept something */

203 count = min(count, (size_t)spacefree(dev));

204 if (dev->wp >= dev->rp)

205 count = min(count, (size_t)(dev->end - dev->wp)); /* to end-of-buf */

206 else /* the write pointer has wrapped, fill up to rp-1 */

207 count = min(count, (size_t)(dev->rp - dev->wp - 1));

208 PDEBUG("Going to accept %li bytes to %p from %p\n", (long)count, dev->wp, buf);

209 if (copy_from_user(dev->wp, buf, count)) {

210 up (&dev->sem);

211 return -EFAULT;

212 }

213 dev->wp += count;

214 if (dev->wp == dev->end)

215 dev->wp = dev->buffer; /* wrapped */

216 up(&dev->sem);

217

218 /* finally, awake any reader */

219 wake_up_interruptible(&dev->inq); /* blocked in read() and select() */

220

221 /* and signal asynchronous readers, explained late in chapter 5 */

222 if (dev->async_queue)

223 kill_fasync(&dev->async_queue, SIGIO, POLL_IN);

224 PDEBUG("\"%s\" did write %li bytes\n",current->comm, (long)count);

225 return count;

226}

与scull_p_read采用简单休眠不同,scull_p_write采用了手工休眠的方式。

194行,获得互斥锁。

198行,调用scull_getwritespace函数。下面看这个函数的定义:

[cpp] view
plaincopy

157/* Wait for space for writing; caller must hold device semaphore. On

158 * error the semaphore will be released before returning. */

159static int scull_getwritespace(struct scull_pipe *dev, struct file *filp)

160{

161 while (spacefree(dev) == 0) { /* full */

162 DEFINE_WAIT(wait);

163

164 up(&dev->sem);

165 if (filp->f_flags & O_NONBLOCK)

166 return -EAGAIN;

167 PDEBUG("\"%s\" writing: going to sleep\n",current->comm);

168 prepare_to_wait(&dev->outq, &wait, TASK_INTERRUPTIBLE);

169 if (spacefree(dev) == 0)

170 schedule();

171 finish_wait(&dev->outq, &wait);

172 if (signal_pending(current))

173 return -ERESTARTSYS; /* signal: tell the fs layer to handle it */

174 if (down_interruptible(&dev->sem))

175 return -ERESTARTSYS;

176 }

177 return 0;

178}

161行,使用spacefree函数判断是否有空间可写,该函数定义如下:

[cpp] view
plaincopy

180/* How much space is free? */

181static int spacefree(struct scull_pipe *dev)

182{

183 if (dev->rp == dev->wp)

184 return dev->buffersize - 1;

185 return ((dev->rp + dev->buffersize - dev->wp) % dev->buffersize) - 1;

186}

如果spacefree函数返回值为0,说明现在scullpipe设备缓冲区已满,没有空间可以写入。写进程需要根据调用者设置的标志位决定是否休眠等待。

162行,使用DEFINE_WAIT函数创建并初始化一个等待队列入口。

164行,在进入休眠前,释放互斥锁。

165 - 166行,如果调用者设置了O_NONBLOCK标志位,则直接退出。

168行,调用prepare_to_wait(&dev->outq, &wait, TASK_INTERRUPTIBLE)函数,将等待队列入口wait插入到dev->outq等待队列中,并设置进程状态为TASK_INTERRUPTIBLE。

169 - 170行,再次调用spacefree(dev)判断是否有空间可写,如果还是确认没有空间,则调用170行的schedule()进行进程调度。这里需要注意的是,如果在169行的if判断和170行的调度之间,有了可写入空间,那么会发生什么情况?这种情况下没有任何问题,只要进程已经把自己放到了等待队列中并修改了进程状态,就不会有任何问题,schedule会把进程状态重新设置为TASK_RUNNING,并返回。

171行,调用finish_wait执行清理操作。

172 - 173行,判断是否是因为收到信号而醒来的,如果是返回-ERESTARTSYS。

174 - 176行,重新申请互斥锁,再次回到while开始处确认是否有空间写入。

177行,如果有空间可写入,返回0。

下面执行流程回到scull_p_write函数的198行,如果有可写入的空间,scull_getwritespace返回值为0,如果是因为收到信号而醒来的,scull_getwritespace返回非0值。

202 - 207行,计算可写入数据的大小。

209 - 212行,将数据从用户空间拷贝到内核空间。

213 - 215行,调整写入指针的位置。

216行,释放互斥锁。

219行,唤醒所有在读取等待队列上休眠的进程。

222 - 223行,是异步通知相关的操作,这里不讨论,后面在相关文章中再分析。

225行,返回写入的字节数。

scullpipe的测试需要打开两个终端窗口,安装好scullpipe后,在一个终端窗口上运行cat /dev/scullpipe0,因为缓冲区中没有可读内容,所以cat操作会休眠等待,如下图所示:



然后在另外一个终端窗口上执行echo “hello” > /dev/scullpipe0,将hello写入scullpipe0,可以看到cat进程会被唤醒,读出并打印”hello”。再次执行ls
work/ >/dev/scullpipe0,会把work目录下的内容写入/dev/scullpipe0,同时,cat会被再次唤醒并打印输出work目录下的内容。



二、scullpipe源码中的一个问题

LDD3提供的scullpipe的代码有一个问题,不知道大家发现了没有,列出大家讨论一下。

首先看下图所示的执行步骤出现的问题:



上图中,先向/dev/scullpipe写入字符串”hello”,然后用cat命令读/dev/scullpipe,正常情况下应该能读出”hello”才对,但是从上图我们可以看出,cat被阻塞住了。原因是什么呢?我们先看scull_p_open函数的实现:

[cpp] view
plaincopy

61static int scull_p_open(struct inode *inode, struct file *filp)

62{

63 struct scull_pipe *dev;

64

65 dev = container_of(inode->i_cdev, struct scull_pipe, cdev);

66 filp->private_data = dev;

67

68 if (down_interruptible(&dev->sem))

69 return -ERESTARTSYS;

70 if (!dev->buffer) {

71 /* allocate the buffer */

72 dev->buffer = kmalloc(scull_p_buffer, GFP_KERNEL);

73 if (!dev->buffer) {

74 up(&dev->sem);

75 return -ENOMEM;

76 }

77 }

78 dev->buffersize = scull_p_buffer;

79 dev->end = dev->buffer + dev->buffersize;

80 dev->rp = dev->wp = dev->buffer; /* rd and wr from the beginning */

81

82 /* use f_mode,not f_flags: it's cleaner (fs/open.c tells why) */

83 if (filp->f_mode & FMODE_READ)

84 dev->nreaders++;

85 if (filp->f_mode & FMODE_WRITE)

86 dev->nwriters++;

87 up(&dev->sem);

88

89 return nonseekable_open(inode, filp);

90}

70 - 80行,如果dev->buffer为NULL,则给dev->buffer分配内存空间,然后设置缓冲区大小和结束位置,以及读写指针位置。

关键在第80行,这行将读写指针位置dev->rp和dev->wp设置为同时指向缓冲区的开始处,而在scullpipe设备中,dev->rp等于dev->wp表明缓冲区中没有数据。

所以,问题的原因就找到了,我们向scullpipe中写入了字符串,但是执行cat /dev/scullpipe时,cat命令会调用open打开/dev/scullpipe文件,这时,就会将dev->rp和dev->wp设置为指向缓冲区的开始处,然后,cat读/dev/scullpipe的时候,就会认为该文件中没有数据可读,就会阻塞住。直到下次有数据写入时,cat才能被唤醒并读出数据。

另外,这个scull_p_open函数的逻辑有问题。在open时给dev->buffer分配内存,就是说,每次打开scullpipe设备,都会为它分配内存,这本来就不合理,尽管70行加了一个判断,只有当dev->buffer为空时,才分配内存。合理的位置应该是在初始化函数中完成这些操作。所以,我修改了scull_p_open函数和scull_p_setup_cdev函数,把对缓冲区的初始化工作放在了scull_p_setup_cdev函数中,该函数由初始化函数scull_p_init调用。列出这两个函数如下:

[cpp] view
plaincopy

static int scull_p_open(struct inode *inode, struct file *filp)

{

struct scull_pipe *dev;

dev = container_of(inode->i_cdev, struct scull_pipe, cdev);

filp->private_data = dev;

if (down_interruptible(&dev->sem))

return -ERESTARTSYS;

/* use f_mode,not f_flags: it's cleaner (fs/open.c tells why) */

if (filp->f_mode & FMODE_READ)

dev->nreaders++;

if (filp->f_mode & FMODE_WRITE)

dev->nwriters++;

up(&dev->sem);

return nonseekable_open(inode, filp);

}

[cpp] view
plaincopy

/*

* Set up a cdev entry.

*/

static void scull_p_setup_cdev(struct scull_pipe *dev, int index)

{

int err, devno;

/* allocate the buffer */

dev->buffer = kmalloc(scull_p_buffer, GFP_KERNEL);

if (!dev->buffer)

return -ENOMEM;

dev->buffersize = scull_p_buffer;

dev->end = dev->buffer + dev->buffersize;

dev->rp = dev->wp = dev->buffer; /* rd and wr from the beginning */

devno = scull_p_devno + index;

cdev_init(&dev->cdev, &scull_pipe_fops);

dev->cdev.owner = THIS_MODULE;

err = cdev_add (&dev->cdev, devno, 1);

/* Fail gracefully if need be */

if (err)

printk(KERN_NOTICE "Error %d adding scullpipe%d", err, index);

}

由于把分配缓冲区的工作从scull_p_open函数移到了模块初始化函数中,连锁反应,还有一个函数需要修改,那就是scull_p_release,修改后的代码如下:

[cpp] view
plaincopy

static int scull_p_release(struct inode *inode, struct file *filp)

{

struct scull_pipe *dev = filp->private_data;

/* remove this filp from the asynchronously notified filp's */

scull_p_fasync(-1, filp, 0);

down(&dev->sem);

if (filp->f_mode & FMODE_READ)

dev->nreaders--;

if (filp->f_mode & FMODE_WRITE)

dev->nwriters--;

// if (dev->nreaders + dev->nwriters == 0) {

// kfree(dev->buffer);

// dev->buffer = NULL; /* the other fields are not checked on open */

// }

up(&dev->sem);

return 0;

}

因为原来在scull_p_open中分配缓冲区,所以在scull_p_release中释放缓冲区,现在scull_p_open中不再分配缓冲区了,所以scull_p_release也就不再释放缓冲区了。所以必须屏蔽掉scull_p_release函数中释放缓冲区的4条语句。

用上面的列出的scull_p_open、scull_p_setup_cdev、scull_p_release函数替换原来的函数,上面的问题就解决了,执行结果如下图所示:

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  signal buffer up user null 终端