您的位置:首页 > 运维架构 > Linux

Android下led控制(下)--Linux驱动部分--platform机制

2016-07-04 17:26 597 查看
前面写了两个博文,一个是Android下,一个是Linux下led控制,但是Linux下那个写的有很多漏洞和不清楚的地方。这里写一篇作为补充,也是我在学习中理解的深入。当然这个可能也会有很多漏洞,如果我有更深入的了解,继续进行补充。我的开发板是全志科技的CQA83T,成都启划公司出的扩展板。

先贴出来驱动源程序的代码,此代码的位置在 lichee\linux-3.4\drivers\char\led.c:

#include <linux/types.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/init.h>
#include <linux/input.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/jiffies.h>
#include <linux/module.h>
#include <linux/gpio.h>
#include <linux/input/matrix_keypad.h>
#include <linux/slab.h>
#include <asm/io.h>
#include <mach/irqs.h>
#include <mach/hardware.h>
#include <mach/sys_config.h>
#include <linux/miscdevice.h>
#include <linux/printk.h>
#include <linux/kernel.h>

#define LED_IOCTL_SET_ON		1
#define LED_IOCTL_SET_OFF			0
static script_item_u			led_val[5];
static script_item_value_type_e		led_type;
static struct semaphore lock;

//led_open
static int led_open(struct inode *inode, struct file *file)
{
if (!down_trylock(&lock))
return 0;
else
return -EBUSY;
}

//led_close
static int  led_close(struct inode *inode, struct file *file)
{
up(&lock);
return 0;
}

//led_ioctl
static long  led_ioctl(struct file *filep, unsigned int cmd,
unsigned long arg)
{
unsigned int n;
n = (unsigned int)arg;
switch (cmd) {
case LED_IOCTL_SET_ON:
if (n < 1)
return -EINVAL;
if(led_val[n-1].gpio.gpio != -1) {
__gpio_set_value(led_val[n-1].gpio.gpio, 1);
printk("led%d on !\n", n);
}
break;

case LED_IOCTL_SET_OFF:
default:
if (n < 1)
return -EINVAL;
if(led_val[n-1].gpio.gpio != -1) {
__gpio_set_value(led_val[n-1].gpio.gpio, 0);
printk("led%d off !\n", n);
}
break;
}

return 0;
}

//led_gpio
static int __devinit led_gpio(void)
{
int i = 0;
char gpio_num[10];

for(i =1 ; i < 6; i++)
{
sprintf(gpio_num, "led_gpio%d", i);

led_type= script_get_item("led_para", gpio_num, &led_val[i-1]);
if(SCIRPT_ITEM_VALUE_TYPE_PIO != led_type) {
printk("led_gpio type fail !");
//			gpio_free(led_val[i-1].gpio.gpio);
led_val[i-1].gpio.gpio	= -1;
continue;
}

if(0 != gpio_request(led_val[i-1].gpio.gpio, NULL)) {
printk("led_gpio gpio_request fail !");
led_val[i-1].gpio.gpio = -1;
continue;
}

if (0 != gpio_direction_output(led_val[i-1].gpio.gpio, 0)) {
printk("led_gpio gpio_direction_output fail !");
//			gpio_free(led_val[i-1].gpio.gpio);
led_val[i-1].gpio.gpio = -1;
continue;
}
}

return 0;
}

//file_operations
static struct file_operations leds_ops = {
.owner			= THIS_MODULE,
.open			= led_open,
.release		= led_close,
.unlocked_ioctl		= led_ioctl,
};

//miscdevice
static struct miscdevice leds_dev = {
.minor = MISC_DYNAMIC_MINOR,
.name = "led",
.fops = &leds_ops,
};

//led_remove
static int __devexit led_remove(struct platform_device *pdev)
{
return 0;
}

//led_probe
static int __devinit led_probe(struct platform_device *pdev)
{
int led_used;
script_item_u	val;
script_item_value_type_e  type;

int err;

printk("led_para!\n");
type = script_get_item("led_para", "led_used", &val);
if (SCIRPT_ITEM_VALUE_TYPE_INT != type) {
printk("%s script_get_item \"led_para\" led_used = %d\n",
__FUNCTION__, val.val);
return -1;
}
led_used = val.val;
printk("%s script_get_item \"led_para\" led_used = %d\n",
__FUNCTION__, val.val);

if(!led_used) {
printk("%s led_used is not used in config,  led_used=%d\n", __FUNCTION__,led_used);
return -1;
}

err = led_gpio();
if (err)
return -1;

sema_init(&lock, 1);
err = misc_register(&leds_dev);
printk("======= cqa83 led initialized ================\n");

return err;
}

//platform_device
struct platform_device led_device = {
.name		= "led",
};

//platform_driver
static struct platform_driver led_driver = {
.probe		= led_probe,
.remove		= __devexit_p(led_remove),
.driver		= {
.name	= "led",
.owner	= THIS_MODULE,
},
};

//led_init
static int __init led_init(void)
{

if (platform_device_register(&led_device)) {
printk("%s: register gpio device failed\n", __func__);
}
if (platform_driver_register(&led_driver)) {
printk("%s: register gpio driver failed\n", __func__);
}

return 0;
}

