浅谈I/O模型
liebian365 2024-11-26 06:01 4 浏览 0 评论
作为程序员,在日常工作中,都或多或少的接触过网络I/O这个概念,接触过网络编程,听说过socket等等,但是对于更深层次的理解,多少还是有点欠缺,通过本文,可以了解网络中最重要的模块I/O,以及对几种网络模型的介绍,在我们日常工作开发过程中,可以针对特定需求,选择特定的网络模型,达到事半功倍的效果。
0 什么是I/O
通常指数据,在内部存储器和外部存储器或其他周边设备之间的输入和输出。
是信息处理系统(例如计算器)与外部世界(可能是人类或另一信息处理系统)之间的通信。输入是系统接收的信号或数据,输出则是从其发送的信号或数据。该术语也可以用作行动的一部分;到“运行I/O”是运行输入或输出的操作。
Unix 系统下,不论是标准输入还是借助套接字接受网络输入,都有两个步骤:
- 等待数据准备好(Waiting for the data to be ready)
- 从内核向进程复制数据(Copying the data from the kernel to the process)
?
输入/出设备是硬件中由人(或其他系统)使用与计算器进行通信的部件。例如,键盘或鼠标是计算器的输入设备,而监视器和打印机是输出设备。计算器之间的通信设备(如电信调制解调器和网卡)通常运行输入和输出操作。 简单来说,就是用户进程与内核交互,而内核与硬件进行交互。
?
1 阻塞式I/O模型
应用程序发起I/O系统调用,在获得结果之前,应用程序进程会一直阻塞,直到获得结果(有数据返回或者操作超时)。
默认情况下,Unix系统上的所有文件描述符都以“阻塞模式”开始。这意味着read、write或connect之类的I/O系统调用在默认情况下,都是阻塞的。
为了理解这一点,我们假如有个程序,在终端上等待标准输入(stdin),此时,假如通过调用read函数来实现该功能,那么该程序将被阻塞,直到有实际的数据可用(例如当用户在键盘上敲入字符时)。具体来说,内核将把进程置于“休眠”状态,直到数据在stdin上可用。其他类型的文件描述符也是如此。例如,如果您尝试从TCP套接字读取数据,那么read调用将阻塞,直到连接的另一端实际发送数据为止。
int main(int argc, char *argv[]) {
char buf[ MAX_BUFFER_LENGTH ];
int length = 0;
if( (length = read( 0, buf, MAX_BUFFER_LENGTH )) < 0 ) {
return -1;
}
buf[length] = '\0';
printf("input: \n%s\n", buf);
return 0;
}
?
当在我们执行了上述代码,那么,在该执行代码的进程内,会调用read函数,最终会进入kernel态,此时,会进入kernel态的第一个步骤即I/O等待数据状态。
从用户进程的角度来说,会被阻塞。直到超时或者键盘输入了数据,从kernel态将数据拷贝到了用户态的内存,此时用户进程才接触阻塞,程序开始执行下面其他步骤。
特点:
用户进程会一直阻塞等待kernel,直到kernel将数据返回
2 非阻塞式I/O模型
通常通过将socket描述符设置为O_NONBLOCK模式。
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
如果一个socket描述符被设置为非阻塞的,那么在数据准备好之前,调用read函数,会返回-1,而errno会被设置为EWOULDBLOCK。
?
从上图可以看出,当在用户进程调用read系统调用之后,如果kernel态没有数据,那么read调用会马上返回,而不会阻塞用户进程。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是用户就可以在本次到下次再发起read询问的时间间隔内做其他事情,或者直接再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存(这一阶段仍然是阻塞的),然后返回。
整个过程,可以概括为,用户进程不断的调用read系统调用,询问kernel数据是否准备好,所以,非阻塞式I/O模式可以理解为是一个不断循环询问kernel的模式。
struct timespec sleep_interval{.tv_sec = 0, .tv_nsec = 1000};
ssize_t nbytes;
for (;;) {
/* try fd1 */
if ((nbytes = read(fd1, buf, sizeof(buf))) < 0) {
if (errno != EWOULDBLOCK) {
perror("read/fd1");
}
} else {
handle_data(buf, nbytes);
}
/* try fd2 */
if ((nbytes = read(fd2, buf, sizeof(buf))) < 0) {
if (errno != EWOULDBLOCK) {
perror("read/fd2");
}
} else {
handle_data(buf, nbytes);
}
/* 处理其他事情 */
// do other
}
非阻塞式I/O较阻塞式I/O来说,性能提升了很多,但仍然存在很多问题,比如:
1、当数据输入非常慢时,程序会频繁而不必要地唤醒,从而浪费CPU资源。
2、当数据进入时,如果程序处于睡眠状态或者正在处理其他逻辑,它可能不会立即读取数据,因此程序的延迟将很差。
3、用这种模式处理大量的文件描述符将变得很麻烦。
特点:
1、用户进程会不断的询问kernel数据是否已经准备好
2、抽象的讲,非阻塞I/O与异步I/O类似,区别是一个不断的去轮询kernel,一个是通过被动通知的方式。2、抽象的讲,非阻塞I/O与异步I/O类似,区别是一个不断的去轮询kernel,一个是通过被动通知的方式。
3 信号驱动式I/O模型
当进程发起一个IO操作,会向内核注册一个信号处理函数,然后进程返回不阻塞;当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用IO读取数据。
?
信号驱动式I/O在TCP中用处不大,这是因为该信号在TCP套接字中产生的过于频繁。
以下条件均会导致对一个TCP套接字产生SIGIO信号:
- 监听套接字上某个连接请求已经完成;
- 某个断连请求已经发起;
- 某个断连请求已经完成;
- 某个连接对端已经关闭;
- 数据到达套接字;
- 数据已经从套接字发送走;
- 发生某个异步错误。
当然,我们可以对TCP监听套接字可以使用SIGIO,这样我们就可以在信号处理函数中处理新连接了。
对于UDP,只有以下两个条件才会产生SIGIO信号:
- 数据报到达套接字;
- 套接字上发生异步错误。
所以,针对UDP套接字产生的SIGIO信号,我们只要调用recvfrom读入到达的数据,或者获取发生的异步错误就可以了。
void io_handler(int signal) {
int numbytes; /* Number of bytes recieved from client */
int addr_len; /* Address size of the sender */
struct sockaddr_in their_addr; /* connector's address information */
if ((numbytes=recvfrom(sock, buf, MAXBUFLEN, 0, \
(struct sockaddr *)&their_addr, &addr_len)) == -1) {
perror("recvfrom");
exit(1);
}
buf[numbytes]='\0';
printf("got from %s --->%s \n ",inet_ntoa(their_addr.sin_addr),buf);
return;
}
int main() {
int length;
struct sockaddr_in server;
sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
perror("opening datagram socket");
exit(1);
}
server.sin_family = AF_INET;
server.sin_addr.s_addr = INADDR_ANY;
server.sin_port = htons(MYPORT);
if (bind(sock, (struct sockaddr *)&server, sizeof server) <0 ){
perror("binding datagram socket");
exit(1);
}
length = sizeof(server);
if (getsockname(sock, (struct sockaddr *)&server, &length) < 0){
perror("getting socket name");
exit(1);
}
printf("Socket port #%d\n", ntohs(server.sin_port));
// 第一步,注册事件函数
signal(SIGIO,io_handler);
// 第二步 设置要接收的进程id或进程组id,通知其自己的进程id或进程的挂起输入组id
if (fcntl(sock,F_SETOWN, getpid()) < 0){
perror("fcntl F_SETOWN");
exit(1);
}
// 第三步,允许接收异步I/O信号
if (fcntl(sock,F_SETFL,FASYNC) <0 ){
perror("fcntl F_SETFL, FASYNC");
exit(1);
}
for(;;)
;
// .......
}
> 该模式比较复杂,在实际使用中不是很多,在内核2.6中才开始引入。
## 4 异步I/O模型
同步I/O意味着当您想读或写某个东西时,可能需要调用一个名为read()或write()的函数,函数会阻塞,阻止执行进一步移动,直到读或写完成。这就是普通文件读写的典型工作方式。打开一个文件,然后调用read(),它用所需的数据填充一个缓冲区,并在完成所有操作后返回,这样就可以用所需的数据填充一个缓冲区。
异步I/O恰恰相反。与读写函数等待请求的操作完成后再返回不同,异步I/O操作将立即返回到程序,而读写操作将在后台继续。
这有什么好处?这意味着你的程序或游戏可以继续扔东西在屏幕上,更新输入,滚动进度条,无论什么,而所有的硬盘驱动器的数据处理你想要的。您还可以向系统发送多个IO请求,这样操作系统就可以找到访问所有所需数据的最有效方法。
?
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
在异步IO中,以下几个概念非常重要:
struct aiocb {
int aio_fildes; /* 文件描述符 */
off_t aio_offset; /* 文件便宜 */
volatile void *aio_buf; /* buffer位置 */
size_t aio_nbytes; /* 传输数据大小 */
int aio_reqprio; /* 请求优先级 */
struct sigevent aio_sigevent; /* 通知的方式 */
int aio_lio_opcode;
};
aio_read()
函数告诉系统要读取的文件、开始读取的偏移量、要读取的字节数以及要将要读取的字节放在何处。
aio_error()
检查IO请求的当前状态。使用这个函数你可以查出请求是否成功。你所要做的就是给它一个地址,地址和你给aio\u read()的地址相同。如果请求成功完成,则函数返回0;如果请求仍在工作,则返回EINPROGRESS;如果发生错误,则返回其他错误代码。
aio_return()
检查IO请求的结果,一旦您发现请求已经完成。如果请求成功,此函数返回读取的字节数。如果失败,那么函数返回-1。
下面是异步I/O模型的一个简单例子,通过本例,可以简单的了解该模型的大致流程。
int main(){
int file = open("blah.txt", O_RDONLY, 0);
if (file == -1)
{
cout << "Unable to open file!" << endl;
return 1;
}
char* buffer = new char[SIZE_TO_READ];
// 定义控制块变量
aiocb cb;
memset(&cb, 0, sizeof(aiocb));
cb.aio_nbytes = SIZE_TO_READ;
cb.aio_fildes = file;
cb.aio_offset = 0;
cb.aio_buf = buffer;
// 读取数据
if (aio_read(&cb) == -1)
{
cout << "Unable to create request!" << endl;
close(file);
}
cout << "Request enqueued!" << endl;
// 等待,知道请求处理完成
while(aio_error(&cb) == EINPROGRESS)
{
cout << "Working..." << endl;
}
// 判断读取的字节数
int numBytes = aio_return(&cb);
if (numBytes != -1)
cout << "Success!" << endl;
else
cout << "Error!" << endl;
// 释放资源
delete[] buffer;
close(file);
return 0;
}
特点:
1、用户程序告诉kernel其要执行某个操作,不等kernel回复就立即返回
2、kernel完成整个操作,包括将获取的数据拷贝到用户的buffer之后,再通知用户。
5 I/O多路复用
I/O多路复用是这样一种能力,它告诉内核,如果一个或多个I/O条件已经就绪,比如输入已经准备好被读取,或者描述符能够获取更多的输出,我们就需要得到通知。
I/O复用模型使用select、poll、epoll函数,这些函数也会阻塞进程,但与阻塞I/O不同的是,这两个函数可以同时阻塞多个I/O操作。对于多个读操作、多个写操作,可以同时检测I/O函数,直到有数据可读或可写时,才实际调用I/O操作函数。
?
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
int maxfdp1, stdineof;
fd_set rset;
char buf[MAXLINE];
int n;
stdineof = 0;
FD_ZERO(&rset);
for ( ; ; ) {
if (stdineof == 0)
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
select(maxfdp1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset)) { /* socket is readable */
if ( (n = read(sockfd, buf, MAXLINE)) == 0) {
if (stdineof == 1)
return; /* normal termination */
else
err_quit("str_cli: server terminated prematurely");
}
write(fileno(stdout), buf, n);
}
if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */
if ( (n = read(fileno(fp), buf, MAXLINE)) == 0) {
stdineof = 1;
shutdown(sockfd, SHUT_WR); /* send FIN */
FD_CLR(fileno(fp), &rset);
continue;
}
writen(sockfd, buf, n);
}
}
IO多路复用适用如下场合:
(1)当客户处理多个描述字时(一般是交互式输入和网络套接口)
(2)当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
(3)如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
(4)如果一个服务器即要处理TCP,又要处理UDP
(5)如果一个服务器要处理多个服务或多个协议
1、多路复用模式有select、poll以及epoll函数,每个函数的性能特点以及开发难以程度各不同,需要根据实际需求,择优选择。
2、现在基本上所有的商用或者大型程序,都是用的多路复用与非阻塞两个模式相结合的方式
参考资料
https://fwheel.net/aio.html
https://www.itzhai.com/articles/it-seems-not-so-perfect-signal-driven-io.html
https://eklitzke.org/blocking-io-nonblocking-io-and-epoll
https://notes.shichao.io/unp/ch6/
相关推荐
- 快递查询教程,批量查询物流,一键管理快递
-
作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...
- 一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递
-
对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...
- 快递查询单号查询,怎么查物流到哪了
-
输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...
- 3分钟查询物流,教你一键批量查询全部物流信息
-
很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...
- 快递单号查询,一次性查询全部物流信息
-
现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...
- 快递查询工具,批量查询多个快递快递单号的物流状态、签收时间
-
最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...
- 快递查询软件,自动识别查询快递单号查询方法
-
当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...
- 教你怎样查询快递查询单号并保存物流信息
-
商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...
- 简单几步骤查询所有快递物流信息
-
在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...
- 物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号
-
最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...
- 连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息
-
快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...
- 快递查询教程,快递单号查询,筛选更新量为1的单号
-
最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...
- 掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析
-
在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...
- 从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息
-
在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...
- 物流单号查询,在哪里查询快递
-
如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)