
文件系统目录和文件操作
文件系统namei.c源文件是用来处理文件系统名和节点关系,文件的一些操作如打开关闭等
一个文件只对应一个inode
目录项结构体
struct dir_entry {
unsigned short inode; // i 节点
char name[NAME_LEN]; // 文件名
};
inode:这个文件/子目录自己的 i 节点号。name:这个文件/子目录的名字。
文件系统目录和文件的操作一切都是围绕着dir_entry和inode两个转的
检测文件读写访问许可权限permission
/**
* @brief 检测文件读写访问许可权限
*
* @param inode i节点
* @param mask 访问属性屏蔽码
* @return int 1-访问许可,0-不允许访问
*/
static int permission(struct m_inode * inode,int mask)
{
int mode = inode->i_mode;
/* special case: not even root can read/write a deleted file */
if (inode->i_dev && !inode->i_nlinks)
return 0;
else if (current->euid==inode->i_uid)
mode >>= 6;
else if (current->egid==inode->i_gid)
mode >>= 3;
if (((mode & mask & 0007) == mask) || suser())
return 1;
return 0;
}
unsigned short i_mode; // 文件类型和属性(rwx 位)
16位数 包含:
文件类型 当前用户权限 用户组权限 其他用户权限
3位(RWX) 3位(RWX) 3位(RWX)
i_mode中关于权限的设置一共占据了低9位
函数首先是判断如果文件删除了那么就不能读取文件
然后如果当前进程是这个文件所有者进程,那么就把mode向右移6位,这样最低的三位就是当前用户权限
如果当前进程是这个文件所有者进程的同一组进程,那么就把mode向右移3位,这样最低三位就是用户组权限
最后去比较,mode&0007也就是取mode低三位,然后和mask作比较,如果结果等于mask那么就说明是有权限的;或者当前用户是超级用户也是有权限
在指定目录查找匹配目录项find_entry
/**
* @brief 在指定的目录中查找一个与名字匹配的目录项
*
* @param dir 指定目录的i节点
* @param name 文件夹名
* @param namelen 文件名长度
* @param res_dir 返回的目录项结构指针
* @return struct buffer_head* 缓冲头指针
*/
static struct buffer_head * find_entry(struct m_inode ** dir,
const char * name, int namelen, struct dir_entry ** res_dir) // 寻找当前文件夹下的某个子文件夹
这个函数是用来寻找当前传入的文件夹下的某个子目录的
查看可容纳目录数
entries = (*dir)->i_size / (sizeof (struct dir_entry)); // 计算本目录中目录项项数 entries(当前文件夹下文件个数)
读取目录项中的高速缓冲区
if (!(block = (*dir)->i_zone[0]))
return NULL;
if (!(bh = bread((*dir)->i_dev,block))) // 从节点所在设备读取指定的目录项数据块
return NULL;
搜索匹配的目录
// 在目录项数据块中搜索匹配指定文件名的目录项,首先让 de 指向数据块,并在不超过目录中目录项数
// 的条件下,循环执行搜索。
i = 0;
de = (struct dir_entry *) bh->b_data;
while (i < entries) {
if ((char *)de >= BLOCK_SIZE+bh->b_data) {
brelse(bh);
bh = NULL;
if (!(block = bmap(*dir,i/DIR_ENTRIES_PER_BLOCK)) ||
!(bh = bread((*dir)->i_dev,block))) {
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;
使用一个dir_entry结构体de去指向高速缓冲区的一块目录项,然后依次遍历通过match函数比较指定目录名和当前目录项名是否一样,相同则返回这个目录项对应的高速缓冲区和目录项结构体
这里的if ((char *)de >= BLOCK_SIZE+bh->b_data)是指如果de指向的超过了本块高速缓冲区内容,那么就需要更新bh指向下一块高速缓冲区
往指定目录添加一个目录add_entry
/**
* @brief 往指定目录中添加一文件目录项
*
* @param dir 指定目录
* @param name 待添加的文件夹名
* @param namelen 文件名长度
* @param res_dir 返回的目录项结构指针
* @return struct buffer_head* 缓冲头指针
*/
static struct buffer_head * add_entry(struct m_inode * dir,
const char * name, int namelen, struct dir_entry ** res_dir)
其实往目录添加一个新的目录和上面的寻找一个匹配目录都差不多。核心思路就是使用一个*dir_entry结构体指针指向这个目录的逻辑块一个块中的一个目录项*
然后遍历去寻找是否当前目录项空闲(inode为空),如果为空就占据这个目录项
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;
if (!(bh = bread(dir->i_dev,block))) {
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;
这里需要注意的是if (i*sizeof(struct dir_entry) >= dir->i_size)表示的是如果de走到的位置超过了当前目录所记载的目录大小,那么就会对当前目录扩容
搜索指定路径名的目录get_dir
/**
* @brief 搜寻指定路径名的目录
*
* @param pathname 路径名
* @return struct m_inode* 目录的 i 节点指针。失败时返回 NULL
*/
static struct m_inode * get_dir(const char * pathname)
这个函数支持相对路径和绝对路径查找
查看是从什么路径开始
char c;
const char * thisname;
struct m_inode * inode;
struct buffer_head * bh;
int namelen,inr,idev;
struct dir_entry * de;
if (!current->root || !current->root->i_count)
panic("No root inode");
if (!current->pwd || !current->pwd->i_count)
panic("No cwd inode");
if ((c=get_fs_byte(pathname))=='/') { // 取得第一个字符,如果是'/',则说明路径为绝对路径
inode = current->root; // 根节点开始
pathname++;
} else if (c)
inode = current->pwd; // 当前目录开始
else
return NULL; /* empty name is bad */
inode->i_count++; // 将取得的 i 节点引用计数增 1
在最前面先去查看当前进程的根目录和当前工作目录是不是合法的,如果不是就直接报错
然后去查看传入的路径名的第一个字符是不是/,如果是就说明使用的绝对路径查找,把inode指针指向根目录,否则就是绝对路径,把inode指针指向当前目录
注意:如果是根目录那么pathname会++跳过根目录/
自顶向下查找目标目录
这里寻找目标目录使用的是一个while大循环
while (1) {
thisname = pathname;
if (!S_ISDIR(inode->i_mode) || !permission(inode,MAY_EXEC)) {
iput(inode);
return NULL;
}
// 从路径名开始起搜索检测字符,直到字符已是结尾符(NULL)或者是'/',此时 namelen 正好是当前处理
// 目录名的长度。如果最后也是一个目录名,但其后没有加'/',则不会返回该最后目录的 i 节点!
// 比如:/var/log/httpd,将只返回 log/目录的 i 节点。
for(namelen=0;(c=get_fs_byte(pathname++))&&(c!='/');namelen++)
/* nothing */ ;
if (!c) // 若字符是结尾符 NULL,则表明已经到达指定目录,则返回该 i 节点指针,-----退出/返回----
return inode;
if (!(bh = find_entry(&inode,thisname,namelen,&de))) { // 寻找子文件夹/子目录项, 放入de
iput(inode);
return NULL;
}
// 取该子目录项的 i 节点号 inr 和设备号 idev,释放包含该目录项的高速缓冲块和该 i 节点
inr = de->inode;
idev = inode->i_dev;
brelse(bh);
iput(inode);
if (!(inode = iget(idev,inr))) // 更新inode,进入子目录; 将dev目录文件的i节点保存在inode_table[32]的指定表项内
return NULL;
}
最主要的就是for(namelen=0;(c=get_fs_byte(pathname++))&&(c!='/');namelen++)这个for循环,以注释的/var/log/httpd路径为例
- 第一次for循环退出是在
c等于第一个/也就是var目录的时候,这时候c不为空,并且*thisname指向的就是pathname+0也就是var/log/httpd*,然后在for循环中*namelen = 3*,所以最后传入find_entry是var;通过find_entry返回var目录的dir_entry并且获取得到它的inode,进入下一次循环 - 第二次for循环退出是在
c等于第二个/也就是log目录的时候,thisname指向的就是pathname+4也就是log/httpd
通过上述循环过程直到c到达字符串结尾,就说明进入到了指定目录,这时候直接返回目录即可
dir_namei
/**
* @brief 返回指定目录名的 i 节点指针,以及在最顶层目录的名称
*
* @param pathname 指定目录路径名
* @param namelen 返回的路径名长度
* @param name 返回的最顶层目录的名称
* @return struct m_inode* 返回的最顶层目录的i节点指针
*/
static struct m_inode * dir_namei(const char * pathname,
int * namelen, const char ** name)
{
char c;
const char * basename;
struct m_inode * dir;
if (!(dir = get_dir(pathname)))
return NULL;
basename = pathname;
while (c=get_fs_byte(pathname++))
if (c=='/')
basename=pathname;
*namelen = pathname-basename-1;
*name = basename;
return dir;
}
1.以路径 /var/log/httpd 为例,实际执行结果:
- 返回值(
dir):/var/log目录的 i 节点指针。 - 输出参数
name:指向字符串"httpd"。 - 输出参数
namelen:值为5(即"httpd"的长度)。
也就是说,dir_namei 把路径拦腰斩断:
- 左边部分(
/var/log)作为目录返回; - 右边最后一部分(
httpd)作为“名称”通过参数带出来。
dir_namei 函数实际返回的是路径中“最后一个 / 之后的名称”,也就是我们常说的**“基本名称”(basename)或“最终路径分量”**。而它返回的 struct m_inode * 指针,则是这个“最终名称”所在的父目录的 i 节点。
2. 路径 /var/log/httpd 的处理(无结尾 /)
- 指针遍历字符串,遇到
/时更新basename指向其后的字符。 - 当遇到最后一个
/(即httpd前面的那个)时,basename被设置为指向字符'h'。 - 继续读取
h t t p d,直到遇到字符串结束符\0,循环退出。 - 此时
pathname指向\0之后的地址,basename指向'h'。 - 计算长度:
(pathname - basename - 1),也就是('h','t','t','p','d','\0')这 6 个字节减去 1,结果为 5。
3. 路径 /var/log/httpd/ 的处理(有结尾 /)
- 指针遍历,遇到最后一个
/(即httpd后面的那个)时,关键点来了:get_fs_byte(pathname++)读取到字符'/',条件成立。- 执行
basename = pathname;,此时pathname已经自增,指向了该/之后的字符,即字符串结束符\0。
- 继续循环,读取到
\0,循环退出。此时pathname指向\0之后的地址。 - 此时
basename指向的是\0这个空字符的地址。 - 计算长度:
(pathname - basename - 1),因为pathname在\0后面一个字节,basename在\0处,两者相差 1,所以1 - 1 = 0。
获取指定路径名的i节点namei
/*
* namei()
*
* is used by most simple commands to get the inode of a specified name.
* Open, link etc use their own routines, but this is enough for things
* like 'chmod' etc.
*/
/**
* @brief 取指定路径名的 i 节点
*
* @param pathname 路径名
* @return struct m_inode* 对应的 i 节点
*/
struct m_inode * namei(const char * pathname)
这个函数既可以获取文件也可以获取目录的inode
1.首先通过dir_namei获取传入路径名中前置目录路径的inode,以及指定文件/目录的名字
if (!(dir = dir_namei(pathname,&namelen,&basename))) // 首先查找指定路径的最顶层目录的目录名及其 i 节点
return NULL;
if (!namelen) /* special case: '/usr/' etc */
return dir;
2.通过得到的目录inode和要寻找的文件/目录名称使用find_entry寻找到对应目录项,获得dir_entry
3.通过dir_entry的inode项(这个不是m_inode而是i节点号),以及设备号传入iget获取得到指定文件/目录的inode
open_namei
open_namei是namei的高级版,基本上所有的打开文件操作都是调用的open_namei
它支持传入flag和mode参数,所以支持对文件做有权限的操作比如如果没有该文件是否打开该文件
/**
* @brief 打开 namei
*
* @param pathname 文件路径名
* @param flag 文件打开标志
* @param mode 文件访问许可属性
* @param res_inode 返回的i节点指针
* @return int 0-成功,others-错误码
*/
int open_namei(const char * pathname, int flag, int mode,
struct m_inode ** res_inode)
但是它和namei的流程基本上是类似的:
1.首先通过dir_namei获取传入路径名中前置目录路径的inode,以及指定文件/目录的名字
2.通过得到的目录inode和要寻找的文件/目录名称使用find_entry寻找到对应目录项,获得dir_entry
不同的是:如果find_entry寻找失败说明当前没有这个文件/目录,那么就会根据传入的flag和mode参数进行文件的创建
bh = find_entry(&dir,basename,namelen,&de); // 在 dir 节点对应的目录中取文件名对应的目录项结构 de 和该目录项所在的高速缓冲区。
if (!bh) {
if (!(flag & O_CREAT)) {
iput(dir);
return -ENOENT;
}
if (!permission(dir,MAY_WRITE)) {
iput(dir);
return -EACCES;
}
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;
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;
}
注意:这个函数返回的inode节点对于/var/log/httpd和/var/log/httpd/都是一样的
因为在函数开始dir_namei解析路径参数的时候
- 如果是
/var/log/httpd则返回/var/log的inode,并且把顶层目录httpd传递出来 - 如果是
/var/log/httpd/那么返回的就是/var/log/httpd的inode节点
然后下面的find_entry函数对于在/var/log寻找httpd如果找不到就会去创建然后返回httpd的inode
如果是对于/var/log/httpd那么就是找到返回的httpd的inode
创建一个文件节点sys_mknod
/**
* @brief 创建一个特殊文件或普通文件节点。由mod和dev指定文件系统节点
*
* @param filename 路径名
* @param mode 指定使用许可+所创建节点类型
* @param dev 设备号
* @return int 0-成功,others-错误号
*/
int sys_mknod(const char * filename, int mode, int dev)
获取上层目录和要创建文件名
const char * basename;
int namelen;
struct m_inode * dir, * inode;
struct buffer_head * bh;
struct dir_entry * de;
if (!suser())
return -EPERM;
if (!(dir = dir_namei(filename,&namelen,&basename)))
return -ENOENT;
if (!namelen) {
iput(dir);
return -ENOENT;
}
if (!permission(dir,MAY_WRITE)) {
iput(dir);
return -EPERM;
}
首先是通过dir_namei获取要在哪个文件目录下创建,返回这个文件目录inode和要创建的文件名称.if (!namelen)表示如果是/var/log/httpd/这样的不指定文件名的直接返回
当然这里面也有一些权限检查
排重
bh = find_entry(&dir,basename,namelen,&de);
//如果当前目录存在了一个名字相同的dir_entry就说明重名了,直接返回
if (bh) {
brelse(bh);
iput(dir);
return -EEXIST;
}
使用find_entry是否当前目录已经有同名文件,如果有则返回
创建新inode节点
inode = new_inode(dir->i_dev);
if (!inode) {
iput(dir);
return -ENOSPC;
}
inode->i_mode = mode;
//如果是块设备或者字符型设备,inode节点第一个直接块号i_zone[0]存放设备号
if (S_ISBLK(mode) || S_ISCHR(mode))
inode->i_zone[0] = dev;
inode->i_mtime = inode->i_atime = CURRENT_TIME;
inode->i_dirt = 1;
将当前文件名加入上层目录dir_entry
bh = add_entry(dir,basename,namelen,&de);
if (!bh) {
iput(dir);
inode->i_nlinks=0;
iput(inode);
return -ENOSPC;
}
inode节点和dir_entry节点映射
//对刚创建完成的inode节点和dir_entry节点进行映射
de->inode = inode->i_num;
bh->b_dirt = 1;
iput(dir);
iput(inode);
brelse(bh);
return 0;
创建一个文件目录sys_mkdir
/**
* @brief 创建目录
*
* @param pathname 路径名
* @param mode 目录使用的权限属性
* @return int 0-成功,Others-错误码
*/
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;
if (!(dir = dir_namei(pathname,&namelen,&basename)))
return -ENOENT;
//不能传入/dev/ 这种目录名字为空的情况
if (!namelen) {
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;
}
创建新的inode节点
inode = new_inode(dir->i_dev);
if (!inode) {
iput(dir);
return -ENOSPC;
}
//当前版本默认文件最多32
inode->i_size = 32;
inode->i_dirt = 1;
inode->i_mtime = inode->i_atime = CURRENT_TIME;
if (!(inode->i_zone[0]=new_block(inode->i_dev))) {
iput(dir);
inode->i_nlinks--;
iput(inode);
return -ENOSPC;
}
inode->i_dirt = 1;
这里给当前要创建文件目录的inode节点的第一块直接映射块分配一个新的逻辑块号
建立自己的inode和dir_entry映射
读取第一块直接映射块
if (!(dir_block=bread(inode->i_dev,inode->i_zone[0]))) {
iput(dir);
free_block(inode->i_dev,inode->i_zone[0]);
inode->i_nlinks--;
iput(inode);
return -ERROR;
}
de = (struct dir_entry *) dir_block->b_data;
这里去读取刚刚分配好的逻辑块,然后用de去指向逻辑块第一个dir_entry条目
建立自己与本层和上层目录的映射
//做inode节点和第一个dir_entry节点的映射
//给第一个的dir_entry的inode节点号赋值为当前创建目录的inode节点号
de->inode=inode->i_num;
strcpy(de->name,".");
de++;
//给第一个的dir_entry的inode节点号赋值为上一层目录的inode节点号
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;
这里i_nlinks为2是因为当前创建的目录自身以及上层目录都挂载到了当前目录
在上层目录建立当前创建目录的dir_entry
//在上层目录创建一个当前目录的dir_entry
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;
检查目录是否为空
/*
* routine to check that the specified directory is empty (for rmdir)
*/
/**
* @brief 检查指定目录是否是空的
*
* @param inode 指定目录的i节点指针
* @return int 1-空,0-不空/异常报错
*/
static int empty_dir(struct m_inode * inode)
1.首先是检查目录结构是否完整
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] ||
!(bh=bread(inode->i_dev,inode->i_zone[0]))) {
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;
}
2.循环遍历dir_entry,如果有一项目录项不为空就返回0表示非空
如果遍历完则说明目录项都是空的,返回1
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;
}
if (!(bh=bread(inode->i_dev,block)))
return 0;
de = (struct dir_entry *) bh->b_data;
}
if (de->inode) {
brelse(bh);
return 0;
}
de++;
nr++;
}
brelse(bh);
return 1;
if (!(bh=bread(inode->i_dev,block)))这里返回0是表示异常报错,并不是目录为非空的意思
删除指定目录
// 删除指定名称的目录
int sys_rmdir(const char * name)
这个函数用于删除指定的目录,并且必须是空目录
1.在前面依然是通过dir_namei去获取得到该目录所在的上层目录,以该指定目录的名称
然后通过find_entry在上层目录寻找basename的目录
最后通过找到的dir_entry中的i节点号调用iget得到这个目录的i节点
const char * basename;
int namelen;
struct m_inode * dir, * inode;
struct buffer_head * bh;
struct dir_entry * de;
if (!suser())
return -EPERM;
if (!(dir = dir_namei(name,&namelen,&basename)))
return -ENOENT;
if (!namelen) {
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;
}
if (!(inode = iget(dir->i_dev, de->inode))) {
iput(dir);
brelse(bh);
return -EPERM;
2.查看当前目录的权限,当前删除这个目录进程的权限
查看该目录是否和父目录在一个设备上
查看传入的参数是不是.
查看当前inode对应的是否是目录
查看当前目录是否是空目录
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) { /* we may not delete ".", but "../dir" is ok */
iput(inode);
iput(dir);
brelse(bh);
return -EPERM;
}
if (!S_ISDIR(inode->i_mode)) {
iput(inode);
iput(dir);
brelse(bh);
return -ENOTDIR;
}
if (!empty_dir(inode)) {
iput(inode);
iput(dir);
brelse(bh);
return -ENOTEMPTY;
}
3.进行当前目录和父目录的索引清理
if (inode->i_nlinks != 2)
printk("empty directory has nlink!=2 (%d)",inode->i_nlinks);
//将父目录中的对应dir_entry中的inode节点号置空
de->inode = 0;
bh->b_dirt = 1;
brelse(bh);
//清理当前目录的链接数
inode->i_nlinks=0;
inode->i_dirt=1;
//父目录链接数-1,并且更新修改时间
dir->i_nlinks--;
dir->i_ctime = dir->i_mtime = CURRENT_TIME;
dir->i_dirt=1;
iput(dir);
iput(inode);
return 0;
硬链接和软链接
**硬链接(Hard Link)和软链接(Symbolic Link / Soft Link)**的本质区别在于:硬链接是同一个文件的多个“名字”,而软链接是一个指向“另一个名字”的独立小文件。
1. 硬链接(Hard Link)—— 在目录中“复印”名字
- 实现机制(对应
sys_link函数):硬链接不会创建新的inode。它只是在某个目录中新增一个dir_entry(目录项),并将这个新目录项的inode数字,设置为指向已有的那个inode。 - 本质:多个文件名(可以散落在不同目录下)指向硬盘上的同一个
m_inode结构体。 - 关键表现:
- 链接计数(
i_nlinks)增加:每次创建硬链接,inode->i_nlinks++。删除文件时,只有i_nlinks变为 0,物理数据块才会被释放(你之前看到的iput逻辑)。 - 无法跨设备:由于
inode号只在设备(分区)内唯一,硬链接不能跨越文件系统(sys_link中检查了dir->i_dev != oldinode->i_dev)。 - 不能给目录创建硬链接:为了防止目录树出现循环导致死循环,
sys_link直接拒绝了S_ISDIR(这也就是为什么ls -l看到的普通目录链接数总是大于等于 2,因为子目录里的..算作父目录的一个硬链接)。
- 链接计数(
2. 软链接(Symbolic Link)—— 创建一个“快捷方式”文件
- 实现机制(虽不在你贴出的代码中,但内核通过
sys_symlink实现):它会创建全新的inode(占用一个新的 inode 号),并将这个inode的数据块(i_zone)里存放一个字符串,这个字符串就是目标文件的路径(比如/var/log/httpd)。 - 本质:它是一个特殊类型的普通文件(文件类型标志为
I_SYMLINK),文件内容就是你输入的路径文本。 - 关键表现:
- 独立的
inode:它有自己的i_size(路径长度)、自己的权限(通常为 0777)和自己的数据块。 - 可以跨设备:因为它存的是字符串路径,不依赖 inode 号,所以可以指向其他分区的文件。
- 可以指向目录:没有循环风险,因为判断循环是在路径解析时由上层逻辑处理,而非内核直接禁止。
- “断链”风险:如果目标文件被删除了,软链接依然存在,但读取时会报错(
ENOENT)。
- 独立的
sys_unlink
// 删除文件名以及可能也删除其相关的文件
// 从文件系统删除一个名字。如果是一个文件的最后一个连接,并且没有进程正打开该文件,
// 则该文件也将被删除,并释放所占用的设备空间
int sys_unlink(const char * name)
1.最开始仍然是通过路径解析最后获取得到指定文件名文件的inode
if (!(dir = dir_namei(name,&namelen,&basename)))
return -ENOENT;
if (!namelen) {
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;
}
if (!(inode = iget(dir->i_dev, de->inode))) {
iput(dir);
brelse(bh);
return -ENOENT;
}
2.进行一些文件权限检查
查看是否可以删除,是否是超级用户,当前进程id是否和文件所有者id相同,是否和目录所有者id相同
是否是目录,如果是目录那么不支持直接返回
查看链接数是不是0,0表示已经被删除了,可能是当后台打开时候前台删除了文件,这时候还是会有一份保留
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;
}
3.设置inode和dir_entry相应位,用来删除文件
//将父目录中的对应dir_entry中的inode节点号置空
de->inode = 0;
bh->b_dirt = 1;
brelse(bh);
//清理当前目录的链接数
inode->i_nlinks--;
inode->i_dirt = 1;
inode->i_ctime = CURRENT_TIME;
//iput内部会调用清空文件数据函数
iput(inode);
iput(dir);
return 0;
文件数据清空函数
在iput函数内有
if (!inode->i_nlinks) { // 链接释放
truncate(inode);
free_inode(inode);
return;
}
这里如果i_nlinks为0就会调用truncate函数
//// 将节点对应的文件长度截为 0,并释放占用的设备空间。
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++) // 释放 i 节点的 7 个直接逻辑块,并将这 7 个逻辑块项全置零
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;
inode->i_dirt = 1;
inode->i_mtime = inode->i_ctime = CURRENT_TIME;
}
其中的释放一次二次间接函数如下:
// 释放一次间接块
static void free_ind(int dev,int block)
{
struct buffer_head * bh;
unsigned short * p;
int i;
if (!block)
return;
if (bh=bread(dev,block)) {
p = (unsigned short *) bh->b_data;
for (i=0;i<512;i++,p++)
if (*p)
free_block(dev,*p); // === key ===
brelse(bh);
}
free_block(dev,block);
}
// 释放二次间接块
static void free_dind(int dev,int block)
{
struct buffer_head * bh;
unsigned short * p;
int i;
if (!block)
return;
if (bh=bread(dev,block)) {
p = (unsigned short *) bh->b_data;
for (i=0;i<512;i++,p++)
if (*p)
free_ind(dev,*p); // === key ===
brelse(bh);
}
free_block(dev,block);
}
sys_link
/**
* @brief 为一个已经存在的文件创建一个新链接(hard link)
*
* @param oldname 原路径名
* @param newname 新的路径名
* @return int 0-成功,others-错误码
*/
int sys_link(const char * oldname, const char * newname)
这里的链接是创建的硬链接
1.首先也是先去解析oldname,获取oldname的inode
然后解析newname,解析出来所要创建在哪个路径,以及获得basename
oldinode=namei(oldname);
if (!oldinode)
return -ENOENT;
if (S_ISDIR(oldinode->i_mode)) {
iput(oldinode);
return -EPERM;
}
dir = dir_namei(newname,&namelen,&basename);
if (!dir) {
iput(oldinode);
return -EACCES;
}
if (!namelen) {
iput(oldinode);
iput(dir);
return -EPERM;
}
2.如果要创建文件的目录和原来文件所在不是一个设备则报错,并且去检查权限
if (dir->i_dev != oldinode->i_dev) {
iput(dir);
iput(oldinode);
return -EXDEV;
}
if (!permission(dir,MAY_WRITE)) {
iput(dir);
iput(oldinode);
return -EACCES;
}
3.在要创建的目录中去寻找是否有同名文件了,如果有了就返回,没有就在目录下创建一个新的dir_entry
bh = find_entry(&dir,basename,namelen,&de);
if (bh) {
brelse(bh);
iput(dir);
iput(oldinode);
return -EEXIST;
}
bh = add_entry(dir,basename,namelen,&de);
if (!bh) {
iput(dir);
iput(oldinode);
return -ENOSPC;
}
4.新的dir_entry存储的inode节点号要和原本文件的一致
并且要将原本文件的inode的引用链接++
de->inode = oldinode->i_num;
bh->b_dirt = 1;
brelse(bh);
iput(dir);
oldinode->i_nlinks++;
oldinode->i_ctime = CURRENT_TIME;
oldinode->i_dirt = 1;
iput(oldinode);
return 0;