//led_exit
static void __exit led_exit(void)
{
platform_driver_unregister(&led_driver);
}

module_init(led_init);
module_exit(led_exit);

MODULE_DESCRIPTION("Led Driver");
MODULE_LICENSE("GPL v2");


首先,在这里我们应该获取这样几个信息,

1、这是一个Linux驱动程序,一个字符驱动,一个杂项字符驱动。从err = misc_register(&leds_dev);可以知道是杂项字符驱动。

2、这里使用到了Linux的GPIO驱动模型。

3、这个驱动是基于platform机制的。

第一,我们先说一说platform机制。

platform机制是Linux2.6引入的一套新的驱动管理和注册机制,Linux大部分设备驱动中都能使用这套机制。platform是一种虚拟总线,主要用来管理CPU的片上资源具有很好的移植性。platform机制本身的使用并不复杂,由platform_device(总是设备)和platform_driver(总线驱动)两部分组成,设备用platform_device表示,驱动用platform_driver注册。系统首先会初始化platform总线,当platform设备想要挂载到总线上时,定义platform_device和platform_driver,然后使用函数platform_device_register注册platform_device,再使用platform_driver_register函数注册platform_driver驱动,这里要记住,platform_device_register它一定要在platform_driver_register之前。也就是一定要先注册设备再注册驱动,因为在驱动注册是,要先查找与之对应的设备,如果能够找到并匹配成功才能注册驱动。具体细节下面会分析。

下面,我们来说一下platform总线,platform总线相关的代码都在内核 linux-3.4\drivers\base\platform.c里面。既然platform总线是在内核启动时初始化,那么先列出初始化函数的调用过程,asmlinkagevoid __init start_kernel(void)[linux-3.4\init\main.c]
--> static noinline void __init_refok rest_init(void)[linux-3.4\init\main.c] --> static
int __init kernel_init(void * unused)[linux-3.4\init\main.c] --> static void __init do_basic_setup(void)
[linux-3.4\init\main.c] --> void __init driver_init(void) [linux-3.4\drivers\base\init.c] --> int
__init platform_bus_init(void) [linux-3.4\drivers\base\platform.c] ,中括号里是文件位置,函数platform_bus_init就是platform的总线初始化函数。

来看platform总线初始化函数platform_bus_init,位于linux-3.4\drivers\base\platform.c中:

int __init platform_bus_init(void)
{
int error;

early_platform_cleanup();

error = device_register(&platform_bus);
if (error)
return error;
error =  bus_register(&platform_bus_type);
if (error)
device_unregister(&platform_bus);
return error;
}
按图索骥,继续深入platform总线初始化函数来看early_platform_cleanup函数,这个函数从名字上就可以看出是一个清理函数。我们来看一下这个函数的源码,位于linux-3.4\drivers\base\platform.c中:

/**
* early_platform_cleanup - clean up early platform code
*/
void __init early_platform_cleanup(void)
{
struct platform_device *pd, *pd2;

/* clean up the devres list used to chain devices */
list_for_each_entry_safe(pd, pd2, &early_platform_device_list,
dev.devres_head) {
list_del(&pd->dev.devres_head);
memset(&pd->dev.devres_head, 0, sizeof(pd->dev.devres_head));
}
}
从注释可以看出,这个函数是清除早期的platform设备链表,list_for_each_entry_safe的作用是遍历先前的platform设备链表early_platform_device_list

,并清零每一个链表节点。

下面继续platform总线初始化函数中的device_register(&platform_bus)的函数,该函数是将platform总线作为设备进行注册。我们先看参数plat_bus,位于linux-3.4\drivers\base\platform.c中:

struct device platform_bus = {
.init_name	= "platform",
};
EXPORT_SYMBOL_GPL(platform_bus);
参数platform_bus是一个device类型的结构体,下面EXPORT_SYMBOL_GPL是宏,这个宏说明其参数所指向的函数只给有GPL认证的模块使用。下面来看一下device结构体,位于linux-3.4\include\linux\device.h中:

struct device {
struct device		*parent;

struct device_private	*p;

struct kobject kobj;
const char		*init_name; /* initial name of the device */
const struct device_type *type;

struct mutex		mutex;	/* mutex 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		*platform_data;	/* Platform specific data, device
core doesn't touch it */
struct dev_pm_info	power;
struct dev_pm_domain	*pm_domain;

#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 device_dma_parameters *dma_parms;

struct list_head	dma_pools;	/* dma pools (if dma'ble) */

struct dma_coherent_mem	*dma_mem; /* internal for coherent mem
override */
#ifdef CONFIG_CMA
struct cma *cma_area;		/* contiguous memory area for dma
allocations */
#endif
/* arch specific additions */
struct dev_archdata	archdata;

struct device_node	*of_node; /* associated device tree node */

dev_t			devt;	/* dev_t, creates the sysfs "dev" */
u32			id;	/* device instance */

spinlock_t		devres_lock;
struct list_head	devres_head;

struct klist_node	knode_class;
struct class		*class;
const struct attribute_group **groups;	/* optional groups */

void	(*release)(struct device *dev);
};


