10无持久存储的文件系统

10无持久存储的文件系统

在内存中的文件系统,信息由内核动态生成

  1. proc文件系统(proc filesystem),它使得内核可以生成与系统的状态和配置有关的信息。该信息可以由用户和系统程序从普通文件读取,而无需专门的工具与内核通信。只有发出读操作请求时,才会生成信息。
  2. Sysfs是另一个特别重要的虚拟文件系统例子。Sysfs按照惯例总是装载在/sys目录,但这不是强制规定,装载到其他位置也是可以的。此外,文件包含的信息并不总是ASCII文本形式,也有可能使用不可读的二进制串。但对于想要收集系统中的硬件和设备间拓扑关联方面详细信息的工具而言,该文件系统是非常有用的.还可以对使用kobject的内核对象创建sysfs项。这使得用户层很容易访问内核中重要的核心数据结构
  3. 用于专门目的的小文件系统,可以由内核提供的标准函数构建。在内核内部,libfs库提供了所需功能。此外,内核提供了易于实现顺序文件的方法。在调试文件系统debugfs中同时使用了这两种技术,该文件系统使得内核开发者能够快速地向用户空间导出值或从用户空间导入值,而无需创建定制的接口或专门的文件系统

10.1 proc文件系统

proc文件系统,可以获得有关内核各子系统的信息(例如,内存利用率、附接的外设,等等),也可以在不重新编译内核源代码的情况下修改内核的行为,或重启系统。与该文件系统密切相关的是系统控制机制(system control mechanism,简称sysctl)

通常,进程数据文件系统(process data filesystem,procfs的全称)装载在/proc,它的缩写proc FS

10.1.1 /proc的内容

proc文件系统的容量依系统而不同(根据硬件配置导出不同的数据,不同的体系结构也会影响其内容),包含的信息可以分为以下几大类

  • 内存管理;
  • 系统进程的特征数据;
  • 文件系统;
  • 设备驱动程序;
  • 系统总线;
  • 电源管理;
  • 终端;
  • 系统控制参数
  1. 特定于进程的数据
    每个进程都对应一个子目录(与其PID同名),包含进程信息.顾名思义,进程数据系统(process data system,简称proc)的初衷就是传递进程数据

    • cmdline 启动进程执行的命令,使用了0字节作为分隔符
    • environ 表示为该程序设置的所有环境变量,其仍然使用了0字节作为分隔符。
    • maps 以文本形式,列出了进程使用的所有库(和进程本身的二进制文件)的内存映射。
    • status 包含了有关进程状态的一般信息(文本格式)
      不仅提供了有关UID/GID及进程其他数值的信息,还包括内存分配、进程能力、各个信号掩码的状态(待决、阻塞,等等)
    • stat和statm以一连串数字的形式,提供了进程及其内存消耗的更多状态信息。
    • fd 目录包含了一些文件,文件名都是数字。这些文件名表示进程的各个文件描述符。这里的每个文件都是一个符号链接,指向文件名对应的文件描述符在文件系统中的位置,当然得假定该描述符确实是文件。其他的文件类型,如果也能够通过文件描述符访问(如管道),那么将给出一个链接目标,如pipe:[1434]。类似地,还有其他指向与进程相关的文件和命令的符号链接
    • cwd指向进程当前工作目录。如果用户有适当的权限,则可以使用cd cwd切换到该目录,而无需知道cwd到底指向哪个目录
    • exe指向包含了应用程序代码的二进制文件。在我们的例子中,它指向/usr/bin/emacs。
    • root指向进程的根目录。这不见得是全局的根目录
  2. 一般性系统信息
    /proc本身包含了一些信息与特定的内核子系统无关(或由几个子系统共享)的一般性信息,一般存放在/proc下的文件中.

    • iomem和ioports提供了用来与设备通信的内存地址和端口的有关信息
    • buddyinfo和slabinfo提供了伙伴系统和slab分配器当前的使用情况
    • meminfo给出了一般性的内存使用情况,分为高端内存、低端内存、空闲内存、已分配区域、共享区域、交换和回写内存
    • vmstat给出了内存管理的其他特征信息,包括当前在内存管理的各个子系统中内存页的数目
    • kallsyms项用于支持内核代码调试。是一个符号表,给出了所有全局内核变量和函数在内存中的地址
    • kcore是一个动态的内核文件,“包含”了运行中的内核的所有数据,即主内存的全部内容。与用户应用程序发生致命错误时进行内存转储所产生的普通内核文件相比,该文件没什么不同之处。可以将调试器用于该二进制文件,来查看运行中系统的当前状态。在本书中,用于说明内核数据结构之间交互作用的许多图,都是用这种方法制备的。可以借助GNU gdb调试器和ddd图形用户界面,来使用内核提供的这些可用的功能。
    • interrupts保存了当前操作期间引发的中断的说明.给出了中断的数目,还对每个中断号,都给出相关设备的名称或负责处理中断的驱动程序。
    • loadavg给出了过去60秒、5分钟、15分钟的平均系统负荷(即,运行队列的长度)
    • uptime给出了系统的运行时间,即从系统启动以来经过的时间
  3. 网络信息
    /proc/net子目录提供了内核的各种网络选项的有关数据。其中保存了各种协议和设备数据

    • udp和tcp提供了IPv4的UDP和TCP套接字的统计数据。IPv6的对应数据保存在udp6和tcp6中。UNIX套接字的统计数据记录在unix
    • arp查看用于反向地址解析的ARP表
    • dev保存了通过系统的网络接口传输的数据量的统计数据(包括环回接口)。该信息可用于检查网络的传输质量,因为其中也包括了传输不正确的数据包、被丢弃的数据包和冲突相关的数据

    有些网络驱动程序(如,流行的英特尔PRO/100芯片组的驱动程序)在/proc/net创建了额外的子目录,提供了更详细的特定于硬件的信息

  4. 系统控制参数
    最多的一部分是用于动态地检查和修改内核行为的系统控制参数数据项.但这并不是修改相关数据的唯一方法,还可以使用sysctl系统调用.在内核版本2.5开发期间,sysctl机制标记为废弃(每次调用sysctl时,内核将输出一个警告信息),计划在未来的某个时候去掉。但是,删除系统调用引起了争论,直至内核版本2.6.25,该调用仍然存在于内核中,而警告信息也仍然会出现

    sysctl参数由一个独立的子目录/proc/sys管理,它进一步划分为各种子目录,对应于内核的各个子系统

    例如,vm子目录包含了一个swappiness文件,表示交换算法在换出页时的“积极”程度。默认值是60,从cat显示的文件内容可以看到:cat /proc/sys/vm/swappiness

