http://blog.chinaunix.net/u3/92745/showart_2145668.html
LINUX设备驱动之设备模型一--kobject

LINUX设备驱动驱动程序模型的核心数据结构是kobject,kobject数据结构在\linux\kobject.h中定义:

struct kobject {

const char             *name;

struct list_head       entry;

struct kobject         *parent;

struct kset             *kset;

struct kobj_type     *ktype;

struct sysfs_dirent  *sd;

struct kref             kref;

unsigned int state_initialized:1;

unsigned int state_in_sysfs:1;

unsigned int state_add_uevent_sent:1;

unsigned int state_remove_uevent_sent:1;

unsigned int uevent_suppress:1;

};

每个kobject都有它的父节点parent、kset、kobj_type指针,这三者是驱动模型的基本结构,kset是kobject的集合,在\linux\kobject.h中定义:

struct kset {

struct list_head list;

spinlock_t list_lock;

struct kobject kobj;

struct kset_uevent_ops *uevent_ops;

};

可以看到每个kset内嵌了一个kobject(kobj字段),用来表示其自身节点,其list字段指向了所包含的kobject的链表头。我们在后面的分析中将看到kobject如果没有指定父节点,parent将指向其kset内嵌的kobject。

每个kobject都有它的kobj_type字段指针,用来表示kobject在文件系统中的操作方法,kobj_type结构也在\linux\kobject.h中定义:

struct kobj_type {

void (*release)(struct kobject *kobj);

struct sysfs_ops *sysfs_ops;

struct attribute ** default_attrs;

};

release方法是在kobject释放是调用,sysfs_ops指向kobject对应的文件操作,default_attrskobject的默认属性,sysfs_ops的将使用default_attrs属性(在后面的分析中我们将会看到)。

从上面的分析我们可以想象到kobject、kset、kobj_type的层次结构:

500)this.width=500;" width="500" border="0">

我们可以把一个kobject添加到文件系统中去(实际上是添加到其父节点所代表的kset中去),内核提供kobject_create_and_add()接口函数:

struct kobject *kobject_create_and_add(const char *name, struct kobject *parent)

{

struct kobject *kobj;

int retval;

kobj = kobject_create();

if (!kobj)

return NULL;

retval = kobject_add(kobj, parent, "%s", name);

if (retval) {

printk(KERN_WARNING "%s: kobject_add error: %d\n",

__func__, retval);

kobject_put(kobj);

kobj = NULL;

}

return kobj;

}

kobject _create()为要创建的kobject分配内存空间并对其初始化。

struct kobject *kobject_create(void)

{

struct kobject *kobj;

kobj = kzalloc(sizeof(*kobj), GFP_KERNEL);

if (!kobj)

return NULL;

kobject_init(kobj, &dynamic_kobj_ktype);

return kobj;

}

kobject_init()对kobject基本字段进行初始化,用输入参数设置kobj_type属性。

这里粘出代码以供参考:

void kobject_init(struct kobject *kobj, struct kobj_type *ktype)

{

char *err_str;

if (!kobj) {

err_str = "invalid kobject pointer!";

goto error;

}

if (!ktype) {

err_str = "must have a ktype to be initialized properly!\n";

goto error;

}

if (kobj->state_initialized) {

/* do not error out as sometimes we can recover */

printk(KERN_ERR "kobject (%p): tried to init an initialized "

"object, something is seriously wrong.\n", kobj);

dump_stack();

}

kobject_init_internal(kobj);

kobj->ktype = ktype;

return;

error:

printk(KERN_ERR "kobject (%p): %s\n", kobj, err_str);

dump_stack();

}

static void kobject_init_internal(struct kobject *kobj)

{

if (!kobj)

return;

kref_init(&kobj->kref);

INIT_LIST_HEAD(&kobj->entry);

kobj->state_in_sysfs = 0;

kobj->state_add_uevent_sent = 0;

kobj->state_remove_uevent_sent = 0;

kobj->state_initialized = 1;

}

接着看kobject_add()函数:

int kobject_add(struct kobject *kobj, struct kobject *parent,

const char *fmt, ...)

{

va_list args;

int retval;

if (!kobj)

return -EINVAL;

if (!kobj->state_initialized) {

printk(KERN_ERR "kobject '%s' (%p): tried to add an "

"uninitialized object, something is seriously wrong.\n",

kobject_name(kobj), kobj);

dump_stack();

return -EINVAL;

}

va_start(args, fmt);

retval = kobject_add_varg(kobj, parent, fmt, args);

va_end(args);

return retval;

}