这个是基本的设备结构体,用于描述设备相关信息设备之间的层次关系,以及设备与总线驱动的关系。其实简单说在Linux内核里用这个结构体来表示一个设备,并用于设备在内核中的注册。网上有较多关于这个结构体的解析这里就不多说了。

接下来看设备注册函数device_register,位于linux-3.4\drivers\base\core.c中:

int device_register(struct device *dev)
{
device_initialize(dev);
return device_add(dev);
}
这个函数作用是向系统注册一个设备,它首先使用函数device_initialize对设备进行初始化,然后使用device_add添加设备。下面分别来看一下这俩函数,但这里不做解释,这俩函数都是位于linux-3.4\drivers\base\core.c中:

void device_initialize(struct device *dev)
{
dev->kobj.kset = devices_kset;
kobject_init(&dev->kobj, &device_ktype);
INIT_LIST_HEAD(&dev->dma_pools);
mutex_init(&dev->mutex);
lockdep_set_novalidate_class(&dev->mutex);
spin_lock_init(&dev->devres_lock);
INIT_LIST_HEAD(&dev->devres_head);
device_pm_init(dev);
set_dev_node(dev, -1);
}


int device_add(struct device *dev)
{
struct device *parent = NULL;
struct kobject *kobj;
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;
}

/* subsystems can specify simple device enumeration */
if (!dev_name(dev) && dev->bus && dev->bus->dev_name)
dev_set_name(dev, "%s%u", dev->bus->dev_name, dev->id);

if (!dev_name(dev)) {
error = -EINVAL;
goto name_error;
}

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

parent = get_device(dev->parent);
kobj = get_device_parent(dev, parent);
if (kobj)
dev->kobj.parent = kobj;

/* 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_sysfs_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->mutex);
/* tie the class to the device */
klist_add_tail(&dev->knode_class,
&dev->class->p->klist_devices);

/* notify any interfaces that the device is here */
list_for_each_entry(class_intf,
&dev->class->p->interfaces, node)
if (class_intf->add_dev)
class_intf->add_dev(dev, class_intf);
mutex_unlock(&dev->class->p->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))
devtmpfs_delete_node(dev);
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;
}


下面我们继续回到platform总线初始化函数platform_bus_init中,来看总线注册函数bus_register(&platform_bus_type),先看一下参数platfor_bus_type,位于linux-3.4\drivers\base\platform.c中:

struct bus_type platform_bus_type = {
.name		= "platform",
.dev_attrs	= platform_dev_attrs,
.match		= platform_match,
.uevent		= platform_uevent,
.pm		= &platform_dev_pm_ops,
};
EXPORT_SYMBOL_GPL(platform_bus_type);


从上面可以看出这是一个bus_type类型的结构体,bus_type类型结构体的定义位于linux-3.4\include\linux\device.h中:

struct bus_type {
const char		*name;
const char		*dev_name;
struct device		*dev_root;
struct bus_attribute	*bus_attrs;
struct device_attribute	*dev_attrs;
struct driver_attribute	*drv_attrs;

int (*match)(struct device *dev, struct device_driver *drv);
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
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);

const struct dev_pm_ops *pm;

struct iommu_ops *iommu_ops;

struct subsys_private *p;
};
这是一个设备总线类型结构体,成员变量指出了总线名称,子设备前缀名(像"foo%u", dev->id),被用作父设备的默认设备,总线属性,设备属性,驱动属性以及一些回调函数。

下面来看一下总线注册函数bus_register,位于linux-3.4\include\linux\device.h中,这是一个宏定义:

/* This is a #define to keep the compiler from merging different
* instances of the __key variable */
#define bus_register(subsys)			\
({						\
static struct lock_class_key __key;	\
__bus_register(subsys, &__key);	\
})


继续看函数_bus_register(subsys, &__key)函数,位于linux-3.4\drivers\base\bus.c中:

int __bus_register(struct bus_type *bus, struct lock_class_key *key)
{
int retval;
struct subsys_private *priv;

priv = kzalloc(sizeof(struct subsys_private), GFP_KERNEL);
if (!priv)
return -ENOMEM;

priv->bus = bus;
bus->p = priv;

BLOCKING_INIT_NOTIFIER_HEAD(&priv->bus_notifier);

retval = kobject_set_name(&priv->subsys.kobj, "%s", bus->name);
if (retval)
goto out;

priv->subsys.kobj.kset = bus_kset;
priv->subsys.kobj.ktype = &bus_ktype;
priv->drivers_autoprobe = 1;

retval = kset_register(&priv->subsys);
if (retval)
goto out;

retval = bus_create_file(bus, &bus_attr_uevent);
if (retval)
goto bus_uevent_fail;

priv->devices_kset = kset_create_and_add("devices", NULL,
&priv->subsys.kobj);
if (!priv->devices_kset) {
retval = -ENOMEM;
goto bus_devices_fail;
}

priv->drivers_kset = kset_create_and_add("drivers", NULL,
&priv->subsys.kobj);
if (!priv->drivers_kset) {
retval = -ENOMEM;
goto bus_drivers_fail;
}

INIT_LIST_HEAD(&priv->interfaces);
__mutex_init(&priv->mutex, "subsys mutex", key);
klist_init(&priv->klist_devices, klist_devices_get, klist_devices_put);
klist_init(&priv->klist_drivers, NULL, NULL);

retval = add_probe_files(bus);
if (retval)
goto bus_probe_files_fail;

retval = bus_add_attrs(bus);
if (retval)
goto bus_attrs_fail;

pr_debug("bus: '%s': registered\n", bus->name);
return 0;

bus_attrs_fail:
remove_probe_files(bus);
bus_probe_files_fail:
kset_unregister(bus->p->drivers_kset);
bus_drivers_fail:
kset_unregister(bus->p->devices_kset);
bus_devices_fail:
bus_remove_file(bus, &bus_attr_uevent);
bus_uevent_fail:
kset_unregister(&bus->p->subsys);
out:
kfree(bus->p);
bus->p = NULL;
return retval;
}
EXPORT_SYMBOL_GPL(__bus_register);
这个函数进行了返回检测,如果注册识别,则进行与注册相反的操作注销device_unregister。这个函数位于linux-3.4\drivers\base\core.c中:

