一、非阻塞IO
详解:https://blog.csdn.net/hguisu/article/details/7453390
https://www.cnblogs.com/myblesh/articles/2367242.html
1、同步与异步
同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)。
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。
而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
2、阻塞与非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
3、结合两概念分析
在进程通信层面,阻塞/非阻塞, 同步/异步基本是同义词, 但是需要注意区分讨论的对象是发送方还是接收方。发送方阻塞/非阻塞(同步/异步)和接收方的阻塞/非阻塞(同步/异步)是互不影响的。
在 IO 系统调用层面( IO system call )层面,非阻塞 IO 系统调用 和 异步 IO 系统调用存在着一定的差别, 它们都不会阻塞进程, 但是返回结果的方式和内容有所差别,但是都属于非阻塞系统调用( non-blocing system call ).
非阻塞系统调用(non-blocking I/O system call 与 asynchronous I/O system call) 的存在可以用来实现线程级别的 I/O 并发, 与通过多进程实现的 I/O 并发相比可以减少内存消耗以及进程切换的开销。
4、为什么有阻塞式
(1)常见的阻塞:wait、pause、sleep等函数;read或write某些文件时(例如read硬盘一个普通文件是不阻塞的,而去操作一个IO设备文件如键盘、鼠标是阻塞的),这两个函数本身并不是阻塞/非阻塞,而是操作不同文件表现不同.
(2)阻塞式的好处
可以提高cpu的利用率,在未等到某个东西时,进程交出CPU休眠,让CPU去做其他的事情.
5、非阻塞:非阻塞时,如果不能立刻得到结果,则该调用者不会阻塞当前线程。因此对应非阻塞的情况,调用者需要定时轮询查看处理状态。
(1)为什么要实现非阻塞
需要多路IO.当前进程需要读取多路IO,如鼠标,键盘,若读取鼠标卡住时,键盘则无法使用.
(2)如何实现非阻塞IO访问:O_NONBLOCK(open打开文件时使用的属性)和fcntl(打开文件后修改属性)
二、阻塞式IO的困境
1、程序中同时读取键盘和鼠标
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int fd = -1;
char buf[200] = {0};
int ret = -1;
int flag = -1;
fd = open("/dev/input/mouse0", O_RDONLY | O_NONBLOCK);
if (fd < 0)
{
printf("open error.\n");
exit(-1);
}
flag = fcntl(0, F_GETFL);
flag |= O_NONBLOCK;
fcntl(0, F_SETFL, flag);
while(1)
{
//读鼠标
memset(buf, 0, sizeof(buf));
ret = read(fd, buf, 50);
if (ret > 0)
{
printf("1.The contents read are:[%s].\n", buf);
ret = -1;
}
//读键盘,键盘是标准输入设备,默认是打开的,对应fd=0;
memset(buf, 0, sizeof(buf));
ret = read(0, buf, 50);
if (ret > 0)
{
printf("2.The contents read are:[%s]\n", buf);
ret = -1;
}
}
return 0;
}
#if 0
int main(int argc, char *argv[])
{
//读取鼠标
int fd = -1;
char buf[200] = {0};
#if 1
fd = open("/dev/input/mouse0", O_RDONLY | O_NONBLOCK);
if (fd < 0)
{
printf("open error.\n");
exit(-1);
}
else
{
memset(buf, 0, sizeof(buf));
printf("before mouse read:\n");
read(fd, buf, 50);
printf("The contents read are:[%s].\n", buf);
}
#endif
#if 1
//读取键盘
//把0号文件描述符(stdin)变成非阻塞式的
int flag = -1;
flag = fcntl(0, F_GETFL); //先获取原来的flag
flag |= O_NONBLOCK; //添加非阻塞属性
fcntl(0, F_SETFL, flag); //更新flag
// 这3步之后,0就变成了非阻塞式的了
memset(buf, 0, sizeof(buf));
printf("before keyboard read:\n");
read(0, buf, 50);
printf("The contents read are:[%s]\n", buf);
#endif
#endif
printf("\n");
return 0;
}
三、并发式IO的解决方案
几种并发式IO的实现方法:https://cloud.tencent.com/developer/article/1671535
1、什么是并发式IO呢?
可以简单理解为比如要同时读取几个文件的数据,但是这些文件什么时候可以读取是不确定的,要实现当某个文件可以读取的时候就立马去读取,这就是并发式。
fcntl()函数详解:
https://www.cnblogs.com/xuyh/p/3273082.html
https://blog.csdn.net/bailyzheng/article/details/7463775
2、非阻塞式IO(类似用一种轮询的方式实现,循环查看等待多路IO)
https://blog.csdn.net/weixin_47221359/article/details/109843773
当程序以非阻塞的方式读取设备数据时,在获取不到设备资源的情况下(设备不可用或数据未准备好),则不会进入休眠态,而是立刻向内核返回一个错误码,表示数据读取失败。应用程序会重新读取数据,循环读取直到数据读取成功。
/* 应用程序阻塞读取数据 */
int fd;
int data = 0;
fd = open("/dev/xxx_dev",O_RDWR); /* 阻塞方式打开 */
fd = open("/dev/xxx_dev",O_RDWR|O_NONBLOCK); /* 非阻塞方式打开 */
ret = read(fd,&data,sizeof(data)); /* 读取数据 */
有着本质的缺陷:错过等待的IO,例如还有1ms所需IO就到了,但是还要经过20ms你才可以再次检测该IO是否到来.
四、IO多路复用原理
详解:https://www.cnblogs.com/yungyu16/p/13064859.html
1、何为IO多路复用(一种技术\手段\编程使用的一种方法)
(1)IO multiplexing,单个线程,通过记录跟踪每个I/O流(sock)的状态,来同时管理多个I/O流 。
(2)用在什么地方?多路非阻塞式IO。
(3)select和poll
(4)外部阻塞式(两个函数本身就是阻塞式的),内部非阻塞式(自动轮询多路阻塞式IO)
2、select函数介绍
详解:https://blog.csdn.net/lingfengtengfei/article/details/12392449
https://www.cnblogs.com/alantu2018/p/8612722.html
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数:
首先我们先看一下最后一个。它指明我们要等待的时间:
struct timeval{
long tv_sec; /*秒 */
long tv_usec; /*微秒 */
};
有三种情况:
timeout == NULL 等待无限长的时间。等待可以被一个信号中断。当有一个描述符做好准备或
者是捕获到一个信号时函数会返回。
timeout->tv_sec == 0 &&timeout->tv_usec == 0不等待,直接返回。加入描述符集的描述
符都会被测试,并且返回满足要求的描述符的个数。这种方法通过轮询,无阻塞地获得了多个文
件描述符状态。
timeout->tv_sec !=0 ||timeout->tv_usec!= 0 等待指定的时间。当有描述符符合条件或者
超过超时时间的话,函数返回。在超时时间即将用完但又没有描述符合条件的话,返回 0。对于
第一种情况,等待也会被信号所中断。
中间的三个参数 readset, writset, exceptset,指向描述符集。这些参数指明了我们关心哪些描述符,和需要满足什么条件(可写,可读,异常)。
对于fd_set类型的变量我们所能做的就是声明一个变量,为变量赋一个同种类型变量的值,或者使用以下几个函数来控制它:
#include <sys/select.h>
int FD_ZERO(int fd, fd_set *fdset);
int FD_CLR(int fd, fd_set *fdset);
int FD_SET(int fd, fd_set *fd_set);
int FD_ISSET(int fd, fd_set *fdset);
FD_ZERO将一个 fd_set类型变量的所有位都设为0,使用FD_SET将变量的某个位置位。清除
某个位时可以使用 FD_CLR,我们可以使用 FD_ISSET来测试某个位是否被置位,某路IO是否发生。
第一个参数:最大的文件描述符加1。
返回值:
>0:就绪描述字的正数目
-1:出错
0:超时
3、poll函数介绍
详解:https://blog.csdn.net/qq_32642107/article/details/107291535
https://www.jb51.net/LINUXjishu/543401.html
select() 和 poll() 系统调用的本质一样,poll() 的机制与 select() 类似,与 select() 在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是 poll() 没有最大文件描述符数量的限制(但是数量过大后性能也是会下降)。
poll() 和 select() 同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
fds:指向一个结构体数组的第0个元素的指针,每个数组元素都是一个struct pollfd结构,
用于指定测试某个给定的fd的条件
struct pollfd{
int fd; //文件描述符
short events; //等待的事件
short revents; //实际发生的事件
};
fd:每一个 pollfd 结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示
poll() 监视多个文件描述符。
events:指定监测fd的事件(输入、输出、错误),每一个事件有多个取值,
revents:revents 域是文件描述符的操作结果事件,内核在调用返回时设置这个域。events 域中请求的任何事件都可能在 revents 域中返回.
注意:每个结构体的 events 域是由用户来设置,告诉内核我们关注的是什么,而 revents 域是返回时内核设置的,以说明对该描述符发生了什么事件
nfds:用来指定第一个参数数组元素个数
timeout: 指定等待的毫秒数,无论 I/O 是否准备好,poll() 都会返回.timeout指定为负
数值表示无限超时;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不
等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。
返回值:
成功时,poll() 返回结构体中 revents 域不为 0 的文件描述符个数;如果在超时前没有
任何事件发生,poll()返回 0;
失败时,poll() 返回 -1,并设置 errno 为下列值之一:
EBADF:一个或多个结构体中指定的文件描述符无效。
EFAULT:fds 指针指向的地址超出进程的地址空间。
EINTR:请求的事件之前产生一个信号,调用可以重新发起。
EINVAL:nfds 参数超出 PLIMIT_NOFILE 值。
ENOMEM:可用内存不足,无法完成请求。
五、IO多路复用实践
1、用select函数实现同时读取键盘鼠标
#include <stdio.h>
#include <sys/select.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
int fd = -1, ret = -1;
char buf[200] = {0};
//printf("%s.\n", buf);
fd_set readfds;
struct timeval tm;
int read_ret = 0;
fd = open("/dev/input/mouse0", O_RDONLY);
if (fd < 0)
{
perror("open mouse0 error");
exit(-1);
}
//当前共有两个fd,一个为0是标准输入默认打开的,一个是我们刚获得的鼠标文件的
//初始化处理readfds,为调用select函数做好准备
FD_ZERO(&readfds);
FD_SET(0, &readfds);
FD_SET(fd, &readfds);
tm.tv_sec = 100;
tm.tv_usec = 0;
ret = select(fd+1, &readfds, NULL, NULL, &tm);
if (ret < 0)
{
perror("select error");
exit(-1);
}
else if (ret == 0)
{
printf("timeout.\n");
}
else
{
//FD_ISSET来测试某个位是否被置位,某路IO是否发生。
if (FD_ISSET(0, &readfds))
{
//this is keyboard
memset(buf, 0, sizeof(buf));
read_ret = read(0, buf, 5);
if(read_ret < 0)
{
printf("read keyboard error.\n");
}
else
{
printf("keyboard information is:[%s].\n", buf);
read_ret = -1;
}
}
if (FD_ISSET(fd, &readfds))
{
//this is mouse
memset(buf, 0, sizeof(buf));
read_ret = read(fd, buf, 5);
if (read_ret < 0)
{
printf("read mouse error.\n");
}
else
{
printf("mouse information is:[%s].\n", buf);
}
}
}
return 0;
}
2、用poll函数实现同时读取键盘鼠标
#include <stdio.h>
#include <poll.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int fd = -1, ret = -1;
char buf[200] = {0};
int read_ret = 0;
struct pollfd fds[2] = {0};//定义结构体变量数组并初始化为空
fd = open("/dev/input/mouse0", O_RDONLY);
if (fd < 0)
{
perror("open mouse0 error");
exit(-1);
}
fds[0].fd = 0; //键盘
fds[0].events = POLLIN;//等待读操作
fds[1].fd = fd; //鼠标
fds[1].events = POLLIN;//等待读操作
ret = poll(fds, fd+1, 100000);
if (ret < 0)
{
perror("select error");
exit(-1);
}
else if (ret == 0)
{
printf("timeout.\n");
}
else
{
//FD_ISSET来测试某个位是否被置位,某路IO是否发生。
if (fds[0].events == fds[0].revents)
{
//this is keyboard
memset(buf, 0, sizeof(buf));
read_ret = read(0, buf, 5);
if(read_ret < 0)
{
printf("read keyboard error.\n");
}
else
{
printf("keyboard information is:[%s].\n", buf);
read_ret = -1;
}
}
if (fds[1].events == fds[1].revents)
{
//this is mouse
memset(buf, 0, sizeof(buf));
read_ret = read(fd, buf, 5);
if (read_ret < 0)
{
printf("read mouse error.\n");
}
else
{
printf("mouse information is:[%s].\n", buf);
}
}
}
return 0;
}
六、异步IO
详解:https://blog.csdn.net/hustcxl/article/details/78098560
https://www.cnblogs.com/skyofbitbit/p/3654531.html
1、何为异步IO
当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
(1)几乎可以认为:异步IO就是操作系统用软件实现的一套中断响应系统。
(2)异步IO的工作方法是:我们当前进程注册一个异步IO事件(使用signal注册一个信号SIGIO的处理函数),然后当前进程可以正常处理自己的事情,当异步事件发生后当前进程会收到一个SIGIO信号从而执行绑定的处理函数去处理这个异步事件。
2、异步IO实现
第一种:
(1)fcntl(F_GETFL、F_SETFL、O_ASYNC、F_SETOWN)
O_ASYNC:启用信号驱动I/O,生成一个信号(默认为SIGI,但这可以当输入或输出在这个文件描述符上成为可能时,通过fcntl(2)更改。此特性仅对终端、伪终端、套接字和(从Linux 2.6开始)管道和fifo。
(2)signal或者sigaction(SIGIO)
SIGIO:文件描述符准备就绪,可以开始进行输入/输出操作.
第二种:https://blog.csdn.net/shixin_0125/article/details/78898185
3.代码实践
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <stdlib.h>
int fd = -1;
void func(int sig)
{
char buf[200] = {0};
if (sig != SIGIO)
return;
read(fd, buf, 50);
printf("鼠标读出的内容是:[%s].\n", buf);
}
int main(int argc, char *argv[])
{
char buf[200] = {0};
int flag = -1;
int read_ret = -1;
fd = open("/dev/input/mouse0", O_RDONLY);
if (fd < 0)
{
perror("open mouse0 error");
exit(-1);
}
//将鼠标的文件描述符设置为可接受异步IO信号
flag = fcntl(fd, F_GETFL);
flag |= O_ASYNC;
fcntl(fd, F_SETFL, flag);
//把异步IO事件的接收进程设置为当前进程
fcntl(fd, F_SETOWN, getpid());
//注册当前进程的SIGIO信号捕获处理函数
signal(SIGIO, func);
//读键盘
while(1)
{
memset(buf, 0, sizeof(buf));
read_ret = read(0, buf, 5);
if (read_ret < 0)
{
printf("read keyboard error.\n");
}
else
{
printf("keyboard information is:[%s].\n", buf);
}
}
return 0;
}
七、存储映射IO
详解:https://blog.csdn.net/qq_36359022/article/details/79992287
https://blog.csdn.net/isunbin/article/details/83547616
1、存储映射I/O能将磁盘文件映射到存储空间的一个缓冲区。从而实现从缓冲区读,写文件。这样,就可以在不使用read和write的情况下执行I/O。使用存储映射来进行I/O操作一般有三个方面的应用。
(1)在两个相关进程之间提供一种通信方式,即共享内存。
(2)对帧缓冲设备的操作,该设备引用位图式显示。有了mmap就方便的多。例如:你需要在一块屏幕上显示不同的图片,并随时刷新新的图片,那么使用mmap就会方便很多。
(3)mmap将会提高对大文件传输的效率。
2、mmap函数
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
int munmap(void *addr, size_t length);
返回值:若成功,返回映射区的起始位置;若出错,返回MAP_FALLED
参数:
addr:建立映射区的首地址,由于Linux内核指定,使用时,直接传递NULL
length:欲创建映射区的大小
prot:映射区权限PROT_READ、PROT_WRITE、PROT_READ | PROT_WRITE
flag:标志位参数(常用于设定更新物理区、设置共享、创建匿名映射区)
MAP_SHARED:会将映射区所做的操作反映到物理设备(磁盘)上。
MAP_PRIVATE:映射区所做的修改不会反映到物理设备。
fd:用来建立映射区的文件描述符。
offset:映射文件的偏移量(4k的整数倍)
2、LCD显示和IPC之共享内存
IPC通信之----Linux共享内存和Android共享内存总结详解:https://blog.csdn.net/qq_22613757/article/details/88798734
3、存储映射IO的特点
(1)共享而不是复制,减少内存操作
(2)处理大文件时效率高,小文件不划算
注:本资料大部分由朱老师物联网大讲堂课程笔记整理而来,如有侵权,联系删除!水平有限,如有错误,欢迎各位在评论区交流。