在上面的初始化中已把位变量设位1

va_start(args, fmt)和va_end(args)使用可变参数(可见参数用法不在这里分析),在kobject_add_varg中将把fmt指向的内容赋给kobject的name字段。下面我们详细看看kobject_add_varg函数:

static int kobject_add_varg(struct kobject *kobj, struct kobject *parent,

const char *fmt, va_list vargs)

{

int retval;

retval = kobject_set_name_vargs(kobj, fmt, vargs);

if (retval) {

printk(KERN_ERR "kobject: can not set name properly!\n");

return retval;

}

kobj->parent = parent;

return kobject_add_internal(kobj);

}

kobject_set_name_vargs(kobj, fmt, vargs),如果kobj的name字段指向的内容为空,则为分配一个内存空间并用fmt指向的内容初始化,把地址赋给kobj的name字段。

int kobject_set_name_vargs(struct kobject *kobj, const char *fmt,

va_list vargs)

{

const char *old_name = kobj->name;

char *s;

if (kobj->name && !fmt)

return 0;

kobj->name = kvasprintf(GFP_KERNEL, fmt, vargs);

if (!kobj->name)

return -ENOMEM;

/* ewww... some of these buggers have '/' in the name ... */

while ((s = strchr(kobj->name, '/')))

s[0] = '!';

kfree(old_name);

return 0;

}

char *kvasprintf(gfp_t gfp, const char *fmt, va_list ap)

{

unsigned int len;

char *p;

va_list aq;

va_copy(aq, ap);

len = vsnprintf(NULL, 0, fmt, aq);

va_end(aq);

p = kmalloc(len+1, gfp);

if (!p)

return NULL;

vsnprintf(p, len+1, fmt, ap);

return p;

}

继续kobject_add_varg()返回kobject_add_internal(kobj),就是在这个函数理为kobj创建文件系统结构:

static int kobject_add_internal(struct kobject *kobj)

{

int error = 0;

struct kobject *parent;

if (!kobj)

return -ENOENT;

if (!kobj->name || !kobj->name[0]) {

WARN(1, "kobject: (%p): attempted to be registered with empty "

"name!\n", kobj);

return -EINVAL;

}

检查kobj和它的name字段,不存在则返回错误信息。

parent = kobject_get(kobj->parent);

获得其父节点,并增加父节点的计数器,kobject结构中的kref字段用于容器的计数,kobject_get和kobject_put分别增加和减少计数器,如果计数器为0,则释放该kobject,kobject_get返回该kobject。

/* join kset if set, use it as parent if we do not already have one */

if (kobj->kset) {

if (!parent)

parent = kobject_get(&kobj->kset->kobj);

kobj_kset_join(kobj);

kobj->parent = parent;

}

在这里我们可以看到,如果调用kobject_create_and_add()时参数parent设为NULL,则会去检查kobj的kset是否存在,如果存在就会把kset所嵌套的kobj作为其父节点,并把kobj添加到kset中去。

pr_debug("kobject: '%s' (%p): %s: parent: '%s', set: '%s'\n",

kobject_name(kobj), kobj, __func__,

parent ? kobject_name(parent) : "<NULL>",

kobj->kset ? kobject_name(&kobj->kset->kobj) : "<NULL>");

打印一些调试信息,接着为kobj创建目录:

error = create_dir(kobj);

if (error) {

kobj_kset_leave(kobj);

kobject_put(parent);

kobj->parent = NULL;

/* be noisy on error issues */

if (error == -EEXIST)

printk(KERN_ERR "%s failed for %s with "

"-EEXIST, don't try to register things with "

"the same name in the same directory.\n",

__func__, kobject_name(kobj));

else

printk(KERN_ERR "%s failed for %s (%d)\n",

__func__, kobject_name(kobj), error);

dump_stack();

} else

kobj->state_in_sysfs = 1;

return error;

}

如果创建不成功,则回滚上面的操作,成功的话则设置kobj的state_in_sysfs标志。

在看看create_dir()函数中具体创建了那些内容:

static int create_dir(struct kobject *kobj)

{

int error = 0;

if (kobject_name(kobj)) {

error = sysfs_create_dir(kobj);

if (!error) {

error = populate_dir(kobj);

if (error)

sysfs_remove_dir(kobj);

}

}

return error;

}