/**
* device_unregister - unregister device from system.
* @dev: device going away.
*
* We do this in two parts, like we do device_register(). First,
* we remove it from all the subsystems with device_del(), then
* we decrement the reference count via put_device(). If that
* is the final reference count, the device will be cleaned up
* via device_release() above. Otherwise, the structure will
* stick around until the final reference to the device is dropped.
*/
void device_unregister(struct device *dev)
{
pr_debug("device: '%s': %s\n", dev_name(dev), __func__);
device_del(dev);
put_device(dev);
}


附上了英文注释,解释很清楚了。

到这里呢,platform总线初始化就结束了,没有做更多的解释,主要原因是我也在学习,还有就是每个函数的作用无论是见名知意还是查看函数说明,这个函数的功能是很明确的。

现在呢,platform总线已经初始化完成,下面就是把platform设备和驱动挂载到platform总线上了。

现在我们还是回到开始led驱动函数led.c中,了解过驱动的人都知道,驱动被加载到内核第一次被调用的函数就是其初始化函数,在led.c中:

module_init(led_init);
初始化函数led_init的源码再写一遍,如下:

static int __init led_init(void)
{

if (platform_device_register(&led_device)) {
printk("%s: register gpio device failed\n", __func__);
}
if (platform_driver_register(&led_driver)) {
printk("%s: register gpio driver failed\n", __func__);
}

return 0;
}


这里第一步是设备注册函数platform_device_register(&led_device),我们先看参数led_device:

struct platform_device led_device = {
.name		= "led",
};


led_device是一个platform_device类型的结构体,platform_device结构体的定义在linux-3.4\include\linux\platform_device.h中:

struct platform_device {
const char	* name;//设备名
int		id;//设备id
struct device	dev;//包含设备结构体
u32		num_resources;//资源个数
struct resource	* resource;//资源结构体

const struct platform_device_id	*id_entry;

/* MFD cell pointer */
struct mfd_cell *mfd_cell;

/* arch specific additions */
struct pdev_archdata	archdata;
};


这个结构体里面封装了device结构体,resource结构体,说明platform_device是device结构体派生出的一个结构体,platform_device是一个特殊的device。下面来看platform_device中最重要的结构体resource结构体,该结构体位于linux-3.4\include\linux\ioprt.h中:

struct resource {
resource_size_t start;//资源起始地址
resource_size_t end;//资源结束地址
const char *name;//定义资源名称
unsigned long flags;//定义资源类型
struct resource *parent, *sibling, *child;//资源树
};


这个结构体表明了设备所拥有的资源。

platform_device结构体包含了device结构体,device结构体描述了设备的详细情况,在面向对象编程中device是所有设备的基类。device结构体在platform总线初始化的时候已经说过了,这里就不说了。

然后我们来看一下platform_device_register 函数,其位于linux-3.4\drivers\base\platform.c中:

/**
* 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);
arch_setup_pdev_archdata(pdev);
return platform_device_add(pdev);
}
EXPORT_SYMBOL_GPL(platform_device_register);
这里首先对设备使用函数device_initalize进行初始化,这个函数在上面platform总线初始化的时候已经说过,这里不说了。在这是不是可以发散一下,只要设备注册进内核,无论是总线设备也好,其他设备也好,内核都把他们看成设备,使用同样的方式初始化。

然后使用arch_setup_pdev_archdata(pdev),这个函数位于linux-3.4\drivers\base\platform.c中:

/**
* arch_setup_pdev_archdata - Allow manipulation of archdata before its used
* @pdev: platform device
*
* This is called before platform_device_add() such that any pdev_archdata may
* be setup before the platform_notifier is called.  So if a user needs to
* manipulate any relevant information in the pdev_archdata they can do:
*
* 	platform_devic_alloc()
* 	... manipulate ...
* 	platform_device_add()
*
* And if they don't care they can just call platform_device_register() and
* everything will just work out.
*/
void __weak arch_setup_pdev_archdata(struct platform_device *pdev)
{
}


这个函数目前是一个空函数,根据注释,这是留给使用者在添加设备前来操作设备相关结构体的,也就是留给用户根据需要使用的。

