[linux 内核] 一篇总结 linux 内核中 kobject、kset 和 ktype 的文章。
文章目录
- 一、kobject、kset、ktype
- (1-1)kobject
- (1-2)ktype
- (1-3)kset
- 二、kobject操作API
- (2-1)kobject_init()
- (2-2)kobject_add()
- (2-3)kobject_rename()
- (2-4)kobject_name()
- (2-5)kobject_get()
- (2-6)kobject_put()
- (2-7)kobject_del()
- (2-8)kobject_move()
- (2-9)kobject_uevent
- (2-10)kobject_uevent_env
- 三、引用计数
- 四、创建简单的kobject
- 五、移除kobject
- 六、kset提供的功能
一、kobject、kset、ktype
kobject
是内核中表示对象的基本结构,kset
是用于组织和管理 kobject
的集合,而 ktype
是用于定义 kobject
的类型。通过使用这三个概念,可以实现内核对象的管理、组织和操作,kobject
、kset
和ktype
是内核中核心基础结构,绚丽多彩的内核世界在这些基础结构上逐一构建。
(1-1)kobject
kobject
是struct kobject
类型的对象。kobjects有一个名称name和一个引用计数。kobject还有一个父指针(允许对象被安排到层次结构中),一个特定的类型ktype
,一个特定的对象集合kset
,以及一个是否在sysfs虚拟文件系统中的状态表示。struct kobject
是linux内核中的数据结构,用于表示内核对象(Kernel Object)。它是Linux设备模型的核心之一,用于表示内核中的各种实体,如设备、驱动程序、总线、类别等。
struct kobject
提供了一种统一的方式来管理内核中的对象。它允许内核开发人员创建具有层次结构的对象树,通过父子关系和链表关系连接不同的对象。通过与sysfs文件系统的集成,可以为每个内核对象创建相应的sysfs文件,使用户空间能够查看和修改对象的属性。
struct kobject
还提供了引用计数的机制,确保在对象不再被使用时能够正确释放相关资源。引用计数允许多个实体引用同一个内核对象,并在所有引用都被释放时自动销毁对象。kobject
通常被嵌入到其他结构中。
????注意:任何结构中只能嵌入一个kobject。
struct kobject
定义如下:
struct kobject {
const char *name;
struct list_head entry;
struct kobject *parent;
struct kset *kset;
struct kobj_type *ktype;
struct *sd;
struct kref kref;
#ifdef CONFIG_DEBUG_KOBJECT_RELEASE
struct delayed_work release;
#endif
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;
};
- name:内核对象的名称
- entry:用于将内核对象添加到父对象的子对象列表中。
- parent:指向父对象的指针,用于构建对象之间的层次关系。
- kset:指向所属的kset(kobject集合)的指针,用于进行对象管理。
- ktype:指向对象类型的指针,描述了对象的行为和属性。
- sd:指向sysfs目录实体(sysfs_dirent)的指针,用于与sysfs文件系统进行交互。
- kref:用于实现引用计数,确保在对象不再需要时能够正确释放资源。
(1-2)ktype
ktype
是嵌入在kobject
中的对象类型,用于描述struct kobject
的类型和行为。每个kobject结构都需要对应的ktype
,因为ktype
用于控制创建和销毁kobjec时发生的事情。
struct kobj_type {
void (*release)(struct kobject *kobj);
const struct sysfs_ops *sysfs_ops;
struct attribute **default_attrs;
const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
const void *(*namespace)(struct kobject *kobj);
};
-
release
:指向释放对象资源的函数指针。当对象的引用计数变为零时,将调用此函数来释放对象相关的资源。 -
sysfs_ops
:指向struct sysfs_ops
的指针,用于定义sysfs文件系统操作的回调函数。通过这些回调函数,可以自定义sysfs文件的读取、写入、属性创建等操作。 -
default_attrs
:指向struct attribute
指针数组的指针,表示对象的默认属性。这些属性将在对象的sysfs目录中显示,并允许用户空间访问和修改。 -
child_ns_type
:指向函数的指针,用于获取对象的子命名空间类型。如果对象具有子对象,并且这些子对象在命名空间上有不同的要求,可以通过此函数提供相应的命名空间类型操作。 -
namespace
:指向函数的指针,用于获取对象的命名空间标识。命名空间标识是一个不透明的指针,用于标识对象所属的命名空间。
struct kobj_type
用于描述struct kobject
的类型和行为,为每个struct kobject
实例提供相关的操作和属性定义。通过自定义struct kobj_type
,可以定制不同类型的内核对象,并指定相应的release
函数、属性、命名空间等。
在使用struct kobject
时,通常会创建一个自定义的struct kobj_type
实例,并将其与struct kobject
关联。这样可以为每个对象提供独立的类型和行为,并在必要时通过回调函数和属性操作与用户空间进行交互。
每个kobject都必须有一个release()
方法,并且该kobject必须持久化存在(保持一致的状态),直到release()
被调用。
(1-3)kset
kset
是一组kobject。这些kobject可以是相同的ktype,也可以属于不同的ktype。kset是kobject集合的基本容器类型。kset中也包含了一个自己的kobject
,但是可以忽略实现细节,因为kset核心代码会自动处理这个kobject。kset
如下定义:
struct kset {
//这个kset的所有kobject的列表
struct list_head list;
//用于遍历kobject的锁
spinlock_t list_lock;
//放在kset中的kobject
struct kobject kobj;
//kset的uevent操作集
const struct kset_uevent_ops *uevent_ops;
};
从根文件系统角度来看,当看到一个包含其他目录的sysfs
目录时,通常每个目录都对应同一个kset中的一个kobject。
总而言之,struct kset
用于表示内核对象的集合,它是一种特殊的struct kobject
,用于将相关的内核对象组织成逻辑上的集合。通过struct kset
,可以将一组具有相似特性或属性的内核对象归类并进行管理。
struct kset
对象通常作为父对象,包含一组子对象,这些子对象可以是不同类型的内核对象,但它们在逻辑上具有某种相关性。例如,设备驱动程序可以创建一个struct kset
对象,作为设备驱动程序的集合,每个具体的设备驱动程序都是其中的一个子对象。
struct kset
提供了一些常用的操作和功能,如添加和删除子对象、遍历子对象等。通过对struct kset
对象进行操作,可以很方便地管理和访问集合中的内核对象。
二、kobject操作API
kobject的接口位于/include/linux/kobject.h中,本小节总结常用的kobject
操作API。
(2-1)kobject_init()
void kobject_init(struct kobject *kobj, struct kobj_type *ktype);
-
kobj
:指向要初始化的内核对象的指针。 -
ktype
:指向描述该内核对象类型的kobj_type
结构体的指针。
ktype是创建kobject所必需的,因为每个kobject都必须有一个相关联的kobj_type。调用kobject_init()后,要向sysfs注册kobject,必须调用函数kobject_add()
(2-2)kobject_add()
int kobject_add(struct kobject *kobj, struct kobject *parent, const char *fmt, ...);
-
kobj
:指向要添加的内核对象的指针。 -
parent
:指向父级内核对象的指针。新创建的内核对象将成为父级对象的子对象。 -
fmt
:一个格式化字符串,用于指定要添加的内核对象的名称。
kobject_add()
将正确地设置kobject的父对象和kobject的名称。如果kobject要与特定的kset相关联,则必须在调用kobject_add()之前分配kobj->kset。如果kset与kobject相关联,那么kobject的父对象可以在调用kobject_add()时设置为NULL,然后kobject的父对象将是kset本身。
由于kobject的名称是在添加到内核时设置的,因此不应该直接操作kobject的名称,如果必须修改kobject的名称,则调用kobject_rename():
一旦通过kobject_add()注册了kobject,就不能直接使用kfree()释放它,唯一安全的方法是使用kobject_put()完成。最好总是在kobject_init()之后使用kobject_put(),以避免出现错误。
(2-3)kobject_rename()
int kobject_rename(struct kobject *kobj, const char *new_name);
注意:kobject_rename()不执行任何锁操作,也不知道什么名称是有效的,因此调用者必须提供自己的健全检查和序列化。
(2-4)kobject_name()
const char *kobject_name(const struct kobject * kobj);
有一个helper函数可以同时初始化和将kobject添加到内核中,它的名字叫kobject_init_and_add():
int kobject_init_and_add(struct kobject *kobj, struct kobj_type *ktype,struct kobject *parent, const char *fmt, ...);
-
kobj
:指向要初始化和添加的内核对象的指针。 -
ktype
:指向描述该内核对象类型的kobj_type
结构体的指针。 -
parent
:指向父级内核对象的指针。新创建的内核对象将成为父级对象的子对象。 -
fmt
:一个格式化字符串,用于指定新创建的内核对象的名称。此字符串可以包含格式化占位符,后面跟着对应的参数,类似于printf()
函数的用法。
参数与上面描述的kobject_init()和kobject_add()函数相同。
(2-5)kobject_get()
kobject_get()
函数用于增加给定 kobject 的引用计数:
struct kobject *kobject_get(struct kobject *kobj);
-
kobj
:要增加引用计数的 kobject 的指针。
当某段代码需要继续使用 kobject 时,可以通过调用 kobject_get()
来增加其引用计数,从而防止其在其他地方被释放。通常,每个 kobject_get()
都应该有对应的 kobject_put()
来减少引用计数,以确保在不再需要时能够正确地释放 kobject。
(2-6)kobject_put()
kobject_put()
函数用于减少给定kobject的引用计数,并在引用计数减为零时释放相应的内存空间:
void kobject_put(struct kobject *kobj);
-
kobj
:要减少引用计数的 kobject 的指针。
使用kobject_put()
函数可以减少kobject的引用计数,当引用计数减为零时,表示没有代码再需要该kobject,并且可以安全地释放其占用的内存。通常每个 kobject_get()
都应该有对应的 kobject_put()
来减少引用计数,以确保在不再需要时能够正确地释放 kobject。
(2-7)kobject_del()
kobject_del()
函数用于从内核对象层次结构中注销给定的 kobject:
void kobject_del(struct kobject *kobj);
-
kobj
:要注销的 kobject 的指针。
调用kobject_del()
函数将从内核对象层次结构中移除指定的kobject,这将使得 kobject 不再可见,但不会释放与之相关的内存,且其引用计数仍保持不变。通常,这个函数用于在不允许睡眠的情况下销毁对象,以确保在后续能够安全地完成对象的清理工作。要完全清理与 kobject 相关的内存,还需要在适当的时候调用 kobject_put()
。
(2-8)kobject_move()
kobject_move()
函数用于将一个 kobject 移动到另一个父对象下:
int kobject_move(struct kobject *kobj, struct kobject *new_parent)
-
kobj
:要移动的 kobject 的指针。 -
new_parent
:新的父对象的指针。
调用kobject_move()
函数会将指定的 kobject 移动到另一个父对象下,这可以用于重新组织内核对象层次结构中的对象。移动后,kobject 将成为新父对象的子对象,并从原父对象的子对象列表中删除。
(2-9)kobject_uevent
int kobject_uevent(struct kobject *kobj, enum kobject_action action);
kobject_uevent()
函数用于向用户空间发送一个与指定kobject
对象关联的uevent
事件。
-
kobj
:指向要发送uevent
事件的kobject
对象的指针。 -
action
:指定要发送的uevent
事件的动作类型,通常是一个枚举类型。
成功时返回0,失败时返回负值。
使用kobject_uevent()
函数,内核可以在发生特定事件时向用户空间发送通知,用户空间的程序可以监听这些事件并执行相应的操作。例如,在设备插入或移除时,内核可以调用kobject_uevent()
函数发送相应的uevent
事件,以通知用户空间发生了相关的变化。
请注意,kobject_uevent()
函数只是将uevent
事件添加到内核中的uevent
队列中,实际的uevent
处理是由对应的守护进程来完成的,它会从内核的uevent
队列中获取事件并执行相应的操作。
enum kobject_action定义如下:
enum kobject_action {
KOBJ_ADD, //对象添加。表示向系统中添加了一个新的kobject对象。
KOBJ_REMOVE, //对象移除。表示从系统中移除了一个kobject对象。
KOBJ_CHANGE, //对象修改。表示kobject对象的属性或状态发生了变化。
KOBJ_MOVE, //对象移动。表示kobject对象被移动到了另一个位置。
KOBJ_ONLINE, //对象上线。表示kobject对象已经准备好在线工作。
KOBJ_OFFLINE, //对象离线。表示kobject对象已经离线,不再处于工作状态。
KOBJ_MAX //动作类型的最大值,用于边界检查。
};
这里给出了枚举类型kobject_action
的定义,它包含了一组常见的kobject
动作类型:
-
KOBJ_ADD
:对象添加。表示向系统中添加了一个新的kobject
对象。 -
KOBJ_REMOVE
:对象移除。表示从系统中移除了一个kobject
对象。 -
KOBJ_CHANGE
:对象修改。表示kobject
对象的属性或状态发生了变化。 -
KOBJ_MOVE
:对象移动。表示kobject
对象被移动到了另一个位置。 -
KOBJ_ONLINE
:对象上线。表示kobject
对象已经准备好在线工作。 -
KOBJ_OFFLINE
:对象离线。表示kobject
对象已经离线,不再处于工作状态。 -
KOBJ_MAX
:动作类型的最大值,用于边界检查。
这些动作类型用于指示kobject_uevent()
函数要发送的uevent
事件的类型。当内核调用kobject_uevent()
函数时,它会指定一个动作类型,告知用户空间发生了哪种类型的事件。用户空间的程序可以根据收到的uevent
事件类型来执行相应的操作。
(2-10)kobject_uevent_env
int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,char *envp_ext[])
kobject_uevent_env()
函数与kobject_uevent()
函数类似,用于向用户空间发送一个与指定kobject
对象关联的uevent
事件,但kobject_uevent_env
允许在发送uevent
事件时传递额外的环境变量。kobject_uevent()
本质上也是调用kobject_uevent_env()
实现,只是envp_ext
位NULL。
-
kobj
:指向要发送uevent
事件的kobject
对象的指针。 -
action
:指定要发送的uevent
事件的动作类型,通常是一个枚举类型。 -
envp_ext[]
:一个字符串数组,包含要传递给用户空间的额外环境变量。数组中的每个元素都是一个以key=value
格式表示的字符串,用于指定一个环境变量的键值对。
使用kobject_uevent_env()
函数,内核可以在发生特定事件时向用户空间发送通知,并传递额外的环境变量信息。用户空间的程序可以监听这些事件并执行相应的操作,并根据收到的环境变量信息来进行进一步处理。
三、引用计数
kobject的关键功能之一是充当嵌入它的对象的引用计数器,只要对对象的引用存在,该对象(和支持它的代码)也必须继续存在。用于操作kobject引用计数的底层函数有:
struct kobject *kobject_get(struct kobject *kobj);
void kobject_put(struct kobject *kobj);
成功调用kobject_get()将增加kobject的引用计数器并返回指向该kobject的指针。当一个引用被释放时,对kobject_put()的调用将减少引用计数,并可能释放该对象。
注意,kobject_init()将引用计数设置为1,因此设置kobject的代码最终需要执行kobject_put()来释放该引用。
因为kobject是动态的,所以它们不能被静态地声明或在堆栈上声明,而是总是动态地分配。内核的未来版本将包含对静态创建的kobject的运行时检查,并将警告开发人员这种不当使用。
四、创建简单的kobject
有时候,我没仅仅想在在sysfs层次结构中创建一个简单的目录,而不必与复杂的kset、show和store函数以及其他细节纠缠在一起。则可以使用下列API创建这样的一个条目:
struct kobject *kobject_create_and_add(const char *name, struct kobject *parent);
-
name
:指定新创建的内核对象的名称。 -
parent
:指向父级内核对象的指针。新创建的内核对象将成为父级对象的子对象。
这个函数将创建一个kobject,并将它放在sysfs中指定的父kobject下面的位置。要创建与这个kobject相关的简单属性,需使用:
int sysfs_create_file(struct kobject *kobj, const struct attribute *attr);
或者
int sysfs_create_group(struct kobject *kobj, const struct attribute_group *grp);
注意:这里使用的两种属性类型,以及使用kobject_create_and_add()创建的kobject,都可以是kobj_attribute类型,因此不需要创建特殊的自定义属性。
五、移除kobject
如果一个kobject成功注册到kobject核心后,当代码完成对其的使用时,必须对其进行清理,这时候需要调用kobject_put()
。通过这样做,kobject核心将自动清理该kobject分配的所有内存。如果已为对象发送了KOBJ_ADD uevent,则将发送相应的KOBJ_REMOVE uevent,并且其他 sysfs 管理都将得到合适的处理。
如果我没需要对kobject进行两阶段删除(例如,当我们需要销毁对象时不能睡眠),那么可以调用kobject_del()
将从sysfs注销kobject,这使得 kobject “不可见”,但它没有被清理,这时候对象的引用计数仍然相同,在后续调用kobject_put()
来完成与kobject相关联的内存清理操作。
kobject_del()
可用于断开对父对象的引用,如果构造了循环引用。在某些情况下,父对象引用子对象是有效的,必须使用显式调用kobject_del()
来中断循环引用,以便调用释放函数。
六、kset提供的功能
一个kset提供以下三个功能:
(1)kset
就像一个装东西的袋子,里面装着一组物体。内核可以使用kset来跟踪“所有块设备”或“所有PCI设备驱动程序”。
(2)kset
也是sysfs
中的一个子目录,其中可以显示与kset相关联的kobject,每个kset都包含一个kobject,它可以被设置为其他kobject的父对象;sysfs层次结构的*目录就是这样构造的。
(3)kset
可以支持kobject
的"热插拔",并影响uevent事件如何报告给用户空间。
在面向对象的术语中,“kset”是*容器类;kset包含它们自己的kobject,但是kobject是由kset代码管理的,不应该由用户直接操作。
kset将其子节点保存在标准的内核链表
中,kobject通过kset字段指向包含它们的kset。在几乎所有的情况下,属于kset的kobject都在它们的父对象中包含kset(或者严格地说是内嵌的kobject)。
由于kset中包含一个kobject,所以始终应该动态创建kset,而不是静态或在堆栈上声明它。
创建一个新的kset,代码如下:
struct kset *kset_create_and_add(const char *name,const struct kset_uevent_ops *uevent_ops,struct kobject *parent_kobj);
当使用完kset之后,需要调用:
void kset_unregister(struct kset *k);
去销毁它。该函数将从sysfs中删除kset并减少其引用计数,当引用计数减为零时,kset将被释放。因为这时候对kset的其他引用可能仍然存在,所以释放操作可能发生在kset_unregister()
返回之后。
如果kset希望控制与它关联的kobject的uevent的ops,它可以使用结构体kset_uevent_ops
来处理,该结构定义如下:
struct kset_uevent_ops {
int (* const filter)(struct kset *kset, struct kobject *kobj);
const char *(* const name)(struct kset *kset, struct kobject *kobj);
int (* const uevent)(struct kset *kset, struct kobject *kobj,
struct kobj_uevent_env *env);
};
-
filter
:用于过滤要发送uevent的struct kobject
对象。如果返回值为非零,则表示该对象的uevent将被发送;如果返回值为零,则表示该对象的uevent将被忽略。 -
name
:用于获取要发送uevent的struct kobject
对象的名称,返回的字符串将作为uevent中的"NAME"字段。 -
uevent
:用于发送uevent的回调函数。它将会在struct kobject
对象的uevent触发时被调用,并接收struct kobject
对象、struct kobj_uevent_env
环境变量作为参数。
通过定义struct kset_uevent_ops
结构体,并将其赋值给struct kset
对象的uevent_ops
成员,可以自定义struct kset
对象的uevent操作行为。当相关的内核对象发生与uevent相关的事件时,内核将调用struct kset_uevent_ops
中定义的函数来处理和发送uevent。
使用struct kset_uevent_ops
,可以灵活地定义struct kset
对象的uevent行为,例如过滤要发送的uevent对象、指定uevent中的名称字段、处理和发送uevent等。这能够在内核对象发生变化时,根据需要触发自定义的操作和通知。
参考链接:
https://www.kernel.org/doc/html/latest/core-api/kobject.html
上一篇: 重新推出云原生 API 网关链接跟踪功能
推荐阅读
-
epoll简介及触发模式(accept、read、send)-epoll的简单介绍 epoll在LT和ET模式下的读写方式 一、epoll的接口非常简单,一共就三个函数:1. int epoll_create(int size);创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close关闭,否则可能导致fd被耗尽。2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);epoll的事件注册函数,它不同与select是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create的返回值,第二个参数表示动作,用三个宏来表示:EPOLL_CTL_ADD:注册新的fd到epfd中;EPOLL_CTL_MOD:修改已经注册的fd的监听事件;EPOLL_CTL_DEL:从epfd中删除一个fd;第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */};events可以是以下几个宏的集合:EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); EPOLLIN事件:EPOLLIN事件则只有当对端有数据写入时才会触发,所以触发一次后需要不断读取所有数据直到读完EAGAIN为止。否则剩下的数据只有在下次对端有写入时才能一起取出来了。现在明白为什么说epoll必须要求异步socket了吧?如果同步socket,而且要求读完所有数据,那么最终就会在堵死在阻塞里。 EPOLLOUT:表示对应的文件描述符可以写; EPOLLOUT事件:EPOLLOUT事件只有在连接时触发一次,表示可写,其他时候想要触发,那要先准备好下面条件:1.某次write,写满了发送缓冲区,返回错误码为EAGAIN。2.对端读取了一些数据,又重新可写了,此时会触发EPOLLOUT。简单地说:EPOLLOUT事件只有在不可写到可写的转变时刻,才会触发一次,所以叫边缘触发,这叫法没错的!其实,如果真的想强制触发一次,也是有办法的,直接调用epoll_ctl重新设置一下event就可以了,event跟原来的设置一模一样都行(但必须包含EPOLLOUT),关键是重新设置,就会马上触发一次EPOLLOUT事件。1. 缓冲区由满变空.2.同时注册EPOLLIN | EPOLLOUT事件,也会触发一次EPOLLOUT事件这个两个也会触发EPOLLOUT事件 EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);EPOLLERR:表示对应的文件描述符发生错误;EPOLLHUP:表示对应的文件描述符被挂断;EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);等待事件的产生,类似于select调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。-------------------------------------------------------------------------------------------- 从man手册中,得到ET和LT的具体描述如下EPOLL事件有两种模型:Edge Triggered (ET)Level Triggered (LT)假如有这样一个例子:1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符2. 这个时候从管道的另一端被写入了2KB的数据3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作4. 然后我们读取了1KB的数据5. 调用epoll_wait(2)......Edge Triggered 工作模式:如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。 i 基于非阻塞文件句柄 ii 只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。Level Triggered 工作模式相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。然后详细解释ET, LT:LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认(这句话不理解)。在许多测试中我们会看到如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当我们遇到大量的idle- connection(例如WAN环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。(未测试)另外,当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,读数据的时候需要考虑的是当recv返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取: 这里只是说明思路(参考《UNIX网络编程》) while(rs) {buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);if(buflen < 0){// 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读// 在这里就当作是该次事件已处理处.if(errno == EAGAIN)break; else return; }else if(buflen == 0) { // 这里表示对端的socket已正常关闭. } if(buflen == sizeof(buf) rs = 1; // 需要再次读取 else rs = 0; } 还有,假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据.所以,需要封装socket_send的函数用来处理这种情况,该函数会尽量将数据写完再返回,返回-1表示出错。在socket_send内部,当写缓冲已满(send返回-1,且errno为EAGAIN),那么会等待后再重试.这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send内部,但暂没有更好的办法. ssize_t socket_send(int sockfd, const char* buffer, size_t buflen) { ssize_t tmp; size_t total = buflen; const char *p = buffer; while(1) { tmp = send(sockfd, p, total, 0); if(tmp < 0) { // 当send收到信号时,可以继续写,但这里返回-1. if(errno == EINTR) return -1; // 当socket是非阻塞时,如返回此错误,表示写缓冲队列已满, // 在这里做延时后再重试. if(errno == EAGAIN) { usleep(1000); continue; } return -1; } if((size_t)tmp == total) return buflen; total -= tmp; p += tmp; } return tmp; } 二、epoll在LT和ET模式下的读写方式 在一个非阻塞的socket上调用read/write函数, 返回EAGAIN或者EWOULDBLOCK(注: EAGAIN就是EWOULDBLOCK) 从字面上看, 意思是: * EAGAIN: 再试一次 * EWOULDBLOCK: 如果这是一个阻塞socket, 操作将被block * perror输出: Resource temporarily unavailable 总结: 这个错误表示资源暂时不够, 可能read时, 读缓冲区没有数据, 或者, write时,写缓冲区满了 。 遇到这种情况, 如果是阻塞socket, read/write就要阻塞掉。 而如果是非阻塞socket, read/write立即返回-1, 同 时errno设置为EAGAIN. 所以, 对于阻塞socket, read/write返回-1代表网络出错了. 但对于非阻塞socket, read/write返回-1不一定网络真的出错了. 可能是Resource temporarily unavailable. 这时你应该再试, 直到Resource available. 综上, 对于non-blocking的socket, 正确的读写操作为: 读: 忽略掉errno = EAGAIN的错误, 下次继续读 写: 忽略掉errno = EAGAIN的错误, 下次继续写 对于select和epoll的LT模式, 这种读写方式是没有问题的. 但对于epoll的ET模式, 这种方式还有漏洞. epoll的两种模式 LT 和 ET
-
[linux 内核] 一篇总结 linux 内核中 kobject、kset 和 ktype 的文章。