颤抖开篇,从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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值