您的位置:首页 > 其它

【讨论】简析文件操作

2009-10-29 18:04 204 查看
文件操作在程序设计中使用较多,常见操作有打开、读、写、关闭等。本文将讨论文件操作涉及的相关内容,以Linux平台为例。

1. 结构
文件操作是应用程序访问文件系统的过程。下面简要讨论文件系统的结构,如图1所示。



图1 文件系统结构示意图

应用程序通过文件描述符int fd访问文件,执行文件操作(如系统调用read)时,先从描述符表中找到文件对象,然后再找到索引节点(在open时,从磁盘的索引节点表中读取初始数据),索引节点记录了文件信息(时间、大小等)和文件数据对应的逻辑块号表
由块号表和文件对象中的偏移量,可得到目标逻辑块号,接着访问磁盘缓冲池(如图2所示),查找该块号对应的缓冲块。若没找到,则要通过磁盘驱动读取磁盘数据到空闲缓冲块,最后将缓冲块的相关数据拷贝到用户空间。
文件系统将数据块部分以逻辑块为单位进行组织(逻辑空间)逻辑块的大小是扇区大小的整数倍,可为512、1024、2048、4096字节等。访问磁盘时,磁盘驱动将块号转换为柱面号、磁道号、扇区号等参数,来访问磁盘。



图2 磁盘缓冲示意图

以如下代码为例,
//读取文件"/tmp/a.dat"偏移100字节处的20字节数据到buf中。
void fun1()
{
char buf[20];
int fd = open( "/tmp/a.dat", O_RDONLY );
lseek( fd, 100, SEEK_SET );
read( fd, buf, sizeof(buf));
close(fd);
}
1—open:从文件系统根目录”/”开始,逐级查找”/tmp/a.dat”的索引节点号。因为目录文件与普通文件不同,保存的是一组子文件名和索引节点号,如图3所示,因此根据文件名,很方便查找到对应的索引节点号。
然后根据节点号,在索引节点缓冲池中查找目标索引节点,若没找到,则将节点数据从索引节点表读到内存(索引节点对象),再新建文件对象,来保存该索引节点指针,最后找到描述符表中的空表项,记录文件对象指针,并返回对应的数组下标值。
2—lseek:根据描述符fd,找到文件对象,调整“文件偏移量”字段的值。
3—read:先找到描述符对应的文件对象和索引节点,然后查找逻辑块号,再从磁盘缓冲池中查找目标缓冲块,并拷贝数据到用户空间。
4—close:释放fd对应的文件对象和索引节点,清空fd对应的描述符表项。



图3 目录文件结构示意图

2. 共享

文件系统中的文件对象和索引节点对象,可以通过引用计数在同一进程或不同进程间共享。
以如下代码为例,对应的数据结构如图4所示。
int fd[6];
void main()
{
fd[0] = open( "a1.dat", O_RDONLY );
fd[1] = open( "a1.dat", O_RDONLY );
fd[2] = dup( fd[1] );
CreateThread( fun ); //创建线程
}
void fun()
{
fd[3] = open( "a1.dat", O_RDONLY );
fd[4] = open( "a2.dat", O_RDONLY );
fd[5] = dup(fd[0]);
}
//调用fork创建子进程时,父进程的描述符表拷贝到子进程,对应文件对象的引用计数全部加1,相当于执行了dup操作。



图4 文件系统相关数据结构示意图

3. 模拟
接下来给出文件操作(open、lseek、read、write、close等)的简化模拟示意程序。

类型定义:

#define SIZE_BLK (1024)			//逻辑块大小
#define SIZE_DIRECT (SIZE_BLK*8)	//假设直接映射区大小为8个逻辑块
#define LEN_FDS (512)			//描述符表长度
#define LEN_BLKNOS (12)			//逻辑块号表长度
#define LEN_HASHS (37)			//哈希表大小

#define OFFSETOF( TYPE, MEMBER )  ( (unsigned int)( &( (TYPE*)NULL )->MEMBER ) )
//从成员指针得到所在对象指针
#define GetEntryFromMember(ptr, type, member) /
( (type *)( (char *)(ptr) - OFFSETOF(type, member) ) )

#define GetDskHashItem(ptr)  GetEntryFromMember( (ptr), disk, node[0] )	//返回哈希表节点所在的磁盘缓冲块
#define GetDskLruItem(ptr)   GetEntryFromMember( (ptr), disk, node[1] )	//返回LRU表节点所在的磁盘缓冲块

struct list		//双向链表节点
{
list *prev;
list *next;
};

