Linux应用编程7之什么是高级IO
1.非阻塞IO
1.1、阻塞与非阻塞
1.2、为什么有阻塞式
(1)常见的阻塞:wait、pause、sleep等函数;read或write某些文件时
(2)阻塞式的好处:这种设计非常有利于操作系统的性能发挥。没有降低CPU的性能。
1.3、非阻塞
(1)为什么要实现非阻塞,非阻塞其实就是轮询
(2)如何实现非阻塞IO访问:O_NONBLOCK和fcntl
2.阻塞式IO的困境
2.1、程序中读取键盘
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
char buff[100];
memset(buff, 0, sizeof(buff));
//读取键盘
//键盘就是标准输入,stdin
printf("before read.\n");
read(0, buff, 2);
printf("读出的内容是:[%s].\n", buff);
return 0;
}
2.2、程序中读取鼠标
int main(void)
{
int fd = -1;
char buff[100];
memset(buff, 0, sizeof(buff));
fd = open("/dev/input/mouse0", O_RDONLY);
if (fd < 0)
{
perror("open");
return -1;
}
read(fd, buff, 5);
printf("读出的内容是:[%s].\n", buff);
return 0;
}
2.3、程序中同时读取键盘和鼠标
int main(void)
{
//读鼠标
int fd = -1;
char buff[100];
memset(buff, 0, sizeof(buff));
fd = open("/dev/input/mouse0", O_RDONLY);
if (fd < 0)
{
perror("open");
return -1;
}
read(fd, buff, 50);
printf("鼠标读出的内容是:[%s].\n", buff);
memset(buff, 0, sizeof(buff));
//读取键盘
//键盘就是标准输入,stdin
read(0, buff, 5);
printf("键盘读出的内容是:[%s].\n", buff);
return 0;
}
2.4、问题分析
必须先鼠标后键盘才能有输出,必须要有先后顺序。
3.并发式IO的解决方案
3.1、非阻塞式IO
int main(void)
{
int flg = -1;
//读鼠标
int fd = -1;
char buff[100];
memset(buff, 0, sizeof(buff));
fd = open("/dev/input/mouse0", O_RDONLY|O_NONBLOCK);
if (fd < 0)
{
perror("open");
return -1;
}
read(fd, buff, 50);
printf("鼠标读出的内容是:[%s].\n", buff);
memset(buff, 0, sizeof(buff));
//读取键盘
//键盘就是标准输入,stdin,要变成非阻塞
//把0的文件描述符变成非阻塞
flg = fcntl(0, F_GETFL);//读取原来的FLAG
flg |= O_NONBLOCK;//设置
fcntl(0, F_SETFL, flg);//写属性
//完成非阻塞设置
read(0, buff, 5);
printf("键盘读出的内容是:[%s].\n", buff);
return 0;
}
int main(void)
{
int ret = -1;
int flg = -1;
//读鼠标
int fd = -1;
char buff[100];
//读取键盘
//键盘就是标准输入,stdin,要变成非阻塞
//把0的文件描述符变成非阻塞
flg = fcntl(0, F_GETFL);//读取原来的FLAG
flg |= O_NONBLOCK;//设置
fcntl(0, F_SETFL, flg);//写属性
//完成非阻塞设置
fd = open("/dev/input/mouse0", O_RDONLY|O_NONBLOCK);
if (fd < 0)
{
perror("open");
return -1;
}
while(1)
{
memset(buff, 0, sizeof(buff));
ret = read(fd, buff, 50);
if(ret > 0)
{
printf("鼠标读出的内容是:[%s].\n", buff);
}
memset(buff, 0, sizeof(buff));
ret = read(0, buff, 5);
if (ret > 0)
{
printf("键盘读出的内容是:[%s].\n", buff);
}
}
return 0;
}
3.2、多路复用IO
3.3、异步通知(异步IO)
4.IO多路复用原理
4.1、何为IO多路复用
(1)IO multiplexing
(2)用在什么地方?多路非阻塞式IO。
(3)select和poll
(4)外部阻塞式,内部非阻塞式自动轮询多路阻塞式IO
4.2、select函数介绍
4.3、poll函数介绍
5.IO多路复用实践
5.1、用select函数实现同时读取键盘鼠标
int main(void)
{
int ret = -1;
int fd = -1;
char buff[100];
fd_set myset;
struct timeval tm;
//读取键盘
fd = open("/dev/input/mouse0", O_RDONLY|O_NONBLOCK);
if (fd < 0)
{
perror("open");
return -1;
}
//目前有两个文件描述符,添加到描述符集
FD_ZERO(&myset);
FD_SET(fd, &myset);
FD_SET(0, &myset);
tm.tv_sec = 5;
tm.tv_usec = 0;
//使用select函数
ret = select(fd+1, &myset, NULL,NULL, &tm);
if(ret < 0)
{
perror("select:");
return -1;
}
else if(ret == 0)
{
printf("超时了!.\n");
return -1;
}
else
{
//检测到底哪个IO到了
if(FD_ISSET(0, &myset))
{
//这里处理键盘
memset(buff, 0, sizeof(buff));
ret = read(0, buff, 5);
if (ret > 0)
{
printf("键盘读出的内容是:[%s].\n", buff);
}
}
if(FD_ISSET(fd, &myset))
{
//这里处理鼠标
memset(buff, 0, sizeof(buff));
ret = read(fd, buff, 50);
if(ret > 0)
{
printf("鼠标读出的内容是:[%s].\n", buff);
}
}
}
return 0;
}
5.2、用poll函数实现同时读取键盘鼠标
int main(void)
{
// 读取鼠标
int fd = -1, ret = -1;
char buf[200];
struct pollfd myfds[2] = {0};
fd = open("/dev/input/mouse1", O_RDONLY);
if (fd < 0)
{
perror("open:");
return -1;
}
// 初始化我们的pollfd
myfds[0].fd = 0; // 键盘
myfds[0].events = POLLIN; // 等待读操作
myfds[1].fd = fd; // 鼠标
myfds[1].events = POLLIN; // 等待读操作
ret = poll(myfds, fd+1, 10000);
if (ret < 0)
{
perror("poll: ");
return -1;
}
else if (ret == 0)
{
printf("超时了\n");
}
else
{
// 等到了一路IO,然后去监测到底是哪个IO到了,处理之
if (myfds[0].events == myfds[0].revents)
{
// 这里处理键盘
memset(buf, 0, sizeof(buf));
read(0, buf, 5);
printf("键盘读出的内容是:[%s].\n", buf);
}
if (myfds[1].events == myfds[1].revents)
{
// 这里处理鼠标
memset(buf, 0, sizeof(buf));
read(fd, buf, 50);
printf("鼠标读出的内容是:[%s].\n", buf);
}
}
return 0;
}
5.3 Select和poll区别
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:
1、select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024。
一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.
2、对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。
当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
基本原理:poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。
它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:
1)大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
2)poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
注意:从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
6.异步IO
6.1、何为异步IO
(1)几乎可以认为:异步IO就是操作系统用软件实现的一套中断响应系统。
(2)异步IO的工作方法是:我们当前进程注册一个异步IO事件(使用signal注册一个信号SIGIO的处理函数),然后当前进程可以正常处理自己的事情,当异步事件发生后当前进程会收到一个SIGIO信号从而执行绑定的处理函数去处理这个异步事件。
6.2、涉及的函数:
(1)fcntl(F_GETFL、F_SETFL、O_ASYNC、F_SETOWN)
(2)signal或者sigaction(SIGIO)
6.3.代码实践
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
int mousefd = -1;
// 绑定到SIGIO信号,在函数内处理异步通知事件
void func(int sig)
{
char buf[200] = {0};
if (sig != SIGIO)
return;
read(mousefd, buf, 50);
printf("鼠标读出的内容是:[%s].\n", buf);
}
int main(void)
{
// 读取鼠标
char buf[200];
int flag = -1;
mousefd = open("/dev/input/mouse1", O_RDONLY);
if (mousefd < 0)
{
perror("open:");
return -1;
}
// 把鼠标的文件描述符设置为可以接受异步IO
flag = fcntl(mousefd, F_GETFL);
flag |= O_ASYNC;
fcntl(mousefd, F_SETFL, flag);
// 把异步IO事件的接收进程设置为当前进程
fcntl(mousefd, F_SETOWN, getpid());
// 注册当前进程的SIGIO信号捕获函数
signal(SIGIO, func);
// 读键盘
while (1)
{
memset(buf, 0, sizeof(buf));
//printf("before 键盘 read.\n");
read(0, buf, 5);
printf("键盘读出的内容是:[%s].\n", buf);
}
return 0;
}
7.存储映射IO
7.1、mmap函数
7.2、LCD显示和IPC之共享内存
7.3、存储映射IO的特点
(1)共享而不是复制,减少内存操作
(2)处理大文件时效率高,小文件不划算