此文编写参考朱有鹏老师的视频教程和《Linux/UNIX系统编程手册》(作者:(德)Michael Kerrisk)
一、阻塞式IO的困境
(1)常见的阻塞:wait、pause、sleep等函数;read或write某些文件时
(2)阻塞式的好处
阻塞式的方法是有好处的,如父进程中替子进程收尸的时候,他不知道什么时候子进程才死亡,所以它就在那阻塞住了,同时将CPU交给其他进程使用,提高CPU的工作效率,但是有时候它也有它的缺点,如下例程所示:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
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);
// 读键盘
memset(buf, 0, sizeof(buf));
printf("before 键盘 read.\n");
read(0, buf, 5);
printf("键盘读出的内容是:[%s].\n", buf);
return 0;
}
分析:如果没有动鼠标,键盘输入是无效的,只有动过鼠标后,对键盘的动作才是有效的,显然这是不合理的,所以这是足赛事IO面临的问题,下面提出解决方案。
二、并发式IO的解决方案
1、非阻塞式IO
2、多路复用IO
3、异步通知(异步IO)
1、非阻塞式IO
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
// 读取鼠标
int fd = -1;
int flag = -1;
char buf[200];
int ret = -1;
fd = open("/dev/input/mouse1", O_RDONLY | O_NONBLOCK);
if (fd < 0)
{
perror("open:");
return -1;
}
// 把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(buf, 0, sizeof(buf));
//printf("before 键盘 read.\n");
ret = read(0, buf, 5);
if (ret > 0)
{
printf("键盘读出的内容是:[%s].\n", buf);
}
}
return 0;
}
fcntl()的用途之一是针对一个打开的文件,获取或修改其访问模式和状态标志(这些值是通过指定open()调用的flag参数来设置的)。要获取这些设置,应将fcntl()的cmd参数设置为F_GETFL。然后可以使用fcntl()的F_SETFL命令来修改打开文件的某些状态标志。允许更改的标志有O_APPEND、O_NONBLOCK、O_NOATIME、O_ASYNC和O_DIRECT。
分析:上述代码可以实现用户既动鼠标用动键盘的要求,但是这里采用的是CPU的轮询方式,显然这是非常耗费资源的,所以这不是最佳的解决方案。
2、多路复用IO
使用select和poll系统调用来实际解决前面的同时读取鼠标和键盘的任务。I/O 多路复用允许我们同时检查多个文件描述符,看其中任意一个是否可执行 I/O 操作。这两个系统调用都允许进程要么一直等待文件描述符成为就绪态,要么在调用中指定一个超时时间。select和poll的工作原理:
外部阻塞式,内部非阻塞式自动轮询多路阻塞式IO
(1)select
函数原型:
#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);
参数解析:
参数readfds、writefds以及exceptfds都是指向文件描述符集合的指针,所指向的数据类型是fd_set。这些参数按照如下方式使用。
readfds是用来检测输入是否就绪的文件描述符集合。
writefds是用来检测输出是否就绪的文件描述符集合。
exceptfds是用来检测异常情况是否发生的文件描述符集合。
术语“异常情况”常常被误解为在文件描述符上出现了一些错误,这并不正确。
参数timeout控制着select()的阻塞行为。该参数可指定为NULL,此时select()会一直阻塞。如果结构体timeval的两个域都为0的话,此时 select()不会阻塞,它只是简单地轮询指定的文件描述符集合,看看其中是否有就绪的文件描述符并立刻返回。否则,timeout将为select()指定一个等待时间的上限值。
操作
所有关于文件描述符集合的操作都是通过四个宏来完成的:FD_ZERO(),FD_SET(),FD_CLR()以及FD_ISSET()。
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
FD_ZERO()将fdset所指向的集合初始化为空。
FD_SET()将文件描述符fd添加到由fdset所指向的集合中。
FD_CLR()将文件描述符fd从fdset所指向的集合中移除。
如果文件描述符fd是fdset所指向的集合中的成员,FD_ISSET()返回true。
文件描述符集合有一个最大容量限制,由常量FD_SETSIZE来决定。在Linux上,该常量的值为1024。
参数 readfds、writefds 和 exceptfds 所指向的结构体都是保存结果值的地方。在调用select()之前,这些参数指向的结构体必须初始化(通过FD_ZERO()和FD_SET()),以包含我们感兴趣的文件描述符集合。之后select()调用会修改这些结构体,当select()返回时,它们包含的就是已处于就绪态的文件描述符集合了。(由于这些结构体会在调用中被修改,如果要在循环中重复调用 select(),我们必须保证每次都要重新初始化它们。)之后这些结构体可以通过FD_ISSET()来检查。如果我们对某一类型的事件不感兴趣,那么相应的fd_set参数可以指定为NULL。参数nfds必须设为比3个文件描述符集合中所包含的最大文件描述符号还要大1。该参数让select()变得更有效率,因为此时内核就不用去检查大于这个值的文件描述符号是否属于这些文件描述符集合。
返回值:
作为函数的返回值,select()会返回如下几种情况中的一种。
(1)返回−1表示有错误发生。可能的错误码包括 EBADF 和 EINTREINTR。EBADF 表示readfds、writefds或者exceptfds中有一个文件描述符是非法的(例如当前并没有打开)。EINTR表示该调用被信号处理例程中断了。
(2)返回0表示在任何文件描述符成为就绪态之前select()调用已经超时。在这种情况下,每个返回的文件描述符集合将被清空。
(3)返回一个正整数表示有1个或多个文件描述符已达到就绪态。返回值表示处于就绪态的文件描述符个数。在这种情况下,每个返回的文件描述符集合都需要检查(通过 FD_ISSET()),以此找出发生的 I/O 事件是什么。如果同一个文件描述符在readfds、writefds和exceptfds中同时被指定,且它对于多个I/O事件都处于就绪态的话,那么就会被统计多次。换句话说,select()返回所有在 3 个集合中被标记为就绪态的文件描述符总数。
例程
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
int main(void)
{
// 读取鼠标
int fd = -1, ret = -1;
char buf[200];
fd_set myset;
struct timeval tm;
fd = open("/dev/input/mouse1", O_RDONLY);
if (fd < 0)
{
perror("open:");
return -1;
}
// 当前有2个fd,一共是fd一个是0
// 处理myset
FD_ZERO(&myset);
FD_SET(fd, &myset);
FD_SET(0, &myset);
tm.tv_sec = 10;
tm.tv_usec = 0;
ret = select(fd+1, &myset, NULL, NULL, &tm);
if (ret < 0)
{
perror("select: ");
return -1;
}
else if (ret == 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;
}
(2)poll
系统调用poll()执行的任务同select()很相似。两者间主要的区别在于我们要如何指定待检查的文件描述符。在select()中,我们提供三个集合,在每个集合中标明我们感兴趣的文件描述符。而在poll()中我们提供一列文件描述符,并在每个文件描述符上标明我们感兴趣的事件。
函数原型:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数fds列出了我们需要poll()来检查的文件描述符。该参数为pollfd结构体数组,其定义如下。
参数nfds指定了数组fds中元素的个数。数据类型nfds_t实际为无符号整形。
pollfd结构体中的events和revents字段都是位掩码。调用者初始化events来指定需要为描述符fd做检查的事件。当poll()返回时,revents被设定以此来表示该文件描述符上实际发生的事件。如果我们对某个特定的文件描述符上的事件不感兴趣,可以将events设为0。另外,给fd字段指定一个负值(例如,如果值为非零,取它的相反数)将导致对应的events字段被忽略,且revents字段将总是返回0。这两种方法都可以用来(也许只是暂时的)关闭对单个文件描述符的检查,而不需要重新建立整个fds列表。
返回值:
作为函数的返回值,poll()会返回如下几种情况中的一种。
(1)返回−1表示有错误发生。一种可能的错误是EINTR,表示该调用被被一个信号处理例程中断。
(2)返回0表示该调用在任意一个文件描述符成为就绪态之前就超时了。
(3)返回正整数表示有1个或多个文件描述符处于就绪态了。返回值表示数组fds中拥有非零revents字段的pollfd结构体数量。
(4)注意select()同poll()返回正整数值时的细小差别。如果一个文件描述符在返回的描述符集合中出现了不止一次,系统调用select()会将同一个文件描述符计数多次。而系统调用poll()返回的是就绪态的文件描述符个数,且一个文件描述符只会统计一次,就算在相应的revents字段中设定了多个位掩码也是如此。
例程
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <poll.h>
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;
}
3、异步IO
1、何为异步IO
(1)几乎可以认为:异步IO就是操作系统用软件实现的一套中断响应系统。
(2)异步IO的工作方法是:我们当前进程注册一个异步IO事件(使用signal注册一个信号SIGIO的处理函数),然后当前进程可以正常处理自己的事情,当异步事件发生后当前进程会收到一个SIGIO信号从而执行绑定的处理函数去处理这个异步事件。
2、涉及的函数:
(1)fcntl(F_GETFL、F_SETFL、O_ASYNC、F_SETOWN)
(2)signal或者sigaction(SIGIO)
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;
}
三、存储映射IO
1、mmap函数
2、LCD显示和IPC之共享内存
3、存储映射IO的特点
(1)共享而不是复制,减少内存操作
(2)处理大文件时效率高,小文件不划算