笔记地址:https://note.youdao.com/ynoteshare1/index.html?id=09f9fa91b90576adac4640f48b9210a3&type=note

前面第一节我们学习了对IO的open、read、write等阻塞式文件操作,这一篇我们将会学习对IO的一些高级操作。 2019/04/30 20:51

目录

一、非阻塞IO的引入

1、阻塞与非阻塞

2、如何修改阻塞为非阻塞

二.阻塞式IO的困境(举例)

1、程序中读取键盘

2、程序中读取鼠标

 

3、程序中同时读取键盘和鼠标

 

4、问题分析

三.并发式IO的解决方案

1、非阻塞式IO

2、利用两个进程实现同时读键盘和鼠标

3、多路复用IO

IO多路复用原理

select函数

select函数测试用例

poll函数

poll函数测试用例

4、异步通知(异步IO)

何为异步IO

异步IO设置的步骤如下:

 

5、存储映射IO

1、存储映射的好处

2、存储映射IO的特点

3、存储映射IO需要使用mmap函数,这里,我们暂不学习。


一、非阻塞IO的引入

1、阻塞与非阻塞

  • a) 函数操作文件时,因文件类型而阻塞,阻塞与函数本身无关

·读某些文件:由于数据不存在会导致调用者永远阻塞

 读管道文件:管道是进程间通信用的特殊文件,读管道时,如果管道中并无数据会导致

 对管道的读操作会阻塞。

  •  b) 写某些文件:在写某些文件时,当文件不能立即接收写入的数据时,也可能会导致
  • c) 某些函数本身就是阻塞的

wait、pause、sleep等函数以及某些进程间通信的函数(如当消息队列的消息接受函数设置了阻塞时),这些函数调用本身就是阻塞的

 

2、如何修改阻塞为非阻塞

通过前面的学习,我们知道某些文件默认打开后默认对文件的操作就是阻塞的,但是利用对文件描述符设置,可将其操作设置为非阻塞的,主要的方法有如下两种:

  • 1)打开文件时指定非阻塞,以非阻塞的方式打开标准输入文件(O_NONBLOCK函数,指定为了非阻塞)

 

int main(void){

int fd = -1;

fd = open("/dev/stdin", O_RDONLY|O_NONBLOCK);

if(fd < 0){

perror("open stdin is fail");

exit(-1);

}

return 0;

}

 

  • 2)将已经打开了的文件描述符设置为非阻塞的

(用fcntl函数进行设置,具体可查看第一节对fcntl函数的梳理)

int main(void)

{

int fd = -1, flag = -1;

/* F_GETFL:获取描述符原有状态给flag的命令,目的是为了保护原有的状态

* STDIN_FILENO:指向标准输入文件的文件描述符0 */

flag = fcntl(STDIN_FILENO, F_GETFL);

flag |= O_NONBLOCK;//将原有文件状态 | 非阻塞标志

//将修改后的包含了非阻塞标志的新状态重新设置回去

fcntl(STDIN_FILENO, F_SETFL, flag);

return 0;

}

 

二.阻塞式IO的困境(举例)

1、程序中读取键盘

int main(void)

{

// 读取键盘

// 键盘就是标准输入,故为0号文件描述符,stdin

char buf[100];

memset(buf, 0, sizeof(buf));

printf("before read.\n");

read(0, buf, 5);

printf("读出的内容是:[%s].\n", buf);

return 0;

}

九.linux中的高级IO

 

2、程序中读取鼠标

我们鼠标的驱动文件如下:当查看mouse1,移动鼠标,并没有数据,当查看mouse0时,有乱码的数据,故我们的鼠标设备文件为:mouse0

九.linux中的高级IO

int main(void)

{

// 读取鼠标

int fd = -1;

char buf[200];

fd = open("/dev/input/mouse0", O_RDONLY);

if (fd < 0)

{

perror("open:");

return -1;

}

memset(buf, 0, sizeof(buf));

printf("before read.\n");

read(fd, buf, 50);

printf("读出的内容是:[%s].\n", buf);

return 0;

}

九.linux中的高级IO

 

3、程序中同时读取键盘和鼠标

int main(void)

{

// 读取鼠标

int fd = -1;

char buf[200];

fd = open("/dev/input/mouse1", O_RDONLY);

if (fd < 0)

{

perror("open:");

return -1;

}

memset(buf, 0, sizeof(buf));

printf("before 鼠标 read.\n");

read(fd, buf, 50);

printf("鼠标读出的内容是:[%s].\n", buf);

// 读键盘

memset(buf, 0, sizeof(buf));

printf("before 键盘 read.\n");

read(0, buf, 5);

printf("键盘读出的内容是:[%s].\n", buf);

return 0;

}

