open
如果打开的文件不存在并且flag带有O_CREAT
标志,就会创建一个新的inode添加到目录项中。如果打开的文件已经存在,就会把指定的inode信息读取到缓冲区。open
还会向内核的文件表中申请一个空间的file,在自己的进程空间的文件表中申请一个空位给file,并返回文件描述符(就是数组的下标)。
lib/open.c
int open(const char * filename, int flag, ...)
{
register int res;
va_list arg;
va_start(arg,flag);
__asm__("int $0x80"
:"=a" (res)
:"0" (__NR_open),"b" (filename),"c" (flag),
"d" (va_arg(arg,int)));
if (res>=0)
return res;
errno = -res;
return -1;
}
调用中断80,就会调用system_call
函数,该函数位于kernel/system_call.s 中,间接就会调用sys_open
函数,注意此时已经从用户态转变成内核态了。
sys_open
fs/open.c
flag是打开文件标志,它可取值:O_RDONLY
(只读)、O_WRONLY
(只写)或O_RDWR
(读写),以及O_EXCL
(被创建文件必须不存在)、O_APPEND
(在文件尾添加数据)等其他一些标志的组合。这些标志位于include/fcntl.h文件中。
当执行execve
函数时,进程此时将完全被新程序替换掉,那么从父进程那里继承来的文件描述符也就没有意义了,close_on_exec
就是这是这样的一个标志位。只要该标志位被置位1,执行execve
就会把对应的文件描述符关闭。
int sys_open(const char * filename,int flag,int mode)
{
struct m_inode * inode; //i-节点
struct file * f;
int i,fd;
mode &= 0777 & ~current->umask; // 当前进程对该文件的模式,就是可读、可写、可执行这些
for(fd=0 ; fd<NR_OPEN ; fd++) //NR_OPEN是20,也就是说一个进程最多打开20个文件
if (!current->filp[fd]) // filp是数组,fd就是文件描述符,也就是数组的索引
break;
if (fd>=NR_OPEN) //检查有没有超过20个文件
return -EINVAL;
current->close_on_exec &= ~(1<<fd); // 执行execve时需要关闭的文件描述符
f=0+file_table;
for (i=0 ; i<NR_FILE ; i++,f++) //在文件表中寻找一个空闲结构项
if (!f->f_count) break;
if (i>=NR_FILE)
return -EINVAL;
//进程对应文件句柄fd的文件结构指针指向搜索到的文件结构,并令文件用计数递增1
//再进程文件表中占据了一个位置,但是此时还不知道inode是多少
(current->filp[fd]=f)->f_count++;
//根据文件名找到inode,找不到就是失败
if ((i=open_namei(filename,flag,mode,&inode))<0) {
current->filp[fd]=NULL;
f->f_count=0;
return i;
}
// 根据已打开文件的i节点的属性字段,我们可以知道文件的具体类型。对于不同类
// 型的文件,我们需要操作一些特别的处理。如果打开的是字符设备文件,那么对于
// 主设备号是4的字符文件(例如/dev/tty0),如果当前进程是组首领并且当前进程的
// tty字段小于0(没有终端),则设置当前进程的tty号为该i节点的子设备号,并设置
// 当前进程tty对应的tty表项的父进程组号等于当前进程的进程组号。表示为该进程
// 组(会话期)分配控制终端。对于主设备号是5的字符文件(/dev/tty),若当前进
// 程没有tty,则说明出错,于是放回i节点和申请到的文件结构,返回出错码(无许可)。
/* ttys are somewhat special (ttyxx major==4, tty major==5) */
if (S_ISCHR(inode->i_mode)) {
if (MAJOR(inode->i_zone[0])==4) {
if (current->leader && current->tty<0) {
current->tty = MINOR(inode->i_zone[0]);
tty_table[current->tty].pgrp = current->pgrp;
}
} else if (MAJOR(inode->i_zone[0])==5)
if (current->tty<0) {
iput(inode);
current->filp[fd]=NULL;
f->f_count=0;
return -EPERM;
}
}
// 如果打开的是块设备文件,则检查盘片是否更换过。若更换过则需要让高速缓冲区
// 中该设备的所有缓冲块失败。
if (S_ISBLK(inode->i_mode))
check_disk_change(inode->i_zone[0]);
f->f_mode = inode->i_mode;
f->f_flags = flag;
f->f_count = 1;
f->f_inode = inode;
f->f_pos = 0;
return (fd);
}
先从进程中找到一个可用的文件描述符fd,然后在文件表中找到一个引用数为0的file,让这个fd的位置指向file。
open_namei
fs/namei.c
O_TRUNC
表示若文件存在,则长度被截为0,属性不变。
O_ACCMODE
的作用是取出flag的低2位,0_RDONLY
(00)只读打开,O_WRONLY
(01)只写打开,O_RDWR
(02)读写打开。所以if ((flag & O_TRUNC) && !(flag & O_ACCMODE))
就表示如果O_TRUNC
标志打开,那么就一定要有写标志。
int open_namei(const char * pathname, int flag, int mode,struct m_inode ** res_inode)
{
const char * basename;
int inr,dev,namelen;
struct m_inode * dir, *inode;
struct buffer_head * bh;
struct dir_entry * de;
if ((flag & O_TRUNC) && !(flag & O_ACCMODE))
flag |= O_WRONLY; //添加写标志
mode &= 0777 & ~current->umask;
mode |= I_REGULAR; //添上普通文件标志
//找到该文件的最顶层目录
if (!(dir = dir_namei(pathname,&namelen,&basename)))
return -ENOENT;
if (!namelen) { /* special case: '/usr/' etc */
if (!(flag & (O_ACCMODE|O_CREAT|O_TRUNC))) {
*res_inode=dir;
return 0;
}
iput(dir);
return -EISDIR;
}
// 从目录项中找到指定名字的inode,并将其读取到缓冲区
bh = find_entry(&dir,basename,namelen,&de);
//如果该高速缓冲指针为NULL,则表示没有找到对应文件名的目录项,因此只可能是创建文件操作
if (!bh) {
if (!(flag & O_CREAT)) { // 如果不是创建文件,则释放该目录的i 节点
iput(dir);
return -ENOENT;
}
if (!permission(dir,MAY_WRITE)) { // 如果用户在该目录没有写的权力,则释放该目录的i 节点
iput(dir);
return -EACCES;
}
//在目录节点对应的设备上申请一个新i 节点,若失败,则释放目录的i 节点
inode = new_inode(dir->i_dev);
if (!inode) {
iput(dir);
return -ENOSPC;
}
inode->i_uid = current->euid;
inode->i_mode = mode;
inode->i_dirt = 1; //新创建的inode,需要回写到磁盘
bh = add_entry(dir,basename,namelen,&de); //在指定目录dir 中添加一新目录项
// 如果返回的应该含有新目录项的高速缓冲区指针为NULL,则表示添加目录项操作失败。于是将该
// 新i 节点的引用连接计数减1;并释放该i 节点与目录的i 节点
if (!bh) {
inode->i_nlinks--;
iput(inode);
iput(dir);
return -ENOSPC;
}
de->inode = inode->i_num;
bh->b_dirt = 1;
brelse(bh);
iput(dir);
*res_inode = inode;
return 0;
}
// 若上面在目录中取文件名对应目录项结构的操作成功(即bh不为NULL),则说明指定打开的文件已
// 经存在。于是取出该目录项的i节点号和其所在设备号,并释放该高速缓冲区以及放回目录的i节点
// 如果此时堵在操作标志O_EXCL置位,但现在文件已经存在,则返回文件已存在出错码退出。
inr = de->inode;
dev = dir->i_dev;
brelse(bh);
iput(dir);
if (flag & O_EXCL)
return -EEXIST;
// 然后我们读取该目录项的i节点内容。若该i节点是一个目录i节点并且访问模式是只写或读写,或者
// 没有访问的许可权限,则放回该i节点,返回访问权限出错码退出。
if (!(inode=iget(dev,inr)))
return -EACCES;
if ((S_ISDIR(inode->i_mode) && (flag & O_ACCMODE)) ||
!permission(inode,ACC_MODE(flag))) {
iput(inode);
return -EPERM;
}
// 接着我们更新该i节点的访问时间字段值为当前时间。如果设立了截0标志,则将该i节点的文件长度
// 截0.最后返回该目录项i节点的指针,并返回0(成功)。
inode->i_atime = CURRENT_TIME;
if (flag & O_TRUNC)
truncate(inode);
*res_inode = inode;
return 0;
}
dir_namei
文件分析看这里
sys_read
fd
就是使用open返回的文件描述符,buf
在用户空间,count
读取多少字节。
fs/read_write.c
int sys_read(unsigned int fd,char * buf,int count)
{
struct file * file;
struct m_inode * inode;
if (fd>=NR_OPEN || count<0 || !(file=current->filp[fd]))
return -EINVAL;
if (!count)
return 0;
// 然后验证存放数据的缓冲区内存限制。并取文件的i节点。用于根据该i节点的属性,分
// 别调用相应的读操作函数。若是管道文件,并且是读管道文件模式,则进行读管道操作,
// 若成功则返回读取的字节数,否则返回出错码,退出。如果是字符型文件,则进行读
// 字符设备操作,并返回读取的字符数。如果是块设备文件,则执行块设备读操作,并
// 返回读取的字节数。
verify_area(buf,count);
inode = file->f_inode;
if (inode->i_pipe)
return (file->f_mode&1)?read_pipe(inode,buf,count):-EIO;
if (S_ISCHR(inode->i_mode))
return rw_char(READ,inode->i_zone[0],buf,count,&file->f_pos);
if (S_ISBLK(inode->i_mode))
return block_read(inode->i_zone[0],&file->f_pos,buf,count);
// 如果是目录文件或者是常规文件,则首先验证读取字节数count的有效性并进行调整(若
// 读去字节数加上文件当前读写指针值大于文件长度,则重新设置读取字节数为文件长度
// -当前读写指针值,若读取数等于0,则返回0退出),然后执行文件读操作,返回读取的
// 字节数并退出。
if (S_ISDIR(inode->i_mode) || S_ISREG(inode->i_mode)) {
if (count+file->f_pos > inode->i_size)
count = inode->i_size - file->f_pos;
if (count<=0)
return 0;
return file_read(inode,file,buf,count);
}
// 执行到这里,说明我们无法判断文件的属性。则打印节点文件属性,并返回出错码退出。
printk("(Read)inode->i_mode=%06o\n\r",inode->i_mode);
return -EINVAL;
}
verify_area
对于80386 CPU,在执行特权级0代码时不会理会用户空间中的页面是否是也保护的, 因此在执行内核代码时用户空间中数据页面来保护标志起不了作用,写时复制机制也就失去了作用。verify_area()函数就用于此目的。但对于80486或后来的CPU,其 控制寄存器CRO中有一个写保护标志WP(位16),内核可以通过设置该标志来禁止特权级0的代码向用户空间只读页面执行写数据,否则将导致发生写保护异常。从而486以上CPU可以通过设置该标志来达到本函数的目的。该函数对当前进程逻辑地址从addr到addr+size这一段范围以页为单位执行写操作前的检测操作。由于检测判断是以页面为单位进行操作,因此程序首先需要找出addr所在页面开始地址start,然后start加上进程数据段基址,使这个start变成CPU 4G线性 空间中的地址。最后循环调用write_verify()对指定大小的内存空间进行写前验证。 若页面是只读的,则执行共享检验和复制页面操作。
kernel/fork.c
void verify_area(void * addr,int size)
{
unsigned long start;
// 首先将起始地址start调整为其所在左边界开始位置,同时相应地调整验证区域
// 大小。下句中的start& 0xfff 用来获得指定起始位置addr(也即start)在所在
// 页面中的偏移值,原验证范围size加上这个偏移值即扩展成以addr所在页面起始
// 位置开始的范围值。因此在下面也需要把验证开始位置start调整成页面边界值。
start = (unsigned long) addr;
size += start & 0xfff;
start &= 0xfffff000; // 此时start是当前进程空间中的逻辑地址。
// 下面start加上进程数据段在线性地址空间中的起始基址,变成系统整个线性空间
// 中的地址位置。对于linux-0.11内核,其数据段和代码在线性地址空间中的基址
// 和限长均相同。
start += get_base(current->ldt[2]);
while (size>0) {
size -= 4096;
write_verify(start);
start += 4096;
}
}
file_read
file中保存着文件的读取偏移地址,使用这个算出文件的逻辑块号nr,将该块号的信息读取到缓冲区,然后一个一个字节的复制到用户空间。
int file_read(struct m_inode * inode, struct file * filp, char * buf, int count)
{
int left,chars,nr;
struct buffer_head * bh;
// 首先判断参数的有效性。若需要读取的字节数count小于等于0,则返回0.若还需要读
// 取的字节数不等于0,就循环执行下面操作,直到数据全部读出或遇到问题。在读循环
// 操作过程中,我们根据i节点和文件表结构信息,并利用bmap()得到包含文件当前读写
// 位置的数据块在设备上对应的逻辑块号nr。若nr不为0,则从i节点指定的设备上读取该
// 逻辑块。如果读操作是吧则退出循环。若nr为0,表示指定的数据块不存在,置缓冲块
// 指针为NULL。(filp->f_pos)/BLOCK_SIZE用于计算出文件当前指针所在的数据块号。
if ((left=count)<=0) //若需要读取的字节数count小于等于0,则返回0
return 0;
while (left) {
if ((nr = bmap(inode,(filp->f_pos)/BLOCK_SIZE))) {
if (!(bh=bread(inode->i_dev,nr)))
break;
} else
bh = NULL;
// 接着我们计算文件读写指针在数据块中的偏移值nr,则在该数据块中我们希望读取的
// 字节数为(BLOCK_SIZE-nr)。然后和现在还需读取的字节数left做比较。其中小值
// 即为本次操作需读取的字节数chars。如果(BLOCK_SIZE-nr) > left,则说明该块
// 是需要读取的最后一块数据。反之还需要读取下一块数据。之后调整读写文件指针。
// 指针前移此次将读取的字节数chars,剩余字节计数left相应减去chars。
nr = filp->f_pos % BLOCK_SIZE;
chars = MIN( BLOCK_SIZE-nr , left );
filp->f_pos += chars;
left -= chars;
// 若上面从设备上读到了数据,则将p指向缓冲块中开始读取数据的位置,并且复制chars
// 字节到用户缓冲区buf中。否则往用户缓冲区中填入chars个0值字节。
if (bh) {
char * p = nr + bh->b_data;
while (chars-->0)
put_fs_byte(*(p++),buf++);
brelse(bh);
} else {
while (chars-->0)
put_fs_byte(0,buf++);
}
}
// 修改该i节点的访问时间为当前时间。返回读取的字节数,若读取字节数为0,则返回
// 出错号。CURRENT_TIME是定义在include/linux/sched.h中的宏,用于计算UNIX时间。
// 即从1970年1月1日0时0分0秒开始,到当前的时间,单位是秒。
inode->i_atime = CURRENT_TIME;
return (count-left)?(count-left):-ERROR;
}