网络I/O,可以理解为网络上的数据流。通常我们会基于socket与远端建立一条TCP或者UDP通道,然后进行读写。单个socket时,使用一个线程即可高效处理;然而如果是10K个socket连接,或者更多,我们如何做到高性能处理?
基本概念介绍
网络I/O的读写过程
linux下的五种网络I/O模型
多路复用I/O深入理解一波
Reactor模型
Proacotr模型
关注公众号,一起交流 :潜行前行 基本概念介绍进程(线程)切换
所有系统都有调度进程的能力,它可以挂起一个当前正在运行的进程,并恢复之前挂起的进程
进程(线程)的阻塞
运行中的进程,有时会等待其他事件的执行完成,比如等待锁,请求I/O的读写;进程在等待过程会被系统自动执行阻塞,此时进程不占用CPU
文件描述符
在Linux,文件描述符是一个用于表述指向文件引用的抽象化概念,它是一个非负整数。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符
linux信号处理
Linux进程运行中可以接受来自系统或者进程的信号值,然后根据信号值去运行相应捕捉函数;信号相当于是硬件中断的软件模拟
在零拷贝机制篇章已介绍过 用户空间和内核空间和缓冲区,这里就省略了
网络IO的读写过程当在用户空间发起对socket套接字的读操作时,会导致上下文切换,用户进程阻塞(R1)等待网络数据流到来,从网卡复制到内核;(R2)然后从内核缓冲区向用户进程缓冲区复制。此时进程切换恢复,处理拿到的数据
这里我们给socket读操作的第一阶段起个别名R1,第二阶段称为R2
当在用户空间发起对socket的send操作时,导致上下文切换,用户进程阻塞等待(1)数据从用户进程缓冲区复制到内核缓冲区。数据copy完成,此时进程切换恢复
linux五种网络IO模型 阻塞式I/O (blocking IO) ssize_t recvfrom(int sockfd,void *buf,size_t len,unsigned int flags, struct sockaddr *from,socket_t *fromlen);最基础的I/O模型就是阻塞I/O模型,也是最简单的模型。所有的操作都是顺序执行的
阻塞IO模型中,用户空间的应用程序执行一个系统调用(recvform),会导致应用程序被阻塞,直到内核缓冲区的数据准备好,并且将数据从内核复制到用户进程。最后进程才被系统唤醒处理数据
在R1、R2连续两个阶段,整个进程都被阻塞
非阻塞式I/O (nonblocking IO)非阻塞IO也是一种同步IO。它是基于轮询(polling)机制实现,在这种模型中,套接字是以非阻塞的形式打开的。就是说I/O操作不会立即完成,但是I/O操作会返回一个错误代码(EWOULDBLOCK),提示操作未完成
轮询检查内核数据,如果数据未准备好,则返回EWOULDBLOCK。进程再继续发起recvfrom调用,当然你可以暂停去做其他事
直到内核数据准备好,再拷贝数据到用户空间,然后进程拿到非错误码数据,接着进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态
进程在R2阶段阻塞,虽然在R1阶段没有被阻塞,但是需要不断轮询
多路复用I/O (IO multiplexing)一般后端服务都会存在大量的socket连接,如果一次能查询多个套接字的读写状态,若有任意一个准备好,那就去处理它,效率会高很多。这就是“I/O多路复用”,多路是指多个socket套接字,复用是指复用同一个进程
linux提供了select、poll、epoll等多路复用I/O的实现方式
select或poll、epoll是阻塞调用
与阻塞IO不同,select不会等到socket数据全部到达再处理,而是有了一部分socket数据准备好就会恢复用户进程来处理。怎么知道有一部分数据在内核准备好了呢?答案:交给了系统系统处理吧
进程在R1、R2阶段也是阻塞;不过在R1阶段有个技巧,在多进程、多线程编程的环境下,我们可以只分配一个进程(线程)去阻塞调用select,其他线程不就可以解放了吗
信号驱动式I/O (SIGIO)需要提供一个信号捕捉函数,并和socket套接字关联;发起sigaction调用之后进程就能解放去处理其他事
当数据在内核准备好后,进程会收到一个SIGIO信号,继而中断去运行信号捕捉函数,调用recvfrom把数据从内核读取到用户空间,再处理数据
可以看出用户进程是不会阻塞在R1阶段,但R2还是会阻塞等待
异步IO (POSIX的aio_系列函数)