深度探究 Linux IO 多路复用的实验过程与原理
十年的索县网站建设经验,针对设计、前端、开发、售后、文案、推广等六对一服务,响应快,48小时及时工作处理。成都全网营销的优势是能够根据用户设备显示端的尺寸不同,自动调整索县建站的显示方式,使网站能够适用不同显示终端,在浏览器中调整网站的宽度,无论在任何一种浏览器上浏览网站,都能展现优雅布局与设计,从而大程度地提升浏览体验。创新互联从事“索县网站设计”,“索县网站推广”以来,每个客户项目都认真落实执行。
Linux 是一种自由、开放源代码的操作系统,能够自由获取、查看和修改源代码。在 Linux,IO 多路复用是一个十分重要的概念,可以大大提高服务器的效率和性能。本文将深度探究 Linux IO 多路复用的实验过程和原理。
一、什么是 IO 多路复用
IO 多路复用是指在一个进程中,同时监视多个文件描述符,对于文件描述符中的任意一种 IO 准备就绪,都能够进行相应的操作。这意味着一个进程可以同时处理多个 I/O 请求,不仅避免了不必要的 I/O 轮询,也可以将多个 I/O 请求交给操作系统同时处理,达到更大的并发能力,提高了系统性能。在 Linux 中,常见的 IO 多路复用函数有 select,poll 和 epoll。
二、实验环境
为了深入了解 Linux IO 多路复用,我们需要先准备好实验环境,本文使用的实验环境如下:
1. 操作系统:Ubuntu 16.04.6 LTS
2. 编译器:gcc 5.4.0
3. 多路复用函数:epoll
三、实验过程
1. 了解 epoll 函数
在开始实验之前,我们需要先了解 epoll 函数的使用方法和参数。epoll 函数是 Linux 中一个高效的 IO 多路复用机制,可以监控多个文件描述符上的 I/O 事件并且能够对这些 I/O 事件进行快速的响应。epoll 函数包括三个主要的 API:
(1)epoll_create() 函数:创建 epoll 的文件描述符。
(2)epoll_ctl() 函数:添加、修改、删除需要监控的文件描述符。
(3)epoll_wt() 函数:等待事件的发生。
2. 创建 epoll 实例
在我们开始使用 epoll 函数进行 IO 多路复用之前,需要先使用 epoll_create() 函数创建一个 epoll 实例。代码如下:
“`c
#include
int epoll_create(int size);
“`
其中 size 参数是 epoll 实例的大小,它很重要,因为它影响了 epoll 实例所使用的内存大小。创建成功后,该函数的调用将返回一个大于0的整数表示 epoll 实例的文件描述符,否则返回-1。
3. 将文件描述符添加到 epoll 实例中
创建好了 epoll 实例之后,我们需要使用 epoll_ctl() 将需要监控的文件描述符添加到 epoll 实例中。代码如下:
“`c
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
“`
其中:
(1)epfd:指定需要操作的 epoll 实例的文件描述符。
(2)op:用于指定需要执行的操作。此参数可以有以下三种值:
- EPOLL_CTL_ADD:将 fd 加入监听的事件中。
- EPOLL_CTL_DEL:将 fd 从监听的事件中删除。
- EPOLL_CTL_MOD:修改 fd 上的监听事件。
(3)fd:需要添加或修改监听事件的文件描述符。
(4)event:需要注册的事件,它是一个结构体类型,可以通过以下代码进行定义:
“`c
struct epoll_event {
__uint32_t events; // epoll 事件类型
epoll_data_t data; // 用户数据
};
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
“`
在实际使用中,最常用的是 EPOLLIN 和 EPOLLOUT 两种事件。EPOLLIN 事件表明文件描述符可以对应的文件被读,EPOLLOUT 事件表示文件描述符可以对应的文件可以被写。在监听事件触发时,用户可以通过 epoll_wt() 获得相应的文件描述符。
4. 等待事件发生
我们使用 epoll_wt() 等待事件的发生,代码如下:
“`c
int epoll_wt(int epfd, struct epoll_event *events, int maxevents, int timeout);
“`
其中:
(1)epfd:需要等待的 epoll 实例的文件描述符。
(2)events:存放满足监听事件的数组,一般将该数组定义为 epoll_event 类型,其大小由参数 maxevents 传递。
(3)maxevents:events 数组的大小,即最多可以同时等到多少个文件描述符。
(4)timeout:等待的超时时间,单位为毫秒。
epoll_wt() 函数返回满足事件的文件描述符的数量。当函数返回 0 时,表示超时;当函数返回 -1 时,表示 epoll_wt() 函数发生了错误,这时可以通过 errno 来获得错误码。
四、实验结果
以下是基于 epoll 的多路复用程序的实现,并加入了测试代码,此代码的功能是从标准输入中读取输入并将其写到标准输出中。通过多路复用机制,可以同时处理来自标准输入和标准输出的 I/O 请求,达到更大的并发能力和性能。
“`c
#include
#include
#include
#include
#include
#define MAX_EVENTS 10
#define MAX_BUF_SIZE 1024
int mn(int argc, char *argv[]) {
struct epoll_event ev, events[MAX_EVENTS];
int listen_fd, epoll_fd, n;
char buf[MAX_BUF_SIZE];
char *message = “Hello, I/O multiplexing!”;
int str_len;
// 创建 epoll 实例
epoll_fd = epoll_create(MAX_EVENTS);
// 添加标准输入到 epoll 实例中
ev.events = EPOLLIN;
ev.data.fd = STDIN_FILENO;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);
// 添加标准输出到 epoll 实例中
ev.events = EPOLLOUT;
ev.data.fd = STDOUT_FILENO;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev);
// 等待事件的发生
while (1) {
n = epoll_wt(epoll_fd, events, MAX_EVENTS, 1000);
if (n
continue;
}
for (int i = 0; i
if (events[i].data.fd == STDIN_FILENO && events[i].events & EPOLLIN) {
// 从标准输入中读取输入
fgets(buf, MAX_BUF_SIZE, stdin);
// 打印读取到的输入
printf(“Read from stdin: %s”, buf);
} else if (events[i].data.fd == STDOUT_FILENO && events[i].events & EPOLLOUT) {
// 将字符串输出到标准输出
write(STDOUT_FILENO, message, strlen(message));
}
}
}
return 0;
}
“`
运行程序后,用户输入的内容将被输出。通过多路复用机制,可以同时处理来自标准输入和标准输出的 I/O 请求,达到更大的并发能力和性能。
五、结论
相关问题拓展阅读:
epoll是linux中IO多路复用的一种机制,I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。当然linux中IO多路复用不仅仅是epoll,其他多路复用机制还有select、poll,但是接下来介绍epoll的内核实现。
events可以是以下几个宏的:
epoll相比select/poll的优势
:
epoll相关的内核代码在fs/eventpoll.c文件中,下面分别分析epoll_create、epoll_ctl和epoll_wait三个函数在内核中的实现,分析所用linux内核源码为4.1.2版本。
epoll_create用于创建一个epoll的句柄,其在内核的系统实现如下:
sys_epoll_create:
可见,我们在调用epoll_create时,传入的size参数,仅仅是用来判断是否小于等于0,之后再也没有其他用处。
整个函数就3行代码,真正的工作还是放在sys_epoll_create1函数中。
sys_epoll_create -> sys_epoll_create1:
sys_epoll_create1 函数流程如下:
sys_epoll_create -> sys_epoll_create1 -> ep_alloc:
sys_epoll_create -> sys_epoll_create1 -> ep_alloc -> get_unused_fd_flags:
linux内核中,current是个宏,返回的是一个task_struct结构(我们称之为进程描述符)的变量,表示的是当前进程,进程打开的文件资源保存在进程描述符的files成员里面,所以current->files返回的当前进程打开的文件资源。rlimit(RLIMIT_NOFILE) 函数获取的是当前进程可以打开的更大文件描述符数,这个值可以设置,默认是1024。
相关视频推荐:
支撑亿级io的底层基石 epoll实战揭秘
网络原理tcp/udp,网络编程epoll/reactor,面试中正经“八股文”
学习视频教程-腾讯课堂
需要更多C/C++ Linux服务器架构师学习资料加群
获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
__alloc_fd的工作是为进程在
sys_epoll_create -> sys_epoll_create1 -> ep_alloc -> get_unused_fd_flags -> __alloc_fd:
然后,epoll_create1会调用anon_inode_getfile,创建一个file结构,如下:
sys_epoll_create -> sys_epoll_create1 -> anon_inode_getfile:
anon_inode_getfile函数中首先会alloc一个file结构和一个dentry结构,然后将该file结构与一个匿名inode节点anon_inode_inode挂钩在一起,这里要注意的是,在调用anon_inode_getfile函数申请file结构时,传入了前面申请的eventpoll结构的ep变量,申请的file->private_data会指向这个ep变量,同时,在anon_inode_getfile函数返回来后,ep->file会指向该函数申请的file结构变量。
简要说一下file/dentry/inode,当进程打开一个文件时,内核就会为该进程分配一个file结构,表示打开的文件在进程的上下文,然后应用程序会通过一个int类型的文件描述符来访问这个结构,实际上内核的进程里面维护一个file结构的数组,而文件描述符就是相应的file结构在数组中的下标。
dentry结构(称之为“目录项”)记录着文件的各种属性,比如文件名、访问权限等,每个文件都只有一个dentry结构,然后一个进程可以多次打开一个文件,多个进程也可以打开同一个文件,这些情况,内核都会申请多个file结构,建立多个文件上下文。但是,对同一个文件来说,无论打开多少次,内核只会为该文件分配一个dentry。所以,file结构与dentry结构的关系是多对一的。
同时,每个文件除了有一个dentry目录项结构外,还有一个索引节点inode结构,里面记录文件在存储介质上的位置和分布等信息,每个文件在内核中只分配一个inode。 dentry与inode描述的目标是不同的,一个文件可能会有好几个文件名(比如链接文件),通过不同文件名访问同一个文件的权限也可能不同。dentry文件所代表的是逻辑意义上的文件,记录的是其逻辑上的属性,而inode结构所代表的是其物理意义上的文件,记录的是其物理上的属性。dentry与inode结构的关系是多对一的关系。
sys_epoll_create -> sys_epoll_create1 -> fd_install:
总结epoll_create函数所做的事:调用epoll_create后,在内核中分配一个eventpoll结构和代表epoll文件的file结构,并且将这两个结构关联在一块,同时,返回一个也与file结构相关联的epoll文件描述符fd。当应用程序操作epoll时,需要传入一个epoll文件描述符fd,内核根据这个fd,找到epoll的file结构,然后通过file,获取之前epoll_create申请eventpoll结构变量,epoll相关的重要信息都存储在这个结构里面。接下来,所有epoll接口函数的操作,都是在eventpoll结构变量上进行的。
所以,epoll_create的作用就是为进程在内核中建立一个从epoll文件描述符到eventpoll结构变量的通道。
epoll_ctl接口的作用是添加/修改/删除文件的监听事件,内核代码如下:
sys_epoll_ctl:
根据前面对epoll_ctl接口的介绍,op是对epoll操作的动作(添加/修改/删除事件),ep_op_has_event(op)判断是否不是删除操作,如果op != EPOLL_CTL_DEL为true,则需要调用copy_from_user函数将用户空间传过来的event事件拷贝到内核的epds变量中。因为,只有删除操作,内核不需要使用进程传入的event事件。
接着连续调用两次fdget分别获取epoll文件和被监听文件(以下称为目标文件)的file结构变量(备注:该函数返回fd结构变量,fd结构包含file结构)。
接下来就是对参数的一些检查,出现如下情况,就可以认为传入的参数有问题,直接返回出错:
当然下面还有一些关于操作动作如果是添加操作的判断,这里不做解释,比较简单,自行阅读。
在ep里面,维护着一个红黑树,每次添加注册事件时,都会申请一个epitem结构的变量表示事件的监听项,然后插入ep的红黑树里面。在epoll_ctl里面,会调用ep_find函数从ep的红黑树里面查找目标文件表示的监听项,返回的监听项可能为空。
接下来switch这块区域的代码就是整个epoll_ctl函数的核心,对op进行switch出来的有添加(EPOLL_CTL_ADD)、删除(EPOLL_CTL_DEL)和修改(EPOLL_CTL_MOD)三种情况,这里我以添加为例讲解,其他两种情况类似,知道了如何添加监听事件,其他删除和修改监听事件都可以举一反三。
为目标文件添加监控事件时,首先要保证当前ep里面还没有对该目标文件进行监听,如果存在(epi不为空),就返回-EEXIST错误。否则说明参数正常,然后先默认设置对目标文件的POLLERR和POLLHUP监听事件,然后调用ep_insert函数,将对目标文件的监听事件插入到ep维护的红黑树里面:
sys_epoll_ctl -> ep_insert:
前面说过,对目标文件的监听是由一个epitem结构的监听项变量维护的,所以在ep_insert函数里面,首先调用kmem_cache_alloc函数,从slab分配器里面分配一个epitem结构监听项,然后对该结构进行初始化,这里也没有什么好说的。我们接下来看ep_item_poll这个函数调用:
sys_epoll_ctl -> ep_insert -> ep_item_poll:
ep_item_poll函数里面,调用目标文件的poll函数,这个函数针对不同的目标文件而指向不同的函数,如果目标文件为套接字的话,这个poll就指向sock_poll,而如果目标文件为tcp套接字来说,这个poll就是tcp_poll函数。虽然poll指向的函数可能会不同,但是其作用都是一样的,就是获取目标文件当前产生的事件位,并且将监听项绑定到目标文件的poll钩子里面(最重要的是注册ep_ptable_queue_proc这个poll callback回调函数),这步操作完成后,以后目标文件产生事件就会调用ep_ptable_queue_proc回调函数。
接下来,调用list_add_tail_rcu将当前监听项添加到目标文件的f_ep_links链表里面,该链表是目标文件的epoll钩子链表,所有对该目标文件进行监听的监听项都会加入到该链表里面。
然后就是调用ep_rbtree_insert,将epi监听项添加到ep维护的红黑树里面,这里不做解释,代码如下:
sys_epoll_ctl -> ep_insert -> ep_rbtree_insert:
前面提到,ep_insert有调用ep_item_poll去获取目标文件产生的事件位,在调用epoll_ctl前这段时间,可能会产生相关进程需要监听的事件,如果有监听的事件产生,(revents & event->events 为 true),并且目标文件相关的监听项没有链接到ep的准备链表rdlist里面的话,就将该监听项添加到ep的rdlist准备链表里面,rdlist链接的是该epoll描述符监听的所有已经就绪的目标文件的监听项。并且,如果有任务在等待产生事件时,就调用wake_up_locked函数唤醒所有正在等待的任务,处理相应的事件。当进程调用epoll_wait时,该进程就出现在ep的wq等待队列里面。接下来讲解epoll_wait函数。
总结epoll_ctl函数:该函数根据监听的事件,为目标文件申请一个监听项,并将该监听项挂人到eventpoll结构的红黑树里面。
epoll_wait等待事件的产生,内核代码如下:
sys_epoll_wait:
首先是对进程传进来的一些参数的检查:
参数全部检查合格后,接下来就调用ep_poll函数进行真正的处理:
sys_epoll_wait -> ep_poll:
ep_poll中首先是对等待时间的处理,timeout超时时间以ms为单位,timeout大于0,说明等待timeout时间后超时,如果timeout等于0,函数不阻塞,直接返回,小于0的情况,是永久阻塞,直到有事件产生才返回。
当没有事件产生时((!ep_events_available(ep))为true),调用__add_wait_queue_exclusive函数将当前进程加入到ep->wq等待队列里面,然后在一个无限for循环里面,首先调用set_current_state(TASK_INTERRUPTIBLE),将当前进程设置为可中断的睡眠状态,然后当前进程就让出cpu,进入睡眠,直到有其他进程调用wake_up或者有中断信号进来唤醒本进程,它才会去执行接下来的代码。
如果进程被唤醒后,首先检查是否有事件产生,或者是否出现超时还是被其他信号唤醒的。如果出现这些情况,就跳出循环,将当前进程从ep->wp的等待队列里面移除,并且将当前进程设置为TASK_RUNNING就绪状态。
如果真的有事件产生,就调用ep_send_events函数,将events事件转移到用户空间里面。
sys_epoll_wait -> ep_poll -> ep_send_events:
ep_send_events没有什么工作,真正的工作是在ep_scan_ready_list函数里面:
sys_epoll_wait -> ep_poll -> ep_send_events -> ep_scan_ready_list:
ep_scan_ready_list首先将ep就绪链表里面的数据链接到一个全局的txlist里面,然后清空ep的就绪链表,同时还将ep的ovflist链表设置为NULL,ovflist是用单链表,是一个接受就绪事件的备份链表,当内核进程将事件从内核拷贝到用户空间时,这段时间目标文件可能会产生新的事件,这个时候,就需要将新的时间链入到ovlist里面。
仅接着,调用sproc回调函数(这里将调用ep_send_events_proc函数)将事件数据从内核拷贝到用户空间。
sys_epoll_wait -> ep_poll -> ep_send_events -> ep_scan_ready_list -> ep_send_events_proc:
ep_send_events_proc回调函数循环获取监听项的事件数据,对每个监听项,调用ep_item_poll获取监听到的目标文件的事件,如果获取到事件,就调用__put_user函数将数据拷贝到用户空间。
回到ep_scan_ready_list函数,上面说到,在sproc回调函数执行期间,目标文件可能会产生新的事件链入ovlist链表里面,所以,在回调结束后,需要重新将ovlist链表里面的事件添加到rdllist就绪事件链表里面。
同时在最后,如果rdlist不为空(表示是否有就绪事件),并且由进程等待该事件,就调用wake_up_locked再一次唤醒内核进程处理事件的到达(流程跟前面一样,也就是将事件拷贝到用户空间)。
到这,epoll_wait的流程是结束了,但是有一个问题,就是前面提到的进程调用epoll_wait后会睡眠,但是这个进程什么时候被唤醒呢?在调用epoll_ctl为目标文件注册监听项时,对目标文件的监听项注册一个ep_ptable_queue_proc回调函数,ep_ptable_queue_proc回调函数将进程添加到目标文件的wakeup链表里面,并且注册ep_poll_callbak回调,当目标文件产生事件时,ep_poll_callbak回调就去唤醒等待队列里面的进程。
总结一下epoll该函数: epoll_wait函数会使调用它的进程进入睡眠(timeout为0时除外),如果有监听的事件产生,该进程就被唤醒,同时将事件从内核里面拷贝到用户空间返回给该进程。
关于linuxio多路复用实验的介绍到此就结束了,不知道你从中找到你需要的信息了吗 ?如果你还想了解更多这方面的信息,记得收藏关注本站。
香港服务器选创新互联,2H2G首月10元开通。
创新互联(www.cdcxhl.com)互联网服务提供商,拥有超过10年的服务器租用、服务器托管、云服务器、虚拟主机、网站系统开发经验。专业提供云主机、虚拟主机、域名注册、VPS主机、云服务器、香港云服务器、免备案服务器等。
当前标题:深度探究linuxio多路复用的实验过程与原理(linuxio多路复用实验)
当前网址:http://www.shufengxianlan.com/qtweb/news32/279932.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联