3.6.1.非阻塞IO
3.6.1.1、阻塞与非阻塞
阻塞:阻塞具有很多优势(是linux系统的默认设置),单路IO的时候使用阻塞式IO没有降低CPU的性能
补充:阻塞/非阻塞, 它们是程序在等待消息(无所谓同步或者异步)时的状态.
阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。
有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。
对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
3.6.1.2、为什么有阻塞式
(1)常见的阻塞:wait(显式回收子进程)、pause、sleep等默认阻塞函数;read或write某些文件时也是默认阻塞式的
(2)阻塞式的好处:当前线程被挂起等待条件满足才返回结果
3.6.1.3、非阻塞
(1)为什么要实现非阻塞
(2)如何实现【非阻塞IO访问】:
1)打开文件时加入O_NONBLOCK
2)fcntl函数,对文件描述符(文件已经打开)进行操作
单路IO就用阻塞式比较好;
多路IO最好是用非阻塞式的。避免资源被一个占有不放,让其他IO有资源可抢。
从CPU的利用角度来看,单路IO就是一对一;多路IO就是一对多,即一个CPU对应多个资源抢占通道。前者单路效率更高,后者需要兼顾分配。单路IO模型只需要监听一个IO流,多路IO模型可以同时监听(内部轮循)多个IO流,CPU要不停去查看。
3.6.2.阻塞式IO的困境
3.6.2.1、程序中读取键盘
3.6.2.2、程序中读取鼠标 cat /dev/input/mouse1
3.6.2.3、程序中同时读取键盘和鼠标
3.6.2.4、问题分析
并不是所有的情况下都适阻塞式io的。
代码示例:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
/*
程序中读取键盘和鼠标
*/
#define NAME "/dev/input/mouse1"
char buff[100]={0};
char buf[100]={0};
int main(int argc,char **argv)
{
int ssize_t=-1;
int fd=-1;
fd=open(NAME,O_RDWR);
if(-1==fd)
{
perror("open");
_exit(-1);
}
printf("打开成功!fd=%d\n",fd);
while(1){
ssize_t=read(fd,buff,sizeof(buff));
if(-1==ssize_t)
{
perror("read");
_exit(-1);
}
printf("读到的鼠标字符数为%d\n",ssize_t);
printf("读到的鼠标字符是[%s]\n",buff);
read(0,&buf,sizeof(buf));
printf("读到的鼠标字符是[%s]\n",buf);
sleep(1);
}
return 0;
}
3.6.3.并发式IO的3种解决方案【并发式IO】
3.6.3.1、非阻塞式IO:性能不够好,有点类似于轮询的方式,CPU不停的去查看
代码示例:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define NAME "/dev/input/mouse1"
char buff[100]={0}; //键盘
char buf[100]={0};//鼠标
int main(void)
{
int ssize_t=-1;
int fd=-1;
int flag=-1;
int ret=-1;
fd=open(NAME,O_RDWR | O_NONBLOCK );
if(-1==fd)
{
perror("open");
_exit(-1);
}
//把标准输入文件描述符0通过fcntl函数变成非阻塞式子
// 把0号文件描述符(stdin)变成非阻塞式的
flag = fcntl(0, F_GETFL); // 先获取原来的flag
flag |= O_NONBLOCK; // 添加非阻塞属性
fcntl(0, F_SETFL, flag); // 更新flag
// 这3步之后,0就变成了非阻塞式的了
while (1)
{
// 读鼠标
memset(buf, 0, sizeof(buf));
//printf("before 鼠标 read.\n");
ret = read(fd, buf, 50);
if (ret > 0)
{
printf("鼠标读出的内容是:[%s].\n", buf);
}
// 读键盘
memset(buff, 0, sizeof(buff));
//printf("before 键盘 read.\n");
ret = read(0, buff, 5);
if (ret > 0)
{
printf("键盘读出的内容是:[%s].\n", buff);
}
}
return 0;
}
3.6.3.2、多路复用IO :性能相对比较好,解决并发性IO的解决
3.6.4.IO多路复用原理
3.6.4.1、何为IO多路复用 【说白了,多路复用其实就是一个管多个】
(1)IO multiplexing
(2)用在什么地方?多路非阻塞式IO(多路及时响应)。
(3)select和poll两个函数:(poll出现的比较晚一点。其性能也要比select函数的性能更好一些)
(4)外部阻塞式(select和poll两个函数本身在调用的时候是阻塞式的,对外表现为阻塞式),内部非阻塞式(
两个函数内部实现的时候,也就是当把要监听的东西(比如A和B两个阻塞式IO)放在这个函数的监听范围)【自动】轮询【这两个多路阻塞式IO】 (轮询的意思就是CPU不停的去查看)
补充:
可是使用Select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要
等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。
3.6.4.2、select函数介绍:
#include <sys/select.h>
#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); //nfds表示文件描述符的个数 nfds is the highest-numbered file descriptor in any of the //three sets, plus 1,是指集合中所有文件描述符的范围,【即所有文件描述符的最大值加1】这一点要注意,所有文件描述符的最大值加1,比如一个文件描述符是0,一个文件描述符是1,则为1+1
相关函数:
(1)void FD_ZERO(fd_set *set); //把文件描述符集合清零
(2)void FD_SET(int fd, fd_set *set); //把某个文件描述符添加到这个集合中去
(3)int FD_ISSET(int fd, fd_set *set);
//检查集合中指定的文件描述符是否可以读写 FD_ISSET() tests to see if a file descriptor is part of the set; this is useful after select() returns.
(4)void FD_CLR(int fd, fd_set *set); //把一个给定的文件描述符从集合中删除
相关结构体:
(1)struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor)。
(2)
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
struct timeval* timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
linux内部代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
fd_set rfds;
struct timeval tv;
int retval;
/* Watch stdin (fd 0) to see when it has input. */
FD_ZERO(&rfds); //先把文件描述符集合清零
FD_SET(0, &rfds); //把0文件描述符添加到这个集合中去
/* Wait up to five seconds. */
tv.tv_sec = 5;
tv.tv_usec = 0;
retval = select(1, &rfds, NULL, NULL, &tv);
/* Don't rely on the value of tv now! */
if (retval == -1)
perror("select()");
else if (retval)
printf("Data is available now.\n");
/* FD_ISSET(0, &rfds) will be true. */
else
printf("No data within five seconds.\n");
exit(EXIT_SUCCESS);
}
3.6.4.3、poll函数介绍
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
#define _GNU_SOURCE /* See feature_test_macros(7) */
结构体:
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
代码示例:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <poll.h>
/*
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
*/
#define NAME "/dev/input/mouse1"
int main(void)
{
char buf[100]={0}; //注意这里的定义的buf不能够使用全局的
//int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd a[2]={0};
int fd= open(NAME,O_RDWR); //定义鼠标文件描述符
if(fd<0)
{
perror("open mouse");
_exit(-1);
}
//实例化结构体
a[0].fd=fd; //鼠标
a[0].events=POLLIN;
a[1].fd=0; //键盘
a[1].events=POLLIN;
int ret=poll(a,fd+1,10000);
if(ret<0)
{
perror("poll");
_exit(-1);
}
if(ret==0)
{
printf("超时了");
}
else //判断是谁发生了IO
{
if(a[0].events==a[0].revents) //鼠标
{
memset(buf,0,sizeof(buf));
read(fd,buf,10);
printf("读出来的鼠标内容是:[%s]\n",buf);
}
if(a[1].events==a[1].revents) //键盘
{
memset(buf,0,sizeof(buf));
read(0,buf,50);
printf("读出来的键盘内容是:[%s]\n",buf);
}
}
return 0;
}
3.6.5.IO多路复用实践
3.6.5.1、用select函数实现同时读取键盘鼠标
select()函数实现IO多路复用的步骤
(1)清空描述符集合
(2)建立需要监视的描述符与描述符集合的关系
(3)调用select函数
(4)检查监视的描述符判断是否已经准备好
(5)对已经准备好的描述符进程IO操作
代码示例:
/*
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#define NAME "/dev/input/mouse1"
char buf[100]={0};
int main(void)
{
int fd=-1;
fd_set readfds=-1;
struct timeval time;
int ret=-1;
fd=open(NAME,O_RDWR);
printf("fd=%d\n",fd);
if(-1==fd)
{
perror("open");
_exit(-1);
}
FD_ZERO(&readfds); //清除文件描述符集合
FD_SET(fd,&readfds);
FD_SET(0,&readfds);
time.tv_sec=5; //设置时间为5秒
time.tv_usec=0;
ret=select(3,&readfds,NULL,NULL,&time); //调用select函数 ,注意第一个参数
if (ret < 0) //表示出错
{
perror("select: ");
return -1;
}
else if (ret == 0) //表示超过了规定的时间限制
{
printf("超时了\n");
}
else
{
// 等到了一路IO,然后去监测到底是哪个IO到了,处理之
if (FD_ISSET(0, &readfds))
{
// 这里处理键盘
memset(buf, 0, sizeof(buf));
read(0, buf, 5);
printf("键盘读出的内容是:[%s].\n", buf);
}
if (FD_ISSET(fd, &readfds))
{
// 这里处理鼠标
memset(buf, 0, sizeof(buf));
read(fd, buf, 50);
printf("鼠标读出的内容是:[%s].\n", buf);
}
}
return 0;
}
3.6.5.2、用poll函数实现同时读取键盘鼠标
poll函数介绍:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
第一个参数 pollfd 结构体定义如下:
引用
/* Data structure describing a polling request. */
struct pollfd
{
int fd; /* poll 的文件描述符. */
short int events; /* fd 上感兴趣的事件(等待的事件或者说是监视的事件). */
short int revents; /* fd 上实际发生的事件. */
};
fd 成员表示感兴趣的,且打开了的文件描述符;
events 成员是位掩码,用于指定针对这个文件描述符感兴趣的事件;
revents 成员是位掩码,用于指定当 poll 返回时,在该文件描述符上已经发生了哪些事情。
events 和 revents 结合下列常数值(宏)指定即将唤醒的事件或调查已结束的 poll() 函数被唤醒的原因,这些宏常数如下:
POLLIN
events 中使用该宏常数,能够在折本文件的可读情况下,结束 poll() 函数。相反,revents 上使用该宏常数,在检查 poll() 函数结束后,可依此判断设备文件是否处于可读状态(即使消息长度是 0)。
POLLPRI
在 events 域中使用该宏常数,能够在设备文件的高优先级数据读取状态下,结束 poll() 函数。相反,revents 上使用该宏常数,在检查 poll() 函数结束后,可依此判断设备文件是否处于可读高优先级数据的状态(即使消息长度是 0)。该宏常数用于处理网络信息包(packet) 的数据传递。
POLLOUT
在 events 域中使用该宏常数,能够在设备文件的写入状态下,结束 poll() 函数。相反,revents 域上使用该宏常数,在检查 poll() 结束后,可依此判断设备文件是否处于可写状态。
POLLERR
在 events 域中使用该宏常数,能够在设备文件上发生错误时,结束 poll() 函数。相反,revents 域上使用该宏函数,在检查 poll() 函数结束后,可依此判断设备文件是否出错。
POLLHUP
在 events 域中使用该宏常数,能够在设备文件中发生 hungup 时,结束 poll() 函数 。相反,在检查 poll() 结束后,可依此判断设备文件是否发生 hungup 。
POLLNVAL
在 events 域中使用该宏函数,能够在文件描述符的值无效时,结束 poll() 。相反,在 revents 域上使用该宏函数时,在检查 poll() 函数后,文件描述符是否有效。可用于处理网络信息时,检查 socket handler 是否已经无效。
最后一个参数 timeout 指定 poll() 将在超时前等待一个事件多长事件。这里有 3 种情况:
1) timeout 为 -1
这会造成 poll 永远等待。poll() 只有在一个描述符就绪时返回,或者在调用进程捕捉到信号时返回(在这里,poll 返回 -1),并且设置 errno 值为 EINTR 。-1 可以用宏定义常量 INFTIM 来代替(在 pth.h 中有定义) 。
2) timeout 等于0
在这种情况下,测试所有的描述符,并且 poll() 立刻返回。这允许在 poll 中没有阻塞的情况下找出多个文件描述符的状态。
3) time > 0
这将以毫秒为单位指定 timeout 的超时周期。poll() 只有在超时到期时返回,除非一个描述符变为就绪,在这种情况下,它立刻返回。如果超时周期到齐,poll() 返回 0。这里也可能会因为某个信号而中断该等待。
和 select 一样,文件描述符是否阻塞对 poll 是否阻塞没有任何影响。
代码示例:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <poll.h>
#define NAME "/dev/input/mouse1"
int main(void)
{
char buf[100]={0}; //注意这里的定义的buf不能够使用全局的
//int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd a[2]={0};
int fd= open(NAME,O_RDWR); //定义鼠标文件描述符
if(fd<0)
{
perror("open mouse");
_exit(-1);
}
//实例化结构体
a[0].fd=fd; //鼠标
a[0].events=POLLIN;
a[1].fd=0; //键盘
a[1].events=POLLIN;
int ret=poll(a,fd+1,10000);
if(ret<0)
{
perror("poll");
_exit(-1);
}
if(ret==0)
{
printf("超时了");
}
else //判断是谁发生了IO
{
if(a[0].events==a[0].revents) //鼠标
{
memset(buf,0,sizeof(buf));
read(fd,buf,10);
printf("读出来的鼠标内容是:[%s]\n",buf);
}
if(a[1].events==a[1].revents) //键盘
{
memset(buf,0,sizeof(buf));
read(0,buf,50);
printf("读出来的键盘内容是:[%s]\n",buf);
}
}
return 0;
}
3.6.6.异步IO
3.6.6.1、何为异步IO
(1)几乎可以认为:异步IO就是操作系统用软件实现的一套中断响应系统(有点类似与硬件中断)。
有两种类型的文件IO同步:同步文件IO和异步文件IO。异步文件IO也就是重叠IO。【在同步文件IO中,线程启动一个IO操作然后就立即进入等待状态,直到IO操作完成后才醒来继续执行。而异步文件IO方式中,线程发送一个IO请求到内核,然后继续处理其他的事情,内核完成IO请求后,将会通知线程IO操作完成了。】
如果IO请求需要大量时间执行的话,异步文件IO方式可以显著提高效率,因为在线程等待的这段时间内,CPU将会调度其他线程进行执行,如果没有其他线程需要执行的话,这段时间将会浪费掉(可能会调度操作系统的零页线程)。如果IO请求操作很快,用异步IO方式反而还低效,还不如用同步IO方式。
同步IO在同一时刻只允许一个IO操作,也就是说对于同一个文件句柄的IO操作是序列化的,即使使用两个线程也不能同时对同一个文件句柄同时发出读写操作。重叠IO允许一个或多个线程同时发出IO请求。
异步IO在请求完成时,通过将文件句柄设为有信号状态来通知应用程序,或者应用程序通过GetOverlappedResult察看IO请求是否完成,也可以通过一个事件对象来通知应用程序。
简单的说“同步在编程里,一般是指某个IO操作执行完后,才可以执行后面的操作。异步则是,将某个操作给系统,主线程去忙别的事情,等内核完成操作后通知主线程异步操作已经完成。”
(2)异步IO的工作方法是:我们当前进程向内核注册一个异步IO事件(使用signal注册一个信号SIGIO的处理函数),然后当前进程可以正常处理自己的事情,内核就去帮你完成或者说是检测你希望的事件是否发生,当异步事件发生后当前进程会收到内核传来的一个SIGIO信号(类似于中断信号)从而执行绑定的处理函数去处理这个异步事件。
3.6.6.2、涉及的函数:
(1)fcntl(F_GETFL、F_SETFL、O_ASYNC、F_SETOWN) 设置异步通知
(2)signal或者sigaction函数(SIGIO)
3.6.3.代码实践
3.6.7.存储映射IO
存储映射IO使一个磁盘文件(物理地址)与存储空间(内存地址)中的一个缓冲区相映射。于是当从缓冲区取数据,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区,则相应字节就自动地写入文件。这样就可以在不使用read和write的情况下执行IO。为了使用这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中,这是由mmap函数实现的。
3.6.7.1、mmap函数:把一个磁盘文件和一个内存映射起来 内存映射函数
3.6.7.2、LCD显示和IPC之共享内存
3.6.7.3、存储映射IO的特点
(1)共享而不是复制,减少内存操作
(2)处理大文件时效率高,小文件不划算(视频用到的也比较多)
函数原型:
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
addr参数用于指定映射存储区的起始地址,通常将其设置为0,这表示由系统选择该映射区的起始地址,此函数的返回地址是该映射区的起始地址。
fd指定要被映射文件的描述符,在映射该文件到一个地址空间之前,先要打开该文件。
length是映射的字节数。
offset是要映射字节在文件中的起始偏移量。
prot参数说明对映射存储区的保护要求,但不能超过文件open模式访问权限,prot可选值如下:
PROT_EXEC Pages may be executed.
PROT_READ Pages may be read.
PROT_WRITE Pages may be written.
PROT_NONE Pages may not be accessed.
flags参数影响映射存储区的多种属性,其中MAP_SHARED和MAP_PRIVATE两者必须选择其一,还有许多其它的MAP_XXX是可选的。
【还有一种就是多进程下,父进程fork创建一个子进程来处理不同的事情】。