同步和异步、阻塞和非阻塞是操作系统的常见的概念,刚开始一直搞不懂,在看了n篇文章之后,终于有了一些感悟,现在就将我的理解贴出来与大家分享!!!
第一步:概念
(1)阻塞的概念:一个进程被阻塞的时候,从cpu的角度来说就要发生上下文切换,那么这个被阻塞的进程就不能被继续执行下去,把cpu让出来给别的进程。
(2)同步:在发出一个同步调用的时候,在没有得到结果之前就不会返回。那么对于发起调用的进程来说,在完成这个调用之前,它接下来的事情就不能进行,换句话说,
就是必须一件一件做,等前一件做完了才能做下一件事.
(3)异步:当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知、和回调函数来通知调用者。这里的重点是在调用
完成后会通过一种机制来告诉调用者。
从另一种角度来说,同步异步针对的是两个事情之间的关系,这种关系与拓扑结构中的先后关系类似类,如果两件事情之间存在拓扑关系,便是同步关系;如果没有时序先后的
关联或相互依赖,则是异步关系。而阻塞非阻塞针对的是发起任务的人(线程)在发起动作之后它所处的状态!
第二步:<<unix网络编程:卷一>>中涉及的I/O模型:
(1)阻塞式I/O模型:
我们可以看到这这种模型中,调用进程并没有继续执行下去,调用线程或进程被挂起让出cpu,等到对应的I/O就绪(想要的数据已经准备好的时候)并拷贝到用户空间之后
才唤醒进程(你可以继续往下执行了!!!)。所以一旦进程或线程以阻塞式的方式进行I/O的话,在某种程度上是同步I/O(即同步阻塞I/O)。
(2)非阻塞式I/O模型:
在非阻塞式的I/O模型中,调用者在调用recvform之后,函数立即返回-1,表示函数调用出错,出错的原因放在errno变量当中。这个时候调用者并没有阻塞,它既可以继续
调用reevform函数,也可以干其它的事。但我们调用recvfrom是为了获得数据,一般会利用循环while语句继续来检测fd是否准备就绪,但是这种做法十分浪费cpu资源,我们看
一个例子:
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <time.h>
#include <strings.h>
#include <errno.h>
int main (int argc,char * argv[]){
int servfd,clifd;
struct sockaddr_in servaddr,cliaddr;
if((servfd=socket(AF_INET,SOCK_STREAM,0))<0){
printf("Create socket error!\n");
return -1;
}
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(5000);
servaddr.sin_addr.s_addr=htonl(INADDR_ANY); //inet_addr("192.168.0.1");
int option=1;
setsockopt(servfd,SOL_SOCKET,SO_REUSEADDR,(char *)&option,sizeof(option));
struct linger li;
li.l_onoff=1;
li.l_linger=1;
setsockopt(servfd,SOL_SOCKET,SO_LINGER,(char *)&li,sizeof(li));
if(bind(servfd,(struct sockaddr * )&servaddr,sizeof(servaddr))<0){
perror("bind to port 5000 failure");
return -1;
}
if(listen(servfd,10)<0){
perror("listen error");
return -1;
}
int flags = fcntl(servfd, F_GETFL, 0);
fcntl(servfd, F_SETFL, flags | O_NONBLOCK);
while(1){
socklen_t len;
len=sizeof(cliaddr);
clifd=accept(servfd,(struct sockaddr *)&cliaddr,&len);
if(clifd < 0){
if (errno==EAGAIN || errno == EWOULDBLOCK){
usleep(10000);
continue;
}
else{
perror("call accept error");
break;
}
}
char szIp[17];
bzero(szIp,17);
inet_ntop(AF_INET,&cliaddr.sin_addr,szIp,16);
printf("from client IP:%s,Port:%d\n",szIp,ntohs(cliaddr.sin_port));
char buf[256];
time_t t;
time(&t);
int datalen=sprintf(buf,"Server:%u\n",(unsigned int)t);
send(clifd,buf,datalen,0);
close(clifd);
}
close(servfd);
return 0;
}
可以看到在服务器端用非阻塞的方式调用accept函数,在返回值是-1的时候,说明在内核的连接队列中没有已经完成的TCP连接,但是程序不会被阻塞,还是会继续执行下去,
所以就用while语句来轮询是否有已经建立的连接!!!!
(3)I/O多路复用模型:
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候
用户进程再调用read操作,将数据从kernel拷贝到用户进程。这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call
(select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。(多说一句。所以,如果处理的连接数
不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能
处理得更快,而是在于能处理更多的连接。)在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的
process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
(4)异步非阻塞I/O:
利用aio进行异步读取数据的一个范例:
#include <aio.h>
int main(){
int fd, ret;
struct aiocb my_aiocb;
fd = open( "file.txt", O_RDONLY );
if (fd < 0) perror("open");
/* Zero out the aiocb structure (recommended) */
bzero( (char *)&my_aiocb, sizeof(struct aiocb) );
/* Allocate a data buffer for the aiocb request */
my_aiocb.aio_buf = malloc(BUFSIZE+1);
if (!my_aiocb.aio_buf) perror("malloc");
/* Initialize the necessary fields in the aiocb */
my_aiocb.aio_fildes = fd;
my_aiocb.aio_nbytes = BUFSIZE;
my_aiocb.aio_offset = 0;
ret = aio_read( &my_aiocb );
if (ret < 0) perror("aio_read");
while ( aio_error( &my_aiocb ) == EINPROGRESS );
if ((ret = aio_return( &my_aiocb )) > 0) {
/* got ret bytes on the read */
} else {
/* read failed, consult errno */
}
}
其中aio_read的返回值只有两种:aio_read 函数在请求进行排队之后会立即返回。如果执行成功,返回值就为 0;如果出现错误,返回值就为 -1,并设置 errno 的值。当请求
提交成功之后,我们就需要调用aio_errno来获取以下返回值:(1)EINPROGRESS,说明请求尚未完成(2)ECANCELLED,说明请求被应用程序取消了(3)-1,说明发生了错
误,具体错误原因可以查阅 errno一旦读取成功就调用aio_return来获取数据。可以看到这个模型中,如果数据没有准备好,我们不用继续调用aio_read,而是调用aio_errno来
获取请求状态。
下图是对以上模型的一个总结: