编程环境:Ubuntu 20.04、gcc-9.4.0
代码仓库:https://gitee.com/AprilSloan/linux0.11-project
linux0.11源码下载(不能直接编译,需进行修改)
完善文件系统,提供文件的增删改等功能。能够更改文件以及创建删除文件和文件夹。
上一章中,write 函数已经能够写入字符设备中。这一节继续完善 write 函数,对普通文件进行修改。
buffer_head 中的 b_dirt 是用来表示 buffer_head 是否被修改过,同样,我们需要一个成员表示 inode 是否被修改过,同时还要记录修改的时间。m_inode 结构体变为如下形式。
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]; // 文件占用的逻辑块的块号
// zone[0]-zone[6]是直接块号
// zone[7]是一次间接块号
// zone[8]是二次间接块号
/* these are in memory also */
unsigned long i_atime; // 最近访问文件的时间
unsigned long i_ctime; // 修改时间
unsigned short i_dev; // 设备号
unsigned short i_num; // inode号
unsigned short i_count; // 引用计数
unsigned char i_dirt; // 是否修改过
};
write 函数的声明如下:
ssize_t write(int fd, const void *buf, size_t count);
更改普通文件的操作需要注意哪些问题?
**是普通文件吗?**需要判断文件描述符所指代的文件是不是普通文件,是的话就进入下一步。
**从哪里开始更改?**在调用 open 函数时,添加上 O_APPEND 标志代表从文件末尾开始添加内容,不然就是从 f_pos 的位置开始。
**要为文件添加逻辑块吗?**假如,一个文件的大小为 1KB,此时要从文件末尾开始添加10个字节。这就需要为文件分配一个新的逻辑块,并将逻辑块号写入文件的 inode。
搞清楚问题后我们来看看更改普通文件的流程。
// read_write.c
int sys_write(unsigned int fd, char *buf, int count)
{
...
if (S_ISCHR(inode->i_mode)) { // 字符设备文件
return rw_char(WRITE, inode->i_zone[0], buf, count, &file->f_pos);
}
if (S_ISREG(inode->i_mode)) { // 普通文件
return file_write(inode, file, buf, count);
}
printk("(Write)inode->i_mode=%06o\n", inode->i_mode);
return -EINVAL;
}
第8-10行:判断文件是否是普通文件。
// file_dev.c
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;
if (filp->f_flags & O_APPEND) // 在文件末尾添加
pos = inode->i_size;
else // 从当前的位置更改
pos = filp->f_pos;
while (i < count) {
// 找到文件读写指针所在的逻辑块
block = create_block(inode, pos / BLOCK_SIZE);
if (!block)
break;
bh = bread(inode->i_dev, block);
if (!bh)
break;
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 += 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);
}
inode->i_mtime = CURRENT_TIME;
if (!(filp->f_flags & O_APPEND)) {
filp->f_pos = pos;
inode->i_ctime = CURRENT_TIME;
}
return (i ? i : -1);
}
第10-13行:解决了第二个问题。确定了更改的起始地点。
第16-18行:找到文件读写指针所在的逻辑块。如果 f_pos 的值为1024,那么我们需要修改的地方是在文件的第二个逻辑块上。此时,若文件只有一个逻辑块,那么我们需要为文件添加一个逻辑块。
第19-21行:将逻辑块读取到内存中。
第22-27行:计算要向逻辑块写入的字符数。假如,文件只有一个逻辑块,有1020个字节的内容。此时,向文件末尾添加10个字节,那么会向第一个逻辑块写入4个字节,创建第二个逻辑块,向第二个逻辑块写入6个字节。
第28-32行:如果文件的更改导致文件的大小发生变化,需要修改文件的大小。
第34-35行:复制 buf 中的数据到文件缓冲区中。
第36行:该文件缓冲区已无用,释放掉。
第14-37:如果写入的内容跨越多个逻辑块,就会多次循环。
第38-42行:修改 inode 的修改时间,如果读写标志不是 O_APPEND,就更改文件读写指针。
最后返回修改是否成功。
// inode.c
static int _bmap(struct m_inode *inode, int block, int create)
{
struct buffer_head *bh;
int i;
if (block < 0)
panic("_bmap: block < 0");
if (block >= 7 + 512 + 512 * 512) // 7个直接 + 1个一级间接 + 1个二级间接
panic("_bmap: block > big");
// 直接
if (block < 7) {
if (create && !inode->i_zone[block])
if ((inode->i_zone[block] = new_block(inode->i_dev))) {
inode->i_ctime = CURRENT_TIME;
inode->i_dirt = 1;
}
return inode->i_zone[block];
}
// 一级间接
block -= 7;
if (block < 512) {
if (create && !inode->i_zone[7])
if ((inode->i_zone[7] = new_block(inode->i_dev))) {
inode->i_dirt = 1;
inode->i_ctime = CURRENT_TIME;
}
if (!inode->i_zone[7])
return 0;
bh = bread(inode->i_dev, inode->i_zone[7]);
if (!bh)
return 0;
i = ((unsigned short *)(bh->b_data))[block];
if (create && !i)
if ((i = new_block(inode->i_dev))) {
((unsigned short *)(bh->b_data))[block] = i;
bh->b_dirt = 1;
}
brelse(bh);
return i;
}
// 二级间接
block -= 512;
if (create && !inode->i_zone[8])
if ((inode->i_zone[8] = new_block(inode->i_dev))) {
inode->i_dirt = 1;
inode->i_ctime = CURRENT_TIME;
}
if (!inode->i_zone[8])
return 0;
bh = bread(inode->i_dev, inode->i_zone[8]);
if (!bh)
return 0;
i = ((unsigned short *)bh->b_data)[block >> 9];
if (create && !i)
if ((i = new_block(inode->i_dev))) {
((unsigned short *)(bh->b_data))[block >> 9] = i;
bh->b_dirt = 1;
}
brelse(bh);
if (!i)
return 0;
bh = bread(inode->i_dev, i);
if (!bh)
return 0;
i = ((unsigned short *)bh->b_data)[block & 511];
if (create && !i)
if ((i = new_block(inode->i_dev))) {
((unsigned short *)(bh->b_data))[block & 511] = i;
bh->b_dirt = 1;
}
brelse(bh);
return i;
}
int create_block(struct m_inode *inode, int block)
{
return _bmap(inode, block, 1);
}
create_block 调用了 _bmap 函数,参数 create 的值为1。
第13-20行:如果是前7个逻辑块,且文件没有该逻辑块,就为文件添加一个逻辑块,返回逻辑块号。
第23-43行:如果传入的是文件的第8-519个逻辑块,且文件没有一级间接逻辑块。要先为文件添加一个一级间接逻辑块,然后再添加一个用于存放文件数据的逻辑块,将逻辑块号写入一级间接逻辑块中,返回逻辑块号。
第46-78行:如果不是以上两种情况,且文件没有二级间接逻辑块,要先为文件添加一个二级间接逻辑块,然后再添加一个一级间接逻辑块,将逻辑块号写入二级间接逻辑块中,接着再添加一个用于存放文件数据的逻辑块,将逻辑块号写入一级间接逻辑块中,返回逻辑块号。
想要为文件添加逻辑块,我们得找到一个空闲的逻辑块,这需要遍历逻辑块位图。linux0.11 使用内联汇编来寻找位图中的第一个0(空闲逻辑块)所在的位置。
// bitmap.c
#define find_first_zero(addr) ({\
int __res; \
__asm__ __volatile__ ( \
"cld\n" \
"1:lodsl\n\t" \
"notl %%eax\n\t" \
"bsfl %%eax,%%edx\n\t" \
"je 2f\n\t" \
"addl %%edx,%%ecx\n\t" \
"jmp 3f\n" \
"2:addl $32,%%ecx\n\t" \
"cmpl $8192,%%ecx\n\t" \
"jl 1b\n" \
"3:" \
: "=c"(__res) \
: "c"(0), "S"(addr)); \
__res; })
__res 被放在 ecx 中,并被初始化为0。addr 是逻辑块位图的起始地址,被放在 esi 中。
cld
会使 esi 在字符操作中自动递增。
lodsl
从 esi 指向的地址把一个双字(4 字节)加载到 eax 中。因为执行了 cld
,esi 会递增。
notl %%eax
会将 eax 按位取反。
bsfl %%eax,%%edx
会从低位开始扫描 eax,看是否有1的位,若有就在 edx 中保存该位的位号。
je 2f
:如果第8行中,eax 的值为0,则跳转到第12行;否则,继续执行第10行指令。
addl %%edx,%%ecx
计算出第一个0位在逻辑块中的位号。
2:addl $32,%%ecx
:4字节有32位,ecx 保存已经遍历的位数。
cmpl $8192,%%ecx
:一个逻辑块有1024字节,8192个位,将已遍历的位数与逻辑块的位数相比较。
jl 1b
:如果已遍历的位数小于逻辑块的位数,说明还没遍历完逻辑块,继续循环查找;否则就结束了,并没有找到空闲的逻辑块。
在找到第一个0位后,我们将对应的逻辑块给文件,并把这个位置1,表示该逻辑块已被使用。这个操作也是用汇编实现的。
// bitmap.c
#define clear_block(addr) \
__asm__ __volatile__( \
"cld\n\t" \
"rep\n\t" \
"stosl" \
:: "a"(0), "c"(BLOCK_SIZE / 4), "D"((long)(addr)))
#define set_bit(nr, addr) ({ \
register int res; \
__asm__ __volatile__( \
"btsl %2,%3\n\t" \
"setb %%al" \
: "=a" (res) \
: "0"(0), "r"(nr), "m"(*(addr))); \
res; })
clear_block 会将文件缓冲区的数据全部设置为0。分配的逻辑块可能还保留有以前的数据,我们要把这些数据全部清理掉。
在 eax 中放入0,在 ecx 中放入BLOCK_SIZE / 4
,在 edi 中放入文件缓冲区的起始地址。
cld rep stosb
会将 edi 指向的地址中填入 eax 的数据。edi 递增,ecx 递减。若ecx等于0,扫描结束。
set_bit 会将0位置1。nr 是位号,addr 是逻辑块位图的起始地址。
将 res 存入 eax 中并赋值0。
btsl %2,%3
的作用是将从 addr 开始的第 nr 位设置为1。
setb %%al
是把 al 的值置为1。
介绍了内联汇编代码后,就继续说说 new_block 函数。我们要对逻辑块位图操作,就要先找到超级块,我们在 read_super 中将逻辑块位图读入了内存中
// bitmap.c
int new_block(int dev)
{
struct buffer_head *bh;
struct super_block *sb;
int i, j;
sb = get_super(dev);
if (!sb)
panic("trying to get new block from nonexistant device");
j = 8192;
for (i = 0; i < 8; i++) {
bh = sb->s_zmap[i];
if (bh) {
j = find_first_zero(bh->b_data);
if (j < 8192)
break;
}
}
if (i >= 8 || !bh || j >= 8192)
return 0;
if (set_bit(j, bh->b_data))
panic("new_block: bit already set");
bh->b_dirt = 1;
j += i * 8192 + sb->s_firstdatazone - 1;
if (j >= sb->s_nzones)
return 0;
bh = getblk(dev, j);
if (!bh)
panic("new_block: cannot get block");
if (bh->b_count != 1)
panic("new block: count is != 1");
clear_block(bh->b_data);
bh->b_uptodate = 1;
bh->b_dirt = 1;
brelse(bh);
return j;
}
第8-10行:找到设备的超级块。
第11-21行:找到逻辑块位图中的第一个0位。
第22-23行:找到0位对应的逻辑块号。但这里找到的不是真正的逻辑块号,毕竟 set_bit 最多会把 j 设置成8191,而文件系统中可能不止8192个逻辑块。
第25-27行:计算出真正的逻辑块号。逻辑块位图可能不止一个(虽然我们的文件系统只有一个),如果我们是在第二个逻辑块位图中找到0位,就需要加上第一个逻辑块位图的位数。数据区的0号逻辑块对应的是文件系统的 s_firstdatazone - 1 号逻辑块。
第28-36行:读取这个逻辑块,并清理旧数据。
最后返回这个空闲的逻辑块块号。
write 系统调用的部分完成了,是不是可以开始测试了?
先等等,我们先实现 lseek 系统调用。为什么还要写这个系统调用?
我发现,当调用 write 后,文件读写指针发生了改变,再调用 read 就读不了修改的内容。思来想去还是觉得得把 lseek 完成了。
//fs.h
#define IS_SEEKABLE(x) ((x) >= 1 && (x) <= 3)
// unistd.h
#define SEEK_SET 0 // 设置文件读写指针
#define SEEK_CUR 1 // 从当前文件读写指针做偏移
#define SEEK_END 2 // 从文件末尾做偏移
// read_write.c
int sys_lseek(unsigned int fd, off_t offset, int origin)
{
struct file *file;
int tmp;
if (fd >= NR_OPEN)
return -EBADF;
file = current->filp[fd];
if (!(file) || !(file->f_inode) || !IS_SEEKABLE(MAJOR(file->f_inode->i_dev)))
return -EBADF;
switch (origin) {
case 0:
if (offset < 0)
return -EINVAL;
file->f_pos = offset;
break;
case 1:
if (file->f_pos + offset < 0)
return -EINVAL;
file->f_pos += offset;
break;
case 2:
tmp = file->f_inode->i_size + offset;
if (tmp < 0)
return -EINVAL;
file->f_pos = tmp;
break;
default:
return -EINVAL;
}
return file->f_pos;
}
第15-18行:检查传入的参数是否有问题,文件所属设备是否支持 lseek。只有内存、软盘和硬盘才支持 lseek。
第21-25行:SEEK_SET 用于设置文件的读写指针。
第26-30行:SEEK_CUR 用来将文件读写指针加上 offset。
第31-36行:SEEK_END 用来将文件读写指针设置为文件末尾加上 offset。
最后更改 init 函数,做测试。
void init(void)
{
int fd;
static char buf[75] = {0};
setup();
open("/dev/tty0", O_RDWR, 0);
dup(0);
dup(0);
fd = open("/usr/root/hello.c", O_RDWR, 0);
if (fd < 0) {
printf("open file failed!\n");
while (1);
}
write(fd, "#include " , 18);
lseek(fd, 0, SEEK_SET);
read(fd, buf, 74);
printf("%s\n", buf);
close(fd);
while (1);
}
结果如下,可以看到,读取的内容确实发生了变化。如果我们把第14-15行代码注释掉,再运行一次,就会发现文件没有被修改。这是因为我们都只是对文件缓冲区进行修改,并没有将这些数据同步到软盘上。这就是我们下一节的内容了。
这一节实现 sync 系统调用,手动地同步文件内容。那为什么不弄成自动同步文件内容呢?好问题,自动同步我没整出来。
同步文件数据需要注意那些事情?
同步 inode 数据。在之前,我们只会修改 inode 结构体,并没有把结构体数据写回文件缓冲区。
同步文件缓冲区数据。把文件缓冲区数据写回软盘。
inode 成员的互斥访问。之前,我们可以让多个进程同时修改一个 inode 结构体的值,但这是不对的,这会导致 inode 数据出现问题。
要互斥访问 inode,我们得设置与锁相关的变量。
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]; // 文件占用的逻辑块的块号
// zone[0]-zone[6]是直接块号
// zone[7]是一次间接块号
// zone[8]是二次间接块号
/* these are in memory also */
struct task_struct *i_wait; // 等待访问inode的进程
unsigned long i_atime; // 最近访问文件的时间
unsigned long i_ctime; // 修改时间
unsigned short i_dev; // 设备号
unsigned short i_num; // inode号
unsigned short i_count; // 引用计数
unsigned char i_lock; // 互斥锁
unsigned char i_dirt; // 是否修改过
};
用 i_lock 记录 inode 是否被上锁,如果被上锁,就用 i_wait 记录当前进程的 pcb。当 inode 解锁时,通过 i_wait 唤醒进程。
// buffer.c
int sys_sync(void)
{
int i;
struct buffer_head *bh;
sync_inodes();
bh = start_buffer;
for (i = 0; i < NR_BUFFERS; i++, bh++) {
wait_on_buffer(bh); // 等待其他进程读取逻辑块结束
if (bh->b_dirt)
ll_rw_block(WRITE, bh);
}
return 0;
}
可以看到,第8-13行代码会遍历所有的文件缓冲区,如果文件缓冲区被更改过,就将文件缓冲区写回软盘。只有在读取逻辑块时才会对文件缓冲区上锁,使用 wait_on_buffer 是为了等待读取结束。
sync_inodes 函数用于同步 inode 数据,这个操作之后才能同步文件缓冲区数据。
// inode.c
void sync_inodes(void)
{
int i;
struct m_inode *inode;
inode = inode_table;
for (i = 0; i < NR_INODE; i++, inode++) {
wait_on_inode(inode);
if (inode->i_dirt)
write_inode(inode);
}
}
这个函数会遍历所有读入内存的 inode,如果 inode 被修改过,就将它同步到文件缓冲区中。同样,wait_on_inode 会等待其他进程修改 inode。
// inode.c
static void write_inode(struct m_inode *inode)
{
struct super_block *sb;
struct buffer_head *bh;
int block;
lock_inode(inode);
if (!inode->i_dirt || !inode->i_dev) {
unlock_inode(inode);
return;
}
sb = get_super(inode->i_dev);
if (!sb) {
panic("trying to write inode without device");
}
block = 2 + sb->s_imap_blocks + sb->s_zmap_blocks +
(inode->i_num - 1) / INODES_PER_BLOCK;
bh = bread(inode->i_dev, block);
if (!bh) {
panic("unable to read i-node block");
}
((struct d_inode *)bh->b_data)
[(inode->i_num - 1) % INODES_PER_BLOCK] =
*(struct d_inode *)inode;
bh->b_dirt = 1;
inode->i_dirt = 0;
brelse(bh);
unlock_inode(inode);
}
write_inode 会把 inode 数据写到文件缓冲区中,这时不能让其他进程修改 inode 的数据,使用 lock_inode 对 inode 上锁,修改完成后,使用 unlock_inode 解锁。
第9-12行:如果 inode 没有设置脏位或设备号为0,这个 inode 出现问题,无需同步。
第13-22行:获取设备超级块,找到 inode 所在的逻辑块号,并将该逻辑块读取到内存中。
第23-28行:同步 inode 数据到文件缓冲区,设置文件缓冲区的脏位,清除 inode 的脏位。
// inode.c
static inline void wait_on_inode(struct m_inode *inode)
{
cli();
while (inode->i_lock)
sleep_on(&inode->i_wait);
sti();
}
static inline void lock_inode(struct m_inode *inode)
{
cli();
while (inode->i_lock)
sleep_on(&inode->i_wait);
inode->i_lock = 1;
sti();
}
static inline void unlock_inode(struct m_inode *inode)
{
inode->i_lock = 0;
wake_up(&inode->i_wait);
}
这三个函数与 wait_on_buffer、lock_buffer 和 unlock_buffer 相似。
当我们从内存读取 inode 数据或是将 inode 数据写入内存时,都需要上锁,防止数据被破环。
// inode.c
static void read_inode(struct m_inode *inode)
{
struct super_block *sb;
struct buffer_head *bh;
int block;
lock_inode(inode); // 上锁
sb = get_super(inode->i_dev);
if (!sb)
panic("trying to read inode without dev");
block = 2 + sb->s_imap_blocks + sb->s_zmap_blocks + (inode->i_num - 1) / INODES_PER_BLOCK; // 计算inode所在的逻辑块
bh = bread(inode->i_dev, block);
if (!bh)
panic("unable to read i-node block");
__asm__("cld"::);
*(struct d_inode *)inode = ((struct d_inode *)bh->b_data)[(inode->i_num - 1) % INODES_PER_BLOCK];
brelse(bh);
unlock_inode(inode); // 解锁
}
当释放 inode 的时候,可以将 inode 同步到文件缓冲区中。
// inode.c
void iput(struct m_inode *inode)
{
if (!inode)
return;
wait_on_inode(inode);
if (!inode->i_count) {
panic("iput: trying to free free inode");
}
if (inode->i_count > 1) {
inode->i_count--;
return;
}
if (inode->i_dirt) {
write_inode(inode);
}
inode->i_count--;
}
如果没有其他进程在使用 inode,而且 inode 被修改过,那么我们就将 inode 数据同步到文件缓冲区中。
在 get_empty_inode 中也可以将 inode 同步到文件缓冲区中。
struct m_inode *get_empty_inode(void)
{
struct m_inode *inode;
static struct m_inode *last_inode = inode_table;
int i;
do {
inode = NULL;
// 遍历查找未使用的结构体
for (i = NR_INODE; i; i--) {
if (++last_inode >= inode_table + NR_INODE)
last_inode = inode_table;
if (!last_inode->i_count) {
inode = last_inode;
if (!inode->i_dirt && !inode->i_lock)
break;
}
}
// 如果结构体都被使用了
if (!inode) {
for (i = 0; i < NR_INODE; i++)
printk("%04x: %6d\t", inode_table[i].i_dev,
inode_table[i].i_num);
panic("No free inodes in mem");
}
wait_on_inode(inode);
if (inode->i_dirt)
write_inode(inode);
} while (inode->i_count);
memset(inode, 0, sizeof(*inode));
inode->i_count = 1;
return inode;
}
添加了第15行,当 i_count、i_dirt、i_lock 都为0时,才能把 inode 分配出去。
第26-28行也是新添加的,在使用 write_inode 之前使用 wait_on_inode 等待。write_inode 里面明明有 lock_inode,为什么要使用 wait_on_inode 呢?而且,i_wait 只能保存一个进程的 pcb,如果多个进程同时执行 wait_on_inode,一些进程不就死锁了吗?改成链表应该会更好。这个代码有点迷,暂时先这样吧,以后有问题再改,但愿不会是那种不可复现的问题。
最后修改一下 main.c。
// main.c
inline _syscall0(int, sync)
void init(void)
{
int fd;
static char buf[75] = {0};
setup();
open("/dev/tty0", O_RDWR, 0);
dup(0);
dup(0);
fd = open("/usr/root/hello.c", O_RDWR, 0);
if (fd < 0) {
printf("open file failed!\n");
while (1);
}
write(fd, "#include " , 18);
lseek(fd, 0, SEEK_SET);
read(fd, buf, 74);
printf("%s\n", buf);
close(fd);
sync();
while (1);
}
运行看看结果。把第16和17行代码注释掉再跑一次,也会是如下的结果,文件确实同步到软盘中。
文件系统软盘被修改后,我一般是用 vscode 中 git 的放弃更改选项来复原文件系统,这样就可以持续使用了。
创建普通文件的系统调用是 creat,它是通过 sys_open 实现的。
int sys_creat(const char *pathname, int mode)
{
return sys_open(pathname, O_CREAT | O_TRUNC, mode);
}
传入 O_CREAT 代表如果没有该文件,就创建这个文件。
接下来的工作主要还是修改 sys_open 的流程。
**找到文件所在目录的 inode。**这一步与之前一样。
**通过目录的 inode 找到逻辑块,查找是否存在该文件。**如果找到了,那就和之前一样。
如果目录逻辑块中没找到该文件,就获取一个空闲的 inode 和逻辑块,并把 inode 号和文件名写入目录的逻辑块中,把逻辑块号写到 inode 中。
// namei.c
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;
dir = dir_namei(pathname, &namelen, &basename);
if (!dir)
return -ENOENT;
if (!namelen) {
iput(dir);
return -EISDIR;
}
bh = find_entry(&dir, basename, namelen, &de);
if (!bh) {
if (!(flag & O_CREAT)) {
iput(dir);
return -ENOENT; // No such file or directory
}
if (!permission(dir, MAY_WRITE)) {
iput(dir);
return -EACCES; // Permission denied
}
inode = new_inode(dir->i_dev);
if (!inode) {
iput(dir);
return -ENOSPC; // No space left on device
}
inode->i_uid = current->euid;
inode->i_mode = mode;
inode->i_dirt = 1;
bh = add_entry(dir, basename, namelen, &de);
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;
}
...
}
O_ACCMODE 的值是3,O_WRONLY 的值是1,O_RDWR 的值是2。flag & O_ACCMODE
是在判断对文件是否有写操作。如果 flag 中同时有 O_TRUNC 标志位以及 O_RDWR、O_WRONLY 这两个标志位的其中一个,就会把文件长度截断为0。
第10-11行:如果 flag 有 O_TRUNC 标志位,没有 O_RDWR、O_WRONLY 标志位,就给 flag 设置上 O_WRONLY 标志位。这样,我们通过 open(path, O_TRUNC, 0) 就能截断文件(将文件长度变为0)。
mode 参数只在创建文件时有用,是对新文件赋予的访问权限,umask 是文件创建权限屏蔽位。第12行代码会屏蔽掉一些权限。第13行代码指定文件类型为普通文件。
第24-54行:如果没有找到该文件,看看是否需要创建该文件。
第25-28行:如果 flag 没有 O_CREAT 标志位,不需要创建文件,返回错误号。
第29-32行:如果我们对目录没有写权限,就无法创建文件。
第33-37行:在 inode 位图上找到第一个0位,并找到对应的空闲 inode,还要在内存中找到一个空闲的 inode 用于存放软盘上的 inode 的数据。如果没有找到空闲的 inode,说明内存或软盘的 inode 已经用完了。
第38-40行:设置文件的用户,文件类型和对文件的访问权限。设置脏位,以标识该 inode 需要写到文件缓冲区中。
第41-48行:将文件名和 inode 号写到目录中。
// bitmap.c
struct m_inode *new_inode(int dev)
{
struct m_inode *inode;
struct super_block *sb;
struct buffer_head *bh;
int i, j;
inode = get_empty_inode(); // 找到内存中空闲的 inode
if (!inode)
return NULL;
sb = get_super(dev);
if (!sb) {
panic("new_inode with unknown device");
}
j = 8192;
for (i = 0; i < 8; i++) {
bh = sb->s_imap[i];
if (bh) {
j = find_first_zero(bh->b_data);
if (j < 8192)
break;
}
}
if (!bh || j >= 8192 || j + i * 8192 > sb->s_ninodes) {
iput(inode);
return NULL;
}
if (set_bit(j, bh->b_data)) {
panic("new_inode: bit already set");
}
bh->b_dirt = 1;
inode->i_count = 1;
inode->i_nlinks = 1;
inode->i_dev = dev;
inode->i_uid = current->euid;
inode->i_gid = current->egid;
inode->i_dirt = 1;
inode->i_num = j + i * 8192;
inode->i_mtime = inode->i_atime = inode->i_ctime = CURRENT_TIME;
return inode;
}
new_inode 和 new_block 的逻辑有些类似。
第9-11行:找到内存中空闲的 inode。
第12-15行:获取设备的超级块。
第16-28行:找到第一个0位,如果没找到,说明软盘上的 inode 已经用完了。
第29-31行:把0位置1,表示已经被使用了。
第32-40行:设置文件缓冲区的脏位,该缓冲区需要写回软盘。初始化 inode 的成员。
// namei.c
static struct buffer_head *add_entry(struct m_inode *dir,
const char *name, int namelen, struct dir_entry **res_dir)
{
int block, i;
struct buffer_head *bh;
struct dir_entry *de;
*res_dir = NULL;
if (namelen > NAME_LEN)
namelen = NAME_LEN;
if (!namelen)
return NULL;
block = dir->i_zone[0];
if (!block)
return NULL;
bh = bread(dir->i_dev, block);
if (!bh)
return NULL;
i = 0; // 遍历的目录项数量
de = (struct dir_entry *)bh->b_data;
while (1) {
if ((char *)de >= BLOCK_SIZE + bh->b_data) {
brelse(bh);
bh = NULL;
block = create_block(dir, i / DIR_ENTRIES_PER_BLOCK);
if (!block)
return NULL;
bh = bread(dir->i_dev, block);
if (!bh) {
i += DIR_ENTRIES_PER_BLOCK;
continue;
}
de = (struct dir_entry *)bh->b_data;
}
if (i * sizeof(struct dir_entry) >= dir->i_size) {
de->inode = 0;
dir->i_size = (i + 1) * sizeof(struct dir_entry);
dir->i_dirt = 1;
dir->i_ctime = CURRENT_TIME;
}
if (!de->inode) { // 未被使用的目录项
dir->i_mtime = CURRENT_TIME;
for (i = 0; i < NAME_LEN; i++)
de->name[i] = (i < namelen) ? get_fs_byte(name + i) : 0;
bh->b_dirt = 1;
*res_dir = de;
return bh;
}
de++;
i++;
}
brelse(bh);
return NULL;
}
// fs.h
#define DIR_ENTRIES_PER_BLOCK ((BLOCK_SIZE) / (sizeof(struct dir_entry)))
在 add_entry 中,我们必须考虑一种情况:如果目录只有一个逻辑块,有64个文件(一个逻辑块可以存放64个目录项),那么再添加一个文件的话,就需要为目录添加一个逻辑块,在新的逻辑块中写入文件名和 inode 号。
第11-14行:文件长度过长会被截断,文件长度为0就直接返回 NULL。
第15-20行:找到目录的第一个逻辑块,并将它读取到内存中。
第21-53行:遍历目录的逻辑块,找到未被使用的目录项,将文件名写到目录项中。如果所有的目录项都被使用,也不能分配新的逻辑块,就会执行到第54行。
第24-36行:如果遍历一个逻辑块仍未找到空闲的目录项,就读取下一个逻辑块。如果遍历目录的所有逻辑块仍没有找到,就为目录分配一个新的逻辑块。
第37-42行:如果已经遍历所有的目录项,我们会添加一个新的目录项,目录的大小会因此发生改变。目录大小发生改变,就要设置目录 inode 的脏位,需要将数据同步到文件缓冲区中。假如目录中有三个文件,如果删除其中一个,目录的大小不会改变。之后在目录中创建一个新的文件,会把被删除的文件的目录项作为新文件的目录项,目录的大小也不会改变。
第43-50行:目录项未被使用或原本的文件被删除的情况下,目录项的 inode 号为0,我们就在这个目录项中写入文件名。
最后来创建一个文件吧。
void init(void)
{
int fd;
static char buf[75] = {0};
setup();
open("/dev/tty0", O_RDWR, 0);
dup(0);
dup(0);
fd = open("/usr/root/main.c", O_RDWR | O_CREAT, 0);
if (fd < 0) {
printf("create file main.c failed!\n");
while (1);
}
write(fd, "Hello World!", 12);
lseek(fd, 0, SEEK_SET);
read(fd, buf, 12);
printf("file content: %s\n", buf);
close(fd);
sync();
while (1);
}
虽然可以使用 creat 来创建文件,不过一般都是用 open 来创建文件。在 /usr/root/ 下创建一个名为 main.c 的文件,并向其中写入"Hello World!"。结果如下:
将上面的代码改成下面的样子,再次运行,结果相同。这说明我们确实创建了一个新文件。
void init(void)
{
int fd;
static char buf[75] = {0};
setup();
open("/dev/tty0", O_RDWR, 0);
dup(0);
dup(0);
fd = open("/usr/root/main.c", O_RDWR, 0);
if (fd < 0) {
printf("create file main.c failed!\n");
while (1);
}
read(fd, buf, 12);
printf("file content: %s\n", buf);
close(fd);
sync();
while (1);
}
删除文件的系统调用是 unlink。要删除文件,我们应该做哪些工作呢?
链接数代表有多少个目录项指向文件。文件的链接数至少为1,目录的链接数至少为2,链接数为0表示文件被删除。
// namei.c
int sys_unlink(const char *name)
{
const char *basename;
int namelen;
struct m_inode *dir, *inode;
struct buffer_head *bh;
struct dir_entry *de;
dir = dir_namei(name, &namelen, &basename);
if (!dir)
return -ENOENT; // No such file or directory
if (!namelen) { // 文件名长度不能为0
iput(dir);
return -ENOENT;
}
if (!permission(dir, MAY_WRITE)) {
iput(dir);
return -EPERM; // Operation not permitted
}
bh = find_entry(&dir, basename, namelen, &de);
if (!bh) {
iput(dir);
return -ENOENT;
}
inode = iget(dir->i_dev, de->inode);
if (!inode) {
iput(dir);
brelse(bh);
return -ENOENT;
}
if ((dir->i_mode & S_ISVTX) && !suser() &&
current->euid != inode->i_uid &&
current->euid != dir->i_uid) {
iput(dir);
iput(inode);
brelse(bh);
return -EPERM;
}
if (S_ISDIR(inode->i_mode)) {
iput(inode);
iput(dir);
brelse(bh);
return -EPERM;
}
if (!inode->i_nlinks) {
printk("Deleting nonexistent file (%04x:%d), %d\n",
inode->i_dev, inode->i_num, inode->i_nlinks);
inode->i_nlinks = 1;
}
de->inode = 0;
bh->b_dirt = 1;
brelse(bh);
inode->i_nlinks--;
inode->i_dirt = 1;
inode->i_ctime = CURRENT_TIME;
iput(inode);
iput(dir);
return 0;
}
第10-12行:找到文件所在目录的 inode。
第17-20行:如果用户对目录没有写权限,不能删除文件。
第21-25行:找到文件对应的目录项,bh 是文件缓冲区,对应了目录项所在的逻辑块。
第26-31行:读取文件的 inode。
第32-39行:如果设置了S_ISVTX,则除非是所有者或者超级管理员,其他人无法删除或重命名文件夹及下面的文件。
第40-45行:unlink 只能删除文件,不能删除目录,删除目录要使用 rmdir 系统调用。
第46-50行:i_nlinks 如果为0表示文件已经被删除。我们不可能对已经删除的文件再删除一次,这明显是出现了什么问题。为了方便继续操作,将 i_nlinks 设置为1。
第51-53行:将目录项的 inode 设置为0,表示这个目录项已经空闲。因为对文件缓冲区做了修改,需要设置文件缓冲区的脏位。之后不会再使用该文件缓冲区,将其释放掉。
第54-57行:删除普通文件时,i_nlinks 减1。当 i_nlinks 为0时,我们需要将文件逻辑块对应的逻辑块位图中的位清零。这个操作放在 iput 进行。
// inode.c
void iput(struct m_inode *inode)
{
...
if (!inode->i_nlinks) {
truncate(inode);
free_inode(inode);
return;
}
if (inode->i_dirt) {
write_inode(inode);
}
inode->i_count--;
}
truncate 会将文件逻辑块对应的逻辑块位图中的位清零。
free_inode 会将文件 inode 对应的 inode 位图中的位清零。
// bitmap.c
void free_inode(struct m_inode *inode)
{
struct super_block *sb;
struct buffer_head *bh;
if (!inode)
return;
if (!inode->i_dev) {
memset(inode, 0, sizeof(*inode));
return;
}
if (inode->i_count > 1) {
printk("trying to free inode with count=%d\n", inode->i_count);
panic("free_inode");
}
if (inode->i_nlinks)
panic("trying to free inode with links");
sb = get_super(inode->i_dev);
if (!sb)
panic("trying to free inode on nonexistent device");
if (inode->i_num < 1 || inode->i_num > sb->s_ninodes)
panic("trying to free inode 0 or nonexistant inode");
bh = sb->s_imap[inode->i_num >> 13];
if (!bh) {
panic("nonexistent imap in superblock");
}
if (clear_bit(inode->i_num & 8191, bh->b_data))
printk("free_inode: bit already cleared.\n\r");
bh->b_dirt = 1;
memset(inode, 0, sizeof(*inode));
}
第13-16行:i_count 的值应该为1,不然在 iput 中就不会执行到 free_inode。
第17-18行:i_nlinks 的值应该为0,不然在 iput 中就不会执行到 free_inode。
第22-27行:检验 inode 号的取值范围是否正确。读取位图的逻辑块。
第28-29行:将 inode 对应的位清零。如果这个位原本就是零,那就有问题了。
第30行:因为修改了位图,而位图在文件缓冲区上,需要设置文件缓冲区的脏位。
第31行:将 inode 清空,一边反复使用。
// bitmap.c
#define clear_bit(nr, addr) ({ \
register int res; \
__asm__ __volatile__( \
"btrl %2,%3\n\t" \
"setnb %%al" \
: "=a"(res) \
: "0"(0), "r"(nr), "m"(*(addr))); \
res; })
将 res 存放在 eax 中,并初始化为0。
btrl %2,%3
将 addr 地址开始的第 nr 个位的值存储到 EFLAGS.CF 中,然后把该位置零。
setnb %%al
用于根据 EFLAGS.CF 设置 al。如果 CF=1 则 al=0,否则 al=1。
// truncate.c
void truncate(struct m_inode *inode)
{
int i;
if (!(S_ISREG(inode->i_mode) || S_ISDIR(inode->i_mode)))
return;
for (i = 0; i < 7; i++) // 直接逻辑块
if (inode->i_zone[i]) {
free_block(inode->i_dev, inode->i_zone[i]);
inode->i_zone[i] = 0;
}
free_ind(inode->i_dev, inode->i_zone[7]); // 一级间接逻辑块
free_dind(inode->i_dev, inode->i_zone[8]); // 二级间接逻辑块
inode->i_zone[7] = inode->i_zone[8] = 0;
inode->i_size = 0; // 文件长度设置为0
inode->i_dirt = 1;
inode->i_mtime = inode->i_ctime = CURRENT_TIME;
}
对于直接逻辑块,我们可以利用 free_block 快速地将位图的位清零,但是对于一级间接逻辑块和二级间接逻辑块,我们需要进行遍历,把逻辑块对应的位一个一个清零。
// truncate.c
static void free_ind(int dev, int block)
{
struct buffer_head *bh;
unsigned short *p;
int i;
if (!block)
return;
bh = bread(dev, block);
if (bh) {
p = (unsigned short *)bh->b_data;
for (i = 0; i < 512; i++, p++)
if (*p)
free_block(dev, *p);
brelse(bh);
}
free_block(dev, block);
}
对于一级间接逻辑块,需要读取逻辑块,一个逻辑块号占2个字节,一个逻辑块中有512个逻辑块号,需要把所有的逻辑块的位都置为0。最后还要把一级间接逻辑块对应的位置为0。
// truncate.c
static void free_dind(int dev, int block)
{
struct buffer_head *bh;
unsigned short *p;
int i;
if (!block)
return;
bh = bread(dev, block);
if (bh) {
p = (unsigned short *)bh->b_data;
for (i = 0; i < 512; i++, p++)
if (*p)
free_ind(dev, *p);
brelse(bh);
}
free_block(dev, block);
}
对于二级间接逻辑块,需要调用 free_ind 将一级间接逻辑块及以下的逻辑块位置零,最后把二级间接逻辑块对应的位置为0。
// bitmap.c
void free_block(int dev, int block)
{
struct super_block *sb;
struct buffer_head *bh;
sb = get_super(dev);
if (!sb) {
panic("trying to free block on nonexistent device");
}
if (block < sb->s_firstdatazone || block >= sb->s_nzones) {
panic("trying to free block not in datazone");
}
bh = get_hash_table(dev, block);
if (bh) {
if (bh->b_count != 1) {
printk("trying to free block (%04x:%d), count=%d\n",
dev, block, bh->b_count);
return;
}
bh->b_dirt = 0;
bh->b_uptodate = 0;
brelse(bh);
}
block -= sb->s_firstdatazone - 1;
if (clear_bit(block & 8191, sb->s_zmap[block / 8192]->b_data)) {
printk("block (%04x:%d) ", dev, block + sb->s_firstdatazone - 1);
panic("free_block: bit already cleared");
}
sb->s_zmap[block / 8192]->b_dirt = 1;
}
第11-13行:检查 block 参数是否合法。
第14行:如果逻辑块没有被读到文件缓冲区,get_hash_table 会返回 NULL;相反,则会返回一个地址。
第15-24行:如果逻辑块被读到文件缓冲区,复原 b_dirt 和 b_uptodate,表示文件缓冲区中无数据,最后释放 bh。
第25-29行:计算逻辑块在位图中的位置,清零该位。
第30行:因为修改了逻辑块位图,设置文件缓冲区的脏位。
最后修改 init 函数,进行测试。
// main.c
void init(void)
{
int fd;
setup();
open("/dev/tty0", O_RDWR, 0);
dup(0);
dup(0);
unlink("/usr/root/hello.c");
fd = open("/usr/root/hello.c", O_RDWR, 0);
if (fd < 0) {
if (errno & ENOENT)
printf("No such file or directory\n");
}
sync();
while (1);
}
第一次运行后,把第9行注释掉再次运行,可以得到一样的结果,说明我们确实删掉了 hello.c。
这一节是对目录的操作。增删目录与增删文件的操作差不多。
int sys_mkdir(const char *pathname, int mode)
{
const char *basename;
int namelen;
struct m_inode *dir, *inode;
struct buffer_head *bh, *dir_block;
struct dir_entry *de;
if (!suser()) // 只有超级用户有权限创建目录
return -EPERM;
// 读取目标目录的上一级目录的inode
dir = dir_namei(pathname, &namelen, &basename);
if (!dir)
return -ENOENT;
if (!namelen) { // 目录名长度不能为0
iput(dir);
return -ENOENT;
}
if (!permission(dir, MAY_WRITE)) { // 需要对上一级目录有写权限
iput(dir);
return -EPERM;
}
bh = find_entry(&dir, basename, namelen, &de); // 查找目录项
if (bh) {
brelse(bh);
iput(dir);
return -EEXIST; // File exists
}
inode = new_inode(dir->i_dev); // 为目录找到一个空闲inode
if (!inode) {
iput(dir);
return -ENOSPC;
}
inode->i_mtime = inode->i_atime = CURRENT_TIME;
inode->i_zone[0] = new_block(inode->i_dev); // 为目录分配逻辑块
if (!inode->i_zone[0]) {
iput(dir);
inode->i_nlinks--;
iput(inode);
return -ENOSPC;
}
dir_block = bread(inode->i_dev, inode->i_zone[0]); // 将逻辑块读入内存
if (!dir_block) {
iput(dir);
free_block(inode->i_dev, inode->i_zone[0]);
inode->i_nlinks--;
iput(inode);
return -ERROR;
}
inode->i_size = 32;
de = (struct dir_entry *)dir_block->b_data;
de->inode = inode->i_num;
strcpy(de->name, "."); // 设置当前目录
de++;
de->inode = dir->i_num;
strcpy(de->name, ".."); // 设置上一级目录
inode->i_nlinks = 2; // 有两个目录项指向该文件
dir_block->b_dirt = 1;
brelse(dir_block);
// 设置文件类型和访问权限
inode->i_mode = I_DIRECTORY | (mode & 0777 & ~current->umask);
inode->i_dirt = 1;
// 将目录添加到上一级目录的目录项中
bh = add_entry(dir, basename, namelen, &de);
if (!bh) {
iput(dir);
free_block(inode->i_dev, inode->i_zone[0]);
inode->i_nlinks = 0;
iput(inode);
return -ENOSPC;
}
de->inode = inode->i_num;
bh->b_dirt = 1;
dir->i_nlinks++; // 目标目录有目录项指向上一级目录
dir->i_dirt = 1;
iput(dir);
iput(inode);
brelse(bh);
return 0;
}
第10-11行:只有超级用户能够创建目录,这很奇怪,因为我们平时不会使用sudo mkdir
来创建目录,用户应该也能够创建目录才对。
第20-23行:需要对上一级目录有写权限。因为创建目录会更改上一级目录的目录项,必须有写权限。
第23-28行:如果找到了目标目录,bh 就是文件缓冲区的地址,这就说明系统中已经存在这个文件,我们不需要再创建了。
第50-56行:为什么要把文件大小设置成32字节呢?我们需要向刚创建的目录添加两个目录项,一个目录项16字节,两个就是32字节了。这两个目录项,一个是".“,代表当前目录,一个是”…",代表上一级目录。
第57行:i_nlinks 表示有多个目录项指向该文件。目录中有一个"."目录项指向自己,上一级目录有一个目录项指向该目录,所以是两个。
第64-71行:将目录添加到上一级目录的目录项中。如果添加失败,需要将分配的 inode 和逻辑块都重新置为空闲状态。
第72-75行:目录项添加成功后,对上一级目录做处理。
// namei.c
int sys_rmdir(const char *name)
{
const char *basename;
int namelen;
struct m_inode *dir, *inode;
struct buffer_head *bh;
struct dir_entry *de;
if (!suser()) // 只有超级用户有权限创建目录
return -EPERM;
dir = dir_namei(name, &namelen, &basename);
if (!dir)
return -ENOENT;
if (!namelen) { // 目录名长度不能为0
iput(dir);
return -ENOENT;
}
if (!permission(dir, MAY_WRITE)) { // 需要对上一级目录有写权限
iput(dir);
return -EPERM;
}
bh = find_entry(&dir, basename, namelen, &de); // 查找目录项
if (!bh) {
iput(dir);
return -ENOENT;
}
inode = iget(dir->i_dev, de->inode); // 找到目录的inode
if (!inode) {
iput(dir);
brelse(bh);
return -EPERM;
}
if ((dir->i_mode & S_ISVTX) && current->euid &&
inode->i_uid != current->euid) {
iput(dir);
iput(inode);
brelse(bh);
return -EPERM;
}
if (inode->i_dev != dir->i_dev || inode->i_count > 1) {
iput(dir);
iput(inode);
brelse(bh);
return -EPERM;
}
if (inode == dir) { // 不能使用"rmdir ."的形式删除目录
iput(inode);
iput(dir);
brelse(bh);
return -EPERM;
}
if (!S_ISDIR(inode->i_mode)) { // 必须是目录
iput(inode);
iput(dir);
brelse(bh);
return -ENOTDIR; // Not a directory
}
if (!empty_dir(inode)) { // 目录中不能有文件
iput(inode);
iput(dir);
brelse(bh);
return -ENOTEMPTY; // Directory not empty
}
if (inode->i_nlinks != 2)
printk("empty directory has nlink!=2 (%d)", inode->i_nlinks);
de->inode = 0; // 该文件被删除
bh->b_dirt = 1;
brelse(bh);
inode->i_nlinks = 0;
inode->i_dirt = 1;
dir->i_nlinks--;
dir->i_ctime = dir->i_mtime = CURRENT_TIME;
dir->i_dirt = 1;
iput(dir);
iput(inode);
return 0;
}
rmdir 和 mkdir 的前一部分是一样的。
第34-40行:如果设置了S_ISVTX,则除非是所有者或者超级管理员,其他人无法删除或重命名文件夹及下面的文件。
第47-52行:不能使用"rmdir ."的形式删除当前目录,但是可以通过"rmdir …/dir"的形式删除当前目录。
第65-66行:当目录为空时,指向该目录的目录项只有两个。
// namei.c
static int empty_dir(struct m_inode *inode)
{
int nr, block;
int len;
struct buffer_head *bh;
struct dir_entry *de;
len = inode->i_size / sizeof(struct dir_entry);
if (len < 2 || !inode->i_zone[0]) {
printk("warning - bad directory on dev %04x\n", inode->i_dev);
return 0;
}
bh = bread(inode->i_dev, inode->i_zone[0]);
if (!bh) {
printk("warning - bad directory on dev %04x\n", inode->i_dev);
return 0;
}
de = (struct dir_entry *)bh->b_data;
if (de[0].inode != inode->i_num || !de[1].inode ||
strcmp(".", de[0].name) || strcmp("..", de[1].name)) {
printk("warning - bad directory on dev %04x\n", inode->i_dev);
return 0;
}
nr = 2;
de += 2;
while (nr < len) {
if ((void *)de >= (void *)(bh->b_data + BLOCK_SIZE)) {
brelse(bh);
block = bmap(inode, nr / DIR_ENTRIES_PER_BLOCK);
if (!block) {
nr += DIR_ENTRIES_PER_BLOCK;
continue;
}
bh = bread(inode->i_dev, block);
if (!bh)
return 0;
de = (struct dir_entry *)bh->b_data;
}
if (de->inode) {
brelse(bh);
return 0;
}
de++;
nr++;
}
brelse(bh);
return 1;
}
第19-24行:第一个目录项是当前目录,第一个目录项 inode 号必须与当前目录的 inode 号相同,文件名是".“。第二个目录项是上一级目录,第二个目录项的 inode 号不能为0,文件名是”…"。有一个不对,就报错并返回。
第25-26行:我们从第3个目录项开始检查目录中是否存在文件。
第28-39行:当遍历完一个逻辑块后,使用 bmap 找到下一个逻辑块,并读入内存,继续循环检查。
这一节只需要看懂这三个函数,其他函数之前都讲过了。
最后修改 init 函数。
// main.c
inline _syscall2(int, mkdir, const char *, _path, mode_t, mode)
inline _syscall1(int, rmdir, const char *, pathname)
void init(void)
{
int res;
setup();
open("/dev/tty0", O_RDWR, 0);
dup(0);
dup(0);
res = mkdir("/sixsixsix", 0666);
if (res < 0) {
printf("mkdir failed!\n");
}
rmdir("/sixsixsix");
sync();
while (1);
}
运行结果为:
那怎么判断创建、删除目录是否成功呢?使用 bless 或 ghex 打开 rootimage,搜索 sixsixsix,如下所示。我们确实在根目录创建了 sixsixsix 目录。它的 inode 号为0,说明这个目录已经被删除了。
最后来修复系统存在的 bug。
首先是 find_entry。find_entry 会在目录逻辑块中查找文件名。我们之前的代码只会查找第一个逻辑块,而不是查找目录的所有逻辑块。
// namei.c
static struct buffer_head *find_entry(struct m_inode **dir,
const char *name, int namelen, struct dir_entry **res_dir)
{
...
i = 0;
de = (struct dir_entry *)bh->b_data;
while (i < entries) {
if ((char *)de >= BLOCK_SIZE + bh->b_data) {
brelse(bh);
bh = NULL;
block = bmap(*dir, i / DIR_ENTRIES_PER_BLOCK);
if (!block) {
i += DIR_ENTRIES_PER_BLOCK;
continue;
}
bh = bread((*dir)->i_dev, block);
if (!bh) {
i += DIR_ENTRIES_PER_BLOCK;
continue;
}
de = (struct dir_entry *)bh->b_data;
}
if (match(namelen, name, de)) {
*res_dir = de;
return bh;
}
de++;
i++;
}
brelse(bh);
return NULL;
}
这次添加了第9-23行代码。第9行是在判断是否遍历完一个逻辑块,如果是,就读取目录的下一个逻辑块。bmap 函数会依次遍历目录的直接逻辑块和间接逻辑块,直至遍历完所有的目录项。
第二处在 open_namei 函数中。
int open_namei(const char *pathname, int flag, int mode, struct m_inode **res_inode)
{
...
inode->i_atime = CURRENT_TIME;
if (flag & O_TRUNC)
truncate(inode);
*res_inode = inode;
return 0;
}
如果使用了 O_TRUNC 标志,调用 open 函数打开文件时会将文件原本的内容全部丢弃,文件大小变为 0。这就需要用到 truncate 函数,我们之前在删除文件的时候也用到了这个函数。它会把文件逻辑块对应逻辑块位图的位清零,并把 inode 的 i_size 设置为0。
如果文件缓冲区被用了怎么办?我们只能等待其他进程释放文件缓冲区,但是,选择等哪个文件缓冲区还是有讲究的。我们根据 b_dirt 和 b_lock 两个参数判断等待哪个文件缓冲区。
下面的 BADNESS 就体现了这一点,BADNESS 越小越好。
// buffer.c
#define BADNESS(bh) (((bh)->b_dirt << 1) + (bh)->b_lock)
struct buffer_head *getblk(int dev, int block)
{
struct buffer_head *tmp, *bh;
repeat:
bh = get_hash_table(dev, block);
if (bh)
return bh;
tmp = free_list;
do {
if (tmp->b_count)
continue;
if (!bh || BADNESS(tmp) < BADNESS(bh)) {
bh = tmp;
if (!BADNESS(tmp))
break;
}
tmp = tmp->b_next_free;
} while (tmp != free_list);
if (!bh) {
sleep_on(&buffer_wait);
goto repeat;
}
wait_on_buffer(bh);
if (bh->b_count)
goto repeat;
while (bh->b_dirt) {
sync_dev(bh->b_dev);
wait_on_buffer(bh);
if (bh->b_count)
goto repeat;
}
if (find_buffer(dev, block))
goto repeat;
bh->b_count = 1; // 读取一个逻辑块
bh->b_dirt = 0; // 数据还未被修改
bh->b_uptodate = 0; // 还未加载数据
remove_from_queues(bh); // 从链表中移除
bh->b_dev = dev;
bh->b_blocknr = block;
insert_into_queues(bh); // 插入到链表末尾
return bh;
}
如果 get_hash_table 返回 NULL,执行到第15行时,!bh
判断为真后就不会判断逻辑或后面的语句,而是直接运行第16行代码。如果这个文件缓冲区既没有设置脏位也没有上锁,就跳出 while 循环。否则我们需要遍历所有的文件缓冲区,找到一个 BADNESS 最小的文件缓冲区。
出了 while 循环后,文件缓冲区未上锁的情况下才能继续执行,如果 b_count 不为0,说明有进程还在使用这个文件缓冲区,我们需要重新找一个。如果文件缓冲区设置了脏位,sync_dev 会将数据写回软盘,等待写回完成后,如果有进程在使用文件缓冲区,还是需要重新找一个。运行到第35行,如果逻辑块已经被读入到内存中,那又要重新选。咦?既然读到了内存中,直接用不就行了,为什么要重选?因为这是其他进程读入内存的,多个进程同时对同一个缓冲区进行更改,容易造成数据错乱。
当运行到37行时,我们终于找到了一个未上锁,未被更改,未被使用的文件缓冲区。
// buffer.c
int sync_dev(int dev)
{
int i;
struct buffer_head *bh;
bh = start_buffer;
for (i = 0; i < NR_BUFFERS; i++, bh++) {
if (bh->b_dev != dev)
continue;
wait_on_buffer(bh);
if (bh->b_dev == dev && bh->b_dirt)
ll_rw_block(WRITE, bh);
}
sync_inodes();
bh = start_buffer;
for (i = 0; i < NR_BUFFERS; i++, bh++) {
if (bh->b_dev != dev)
continue;
wait_on_buffer(bh);
if (bh->b_dev == dev && bh->b_dirt)
ll_rw_block(WRITE, bh);
}
return 0;
}
这个函数中,首先将所有设置脏位的文件缓冲区写回软盘,再将 inode 同步到文件缓冲区,最后将文件缓冲区写回软盘。那为什么不干脆先把 inode 同步到文件缓冲区,再将文件缓冲区写回软盘?
这里执行两次同步操作是为了提高内核效率。我不太懂为什么能提高效率。让我动手写的话,我估计会把第7-14行删掉。
可以看到,只有当文件缓冲区用完的情况下,才会自动把文件缓冲区的数据同步到软盘。这功能有点鸡肋。
我的博客写得不怎么好,有些东西自己解释不通,就只是讲解代码,而不是更具有启发式的理解。比如 wait_on_inode 这个函数,我只讲了它应该出现在哪里,但我解释不了它为什么在那里,其他地方为什么不需要用这个函数。
下一章是可执行文件加载,a.out 格式已经被淘汰了,与其学这个,不如学学 elf 格式的可执行文件。我准备把 linux1.2 的 execve 代码搬到 linux0.11 中,不知道多久能弄好。系统需要提供一个的静态链接库以供编译,这部分可以把 glibc 搬过来。另外还需要一些可执行文件,比如 sh,ls 等等,这部分可以用 busybox 的代码。