sysfs_create_dir()先为kobj创建了一个目录文件

int sysfs_create_dir(struct kobject * kobj)

{

struct sysfs_dirent *parent_sd, *sd;

int error = 0;

BUG_ON(!kobj);

if (kobj->parent)

parent_sd = kobj->parent->sd;

else

parent_sd = &sysfs_root;

error = create_dir(kobj, parent_sd, kobject_name(kobj), &sd);

if (!error)

kobj->sd = sd;

return error;

}

如果kobj->parent为NULL,就把&sysfs_root作为父节点sd,即在/sys下面创建结点。

然后调用populate_dir:

static int populate_dir(struct kobject *kobj)

{

struct kobj_type *t = get_ktype(kobj);

struct attribute *attr;

int error = 0;

int i;

if (t && t->default_attrs) {

for (i = 0; (attr = t->default_attrs[i]) != NULL; i++) {

error = sysfs_create_file(kobj, attr);

if (error)

break;

}

}

return error;

}

得到kobj的kobj_type,历遍kobj_type的default_attrs并创建属性文件,文件的操作会回溯到sysfs_ops的show和store会调用封装了attribute的kobj_attribute结构的store和show方法(在后面的代码中将会分析)。

由于上面kobject_init(kobj, &dynamic_kobj_ktype)用默认dynamic_kobj_ktype作为kobj_type参数,而dynamic_kobj_ktype的default_attrs为NULL,所以这里没有创建属性文件。

至此,我们已经知道了kobject_create_and_add()函数创建kobject,挂到父kobject,并设置其kobj_type,在文件系统中为其创建目录和属性文件等。

另外,如果我们已静态定义了要创建的kobject,则可以调用kobject_init_and_add()来注册kobject,其函数如下:

int kobject_init_and_add(struct kobject *kobj, struct kobj_type *ktype,

struct kobject *parent, const char *fmt, ...)

{

va_list args;

int retval;

kobject_init(kobj, ktype);

va_start(args, fmt);

retval = kobject_add_varg(kobj, parent, fmt, args);

va_end(args);

return retval;

}

通过上面的分析我们很轻松就能理解这个函数。

内核提供注销kobject的函数是kobject_del()

void kobject_del(struct kobject *kobj)

{

if (!kobj)

return;

sysfs_remove_dir(kobj);

kobj->state_in_sysfs = 0;

kobj_kset_leave(kobj);

kobject_put(kobj->parent);

kobj->parent = NULL;

}

删除kobj目录及其目录下的属性文件,清kobj的state_in_sysfs标志,把kobj从kset中删除,减少kobj->parent的计数并设其指针为空。

LINUX设备驱动之设备模型二--kset

我们已经知道了kset内嵌了kobject来表示自身的节点,创建kset就要完成其内嵌kobject,注册kset时会产生一个事件,事件而最终会调用uevent_ops字段指向结构中的函数,这个事件是通过用户空间的hotplug程序处理。下面我们一步一步分析。

内核同样提供了创建和注册kset的函数kset_create_and_add()

struct kset *kset_create_and_add(const char *name,

struct kset_uevent_ops *uevent_ops,

struct kobject *parent_kobj)

{

struct kset *kset;

int error;

kset = kset_create (name, uevent_ops, parent_kobj);

if (!kset)

return NULL;

error = kset_register(kset);

if (error) {

kfree(kset);

return NULL;

}

return kset;

}

输入参数有一个kset_uevent_ops类型的结构变量,其结构包含三个函数指针,我们在后面的分析到这三个函数在什么时候被调用,kset_uevent_ops结构定义如下:

struct kset_uevent_ops {

int (*filter)(struct kset *kset, struct kobject *kobj);

const char *(*name)(struct kset *kset, struct kobject *kobj);

int (*uevent)(struct kset *kset, struct kobject *kobj,

struct kobj_uevent_env *env);

};

继续看上面的函数,先调用kset_create ()创建一个kset,接着调用kset_register()注册它。

static struct kset *kset_create(const char *name,

struct kset_uevent_ops *uevent_ops,

struct kobject *parent_kobj)

