epoll源码总结

目录

 

1.等待队列

2.内核接收网络数据的全过程

3.文件描述符在内核中的数据结构

4.epoll流程图

5.结构体关系图

6.循环嵌套情况

7.slab缓存

8.驱动的理解

9.epoll水平触发和边缘触发的不同


1.等待队列

等待队列在epoll实现中非常重要,首先讲述一下等待队列的作用。

每个文件都有一个等待队列头,虽然没有看到这个队列头具体在哪个结构体中,但是可以通过每个文件默认操作函数poll看出来。

等待队列是由等待队列头和等待队列的成员组成的,当队列头被唤醒以后,就会依次调用该队列中的每一个成员的回调函数。在epoll中等待队列被这样使用:队列头被文件所拥有,而每一个成员一定指向一个线程或者进程。当文件的状态发生改变时,就会触发中断,激活队列头,从而把在这个队列上的线程或者进程激活,调用其回调函数

等待队列具体的操作函数可以参考这篇博客https://www.cnblogs.com/apprentice89/archive/2013/05/09/3068274.html其中等待队列只有两种结构体,一种是队列头,一种是队列元素,具体定义分别是

struct __wait_queue_head {

      /*因为等待队列可以在中断时随时修改,因此设置一个自旋锁保证一致性*/

     spinlock_t lock;   

     struct list_headtask_list;

};
typedef struct __wait_queue_head  wait_queue_head_t

struct __wait_queue {

    unsigned int  flags;     /*指明等待的进程是互斥进程还是非互斥进程*/

#define WQ_FLAG_EXCLUSIVE    0x01

    void *private;           /*指向进程的task_struct进程控制块或者线程的控制块*/

    wait_queue_func_t  func;

    struct list_head  task_list;

};
typedef struct __wait_queue  wait_queue_t

可以看到,等待队列有以下几种操作:添加队列元素,删除队列元素,在等待队列上睡眠以及唤醒睡眠的进程。

注意:睡眠一般可以具体到每个队列元素,但是唤醒是由头结点来管理的,唤醒函数会唤醒以输入参数x为头结点的等待队列中的指定数目的进程。

一般队列元素都是和一个进程绑定的,private成员就指向一个进程。表明这个进程就会在等待队列之中,并且可以给这个进程进行睡眠和唤醒操作。

阻塞当前进程的原理:将当前进程放置在等待队列中(一般都是文件描述符对应的等待队列上),用函数set_current_state()修改当前进程标志位为TASK_INTERRUPTIBLE(不可中断睡眠)或TASK_UNINTERRUPTIBLE(可中断睡眠)状态,然后调用schedule()告诉内核重新调度,由于当前进程状态已经为睡眠状态,自然就不会被调度。schedule()简单说就是告诉内核当前进程主动放弃CPU控制权。这样来,就可以说当前进程在此处睡眠,即阻塞在这里。

2.内核接收网络数据的全过程

主要参考这篇文章select流程之前的内容。其中http://www.sohu.com/a/317847036_463994

注意:每个文件描述符都有自己的等待队列,可能不止一个,比如epollfd就有两个等待队列,wq和poll_wait

文章中提到的工作队列和等待队列,和我们理解的有一些不一样,其中工作队列中的进程就是没有睡眠的进程,而等待队列中的进程就是睡眠的进程。但是其实真正的等待队列中的进程不一定就是睡眠的进程,也可以是没有睡眠的进程,比如在epoll源码中,eppoll_entry->wait就会挂在文件描述符下的等待队列中,但是这个进程并没有在这个队列上睡眠。所以我觉得更好的理解就是没有睡眠的进程和睡眠的进程,阻塞的进程就是睡眠的进程。

更通俗的理解是,挂在等待队列上的进程不一定就阻塞,因为阻塞还需要设置进程状态,利用schedule函数告知cpu休眠等操作。如果只是挂在等待队列上,其作用就是一旦文件发生状态的改变,就可以触发中断,并告知等待队列头,执行队列成员的回调函数。所以epoll中是一个进程挂在多个等待队列上,并且只在epollfd文件描述符下的等待队列中阻塞。

在分析epoll具体过程时,我不认为多个socket都会加入到eventpoll的等待队列中去,而是进程A加入到每个socket自己的等待队列中,但是进程A没有执行睡眠函数。我认为这张图:

的正确画法应该如下:

 

3.文件描述符在内核中的数据结构

文件描述符在用户层是一个int类型的数据,但是在内核中对应三个结构体file,inode,file_operations

