草稿

 
                                                    典型IO
 
五种典型的IO模型/多路转接IO模型:select/poll/epoll
五种典型IO模型:阻塞IO / 非阻塞IO / 信号驱动IO / 异步IO / 多路转接IO
    IO的过程就是等待与数据拷贝的过程
阻塞IO:发起IO调用后,若当前不具备IO条件则阻塞等待,
草稿
信号驱动IO:自定义一个IO信号的处理方式,收到IO信号则认为IO就绪,在信号的回调函数中发起IO调        用,相对于非阻塞IO,信号驱动IO,更加的灵活,更加的实时。但是流程也更加的复杂
 
当时用信号驱动IO的时候,由于是注册了信号处理函数,所以不需要配合循环来关注IO操作,并且内核将数据准备好,通过SIGIO来通知用户进程
 
异步IO:定义IO信号处理,发起异步IO调用,可以立即返回,IO的等待与数据拷贝过程都由操作系统内核完成;完成后通过信号通知进程。
草稿
 
这四种IO,从阻塞到非阻塞再到信号驱动最后到异步,这几个过程,对资源(cpu/内存)的利用率越来越高,同时能够发起更多的IO调用,让一个IO程序的效率越来越高,但是伴随着效率的越来越高,他们的流程越来越复杂。
 
阻塞与非阻塞的区别:发起一个调用之后,若不具备完成功能的条件,是否立即返回
 
非阻塞IO 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码,也就意味着,非阻塞的调用需要搭配循环来使用,换个实际的场景,也就是意味着,当用户发起一个read操作之后,如果内核没有准备好数据,则read返回;非阻塞IO虽然需要配合循环来使用,但是并不一定CPU使用率就很好,CPU使用率很高,意味着CPU在做大量的运算(逻辑运算或者算术运算),所以不一定CPU使用率会高
 
 
异步IO内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)
 
同步:为了完成一个功能而发起调用, 进程自己完成功能,功能没有完成之前流程无法继续向下。功能完成是串行的
    同步阻塞/同步非阻塞(waitpid):
异步:为了完成功能发起调用,但是这个功能并不由自己完成,由系统完成,完成后通知进程。功能什么时候完成自己也不知道
    异步阻塞/异步非阻塞:区别在于进程是否等待别人完成调用功能
 
同步与异步的区别:发起调用,是否由进程自身完成功能。
 
 
多路转接IO:对大量的IO(描述符)进行事件监控(IO-可读/可写/异常)判断哪些IO就绪了,若就绪了,则可以让进程仅针对就绪的IO进行操作。让进程避免对大量的未就绪的描述符进行操作,降低效率而进程仅仅针对就绪的描述符进行操作,则可以避免因为IO为就绪而导致的阻塞情况,并且仅针对就绪的描述符进行操作提高了效率。
 
 
 
 
 
 
 
 
 
 
四种IO各自的一个操作流程以及各自的特性优缺点。
阻塞:为了完成一个功能发起调用,
tcp服务器:基础版本中,一个服务器只能与一个客户端通信一次,就是因为程序会因recv/send/accept                      而阻塞,假如我们知到哪个描述符有数据到来了,然后再去recv/accept程序就不会因为recv                     没有数据/accept没有新连接而阻塞。
多路转接IO的实现模型:select模型(可以跨平台)/poll/模型(处于淘汰的边缘)/epoll模型(高性能)
 
select模型:对大量的描述符进行用户关心的事件(可读事件/可写事件/异常)监控,若有描述符就绪了用户关心的事件则返回,让用户对就绪的描述符进行操作。
 
描述符的可读事件:接收缓冲区中的数据大小大于低水位标记(基准值,默认为1字节)
描述符可写事件:发送缓冲区中的剩余空间大小大于低水位标记(基准值,默认1字节)
 
 
    实现流程:
        1、定义描述符事件集合(可读事件的描述符结合/可写事件的描述符集合/异常事件的描述符集合)
        2、用户在进程中,对哪个描述符关心哪个事件,则将这个描述符加入到指定的事件描述符集合中
        3、将这个集合拷贝到内核中,进行轮询遍历监控
        4、若某个集合中有描述符就绪了对应关心的事件,则返回,但是在返回前,会将所有集合中没有                    就绪的描述符从集合中删除掉 。(当select监控调用返回的时候,集合中只有就绪的描述符)
        5、用户判断哪个描述符还在集合中,哪个就是就绪的 ,就可以对这个描述符进行对应事件的操作
 
 select   
具体代码操作:
1、定义描述符事件集合 struct fd_set 这个结构体中只有一个数组成员变量, 这个数组被当成二进制位图使用;有多少位就可以添加多少描述符每个描述符都是一个数字,将描述符添加到集合中,就是将描述符这个位置对应的二进制置1。
这个数组中有多少二进制位,取决于宏‘FD_SETSIZE’默认是1024,表示默认的情况下select最多监控1024个描述符。 
    void FD_ZERO(fd_set *set); //初始化清空set集合
2、将描述符添加到所关心的事件集合中:void FD_SET(int fd,fd_set *set)//向set集合中添加放到描述符
3、将集合拷贝到内核中,进行轮询遍历,判断是否就绪;
    int select(int nfds,fd_set *readfds, fd_set *writefds ,fd_set *exceptfds ,struct timeout)
        nfds:集合中的最大描述符+1(为了防止空遍历,提高遍历效率);select是轮询遍历监控,集合默认大小是1024;通过指定最大描述符+1决定集合遍历大小,提高遍历效率     
        readfds:可读事件集合/writefds:可写事件集合/exceptfds:异常事件集合
        timeout:设置select是否阻塞/限制阻塞时间;
             NULL-若没有就绪就一直监控下去
             timeout内部数据为0-非阻塞 ,遍历一次若没有就绪则直接返回
             timeout设置阻塞时长:tv_sec/tv_usec 若在指定的时间内都没有就绪,则返回。
                    struct timeval tv; tv.tv_sec=3(秒); tv.tv_usec=0(微妙)
        返回值:>0返回值为就绪的描述符个数,==0返回值表示等待超时 ,没有就绪,<0返回值表示监控            出错
        每次遍历完毕:判断是否有就绪,若有就绪,将集合中的未就绪的描述符移除然后调用返回,若没有就绪,则判断是否监控等待超时,若超时则从集合中移除所有的描述符,然后调用返回,若没有超时,则休眠一会,重新遍历判断。
        在select返回的时候,所传入的集合中只保留已经就绪的描述符信息,(在返回之前将集合中所有                没有就绪的描述符从集合中移除)
        因为select每次都会改变传入的集合中的数据,因此每次重新监控之前都要重新向集合中添加描述符信息
4、select返回给用户就绪的描述符集合,用户在进程中通过遍历判断哪个描述符在集合中,得知描述符是就绪的,针对就绪的描述符进行操作
        int FD_ISSET(int fd,fd_set *set)   //判断描述符是否在集合中,返回值(以返回值真假表示是否在集合中,)
5、若某个描述符关闭了连接,则需要从集合中移除(不再监控它了)
        void FD_CLR(int fd ,fd_set *set) //从set集合中移除fd描述符
 
使用C++对select进行封装,使用封装后的代码,实现一个TCP并发服务器
并发:数据进行轮询处理(一个个判断谁就绪了,然后一个个去处理,CPU的分时机制)
并行:数据同时处理(若在单核心CPU的计算机上多线程/多进程其实也是并发,操作系统层面的均衡并发),
若有一万个描述符就绪了,我们自己使用的多路转接模型实现的并发,一个一个往下处理,采用多进程/多线程,操作系统以CPU分时机制 实现每个进程的轮询调度。
(几种多路转接模型之间的优缺点对比,这是面试中最常问的问题)
select的优缺点:
    1、select所能监控的描述符是由最大上限的,取决于宏_FD_SETSIZE,默认为1024个
    2、select的监控原理,是在内核中进行轮询遍历判断, 这种轮询遍历会随着描述符的增多而性能下降
    3、select返回的时候会移除所有未就绪描述符,给用户返回就绪的描述符集合,但是没有直接告诉用户哪个描述符就绪,需要程序员自己遍历哪个描述符在集合中,才能获知哪个描述符就绪,操作流程比较麻烦
    4、每次监控都需要重新将集合拷贝到内核中才能监控
优点:
select遵循POSIX标准,跨平台移植性比较好,监控的超时等待时间,可以精确到微妙。
    
#include <iostream>
#include <vector>
#include <sys/select.h>
#include <time.h>
#include "tcpsocket.hpp"
class Select
{
    public:
        Select():_maxfd(-1){
            FD_ZERO(&_rfds);//初始化清空集合
        }
        bool Add(TcpSocket &sock) {
            //将描述符添加到监控集合
            int fd = sock.GetFd();
            FD_SET(fd, &_rfds);// 将描述符添加到集合中
            _maxfd = _maxfd > fd ? _maxfd : fd;//每次添加监控重新判断最大描述符
            return true;
        }
        bool Del(TcpSocket &sock) {
            //从监控集合中移除描述符
            int fd = sock.GetFd();
            FD_CLR(fd, &_rfds);// 从集合中移除描述符 // 0 3 5 8
            if (fd != _maxfd) {//判断删除的描述符是否是最大的描述符
                return true;
            }
            for (int i = fd; i >= 0; i--) {//重新判断最大的描述符是多少
                if (FD_ISSET(i, &_rfds)) {
                    _maxfd = i;
                    break;
                }
            }
            return true;
        }
        bool Wait(std::vector<TcpSocket> *list, int mtimeout = 3000) {
            //开始监控,并返回就绪的socket信息
            struct timeval tv;
            tv.tv_usec = (mtimeout % 1000) * 1000;
            tv.tv_sec = mtimeout / 1000;//mtimeout 单位为毫秒
            //因为select会修改描述符集合,返回时将未就绪的描述符全部移除
            //因此不能直接使用_rfds,而是使用临时集合,避免对_rfds做出修改
            fd_set tmp_rfds = _rfds;
            int nfds = select(_maxfd + 1, &tmp_rfds, NULL, NULL, &tv);
            if (nfds < 0) {//监控出错
                perror("select error");
                return false;
            }else if (nfds == 0) {//监控等待超时
                printf("wait timeout\n");
                return true;
            }
            for (int i = 0; i <= _maxfd; i++) {//这是一种笨办法,从0到_maxfd逐个判断谁在集合中
                if (FD_ISSET(i, &tmp_rfds)) {     //谁在集合,谁就是就绪的
                    //就绪的描述符
                    TcpSocket sock;
                    sock.SetFd(i);
                    list->push_back(sock);
                }
            }
            return true;
        }
    private:
        int _maxfd;  //最大的描述符
        fd_set _rfds;//可读事件的描述符集合
};
 
poll模型:
    相较于select有一个好处:select将事件的监控分了多个集合但是poll采用了一种事件结构,关心什么事件添加标志位就可以,不需要定义多个集合,稍微简化了流程。
struct pollfd
{
    int fd; 用户关心的描述符
    short events;  针对fd描述符用户所关心的事件-POLLIN-可读 POLLOUT-可写 POLLIN | POLLOUT
    short revents;  这个成员用于保存当poll返回的时候,当前fd描述符就绪的事件
}
 
1、定义pollfd描述符事件数组(要监控多少描述符,数组就定义多大),将描述符的信息填充进去
     struct pollfd arr[1]  arr[0].fd = 0 arr[0].events = POLLIN 对标准输入监控可读事件
2、将数组拷贝到内核中进行轮询遍历监控
     int poll(struct pollfd *fds ,nfds_t nfds ,int timeout);
        fds:定义好的事件结构体数组,nfds:数字中有效的事件个数  timeout:监控超时等待时间,以毫秒为单位
        poll在返回时,会将各个描述符就绪的事件放到各自对应的事件结构节点的revents成员中
3、调用返回后,只需要根据事件结构数组中各个节点的revents成员就能判断就绪了什么事件,进而进            行相应的操作。
poll优缺点分析:
1、poll也需要每次将事件信息拷贝到内核中进行监控
2、poll监控的原理也是在内核进行轮询遍历判断,性能会随着描述符的增多而下降
3、仅支持linux,跨平台移植性比较差
4、poll也没有直接告诉用户哪个描述符就绪了哪个事件,需要用户遍历,通过节点的revents成员获知
优点:
1、采用事件结构方式对描述符进行事件监控,相较于select简化了很多流程
2、对监控的描述符数量没有上限(监控多少描述符就创建多大的结构体)
3、相较于select每次都重新添加描述符,他不需要去重新初始化事件节点。
 
epoll模型:linux下性能最高的事件通知工具(多路转接模型)
1、创建并初始化内核中的eventpoll结构
    int epoll_create(int size)
size:决定能够监控 多少个描述符,但是linux2.6.8后被忽略,但是必须大于0
        这个函数返回epoll的操作句柄(文件描述符),通过这个文件描述符,可以找到内核中的eventpoll结构体
2、若用户需要监控什么描述符的什么事件,则为描述符构建一个事件结构 struct epoll_event,添加到内核的eventpoll中。
struct epoll_event{
    uint32_t events ; 用户针对描述符所关心的事件 EPOLLIN- 可读 EPOLLOUT-可写
    typedef union epoll_data_t{
        int fd ; 用户需要监控的描述符
        void * ptr
    } data;     
}; 
            struct epoll_event ev; ev.events = EPOLLIN ev.data.fd = 0;对标准输入监控可读事件
    int epoll_ctl(int epfd ,int how ,int fd ,struct epoll_event * event);--通过这个结构将事件添加到内核中
