基础IO模型
场景假设
假设妈妈有一个孩子,孩子在房间里睡觉,妈妈需要及时获知孩子是否醒了,如何做?
- 时不时进房间看一下:简单,空闲时间还能干点别的,但是很累
- 进到房间陪着孩子一起睡觉,孩子醒了会吵醒妈妈:不累,但是不能干别的了
- 妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈:互不耽误
阻塞型模型
- 当程序调用某些接口时,如果期望的动作无法触发,那么进程会进入阻塞态(等待状态,让出CPU的调度),当期望动作可以被触发了,那么会被唤醒,然后处理事务。
- 阻塞I/O模式是最普遍使用的I/O模式,大部分程序使用的都是阻塞模式的I/O 。
- 前面学习的很多读写函数在调用过程中会发生阻塞。
读操作中的:read、recv、recvfrom、gets、scanf、fgets、getchar
写操作中的:write、send
其他操作:accept、connect
实例:
#include <stdio.h>
int main(int argc, char const *argv[])
{
char buf[64] = {0};
while (1)
{
gets(buf); //进程进入等待状态,发生CPU调度
printf("stdin -- %s\n", buf);
}
return 0;
}
程序运行后使用top命令查看当前进程的资源占用(CPU)非常小,进入了阻塞状态。所以大多数的系统接口都优先设计成阻塞型,以便能随时释放CPU资源。
非阻塞型模型
- 当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做轮询)。
- 应用程序不停的轮询内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ...);
功能:
获取/改变文件属性(linux中一切皆文件)
参数:
fd:文件描述符
cmd:设置的命令
F_GETFL //获取文件的属性
F_SETFL //设置文件的属性
第三个参数:由第二个参数决定,set时候需要设置的值,get时候填0
返回值:
文件状态标志(文件的属性)
-1 :失败
典型代码
int flag;//文件状态的标志
flag = fcntl(fd, F_GETFL); //读
flag |= O_NONBLOCK;//改 O_NONBLOCK = 0x00004000
fcntl(fd, F_SETFL, flag);//写
结论:非阻塞非常占用CPU,不到万不得已,不要用这种属性。
异步IO模型
- 通过信号方式,当内核检测到设备数据后,会主动给应用发送信号SIGIO。
- 应用程序收到信号后做异步处理即可。
- 应用程序需要把自己的进程号告诉内核,并打开异步通知机制。
标准模板
//将APP进程号告诉内核
fcntl(fd, F_SETOWN, getpid());
//使能异步通知
int flag;
flag = fcntl(fd, F_GETFL);
flag |= O_ASYNC; //也可以用FASYNC标志
fcntl(fd, F_SETFL, flag);
//异步通知
signal(SIGIO, handler);
示例:用异步IO的方式监听鼠标的数据(普通用户读取鼠标需要增加root权限)
用这个命令可以查看鼠标的原始数据
sudo hexdump /dev/input/mouse0
用阻塞模读取鼠标数据
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char const *argv[])
{
char buf[64] = {0};
int len;
//打开鼠标设备
int fd = open("/dev/input/mouse0", O_RDONLY);
if(fd < 0)
{
perror("open err");
return -1;
}
while (1)
{
//阻塞型
len = read(fd, buf, 64);
if(len < 0)
{
perror("read err");
return -1;
}
else
{
printf("len = %d\n", len);
}
}
return 0;
}
用异步IO模型来读取鼠标数据
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
static int fd;
void handler(int signum)
{
char buf[64] = {0};
int len;
//如果一旦这个方法被调用,也就意味着SIGIO信号被发出了
//代表当前设备有数据
len = read(fd, buf, 64);
if(len < 0)
{
perror("read err");
}
else
{
printf("len = %d\n", len);
}
}
int main(int argc, char const *argv[])
{
//打开鼠标设备
fd = open("/dev/input/mouse0", O_RDONLY);
if(fd < 0)
{
perror("open err");
return -1;
}
//以下代码是改成异步IO模式
#if 1
//将APP进程号告诉内核
fcntl(fd, F_SETOWN, getpid());
//使能异步通知
int flag;
flag = fcntl(fd, F_GETFL);
flag |= O_ASYNC; //也可以用FASYNC标志
fcntl(fd, F_SETFL, flag);
//异步通知
signal(SIGIO, handler);
#endif
while (1)
{
//可以做一些别的事情
printf("喝茶\n");
sleep(3);
}
return 0;
}
PS:当进程调用sleep函数进入休眠状态时,如果收到SIGIO信号,sleep函数的调用会被中断,进程会转而执行信号处理函数。处理完信号后,进程通常不会继续休眠,而是继续执行后续的代码。这种机制允许进程在休眠期间响应某些异步事件,如输入/输出操作完成等。
IO多路复用
场景假设
假设妈妈有三个孩子,分别不同的房间里睡觉,需要及时获知每个孩子是否醒了,如何做?
- 不停进每个房间看一下:简单,空闲时间还能干点别的,但是很累
- 告诉爸爸,让爸爸帮忙监听,妈妈可以干别的或者睡觉,孩子醒了后,爸爸告诉妈妈哪个孩子需要照顾,及时处理即可:既能得到休息,也能及时获知每个孩子的状态。
处理思想
- 应用程序中同时处理多路输入输出流,若采用阻塞模式,将得不到预期的目的;
- 若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间;
- 若设置多个进程/线程,分别处理一条数据通路,将新产生进程/线程间的同步与通信问题,使程序变得更加复杂;
- 比较好的方法是使用I/O多路复用技术。其基本思想是:
- 先构造一张监听描述符表(最大1024),然后调用一个函数进入监听状态。
- 当这些文件描述符中的一个或多个有数据时,函数才返回。
- 函数返回时告诉进程哪个描述符已就绪,可以进行I/O操作。
接口
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能:
实现IO的多路复用
参数:
nfds:关注的最大的文件描述符+1
readfds:关注的读表
writefds:关注的写表
exceptfds:关注的异常表
timeout:超时的设置
NULL:一直阻塞,直到有文件描述符就绪或出错
时间值为0:仅仅检测文件描述符集的状态,然后立即返回
时间值不为0:在指定时间内,如果没有事件发生,则超时返回0,并清空设置的时间值
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微秒 = 10^-6秒 */
};
返回值:
准备好的文件描述符的个数
-1 :失败:
0:超时检测时间到并且没有文件描述符准备好
注意:
select返回后,关注列表中只存在准备好的文件描述符
void FD_CLR(int fd, fd_set *set); //清除集合中的fd位
int FD_ISSET(int fd, fd_set *set);//判断fd是否在集合中 是--》1 不是---》0
void FD_SET(int fd, fd_set *set);//将fd放入关注列表中
void FD_ZERO(fd_set *set);//清空关注列表
编程步骤
- 把关注的文件描述符放入集合--FD_SET
- 监听集合中的文件描述符--select
- 依次判断哪个文件描述符有数据--FD_ISSET
- 依次处理有数据的文件描述符的数据
以下提到的监听表,都是位表,第几位被置位,那么就代表这个文件描述符需要被监听或者有数据
参考程序
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
//需求:同时监听标准输入和鼠标,不管哪一个有数据,及时处理,并且主进程不能占用太多资源。
int main(int argc, char const *argv[])
{
int fd;
//打开鼠标设备
fd = open("/dev/input/mouse0", O_RDONLY);
if(fd < 0)
{
perror("open err");
return -1;
}
fd_set rdfds;
int ret;
char buf[64] = {0};
while (1)
{
//1.把关注的文件描述符放入集合--FD_SET
FD_ZERO(&rdfds); //清空关注列表
FD_SET(0, &rdfds); //把标准输入放入到表中进行监听
FD_SET(fd, &rdfds); //把鼠标放入到表中进行监听
memset(buf, 0, 64);
//2.监听集合中的文件描述符--select
ret = select(fd + 1, &rdfds, NULL, NULL, NULL);
if(ret < 0)
{
perror("select err");
return -1;
}
//3.依次判断哪个文件描述符有数据--FD_ISSET
if(FD_ISSET(0, &rdfds)) //判断一下标准输入是否有数据
{
//标准输入有数据,那么就获取它的数据
gets(buf);
printf("stdin = %s\n", buf);
}
if(FD_ISSET(fd, &rdfds)) //判断一下鼠标是否有数据
{
//鼠标有数据,那么就获取它的数据
int len = read(fd, buf, 64);
if(len > 0)
{
printf("read mouse len = %d\n", len);
}
}
}
return 0;
}
总结
阻塞模型:不占用CPU,但是不能同时处理多个设备
非阻塞模型:能同时处理多个设备,但是非常耗费CPU
异步IO/信号驱动IO:完全解放了主线程,有数据会主动通知,异步调用处理方法,但是也不能同时处理多个设备。
IO多路复用:既能同时处理多个设备,又不占用CPU
以上这4种方法没有绝对优劣之分,在不同的场合使用不同的方法。
其它多路复用方案
select
• 一个进程最多只能监听1024个文件描述符 (千级别)
• select每次会清空表,每次都需要拷贝用户空间的表到内核空间,效率低
poll
• 优化文件描述符个数的限制(根据poll函数第一个函数的参数来定,如果监听的事件为1个,则结构体数组的大小为1,如果想监听100个,那么这个结构体数组的大小就为100,由程序员自己来决定)
• poll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
作用:监视并等待多个文件描述符的属性变化
参数:
fds:指向一个结构体数组的第0个元素的指针,每个数组元素都是一个struct pollfd结构,用于指定某个给定的fd的条件
nfds:一共需要监听几个描述符
timeout:等待的超时时间,-1:永远等待,0:立即返回,>0:等待相应的ms
返回值:同select
struct pollfd{
int fd; //文件描述符
short events; //等待的事件--POLLIN、POLLOUT、POLLERR
short revents; //实际发生的事件
};
poll机制参考程序
#include <stdio.h>
#include <poll.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, const char *argv[])
{
int fd_mouse;
// 1.创建一个用于保存要监测文件描述符的结构体
struct pollfd fds[2];
// 2.将关心的文件描述符及对应关心事件添加到结构体数据中
fd_mouse = open("/dev/input/mouse0", O_RDONLY);
if (fd_mouse < 0)
{
perror("open mouse2 err.");
return -1;
}
//标准输入放到表里,监听读事件
fds[0].fd = 0;
fds[0].events = POLLIN;
//鼠标放到表里,监听读事件
fds[1].fd = fd_mouse;
fds[1].events = POLLIN;
char buf[32];
int ret;
while (1)
{
if ((ret = poll(fds, 2, -1)) < 0)
{
perror("poll err.");
return -1;
}
else if (ret == 0)
{
printf("timeout .........\n");
}
if (fds[0].revents == POLLIN)
{
fgets(buf, 32, stdin);
printf("recv from stdin:%s\n", buf);
}
if (fds[1].revents == POLLIN)
{
memset(buf, 0, 32);
int ret = read(fd_mouse, buf, 32);
printf("read from mouse len = %d\n", ret);
}
}
close(fd_mouse);
return 0;
}
select() 和 poll() 系统调用的本质一样,poll() 的机制与 select() 类似,与 select() 在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是 poll() 没有最大文件描述符数量的限制(但是数量过大后性能也是会下降)。poll() 和 select() 同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
epoll
• 监听的最大的文件描述符没有个数限制(理论上,取决与你自己的系统)
• 异步I/O,Epoll当有事件产生被唤醒之后,文件描述符主动调用callback(回调函数)函数直接拿到唤醒的文件描述符,不需要轮询,效率极高