linux中struct class

    参考:http://www.wowotech.NET/device_model/class.html

        firstdrv_class = class_create(THIS_MODULE, “firstdrv”);
        firstdrv_class_dev = device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, “xyz”); /* /dev/xyz */

    于是乎,这两行代码被糊里糊涂的复制粘贴了好多次,差点成为一种习惯~        

    前面分析设备总线驱动模型的时候,我们知道,将一个设备调用 device_add 函数注册到内核中去的时候,如果指定了设备号,那么用户空间的 mdev 会根据 sysfs 文件系统中的设备信息去自动创建设备节点。我们看到前面第二行代码里有一个 device_create ,参数里还有设备号 ,八九不离十,里边也间接调用了device_add ,不信一会分析代码。

    类是一个设备的高层视图,它抽象出了低层的实现细节,大概意思就是抽象出了一个通用的接口吧。常见的类设备有 Input 、tty 、usb 、rtc 等等。

    class 就好比 bus ,我们在设备总线驱动模型中创建设备时,要指定它所属的 Bus ,那么在创建类设备的时候也需要指定它所从属的类,class 也离不开 Kobject ,因此如果你了解总线设备驱动模型,你就会发现,其实真的都是差不多的东西。

  1. struct class {  
  2.     const char      *name;  
  3.     struct module       *owner;  
  4.     struct class_attribute      *class_attrs;  
  5.     struct device_attribute     *dev_attrs;  
  6.     struct kobject          *dev_kobj;  
  7.     int (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env);  
  8.     char *(*devnode)(struct device *dev, mode_t *mode);  
  9.     void (*class_release)(struct class *class);  
  10.     void (*dev_release)(struct device *dev);  
  11.     int (*suspend)(struct device *dev, pm_message_t state);  
  12.     int (*resume)(struct device *dev);  
  13.     const struct dev_pm_ops *pm;  
  14.     struct class_private *p;  
  15. };  
struct class {
    const char      *name;
    struct module       *owner;
    struct class_attribute      *class_attrs;
    struct device_attribute     *dev_attrs;
    struct kobject          *dev_kobj;
    int (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env);
    char *(*devnode)(struct device *dev, mode_t *mode);
    void (*class_release)(struct class *class);
    void (*dev_release)(struct device *dev);
    int (*suspend)(struct device *dev, pm_message_t state);
    int (*resume)(struct device *dev);
    const struct dev_pm_ops *pm;
    struct class_private *p;
};

    name,class的名称,会在“/sys/class/”目录下体现。
    class_atrrs,该class的默认attribute,会在class注册到内核时,自动在“/sys/class/xxx_class”下创建对应的attribute文件。
    dev_attrs,该class下每个设备的attribute,会在设备注册到内核时,自动在该设备的sysfs目录下创建对应的attribute文件。
    dev_bin_attrs,类似dev_attrs,只不过是二进制类型attribute。
    dev_kobj,表示该class下的设备在/sys/dev/下的目录,现在一般有char和block两个,如果dev_kobj为NULL,则默认选择char。
    dev_uevent,当该class下有设备发生变化时,会调用class的uevent回调函数。
    class_release,用于release自身的回调函数。
    dev_release,用于release class内设备的回调函数。在device_release接口中,会依次检查Device、Device Type以及Device所在的class,是否注册release接口,如果有则调用相应的release接口release设备指针。

  1. struct class_private {  
  2.     struct kset class_subsys;  
  3.     struct klist class_devices;  
  4.     struct list_head class_interfaces;  
  5.     struct kset class_dirs;  
  6.     struct mutex class_mutex;  
  7.     struct class *class;  
  8. };  
struct class_private { 
struct kset class_subsys;
struct klist class_devices;
struct list_head class_interfaces;
struct kset class_dirs;
struct mutex class_mutex;
struct class *class;
};
    struct class_interface是这样的一个结构:它允许class driver在class下有设备添加或移除的时候,调用预先设置好的回调函数(add_dev和remove_dev)。那调用它们做什么呢?想做什么都行(例如修改设备的名称),由具体的class driver实现。
    该结构的定义如下:
  1. struct class_interface {  
  2.     struct list_head        node;  
  3.     struct class            class;  
  4.   
  5.     int (*add_dev)          (struct device , struct class_interface );  
  6.     void (*remove_dev)      (struct device , struct class_interface *);  
  7. };  
struct class_interface { 
struct list_head node;
struct class *class;

int (*add_dev)          (struct device *, struct class_interface *);
void (*remove_dev)      (struct device *, struct class_interface *);

};

    下面,我们来看 Class 的注册过程,前面我们提到,class->name 会出现在/sys/class 目录下,那么这个目录是哪里来的,代码一看便知。

  1. int __init classes_init(void)  
  2. {  
  3.     class_kset = kset_create_and_add(”class”, NULL, NULL);  
  4.     if (!class_kset)  
  5.         return -ENOMEM;  
  6.     return 0;  
  7. }  
int __init classes_init(void) 
{
class_kset = kset_create_and_add("class", NULL, NULL);
if (!class_kset)
return -ENOMEM;
return 0;
}
    下面,我们来看一下一个Class 的注册过程

  1. #define class_create(owner, name)       \  
  2. ({                      \  
  3.     static struct lock_class_key __key; \  
  4.     __class_create(owner, name, &__key);    \  
  5. })  
#define class_create(owner, name)      \ 
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
  1. struct class *__class_create(struct module *owner, const char *name,  
  2.                  struct lock_class_key *key)  
  3. {  
  4.     struct class *cls;  
  5.     int retval;  
  6.   
  7.     cls = kzalloc(sizeof(*cls), GFP_KERNEL);  
  8.     if (!cls) {  
  9.         retval = -ENOMEM;  
  10.         goto error;  
  11.     }  
  12.   
  13.     cls->name = name;  
  14.     cls->owner = owner;  
  15.     cls->class_release = class_create_release;  
  16.   
  17.     retval = __class_register(cls, key);  
  18.     if (retval)  
  19.         goto error;  
  20.   
  21.     return cls;  
  22.   
  23. error:  
  24.     kfree(cls);  
  25.     return ERR_PTR(retval);  
  26. }  
struct class *__class_create(struct module *owner, const char *name, 
struct lock_class_key *key)
{
struct class *cls;
int retval;

cls = kzalloc(sizeof(*cls), GFP_KERNEL);
if (!cls) {
    retval = -ENOMEM;
    goto error;
}

cls->name = name;
cls->owner = owner;
cls->class_release = class_create_release;

retval = __class_register(cls, key);
if (retval)
    goto error;

return cls;

error:
kfree(cls);
return ERR_PTR(retval);
}    在 class_create 函数中,只是简单构造了一个class结构体,设置了名字以及所属的模块,然后调用 class_register 

  1. int __class_register(struct class cls, struct lock_class_key *key)  
  2. {  
  3.     struct class_private *cp;  
  4.     int error;  
  5.   
  6.     pr_debug(”device class ’%s’: registering\n”, cls->name);  
  7.   
  8.     cp = kzalloc(sizeof(*cp), GFP_KERNEL);  
  9.     if (!cp)  
  10.         return -ENOMEM;  
  11.     klist_init(&cp->class_devices, klist_class_dev_get, klist_class_dev_put);  
  12.     INIT_LIST_HEAD(&cp->class_interfaces);  
  13.     kset_init(&cp->class_dirs);  
  14.     __mutex_init(&cp->class_mutex, ”struct class mutex”, key);  
  15.     error = kobject_set_name(&cp->class_subsys.kobj, ”%s”, cls->name);  
  16.     if (error) {  
  17.         kfree(cp);  
  18.         return error;  
  19.     }  
  20.   
  21.     / set the default /sys/dev directory for devices of this class /  
  22.     if (!cls->dev_kobj)  
  23.         cls->dev_kobj = sysfs_dev_char_kobj;  
  24.   
  25. #if defined(CONFIG_SYSFS_DEPRECATED) && defined(CONFIG_BLOCK)  
  26.     / let the block class directory show up in the root of sysfs */  
  27.     if (cls != &block_class)  
  28.         cp->class_subsys.kobj.kset = class_kset;  
  29. #else  
  30.     cp->class_subsys.kobj.kset = class_kset;  
  31. #endif  
  32.     cp->class_subsys.kobj.ktype = &class_ktype;  
  33.     cp->class = cls;  
  34.     cls->p = cp;  
  35.   
  36.     error = kset_register(&cp->class_subsys);  
  37.     if (error) {  
  38.         kfree(cp);  
  39.         return error;  
  40.     }  
  41.     error = add_class_attrs(class_get(cls));  
  42.     class_put(cls);  
  43.     return error;  
  44. }  
int __class_register(struct class *cls, struct lock_class_key *key) 
{
struct class_private *cp;
int error;

pr_debug("device class '%s': registering\n", cls->name);

cp = kzalloc(sizeof(*cp), GFP_KERNEL);
if (!cp)
    return -ENOMEM;
klist_init(&cp->class_devices, klist_class_dev_get, klist_class_dev_put);
INIT_LIST_HEAD(&cp->class_interfaces);
kset_init(&cp->class_dirs);
__mutex_init(&cp->class_mutex, "struct class mutex", key);
error = kobject_set_name(&cp->class_subsys.kobj, "%s", cls->name);
if (error) {
    kfree(cp);
    return error;
}

/* set the default /sys/dev directory for devices of this class */
if (!cls->dev_kobj)
    cls->dev_kobj = sysfs_dev_char_kobj;
#if defined(CONFIG_SYSFS_DEPRECATED) && defined(CONFIG_BLOCK) /* let the block class directory show up in the root of sysfs */ if (cls != &block_class) cp->class_subsys.kobj.kset = class_kset; #else cp->class_subsys.kobj.kset = class_kset; #endif cp->class_subsys.kobj.ktype = &class_ktype; cp->class = cls; cls->p = cp; error = kset_register(&cp->class_subsys); if (error) { kfree(cp); return error; } error = add_class_attrs(class_get(cls)); class_put(cls); return error; }    代码第15行,将 cp->class_subsys.kobj 的 name 设置为cls->name

    代码第28行,将cp->class_subsys.kobj.kest 设置为class_kest

    代码第36行,将cp->class_subsys 注册进内核,没有设置 cp->class_subsys.kobj.parent ,内核会将cp->class_subsys.kobj.kset.kobj 设置成它的Parent ,这也就是为什么说 class->name  会出现在 /sys/class 目录下的原因。


    下面,来看向 class 注册 device 的过程

  1. struct device *device_create(struct class *classstruct device *parent,  
  2.                  dev_t devt, void *drvdata, const char *fmt, …)  
  3. {  
  4.     va_list vargs;  
  5.     struct device *dev;  
  6.   
  7.     va_start(vargs, fmt);  
  8.     dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);  
  9.     va_end(vargs);  
  10.     return dev;  
  11. }  
struct device *device_create(struct class *class, struct device *parent,
                 dev_t devt, void *drvdata, const char *fmt, ...)
{
    va_list vargs;
    struct device *dev;

    va_start(vargs, fmt);
    dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);
    va_end(vargs);
    return dev;
}
  1. struct device *device_create_vargs(struct class *classstruct device *parent,  
  2.                    dev_t devt, void *drvdata, const char *fmt,  
  3.                    va_list args)  
  4. {  
  5.     struct device *dev = NULL;  
  6.     int retval = -ENODEV;  
  7.   
  8.     if (class == NULL || IS_ERR(class))  
  9.         goto error;  
  10.   
  11.     dev = kzalloc(sizeof(*dev), GFP_KERNEL);  
  12.     if (!dev) {  
  13.         retval = -ENOMEM;  
  14.         goto error;  
  15.     }  
  16.   
  17.     dev->devt = devt;  
  18.     dev->class = class;  
  19.     dev->parent = parent;  
  20.     dev->release = device_create_release;  
  21.     dev_set_drvdata(dev, drvdata);  
  22.   
  23.     retval = kobject_set_name_vargs(&dev->kobj, fmt, args);  
  24.     if (retval)  
  25.         goto error;  
  26.   
  27.     retval = device_register(dev);  
  28.     if (retval)  
  29.         goto error;  
  30.   
  31.     return dev;  
  32.   
  33. error:  
  34.     put_device(dev);  
  35.     return ERR_PTR(retval);  
  36. }  
