005. IO多路复用
文章目录
1. 一切皆文件
1.1. 文件的理解
-
狭义:文件系统中磁盘中的具体文件
-
广义:设备、管道、内存等一切对象
-
一切皆文件的意义:统一对各种设备的操作方式(open、read、write、close),如:
- IO设备(命令行输入,显示器)
- 网络设备(网卡)
- 串口…
1.2. 文件描述符
-
文件描述符是一个非负整数值,本质是一个句柄(一切对程序员透明的资源标志都可以看做是句柄)
-
用户使用文件描述符与内核进行交互,内核再通过文件描述符操作对应资源
-
文件描述符有可能会使用完,所以每次使用完毕后要记得close
1.3. 文件操作编程模式
实例:以文件的方式操作输入输出设备(原来可以使用scanf和printf函数进行输入输出操作)
步骤一:打开输入输出设备(linux内核启动时已默认打开,所以代码中就不需要再次open了,同时也不需要close)
步骤二:获取输入输出的文件描述符(输入输出设备的文件描述符为0)
步骤三:直接对输入输出文件描述符进行操作
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
int iofd = 0;
char s[] = "hello input/output device\n";
int len = 0;
write(0, s, strlen(s)); //往标准输入输出设备写,写入数据之后,终端即可打印出对应的字段
len = read(0, s, 5); //读取标准输入输出设备
s[len] = 0;
printf("%s\n", s);
return 0;
}
/*
linux@ubuntu:~/demos/demos/select$ ./a.out
hello input/output device
abc
abc
linux@ubuntu:~/demos/demos/select$
*/
终端输入后可以回显
2. 事件函数分类
-
阻塞式函数
- 函数调用后需要等待某个事件完成后才能返回(如write、send、read等)
-
非阻塞式函数(相当于通知)
- 回调
- 函数调用后能够及时返回(仅标记需等待的事件)
- 直到事件发生后以回调方式进行传递(通知),类似生活中的实例给一个电话号码,有事情打电话
- 轮训
- 每隔一段时间依次询问每个设备是否需要服务(服务指的是读和写等)
- 可用于解决阻塞函数导致程序无法继续执行的问题
- 回调
3. select函数
-
select()函数可以将
多个文件描述符集中到一起统一监视
。 监视项目包括:是否存在套接字接收数据?无需阻塞传输数据的套接字有哪些?- select函数用于监视
指定的
文件描述符是否产生事件 - 通过轮训方式检测目标事件(事件产生则标记变化,下次轮询时就会检测到这个变化),如检测读写事件
- 事件如果发生变化后,根据事件类型做出具体处理(如:读取数据,如果数据到了,事件处理方式就是读数据)
- select函数用于监视
-
IO多路复用
IO多路复用(IO Multiplexing)是指单个进程/线程就可以同时处理多个IO请求
3.1 select函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);//其中,struct fd_set 是一个可以存放是文件描述符的集合。
-
返回值:判断是否有事件发生
- 负值:select错误
- 正值:某些文件可读写或出错
- 0:等待超时,没有可读写或错误的文件
-
nfds:集合中所有文件描述符的范围,即所有文件描述符的最大值加1;
-
readfds:如果有事件发生,是否是读事件;
-
writefds:写事件;
-
exceptfds:异常事件;
-
timeout:指定超时时间,即轮询时到一个设备时,可停留时间,(如使用轮询收取快递时,如果快递没有来会在快递点待进行几分钟,如果快递还是没来,那就走);
3.2 select函数使用步骤:
- 监听某个文件描述符
- 设置指定文件描述符中的哪些事件被监听,如监听键盘中的输入输出事件,则将读事件的文件描述符数据结构设置文件描述符0的对应位的值为1
- 设置监听超时
- 不停调用select函数
- 查看监听结果
- 处理目标事件,再调用select函数进行轮询
3.3 select()相关数据类型以及操作
利用select函数可以同时监听多个文件描述符(监听文件描述符也可以理解为监听套接字),此时需要将监视的文件描述符集中到一起。集中时也要按照监视项(接收
、传输
、异常
)进行区分,即按照上述三种监视项分成3类。使用fd_set数组变量执行监听多个文件描述符。
如下图所示fd_set结构体:该数组是存有0和1的位
数组。
最左端的位表示文件描述符0(所在位置),如果该位设置为1,则表示该文件描述符(即0)是监视对象。如图所示文件描述符1、3表示被监视的对象
另外由于对fd_set对象来说其是直接以位为单位进行直接操作的,这也意味着操作该变量会比较底层。所以针对该变量提供了如下宏进行直接操作
-
将所有位置零,将fd_set变量的所有位都置为0
FD_ZERO(fd_set* fdset)
如下代码执行结果如下图所示
fd_set set; FD_ZERO(set)
-
在fd_set变量集合中指定需要监听的fd
FD_SET(int fd,fd_set* fdset)
如:
fd_set set; FD_ZERO(set); FD_SET(1,&set);//对应图1 FD_SET(2,&set); //对应图2
-
在fd_set中剔除指定fd,不再监听
FD_CLR(int fd,fd_set* fdset)
如:
fd_set set; FD_CLR(2,&set);//初始状态为2中fd1、fd2为1
-
在fd_set检索是否包含fd
FD_ISSET(int fd,fd_set* fdset)
4. 实例
实验目的为证明往操作符中写数据不影响其轮询
#include <sys/select.h>
#include <sys/time.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
int iofd = 0;
char s[] = "D.T.Software\n";
int len = 0;
fd_set reads = {0};
fd_set temps = {0};
struct timeval timeout = {0};
FD_ZERO(&reads);
FD_SET(iofd, &reads);//設置reads集合100000...,即只监听read集合中的第0位,即文件描述符为0的资源
while( 1 )
{
int r = -1;
temps = reads; // 每次select轮询时会改变temp的参数,所以每次循环都需要将设置好的值重新对变量进行赋值这样就能保证每次参数都是设置好的参数
timeout.tv_sec = 0;
timeout.tv_usec = 50000;
r = select(1, &temps, 0, 0, &timeout);
if( r > 0 )
{
len = read(iofd, s, sizeof(s)-1);
s[len] = 0;
printf("Input: %s\n", s);
}
else if( r == 0 )
{
static int count = 0;
usleep(10000); // do something else
count++;
if( count > 100 )
{
printf("do something else\n");
count = 0;
}
}
else
{
break;
}
}
return 0;
}
/*
linux@ubuntu:~/demos/demos/select$ gcc select.c
linux@ubuntu:~/demos/demos/select$ ./a.out
1
Input: 1
1
Input: 1
1
Input: 1
do something else
1
Input: 1
1
Input: 1
*/
如果不使用select函数程序会一直阻塞在read函数处,直到有数据;使用了select函数相当于是在read函数阻塞之前通过select函数返回值,增加了一个判断:有数据才会让read;无数据继续做其他事情
实验结果表明当数据到来时程序可以直接读取数据进而将数据打印出来,当没有数据时又可以做其他事(打印了do something else),即证明了函数select的轮询作用
下一节将会实现使用select完成对网络socket server的多路IO的复用,支持多个客户端的连接