{

struct kset *kset;

int retval;

kset = kzalloc(sizeof(*kset), GFP_KERNEL);

if (!kset)

return NULL;

retval = kobject_set_name(&kset->kobj, name);

if (retval) {

kfree(kset);

return NULL;

}

kset->uevent_ops = uevent_ops;

kset->kobj.parent = parent_kobj;

/*

* The kobject of this kset will have a type of kset_ktype and belong to

* no kset itself.  That way we can properly free it when it is

* finished being used.

*/

kset->kobj.ktype = &kset_ktype;

kset->kobj.kset = NULL;

return kset;

}

为kset分配内存,如我们上面分析,初始化了kset内嵌的kobject(这里还未将kobject注册到文件系统),另外用输入参数初始化kset的uevent_ops字段。

接着看kset的注册函数kset_register():

int kset_register(struct kset *k)

{

int err;

if (!k)

return -EINVAL;

kset_init(k);

err = kobject_add_internal(&k->kobj);

if (err)

return err;

kobject_uevent(&k->kobj, KOBJ_ADD);

return 0;

}

在这里终于看到调用kobject_add_internal()将kset内嵌的kobject注册到文件系统,这个函数我们在上面已经分析。

我们上面说到注册kset会产生一个事件,就是在这里调用了kobject_uevent(&k->kobj, KOBJ_ADD)

kobject_uevent()在\lib\ kobject_uevent.c中:

int kobject_uevent(struct kobject *kobj, enum kobject_action action)

{

return kobject_uevent_env(kobj, action, NULL);

}

转入kobject_uevent_env():

这个函数比较长,我们分段分析

int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,

char *envp_ext[])

