颤抖开篇,从php角度谈谈IO模型(BIO)
创始人
2024-05-04 17:58:51
0

颤抖开篇,从php角度谈谈IO模型(BIO)

IO 是什么?

在计算机系统中I/O就是输入(input)和输出(Output)的意思。针对不同的操作对象,可以划分为磁盘I/O模型,网络I/O模型,内存映射I/O,Direct I/O、数据库I/O等,只要具有输入输出类型的交互系统都可以认为是I/O系统,也可以说是整个操作系统数据交换与人机交互的通道,这个概念与选用的开发语言没有关系,是一个通用概念。

谈谈 (阻塞)

在学习 IO 中必须要搞懂的几个概念:(阻塞,非阻塞)与(同步,异步)

本篇文章只介绍阻塞,其余几个概念将在后面篇章中挨个介绍学习。

在了解阻塞IO前,我们先看看网络数据包接收流程,在这里我们可以将整个流程总结为两个阶段:

图片
数据接收阶段.png

  • 数据准备阶段:
    在这个阶段,网络数据包到达网卡,通过DMA的方式将数据包拷贝到内存中,然后经过硬中断,软中断,接着通过内核线程ksoftirqd经过内核协议栈的处理,最终将数据发送到内核Socket的接收缓冲区中。

  • 数据拷贝阶段: 当数据到达内核Socket的接收缓冲区中时,此时数据存在于内核空间中,需要将数据拷贝到用户空间中,才能够被应用程序读取。

阻塞

阻塞主要发生在第一阶段:数据准备阶段。

当应用程序发起系统调用 read 读操作时,线程从用户态转为内核态,读取内核Socket的接收缓冲区中的网络数据。

如果这时内核Socket的接收缓冲区没有数据,那么线程就会一直等待,直到Socket接收缓冲区有数据为止。随后将数据从内核空间拷贝到用户空间,系统调用 read 返回。

在这里插入图片描述

从图中我们可以看出:阻塞的特点是在第一阶段和第二阶段都会等待。

阻塞IO(BIO)

在这里插入图片描述

经过前一小节对阻塞这个概念的介绍,相信大家可以很容易理解阻塞IO的概念和过程。

既然这小节我们谈的是IO,那么下边我们来看下在阻塞IO模型下,网络数据的读写过程。

阻塞读

当用户线程发起read系统调用,用户线程从用户态切换到内核态,在内核中去查看Socket接收缓冲区是否有数据到来。

Socket接收缓冲区中有数据,则用户线程在内核态将内核空间中的数据拷贝到用户空间,系统IO调用返回。

Socket接收缓冲区中无数据,则用户线程让出CPU,进入阻塞状态。当数据到达Socket接收缓冲区后,内核唤醒阻塞状态中的用户线程进入就绪状态,随后经过CPU的调度获取到CPU quota进入运行状态,将内核空间的数据拷贝到用户空间,随后系统调用返回。

阻塞写

当用户线程发起send系统调用时,用户线程从用户态切换到内核态,将发送数据从用户空间拷贝到内核空间中的Socket发送缓冲区中。

当Socket发送缓冲区能够容纳下发送数据时,用户线程会将全部的发送数据写入Socket缓冲区,然后执行在网络包发送流程,然后返回。

当Socket发送缓冲区空间不够,无法容纳下全部发送数据时,用户线程让出CPU,进入阻塞状态,直到Socket发送缓冲区能够容纳下全部发送数据时,内核唤醒用户线程,执行后续发送流程。

阻塞IO模型下的写操作做事风格比较硬刚,非得要把全部的发送数据写入发送缓冲区才肯善罢甘休。

由于BIO 的阻塞特性,想让BIO 同时为多个客户端服务,每个请求都需要被一个独立的线程处理。一个线程在同一时刻只能与一个连接绑定。来一个请求,服务端就需要创建一个线程用来处理请求。

阻塞IO模型

在这里插入图片描述

在早期 Java 中 BIO 的实现就是一个客户端连接创建一个线程来处理请求,由于php 对多线程支持不是特别好,在php 中如何实现BIO呢? 咱们先来个例子

