Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1716222
  • 博文数量: 177
  • 博客积分: 9416
  • 博客等级: 中将
  • 技术积分: 2513
  • 用 户 组: 普通用户
  • 注册时间: 2006-01-06 16:08
文章分类

全部博文(177)

文章存档

2013年(4)

2012年(13)

2011年(9)

2010年(71)

2009年(12)

2008年(11)

2007年(32)

2006年(25)

分类:

2007-12-22 21:39:59

有些文件操作需要打开文件:比如操作文件的数据;而另外一些操作则不用打开文件,如读取文件属性。前一篇中讲到了如何打开关闭文件,这里我们便来看看如何操作文件中的数据。
 
读取文件数据
read()系统调用的原型为:
size_t read(int fd, void * buffer, size_t count);

从fd表示的文件中读取count个字节存放到buffer中,并返回所读出的字节数。这其中涉及到一个问题就是把数据从内核空间拷贝到用户空间,copy_to_user()就是用来做这件事的。下面列出了read()系统调用的实现伪代码。
Size Read(Filedescriptor fd, DataBuffer *buffer, Size count)
{
    FileObj * aFileObj = fileTable[fd];
    检查文件的可访问性;
    为用户地址、字节数、I/O用户设置u area中的参数;
    INode * anINode = aFileObj->inode;
    Lock(anINode);
    根据aFileObj中的offset设置u area中的offset;
    while (没有得到足够的数据)
    {
        将offset转换成磁盘块(bmap());
        计算磁盘块中的offset以及要读取得字节数;
        if (要读取的字节数为0)
        {
            break; // EOF
        }
        读取数据;
        copy_to_user(buffer);
        更新u area;
    }
    Unlock(anINode);
    更新aFileObj->offest;
    return 读取到的字节数;
}
 
在u area中设置的I/O参数包括:
1、mode:读还是写;
2、count:要操作的字节数;
3、offset:文件中的字节偏移量;
4、address:指示该地址是在用户空间还是内核空间;
 
在while循环中,内核将尝试尽量满足用户要读取的字节数。三种情况下会跳出该循环:1、读取到用户请求的字节数;2、到达文件尾;3、从磁盘读取数据时出错。
 
还有一个问题就是同步/异步读取的问题。在调用open()系统调用时可以指定non_block标志,这样的话所有的读写该文件操作都将是一个异步的:read()/write()立刻返回,但进程所请求的数据没有完成操作;如果没有指定该标志,那么就是同步读/写:read()/write()阻塞到数据读/写的完成或者出错才返回。
 
另一个问题就是数据读取。每次读取数据,read()都将调用bread()或者breada(),这两者都是每次读取一个磁盘块,那么当请求的字节数与offset之和正好在一个block之内时,read()操作读取整块磁盘块,然后返回给用户所需要的字节。当read()的count参数随机选取的话,那么很有可能出现为了读取一段字节数小于或者等于磁盘块大小的数据时,需要读取两个磁盘块——也就是说,对数据的读取可能会跨磁盘块。考虑下面的情况:第一个read()从0字节开始读取20字节,第二个read()接着读取1024个字节,那么,第二个read()操作将会读取两个磁盘块:第一个磁盘块的1004个字节和第二个磁盘块的20个字节。因此,在调用read()时,count参数最好是磁盘块大小的整数倍。标准IO的一个目的就是为了对用户隐藏这个事实,使得用户不用知道磁盘块的大小也能写出相当有效率的IO操作。
 
当文件中有“空洞”时——例如从某个文件的第n个字节开始才有数据,前n个字节就是“空洞”,调用read()读取“空洞”中的数据将导致返回给用户的buffer所包含的数据全部为0,相当于对buffer调用了bzero()。(回忆一下inode如何存储文件的数据:direct数据块、一级、二级、三级间接数据块。“空洞”所对应的数据块号为0。此时read()读取的块号是0,相当于不读取数据,这与没有数据可读是不同的。)
 
由于文件可以被多个进程同时打开,那么就存在一种情况:进程A读取某段数据时进入睡眠,而进程B往进程A读取的数据区里写数据。进程B的写操作先于进程A的读操作完成,那么A将读取到不一致的数据。因此需要在读取数据之前锁住inode,读完之后解锁。——当然,写操作也要相应的执行加/解锁操作。
 
上述同步问题又带来另一个问题:那就是当多个进程同时操作一个文件的内容时,每个进程的操作结果将会依赖于执行的顺序。——自己模拟一下,就知道怎么回事了。既然会有这样的问题,那为什么不从打开到关闭一个文件的期间都锁住inode呢?其实这样带来的问题会更严重。因为现有的方式虽然会导致数据不一致性,但不会破坏内核的数据结构。而从打开到关闭文件期间都锁住inode带来的问题就是:如果一个恶意的用户登录之后,打开/etc/passwd,那么所有的用户都不能登录了。为了解决这个问题,引入了文件和记录锁,稍后会介绍。
 