九.linux中的高级IO

 

4、问题分析

(程序上设计是先读取鼠标,再读取键盘),如果顺序用反了,会怎么样???

 

运行程序时由于先读的是鼠标,它阻塞了键盘,所以我们先键盘敲入数据是没有用的,当移动鼠标后,鼠标数据打印出来,这时进程又阻塞在了读鼠标处,所以此时移动鼠标是没有用的,这时必须键盘敲入数据,才能回到读鼠标处(标准输入处),由于鼠标数据是整形的坐标值,所打印出来是乱码。

 

所以,我们先输入键盘后鼠标,则会出错,这就是阻塞式IO的问题

那么怎么解决这个问题纳,那就得引入并发式IO的解决方案了。

 

 

三.并发式IO的解决方案

1、非阻塞式IO

上面我们知道,键盘和鼠标的读导致了相互的阻塞,我们输入时并不通畅,现在我们将它们都改为非阻塞的,那么它们就不会相互阻塞,输入就会变得通畅

int main(void)

{

// 读取鼠标

int fd = -1;

char buf[200];

int flag=-1;

int ret=-1;

fd = open("/dev/input/mouse0", O_RDONLY | O_NONBLOCK);

if (fd < 0)

{

perror("open:");

return -1;

}

//F_GETFL:获取描述符原有状态给flag的命令,目的是为了保护原有的状态

// STDIN_FILENO:指向标准输入文件的文件描述符0

flag = fcntl(STDIN_FILENO, F_GETFL);

flag |= O_NONBLOCK;//将原有文件状态 | 非阻塞标志

fcntl(STDIN_FILENO, F_SETFL, flag);//将修改后的包含了非阻塞标志的新状态重新设置回去

 

while(1)

{

//读鼠标

memset(buf, 0, sizeof(buf));

//printf("before 鼠标 read.\n");

ret=read(fd, buf, 50);

if(ret>0) //>0,表示已经接收到数据

{

printf("鼠标读出的内容是:[%s].\n", buf);

}

// 读键盘

memset(buf, 0, sizeof(buf));

//printf("before 键盘 read.\n");

ret=read(0, buf, 5);

if(ret>0)

{

printf("键盘读出的内容是:[%s].\n", buf);

}

 

}

return 0;

}

这么设置后,鼠标和键盘不再相互阻塞,所以运行这个程序时不必再忌讳谁先输入的问题了,谁先输入都可以。

只是这种非阻塞会导致进程时刻都处在循环的,这种轮询的机制会非常的消耗cpu资源,并不提倡这方法

 

2、利用两个进程实现同时读键盘和鼠标

int main(void)

{

char buf[100] = {0};

int fd = -1, ret = -1;

 

/* 开两个进程,父进程读鼠标,子进程读键盘 */

ret = fork();

if(ret == 0)//为0,表示还没有接收到东西

{

while(1)

{

/* 读键盘 */

memset(buf, 0, sizeof(buf));

ret=read(0, buf, 5);

if(ret > 0) {printf("键盘读出的内容是:[%s].\n", buf);}

}

}

else if(ret > 0)

{

fd = open("/dev/input/mouse0", O_RDONLY |O_NONBLOCK);

if(fd < 0)

{

perror("open error");

return -1;

}

while(1)

{

/* 读鼠标 */

memset(buf, 0, sizeof(buf));

ret = read(fd, buf, 50);

if(ret > 0) {printf("鼠标读出的内容是:[%s].\n", buf); }

}

}

return 0;

}

我们打开两个进程,一个进程读键盘,一个进程读鼠标,由于进程本身就是迸发同时向前运行的,所以这里再也不需要将键盘和鼠标设置为非阻塞,这种多进程的方法,进程之间切换也是很耗费资源的,并且当程之间需要相互共享资源的话,这就需要加入进程间通信机制,这就会使得我们的程序变得更加的复杂。

 

 

3、多路复用IO

  • IO多路复用原理

其基本思想是构造一个文件描述符的表,在这个表里面存放了阻塞的文件描述符,然后调用多路复用函数,该函数每隔一段时间会去检查一次,看表中是否有某个或几个文件描述符有动作,没有就休眠一段时间,再隔一段再去检查一次,如果其中的一个或多个文件名描述符有了动作,函数返回不在休眠,将分别对其有动作的文件描述符做相应操作。

 

实际上多路复用本身也存在轮询检查过程,但是绝大部分时间却是在休眠,所以就避免了一般的轮询机制,以及多进程实现所带来的相当的资源损耗。多路复用原理如下图所示:

九.linux中的高级IO

 

注意:一般情况下集合中都设置阻塞的文件描述符,设置非阻塞的描述符是没有意义的。