struct file就是和文件描述符一一对应的结构体,一个fd对应一个struct file

struct file
{
	mode_t f_mode;//表示文件是否可读或可写,FMODE_READ或FMODE_WRITE
	dev_ t  f_rdev ;// 用于/dev/tty
	off_t  f_ops;//当前文件位移
	unsigned short f_flags;//文件标志,O_RDONLY,O_NONBLOCK和O_SYNC
	unsigned short f_count;//打开的文件数目
	unsigned short f_reada;
	struct inode *f_inode;//指向inode的结构指针
	struct file_operations *f_op;//文件索引指针
}

可以看到file中有指向inode和file_operation的指针

struct inode是表示具体文件的信息,一个文件可以被多个进程打开,也就是有多个文件描述符,但是这些文件描述符,或者file结构都指向同一个struct inode结构体。其中最重要的两个成员

dev_t i_rdev; //含有真正的设备号
struct cdev *i_cdev; //struct cdev是内核内部表示字符设备的结构.

struct file_operations是操作文件描述符的具体函数

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 (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iterate) (struct file *, struct dir_context *);
	int (*iterate_shared) (struct file *, struct dir_context *);
	__poll_t (*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 *);
	unsigned long mmap_supported_flags;
	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 (*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 **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
	ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
			loff_t, size_t, unsigned int);
	int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
			u64);
	ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *,
			u64);
} __randomize_layout;

这些函数都有默认的操作函数与之对应,但是也可以自定义。

其中比较重要的有一下几个

ssize_t (*read)(struct file *,char *, size_t, loff_t *);//从设备同步读取数据
ssize_t (*write)(struct file *,const char *, size_t, loff_t *);
int (*ioctl) (struct  inode *,  struct file *, unsigned int,  unsigned long);//执行设备IO控制命令
int (*open) (struct inode *, struct file *);//打开
int (*release)(struct inode *, struct file *);//关闭
__poll_t (*poll) (struct file *, struct poll_table_struct *);//探测设备文件是否有数据可读

注意

在epoll源码中,poll函数使用的比较多,首先讲解一下默认的poll函数实现功能

poll函数主要分两步,第一步调用poll_wait函数,将当前进程挂在对应文件描述符的等待队列中。这样就可以在文件描述符产生事件时,知道唤醒哪个进程。

第二步就是检查当前有没有事件发生,并返回掩码表示值。

而在后面我个人理解如果文件描述符状态改变了,就会产生中断,中断程序会等待队列的唤醒函数来唤醒队列头。

而在epoll源码中,有两种poll()函数,

一种是专门为epollfd自定义的poll函数,是为了处理循环嵌套的情况,比如将epollfd1挂在epollfd2上,属于嵌套监听,需要额外定义poll函数,其中不同也就是并不是放在普通的等待队列中的,而是放在专门为epollfd设置的等待队列poll_wait上。

另外一种是采用了默认的poll函数,但是在两处采用了自定义的回调函数,分别是在调用poll函数以后,会调用ep_ptable_queue_proc,另一个是当对应的事件发生时,会调用ep_poll_callback函数,而不是使用默认的唤醒函数default_wake_function来唤醒等待队列上的进程。

4.epoll流程图

 

 

注释1:不同类型的文件描述符对应不同的poll函数,以socket为例,因为socket有多种类型,如tcp、udp等,所以socket层会实现一个通用的poll回调函数,这个函数就是sock_poll()。在sock_poll()函数中通常会调用某一具体类型协议对应的poll回调函数,以tcp为例,那么这个poll()回调函数就是tcp_poll()。而tcp_poll()会调用sock_poll_wait(),在sock_poll_wait()中会调用到epq.pt.qproc指向的函数,也就是ep_ptable_queue_proc()。

所以整个调用过程是:poll(),sock_poll(),tcp_poll(),sock_poll_wait(),ep_ptable_queue_proc()

注意:在epoll源码中,其实用到了两种等待队列,一种是在eventpoll结构体中,这是用来在epoll_wait函数中阻塞的等待队列,还有一种是被监听文件描述符的等待队列,每个被监听的文件描述符都有这样的等待队列,这个等待队列上挂有当前的进程,其实也就是使用epoll_ctl的进程,这些等待队列是为了监听这些描述符,一旦这些文件描述符发生事件,就会执行中断函数,中断函数原来是默认的等待队列的唤醒函数,现在是自定义的ep_poll_callback函数。所以在被监听的文件描述符上,进程是不会睡眠的。

