广义同步&异步 阻塞&非阻塞 及 网络IO中的同步&异步,阻塞&非阻塞

广义的同步/异步,阻塞/非阻塞

同步 VS 异步 (synchronous VS asynchronous)

同步和异步关注的消息通信机制。

同步和异步仅仅是关于所关注的消息如何通知的机制。同步的情况下,是由处理消息者自己去等待消息是否被触发(主动去获取状态),而异步的情况下是由触发机制来通知处理消息者。


阻塞 VS 非阻塞 (blocking VS nonblocking)

阻塞和非阻塞关注的程序在等待调用结果(消息,返回值)时的状态。

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

非阻塞调用是指不能立刻得到结果之前,该调用不会阻塞当前线程(可能会返回一个error来通知进程操作得不到结果)。


Linux Unix 网络IO模型 & 网络模型中的同步/异步,阻塞/非阻塞

对于一个network IO,它会涉及两个系统对象,一个是调用这个IO的process(or thread),另一个就是系统内核(kernel)。 一个输入操作通常包括两个不同的阶段:

  • 第一阶段 - 等待数据准备好 (waiting for the date to be ready)
  • 第二阶段- 从内核向进程复制数据 (copy the data from the kernel to the process)

对于一个套接字上的输入操作,第一步通常涉及等待数据从网络到达。当所有等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用程序缓冲区


5种类Unix下可用的I/O模型:

阻塞I/O模型

默认情况下,所有套接字都是阻塞的。

当用户进程调用了 recvfrom 这个系统调用, kernel就开始了IO的第一个阶段,准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会把数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才接触block的状态,重新运行起来。

所以,blocking IO的特点就是在IO执行的两个阶段都被block了


标红的这部分过程就是阻塞,直到阻塞结束 recvfrom 才能返回。


非阻塞I/O模型

在linux下,可以通过设置socket使其变为non-blocking。

进程把一个套接字设置成非阻塞是在通知内核,当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把进程投入睡眠,而是返回一个错误。

当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个 error。从用户角度讲,它发起一个read操作后,并不需要等待,而是马上就得到一个结果。用户进程判断结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call时,那么它马上就将数据拷贝到了用户内存,然后返回。所以,用户进程其实是需要不断的主动询问kernel数据好了没有。


可以看出 recvfrom 总是立即返回(数据没准备好的时候)。


I/O多路复用模型

select / epoll的好处就在于单个 process就可以同时处理多个网络连接的IO。它的基本原理就是select /epoll 这个函数会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

当用户进程调用了 select, 那么整个进程会被block, 而同时, kernel 会监视所有 select 负责的 socket, 当任何一个socket中数据准备好了,select就会返回。这个时候用户进程再调用 read 操作,将数据从 kernel 拷贝到用户进程。

select 的优势在于它可以同事处理多个connection (所以,如果处理的连接不是很高的话,使用select / epoll 的web server不一定比使用multi-threading + blocking io 的web server性能更好,可能延时还更大,select / epoll 的优势并不是对于单个连接能处理的更快,而是在于能处理更多的连接)


(在使用select 函数之后,调用这阻塞在 select 函数之上。select 函数在执行一个死循环,这个死循环,在select函数内部一遍又一遍查询是否有socket准备好了数据。其实select 函数就是把非阻塞IO的轮询工作封装了下,本质上仍然是一个同步操作)



信号驱动式I/O

用的较少


I/O多路复用,信号驱动

这两种方式在处理业务逻辑上可以说有异步,但在I/O操作层面上来说还是同步的。


异步I/O模型

这类函数的工作机制时告知内核启动某个操作,并让内核在整个操作(包括将数据从内核拷贝到用户空间)完成后通知我们。

用户进程发起read操作之后,立刻就可以开始去做其他的事。而另一方面,从kernel的角度,当它收到一个异步read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据注备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。


注意红线标记处说明在调用时就可以立马返回,等函数操作完成后会通知我们。


总结

前四种I/O模型都是同步I/O操作,他们的区别是在于第一阶段,而他们的第二阶段是一样的。在数据从内核复制到应用缓冲区期间(用户区间),进程阻塞于recvfrom调用。

相反,异步I/O模型在这两个阶段都要处理。


POXIS严格定义的异步I/O时要求没有任何一点阻塞,而上述的前四个都不同程度阻塞了,而且都有一个共同的阻塞:内核拷贝数据到进程空间的这段时间需要等待。


POXIS对这两个术语的定义:

  • 同步I/O操作:导致请求进程阻塞,直到I/O操作完成
  • 异步I/O操作:不导致请求进程阻塞