最后使用了platform_device_add(pdev)来添加设备。该函数位于linux-3.4\drivers\base\platform.c中:

/**
* 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;

<span style="color:#ff0000;">pdev->dev.bus = &platform_bus_type;</span>

if (pdev->id != -1)
dev_set_name(&pdev->dev, "%s.%d", pdev->name,  pdev->id);
else
dev_set_name(&pdev->dev, "%s", pdev->name);

for (i = 0; i < pdev->num_resources; i++) {
struct resource *p, *r = &pdev->resource[i];

if (r->name == NULL)
r->name = dev_name(&pdev->dev);

p = r->parent;
if (!p) {
if (resource_type(r) == IORESOURCE_MEM)
p = &iomem_resource;
else if (resource_type(r) == IORESOURCE_IO)
p = &ioport_resource;
}

if (p && <span style="color:#ff0000;">insert_resource(p, r)</span>) {
printk(KERN_ERR
"%s: failed to claim resource %d\n",
dev_name(&pdev->dev), i);
ret = -EBUSY;
goto failed;
}
}

pr_debug("Registering platform device '%s'. Parent at %s\n",
dev_name(&pdev->dev), dev_name(pdev->dev.parent));

ret = <span style="color:#ff0000;">device_add(&pdev->dev);</span>
if (ret == 0)
return ret;

failed:
while (--i >= 0) {
struct resource *r = &pdev->resource[i];
unsigned long type = resource_type(r);

if (type == IORESOURCE_MEM || type == IORESOURCE_IO)
release_resource(r);
}

return ret;
}
EXPORT_SYMBOL_GPL(platform_device_add);


该函数有三个重点,一个是设备如果没有父设备,则把platform_bus设置为其父设备,一个是插入资源,还有一个是调用了device_add来添加设备。这里说明device_register和platform_device_register有很多相似之处。

下面我们回到led_init函数继续往下看驱动注册函数platform_driver_register(&led_driver),还是先看参数led_driver:

static struct platform_driver led_driver = {
.probe		= led_probe,
.remove		= __devexit_p(led_remove),
.driver		= {
.name	= "led",
.owner	= THIS_MODULE,
},
};


led_driver是一个platform_driver类型的结构体,里面主要是指向了一些操作函数。我们来看一下platform_driver结构体,其位于linux-3.4\include\linux\platform_device.h中:

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 (*resume)(struct platform_device *);
struct device_driver driver;
const struct platform_device_id *id_table;
};
该结构体主要包含了设备操作的一些函数,并且包含了device_driver结构体,用面向对象的思想说明platform_driver继承了device_driver结构体。也即是device_driver结构体派生了platform_driver结构体,device_driver是platform_driver的基类。结构体device_driver位于linux-3.4\include\linux中:

struct device_driver {
const char		*name;
struct bus_type		*bus;

struct module		*owner;
const char		*mod_name;	/* used for built-in modules */

bool suppress_bind_attrs;	/* disables bind/unbind via sysfs */

const struct of_device_id	*of_match_table;

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);
const struct attribute_group **groups;

const struct dev_pm_ops *pm;

struct driver_private *p;
};
该结构体包含了设备驱动的相关数据,比如设备驱动名称,总线类型,拥有者,操作函数等等。

这里最重要的俩变量name和owner,name的主要作用是把platform驱动和对应的platform设备连接起来,在platform_device结构体里也存在name成员。只有这两个name的名称一样才能成功注册设备的驱动。owner的作用是说明驱动的所有者,通常初始化为THIS_MODULE。

我们接下来看platform设备驱动注册函数platform_driver_register,该函数位于linux-3.4\drivers\base\platform.c中:

/**
* platform_driver_register - register a driver for platform-level devices
* @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;

return driver_register(&drv->driver);
}
EXPORT_SYMBOL_GPL(platform_driver_register);


该函数首先声明定义自己所挂载的总线类型,这一点很重要,因为platform_driver和platform_device都是挂载到platform_bus中,platform_driver和platform_device是通过platform_bus_type中注册的回调函数platform_match来完成的,所以一定要先注册设备再注册驱动,否则无法匹配成功,驱动也就无法使用;然后给探测(probe),移除(remove),关闭(shutdown)函数指针赋值,最后使用driver_register函数进行设备的驱动注册。我们来看一下driver_register函数,其在linux-3.4\drivers\base\driver.c中:

/**
* 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)
{
int ret;
struct device_driver *other;

BUG_ON(!drv->bus->p);

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);

other = driver_find(drv->name, drv->bus);
if (other) {
printk(KERN_ERR "Error: Driver '%s' is already registered, "
"aborting...\n", drv->name);
return -EBUSY;
}

ret = bus_add_driver(drv);
if (ret)
return ret;
ret = driver_add_groups(drv, drv->groups);
if (ret)
bus_remove_driver(drv);
return ret;
}
EXPORT_SYMBOL_GPL(driver_register);


首先如果总线的方法和设备自己的方法同时存在,则打印警告信息。如果设备驱动已经注册,则返回-EBUSY,否则使用bus_add_driver(drv)向总线添加驱动。

下面来看一下bus_add_driver函数,这个函数位于linux-3.4\drivers\base\bus.c中:

/**
* bus_add_driver - Add a driver to the bus.
* @drv: driver.
*/
int bus_add_driver(struct device_driver *drv)
{
struct bus_type *bus;
struct driver_private *priv;
int error = 0;

bus = bus_get(drv->bus);
if (!bus)
return -EINVAL;

pr_debug("bus: '%s': add driver %s\n", bus->name, drv->name);

priv = kzalloc(sizeof(*priv), GFP_KERNEL);
if (!priv) {
error = -ENOMEM;
goto out_put_bus;
}
klist_init(&priv->klist_devices, NULL, NULL);
priv->driver = drv;
drv->p = priv;
priv->kobj.kset = bus->p->drivers_kset;
error = kobject_init_and_add(&priv->kobj, &driver_ktype, NULL,
"%s", drv->name);
if (error)
goto out_unregister;

<span style="color:#ff0000;">if (drv->bus->p->drivers_autoprobe) {
error = driver_attach(drv);
if (error)
goto out_unregister;
}</span>
klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers);
module_add_driver(drv->owner, drv);

