linux通信模型

linux的通信模型,大致可以划分成网络IO模型和进/线程模型两大部分。

在了解这些模型前,我们先了解下一些前置知识。

  • 进程的状态 processLiving
  • 阻塞调用与非阻塞调用
    • 阻塞调用是指调用系统函数时,没得到结果的时候,进程是否会被阻塞。失效性不足。
    • 非阻塞调用是指不能立刻得到结果之前,这个进程不会被阻塞,时间片内一直占用cpu资源。
    • 阻塞与非阻塞这两个概念讨论的是发起系统调用的对象(例如用户态进程)。
  • 进程的IO事件通常包括两个不同的阶段
    • 等待数据准备好
    • 从内核向进程复制数据

UNIX下的五种IO模型

  1. 阻塞式I/O模型

这种IO模型,只能使用ppc或者tpc。并发连接多的时候,对系统资源占用比较多。 2. ##### 非阻塞式I/O模型 这种IO模型,有点浪费cpu资源,因为它会做大量的无用轮训。 3. ##### IO多路复用模型 这种IO模型是非阻塞同步的。select,poll,epoll就是实现这种IO多路复用思想的内核机制。 4. ##### 信号驱动式IO模型 5. ##### 异步IO模型

下面重点讲述下IO多路复用模型中的select机制和epoll的工作原理。

select

看一个小demo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php
//创建一个tcp socket服务器
$host = '0.0.0.0';
$port = 8888;
$tcp_socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
//绑定IP端口
socket_bind($tcp_socket, $host, $port);
//启动监听
socket_listen($tcp_socket);
//select也要监听listen_socket上发生事件,简单demo只针对read事件。
$client = [ $tcp_socket ];
$write = [];
$exp = [];
// 开始死循环
while (true) {
    //初始化&&更新需要监听的socket集合
    $read = $client;
    if (socket_select($read, $write, $exp, null) > 0) {
        // 判断listen_socket有没有发生变化
        if (in_array($tcp_socket, $read)) {
            // 将客户端socket加入到client数组中
            $client_socket = socket_accept($tcp_socket);
            $client[] = $client_socket;
            // 然后去重
            $key = array_search($tcp_socket, $read);
            unset($read[ $key ]);
        }
        // 查看是否有其他read事件。
        if (count($read) > 0) {
            foreach ($read as $socket_item) {
                // 从可读取的fd中读取出来数据内容
                $content = socket_read($socket_item, 2048);
                // 循环client数组,将内容发送给其余所有客户端
                foreach ($client as $client_socket) {
                    // 只发给其他客户端client。
                    if ($client_socket != $tcp_socket && $client_socket != $socket_item) {
                        socket_write($client_socket, $content, strlen($content));
                    }
                }
            }
        }
    }
}
epoll

epoll工作原理图大致如下:

其中两个函数比较关键

** ep_ptable_queue_proc **

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
                 poll_table *pt)
{
    struct epitem *epi = ep_item_from_epqueue(pt);
    struct eppoll_entry *pwq;  //[小节2.4.7]

    if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {
        //初始化回调方法
        init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
        pwq->whead = whead;
        pwq->base = epi;
        //将ep_poll_callback放入等待队列whead
        add_wait_queue(whead, &pwq->wait);
        //将llink 放入epi->pwqlist的尾部
        list_add_tail(&pwq->llink, &epi->pwqlist);
        epi->nwait++;
    } else {
        epi->nwait = -1; //标记错误发生
    }
}

这个函数主要是实现关联epitem和具体的socket文件,而且注册了回调函数ep_poll_callback。

** ep_poll_callback **

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
    int pwake = 0;
    unsigned long flags;
    struct epitem *epi = ep_item_from_wait(wait);
    struct eventpoll *ep = epi->ep;

    spin_lock_irqsave(&ep->lock, flags);
     // 如果正在将事件传递给用户空间,我们就不能保持锁定
     //(因为我们正在访问用户内存,并且因为linux f_op-> poll()语义)。
     // 在那段时间内发生的所有事件都链接在ep-> ovflist中并在稍后重新排队。
    if (unlikely(ep->ovflist != EP_UNACTIVE_PTR)) {
        if (epi->next == EP_UNACTIVE_PTR) {
            epi->next = ep->ovflist;
            ep->ovflist = epi;
            if (epi->ws) {
                __pm_stay_awake(ep->ws);
            }
        }
        goto out_unlock;
    }

    //如果此文件已在就绪列表中,很快就会退出
    if (!ep_is_linked(&epi->rdllink)) {
        //将epi就绪事件 插入到ep就绪队列
        list_add_tail(&epi->rdllink, &ep->rdllist);
        ep_pm_stay_awake_rcu(epi);
    }

    // 如果活跃,唤醒eventpoll等待队列和 ->poll()等待队列
    if (waitqueue_active(&ep->wq))
        wake_up_locked(&ep->wq);  //当队列不为空,则唤醒进程
    if (waitqueue_active(&ep->poll_wait))
        pwake++;

out_unlock:
    spin_unlock_irqrestore(&ep->lock, flags);
    if (pwake)
        ep_poll_safewake(&ep->poll_wait);

    if ((unsigned long)key & POLLFREE) {
        list_del_init(&wait->task_list); //删除相应的wait
        smp_store_release(&ep_pwq_from_wait(wait)->whead, NULL);
    }
    return 1;
}

这个回调函数,主要作用于socket事件到达时触发。将该epitem存放于epoll_isntance的readylist链表中。

进程/线程模型

  • 单进程
  • 多进程
  • 多线程

Reactor模式

首先我们先看一下最传统的通信模型,pps和tps。即一个连接一个线程。

这种通信形态是由于阻塞型IO的先天不足决定的。弊端显而易见,并发连接低下。

为了应对高并发业务场景,reactor就出现了。本质上,reactor模式就是IO多路复用模型加上进/线程模型的组合。解决了传统通信模型的短板。 reactor设计模式的类图大致如下

reactor模式的大致工作流程大致如下。

其中servicehandle(master进程)和Eventhandle(woker进程)可以使用不同的进程模型进行组合, 理论上有Reactor有四种形式。由于多master单worker进程模式先天不足。所以有3种典型的实现:

  • 单servicehandle单线程(redis)
  • 单servicehandle多线程
  • 主从servicehandle多线程(nginx)