目录
10.1.3 epoll超时检测 -epoll也可以实现超时时间检测
概述:
在Linux环境下,主要存在四种IO模型,它们分别是阻塞IO(Blocking IO)、非阻塞IO(Non-blocking IO)、IO多路复用(I/O Multiplexing)和异步IO(Asynchronous IO)。下面我将逐一介绍这些模型的定义:
1. 阻塞IO (Blocking IO)
阻塞IO是最传统的IO模型。当一个进程发起一个IO请求时,比如读或写操作,如果数据尚未准备好(例如在读操作中,数据尚未到达),那么这个进程会被挂起,直到数据准备好为止。这意味着进程在此期间不能做任何其他事情,直到IO操作完成。这是由于在内核态和用户态之间的切换,内核必须完成IO操作并将控制权交回给用户态的应用程序。
2. 非阻塞IO (Non-blocking IO)
非阻塞IO与阻塞IO的主要区别在于,当IO操作未完成时,进程不会被挂起。相反,如果数据尚未准备好,系统调用会立即返回一个错误。这允许应用程序检查错误并立即进行下一次尝试,而不是等待数据准备好。然而,这通常意味着应用程序需要不断轮询,直到数据可用,这可能会导致不必要的CPU使用。
3. IO多路复用 (I/O Multiplexing)
IO多路复用模型允许一个单一的进程同时监听多个文件描述符(如网络套接字)的IO事件。当其中一个文件描述符准备好进行IO操作时,应用程序会被通知。这通常通过select()
, poll()
, 或 epoll()
等系统调用来实现。这些函数会阻塞,直到至少有一个文件描述符准备好,然后返回,允许应用程序处理那些已经准备好的描述符。这种方式大大提高了处理多个并发连接的效率。
4. 信号驱动IO (Signal-driven IO)
信号驱动IO是一种异步IO机制,它允许应用程序在数据准备好时通过信号通知来处理IO事件。这种模型特别适合于多路复用场景,尤其是当处理大量并发连接时。
阻塞式IO
- 特点:最简单,最常用,但是效率低。
- 当前学习函数:
- 读阻塞:
read
,recv
,recvfrom
- 写阻塞:
write
,send
,accept
,connect
TCP: 有链接 : 有发送缓存区,有接收缓存区
UDP: 无连接 : 没有发送缓存区,但是有接受缓存区 (不会出现TCP粘包)
非阻塞式IO
- 特点:避免了长时间等待,但可能频繁检查资源状态,浪费CPU资源。
fcntl函数 声明: int fcntl (int fd, int cmd, ...arg); 头文件: #include<fcntl.h> #include<unistd.h> 功能:设置文件描述符的属性 参数:fd:文件描述符 cmd: 操作功能选项 (可以定义个变量,通过vi -t F_GETFL 来找寻功能赋值 ) F_GETFL:获取文件描述符的状态信息 //不需要第三个参数,返回值为获取到的属性 F_SETFL:设置文件描述符的状态信息 - 需要填充第三个参数 //需要填充第三个参数 O_RDONLY, O_RDWR ,O_WRONLY ,O_CREAT O_NONBLOCK 非阻塞 O_APPEND追加 O_ASYNC 异步 O_SYNC 同步 O_NOATIME 读取文件时不更新文件访问时间 arg:文件描述符的属性 如果需要设置文件描述符的状态,则需要该参数 返回值: 特殊选择:根据功能选择返回 (int 类型) 其他: 成功0 失败: -1; 使用: int flag; // 1.获取该文件描述符0 (标准输入) 的原属性 :标准输入原本具有阻塞的功能 int flag = fcntl(0, F_GETFL); //获取文件描述符原有信息后,保存在flag变量内 // 2.修改对应的位nonblock(非阻塞) int flag |= O_NONBLOCK; ( flag = flag | O_NONBLOCK) // 3. 将修改好的属性写回去 (0 标准输入 -- 阻塞 改为 非阻塞) fcntl (0, F_SETFL, flag); //文件描述符 设置状态 添加的新属性
信号驱动IO(Signal-driven IO)
- 特点:
异步通知模式, 需要底层驱动的支持
//1.设置将APP进程号提交给内核驱动 fcntl(fd,F_SETOWN,getpid());//F_SETOWN将进程号交给内核驱动 //getgid 进程号 //2.设置异步通知 int flags; flags = fcntl(fd, F_GETFL); //获取原属性 flags |= O_ASYNC; //设置异步 O_ASUNC 通知 fcntl(fd, F_SETFL, flags); //修改的属性设置进去 //3.signal捕捉SIGIO信号 --- SIGIO:信号驱动,自定义信号驱动 signal(SIGIO,handler); 头文件: #include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler) 功能:信号处理函数(注册信号) 参数: int signum:要处理的信号(要修改的信号) sighandler_t handler: 函数指针: void(*handler)(int) (修改的功能:) SIG_IGN:忽略该信号。 SIG_DFL:采用系统默认方式处理信号。 handler:------void handler(int num) 自定义的信号处理函数指针 返回值:成功:设置之前的信号处理方式 失败:SIG_ERR
信号IO实例:
操作鼠标设备,当有输入的时候获取输入数据,没有输入时循环输出hello world。
编辑
IO多路复用 (I/O Multiplexing)
头文件
C
#include<sys/select.h> #include<sys/time.h> #include<sys/types.h> #include<unistd.h>
声明
C
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
功能
select
函数用于监测一组文件描述符的IO事件,直到其中一个或多个描述符就绪或超时为止。
参数
nfds
:最大的文件描述符加一,即监测的最大文件描述符数量。readfds
:读就绪描述符集。writefds
:写就绪描述符集(可为NULL)。exceptfds
:异常就绪描述符集(可为NULL)。timeout
:超时时间,为NULL则无限期阻塞等待。
返回值
<0
:错误。>0
:有事件产生。==0
:超时。
超时时间结构体
C
struct timeval { long tv_sec; // 秒 long tv_usec; // 微秒 };
Select宏函数
FD_CLR(fd, set)
: 清除描述符fd
在集合set
中的状态。FD_ISSET(fd, set)
: 判断fd
是否在set
集合中产生事件。FD_SET(fd, set)
: 将fd
加入到集合set
中。FD_ZERO(set)
: 清空集合set
。
基本流程
- 构建文件描述符集合。
- 清空集合。
- 添加关心的文件描述符。
- 调用
select
。 - 检查产生事件的文件描述符。
- 执行相应的逻辑处理。
Select的特点与限制
- 最多监听1024个文件描述符(千级别)。
- 被唤醒后需重新轮询所有描述符,效率较低。
- 每次调用
select
会清空描述符集合,需频繁拷贝用户空间到内核空间,效率低下。
规则
- 监测范围通常为0至1023。
- 标准输入、输出、错误分别占据0、1、2三个文件描述符。
- 最大监测文件描述符数量为
fd+1
。 - 事件产生时,对应描述符在集合中会被置1,未产生事件的置0。
select
调用后会清空集合,需在调用前备份集合以优化性能。
select实例:
同时检测键盘输入和sockfd事件 -TCP实现同时连接多个客户端
编辑
编辑
Poll函数详解
特点
- 动态文件描述符个数:根据
poll
函数的第一个参数确定,提供了比select
更灵活的文件描述符数量控制。 - 轮询效率:虽然被唤醒后仍需遍历所有描述符,但无需像
select
那样每次调用都重建或清空文件描述符集合,仅需一次从用户空间到内核空间的数据拷贝,效率相对较高。
流程
- 创建
pollfd
结构体数组。 - 配置每个结构体的文件描述符及其关注的事件。
- 记录数组中最后一个有效元素的下标。
- 调用
poll
函数进行事件监测。 - 遍历数组,检查哪些文件描述符产生了事件。
- 根据触发的事件执行相应的处理逻辑。
声明与头文件
C
1int poll(struct pollfd *fds, nfds_t nfds, int timeout); 2#include <poll.h>
功能
poll
函数用于监视并等待多个文件描述符的属性变化,直到其中一个或多个描述符就绪或超时为止。
参数
fds
:关心的文件描述符数组。nfds
:数组中有效元素的数量。timeout
:超时时间(毫秒)。-1为无限期阻塞,0为非阻塞。
结构体pollfd
C
1struct pollfd { 2 int fd; // 文件描述符 3 short events; // 关注的事件类型 4 short revents; // 实际发生的事件 5};
返回值
<0
:错误。>0
:有事件产生。==0
:超时时间已到。
优势与局限
- 优势:不受1024文件描述符限制,无需每次调用都重设或清空集合,提高了处理大量描述符的效率。
- 局限:被唤醒后仍需遍历所有描述符,可能在高并发场景下影响性能。
Poll 实例:
编辑
编辑
epoll:高效事件驱动的I/O模型
特点对比
- select 和 poll:同步轮询模型,逐一检查所有文件描述符的就绪状态。
- epoll:异步事件驱动模型,基于事件的触发机制,只处理真正就绪的文件描述符,极大提升了效率。
epoll机制概览
- 红黑树:用于高效管理大量文件描述符,每个节点是一个文件描述符及其相关属性。
- 链表:事件链表,当文件描述符上的事件发生时,通过回调机制将其添加到链表中,供后续处理。
epoll的使用步骤
- 创建epoll实例(红黑树的根节点)。
- 注册、修改或删除文件描述符及事件监听。
- 阻塞等待事件,一旦有事件产生,进行处理。
函数接口
epoll_create
C
1int epoll_create(int size);
- 功能:创建epoll实例,即红黑树的根节点。
- 返回值:成功返回epoll文件描述符,失败返回-1。
epoll_ctl
C
1int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 功能:控制epoll实例,包括添加、修改和删除文件描述符的监听事件。
- 参数:
epfd
:epoll文件描述符。op
:操作类型。fd
:目标文件描述符。event
:事件结构体。
- 返回值:成功返回0,失败返回-1。
epoll_wait
C
1int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 功能:等待并获取就绪事件。
- 参数:
epfd
:epoll文件描述符。events
:事件集合,用于接收就绪事件。maxevents
:单次调用最多返回的事件数量。timeout
:超时时间(毫秒)。
- 返回值:成功返回实际发生的事件数量,失败返回-1。
注意事项
- epoll的效率远高于select和poll,尤其在处理大量并发连接时。
- epoll的文件描述符上限受系统限制,一般远大于1024,可达数十万。
- epoll的事件处理机制使得它非常适合构建高并发的网络服务器。
epoll 实例:
编辑
编辑
三者的特点以及区别
编辑
网络超时检测
使用网络超时事件检测的原因:
1) 避免进程在没有数据时无限制的阻塞。
2)当设定的时间到, 进程从原操作进行返回,然后继续执行
10.1 函数的参数可以设置超时
10.1.1 select 超时检测
头文件: #include<sys/select.h> #include<sys/time.h> #include<sys/types.h> #include<unistd.h> 声明: int select(int nfds, fd_set *readfds, fd_set *writefds,\ fd_set *exceptfds, struct timeval *timeout); 功能:监测是哪些文件描述符产生事件;,阻塞等待产生. 参数:nfds: 监测的最大文件描述个数(文件描述符从0开始,这里是个数,记得+1) readfds: 读事件集合; // 键盘鼠标的输入,客户端连接都是读事件 writefds: 写事件集合; //NULL表示不关心 exceptfds:异常事件集合; //NULL 表示不关心 timeout: 超时检测 //如果不做超时检测:传 NULL 超时时间检测: 当程序执行到该语句时,我们设定好时间,如果规定时间 内未完成函数功能, 返回一个超时的信息,我们可以根据该信息设定相应需求; 返回值: <0 出错 >0 表示有事件产生; ------------ 如果设置了超时检测时间:&tv ------------ <0 出错 >0 表示有事件产生; ==0 表示超时时间已到; 超时时间检测的结构体如下: struct timeval { long tv_sec; 以秒为单位,指定等待时间 long tv_usec; 以毫秒为单位,指定等待时间 1s = 1000us }; struct timespec { long tv_sec; 以秒为单位 long tv_nsec; 以纳秒为单位 1s = 1000000ns };
编辑
10.1.2 poll超时检
声明:int poll(struct pollfd *fds, nfds_t nfds, int timeout); 头文件: #include<poll.h> 功能: 监视并等待多个文件描述符的属性变化 参数: 1. struct pollfd *fds: 关心的文件描述符数组,大小自己定义 若想检测的文件描述符较多,则建立结构体数组struct pollfd fds[N]; struct pollfd{ int fd; //文件描述符 short events; //等待的事件触发条件----POLLIN读时间触发 short revents; //实际发生的事件(未产生事件: 0 )) } 2. nfds: 最大文件描述符个数 3. timeout: 超时检测 (毫秒级):1000 == 1s 如果-1,阻塞 如果0,不阻塞 返回值: <0 出错 >0 表示有事件产生; 如果设置了超时检测时间:&tv <0 出错 >0 表示有事件产生; ==0 表示超时时间已到;
编辑
10.1.3 epoll超时检测 -epoll也可以实现超时时间检测
声明: int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); 功能:等待事件的产生,类似于select的用法 参数: epfd:句柄; events:用来保存从链表中拿取响应事件的集合; maxevents: 表示每次在链表中拿取响应事件的个数; timeout:超时时间,毫秒级别,0立即返回 ,-1阻塞 返回值: < 0 出错 >0 实际从链表中拿出的数目 如果设置了超时检测: < 0出错 >0实际从链表中拿出的数目 ==0 表示超时或者没事件产生
编辑
10.2 setsockopt 设置套接字属性
10.2.1 socket属性
头文件: #include<sys.socket.h> #include<sys/types.h> #include<sys/time.h> int setsockopt(int sockfd,int level,int optname,void *optval,socklen_t optlen) 功能:获得/设置套接字属性 参数: sockfd:套接字描述符 level:协议层 optname:选项名 optval:选项值 optlen:选项值大小 返回值: 成功: 0 失败 -1
选项名称 |
说明 |
数据类型 |
========= SOL_SOCKET 应用层 ========== |
||
SO_BROADCAST |
允许发送广播数据 |
int |
SO_DEBUG |
允许调试 |
int |
SO_DONTROUTE |
不查找路由 |
int |
SO_ERROR |
获得套接字错误 |
int |
SO_KEEPALIVE |
保持连接 |
int |
SO_LINGER |
延迟关闭连接 |
struct linger |
SO_OOBINLINE |
带外数据放入正常数据流 |
int |
SO_RCVBUF |
接收缓冲区大小 |
int |
SO_SNDBUF |
发送缓冲区大小 |
int |
SO_RCVLOWAT |
接收缓冲区下限 |
int |
SO_SNDLOWAT |
发送缓冲区下限 |
int |
SO_RCVTIMEO |
接收超时 |
struct timeval |
SO_SNDTIMEO |
发送超时 |
struct timeval |
SO_REUSEADDR |
允许重用本地地址和端口 |
int |
SO_TYPE |
获得套接字类型 |
int |
SO_BSDCOMPAT |
与BSD系统兼容 |
int |
========== IPPROTO_IP IP层/网络层 ========== |
||
IP_HDRINCL |
在数据包中包含IP首部 |
int |
IP_OPTINOS |
IP首部选项 |
int |
IP_TOS |
服务类型 |
int |
IP_TTL |
生存时间 |
int |
IP_ADD_MEMBERSHIP |
将指定的IP加入多播组 |
struct ip_mreq |
============ IPPRO_TCP 传输层 ============== |
||
TCP_MAXSEG |
TCP最大数据段的大小 |
int |
TCP_NODELAY |
不使用Nagle算法 |
int |
编辑
编辑
设置 接收超时 设置超时检测操作的结构体: struct timeval { long tv_sec; /*秒*/ long tv_usec; /*微秒*/ }; struct timeval tm={2,0}; setsockopt(acceptfd,SOL_SOCKET,SO_RCVTIMEO,&tm,sizeof(tm)); //设置超时之后时间一旦到达,会打断接下来的阻塞,直接错误返回 int recvbyte = recv(acceptfd, .......); 设置端口和地址重用(在绑定bind上面写) int optval=1; setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(optval));
10.3.1 sigaction 修改信号的行为
头文件: #include <signal.h> 声明: int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); 功能:对接收到的指定信号处理 参数: 1. signum 信号 2. //act为设置新行为 oldact为设置旧行为 结构体如下: struct sigaction { void (*sa_handler)(int); //函数指针 其他的结构体成员如mark(信号集),flag(对信号的标记)都不常用 }; ===============需要定义一个函数接收==================== void handler() { printf("timeout .....\n"); } 一般,给目标设置新的属性,流程都为: 先获取原来的属性 修改属性 将属性写回去 #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <signal.h> void handler(int sig) { printf("1111111\n"); } int main(int argc, const char *argv[]) { //1.定义结构体变量 struct sigaction act; //2.获取原来的属性 sigaction(SIGALRM,NULL,&act); //3.修改属性 act.sa_handler = handler; //4.写回属性 sigaction(SIGALRM,&act,NULL); char buf[128] = ""; while(1) { alarm(2); if(fgets(buf,sizeof(buf),stdin) == NULL) { perror("fgets is err:"); continue; } printf("!!!!!!!\n"); } return 0; }