epfd:epoll_create返回的epoll的操作句柄
how:EPOLL_CTL_ADD(添加事件) / EPOLL_CTL_DEL(移除事件) / EPOLL_CTL_MOD(修改事件),对内核中描述符的事件信息进行的操作
fd 要监控的描述符,
event:fd描述符对应的事件结构体信息,当这个描述符就绪了就会给进程返回这个事件结构体
 
struct epoll_event{
    uint32_t events ; 这个是 epoll_ctl参数fd描述符关心的事件-epoll监控返回时通过这个成员知道描述符对应就绪了什么事件,用户针对描述符所关心的事件 EPOLLIN- 可读 EPOLLOUT-可写
    typedef union epoll_data_t{
        int fd ; 用户需要监控的描述符,通过这个参数知道就绪的是哪个描述符
        void * ptr
    } data;     
}; 
 
epoll_ctl(epfd ,EPOLL_CTL_ADD, 0, &ev) 这一步将事件结构以及监控的描述符添加到内核中进行监控
                如果描述符0就绪了ev中定义的事件,就会给用户返回这个ev结构。用户就可以直接针对                                    ev.data.fd这个描述符进行操作。
epoll监控原理,是一种异步阻塞/非阻塞操作,将节点添加到内核中的eventpoll的红黑树中,一旦开始监控,若哪个描述符就绪,将这个描述符对应的epoll_event结构添加到双向链表当中并且这个过程操作系统采用事件回调方式完成,为每个描述符的就绪事件定义回调函数,让这个回调函数把描述符事件结构添加到双向链表中)。而我么的进程只需要判断双向链表中是否有节点就可以判断是否有序,并且可以直接获取到就绪描述符对应的事件结构信息epoll_event
3、开始监控
    内核中操作系统开始描述符的事件监控,进程自身只需要判断双向链表中是否有数据,就可以判断是否有就续事件。
int epoll_wait(int epfd struct epoll_event * evs int maxevent ,int timeout) 
 epfd :epoll操作句柄,通过这个句柄能够找到内核中的eventpoll结构
evs:struct epoll_event 结构体数组,用于获取双向链表中就绪的描述符对应的事件结构信息,添加数组内容的过程是由epoll_wait函数自己完成的。
maxevent:evs数组的节点个数,也是本次想要获取的就绪事件的最大个数。
timeout:超时事等待时间,毫秒为单位。
返回值:返回值大于0,表示就绪事件个数;返回值等于0,表示等待超时,返回值小于0,表述出错。
 
操作流程
1、在内核中创建epoll句柄
2、向内核中添加监控的描述符以及对应事件节点struct epoll_event
3、开始监控,给用户直接返回就绪的事件节点(异步阻塞操作:监控由系统完成,进程只判断是否有就绪)
4、用户直接针对就绪的事件进行操作
 
性能高的原因:
    epoll的监控只需要判断双向链表是否为空就可以完成判断,不需要遍历,因此性能不会随着描述符的增多而下降
    epoll的事件节点只需要向内核中添加一次,不需要重复添加
    直接给用户返回就绪的节点,用户不需要进行空遍历判断谁就绪
    epoll向用户返回就绪节点采用内存映射,避免了数据的拷贝过程
 
