Socket与ServerSocket
网络通信需要一对socket,即通信的两端各有一个socket,两个socket之间形成一个管道,进行数据流的通信。
ServerSocket
ServerSocket监听服务器端的一个端口,当一个客户端发送来连接时,ServerSocket来处理连接,成功后返回一个常规的Socket对象,用来与客户端socket进行数据传输。
同步 异步 阻塞 非阻塞
同步与异步主要是从消息通知机制角度来说的。
当一个同步调用发出后,调用者要一直等待返回消息(结果)后,才能进行后续的执行;当一个异步过程调用发出后,调用者不能立刻得到返回消息(结果),实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。异步是调用完成后由别人来通知他。
阻塞与非阻塞主要是程序(线程)等待消息通知时的状态角度来说的。
阻塞调用是指调用结果返回之前,当前线程会被挂起,一直处于等待消息通知,不能够执行其他业务。
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。如果在这个等待的过程中,等待者除了等待消息通知之外不能做其它的事情,那么该机制就是阻塞的。
阻塞与同步是不同的。如果这个线程在等待函数返回时,仍在执行其他消息处理,那么这就是同步非阻塞。如果这个线程在等待函数返回时,没有执行其他消息处理,而是挂起等待,那么就是同步阻塞
概念说明
操作系统将内存空间分为了内核空间和用户空间。
进程切换非常的耗资源,所以能不挂起进程就不挂起进程。
将进程阻塞是让出CPU资源。
对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:1. 等待数据准备 (Waiting for the data to be ready)2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
Linux IO的五种模型
阻塞IO

当应用进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在进程这边,整个应用进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,应用进程才解除block的状态,重新运行起来。在内核执行I/O的两个阶段,都是阻塞的。每个连接都需要配套一个线程,不适合高并发的情况。
在阻塞的过程中,这个线程被挂起了,但是他让出了CPU,其他应用进程可以继续占用CPU执行。
非阻塞IO
基于轮询的方式。
当所请求的I/O操作不能满足要求时候,不把本进程投入睡眠,而是返回一个错误。也就是说当数据没有到达时并不等待,而是以一个错误返回。并且进程会多次轮询的请求I/O操作。应用程序的线程需要不断的进行 I/O 系统调用,轮询数据是否已经准备好,如果没有准备好,继续轮询,直到完成系统调用为止 。这样,好处是线程不需要一直阻塞,但是需要不断地进行I/O系统调用,不断轮询,浪费CPU。不断发起I/O操作及其浪费CPU资源。同步非阻塞就是 “每隔一会儿瞄一眼进度条” 的轮询(polling)方式。
需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。
I/O多路复用
基于操作系统的poll,select,epoll。
非阻塞IO问题:
由于同步非阻塞方式需要不断主动轮询,轮询占据了很大一部分过程,轮询会消耗大量的CPU时间,但是服务器端可能会有多个连接,这样他对每一个连接都这样做,要是连接数量太多时,是不适合的。
IO多路复用有两个特别的系统调用select、poll、epoll函数。select调用是内核级别的,select轮询相对非阻塞的轮询的区别在于—前者可以等待多个socket,能实现同时对多个IO端口进行监听,当其中任何一个socket的数据准好了,就能返回进行可读,然后进程再进行recvform系统调用,将数据由内核拷贝到用户进程,当然这个过程是阻塞的。
select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作(只需要阻塞一个select函数)。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时(注意不是全部数据可读或可写),才真正调用I/O操作函数。
上面的图和blocking IO的图其实并没有太大的不同,在两个阶段都需要阻塞。当连接数量很少的时候,I/O多路复用可能比BIO效率还要低,因为I/O多路复用需要多执行一个select内核操作。但是I/O多路复用的优势在于他可以处理更多的连接,而不是处理单个连接速度更快。与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降底了系统的维护工作量,节省了系统资源。
信号驱动IO

在信号驱动式I/O模型中,应用程序使用套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
基本不怎么用。
异步IO

