文件系统安装
来源:互联网 发布:网络犯罪数据研究报告 编辑:程序博客网 时间:2024/06/06 08:25
上一篇博文我们详细讲解了与文件系统安装相关的数据结构,现在我们就来谈谈一个特定的文件系统是怎么安装的。
1 安装普通文件系统
在sys_mount中调用do_mount()函数执行文件系统安装实务:
retval = do_mount((char *)dev_page, dir_page, (char *)type_page,
flags, (void *)data_page);
do_mount()函数通过执行下列操作处理真正的安装操作:
long do_mount(char *dev_name, char *dir_name, char *type_page,
unsigned long flags, void *data_page)
{
struct nameidata nd;
int retval = 0;
int mnt_flags = 0;
/* Discard magic */
if ((flags & MS_MGC_MSK) == MS_MGC_VAL)
flags &= ~MS_MGC_MSK;
/* Basic sanity checks */
if (!dir_name || !*dir_name || !memchr(dir_name, 0, PAGE_SIZE))
return -EINVAL;
if (dev_name && !memchr(dev_name, 0, PAGE_SIZE))
return -EINVAL;
if (data_page)
((char *)data_page)[PAGE_SIZE - 1] = 0;
/* 如果已安装文件系统对象中的安装标志MS_NOSUID、MS_NODEV、MS_NOATIME、MS_NODIRATIME、MS_NODEV或MS_NOEXEC中任一个被设置,
* 则清除它们,并在已安装文件系统对象中设置相应的标志(MNT_NOSUID、MNT_NODEV、MNT_NOEXEC、MNT_NOATIME、MNT_NODIRATIME)。*/
if (flags & MS_NOSUID)
mnt_flags |= MNT_NOSUID;
if (flags & MS_NODEV)
mnt_flags |= MNT_NODEV;
if (flags & MS_NOEXEC)
mnt_flags |= MNT_NOEXEC;
if (flags & MS_NOATIME)
mnt_flags |= MNT_NOATIME;
if (flags & MS_NODIRATIME)
mnt_flags |= MNT_NODIRATIME;
flags &= ~(MS_NOSUID | MS_NOEXEC | MS_NODEV | MS_ACTIVE |
MS_NOATIME | MS_NODIRATIME);
/* ... and get the mountpoint 调用path_lookup()查找安装点的路径名;
* 该函数把路径名查找的结果存放在nameidata类型的局部变量nd中(参见下一博的“路径名查找”)。*/
retval = path_lookup(dir_name, LOOKUP_FOLLOW, &nd);
if (retval)
return retval;
retval = security_sb_mount(dev_name, &nd, type_page, flags, data_page);
if (retval)
goto dput_out;
/* 如果MS_REMOUNT标志被指定,其目的通常是改变超级块对象s_flags字段的安装标志,
* 以及已安装文件系统对象mnt_flags字段的安装文件系统标志。do_remount()函数执行这些改变。*/
if (flags & MS_REMOUNT)
retval = do_remount(&nd, flags & ~MS_REMOUNT, mnt_flags,
data_page);
/* 否则,检查MS_BIND标志。如果它被指定,则用户要求在系统目录树的另一个安装点上的文件或目录能够可见。*/
else if (flags & MS_BIND)
retval = do_loopback(&nd, dev_name, flags & MS_REC);
else if (flags & (MS_SHARED | MS_PRIVATE | MS_SLAVE | MS_UNBINDABLE))
retval = do_change_type(&nd, flags);
/* 否则,检查MS_MOVE标志。如果它被指定,则用户要求改变已安装文件系统的安装点。do_move_mount()函数原子地完成这一任务。*/
else if (flags & MS_MOVE)
retval = do_move_mount(&nd, dev_name);
/* 否则,调用do_new_mount()。这是最普通的情况。
* 当用户要求安装一个特殊文件系统或存放在磁盘分区中的普通文件系统时,触发该函数。*/
else
retval = do_new_mount(&nd, type_page, flags, mnt_flags,
dev_name, data_page);
dput_out:
path_release(&nd);
return retval;
}
我们还是来看最普通的情况:
static int do_new_mount(struct nameidata *nd, char *type, int flags,
int mnt_flags, char *name, void *data)
{
struct vfsmount *mnt;
if (!type || !memchr(type, 0, PAGE_SIZE))
return -EINVAL;
/* we need capabilities... */
if (!capable(CAP_SYS_ADMIN))
return -EPERM;
mnt = do_kern_mount(type, flags, name, data);
if (IS_ERR(mnt))
return PTR_ERR(mnt);
return do_add_mount(mnt, nd, mnt_flags, NULL);
}
do_new_mount调用do_kern_mount()函数,给它传递的参数为文件系统类型、安装标志以及块设备名。
do_kern_mount()处理实际的安装操作并返回一个新安装文件系统描述符的地址:
struct vfsmount *
do_kern_mount(const char *fstype, int flags, const char *name, void *data)
{
struct file_system_type *type = get_fs_type(fstype); /* 我们熟悉的get_fs_type函数,请看上一篇博文 */
struct vfsmount *mnt;
if (!type)
return ERR_PTR(-ENODEV);
mnt = vfs_kern_mount(type, flags, name, data);
put_filesystem(type);
return mnt;
}
struct vfsmount *
vfs_kern_mount(struct file_system_type *type, int flags, const char *name, void *data)
{
struct vfsmount *mnt;
char *secdata = NULL;
int error;
if (!type)
return ERR_PTR(-ENODEV);
error = -ENOMEM;
mnt = alloc_vfsmnt(name); /* 呵呵,我们熟悉的alloc_vfsmnt函数,请看上一篇博文 */
if (!mnt)
goto out;
if (data) {
secdata = alloc_secdata();
if (!secdata)
goto out_mnt;
error = security_sb_copy_data(type, data, secdata);
if (error)
goto out_free_secdata;
}
/* 调用依赖于文件系统的type->get_sb()函数分配,并初始化一个新的超级到mnt->mnt_sb */
error = type->get_sb(type, flags, name, data, mnt);
if (error < 0)
goto out_free_secdata;
error = security_sb_kern_mount(mnt->mnt_sb, secdata);
if (error)
goto out_sb;
/* 将mnt->mnt_root字段初始化为与文件系统根目录对应的目录项对象的地址,
并增加该目录项对象的引用计数器值。*/
mnt->mnt_mountpoint = mnt->mnt_root;
/* 用mnt中的值初始化mnt->mnt_parent字段(对于普通文件系统,
* 当后面讲到的graft_tree()把已安装文件系统的描述符插入到合适的链表中时,
* 要把mnt_parent字段置为合适的值)。 */
mnt->mnt_parent = mnt;
up_write(&mnt->mnt_sb->s_umount);
free_secdata(secdata);
return mnt;
out_sb:
dput(mnt->mnt_root);
up_write(&mnt->mnt_sb->s_umount);
deactivate_super(mnt->mnt_sb);
out_free_secdata:
free_secdata(secdata);
out_mnt:
free_vfsmnt(mnt);
out:
return ERR_PTR(error);
}
然后,do_new_mount()函数调用do_add_mount():
int do_add_mount(struct vfsmount *newmnt, struct nameidata *nd,
int mnt_flags, struct list_head *fslist)
{
int err;
down_write(&namespace_sem);
/* Something was mounted here while we slept */
while (d_mountpoint(nd->dentry) && follow_down(&nd->mnt, &nd->dentry))
;
err = -EINVAL;
if (!check_mnt(nd->mnt))
goto unlock;
/* Refuse the same filesystem on the same mount point */
err = -EBUSY;
if (nd->mnt->mnt_sb == newmnt->mnt_sb &&
nd->mnt->mnt_root == nd->dentry)
goto unlock;
err = -EINVAL;
if (S_ISLNK(newmnt->mnt_root->d_inode->i_mode))
goto unlock;
newmnt->mnt_flags = mnt_flags;
if ((err = graft_tree(newmnt, nd)))
goto unlock;
if (fslist) {
/* add to the specified expiration list */
spin_lock(&vfsmount_lock);
list_add_tail(&newmnt->mnt_expire, fslist);
spin_unlock(&vfsmount_lock);
}
up_write(&namespace_sem);
return 0;
unlock:
up_write(&namespace_sem);
mntput(newmnt);
return err;
}
其本质上执行下列操作:
1、获得当前进程的写信号量namespace_sem,因为函数要更改namespace结构。
2、do_kern_mount()函数可能让当前进程睡眠;同时,另一个进程可能在完全相同的安装点上安装文件系统或者甚至更改根文件系统(current->namespace->root)。验证在该安装点上最近安装的文件系统是否仍指向当前的namespace;如果不是,则释放读/写信号量并返回一个错误码。
3、如果要安装的文件系统已经被安装在由系统调用的参数所指定的安装点上,或该安装点是一个符号链接,则释放读/写信号量并返回一个错误码。
4、初始化由do_kern_mount()分配的新安装文件系统对象的mnt_flags字段的标志。
5、调用graft_tree()把新安装的文件系统对象插入到namespace链表、散列表中(在graft_tree()函数中调用attach_recursive_mnt函数实现)
6、父文件系统的子链表中。
7、释放namespace_sem读/写信号量并返回。
static int graft_tree(struct vfsmount *mnt, struct nameidata *nd)
{
int err;
if (mnt->mnt_sb->s_flags & MS_NOUSER)
return -EINVAL;
if (S_ISDIR(nd->dentry->d_inode->i_mode) !=
S_ISDIR(mnt->mnt_root->d_inode->i_mode))
return -ENOTDIR;
err = -ENOENT;
mutex_lock(&nd->dentry->d_inode->i_mutex);
if (IS_DEADDIR(nd->dentry->d_inode))
goto out_unlock;
err = security_sb_check_sb(mnt, nd);
if (err)
goto out_unlock;
err = -ENOENT;
if (IS_ROOT(nd->dentry) || !d_unhashed(nd->dentry))
err = attach_recursive_mnt(mnt, nd, NULL);
out_unlock:
mutex_unlock(&nd->dentry->d_inode->i_mutex);
if (!err)
security_sb_post_addmount(mnt, nd);
return err;
回到do_mount()函数,最后调用path_release()终止安装点的路径名查找(参见下一篇博文“路径名查找”)并返回。
2 分配超级块对象
文件系统对象的get_sb方法通常是由单行函数实现的。例如,在Ext2文件系统中该方法的实现如下:
//fs/ext2/Super.c
static int ext2_get_sb(struct file_system_type *fs_type,
int flags, const char *dev_name, void *data, struct vfsmount *mnt)
{
return get_sb_bdev(fs_type, flags, dev_name, data, ext2_fill_super, mnt);
}
get_sb_bdev() VFS函数分配并初始化一个新的适合于磁盘文件系统的超级块;它接收ext2_fill_super()函数的地址,该函数从Ext2磁盘分区读取磁盘超级块。
为了分配适合于特殊文件系统的超级块,VFS也提供get_sb_pseudo()函数,对于没有安装点的特殊文件系统,例如pipefs()、get_sb_single()函数等(对于具有唯一安装点的特殊文件系统,例如.sysfs)以及get_sb_nodev()函数(对于可以安装多次的特殊文件系统,例如tmpfs;参见下面)。
get_sb_bdev()函数位于/fs/Super.c,代码如下:
int get_sb_bdev(struct file_system_type *fs_type,
int flags, const char *dev_name, void *data,
int (*fill_super)(struct super_block *, void *, int),
struct vfsmount *mnt)
{
struct block_device *bdev;
struct super_block *s;
int error = 0;
/* 调用open_bdev_excl()打开设备文件名为dev_name的块设备。*/
bdev = open_bdev_excl(dev_name, flags, fs_type);
if (IS_ERR(bdev))
return PTR_ERR(bdev);
/*
* once the super is inserted into the list by sget, s_umount
* will protect the lockfs code from trying to start a snapshot
* while we are mounting
*/
down(&bdev->bd_mount_sem);
/* 调用sget()搜索文件系统的超级块对象链表(type->fs_supers,参见前面的博文中“文件系统安装数据结构”部分)。
* 如果找到一个与块设备相关的超级块,则返回它的地址。否则,分配并初始化一个新的超级块对象,
* 把它插入到文件系统链表和超级块全局链表中,并返回其地址。*/
s = sget(fs_type, test_bdev_super, set_bdev_super, bdev);
up(&bdev->bd_mount_sem);
if (IS_ERR(s))
goto error_s;
if (s->s_root) { /* 如果不是新的超级块(注意,是通过s->s_root是否为空来判断的) */
if ((flags ^ s->s_flags) & MS_RDONLY) {
up_write(&s->s_umount);
deactivate_super(s);
error = -EBUSY;
goto error_bdev;
}
close_bdev_excl(bdev);
} else {
char b[BDEVNAME_SIZE];
/* 把参数flags中的值拷贝到超级块的s_flags字段,
* 并将s_id、s_old_blocksize以及s_blocksize字段设置为块设备的合适值。*/
s->s_flags = flags;
strlcpy(s->s_id, bdevname(bdev, b), sizeof(s->s_id));
sb_set_blocksize(s, block_size(bdev));
/* 调用依赖文件系统的函数(例子中是ext2_fill_super函数,我们在讨论ext2的时候会讲它)
* 访问磁盘上的超级块信息,并填充新超级块对象的其他字段。*/
error = fill_super(s, data, flags & MS_SILENT ? 1 : 0);
if (error) {
up_write(&s->s_umount);
deactivate_super(s);
goto error;
}
s->s_flags |= MS_ACTIVE;
bdev_uevent(bdev, KOBJ_MOUNT);
}
return simple_set_mnt(mnt, s);
error_s:
error = PTR_ERR(s);
error_bdev:
close_bdev_excl(bdev);
error:
return error;
}
3 安装根文件系统
安装根文件系统是系统初始化的关键部分。这是一个相当复杂的过程,因为Linux内核允许根文件系统存放在很多不同的地方,比如硬盘分区、软盘、通过NFS共享的远程文件系统,甚至保存在ramdisk中(RAM中的虚拟块设备)。
为了使叙述变得简单,让我们假定根文件系统存放在硬盘分区(毕竟这是最常见的情行)。当系统启动时,内核就要在变量ROOT_DEV中寻找包含根文件系统的磁盘主设备号:
//init/Do_mounts.c
dev_t ROOT_DEV;
当编译内核时,或者向最初的启动装入程序传递一个合适的“root”选项时,根文件系统可以被指定为/dev目录下的一个设备文件。类似地,根文件系统的安装标志存放在root_mountflags变量中:
//init/Do_mounts.c
int root_mountflags = MS_RDONLY | MS_SILENT;
用户可以指定这些标志,或者通过对已编译的内核映像使用rdev外部程序,或者向最初的启动装入程序传递一个合适的rootflags选项来达到。
安装根文件系统分两个阶段:
(1)内核安装特殊rootfs文件系统,该文件系统仅提供一个作为初始安装点的空目录。
(2)内核在空目录上安装实际根文件系统。
为什么内核不怕麻烦,要在安装实际根文件系统之前安装rootfs文件系统呢?这是因为,rootfs文件系统允许内核容易地改变实际根文件系统。实际上,在大多数情况下,系统初始化是内核会逐个地安装和卸载几个根文件系统。例如,一个发布版的初始启动光盘可能把具有一组最小驱动程序的内核装人RAM中,内核把存放在ramdisk中的一个最小的文件系统作为根安装。接下来,在这个初始根文件系统中的程序探测系统的硬件(例如,它们判断硬盘是否是EIDE、SCSI等等),装入所有必需的内核模块,并从物理块设备重新安装根文件系统。
阶段1:安装rootfs文件系统
第一阶段是由init_rootfs()和init_mount_tree()函数完成的,它们在系统初始化过程中执行。
init_rootfs()函数注册特殊文件系统类型rootfs:
static struct file_system_type rootfs_fs_type = {
.name = "rootfs",
.get_sb = rootfs_get_sb,
.kill_sb = kill_litter_super,
};
init_mount_tree()函数主要完成根文件系统的初始化:
static void __init init_mount_tree(void)
{
struct vfsmount *mnt;
struct namespace *namespace;
struct task_struct *g, *p;
mnt = do_kern_mount("rootfs", 0, "rootfs", NULL);
if (IS_ERR(mnt))
panic("Can't create rootfs");
namespace = kmalloc(sizeof(*namespace), GFP_KERNEL);
if (!namespace)
panic("Can't allocate initial namespace");
atomic_set(&namespace->count, 1);
INIT_LIST_HEAD(&namespace->list);
init_waitqueue_head(&namespace->poll);
namespace->event = 0;
list_add(&mnt->mnt_list, &namespace->list);
namespace->root = mnt;
mnt->mnt_namespace = namespace;
init_task.namespace = namespace;
read_lock(&tasklist_lock);
do_each_thread(g, p) {
get_namespace(namespace);
p->namespace = namespace;
} while_each_thread(g, p);
read_unlock(&tasklist_lock);
set_fs_pwd(current->fs, namespace->root, namespace->root->mnt_root);
set_fs_root(current->fs, namespace->root, namespace->root->mnt_root);
}
看到了吧,init_mount_tree首先调用do_kern_mount()函数,把字符串“rootfs”作为文件系统类型参数传递给它,文件系统标志是0,没有data,并把该函数返回的新安装文件系统描述符的地址保存在mnt局部变量中。正如前面介绍的,do_kern_mount()最终调用rootfs文件系统的get_sb方法,也即rootfs_get_sb()函数:
static int rootfs_get_sb(struct file_system_type *fs_type,
int flags, const char *dev_name, void *data, struct vfsmount *mnt)
{
return get_sb_nodev(fs_type, flags|MS_NOUSER, data, ramfs_fill_super,
mnt);
}
get_sb_nodev()函数前面一件提到了,针对rootfs文件系统:
1、调用sget()函数分配新的超级块,传递set_anon_super()函数的地址作为参数。接下来,用合适的方式设置超级快的s_dev字段:主设备号为O,次设备号不同于其他已安装的特殊文件系统的次设备号。
2、将flags参数的值拷贝到超级块的s_flags字段中。
3、调用ramfs_fill_super()函数分配索引节点对象和对应的目录项对象并填充超级块字段值。由于rootfs是一种特殊文件系统,没有磁盘超级块,因此只需执行两个超级块操作:
static int ramfs_fill_super(struct super_block * sb, void * data, int silent)
{
struct inode * inode;
struct dentry * root;
sb->s_maxbytes = MAX_LFS_FILESIZE;
sb->s_blocksize = PAGE_CACHE_SIZE;
sb->s_blocksize_bits = PAGE_CACHE_SHIFT;
sb->s_magic = RAMFS_MAGIC;
sb->s_op = &ramfs_ops;
sb->s_time_gran = 1;
inode = ramfs_get_inode(sb, S_IFDIR | 0755, 0);
if (!inode)
return -ENOMEM;
root = d_alloc_root(inode);
if (!root) {
iput(inode);
return -ENOMEM;
}
sb->s_root = root;
return 0;
}
4、返回新超级块的地址。
回到init_mount_tree()函数,继续:
为进程0的命名空间分配一个namespace对象,并将它插入到由do_kern_mount()函数返回的已安装文件系统描述符中:
namespace = kmalloc(sizeof(*namespace), GFP_KERNEL);
list_add(&mnt->mnt_list, &namespace->list);
namespace->root = mnt;
mnt->mnt_namespace = init_task.namespace = namespace;
将系统中其他每个进程的namespace字段设置为namespace对象的地址;同时初始化引用计数器namespace->count(缺省情况下,所有的进程共享同一个初始namespace)。
将进程0的根目录和当前工作目录设置为根文件系统。
set_fs_pwd(current->fs, namespace->root, namespace->root->mnt_root);
set_fs_root(current->fs, namespace->root, namespace->root->mnt_root);
阶段2:安装实际根文件系统
根文件系统安装操作的第二阶段是由内核在系统初始化即将结束时进行的。根据内核被编译时所选择的选项,和内核装入程序所传递的启动选项,可以有几种方法安装实际根文件系统。为了简单起见,我们只考虑磁盘文件系统的情况,它的设备文件名已通过“root”启动参数传递给内核。同时我们假定除了rootfs文件系统外,没有使用其他初始特殊文件系统。
内核主要是调用prepare_namespace()函数执行安装实际根文件系统的操作:
void __init prepare_namespace(void)
{
int is_floppy;
if (root_delay) {
printk(KERN_INFO "Waiting %dsec before mounting root device.../n",
root_delay);
ssleep(root_delay);
}
md_run_setup();
if (saved_root_name[0]) {
/* 把root_device_name变量置为从启动参数“root”中获取的设备文件名。
* 同样,把ROOT_DEV变量置为同一设备文件的主设备号和次设备号。*/
root_device_name = saved_root_name;
if (!strncmp(root_device_name, "mtd", 3)) {
/* 调用mount_block_root()函数,将最常用的块设备作为rootfs文件系统的子文件系统 */
mount_block_root(root_device_name, root_mountflags);
goto out;
}
ROOT_DEV = name_to_dev_t(root_device_name);
if (strncmp(root_device_name, "/dev/", 5) == 0)
root_device_name += 5;
}
is_floppy = MAJOR(ROOT_DEV) == FLOPPY_MAJOR;
if (initrd_load())
goto out;
if (is_floppy && rd_doload && rd_load_disk(0))
ROOT_DEV = Root_RAM0;
mount_root();
out:
/* 移动rootfs文件系统根目录上的已安装文件系统的安装点。 */
sys_mount(".", "/", NULL, MS_MOVE, NULL);
sys_chroot(".");
security_sb_post_mountroot();
}
注意,rootfs特殊文件系统没有被卸载:它只是隐藏在基于磁盘的根文件系统下了。
4 卸载文件系统
umount()系统调用用来卸载一个文件系统。我们不去详细讨论它的代码了,比较简单。相应的sys_umount()服务例程作用于两个参数:文件名(多是安装点目录或是块设备文件名)和一组标志。该函数执行下列操作:
1.调用path_lookup()查找安装点路径名;该函数把返回的查找操作结果存放在nameidata类型的局部变量nd中。
2.如果查找的最终目录不是文件系统的安装点,则设置retval返回码为-EINVAL并跳到第6步。这种检查是通过验证nd->mnt->mnt_root(它包含由nd.dentry指向的目录项对象地址)进行的。
3.如果要卸载的文件系统还没有安装在命名空间中,则设置retval返回码为-EINVAL并跳到第6步(回想一下,某些特殊文件系统没有安装点)。这种检查是通过在nd->mnt上调用check_mnt()函数进行的。
4.如果用户不具有卸载文件系统的特权,则设置retval返回码为-EPERM并跳到第6步。
5.调用do_umount(),传递给它的参数为nd.mnt(已安装文件系统对象)和flags(一组标志)。该函数执行下列操作:
a)从已安装文件系统对象的mnt_sb字段检索超级块对象sb的地址。
b)如果用户要求强制卸载操作,则调用umount_begin超级块操作中断任何正在进行的安装操作。
c)如果要卸载的文件系统是根文件系统,且用户并不要求真正地把它卸载下来则调用do_remount_sb()重新安装根文件系统为只读并终止。
d)为进行写操作而获取当前进程的namespace->sem读/写信号量和vfsmount_lock自旋锁。
e)如果已安装文件系统不包含任何子安装文件系统的安装点,或者用户要求强制卸载文件系统,则调用umount_tree()卸载文件系统(及其所有子文件系统)
f)释放vfsmount_lock自旋锁和当前进程的namespace->sem读/写信号量。
6.减少相应文件系统根目录的目录项对象和已安装文件系统描述符的引用计数器值;这些计数器值由path_lookup()增加。
7.返回retval的值。
- 文件系统安装
- 文件系统-- 安装根文件系统阶段
- 文件系统-- 安装根文件系统阶段(安装rootfs文件系统)
- 文件系统-- 安装根文件系统阶段(安装实际根文件系统)
- 文件系统安装预备知识
- 安装根文件系统
- Ceph 文件系统安装
- Ceph 文件系统安装
- hadoop 分布式文件系统安装
- mfs文件系统配置安装
- glusterfs文件系统安装配置
- Ceph文件系统安装步骤
- 分布式文件系统安装部署
- 源码安装lustre文件系统
- TFS文件系统 安装
- 安装GlusterFS分布式文件系统
- BTRFS文件系统安装ArchLinux
- fastDFS文件系统安装
- .NET中DataList嵌套DataList的实例
- 探察MFC中框架宏(RUNTIME_CLASS等)的秘密
- 关于vim的一些配置资源
- Ajax中的UpdatePanel与Freetextbox兼容问题
- Spring 中的定时器TimerTask 与 quartz
- 文件系统安装
- 这几天用多线程
- 我要学什么?
- Shell命令中的扩展和替换
- 《Microsoft Sql server 2008 Internals》读书笔记--第八章The Query Optimizer(7)
- 串匹配的有限自动机,说白了是一种状态转移函数;
- HLSL的一些见解
- 环境变量生效
- 关于在C#下实现人民币大小写转换问题