外部阻塞式,内部非阻塞式自动轮询多路阻塞式IO

 

·select机制;

  • select函数

1)、函数原型和所需头文件

 

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,

struct timeval *timeout);

int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);

 

2)、函数功能

这两个函数都是为了实现多路复用,但是这两个函数都能够被信号中断,pselect函数能够通过sigmask参数屏蔽掉那些我们不希望中断pselect系统函数的信号。

 

3)、函数参数

·int nfds:readfds, writefds, exceptfds这三个集合中最大描述符+1(因为描述符是从0算起的),用以说明需要关心描述符的范围,这个范围必须包含所有集合中的文件描述符。

 

·fd_set *readfds:读集合,设置读会阻塞的描述符。

·fd_set *writefds: 写集合,设置写会阻塞的描述符。

·fd_set *exceptfds: 设置异常描述符的集合。

·struct timeval *timeout:成员结构如下:

struct timeval

{

               long    tv_sec;  /* seconds(秒) */

               long    tv_usec; /* microseconds (微秒)*/

};

(1)该参数填NULL表示不设置超时,这种情况下如果没有描述符响应,同时也没有 信号中断的话则永远阻塞。

(2)如果需要设置超时,则需填写设置了时间结构体的地址,如果没有描述符响应,但设置的时间却到了,立即返回而不再阻塞。

 

4)、函数返回值

·返回-1:说明函数调用失败,errno被设置。

·返回0:超时时间到并且没有一个描述符有响应,返回0说明没有一个描述符准备好。

·返回值>0:返回有响应的文件描述符的数量。

 

 

  • select函数测试用例

int main(void)

{

// 读取鼠标

int fd = -1;

char buf[200];

int ret=-1;

fd_set myset;

struct timeval tim;

fd = open("/dev/input/mouse0", O_RDONLY | O_NONBLOCK);

if (fd < 0)

{

perror("open:");

return -1;

}

// 当前有2个fd,一个是fd:用于鼠标,一个是标准输入0 键盘

/* 设置读集合,设置操作必须放在循环内 */

FD_ZERO(&myset); //清空读集合

FD_SET(fd, &myset); //将fd设置到读集合中

FD_SET(0, &myset);//将标准输入0也设置到读集合中

/* 设置超时时间,设置必须放在循环内 */

tim.tv_sec= 3; //秒

tim.tv_usec= 0; //微秒

 

/* -如果集合中没有描述符响应

* 1.如果第四个参数被设为NULL,select将一直阻塞知道集

* 合中描述符有响应为止

* 2.如果第四个参数设置了超时时间,时间到则函数超时返回

* -如果集合中有一个或多个描述符响应,则集合被内核重新设

* 置,用于存放有响应的描述符,设置的超时时间也被清空*/

ret = select(fd+1, &myset, NULL, NULL, &tim);

if (ret < 0)//说明函数调用失败,errno被设置

{

perror("select: ");

return -1;

}

else if (ret == 0)//返回0说明没有一个描述符准备好。

{

printf("超时了\n");

}

else //返回有响应的文件描述符的数量

{

// 等到了一路IO,然后去监测到底是哪个IO到了,处理之

if (FD_ISSET(0, &myset))

{

// 这里处理键盘

memset(buf, 0, sizeof(buf));

read(0, buf, 5);

printf("键盘读出的内容是:[%s].\n", buf);

}

if (FD_ISSET(fd, &myset))

{

// 这里处理鼠标

memset(buf, 0, sizeof(buf));

read(fd, buf, 50);

printf("鼠标读出的内容是:[%s].\n", buf);

}

}

 

return 0;

}

 

九.linux中的高级IO

 

·Poll机制;

  • poll函数

实际上poll机制,与select差不多,只是具体调用的实现不一样,与select 不同,poll不是为每个条件构造一个描述符集,而是构造一个 pollfd结构数组,每个数组元素指定一个描述符编号以及对其所关心的条件。

1)、函数原型和所需头文件

 

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

int ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *timeout,

const sigset_t *sigmask);

 

2)、函数功能

·这两个函数都是为了实现多路复用,这两个函数都能够被信号中断,但是ppoll函数能够通过sigmask参数屏蔽掉那些我们不希望中断ppoll系统调用的信号。

 

3)、函数参数

·struct pollfd *fds:struct pollfd结构体数组,每个数组成员设置了我们需要多路IO操作的每个描述符,该结构体成员结构如下:

   struct pollfd {

               int   fd;         /* file descriptor:文件描述符 */

               short events;     /* requested events:设置我们希望发生的事件 */

               short revents;    /* returned events :实际发生的事件*/

           };

·nfds_t nfds:结构体数组struct pollfd *fds的成员数量。