10.1.2 数据结构

  1. proc数据项的表示 proc_dir_entry

    //include/linux/proc_fs.h
    //proc文件系统中的每个数据项都由proc_dir_entry的一个实例描述
    struct proc_dir_entry {
        unsigned int low_ino;//inode的编号
        unsigned short namelen;//指定了文件名的长度
        const char *name;//存储文件名的字符串的指针
        mode_t mode;//与经典文件系统相同,数据项类型,如文件,目录等,以及访问权限,"所有者、组、其他"
        nlink_t nlink;//建立文件系统层次结构,指定了目录中子目录和符号链接的数目
        uid_t uid;//该文件所有者的用户ID,一般为0表示root
        gid_t gid;//该文件所有者的组ID,一般为0表示root
        loff_t size;//按字节计算的文件大小,由于proc数据项是动态生成的,所以文件的长度通常无法预先知道。在这种情况下,该值为0
        const struct inode_operations *proc_iops;//inode操作函数集合的指针(如创建链接,文件重命名,在目录中生成新文件,删除文件等)
        const struct file_operations *proc_fops;//对文件操作的函数集合的指针(如打开,读取,写入等)
        get_info_t *get_info;//函数指针,从内核读取数据,可以指定所需范围的偏移量和长度,这样就不必读取整个数据集。该接口很有用,例如,可以用于proc数据项的自动分析.与 read_proc 作用相同
        struct module *owner;//如果一个proc数据项由动态加载的模块产生,那么owner指向相关联模块在内存中的数据结构,编入内核的为NULL
        /*
        * 建立文件系统层次结构,
        * parent:指向父目录的指针
        * subdir:指向目录中的第一个子数据项,可能是文件或目录
        * next:将目录下的所有常见数据项都群集到一个单链表中
        * */
        struct proc_dir_entry *next, *parent, *subdir;
        void *data;//额外参数,在调用 read_proc 和 write_proc 回调函数时传入
        read_proc_t *read_proc;//函数指针,从内核读取数据,与 get_info 作用相同,多了data参数
        write_proc_t *write_proc;//函数指针,向内核写入数据
        ...
    };
    
  2. proc inode

    //include/linux/proc_fs.h
    union proc_op {
        int (*proc_get_link)(struct inode *, struct dentry **, struct vfsmount **);//用于获得特定于进程的信息
        int (*proc_read)(struct task_struct *task, char *page);//用于在虚拟文件系统中建立链接,指向特定于进程的数据
    };
    
    //用来支持以inode的方式查看proc文件系统的数据项,用来将特定于proc的数据与VFS层的inode数据关联起来
    struct proc_inode {
        struct pid *pid;//结构体表示进程时使用,指向进程的pid
        int fd;//结构体表示进程时使用,记录了文件描述符,它对应于/proc/<pid>/fd/中的某个文件。借助fd,该目录下的所有文件都可以使用同一file_operations
        union proc_op op;//结构体表示进程时使用,指向操作函数
        struct proc_dir_entry *pde;//指向关联到proc数据项的proc_dir_entry实例
        struct inode vfs_inode;
    };
    
    //通过inode获取proc_inode 
    static inline struct proc_inode *PROC_I(const struct inode *inode)
    

    在这里插入图片描述

10.1.3 初始化 proc_root_init

/proc的外观和内容随着平台和体系结构的变化很大,代码中充满了#ifdef预处理器语句

在这里插入图片描述

函数流程:

  1. 创建slab缓存
  2. 注册文件系统
  3. 装载文件系统
  4. 创建proc主目录中的各种文件项,将各项与特定读函数关联
  5. 创建与网络相关的文件项/proc/net下的文件
  6. 创建proc的空子目录,之后添加目录项时需要用到
  7. 进一步的目录初始化工作,不再由proc层自身负责,而是由提供相关信息的其他内核部分接手,如proc/net中的文件就是由网络层创建的

在内核中定义一个新的sysctl时,系统控制机制会建立对应的文件,并添加到proc_sys_root中

10.1.4 装载proc文件系统

mount装载流程与普通文件系统过程相同

root@meitner # mount -t proc proc /proc
//fs/proc/root.c
static struct file_system_type proc_fs_type = {
	.name		= "proc",
	.get_sb		= proc_get_sb,
	.kill_sb	= proc_kill_sb,
};

//fs/proc/inode.c
//对proc文件系统的超级块的操作函数集合
static const struct super_operations proc_sops = {
	.alloc_inode	= proc_alloc_inode,
	.destroy_inode	= proc_destroy_inode,
	.read_inode	= proc_read_inode,
	.drop_inode	= generic_delete_inode,
	.delete_inode	= proc_delete_inode,
	.statfs		= simple_statfs,
	.remount_fs	= proc_remount,
};

//fs/proc/root.c
struct proc_dir_entry proc_root = {
	.low_ino	= PROC_ROOT_INO, 
	.namelen	= 5, 
	.name		= "/proc",
	.mode		= S_IFDIR | S_IRUGO | S_IXUGO, 
	.nlink		= 2, 
	.count		= ATOMIC_INIT(1),
	.proc_iops	= &proc_root_inode_operations, 
	.proc_fops	= &proc_root_operations,
	.parent		= &proc_root,
};

//proc根文件目录inode操作
static const struct inode_operations proc_root_inode_operations = {
	.lookup		= proc_root_lookup,
	.getattr	= proc_root_getattr,
};

//proc根文件目录操作
static const struct file_operations proc_root_operations = {
	.read		 = generic_read_dir,
	.readdir	 = proc_root_readdir,
};

10.1.5 /proc数据项的操作函数

用代码来创建新的数据项并不是常例。尽管如此,在进行测试时,这些接口很有用处。借助这些简单、轻量级的接口,我们就可以用很小的代价在内核与用户空间之间打开一条通信渠道用于测试

