linux源码解读(六):文件系统——虚拟文件系统VFS
linux的设计理念:万物皆文件!换句话说:所有的设备,包括但不限于磁盘、串口、网卡、pipe管道、打印机等统一看成是文件。对于用户来说,所有操作都是通过open、read、write、ioctl、close等接口操作的,确实很方便;但是对于linux,底层明明是不同的硬件设备,这些设备怎么才能统一被上述接口识别和适配了?识别和适配这层接口的功能就是虚拟文件系统,简称VFS,整体架构图如下:
1、file_dev.c定义了文件的读写函数;
(1)为了更好地管理文件,linux同样也定义了file结构体:
struct file { unsigned short f_mode;/*FMODE_READ或FMODE_WRITE,标识标识文件是否可读或可写*/ unsigned short f_flags;/*O_RDONLY/O_NONBLOCK/O_SYNC:O_NONBLOCK 打开文件是否阻塞*/ unsigned short f_count;/*文件被多少进程引用?*/ struct m_inode * f_inode;/*文件对应的inode节点,里面存了很多文件的元信息;文件存放的描述:磁盘block->inode->struct file->file_table->file descriptor*/ off_t f_pos;/*当前文件的读写位置偏移,lseek修改的*/ };
(2)这个file_read函数已经很接近用户使用的read函数了,仅仅是多了第一个inode参数:
//// 文件读函数 - 根据i节点和文件结构,读取文件中数据。 // 由i节点我们可以知道设备号,由filp结构可以知道文件中当前读写指针位置。buf指定 // 用户空间中缓冲区位置,count是需要读取字节数。返回值是实际读取的字节数,或出错号(小于0). 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) return 0; while (left) { if ((nr = bmap(inode,(filp->f_pos)/BLOCK_SIZE))) {/*把f_pos位置之前数据所在磁盘的block号返回*/ if (!(bh=bread(inode->i_dev,nr)))/*从对应的磁盘block号读取数据,也就是从磁盘读取(filp->f_pos)/BLOCK_SIZE块的数据到内存的缓存区*/ break; } else bh = NULL; // 接着我们计算文件读写指针在数据块中的偏移值nr,则在该数据块中我们希望读取的 // 字节数为(BLOCK_SIZE-nr)。然后和现在还需读取的字节数left做比较。其中小值 // 即为本次操作需读取的字节数chars。如果(BLOCK_SIZE-nr) > left,则说明该块 // 是需要读取的最后一块数据。反之还需要读取下一块数据。之后调整读写文件指针。 // 指针前移此次将读取的字节数chars,剩余字节计数left相应减去chars。 nr = filp->f_pos % BLOCK_SIZE;/*f_pos在block内的offset*/ chars = MIN( BLOCK_SIZE-nr , left );/*BLOCK_SIZE-nr:当前块的剩余区域;left:文件要读取的剩余size; 注意:要读取的count不一定等于文件当前大小f_pos*/ 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++);/*从内核缓存区copy到用户指定的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; }
与上面对应的是file_write,把用户指定的数据写入缓存区(注意:此时并未调用sys_sync函数将数据从缓存区写入磁盘):
//// 文件写函数 - 根据i节点和文件结构信息,将用户数据写入文件中。 // 由i节点我们可以知道设备号,而由file结构可以知道文件中当前读写指针位置。buf指定 // 用户态缓冲区的位置,count为需要写入的字节数。返回值是实际写入的字节数,或出错号。 int file_write(struct m_inode * inode, struct file * filp, char * buf, int count) { off_t pos; int block,c; struct buffer_head * bh; char * p; int i=0; /* * ok, append may not work when many processes are writing at the same time * but so what. That way leads to madness anyway. */ // 首先确定数据写入文件的位置。如果是要向文件后添加数据,则将文件读写指针移到 // 文件尾部。否则就将在文件当前读写指针处写入。 if (filp->f_flags & O_APPEND) pos = inode->i_size; else pos = filp->f_pos; // 然后在已写入字节数i(刚开始为0)小于指定写入字节数count时,循环执行以下操作。 // 在循环操作过程中,我们先取文件数据块号(pos/BLOCK_SIZE)在设备上对应的逻辑 // 块号block。如果对应的逻辑块不存在就创建一块。如果得到的逻辑块号=0,则表示 // 创建失败,于是退出循环。否则我们根据该逻辑块号读取设备上的相应逻辑块,若出 // 错也退出循环。 while (i<count) { if (!(block = create_block(inode,pos/BLOCK_SIZE))) break; if (!(bh=bread(inode->i_dev,block))) break; // 此时缓冲块指针bh正指向刚读入的文件数据库。现在再求出文件当前读写指针在该 // 数据块中的偏移值c,并将指针p指向缓冲块中开始写入数据的位置,并置该缓冲块已 // 修改标志。对于块中当前指针,从开始读写位置到块末共可写入c=(BLOCK_SIZE - c) // 个字节。若c大于剩余还需写入的字节数(count - i),则此次只需再写入c = (count - i) // 个字节即可。 c = pos % BLOCK_SIZE; p = c + bh->b_data; bh->b_dirt = 1; c = BLOCK_SIZE-c; if (c > count-i) c = count-i; // 在写入数据之前,我们先预先设置好下一次循环操作要读写文件中的位置。因此我们 // 把pos指针前移此次需写入的字节数。如果此时pos位置值超过了文件当前长度,则 // 修改i节点中文件长度字段,并置i节点已修改标志。然后把此次要写入的字节数c累加到 // 已写入字节计数值i中,供循环判断使用。接着从用户缓冲区buf中复制c个字节到告诉缓 // 冲块中p指向的开始位置处。复制完后就释放该缓冲块。 pos += c; if (pos > inode->i_size) { inode->i_size = pos; inode->i_dirt = 1; } i += c; while (c-->0) *(p++) = get_fs_byte(buf++);/*注意:此时数据只是写入了缓存区,并未立即写入磁盘*/ brelse(bh); } // 当数据已全部写入文件或者在写操作工程中发生问题时就会退出循环。此时我们更改文件修改 // 时间为当前时间,并调整文件读写指针。如果此次操作不是在文件尾部添加数据,则把文件 // 读写指针调整到当前读写位置pos处,并更改文件i节点的修改时间为当前时间。最后返回写入 // 的字节数,若写入字节数为0,则返回出错号-1. inode->i_mtime = CURRENT_TIME; if (!(filp->f_flags & O_APPEND)) { filp->f_pos = pos; inode->i_ctime = CURRENT_TIME; } return (i?i:-1); }
2、和读写文件类似,linux 0.11版本也提供了读写块设备的api,在block_dev.c文件中;代码结构和读写文件没本质区别:
注意:这里求block号、block内部偏移的代码:int block = *pos >> BLOCK_SIZE_BITS; int offset = *pos & (BLOCK_SIZE-1); 搞逆向时遇到这类代码,需要第一时间知道代码背后的逻辑意义!
//// 数据块写函数 - 向指定设备从给定偏移出写入制定长度数据。 // 参数:dev - 设备号; pos - 设备文件中偏移量指针;buf - 用户空间中缓冲区地址; // count - 要传送的字节数 // 返回已写入字节数。若没有写入任何字节或出错,则返回出错号。 // 对于内核来说,写操作是向高速缓冲区中写入数据。什么时候数据最终写入设备是由高 // 速缓冲管理程序决定并处理的。另外,因为块设备是以块为单位进行读写,因此对于写 // 开始位置不处于块起始处时,需要先将开始字节所在的整个块读出,然后将需要写的数 // 据从写开始处填写满该块,再将完整的一块数据写盘(即交由高速缓冲程序去处理)。 int block_write(int dev, long * pos, char * buf, int count) { // 首先由文件中位置pos换算成开始读写盘快的块序号block,并求出需写第1字节在该 // 块中的偏移位置offset. int block = *pos >> BLOCK_SIZE_BITS; // pos所在文件数据块号,相当于除以1024 int offset = *pos & (BLOCK_SIZE-1); // pos在数据块中偏移值,相当于模1024 int chars; int written = 0; struct buffer_head * bh; register char * p; // 局部寄存器变量,被存放在寄存器中 // 然后针对要写入的字节数count,循环执行以下操作,知道数据全部写入。在循环执行 // 过程中,先计算在当前处理的数据块中可写入的字节数。如果写入的字节数填不满一块, // 那么就只需写count字节。如果正要写1块数据内容,则直接申请1块高速缓冲块,并把 // 用户数据放入即可。否则就需要读入将被写入部分数据的数据块,并预读下两块数据。 // 然后将块号递增1,为下次操作做好准备。如果缓冲块操作失败,则返回已写字节数, // 如果没有写入任何字节,则返回出错号(负数). while (count>0) { chars = BLOCK_SIZE - offset; if (chars > count) chars=count; if (chars == BLOCK_SIZE) bh = getblk(dev,block); else bh = breada(dev,block,block+1,block+2,-1); block++; if (!bh) return written?written:-EIO; // 接着先把指针p指向读出数据的缓冲块中开始写入数据的位置处。若最后一次循环写入 // 的数据不足一块,则需从块开始处填写(修改)所需的字节,因此这里需预先设置offset // 为零。此后将文件中偏移指针pos前移此次将要写的字节数chars,并累加这些要写的 // 字节数到统计值written中,再把还需要写的计数值count减去此次要写的字节数chars. // 然后我们从用户缓冲区复制chars个字节到p指向的高速缓冲中开始写入的位置处。复制 // 完后就设置该缓冲区块已修改标志,并释放该缓冲区(也即该缓冲区引用计数递减1)。 p = offset + bh->b_data; offset = 0; *pos += chars; written += chars; // 累计写入字节数 count -= chars; while (chars-->0) *(p++) = get_fs_byte(buf++); bh->b_dirt = 1; brelse(bh); } return written; } //// 数据块读函数 - 从指定设备和位置处读入指定长度数据到用户缓冲区中。 // 参数:dev - 设备号;pos - 设备文件中偏移量指针;buf - 用户空间缓冲区地址; // count - 要传送的字节数。 // 返回已读入字节数。若没有读入任何字节或出错,则返回出错号。 int block_read(int dev, unsigned long * pos, char * buf, int count) { // 首先由文件中位置pos换算成开始读写盘块的块序号block,并求出需读第1个字节在块中 // 的偏移位置offset. int block = *pos >> BLOCK_SIZE_BITS; int offset = *pos & (BLOCK_SIZE-1); int chars; int read = 0; struct buffer_head * bh; register char * p; // 然后针对要读入的字节数count,循环执行以下操作,直到数据全部读入。在循环执行 // 过程中,先计算在当前处理的数据块中需读入的字节数。如果需要读入的字节数还不满 // 一块,那么就只需要读count字节。然后调用读块函数breada()读如需要的数据块,并 // 预读下两块数据,如果读操作出错,则返回已读字节数,如果没有读入任何字节,则 // 返回出错号。然后将块号递增1.为下次操作做好准备。如果缓冲块操作失败,则返回已 // 写字节数,如果没有读入任何字节,则返回出错号(负数)。 while (count>0) { chars = BLOCK_SIZE-offset; if (chars > count) chars = count; if (!(bh = breada(dev,block,block+1,block+2,-1))) return read?read:-EIO; block++; // 接着先把指针p指向读出盘块的缓冲中开始读入数据的位置处。若最后一次循环读 // 操作的数据不足一块,则需从块起始处读取所需字节,因此这里需预先设置offset // 为零。此后将文件中偏移指针pos前移此次将要读的字节数chars,并且累加这些要读 // 的字节数到统计值read中。再把还需要读的计数值count减去此次要读的字节数chars。 // 然后我们从高速缓冲块中p指向的开始读的位置处复制chars个字节到用户缓冲区中, // 同时把用户缓冲区指针前移。本次复制完后就释放该缓冲块。 p = offset + bh->b_data; offset = 0; *pos += chars; read += chars; // 读入累计字节数 count -= chars; while (chars-->0) put_fs_byte(*(p++),buf++); brelse(bh); } return read; }
3、除了常见的磁盘等块设备,还有串口这类的字符型设备,读写接口定义在了char_dev.c文件中了:
extern int tty_read(unsigned minor,char * buf,int count); extern int tty_write(unsigned minor,char * buf,int count); // 定义字符设备读写函数指针类型 typedef int (*crw_ptr)(int rw,unsigned minor,char * buf,int count,off_t * pos); //// 串口终端读写操作函数。 // 参数:rw - 读写命令;minor - 终端子设备号;buf - 缓冲区;count - 读写字节数 // pos - 读写操作当前指针,对于中断操作,该指针无用 // 返回:实际读写的字节数。若失败则返回出错码。 static int rw_ttyx(int rw,unsigned minor,char * buf,int count,off_t * pos) { return ((rw==READ)?tty_read(minor,buf,count): tty_write(minor,buf,count)); } //// 终端读写操作函数。 // 同rw_ttyx,只是增加了对进程是否有控制终端的检测。 static int rw_tty(int rw,unsigned minor,char * buf,int count, off_t * pos) { // 若进程没有控制终端,则返回出错号。否则调用终端读写函数rw_ttyx(), // 并返回实际读写字节数。 if (current->tty<0) return -EPERM; return rw_ttyx(rw,current->tty,buf,count,pos); } // 内存数据读写,早期版本暂时没实现 static int rw_ram(int rw,char * buf, int count, off_t *pos) { return -EIO; } // 物理内存数据读写,早期版本暂时没实现 static int rw_mem(int rw,char * buf, int count, off_t * pos) { return -EIO; } // 内核虚拟内存数据读写,早期版本暂时没实现 static int rw_kmem(int rw,char * buf, int count, off_t * pos) { return -EIO; } //// 端口读写操作函数 // 参数:rw - 读写命令; buf - 缓冲区; cout - 读写字节数; pos - 端口地址。 // 返回:实际读写的字节数。 static int rw_port(int rw,char * buf, int count, off_t * pos) { int i=*pos; // 对于所要求读写的字节数,并且端口地址小于64K时,循环执行单个字节的 // 读写操作。若是读命令,则从端口i中读取一个字节内容并放到用户缓冲区中。 // 若是写命令,则从用户数据缓冲区中取一字节输出到端口i。 while (count-->0 && i<65536) { if (rw==READ) put_fs_byte(inb(i),buf++); else outb(get_fs_byte(buf++),i); i++; } // 然后计算读/写字节数,调整相应读写指针,并返回读/写的字节数。 i -= *pos; *pos += i; return i; } //// 内存读写操作函数 static int rw_memory(int rw, unsigned minor, char * buf, int count, off_t * pos) { // 根据内存设备子设备号,分别调用不同的内存读写函数。 switch(minor) { case 0: return rw_ram(rw,buf,count,pos); case 1: return rw_mem(rw,buf,count,pos); case 2: return rw_kmem(rw,buf,count,pos); case 3: return (rw==READ)?0:count; /* rw_null */ case 4: return rw_port(rw,buf,count,pos); default: return -EIO; } }
这里的编码方式非常巧妙:先定义一个数组,数组的每个元素都是函数入口;然后在rw_char函数中,根据major(dev)找到对应所需的函数入口,然后通过函数指针的方式调用:
// 字符设备读写函数指针表 file_operations static crw_ptr crw_table[]={ NULL, /* nodev */ rw_memory, /* /dev/mem etc */ NULL, /* /dev/fd */ NULL, /* /dev/hd */ rw_ttyx, /* /dev/ttyx */ rw_tty, /* /dev/tty */ NULL, /* /dev/lp */ NULL}; /* unnamed pipes */ // 字符设备读写操作函数 // 参数:rw - 读写命令;dev - 设备号;buf - 缓冲区; count - 读写字节数;pos - 读写指针。 // 返回:实际读/写字节数 int rw_char(int rw,int dev, char * buf, int count, off_t * pos) { crw_ptr call_addr; // 如果设备号超出系统设备数,则返回出错码。如果该设备没有对应的读/写函数,也 // 返回出错码。否则调用对应设备的读写操作函数,并返回实际读/写的字节数。 if (MAJOR(dev)>=NRDEVS) return -ENODEV; if (!(call_addr=crw_table[MAJOR(dev)])) return -ENODEV; return call_addr(rw,MINOR(dev),buf,count,pos); }
4、前面说了,不同硬件设备有不同的读写接口,但是在VFS这一层确统一起来了,linux 0.11版本是怎么做的了?在rea_write.c函数中做了封装:
(1)这里先是导入不同类型的读写函数:
// 字符设备读写函数。 extern int rw_char(int rw,int dev, char * buf, int count, off_t * pos); // 读管道操作函数。 extern int read_pipe(struct m_inode * inode, char * buf, int count); // 写管道操作函数 extern int write_pipe(struct m_inode * inode, char * buf, int count); // 块设备读操作函数 extern int block_read(int dev, off_t * pos, char * buf, int count); // 块设备写操作函数 extern int block_write(int dev, off_t * pos, char * buf, int count); // 读文件操作函数 extern int file_read(struct m_inode * inode, struct file * filp, char * buf, int count); // 写文件操作函数 extern int file_write(struct m_inode * inode, struct file * filp, char * buf, int count);
(2)通过sys_read和sys_write彻底封装了上述导入的不同类型设备的读写函数:这两个函数内部都会通过S_ISCHR(inode->i_mode)、S_ISBLK(inode->i_mode)、S_ISDIR(inode->i_mode) 、S_ISREG(inode->i_mode)等方式判断,根据不同的设备类型调用不同的读写函数!
//// 读文件系统调用 // 参数fd是文件句柄,buf是缓冲区,count是预读字节数 int sys_read(unsigned int fd,char * buf,int count) { struct file * file; struct m_inode * inode; // 函数首先对参数有效性进行判断。如果文件句柄值大于程序最多打开文件数NR_OPEN, // 或者需要读取的字节计数值小于0,或者该句柄的文件结构指针为空,则返回出错码并 // 退出。若需读取的字节数count等于0,则返回0退出。 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; } //// 写文件系统调用 // 参数fd是文件句柄,buf是用户缓冲区,count是欲写字节数。 int sys_write(unsigned int fd,char * buf,int count) { struct file * file; struct m_inode * inode; // 同样地,我们首先判断函数参数的有效性。若果进程文件句柄值大于程序最多打开文件数 // NR_OPEN,或者需要写入的字节数小于0,或者该句柄的文件结构指针为空,则返回出错码 // 并退出。如果需读取字节数count等于0,则返回0退出。 if (fd>=NR_OPEN || count <0 || !(file=current->filp[fd])) return -EINVAL; if (!count) return 0; // 然后验证存放数据的缓冲区内存限制。并取文件的i节点。用于根据该i节点属性,分别调 // 用相应的读操作函数。若是管道文件,并且是写管道文件模式,则进行写管道操作,若成 // 功则返回写入的字节数,否则返回出错码退出。如果是字符设备文件,则进行写字符设备 // 操作,返回写入的字符数退出。如果是块设备文件,则进行块设备写操作,并返回写入的 // 字节数退出。若是常规文件,则执行文件写操作,并返回写入的字节数,退出。 inode=file->f_inode; if (inode->i_pipe) return (file->f_mode&2)?write_pipe(inode,buf,count):-EIO; if (S_ISCHR(inode->i_mode)) return rw_char(WRITE,inode->i_zone[0],buf,count,&file->f_pos); if (S_ISBLK(inode->i_mode)) return block_write(inode->i_zone[0],&file->f_pos,buf,count); if (S_ISREG(inode->i_mode)) return file_write(inode,file,buf,count); // 执行到这里,说明我们无法判断文件的属性。则打印节点文件属性,并返回出错码退出。 printk("(Write)inode->i_mode=%06o\n\r",inode->i_mode); return -EINVAL; }
(3)open.c的sys_open函数,返回文件句柄(本质就是file结构体的指针):
//// 打开(或创建)文件系统调用。 // 参数filename是文件名,flag是打开文件标志,它可取值:O_RDONLY(只读)、O_WRONLY // (只写)或O_RDWR(读写),以及O_EXCL(被创建文件必须不存在)、O_APPEND(在文件 // 尾添加数据)等其他一些标志的组合。如果本调用创建了一个新文件,则mode就用于指 // 定文件的许可属性。这些属性有S_IRWXU(文件宿主具有读、写和执行权限)、S_IRUSR // (用户具有读文件权限)、S_IRWXG(组成员具有读、写和执行权限)等等。对于新创 // 建的文件,这些属性只应用与将来对文件的访问,创建了只读文件的打开调用也将返回 // 一个可读写的文件句柄。如果调用操作成功,则返回文件句柄(文件描述符),否则返回出错码。 int sys_open(const char * filename,int flag,int mode) { struct m_inode * inode; struct file * f; int i,fd; // 首先对参数进行处理。将用户设置的文件模式和屏蔽码相与,产生许可的文件模式。 // 为了为打开文件建立一个文件句柄,需要搜索进程结构中文件结构指针数组,以查 // 找一个空闲项。空闲项的索引号fd即是文件句柄值。若已经没有空闲项,则返回出错码。 mode &= 0777 & ~current->umask; for(fd=0 ; fd<NR_OPEN ; fd++) if (!current->filp[fd]) break; if (fd>=NR_OPEN) return -EINVAL; // 然后我们设置当前进程的执行时关闭文件句柄(close_on_exec)位图,复位对应的 // bit位。close_on_exec是一个进程所有文件句柄的bit标志。每个bit位代表一个打 // 开着的文件描述符,用于确定在调用系统调用execve()时需要关闭的文件句柄。当 // 程序使用fork()函数创建了一个子进程时,通常会在该子进程中调用execve()函数 // 加载执行另一个新程序。此时子进程中开始执行新程序。若一个文件句柄在close_on_exec // 中的对应bit位被置位,那么在执行execve()时应对应文件句柄将被关闭,否则该 // 文件句柄将始终处于打开状态。当打开一个文件时,默认情况下文件句柄在子进程 // 中也处于打开状态。因此这里要复位对应bit位。 current->close_on_exec &= ~(1<<fd); // 然后为打开文件在文件表中寻找一个空闲结构项。我们令f指向文件表数组开始处。 // 搜索空闲文件结构项(引用计数为0的项),若已经没有空闲文件表结构项,则返回 // 出错码。 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。然后调用函数open_namei()执行打开操作,若返回值小于0,则说 // 明出错,于是释放刚申请到的文件结构,返回出错码i。若文件打开操作成功,则 // inode是已打开文件的i节点指针。 (current->filp[fd]=f)->f_count++; 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; } } /* Likewise with block-devices: check for floppy_change */ // 如果打开的是块设备文件,则检查盘片是否更换过。若更换过则需要让高速缓冲区 // 中该设备的所有缓冲块失败。 if (S_ISBLK(inode->i_mode)) check_disk_change(inode->i_zone[0]); // 现在我们初始化打开文件的文件结构。设置文件结构属性和标志,置句柄引用计数 // 为1,并设置i节点字段为打开文件的i节点,初始化文件读写指针为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); }
为了方便直观理解,图示如下:
为什么linux系统的理念是“万物皆文件”了?我们现在使用的这套软硬件系统最原始的名称叫information technology,核心目的是存储和读取数据!IT高速发展到现在,最核心的目的还是存储和读取数据,这一点几十年都没变!为了方便上层app读写数据,linux抽象出了VFS:上层app所有的操作都统一了接口名称,不同的设备实现这些接口就行了!对于app来说,只认接口就足够了!
参考:
1、https://zhuanlan.zhihu.com/p/66597013 详解 Linux 中的虚拟文件系统
2、https://www.bilibili.com/video/BV1tQ4y1d7mo?p=27 linux内核精讲