网络I/O – 阻塞、非阻塞、同步和异步及源码示例

一. 前言

    在网络编程时,阻塞I/O,非阻塞I/O,同步I/O和异步I/O经常被提及。这篇博文中,我将结合相关材料及源码,尝试对这几种I/O模型进行区别和理解。

    Richard Stevens大神的《UNIX Network Programming Volume 1: The Sockets Networking API, Third Edition》一书在第六章I/O Multiplexing: The select and poll Functions中有对这几种I/O模型的详细描述,人民邮电出版社有中文译本,大家可以参考此书。本篇博客的I/O模型图亦截取于此书。

    为便于描述,后续内容将以网络I/O的读操作(read)为例进行说明。

二. 数据包的接收流程

    首先,先简单描述正常情况下(抛开诸如DMA等其它不经内核的包接收方式),数据包从进入网卡开始到用户进程收到数据的过程。

    为简便,这个过程大致可划分为内核态和用户态过程。

    在内核态,数据帧达到网卡,网卡产生中断给内核,内核调用驱动程序读取网卡缓冲区中的数据,拷贝进入内核缓冲区中,并经协议栈进行层级处理。

    在用户态,用户进程通过系统调用,读取Socket对应内核缓冲区中的数据,将数据拷贝至用户空间。

    总结来说,这样一个读操作包含两个阶段:

    (1)内核等待数据就绪

    (2)将内核读到的数据拷贝至用户空间

三. 相同与不同

    以上说了读操作包含的两个阶段。这两个阶段用户进程的状态和机制,就是区分阻塞I/O和非阻塞I/O,以及同步I/O和异步I/O的关键所在

    先说一下阻塞I/O和非阻塞I/O阻塞和非阻塞侧重点在于用户进程在等待调用结果(即得到反馈)时的状态,尤其是当内核数据未就绪的时候。

      相同点:若数据已经就绪,则阻塞和非阻塞没有区别,读取数据后返回

      不同点:若数据未就绪,阻塞I/O一直等待数据就绪,读取数据后返回;非阻塞I/O则立即返回

    可见,区分阻塞和非阻塞,要在数据未就绪的时候,看二者等待后的状态是否立即返回。

    再说一下同步I/O和异步I/O同步和异步侧重点在于用户进程和待接收数据的消息通信机制,而不论该数据是否已经就绪

      不同点:同步I/O在读取数据时,直到读完数据后才会返回;而异步I/O在发出读数据操作时,直接返回进行其它操作,不论数据是否就绪,且数据的读取不由该读操作负责

      相同点:无

    本文后续部分将按照Richard Stevens书上的说明,来简单描述一下5种I/O模型:阻塞I/O,非阻塞I/OI/O复用,信号驱动I/O,异步I/O

四. 阻塞I/O(Blocking I/O)

    阻塞I/O的模型图如下图所示。


    当用户进程调用读操作时,阻塞在内核等待数据就绪状态;若数据就绪,则阻塞在将数据从内核拷贝至用户空间状态。

   Linux系统中所有的Socket默认状态下都是阻塞的。

    以下为示例源码Server.cServer进程阻塞在recvfrom函数处,等待客户端数据达到。一旦数据达到,则读取数据,结束。

#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define MAX_MSG_LENGTH 	255 
#define SERV_PORT 		8888 

int main(int argc, char **argv)
{
	int 		serv_sock_fd;
	struct 		sockaddr_in serv_addr, cli_addr;
	char 		msg[MAX_MSG_LENGTH] = {0};
	int 		sock_len;
	time_t		start_time, end_time;

	serv_sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
	bzero(&serv_addr, sizeof(serv_addr));
	serv_addr.sin_family 		= AF_INET;
	serv_addr.sin_addr.s_addr 	= htonl(INADDR_ANY);
	serv_addr.sin_port			= htons(SERV_PORT);

	if(bind(serv_sock_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0){
		printf("Bind error!\n");
		exit(-1);
	}
	
	sock_len = sizeof(struct sockaddr);

	start_time = time(NULL);
	printf("%s", ctime(&start_time));
	printf("\tBlocking, waiting for client message\n");
	if (-1 != recvfrom(serv_sock_fd, msg, MAX_MSG_LENGTH, 0, (struct sockaddr *)&cli_addr, &sock_len)){
		end_time = time(NULL);
		printf("%s", ctime(&end_time));	
		printf("\tReceive client message\n");		
	}
	
	close(serv_sock_fd);

	return 0; 
}

    以下为示例源码Client.c。客户端发送UDP报文给服务器端。

#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>

#define SERV_IP		"127.0.0.1" 
#define SERV_PORT 	8888

char msg[] = "hello";

int main(int argc, char **argv)
{
	int cli_sock_fd;
	struct sockaddr_in serv_addr;
	int 		sock_len;

	bzero(&serv_addr, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);
	serv_addr.sin_addr.s_addr = inet_addr(SERV_IP);
	
	cli_sock_fd = socket(AF_INET, SOCK_DGRAM, 0);

	sock_len = sizeof(struct sockaddr);
	sendto(cli_sock_fd, msg, strlen(msg),0, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

	return 0;
}

    编译指令

#gcc -g Server.c -o Server
#gcc -g Client.c -o Client

    先运行服务器端,再运行客户端。服务器端输出结果如下:

Thu Mar 31 14:28:33 2016
        Blocking, waiting for client message
Thu Mar 31 14:28:47 2016
        Receive client message

五. 非阻塞I/O(Non-Blocking I/O)

    可以将创建的Socket标志位设置为non-blocking,将其转变为非阻塞I/O。但非阻塞I/O的数据未就绪时,对其操作会返回错误提示(常用EWOULDBLOCK/EAGAIN)。依据错误提示,判断Socket是否出错,或者需要进行下一次的读取。


    下面给出Server.c代码,Client.c代码和以上一致,无需修改。

#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

#define MAX_MSG_LENGTH 	255 
#define SERV_PORT 		8888 

int main(int argc, char **argv)
{
	int 		serv_sock_fd;
	struct 		sockaddr_in serv_addr, cli_addr;
	char 		msg[MAX_MSG_LENGTH] = {0};
	int 		sock_len;
	time_t		start_time, end_time;
	int 		sock_flags;

	serv_sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
	sock_flags = fcntl(serv_sock_fd, F_GETFL, 0);
	fcntl(serv_sock_fd, F_SETFL, sock_flags|O_NONBLOCK);
		
	bzero(&serv_addr, sizeof(serv_addr));
	serv_addr.sin_family 		= AF_INET;
	serv_addr.sin_addr.s_addr 	= htonl(INADDR_ANY);
	serv_addr.sin_port			= htons(SERV_PORT);

	if(bind(serv_sock_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0){
		printf("Bind error!\n");
		exit(-1);
	}
	
	sock_len = sizeof(struct sockaddr);

	for(;;){
		start_time = time(NULL);
		printf("%s", ctime(&start_time));
		printf("\tNon Blocking, waiting for client message\n");
		if (-1 != recvfrom(serv_sock_fd, msg, MAX_MSG_LENGTH, 0, (struct sockaddr *)&cli_addr, &sock_len)){
			end_time = time(NULL);
			printf("%s", ctime(&end_time));	
			printf("\tReceive client message\n");
			break;
		}else if (errno == EAGAIN){
			sleep(1);	
		}else{
			end_time = time(NULL);
			printf("%s", ctime(&end_time));	
			printf("\tSocket Error\n");
			break;
		}		
	}

	close(serv_sock_fd);
	return 0; 
}
    先运行服务器端,再运行客户端。服务器端输出结果如下:

Thu Mar 31 14:51:29 2016
        Non Blocking, waiting for client message
Thu Mar 31 14:51:30 2016
        Non Blocking, waiting for client message
Thu Mar 31 14:51:31 2016
        Non Blocking, waiting for client message
Thu Mar 31 14:51:32 2016
        Non Blocking, waiting for client message
Thu Mar 31 14:51:33 2016
        Non Blocking, waiting for client message
Thu Mar 31 14:51:34 2016
        Non Blocking, waiting for client message
Thu Mar 31 14:51:35 2016
        Non Blocking, waiting for client message
Thu Mar 31 14:51:35 2016
        Receive client message

    从运行结果来看,服务器端每次调用 recvfrom时,由于没有数据达到,直接返回。直到有数据到达时,读取数据并退出。

六. I/O复用(I/O Multiplexing)

    I/O多路复用模型,就是将多路I/O阻塞在同一个地方,等到某个或者某些I/O有事件到达时,就通知进程进行相应Socket的数据读写操作。

    I/O复用的好处,就是将原先阻塞在各自I/O读写系统调用的地方,统一迁移到由某个系统调用函数来管理。而数据就绪时,由该系统调用函数负责通知。这样,对I/O的操作就不会阻塞在各自的等待数据就绪上面。

 

    Linux下的selectepoll函数就可以实现I/O复用功能。

    此处例子就不给出了。

七. 信号驱动I/O(Signal-driven I/O)

    信号驱动I/O需要Socket打开信号驱动I/O模式,并且通过sigaction系统调用注册SIGIO信号处理函数。当数据准备就绪时,内核通过发送SIGIO信号通知用户进程,用户进程通过信号处理函数读取数据。通过信号通知这种方式,用户进程不会因为数据未就绪而被阻塞在I/O上。

    让Socket可以工作于信号驱动I/O模式,一般需要完成以下三个步骤:

    (1).注册SIGIO信号处理程序 
    (2).设置
Socket所有者 
    (3).置位
SocketO_ASYNC标志,允许套接字信号驱动I/O。 

    服务器端源码Server.c如下。

#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>

#define MAX_MSG_LENGTH 	255 
#define SERV_PORT 		8888 

int 		serv_sock_fd;
	
void handle_sig_io(int sig)
{
    int cli_sock_fd, sock_len;
    struct sockaddr_in cli_addr;
    char msg[MAX_MSG_LENGTH];
	time_t		now_time;
 
	if (-1 != recvfrom(serv_sock_fd, msg, MAX_MSG_LENGTH, 0, (struct sockaddr *)&cli_addr, &sock_len)){
		now_time = time(NULL);
		printf("%s", ctime(&now_time));
		printf("\tReceive client message\n");		
	}
}
	
int main(int argc, char **argv)
{
	struct 		sockaddr_in serv_addr, cli_addr;
	char 		msg[MAX_MSG_LENGTH] = {0};
	int 		sock_len;
	time_t		start_time, end_time;
	int 		sock_flags;
	struct 		sigaction sig_io_action;

	serv_sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
	sock_flags = fcntl(serv_sock_fd, F_GETFL, 0);
	fcntl(serv_sock_fd, F_SETFL, sock_flags|O_NONBLOCK);
		
	bzero(&serv_addr, sizeof(serv_addr));
	serv_addr.sin_family 		= AF_INET;
	serv_addr.sin_addr.s_addr 	= htonl(INADDR_ANY);
	serv_addr.sin_port			= htons(SERV_PORT);

    memset(&sig_io_action, 0, sizeof(sig_io_action));
    sig_io_action.sa_flags = 0;
    sig_io_action.sa_handler = handle_sig_io;
    sigaction(SIGIO, &sig_io_action, NULL);
 
    fcntl(serv_sock_fd, F_SETOWN, getpid());
    sock_flags = fcntl(serv_sock_fd, F_GETFL, 0);
    sock_flags |= O_ASYNC | O_NONBLOCK;
    fcntl(serv_sock_fd, F_SETFL, sock_flags);
 	
	if(bind(serv_sock_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0){
		printf("Bind error!\n");
		exit(-1);
	}
	
	while(1){
		sleep(1);
		start_time = time(NULL);
		printf("%s", ctime(&start_time));
		printf("Main Thread, doing jobs!\n");
	}
	
	close(serv_sock_fd);
	return 0; 
}

    先运行服务器端程序,再运行客户端程序。服务器端程序输出如下:

Thu Mar 31 15:44:02 2016
        Main Thread, doing jobs!
Thu Mar 31 15:44:03 2016
        Main Thread, doing jobs!
Thu Mar 31 15:44:04 2016
        Main Thread, doing jobs!
Thu Mar 31 15:44:05 2016
        Main Thread, doing jobs!
Thu Mar 31 15:44:06 2016
        Main Thread, doing jobs!
Thu Mar 31 15:44:07 2016
        Receive client message
Thu Mar 31 15:44:07 2016
        Main Thread, doing jobs!
Thu Mar 31 15:44:08 2016
        Main Thread, doing jobs!
Thu Mar 31 15:44:09 2016
        Main Thread, doing jobs!
    可以看到,服务器端不会阻塞在 I/O 等待上面,而是执行其它操作。当客户端数据到达时,由信号处理函数负责处理。

八. 异步I/O(Asynchronous I/O)

    异步I/O是指,当用户进程发起I/O操作时,内核立即给用户进程返回,用户进程不受到任何阻拦,并且可以去完成其它操作。而用户进程所发起的I/O操作,由内核负责进行数据的准备和内核态到用户空间的数据拷贝。当这一切工作都完成时,内核向用户进程发送一个信号,告知读操作已经完成。

    可见,以上提到的阻塞I/O,非阻塞I/O和信号I/O,都归属于同步I/O的范畴,而在执行读操作时,都属于阻塞操作的范畴。而异步I/O,则是真正的非阻塞,因为它不会对用户进程产生任何的阻塞。


九. 总结

    最后,还是借用Richard Stevens书中的一幅图,来把阻塞I/O,非阻塞I/O,同步I/O,异步I/O进行一下总结。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值