Linux内核源码阅读之打开文件篇

Linux虚拟文件系统--文件路径名的解析(1)--整体过程 文件路径名的解析是VFS中最基本也是最频繁用到的一个部分,它的代码实现还是十分繁杂的,主要是因为除了普通文件名的解析,内核还要考虑各种可能出现的情况,如一个目录下挂载了多个文件系统,路径中的符号链接等等……后面我会分几次将整个过程进行一个尽量仔细的分析,其中所涉及到的各种数据结构在ULK等相关内核书籍上都有比较详细的介绍,就不列出来了        文件路径名的解析路口函数为path_looku 阅读详情

1.          引言       从事Linux环境工作2年有余,一直懵懵懂懂,1年前拜读了《莱昂氏UNIX源代码分析》一书,感觉自己的学习道路漫漫且修远。最近受chinaunix的精华文帖启发,拟将近来的部分内核调用分析笔记拿出来与各前辈先进共同探讨学习,以壮个人学习之路。       本部分主要讲述的是文件I/O操作的2.6.11内核版本实现,包括了主要的数据结构、宏定义和函数流程。以下分别讲述open,create,close,read,write,lseek系统调用。 2.          主要参考 《莱昂氏UNIX源代码分析》 《UNIX环境高级编程》     www.kernel.org 3.          主要数据结构 3.1.          FD         对于内核而言,所有打开文件都由文件描述符引用。文件描述符是一个非负整数。当打开一个现存文件或创建一个新文件时,内核向进程返回一个文件描述符。       当读、写一个文件时,用open或creat返回的文件描述符fd标识该文件,将其作为参数传送给read或write。在POSIX.1应用程序中,文件描述符为常数0、1和2分别代表STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,意即标准输入,标准输出和标准出错输出,这些常数都定义在头文件<unistd.h>;中。 文件描述符的范围是0~OPEN_MAX,在目前常用的linux系统中,是32位整形所能表示的整数,即65535,64位机上则更多。 3.2.          File struct file {      struct list_head          f_list; //文件链表指针     struct dentry                  *f_dentry; // 文件对应的目录结构     struct vfsmount           *f_vfsmnt; // 虚拟文件系统挂载点     struct file_operations          *f_op; // 文件操作函数指针     atomic_t                  f_count;      unsigned int                   f_flags;      mode_t                          f_mode; // 文件模式     int                          f_error;      loff_t                          f_pos; // 文件offset      struct fown_struct          f_owner; //文件owner 结构     unsigned int                  f_uid, f_gid;      struct file_ra_state          f_ra; // 跟踪上次文件操作状态的结构指针     size_t                          f_maxcount; // 文件大小     unsigned long                  f_version;      void                          *f_security; // hook 文件操作的security结构指针     void                          *private_data; // tty 驱动器所需数据 #ifdef CONFIG_EPOLL      struct list_head          f_ep_links; // EPOLL 机制检测所需链表结构     spinlock_t                  f_ep_lock; // 兼容早期gcc bug 的标志 #endif /* #ifdef CONFIG_EPOLL */      struct address_space          *f_mapping; // 地址映射表 } 3.3.          File_struct File_struct结构保存了进程打开的所有文件表数据。 struct files_struct {      atomic_t count; // 自动增量     spinlock_t file_lock; // 低位成员保护标识     int max_fds; // 最大文件句柄数目     int max_fdset; // 最大的fd集合容量     int next_fd; // 下一个空闲fd      struct file ** fd; // 当前fd对应的文件结构指针列表     fd_set *close_on_exec; // 可执行close的fd集合     fd_set *open_fds; // 打开的fd集合     fd_set close_on_exec_init; //      fd_set open_fds_init;      struct file * fd_array[NR_OPEN_DEFAULT]; // 默认打开的fd队列 }; 4.          open 函数 4.1.          原型与参数      int open(const char * pathname,    int oflag, .../*, mode_t mode * / )    -1代表错误。      这里的oflag是一个整形,主要供open 函数使用,部分fcntl函数也会使用。详细的说明请用 man 2 open就可以看到了。以下列出了2.6内核定义的open和fcntl函数所使用的flag宏定义,说明的格式如宏定义名称<实际常数值>;: 描述。 O_ACCMODE          <0003>;: 读写文件操作时,用于取出flag的低2位。 O_RDONLY<00>;: 只读打开 O_WRONLY<01>;: 只写打开 O_RDWR<02>;: 读写打开 O_CREAT<0100>;: 文件不存在则创建,需要mode_t,not fcntl O_EXCL<0200>;: 如果同时指定了O_CREAT,而文件已经存在,则出错, not fcntl O_NOCTTY<0400>;: 如果pathname指终端设备,则不将此设备分配作为此进程的控制终端。O_TRUNC<01000>;: 如果此文件存在,而且为只读或只写成功打开,则将其长度截短为0。      O_APPEND<02000>;: 每次写时都加到文件的尾端 O_NONBLOCK<04000>;: 如果p a t h n a m e指的是一个F I F O、一个块特殊文件或一个字符特殊文件,则此选择项为此文件的本次打开操作和后续的I / O操作设置非阻塞方式。 O_NDELAY<O_NONBLOCK>;; O_SYNC<010000>;: 使每次write都等到物理I/O操作完成。 FASYNC<020000>;: 兼容BSD的fcntl同步操作 O_DIRECT<040000>;: 直接磁盘操作标识 O_LARGEFILE<0100000>;: 大文件标识 O_DIRECTORY<0200000>;:          必须是目录 O_NOFOLLOW<0400000>;: 不获取连接文件 O_NOATIME<01000000>;: 暂无 当新创建一个文件时,需要指定mode 参数,以下说明的格式如宏定义名称<实际常数值>;: 描述。      S_IRWXU<00700>;:文件拥有者有读写执行权限      S_IRUSR (S_IREAD)<00400>;:文件拥有者仅有读权限      S_IWUSR (S_IWRITE)<00200>;:文件拥有者仅有写权限      S_IXUSR (S_IEXEC)<00100>;:文件拥有者仅有执行权限      S_IRWXG<00070>;:组用户有读写执行权限      S_IRGRP<00040>;:组用户仅有读权限      S_IWGRP<00020>;:组用户仅有写权限      S_IXGRP<00010>;:组用户仅有执行权限      S_IRWXO<00007>;:其他用户有读写执行权限      S_IROTH<00004>;:其他用户仅有读权限      S_IWOTH<00002>;:其他用户仅有写权限      S_IXOTH<00001>;:其他用户仅有执行权限 4.2.         实现分析 4.2.1.         主要函数调用关系图     sys_open( 见4.2.2 节)     | ----------- getname( 见4.2.3 节 )     | ----------- filp_open( 见4.2.4节 )     |                         | ------------ open_namei( 见4.2.4.1节 )     |                         |                   | ----------- may_open     |                         | ------------ dentry_open( 见4.2.4.2节 ) 4.2.2.         主调用函数sys_open,函数定义在/fs/open.c文件 asmlinkage long sys_open(const char __user * filename, int flags, int mode){          char * tmp;          int fd, error;

// 如果不是32位处理器,则增加大文件标识 #if BITS_PER_LONG != 32          flags |= O_LARGEFILE; #endif          // 为了提高使用效率,在使用之前先将文件名拷贝到内核数据区。见3.2.2说明          tmp = getname(filename);          // 获取到返回值,如果出错,则返回,否则执行打开操作。          fd = PTR_ERR(tmp);          if (!IS_ERR(tmp)) {                  // 从进程的文件表中找出一个空闲的文件表指针,如果出错,则返回                  fd = get_unused_fd();                  if (fd >= 0) {                          // 执行打开操作。见3.2.3说明                          struct file *f = filp_open(tmp, flags, mode);                          // 获取返回结果,如果出错,则跳转至out_error,否则执行fd_install                          error = PTR_ERR(f);                          if (IS_ERR(f))                                  goto out_error;                          // 添加打开的文件表 f 到当前进程的文件表队列中。见3.2.4说明                          fd_install(fd, f);                  } out:                  // 释放getname分配的内存空间                  putname(tmp);          }          return fd; out_error:                  // 将文件表指针返回到进程的文件表中,并返回错误代码。          put_unused_fd(fd);          fd = error;          goto out; } 4.2.3.         sys_open子函数getname getname函数主要功能是在使用文件名之前将其拷贝到内核数据区,正常结束时返回内核分配的空间首地址,出错时返回错误代码。其调用了函数do_getname来实现。 static inline int do_getname(const char __user *filename, char *page){          int retval;          unsigned long len = PATH_MAX; // 内核允许的最大路径长度

         // 如果进程的地址限制是否和KERNEL_DS相等,则检查文件名是否小于用户进程空间          if (!segment_eq(get_fs(), KERNEL_DS)) {                  // 文件名地址大于用户进程空间,则返回错误-EFAULT                  if ((unsigned long) filename >= TASK_SIZE)                          return -EFAULT;                  // 获取较小的地址长度                  if (TASK_SIZE - (unsigned long) filename < PATH_MAX)                          len = TASK_SIZE - (unsigned long) filename;          }

         // 将filename拷贝len长度到page中,返回实际拷贝长度          retval = strncpy_from_user(page, filename, len);          if (retval >0) {                  // 如果retval大于等于len,则返回-ENAMETOOLONG                  if (retval < len)                          return 0;                  return -ENAMETOOLONG;          } else if (!retval)                  // filename 为空,则返回-ENOENT                  retval = -ENOENT;          return retval; }

char * getname(const char __user * filename){          char *tmp, *result;

         result = ERR_PTR(-ENOMEM);          // 从内核缓存中分配空间,如果成功,则执行do_getname          tmp = __getname();          if (tmp)   {                  // 执行文件名拷贝操作                  int retval = do_getname(filename, tmp);

                 result = tmp;                  if (retval < 0) {                          // do_getname出错,则释放空间,并返回错误代码                          __putname(tmp);                          result = ERR_PTR(retval);                  }          }          // 如果前面操作成功,且audit_context不为空,则将当前文件名添加到audit列表中          if (unlikely(current->;audit_context) && !IS_ERR(result) && result)                  audit_getname(result);          // 返回处理结果          return result; } 4.2.4.         sys_open子函数filp_open 在较高版本的内核里函数名改为do_filp_open,但是函数内容一样。 这后面的函数使用了一个nameidata的数据结构来描述文件相关的操作数据。 struct nameidata {          struct dentry         *dentry; // 目录数据          struct vfsmount *mnt; // 虚拟文件挂载点数据          struct qstr         last; // hash值          unsigned int         flags; // 文件操作标识          int                 last_type; // 类型          unsigned         depth;          char *saved_names[MAX_NESTED_LINKS + 1];          union {                  struct open_intent open;          } intent; // 专用数据 }; struct file *filp_open(const char * filename, int flags, int mode){          int namei_flags, error;          struct nameidata nd;

         namei_flags = flags;          if ((namei_flags+1) & O_ACCMODE)                  namei_flags++; // 如果flags有O_WRONLY,则增加O_RDONLY          if (namei_flags & O_TRUNC)                  namei_flags |= 2; // 如果有O_TRUNC,则增加O_RDWR

         error = open_namei(filename, namei_flags, mode, &nd); // 如3.2.3.1 描述          if (!error)                  return dentry_open(nd.dentry, nd.mnt, flags); // 如3.2.3.2描述

         return ERR_PTR(error); // 返回错误代码 } 4.2.4.1.         filp_open子函数open_namei open_namei函数主要执行文件操作的inode部分的打开等操作。 int open_namei(const char * pathname, int flag, int mode, struct nameidata *nd){          int acc_mode, error = 0;          struct dentry *dentry;          struct dentry *dir;          int count = 0;

         acc_mode = ACC_MODE(flag); // 取出低2位操作标识

         if (flag & O_APPEND) // 取出O_APPEND操作标识                  acc_mode |= MAY_APPEND;

         //赋值open函数的专用数据          nd->;intent.open.flags = flag;          nd->;intent.open.create_mode = mode;

         // 如果不需要创建文件,则在进程目录文件表搜索已有文件,并把结果拷贝到nd中          if (!(flag & O_CREAT)) {                  error = path_lookup(pathname, lookup_flags(flag)|LOOKUP_OPEN, nd);                  if (error) // 错误代码有ENOENT,ENOTDIR,EAGAIN,ESTALE,                          return error;                  goto ok; // 否则执行打开函数,更新inode数据          }

// 在进程文件表中搜索该文件,如果不存在,则创建,结果由nd保存          error = path_lookup(pathname, LOOKUP_PARENT|LOOKUP_OPEN|LOOKUP_CREATE, nd);          if (error)                  return error;

         // 检测nd的结果是否是一个目录文件,是则返回          error = -EISDIR;          if (nd->;last_type != LAST_NORM || nd->;last.name[nd->;last.len])                  goto exit;          // 获取文件的相关目录数据,结果返回到dentry中。          dir = nd->;dentry;          nd->;flags &= ~LOOKUP_PARENT;          down(&dir->;d_inode->;i_sem);          dentry = __lookup_hash(&nd->;last, nd->;dentry, nd);

do_last:          // 如果dentry是一个错误值,则返回          error = PTR_ERR(dentry);          if (IS_ERR(dentry)) {                  up(&dir->;d_inode->;i_sem);                  goto exit;          }

         // 如果dentry不存在,则创建他          if (!dentry->;d_inode) {                  if (!IS_POSIXACL(dir->;d_inode))                          mode &= ~current->;fs->;umask;                  error = vfs_create(dir->;d_inode, dentry, mode, nd); // 创建inode                  up(&dir->;d_inode->;i_sem);                  dput(nd->;dentry);                  nd->;dentry = dentry;                  if (error)                          goto exit;                  acc_mode = 0;                  flag &= ~O_TRUNC;                  goto ok;          }

up(&dir->;d_inode->;i_sem);

         error = -EEXIST; // 如果指定了O_EXCL和O_CREAT,文件存在时,出错          if (flag & O_EXCL)                  goto exit_dput;

         if (d_mountpoint(dentry)) { // 检测文件是否是连接文件                  error = -ELOOP;                  if (flag & O_NOFOLLOW) // 如果指定不遍历连接文件,则返回                          goto exit_dput;                  // 检测dentry挂载点                  while (__follow_down(&nd->;mnt,&dentry) && d_mountpoint(dentry));          }          error = -ENOENT;          if (!dentry->;d_inode) // inode 不存在,则返回                  goto exit_dput;          if (dentry->;d_inode->;i_op && dentry->;d_inode->;i_op->;follow_link)                  goto do_link; // 允许遍历连接文件,则手工找到连接文件对应的文件                  // 将处理后的dentry复制到nd结构中,并判断其是否是目录,是则返回错误          dput(nd->;dentry);          nd->;dentry = dentry;          error = -EISDIR;          if (dentry->;d_inode && S_ISDIR(dentry->;d_inode->;i_mode))                  goto exit; ok:          error = may_open(nd, acc_mode, flag); // 打开文件,返回处理结果代码。如3.2.3.1.1描述          if (error)                  goto exit;          return 0;

exit_dput:          dput(dentry); // 释放dentry exit:          path_release(nd); // 释放nd结构          return error; // 返回错误代码

do_link:          error = -ELOOP;          if (flag & O_NOFOLLOW)                  goto exit_dput; // 不允许遍历连接文件,则返回错误                  // 以下代码是手工找到连接文件对应的文件dentry数据          nd->;flags |= LOOKUP_PARENT;          error = security_inode_follow_link(dentry, nd);          if (error)                  goto exit_dput;          error = __do_follow_link(dentry, nd);          dput(dentry);          if (error)                  return error;          nd->;flags &= ~LOOKUP_PARENT;          if (nd->;last_type == LAST_BIND) {                  dentry = nd->;dentry;                  goto ok;          }          error = -EISDIR;          if (nd->;last_type != LAST_NORM)                  goto exit;          if (nd->;last.name[nd->;last.len]) {                  putname(nd->;last.name);                  goto exit;          }          error = -ELOOP;          if (count++==32) {                  putname(nd->;last.name);                  goto exit;          }          dir = nd->;dentry;          down(&dir->;d_inode->;i_sem);          dentry = __lookup_hash(&nd->;last, nd->;dentry, nd);          putname(nd->;last.name);          goto do_last; } 4.2.4.1.1.         filp_open子函数may_open 在较高版本的内核里作为open_namei的子函数。。。 may_open执行权限检测和文件打开,和truncate的操作。 int may_open(struct nameidata *nd, int acc_mode, int flag){          struct dentry *dentry = nd->;dentry;          struct inode *inode = dentry->;d_inode;          int error;

         if (!inode) return -ENOENT; // inode为空,则返回错误

         if (S_ISLNK(inode->;i_mode)) // 连接文件,返回错误                  return -ELOOP;                  if (S_ISDIR(inode->;i_mode) && (flag & FMODE_WRITE))                  return -EISDIR; // 是目录且仅有写权限,返回错误

         error = permission(inode, acc_mode, nd); // 见擦inode的accmode          if (error)                  return error;

         if (S_ISFIFO(inode->;i_mode) || S_ISSOCK(inode->;i_mode)) {                      flag &= ~O_TRUNC; // 如果是FIFO文件,则不允许truncate          } else if (S_ISBLK(inode->;i_mode) || S_ISCHR(inode->;i_mode)) {                  if (nd->;mnt->;mnt_flags & MNT_NODEV)                          return -EACCES; // 如果是设备,则不允许truncate,否则返回错误                  flag &= ~O_TRUNC;          } else if (IS_RDONLY(inode) && (flag & FMODE_WRITE))                  return -EROFS; 如果flag标识和inode权限冲突,则返回错误                  // 如果inode只允许append方式写入,则不允许truncate和非append写入方式。 if (IS_APPEND(inode)) {                  if   ((flag & FMODE_WRITE) && !(flag & O_APPEND))                          return -EPERM;                  if (flag & O_TRUNC)                          return -EPERM;          }

         // O_NOATIME方式仅在inode用户是文件拥有者或者超级用户情况下才被允许          if (flag & O_NOATIME)                  if (current->;fsuid != inode->;i_uid && !capable(CAP_FOWNER))                          return -EPERM;

         // 检查是否有其他进程在使用该文件          error = break_lease(inode, flag);          if (error)                  return error;

         if (flag & O_TRUNC) {                  error = get_write_access(inode); // 获取一次inode写操作权限                  if (error)                          return error;

                 // 锁定inode                  error = locks_verify_locked(inode);                  if (!error) {                          DQUOT_INIT(inode); // 对inode执行配额初始化                                                  error = do_truncate(dentry, 0); // truncate dentry                  }                  put_write_access(inode); // 释放当前写操作权限                  if (error)                          return error;          } else                  if (flag & FMODE_WRITE) // 如果有写标识,则对inode执行配额初始化                          DQUOT_INIT(inode);

         return 0; } 4.2.4.2.         open_namei子函数dentry_open dentry_open函数主要实现文件表的对应打开等操作,返回文件指针。 struct file *dentry_open(struct dentry *dentry, struct vfsmount *mnt, int flags){          struct file * f;          struct inode *inode;          int error;

         error = -ENFILE;          f = get_empty_filp(); // 从进程文件表中获取一个未使用的文件结构指针,空则出错返回          if (!f)                  goto cleanup_dentry;          // 设置文件的flags和mode标识          f->;f_flags = flags;          f->;f_mode = ((flags+1) & O_ACCMODE) | FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE;          inode = dentry->;d_inode;          if (f->;f_mode & FMODE_WRITE) {                  error = get_write_access(inode); // 获取一次inode写操作权限                  if (error)                          goto cleanup_file;          }          // 初始化文件结构          f->;f_mapping = inode->;i_mapping;          f->;f_dentry = dentry;          f->;f_vfsmnt = mnt;          f->;f_pos = 0;          f->;f_op = fops_get(inode->;i_fop);          file_move(f, &inode->;i_sb->;s_files);

         // 调用文件驱动模块初始化物理磁盘          if (f->;f_op && f->;f_op->;open) {                  error = f->;f_op->;open(inode,f);                  if (error)                          goto cleanup_all;          }          f->;f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);

         // 初始化上次读取状态          file_ra_state_init(&f->;f_ra, f->;f_mapping->;host->;i_mapping);

         // 如果设置了O_DIRECT,则检测文件结构中是否有驱动的操作函数指针          if (f->;f_flags & O_DIRECT) {                  if (!f->;f_mapping->;a_ops || !f->;f_mapping->;a_ops->;direct_IO) {                          fput(f);                          f = ERR_PTR(-EINVAL);                  }          }

         return f; // 返回文件结构

cleanup_all: // 出错,则释放资源并返回          fops_put(f->;f_op);          if (f->;f_mode & FMODE_WRITE)                  put_write_access(inode);          file_kill(f);          f->;f_dentry = NULL;          f->;f_vfsmnt = NULL; cleanup_file:          put_filp(f); cleanup_dentry:          dput(dentry);          mntput(mnt);          return ERR_PTR(error); } 4.3.         总结 open函数的主要操作就是为文件初始化inode,初始化文件结构,刷新进程文件链表。 5.         creat 函数 asmlinkage long sys_creat(const char __user * pathname, int mode){          return sys_open(pathname, O_CREAT | O_WRONLY | O_TRUNC, mode); }          从上面的简短的实现代码可以看出,creat的系统调用直接以O_CREAT|O_WRONLY|O_TRUNC标识调用open函数实现的。 文章出处:飞诺网(www.diybl.com):http://www.diybl.com/course/6_system/linux/Linuxjs/20101230/550127.html

 
 
Linux中打开文件是通过open系统调用实现,其函数中调用了do_sys_open()函数完成打开功能,所以下面主要分析do_sys_open()函数,首先先看下open系统调用的入口函数,再具体看do_sys_open()函数:

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, int, mode)

{

         long ret;

 

         if (force_o_largefile())

                   flags |= O_LARGEFILE;

 

         ret = do_sys_open(AT_FDCWD, filename, flags, mode);

         /* avoid REGPARM breakage on x86: */

         asmlinkage_protect(3, ret, filename, flags, mode);

         return ret;

}

 

 

long do_sys_open(int dfd, const char __user *filename, int flags, int mode)

{

/*获取文件名称,由getname()函数完成,其内部首先创建存取文件名称的空间,然后*从用户空间把文件名拷贝过来*/

         char *tmp = getname(filename);

         int fd = PTR_ERR(tmp);

 

         if (!IS_ERR(tmp)) {

/*获取一个可用的fd,此函数调用alloc_fd()函数从fd_table中获取一个可用fd,并做些简单初始化,此函数内部实现比较简单,此次分析不细看*/

                   fd = get_unused_fd_flags(flags);

                   if (fd >= 0) {

/*fd获取成功则开始打开文件,此函数是主要完成打开功能的函数,在此先放一放,下面详细分析*/

                            struct file *f = do_filp_open(dfd, tmp, flags, mode, 0);

                            if (IS_ERR(f)) {

                                     /*打开失败,释放fd*/

                                     put_unused_fd(fd);

                                     fd = PTR_ERR(f);

                            } else {

                                     /*文件如果已经被打开了,调用fsnotify_open()函数*/

                                     fsnotify_open(f->f_path.dentry);

                                     /*将文件指针安装在fd数组中*/

                                     fd_install(fd, f);

                            }

                   }

                   /*释放放置从用户空间拷贝过来的文件名的存储空间*/

                   putname(tmp);

         }

         return fd;

}

 

接下来即将进入到打开功能的真正实现功能的函数do_filp_open()函数:

struct file *do_filp_open(int dfd, const char *pathname,

                   int open_flag, int mode, int acc_mode)

{

         /*

         *…若干变量声明

         */

         /*改变参数flag的值,具体做法是flag+1*/

         int flag = open_to_namei_flags(open_flag);

         int force_reval = 0;

 

         /*根据__O_SYNC标志来设置O_DSYNC 标志,用以防止恶意破坏程序*/

         if (open_flag & __O_SYNC)

                   open_flag |= O_DSYNC;

         /*设置访问权限*/

         if (!acc_mode)

                   acc_mode = MAY_OPEN | ACC_MODE(open_flag);

 

         /*根据 O_TRUNC标志设置写权限 */

         if (flag & O_TRUNC)

                   acc_mode |= MAY_WRITE;

 

         /* 设置O_APPEND 标志*/

         if (flag & O_APPEND)

                   acc_mode |= MAY_APPEND;

 

         /*如果不是创建文件*/

         if (!(flag & O_CREAT)) {

                   /*返回特定的file结构体指针*/

                   filp = get_empty_filp();

 

                   if (filp == NULL)

                            return ERR_PTR(-ENFILE);

                   /*填充nameidata 结构*/

                   nd.intent.open.file = filp;

                   filp->f_flags = open_flag;

                   nd.intent.open.flags = flag;

                   nd.intent.open.create_mode = 0;

/*当内核要访问一个文件的时候,第一步要做的是找到这个文件,而查找文件的过程在vfs里面是由path_lookup或者path_lookup_open函数来完成的。这两个函数将用户传进来的字符串表示的文件路径转换成一个dentry结构,并建立好相应的inode和file结构,将指向file的描述符返回用户。用户随后通过文件描述符,来访问这些数据结构*/                   error = do_path_lookup(dfd, pathname,

                                               lookup_flags(flag)|LOOKUP_OPEN, &nd);

                   if (IS_ERR(nd.intent.open.file)) {

                            if (error == 0) {

                                     error = PTR_ERR(nd.intent.open.file);

                                     /*减少dentry和vsmount得计数*/

                                     path_put(&nd.path);

                            }

                   } else if (error)

                            /*如果查找失败则释放一些资源*/

                            release_open_intent(&nd);

                   if (error)

                            return ERR_PTR(error);

                   goto ok;

         }

 

         /*到此则是要创建文件*/

reval:

         /* path-init为查找作准备工作,path_walk真正上路查找,这两个函数联合起来根据一段路径名找到对应的dentry */

         error = path_init(dfd, pathname, LOOKUP_PARENT, &nd);

         if (error)

                   return ERR_PTR(error);

         if (force_reval)

                   nd.flags |= LOOKUP_REVAL;

/*这个函数相当重要,就如源代码注释的那样,是整个NFS的名字解析函数,其实也是NFS得以构筑的函数。这里作一重点分析。这里先作一个综述。该函数采用一个for循环,对name路径根据目录的层次,一层一层推进,直到终点或失败。在推进的过程中,一步步建立了目录树的dentry和对应的inode */

         error = path_walk(pathname, &nd);

         if (error) {

                   if (nd.root.mnt)

                            path_put(&nd.root);

                   return ERR_PTR(error);

         }

         if (unlikely(!audit_dummy_context()))

                   /*保存inode节点信息*/

                   audit_inode(pathname, nd.path.dentry);

 

         /*父节点信息*/

         error = -EISDIR;

         if (nd.last_type != LAST_NORM || nd.last.name[nd.last.len])

                   goto exit_parent;

 

         error = -ENFILE;

         /*获取文件指针*/

         filp = get_empty_filp();

         if (filp == NULL)

                   goto exit_parent;

         /*填充nameidata 结构*/

         nd.intent.open.file = filp;

         filp->f_flags = open_flag;

         nd.intent.open.flags = flag;

         nd.intent.open.create_mode = mode;

         dir = nd.path.dentry;

         nd.flags &= ~LOOKUP_PARENT;

         nd.flags |= LOOKUP_CREATE | LOOKUP_OPEN;

         if (flag & O_EXCL)

                   nd.flags |= LOOKUP_EXCL;

         mutex_lock(&dir->d_inode->i_mutex);

         /*从哈希表中查找nd对应的dentry*/

         path.dentry = lookup_hash(&nd);

         path.mnt = nd.path.mnt;

 

do_last:

         error = PTR_ERR(path.dentry);

         if (IS_ERR(path.dentry)) {

                   mutex_unlock(&dir->d_inode->i_mutex);

                   goto exit;

         }

 

         if (IS_ERR(nd.intent.open.file)) {

                   error = PTR_ERR(nd.intent.open.file);

                   goto exit_mutex_unlock;

         }

 

         /*如果此dentry结构没有对应的inode节点,说明是无效的,应该创建文件节点 */

         if (!path.dentry->d_inode) {

                   /*write权限是必需的*/

                   error = mnt_want_write(nd.path.mnt);

                   if (error)

                            goto exit_mutex_unlock;

                   /*按照namei格式的flag open*/

                   error = __open_namei_create(&nd, &path, flag, mode);

                   if (error) {

                            mnt_drop_write(nd.path.mnt);

                            goto exit;

                   }

                   /*根据nameidata 得到相应的file结构*/

                   filp = nameidata_to_filp(&nd);

                   /*放弃写权限*/

                   mnt_drop_write(nd.path.mnt);

                   if (nd.root.mnt)

                            /*计数减一*/

                            path_put(&nd.root);

                   if (!IS_ERR(filp)) {

                            error = ima_file_check(filp, acc_mode);

                            if (error) {

                                     fput(filp);

                                     filp = ERR_PTR(error);

                            }

                   }

                   return filp;

         }

 

         /*要打开的文件已经存在*/

         mutex_unlock(&dir->d_inode->i_mutex);

         /*保存inode节点*/

         audit_inode(pathname, path.dentry);

 

/*

         *省略若干flag标志检查代码

         */

 

         /*路径装化为相应的nameidata 结构*/

         path_to_nameidata(&path, &nd);

         error = -EISDIR;

         /*如果是文件夹*/

         if (S_ISDIR(path.dentry->d_inode->i_mode))

                   goto exit;

ok:

         /*检测是否截断文件标志*/

         will_truncate = open_will_truncate(flag, nd.path.dentry->d_inode);

         if (will_truncate) {

         /*要截断的话就要获取写权限*/

                   error = mnt_want_write(nd.path.mnt);

                   if (error)

                            goto exit;

         }

         //may_open执行权限检测、文件打开和truncate的操作

         error = may_open(&nd.path, acc_mode, flag);

         if (error) {

                   if (will_truncate)

                            mnt_drop_write(nd.path.mnt);

                   goto exit;

         }

         filp = nameidata_to_filp(&nd);

         if (!IS_ERR(filp)) {

                   error = ima_file_check(filp, acc_mode);

                   if (error) {

                            fput(filp);

                            filp = ERR_PTR(error);

                   }

         }

         if (!IS_ERR(filp)) {

                   if (acc_mode & MAY_WRITE)

                            vfs_dq_init(nd.path.dentry->d_inode);

 

                   if (will_truncate) {

                            //处理截断

                            error = handle_truncate(&nd.path);

                            if (error) {

                                     fput(filp);

                                     filp = ERR_PTR(error);

                            }

                   }

         }

         //安全的放弃写权限

         if (will_truncate)

                   mnt_drop_write(nd.path.mnt);

         if (nd.root.mnt)

                   path_put(&nd.root);

         return filp;

 

exit_mutex_unlock:

         mutex_unlock(&dir->d_inode->i_mutex);

exit_dput:

         path_put_conditional(&path, &nd);

exit:

         if (!IS_ERR(nd.intent.open.file))

                   release_open_intent(&nd);

exit_parent:

         if (nd.root.mnt)

                   path_put(&nd.root);

         path_put(&nd.path);

         return ERR_PTR(error);

//允许遍历连接文件,则手工找到连接文件对应的文件

do_link:

         error = -ELOOP;

         if (flag & O_NOFOLLOW)

                   //不允许遍历连接文件,返回错误

                   goto exit_dput;

         /*

          以下是手工找到链接文件对应的文件dentry结构代码

          */

         // 设置查找LOOKUP_PARENT标志

         nd.flags |= LOOKUP_PARENT;

         //判断操作是否安全

         error = security_inode_follow_link(path.dentry, &nd);

         if (error)

                   goto exit_dput;

         // 处理符号链接

         error = __do_follow_link(&path, &nd);

         path_put(&path);

         if (error) {

         

                   release_open_intent(&nd);

                   if (nd.root.mnt)

                            path_put(&nd.root);

                   if (error == -ESTALE && !force_reval) {

                            force_reval = 1;

                            goto reval;

                   }

                   return ERR_PTR(error);

         }

         nd.flags &= ~LOOKUP_PARENT;

         // 检查最后一段文件或目录名的属性情况

         if (nd.last_type == LAST_BIND)

                   goto ok;

         error = -EISDIR;

         if (nd.last_type != LAST_NORM)

                   goto exit;

         if (nd.last.name[nd.last.len]) {

                   __putname(nd.last.name);

                   goto exit;

         }

         error = -ELOOP;

         // 出现回环标志: 循环超过32次

         if (count++==32) {

                   __putname(nd.last.name);

                   goto exit;

         }

         dir = nd.path.dentry;

         mutex_lock(&dir->d_inode->i_mutex);

         // 更新路径的挂接点和dentry

         path.dentry = lookup_hash(&nd);

         path.mnt = nd.path.mnt;

         __putname(nd.last.name);

         goto do_last;

}

分析完上述主要函数以后,我们来看一下整个打开流程是如何做到的:

在内核中要打开一个文件,首先应该找到这个文件,而查找文件的过程在vfs里面是由do_path_lookup或者path_lookup_open函数来完成的。这两个函数将用户传进来的字符串表示的文件路径转换成一个dentry结构,并建立好相应的inode和file结构,将指向file的描述符返回用户。用户随后通过文件描述符,来访问这些数据结构。

基本函数流程及调用方式如下所示:

打开过程首先是open系统调用访问SYSCALL_DEFINE3函数,然后调用do_sys_open 函数完成主要功能,再调用函数do_filp_open完成主要的打开功能,下面详细看下do_filp_open中调用的do_path_lookup主要过程:

staic int  do_path_lookup(int dfd,const char

*name,unsigned int flags,strucy nameidata *nd)

{

           int retval=path_init(dfd,name,flags,nd);

//设置nd->root=根路径(绝对地址)或者当前工作目录(相对地址) 。

//这一步做完了后,内核会建立一些数据结构(dentry,inode)来初始化查找的起点

           if(!retval)

                       retval = path_walk(name,nd);

//path_walk,会遍历路径的每一份量,也就是用“/”分隔开的每一部分,

//最中找到name指向的文件,walk的意思就是walk path的每一个组分(component)

        }

 

我们进一步看看path_walk

int path_walk(const char *name,struct nameidata *nd)

{

            return link_path_walk(name,nd);

//path_walk其实相当于直接调用link_path_walk来完成工作

}

link_path_walk的主要工作是有其内部函数__link_path_walk 来完成的

          result = __link_path_walk(name,nd)

         

至此我们转向最重要的代码__link_walk_path,该函数把传进来的字符串name,也就是用户指定的路径,按路径分隔符分解成一系列小的component。比如用户说,我要找/path/to/dest这个文件,那么我们的文件系统就会按path,to,dest一个一个来找,知道最后一个分量是文件或者查找完成。他找的时候,会先用path_init初始化过的根路径去找第一个分量,也就是path。然后用path的dentry->d_inode去找to,这样循环到最后一个。注意,内核会缓存找到的路径分量,所以往往只有第一次访问一个路径的时候,才会去访问磁盘,后面的访问会直接从缓存里找,下面会看到,很多与页告诉缓存打交道的代码。但不管怎样,第一遍查找总是会访问磁盘的。

static int __link_path_walk(const char *name,strucy

nameidata *nd)

{

}

至此,按照每一个component查找完成之后,就会找到相应的文件,然后相应的打开工作就基本完成了。


 

 

 

open函数的流程 1.          引言       从事Linux环境工作2年有余,一直懵懵懂懂,1年前拜读了《莱昂氏UNIX源代码分析》一书,感觉自己的学习道路漫漫且修远。最近受chinaunix的精华文帖启发,拟将近来的部分内核调用分析笔记拿出来与各前辈先进共同探讨学习,以壮个人学习之路。       本部分主要讲述的是文件I/O操作的2.6.11内核版本实现,包括了主要的数据结构、宏定义和函数流程 阅读详情

相关推荐

SourceInsight:Linux内核源码阅读器

Source Insight是一个面向项目开发的程序编辑器和代码浏览器,它拥有内置的对C/C++, C#和Java等程序的分析。SourceInsight是基于windows操作系统的源码阅读器,在阅读Linux内核源码之前要先把它放到windows操作系统下。

qq_35713097的博客 2146

Linux文件系统IO:直接IO原理与实现:缓存I/O、直接I/O

目录 缓存I/O 缓存I/O的优缺点 直接I/O 直接I/O实现 -direct_IO(),brw_kiovec() 推荐阅读 缓存I/O 一般来说,当调用open()系统调用打开文件时,如果不指定O_DIRECT标志,那么就是使用缓存I/O来对文件进行读写操作。我们先来看看open()系统调用的定义: int open(const char *pathname, int flags, ... /*, mode_t mode */ ); 下面说明一下各个参数的作用:...

RToax 4299

Linux内核基础——Linux源码阅读工具Source Insight4.0

Linux内核源码阅读工具——source insight4.0Source insight4.0工具的使用入门一、Souce insight建立工程、导入源码二、遍历所有源码文件建立符号索引 Source insight4.0工具的使用入门 本篇博客为学习同步更新博客,学习如何使用source insight查看linux内核源码以及工具的使用技巧。 一、Souce insight建立工程、导入...

SuperBoy_Zjc 4717

Linux内核文件操作

Linux内核文件操作前言一、文件操作结构体二、VFS之file_operations对象1.文件打开filp_open2.文件关闭filp_close3.文件读取vfs_read4.文件写入vfs_write4.注意点驱动模块实例 前言 Linux系统中的文件系统由两层结构进行构建:第一层为虚拟文件系统(VFS),第二层则是各种不同的具体的文件系统。VFS则是将各种具体的文件系统的公共部分抽取出来,从而形成一个抽象层,是Linux系统内核的一部分,它位于用户程序和具体的文件系统之间,对用户提供了标准的

年少生而为人 1817

VSCode+clangd阅读linux内核源码

VSCode+clangd阅读linux内核源码

Jaylen.sun的嵌入式之旅 1万+

linux 应用程序中的open 操作与硬件驱动之间的联系浅析

第一节 open 函数的系统操作过程,在fs/open.c 文件中 read、write 函数的系统操作过程,在fs/read_write.c 文件中 对于linux 下所有的系统调用函数,均可找到与其对应的内核函数sys_xxx().比如此处的open函数,对应内核函数就是sys_open。只不过,在此内核版本中,它的定义是如下这种形式 SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, int,mode)。 这个SYSCALL_DEFINE3 就是个宏定义,如果展开,到最后,其实上面这个函数就是 asmlinkage long sys_open(const char __user filename,int flags,int mode)。 那么,就是上面说过的,open 函数,对应的内核函数,就是sys_open,这种形式。

文件系统的系统调用过程

<br />有关文件系统的系统调用中有好几个,这里分析了其中的系统调用open、sys_read。其它的系统调用参阅内核源代码。 系统调用open <br />系统调用open是由函数sys_open(fs/open.c)实现的。函数sys_open最终通过具体文件系统的节点的lookup函数,用 文件路径名查找得到路径名对应的dentry结构,通过dentry得到对应的inode结构,分配文件描述符,用fd文件描述符作为file的索引,用 dentry结构中数据填充这个file结构。以后对这个

lcw_202的专栏 1701

自动驾驶仿真(二)—— 基于PreScan与Simulink的联合仿真

自动驾驶仿真软件介绍二—— PreScan与Simulink联合仿真1. Prescan仿真软件2. Prescan与Simulink联合仿真2. 1 Prescan和matlab软件安装2. 2 联合仿真配置3. 联合仿真实例3.1 一辆不按法规行驶的小车3.2 另一辆的正常行驶的小车3.3 视觉辅助摄像机3.4 传感器检测4. 系列学习视频教程5. 参考学习的书目教材 上一篇博文简单介绍了Carsim与Simulink联合仿真,接着这篇就再继续简单介绍另一款自动驾驶仿真软件Prescan,并且简单做个P

zhudj08的博客 1万+

使用 Source Insight 阅读 Linux 内核源码

在后面开发驱动程序时,驱动程序中用到的函数都是来自内核,所以可以先在 Windows 下创建内核的Source Insight 工程。 如果你不想学习驱动开发,那么可以不创建内核的工程。但是以后学习大型 APP 时,也可以使用 Source Insight 来阅读、编写代码,可以借鉴本节讲解的 Source Insight 用法。 7.4.1 Source Insight 简介 Source Insight 是 Source Dynamics 公司出品的源代码编辑器。Source Insight 提供语法突

韦东山嵌入式专栏 8061

H3C ACL的应用与配置1

SWHC1-acl-ipv4-adv-3000]rule 30 permit ip source 192.168.50.252 0 destination 192.168.70.252 0 //设置一条ID为30的过滤,允许源地址为 192.168.50.252 访问目的地址为192.168.70.252 的设备。高级ACL范围3000-3999:对源IP,目地IP,协议,端口号进行过滤。[SWHC1]acl number 3000 //创建高级ACL 3000。

weixin_41120428的博客 2425

二元交叉熵:分类模型概率校准的数学根基

二元交叉熵(BCE)是二分类任务中实现概率校准的核心损失函数,其本质源于最大似然估计与KL散度,确保模型输出具备真实可解释的概率意义。相比均方误差(MSE)或Hinge Loss,BCE强制优化预测分布与真实伯努利分布之间的信息差异,使p=0.8真正对应约80%的正例发生率,支撑风控、医疗、推荐等高信度场景的阈值决策。它不只影响训练收敛,更决定部署后概率的业务可信度——这正是Focal Loss等变体无法替代其基础地位的根本原因。理解BCE,就是理解分类模型如何从‘打分器’进化为‘概率契约签署者’。

congli3478的博客 1334

【Linux 内核】Linux 内核源码结构 ( 下载 Linux 内核源码 | 使用 VSCode 阅读 Linux 内核源码 )

一、下载 Linux 内核源码、 二、使用 VSCode 阅读 Linux 内核源码

让 学习 成为一种 习惯 ( 韩曙亮 の 技术博客 ) 5523

MATLAB实战:手把手教你实现WVD信号分析(附完整代码与避坑指南)

本文通过MATLAB实战,手把手教你实现Wigner-Ville分布(WVD)信号分析。文章提供了从零构建WVD函数的完整代码,并重点解析了使用解析信号、处理交叉项、优化计算效率等关键避坑指南,帮助工程师高效处理非平稳信号,获得高分辨率的时频分析结果。

purple的博客 1081

Linux内核源码阅读以及工具详解

接上篇Linux内核源码下载方法 这篇总结了如何利用source insight对Linux内核代码进行阅读和学习(资料来源于网络) 随着linux的逐步普及,现在有不少人对于Linux的安装及设置已经比较熟悉了。与Linux 的蓬勃发展相适应,想深入了解Linux的也越来越多。而要想深入了解Linux,就需要阅读和分析linux内核的源代码。 Linux的内核源代码可以从很多途径得到。一般来讲...

CodeAllen嵌入式 4537

【verilog语法详解:从入门到精通】

Verilog语法详解:从入门到精通 摘要:本文系统讲解Verilog硬件描述语言的核心语法与应用。Verilog作为数字电路设计标准,具有并行执行、硬件映射和层次化三大特点。文章从基础语法框架入手,详细介绍了模块定义、端口类型(input/output/inout)声明规范,以及线网型(wire)和寄存器型(reg)两种核心数据类型的区别与应用场景。重点讲解了组合逻辑(assign连续赋值)和时序逻辑(always块)的实现方式,包括阻塞赋值(=)与非阻塞赋值(<=)的关键差异。此外,还涵盖了参数(

m0_58476824的博客 4684

QEMU pcie config空间访问机制

而pci_host_conf/data_le_ops的定义是在hw/pci/pci-host.c里,对于0xCF8端口的处理,因为是地址选通,所以只需要在write的时候记录、read的时候返回记录的值即可。而pci_host_config_write/read_common接口,在前256Bconfig空间的模拟中,也同样是会调用这个接口,所以后续的处理都是一样的。这是真实的硬件设计,而对于QEMU+KVM的虚机场景,显然是要基于硬件实现和虚拟化的需求设计虚机访问config空间的完整流程。

leiyanjie8995的博客 1953

linux 内核代码阅读工具,linux内核源码阅读工具

一 linux内核源码阅读工具windows下当然首选source insight, 但是linux下就没有sourceinsight这么优秀的工具了,但是也有不少的替代品,但觉绝对部分人会选择vim+ctags+cscope的组合,还有部分人或选择wine中的sourceinsight或选择navigatror,当然对于代码阅读来说vim+ctags+cscope的组合还是比较好的一个选择方案,...

weixin_42306002的博客 1991

别再死磕GAN了!用PyTorch从零实现DDPM图像生成(附CIFAR-10完整代码)

本文详细介绍了如何使用PyTorch从零实现DDPM(Denoising Diffusion Probabilistic Models)图像生成模型,并在CIFAR-10数据集上进行训练和测试。通过对比GAN,DDPM展现出更稳定的训练过程和更高质量的生成效果。文章包含完整的代码实现、训练技巧和优化建议,帮助开发者快速掌握这一前沿的生成模型技术。

weixin_33720078的博客 630

Linux内核源码阅读入门(全网最详细教程)

通过阅读Linux内核,你能深入理解Linux底层运行机制,对操作系统有深入理解,会对整个计算机体系都有一个更深刻的认识。

猪猪和超超的博客 1万+

ABAP CDS SELECT, parameters 全面解析:把参数化视图玩明白(含可运行示例)

这篇技术博客介绍了ABAP CDS中参数化视图的用法与实践。文章从业务需求出发,解释了参数化视图的必要性,并详细说明了语法规则、参数类型匹配、视图内部参数引用等关键点。重点展示了在JOIN和ASSOCIATION中传递参数的写法,提供了航线分析的完整业务案例和可运行代码示例。文章还总结了视图实体与DDIC视图的差异、会话变量使用技巧等实践要点,特别强调了JOIN中必须显式传参的语法要求。通过真实场景演示,帮助开发者掌握参数化CDS的高效应用。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 363

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

vscode+clangd阅读Linux内核源码

运行上述命令后,在内核源码根目录生成了compile_commands.json文件。至此,配置已经完成。打开任意c文件,点击函数以后,就能自动跳转到其定义处。添加下面的选项参数到clangd server。[在这里插入图片描述]t。

Yuke的博客 1907

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

如何阅linux内核源码,阅读 Linux 内核源码的正确姿势

首页 专栏 linux 文章详情0 阅读 Linux 内核源码的正确姿势 Constantine3 发布于 2 月 28 日引言动机最近有阅读 Linux 内核源码的需求,而现在主力机是 Mac,所以 Windows 环境下传统的 Source Insight 工具不能继续使用了。那么,只好祭出宇宙最强编辑器——VSCode!效果先看最终效果图:代码语法高亮也不会有讨厌的红色波浪线报错。同时支持跳...

weixin_35997546的博客 1061

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

搞会linux内核源码,linux内核源码阅读工具

一 linux内核源码阅读工具windows下当然首选source insight, 但是linux下就没有sourceinsight这么优秀的工具了,但是也有不少的替代品,但觉绝对部分人会选择vim+ctags+cscope的组合,还有部分人或选择wine中的sourceinsight或选择navigatror,当然对于代码阅读来说vim+ctags+cscope的组合还是比较好的一个选择方案,...

weixin_42174098的博客 186

linux内核源码怎么读,linux内核源码阅读工具

一 linux内核源码阅读工具windows下当然首选source insight, 但是linux下就没有sourceinsight这么优秀的工具了,但是也有不少的替代品,但觉绝对部分人会选择vim+ctags+cscope的组合,还有部分人或选择wine中的sourceinsight或选择navigatror,当然对于代码阅读来说vim+ctags+cscope的组合还是比较好的一个选择方案,...

weixin_35725389的博客 244
上一篇: kobject,kset,子系统层次结构 、platform_device platform_driver
下一篇: 关于platform_driver 是如何匹配 platform_device的和如何调用到platform_driver中的probe函数的研究
ADC0809608
博客等级 码龄17年 44粉丝 10原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值