背景
linux 4.19
epoll()也是一种I/O多路复用的技术,但是完全不同于select()/poll()。更加高效,高效的原因其他博客也都提到了,这篇笔记主要是从源码的角度来分析一下实现过程。
作为自己的学习笔记,分析都在代码注释中,后续回顾的时候看注释好一点。
相关链接:
- Linux内核学习之 – ARMv8架构的系统调用笔记
- Linux内核学习之 – 系统调用open()和write()的实现笔记
- Linux内核学习之 – 系统调用poll()分析笔记
对于epoll()的分析建立在《Linux内核学习之 – 系统调用open()和write()的实现笔记》 + 《Linux内核学习之 – 系统调用poll()分析笔记》的基础上,因为有很多通用的步骤和环节。所以这篇文章写的没有那么细致,有些地方一笔带过。最好先瞄一眼这两篇。
同时对一些细节的处理分析没有写在博客中,有点麻烦,都写到注释中了。如epoll如何处理不同标志位如EPOLLONESHOT/EPOLLLT/EPOLLET,epoll中实现的一些检查宏等等
还有一件事,就是看了很多博客,感觉很多都是复制粘贴的,根本没看过内核真正的实现过程。就以4.19内核来讲,很多博客提到的两点感觉都不对:
- 关于epoll_wait()休眠时,唤醒进程的到底是啥。这里很多博客没搞明白嵌套epollfd的概念。
- 关于epoll使用共享内存来进行用户态/内核态传参这件事,看完后发现,根本就没用共享内存,起码4.19没用。
一、epoll()的使用方法
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <sys/epoll.h>
#include <sys/time.h>
#include <errno.h>
int main()
{
int nfds;
struct epoll_event event;
struct epoll_event events[1024];
char buff[10];
unsigned char cnt = 0;
int epoll_fd = -1;
int dev_fd = -1;
epoll_fd = epoll_create(1); // g, linux2.6之后传入参数size > 0即可,不会影响什么
if(epoll_fd <= 0)
printf("create err\n");
dev_fd = open("/dev/xx",O_RDONLY);
if(dev_fd < 0){
printf("Failed open\n");
}
event.events = EPOLLET;
event.data.fd = dev_fd;
// g, 添加要监听的设备
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, dev_fd, &event); // g, 参数为:epollfd, op(要执行什么操作), 要监听的fd, 事件
while(1) {
// g, 倒数第二个参数不能大于epoll_create()传递的参数
nfds = epoll_wait(epoll_fd, &events, 1, 500 );
if( nfds < 0 ) {
printf("err\n");
break;
}
for (int n = 0; n < nfds; ++n) {
if (events[n].events & EPOLLIN) {
// g, 处理可读事件
} else if(events[n].events & EPOLLOUT) {
// g, 处理可写事件
}
...
...
}
}
return 0;
}
同样,与poll()一样,要求在驱动程序中实现dev_poll()
二、epoll()系统调用
2.1 epoll_create()
该系统调用如下:
fs/eventpoll.c:
SYSCALL_DEFINE1(epoll_create, int, size)
{
if (size <= 0)
return -EINVAL;
return do_epoll_create(0);
}
所以说,你传入什么参数无所谓,只要大于0就行了,没影响。之后单纯的调用了函数do_epoll_create():
static int do_epoll_create(int flags)
{
int error, fd;
// g, 这个结构体里面是有一颗红黑树的
struct eventpoll *ep = NULL;
struct file *file;
/* Check the EPOLL_* constant for consistency. */
BUILD_BUG_ON(EPOLL_CLOEXEC != O_CLOEXEC);
if (flags & ~EPOLL_CLOEXEC)
return -EINVAL;
/*
* Create the internal data structure ("struct eventpoll").
*/
error = ep_alloc(&ep); // g, 使用kzalloc而不是slab来分配。申请内存后初始化一些参数
if (error < 0)
return error;
/*
* Creates all the items needed to setup an eventpoll file. That is,
* a file structure and a free file descriptor.
*/
fd = get_unused_fd_flags(O_RDWR | (flags & O_CLOEXEC)); // g, 与do_sys_open()中使用了一样同样的获取fd的函数
if (fd < 0) {
error = fd;
goto out_free_ep;
}
// g, 创建一个struct file,初始化该file,绑定匿名anon_inode_inode,
// g, 正常的inode都能在文件系统中找到对应的dentry,往往是inode->dentry->file->fd,但是对于匿名inode,来说不出现在文件系统中
// g, 但是不是说没有dentry,仍然要为其创建一个struct dentry,只是该dentry目录不会出现在文件系统中,就叫"[eventpoll]"
// g, 其中会使用 file->private_data = priv将ep_alloc()创建的eventpoll对象赋值给struct file的private_data 成员变量
// g, 会绑定file->f_op = eventpoll_fops,其中实现了.poll
file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep,
O_RDWR | (flags & O_CLOEXEC));
if (IS_ERR(file)) {
error = PTR_ERR(file);
goto out_free_fd;
}
ep->file = file; // g, 绑定ep与刚申请的file的关系
fd_install(fd, file); // g,建立fd与file的联系,与do_sys_open()调用的fd_install是一样的
return fd;
out_free_fd:
put_unused_fd(fd);
out_free_ep:
ep_free(ep);
return error;
}
可以认为,该函数做了三件事:
- 申请一个没用过的fd
- 创建一个struct file结构体,绑定一个匿名inode。同时绑定file->private_data = 分配的struct eventpoll,该结构体贯穿三个系统调用
- 建立fd与struct file的关系,返回该fd
可以认为,是青春版open()。不同的地方就在于第二步。
对于open()来说,我是实实在在的希望能找到一个真正的文件,或者创建一个真正的文件,所以struct file最终绑定的inode都是在文件系统中能找到的,代表一个真实存在的文件。但是epoll()不一样,不需要一个真正存在的文件,只是希望凑齐三板斧struct file/struct dentry/struct inode,毕竟其它程序都是这么做的,为了适应大环境(比如说一些系统调用,一些vfs提供的api等等)也得这么干。但是真没必要来个真实的inode,所以就出现了一种奇怪的inode:匿名inode。
关于匿名inode,可以看一下这篇文章:Linux fd 系列“匿名句柄” 是一切皆文件背后功臣
我个人的理解呢,就是你需要文件系统那一套(因为Linux下一切皆文件),但是又不需要真正存在这个文件,只是用它那一套规则,就可以用一个匿名inode。而且还能给struct file绑定自己设置的ops,而不是像open()时那样把inode->ops绑定到file->ops。还挺方便。
同时内核中有很多使用匿名inode的模块,所以把申请匿名inode的代码给平台化了,想用就调接口就行了:
fs/anon_inodes.c:
struct file *anon_inode_getfile(const char *name,
const struct file_operations *fops,
void *priv, int flags)
{
struct file *file;
if (IS_ERR(anon_inode_inode))
return ERR_PTR(-ENODEV);
if (fops->owner && !try_module_get(fops->owner))
return ERR_PTR(-ENOENT);
/*
* We know the anon_inode inode count is always greater than zero,
* so ihold() is safe.
*/
ihold(anon_inode_inode); // g, 判断inode->i_count+1后是否 < 2
// g, 虽然匿名inode不会有"实际的"dentry,但是该函数里面仍然会分配一个struct dentry绑定给file,这不过这个dentry不会有真实的路径
// g, 传入的name就是dentry对应的name, anon_inode_mnt是分配dentry所要使用的文件系统,要用到该文件系统的super block。看样子文件系统叫anon_inode_mnt
// g, 传入的anon_inode_inode是要与函数中分配的dentry绑定的inode,最终会绑定dentry->d_inode = inode;
// g, 对于这个anon_inode_inode,会在start_kenerl时就进行申请和创建了。
// g, 最终结果:绑定file->f_path = path, file->f_op = fops, path.dentry = 新分配的dentry, path.dentry->d_inode = anon_inode_inode,file->f_inode = path->dentry->d_inode;
file = alloc_file_pseudo(anon_inode_inode, anon_inode_mnt, name,
flags & (O_ACCMODE | O_NONBLOCK), fops);
if (IS_ERR(file))
goto err;
file->f_mapping = anon_inode_inode->i_mapping;
file->private_data = priv; // g,绑定申请的file->private_data = 申请的struct eventpoll结构体,会在后续的其他系统调用中用到
return file;
err:
iput(anon_inode_inode); // g,对inode->i_count - 1
module_put(fops->owner);
return file;
}
直接调这个函数,申请一个struct dentry和struct file,并且初始化。最终初始化的结果就是:
file->f_op = fops;
file->f_inode = anon_inode_inode;
dentry->d_inode = anon_inode_inode;
fops由调用者传入,在epoll()模块中传入的是eventpoll_fops(),这个后面再说。
这个anon_inode_inode,就是一个提前创建好的匿名inode,其创建过程:
// g, 此函数会在linux启动早期调用,因为被xx_initcall宏注册了
// g, 其实本文件下的代码相当于是一个"匿名inode"文件系统,提供一些函数,提供一个匿名inode
static int __init anon_inode_init(void)
{
// g, 先mount一个文件系统。然后将会在anon_inode_fs_type文件系统中分配inode,算是创建了一个vfsmount实例
anon_inode_mnt = kern_mount(&anon_inode_fs_type);
if (IS_ERR(anon_inode_mnt))
panic("anon_inode_init() kernel mount failed (%ld)\n", PTR_ERR(anon_inode_mnt));
// g, 关于xx_initcall宏,移步这一篇博客:https://blog.youkuaiyun.com/weiqifa0/article/details/136795242?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522172068035616800186560008%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=172068035616800186560008&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-1-136795242-null-null.142^v100^pc_search_result_base8&utm_term=fs_initcall&spm=1018.2226.3001.4187
// g, 分配了一个匿名inode
anon_inode_inode = alloc_anon_inode(anon_inode_mnt->mnt_sb); // g, 很多模块都会用到这个匿名inode,anon_inode_inode
if (IS_ERR(anon_inode_inode))
panic("anon_inode_init() inode allocation failed (%ld)\n", PTR_ERR(anon_inode_inode));
return 0;
}
2.2 epoll_ctl()
当我们使用epoll_create()创建了epollfd之后,我们需要通过epoll_ctl()来添加我们需要监听的fd,已经要监听的事件。epoll_ctl()的使用方法在开头的使用示例中已经描述。
在分析之前,先声明两个别名,后面的分析我会经常用到:
- epollfd,是第一步通过epoll_create()创建的fd
- tfd,也就是target fd,也就是我们要监控的目标fd
接下来就分析一下该函数在内核中的实现过程:
SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
struct epoll_event __user *, event)
{
int error;
int full_check = 0;
struct fd f, tf;
struct eventpoll *ep;
struct epitem *epi;
struct epoll_event epds;
struct eventpoll *tep = NULL;
error = -EFAULT;
if (ep_op_has_event(op) && // g, 如果op不是删除操作
copy_from_user(&epds, event, sizeof(struct epoll_event))) // g, 拷贝用户的event
goto error_return;
error = -EBADF;
f = fdget(epfd); // g, 获取epfd对应的struct file,也就是在epoll_create那一步创建的fd
if (!f.file