struct inode	//索引节点
{
int cnt;	//引用计数
//时间信息等
int sz;	//文件长度
int blkNos[LEN_BLKNOS];	//逻辑块号表
//...........
};

struct file		//文件对象
{
int cnt;	//引用计数
int flag;	//文件属性(可读可写等)
int offset;	//当前偏移
inode *i;
//...........
};

struct process	//进程结构
{
file *fds[LEN_FDS];	//进程描述符表
//...........
};

struct disk		//磁盘缓冲块
{
list node[2];	//0为hash表节点,1为lru表节点
int no;		//逻辑块号
int flag;	//属性(脏标记等)
void *buf;	//缓冲数据
//...........
};

struct diskPool	//磁盘缓冲池
{
list hashs[LEN_HASHS];
list lru;
//...........
};
diskPool pool;


函数定义:

//////////////////////////////全局函数///////////////////////////////////////////
void * memalloc( unsigned int sz )
{
//申请sz个字节
void * p = NULL;
//...................
return p;
}
void memfree( void *p )
{
//释放p
}
void memcopy( void *d, const void *s, unsigned int sz )
{
//从s拷贝sz个字节到d
}

process * current()
{
//返回当前进程结构指针
}

int name2ino( const char *name )
{
//将文件名转换为索引节点号(从根目录开始,逐级查找目录项)
}

inode * getINode( int n )
{
//先在索引节点缓冲池查找。
//若找到,则增加引用计数,并返回。
//若没找到,就申请一个新节点,并从磁盘读入索引节点内容,然后返回。
}

void delINode( inode *i )
{
if ( i && --i->cnt<= 0 )
{
//将索引节点数据写回磁盘
//将索引节点加入缓冲池的空闲表中
}
}

file * newFile( int flag, inode * i )
{
//新建文件对象
file * f = (file *)memalloc( sizeof(file) );
f->cnt = 1;
f->offset = 0;
f->flag = flag;
f->i = i;
return f;
}

void delFile( file * f )
{
//释放文件对象
if ( f && --f->cnt<= 0 )
{
delINode( f->i );	//释放索引节点
memfree( f );
}
}

////////////////////////////disk成员函数/////////////////////////////
void diskRead( disk* dsk, int no )
{
//将块号no转换为柱面号、磁道号、扇区号等参数,调用磁盘驱动。
//然后进程被阻塞等待磁盘操作完成。
}
//////////////////////////inode成员函数///////////////////////////////
int calBlkNo( inode *i, int off )
{
//由文件偏移量得到逻辑块号
int no;
if ( off < SIZE_DIRECT )	//直接区
{
no = i->blkNos[off/SIZE_BLK];
}
else
{
//间接映射区要先找到间接块,然后在间接块上得到目标逻辑块号
}
return no;
}
///////////////////////////diskPool成员函数/////////////////////////////
disk * findDisk( diskPool *p, int no )
{
//在哈希表中查找磁盘缓冲块(根据逻辑块号)
disk * dsk = NULL;
list *head = &p->hashs[no%LEN_HASHS];	//得到哈希表头
list *lst = head->next;
while ( lst != head )
{
disk * d = GetDskHashItem( lst );
if ( d->no == no )
{
dsk = d;
//调整dsk在LRU表中的位置(放到最前端)
break;
}
lst = lst->next;
}
return dsk;
}

disk * allocDisk( diskPool *p )
{
//在LRU表中取下最近最少使用的缓冲块。
//如果是脏缓冲块,要先将数据写入磁盘。
//.................
}

disk * getDisk( diskPool *p, int no )
{
//返回块号为no的磁盘缓冲块
disk * dsk = findDisk( p, no );
if ( !dsk )
{
dsk = allocDisk( p );
if ( dsk )
{
dsk->no = no;
diskRead( dsk, no );
//再将d加入哈希表和LRU表中
}
}
return dsk;
}
////////////////////////////////系统调用//////////////////////////////////////
int open( const char*name, int flag )
{
int fd = -1;
int ino = name2ino( name );		//转换成索引节点号
if ( ino >= 0 )
{
inode * i = getINode( ino );	//得到索引节点
if ( i )
{
file * f = newFile( flag, i );
process *p = current();
for ( int n = 0; n < LEN_FDS; ++n )
{
if ( !p->fds
)
{
p->fds
= f;
fd = n;			//得到描述符
break;
}
}
}
}
return fd;
}