struct device *device_create_vargs(struct class *class, struct device *parent,
                   dev_t devt, void *drvdata, const char *fmt,
                   va_list args)
{
    struct device *dev = NULL;
    int retval = -ENODEV;

    if (class == NULL || IS_ERR(class))
        goto error;

    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    if (!dev) {
        retval = -ENOMEM;
        goto error;
    }

    dev->devt = devt;
    dev->class = class;
    dev->parent = parent;
    dev->release = device_create_release;
    dev_set_drvdata(dev, drvdata);

    retval = kobject_set_name_vargs(&dev->kobj, fmt, args);
    if (retval)
        goto error;

    retval = device_register(dev);
    if (retval)
        goto error;

    return dev;

error:
    put_device(dev);
    return ERR_PTR(retval);
}
    上边代码也没有什么好分析的,与我们分析设备总线驱动模型时分析 device 时有一点不一样的就是这里设置的是 dev->class 而不是 dev->bus ,同时这里为 dev设置了设备号 devt ,因此,在sysfs中会创建 dev 属性文件,mdev 就会自动为我们创建设备节点了。

  1. int device_register(struct device *dev)  
  2. {  
  3.     device_initialize(dev);  
  4.     return device_add(dev);  
  5. }  
int device_register(struct device *dev)
{
    device_initialize(dev);
    return device_add(dev);
}
  1. int device_add(struct device *dev)  
  2. {  
  3.     struct device *parent = NULL;  
  4.     struct class_interface *class_intf;  
  5.     int error = -EINVAL;  
  6.   
  7.     dev = get_device(dev);  
  8.     if (!dev)  
  9.         goto done;  
  10.   
  11.     if (!dev->p) {  
  12.         error = device_private_init(dev);  
  13.         if (error)  
  14.             goto done;  
  15.     }  
  16.   
  17.     /* 
  18.      * for statically allocated devices, which should all be converted 
  19.      * some day, we need to initialize the name. We prevent reading back 
  20.      * the name, and force the use of dev_name() 
  21.      */  
  22.     if (dev->init_name) {  
  23.         dev_set_name(dev, ”%s”, dev->init_name);  
  24.         dev->init_name = NULL;  
  25.     }  
  26.   
  27.     if (!dev_name(dev))  
  28.         goto name_error;  
  29.   
  30.     pr_debug(”device: ’%s’: %s\n”, dev_name(dev), __func__);  
  31.   
  32.     parent = get_device(dev->parent);  
  33.     setup_parent(dev, parent);  
  34.   
  35.     /* use parent numa_node */  
  36.     if (parent)  
  37.         set_dev_node(dev, dev_to_node(parent));  
  38.   
  39.     /* first, register with generic layer. */  
  40.     /* we require the name to be set before, and pass NULL */  
  41.     error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);  
  42.     if (error)  
  43.         goto Error;  
  44.   
  45.     /* notify platform of device entry */  
  46.     if (platform_notify)  
  47.         platform_notify(dev);  
  48.   
  49.     error = device_create_file(dev, &uevent_attr);  
  50.     if (error)  
  51.         goto attrError;  
  52.   
  53.     if (MAJOR(dev->devt)) {  
  54.         error = device_create_file(dev, &devt_attr);  
  55.         if (error)  
  56.             goto ueventattrError;  
  57.   
  58.         error = device_create_sys_dev_entry(dev);  
  59.         if (error)  
  60.             goto devtattrError;  
  61.   
  62.         devtmpfs_create_node(dev);  
  63.     }  
  64.   
  65.     error = device_add_class_symlinks(dev);  
  66.     if (error)  
  67.         goto SymlinkError;  
  68.     error = device_add_attrs(dev);  
  69.     if (error)  
  70.         goto AttrsError;  
  71.     error = bus_add_device(dev);  
  72.     if (error)  
  73.         goto BusError;  
  74.     error = dpm_sysfs_add(dev);  
  75.     if (error)  
  76.         goto DPMError;  
  77.     device_pm_add(dev);  
  78.   
  79.     /* Notify clients of device addition.  This call must come 
  80.      * after dpm_sysf_add() and before kobject_uevent(). 
  81.      */  
  82.     if (dev->bus)  
  83.         blocking_notifier_call_chain(&dev->bus->p->bus_notifier,  
  84.                          BUS_NOTIFY_ADD_DEVICE, dev);  
  85.   
  86.     kobject_uevent(&dev->kobj, KOBJ_ADD);  
  87.     bus_probe_device(dev);  
  88.     if (parent)  
  89.         klist_add_tail(&dev->p->knode_parent,  
  90.                    &parent->p->klist_children);  
  91.   
  92.     if (dev->class) {  
  93.         mutex_lock(&dev->class->p->class_mutex);  
  94.         /* tie the class to the device */  
  95.         klist_add_tail(&dev->knode_class,  
  96.                    &dev->class->p->class_devices);  
  97.   
  98.         /* notify any interfaces that the device is here */  
  99.         list_for_each_entry(class_intf,  
  100.                     &dev->class->p->class_interfaces, node)  
  101.             if (class_intf->add_dev)  
  102.                 class_intf->add_dev(dev, class_intf);  
  103.         mutex_unlock(&dev->class->p->class_mutex);  
  104.     }  
  105. done:  
  106.     put_device(dev);  
  107.     return error;  
  108.  DPMError:  
  109.     bus_remove_device(dev);  
  110.  BusError:  
  111.     device_remove_attrs(dev);  
  112.  AttrsError:  
  113.     device_remove_class_symlinks(dev);  
  114.  SymlinkError:  
  115.     if (MAJOR(dev->devt))  
  116.         device_remove_sys_dev_entry(dev);  
  117.  devtattrError:  
  118.     if (MAJOR(dev->devt))  
  119.         device_remove_file(dev, &devt_attr);  
  120.  ueventattrError:  
  121.     device_remove_file(dev, &uevent_attr);  
  122.  attrError:  
  123.     kobject_uevent(&dev->kobj, KOBJ_REMOVE);  
  124.     kobject_del(&dev->kobj);  
  125.  Error:  
  126.     cleanup_device_parent(dev);  
  127.     if (parent)  
  128.         put_device(parent);  
  129. name_error:  
  130.     kfree(dev->p);  
  131.     dev->p = NULL;  
  132.     goto done;  
  133. }  
