apue 多路复用
需求来自用户,用户的需求来自实际的使用场景。在实际运用中,一个系统或者程序需要处理的事件并不是只有一个或一类,而是存在各种各样的事件在一小段事件内一起发生,此时按照没学多线程的逻辑的处理方式就是这样:
if(read(事件1) > 0) //阻塞
{
处理事件1;
}
if(read(事件2) > 0) //阻塞
{
处理事件2;
}
这种就会导致如果事件1阻塞了,会导致如果事件2准备好了,也没办法处理。根据上一篇多线程的内容,我们可以让线程各自阻塞,通过操作系统来调度不同的线程运行,大致如下:
typdef work_ctx_s
{
int fd_business1;
int fd_business2;
};
int main()
{
pthread_t tt;
pthread_attr_t attr;
worker_ctx_t ctx;
pthread_create(&tt,&attr,fd_business1,&ctx);
pthread_create(&tt,&attr,fd_business2,&ctx);
while()
{
;
}
}
void *business1_worker(void *args)
{
worker_ctx_t ctx = args;
if(read(ctx->business1,buf,sizeof(buf)) > 0)
{
执行事件1;
}
}
void *business2_worker(void *args)
{
worker_ctx_t ctx = args;
if(read(ctx->business2,buf,sizeof(buf)) > 0)
{
执行事件2;
}
}
当有多个多种事件同一段时间发生时,多进程/多线程模式可以解决,但是创建进程和创建线程都是需要时间开销的。在编写服务器客户端程序的时候,如果服务器性能一般,但是客户端连接的又太多的时候,这会造成很大的代价。该篇要说的多路复用就是解决这中问题的办法之一。通过多路复用来监听,是否有事件发生,并把发生的事件是什么告诉服务器端。这种方法叫做多路复用。
多路复用 select()函数实现逻辑
文章目录
前言
学习多路复用之前我们需要掌握一些相关的知识
比如同步与异步,阻塞与非阻塞。
提示:以下是本篇文章正文内容,下面案例可供参考
一、学前预备
同步与异步
同步(sync)和异步(async)的概念描述的是用户线程与内核的交互方式。
同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;
异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。
阻塞与非阻塞
阻塞和非阻塞的概念描述的是用户线程调用内核操作的方式。
阻塞是指IO操作在没有接收完数据或者没有得到结果之前不会返回,需要彻底完成后才返回到用户空间;
非阻塞是指IO操作后立即返回给用户一个状态值,无需等到IO操作彻底完成。
在Linux下进行网络编程是,服务器端编程需要构造高性能的IO模型,常见的IO模型有五种:
-
同步阻塞IO(blocking IO):发送方发送请求之后一直等待响应,接收方处理请求时进行的的IO操作如果不能马上等到返回结果,就一直等到返回结果后,才响应发送方,期间不能进行其它工作。即传统的IO模型。网络编程都是从listen()、read()、write()等接口开始的,这些接口都是阻塞型的,想要不阻塞,我们可以使用多进程或者多线程。
-
同步非阻塞IO(Non-blocking IO):发送方发送请求之后,一直等待响应,接收方处理请求时进行的IO操作如果不能马上的得到结果就立即返回,去做别的事情,但是发送方没有得到请求处理的结果,一处于等待状态,当IO操作完成以后,将完成状态和结果通知接收方,接收方再响应发送方,发送方才进入下一次请求过程。
-
IO多路复用(IO multiplexing):一句话解释就是单线程或单进程同时检查若干文件描述符是否可以执行IO操作的能力。IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。
-
信号驱动IO(signal driven IO):调动sigaltion系统调用,当内核中IO数据就绪时以SIGIO信号通知请求进程,请求进程再把数据从内核读入到用户空间,这一步是阻塞的。
-
异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步阻塞IO,“真正”的异步IO需要操作系统更强的支持,在IO多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据,而在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。
这是一个讲同步和异步,阻塞和非阻塞的,我觉得挺清楚的链接
二、实现多路复用的三种方法
1.select()函数实现多路复用
select()函数允许进程指示内核等待多个事件(文件描述符)中的任何一个发生,并只在有一个或多个事件发生或经历一段指定事件后才唤醒它,然后接下来判断究竟是哪个文件描述符发生了事件并进行响应的处理。
一下是对select()函数的一些说明:
#include <sys/select.h>
#include <sys/time.h>
int select(int max_fd,fd_set *readset,fd_set *writeset,fd_set *exceptset,struct timeval *timeout)
该函数的返回值是就绪描述符的数目,超时返回0,出错返回-1;
参数说明如下:
第一个参数:max_fd是指待测试的fd的总个数,它的值是待测试的最大文件描述符加1,假设需要检测的文件描述符是7,那么Linux内核实际也要监测0~7。
中间三个参数readset,writeset,exceptset指定要让内核测试读、写和异常条件的fd集合,如果不需要测试可以设置为NULL。
最后一个参数是设置select的超时事件,如果设置为NULL则用不超时。
其中最后一个参数结构体如下:
struct timeval
{
long tv_sec; //seconds
long tv_usec; //micriseconds
};
与该函数一起使用的函数有以下:
FD_ZERO(fd_set * fds) //清空集合
FD_SET(int fd,fd_set* fds) //将给定的描述符加入集合
FD_ISSET(int fd,fd_set *fds)//判断指定描述符是否在集合中
FD_CLR(int fd,fd_set *fds) //将给定的描述符从文件中删除
基于select的IO复用模型的单进程执行可以为多个客户端服务,这样可以减少创建线程所需要的CPU时间片或内存资源的开销;并且几乎所有的平台上都支持select(),具有良好的跨平台性。
接下来说说缺点:①每次调用select()都需要把fd集合从用户态拷贝到内核态,之后内核需要遍历所有传递进来的fd,这是如果客户端fd很多时会导致系统的开销很大。②单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过setrlimit()函数、修改宏定义甚至重新编译内核等方式来提升这一限制,但是提升了文件描述符数量之后又会引发问题①导致效率低下。
2.poll()函数实现多路复用
select()和poll()系统调用在本质上是没有太大区别的,poll()的机制与select()类似,管理多个描述符也是进行轮询,然后根据描述符的状态进行处理,但是poll()函数没有最大文件描述符的数量限制(不过数量过多之后还是会下降)。poll()和select()的一个共同之处就是:包含大量文件描述符的数据会被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,而文件描述符的数据增加之后,系统的开销也就增大。
下面来介绍以下poll()函数的原型:
#include <poll.h>
int poll(struct pollfd *fds,nfds_t nfds,int timeout);
参数说明:
第一个参数类型是结构体 pollfd
struct pollfd
{
int fd; //文件描述符
short events //等待的事件
short revents //实际发生了的事件
};
第一个参数用来指向该结构体类型的数组,每一个pollfd结构体指定了一个被监视的文件描述符,指示poll()监视多个文件描述符,每个结构体的events域是监视该文件描述符的事件掩码,
由用户来设置这个域。revents域是文件描述符的操作结果事件掩码,events域中请求的任何事件都可能在revents域中返回。
第二个参数nfds指定数组中监听的元素个数
第三个参数timeout指定等待的毫秒数,无论IO是否准备好,poll都会返回。timeoout指定为负数值表示无线超时,使poll()一直挂起直到一个指定事件发生;timeout为0表示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。也即是,该文件描述符的IO一旦准备好就直接选举出来。
3.epoll()函数实现多路复用
epoll是Linux内核为处理
大批量文件描述符而对poll做了改进的,相当于是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。有一点原因是在获取事件的时候,它不需要遍历整个被监听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入ready队列的描述符集合就行了。
.
此外还有一点就是epoll处理提供select/poll那种IO事件的水平触发外,还提供了边缘触发,这一点使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
epoll的设计和实现与select完全不同,epoll通过在Linux内核中申请一个简易的文件系统,把原先的select/poll调用分成了3个部分:
- 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
- 调用epoll_ctl向epoll对象中添加这100万个连接的套接字
- 调用epoll_wait收集发生的事件的连接
这三个函数的说明如下:
#include <sys/epoll.h>
int epoll_create(int size)
系统调用epoll_create()创建了一个新的epoll实例,其对应的兴趣列表初始化为空。若成功创建返回文件描述符,
若创建出错返回-1,参数size指定了我们想要通过epoll实例来检查的文件描述符的个数,该参数并不是一个上限,而是告诉内核应该如何为内部数据结构划分初始大小。从Linux2.6.8依赖,size参数被忽略不用
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *ev)
该函数能够修改由文件描述符epfd所代表的epoll实例中的兴趣列表,若成功就返回0,若失败就返回-1;
参数说明:
第一个参数epfd是epoll_create()的返回值;
第二个参数op是用来指定需要执行的操作,可以是EPOLL_CTL_ADD(将描述符fd添加到epoll实例中的兴趣列表中去),EPOLL_CTL_MOD(修改描述符上设定的事件),EPOLL_CTL_DEL(将文件描述符从兴趣列表中移除)
第三个参数指明了要修改兴趣列表中的哪一个文件描述符的设定
第四个参数ev是指向结构体epoll_event的指针
第四个参数的结构体如下:
struct epoll_event
{
uint32_t events; //epoll events
epoll_data_t data; //user data
};
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}eppoll_data_t;
参数ev为文件描述符fd所作的设置
- events字段是一个位掩码,它指定了我们为待检查的描述符fd上所感兴趣的事件集合;
- data字段是一个联合体,当描述符fd稍后称为就绪态时,联合成员可用来指定传回给调用进程的信息
int epoll_wait(int epfd,struct epoll_event *evlist,int maxevents,int timeout);
系统调用epoll_wait()返回实例中处于就绪态的文件描述符的信息,数组evlist中的元素个数,如果在timeout超时间间隔内没有任何文件描述符处于就绪态的话就返回0,出错返回-1.
参数说明:
第一个参数是epoll_create()的返回值
第二个参数所指向的结构体中返回的是有关就绪态文件描述符的信息,数组evlist的空间由调度者负责申请;
第四个参数是timeout用来确定epoll_wait()的阻塞行为
1. -1:调用将一直阻塞,直到兴趣列表中的文件描述有事件产生或捕捉到一个信号为止
2. 0:执行一次非阻塞式的检查,看兴趣列表中的描述符产生了哪个事件
3. >0:调用将阻塞至多timeout毫秒,直到文件描述符上有事件发生,或者直到捕捉到 一个信号为止
数组evlist中,每个元素返回的都是单个就绪态文件描述符的信息。events字段返回了在该描述符上已经发生的事件掩码。
data字段返回的是我们在描述符上使用epoll_ctl()注册感兴趣的事件时在ev.data中所指定的值。注意,data字段是唯一可获知同
这个事件相关的文件描述符的途径。因此,当我们调用epoll_ctl()将文件描述符添加到感兴趣列表中时,应该要么将ev.date.fd设
为文件描述符号,要么将ev.date.ptr设为指向包含文件描述符号的结构体。
当我们调用epoll_ctl()时可以在ev.events中指定的位掩码以及由epoll_wait()返回的evlist[].events中的值如下所示:
默认情况下,一旦通过epoll_ctl()的EPOLL_CTL_ADD操作将文件描述符添加到epoll实例的兴趣列表中后,它会保持激活状态
(即,之后对epoll_wait()的调用会在描述符处于就绪态时通知我们)直到我们显示地通过epoll_ctl()的EPOLL_CTL_DEL操作将
其从列表中移除。如果我们希望在某个特定的文件描述符上只得到一次通知,那么可以在传给epoll_ctl()的ev.events中指定
EPOLLONESHOT标志。如果指定了这个标志,那么在下一个epoll_wait()调用通知我们对应的文件描述符处于就绪态之后,这
个描述符就会在兴趣列表中被标记为非激活态,之后的epoll_wait()调用都不会再通知我们有关这个描述符的状态了。如果需
要,我们可以稍后用过调用epoll_ctl()的EPOLL_CTL_MOD操作重新激活对这个文件描述符的检查。
总结
下一篇更新实现的具体代码