5.结构体关系图

以下是结构体的关系图,其中eventpoll是核心结构体。eventpoll结构体中有红黑树的根节点,并且eventpoll结构体指向了应用层对应的是epollfd的struct file结构体(也就是左边的struct file),并且注意左边的struct file结构体也指向eventpoll(原理是当epollfd睡在eventpoll中的等待队列时,如果被唤醒,对应epollfd的内核结构体是struct file,所以需要通过struct file找到eventpoll),file_operations则是对应epollfd的操作函数,最左边四个结构体则比较底层了,其中task_struct是表示这个文件描述符所在进程的信息。

eventpoll的右边一块属于被关注的文件描述符对应的结构体,其实数量上可以有很多。struct epitem则是挂在红黑树上的节点,并且有成员指向需要关注的struct file,相当于struct epitem是红黑树节点和struct file的纽带,通过struct epitem把struct file挂在红黑树上。另外epitem所在的进程通过eppoll_entry间接挂在等待队列上,并且这个等待队列可以看到,是属于struct file的,可以通过虚线得到。从底层角度来看,当文件描述符发生事件,肯定可以很容易的通知自己等待队列上的进程,所以这个进程挂在了所有被关注的文件描述符的等待队列上,一有事件发生,就会产生回调函数。

 

6.循环嵌套情况

在使用epoll时,会有如下情况发生

fd = socket(...);
efd1 = epoll_create();
efd2 = epoll_create();
epoll_ctl(efd1, EPOLL_CTL_ADD, fd, ...);
epoll_ctl(efd2, EPOLL_CTL_ADD, efd1, ...);

如上,efd1监控fd,而efd2监控了efd1,即嵌套的epoll监控:epoll监控另一个epoll句柄 
efd2要监控efd1,将调用efd1的poll函数 
回忆之前说过:文件f_op->poll需要配合驱动提供的等待队列 
对于epollfd,等待队列就是poll_wait 
efd2监听efd1,会调用efd1->f_op->poll,于是把当前进程放到efd1的poll_wait队列上 
在epoll的内核实现中,当efd1本身监听到fd事件产生后,会顺便唤醒poll_wait上的进程 
于是,“efd1监听到事件” 被通知到efd2。这样,就实现了epollfd被其他多路复用监听了!
 

而如果是这样:

efd1 = epoll_create();
efd2 = epoll_create();
epoll_ctl(efd1, EPOLL_CTL_ADD, efd2, ...);
epoll_ctl(efd2, EPOLL_CTL_ADD, efd1, ...);

这样是会报错的,因为这样循环监听,就无限循环了。

7.slab缓存

slab分配器是基于对象进行管理的,所谓的对象就是内核中的数据结构(例如:task_struct,file_struct 等)。相同类型的对象归为一类,每当要申请这样一个对象时,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免内部碎片。slab分配器并不丢弃已经分配的对象,而是释放并把它们保存在内存中。slab分配对象时,会使用最近释放的对象的内存块,因此其驻留在cpu高速缓存中的概率会大大提高。

我的理解:相同的结构体,所需要的内存一般是固定的,所以slab对一类结构体建一个列表,只要是这一类的结构体,就从这个列表中取内存。所以可以看到,epoll中主要在创建eppoll_entry和epitem这两个会重复创建销毁的结构体上。

8.驱动的理解

在百度等待队列时,经常会有驱动的等待队列这种说法,我认为由于linux中一切皆文件,所以驱动其实就是文件的一种说法,只不过驱动更加底层,我一般都把驱动等价为文件来看待。

9.epoll水平触发和边缘触发的不同

在epoll队列中维护着一个已发生事件的链表,如果是边缘触发,那么将这个已发生事件的文件描述符对应放入epoll_event结构体中以后,就将这个从已发生事件的链表中清除,但是水平触发会将这个文件描述符放入epoll_event结构体以后,仍然会继续加入已发生事件的链表中,等待下一次处理已发生事件的链表是时,仍然会处理这个文件描述符,如果这个事件已经被处理了,那么就不再产生epoll_event结构体,相当于空循环一次。

参考资料:https://www.cnblogs.com/l2017/p/10830391.html

https://blog.csdn.net/linkedin_38454662/article/details/73337208

https://www.cnblogs.com/apprentice89/archive/2013/05/09/3068274.html

http://192.168.73.130/www.sohu.com/a/317847036_463994

https://www.cnblogs.com/sduzh/p/6714281.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值