网络IO

Posted imreW

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络IO相关的知识,希望对你有一定的参考价值。

(一)I/O到底是什么?

I/O 其实就是 input 和 output 的缩写,即输入/输出。

那输入输出啥呢?

比如我们用键盘来敲代码其实就是输入,那显示器显示图案就是输出,这其实就是 I/O。

而我们时常关心的磁盘 I/O 指的是硬盘和内存之间的输入输出。

读取本地文件的时候,要将磁盘的数据拷贝到内存中,修改本地文件的时候,需要把修改后的数据拷贝到磁盘中。

网络 I/O 指的是网卡与内存之间的输入输出。

当网络上的数据到来时,网卡需要将数据拷贝到内存中。当要发送数据给网络上的其他人时,需要将数据从内存拷贝到网卡里。

那为什么都要跟内存交互呢?

我们的指令最终是由 CPU 执行的,究其原因是 CPU 与内存交互的速度远高于 CPU 和这些外部设备直接交互的速度。

因此都是和内存交互,当然假设没有内存,让 CPU 直接和外部设备交互,那也算 I/O。

总结下:I/O 就是指内存与外部设备之间的交互(数据拷贝)。

(二)为什么网络 I/O 会被阻塞?

其实了解了 Socket 的通讯内幕就能回答这个问题。

详细来看这个问题要从建连和通讯涉及到个各个方法来入手,分别是 accept、connect、read、write 。

我们慢慢分析下。

1.创建 socket

首先服务端需要先创建一个 socket。在 Linux 中一切都是文件,那么创建的 socket 也是文件,每个文件都有一个整型的文件描述符(fd)来指代这个文件。

int socket(
int domain, //这个参数用于选择通信的协议族,比如选择 IPv4 通信,还是 IPv6 通信等等
int type, //选择套接字类型,可选字节流套接字、数据报套接字等等。
int protocol);//指定使用的协议。

这个 protocol 通常可以设为 0 ,因为由前面两个参数可以推断出所要使用的协议。

比如socket(AF_INET, SOCK_STREAM, 0);,表明使用 IPv4 ,且使用字节流套接字,可以判断使用的协议为 TCP 协议。

这个方法的返回值为 int ,其实就是创建的 socket 的 fd

2.bind

现在我们已经创建了一个 socket,但现在还没有地址指向这个 socket。

众所周知,服务器应用需要指明 IP 和端口,这样客户端才好找上门来要服务,所以此时我们需要指定一个地址和端口来与这个 socket 绑定一下

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数里的 sockfd 就是我们创建的 socket 的文件描述符,执行了 bind 参数之后我们的 socket 距离可以被访问又更近了一步。

3.listen

 

从经典网络IO模型到新异步IO框架io_uring《重制版》

网络IO模型

网络IO涉及用户空间内核空间,一般会经历两个阶段:

  • 一阶段等待数据准备就绪,即等待网络数据被copy到内核缓冲区(wait for data)
  • 二阶段将数据从内核缓冲区copy到用户缓冲区(copy data from kernel to user)

上述数据准备就绪可理解为socket中有数据到来,根据以上两阶段的不同,出现多种网络IO模型,接下来将根据这两阶段来进行分析。

阻塞IO(Blocking IO)

linuxsocket默认blocking,从下图可以看出,用户进程全程阻塞直到两阶段完成,即,一阶段等待数据会阻塞,二阶段将数据从内核copy到用户空间也会阻塞,只有copy完数据后内核返回,用户进程才会解除阻塞状态,重新运行。

结论:阻塞IO,两阶段都阻塞。

io_blocking

非阻塞IO (Non-blocking IO)

可使用fcntlsocket设置为NON-BLOCKING(fcntl(fd, F_SETFL, O_NONBLOCK);),使其变为非阻塞。如下图,用户进程recvfrom时,如果没有数据,则直接返回,因此一阶段不会阻塞用户进程。但是,用户进程需要不断的询问内核数据是否准备好(会造成CPU空转浪费资源,因此很少使用)。当数据准备好时,用户进程会阻塞直到数据从内核空间copy到用户空间完成(二阶段),内核返回结果。

**结论:**非阻塞IO一阶段不阻塞,二阶段阻塞。

从经典网络IO模型到新异步IO框架io_uring《重制版》
nonblocking_io

图中recvfrom 返回值含义:

  • error of  EWOULDBLOCK, 无数据
  • 大于0,接收数据完毕,返回值即收到的字节数
  • 等于0,连接已经正常断开
  • 等于-1,errno为 EAGAIN表示recv操作未完成,否则,表示recv操作遇到系统错误errno.

IO多路复用 (IO multiplexing-select/poll/epoll)

也称为事件驱动IO(event driven IO),通过使用select/poll/epoll系统调用,可在单个进程/线程中同时监听多个网络连接的socket fd,一旦有事件触发则进行相应处理,其中select/poll/epoll本身是阻塞的。(IO多路复用后面会用专门文章来详细讲解)

**结论:**两阶段都处于阻塞状态,优点是单个线程可同时监听和处理多个网络连接

从经典网络IO模型到新异步IO框架io_uring《重制版》
io_multiplexing

如果连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟更大。因为前者需要两个系统调用(select/epoll + read),而后者只有一个(read)。但是在连接数很多的情况下,select/epoll的优势就凸显出来了。(高效事件驱动模型libevent,libev库)

信号驱动IO (signal driven IO, SIGIO)

通过sigaction系统调用,建立起signal-driven I/O的socket,并绑定一个信号处理函数sigaction不会阻塞,立即返回。

当数据准备好,内核就为进程产生一个SIGIO信号,随后在信号处理函数中调用recvfrom接收数据。

**结论:**一阶段不阻塞,二阶段阻塞

