【转载】socket编程之阻塞/非阻塞,同步/异步以及I/O模型

在学习socket编程知识的过程中遇到一些疑惑,查阅了一些资料,主要包括以下内容:阻塞/非阻塞,同步/异步,五种I/O模型以及select和epoll的区别。文章转载自CSDN博客以及知乎社区,原文链接地址见下方。

1.理解概念

(1)同步

1)同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)

2)所谓同步,就是在发出一个*调用*时,在没有得到结果之前,该*调用*就不返回。但是一旦调用返回,就得到返回值了。

换句话说,就是由*调用者*主动等待这个*调用*的结果。

(2)异步

而异步则是相反,*调用*在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在*调用*发出后,*被调用者*通过状态、通知来通知调用者,或通过回调函数处理这个调用。

(3)阻塞

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

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

(4)非阻塞

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

(5)此处载录网络上一个形象的例子:

老张爱喝茶,废话不说,煮开水。
出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
1 老张把水壶放到火上,立等水开。(同步阻塞)
老张觉得自己有点傻
2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)
老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。
3 老张把响水壶放到火上,立等水开。(异步阻塞)
老张觉得这样傻等意义不大
4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
老张觉得自己聪明了。所谓同步异步,只是对于水壶而言。
普通水壶,同步;响水壶,异步。
虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。
同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。所谓阻塞非阻塞,仅仅对于老张而言。
立等的老张,阻塞;看电视的老张,非阻塞。
情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。
2.Linux下的五种I/O模型
  • 阻塞I/O(blocking I/O)
  • 非阻塞I/O (nonblocking I/O)
  •  I/O复用(select 和poll) (I/O multiplexing)
  • 信号驱动I/O (signal driven I/O (SIGIO))
  • 异步I/O (asynchronous I/O (the POSIX aio_functions))

前四种都是同步,只有最后一种才是异步IO。

(1)阻塞I/O
最流行的I/O模型是阻塞式I/O模型,默认情形下,所有套接字都是阻塞的。

进程会一直阻塞,直到数据拷贝完成返回给调用者。

blockI/O

1)应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。 如果数据没有准备好,一直等待….数据准备好了,从内核拷贝到用户空间,IO函数返回成功指示。

2)当调用recv()函数时,系统首先查是否有准备好的数据。如果数据没有准备好,那么系统就处于等待状态。当数据准备好后,将数据从内核空间复制到用户空间,然后该函数返回。

3)优点:使用阻塞模式的套接字,开发网络程序比较简单,容易实现。当希望能够立即发送和接收数据,且处理的套接字数量比较少的情况下,使用阻塞模式来开发网络程序比较合适。

4)缺点:阻塞模式套接字的不足表现为,在大量建立好的套接字线程之间进行通信时比较困难。阻塞模式给网络编程带来了一个很大的问题,如在调用 send()的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。这给多客户机、多业务逻辑的网络编程带来了挑战。这时,我们可能会选择多线程的方式来解决这个问题:让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。

具体使用多进程还是多线程,并没有一个特定的模式。传统意义上,进程的开销要远远大于线程,所以,如果需要同时为较多的客户机提供服务,则不推荐使用多进程;如果单个服务执行体需要消耗较多的 CPU 资源,譬如需要进行大规模或长时间的数据运算或文件访问,则进程较为安全。通常,使用 pthread_create () 创建新线程,fork() 创建新进程。
多线程/进程服务器同时为多个客户机提供应答服务。模型如下:fwnx5mqahxv1ngs70(2)非阻塞I/O模型
进程把一个套接字设置成非阻塞是在通知内核:当所请求的I/O操作非得把进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。

非阻塞IO通过进程反复调用IO函数(多次系统调用,并马上返回);在数据拷贝的过程中,进程是短暂阻塞的。nonblockI/O

1)可以通过使用fcntl函数将套接口设置为非阻塞模式:
fcntl(fd, F_SETFL, flag|O_NONBLOCK);

2)将套接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误(EWOULDBLOCK)。这样我们的I/O操作函数将不断的测试数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU时间片。所以不推荐使用。

(3)I/O复用
有了I/O复用,我们就可以调用selectpoll,阻塞在这两个系统调用中的某一个之上,而不是阻塞在真正的I/O系统调用上。

I/Omultiplexing

1)我们阻塞于select调用,等待数据报套接字变为可读。当select返回套接字可读这一条件时,我们调用recvfrom把所读数据报复制到应用进程缓冲区。
2)比较阻塞式I/O,I/O复用并不显得有什么优势,事实上由于使用select需要两个而不是单个系统调用,I/O复用还稍有劣势。不过我们使用select的优势在于我们可以等待多个描述符就绪。

(4)信号驱动I/O
我们也可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们,我们称这种模型为信号驱动式I/O。

SIGI/O

1)我们要首先开启套接字的信号驱动I/O功能,并通过sigaction系统调用安装一个信号处理函数。进程继续运行并不阻塞,当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
2)这种模型也不常用。

(5)异步I/O
该模型与前一个信号驱动模型的主要区别在于:信号驱动式I/O是在内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成
asyncI/O1)这种I/O的效率最高,通过aio_read函数实现。该函数提交一个请求并会递交了一个应用层缓冲区buf。即使内核中没有数据到来,该函数也立即返回;
2)当有数据到来,内核就会自动地将这些数据拷贝到应用层的缓冲区buf,一旦复制完成,会通过一个信号来通知应用进程的程序。

(6)五种I/O模型的比较5I/O

小结:
前四种I/O模型——阻塞式I/O模型、非阻塞式I/O模型、I/O复用模型和信号驱动式I/O模型都是同步I/O模型,因为其中真正的I/O操作(recvfrom)将阻塞进程。只有异步I/O模型与POSIX定义的异步I/O相匹配。

3.select, poll, epoll

epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里又都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现

(1)select

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

  • 一个进程可监视的fd数量被限制,即一个进程能打开的最大文件描述符限制:一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个,64位机默认是2048。(可通过命令ulimit -n 修改);
    此外,fd_set集合容量的限制(FD_SETSIZE),这需要重新编译内核。
  • 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll做的。
  • 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

(2)poll

1)poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

2)它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:

  • 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
  • poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

(3)epoll

1)epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

2)epoll的优点:

  • 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
  • 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;(即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。)
  • 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递。(即epoll使用mmap减少复制开销)

(4)select,poll, epoll区别总结

1)支持一个进程所能打开的最大连接数:

  • select:单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32*32,同理64位机器上FD_SETSIZE为32*64),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。
  • poll:poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的。
  • epoll:虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接。

2)FD剧增后带来的IO效率问题:

  • select:因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。
  • poll:同上
  • epoll:因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。

3)消息传递方式:

  • select:内核需要将消息传递到用户空间,需要内核拷贝动作。
  • poll:同上
  • epoll:epoll通过内核和用户空间共享一块内存来实现的。

4)总结:

  • 表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
  • select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善。

内容转载自:
http://blog.csdn.net/hguisu/article/details/7453390
https://www.zhihu.com/question/19732473

发表评论

电子邮件地址不会被公开。 必填项已用*标注