int close( int fd )
{
int ret = 0;
if ( fd >= 0 )
{
process *p = current();
file *f = p->fds[fd];
if ( f )
{
delFile( f );	//释放文件对象
ret = 1;
}
}
return ret;
}

int lseek( int fd, int step, int flag )
{
int pos = -1;
if ( fd >= 0 )
{
process *p = current();		//当前进程结构
file *f = p->fds[fd];		//fd对应的文件对象
if ( f )
{
inode *i = f->i;
if ( 0 <= flag && flag <= 2 )
{
if ( flag == 0 )
{
pos = step;
}
else if ( flag == 1 )
{
pos = f->offset + step;
}
else if ( flag == 2 )
{
pos = i->sz - step;
}

if ( pos > i->sz )
{
pos = i->sz;
}
if ( pos < 0 )
{
pos = 0;
}
f->offset = pos;
}

}
}
return pos;
}

int read( int fd, void *buf, unsigned int sz )
{
if ( fd < 0 )		//无效描述符
{
return -1;
}
int nRead = 0;
process *p = current();		//当前进程结构
file *f = p->fds[fd];		//fd对应的文件对象
if ( !f )			//无效文件对象
{
return -1;
}
while ( sz > 0 )
{
int blkNo = calBlkNo( f->i, f->offset );
if ( blkNo == 0 )	//0不是有效逻辑块
{
break;
}
disk * dsk = getDisk( &pool, blkNo );	//获取磁盘缓冲块
if ( !dsk )
{
break;
}

int nCpy = SIZE_BLK - f->offset%SIZE_BLK;
if ( nCpy > sz )
{
nCpy = sz;
}
int left = f->i->sz - f->offset;		//文件剩余字节数
if ( nCpy > left )
{
nCpy = left;
}
if ( nCpy <= 0 )
{
break;
}
memcopy( (char*)buf+nRead, (char*)dsk->buf+f->offset%SIZE_BLK, nCpy );	//从磁盘缓冲拷贝到用户缓冲
nRead += nCpy;
sz -= nCpy;
f->offset += nCpy;
}
return nRead;
}

int write( int fd, void *buf, unsigned int sz )
{
if ( fd < 0 )	//无效描述符
{
return -1;
}
int nWrite = 0;
process *p = current();		//当前进程结构
file *f = p->fds[fd];		//fd对应的文件对象
if ( !f )		//无效文件对象
{
return -1;
}
while ( sz > 0 )
{
int blkNo = calBlkNo( f->i, f->offset );
if ( blkNo == 0 )	//0不是有效逻辑块
{
break;
}
disk * dsk = getDisk( &pool, blkNo );	//获取磁盘缓冲块
if ( !dsk )
{
break;
}

int nCpy = SIZE_BLK - f->offset%SIZE_BLK;
if ( nCpy > sz )
{
nCpy = sz;
}
int left = f->i->sz - f->offset;		//文件剩余字节数
if ( nCpy > left )
{
nCpy = left;
}
if ( nCpy <= 0 )
{
break;
}
memcopy( (char*)dsk->buf+f->offset%SIZE_BLK, (char*)buf+nWrite, nCpy );	//从用户缓冲拷贝到磁盘缓冲
nWrite += nCpy;
sz -= nCpy;
f->offset += nCpy;
}
return nWrite;
}


4. 缓冲

除了以上讨论的,直接通过系统调用来访问文件,C标准库还提供了缓冲访问方式。
以相同功能的代码为例,
void fun2()
{
char buf[20];
FILE* fp = fopen( "/tmp/a.dat", “r” );
fseek(fp, 100, SEEK_SET );
fread(buf, 1, sizeof(buf), fp);
fclose(fp);
}
fopen、fread等库函数,使得应用程序与文件系统之间多了个中间层,如图5所示,库函数操作对象是FILE* fp,其中记录了对应的文件描述符。FILE结构的定义如下,
struct _iobuf {
char *_ptr; //缓冲区当前指针
int _cnt; //缓冲区中的字符数
char *_base; //流缓冲区
int _flag;
int _file; //文件描述符
int _charbuf;
int _bufsiz; //缓冲区大小
char *_tmpfname;
};
typedef struct _iobuf FILE;



图5 缓冲访问方式示意图

在缓冲方式下,应用程序跟流缓冲区交互,后者再与磁盘缓冲块交互,如图6所示。
流缓冲区的一个作用是,减少进程陷入内核的次数,从而节省了上下文切换的相关开销。
同样道理,磁盘缓冲块的作用是,减少访问磁盘设备的次数,从而节省访问磁盘的相关开销。



图6 文件操作交互示意图
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: