Linux网络编程(四)

1.IO多路复用(IO多路转接)

I/O多路复用使得程序能同时监听多个文件描述符,能够提高程序的性能,Linux 下实现 I/O 多路复用的系统调用主要有 selectpollepoll

BIO(blocking IO)模型

通过多线程或进程处理多个客户端,每个线程/进程对应一个客户端
在这里插入图片描述

NIO模型

在这里插入图片描述

IO多路转接技术

select/poll:
在这里插入图片描述
epoll:
在这里插入图片描述

2.select

select原理

  1. 首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中;
  2. 调用一个系统函数(函数是阻塞的),监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行I/O操作时,该函数才返回(函数对文件描述符的检测的操作是由内核完成的)。
  3. 在返回时,函数会告诉进程有多少(哪些)描述符要进行I/O操作。

select相关函数

// sizeof(fd_set) = 128 1024 
#include <sys/time.h> 
#include <sys/types.h> 
#include <unistd.h> 
#include <sys/select.h> 
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 
    参数: 
         - nfds : 委托内核检测的最大文件描述符的值 + 1 
         - readfds : 要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性 
                     - 一般只检测读操作 
                     - 对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区 
                     - 是一个传入传出参数 
         - writefds : 要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性 
                      - 委托内核检测写缓冲区是不是还可以写数据(不满的就可以写) 
         - exceptfds : 检测发生异常的文件描述符的集合
         - timeout : 设置的超时时间
                     struct timeval { 
                            long tv_sec; /* seconds */ 
                            long tv_usec; /* microseconds */ 
                     };
                     - NULL : 永久阻塞,直到检测到了文件描述符有变化 
                     - tv_sec = 0 tv_usec = 0, 不阻塞 
                     - tv_sec > 0 tv_usec > 0, 阻塞对应的时间 
         - 返回值 : - -1 : 失败 
                   - >0(n) : 检测的集合中有n个文件描述符发生了变化

// 将参数文件描述符fd对应的标志位设置为0 
void FD_CLR(int fd, fd_set *set); 
// 判断fd对应的标志位是0还是1, 返回值 : fd对应的标志位的值,0,返回0, 1,返回1 
int FD_ISSET(int fd, fd_set *set); 
// 将参数文件描述符fd 对应的标志位,设置为1 
void FD_SET(int fd, fd_set *set);
// fd_set一共有1024 bit, 全部初始化为0 
void FD_ZERO(fd_set *set);

select()工作过程分析

客户端A、B、C、D对应的描述符为3、4、100、101,并连接到服务器,
假设要检测读时间,那么首先创建fd_set_reads集合,fd_set有1024个比特位,每一个比特位对应一个文件描述符;
要检测这四个客户端是否有数据到达,就通过FD_SET()将客户端对应的描述符的标志位置为1,然后调用select(),将fd_set从用户态拷贝到内核态;
假设A、B发送了数据,那么内核从0遍历到101,当遍历到3-4时发现有数据,就将3-4置为1,当遍历到100-101时发现没有数据,就将100-101置为0;
内核检测完成后将fd_set从内核态拷贝到用户态,遍历fd_set发现3-4发生变化、接收到了数据。
在这里插入图片描述

在这里插入图片描述

select()缺点

在这里插入图片描述

3.poll

poll相关函数

对select()的改进,实现原理类似

#include <poll.h> 
struct pollfd { 
    int fd;         //委托内核检测的文件描述符
    short events;   //委托内核检测文件描述符的什么事件
    short revents;  //文件描述符实际发生的事件
};

例如:
struct pollfd myfd; 
myfd.fd = 5; 
myfd.events = POLLIN | POLLOUT; 

int poll(struct pollfd *fds, nfds_t nfds, int timeout); 
参数:
     - fds : 是一个struct pollfd 结构体数组,需要检测的文件描述符的集合 
     - nfds : 第一个参数数组中最后一个有效元素的下标 + 1 
     - timeout : 阻塞时长 
                 0 : 不阻塞 
                 -1 : 阻塞,当检测到需要检测的文件描述符有变化,解除阻塞 
                 >0 : 阻塞的时长 
返回值: 
       -1 : 失败 
       >0(n) : 成功,n表示检测到集合中有n个文件描述符发生变化

在这里插入图片描述

poll()缺点

poll()改进了select()支持的文件描述符数量太小和fds集合不能重用的缺点,但依然每次调用poll()时需要把fd集合从用户态拷贝到内核态,并且在内核态遍历传递进来的所有fd.

4.epoll()

epoll原理

