前言:我们在上一讲 Linux 网络编程的5种IO模型:阻塞IO与非阻塞IO,对于其中的 阻塞/非阻塞IO 进行了说明。
这一讲我们来看 多路复用机制。
IO复用模型 ( I/O multiplexing )
所谓I/O多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要额外的功能来配合: select、poll、epoll。
select、poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。
select时间复杂度O(n):它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
poll时间复杂度O(n):poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.
epoll时间复杂度O(1):epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))
在多路复用IO模型中,会有一个内核线程不断去轮询多个socket的状态,只有当真正读写事件发生时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。
小编推荐自己的linuxC/C++语言技术交流群:【1106675687】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!
poll
select() 和 poll() 系统调用的本质一样,poll() 的机制与 select() 类似,与 select() 在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是 poll() 没有最大文件描述符数量的限制( 基于链表来存储的,但是数量过大后性能也是会下降)。poll() 和 select() 同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <signal.h>
#include <poll.h>
int ppoll(struct pollfd *fds, nfds_t nfds,
const struct timespec *tmo_p, const sigset_t *sigmask);
struct pollfd{
int fd; //文件描述符
short events; //等待的事件
short revents; //实际发生的事件
};
描述: 监视并等待多个文件描述符的属性变化
参数解析:
-fds : 指向一个struct pollfd数组的指针,用于指定测试某个给定的fd的条件
- fd :每一个 pollfd 结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示 poll() 监视多个文件描述符。
- events: 指定监测fd的事件(输入、输出、错误),每一个事件有多个取值
- revents:revents 域是文件描述符的操作结果事件,内核在调用返回时设置这个域。events 域中请求的任何事件都可能在revents 域中返回。
注意:每个结构体的 events 域是由用户来设置,告诉内核我们关注的是什么,而 revents 域是返回时内核设置的,以说明对该描述符发生了什么事件
nfds : 指定 fds 数组元素个数 ,相当于要检查多少个文件描述符
timeout:指定等待的毫秒数,无论 I/O 是否准备好,poll() 都会返回。
- -1 : 永远等待,直到事件发生
- 0:立即返回
- > 0:等待指定的毫秒数
返回值:
成功时,poll() 返回结构体中 revents 域不为 0 的文件描述符个数;如果在超时前没有任何事件发生,poll()返回 0;
失败时,poll() 返回 -1,并设置 errno 为下列值之一:
- EBADF:一个或多个结构体中指定的文件描述符无效。
- EFAULT:fds 指针指向的地址超出进程的地址空间。
- EINTR:请求的事件之前产生一个信号,调用可以重新发起。
- EINVAL:nfds 参数超出 PLIMIT_NOFILE 值。
- ENOMEM:可用内存不足,无法完成请求。
events & revents的取值如下:
poll例程
使用poll函数监控标准输入
#include <stdio.h>
#include <unistd.h>
#include <poll.h>
int main()
{
struct pollfd poll_fd;
char buf[1024];
poll_fd.fd = 0;
poll_fd.events=POLLIN;
for(;;)
{
int ret = poll(&poll_fd,1,2000);
if(ret<0)
{
perror("poll");
continue;
}
if(ret==0)
{
printf("poll timeout!\n");
continue;
}
if(poll_fd.revents==POLLIN)
{
read(0,buf,sizeof(buf)-1);
printf("sdin:%s",buf);
}
}
}
poll 处理 tcp通信
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/poll.h>
#define MAX_SOCKET 5
typedef struct _info {
char name[10];
char text[54];
}info;
int main(int argc, char *argv[])
{
info send_buf;
info recv_buf;
int listen_socket,newsk;
int connected_sockets[MAX_SOCKET];
int connected_cnt = 0;
int i;
// 1 创建一个套接字,用于网络通信
listen_socket = socket(PF_INET, SOCK_STREAM, 0);
if (listen_socket == -1)
{
perror("socket");
return -1;
}
// 2 绑定服务的IP与端口
struct sockaddr_in ser_addr;
ser_addr.sin_family = PF_INET;
ser_addr.sin_port = htons (12345) ;
ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret = bind(listen_socket, (struct sockaddr *)&ser_addr,sizeof(ser_addr));
if (ret == -1)
{
perror("bind");
return -1;
}
// 3 监听端口的数据
ret = listen(listen_socket,MAX_SOCKET);
if (ret == -1)
{
perror("listen");
return -1;
}
// 监听的总套接字数组
struct pollfd pollfds[MAX_SOCKET+1];
int poll_ret;
// 主套接字信息
pollfds[0].fd = listen_socket;
pollfds[0].events = POLLIN|POLLPRI; //设置为 任意优先级可读 事件监听
while(1)
{
printf("Poll all fds\n");
poll_ret = poll(pollfds, connected_cnt + 1, -1); //-1 阻塞模式进行监听
printf("ret = %d\n", poll_ret); //返回值为1 : 正确监听到变化
printf("Need to handld %d fd(s)\n", poll_ret); //返回值为1 : 正确监听到变化
if(poll_ret == 0)
{
printf("timeout!\n"); //监听超时
} else if( poll_ret == -1 )
{
perror("err!"); //监听出错
}
//正确监听到变化
if(pollfds[0].revents & POLLIN || pollfds[0].revents & POLLPRI) //如果新客户端连接
{
// 响应用户连接
connected_sockets[connected_cnt] = accept(listen_socket,NULL,NULL); //返回 新的连接响应的套接字
if(connected_sockets[connected_cnt] == -1)
{
perror("accept");
return -1;
}
printf("new accept from %d!\n", connected_sockets[connected_cnt]);
//更新客户端套接字集合
pollfds[connected_cnt+1].fd = connected_sockets[connected_cnt];
pollfds[connected_cnt+1].events = POLLIN|POLLPRI; //任意优先级可读
connected_cnt++;
} else
{
printf("new read/write !\n");
for(i = 0; !(pollfds[i+1].revents & POLLIN) ;i++ ) ;//如果是 客户端发生了数据可读)
//4 接收与发送数据
newsk = connected_sockets[i];
memset(&recv_buf, 0, sizeof(recv_buf));
ret = recv(newsk, &recv_buf, sizeof(recv_buf), 0);
if (ret == -1)
{
perror("recv");
return -1;
}
if(errno == EINTR) continue;
if(ret == 0 ) //客户端断开
{
printf("%d disconnectded\n", connected_sockets[i]);
close(connected_sockets[i]);
connected_sockets[i] = -1;
memset(&pollfds[i+1] , 0, sizeof(struct pollfd )); //清空断开套接字监听信息
continue;
}
printf("[%d],[%s] : %s\n", connected_sockets[i], recv_buf.name, recv_buf.text);
sprintf(send_buf.name, "Server");
sprintf(send_buf.text, "Had recvied your[%d] message", connected_sockets[i]);
//sendto(my_socket, &buf, sizeof(buf), 0, NULL, NULL);
send(connected_sockets[i], &send_buf, sizeof(send_buf), 0);
}
sleep(2);
}
// 5 关闭套接字
for(i = 0; i < connected_cnt; i++)
{
close(connected_sockets[i]);
}
return 0;
}
poll函数的优缺点
通过poll函数的结构以及小测试程序的编写,我们不难发现poll函数的一些特点:
1、优点
- (1)poll() 不要求开发者计算最大文件描述符加一的大小。
- (2)poll() 在应付大数目的文件描述符的时候速度更快,相比于select。
- (3)它没有最大连接数的限制,原因是它是基于链表来存储的。
- (4)在调用函数时,只需要对参数进行一次设置就好了
2、缺点
- (1)大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
- (2)与select一样,poll返回后,需要轮询pollfd来获取就绪的描述符,这样会使性能下降
- (3)同时连接的大量客户端在一时刻可能只有很少的就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降