大家好,又见面了,我是你们的朋友全栈君。
- 内核中的platform driver机制需要将设备本身的资源注册进内核,由内核统一管理,在驱动程序中使用这些资源时通过platform device提供的标准接口进行申请并使用。这样可以提高驱动和资源管理的独立性。本文的目的就是希望弄清楚platform device和driver之间的关系。
- 1.1 相关数据结构
- 1.1.1 device
- 这个结构体定义为:
- struct device {
- struct klist klist_children;
- struct klist_node knode_parent; /* node in sibling list */
- struct klist_node knode_driver;
- struct klist_node knode_bus;
- struct device *parent;
- struct kobject kobj;
- char bus_id[BUS_ID_SIZE]; /* position on parent bus */
- struct device_type *type;
- unsigned is_registered:1;
- unsigned uevent_suppress:1;
- struct device_attribute uevent_attr;
- struct device_attribute *devt_attr;
- struct semaphore sem; /* semaphore to synchronize calls to
- * its driver.
- */
- struct bus_type * bus; /* type of bus device is on */
- struct device_driver *driver; /* which driver has allocated this
- device */
- void *driver_data; /* data private to the driver */
- void *platform_data; /* Platform specific data, device
- core doesn’t touch it */
- struct dev_pm_info power;
- #ifdef CONFIG_NUMA
- int numa_node; /* NUMA node this device is close to */
- #endif
- u64 *dma_mask; /* dma mask (if dma’able device) */
- u64 coherent_dma_mask;/* Like dma_mask, but for
- alloc_coherent mappings as
- not all hardware supports
- 64 bit addresses for consistent
- allocations such descriptors. */
- struct list_head dma_pools; /* dma pools (if dma’ble) */
- struct dma_coherent_mem *dma_mem; /* internal for coherent mem
- override */
- /* arch specific additions */
- struct dev_archdata archdata;
- spinlock_t devres_lock;
- struct list_head devres_head;
- /* class_device migration path */
- struct list_head node;
- struct class *class;
- dev_t devt; /* dev_t, creates the sysfs “dev” */
- struct attribute_group **groups; /* optional groups */
- void (*release)(struct device * dev);
- };
- 这个结构体有点复杂,不过我们暂时用不了这么多。
- 1.1.2 resource
- 这个结构体定义为:
- /*
- * Resources are tree-like, allowing
- * nesting etc..
- */
- struct resource {
- resource_size_t start;
- resource_size_t end;
- const char *name;
- unsigned long flags;
- struct resource *parent, *sibling, *child;
- };
- 在这个结构体中,start和end的意义将根据flags中指定的资源类型进行解释。内核对资源进行了分类,一共有四种类型:
- #define IORESOURCE_IO 0x00000100 /* Resource type */
- #define IORESOURCE_MEM 0x00000200
- #define IORESOURCE_IRQ 0x00000400
- #define IORESOURCE_DMA 0x00000800
- 对于DM9000来说,其定义的资源如下:
- static struct resource dm9000_bfin_resources[] = {
- {
- .start = 0x2C000000,
- .end = 0x2C000000 + 0x7F,
- .flags = IORESOURCE_MEM,
- }, {
- .start = IRQ_PF10,
- .end = IRQ_PF10,
- .flags = IORESOURCE_IRQ | IORESOURCE_IRQ_LOWLEVEL,
- },
- };
- 也就是说,它定义了两种类型的资源。从这里也可以看出resource结构体里面的name成员没有太大的用处。
- 1.1.3 platform_device
- 这个结构体定义为:
- struct platform_device {
- const char * name;
- u32 id;
- struct device dev;
- u32 num_resources;
- struct resource * resource;
- };
- 它对device加了一层包装,添加了resource的内容。看看DM9000的定义:
- static struct platform_device dm9000_bfin_device = {
- .name = “dm9000”,
- .id = -1,
- .num_resources = ARRAY_SIZE(dm9000_bfin_resources),
- .resource = dm9000_bfin_resources,
- };
- 注意这里的name。
- 1.1.4 device_driver
- 这个结构体定义为:
- struct device_driver {
- const char * name;
- struct bus_type * bus;
- struct kobject kobj;
- struct klist klist_devices;
- struct klist_node knode_bus;
- struct module * owner;
- const char * mod_name; /* used for built-in modules */
- struct module_kobject * mkobj;
- int (*probe) (struct device * dev);
- int (*remove) (struct device * dev);
- void (*shutdown) (struct device * dev);
- int (*suspend) (struct device * dev, pm_message_t state);
- int (*resume) (struct device * dev);
- };
- 1.1.5 platform_driver
- 这个结构体定义为:
- struct platform_driver {
- int (*probe)(struct platform_device *);
- int (*remove)(struct platform_device *);
- void (*shutdown)(struct platform_device *);
- int (*suspend)(struct platform_device *, pm_message_t state);
- int (*suspend_late)(struct platform_device *, pm_message_t state);
- int (*resume_early)(struct platform_device *);
- int (*resume)(struct platform_device *);
- struct device_driver driver;
- };
- 它在device_driver的基础上封装了几个操作函数。
- 1.1.6 bus_type
- 这个结构体定义为:
- struct bus_type {
- const char * name;
- struct module * owner;
- struct kset subsys;
- struct kset drivers;
- struct kset devices;
- struct klist klist_devices;
- struct klist klist_drivers;
- struct blocking_notifier_head bus_notifier;
- struct bus_attribute * bus_attrs;
- struct device_attribute * dev_attrs;
- struct driver_attribute * drv_attrs;
- struct bus_attribute drivers_autoprobe_attr;
- struct bus_attribute drivers_probe_attr;
- int (*match)(struct device * dev, struct device_driver * drv);
- int (*uevent)(struct device *dev, char **envp,
- int num_envp, char *buffer, int buffer_size);
- int (*probe)(struct device * dev);
- int (*remove)(struct device * dev);
- void (*shutdown)(struct device * dev);
- int (*suspend)(struct device * dev, pm_message_t state);
- int (*suspend_late)(struct device * dev, pm_message_t state);
- int (*resume_early)(struct device * dev);
- int (*resume)(struct device * dev);
- unsigned int drivers_autoprobe:1;
- };
- 1.2 资源注册
- 在arch/blackfin/mach-bf561/boards/ezkit.c中有这样的代码:
- static int __init ezkit_init(void)
- {
- int ret;
- printk(KERN_INFO “%s(): registering device resources/n”, __func__);
- ret = platform_add_devices(ezkit_devices, ARRAY_SIZE(ezkit_devices));
- if (ret < 0)
- return ret;
- return 0;
- }
- arch_initcall(ezkit_init);
- 这里使用了arch_initcall来对ezkit_init函数进行调用次序的限制,而驱动的加载通常是使用module_init进行限制的,因此ezkit_init函数将先于驱动加载。
- 在这里ezkit_devices的定义为:
- static struct platform_device *ezkit_devices[] __initdata = {
- &dm9000_bfin_device,
- …………
- };
- 1.2.1 platform_add_devices
- 这个函数比较简单:
- /**
- * platform_add_devices – add a numbers of platform devices
- * @devs: array of platform devices to add
- * @num: number of platform devices in array
- */
- int platform_add_devices(struct platform_device **devs, int num)
- {
- int i, ret = 0;
- for (i = 0; i < num; i++) {
- ret = platform_device_register(devs[i]);
- if (ret) {
- while (–i >= 0)
- platform_device_unregister(devs[i]);
- break;
- }
- }
- return ret;
- }
- 为这个数组中的每个元素调用platform_device_register,如果出错则注销此前注册的所有platform device。
- 1.2.2 platform_device_register
- 这个函数的实现为:
- /**
- * platform_device_register – add a platform-level device
- * @pdev: platform device we’re adding
- *
- */
- int platform_device_register(struct platform_device * pdev)
- {
- device_initialize(&pdev->dev);
- return platform_device_add(pdev);
- }
- 也比较简单,先调用device_initialize初始化platform_device::dev,这里仅仅是对device结构体的成员赋初值,略过它不做分析。接下来的关键是platform_device_add。
- 1.2.3 platform_device_add
- 这个函数定义为:
- /**
- * platform_device_add – add a platform device to device hierarchy
- * @pdev: platform device we’re adding
- *
- * This is part 2 of platform_device_register(), though may be called
- * separately _iff_ pdev was allocated by platform_device_alloc().
- */
- int platform_device_add(struct platform_device *pdev)
- {
- int i, ret = 0;
- if (!pdev)
- return -EINVAL;
- if (!pdev->dev.parent)
- pdev->dev.parent = &platform_bus;
- pdev->dev.bus = &platform_bus_type;
- if (pdev->id != -1)
- snprintf(pdev->dev.bus_id, BUS_ID_SIZE, “%s.%u”, pdev->name, pdev->id);
- else
- strlcpy(pdev->dev.bus_id, pdev->name, BUS_ID_SIZE);
- for (i = 0; i < pdev->num_resources; i++) {
- struct resource *p, *r = &pdev->resource[i];
- if (r->name == NULL)
- r->name = pdev->dev.bus_id;
- p = r->parent;
- if (!p) {
- if (r->flags & IORESOURCE_MEM)
- p = &iomem_resource;
- else if (r->flags & IORESOURCE_IO)
- p = &ioport_resource;
- }
- if (p && insert_resource(p, r)) {
- printk(KERN_ERR
- “%s: failed to claim resource %d/n”,
- pdev->dev.bus_id, i);
- ret = -EBUSY;
- goto failed;
- }
- }
- pr_debug(“Registering platform device ‘%s’. Parent at %s/n”,
- pdev->dev.bus_id, pdev->dev.parent->bus_id);
- ret = device_add(&pdev->dev);
- if (ret == 0)
- return ret;
- failed:
- while (–i >= 0)
- if (pdev->resource[i].flags & (IORESOURCE_MEM|IORESOURCE_IO))
- release_resource(&pdev->resource[i]);
- return ret;
- }
- 在这个函数里做了两件关键的事情,一个是注册device设备,它将device::bus指定为platform_bus_type。另一个是注册resource。看下面的这几行代码:
- if (!p) {
- if (r->flags & IORESOURCE_MEM)
- p = &iomem_resource;
- else if (r->flags & IORESOURCE_IO)
- p = &ioport_resource;
- }
- 对照DM9000的资源定义:
- static struct resource dm9000_bfin_resources[] = {
- {
- .start = 0x2C000000,
- .end = 0x2C000000 + 0x7F,
- .flags = IORESOURCE_MEM,
- }, {
- .start = IRQ_PF10,
- .end = IRQ_PF10,
- .flags = IORESOURCE_IRQ | IORESOURCE_IRQ_LOWLEVEL,
- },
- };
- 它的中断资源并没有进行注册。
- 1.2.4 device_add
- 这一函数定义为:
- /**
- * device_add – add device to device hierarchy.
- * @dev: device.
- *
- * This is part 2 of device_register(), though may be called
- * separately _iff_ device_initialize() has been called separately.
- *
- * This adds it to the kobject hierarchy via kobject_add(), adds it
- * to the global and sibling lists for the device, then
- * adds it to the other relevant subsystems of the driver model.
- */
- int device_add(struct device *dev)
- {
- ……………………..
- if ((error = device_add_attrs(dev)))
- goto AttrsError;
- if ((error = device_pm_add(dev)))
- goto PMError;
- if ((error = bus_add_device(dev)))
- goto BusError;
- …………………..
- }
- 这里有一个关键调用bus_add_device,它将把dev添加到platform_bus_type这一全局变量中的列表。
- 1.2.5 bus_add_device
- 这个函数定义为:
- /**
- * bus_add_device – add device to bus
- * @dev: device being added
- *
- * – Add the device to its bus’s list of devices.
- * – Create link to device’s bus.
- */
- int bus_add_device(struct device * dev)
- {
- struct bus_type * bus = get_bus(dev->bus);
- int error = 0;
- if (bus) {
- pr_debug(“bus %s: add device %s/n”, bus->name, dev->bus_id);
- error = device_add_attrs(bus, dev);
- if (error)
- goto out_put;
- error = sysfs_create_link(&bus->devices.kobj,
- &dev->kobj, dev->bus_id);
- if (error)
- goto out_id;
- error = sysfs_create_link(&dev->kobj,
- &dev->bus->subsys.kobj, “subsystem”);
- if (error)
- goto out_subsys;
- error = make_deprecated_bus_links(dev);
- if (error)
- goto out_deprecated;
- }
- return 0;
- out_deprecated:
- sysfs_remove_link(&dev->kobj, “subsystem”);
- out_subsys:
- sysfs_remove_link(&bus->devices.kobj, dev->bus_id);
- out_id:
- device_remove_attrs(bus, dev);
- out_put:
- put_bus(dev->bus);
- return error;
- }
- 注意当执行到此函数时dev->bus指向platform_bus_type这一全局变量,因而这一函数将把dev添加到platform_bus_type的链表中。
- 1.3 驱动注册
- 下面是DM9000网卡的驱动加载代码:
- static int __init
- dm9000_init(void)
- {
- printk(KERN_INFO “%s Ethernet Driver/n”, CARDNAME);
- return platform_driver_register(&dm9000_driver); /* search board and register */
- }
- module_init(dm9000_init);
- 很简单的代码,直接调用platform_driver_register注册驱动,这里dm9000_driver的定义为:
- static struct platform_driver dm9000_driver = {
- .driver = {
- .name = “dm9000”,
- .owner = THIS_MODULE,
- },
- .probe = dm9000_probe,
- .remove = dm9000_drv_remove,
- .suspend = dm9000_drv_suspend,
- .resume = dm9000_drv_resume,
- };
- 1.3.1 platform_driver_register
- 这个函数定义为:
- /**
- * platform_driver_register
- * @drv: platform driver structure
- */
- int platform_driver_register(struct platform_driver *drv)
- {
- drv->driver.bus = &platform_bus_type;
- if (drv->probe)
- drv->driver.probe = platform_drv_probe;
- if (drv->remove)
- drv->driver.remove = platform_drv_remove;
- if (drv->shutdown)
- drv->driver.shutdown = platform_drv_shutdown;
- if (drv->suspend)
- drv->driver.suspend = platform_drv_suspend;
- if (drv->resume)
- drv->driver.resume = platform_drv_resume;
- return driver_register(&drv->driver);
- }
- 注意由于DM9000的platform_driver中指定了probe,remove,suspend,resume这四个函数,因此device_driver结构体中的这几个函数指针将进行初始化设置。最后再调用driver_register注册driver成员,有点奇怪,怎么就抛弃了platform_driver呢?
- 1.3.2 driver_register
- 这个函数定义为:
- /**
- * driver_register – register driver with bus
- * @drv: driver to register
- *
- * We pass off most of the work to the bus_add_driver() call,
- * since most of the things we have to do deal with the bus
- * structures.
- */
- int driver_register(struct device_driver * drv)
- {
- if ((drv->bus->probe && drv->probe) ||
- (drv->bus->remove && drv->remove) ||
- (drv->bus->shutdown && drv->shutdown)) {
- printk(KERN_WARNING “Driver ‘%s’ needs updating – please use bus_type methods/n”, drv->name);
- }
- klist_init(&drv->klist_devices, NULL, NULL);
- return bus_add_driver(drv);
- }
- 当函数执行到这里的时候,drv->bus指向的是platform_bus_type这一全局变量。
- struct bus_type platform_bus_type = {
- .name = “platform”,
- .dev_attrs = platform_dev_attrs,
- .match = platform_match,
- .uevent = platform_uevent,
- .suspend = platform_suspend,
- .suspend_late = platform_suspend_late,
- .resume_early = platform_resume_early,
- .resume = platform_resume,
- };
- 1.3.3 bus_add_driver
- 这个函数定义为:
- /**
- * bus_add_driver – Add a driver to the bus.
- * @drv: driver.
- *
- */
- int bus_add_driver(struct device_driver *drv)
- {
- struct bus_type * bus = get_bus(drv->bus);
- int error = 0;
- if (!bus)
- return -EINVAL;
- pr_debug(“bus %s: add driver %s/n”, bus->name, drv->name);
- error = kobject_set_name(&drv->kobj, “%s”, drv->name);
- if (error)
- goto out_put_bus;
- drv->kobj.kset = &bus->drivers;
- if ((error = kobject_register(&drv->kobj)))
- goto out_put_bus;
- if (drv->bus->drivers_autoprobe) {
- error = driver_attach(drv);
- if (error)
- goto out_unregister;
- }
- klist_add_tail(&drv->knode_bus, &bus->klist_drivers);
- module_add_driver(drv->owner, drv);
- error = driver_add_attrs(bus, drv);
- if (error) {
- /* How the hell do we get out of this pickle? Give up */
- printk(KERN_ERR “%s: driver_add_attrs(%s) failed/n”,
- __FUNCTION__, drv->name);
- }
- error = add_bind_files(drv);
- if (error) {
- /* Ditto */
- printk(KERN_ERR “%s: add_bind_files(%s) failed/n”,
- __FUNCTION__, drv->name);
- }
- return error;
- out_unregister:
- kobject_unregister(&drv->kobj);
- out_put_bus:
- put_bus(bus);
- return error;
- }
- 当函数执行到此的时候,drv->bus将指向platform_bus_type这一全局变量,而这一全局变量的drivers_autoprobe成员在bus_register这一全局初始化函数中设置为1。因此这里将调用driver_attach函数,注意此时传递进去的参数drv指向的是dm9000_driver的driver成员。
- 1.3.4 driver_attach
- 这一函数定义为:
- /**
- * driver_attach – try to bind driver to devices.
- * @drv: driver.
- *
- * Walk the list of devices that the bus has on it and try to
- * match the driver with each one. If driver_probe_device()
- * returns 0 and the @dev->driver is set, we’ve found a
- * compatible pair.
- */
- int driver_attach(struct device_driver * drv)
- {
- return bus_for_each_dev(drv->bus, NULL, drv, __driver_attach);
- }
- 很简单,转向bus_for_each_dev。
- 1.3.5 bus_for_each_dev
- 这一函数定义为:
- /**
- * bus_for_each_dev – device iterator.
- * @bus: bus type.
- * @start: device to start iterating from.
- * @data: data for the callback.
- * @fn: function to be called for each device.
- *
- * Iterate over @bus’s list of devices, and call @fn for each,
- * passing it @data. If @start is not NULL, we use that device to
- * begin iterating from.
- *
- * We check the return of @fn each time. If it returns anything
- * other than 0, we break out and return that value.
- *
- * NOTE: The device that returns a non-zero value is not retained
- * in any way, nor is its refcount incremented. If the caller needs
- * to retain this data, it should do, and increment the reference
- * count in the supplied callback.
- */
- int bus_for_each_dev(struct bus_type * bus, struct device * start,
- void * data, int (*fn)(struct device *, void *))
- {
- struct klist_iter i;
- struct device * dev;
- int error = 0;
- if (!bus)
- return -EINVAL;
- klist_iter_init_node(&bus->klist_devices, &i,
- (start ? &start->knode_bus : NULL));
- while ((dev = next_device(&i)) && !error)
- error = fn(dev, data);
- klist_iter_exit(&i);
- return error;
- }
- 简单枚举此总线上注册的device,然后为其调用__driver_attach函数,试图将一个device和传递进来的driver相匹配。
- 1.3.6 __driver_attach
- 这一函数定义为:
- static int __driver_attach(struct device * dev, void * data)
- {
- struct device_driver * drv = data;
- /*
- * Lock device and try to bind to it. We drop the error
- * here and always return 0, because we need to keep trying
- * to bind to devices and some drivers will return an error
- * simply if it didn’t support the device.
- *
- * driver_probe_device() will spit a warning if there
- * is an error.
- */
- if (dev->parent) /* Needed for USB */
- down(&dev->parent->sem);
- down(&dev->sem);
- if (!dev->driver)
- driver_probe_device(drv, dev);
- up(&dev->sem);
- if (dev->parent)
- up(&dev->parent->sem);
- return 0;
- }
- 很简单,转而调用driver_probe_device进行驱动的匹配。
- 1.3.7 driver_probe_device
- 这个函数定义为:
- /**
- * driver_probe_device – attempt to bind device & driver together
- * @drv: driver to bind a device to
- * @dev: device to try to bind to the driver
- *
- * First, we call the bus’s match function, if one present, which should
- * compare the device IDs the driver supports with the device IDs of the
- * device. Note we don’t do this ourselves because we don’t know the
- * format of the ID structures, nor what is to be considered a match and
- * what is not.
- *
- * This function returns 1 if a match is found, -ENODEV if the device is
- * not registered, and 0 otherwise.
- *
- * This function must be called with @dev->sem held. When called for a
- * USB interface, @dev->parent->sem must be held as well.
- */
- int driver_probe_device(struct device_driver * drv, struct device * dev)
- {
- int ret = 0;
- if (!device_is_registered(dev))
- return -ENODEV;
- if (drv->bus->match && !drv->bus->match(dev, drv))
- goto done;
- pr_debug(“%s: Matched Device %s with Driver %s/n”,
- drv->bus->name, dev->bus_id, drv->name);
- ret = really_probe(dev, drv);
- done:
- return ret;
- }
- 此时的drv->bus指向platform_bus_type这一全局变量,而它的match函数为platform_match,且让我们看看它是如何确定device和driver是否匹配的。
- /**
- * platform_match – bind platform device to platform driver.
- * @dev: device.
- * @drv: driver.
- *
- * Platform device IDs are assumed to be encoded like this:
- * “<name><instance>”, where <name> is a short description of the
- * type of device, like “pci” or “floppy”, and <instance> is the
- * enumerated instance of the device, like ‘0’ or ’42’.
- * Driver IDs are simply “<name>”.
- * So, extract the <name> from the platform_device structure,
- * and compare it against the name of the driver. Return whether
- * they match or not.
- */
- static int platform_match(struct device * dev, struct device_driver * drv)
- {
- struct platform_device *pdev = container_of(dev, struct platform_device, dev);
- return (strncmp(pdev->name, drv->name, BUS_ID_SIZE) == 0);
- }
- 也就是说,它通过比较pdev->name和drv->name是否匹配来决定。
- 对于DM9000的驱动来说,这里的pdev指向dm9000_bfin_device,看看它的初始值:
- static struct platform_device dm9000_bfin_device = {
- .name = “dm9000”,
- .id = -1,
- .num_resources = ARRAY_SIZE(dm9000_bfin_resources),
- .resource = dm9000_bfin_resources,
- };
- 再看drv,其指向dm9000_driver这一变量中的driver成员。
- static struct platform_driver dm9000_driver = {
- .driver = {
- .name = “dm9000”,
- .owner = THIS_MODULE,
- },
- .probe = dm9000_probe,
- .remove = dm9000_drv_remove,
- .suspend = dm9000_drv_suspend,
- .resume = dm9000_drv_resume,
- };
- 在进行了正确的名称匹配之后,将调用really_probe进行硬件检测。
- 1.3.8 really_probe
- 这一函数定义为:
- static int really_probe(struct device *dev, struct device_driver *drv)
- {
- int ret = 0;
- atomic_inc(&probe_count);
- pr_debug(“%s: Probing driver %s with device %s/n”,
- drv->bus->name, drv->name, dev->bus_id);
- WARN_ON(!list_empty(&dev->devres_head));
- dev->driver = drv;
- if (driver_sysfs_add(dev)) {
- printk(KERN_ERR “%s: driver_sysfs_add(%s) failed/n”,
- __FUNCTION__, dev->bus_id);
- goto probe_failed;
- }
- if (dev->bus->probe) {
- ret = dev->bus->probe(dev);
- if (ret)
- goto probe_failed;
- } else if (drv->probe) {
- ret = drv->probe(dev);
- if (ret)
- goto probe_failed;
- }
- driver_bound(dev);
- ret = 1;
- pr_debug(“%s: Bound Device %s to Driver %s/n”,
- drv->bus->name, dev->bus_id, drv->name);
- goto done;
- probe_failed:
- devres_release_all(dev);
- driver_sysfs_remove(dev);
- dev->driver = NULL;
- if (ret != -ENODEV && ret != -ENXIO) {
- /* driver matched but the probe failed */
- printk(KERN_WARNING
- “%s: probe of %s failed with error %d/n”,
- drv->name, dev->bus_id, ret);
- }
- /*
- * Ignore errors returned by ->probe so that the next driver can try
- * its luck.
- */
- ret = 0;
- done:
- atomic_dec(&probe_count);
- wake_up(&probe_waitqueue);
- return ret;
- }
- 此时的drv->bus指向platform_bus_type这一全局变量,其probe回调函数没有指定,而drv->probe函数则指向dm9000_probe。因此转向dm9000_probe执行,并将dm9000_bfin_device做为参数传递进去。
- 1.4 结论
- platform device和driver分别向platform_bus_type这一中介注册,并通过名称进行相互间的匹配。很是有点婚姻中介的味道,还有点对暗号的神秘,呵呵!
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/163801.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...