通过epoll_create在内核创建一个epoll实例(eventpoll结构体类型),返回值是一个文件描述符,可以通过文件描述符使用epoll API操作内核中的epoll实例;
通过epoll_ctl()委托内核向eventpoll数据结构中添加要检测数据发送改变的文件描述符的信息,在eventpoll数据结构中,有两个重要的成员:rbr是需要检测的文件描述符的信息(红黑树)rdlist是就绪列表,存放检测到数据发生改变的文件描述符信息(双向链表)
通过epoll_wait()委托内核检测eventpoll数据结构中是否有数据发生改变,如果有就发生改变的文件描述符信息存储到rdlist中,并将改变了的数据返回;
最后通过for循环遍历数据发生改变的数据进行读写操作。
在这里插入图片描述

epoll相关函数

#include <sys/epoll.h> 
int epoll_create(int size); 
参数:
     size : 没有意义,任意一个大于0的数都可 
返回值: -1 : 失败 
        > 0 : 文件描述符,操作epoll实例的

// 对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息 
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 
    参数: 
         - epfd : epoll实例对应的文件描述符 
         - op : 要进行什么操作 
                EPOLL_CTL_ADD: 添加 
                EPOLL_CTL_MOD: 修改 
                EPOLL_CTL_DEL: 删除 
         - fd : 要检测的文件描述符 
         - event : 检测文件描述符什么事情,读还是写

struct epoll_event { 
    uint32_t events;      //Epoll检测事件
    epoll_data_t data;    //用户数据信息
};
常见的Epoll检测事件: 
   - EPOLLIN
   - EPOLLOUT 
   - EPOLLERR 
typedef union epoll_data { 
    void *ptr; 
    int fd; 
    uint32_t u32; 
    uint64_t u64; 
} epoll_data_t; 

// 检测函数 
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 
    参数:
         - epfd : epoll实例对应的文件描述符 
         - events : 传出参数,保存了发送了变化的文件描述符的信息(与epoll_ctl不一样)
         - maxevents : 第二个参数结构体数组的大小 
         - timeout : 阻塞时间 
                     0 : 不阻塞 
                     -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞 - 
                     > 0 : 阻塞的时长(毫秒) 
返回值: - 成功,返回发送变化的文件描述符的个数 > 0 
        - 失败,返回-1

epoll的两种工作模式

LT模式(水平触发)

LT(level - triggered)是缺省的工作方式,并且同时支持 block 和 no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,内核还是会继续通知你的。

示例:
假设委托内核检测读事件 -> 检测fd的读缓冲区
读缓冲区有数据 - > epoll检测到了会给用户通知
a.用户不读数据,数据一直在缓冲区,epoll 会一直通知
b.用户只读了一部分数据,epoll会通知
c.缓冲区的数据读完了,不通知

ET模式(边沿触发)

ET(edge - triggered)是高速工作方式,只支持 no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个 fd 作 IO 操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。

ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

示例:
假设委托内核检测读事件 -> 检测fd的读缓冲区
读缓冲区有数据 - > epoll检测到了会给用户通知
a.用户不读数据,数据一致在缓冲区中,epoll下次检测的时候就不通知了
b.用户只读了一部分数据,epoll不通知
c.缓冲区的数据读完了,不通知

struct epoll_event { 
    uint32_t events;      //Epoll检测事件
    epoll_data_t data;    //用户数据信息
};
常见的Epoll检测事件: 
   - EPOLLIN
   - EPOLLOUT 
   - EPOLLERR
   - EPOLLET     //设置边沿触发

EPOLLONESHOT事件

即使使用 ET 模式,一个socket 上的某个事件还是可能被触发多次。这在并发程序中就会引起一个问题。比如一个线程在读取完某个socket上的数据后开始处理这些数据,而在数据的处理过程中该socket 上又有新数据可读(EPOLLIN 再次被触发),此时另外一个线程被唤醒来读取这些新的数据。于是就出现了两个线程同时操作一个 socket 的局面。一个socket连接在任一时刻都只被一个线程处理,可以使用epoll 的 EPOLLONESHOT 事件实现。

对于注册了 EPOLLONESHOT 事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次,除非我们使用 epoll_ctl 函数重置该文件描述符上注册的 EPOLLONESHOT 事件。这样,当一个线程在处理某个 socket 时,其他线程是不可能有机会操作该 socket 的。但反过来思考,注册了 EPOLLONESHOT 事件的 socket 一旦被某个线程处理完毕, 该线程就应该立即重置这个socket 上的 EPOLLONESHOT 事件,以确保这个 socket 下一次可读时,其 EPOLLIN 事件能被触发,进而让其他工作线程有机会继续处理这个 socket。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值