error = driver_create_file(drv, &driver_attr_uevent);
if (error) {
printk(KERN_ERR "%s: uevent attr (%s) failed\n",
__func__, drv->name);
}
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",
__func__, drv->name);
}

if (!drv->suppress_bind_attrs) {
error = add_bind_files(drv);
if (error) {
/* Ditto */
printk(KERN_ERR "%s: add_bind_files(%s) failed\n",
__func__, drv->name);
}
}

kobject_uevent(&priv->kobj, KOBJ_ADD);
return 0;

out_unregister:
kobject_put(&priv->kobj);
kfree(drv->p);
drv->p = NULL;
out_put_bus:
bus_put(bus);
return error;
}


从上面的红线部分,如果驱动是自动probe的话,将调用driver_attach来绑定设备和驱动。函数driver_attach位于linux-3.4\drivers\base\dd.c中:

/**
* 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);
}
EXPORT_SYMBOL_GPL(driver_attach);


这里可以看到,调用了bus_for_each_dev函数,该函数位于linux-3.4\drivers\based\bus.c中:

/**
* 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 so, 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 || !bus->p)
return -EINVAL;

klist_iter_init_node(&bus->p->klist_devices, &i,
(start ? &start->p->knode_bus : NULL));
while ((dev = next_device(&i)) && !error)
error = fn(dev, data);
klist_iter_exit(&i);
return error;
}
EXPORT_SYMBOL_GPL(bus_for_each_dev);


这里可以发现该函数是遍历总线上的每一个设备,并调用了函数fn,,这里的fn函数就是__driver__attach函数。我们来看一下__driver__attach函数,该函数位于linux-3.4\drivers\base\dd.c中:

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 (!driver_match_device(drv, dev))
return 0;

if (dev->parent)	/* Needed for USB */
device_lock(dev->parent);
device_lock(dev);
if (!dev->driver)
<span style="color:#ff0000;">driver_probe_device(drv, dev);</span>
device_unlock(dev);
if (dev->parent)
device_unlock(dev->parent);

return 0;
}


这里首先是driver 匹配device,然后调用了driver_probe_device函数,该函数位于linux-3.4\drivers\base\dd.c中:

