在Go语言中,网络I/O调度是通过goroutine和操作系统的底层I/O复用机制相结合来实现的。
下面是Go语言网络I/O调度的基本原理:
-
Goroutine:Go语言通过goroutine来实现并发。goroutine是一种轻量级的线程,可以在Go程序中创建大量的goroutine,并且它们的切换成本非常低。当一个goroutine进行网络I/O操作时,它会阻塞,但是其他goroutine可以继续执行。
-
I/O多路复用:Go语言利用操作系统提供的底层I/O多路复用机制,通常是使用epoll(在Linux系统上)或kqueue(在BSD和macOS系统上)来实现。这些机制允许一个线程同时监视多个文件描述符(包括套接字),并在其中任意一个文件描述符就绪时通知应用程序。
-
网络库:Go语言的标准库中提供了net包,其中包含了用于网络编程的一些基本功能,例如创建套接字、进行读写操作等。net包通过底层的I/O多路复用机制实现了高效的网络I/O调度。
-
goroutine调度:当一个goroutine进行网络I/O操作时,它会向操作系统注册文件描述符,并将自己标记为阻塞状态。然后,调度器会挂起这个goroutine,并选择其他可运行的goroutine继续执行。当操作系统通知某个文件描述符就绪时,调度器会重新唤醒相应的goroutine,使其继续执行。
-
回调机制:为了实现高效的网络编程,Go语言中的网络库通常使用回调机制。当一个网络事件发生时(如有数据可读或可写),操作系统会通知应用程序,并调用相应的回调函数来处理事件。在回调函数中,可以进行数据的读取、处理和写入等操作。
在Linux系统中,一般通过epoll实现多路复用;相对于 select() 和 poll() 来说,epoll 没有描述符限制。epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。
epoll
数据结构
epoll
使用了一个名为 epoll
实例的文件系统对象作为一个集合,其中存储了所有被监听的文件描述符。该集合包含了三个数据结构:
- 红黑树(Red-Black Tree):用于存储所有被监听的文件描述符及其关联的事件;
- 双向链表(Double Linked List):用于存储触发事件的文件描述符及其关联的事件;
- 事件源数组(Event Source Array):用于存储触发事件的文件描述符及其关联的事件,与双向链表结构相同。
epoll_create
函数:int epoll_create(int size);
在使用 epoll
前,需要通过 epoll_create
函数创建一个 epoll
实例。该函数返回一个文件描述符,该文件描述符用于标识该 epoll
实例。
epoll_ctl
函数:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
在创建 epoll
实例后,可以通过 epoll_ctl
函数向 epoll
实例中添加或删除一个文件描述符及其关联的事件。epoll_ctl
函数的第一个参数是指定要操作的 epoll
实例的文件描述符,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从 epfd 中删除一个 fd;
第三个参数是指定要添加或删除的文件描述符,第四个参数是指定要添加或删除的事件,事件的定义如下:
// 跟事件相关联的数据定义
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
// 事件定义
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 队列里
epoll_wait
函数:int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
一旦向 epoll
实例中添加了文件描述符及其关联的事件,就可以调用 epoll_wait
函数来等待事件发生。epoll_wait
函数的第一个参数是指定要等待的 epoll
实例的文件描述符,第二个参数是指定一个数组用于存储触发事件的文件描述符及其关联的事件,第三个参数是指定数组的大小,第四个参数是指定超时时间。
epoll_wait
函数会阻塞,直到一个或多个文件描述符上的事件发生或超时。如果一个或多个文件描述符上的事件已经发生,epoll_wait
函数将把触发事件的文件描述符及其关联的事件存储到指定的数组中并返回,然后应用程序可以根据事件类型进行相应的处理。
epoll 对文件描述符的操作有两种模式:LT(level trigger)和 ET(edge trigger)。
- Level-Triggered (LT) 模式:
- LT 是
epoll
的默认模式。 - 当一个文件描述符上的 I/O 事件就绪时,
epoll_wait
将立即返回,并将该文件描述符添加到触发事件的文件描述符列表中。 - 应用程序需要循环调用
epoll_wait
来获取触发的事件。 - 对于可读事件,应用程序需要持续读取数据直到返回 EAGAIN 或 EWOULDBLOCK 错误。
- 对于可写事件,应用程序需要持续写入数据直到返回 EAGAIN 或 EWOULDBLOCK 错误。
- LT 模式适用于需要处理大量的数据,且可能存在阻塞的情况。
- Edge-Triggered (ET) 模式:
- ET 模式需要通过
epoll_ctl
显式地将文件描述符设置为 ET 模式。 - 当一个文件描述符上的 I/O 事件从未就绪状态变为就绪状态时,
epoll_wait
将立即返回,并将该文件描述符添加到触发事件的文件描述符列表中。 - 应用程序需要在事件就绪后立即处理,并确保将该文件描述符上的所有数据都处理完毕。
- 对于可读事件,应用程序只需读取一次数据即可,不需要循环读取直到返回 EAGAIN 或 EWOULDBLOCK 错误。
- 对于可写事件,应用程序只需写入一次数据即可,不需要循环写入直到返回 EAGAIN 或 EWOULDBLOCK 错误。
- ET 模式适用于需要实时响应的场景,例如高性能的网络服务器。
相比于poll和select,epoll的优点如下:
-
高效:epoll 可以同时处理大量的文件描述符,能够处理数百万个并发连接,相比于 select 和 poll,它的效率更高。
-
没有最大并发连接数限制:在 epoll 中,内核维护了一个事件表,可以同时监控大量的文件描述符,没有最大并发连接数的限制。
-
内存拷贝次数少:与 select 和 poll 相比,epoll 采用基于事件驱动的机制,只有活跃的文件描述符才会被放在内核事件表中,因此减少了内存拷贝次数。
-
更快的应用响应速度:epoll 采用的是事件通知机制,只有在文件描述符状态发生变化时才会通知应用程序,这样能够更快地响应客户端请求。
-
支持 ET 和 LT 两种触发模式:epoll 支持 Edge-Triggered (ET) 和 Level-Triggered (LT) 两种触发模式,可以根据应用需求灵活选择。
总结起来,Go语言的网络I/O调度是通过将网络I/O操作交给操作系统进行处理,并利用goroutine和I/O多路复用机制来实现高效的并发。当一个goroutine进行网络I/O操作时,它会被挂起,其他可运行的goroutine会继续执行,直到操作系统通知相应的文件描述符就绪,然后调度器会重新唤醒该goroutine,使其继续执行。这种调度方式可以使Go程序在高并发的网络环境下高效地处理大量的并发连接。