AIX 内核的文件操作流程 Author: sinister
Email: sinister@whitecell.org
Homepage:http://www.whitecell.org
Date: 2005-11-16
2005-11-16
在 UNIX 操作系统中,不管是对文件的操作,还是对设备及进程的操作,都被视为
对文件的操作。可见文件操作在 UNIX 系统中所站的重要位置,本文以 AIX 操作系统
为主介绍 AIX 内核对文件的操作流程。
在 AIX 操作系统中,当用户想要对磁盘上的文件进行操作,首先调用相应的系统调
用如:open() / read() / write(),系统调用会触发 Power PC 的硬件指令 svca,在
svca 指令触发时r2 寄存器标明了当前系统调用的功能号,功能号用来区分具体操作
类型。当根据系统调用从应用层切换到内核层时,在系统调用表(system call table)
中调用相应的派发函数。(本文主要以介绍内核层文件操作为主,以上只是简单介绍下
流程,如果想了解其中更多细节,请参阅其它文章)内核将逻辑文件路径通过 虚拟文
件系统框架 转换成 vnode/gnode 结构,由 vnode/gnode 结构调用下层实际文件系统的
文件操作函数完成对文件的操作。我们先来看下什么是 vnode/gnode结构?简单来说 vn
ode/gnode 结构是 虚拟文件系统框架(Virtual File System) 提供的对上层系统调用
(System call)及下层实际文件系统对文件操作的一个统一接口。目的是为了让系统调
用不必关心下层实际文件系统所用的操作类型。如下层实际文件系统操作是 JFS,那么
它对文件的操作是由于 inode 来控制,如果是procfs 那么则是由 prnode 来控制。而
这些下层接口是通过 vnode/gnode 统一起来提供给上层的。可以说一个内核的 vnode就
代表一个文件。对一个文件,进程或设备的打开、读、写、删除操作在内核来看也就是
对一个相关 vnode 的操作。下面我们先来看下 vnode 的结构。
struct vnode {
ushort v_flag; /* see definitions below */
ulong v_count; /* the use count of this vnode */
int v_vfsgen; /* generation number for the vfs */
Simple_lock v_lock; /* lock on the structure */
struct vfs *v_vfsp; /* pointer to the vfs of this vnode */
struct vfs *v_mvfsp; /* pointer to vfs which was mounted over this */
/* vnode; NULL if no mount has occurred */
struct gnode *v_gnode; /* ptr to implementation gnode */
struct vnode *v_next; /* ptr to other vnodes that share same gnode */
struct vnode *v_vfsnext; /* ptr to next vnode on list off of vfs */
struct vnode *v_vfsprev; /* ptr to prev vnode on list off of vfs */
union v_data {
void * _v_socket; /* vnode associated data */
struct vnode * _v_pfsvnode; /* vnode in pfs for spec */
} _v_data;
char * v_audit; /* ptr to audit object */
};
#define v_socket _v_data._v_socket
#define v_pfsvnode _v_data._v_pfsvnode
#define V_ROOT 0x01 /* vnode is the root of a vfs */
#ifdef _SUN
#define VROOT V_ROOT
#endif /* _SUN */
#define V_INTRANSIT 0x04 /* vnode is midway through */
/* vfs_vget processing */
#define V_DMA 0x08 /* buffer bypass */
#define V_TEXT 0x10 /* currently being executed */
#define V_RMDC 0x20 /* Usable by remote directory */
/* cache */
#define V_RENAME 0x40 /* Rename is in process */
#define V_LOCK 0x80 /* Serialize exec's */
#define V_SPEC 0x100 /* vnode for a specfs object */
vnode 结构中的 v_flag 代表当前 vnode 属性标志,如当前 vnode 是一个 vfs 对象
的根节点则为 V_ROOT,如是 SPEC 文件系统的 vnode 则为 V_SPEC。远程文件系统目
录则为 V_RMDC。v_count 代表 vnode 的引用记数,也就是这个文件被引用的次数。
内核的 VNOP_HOLD 宏是用来保持 vnode 的,在每次对 vnode 操作前都应该用 VNOP_
HOLD 宏来保持 vnode 的有效性,操作完后再用 VNOP_RELE 宏来释放,其实这两个宏
就是对 vnode->v_count 进行加/减操作。如果 VNOP_RELE 发现 vnode->v_count 等于
0 时,才会真正的释放这个 vnode。vnode 结构中的 v_vfsgen 我观察总是为 0 ?
没有很明白它的用途。v_lock 是一个锁对象,对 vnode 操作时需要锁定当前vnode 则
调用 simple_lock_init() 相关函数来初始化 v_lock 对象。v_vfsp 是一个 vfs 对象,
它代表当前 vnode 属于哪个虚拟文件系统。v_mvfsp 也是一个 vfs 对象,它表明当前
vnode 是否是一个虚拟文件系统的安装节点,NULL 则不是安装节点,否则即代表一个
安装节点。vnode 结构中的v_vfsp 和 vmvfsp 是与 vfs结构中的 vfs_vnodes 和 vfs_
mntdover 是相互对应的,这样可以很方便的通过一个 vnode 找到它所在的 vfs,同样
可以找到一个 vfs 所有的 vnode 及安装的节点。v_next 代表下一个 vnode,他们都是
共用当前的 gnode 对象。v_vfsnext 与 v_vfsprev 组成一个双项链,表明当前 vnode
所在 vfs 对象中的下一个 vnode。 v_data 应该是专门对应远程文件系统特有属性的,
如 nfs 的 rnode,从一个 vnode 得到 rnode 可以采用 vtor 宏实现,vtor 宏定义如
下:#define vtor(vp)((struct rnode *)((vp)->v_data))。而非远程文件系统特
有属性则不是使用 vnode 中的 v_data,这个在下面介绍 gnode 结构中会讲到。v_aud
it 字段是用来审计当前 vnode 操作的,如create,exec,link 等,会调用相关的 au
d_vn_xxx 函数来执行审计。可以通过 AUDITOBJ 宏将 v_audit 指向一个审计对象。这
些操作依赖于 AIX 内核将审计功能打开。为了更好的结合下文讲解 gnode 结构,我们
将 v_gnode 放在最后来讲,v_gnode 是一个 gnode 结构,它是个很重要的结构,包含
了指向对文件实际操作的函数和各类计数,通过上面对 vnode 结构的分析,我们可以发
现 vnode 中除标明一些类型和连接所在的 vfs 外,并没有针对文件的操作函数和各类
计数,这点上与 SOLARIS 内核的 vnode 是不同的。那么这些文件操作函数和各类计数
正是由 gnode 对象所提供的。之所以又抽象出一层 gnode 对象,是为了与 gfs 对象
结合起来,gfs 对象中的 gn_ops 正是 vnode->v_gnode->gn_ops,这样一个 gfs 对象
不单可以操作与文件系统相关函数,也可以操作具体文件。( gfs相关知识见 《AIX
内核的虚拟文件系统框架》)下面我们来看下 gnode 结构。
gnode 结构
struct gnode {
enum vtype gn_type; /* type of object: VDIR,VREG,... */
short gn_flags; /* attributes of object */
ulong gn_seg; /* segment into which file is mapped */
long gn_mwrcnt; /* count of map for write */
long gn_mrdcnt; /* count of map for read */
long gn_rdcnt; /* total opens for read */
long gn_wrcnt; /* total opens for write */
long gn_excnt; /* total opens for exec */
long gn_rshcnt; /* total opens for read share */
struct vnodeops *gn_ops;
struct vnode *gn_vnode; /* ptr to list of vnodes per this gnode */
dev_t gn_rdev; /* for devices, their "dev_t" */
chan_t gn_chan; /* for devices, their "chan", minor's minor */
Simple_lock gn_reclk_lock; /* lock for filocks list */
int gn_reclk_event; /* event list for file locking */
struct filock *gn_filocks; /* locked region list */
caddr_t gn_data; /* ptr to private data (usually contiguous) */
};
vnode 类型
enum vtype { VNON, VREG, VDIR, VBLK, VCHR, VLNK, VSOCK, VBAD, VFIFO, VMPC };
gnode 中的枚举类型 gn_type标明当前文件或设备的类型,VNON 未知类型,VREG 普通
磁盘文件类型,VDIR 目录,VBLK 块设备,VCHR 字符设备,VLNK 连接,VSOCK 网络套
接字,VFIFO FIFO类型,VBAD 坏的 vnode 块,VMPC 多元字符设备。gn_flags 代表当
前 gnode 的操作模式与是否进行审计,如当 GNF_TCB 标志与审计标志为真时会调用相
关的审计函数进行记录。gn_seg 是当前文件映射标志,它是相关内存页面的一个 ID。
当分配/编辑/删除一个内存页面时都需要用到这个标志。gn_mwrcnt,mrdcnt 是读/写
计数器,用于记录当前文件被读/写的次数,但这两个值只在以映射方式操作时才会进
行加/减,对这两个值的操作一般是下层实际文件系统的文件操作函数来控制的,如 J
FS 的 jfs_map() 等函数。gn_rdcnt 是记录读文件总次数的字段,gn_wrcnt是记录写
文件总次数的字段,gn_excnt 是记录执行文件总次数的字段,gn_rshcnt 是记录共享读
文件总次数的字段。gn_rdcnt,gn_wrcnt,gn_excnt,gn_excnt,gn_rshcnt这些字段
只有在以非映射的方式操作文件的情况下才会被更新,也就是说只有在如 JFS 的 jfs
_open() 函数调用时才会根据操作标志来加/减相对应的字段,而 jfs_map() 函数则不
会修改以上字段。gn_vnode 标明当前 gnode 所在的 vnode,它跟上面介绍 vnode 中
的 v_gnode 字段含义一样,一个是通过 vnode 找到下面的 gnode,一个是通过 gnode
找到所属的 vnode。 gn_rdev 字段是当前 vnode/gnode的设备类型编号,如字符设备
VCHR,块设备 VBLK,都会从下层实际文件系统的inode 中取 i_rdev 字段来填充这个值
,如果当前不是设备类型,则用 inode 中的 i_dev 字段来填充,还有如 VFIFO 类型,
这个字段则会被设置成 NODEVICE。gn_chan 字段也是跟设备相关的,它用于处理 VMPC
类型的设备。gn_reclk_lock 是一个锁结构,是专门为 gn_filocks 所提供的。gn_re
clk_event 也是为操作 gn_filocks 时所提供的事件。那么我们看下 gn_filocks 到底
是什么?gn_filocks 是一个 filock 结构,它是一个文件锁结构,包含了很多信息状态
,如锁的状态,类型,vfs 类型,进程ID 等。系统通过 common_reclock() 函数来获
得和检查当前状态。gn_data 类型是个很重要的类型,上面在讲 vnode 结构时提到过其
中的 v_data 联合是用来专门对应远程文件系统特有属性的,那么 gnode 中的 gn_data
则是用来对应除远程文件系统外所有本地实际文件系统特有属性的,如 JFS 中用到的
inode 类型,从一个 gnode 得到一个 inode 可以采用 GTOIP 宏实现,GTOIP 宏定义
如下:#define GTOIP(x)((struct inode *)(((struct gnode *)(x))->gn_data)),通
过 GTOIP 宏可以看到正是由 gn_data 来实现的。为了更好的结合下文,我们将 gn_op
s 放在最后来讲。在上面介绍 vnode 结构中我们已经简单介绍过 gn_ops 字段的用途,
gn_ops 是一个 vnodeops 结构,它指向了虚拟文件系统中所有与文件操作相关的函数,
且这些函数与下层实际文件系统的文件操作函数关联。虚拟文件系统层的文件操作函数
如下:
gn_ops 中所指向的文件操作函数
struct vnodeops {
/* creation/naming/deletion */
int (*vn_link)(struct vnode *, struct vnode *, char *,
struct ucred *);
int (*vn_mkdir)(struct vnode *, char *, int, struct ucred *);
int (*vn_mknod)(struct vnode *, caddr_t, int,
dev_t, struct ucred *);
int (*vn_remove)(struct vnode *, struct vnode *, char *,
struct ucred *);
int (*vn_rename)(struct vnode *, struct vnode *, caddr_t,
struct vnode *,struct vnode *,caddr_t,struct ucred *);
int (*vn_rmdir)(struct vnode *, struct vnode *, char *,
struct ucred *);
/* lookup, file handle stuff */
int (*vn_lookup)(struct vnode *, struct vnode **, char *, int,
struct vattr *, struct ucred *);
int (*vn_fid)(struct vnode *, struct fileid *, struct ucred *);
/* access to files */
int (*vn_open)(struct vnode *, int, int, caddr_t *, struct ucred *);
int (*vn_create)(struct vnode *, struct vnode **, int, caddr_t,
int, caddr_t *, struct ucred *);
int (*vn_hold)(struct vnode *);
int (*vn_rele)(struct vnode *);
int (*vn_close)(struct vnode *, int, caddr_t, struct ucred *);
int (*vn_map)(struct vnode *, caddr_t, uint, uint, uint,
struct ucred *);
int (*vn_unmap)(struct vnode *, int, struct ucred *);
/* manipulate attributes of files */
int (*vn_access)(struct vnode *, int, int, struct ucred *);
int (*vn_getattr)(struct vnode *, struct vattr *, struct ucred *);
int (*vn_setattr)(struct vnode *, int, int, int, int,
struct ucred *);
/* data update operations */
#ifdef _LONG_LONG
int (*vn_fclear)(struct vnode *, int, offset_t, offset_t,
caddr_t, struct ucred *);
#else
int (*vn_fclear)();
#endif
int (*vn_fsync)(struct vnode *, int, int, struct ucred *);
#ifdef _LONG_LONG
int (*vn_ftrunc)(struct vnode *, int, offset_t, caddr_t,
struct ucred *);
#else
int (*vn_ftrunc)();
#endif
int (*vn_rdwr)(struct vnode *, enum uio_rw, int, struct uio *,
int, caddr_t, struct vattr *, struct ucred *);
#ifdef _LONG_LONG
int (*vn_lockctl)(struct vnode *, offset_t, struct eflock *, int,
int (*)(), ulong *, struct ucred *);
#else
int (*vn_lockctl)();
#endif
/* extensions */
int (*vn_ioctl)(struct vnode *, int, caddr_t, size_t, int,
struct ucred *);
int (*vn_readlink)(struct vnode *, struct uio *, struct ucred *);
int (*vn_select)(struct vnode *, int, ushort, ushort *, void (*)(),
caddr_t, struct ucred *);
int (*vn_symlink)(struct vnode *, char *, char *, struct ucred *);
int (*vn_readdir)(struct vnode *, struct uio *, struct ucred *);
/* buffer ops */
int (*vn_strategy)(struct vnode *, struct buf *, struct ucred *);
/* security things */
int (*vn_revoke)(struct vnode *, int, int, struct vattr *,
struct ucred *);
int (*vn_getacl)(struct vnode *, struct uio *, struct ucred *);
int (*vn_setacl)(struct vnode *, struct uio *, struct ucred *);
int (*vn_getpcl)(struct vnode *, struct uio *, struct ucred *);
int (*vn_setpcl)(struct vnode *, struct uio *, struct ucred *);
};
#ifdef _KERNEL
我们通过 vn_xxx 函数名可以看到它们提供了全部文件操作。顾名思义,如 vn_open()
正是对应 open() 系统调用,vn_map() 对应 mmap() 系统调用,vn_create 对应 crea
te() 系统调用......。AIX 内核将这些虚拟文件系统的文件操作函数用 vnop_xxx()
函数形式加以封状如:vnop_map() 其实就是调用 gnode->gn_ops->vn_map(),然后用
VNOP_XXX 宏提供给调用者,如:#define VNOP_MAP(vp, addr, length, offset, flag
s, ucred) \ vnop_map(vp, addr, length, offset, flags, ucred)。而 gn_ops 中的
vn_xxx() 函数会对应到下层实际文件系统的文件操作函数,也就是说下层实际文件系
统的文件操作函数必须与虚拟文件系统的文件操作函数一一对应,下层实际文件系统如
果不支持一些文件操作可以不提供具体函数功能,但要有函数对应。这样就可以用一步
操作将所有函数对应起来,如下层实际文件系统是 JFS 我们可以在初始化一个 gnode
同时用 gp->gn_ops = &jfs_vops; 来完成连接。
为了让大家有一个更清楚的认识,下面举一个具体例子。我们会看到不论是 虚拟文件
系统,还是下层实际文件系统的文件操作都会频繁使用vnode / gnode 结构中的字段,
这也是为什么如此详细的介绍 vnode / gnode 中每个字段的含义及作用的原因。我们假
设下层实际文件系统是 JFS,当用户打开一个文件,利用系统调用 open() 切换到内核
后首先判断路径是否是以 " / " 开始的绝对路径,如果是绝对路径则从根目录开始遍历,
先判断 U 区中的 U_rdir (vnode 结构), 如果为 NULL 则取全局的 rootdir (vnode
结构) 。如果是相对路径则直接从 U.U_cdir (vnode 结构) 里得到当前目录的 vnode。
无论是绝对还是相对路径,当得到一个目录的 vnode 后都会用 VNOP_HOLD 宏来增加 v
node->v_count 引用计数,这样来保持此 vnode 的有效性。通过以上操作得到的 vnode
有效情况下,继续分解路径,得到后续路径的 vnode, 下面首先判断 vnode->v_mvfsp 是
否为 NULL,如果不为空则当前目录的 vnode 是一个 VFS 的安装节点,调用 VFS_ROOT
宏得到这个 VFS 的根 vnode。如果不是一个 VFS 安装节点,则继续判断 vnode->v_gno
de->gn_type 是否等于 VDIR类型,如果是则调用 VNOP_LOOKUP 宏,上面再讲 gnode 结
构时提到过 gn_ops 指向的函数都被 AIX 内核定义成相关的宏,实质上是调用了 vnode-
>v_gnode->gn_ops->vn_lookup(); 函数,写成宏是为了更具有可读性。vn_lookup() 函
数会调用 jfs_lookup() 函数,它首先会用 VTOIP 宏将 vnode 转换成 inode 并在DNL
C(目录名高速缓存)中查找,如果找到则用 VNOP_HOLD 宏来增加 vnode->v_count 引用
计数,并将此 vnode 直接返回。如果没有找到则调用 dir_lookup() 函数在实际文件系
统中查找,找到后用 dnlc_enter() 函数把此 inode 添加到 DNLC 中。当找到对应的
inode 后首先在 hinode 哈希表中搜索是否有此 inode 对应的 vnode。如果有则利用
inode->i_gnode.gn_vnode 方法得到此 vnode 并增加 v_count引用计数返回给调用者,
如果没有则利用 ITOGP 方法得到一个 gnode 并重新初始化各字段,ITOGP 宏定义如下
:#define ITOGP(x)((struct gnode *)(&(((struct inode *)(x))->i_gnode)))。然
后通过 gnode->gn_vnode 方法得到inode 对应的 vnode。将此 vnode 重新初始化,设
置 vnode->v_vfsnext 与 vnode-> v_vfsprev 加入到 vfs 链中,并将 vnode 返回。
这时调用者得到了一个 vnode。如果分解路径时处理的是 ".." 则判断 vnode->v_f
lag & V_ROOT 的情况,因为所有根目录都有 V_ROOT 标志。以上所介绍的将路径转换
成一个 vnode 的过程都是在一个大的循环中实现的,不断的重复上述过程直到获得了
需要的目录 vnode及文件 vnode。其实以上操作也就是内核提供的 lookuppn() 函数的
具体实现,lookupname() 不过是对 lookuppn() 函数又进行了一次封状。因为我们只关
注 JFS 的普通类型目录、文件,所以上面我们只是介绍了 vnode->v_gnode->gn_type
等于 VDIR, VREG 的情况,其他象 VBLK, VCHR, VLNK, VSOCK, VBAD, VFIFO, VMPC 等
情况我们就不逐一介绍了,不过有一点是一样的,他们都会通过 vnode->v_gnode->gn
_ops->vn_xxx() 函数形式调用下层实际文件系统的文件处理函数来完成相关操作。好
了,我们切回主题,我们已经把路径转换成了一个我们需要打开的 vnode。随后判断
vnode->v_gnode->gn_flags & GNF_TCB 是否需要跟踪审计,如果为真则在返回后调用
audit_xxx() 相关函数。接着判断文件操作方式,FCREAT/FREAD/FWRITE 如果为 FCRE
AT 则调用 VNOP_CREATE 宏(vnode->v_gnode->gn_ops->vn_create),如果是FREAD/
FWRITE 则调用 VNOP_OPEN(vnode->v_gnode->gn_ops->vn_open),vn_open() 函数向
下调用 jfs_open() 利用 VTOIP 宏将 vnode 转换成 inode。调用 ip_access() 函数
检查是否对文件有访问权限,在检查过程中用 ITOGP 宏将 inode 转换成 gnode 形式,
方便检测 gnode->gn_wrcnt,gnode-> gn_rdcnt, gp->gn_rshcnt 及 gnode->gn_flag
s & GNF_NSHARE 字段。如无权操作则直接返回,否则根据打开标志设置 gnode->gn_wr
cnt,gnode-> gn_rdcnt, gp->gn_rshcnt 将计数加一,和 gnode->gn_flags |= GNF_
NSHARE。在对 inode 操作完成后返回调用者。最后将指向vnode 和打开方式标志保存
到文件对象(struct file)中,把文件描述符在表中的索引返回给用户。以上则是用户
打开文件的完整流程。本想在本文中继续跟大家讨论文件的读/写 (read/write()->vn
_rdwr()->jfs_rdwr ()), 映射 (mmap()->vn_map()->jfs_map())等流程,现在看来还
是另起篇文章比较好。其实跟大家探讨到这里已经有很多方面的事情可做,如我们可
以做出 Anti rootkit 的工具,让那些 hook system call 的 rootkit 完全失效,或
者我们可以利用 VFS/JFS 对文件操作的流程做一个更深层次的 rootkit,不必仅局限
于挂接系统调用。还有在文件系统上做一层过滤的内核模块,实现透明加/解密,我们
只要处理好一些 page in/out 的操作完全可以实现。将工作做在 虚拟文件系统及下
层实际文件系统的好处在于更加底层,更加高效。我在 SOLARIS 上写过一个虚拟文件
系统的过滤模块跟挂接系统调用实现同样功能的模块相比效率提高了3-4 倍。还有什
么就有待大家一起开发,总之利用 UNIX 操作系统大量依赖文件系统的特点可以做出
很多东西。
因最近工作重点偏重于 AIX,SOLARIS 下的内核模块开发,所以将自己的一些心得写
出来与大家分享,文章中的一些概念与方法是我个人的理解,错误再所难免,还望得
到您的指正。
参考资源:
Kernel Extensions and Device Support Programming Concepts
http://publib.boulder.ibm.com/infocenter/pseries/topic/com.ibm.aix.doc/aixprggd/kernextc/kernextc.pdf
KDB Kernel debugger and kdb command
http://publib.boulder.ibm.com/infocenter/pseries/topic/com.ibm.aix.doc/aixprggd/kdb/kdb.pdf
感谢 lgx 与我探讨。
WSS(Whitecell Security Systems),一个非营利性民间技术组织,致力于各种系统安全技术的研究。坚持传统的hacker精神,追求技术的精纯。
WSS 主页:http://www.whitecell.org/
WSS 论坛:http://www.whitecell.org/forums/