从经典网络IO模型到新异步IO框架io_uring《重制版》
sigio

以上四种模型都有一个共同点二阶段阻塞,也就是在真正IO操作(recvfrom)的时候需要用户进程参与,因此以上四种模型均称为同步IO模型

异步IO (Asynchronous IO)

POSIX中提供了异步IO的接口aio_readaio_write,如下图,内核收到用户进程的aio_read之后会立即返回,不会阻塞,aio_read会给内核传递文件描述符缓冲区指针缓冲区大小文件偏移等;当数据准备好,内核直接将数据copy到用户空间,copy完后给用户进程发送一个信号,进行用户数据异步处理(aio_read)。因此,异步IO中用户进程是不需要参与数据从内核空间copy到用户空间这个过程的,也即二阶段不阻塞

结论:两阶段都不阻塞

从经典网络IO模型到新异步IO框架io_uring《重制版》
aysnchronous_io

上述五种IO模型对比

从上述分析可以得知,阻塞和非阻塞的区别在于内核数据还没准备好时,用户进程是否会阻塞(一阶段是否阻塞);同步与异步的区别在于当数据从内核copy到用户空间时,用户进程是否会阻塞/参与(二阶段是否阻塞)。以下为五种IO模型的对比图,可以清晰看到各种模型各个阶段的阻塞情况。

five_ios

以下为Richard Stevens对同步和异步IO的描述,可以把I/O operation是否阻塞看作为两者的区别。

POSIX defines these two terms as followers:

  • A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;

  • An asynchronous I/O operation does not cause the requesting process to be blocked;

还有一个概念需要区分:异步IO和IO异步操作,IO异步操作其实是属于同步IO模型。

io_uring

POSIX中提供的异步IO接口aio_readaio_write性能一般,几乎形同虚设,很少被用到,性能不如Epoll等IO多路复用模型。

Linux 5.1引入了一个重大feature:io_uring,由block IO大神Jens Axboe开发,这意味着Linux native aio的时代即将称为过去,io_uring异步IO新时代即将开启

以下贴出Jens Axboe的测试数据

io_uring_data

从以上数据看出,在非Polling模式下,io_uring性能提升不大,但是polling模式下,io_uring性能远远超出libaio,并接近spdk.

io_uring围绕高效进行设计,采用一对共享内存ringbuffer用于应用和内核间通信,避免内存拷贝和系统调用(感觉这应该是io_uring最精髓的地方):

  • 提交队列(SQ):应用是IO提交的生产者,内核为消费者
  • 完成队列(CQ):内核是完成事件的生产者,应用是消费者

io_uring系统调用相关接口

// 1. 初始化阶段
// The io_uring_setup() system call sets up a submission queue (SQ) and completion queue (CQ)
// returns a file descriptor which can be used to perform subsequent operations on the io_uring instance
// The submission and completion queues are shared between userspace and the kernel
// which eliminates the need to copy data when initiating and completing I/O
// 其中SQ, CQ为ringbuffer.
// io_setup返回一个fd,应用程序使用这个fd进行mmap,和kernel共享一块内存
int io_uring_setup(u32 entries, struct io_uring_params *p);
// IO提交的做法是找到一个空闲的 SQE,根据请求设置 SQE,并将这个 SQE 的索引放到 SQ 中
// SQ 是一个典型的 RingBuffer,有 head,tail 两个成员,如果 head == tail,意味着队列为空。
// SQE 设置完成后,需要修改 SQ 的 tail,以表示向 RingBuffer 中插入一个请求。
/* 
 * initiate and/or complete asynchronous I/O 
 * io_uring_enter() is used to initiate and complete I/O using the shared submission and completion 
 * queues setup by a call to io_uring_setup(2). A single call can both submit new I/O and wait for 
 * completions of I/O initiated by this call or previous calls to io_uring_enter(). 
 */

int io_uring_enter(unsigned int fd, unsigned int to_submit,
                unsigned int min_complete, unsigned int flags,
                sigset_t *sig)
;
/*
* register files or user buffers for asynchronous I/O 
*  The io_uring_register() system call registers user buffers or files for use in an io_uring(7) instance 
* referenced by fd. Registering files or user buffers allows the kernel to take long term references to 
* internal data structures or create long term mappings of application memory, greatly reducing * per-I/O overhead.
*/

int io_uring_register(unsigned int fd, unsigned int opcode,
                void *arg, unsigned int nr_args)

liburing

为方便使用,Jens Axboe还开发了一套liburing库,用户不必了解诸多细节,简单example如下

/* setup io_uring and do mmap */
io_uring_queue_init(ENTRIES, &ring, 0);
/* get an sqe and fill in a READV operation */
sqe = io_uring_get_sqe(&ring);
io_uring_prep_readv(sqe, fd, &iovec, 1, offset);
/* tell the kernel we have an sqe ready for consumption */
io_uring_submit(&ring);
/* wait for the sqe to complete */
io_uring_wait_cqe(&ring, &cqe);
/* read and process cqe event */
io_uring_cqe_seen(&ring, cqe);
/* tear down */
io_uring_queue_exit(&ring);

通过全新的设计,共享内存,IO 过程不需要系统调用,由内核完成 IO 的提交, 以及 IO completion polling 机制,实现了高IOPS,高 Bandwidth。相比 kernel bypass,这种 native 的方式显得友好一些。

参考

《UNIX Network Programming Volume.1.3rd.Edition》 --- Richard Stevens

io_uring1

io_uring2


以上是关于网络IO的主要内容,如果未能解决你的问题,请参考以下文章

从经典网络IO模型到新异步IO框架io_uring《重制版》

从经典网络IO模型到新异步IO框架io_uring《重制版》

网络IO模型

网络IO模型

Linux网络IO精华指南

IO流基础网络IO