{

struct kobj_uevent_env *env;

const char *action_string = kobject_actions[action];

const char *devpath = NULL;

const char *subsystem;

struct kobject *top_kobj;

struct kset *kset;

struct kset_uevent_ops *uevent_ops;

u64 seq;

int i = 0;

int retval = 0;

pr_debug("kobject: '%s' (%p): %s\n",

kobject_name(kobj), kobj, __func__);

/* search the kset we belong to */

top_kobj = kobj;

while (!top_kobj->kset && top_kobj-> parent)

top_kobj = top_kobj->parent;

if (!top_kobj->kset) {

pr_debug("kobject: '%s' (%p): %s: attempted to send uevent "

"without kset!\n", kobject_name(kobj), kobj,

__func__);

return -EINVAL;

}

kset = top_kobj->kset;

uevent_ops = kset-> uevent_ops;

如果如果kobj的kset和parent字段都不存在,说明找不到所属kset,也就没有uevent_ops,不能产生事件,返回错误信息;相反则找到了存在kset的kobj或父kobject(依次往上找),并赋值给uevent_ops。

/* skip the event, if uevent_suppress is set*/

if (kobj-> uevent_suppress) {

pr_debug("kobject: '%s' (%p): %s: uevent_suppress "

"caused the event to drop!\n",

kobject_name(kobj), kobj, __func__);

return 0;

}

如果设置了uevent_suppress字段,说明不希望产生事件,忽略事件正确返回。注意驱动程序将在适当的地方产生改事件。

/* skip the event, if the filter returns zero. */

if (uevent_ops && uevent_ops->filter)

if (!uevent_ops->filter(kset, kobj)) {

pr_debug("kobject: '%s' (%p): %s: filter function "

"caused the event to drop!\n",

kobject_name(kobj), kobj, __func__);

return 0;

}

如果uevent_ops->filter返回0,同样忽略事件正确返回。

if (uevent_ops && uevent_ops->name)

subsystem = uevent_ops->name(kset, kobj);

else

subsystem = kobject_name(&kset->kobj);

if (!subsystem) {

pr_debug("kobject: '%s' (%p): %s: unset subsystem caused the "

"event to drop!\n", kobject_name(kobj), kobj,

__func__);

return 0;

}

获得子系统的名称,不存在则返回。

/* environment buffer */

env = kzalloc(sizeof(struct kobj_uevent_env), GFP_KERNEL);

if (!env)

return -ENOMEM;

分配一个kobj_uevent_env结构内存,用于存放环境变量的值。

/* complete object path */

devpath = kobject_get_path(kobj, GFP_KERNEL);

if (!devpath) {

retval = -ENOENT;

goto exit;

}

获得引发事件的kobject在sysfs中的路径。

/* default keys */

retval = add_uevent_var(env, "ACTION=%s", action_string);

if (retval)

goto exit;

retval = add_uevent_var(env, "DEVPATH=%s", devpath);

if (retval)

goto exit;

retval = add_uevent_var(env, "SUBSYSTEM=%s", subsystem);

if (retval)

goto exit;

/* keys passed in from the caller */

if (envp_ext) {

for (i = 0; envp_ext[i]; i++) {

retval = add_uevent_var(env, "%s", envp_ext[i]);

if (retval)

goto exit;

}

}

调用add_uevent_var()kobj_uevent_env填充action_string,kobject路径,子系统名称以及其他指定环境变量。

/* let the kset specific function add its stuff */

if (uevent_ops && uevent_ops->uevent) {

retval = uevent_ops->uevent(kset, kobj, env);

if (retval) {

pr_debug("kobject: '%s' (%p): %s: uevent() returned "

"%d\n", kobject_name(kobj), kobj,

__FUNCTION__, retval);

goto exit;

}

}

调用uevent_ops的uevent函数,编程人员可在此函数中实现自定义的功能。

/*

* Mark "add" and "remove" events in the object to ensure proper

* events to userspace during automatic cleanup. If the object did

* send an "add" event, "remove" will automatically generated by

* the core, if not already done by the caller.

*/

if (action == KOBJ_ADD)

kobj->state_add_uevent_sent = 1;

else if (action == KOBJ_REMOVE)

kobj->state_remove_uevent_sent = 1;

设置KOBJ_ADD和KOBJ_REMOVE的标志。

/* we will send an event, so request a new sequence number */

spin_lock(&sequence_lock);

seq = ++uevent_seqnum;

spin_unlock(&sequence_lock);

retval = add_uevent_var(env, "SEQNUM=%llu", (unsigned long long)seq);

if (retval)

goto exit;

#if defined(CONFIG_NET)

/* send netlink message */

if (uevent_sock) {

struct sk_buff *skb;

size_t len;

/* allocate message with the maximum possible size */

len = strlen(action_string) + strlen(devpath) + 2;

skb = alloc_skb(len + env->buflen, GFP_KERNEL);

if (skb) {

char *scratch;

/* add header */

scratch = skb_put(skb, len);

sprintf(scratch, "%s@%s", action_string, devpath);

/* copy keys to our continuous event payload buffer */

for (i = 0; i < env->envp_idx; i++) {

len = strlen(env->envp[i]) + 1;

scratch = skb_put(skb, len);

strcpy(scratch, env->envp[i]);

}

NETLINK_CB(skb).dst_group = 1;

retval = netlink_broadcast(uevent_sock, skb, 0, 1,

GFP_KERNEL);

/* ENOBUFS should be handled in userspace */

if (retval == -ENOBUFS)

retval = 0;

} else

retval = -ENOMEM;

}

#endif

/* call uevent_helper, usually only enabled during early boot */

if (uevent_helper[0]) {

char *argv [3];

argv [0] = uevent_helper;

argv [1] = (char *)subsystem;

argv [2] = NULL;

retval = add_uevent_var(env, "HOME=/");

if (retval)

goto exit;

retval = add_uevent_var(env,

"PATH=/sbin:/bin:/usr/sbin:/usr/bin");

if (retval)

goto exit;

添加HOME和PATH环境变量。

retval = call_usermodehelper(argv[0], argv,

env->envp, UMH_WAIT_EXEC);

}

exit:

kfree(devpath);

kfree(env);

return retval;

}

调用hotplug函数。

看一下kset_unregister()

void kset_unregister (struct kset *k)

{

if (!k)

return;

kobject_put(&k-> kobj);

}

减少其内嵌的kobj计数,为0则释放其内存空间。

已经分析完kobject和kset,linux的设备模型就是基于这两个数据结构的,在此基础上,后续将分析设备模型中的device、driver、和bus。

转载于:https://www.cnblogs.com/yuanfang/archive/2010/12/24/1916229.html