阻塞,非阻塞:进程/线程要访问的数据是否就绪,进程/线程是否需要等待数据就绪 (针对第一阶段

同步,异步:访问数据的方式,同步需要主动读写数据,在读写数据的过程中还是会阻塞;异步只需要I/O操作完成的通知,并不主动读写数据,由操作系统内核完成数据的读写 (第一 + 第二阶段


在处理IO的时候,阻塞和非阻塞都是同步IO;只有使用了特殊的API,才是异步IO。


POSIX defines these two terms as follows:

· A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes.

· An asynchronous I/O operation does not cause the requesting process to be blocked.

Using these definitions, the first four I/O models—blocking, nonblocking, I/O multiplexing, and signal-driven I/O—are all synchronous because the actual I/O operation (recvfrom) blocks the process. Only the asynchronous I/O model matches the asynchronous I/O definition.

原文自<<UNIX网络编程>> 第三版 6.2节

两者的区别就在于同步IO做IO Operation的时候会将process阻塞。按照这个定义,之前所述的前四种都属于同步IO。有人可能会说, non-blocking IO并没有被阻塞啊?这里有个非常狡猾的地方,定义中所指的IO Operation是指真实的IO操作,就是例子中的recvfrom 这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel 的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,这段时间内,进程是被block的。而异步IO则不一样,当进程发起IO操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成,在这整个过程中,进程完全没有被block。


各个IO模型的比较如图所示:




对unix来讲:阻塞式I/O(默认),非阻塞式I/O(nonblock),I/O复用(select/poll/epoll)都属于同步I/O,因为它们在数据由内核空间复制回进程缓冲区时 (recvfrom, 第二阶段)都是阻塞的(不能干别的事)。只有异步I/O模型(AIO)是符合异步I/O操作的含义的,即在1数据准备完成、2由内核空间拷贝回缓冲区后 通知进程,在等待通知的这段时间里可以干别的事。


阻塞/非阻塞 - socket模式下数据的发送接收


发送 - send(TCP), sendto(UDP)

首先需要说明的是,不管阻塞还是非阻塞,在发送时都会将数据从应用缓冲区拷贝到内核缓冲区(SO_RCVBUF选项声明,除非缓冲区大小为0)。


  •  在阻塞模式下send操作将会等待所有数据均被拷贝到发送缓冲区后才会返回。

 如果当前发送缓冲总大小为8192,已经拷贝到缓冲的数据为8000,那剩余的大小为192,现在需要发送2000字节数据,那阻塞发送就会等待缓冲区足够把所有2000字节数据拷贝进去,如第一次拷贝进192字节,当缓冲区成功发送出1808字节后,再把应用缓冲区剩余的1808字节拷贝到内核缓冲,而后send操作返回成功发送字节数。

 从上面的过程不难看出,阻塞的send操作返回的发送大小,必然是你参数中的发送长度的大小。

  •  在阻塞模式下的sendto操作不会阻塞。

 关于这一点的原因在于:UDP并没有真正的发送缓冲区,它所做的只是将应用缓冲区拷贝给下层协议栈,在此过程中加上UDP头,IP头,所以实际不存在阻塞。

  •  在非阻塞模式下send操作调用会立即返回。

 关于立即返回大家都不会有异议。还是拿阻塞send的那个例子来看,当缓冲区只有192字节,但是却需要发送2000字节时,此时调用立即返回,并得到返回值为192。从中可以看到,非阻塞send仅仅是尽自己的能力向缓冲区拷贝尽可能多的数据,因此在非阻塞下send才有可能返回比你参数中的发送长度小的值。

 如果缓冲区没有任何空间时呢?这时肯定也是立即返回,但是你会得到WSAEWOULDBLOCK/E WOULDBLOCK 的错误,此时表示你无法拷贝任何数据到缓冲区,你最好休息一下再尝试发送。

  •  在非阻塞模式下sendto操作 不会阻塞(与阻塞一致,不作说明)。

接收 - recv(TCP), recvfrom(UDP)

在阻塞模式下recv,recvfrom操作将会阻塞 到缓冲区里有至少一个字节(TCP)或者一个完整UDP数据报才返回。

在没有数据到来时,对它们的调用都将处于睡眠状态,不会返回。


在非阻塞模式下recv,recvfrom操作将会立即返回。

如果缓冲区 有任何一个字节数据(TCP)或者一个完整UDP数据报,它们将会返回接收到的数据大小。而如果没有任何数据则返回错误 WSAEWOULDBLOCK/E WOULDBLOCK。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值