·int timeout:超时时间,单位为毫秒,如填写为3000,表示3秒超时,如果不希望设置超时,该参数填写负数(如-1)即可。

 

poll的events和revents标志的设置如下表:

 

九.linux中的高级IO

 

4)、函数返回值

·返回-1:说明函数调失败,errno被设置。

·返回0:超时时间到并且没有文件描述符有响应。

·返回值>0:返回有响应的文件描述符的数量。

 

  • poll函数测试用例

int main(void)

{

// 读取鼠标

int fd = -1, ret = -1;

char buf[200];

struct pollfd myfds[2] = {0};

fd = open("/dev/input/mouse0", O_RDONLY| O_NONBLOCK);//打开鼠标

if (fd < 0)

{

perror("open:");

return -1;

}

/*向数组中设置设置需要多路监听的描述符,设置只需要设置一次就行 */

// 初始化我们的pollfd

myfds[0].fd = 0; // 键盘 标准输入IO

myfds[0].events = POLLIN; // 等待读操作

myfds[1].fd = fd; // 鼠标

myfds[1].events = POLLIN; // 等待读操作

 

/* myfds:struct pollfd结构提数组,fd+1:需要多路监听的数量,3000:超时时间 3秒*/

ret = poll(myfds, fd+1, 3000);

if (ret < 0)//说明函数调失败,errno被设置。

{

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;

}

当描述符有响应时,revents会被内核填写响应的类型,如events==revents,说明这个响应是我们希望的响应,利用该文件描述符实现相应操作,否则就不是我们希望的响应。

 

4、异步通知(异步IO)

  • 何为异步IO

所谓异步io就是,当某个事件准备好,进程会被发送一个SIGIO的异步信号,进程受到这个信号的通知后,会调用信号处理函数去处理事件,在事件没有准备好时,进程并不需要轮询事件或者阻塞等待事件,进程可以忙自己的事情直到等到别人发送异步信号SIGIO通知某事件发生。

 

异步IO有点类似于中断响应系统,而发送异步信号SIGIO通知某事件发生====响应中断并执行中断服务函数。

 

  • 异步IO设置的步骤如下:

(1) 调用signal或sigaction为该信号建立一个信号处理程序。

(2) 以命令F_SETOWN调用fcntl来设置接收信号进程PID和进程组GID。

(3) 以命令F_SETFL调用fcntl设置O_ASYNC状态标志,使在该描述符上可以进行异步I/O。第3步仅用于指向终端或网络的描述符

 

char buf[200];

int myfd = -1;

 

void func(int sig);

/*绑定SIGIO信号,在函数内部处理异步通知事件,类似于硬件中断服务函数*/

void func(int sig)

{

if(sig !=SIGIO)

{

return;

}

memset(buf, 0, sizeof(buf));

printf("before 鼠标 read.\n");

read(myfd, buf, 50);

printf("鼠标读出的内容是:[%s].\n", buf);

}

 

int main(void)

{

// 读取鼠标

int flag = -1;

myfd = open("/dev/input/mouse0", O_RDONLY);

if (myfd < 0)

{

perror("open:");

return -1;

}

//设置异步IO通知

//①把鼠标的文件描述符设置为可以接收异步IO

flag = fcntl(myfd, F_GETFL);

flag |= O_ASYNC;

fcntl(myfd, F_SETFL, flag);

//②设置当前进程获取SIGIO信号

fcntl(myfd, F_SETOWN, getpid());

//③设置当前进程的捕获SIGIO信号

signal(SIGIO, func);

/*有了以上的设置,则对鼠标的异步IO处理设置已经搞定 */

while(1)

{

// 读键盘

memset(buf, 0, sizeof(buf));

printf("before 键盘 read.\n");

read(0, buf, 5);

printf("键盘读出的内容是:[%s].\n", buf);

}

return 0;

}

 

九.linux中的高级IO

 

5、存储映射IO

  • 1、存储映射的好处

存储映射I/O使一个磁盘文件与虚拟存储空间中的一段缓存相映射。于是当从缓存中取数据,就相当于读文件中的相应字节。与其类似,将数据存入缓存,则相应字节就自动地写入文件。这样,就可以在不使用read和write的情况下执行I/O。

可以理解为,建立一个缓存区,这个缓存区就是真实需要访问的数据区的影子,不占用任何实际物理空间,当对这个“影子缓存区操作”,即是对真实文件的读写操作

九.linux中的高级IO

 

  • 2、存储映射IO的特点

(1)共享而不是复制,减少内存操作

(2)处理大文件时效率高,小文件不划算

 

 

  • 3、存储映射IO需要使用mmap函数,这里,我们暂不学习。

 

参考博文注明:https://www.cnblogs.com/tshua/p/5763744.html

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

相关文章: