diff --git a/notes/Socket.md b/notes/Socket.md index 56cbad75..d179b387 100644 --- a/notes/Socket.md +++ b/notes/Socket.md @@ -48,15 +48,15 @@ Unix 有五种 I/O 模型: ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); ``` -

+

### 非阻塞式 I/O -应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。 +应用进程执行系统调用之后,如果数据还没有准备好,内核立即返回错误码 EAGAIN(也写作 EWOULDBLOCK)。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。 由于 CPU 要处理更多的系统调用,因此这种模型的 CPU 利用率比较低。 -

+

### I/O 复用 @@ -66,7 +66,7 @@ ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr * 如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都需要创建一个线程去处理。如果同时有几万个连接,那么就需要创建相同数量的线程。相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小。 -

+

### 信号驱动 I/O @@ -74,7 +74,7 @@ ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr * 相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。 -

+

### 异步 I/O @@ -82,7 +82,7 @@ ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr * 异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。 -

+

### 五大 I/O 模型比较 @@ -93,7 +93,7 @@ ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr * 非阻塞式 I/O 、信号驱动 I/O 和异步 I/O 在第一阶段不会阻塞。 -

+

## 二、I/O 复用 @@ -138,17 +138,21 @@ tv.tv_usec = 0; int ret = select( largest_sock + 1, &fd_in, &fd_out, NULL, &tv ); // Check if select actually succeed -if ( ret == -1 ) +if ( ret == -1 ) { // report error and abort -else if ( ret == 0 ) +} +else if ( ret == 0 ) { // timeout; no event detected +} else { - if ( FD_ISSET( sock1, &fd_in ) ) + if ( FD_ISSET( sock1, &fd_in ) ) { // input event on sock1 + } - if ( FD_ISSET( sock2, &fd_out ) ) + if ( FD_ISSET( sock2, &fd_out ) ) { // output event on sock2 + } } ``` @@ -184,12 +188,14 @@ fds[1].fd = sock2; fds[1].events = POLLOUT; // Wait 10 seconds -int ret = poll( &fds, 2, 10000 ); +int ret = poll( fds, 2, 10000 ); // Check if poll actually succeed -if ( ret == -1 ) +if ( ret == -1 ) { // report error and abort -else if ( ret == 0 ) +} +else if ( ret == 0 ) { // timeout; no event detected +} else { // If we detect the event, zero it out so we can reuse the structure @@ -234,7 +240,7 @@ epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描 从上面的描述可以看出,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符。 -

+

epoll 仅适用于 Linux OS。 @@ -247,8 +253,9 @@ epoll 对多线程编程更加友好,一个线程调用了 epoll_wait() 另一 // The function argument is ignored (it was not before, but now it is), so put your favorite number here int pollingfd = epoll_create( 0xCAFE ); -if ( pollingfd < 0 ) - // report error +if ( pollingfd < 0 ) { + // report error +} // Initialize the epoll structure in case more members are added in future struct epoll_event ev = { 0 }; @@ -261,8 +268,9 @@ ev.data.ptr = pConnection1; ev.events = EPOLLIN | EPOLLONESHOT; // Add the descriptor into the monitoring list. We can do it even if another thread is // waiting in epoll_wait - the descriptor will be properly added -if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev ) != 0 ) +if ( epoll_ctl( pollingfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev ) != 0 ) { // report error +} // Wait for up to 20 events (assuming we have added maybe 200 sockets before that it may happen) struct epoll_event pevents[ 20 ]; @@ -270,10 +278,12 @@ struct epoll_event pevents[ 20 ]; // Wait for 10 seconds, and retrieve less than 20 epoll_event and store them into epoll_event array int ready = epoll_wait( pollingfd, pevents, 20, 10000 ); // Check if epoll actually succeed -if ( ret == -1 ) +if ( ready == -1 ) { // report error and abort -else if ( ret == 0 ) +} +else if ( ready == 0 ) { // timeout; no event detected +} else { // Check if any events detected @@ -337,3 +347,4 @@ poll 没有最大描述符数量的限制,如果平台支持并且对实时性 - [poll vs select vs event-based](https://daniel.haxx.se/docs/poll-vs-select.html) - [select / poll / epoll: practical difference for system architects](http://www.ulduzsoft.com/2014/01/select-poll-epoll-practical-difference-for-system-architects/) - [Browse the source code of userspace/glibc/sysdeps/unix/sysv/linux/ online](https://code.woboq.org/userspace/glibc/sysdeps/unix/sysv/linux/) +- [epoll(7) — Linux manual page](https://man7.org/linux/man-pages/man7/epoll.7.html) diff --git a/notes/pics/io-async.png b/notes/pics/io-async.png new file mode 100644 index 00000000..728ab3a7 Binary files /dev/null and b/notes/pics/io-async.png differ diff --git a/notes/pics/io-blocking.png b/notes/pics/io-blocking.png new file mode 100644 index 00000000..d2d0f503 Binary files /dev/null and b/notes/pics/io-blocking.png differ diff --git a/notes/pics/io-compare.png b/notes/pics/io-compare.png new file mode 100644 index 00000000..470432bc Binary files /dev/null and b/notes/pics/io-compare.png differ diff --git a/notes/pics/io-multiplex.png b/notes/pics/io-multiplex.png new file mode 100644 index 00000000..f00a310b Binary files /dev/null and b/notes/pics/io-multiplex.png differ diff --git a/notes/pics/io-nonblocking.png b/notes/pics/io-nonblocking.png new file mode 100644 index 00000000..1c65449e Binary files /dev/null and b/notes/pics/io-nonblocking.png differ diff --git a/notes/pics/io-signal.png b/notes/pics/io-signal.png new file mode 100644 index 00000000..b700a40b Binary files /dev/null and b/notes/pics/io-signal.png differ diff --git a/tools/diagram/src/io-async.svg b/tools/diagram/src/io-async.svg new file mode 100644 index 00000000..a2b50970 --- /dev/null +++ b/tools/diagram/src/io-async.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + 异步 I/O + + 应用进程 + + 内核 + + + + ① 等待数据 + 例如等对方发来 hello + + ② 复制到用户缓冲区 + 数据到达 + + aio_read(fd, buf, 1024) + + 立即返回 + + 继续执行其它代码 + + 完成通知:buf 中已经是 hello + 两个阶段都由内核完成,进程收到通知时数据已经在自己的缓冲区中。Linux 上实际的异步 I/O 多用 io_uring。 + diff --git a/tools/diagram/src/io-blocking.svg b/tools/diagram/src/io-blocking.svg new file mode 100644 index 00000000..7233fcea --- /dev/null +++ b/tools/diagram/src/io-blocking.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + 阻塞式 I/O + + 应用进程 + + 内核 + + + + ① 等待数据 + 例如等对方发来 hello + + ② 复制到用户缓冲区 + 数据到达 + + recvfrom(fd, buf, 1024) + + 两个阶段都阻塞 + + 返回 5(读到 hello) + 本机测试:对方 300 ms 后才发送,recv 阻塞 0.31 秒后返回。等待期间线程不占用 CPU,但也不能做别的事。 + diff --git a/tools/diagram/src/io-compare.svg b/tools/diagram/src/io-compare.svg new file mode 100644 index 00000000..c477b96b --- /dev/null +++ b/tools/diagram/src/io-compare.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + 五种 I/O 模型的比较 + ① 等待数据 + ② 复制数据 + + 阻塞式 + + 阻塞 + + 阻塞 + 非阻塞式 + + 反复轮询 + + 阻塞 + I/O 复用 + + 阻塞在 select + + 阻塞 + 信号驱动 + + 不阻塞 + + 阻塞 + 异步 + + 不阻塞 + + 不阻塞 + + 同步 I/O:第二阶段由进程自己 recvfrom,都会阻塞 + 异步 I/O + 红色表示进程阻塞,浅青色表示进程可以做别的事。前四种的区别只在第一阶段。 + diff --git a/tools/diagram/src/io-multiplex.svg b/tools/diagram/src/io-multiplex.svg new file mode 100644 index 00000000..962b47f5 --- /dev/null +++ b/tools/diagram/src/io-multiplex.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + I/O 复用 + + 应用进程 + + 内核 + + + + ① 等待数据 + 例如等对方发来 hello + + ② 复制到用户缓冲区 + 数据到达 + + select([fd1, fd2, …]) + + 阻塞在 select + + fd1 可读 + + recvfrom(fd1) + + 复制时阻塞 + + 返回 5 + 和阻塞式 I/O 一样要等,但一次可以等很多个连接,任何一个就绪就返回,一个线程就能处理大量连接。 + diff --git a/tools/diagram/src/io-nonblocking.svg b/tools/diagram/src/io-nonblocking.svg new file mode 100644 index 00000000..7d9b9a53 --- /dev/null +++ b/tools/diagram/src/io-nonblocking.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + 非阻塞式 I/O + + 应用进程 + + 内核 + + + + ① 等待数据 + 例如等对方发来 hello + + ② 复制到用户缓冲区 + 数据到达 + + recvfrom + + EAGAIN(没有数据) + + recvfrom + + EAGAIN ……共 6 次 + + recvfrom + + 轮询 + + 复制时阻塞 + + 返回 5 + 本机测试:每 50 ms 调用一次 recv,前 6 次返回 EAGAIN(没有数据),第 7 次读到 hello。轮询本身要消耗 CPU。 + diff --git a/tools/diagram/src/io-signal.svg b/tools/diagram/src/io-signal.svg new file mode 100644 index 00000000..4f989282 --- /dev/null +++ b/tools/diagram/src/io-signal.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + 信号驱动 I/O + + 应用进程 + + 内核 + + + + ① 等待数据 + 例如等对方发来 hello + + ② 复制到用户缓冲区 + 数据到达 + + sigaction(SIGIO, handler) + + 立即返回 + + 继续执行其它代码 + + SIGIO 信号 + + 在处理函数中 recvfrom + + 复制时阻塞 + + 返回 5 + 等待阶段不阻塞,数据到达时内核发信号通知“可以读了”,读取仍由进程自己完成。 +