Web开发来一发(十五)Linux IO模式和epoll原理

一、基础概念

先操作系统的一些基础概念做一下解释,便于后续内容的理解。

1、用户空间和内核空间

操作系统为了保证内核程序运行不会被用户程序破坏,对虚拟内存分成了两部分:和用户空间内核空间。用户程序只能访问用户空间,如果需要访问内核空间,则必须先交出控制权,由内核程序来访问内核空间,再将数据拷贝到用户空间。

比如32位的操作系统,其寻址空间为4G(2的32次方),Linux将地址高的1G交由内核使用,3G留给用户程序。

2、文件描述符

文件描述符 File descriptor,即fd,是一个指向文件操作记录的索引。当程序对文件进行操作的时候,内核就会向程序返回一个文件描述符。

3、缓存I/O

缓存I/O是指数据先会进入操作系统内核的缓冲区,然后才从缓冲区拷贝到应用程序的地址空间。

缓存I/O也是为了避免应用程序直接操作硬件资源,但会带来CPU内存开销,也会影响处理效率。

二、I/O模式

由于缓存I/O的存在,数据操作会分为两个阶段:

1)等待数据准备,数据先被拷到内核缓冲区

2)将数据从内核缓冲区拷贝到用户空间

下面介绍Linux具体的四种网络模式:同步阻塞I/O(Blocking IO,BIO)、同步非阻塞 I/O(nonblocking IO, NIO)、I/O 多路复用( IO multiplexing)、异步 I/O(asynchronous IO,AIO)。

1、BIO 同步阻塞I/O

BIO 是指用户程序会一直阻塞直到上述两个阶段执行完成。

2、NIO 同步非阻塞I/O

NIO 是指用户程序发出read操作时,直接返回结果,如果数据没有准备好,就会返回错误。因此用户需要不断主动询问是否已准备好数据。

3、I/O 多路复用

I/O 多路复用网络编程中的概念,就是下文将要讲的select、epoll做的事情,是为了解决同时监听多个socket的问题。

如果要同时监听多个socket,原本可以用多线程加阻塞I/O的方式实现,但不断增加线程会很快耗尽系统资源;而I/O多路复用的思路就是用单线程处理多个网络I/O,以select模式为例,用户进程只会阻塞在调用select函数,一但有数据准备好,select模式就可以继续进行数据读取,该模式在后文详解。

4、AIO 异步I/O

AIO 是指用户程序发出read操作后,直接去做其他事了,当内核将数据拷到用户空间后,会通知用户程序操作已完成。

NIO 和 AIO 的区别

NIO 和 AIO 的区别在于,NIO 用户程序持续关心结果,因此虽然用户程序不会被阻塞,但仍需要主动调用去通知内核程序处理数据;而 AIO 用户程序是不直接关心处理进度,直到内核将数据处理完,通知用户程序就好了。

三、简单网络程序执行过程

一个简单的TCP服务端伪代码如下:

// 创建socket
int s = socket(...);   
// 绑定端口等
bind(s, ...)
// 监听socket
listen(s, ...)
// 接受客户端连接
int c = accept(s, ...)
// 接收客户端数据
recv(c, ...);
// 数据处理程序
...

recv是阻塞方法,当执行到recv的时候,应用程序进程会进入阻塞状态,并加入到socket的等待队列(等待队列可以理解为socket对象里的一个列表,当socket接收到数据,会通知队列成员)。当有数据到来时,网络数据会写入内存,网卡发起中断信号,CPU执行中断程序,将数据写入socket缓冲区,并唤醒应用程序,应用程序从等待队列进入工作队列,从阻塞态转为就绪态,得到系统资源后执行读程序。

由于应用程序监听某个socket的时候会阻塞在recv,因此无法监听多个socket,为了处理这个问题,出现了select、poll和epoll,poll相对于select改进不大,因此不做介绍了。

四、select

select处理的伪代码如下:

// 多个socket
int s1 = socket(...); 
bind(s1, ...);
listen(s1, ...);

int s2 = socket(...); 
bind(s2, ...);
listen(s2, ...);

...

int fds[] = {s1, s2, ...};
while(1){
    int n = select(..., fds, ...)
    for(int i=0; i < fds.count; i++){
        if(FD_ISSET(fds[i], ...)){
            // 处理有数据的socket
        }
    }}

应用程序创建n个socket后,用一个文件描述符数据fds存放所有需要监听的socket,然后调用select函数,此时应用程序会阻塞。注意这时应用程序会被加入所有socket的等待队列(上文讲了每个socket都有一个等待队列),当有网络数据到来时,应用程序会被内核唤醒,然后遍历所有socket,处理对应socket的数据。

select用单个线程处理了所有socket的监听,但有两个问题:

1)将socket添加到fds数组效率很低,需要遍历;

2)处理数据需要遍历所有socket才知道哪个socket要处理;

epoll针对上述两点做了处理。

五、epoll

epoll处理伪代码如下:

// 创建n个socket
int s = socket(...);   
bind(s, ...)
listen(s, ...)
...

// 创建epoll对象eventpoll
int eventpoll = epoll_create(...);

// 将所有需要监听的socket添加到eventpoll中
epoll_ctl(eventpoll, ...); 

while(1){
    // 此处阻塞
    int n = epoll_wait(...)

    // 遍历需要处理的socket
}

如上,epoll有三步:

1)调用epoll_create创建eventpoll对象,eventpoll有一个监听着socket的红黑树和一个就绪列表rdllist。

2)调用epoll_ctl将所有需要监听的socket添加到event对象中;

3)调用epoll_wait阻塞进程,观察就绪列表rdllist是否为空,当就绪列表rdllist为空表示没有数据,继续sleep;当就绪列表有不空,就处理rdllist中socket的数据。

epoll针对select问题的两点优化:

1、epoll将fds数据改为了红黑树,查找和删除效率提高很多;

2、epoll将就绪的socket存在rdllist中,不需要遍历所有的socket。rdllist结构是双向链表,也是为了提高插入、删除效率。

 

非常赞的几篇参考:

I/O模式 https://segmentfault.com/a/1190000003063859

select 和 epoll https://www.jianshu.com/p/e6b9481ca754

epoll https://blog.csdn.net/daaikuaichuan/article/details/83862311

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值