// 创建 ipv4 tcp socket
$sockfd = socket_create(AF_INET, SOCK_STREAM, 0);
if (!is_resource($sockfd)) {fprintf(STDOUT, "socket create fail:%s\n", socket_strerror(socket_last_error($this->_sockfd)));}
// 绑定地址 端口
socket_bind($sockfd,'0.0.0.0',6379);
// 监听 socket
socket_listen($sockfd,10);while (1){// 第一个阻塞函数   获取客户端连接,没有客户端连接会一直阻塞,阻塞状态的进程是不会占据CPU的$connfd = socket_accept($sockfd);if(empty($connfd)){continue;}// 第二个阻塞函数 , 没有数据会一直阻塞,直到客户端发送数据过来,才往下执行$buff = socket_read($connfd,1024);fprintf(STDOUT,"%s",$buff);// 向客户端发送一个helloworld$msg = "helloworld\r\n";// 向客户端发送数据socket_write($connfd, $msg, strlen( $msg ) );echo time().' : a new client'.PHP_EOL;// 服务端 主动断开客户端连接socket_close($connfd);}
// 关闭 监听 socket
socket_close($sockfd);

简单解析一下上述代码来说明一下tcp socket服务器的流程:

  • 首先,根据协议族(或地址族)、套接字类型以及具体的的某个协议来创建一个socket。
  • 第二,将上一步创建好的socket绑定(bind)到一个ip:port上。
  • 第三,开启监听linten。
  • 第四,使服务器代码进入无限循环不退出,当没有客户端连接时,程序阻塞在accept
    上,有连接进来时才会往下执行,接着阻塞在read 上,客户端发送数据才会往下执行 ,然后再次循环下去,为客户端提供持久服务。

上面这个案例中,有两个很大的缺陷:

  1. 一次只可以为一个客户端提供服务,如果第一个客户端连接没有发送数据,导致一直阻塞在 read 上,这时有第二个客户端来连接,那么第二个客户端就必须要等待第一个连接发送数据才行。
  2. 很容易受到攻击,造成拒绝服务。

分析了上述问题后,又联想到了前面说的多进程,那我们可以在accpet到一个请求后就fork一个子进程来处理这个客户端的请求,这样当accept了第二个客户端后再fork一个子进程来处理第二个客户端的请求,这样问题不就解决了吗?

// 创建 ipv4 tcp socket
$sockfd = socket_create(AF_INET, SOCK_STREAM, 0);
if (!is_resource($sockfd)) {fprintf(STDOUT, "socket create fail:%s\n", socket_strerror(socket_last_error($this->_sockfd)));}
// 绑定地址 端口
socket_bind($sockfd,'0.0.0.0',6379);
// 监听 socket
socket_listen($sockfd,10);while (1){// 第一个阻塞函数   获取客户端连接,没有客户端连接会一直阻塞,阻塞状态的进程是不会占据CPU的$connfd = socket_accept($sockfd);if(empty($connfd)){continue;}$pid = pcntl_fork();if($pid == 0){// 第二个阻塞函数 , 没有数据会一直阻塞,直到客户端发送数据过来,才往下执行$buff = socket_read($connfd,1024);fprintf(STDOUT,"%s",$buff);// 向客户端发送一个helloworld$msg = "helloworld\r\n";socket_write($connfd, $msg, strlen( $msg ) );// 休眠5秒钟,可以用来观察时候可以同时为多个客户端提供服务echo time().' : a new client'.PHP_EOL;socket_close($connfd);}}
// 关闭 监听 socket
socket_close($sockfd);

通过 fork 多进程的确可以同时服务多个客户端,但当客户端请求的并发量突然增大时,服务端在一瞬间就会创建出大量的进程,而创建进程是需要系统资源开销的,这样一来就会一瞬间占用大量的系统资源。

如果客户端创建好连接后,但是一直不发数据,通常大部分情况下,网络连接也并不总是有数据可读,那么在空闲的这段时间内,服务端进程就会一直处于阻塞状态,无法干其他的事情。CPU也无法得到充分的发挥,同时还会导致大量进程切换的开销。

编写一个例子模拟大量客户端连接

for ($i = 10000; $i < 65000; $i++){// 创建 socket $_sockfd = socket_create(AF_INET, SOCK_STREAM, 0);// 绑定客户端 ip地址 端口socket_bind($_sockfd,'192.168.0.102',$i);// 连接服务端if(socket_connect($_sockfd,'111.230.247.213',6379)){fprintf(STDOUT,"客户端连接成功 ip=%s\n",'192.168.0.102:'.$i);}
}

执行客户端模拟连接脚本
在这里插入图片描述
服务端的确可以同时处理很多请求,但是也创建了大量进程,消耗大量系统资源与同时还会导致大量进程切换的开销。
在这里插入图片描述
在这里插入图片描述

所以,我们就再次提出增进型解决方案。我们可以预估一下业务量,然后在服务启动的时候就fork出固定数量的子进程,每个子进程处于无限循环中并阻塞在 accept 上,当有客户端连接挤进来就处理客户请求,当处理完成后仅仅关闭连接但本身并不销毁,而是继续等待下一个客户端的请求。这样,不仅避免了进程反复fork销毁巨大资源浪费,而且通过固定数量的子进程来保护系统不会因无限fork而崩溃,其实这就是资源池化解决方案。

$sockfd = socket_create(AF_INET, SOCK_STREAM, 0);
if (!is_resource($sockfd)) {fprintf(STDOUT, "socket create fail:%s\n", socket_strerror(socket_last_error($this->_sockfd)));}
socket_bind($sockfd,'0.0.0.0',6379);
socket_listen($sockfd,10);// 按照数量fork出固定个数子进程
for( $i = 1; $i <= 10; $i++ ){$pid = pcntl_fork();if( 0 == $pid ){cli_set_process_title('phpserver worker process');while( true ){$conn_socket = socket_accept( $sockfd );$msg = "helloworld\r\n";socket_write($conn_socket, $msg, strlen( $msg ) );socket_close($conn_socket);}}
}
// 父进程回收子进程退出,回收资源
while( true ){$pid = pcntl_wait($status);if($pid > 0){fprintf(STDOUT,"PID=%d 子进程退出了",$pid);}
}
socket_close($sockfd );

启动php BIO 服务端 ,通过 ps -ef|grep phpserver 命令查看阻塞在 socket_accept 等待处理客户端连接的 10 个子进程

在这里插入图片描述

预先创建10个子进程 处于等待服务状态,再同一个时刻可以同时为10个客户端提供服务。

适用场景

基于以上阻塞IO模型的特点,该模型只适用于连接数少,并发度低的业务场景。

比如公司内部的一些管理系统,通常请求数在100个左右,使用阻塞IO模型还是非常适合的。而且性能也不错。

文章部分内容参考文献

  1. https://mp.weixin.qq.com/s/zAh1yD5IfwuoYdrZ1tGf5Q

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
客厅放八骏马摆件可以吗(家里摆... 今天给各位分享客厅放八骏马摆件可以吗的知识,其中也会对家里摆八骏马摆件好吗进行解释,如果能碰巧解决你...
苏州离哪个飞机场近(苏州离哪个... 本篇文章极速百科小编给大家谈谈苏州离哪个飞机场近,以及苏州离哪个飞机场近点对应的知识点,希望对各位有...