I/O模型:用select实现多路复用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. 大致实现步骤:
创建文件描述符的集合/表, 把想要监测的文件描述符加入到集合中
监测集合中是否有事件产生
判断是谁产生了什么事件
处理事件
总之就是调用函数来操作文件描述符,那怎么调函数呢?调什么函数呢?本文我们用select函数来实现I/O多路复用,让我们一起来看看详细操作吧。
2. select函数介绍
(1)select函数
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
作用:
实现IO多路复用/监测集合中是否有事件产生
参数:
nfds: 最大文件描述符+1
readfds: 读事件集合
writefds: 写事件集合
exceptfds: 其他事件集合
timeout: 超时设置, NULL -- 阻塞
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
返回值:
>0: 有事件产生
=0: 有超时设置,且再设定的实践范围内没有事件产生返回
-1: 失败,并设置errno
(2)辅助功能函数
void FD_CLR(int fd, fd_set *set); //删除集合中fd文件描述符
int FD_ISSET(int fd, fd_set *set); //判断fd是否还在集合set中
void FD_SET(int fd, fd_set *set); //把fd加入集合set中
void FD_ZERO(fd_set *set); //清空集合set
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>int main(int argc, char *argv[])
{ //0.打开设备int fd = open("/dev/input/mouse0", O_RDONLY); //鼠标设备的文件路径if(0 > fd){perror("open");return -1;}//1.创建文件描述符集合,把想要监测的文件描述符加入集合中fd_set fds, rfds; //创建集合FD_ZERO(&fds); //清空集合FD_SET(0, &fds); //把键盘加入集合中FD_SET(fd, &fds); //把鼠标加入//2.监测是否有事件产生int retval;char buf[1024];while(1) //循环监测事件产生{struct timeval tv = {1}; //超时设置:没有信息时传来时1s发送一句超时提示rfds = fds; //保存原集合retval = select(fd+1, &rfds, NULL, NULL, &tv); //获取返回值if(retval < 0) //返回值小于0:select失败{perror("select");break;}else if(retval == 0) //返回值等于0:有超时设置,且在设定的时间范围内没有事件产生返回{printf("timeout...\n");continue;}//返回值大于0:有事件产生//3.判断是谁产生了事件if(FD_ISSET(0, &rfds))//此时数据来源于键盘{//4.处理事件read(0, buf, sizeof(buf)); //读取来自键盘的数据buf[strlen(buf)-1] = '\0';if(strcmp(buf, "exit") == 0) //设置退出条件break;printf("Data from keyboard\n"); //提示该条数据来自键盘}if(FD_ISSET(fd, &rfds))//此时数据来源于鼠标{//4.处理事件read(fd, buf, sizeof(buf)); //读取来自鼠标的数据(移动鼠标就会产生数据)printf("Data from mouse\n"); //提示该条数据来自鼠标}}return 0;
}
这里为什么要保存原集合fds?
select函数会原地修改传入的fd_set参数,返回时集合中仅保留就绪的文件描述符。
假设初始 fds 包含{0,fd}(键盘和鼠标),调用 select 后:
(1)如果只有键盘有数据,rfds 会被修改为 { 0 }
(2)如果只有鼠标有数据,rfds 会被修改为 { fd }
(3)如果两者都有数据,rfds 会被修改为 { 0,fd }
不保存的后果:若不保存原始集合,每次循环时fds会被 select() 破坏,导致后续无法正确监测所有描述符。
例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>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.创建文件描述符集合,把想要监测的文件描述符加入集合中fd_set fds, rfds; //创建集合FD_ZERO(&fds); //清空集合FD_SET(0, &fds); //把键盘加入集合中FD_SET(sockfd, &fds); //把监听套接字加入int maxfd = sockfd; //存放监测的最大文件描述符//2.循环监测是否有事件产生int connfd;while(1){rfds = fds; //保存原集合int retval = select(maxfd+1, &rfds, NULL, NULL, NULL);if(retval < 0){perror("select");break;}//3.判断是谁产生了事件char buf[1024];memset(buf, 0, sizeof(buf));int ret;int i = 0;//从0到maxfd遍历所有文件描述符判断是否是i产生事件for(i; i < maxfd+1; i++){if(FD_ISSET(i, &rfds)){if(i == 0) //键盘产生了事件{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 == i)//监听套接字产生了事件{//接收客户端的连接, 并生成通信套接字connfd = accept(sockfd, NULL, NULL);if(0 > connfd){perror("accept");return -1;}printf("accept success\n");FD_SET(connfd, &fds); //将通信套接字加入集合maxfd = connfd > maxfd ? connfd : maxfd; //选出最大的文件描述符}else //通信套接字产生了事件{//与客户端通信:接收客户端的数据并打印ret = read(i, buf, sizeof(buf));if(0 > ret){perror("recv");break;}else if(0 == ret){printf("client close\n");FD_CLR(i, &fds);close(i);break;}printf("recv: %s\n", buf);//设置退出条件if(strcmp(buf, "exit") == 0){FD_CLR(i, &fds);close(i);break;}}}}}//6.关闭套接字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. 用 select 函数实现I/O多路复用,简单直观,超时精度高,可同时监控多种事件(读、写、异常三种事件类型),是经典的实现I/O多路复用
2. 但有文件描述符数量限制,默认最多只能监控 1024 个文件描述符,而且性能随连接数增加而下降等缺点
3. 除了 select 函数,还可用 poll、epoll 来实现I/O多路复用,想了解更多的同学欢迎浏览主页相关文章!
感谢观看!如有疑问欢迎提出!
----香菜小猫祝这位uu天天开心----