Linux内核学习之 -- epoll()一族系统调用分析笔记

背景

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内核来讲,很多博客提到的两点感觉都不对:

  1. 关于epoll_wait()休眠时,唤醒进程的到底是啥。这里很多博客没搞明白嵌套epollfd的概念。
  2. 关于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;
}

可以认为,该函数做了三件事:

  1. 申请一个没用过的fd
  2. 创建一个struct file结构体,绑定一个匿名inode。同时绑定file->private_data = 分配的struct eventpoll,该结构体贯穿三个系统调用
  3. 建立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()的使用方法在开头的使用示例中已经描述。

在分析之前,先声明两个别名,后面的分析我会经常用到

  1. epollfd,是第一步通过epoll_create()创建的fd
  2. 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值