Linux的五个I/O模型
前面的 fcntl()函数解决了文件的共享问题, 接下来该处理 I/O 复用的情况了。
总的来说, I/O 处理的模型有 5 种。
1.阻塞 I/O 模型:
在这种模型下, 若所调用的 I/O 函数没有完成相关的功能, 则会使进程挂起, 直到相关数据到达才会返回。 如常见对管道设备、 终端设备和网络设备进行读写时经常会出现这种情况。
2.非阻塞 I/O 模型:
在这种模型下, 当请求的 I/O 操作不能完成时, 则不让进程睡眠,而且立即返回。 非阻塞 I/O 使用户可以调用不会阻塞的 I/O 操作, 如 open()、 write()和 read()。 如果该操作不能完成, 则会立即返回出错(如打不开文件) 或者返回 0(如 在缓冲区中没有数据可以读取或者没空间可以写入数据)。 如何实现非阻塞IO访问: O_NONBLOCK: 只能在打开的时候设置 fcntl: 可以在文件打开后设置
3.I/O 多路复用模型:
在这种模型下, 如果请求的 I/O 操作阻塞, 且它不是真正阻塞I/O, 而是让其中的一个函数等待, 在此期间, I/O 还能进行其他操作。 外部表现为阻塞式,内部非阻塞式自动轮询多路阻塞式IO。其实内部就是while加非阻塞(类似),但是它内部不会占用CPU 太多时间。
优点:
不会浪费太多CPU时间,且达到while加非阻塞的效果
缺点:
1.select和poll本身是阻塞式的,当里面的条件没满足或者超时,就会一直阻塞,
2.这两个函数可以同时注册多个阻塞,但是只要有一个阻塞发生就会马上退出函数,而不会等其它阻塞发生,更不会等全部阻塞发生后才退出
4.信号驱动 I/O 模型:
在这种模型下, 进程要定义一个信号处理程序, 系统可以自动捕获特定信号的到来, 从而启动 I/O。 这是由内核通知用户何时可以启动一个 I/O操作决定的。它是非阻塞的。 当有就绪的数据时, 内核就向该进程发送 SIGIO 信号。 无论我们如何处理 SIGIO 信号, 这种模型的好处是当等待数据到达时, 可以不阻塞。 主程序继续执行,只有收到 SIGIO 信号时才去处理数据即可。
优点:快速,因为是内核进行处理
缺点:只能处理一种情况,因为只有一个信号
5.异步 I/O 模型:
在这种模型下, 进程先让内核启动 I/O 操作, 并在整个操作完成后通知该进程。 这种模型与信号驱动模型的主要区别在于: 信号驱动 I/O 是由内核通知我们何时可以启动一个 I/O 操作, 而异步 I/O 模型是由内核通知进程 I/O 操作何时完成的。 现在, 并不是所有的系统都支持这种模型。
6.特殊方式——轮询
不断对文件进行判断是否能使用,
优点:简单
缺点:浪费CPU时间
图参考:
https://www.cnblogs.com/chaser24/p/6112071.html
1.非阻塞IO
对于open,write和read有两种方法使其非阻塞
(1)如果调用open获得描述符,则可指定O_NONBLOCK标志。
fd = open("test", O_REWR | O_NONBLOCK);
(2)对于调已经打开的一个描述符,则可用fcntl,由该函数打开O_NONBLOCK文件状态标志(见3.14节),图3-12中的函数可用来为一个描述符打开任一文件状态标志。
fctnl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
2.I/O多路复用
1.select()函数的语法要点:
文件描述符集处理函数:
使用 select()函数之前,
使用 FD_ZERO()初始化文件描述符集
FD_SET()来设置文件描述符集
(在需要重复调用 select()函数时, 先把一次初始化好的文件描述符集备份下来,每次读取它即可)。
在 select()函数返回后,
可循环使用 FD_ISSET()来检查描述符集, 在执行完对相关文件描述符的操作后,
使用 FD_CLR()来清除描述符集。
FD_ISSET(int fd, fd_set *set) 这个函数很有意思,当注册完一个文件后,再调用这个函数检查这个文件描述符,得到的值式非零, 当调用了select()后,再退出后,如果这个文件描述符的阻塞没有发生,则再调用这个函数就会返回0,如果发生了就会返回非0,
可以用于检测阻塞有没有发生
另外, select()函数中的 timeout 是一个 struct timeval 类型的指针,该结构体如下所示:
struct timeval
{
long tv_sec; /* 秒 */
long tv_unsec; /* 微秒 */
}
2.poll()函数的语法要点:
timeout ==-1 永远等待
timeout ==0 不等待
timeout> 0 等待tiweout毫秒
3.select的变体pselect函数:
改进:
更精准的超时时间:
select的超时值用timeval结构指定, timespec结构以秒和纳秒表示超时值,而非秒和微秒。
pselect的超时值被声明为const,这保证了调用pselect不会改变此值。
pselect可使用可选信号屏蔽字。
siamask=NULL,那么在与信号有关的方面, pselect的运行状况和select相同。
否则, sigmask指向一信号屏蔽字,在调用, pselect时,以原子操作的方式安装该信号屏蔽字。在返回时,恢复以前的信号屏蔽字。
select()函数的实例。
#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/mouse0", 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); //添加文件描述
if (FD_ISSET(0, &myset))
{
printf("键盘读出的内容是:.\n");
}
if (FD_ISSET(fd, &myset))
{
printf("鼠标读出的内容是:.\n");
}
tm.tv_sec = 5;
tm.tv_usec = 0;
while (1) {
tm.tv_sec = 5;
tm.tv_usec = 0;
FD_SET(fd, &myset); //添加文件描述
FD_SET(0, &myset); //添加文件描述
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))
{
printf("鼠标是否变化:%d\n", FD_ISSET(fd, &myset));
// 这里处理键盘
memset(buf, 0, sizeof(buf));
read(0, buf, 5);
printf("键盘读出的内容是:[%s].\n", buf);
}
if (FD_ISSET(fd, &myset))
{
printf("键盘是否变化:%d\n", FD_ISSET(0, &myset));
// 这里处理鼠标
memset(buf, 0, sizeof(buf));
read(fd, buf, 50);
printf("鼠标读出的内容是:[%s].\n", buf);
}
}
}
return 0;
}
poll()函数的实例。
#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/mouse0", 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, 2, 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
int aio_read(struct aiocb *aiocbp); /* 提交一个异步读 */
int aio_write(struct aiocb *aiocbp); /* 提交一个异步写 */
int aio_cancel(int fildes, struct aiocb *aiocbp); /* 取消一个异步请求(或基于一个fd的所有异步请求,aiocbp==NULL) */
int aio_error(const struct aiocb *aiocbp); /* 获取一个异步请求的状态(进行中EINPROGRESS?还是已经结束或出错?) */
ssize_t aio_return(struct aiocb *aiocbp); /* 获取一个异步请求的返回值(跟同步读写定义的一样) */
int aio_suspend(const struct aiocb * const list[], int nent, const struct timespec *timeout); /* 阻塞等待,让所有IO请求完成 */
int aio_fsync(int op, struct aiocb *aiocbp); //不等待,把数据写入磁盘中
int lio_listio(int mode, struct aiocb *const aiocb_list[],//发起一系列异步I/O请求
int nitems, struct sigevent *sevp);
重要结构体:
strcut aiocb{
int aio_fildes; /* 要被读写的fd */
volatile void *aio_buf; /* 读写操作对应的内存buffer */
off_t aio_offset; /* 读写操作对应的文件偏移 */
size_t aio_nbytes; /* 需要读写的字节长度 */
int aio_reqprio; /* 请求的优先级 */
struct sigevent aio_sigevent; /* 异步事件,定义异步操作完成时的通知信号或回调函数 */
int aio_lio_opconde //不知道
}
I/O事件完成后,如何通知应用程序,有下面的结构体表示:
struct sigevent {
int sigev_notify; //通知类型
int sigev_signo; //信号的编号
union sigval sigev_value; //sigev_notify_function传递的参数
void (*sigev_notify_function)(union sigval); /* 异步IO请求完成后,执行的函数 */
pthread_attr_t *sigev_notify_attributes; /* notify attrs */
};
sigev_notify字段控制通知的类型。取值可能是以下3个中的一个。
SIGEV_NONE
异步IO请求完成后,不通知进程。
SIGEV_SIGNAL
异步IO请求完成后,产生由sigev_signo字段指定的信号。如果应用程序已选择捕捉信号,且在建立信号处理程序的时候指定了SA SIGINFO标志,那么该信号将被入队(如果实现支持排队信号),信号处理程序会传送给一个siqinfo结构,该结构的si_value字段被设置为sigev_value (如果使用了SA_SIGINFO标志),
SIGEV_THREAD
当异步IO请求完成时,由sigev_notify_function字段指定的函数被调用.siqev_value字段被传入作为它的唯一参数。除非sigev-notify attributes字段被设定为pthread属性结构的地址,且该结构指定了一个另外的线程属性,否则该函数将在分离状态下的一个单独的线程中执行。
(1)异步读aio_read
(2)异步写aio_write
(3)强制写磁盘aio_fsync
要想强制所有等待中的异步操作不等待而写入持久化的存储中,可以设立一个AIO控制块并,调用aio_fsync函数。
AIO控制块中的aio_fildes字段指定了其异步写操作被同步的文件。
如果op参数设定为O_DSYNC,那么操作执行起来就会像调用了fdatasync一样。否则,
如果op参数设定为O_SYNC,那么操作执行起来就会像调用了fsync一样。
像aio_read和aio_write函数一样,在安排了同步时,aio_fsync操作返回。在异步同步操作完成之前,数据不会被持久化。AIO控制块控制我们如何被通知,就像aio read和 aio_write函数一样。
(4)获取异步请求的状态aio_error
返回值为下面4种情况中的一种。
0:异步操作成功完成。需要调用aio return函数获取操作返回值。
-1:对aio_error的调用失败。这种情况下, errno会告诉我们为什么。
EINPROGRESS :异步读、写或同步操作仍在等待。
其他情况: 其他任何返回值是相关的异步操作失败返回的错误码。
(5)获取一个异步请求的返回值aio_return
如果异步操作完成,可以用这个或返回值
不应该在异步操作完成之前,调用这个函数
(6)阻塞等待,让所有IO请求完成aio_suspend
如果一个事件(或一个进程)完成了,然而阻塞的异步IO还没完成,就可以调用这个函数,等待所有异步IO完成
(7) 取消一个异步请求aio_cancel
(8)发起一系列异步I/O请求lio_listio
实例代码:
注意:由于是动态链接,因此编译时要加 -ltr
1.aio_read
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <aio.h>
#define BUFFER_SIZE 1024
int MAX_LIST = 2;
int main(int argc,char **argv)
{
//aio操作所需结构体
struct aiocb rd;
int fd,ret,couter;
//将rd结构体清空
bzero(&rd,sizeof(rd));
//为rd.aio_buf分配空间
rd.aio_buf = malloc(BUFFER_SIZE + 1);
//填充rd结构体
rd.aio_fildes = 0;
rd.aio_nbytes = BUFFER_SIZE;
rd.aio_offset = 0;
//进行异步读操作
ret = aio_read(&rd);
if(ret < 0)
{
perror("aio_read");
exit(1);
}
couter = 0;
// 循环等待异步读操作结束
while(aio_error(&rd) == EINPROGRESS)
{
printf("第%d次\n",++couter);
sleep(1);
}
//获取异步读返回值
ret = aio_return(&rd);
printf("内容为 [%s]\n", (char *)rd.aio_buf);
printf("返回值为:%d\n",ret);
return 0;
}
# gcc 1.c -lrt
# ./a.out
第1次
第2次
第3次
dfasdfasd
内容为 [dfasdfasd
]返回值为:10
2.aio_suspend
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <aio.h>
#define BUFFER_SIZE 1024
int MAX_LIST = 2;
int main(int argc,char **argv)
{
//aio操作所需结构体
struct aiocb rd;
int fd,ret,couter;
struct aiocb *aiocb_list[2] = {0};
//将rd结构体清空
bzero(&rd,sizeof(rd));
//为rd.aio_buf分配空间
rd.aio_buf = malloc(BUFFER_SIZE + 1);
//填充rd结构体
rd.aio_fildes = 0;
rd.aio_nbytes = BUFFER_SIZE;
rd.aio_offset = 0;
//将读fd的事件注册
aiocb_list[0] = &rd;
//进行异步读操作
ret = aio_read(&rd);
if(ret < 0)
{
perror("aio_read");
exit(1);
}
if ( (ret = aio_return(&rd)) > 0)
{
printf("内容为 [%s]\n", (char *)rd.aio_buf);
printf("\n\n返回值为:%d\n",ret);
exit(0);
}
printf("我等等你 \n");
aio_suspend(aiocb_list,MAX_LIST,NULL);
if ( (ret = aio_return(&rd)) > 0)
{
printf("内容为 [%s]\n", (char *)rd.aio_buf);
printf("返回值为:%d\n",ret);
exit(0);
}
exit(0);
}
# ./a.out
我等等你
dfasfd
内容为 [dfasfd
]
返回值为:7
3.lio_listio函数
aio同时还为我们提供了一个可以发起多个或多种I/O请求的接口lio_listio
这个函数效率很高,因为我们只需一次系统调用(一次内核上下位切换)就可以完成大量的I/O操作
其函数原型如下 int lio_listio(int mode,struct aiocb *list[],int nent,struct sigevent *sig);
第一个参数mode可以有俩个实参,LIO_WAIT和LIO_NOWAIT,前一个会阻塞该调用直到所有I/O都完成为止,后一个则会挂入队列就返回
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <aio.h>
int MAX_LIST = 2;
int main(int argc,char **argv)
{
struct aiocb *listio[2];
struct aiocb rd,wr;
int fd,ret;
char buf[20] = {"Hello world"};
//异步读事件
bzero(&rd,sizeof(rd));
rd.aio_buf = (char *)malloc(BUFFER_SIZE);
if(rd.aio_buf == NULL)
{
perror("aio_buf");
}
rd.aio_fildes = 0;
rd.aio_nbytes = 1024;
rd.aio_offset = 0;
rd.aio_lio_opcode = LIO_READ; ///lio操作类型为异步读
//将异步读事件添加到list中
listio[0] = &rd;
//异步写事件
fd = open("test.txt",O_WRONLY | O_APPEND);
if(fd < 0)
{
perror("test.txt");
exit(-1);
}
bzero(&wr,sizeof(wr));
wr.aio_buf = (char *)malloc(BUFFER_SIZE);
if(wr.aio_buf == NULL)
{
perror("aio_buf");
}
wr.aio_fildes = 1;
wr.aio_nbytes = strlen(buf);
wr.aio_buf = buf;
wr.aio_lio_opcode = LIO_WRITE; ///lio操作类型为异步写
//将异步写事件添加到list中
listio[1] = ≀
//使用lio_listio发起一系列请求
ret = lio_listio(LIO_WAIT, listio, MAX_LIST, NULL);
//当异步读写都完成时获取他们的返回值
ret = aio_return(&rd);
printf("\n读返回值:%d\n",ret);
ret = aio_return(&wr);
printf("\n写返回值:%d\n",ret);
return 0;
}
# ./a.out
Hello world123456读返回值:7
写返回值:11
4.I/O完成时进行异步通知
当我们的异步I/O操作完成之时,我们可以通过信号通知我们的进程也可用回调函数来进行异步通知,接下来我会为大家主要介绍以下回调函数来进行异步通知,关于信号通知有兴趣的同学自己去学习吧
使用回调进行异步通知
该种通知方式使用一个系统回调函数来通知应用程序,要想完成此功能,我们必须在aiocb中设置我们想要进行异步回调的aiocb指针,以用来回调之后表示其自身 实例如下
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <aio.h>
#define BUFFER_SIZE 1024
struct aiocb rd;
int MAX_LIST = 2;
void aio_completion_handler(sigval_t sigval)
{
//用来获取读aiocb结构的指针
struct aiocb *prd;
int ret;
prd = (struct aiocb *)sigval.sival_ptr;
printf("hello ,my is handle fuction\n");
//判断请求是否成功
if(aio_error(prd) == 0)
{
//获取返回值
ret = aio_return(prd);
printf("读返回值为:%d\n",ret);
printf("内容为 [%s]\n", (char *)rd.aio_buf);
}
}
int main(int argc,char **argv)
{
int fd,ret;
//填充aiocb的基本内容
bzero(&rd,sizeof(rd));
rd.aio_fildes = 0;
rd.aio_buf = (char *)malloc(sizeof(BUFFER_SIZE + 1));
rd.aio_nbytes = BUFFER_SIZE;
rd.aio_offset = 0;
//填充aiocb中有关回调通知的结构体sigevent
rd.aio_sigevent.sigev_notify = SIGEV_THREAD;//使用线程回调通知
rd.aio_sigevent.sigev_notify_function = aio_completion_handler;//设置回调函数
rd.aio_sigevent.sigev_notify_attributes = NULL;//使用默认属性
rd.aio_sigevent.sigev_value.sival_ptr = &rd;//在aiocb控制块中加入自己的引用
//异步读取文件
ret = aio_read(&rd);
if(ret < 0)
{
perror("aio_read");
}
printf("异步读以开始\n");
sleep(5);
printf("异步读结束\n");
return 0;
}
# ./a.out
异步读以开始
12345
hello ,my is handle fuction
读返回值为:6
内容为 [12345
]
异步读结束
线程会掉是通过使用aiocb结构体中的aio_sigevent结构体来控制的, 其定义如下
struct sigevent
{
sigval_t sigev_value;
int sigev_signo;
int sigev_notify;
union {
int _pad[SIGEV_PAD_SIZE];
int _tid;
struct {
void (*_function)(sigval_t);
void *_attribute; /* really pthread_attr_t */
} _sigev_thread;
} _sigev_un;
}
#define sigev_notify_function _sigev_un._sigev_thread._function
#define sigev_notify_attributes _sigev_un._sigev_thread._attribute
#define sigev_notify_thread_id _sigev_un._tid1
4.信号驱动 I/O 模型
信号驱动IO模式是利用文件描述符的IO状态的变化,产生SIGIO信号,通过对SIGIO信号的处理,读写相应的数据。
原理:
把相应文件设置为信号驱动模型,然后每当文件发生变化时都会发送SIGIO信号
实现步骤:
1.利用signal或signaction函数定义信号SIGIO的处理函数
2.利用fcntl函数对文件描述符在状态发生变化产生SIGIO信号时,设置信号发送的对象
3.在运用open函数打开文件时或运用fcntl函数将已打开的文件设置为O_ASTNC方式
实例代码:
#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/mouse0", 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);
for(;;)
{
printf("buf \n");
pause(); //等待一个信号
}
return 0;
}
更多推荐
所有评论(0)