/*===============================================================
*   Copyright (C) . All rights reserved.")
*   文件名称:
*   创 建 者:zhang
*   创建日期:
*   描    述:封装epoll的基本操作;向外提供添加监控/移除监控/获取就绪信息操作
================================================================*/
#include <cstdio>
#include <iostream>
#include <vector>
#include <sys/epoll.h>
#include "tcpsocket.hpp"
class Epoll
{
    public:
        Epoll(): _epfd(-1){
            //epoll_create(int maxevent) // maxevent已经忽略了,非0就可以
            _epfd = epoll_create(1);
            if (_epfd < 0) {
                perror("epoll_create error");
                exit(-1);//因为构造函数没有返回值,不知道构造成功与否,因此失败直接退出
            }
        }
        bool Add(TcpSocket &sock){
            int fd = sock.GetFd();
            //epoll_ctl(epoll句柄,操作, 描述符, 对应的事件结构)
            struct epoll_event ev;
            ev.events = EPOLLIN | EPOLLET;//EPOLLIN-可读事件
            ev.data.fd = fd;
            int ret = epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &ev);
            if (ret < 0) {
                perror("epoll_ctl add error");
                return false;
            }
            return true;
        }
        bool Del(TcpSocket &sock) {
            int fd = sock.GetFd();
            int ret = epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, NULL);
            if (ret < 0) {
                perror("epoll_ctl del error");
                return false;
            }
            return true;
        }
        bool Wait(std::vector<TcpSocket> *list, int timeout = 3000){
            //epoll_wait(操作句柄,事件数组首地址,数组节点数量,超时时间)
            struct epoll_event evs[10];
            int ret = epoll_wait(_epfd, evs, 10, timeout);
            if (ret < 0) {
                perror("epoll wait error");
                return false;
            }else if (ret == 0) {
                printf("timeout\n");
                return true;
            }
            //就绪的事件都放在evs中,就绪的个数就是返回值
            for (int i = 0; i < ret; i++) {
                if (evs[i].events & EPOLLIN) {//判断就绪的事件是否是可读事件
                    //这里其实可以不用判断,因为我们的添加函数就只监控了可读事件
                    TcpSocket sock;
                    sock.SetFd(evs[i].data.fd);
                    list->push_back(sock);
                }
            }
            return true;
        }
    private:
        int _epfd;//epoll的操作句柄
};
 
事件的就绪是什么意思
    接收缓冲区中的数据大小大于低水位标记,则数据是可读的
    发送缓冲区中剩余的空间大小大于低水位标记,则数据是可写的。
epoll对描述符就绪事件的触发方式,
    水平触发:EPOLLT 默认的select和poll支持
        可读事件: 只要接收缓冲区中的数据大小大于低水位标记,就会触发可读事件就绪
        可写事件:只要发送缓冲区中的数据大小大于低水位标记,就会触发可写事件就绪
    边缘触发:EPOLLET   只有epoll支持
        可读事件:只有新数据到来的时候才会触发可读就绪事件(不管上一次数据有没有读完,缓冲区中有没有数据遗留)
        可写事件:只有发送缓冲区剩余空间从不可写变为可写的时候才会触发一次可写就绪事件。
 
主要是为了避免就绪事件的判断方式导致程序不断的对就绪事件进行大量的遍历操作
但是边缘触发,也就是要求我们一次性处理完所有的数据。否则剩余的数据不会触发就绪,要等到下一次就绪的到来才会处理。
 
设置描述符的非阻塞属性可以解决:
    int fcntl(int fd ,int cmd , /* arg*/);
fd:描述符;cmd :F_GETFL--获取已有的属性,通过返回值返回/F_SETFT--设置属性,通过arg参数设置
非阻塞属性:O_NONBLOCK 一个描述符若被设置为非阻塞,而接受数据的时候缓冲器没有数据则会报错 EAGAIN/EWOULDBLOCK,这个错是可以原谅的,return true就可以了,因为没有数据了。
 
优缺点分析:
1、监控的描述符数量没有上限
2、监控采用异步阻塞操作,性能不会随着描述符的增多而下降
3、直接给进程提供就绪的事件以及描述符操作,不需要进程进行空遍历
4、描述符的事件信息,只需要向内核拷贝一次
5、给进程返回就绪的事件信息,通过内存映射完成,节省了数据的拷贝过程
缺点:
无法跨平台
epoll的惊群问题
有少量的描述符,但是操作系统调用了大量的的等待线程
 
多路复用的需求让select,poll,epoll等事件模型更为受到欢迎,所谓的事件模型即阻塞在事件上而不是阻塞在事务上。内核仅仅通知发生了某件事,具体发生了什么事,则有处理进程或者线程自己来poll。如此一来,这个事件模型(无论其实现是select,poll,还是epoll)便可以一次搜集多个事件,从而满足多路复用的需求
 
ET事件发生仅通知一次的原因是只被添加到rdlist中一次,而LT可以有多次添加的机会,由于ET模式只通知一次的机制,所以在使用ET模式的收,需要搭配成非阻塞来使用
epoll理论上可以监控无限多的文件描述符,虽然每个进程有打开文件描述符的上线限制
tcp服务器的并发处理
    多进程/多线程/多路转接模型
多进程-资源消耗高
多线程-资源消耗较低,并且所有的描述符的处理通过操作系统层面的CPU分时技术实现均衡处理
多路转接模型: 用户在进程中对所有的描述符轮询处理 --最后一个描述符等待的时间比较长(需要用户考虑均衡处理问题),适用于有大量描述符需要监控,但是 同一时间有少量描述符活跃的场景。