I/O模型:用poll实现多路复用I/O(linux下C语言版)
1. 多路复用I/O
1. 在UNIX/Linux下主要有4种I/O 模型:阻塞I/O、非阻塞I/O、多路复用I/O、信号驱动I/O,在本文我们主要讲解多路复用I/O
2. 为什么要选择多路复用I/O?
(1)应用程序中同时处理多路输入输出流时,若采用阻塞模式,将得不到预期的目的;
(2)若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间;
(3)若设置多个进程,分别处理一条数据通路,将新产生进程间的同步与通信问题,则会使程序变得更加复杂;
(4)比较好的方法是使用I/O多路复用,其基本思想是:
先构造一张有关文件描述符的表,然后调用一个函数,当这些文件描述符中的一个或多个已准备好进行I/O时,函数才返回,函数返回时告诉进程那个描述符已就绪,可以进行I/O操作。
3. 大致实现步骤:
1. 创建文件描述符的集合/表, 把想要监测的文件描述符加入到集合中
2. 监测集合中是否有事件产生
3. 判断是谁产生了什么事件
4. 处理事件
总之就是调用函数来操作文件描述符,那怎么调函数呢?调什么函数呢?本文我们用poll函数来实现I/O多路复用,让我们一起来看看详细操作吧。
2. poll函数介绍
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
作用: 监测集合中是否有事件产生
参数:
fds --- 想要监测的文件描述符集合
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
POLLIN -- 读事件 POLLOUT- 写事件
short revents; /* returned events */
};
nfds --- 要监测的文件描述符个数
timeout --- 超时设置,单位毫秒, -1:阻塞模式
返回值:
>0: 有事件产生
=0: 有超时设置,且再设定的实践范围内没有事件产生返回
-1: 失败,并设置errno
3. 举例实现select函数的I/O多路复用
这里我们举两个例子来用select实现I/O多路复用
1. 同时检测键盘和鼠标,获取数据来自哪里
2. 在客户端/服务器模型中,服务器同时监测键盘、监听套接字和通信套接字,接收键盘输入的数据、客户端的发送的数据,并打印在终端上
例1:监测来源于键盘和鼠标的数据
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <poll.h>int main(int argc, char *argv[])
{ //0.打开设备int fd = open("/dev/input/mouse0", O_RDONLY);if(0 > fd){perror("open");return -1;}//1.创建文件描述符集合,把想要监测的文件描述符加入集合中struct pollfd fds[2];memset(fds, 0, sizeof(fds));fds[0].fd = 0; //把键盘加入集合fds[0].events = POLLIN;fds[1].fd = fd; //把鼠标加入集合fds[1].events = POLLIN;//2.监测是否有事件产生int retval;char buf[1024];while(1){retval = poll(fds, 2, 1000); //以阻塞形式监测if(retval < 0){perror("poll");break;}else if(retval == 0){printf("timeout...\n");continue;}//3.判断是谁产生了事件if(fds[0].revents == POLLIN) //监测一个事件时可用=={//4.处理事件read(0, buf, sizeof(buf));buf[strlen(buf)-1] = '\0';if(strcmp(buf, "exit") == 0)break;printf("Data from keyboard\n");}if(fds[1].revents & POLLIN) //更推荐使用&{//4.处理事件read(fd, buf, sizeof(buf));printf("Data from mouse\n");}}return 0;
}
例2:服务器接收客户端的发送的数据并打印在终端上
(1)server.c
服务器端代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <fcntl.h>
#include <time.h>
#include <poll.h>int main(int argc, char *argv[])
{ //创建套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){perror("socket");return -1;}printf("socket create success\n");//绑定本机地址和端口struct sockaddr_in srvaddr;memset(&srvaddr, 0, sizeof(srvaddr));srvaddr.sin_family = AF_INET;srvaddr.sin_port = htons(60621);srvaddr.sin_addr.s_addr = inet_addr("192.168.2.154");if(0 > bind(sockfd, (struct sockaddr *)&srvaddr, sizeof(srvaddr))){perror("bind");return -1;}printf("bind success\n");//设置监听套接字if(0 > listen(sockfd, 1)){perror("listen");return -1;}printf("listen success\n");//1.创建文件描述符集合,把想要监测的文件描述符加入集合中struct pollfd fds[5]; //创建可装5个文件描述符的集合int m; //将集合进行初始化for(m = 0; m < 5; m++){fds[m].fd = -1; //此时集合位置m空闲,可被使用fds[m].events = 0;fds[m].revents = 0;}fds[0].fd = 0;fds[0].events = POLLIN;fds[1].fd = sockfd;fds[1].events = POLLIN;//2.循环监测是否有事件产生int connfd;char buf[1024];int ret;int n;while(1){int retval = poll(fds, 5, -1);if(retval < 0){perror("poll");break;}//3.判断是谁产生了事件int i = 0;//从0到maxfd遍历所有文件描述符判断是否是i产生事件for(i; i < 5; i++){if(fds[i].revents & POLLIN){if(fds[i].fd == 0) //键盘产生了事件{4.处理事件:打印键盘输入的数据memset(buf, 0, sizeof(buf));read(0, buf, sizeof(buf));buf[strlen(buf)-1] = '\0';if(strcmp(buf, "exit") == 0)return 0;printf("keyboard: %s\n", buf);}else if(sockfd == fds[i].fd)//监听套接字产生了事件{//4.处理事件:接收客户端的连接, 并生成通信套接字connfd = accept(sockfd, NULL, NULL);if(0 > connfd){perror("accept");return -1;}printf("accept success\n");//寻找空位for(n = 0; n < 5; n++){if(fds[n].fd == -1) {fds[n].fd = connfd;fds[n].events = POLLIN;break;}}}else //通信套接字产生了事件{//4.处理事件:与客户端通信,接收客户端的数据并打印,再给客户端发送数据ret = read(fds[n].fd, buf, sizeof(buf));if(0 > ret){perror("recv");break;}else if(0 == ret){printf("client close\n");//从集合中清除通信套接字close(fds[n].fd);fds[n].fd = -1;fds[n].events = 0;fds[n].revents = 0;break;}printf("recv: %s\n", buf);//设置退出条件if(strcmp(buf, "exit") == 0){//从集合中清除通信套接字close(fds[n].fd);fds[n].fd = -1;fds[n].events = 0;fds[n].revents = 0;break;}}}}}//关闭套接字close(sockfd);return 0;
}
(2)client.c
客户端代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <time.h>int main(int argc, char *argv[])
{ //创建套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if(0 > sockfd){perror("socket");return -1;}printf("socket create success\n");//主动连接服务器struct sockaddr_in srvaddr;memset(&srvaddr, 0, sizeof(srvaddr));srvaddr.sin_family = AF_INET;srvaddr.sin_port = htons(60621);srvaddr.sin_addr.s_addr = inet_addr("192.168.2.154");if(0 > connect(sockfd, (struct sockaddr *)&srvaddr, sizeof(srvaddr))){perror("connect");return -1;}//与服务器通信:将键盘输入的数据发送给服务器int ret;char buf[1024];while(1){printf("send: ");fgets(buf, sizeof(buf), stdin);buf[strlen(buf)-1] = '\0';if(0 > send(sockfd, buf, sizeof(buf), 0)){perror("send");break;}//设置退出条件if(strcmp(buf, "exit") == 0)break;}//关闭套接字close(sockfd);return 0;
}
(3)运行注意事项
1. 我们在第一个终端编译 gcc server.c -o s //重命名执行文件名
2. 在第二个终端(另外开一个终端)编译 gcc client.c -o c
(为什么要重命名执行文件名?这是因为如果像平常一样进行编译,两个程序编译后生成同一个文件名(a.out),互相覆盖,无法同时执行,所以,通常我们会用 -o 指定不同的输出文件名)
3. 在第一个终端执行 ./s
4. 在第二个终端执行 ./c
4. 总结
1. 使用 poll 函数实现 I/O 多路复用无文件描述符数量限制,支持数万个文件描述符,适用于高并发场景
2. poll 函数通过 poll 结构体数组直接监听事件,无需每次调用都重置文件描述符集合,事件检测更高效
3. 但 poll 函数仍需遍历所有文件描述符和 select 一样,poll 返回后仍需遍历整个 pollfd 数组检查就绪事件,时间复杂度仍是 O(n)
4. 除了 poll 函数,还可用 select、epoll 来实现I/O多路复用,它们各有各的特点,想了解更多的同学欢迎浏览主页相关文章!
感谢观看!如有疑问欢迎提出!
----香菜小猫祝这位uu天天开心----