下列为向proc中添加数据项的辅助函数

  1. 数据项的创建和注册 create_proc_entry create_proc_info_entry create_proc_read_entry
    新数据项分两个步骤添加到proc文件系统。首先,创建proc_dir_entry的一个新实例,填充描述该数据项的所有需要的信息。然后,将该实例注册到proc的数据结构,内核提供辅助函数合并了这两个操作

    //fs/proc/generic.c
    //创建新的proc数据项
    struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode,
    				 struct proc_dir_entry *parent)
    
    //include/linux/proc_fs.h
    //创建一个新的可读取的数据项,使用get_info_t函数
    static inline struct proc_dir_entry *create_proc_info_entry(const char *name,
        mode_t mode, struct proc_dir_entry *base, get_info_t *get_info)
    //创建一个新的可读取的数据项,使用read_proc_t函数
    static inline struct proc_dir_entry *create_proc_read_entry(const char *name,
        mode_t mode, struct proc_dir_entry *base, 
        read_proc_t *read_proc, void * data)
    
    • create_proc_entry
      • proc_create
      • proc_register
    //fs/proc/generic.c
    //proc普通文件inode操作
    static const struct inode_operations proc_file_inode_operations = {
        .setattr	= proc_notify_change,
    };
    
    //proc普通文件操作
    static const struct file_operations proc_file_operations = {
        .llseek		= proc_file_lseek,
        .read		= proc_file_read,
        .write		= proc_file_write,
    };
    
    //proc链接文件inode操作
    static const struct inode_operations proc_link_inode_operations = {
        .readlink	= generic_readlink,
        .follow_link	= proc_follow_link,
    };
    
    //fs/proc/generic.c
    //创建一个新目录
    struct proc_dir_entry *proc_mkdir(const char *name,
            struct proc_dir_entry *parent)
    //创建一个新目录,目录的访问权限可以显式指定
    struct proc_dir_entry *proc_mkdir_mode(const char *name, mode_t mode,
            struct proc_dir_entry *parent)
    //生成一个符号链接
    struct proc_dir_entry *proc_symlink(const char *name,
            struct proc_dir_entry *parent, const char *dest)
    //从proc目录中删除一个文件
    void remove_proc_entry(const char *name, struct proc_dir_entry *parent)
    

    函数使用说明:Documentation/DocBook/procfs_example.c

  2. 查找proc数据项 proc_root_lookup
    查找操作到达 real_lookup .该函数将调用inode_operations的lookup函数指针(proc_root_lookup),根据文件名的各个路径分量,来确定文件名所对应的inode

    在这里插入图片描述

10.1.6 读取和写入信息 proc_file_read和proc_file_write

  1. proc_file_read的实现
    1. 分配一个内存页
    2. 调用读函数填充页
    3. 将数据从内核空间复制到用户空间
  2. proc_file_write的实现
    1. 通过inode获取proc_dir_entry
    2. 调用注册的写函数write_proc(由具体的内核模块实现)
      1. 通常会执行以下操作
      2. 检查用户输入的长度(使用count参数确定)不超过分配区域的长度
      3. 从用户空间复制数据到内核空间
      4. 解析字符串
      5. 根据解析操作子系统

10.1.7 进程相关的信息 proc_pid_lookup

proc_pid_lookup负责打开/proc/<pid>中特定于PID的文件
在这里插入图片描述