/**
* 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
*
* This function returns -ENODEV if the device is not registered,
* 1 if the device is bound successfully and 0 otherwise.
*
* This function must be called with @dev lock held.  When called for a
* USB interface, @dev->parent lock 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;

pr_debug("bus: '%s': %s: matched device %s with driver %s\n",
drv->bus->name, __func__, dev_name(dev), drv->name);

pm_runtime_get_noresume(dev);
pm_runtime_barrier(dev);
ret =<span style="color:#ff0000;"> really_probe(dev, drv);</span>
pm_runtime_put_sync(dev);

return ret;
}


这里首先检测了设备是否被注册,然后调用了really_probe函数,这个函数位于linux-3.4\drivers\base\dd.c中:

static int really_probe(struct device *dev, struct device_driver *drv)
{
int ret = 0;

atomic_inc(&probe_count);
pr_debug("bus: '%s': %s: probing driver %s with device %s\n",
drv->bus->name, __func__, drv->name, dev_name(dev));
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",
__func__, dev_name(dev));
goto probe_failed;
}

if (dev->bus->probe) {
ret = dev->bus->probe(dev);
if (ret)
goto probe_failed;
} else if (drv->probe) {
<span style="color:#ff0000;">ret = drv->probe(dev);</span>
if (ret)
goto probe_failed;
}

driver_bound(dev);
ret = 1;
pr_debug("bus: '%s': %s: bound device %s to driver %s\n",
drv->bus->name, __func__, dev_name(dev), drv->name);
goto done;

probe_failed:
devres_release_all(dev);
driver_sysfs_remove(dev);
dev->driver = NULL;

if (ret == -EPROBE_DEFER) {
/* Driver requested deferred probing */
dev_info(dev, "Driver %s requests probe deferral\n", drv->name);
driver_deferred_probe_add(dev);
} else 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_name(dev), ret);
} else {
pr_debug("%s: probe of %s rejects match %d\n",
drv->name, dev_name(dev), 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->probe(dev),而这个就是我们定义platform_driver结构体里声明的probe函数,在驱动led.c中就是函数led_probe函数:

//led_probe
static int __devinit led_probe(struct platform_device *pdev)
{
int led_used;
script_item_u	val;
script_item_value_type_e  type;

int err;

printk("led_para!\n");
type = script_get_item("led_para", "led_used", &val);
if (SCIRPT_ITEM_VALUE_TYPE_INT != type) {
printk("%s script_get_item \"led_para\" led_used = %d\n",
__FUNCTION__, val.val);
return -1;
}
led_used = val.val;
printk("%s script_get_item \"led_para\" led_used = %d\n",
__FUNCTION__, val.val);

if(!led_used) {
printk("%s led_used is not used in config,  led_used=%d\n", __FUNCTION__,led_used);
return -1;
}

err = led_gpio();
if (err)
return -1;

sema_init(&lock, 1);
err = misc_register(&leds_dev);
printk("======= cqa83 led initialized ================\n");

return err;
}
到这里,platform设备的设备和驱动初始化和绑定,探测就结束了,其实也意味着驱动已经设备和注册成功了。

我觉得有一点还是要说一下,那就是设备和驱动的匹配,在驱动注册是会回调总线注册的匹配函数platform_match,该函数位于linux-3.4\drivers\base\platform.c中:

/**
* 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 = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);

/* Attempt an OF style match first */
if (of_driver_match_device(dev, drv))
return 1;

/* Then try to match against the id table */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;

/* fall-back to driver name match */
<span style="color:#ff0000;">return (strcmp(pdev->name, drv->name) == 0);</span>
}


这里的匹配就是通过字符串对比来进行的,这就是开始说的platform_device的name成员和platform_driver的name成员要一样。

在这里简单总结一下,platform设备加载驱动的过程。首先有一个platform总线,这个总线呢会在系统初始化的时候对其进行初始化。在总线初始化完成之后,如果你想要往总线上挂载platform设备,那么这个要分为两部分,一是设备,二是驱动,也即是片platform_device和platform_driver,这两个都是要挂载到platform总线上。但是挂载有一个顺序,一定要先挂载设备,再挂载驱动,因为驱动是遍历总线上所有的设备节点来匹配的。那么这俩东西靠什么来匹配呢?他们靠的是其结构体下的name成员变量,如果名字一样才能匹配成功,这也就是为什么要求platform_device和platform_driver的名字要一样的原因了。

platform_device结构体提供的是资源,而platform_driver结构体提供的是操作,也就是驱动操作设备。platform_driver主要完成了设备的注册和初始化,还有移除是的资源释放等。在驱动led.c中很容易可以看出来,led_probe调用了led_gpio函数。到platform设备驱动加载完成,其实是在目录/dev/platform下会出现你的设备。然而这并不能做什么,但是Linux里有一句话“一切皆文件”,设备也是文件。那么这些完成之后,下面就是文件操作了。

一个问题是,我们的应用程序如何去使用驱动程序中的函数?比如打开设备,关闭设备,使用设备等等。这里就是说对应用程序来说需要一个入口,一个可以通过驱动程序控制设备的入口。这里就引入了一个重要的数据结构file_operrations,这个结构体包含了一组函数指针,这些指针所指向的函数就是用来操作设备的。

这个结构体位于linux-3.4\include\linux\fs.h中:

struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
};


那么我们来对比一下驱动代码led.c中的file_operations结构体:

//file_operations
static struct file_operations leds_ops = {
.owner			= THIS_MODULE,
.open			= led_open,
.release		= led_close,
.unlocked_ioctl		= led_ioctl,
};


这里只定义了open函数,release函数,unlocked_ioctl函数,并且定义了其拥有者是THIS_MODULE。这几个函数的源代码分别是:

//led_open
static int led_open(struct inode *inode, struct file *file)
{
if (!down_trylock(&lock))
return 0;
else
return -EBUSY;
}


led_open函数是开始时对设备加锁,防止多应用程序访问。

//led_close
static int  led_close(struct inode *inode, struct file *file)
{
up(&lock);
return 0;
}


led_close函数是设备使用完之后对设备进行解锁方便其他程序使用。

//led_ioctl
static long  led_ioctl(struct file *filep, unsigned int cmd,
unsigned long arg)
{
unsigned int n;
n = (unsigned int)arg;
switch (cmd) {
case LED_IOCTL_SET_ON:
if (n < 1)
return -EINVAL;
if(led_val[n-1].gpio.gpio != -1) {
__gpio_set_value(led_val[n-1].gpio.gpio, 1);
printk("led%d on !\n", n);
}
break;

case LED_IOCTL_SET_OFF:
default:
if (n < 1)
return -EINVAL;
if(led_val[n-1].gpio.gpio != -1) {
__gpio_set_value(led_val[n-1].gpio.gpio, 0);
printk("led%d off !\n", n);
}
break;
}

return 0;
}


led_ioctl里面主要是对设备的控制,这个在前面已经分析过了,这里不再进行分析。

到此呢,驱动也加载了,应用程序也有了入口,但是还有一个重要问题没有说,那就是它是何时加载到驱动的呢?

