最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 大名鼎鼎的 IO 多路复用

    正文概述 转载于:掘金(夜深忽梦少年事丶)   2021-05-03   61

    什么是操作系统

    基础概念

    大名鼎鼎的 IO 多路复用

    你不需要自己写程序去访问键盘、硬盘等硬件。比如当你在 windows 上打开一个记事本,通过键盘上的各种按键,windows 帮你把字符写入到文件内。同时在你的 windows 电脑上,同时运行着 QQ、微信、邮件等不同的应用,每个应用会占用磁盘、内存、端口以及被分配 CPU 时间片。

    用户态和内核态

    大名鼎鼎的 IO 多路复用

    大名鼎鼎的 IO 多路复用

    Inter CPU 指令级别分别是 Ring0~Ring3,Ring0 级别最高,Ring3 级别最低。 在 Linux 系统中,Ring0 作为内核态,Ring3 作为用户态。

    假如 Linux 进程的有 4GB 地址空间,3G-4G 部分大家是共享的,是内核态的地址空间,这里存放在整个内核的代码和所有的内核模块,以及内核所维护的数据。用户运行一个程序,该程序所创建的进程开始是运行在用户态的,如果要执行文件操作,网络数据发送等操作,必须通过 writesend 等系统调用,这些系统调用会调用内核中的代码来完成操作,这时必须切换到 Ring0,然后进入 3GB-4GB 中的内核地址空间去执行这些代码完成操作,完成后,切换回 Ring3,回到用户态。这样,用户态的程序就不能随意操作内核地址空间,具有一定的安全保护作用。

    系统调用

    <?php
    $arr = file_get_contents('./tmp');
    print_r($arr);
    

    上面是一段很简单 php 代码,读取 tmp 文件的内容。由上文可知用户态访问磁盘必须使用系统调用,下面使用 strace 命令查看这个进程所使用的系统调用。

    大名鼎鼎的 IO 多路复用

    计算机网络模型

    大名鼎鼎的 IO 多路复用

    上图是 TCP/IP 五层模型(也有说是四层,链路层和物理层统称为数据链路层)。应用程序主要位于应用层,下面四层主要由内核来使用,为什么要这样设计?

    在你的电脑上,QQ、微信、邮件都可以对外部进行网络通信,这些进程在通信的时候,有很大一部分功能是一致的、重复的,因此代码不需要重复去开发。另外一个就是,网卡就那么一块,每个进程都是通过这块网卡向外发送数据,访问不同的服务器,那么如何管理好这个通用的资源,就是由操作系统内核来进行调度。

    大名鼎鼎的 IO 多路复用

    nc 命令可以指定与远程主机建立 TCP 连接(UDP 也可以),当我们与百度服务器建立 TCP 连接后,也就是说传输层及下层的协议,不需要我们来实现,而是由操作系统帮我们搞定,我们只需要实现应用层的协议即可。

    大名鼎鼎的 IO 多路复用

    大名鼎鼎的 IO 多路复用

    大名鼎鼎的 IO 多路复用

    网络 IO 模型

    BIO(Blocking IO)

    <?php
    $host = '127.0.0.1';
    $port = '19990';
    //创建 socket 
    //AF_INET  表示网络层协议 IPv4
    //SOCK_STREAM 表示传输层数据格式,TCP 协议基于字节流式套接字
    //SOL_TCP 表示传输层协议 TCP
    if (($socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) < 0) {
        echo "socket_create error: " . socket_strerror($socket) . PHP_EOL;
        exit();
    }
    
    //把 socket 绑定在一个 IP 和端口上
    if (socket_bind($socket, $host, $port) < 0) {
        echo "socket_bind error: " . socket_strerror($socket) . PHP_EOL;
        exit();
    }
    
    //监听由指定socket的所有连接
    if (socket_listen($socket, 256) < 0) {
        echo "socket_listen error: " . socket_strerror($socket) . PHP_EOL;
        exit();
    }
    echo "Start time:" . date('Y-m-d H:i:s') . PHP_EOL;
    echo "Listening at " . $host . ':' . $port . PHP_EOL;
    
    while (true) {
        //接收一个Socket连接
        if (($conn = socket_accept($socket)) < 0) {
            echo "socket_accept error: " . socket_strerror($conn) . PHP_EOL;
            break;
        } else {
            //连接进来了,fork 一个进程,处理该连接的消息
            if (pcntl_fork() == 0) {
                //发送到客户端
                $msg = "From server: tcp socket connect successful...\n";
                socket_write($conn, $msg, strlen($msg));
                while (true) {
                    // 获得客户端的输入
                    $buf = socket_read($conn, 2048);
                    // 把客户端输出打印到控制台
                    if ($buf === '') {
                        socket_close($conn);
                        exit(0);
                    } else {
                        echo "From Cilent:{$buf}\n";
                    }
                }
            }
        }
    }
    //关闭socket
    socket_close($socket);
    

    主进程负责等待接收 socket 连接,一旦有 socket 连接进入以后,fork 一个子进程,子进程会等待这个 socket 发消息,收到消息后就把消息内容打印到终端。下面使用 strace 查看这些进程的系统调用情况。

    运行 server 后我们能看到有三个进程,其中 1261 是主进程,1264 和 1267 是子进程

    大名鼎鼎的 IO 多路复用

    同时在当前目录能看到这三个进程所有系统调用输出的文件,分别查看几个进程的系统调用

    大名鼎鼎的 IO 多路复用

    大名鼎鼎的 IO 多路复用

    大名鼎鼎的 IO 多路复用

    大名鼎鼎的 IO 多路复用

    能够发现主进程阻塞在 accept 系统调用,因为会一直等待连接,子进程阻塞在 recvfrom,因为一直在等待 client 发消息。

    NIO (Nonblocking IO)

    NIO 表示非阻塞 IO,也就是我们希望不要再 accept 和 revfrom 两个系统调用上阻塞,如果没有连接/没有消息,就返回 false。

    //设置为非阻塞 IO
    socket_set_nonblock($socket);
    //已建立连接的 socket
    $activeConn = [];
    while (true) {
        //接收一个Socket连接,此时 accept 返回 false
        if (($conn = socket_accept($socket)) < 0) {
            echo "socket_accept error: " . socket_strerror($conn) . PHP_EOL;
            break;
        } else {
            //如果有连接进来,添加到 activeConn
            //如果没有连接进来,遍历 activeConn,依次读取每个 conn 的消息
            if ($conn) {
                //已经建立连接的 socket 设置非阻塞 IO
                socket_set_nonblock($conn);
                $msg = "From server: tcp socket connect successful...\n";
                socket_write($conn, $msg, strlen($msg));
                $activeConn[] = $conn;
            } else {
                if ($activeConn) {
                    while (true) {
                        foreach ($activeConn as $conn) {
                            // 获得客户端的输入,此时 read 也不是阻塞的了,返回 false
                            $buf = socket_read($conn, 2048);
                            if ($buf) {
                                echo "From Cilent:{$buf}\n";
                            }
                        }
                        break;
                    }
                }
            }
        }
    }
    

    由于阻塞式 IO 会依赖于进程数来解决连接数,所以我们可以使用非阻塞 IO,来同时判断是否有连接进来,以及进来的连接是否有数据,如果没有连接进来,也没有数据进来,会一直进行 accept/recvfrom 系统调用。

    当没有连接进来时,accept 系统调用不会阻塞,而是会一直返回 -1

    大名鼎鼎的 IO 多路复用

    当连接进来后,如果没有发消息,recvfrom 也不会一直阻塞,会一直返回 -1

    大名鼎鼎的 IO 多路复用

    IO多路复用

    select

    //需要通过 select 遍历的 socket 数组
    $socketArr = [$listenSocket];
    while (true) {
        // socket_select 一共四个参数
        // read 返回可读的 $socket 数组
        // write 返回可写的 $socket 数组
        // except 返回异常的 $socket 数组
        // tv_sec 设置 select 等待时间,null 表示当没有事件发生时阻塞在 select
        // https://www.php.net/manual/zh/function.socket-select
        $reads = $socketArr;
        // select 返回:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
        // 最后一个参数设置 null 表示阻塞在 select 处
        if (socket_select($reads, $writes, $excepts, null) > 0) {
            //进来则表示有事件发生了
            if (in_array($listenSocket, $reads)) {
                //如果是新连接进来了,那么就调用 accept
                $conn = socket_accept($listenSocket);
                $msg = "From server: tcp socket connect successful...\n";
                socket_write($conn, $msg, strlen($msg));
                //把新连接加到 socket 数组
                $socketArr[] = $conn;
                //把监听的 socket 删掉,那么剩下的全是已经建立连接的 socket
                $key = array_search($listenSocket, $reads);
                unset($reads[$key]);
            }
            if (!empty($reads)) {
                //如果已经建立连接的 socket 有数据返回,那么遍历依次读取数据
                foreach ($reads as $conn) {
                    $buf = socket_read($conn, 2048);
                    if ($buf === '') {
                        //当 buf === '' 表示 client 已经断开连接
                        //这个时候就要关闭这个 socket,并且从 socketArr 删除
                        socket_close($conn);
                        $key = array_search($conn, $socketArr);
                        unset($socketArr[$key]);
                    } else {
                        echo "From Cilent:{$buf}\n";
                    }
                }
            }
        }
    }
    

    开启服务

    大名鼎鼎的 IO 多路复用

    最后一行能看到,目前没有任何连接进来,也没有任何连接产生数据,当前阻塞在 select 系统调用处。细心的同学会发现,socket_select 我们传入四个参数,但是 select 系统调用有五个参数,第一个是php 底层扩展自动填入的。4 表示的是待测试的描述符个数,它的值是待测试的最大描述符加 1,这里隐藏了另外三个 FD,分别是 0(标准输入)1(标准输出)2(标准错误),当前监听的可读 socket 对应的 FD 是 [3],剩下两个则是可写的 FD 和 异常的 FD,最后一个则是设置 select 为阻塞状态。

    client 建立连接

    大名鼎鼎的 IO 多路复用

    开启一个连接,首先看这一行

    FD3 上有事件发生了(新连接进来了),那么 select 返回的可读 FDARR = [3],因为 FD3 是监听端口的 socket,所以下一句就是 accept

    然后又阻塞在了 select,因为进来新连接以后就没有事件产生了

    client 发送数据

    大名鼎鼎的 IO 多路复用

    我们发送一下数据,这个时候 select 监听到 FDARR 有数据产生,同样会返回有事件发生的 FD4,这个时候我们从 FD4 里调用 recvfrom,取出数据,随后又阻塞在 select 处

    使用 select 函数,通知内核挂起进程,当一个或多个 I/O 事件发生后,控制权返还给应用程序,由应用程序进行 I/O 事件的处理。

    描述符就绪条件

    通过上面的测试能发现,我们使用 select 系统调用,内核就能告诉我们哪些 FD 上有事件发生,那么我最大的疑惑就是,内核是怎么知道哪些 FD 有事件发生的?

    UNIX 网络编程中,有这么一段解释:

    书中的解释比较专业,我个人的理解是,不应该从应用程序的角度去思考,比如当 client 通过 TCP 连接发送数据,这个连接对应的 FD 就是可写的,这种思路其实不对。而是应该从套接字本身出发,select 检测套接字可写,完全是基于套接字本身的特性来说,当套接字本身的状态产生了变化,由此内核判断套接字是否可读可写

    select 的缺点

    select 这么牛逼,为什么高性能的 Application 都用 epoll?

    • 单个进程所打开的FD是有限制的,通过 FD_SETSIZE 设置,默认1024
    • 每次调用select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在fd很多时会很大
    • 对socket扫描时是线性扫描,采用轮询的方法,效率较低(高并发时)

    poll

    poll 解决了 select 对于 FD_SETSIZE 的限制,然而对于检测可读可写的 FD 依旧是放入全部 FD 集合,然后轮询每一个 FD,因此在高并发下效率依然不高

    缺点

    • 每次调用poll,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
    • 对socket扫描时是线性扫描,采用轮询的方法,效率较低(高并发时)

    epoll

    上文提到的 select 存储 fd 的数据结构为 bitmap,poll 存储 fd 的数据结构为 array,但是两者监听 fd 的状态都是采用轮询的方法,因此都需要做线性扫描,因此在高并发时,如果大量的 fd 集合中只有一个 fd 有事件产生,那么仍需要遍历整个集合

    epoll 存储 fd 的数据结构为红黑树,并且单独为有事件已经就绪的 fd 设置了一个存储结构——链表

    那么,这个准备就绪链表是怎么维护的呢?

    epoll 正是基于在 CPU 的软件中断上注册回调函数,从而能够知道哪一个 fd 状态发生改变,从而能够拿到一系列就绪的 fd 集合。因此,相比 select 和 poll,epoll 具有更高的性能以及更低的资源消耗,但是 epoll 只能工作在 linux 下

    selectpollepoll
    数据结构位图数组红黑树最大连接数1024无限制无限制fd拷贝每次调用select拷贝每次调用poll拷贝fd首次调用epoll_ctl拷贝,每次调用epoll_wait不拷贝工作效率O(n)O(n)O(1)

    github

    PHP IO-Multiplexing

    参考

    [1]Unix 环境高级编程

    [2]Unix 网络编程

    [3]韩天峰-PHP并发IO编程之路

    [4]彻底理解 IO多路复用

    [5]Socket 读写就绪条件

    [6]浅析CPU中断技术

    [7]谈谈epoll实现原理


    起源地 » 大名鼎鼎的 IO 多路复用

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元