proc_pid_lookup两个执行过程:

  1. self目录

    /proc/self目录中为当前进程的信息,是一个符号链接

    //fs/proc/base.c
    //用于/proc/self的inode操作
    static const struct inode_operations proc_self_inode_operations = {
        .readlink	= proc_self_readlink,
        .follow_link	= proc_self_follow_link,
    };
    

    利用当前进程信息创建inode

  2. PID目录

    • 创建目录inode

      1. 将字符串文件名转为int类型pid号
      2. 根据pid和命名空间查找进程结构体
      3. 创建inode
    • 处理文件
      在特定于PID的目录/proc/pid中处理一个文件(或目录)时,这是使用该目录的inode操作完成的,

      //fs/proc/base.c
      //proc/pid目录中文件的inode操作函数
      static const struct inode_operations proc_tgid_base_inode_operations = {
          .lookup		= proc_tgid_base_lookup,
          .getattr	= pid_getattr,
          .setattr	= proc_setattr,
      };
      
      //fs/proc/base.c
      //proc/pid中内容都是相同的,定义了目录中的所有内容
      static const struct pid_entry tgid_base_stuff[] = {
      ...
      }
      
      //proc/pid目录中每项的属性
      struct pid_entry {
          char *name;
          int len;
          mode_t mode;
          const struct inode_operations *iop;
          const struct file_operations *fop;
          union proc_op op;
      };
      
      /*创建目录文件数据结构,pid_entry结构体初始化*/
      #define DIR(NAME, MODE, OTYPE)							\
          NOD(NAME, (S_IFDIR|(MODE)),						\
              &proc_##OTYPE##_inode_operations, &proc_##OTYPE##_operations,	\
              {} )
      /*创建链接文件数据结构,pid_entry结构体初始化*/
      #define LNK(NAME, OTYPE)					\
          NOD(NAME, (S_IFLNK|S_IRWXUGO),				\
              &proc_pid_link_inode_operations, NULL,		\
              { .proc_get_link = &proc_##OTYPE##_link } )
      /*创建普通文件数据结构,pid_entry结构体初始化*/
      #define REG(NAME, MODE, OTYPE)				\
          NOD(NAME, (S_IFREG|(MODE)), NULL,		\
              &proc_##OTYPE##_operations, {})
      /*创建普通文件数据结构,pid_entry结构体初始化*/
      #define INF(NAME, MODE, OTYPE)				\
          NOD(NAME, (S_IFREG|(MODE)), 			\
              NULL, &proc_info_file_operations,	\
              { .proc_read = &proc_##OTYPE } )
      

      查找子目录函数
      在这里插入图片描述

      函数流程:

      1. 遍历tgid_base_stuff数组比较查找的文件名字
      2. 创建inode

10.1.8 系统控制机制

用户层2种方式使用该机制:

  1. 编写程序使用sysctl系统调用
  2. 用echo和cat命令操作/proc/sys中的文件

sysctl系统调用控制内核行为(没有标准定义sysctl集合,被认为过时,使得所有兼容系统都实现该集合).用户层必须编写一个程序读取参数并使用sysctl将参数传递给内核

为解决这种情况,Linux借助于proc文件系统。内核重排了所有的sysctl,建立起一个层次结构,并导出到/proc/sys目录下。可以使用简单的用户空间工具来读取或操纵这些参数。要修改内核的运行时行为,cat和echo就足够了

  1. 使用sysctl
    每个类UNIX操作系统中的许多sysctl,都组织为一个明确的层次结构,反映了文件系统中所使用的我们熟悉的树形结构:正是因为这种特性,才使得sysctl能够如此简单地通过一个虚拟文件系统导出

    sysctl使用整数来表示路径分量。与字符串形式的路径名相比,内核更容易解析这种格式

    内核提供了几个“基本类别”,包括CTL_DEV(外设有关信息)、CTL_KERN(内核本身有关信息)和CTL_VM(内存管理信息和参数)。如路径 CTL_DEV/DEV_CDROM/DEV_CDROM_INFO查看cdrom驱动信息

    所有类别信息在include/linux/sysctl.h中定义
    用户层在/usr/include/sys/sysctl.h

    sysctl层次结构的一个片段
    在这里插入图片描述

  2. 数据结构

    //include/linux/sysctl.h
    //sysctl项结构体,用于建立层次结构
    struct ctl_table 
    {
        int ctl_name;			/*ID,在该sysctl项所在的层次上必须是唯一的,但不必在整个sysctl表中是唯一的。CTL_KERN等*//* Binary ID */
        const char *procname;		/*字符串,如vm等,包含了proc/sys下目录项的可理解的描述信息。所有根数据项(即对应于几个基本类别的sysctl)的名称,都表现为/proc/sys下的目录名,如果不导出到proc,可以为NULL*//* Text ID for /proc/sys, or zero */
        void *data;//可以指定任何值,通常是一个函数指针或字符串,字符串由特定于sysctl的函数处理。通用代码部分不会访问该成员
        int maxlen;//指定了一个sysctl能够接收或输出的数据的最大长度(按字节计算)
        mode_t mode;//控制了对数据的访问权限,确定数据是否可以读/写,谁可以读/写。权限是使用虚拟文件系统的常数指定
        struct ctl_table *child;//指向一个数组,各个数组项都是ctl_table实例,这些ctl_table实例是当前数据项的子结点。例如,在CTL_KERN sysctl项中,child指向一个数组,包含的sysctl项 如KERN_OSTYPE(操作系统类型)、KERN_OSRELEASE(内核本号)和KERN_HOSTNAME(内核运行的宿主机的名称),因为这些项在层次上属于CTL_KERN sysctl的下一级。因为ctl_table数组的长度没有显式存储,因而数组的最后一项必须总是NULL指针,标志数组的结束
        struct ctl_table *parent;	/* Automatically set */
        proc_handler *proc_handler;	/*在通过proc接口输出数据时调用,内核可以直接输出保存在内核中的数据,但也可以将其转换为更容易阅读的形式(例如,将数值常数转换为字符串形式)*//* Callback for text formatting */
        ctl_handler *strategy;		/*由内核用来读写sysctl的值,在执行系统调用时使用*//* Callback function for all r/w */
        void *extra1;//proc的额外数据,通用的sysctl代码处理不会处理这个变量,通常用于定义数值参数的上下限
        void *extra2;//proc的额外数据,通用的sysctl代码处理不会处理这个变量,通常用于定义数值参数的上下限
    };
    
    //ctl_table链表头,用于将几个sysctl表维护在一个链表中
    struct ctl_table_header
    {
        struct ctl_table *ctl_table;//指向 ctl_table 数组
        struct list_head ctl_entry;//链表头和元素
        int used;
        struct completion *unregistering;
    };
    

    上面两个结构体的关系如下:
    在这里插入图片描述

  3. 静态的sysctl表
    sysctl静态表顶层类别数组root_table
    sysctl顶层类别的链表root_table_header

    //kernel/sysctl.c
    //sysctl静态表顶层类别数组
    static struct ctl_table root_table[];
    //sysctl根结点链表结构根
    static struct ctl_table_header root_table_header =
        { root_table, LIST_HEAD_INIT(root_table_header.ctl_entry) };
    
  4. 注册sysctl register_sysctl_table
    除了静态定义的sysctl之外,内核还提供了一个接口,用于动态注册(register_sysctl_table)和注销(unregister_sysctl_table)新的系统控制功能(动态增加新模块时使用)

    注册sysctl项,不会自动地创建将sysctl项关联到proc数据项的inode实例。因为大多数sysctl从来都不通过proc使用,这种做法太浪费内存。相反,与proc文件的关联是动态创建的

    //fs/proc/proc_sysctl.c
    // /proc/sys文件的 inode 操作函数集合
    static struct inode_operations proc_sys_inode_operations = {
        .lookup		= proc_sys_lookup,
        .permission	= proc_sys_permission,
        .setattr	= proc_sys_setattr,
    };
    
  5. /proc/sys文件操作

    //fs/proc/proc_sysctl.c
    // // /proc/sys文件的 file 操作函数集合
    static const struct file_operations proc_sys_file_operations = {
        .read		= proc_sys_read,
        .write		= proc_sys_write,
        .readdir	= proc_sys_readdir,
    };
    

    读写操作:

    1. 查找sysctl表项
    2. 检查权限
    3. 调用sysctl表项中的proc_handler处理函数完成操作

    使用频繁的proc_handler函数:

    //kernel/sysctl.c
    //读写int整数值
    int proc_dointvec(struct ctl_table *table, int write, struct file *filp,
    	     void __user *buffer, size_t *lenp, loff_t *ppos)
    
    //读写int整数值,且确保值都在由table->extra1和table->extra2指定的范围内(前者为下限,后者为上限)。所有超出该范围的值都被忽略
    int proc_dointvec_minmax(struct ctl_table *table, int write, struct file *filp,
    	  void __user *buffer, size_t *lenp, loff_t *ppos)
    
    //读写整数值,使用unsigned long,且确保值都在由table->extra1和table->extra2指定的范围内(前者为下限,后者为上限)。所有超出该范围的值都被忽略
    int proc_doulongvec_minmax(struct ctl_table *table, int write, struct file *filp,
    		   void __user *buffer, size_t *lenp, loff_t *ppos)
    
    //读取一个整数表。这些值都转换为jiffies
    int proc_dointvec_jiffies(struct ctl_table *table, int write, struct file *filp,
                void __user *buffer, size_t *lenp, loff_t *ppos)
    
    //读取一个整数表。这些值都转换为 ms
    int proc_dointvec_ms_jiffies(struct ctl_table *table, int write, struct file *filp,
                    void __user *buffer, size_t *lenp, loff_t *ppos)
    
    //在内核和用户空间之间传输字符串,可以提供双向传输。超出sysctl项内部缓冲区长度的字符串将自动截断。在数据复制到用户空间时,将自动地附加一个回车(\n),这样在信息输出(例如,使用cat)后将增加一个换行
    int proc_dostring(struct ctl_table *table, int write, struct file *filp,
    	    void __user *buffer, size_t *lenp, loff_t *ppos)
    

10.2 简单的文件系统

文件系统库libfs,包含了实现文件系统所需的几乎所有要素。开发者只需要提供到其数据的一个接口,文件系统就完成了

此外,还有一些以seq_file机制提供的标准例程可用,使得顺序文件的处理毫不费力。最后,开发者可能只是想要向用户空间导出一两个值,而不想和现存的文件系统(如proc)打交道。内核对此也提供了一种方案:debugfs文件系统允许只用几个函数调用,就实现一个双向的调试接口

10.2.1 序列文件接口(提供对文件的读写,需要底层文件系统支持)

小的文件系统中的文件,通常用户层是从头到尾顺序读取的,其内容可能是遍历一些数据项创建的

fs/seq_file.c中的例程容许用最小代价来实现此类文件。不论名称如何,但顺序文件是可以进行定位(seek)操作的,但其实现不怎么高效。顺序访问,即逐个读取数据项,显然是首选的访问模式。某个方面具有优势,通常会在其他方面付出代价。函数前缀seq_

kprobe机制包含了到上述debugfs文件系统的一个接口。一个顺序文件向用户层提供了所有注册的探测器。例子kprobe的实现,来说明顺序文件的思想

kprobes子系统实现例子:

说明:kprobes机制允许向内核中某些位置附加探测器。所有注册的探测器散列到数组kprobe_table中,该数组的长度是静态定义的,即KPROBE_TABLE_SIZE。顺序文件的文件游标(pos)解释为该数组的索引,调试文件应该显示所有注册的探测器的有关信息,文件的内容需要根据散列表的内容构建

  1. 编写顺序文件处理程序

    //kernel/kprobes.c
    //debugfs文件系统文件操作,只需实现open函数,其他的使用seq库的函数
    static struct file_operations debugfs_kprobes_operations = {
        .open           = kprobes_open,
        .read           = seq_read,
        .llseek         = seq_lseek,
        .release        = seq_release,
    };
    
    static struct seq_operations kprobes_seq_ops = {
        .start = kprobe_seq_start,
        .next  = kprobe_seq_next,
        .stop  = kprobe_seq_stop,
        .show  = show_kprobe_addr
    };
    
    static int __kprobes kprobes_open(struct inode *inode, struct file *filp)
    {
        //建立顺序文件机制所需的数据结构,关联到文件的private_data成员中
        return seq_open(filp, &kprobes_seq_ops);
    }
    
    //include/linux/seq_file.h
    //序列文件结构体
    struct seq_file {
        char *buf;//内存缓冲区,用于构建传输给用户层的数据
        size_t size;//缓冲区中总的字节数
        size_t from;//读操作的起始位置,将数据复制到用户空间
        size_t count;//指定了需要传输到用户层的剩余的字节数
        loff_t index;//写操作,buf下标,标记了内核向缓冲区写入下一个新记录的起始位置
        u64 version;
        struct mutex lock;
        const struct seq_operations *op;//序列文件操作函数集合
        void *private;
    };
    
    //序列文件操作函数集合
    struct seq_operations {
        void * (*start) (struct seq_file *m, loff_t *pos);//对序列文件开始操作时调用
        void (*stop) (struct seq_file *m, void *v);//关闭序列文件
        void * (*next) (struct seq_file *m, void *v, loff_t *pos);//在需要将下标移动到下一个位置时,需要调用next函数
        int (*show) (struct seq_file *m, void *v);//生成序列文件的实际内容
    };
    

    在这里插入图片描述

  2. 与虚拟文件系统的关联

    1. 将1中的 debugfs_kprobes_operations 与VFS关联
    2. open函数将seq_file与file->private_data关联.
    3. 如果有些数据等待写出(如果struct seq_file的count成员为正值),则使用copy_to_user将其复制到用户层。此外,还需要更新seq_file的各个状态成员
    4. 下一步,会产生新的数据。在调用start之后,内核接连调用show和next,直至填满可用的缓冲区。最后,调用stop,使用copy_to_user将生成的数据复制到用户空间

10.2.2 用libfs编写文件系统(文件系统库用于提供了对文件系统操作的底层支持)

libfs是一个库,提供了几个非常通用的标准例程,可用于创建服务于特定用途的小型文件系统。这些例程很适合于没有后备存储器的内存文件。显然,libfs的代码无法与特定的磁盘格式交互。这需要由完整的文件系统实现来正确处理。该库的代码包含在一个文件中,即fs/libfs.c,头文件为fs.h,函数前缀simple_

使用libfs建立的虚拟文件系统,其文件和目录层次结构可使用dentry树产生和遍历。这意味着在该文件系统的生命周期内,所有的dentry都必须驻留在内存中。除非通过unlink或rmdir显式删除,否则不能消失。但这个要求很容易做到:代码只需要确保所有dentry的使用计数都是正值即可。

为更好地理解libfs的思想,我们来讨论实现目录处理的方法。libfs提供了目录的inode_operations和file_operations的实例模板,任何利用了libfs来实现的虚拟文件系统都可以重用。

//fs/libfs.c
//libfs库实现的文件操作实例,可直接使用
const struct file_operations simple_dir_operations = {
	.open		= dcache_dir_open,
	.release	= dcache_dir_close,
	.llseek		= dcache_dir_lseek,
	.read		= generic_read_dir,
	.readdir	= dcache_readdir,
	.fsync		= simple_sync_file,
};

//libfs库实现的inode操作实例,可直接使用
const struct inode_operations simple_dir_inode_operations = {
	.lookup		= simple_lookup,
};

debugfs文件系统的目录文件使用了libfs(在debugfs_get_inode中关联),普通文件不能使用libfs的 file_operations

超级块的填充函数:

//fs/libfs.c
//填充超级块,files参数提供了一种非常便捷的方法来向虚拟文件系统添加文件。遗憾的是,这种方法只能指定同处一个目录下的文件
int simple_fill_super(struct super_block *s, int magic, struct tree_descr *files)

struct tree_descr { char *name; const struct file_operations *ops; int mode; };

10.2.3 调试文件系统 debugfs

seq_file机制使用libfs提供的底层文件操作接口来将数据写入到文件中,而libfs则提供了对文件系统操作的底层支持,包括对seq_file的支持

使用了libfs函数的一个特别的文件系统是调试文件系统debugfs。它向内核开发者提供了一种向用户层提供信息的可能方法。这些信息并不会编译到产品内核中。它只是开发新特性时的一种辅助手段.仅当内核编译时启用了DEBUG_FS配置选项,才会激活对debugfs的支持。因而向debugfs注册文件的代码,都会被C预处理器条件语句包围,来检查CONFIG_DEBUG_FS

  1. 示例

    //kernel/kprobes.c
    static int __kprobes debugfs_kprobe_init(void)
    

    debugfs_kprobe_init中用序列文件接口机制创建了文件

  2. 编程接口

    //include/linux/debugfs.h
    //创建文件
    struct dentry *debugfs_create_file(const char *name, mode_t mode,
    			   struct dentry *parent, void *data,
    			   const struct file_operations *fops);
    
    //创建目录
    struct dentry *debugfs_create_dir(const char *name, struct dentry *parent);
    
    //创建链接文件
    struct dentry *debugfs_create_symlink(const char *name, struct dentry *parent,
                        const char *dest);
    
    //删除文件
    void debugfs_remove(struct dentry *dentry);
    
    //重命名文件
    struct dentry *debugfs_rename(struct dentry *old_dir, struct dentry *old_dentry,
                struct dentry *new_dir, const char *new_name);
    
    //创建的文件可用于用户空间进行读写操作,xx表示操作的类型,u8,u16等代替xx表示用户空间能读不能写,x8,x16等代替xx表示用户空间可读可写
    struct dentry *debugfs_create_xx(const char *name, mode_t mode,
    			 struct dentry *parent, u8 *value);
    
    //用于二进制数据结构
    struct debugfs_blob_wrapper {
        void *data;
        unsigned long size;
    };
    
    

10.2.4 伪文件系统

内核支持伪文件系统,其中收集了一些相关的inode,但不能装载,因而对用户层也是不可见的,libfs也提供了一个辅助函数,来实现这种特殊类型的文件系统

内核使用了一个伪文件系统来跟踪表示块设备的所有inode

//fs/block_dev.c
//用于伪文件系统,跟踪块设备的inode的伪文件系统,用户层不可见
static int bd_get_sb(struct file_system_type *fs_type,
	int flags, const char *dev_name, void *data, struct vfsmount *mnt)

static struct file_system_type bd_type = {
	.name		= "bdev",
	.get_sb		= bd_get_sb,
	.kill_sb	= kill_anon_super,
};

为使用伪文件系统,内核需要使用kern_mount或kern_mount_data装载它。它可用于收集inode,而无需写一个专门的数据结构。对于bdev,所有表示块设备的inode都群集起来。但该集合只能从内核看到,用户空间无法看到

而为了对块设备文件进行便捷的组织与管理,Linux内核创建了bdev文件系统,该文件系统的目的是为了建立块设备文件在外部表现与内部实现之间的关联性。bdev文件系统是一个“伪”文件系统,它只被内核使用,而无需挂载到全局的文件系统树上

块设备文件除了与常规文件类似的在根文件系统上存在inode之外,其在bdev文件系统上也存在对应的inode。两个inode之间通过块设备编号相关联,需要注意的是,前者的inode称之为次inode,而后者称之为主inode

对于每个块设备,在bdev文件系统中都有一个indoe,同时磁盘和分区也会有属于自己的inode。Linux内核利用blokc_inode数据结构表示块设备的inode,其中包含了两个字段,分别是struct block_device,即块设备描述符。另一个是struct inode,即inode描述符。但是Linux系统为了能够对整体的inode进行统一的管理,因此在宿主系统中创建了与bdev文件系统中相对应的inode。

10.3 sysfs

kset和kobject关系

sysfs是一个向用户空间导出内核对象的文件系统,它不仅提供了察看内核内部数据结构的能力,还可以修改这些数据结构。特别重要的是,该文件系统高度层次化的组织:sysfs的数据项来源于内核对象(kobject),而内核对象的层次化组织直接反映到了sysfs的目录布局中。由于系统的所有设备和总线都是通过kobject组织的,所以sysfs提供了系统的硬件拓扑的一种表示。

sysfs已经成为老式的IOCTL机制的一种替代品

类似于许多虚拟文件系统,sysfs最初基于ramfs。因而其实现使用了许多与内核其他部分不同的技巧。请注意,只要配置时启用了该特性,sysfs总是编译到内核中,不可能将其生成为模块。sysfs的标准装载点是/sys

内核源代码包含了一些有关sysfs的文档,包括sysfs与驱动程序模型的关系、sysfs与kobject框架的关系,等等。该文档可以在Documentation/filesystems/sysfs.txtDocumentation/filesy-stems/sysfs-pci.txt中找到。sysfs开发者本人对sysfs的概述,可以在2005年渥太华Linux研讨会的会议记录上找到,该会议记录在www.linuxsymposium.org/2005/linuxsymposium_procv1.pdf中。
最后,请注意kobject与sysfs之间的关联不是自动建立的。独立的kobject实例默认情况下并不集成到sysfs。要使一个对象在sysfs文件系统中可见,需要调用kobject_add。但如果kobject是某个内核子系统的成员,那么向sysfs的注册是自动进行的

10.3.1 概述

kobject包含在一个层次化的组织中。最重要的一点是,它们可以有一个父对象,可以包含到一个kset中。这决定了kobject出现在sysfs层次结构中的位置:如果存在父对象,那么需要在父对象对应的目录中新建一项。否则,将其放置到kobject所在的kset所属的kobject对应的目录中(如果上述两种情况都不成立,那么该kobject对应的数据项将放置到系统层次结构的顶级目录下,当然这种情况比较罕见)。

每个kobject在sysfs中都表示为一个目录。出现在该目录中的文件是对象的属性。用于导出和设置属性的操作由对象所属的子系统提供(类、驱动程序,等等)。

总线、设备、驱动程序和类是使用kobject机制的主要内核对象,因而也占据了sysfs中几乎所有的数据项

10.3.2 sysfs数据结构

  1. 目录项

    //sysfs文件系统的目录结构体,关联到dentry->d_fsdata
    struct sysfs_dirent {
        atomic_t		s_count;//引用计数,使用时+1
        atomic_t		s_active;//活动引用计数,因为每当打开一个sysfs结点时,也引用了与之关联的kobject。因而,只要用户层应用程序保持开来一个sysfs文件,就能防止内核删除对应的kobject实例。为规避这种情况,内核要求每次(通过sysfs_elem_*)访问相关的内部对象时,都要持有对sysfs_direntry实例的一个活动引用,就是该值.在应该删除一个sysfs文件时,可以将活动引用计数器设置为负值,来撤销对相关内部对象的访问,辅助函数sysfs_dectivate即用于此。在这个计数器为负值时,就不能对相关的kobject执行操作了。在kobject的所有用户都消失时,内核才可以安全地删除它。但sysfs文件和sysfs_dirent实例仍然可以存在,即使二者已经没有意义了.可以通过sysfs_get_active或sysfs_get_active_two(后者获得对给定sysfs_direntry实例及其父结点的引用)获得活动引用。在结束对内部对象的操作之后,必需立即用sysfs_put_active(或sysfs_put_active_two)释放活动引用
        struct sysfs_dirent	*s_parent;
        struct sysfs_dirent	*s_sibling;//链表元素,用于连接同一父节点的子节点,表头为sysfs_dirent->s_dir->children
        const char		*s_name;//表示文件、目录或符号链接的名称
    
        //根据sysfs数据项的类型不同,与之关联的数据类型也不同,一次只能表示联合体中的一种类型
        union {
            struct sysfs_elem_dir		s_dir;//目录文件
            struct sysfs_elem_symlink	s_symlink;//软链接文件
            struct sysfs_elem_attr		s_attr;//属性文件
            struct sysfs_elem_bin_attr	s_bin_attr;//二进制属性文件
        };
    
        unsigned int		s_flags;//两部分组成,低8位用作类型,可以用辅助函数sysfs_type访问,为SYSFS_DIR等,表示该数据项是目录、普通属性、二进制属性或符号链接.剩余比特位用作标志位。当前,只定义了SYSFS_FLAG_REMOVED。如果正在删除某个sysfs数据项,则设置该标志
        ino_t			s_ino;//用来给子结点链表(sysfs_dirent->s_dir->children)中的所有结点排序,以该值递减排序
        umode_t			s_mode;//访问权限
        struct iattr		*s_iattr;//属性,指向的一个iattr实例,如果为NULL指针,则使用默认属性集合
    };
    

    sys_dirent目录结构:
    在这里插入图片描述

  2. 属性

    • 数据结构

      //include/linux/sysfs.h
      //文件属性结构体
      struct attribute {
          const char		*name;//属性名,在sysfs中用作文件名
          struct module		*owner;//属性的所有者所属的module实例
          mode_t			mode;//访问权限
      };
      //属性组
      struct attribute_group {
          const char		*name;//属性组的名称
          struct attribute	**attrs;//指向一个数组,数组项是指向attribute实例的指针,NULL指针标志数组的结束
      };
      

      将属性和属性访问函数分开是因为一个实体(驱动程序,设备类别等)的所有属性都使用同样的方法修改

      子系统的show和store操作依赖属性相关的show和store方法,而后两者在内部与属性是关联的,不同属性的show和store方法也不同。具体的实现细节由对应的子系统负责,sysfs对此并不关注

      对于可读写的属性,需要提供两个方法(show和store)。内核提供了下列数据结构,来一同维护这两个方法

      //include/linux/sysfs.h
      //可读写的属性的操作函数
      struct sysfs_ops {
          ssize_t	(*show)(struct kobject *, struct attribute *,char *);//读
          ssize_t	(*store)(struct kobject *,struct attribute *,const char *, size_t);//写
      };
      
      //二进制属性结构体,二进制文件的属性结构与普通文件的结构体不同
      struct bin_attribute {
          struct attribute	attr;
          size_t			size;//与属性关联的二进制数据的长度
          void			*private;//通常指向数据实际存储的位置
          ssize_t (*read)(struct kobject *, struct bin_attribute *,
                  char *, loff_t, size_t);//读
          ssize_t (*write)(struct kobject *, struct bin_attribute *,
                  char *, loff_t, size_t);//写
          int (*mmap)(struct kobject *, struct bin_attribute *attr,
                  struct vm_area_struct *vma);//映射
      };
      
    • 属性的使用
      在内核中有许多方法可以声明特定于子系统的属性,但这些方法的实现都具有共同的基本结构。因此,以其中一个实现为例来讲解其底层机制,就足够了。例如,考虑通用硬盘代码如何定义一个结构,即可将一个属性以及读写该属性的方法关联起来

      //include/linux/genhd.h
      //通用硬盘属性
      struct disk_attribute {
          struct attribute attr;//属性
          ssize_t (*show)(struct gendisk *, char *);//属性读操作函数
          ssize_t (*store)(struct gendisk *, const char *, size_t);//属性写操作函数
      };
      
      //block/genhd.c
      //通用硬盘属性文件读写函数
      static struct sysfs_ops disk_sysfs_ops = {
          .show	= &disk_attr_show,
          .store	= &disk_attr_store,
      };
      

10.3.3 装载文件系统

mount最终调用 sysfs_fill_super 填充超级块

//fs/sysfs/mount.c
//sysfs伪文件系统超级块填充函数,mount sysfs时使用
static int sysfs_fill_super(struct super_block *sb, void *data, int silent)

//sysfs文件系统根目录
struct sysfs_dirent sysfs_root = {
	.s_name		= "",
	.s_count	= ATOMIC_INIT(1),
	.s_flags	= SYSFS_DIR,
	.s_mode		= S_IFDIR | S_IRWXU | S_IRUGO | S_IXUGO,
	.s_ino		= 1,
};

在这里插入图片描述

函数流程:

  1. 创建inode,并初始化
  2. 为根目录创建文件夹
  3. 建立inode与文件夹的关系

在这里插入图片描述

函数流程:

  1. 初始化inode各成员
  2. 设置文件属性
  3. 根据不同文件关联文件操作函数

10.3.4 文件和目录操作

//fs/sysfs/file.c
//sysfs文件操作函数
const struct file_operations sysfs_file_operations = {
	.read		= sysfs_read_file,
	.write		= sysfs_write_file,
	.llseek		= generic_file_llseek,
	.open		= sysfs_open_file,
	.release	= sysfs_release,
	.poll		= sysfs_poll,
};

//sysfs普通文件inode操作
static const struct inode_operations sysfs_inode_operations ={
	.setattr	= sysfs_setattr,
};

//fs/sysfs/dir.c
//sysfs 目录inode操作函数
const struct inode_operations sysfs_dir_inode_operations = {
	.lookup		= sysfs_lookup,
	.setattr	= sysfs_setattr,
};
  1. 打开文件

    • 数据结构
      为便于在用户层和sysfs实现之间交换数据,内核需要提供一些缓冲区。缓冲区由下列数据结构提供

      //fs/sysfs/file.c
      //缓冲区,用于用户层和sysfs实现之间交换数据
      struct sysfs_buffer {
          size_t			count;//缓冲区中数据的长度
          loff_t			pos;//数据内部当前的位置
          char			* page;//指向一页,用于存储数据,限制只使用一页
          struct sysfs_ops	* ops;//指向一个sysfs_ops实例,该实例属于与打开文件相关联的sysfs数据项
          struct mutex		mutex;
          int			needs_read_fill;//指定缓冲区的内容是否需要填充(填充数据在第一次读操作时进行,如果同时不进行写操作,那么后续的读操作不必重复填充)
          int			event;
          struct list_head	list;//链表元素,表头为 sysfs_open_dirent->buffers
      };
      

      在这里插入图片描述

    • 函数实现 sysfs_open_file
      在这里插入图片描述

      函数流程:

      1. 获取文件操作函数集
      2. 检查读写权限和读写函数
      3. 申请 sysfs_buffer 关联到file->private_data
      4. 将缓冲区与目录关联
  2. 读写文件内容

    • 读数据 sysfs_read_file
      在这里插入图片描述

      函数流程:

      1. 检查是否需要填充文件内容,第一次访问或写操作修改了文件内容时需要重新填充.如果需要填充,调用ops->show函数填充数据
      2. 检查边界,从内核向用户空间进行一次内存复制操作
    • 写数据 sysfs_write_file
      在这里插入图片描述

      函数流程:

      1. 分配一个页帧,将来自用户空间的数据复制到页帧中,设置buffer->needs_read_fill
      2. 调用特定于文件的sysfs_ops实例提供的store方法
  3. 目录遍历 sysfs_lookup
    在这里插入图片描述

    函数流程:

    1. 遍历目录比较文件名
    2. 获取inode
    3. 将dentry和inode关联起来

10.3.5 向sysfs添加内容

以通用硬盘为例(/sys/block),系统中每个块设备都对应于一个子目录

//include/linux/genhd.h
//通用硬盘属性结构体
struct disk_attribute {
	struct attribute attr;//属性
	ssize_t (*show)(struct gendisk *, char *);//属性读操作函数
	ssize_t (*store)(struct gendisk *, const char *, size_t);//属性写操作函数
};

//block/genhd.c
static struct disk_attribute disk_attr_uevent = {
	.attr = {.name = "uevent", .mode = S_IWUSR },
	.store	= disk_uevent_store
};
static struct disk_attribute disk_attr_dev = {
	.attr = {.name = "dev", .mode = S_IRUGO },
	.show	= disk_dev_read
};
...
static struct disk_attribute disk_attr_stat = {
	.attr = {.name = "stat", .mode = S_IRUGO },
	.show	= disk_stats_read
};

//硬盘设备在sysfs中的属性集合
static struct attribute * default_attrs[] = {
	&disk_attr_uevent.attr,
	&disk_attr_dev.attr,
	&disk_attr_range.attr,
	&disk_attr_removable.attr,
	&disk_attr_size.attr,
	&disk_attr_stat.attr,
	&disk_attr_capability.attr,
#ifdef CONFIG_FAIL_MAKE_REQUEST
	&disk_attr_fail.attr,
#endif
	NULL,
};

//通用硬盘属性文件读写函数
static struct sysfs_ops disk_sysfs_ops = {
	.show	= &disk_attr_show,
	.store	= &disk_attr_store,
};

//将默认属性集关联到属于genhd子系统的所有kobject的kobj_type
static struct kobj_type ktype_block = {
	.release	= disk_release,
	.sysfs_ops	= &disk_sysfs_ops,
	.default_attrs	= default_attrs,
};

//创建一个对应于kobj_type的kset
decl_subsys(block, &ktype_block, &block_uevent_ops);

//初始化硬盘设备注册到sysfs,kobject
static int __init genhd_device_init(void)
  • genhd_device_init
    • subsystem_register
      • kset_register
        • kset_add
          • kobject_add
            • create_dir
              • populate_dir 遍历所有默认属性,并为每个属性创建一个sysfs文件,因为通用硬盘的子元素(即,分区)关联到上面介绍的kset,根据kobject模型,它们自动继承了所有默认属性

总结

proc文件系统,用于/proc目录,可用于获取内核各子系统信息

用序列文件接口和libfs可以实现简单的文件系统.debugfs 文件系统使用了该方式

sysfs文件系统用 kobject 创建了目录结构,向用户空间导出内核对象

=========================================

涉及的命令和配置:

od 命令是Linux系统中的一个常用命令,用于显示文件的字节流。它可以将文件的内容以十六进制或十进制的形式显示出来,方便用户查看和分析文件内容。

基本语法:

od [选项] [文件名]

常用选项:

-b:以字节为单位显示文件内容
-c:以字符为单位显示文件内容
-d:只显示十进制数值
-h:以人类可读的格式显示文件内容(自动选择合适的单位)
-x:以十六进制显示文件内容
-t:显示ASCII字符的teletype字体

全局vfsmount指针proc_mnt

root@meitner # mount -t proc proc /proc

proc函数使用说明:Documentation/DocBook/procfs_example.c

sysctl根结点对应的顶层类别数组是全局root_table
全局顶层类别的链表root_table_header

全局数组存储所有注册的探测器kprobe_table[KPROBE_TABLE_SIZE]

sysfs说明文档:Documentation/filesystems/sysfs.txt和Documentation/filesy-stems/sysfs-pci.txt
开发者对sysfs的会议记录:www.linuxsymposium.org/2005/linuxsymposium_procv1.pdf

硬盘在sysfs中的路径/sys/block

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值