int device_add(struct device *dev)
{
    struct device *parent = NULL;
    struct class_interface *class_intf;
    int error = -EINVAL;

    dev = get_device(dev);
    if (!dev)
        goto done;

    if (!dev->p) {
        error = device_private_init(dev);
        if (error)
            goto done;
    }

    /*
     * for statically allocated devices, which should all be converted
     * some day, we need to initialize the name. We prevent reading back
     * the name, and force the use of dev_name()
     */
    if (dev->init_name) {
        dev_set_name(dev, "%s", dev->init_name);
        dev->init_name = NULL;
    }

    if (!dev_name(dev))
        goto name_error;

    pr_debug("device: '%s': %s\n", dev_name(dev), __func__);

    parent = get_device(dev->parent);
    setup_parent(dev, parent);

    /* use parent numa_node */
    if (parent)
        set_dev_node(dev, dev_to_node(parent));

    /* first, register with generic layer. */
    /* we require the name to be set before, and pass NULL */
    error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);
    if (error)
        goto Error;

    /* notify platform of device entry */
    if (platform_notify)
        platform_notify(dev);

    error = device_create_file(dev, &uevent_attr);
    if (error)
        goto attrError;

    if (MAJOR(dev->devt)) {
        error = device_create_file(dev, &devt_attr);
        if (error)
            goto ueventattrError;

        error = device_create_sys_dev_entry(dev);
        if (error)
            goto devtattrError;

        devtmpfs_create_node(dev);
    }

    error = device_add_class_symlinks(dev);
    if (error)
        goto SymlinkError;
    error = device_add_attrs(dev);
    if (error)
        goto AttrsError;
    error = bus_add_device(dev);
    if (error)
        goto BusError;
    error = dpm_sysfs_add(dev);
    if (error)
        goto DPMError;
    device_pm_add(dev);

    /* Notify clients of device addition.  This call must come
     * after dpm_sysf_add() and before kobject_uevent().
     */
    if (dev->bus)
        blocking_notifier_call_chain(&dev->bus->p->bus_notifier,
                         BUS_NOTIFY_ADD_DEVICE, dev);

    kobject_uevent(&dev->kobj, KOBJ_ADD);
    bus_probe_device(dev);
    if (parent)
        klist_add_tail(&dev->p->knode_parent,
                   &parent->p->klist_children);

    if (dev->class) {
        mutex_lock(&dev->class->p->class_mutex);
        /* tie the class to the device */
        klist_add_tail(&dev->knode_class,
                   &dev->class->p->class_devices);

        /* notify any interfaces that the device is here */
        list_for_each_entry(class_intf,
                    &dev->class->p->class_interfaces, node)
            if (class_intf->add_dev)
                class_intf->add_dev(dev, class_intf);
        mutex_unlock(&dev->class->p->class_mutex);
    }