研究UEVENT相关东西,看到2篇优秀的博文,转载与此相关推荐

  1. 68页PPT教你撰写一篇优秀的机器学习研究论文!

    来源:新智元 本文共多图,建议阅读15分钟. 本文从研究人员.审稿人和读者的角度来探讨优秀论文的写作过程,值得细细品味. [ 导读 ]近期,Deepmind 和纽约大学研究科学家一起作了一场关于如何撰 ...

  2. Steve Lin:如何撰写一篇优秀的SIGGRAPH论文

     英文原版 PPT下载:http://vdisk.weibo.com/s/z7VKRh2i3R4YO 一篇优秀的论文应该是这样的 广大的研究同仁介绍了这篇论文所包含的重要想法和所获得的结果 在论文中描 ...

  3. 如何写好一篇优秀的硕士毕业论文

    论文写作对每一个大学生或者科学工作者来说,一种技能,也是一道难关.特别是对于硕士毕业生,论文是能否顺利毕业的关键.即使写作水平再出色.写作经验再丰富的人,在写作硕士毕业论文时也要尽心竭力.整理了一份硕 ...

  4. 一篇优秀的硕士毕业论文是怎么写成的

    论文写作对每一个大学生或者科学工作者来说,一种技能,也是一道难关.特别是对于硕士毕业生,论文是能否顺利毕业的关键.即使写作水平再出色.写作经验再丰富的人,在写作硕士毕业论文时也要尽心竭力.下面小编整理 ...

  5. 转计算机网络应用申请书,转专业相关申请书最新6篇

    高校大学生在入学后对录取专业的适应性较差,或者存在兴趣发生变化和偏移,都有可能使学生产生转专业的想法.以下是小编给大家整理的一些转专业相关申请书,希望对大家有所帮助,欢迎阅读与借鉴.觉得有用的话可以收 ...

  6. 研究Xposed相关二:如何root android模拟器(android4.3.1)

    从上一篇文章 研究Xposed相关一:Xposed框架的安装 了解了Xposed框架的安装过程,但是Xposed的使用有一个基础前提,那就是设备的Root权限已经获取的状态下:那这一篇文章就来讲述下如 ...

  7. 计算机图形学之光线跟踪算法的研究与实现2017年我的优秀毕业论文

    计算机图形学之光线跟踪算法的研究与实现2017年我的优秀毕业论文 版权所有使用者请联系我 刘创 QQ:903188593 2.2.2 Phong光照模型 事实上对于漫反射的物体表面,使用Lambert ...

  8. 如何将任意辣鸡话题写成一篇优秀的毕业论文——以本文为例

    [摘要]随着毕业季的临近,毕业论文已经令广大学生肝肠寸断.本文以科研的视角展开,具体从论文框架.语言技巧.作图风格.查重等多个方面论述如何将自己的辣鸡科研写成一篇优秀的毕业论文. [关键词] 毕业论文 ...

  9. 高等数学与计算机的关联论文,数学建模与计算机关系研究的相关论文(2)

    数学建模与计算机关系研究的相关论文篇二 <数学建模与计算机关系研究> [摘要]高等数学与计算机教学具有内在相关性,尤其是在数学建模应用中,根据计算机学科发展来发挥数学建模理论的作用及效果, ...

最新文章

  1. 软件开发 理想_我如何在12个月内找到理想的软件工作
  2. mmdetection训练出现:IndexError: list index out of range 错误
  3. [react] 说说你喜欢react的原因是什么?它有什么优缺点?
  4. python中构造方法的名字,【填空题】Python提供了名称为 的构造方法,实现让类的对象完成初始化。...
  5. 乔布斯不在了,世界一大步,苹果一小步。
  6. 大学生使用计算机趋势英语作文,学习使用电脑StudentUseofComputers
  7. ip dhcp snooping的设置
  8. 生成和解析二维码(zxing)
  9. scala学习笔记二----交互式shell编程使用
  10. Linux虚拟文件系统(安装根文件系统)
  11. sockscap+Bitvise Tunnelier 做内网渗透
  12. PS 图片清晰度
  13. python对excel中需要的数据的单元格填充颜色
  14. 跨平台,跨浏览器兼容性问题_跨浏览器兼容性检查的完整指南
  15. linux top命令看到的实存(RES)与虚存(VIRT)分析
  16. LeetCode笔记05:最长公共前缀
  17. PS图片素描化(画)
  18. python获取列表数字的下标
  19. 《Microsoft COCO Captions Data Collection and Evaluation Server》论文笔记
  20. 激活函数(1)Sigmoid激活函数

热门文章

  1. JS读取id和name的操作
  2. 博客园T恤内部泄漏版(附图)
  3. 'adb' 不是内部或外部命令,也不是可运行的程序或批处理文件
  4. android fragment 生命周期
  5. android 带清除功能的输入框控件
  6. WebKit 内核源码分析 (五)
  7. 魅族8.0系统手机最完美激活xposed框架的步骤
  8. [CQOI2009]叶子的染色
  9. Java | Python 流程控制对比
  10. nginx介绍(三) - 虚拟主机