转:linux文件读写
读写文件,是作为一个操作系统所提供的最基本接口之一。
我们就从写文件过程:open,write,close这几个接口来说起,描述写文件的那些事儿。
平时,我们做应用程序的时候,常常用到读写文件的函数接口,就拿写文件来说,我们用C/C++编写时,用到了以下的函数接口:
1> FILE* fopen(const char* restrict filename,const char* restrict mode);
2> size_t fwrite(const void* restrict buffer,size_t size,size_t n,FILE * restrict fp);
3> int fclose(FILE * fp) ;
以上这几个函数接口大家都比较熟悉,如果按照这个来分析似乎更加明了。然而,上面的这些接口已经是现代版本的接口,其实现依赖于现在的成熟系统,分析现行系统的庞大代码我还嫩了点,所以就拿过去版本的linux系统和一些原始接口进行分析吧。(其实大家都知道,现行操作系统内核的代码量已经不是一个人一辈子能看完的了,我们主要是借鉴linux的系统思想,去作我们自己的嵌入式操作系统)
老版本的接口是这个样子的:
1> int open(const char* filename,int flag,...) ;
2> int write(int fildes,const char* buf,off_t count) ;
3> int close(int fildes) ;
这几个接口的声明在头文件中,实现在系统的LIB库文件中,所以使用的时候,我们只需要包含几个相应的头文件,然后使用接口,在编译的时候,编译器把LIB库文件中的二进制实现链接进去,这样就行了。
当然,仅仅是使用不是本文的目的,我们是要探究的是这个使用的背后是什么,操作系统为我们做了什么。
首先,库文件中的open是怎么实现的呢?
int open(const char * filename,int flag,...){
register int res ;
va_list arg ;
va_start(arg,flag) ;
__asm__("int $0x80"
:"=a"(res)
:""(__NR_open),"b"(filename),"c"(flag),"d"(va_arg(arg,int))
);
if(res>=0)
return res ;
errno = -res ;
return -1 ;
}
库文件中的open函数封装了汇编代码“调用系统”,这个系统调用的返回值通过eax寄存器传递给了res,系统调用的输入参数分别存放在ebx,ecx,edx寄存器中。
系统调用是一个中断,是由汇编语言int 中断号促发,所以好多教材上称其为软中断或软件中断。
系统中断中断发生,cpu停止当前任务的处理,把用户态的五个关键信息保存在内核态栈中,分别是:eflag,ss,esp,eip和cs寄存器,他们记录着进程用户态的关键信息(恢复用户态运行时用到),把他们压栈到内核栈中。当然,内核栈地址在进程结构信息中早有记录,上边的五个寄存器的用户态信息保存与赋予内核态信息这个过程是由CPU自动完成的,只要我们在前边的任务数据结构中设置好了就行。
任务运行在内核态中,这里有一切系统的代码(包括各种中断处理程序和文件系统以及各种设备的驱动程序)。
呃。。。写博客好费时间啊,不过也是个再次详细学习的过程,值了,毕竟能说出来才说明掌握的透彻,不像现在,边写边翻资料。。。吃饭去了,回来再写。。。
呵呵,再次拿起来这个帖子,都过去一周了。接着写,总比玩游戏强。
依据中断向量表的设置,程序运行到软中断处理程序的入口处(此时,用户态的关键信息eflag,ss,esp,eip和cs都已经保存到内核栈中了),在这里(是用汇编写的)手工压栈保存用户态的其他信息,注意,这里的保存,在中断退出时,还要手工退栈恢复原:
push %ds
push %es
push %fs
pushl %eax
pushl %edx
pushl %ecx
pushl %ebx
movl $0x10,%edx #0x10即0001,0000 (绿色两位是请求特权级,红色一位是GDT(0)/LDT(1),蓝色四位是第几项),这么说,edx寄存器中放的是GDT表的第0x001,0+1项(即第3项,0x0000是第一项),是系统数据段的选择符。
#把系统数据段的选择符放入ds和es寄存器中,则用到的数据都将是系统数据段(ds指示)中的数据。
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx #0x17即0001,0111 (绿色两位是请求特权级,红色一位是GDT(0)/LDT(1),蓝色四位是第几项),这么说,edx寄存器中存放的是LDT表的第0x001,0+1项(即第3项,0x0000是第一项),是用户态数据段的选择符。
#把用户态数据段的选择符放入fs寄存器中,则可以通过fs寄存器来实现从内核态读/写用户态的数据(在内核中,有好多这样的操作,诸如:get_fs_long(),set_fs_long(),get_fs_word(),set_fs_word()等等)。
mov %dx,%fs
......
call _sys_call_table(,%eax,4)
pushl %eax #把eax中的返回值压入栈
......
#中断返回时候,手工恢复各寄存器成用户态时的内容
popl %eax #保存着系统调用的返回值,放入eax,在用户态的库函数open中的返回值就是通过这里的eax传递的。
popl %ebx
popl %ecx
popl %edx
#此时,理论上应该 popl %eax 了,但是。。。
addl $4,%esp #放弃 系统中断调用时的 压栈保存的eax(上边红色eax保存着软中断的向量号码)
pop %fs
pop %es
pop %ds
iret #中断返回
上边的call _sys_call_table(,%eax,4)就是调用具体的系统调用 中断处理函数,_sys_call_table是定义在其他文件中定义的函数指针数组:
fn_ptr sys_call_table[sys_setup,sys_exit,sys_fork,sys_read,sys_write,sys_open,......];
fn_ptr :typedef int (*fn_ptr)() ;
sys_open:extern int sys_open() ;
可以看到,sys_open在数组的第六项,这就对应了上边在用户态定义的#define __NR_open 5
而 extern int sys_open对应的实现函数在另外的文件fs/open.c所实现:
int sys_open(const char* filename,int flag,int mode){
struct m_inode * inode ;
struct file * f ;
int i,fd ;
for(fd=0;fd<NR_OPEN;fd++){
if( !current->filp[fd] ) break ;
}
f = 0 + file_table ;
for(i=0;i<NR_FILE;i++,f++){
if(!f->f_count) break ;
}
current->filp[fd] = f ;
open_namei(filename,flag,mode,&inode) ;
f->f_mode = inode->i_mode ;
f->f_flags = flag ;
f->f_count = 1 ;
f->f_inode = inode ;
f->f_pos = 0 ;
return (fd) ;
}
int open_namei(const char* pathname,int flag,int mod,struct m_inode ** res_inode){
struct m_inode * dir,* inode ;
struct buffer_head * bh ;
struct dir_entry * de ;
dir = dir_namei(pathname,&namelen,&basename,NULL) ;
bh = find_empty(&dir,basename,namelen,&de) ;
if(!bh){
inode = new_inode(dir->i_dev) ;
inode->i_uid = current->euid ;
inode->i_mode = mode ;
inode->i_dirt = 1 ;
bh = add_entry(dir,basename,namelen,&de) ;
de->inode = inode->i_num ;
de->b_dirt = 1 ;
brelse(bh) ;
iput(dir) ;
* res_inode = inode ;
return 0 ;
}
inr = de->inode ;
dev = dir->i_dev ;
brelse(bh) ;
inode = follow_link(dir,iget(dev,inr)) ;
*res_inode = inode ;
return 0 ;
}
呵呵,文件系统是操作系统最复杂的部分,所以代码量也最大,先写到这里,再有空了接着写,总不能不工作,天天写这个东西呀。这是学习,学习就是为了更好的工作,不工作了哪行。下次接着写,呵呵