done:
    put_device(dev);
    return error;
 DPMError:
    bus_remove_device(dev);
 BusError:
    device_remove_attrs(dev);
 AttrsError:
    device_remove_class_symlinks(dev);
 SymlinkError:
    if (MAJOR(dev->devt))
        device_remove_sys_dev_entry(dev);
 devtattrError:
    if (MAJOR(dev->devt))
        device_remove_file(dev, &devt_attr);
 ueventattrError:
    device_remove_file(dev, &uevent_attr);
 attrError:
    kobject_uevent(&dev->kobj, KOBJ_REMOVE);
    kobject_del(&dev->kobj);
 Error:
    cleanup_device_parent(dev);
    if (parent)
        put_device(parent);
name_error:
    kfree(dev->p);
    dev->p = NULL;
    goto done;
}
    代码第41行,将 dev->kobj 注册进内核,会在/sys/devices 目录下创建目录。

    代码第53-63行,是创建属性文件dev 的过程,也就是这一步,让mdev能够自动为我们创建设备节点。

    代码第65行,创建 /sys/class 到 /sys/device/xx/dev->name的符号链接,这个跟设备总线驱动模型中创建/sys/bus 到 /sys/device/xx/dev->name的符号链接是一样一样的。

    代码第92-103行,将 dev 加入 class的设备链表,并调用 class_interfaces 链表中的每一个 class_intf 结构,调用里面的 add_dev 函数。

    分析到这,class 好像并没有干什么实质性的事情。后面到input、tty、rtc在具体分析吧。









   

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux,通常使用内核模块进行驱动程序开发。要注册一个class,可以使用struct class结构体,具体步骤如下: 1. 在驱动程序的初始化函数,创建一个struct class结构体对象。 2. 为struct class结构体对象设置合适的属性,例如class的名字、父class等。 3. 调用函数class_register()将struct class结构体对象注册到系统。 4. 如果注册成功,将返回一个指向struct class结构体对象的指针,否则返回错误码。 下面是一个简单的示例代码: ``` #include <linux/init.h> #include <linux/module.h> #include <linux/device.h> static struct class *my_class; static int __init my_init(void) { // 创建一个class my_class = class_create(THIS_MODULE, "my_class"); if (IS_ERR(my_class)) { printk(KERN_ERR "Failed to register class\n"); return PTR_ERR(my_class); } printk(KERN_INFO "Class registered successfully\n"); return 0; } static void __exit my_exit(void) { // 注销class class_unregister(my_class); printk(KERN_INFO "Class unregistered successfully\n"); } module_init(my_init); module_exit(my_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("A simple module that registers a class"); ``` 在上面的代码,我们使用了函数class_create()来创建一个class,然后使用函数class_unregister()来注销这个class。在实际的驱动程序,我们还需要在设备的probe函数调用device_create()函数来创建设备节点,并在设备的remove函数调用device_destroy()函数来销毁设备节点。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值