linux 网络编程 ---高级I/O

Posted 蚍蜉撼树谈何易

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux 网络编程 ---高级I/O相关的知识,希望对你有一定的参考价值。

什么是高级I/O

I/O分类


阻塞I/O:比如我们调用recv()接收客户端发来的消息时,此时若没有消息,则我们会阻塞在那里。
非阻塞型I/O:指的是当我们没有接收到消息时,我们暂时就不等了,但是非阻塞型I/O我们需要配合轮询检测。
信号驱动I/O: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作。

多路复用型I/O:区别于信号驱动I/O,核心在于IO多路转接能够同时等待多个文件描述符的就绪状态.

异步I/O:由内核在数据拷贝完成时, 通知应用程序。(它自己全程不参与)

阻塞I/O与非阻塞I/O

同步I/O与异步I/O

同步通信与异步通信对比

非阻塞型I/O–read举例代码

errno 是记录系统的最后一次错误代码。代码是一个int型的值,在errno.h中定义。查看错误代码errno是调试程序的一个重要方法。

EAGAIN:
从字面上来看,是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。例如,O_NONBLOCK的标志打开文件/socket/FIFO,如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。

 1 #include<iostream>
  2 #include<unistd.h>
  3 #include<fcntl.h>
  4 #include<errno.h>
  5 using namespace std;
  6 void set_tonblock(int fd)
  7 {
  8     int fl=fcntl(fd,F_GETFL);
  9     if(fl<0)
 10     {
 11         cout<<"fl error"<<endl;
 12         return;
 13     }
 14     fcntl(fd,F_SETFL,fl|O_NONBLOCK);
 15 
 16 
 17 }
 18 int main()
 19 {
 20     set_tonblock(0);
 21     char c;
 22     while(1)
 23     {
 24         sleep(1);
 25         ssize_t s=read(0,&c,1);
 26         if(s>0)
 27         {
 28             cout<<c<<endl;
 29         }                                                                                         
 30         else if(s<0&&errno!=0)
 31         {
 32             cout<<s<<endl;
 33             cout<<"errno ="<<errno<<endl;
 34         }
 35         else
 36         {
 37             cout<<"read error"<<endl;
 38         }
 39     }
 40     return 0;
 41 }

-1是read读取失败返回值,而errno =11,证明是没有输入数据所引起的错误。

select

接口:



rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合


返回值:
本质:就绪事件通知方式。

常用位图fd_set操作说明

void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位

select执行过程

理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节, fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd.
(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd= 5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1) (3)若再加入fd= 2, fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变0000,0011。注意:没有事件发生的fd=5被清空

select就绪条件

读就绪

socket内核中, 接收缓冲区中的字节数, 大于等于低水位标SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
监听的socket上有新的连接请求;

socket上有未处理的错误

写就绪

1)socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
2)socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
3)socket使用非阻塞connect连接成功或失败之后;
4)socket上有未读取的错误

select缺点

poll

接口介绍


events和revents取值:


select与poll对比

poll优点:
pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式. 接口使用比select更方便.
poll对比于select来说没有数量限制
poll 不用在每次检测后重新设置。
poll缺点:
1)底层仍旧采用轮询检测的方式,所以与select一样,不适用于海量文件描述符。

epoll

概念

是为处理大批量句柄而作了改进的poll.

接口函数

创建一个epoll的句柄,size可任意填

int epoll_create(int size);

返回值:文件描述符

epoll_ctl

也有可能是删除、修改,这里更正一下。

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

epoll的事件注册函数.

它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型.
第一个参数是epoll_create()的返回值(epoll的句柄).
第二个参数表示动作,用三个宏来表示.
第三个参数是需要监听的fd.
第四个参数是告诉内核需要监听什么事

第二个参数的取值:
EPOLL_CTL_ADD :注册新的fd到epfd中;
EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
EPOLL_CTL_DEL :从epfd中删除一个fd;


events可以是以下几个宏的集合:
EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
EPOLLOUT : 表示对应的文件描述符可以写;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR : 表示对应的文件描述符发生错误;
EPOLLHUP : 表示对应的文件描述符被挂断;
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要
再次把这个socket加入到EPOLL队列里

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

2,3个参数为输出型参数,内核告诉用户。
1)参数events是分配好的epoll_event结构体数组.
2)epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存).
3)maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size.
4)参数timeout是超时时间 (毫秒, 0会立即返回, -1是永久阻塞).
返回值:如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函数失败。

epoll原理

建议全文背诵~~哈哈。

epoll对应数据结构

struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};

1)每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件 .–epoll模型
2)这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度).
3)而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法. –
(2,3)可归结到epoll_ctl这个方法,其本质动作就是对红黑树执行插入,删除,修改操作。
4)这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中.
5)在epoll中,对于每一个事件,都会建立一个epitem结构体

struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}

6)当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可.
7)如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度是O(1).

epoll优点

接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.
没有数量限制: 文件描述符数目无上

epoll工作方式

水平触发Level Triggered 工作模式

epoll默认状态下就是LT工作模式.
当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.
直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.
支持阻塞读写和非阻塞读写

边缘触发Edge Triggered工作模式

1)如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式
2)当epoll检测到socket上事件就绪时, 必须立刻处理.
3)也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会.
4)ET的性能比LT性能更高( epoll_wait 返回的次数少了很多). Nginx默认采用ET模式使用epoll.
5)只支持非阻塞的读写

在它检测到有 I/O 事件时,通过 epoll_wait 调用会得到有事件通知的文件描述符,对于每一个被通知的文件描述符,如可读,则必须将该文件描述符一直读到空,让 errno 返回 EAGAIN 为止,否则下次的 epoll_wait 不会返回余下的数据,会丢掉事件。如果ET模式不是非阻塞的,那这个一直读或一直写势必会在最后一次阻塞。

两种触发对比


1)LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完
2)LT模式下文件的是否非阻塞没有多大的影响,而ET模式下必须非阻塞,要不然可能会阻塞。

3)ET的代码实现起来较为复杂相比LT来说

面试

select使用细节,select缺点?

poll缺点

poll与select对比

epoll高效的原因

epoll、poll、select对比

多路转接适用场景


以上是关于linux 网络编程 ---高级I/O的主要内容,如果未能解决你的问题,请参考以下文章

linux 查看当前的网络配置

linux网络设置

理解 Linux 网络栈 (Linux networking stack):Linux 网络协议栈简单总结

Linux网络协议:当eBPF遇上Linux内核网络 | Linux内核之旅

Kali Linux该怎么进行网络配置

linux如何消耗网络流量?