一、起源
在init/main.c文件中有init函数,其开头有如下代码:
static inline _syscall1(int, setup, void *, BIOS) void init(void) { int pid, i; setup((void *)&drive_info); //... } // 而_syscall1 在include/unistd.h中,定义为: #define __NR_setup 0 /* used only by init, to get system going */ #define _syscall1(type,name,atype,a) \ type name(atype a) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name),"b" ((long)(a))); \ if (__res >= 0) \ return (type) __res; \ errno = -__res; \ return -1; \ }
在 init 函数中,setup 函数实现了根文件系统挂载功能。setup 函数由宏 _syscall1(int, setup, void *, BIOS) 定义,其展开后为(可参考:Linux 0.11 fork 函数(二) ):
// __NR_##name 展开后为 __NR_setup, 而其又是一个宏定义,值为0 #define __NR_setup 0 /* used only by init, to get system going */ // 展开后 static inline int setup(void* BIOS) { long __res; __asm__ volatile ("int $0x80" : "=a" (__res) : "0" (0),"b" ((long)(a))); if (__res >= 0) return (type) __res; errno = -__res; return -1; }
二、系统调用
setup 函数是个系统调用(可参考:Linux 0.11 fork 函数(二) ),其响应函数 system_call 函数定义在 kernel/system_call.s 文件中。其最后会调用 sys_setup 函数。
三、sys_setup 之一函数读入超级块信息
sys_setup 函数在 kernel/blk_drv/hd.c 文件中。其利用 boot/setup.s 程序提供的信息对系统中所含硬盘驱动器的参数进行了设置。然后读取硬盘分区表,并尝试把启动引导盘上的虚拟盘根文件系统映像文件复制到内存虚拟盘中,若成功则加载虚拟盘中的根文件系统,否则就继续执行普通根文件系统加载操作。
static struct hd_struct { long start_sect; // 分区在硬盘中的起始物理(绝对)扇区 long nr_sects; // 分区中扇区总数 } hd[5*MAX_HD]={{0,0},}; // 函数参数BIOS是由初始化程序 init/main.c 中 init 子程序设置为指向硬盘参数表结构的指针。 // 该硬盘参数表结构包含2个硬盘参数表的内容(共32字节),是从内存0x90080处复制而来。 int sys_setup(void * BIOS) { // ... for (i = NR_HD ; i < 2 ; i++) { hd[i*5].start_sect = 0; hd[i*5].nr_sects = 0; } // 前面省略的代码,确定了系统中所含的硬盘个数NR_HD(跟踪发现为1个)。现在我们来读取每个 // 硬盘上第1个山区中的分区表信息,用来设置分区结构数组hd[]中硬盘各分区的信息。 // 首先利用bread函数读取硬盘上第1个数据块(第1个参数为硬盘设备号,第2个参数为所需读取的块号), // 若读取成功,则数据会被存放在缓冲块bh的数据区中。 // 然后根据硬盘第1个扇区最后两个字节是否为0xAA55来判断扇区中数据的有效性,若有效则取分区 // 表信息(位于扇区偏移0x1BE处)。将分区表信息放入到hd[]中。最后释放bh缓冲区。 for (drive=0 ; drive<NR_HD ; drive++) { if (!(bh = bread(0x300 + drive*5,0))) { printk("Unable to read partition table of drive %d\n\r", drive); panic(""); } if (bh->b_data[510] != 0x55 || (unsigned char) bh->b_data[511] != 0xAA) { printk("Bad partition table on drive %d\n\r",drive); panic(""); } p = 0x1BE + (void *)bh->b_data; for (i=1;i<5;i++,p++) { hd[i+5*drive].start_sect = p->start_sect; hd[i+5*drive].nr_sects = p->nr_sects; } brelse(bh); } if (NR_HD) printk("Partition table%s ok.\n\r",(NR_HD>1)?"s":""); rd_load(); // 尝试创建并加载虚拟盘 mount_root(); // 安装根文件系统 return (0); }
1、bread函数
bread函数位于fs/buffer.c中。
struct buffer_head * bread(int dev,int block) { struct buffer_head * bh; if (!(bh=getblk(dev,block))) panic("bread: getblk returned NULL\n"); if (bh->b_uptodate) return bh; ll_rw_block(READ,bh); // 注意此处会进入睡眠等待硬盘数据读取 wait_on_buffer(bh); if (bh->b_uptodate) return bh; brelse(bh); return NULL; }
ll_rw_block(READ,bh) 采用异步方式读取硬盘上的信息,此处使用 wait_on_buffer(bh) 进行同步等待(进入睡眠)读取磁盘信息结束。
1.1 getblk 函数
getblk 函数用于获取一块空闲缓冲区块。其执行流程图如下:
1.2 ll_rw_block 函数
fs/buffer.c 中 bread 函数会调用 kernel/blk_drv/ll_rw_blk.c 中 ll_rw_block 函数。
ll_rw_block 用来读写块设备中的数据。其为块设备创建块设备读写请求项,并插入到指定块设备请求队列中。实际的读写操作则是由设备的请求项处理函数 request_fn() 完成(硬盘为 do_hd_request(),软盘为 do_fd_request(),虚拟盘为 do_rd_request() )。
若 ll_rw_block 为一个块设备建立起一个请求项,并通过测试块设备的当前请求项指针为空而确定设备空闲时,就会设置该新建的请求项为当前请求项,并直接调用 request_fn() 对该请求项进行操作。否则就会使用电梯算法将新建的请求项插入到该设备的请求项链表中等待处理。而当 request_fn() 结束对一个请求项的处理,就会把该请求项从链表中删除
由于 request_fn() 在每个请求项处理结束时,都有通过中断回调C函数(主要是 read_intr 和 write_intr )再次调用request_fn() 自身去处理链表中其余的请求项,因此,只要设备的请求项链表中有未处理的请求项存在,都会陆续地被处理,直到设备的请求项链表为空为止。
代码如下:
struct blk_dev_struct blk_dev[NR_BLK_DEV] = { { NULL, NULL }, /* no_dev */ { NULL, NULL }, /* dev mem */ { NULL, NULL }, /* dev fd */ { NULL, NULL }, /* dev hd */ { NULL, NULL }, /* dev ttyx */ { NULL, NULL }, /* dev tty */ { NULL, NULL } /* dev lp */ }; // 该函数把已经设置好的请求项req添加到指定设备的请求项链表中。如果该设备的当前请求项指针为空, // 则可以设置req为当前请求项并立即调用设备请求项处理函数。否则就把req请求项插入到该请求项链表中 static void add_request(struct blk_dev_struct * dev, struct request * req) { struct request * tmp; req->next = NULL; cli(); if (req->bh) req->bh->b_dirt = 0; if (!(tmp = dev->current_request)) { // 当前没有请求,则直接调用请求函数,对于硬盘是 do_hd_request() dev->current_request = req; sti(); (dev->request_fn)(); return; } for ( ; tmp->next ; tmp=tmp->next) if ((IN_ORDER(tmp,req) || !IN_ORDER(tmp,tmp->next)) && IN_ORDER(req,tmp->next)) break; req->next=tmp->next; tmp->next=req; sti(); } // 创建请求项并插入请求队列中 // 参数major是主设备号,rw是指定命令,bh是存放数据的缓冲区头指针。 static void make_request(int major,int rw, struct buffer_head * bh) { struct request * req; int rw_ahead; /* WRITEA/READA is special case - it is not really needed, so if the */ /* buffer is locked, we just forget about it, else it's a normal read */ if ((rw_ahead = (rw == READA || rw == WRITEA))) { if (bh->b_lock) return; if (rw == READA) rw = READ; else rw = WRITE; } if (rw!=READ && rw!=WRITE) panic("Bad block dev command, must be R/W/RA/WA"); // 在开始生成和添加相应的读写请求项前,我们再来看看此次是否有必要添加请求项。在两种情况下可以 // 不必添加请求项。一是当命令是写,但缓冲区中的数据在读入之后并没有被修改过;二是当命令是读, // 但缓冲区中的数据已经是更新过的,即与块设备上的完全一样。 lock_buffer(bh); if ((rw == WRITE && !bh->b_dirt) || (rw == READ && bh->b_uptodate)) { unlock_buffer(bh); return; } repeat: /* 我们不能让队列中全部是写请求项:读操作是优先的。请求队列的后三分之一空间仅 * 用于读请求项。 * 从后往前搜索一个空闲的请求项。request->dev为-1表示空闲。 */ if (rw == READ) req = request+NR_REQUEST; else req = request+((NR_REQUEST*2)/3); /* find an empty request */ while (--req >= request) if (req->dev<0) break; /* if none found, sleep on new requests: check for rw_ahead */ if (req < request) { if (rw_ahead) { unlock_buffer(bh); return; } sleep_on(&wait_for_request); goto repeat; } /* fill up the request-info, and add it to the queue */ req->dev = bh->b_dev; // 设备号 req->cmd = rw; // 命令(READ/WRITE) req->errors=0; // 操作时产生的错误次数 req->sector = bh->b_blocknr<<1; // 起始扇区。块号转换成扇区号(1块=2扇区) req->nr_sectors = 2; // 本请求项需要读写的扇区数 req->buffer = bh->b_data; // 请求项缓冲区指针指向需读写的数据缓冲区 req->waiting = NULL; // 任务等待操作执行完成的地方 req->bh = bh; // 缓冲块头指针 req->next = NULL; // 指向下一请求项 add_request(major+blk_dev,req); // 将请求项加入队列中,相当 (blk_dev[major], req) } void ll_rw_block(int rw, struct buffer_head * bh) { unsigned int major; if ((major=MAJOR(bh->b_dev)) >= NR_BLK_DEV || !(blk_dev[major].request_fn)) { printk("Trying to read nonexistent block-device\n\r"); return; } make_request(major,rw,bh); }
do_hd_request 函数
add_request 函数中 (dev->request_fn)(); 会调用相应的处理函数。对于硬盘此处调用的就是 do_hd_request 函数。其位于文件 kernel/blk_drv/hd.c 中。
void do_hd_request(void) { int i,r = 0; unsigned int block,dev; unsigned int sec,head,cyl; unsigned int nsect; INIT_REQUEST; dev = MINOR(CURRENT->dev); block = CURRENT->sector; if (dev >= 5*NR_HD || block+2 > hd[dev].nr_sects) { end_request(0); goto repeat; } block += hd[dev].start_sect; dev /= 5; __asm__("divl %4":"=a" (block),"=d" (sec):"0" (block),"1" (0), "r" (hd_info[dev].sect)); __asm__("divl %4":"=a" (cyl),"=d" (head):"0" (block),"1" (0), "r" (hd_info[dev].head)); sec++; nsect = CURRENT->nr_sectors; if (reset) { reset = 0; recalibrate = 1; reset_hd(CURRENT_DEV); return; } if (recalibrate) { recalibrate = 0; hd_out(dev,hd_info[CURRENT_DEV].sect,0,0,0, WIN_RESTORE,&recal_intr); return; } if (CURRENT->cmd == WRITE) { hd_out(dev,nsect,sec,head,cyl,WIN_WRITE,&write_intr); for(i=0 ; i<3000 && !(r=inb_p(HD_STATUS)&DRQ_STAT) ; i++) /* nothing */ ; if (!r) { bad_rw_intr(); goto repeat; } port_write(HD_DATA,CURRENT->buffer,256); } else if (CURRENT->cmd == READ) { hd_out(dev,nsect,sec,head,cyl,WIN_READ,&read_intr); } else panic("unknown hd-command"); }
do_hd_request() 是硬盘请求项的操作函数。其操作流程如下:
- 首先判断当前请求项是否存在,若当前请求项指针为空,则说明目前硬盘块设备已经没有待处理的请求项,因此立刻退出程序。这是在宏 INIT_REQUEST 中执行的语句。否则就继续处理当前请求项。
- 对当前请求项中指明的设备号和请求的盘起始扇区号的合理性进行验证;
- 根据当前请求项提供的信息计算请求数据的磁盘磁道号、磁头号和柱面号;
- 如果复位标志(reset)已被设置,则也设置硬盘重新校正标志(recalibrate),并对硬盘执行复位操作,向控制器重新发送"建立驱动器参数"命令(WIN_SPECIFY)。该命令不会引发硬盘中断;
如果重新校正标志被置位的话,就向控制器发送硬盘重新校正命令(WIN_RESTORE),并在发送
之前预先设置好该命令引发的中断中需要执行的C 函数(recal_intr()),并退出。recal_intr() 函数的主要作用是:当控制器执行该命令结束并引发中断时,能重新(继续)执行本函数。
如果当前请求项指定是写操作,则首先设置硬盘控制器调用的 C 函数为 write_intr(),向控制器发送写操作的命令参数块,并循环查询控制器的状态寄存器,以判断请求服务标志(DRQ)是否置位。若该标志置位,则表示控制器已"同意"接收数据,于是接着就把请求项所指缓冲区中的数据写入控制器的数据缓冲区中。若循环查询超时后该标志仍然没有置位,则说明此次操作失败。 于是调用 bad_rw_intr() 函数,根据处理当前请求项发生的出错次数来确定是放弃继续当前请求项还是需要设置复位标志,以继续重新处理当前请求项。
- 如果当前请求项是读操作,则设置硬盘控制器调用的 C 函数为 read_intr(),并向控制器发送读盘操作命令。
hd_out 函数
static void hd_out(unsigned int drive,unsigned int nsect,unsigned int sect, unsigned int head,unsigned int cyl,unsigned int cmd, void (*intr_addr)(void)) { register int port asm("dx"); if (drive>1 || head>15) panic("Trying to write bad sector"); if (!controller_ready()) panic("HD controller not ready"); do_hd = intr_addr; outb_p(hd_info[drive].ctl,HD_CMD); port=HD_DATA; outb_p(hd_info[drive].wpcom>>2,++port); outb_p(nsect,++port); outb_p(sect,++port); outb_p(cyl,++port); outb_p(cyl>>8,++port); outb_p(0xA0|(drive<<4)|head,++port); outb(cmd,++port); }
hd_out() 是硬盘控制器操作命令发送函数。该函数带有一个中断过程中调用的 C 函数指针参数,在向控制器发送命令之前,它首先使用这个参数预置好中断过程中会调用的函数指针(do_hd,例如 read_intr() ),然后它按照规定的方式依次向硬盘控制器 0x1f0 至 0x1f7 端口发送命令参数块,随后就立刻退出函数返回而并不会等待硬盘控制器执行读写命令。除控制器诊断(WIN_DIAGNOSE)和建立驱动器参数(WIN_SPECIFY)两个命令以外,硬盘控制器在接收到任何其他命令并执行了命令以后,都会向 CPU 发出中断请求信号,从而引发系统去执行硬盘中断处理过程(在 system_call.s 中 221 行 hd_interrupt 函数)
注意:此处的中断设置是在 kernel/blk_drv/hd.c 文件中 hd_init 函数中设置的:
void hd_init(void) { blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST; // 设置硬盘的中断处理函数 set_intr_gate(0x2E,&hd_interrupt); outb_p(inb_p(0x21)&0xfb,0x21); outb(inb_p(0xA1)&0xbf,0xA1); }
2、硬盘中断响应处理 hd_interrupt
hd_interrupt 函数在 kernel/system_call.s 中 221 行定义。
hd_interrupt: pushl %eax pushl %ecx pushl %edx push %ds push %es push %fs movl $0x10,%eax mov %ax,%ds mov %ax,%es movl $0x17,%eax mov %ax,%fs movb $0x20,%al outb %al,$0xA0 # EOI to interrupt controller #1 jmp 1f # give port chance to breathe 1: jmp 1f 1: xorl %edx,%edx xchgl do_hd,%edx testl %edx,%edx jne 1f movl $unexpected_hd_interrupt,%edx 1: outb %al,$0x20 call *%edx # 调用hd指向的函数 pop %fs pop %es pop %ds popl %edx popl %ecx popl %eax iret
int 46(int 0x2E)硬盘中断处理程序,响应硬件中断请求 IRQ14 。当请求的硬盘操作完成或出错就会发出此中断信息。然后去变量 do_hd 中的函数指针放入 edx,然后调用响应的函数(read_intr,write_intr,unexpected_hd_interrupt)。
read_intr 函数
static void read_intr(void) { if (win_result()) { bad_rw_intr(); do_hd_request(); return; } port_read(HD_DATA,CURRENT->buffer,256); CURRENT->errors = 0; CURRENT->buffer += 512; CURRENT->sector++; if (--CURRENT->nr_sectors) { do_hd = &read_intr; return; } end_request(1); do_hd_request(); }
read_intr 函数首先调用 win_result 函数,读取控制器的状态寄存器,以判断是否有错误发生。使用 port_read 函数从控制器缓冲区把一个扇区的数据复制到请求项指定的缓冲区中。然后根据当前请求项中指明的欲读扇区总数,判断是否已经读取了所有的数据。若还有数据要读,则退出,以等待下一个中断的到来。若数据已经全部获得,则调用 end_request 函数来处理当前请求项的结束事宜:唤醒等待当前请求项完成的进程、唤醒等待空闲请求项的进程(若有的话)、设置当前请求项所指缓冲区数据已更新标志、释放当前请求项(从块设备链表中删除该项)。最后继续调用 do_hd_request 函数,以继续处理硬盘设备的其他请求项。
四、sys_setup 之二挂载文件系统
代码位于文件 kernel/blk_drv/hd.c 中
int sys_setup(void * BIOS) { // ... rd_load(); // 尝试创建并加载虚拟盘 mount_root(); // 安装根文件系统 return (0); }
读取硬盘分区表后,现在开始尝试创建并加载虚拟盘(rd_load 函数)和安装根文件系统(mount_root 函数)。
跟踪发现系统未启用虚拟盘,因此略过此函数。
1、mount_root 挂载文件系统
mount_root 函数位于 fs/super.c 文件中。其执行流程如下:
其代码如下:
// fs/super.c void mount_root(void) { int i,free; struct super_block * p; struct m_inode * mi; if (32 != sizeof (struct d_inode)) panic("bad i-node size"); for(i=0;i<NR_FILE;i++) file_table[i].f_count=0; if (MAJOR(ROOT_DEV) == 2) { printk("Insert root floppy and press ENTER"); wait_for_keypress(); } for(p = &super_block[0] ; p < &super_block[NR_SUPER] ; p++) { p->s_dev = 0; p->s_lock = 0; p->s_wait = NULL; } if (!(p=read_super(ROOT_DEV))) panic("Unable to mount root"); if (!(mi=iget(ROOT_DEV,ROOT_INO))) panic("Unable to read root i-node"); mi->i_count += 3 ; /* NOTE! it is logically used 4 times, not 1 */ p->s_isup = p->s_imount = mi; current->pwd = mi; current->root = mi; // 我们对根文件系统上的资源做统计工作。统计该设备上空闲块数和空闲i节点数。首先令i等于 // 超级块中表明的设备逻辑块总数。然后根据逻辑块位图中相应比特位的占用情况统计出空闲块数。 // 治理宏函数set_bit()只是在测试比特位,而非设置比特位。 free=0; i=p->s_nzones; while (-- i >= 0) if (!set_bit(i&8191,p->s_zmap[i>>13]->b_data)) free++; printk("%d/%d free blocks\n\r",free,p->s_nzones); free=0; i=p->s_ninodes+1; while (-- i >= 0) if (!set_bit(i&8191,p->s_imap[i>>13]->b_data)) free++; printk("%d/%d free inodes\n\r",free,p->s_ninodes); }
read_super函数
read_super() 用于把指定设备的文件系统的超级块读入到缓冲区中,并登记到超级块表中,同时也把文件系统的 i 节点位图和逻辑块位图读入内存超级块结构的相应数组中。最后并返回该超级块结构的指针。
static struct super_block * read_super(int dev) { struct super_block * s; struct buffer_head * bh; int i,block; if (!dev) return NULL; check_disk_change(dev); if ((s = get_super(dev))) return s; for (s = 0+super_block ;; s++) { if (s >= NR_SUPER+super_block) return NULL; if (!s->s_dev) break; } s->s_dev = dev; s->s_isup = NULL; s->s_imount = NULL; s->s_time = 0; s->s_rd_only = 0; s->s_dirt = 0; // 锁定该超级块,并从设备上读取超级块信息到bh指向的缓冲块中。超级块位于块设备 // 的第2个逻辑块(1号block)中,(第1个是引导盘块)。 lock_super(s); if (!(bh = bread(dev,1))) { s->s_dev=0; free_super(s); return NULL; } *((struct d_super_block *) s) = *((struct d_super_block *) bh->b_data); brelse(bh); if (s->s_magic != SUPER_MAGIC) { s->s_dev = 0; free_super(s); return NULL; } // 下面开始读取设备上i节点位图和逻辑块位图数据。首先初始化内存超级块结构中位图空间。 // 然后从设备上读取i节点位图和逻辑块位图信息,并存放在超级块对应字段中。i节点位图 // 保存在设备上2号块开始的逻辑块中,共占用s_imap_blocks(3)个块。逻辑块位图在i节点 // 位图所在块的后续块中,共占用s_zmap_blocks(8)个块。 for (i=0;i<I_MAP_SLOTS;i++) s->s_imap[i] = NULL; for (i=0;i<Z_MAP_SLOTS;i++) s->s_zmap[i] = NULL; block=2; // 读取设备中i节点位图 for (i=0 ; i < s->s_imap_blocks ; i++) if ((s->s_imap[i]=bread(dev,block))) block++; else break; // 读取设备中逻辑块位图 for (i=0 ; i < s->s_zmap_blocks ; i++) if ((s->s_zmap[i]=bread(dev,block))) block++; else break; if (block != 2+s->s_imap_blocks+s->s_zmap_blocks) { for(i=0;i<I_MAP_SLOTS;i++) brelse(s->s_imap[i]); for(i=0;i<Z_MAP_SLOTS;i++) brelse(s->s_zmap[i]); s->s_dev=0; free_super(s); return NULL; } // 0号i节点不能用的,所以这里将位图中第1块的最低比特位设置为1,以防止文件系统分配0号i节点。 // 同样的道理,也将逻辑块位图的最低位设置为1. s->s_imap[0]->b_data[0] |= 1; s->s_zmap[0]->b_data[0] |= 1; free_super(s); return s; }
super_block结构体
内存中磁盘超级块结构体
struct super_block { unsigned short s_ninodes; // 节点数 unsigned short s_nzones; // 逻辑块数 unsigned short s_imap_blocks; // i 节点位图所占用的数据块数 unsigned short s_zmap_blocks; // 逻辑块位图所占用的数据块数 unsigned short s_firstdatazone; // 第一个数据逻辑块号 unsigned short s_log_zone_size; // log(数据块数/逻辑块)(以2为底) unsigned long s_max_size; // 文件最大长度 unsigned short s_magic; // 文件系统魔数 /* These are only in memory */ struct buffer_head * s_imap[8]; // i 节点位图缓冲块指针数组(占用8块,可表示64M) struct buffer_head * s_zmap[8]; // 逻辑块位图缓冲块指针数组(占用8块) unsigned short s_dev; // 超级块所在的设备号 struct m_inode * s_isup; // 被安装的文件系统根目录的 i 节点(isup-super i) struct m_inode * s_imount; // 被安装到的 i 节点 unsigned long s_time; // 修改时间 struct task_struct * s_wait; // 等待该超级块的进程 unsigned char s_lock; // 被锁定标志 unsigned char s_rd_only; // 只读标志 unsigned char s_dirt; // 已修改(脏)标志 }; // 磁盘上超级块结构,与上面super_block的前面部分相同 struct d_super_block { unsigned short s_ninodes; unsigned short s_nzones; unsigned short s_imap_blocks; unsigned short s_zmap_blocks; unsigned short s_firstdatazone; unsigned short s_log_zone_size; unsigned long s_max_size; unsigned short s_magic; };
iget 函数
iget() 函数用于从设备 dev 上读取指定节点号 nr 的 i 节点,并且把节点的引用计数字段值 i_count 增 1。其操作流程见下图所示。该函数首先判断参数 dev 的有效性,并从 i 节点表中取一个空闲 i 节点。然后扫描 i 节点表,寻找指定节点号 nr 的 i 节点,并递增该 i 节点的引用次数。如果当前扫描的 i 节点的设备号不等于指定的设备号或者节点号不等于指定的节点号,则继续扫描。否则说明已经找到指定设备号和节点号的 i 节点,就等待该节点解锁(如果已上锁的话)。在等待该该节点解锁的阶段,节点表可能会发生变化,此时如果该 i 节点的设备号不等于指定的设备号或者节点号不等于指定的节点号,则需要再次。重新扫描整个 i 节点表。随后把 i 节点的引用十数值增 1,并且判断该 i 节点是否是其他文件系统的安装点。
若该 i 节点是某个文件系统的安装点,则在超级块表中搜寻安装在此 i 节点的超级块。若没有找到相应的超级块,则显示出错信息,并释放函数开始获取的空闲节点,返回该 i 节点指针。若找到了相应的超级块,则将该 i 节点写盘。再从安装在此 i 节点文件系统的超级块上取设备号,并令 i 节点号为 1。然后重新再次扫描整个 i 节点表,来取该被安装文件系统的根节点。
若该 i 节点不是其他文件系统的安装点,则说明已经找到了对应的 i 节点,因此此时可以放弃临时申请的空闲 i 节点,并返回找到的 i 节点指针。
如果在 i 节点表中没有找到指定的 i 节点,则利用前面申请的空闲 i 节点在 i 节点表中建立该节点。并从相应设备上读取该 i 节点信息。返回该 i 节点指针。
iget 函数执行流程:
代码:
// fs/inode.c // 从设备上读取指定节点号的i节点到内存i节点表中,并返回该i节点指针 // 首先在位于高速缓冲区中的i节点表中搜寻,若找到指定节点号的i节点则在经过一些判断 // 处理后返回该i节点指针。否则从设备dev上读取指定i节点号的i节点信息放入i节点表中, // 并返回该i节点指针 struct m_inode * iget(int dev,int nr) { struct m_inode * inode, * empty; if (!dev) panic("iget with dev==0"); empty = get_empty_inode(); inode = inode_table; while (inode < NR_INODE+inode_table) { if (inode->i_dev != dev || inode->i_num != nr) { inode++; continue; } wait_on_inode(inode); if (inode->i_dev != dev || inode->i_num != nr) { inode = inode_table; continue; } inode->i_count++; if (inode->i_mount) { int i; for (i = 0 ; i<NR_SUPER ; i++) if (super_block[i].s_imount==inode) break; if (i >= NR_SUPER) { printk("Mounted inode hasn't got sb\n"); if (empty) iput(empty); return inode; } iput(inode); dev = super_block[i].s_dev; nr = ROOT_INO; inode = inode_table; continue; } if (empty) iput(empty); return inode; } if (!empty) return (NULL); inode=empty; inode->i_dev = dev; inode->i_num = nr; read_inode(inode); return inode; }
m_inode 结构体
m_inode 描述了 i 节点信息
struct m_inode { unsigned short i_mode; // 文件类型和属性(rwx位) unsigned short i_uid; // 用户id(文件拥有者标识符) unsigned long i_size; // 文件大小(字节数) unsigned long i_mtime; // 修改时间(自1970.1.1:0算起,单位秒) unsigned char i_gid; // 组 id(文件拥有者所在的组) unsigned char i_nlinks; // 文件目录项链接数 unsigned short i_zone[9]; // 直接(0-6)、间接(7)或双重间接(8)逻辑块号 /* these are in memory also */ struct task_struct * i_wait; // 等待该i节点的进程 unsigned long i_atime; // 最后访问时间 unsigned long i_ctime; // i节点自身修改时间 unsigned short i_dev; // i节点所在的设备号 unsigned short i_num; // i节点号 unsigned short i_count; // i节点被使用的次数,0表示该i节点空闲 unsigned char i_lock; // 锁定标志 unsigned char i_dirt; // 已修改(脏)标志 unsigned char i_pipe; // 管道标志 unsigned char i_mount; // 安装标志 unsigned char i_seek; // 搜寻标志(lseek时) unsigned char i_update; // 更新标志 }; struct d_inode { unsigned short i_mode; unsigned short i_uid; unsigned long i_size; unsigned long i_time; unsigned char i_gid; unsigned char i_nlinks; unsigned short i_zone[9]; };
read_inode函数
fs/inode.c
#define BLOCK_SIZE 1024 #define INODES_PER_BLOCK ((BLOCK_SIZE)/(sizeof (struct d_inode))) static void read_inode(struct m_inode * inode) { struct super_block * sb; struct buffer_head * bh; int block; lock_inode(inode); if (!(sb=get_super(inode->i_dev))) panic("trying to read inode without dev"); // 第0个引导块,第1个超级块,s_imap_blocks i节点块数, s_zmap_blocks 逻辑块块数 // 节点就位于这些块之后,每块大小为1K,磁盘上i节点占用的空间为sizeof (struct d_inode)(8*8字节) // 所以可知i节点依次放在磁盘上,每块可放16个i节点 block = 2 + sb->s_imap_blocks + sb->s_zmap_blocks + (inode->i_num-1)/INODES_PER_BLOCK; if (!(bh=bread(inode->i_dev,block))) panic("unable to read i-node block"); *(struct d_inode *)inode = ((struct d_inode *)bh->b_data) [(inode->i_num-1)%INODES_PER_BLOCK]; brelse(bh); unlock_inode(inode); }
盘上的 i 节点部分存放着文件系统中文件或目录名的索引节点,每个文件或目录名都有一个 i 节点。每个 i 节点结构中存放着对应文件的相关信息,如文件宿主的 id(uid)、文件所属组 id(gid)、文件长度、访问修改时间以及文件数据块在盘上的位置等。整个结构共使用 32 个字节。
i_mode 字段用来保存文件的类型和访问权限属性。其比特位 15-12 用于保存文件类型,位 11-9 保存执行文件时设置的信息,位 8-0 表示文件的访问权限,见下图所示。
当所有 i 节点都被使用时,查找空闲 i 节点的函数会返回值 0,因此 i 节点位图最低比特位和 i 节点 0 都不使用。i 节点 0 的结构被初始化成全零,并在创建文件系统时将 i 节点 0 的比特位置位。
对于 PC 机来讲,一般以一个扇区的长度( 512 字节 )作为块设备的数据块长度。而 MINIX 文件系
统则将连续的 2 个扇区数据(1024 字节)作为一个数据块来处理,称之为一个磁盘块或盘块。其长度与高速缓冲区中的缓冲块长度相同。编号是从盘上第一个盘块开始算起,也即引导块是 0 号盘块。而上述的逻辑块或区块,则是盘块的 2 的幕次倍数。一个逻辑块长度可以等于 1、2、4 或 8 个盘块长度。对于本书所讨论的 linux 内核,逻辑块的长度等于盘块长度。因此在代码注释中这两个术语含义相同。但是术语数据逻辑块(或数据盘块)则是指盘设备上数据部分中,从第一个数据盘块开始编号的盘块。
作者注:
因此,从上分析可知,通过超级块信息,找到根文件系统 i 节点存放在磁盘上的位置,读入根文件系统 i 节点信息,然后通过i节点中的 i_zone 信息就可以找到根文件下的目录和文件。