目录
基本概念
同步与异步
-
同步是指一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成。
-
异步是指不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了,异步一般使用状态、通知和回调。
阻塞与非阻塞
- 阻塞是指调用结果返回之前,当前线程会被挂起,一直处于等待消息通知,不能够执行其他业务。
- 非阻塞是指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
五种 IO 模型
对于一次IO访问,数据会先被拷贝到内核的缓冲区中,然后才会从内核的缓冲区拷贝到应用程序的地址空间。需要经历两个阶段:
- 准备数据。
- 将数据从内核缓冲区拷贝到进程地址空间。
由于存在这两个阶段,Linux 具有下面五种 I/O 模型。
阻塞 IO
当用户进程调用了 recvfrom() 时,内核进入 IO 的第一个阶段:准备数据(内核需要等待足够多的数据再拷贝),这个过程需要等待,用户进程会被阻塞,等内核将数据准备好,然后拷贝到用户地址空间,内核返回结果,用户进程才从阻塞态进入就绪态。
Linux 中,默认情况下所有的 Socket 都是阻塞的。
非阻塞 IO
当用户进程发出 read() 调用时,如果 Kernel 中的数据还没有准备好,那么它并不会阻塞用户进程,而是立刻返回一个 Error。用户进程判断结果是一个 Error 时,它就知道数据还没有准备好,于是它可以再次发送 read() 调用。一旦 Kernel 中的数据准备好了,并且又再次收到了用户进程的系统调用,那么它马上就将数据拷贝到了用户内存,然后返回。
非阻塞 IO 模式下用户进程需要不断地询问内核的数据准备好了没有,如果没有准备好,那么在某些场景中,用户进程可以去做别的事情而不需要一直等待。
Linux 下可以通过设置 Socket 为 non-blocking 模式。
同步 IO(信号驱动)
内核文件描述符就绪后,通过 Signal(信号)通知用户进程,用户进程再通过系统调用读取数据。此方式属于同步 IO,因为实际读取数据到用户进程缓存的工作仍然是由用户进程自己负责的。
异步 IO
用户进程发起 read() 调用之后,立刻就可以开始去做其它的事。内核收到一个异步 IO read 之后,会立刻返回,不会阻塞用户进程。内核会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,内核会给用户进程发送一个 Signal(信号),告诉它 read() 完成了。用户进程再从用户内存读取数据。
IO 多路复用
通过一种机制,一个进程可以监视多个文件描述符(套接字描述符),一旦某个文件描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。这样就不需要每个用户进程不断的询问内核数据准备好了没有。
select
Kernel 会监视所有 select() 负责的 Socket,当任意 Socket 中的数据准备好了,select 就会返回。这个时候用户进程再调用 read(),将数据从 Kernel 拷贝到用户进程。
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() 的一个缺点在于单个进程能够监视的文件描述符数量存在限制,在 Linux 上一般为 1024 个。
poll
poll() 使用了一个 pollfd 的指针实现。
int poll(struct pollfd *fds, unsigned int nfds, int timeout);
结构体类型参数 pollfd 包含了要监视的 Event 和发生的 Event。
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
和 select() 一样,poll() 返回后,需要遍历 pollfd 来获取就绪的描述符。区别在于 poll 没有监听的最大数量限制。
epoll
epoll 使用一个文件描述符来管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,采用监听回调的机制,这样在用户空间和内核空间的数据拷贝只需要进行一次,避免再次遍历就绪的文件描述符列表,从而提升了性能。
epoll 的操作过程需要三个接口:
- 创建一个 epoll 的句柄,形参 size 用来告诉内核这个监听的数目一共有多大。
int epoll_create(int size);
- 对指定描述符 fd 执行 op 操作。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- epfd:是 epoll_create() 的返回值。
- op:表示操作,用三个宏来表示:EPOLL_CTL_ADD,EPOLL_CTL_DEL,EPOLL_CTL_MOD。分别添加、删除和修改对 fd 的监听事件。
- fd:是需要监听的 fd(文件描述符)。
- epoll_event:是告诉内核需要监听什么事件,struct epoll_event 结构如下:
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events 可以是以下几个宏的集合:
- EPOLLIN :表示对应的文件描述符可以读(包括对端 Socket 正常关闭);
- EPOLLOUT:表示对应的文件描述符可以写;
- EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
- EPOLLERR:表示对应的文件描述符发生错误;
- EPOLLHUP:表示对应的文件描述符被挂断;
- EPOLLET: 将 epoll 设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的;
- EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 Socket 的话,需要再次把这个 Socket 加入到 epoll 队列里。
- 等待 epfd 上的 IO 事件,最多返回 maxevents 个事件。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- events:用来从内核得到事件的集合
- maxevents:告之内核这个 events 有多大,这个 maxevents 的值不能大于创建 epoll_create() 时指定的 size
- timeout:超时时间,单位毫秒,0 表示立即返回,-1 表示不确定,也有说法说是永久阻塞
该函数返回需要处理的事件数目,如返回 0 表示已超时。
epoll 的两种工作模式:
- LT(Level Trigger,水平触发)模式:当 epoll_wait 检测到描述符就绪,将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用 epoll_wait 时,会再次响应应用程序并通知此事件。LT 模式是默认的工作模式,同时支持阻塞和非阻塞 Socket。
- ET(Edge Trigger,边缘触发)模式:当 epoll_wait 检测到描述符就绪,将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait 时,不会再次响应应用程序并通知此事件。ET 是高速工作方式,只支持非阻塞 Socket。ET 模式减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。
来源:oschina
链接:https://my.oschina.net/u/4416145/blog/4267347