用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知(回调函数)。IO两个阶段,进程都是非阻塞的。
异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。
总结
注意:同步非阻塞I/O和I/O多路复用,在返回可读条件后,都需要再调用一次I/O操作,进行复制数据。首先一个IO操作其实分成了两个步骤:发起IO请求和实际的IO操作,同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO,如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO。阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。
select poll epoll
目前支持I/O多路复用的系统调用有 select,poll,epoll,I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。select,pselect,poll,epoll本质上都是同步I/O。
文件描述符
用于表述指向文件的引用的抽象化概念。。
当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
Linux的socket事件wakeup callback机制
linux wakeup callback机制是IO多路复用的本质。
Linux通过socket睡眠队列来管理所有等待socket的某个事件的进程(Process),同时通过wakeup机制来异步唤醒整个睡眠队列上等待事件的Process,通知Process相关事件发生。
每个socket维护了一个队列,比如socket可读的时候,内核就会唤醒队列里的各个Process,并且执行每个Process的callback函数。
每一个socket都有sleep_list,当某个进程所关心的事件在socket中并没有发生,那么将进程插入到sleep_list,当socket的时间发生了,那么就去遍历他sleep_list中的每个进程的callback函数。
select
我们以read事件为例子。
当socket上所监听的事件发生了,那么相应的进程就去处理。那么怎么获取监听的事件发生呢?
我们应该block在等待事件的发生上,这个事件简单点就是关心的N个socket中一个或多个socket有数据可读了,当block解除的时候,就意味着,我们一定可以找到一个或多个socket上有可读的数据。
根据wakeup callback。
所以,进程需要同时插入到我们管理的这好多个socket的sleep_list上等待任意一个socket可读事件发生而被唤醒,当Process被唤醒的时候,其callback里面应该有个逻辑去检查具体哪些socket可读了。
举个例子:
假设c1,c2,c3连接到了服务器端,我们想监听他们的读事件,因此我们将进程p1插入到他们与服务器各自的socket中sleep_list中去,此时没有事件发生,因此进程处于睡眠状态,当c1客户端发送来了数据,有数据读取的时候,p1进程被唤醒。
伪代码:
1 | private int sk_event; |
当receive queue不为空的时候(即收到了消息),我们就给这个socket的sk_event添加一个POLL_IN事件,用来表示当前这个socket可读。将来Process遍历到这个socket,发现其sk_event包含POLL_IN的时候,就可以对这个socket进行读取数据操作了。
接下来,p1执行select,select会将需要监控的readfds集合拷贝到内核空间(因为内核才能通知说某个socket可读),然后遍历自己监控的socket,挨个调用socket的poll逻辑以便检查该socket是否有可读事件。
遍历完所有的socket后,如果没有任何一个sk可读,那么select会调用schedule,使得Process进入睡眠(或者睡眠timeout这么长时间)。如果在timeout时间内某个socket上有数据可读了,或者等待timeout了,则调用select的Process会被唤醒。
伪代码如下:
1 | for (socket in readfds) { |
1 | int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); |
select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。
select缺点:
1.文件描述符的数量存在最大限制,在Linux上一般为1024。
2.对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。
每次select()都需要扫描所有的fd_set。而epoll是通过注册回调函数来实现的,所以epoll效率大大高于select.
3.需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
poll
解决了文件描述符数量限制的情况。
1 | int poll (struct pollfd *fds, unsigned int nfds, int timeout); |
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。
与select两点区别:
1.文件描述符大小不限制。
2.不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。
1 | struct pollfd { |
pollfd结构包含了要监视的event和发生的event。
epoll
解决了需要循环遍历文件描述符的缺点,解决了需要将大量的fds从内核拷贝到用户空间的问题。
将大量的fds从内核拷贝到用户空间的解决:
共享内存。
epoll通过内核与用户空间mmap同一块内存来解决。mmap将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址(不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射映射到物理地址),使得这块物理内存对内核和对用户均可见,减少用户态和内核态之间的数据交换。
需要循环遍历文件描述符的解决:
epoll引入了一个中间层,一个双向链表ready_list,一个单独的睡眠队列single_epoll_wait_list。
1.调用epoll之前,我们希望我们的MyProcess可以管理四个socket。
2.四个socket都没有事件,这时候MyProcess进入single_epoll_wait_list并且sleep。
3.有一个socket(大红色)收到了数据,触发其wait_entry_sk,把这个socket加入到ready_list里。
4.MyProcess被唤醒(从single_epoll_wait_list出来了表示被唤醒),来处理ready_list中的所有socket:遍历epoll的ready_list,挨个调用每个socket的poll逻辑收集发生的事件,对于监控可读事件而已,ready_list上的每个socket都是有数据可读的,这里的遍历必要的。
epoll有两种工作模式,LT和ET。
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
ServerSocket与Socket
Linux 五种IO模型
select、poll、epoll详解
Linux IO模式及 select、poll、epoll详解
深入理解select,poll,epoll