每次打开一个文件就对应一个fd,也就是说对应一个fileObj,那么,对同一个文件打开多次,对每次打开的fd的操作都是互不相干的——不是数据上的不相干,而是偏移量上的不相干。——如果要共享偏移量,那就不能打开两次,而是应该使用dup()(后面将介绍)。
 
下面是Linux 0.99.15的实现:
asmlinkage int sys_read(unsigned int fd,char * buf,unsigned int count)
{
 int error;
 struct file * file; // File Object
 struct inode * inode;
 if (fd>=NR_OPEN || !(file=current->filp[fd]) || !(inode=file->f_inode))
  return -EBADF; // 检查fd的合法性
 if (!(file->f_mode & 1))
  return -EBADF;
 if (!file->f_op || !file->f_op->read)
  return -EINVAL; // 检查read()操作的合法性
 if (!count)
  return 0;
 error = verify_area(VERIFY_WRITE,buf,count); // 不明白……=_=!
 if (error)
  return error;
 return file->f_op->read(inode,file,buf,count); // 真正的read操作
}
 
注意,真正执行read操作的是“return file->f_op->read(inode,file,buf,count); ”。一般来说,file->f_op->read()实在设备驱动里实现的,下面是块设备的read()——因为磁盘是块设备(仅列出主要逻辑):
// Code snippet from Linux 0.99.15
int block_read(struct inode * inode, struct file * filp, char * buf, int count)
{
 ……
 offset = filp->f_pos; // 获取起始位置
 ……
 read = 0; // 读取到的字节数
……
 if (filp->f_reada) { // 预读
  blocks += read_ahead[MAJOR(dev)] / (blocksize >> 9);
  if (block + blocks > size)
   blocks = size - block;
 }
 ……
 do { // 为了最大限度地利用buffer cache
  bhrequest = 0;
  uptodate = 1;
  while (blocks) {
   --blocks;
   *bhb = getblk(dev, block++, blocksize);
   if (*bhb && !(*bhb)->b_uptodate) {
    uptodate = 0;
    bhreq[bhrequest++] = *bhb;
   }
   if (++bhb == &buflist[NBUF])
    bhb = buflist;
   /* If the block we have on hand is uptodate, go ahead
      and complete processing. */
   if (uptodate)
    break;
   if (bhb == bhe)
    break;
  }
  ……
  do { // 主循环
   ……
   if (left < blocksize - offset)
    chars = left;
   else
    chars = blocksize - offset;
   filp->f_pos += chars;
   left -= chars;
   read += chars;
   if (*bhe) {
    memcpy_tofs(buf,offset+(*bhe)->b_data,chars);
    brelse(*bhe);
    buf += chars;
   } else {
    while (chars-->0) // 一次只拷贝一个字节到用户空间。效率有点低……
     put_fs_byte(0,buf++);
   }
   offset = 0;
   if (++bhe == &buflist[NBUF])
    bhe = buflist;
  } while (left > 0 && bhe != bhb && (!*bhe || !(*bhe)->b_lock));
 } while (left > 0);
……
 if (!read)
  return -EIO;
 filp->f_reada = 1;
 return read;
}
写数据
原型:size_t write(int fd, void * buffer, size_t count);向fd指定的文件写入count个字节,数据来源为buffer。
 
写数据操作与读数据操作的过程基本相同,只不过是从用户空间把数据拷贝到内核空间并更新到磁盘上(立即写或者延迟写)。有时为了写出一个文件“空洞”,内核将为文件分配必要的数据块,从正确的位置开始写数据。写完之后更新文件的大小——如果文件大小被更新了。
 
文件和记录锁
最早的UNIX系统并不支持文件和记录锁,因为当时UNIX并不面向数据库系统。后来为了吸引商业用户,添加了这些功能,主要用fcntl来实现。关于fcntl的使用,在“Advanced Programming in UNIX Environment”中说得比较清楚了,在此就不重复。至于如何加锁,可以参考“UNIX内核(1):加锁解锁——等待事件及唤醒”。
 
调整I/O的位置
进程可以用lseek()系统调用来定位I/O操作,允许对文件数据的随机访问。原型如下:off_t lseek(int fd, off_t offset, int whence);其含义为:对于打开的文件描述符fd,相对于whence处,第offset个字节作为下次I/O的开始位置。lseek()的操作很简单,仅仅是更新file object中的offset。
 
参考:
The Design of The UNIX Operation System, by Maurice J. Bach
Linux Kernel Source Code v0.99.15, by Linus Torvalds
Linux Kernel Source Code v2.6.22, by Linus Torvalds and Linux community.
Understanding The Linux Kernel, 3rd edition, by Daniel P. Bovet, Marco Cesati
 
Copyleft (C) 2007 raof01. 本文可以用于除商业用途外的所有用途。若要用于商业用途,请与作者联系。
阅读(2407) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~