我们知道Linux系统驱动加载一般是两种方式,一个是编译成ko模块加载,一个是编译进内核,系统启动时自动加载。模块加载有两种方式,一个是手动加载,一个是使用脚本在系统启动时加载,但是这两种方式都会使用到mknod,insmod命令等。

那么这里是怎么加载的呢?我们先查看其系统启动的配置文件init.sun8i.rc,里面关于led的启动设置是这样的

# led
chmod 777 /dev/led
而不像lcd,lcd是这样的:

# lcd
insmod /system/vendor/modules/disp.ko
insmod /system/vendor/modules/hdmi.ko


led的配置并没有使用到insmod名令,并且在Android设备中也找不到其相应的ko文件。所以说这个led驱动应该是静态编译进内核的。一般字符驱动设备静态编译进内核都会在相应的makefile中加入mknod命令来创建节点。那么我们就去led.c对应的makefile中找一下:

然而其关于led的只有:

obj-$(CONFIG_SUNXI_LED)		+= led.o


并没有mknod命令。那这到底是咋回事呢?

其实我们再回到led.c代码中的led_probe函数:

//led_probe
static int __devinit led_probe(struct platform_device *pdev)
{
int led_used;
script_item_u	val;
script_item_value_type_e  type;

int err;

printk("led_para!\n");
type = script_get_item("led_para", "led_used", &val);
if (SCIRPT_ITEM_VALUE_TYPE_INT != type) {
printk("%s script_get_item \"led_para\" led_used = %d\n",
__FUNCTION__, val.val);
return -1;
}
led_used = val.val;
printk("%s script_get_item \"led_para\" led_used = %d\n",
__FUNCTION__, val.val);

if(!led_used) {
printk("%s led_used is not used in config,  led_used=%d\n", __FUNCTION__,led_used);
return -1;
}

err = led_gpio();
if (err)
return -1;

sema_init(&lock, 1);
<span style="background-color: rgb(255, 0, 0);">err = misc_register(&leds_dev);</span>
printk("======= cqa83 led initialized ================\n");

return err;
}


来看红色的部分,再看参数leds_dev:

//miscdevice
static struct miscdevice leds_dev = {
.minor = MISC_DYNAMIC_MINOR,
.name = "led",
.fops = &leds_ops,
};


到这里是否明白了呢?

这个led设备驱动呢是一个杂项字符驱动,这就是我开始说的那三点中的一点。那这个有什么关系呢?

misc_device是特殊字符设备。注册驱动程序时采用misc_register函数注册,此函数中会自动创建设备节点,即设备文件。无需mknod指令创建设备文件。

因为misc_register()会调用class_device_creat或者device_creat().

关于杂项字符设备网上有很多资料,大家可以查一下。我会在后面的博文中写一篇来说杂项字符设备。

到这里我们解决了没有mknod的疑问,但是我们是如何配置才能把驱动编译进内核呢?

可以这样做,在内核源代码目录下执行make menuconfig命令,这会弹出一个对话界面。找到对应的驱动,然后用空格把前面的尖括号里变为*号,然后保存退出。编译系统就可以了。由于我的电脑不能截屏,就不能给大家上图了,抱歉。不过网上有很多资料。我下面会给出连接。

到这里这个驱动的分析基本上就完成了。但是还有俩问题需要另外来写一下,一个是LinuxGPIO驱动模型,一个是杂项字符设备。

我参考了很多网上朋友的作品,但是由于这个文章写了好多天,可能有的连接没有及时保存,在这里表示抱歉,也在此表示真心的感谢,感谢大家的分享:
http://blog.csdn.net/chocolate001/article/details/7572203 http://blog.csdn.net/zjg555543/article/details/7420650 http://blog.sina.com.cn/s/blog_966f8e8501010xhw.html http://www.cnblogs.com/geneil/archive/2011/12/03/2272869.html http://www.cnblogs.com/myblesh/articles/2367520.html http://www.embedu.org/Column/Column425.htm http://blog.csdn.net/weiqing1981127/article/details/8245665 http://blog.csdn.net/liuhaoyutz/article/details/15504127 http://blog.csdn.net/qingfengtsing/article/details/19211021 http://blog.chinaunix.net/uid-26285146-id-3307147.html http://blog.csdn.net/chocolate001/article/details/7572203 http://blog.csdn.net/gdt_a20/article/details/6429451 http://blog.chinaunix.net/uid-20729605-id-1884305.html http://www.51hei.com/bbs/dpj-30117-1.html http://blog.chinaunix.net/uid-20729605-id-1884305.html http://blog.csdn.net/abo8888882006/article/details/5424363 http://blog.csdn.net/engerled/article/details/6237884 http://blog.csdn.net/cppgp/article/details/6333359 http://blog.csdn.net/engerled/article/details/6243891 http://blog.csdn.net/cug_fish_2009/article/details/6518856 http://blog.sina.com.cn/s/blog_7943319e01018m3w.html http://blog.chinaunix.net/uid-26694208-id-3128890.html http://www.cnblogs.com/Daniel-G/archive/2013/08/27/3284791.html http://blog.chinaunix.net/uid-20769502-id-147170.html
等等。

再次表示感谢!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息