2021-04-13 Linux I/O模型

一 什么是同步,什么是异步?

同步指的是发起I/O请求后,必须等待I/O响应,从内核缓冲区获取到数据,程序才能继续往下执行

异步指的是发起I/O请求后,无需等待I/O响应,程序继续往下执行,如果内核缓冲区数据准备好了则通知用户进程或者执行注册的回调函数

二 什么是阻塞,什么是非阻塞?

阻塞指的是I/O请求时,内核缓冲区没有数据,则需要阻塞等待数据准备好。注意这里也并不是一直阻塞,CPU时间分片到了,则会把请求放入到一个等待队列,然后去执行其他线程了,等待数据返回,触发中断,然后中断处理程序会将该线程移到运行队列,等待被CPU执行非阻塞指的是I/O请求时,内核缓冲区没有数据,则直接返回空或者异常

三 同步和阻塞有什么关系?异步和非阻塞有什么关系?

同步和阻塞是针对不同空间的概念,其实说的本质都是一样的。同步是站在用户空间角度说的,阻塞是站在内核空间角度讲的。
异步和异步是针对不同空间的概念,其实说的本质都是一样的。异步是站在用户空间角度说的,非阻塞是站在内核空间角度讲的

四 I/O模型

4.1 同步阻塞模型(Blocking I/O)

#1 用户程序调用read函数,CPU进入内核态,调用read系统调用
#2 CPU检查内核缓冲区是否有数据准备好
#3 如果有数据准备好,则直接返回,将数据拷贝到用户空间,让后往下执行;如果没有则阻塞等待。如果阻塞时候,时间片到期,CPU则会把线程从运行队列移出,放到等待队列。等待内核缓冲区有数据的时候,中断处理程序会将对应的线程从等待队列移到运行队列,等待被CPU执行

4.2 同步非阻塞模型(Non-Blocking I/O)

#1 用户程序调用read函数,CPU进入内核态,调用read系统调用
#2 CPU检查内核缓冲区是否有数据准备好
#3 如果有数据准备好,则直接返回,将数据拷贝到用户空间,让后往下执行;如果数据没有准备好,则返回一个错误
#4 用户程序收到是错误,则继续调用read函数,然后CPU继续检查内缓冲区数据是否以经准备好,继续重复

4.3 异步非阻塞调用模型(Asynchronous I/O )

#1 当用户进程执行read函数,不用等待返回结果,可以直接执行下面的程序
#2 内核缓冲区收到read系统调用,则检查内核缓冲区是否有数据准备好
#3 如果有,则向用户进程发送一个信号,通知用户进程数据已经准备好了;如果没有,则准备数据

4.4 多路复用模型(I/O Multiplexing)

4.4.1 为什么出现多路复用模型?什么是I/O多路复用? 工作原理是什么?

4.4.1.1 为什么出现多路复用模型

我们知道传统I/O模型,比如同步阻塞和同步非阻塞都存在问题。如果有多个并发请求,那么就有多个I/O线程可能会被创建,上下文频繁切换,可能导致C10K问题。

4.4.1.2 什么是I/O多路复用

多个I/O请求通过多路复用器注册文件描述符和描述符感兴趣的事件,然后多路复用器对注册的文件描述符进行监视,如果有事件就绪则通知用户进程,然后这样一个多路复用器可以处理多个I/O请求,极大的提升系统的性能。

4.4.1.3 多路复用工作原理

第一: 文件描述符向多路复用器注册,底层会调用多路复用的具体实现进行注册,比如poll就是在描述符集合表添加一个描述符和事件构成的结构体数组中,比如select根据事件类型,将文件描述符放到不同的描述符集合中
第二: 用户指定超时时间,在超时时间内,没有事件就绪的描述符,返回就绪事件数为0,则继续轮询;否则中断服务处理程序会将数据放到内核缓冲队列,然后从等待队列将对应的线程放到运行队列,等待被CPU执行,返回准备就绪的事件数量
第三: 用户进程收到就绪数量,select和poll因为不知道具体是哪些描述符已经就绪,所以需要遍历描述符集合

4.4.2 select

4.4.2.1 select函数
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

nfds:集合所允许的监视的文件描述符数量,包括关心读写以及异常事件的所有文件描述符数量,但是默认不超过1024. 比如现在有3个读事件socket 描述符, 2个写事件 socket 描述符,1个异常事件描述符, 那么总共就是3+2+1 = 6个文件描述符
readfds: 需要监控读事件的socket 描述符集合(既需要从内核缓冲区读数据socket 描述符集合)
writefds: 需要监控写事件的socket 描述符集合(既需要向内核缓冲区写数据的socket描述符集合)
exceptfds: 需要监控异常事件的socket 描述符集合
timeout: 轮询等待的超时时间,如果不指定超时时间,默认一直阻塞,直到有数据返回。一般在还需要selector线程做其他事情的
时候有用。

4.4.2.2 select工作原理(网络I/O)

#1 用户程序调用多路复用器(selector)的register注册函数,注册Socket描述符及其关注的事件
#2 CPU进入内核态,调用系统调用select, 然后根据事件类型,将文件描述符加入到对应的读写或者异常描述符集合
#3 多路复用器调用函数select, 开始监视注册的文件描述符,如果没有没有就绪的文件描述符,且没有指定超时时间则一直阻塞
#4 如果在CPU调度期间,Socket套接字缓冲区数据有数据可读或者可以写,则将bitmap中对应的Socket描述符状态置为1,然后返回到用户空间
#5 如果在CPU调度期间,Socket套接字缓冲区数据没有数据可读或者可以写,则CPU分片到期,则会把这个线程对象从运行队列移出,然后放到等待队列。待网卡收到数据之后,通过中断服务处理程序处理收到的缓冲区的数据,然后将等待队列中的线程移到就绪队列,等待被CPU调度
#6 用户进程收到返回的就绪Socket描述符数量,开始遍历所有的事件,判断每一个是否就绪,如果就绪则处理;没有则遍历下一个,遍历完了之后继阻塞等待,注意会将bitmap重新置为0

4.2.2.3 select 缺点

第一:文件描述符有限制,最多不能超过1024大小
第二:有就绪事件发生,因为不知道具体是哪些文件描述符有就绪事件发生,所以需要遍历全部描述符状态,性能低

4.4.3 poll

我们知道select函数有长度限制,最多只允许1024个文件描述符,如果想要修改这个值,则需要重新编译操作系统。所以poll可以设置文件描述符数量,且没有1024这个限制,提升I/O多路复用的处理能力

4.4.3.1 poll函数
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
    int   fd;         /* 文件描述符 */
    short events;     /* 请求事件 */
    short revents;    /* 返回事件 */
};

pollfd对象有3个属性:文件描述符、文件描述符感兴趣的事件、以及返回的事件
fds:是一个数组,表示需要监视的所有文件描述符
nfds:文件描述符数量限制
timeout:超时时间

4.4.3.2 poll工作原理(网络I/O)

#1 用户程序调用多路复用器(selector)的register注册函数,将文件描述符和感兴趣的事件注册到多路复用器,放到读事件集合、写事件集合和异常事件集合
#2 用户程序调用select函数
#3 调用系统调用select,CPU切换到内核态,遍历读描述符集合、写描述符集合等,检查是否有socket就绪,如果有则将bitmap对应socket状态置为1,然后返回就绪的socket数量;如果没有,则阻塞等待直到超时。
#4 如果一直阻塞,CPU也不可能一直调度,时间片到期,则会把这个线程从运行队列移到等待队列。如果网卡收到数据之后,通过中断服务处理程序处理收到的缓冲区的数据,然后将等待队列中的线程移到就绪队列,等待被CPU调度
#5 用户程序收到结果,如果到期了也没有就绪事件,则继续调用select函数,重复步骤2;如果收到,select不知道哪些socket描述符已经就绪,需要遍历描述符集合、写描述符集合等,判断bitmap中是否有状态为1的socket,如果不是1,则处理下一个;如果是1说明该socket描述符已经有就绪的事件

4.4.3.3 poll的缺点

poll函数仍然不知道返回的已经就绪的文件描述符是哪些,需要遍历整个pollfd数组,检查revent事件才可以直到,所以时间复杂度依然是O(N),性能依然不高

4.4.4 epoll

4.4.4.1 epoll函数
4.4.4.1.1 epoll_create
int epoll_create(int size);

创建epoll实例,并且返回一个epfd文件描述符。其实就是在内核申请一块空间存放一些数据,主要包括以下数据:
第一:需要监控的socket描述符的红黑树
第二:存储已就绪的socket链表
第三:存储socket等待队列,等待I/O事件就绪

4.4.4.1.2 epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

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;

这个函数主要是对监听的socket描述符进行维护,比如增删改。比如某一个socket之前注册了读事件,后面需要注册写事件
epfd:创建存储数据的epoll实例的文件描述符
op: 操作类型, 增加、删除还是修改
fd: 要操作的文件描述符
*event: 事件数组

4.4.4.1.3 epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

等待I/O事件就绪或者说等待就绪的socket
epfd:创建存储数据的epoll实例,返回的文件描述符
*events:一个空的事件数组,如果有就绪的Socket,则放入到这个数组中
maxevents: 限制了events数组最大数量,如果就绪的socket数量超过了maxevents,则会在后续处理,这样可以避免有大量客户端,但是只有很少的socket就绪,从而不得不遍历所有socket描述符带来的性能问题。我每次指定20,如果就绪事件没超过20,则也不会带来什么性能问题,如果超过20次,处理完了又继续处理呗。
timeout:超时时间,如果是-1,表示是阻塞等待;如果等于0表示不阻塞;如果大于0表示阻塞时长。

4.4.4.2 epoll工作原理

第一:创建epoll实例,返回一个文件描述符epfd,就是在内核空间开辟一块空间,存储一些数据,这个空间它主要包括三个重要的部分:
#1 存放添加的需要监控的socket描述符的红黑树
#2 存放已经就绪的socket链表
#3 还有一个存放调用epoll_wait的等待就绪事件的socket等待队列
第二:调用epoll_ctl,添加需要监控的socket描述符到红黑树
第三:调用epoll_wait,可以指定是否阻塞还是不阻塞,还需要传递一个空的epoll_event数组,用于从就绪列表中获取就绪的socket返回,因为为了限制每次需要进行O(N)的遍历,所以给这个数组设置了最大值maxevents,如果不超过这个对性能影响也不大,如果超过了则用户进程处理完了又来获取就完事了。如果就绪链表有就绪socket则获取然后装到epoll_event数组中,并且返回当前已经就绪socket数量

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

莫言静好、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值