并发异步同步阻塞非阻塞模型

10次阅读

共计 5178 个字符,预计需要花费 13 分钟才能阅读完成。

网络从网卡到线程过程

1. 一个以太网接口接收发送到它的单播地址和以太网广播地址的帧。当一个完整的帧可用时,接口就产生一个硬中断,并且内核调用接口层函数 leintr
2.leintr 检测硬件,并且如果有一个帧到达,就调用 leread 把这个帧从接口转移到一个 mbuf(各层之间传输数据都用这个)链中,构造单独的地址信息 etherheaher。
etherinput 检查结构 etherheaher 来判断接收到的数据的类型,根据以太网类型字段来跳转。对于一个 IP 分组,schednetisr 调度一个 IP 软件中断,并选择 IP 输入队列,ipintrq。对于一个 ARP 分组,调度 ARP 软件中断,并选择 arpintrq。并将接收到的分组加入到队列中等待处理。
3. 当收到的数据报的协议字段指明这是一个 TCP 报文段时,ipintrq(通过协议转换表中的 prinput 函数)会调用 tcpinp t 进行处理. 从 mbuf 中取 ip,tcp 首部,寻找 pcb, 发送给插口层
3.1 个关于 pcb
每个线程的 task_struct 都有打开文件描述符,如果是 sock 类型还会关联到全局 inpcb 中.

listen 某个 fd 时(new socket()=>bind(listen 的端口)=>lisnten())在 pcb 中该 fd 是 listen 状态
3.2in_pcblookup
搜索它的整个 Internet PCB 表,找到一个匹配。完全匹配获得最高优先级,包含通配的最小通配数量的优先级高。所以当同一个端口已经建立连接后有了外部地址和端口,再有数据会选择该插口。比如当 140.252.1.11:1500 来的数据直接就匹配到第三个的插口,其他地址的发送到第一个插口。
4. 插口层
4.1 listen 如果监听插口收到了报文段
listen 状态下只接收 SYN,即使携带了数据也等建立连接后才发送,创建新的 so,在收到三次握手的最后一个报文段后,调用 soisconnected 唤醒插口层 accept

4.2 插口层 accept
while (so->so_qlen == 0 && so->so_error == 0) {tsleep((caddr_t)&so->so_timeo, PSOCK | PCATCH,netcon, 0))}
当 so_qlen 不是 0,调用 falloc(p, &fp, &tmpfd)创建新的插口 fd,从插口队列中复制,返回,此时该 fd 也在线程的打开文件中了。调用 soqremque 将插口从接收队列中删除。
4.3 插口层 read,send 从缓冲区复制 mbuf。略
5. 线程调用内核的 accept,从调用开始会 sleep 直到此时可以返回 fd。
【后文若用了 epoll 在 lsfd 有事件时通知线程再调用 accept 会节省调用到 sleep 时间。】

epoll

  • 原理
    poll/select/epoll 的实现都是基于文件提供的 poll 方法(f_op->poll),该方法利用 poll_table 提供的_qproc 方法向文件内部事件掩码_key 对应的的一个或多个等待队列(wait_queue_head_t) 上添加包含唤醒函数 (wait_queue_t.func) 的节点 (wait_queue_t),并检查文件当前就绪的状态返回给 poll 的调用者(依赖于文件的实现)。当文件的状态发生改变时(例如网络数据包到达),文件就会遍历事件对应的等待队列并调用回调函数(wait_queue_t.func) 唤醒等待线程。
  • 数据结构:
  • epoll_crete 创建 event_poll,实际上创建了一个 socketfd,称 epfd。
  • epoll_ctl 将回调为 ep_poll_callback 的节点 加入到 epitem 对应 fd 的等待队列中(即 sk_sleep 的 wait_queue_head_t),关联到 event_poll 的红黑树等结构体中
  • epoll_wait 将回调为 try_to_wake_up 的节点 加入到 epfd 的等待队列中你。
    当发生事件,socket 调用 ep_poll_callback 会调用 try_to_wake_up 进而唤醒 wait 的线程,向用户赋值 rdlist 数据,用户线程继续执行
    (以 scoket 为例,当 socket 数据 ready,终端会调用相应的接口函数比如 rawv6_rcv_skb,此函数会调用 sock_def_readable 然后,通过 sk_has_sleeper 判断 sk_sleep 上是否有等待的进程,如果有那么通过 wake_up_interruptible_sync_poll 函数调用 ep_poll_callback。从 wait 队列中调出 epitem, 检查状态 epitem 的 event.events, 若是感兴趣的事情发生,加入到 rdllist 或者 ovflist 中,调用 try_to_wake_up。)
  • 数据拷贝:
    1. 拷贝新添加的 events
    YSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,struct epoll_event __user *, event)
    copy_from_user(&epds, event, sizeof(struct epoll_event)))
    2. 传给用户的就绪的 event
    在 ep_send_events_proc 时
    __put_user(revents, &uevent->events。__put_user(epi->event.data, &uevent->data)
  • 与 select 区别
    重复读入参数,全量的扫描文件描述符;调用开始,将进程加入到每个文件描述符的等待队列,在调用结束后又把进程从等待队列中删除;(每次发生事件 fd 位图结构改变,重新清除再 select)select 最多支持 1024 个文件描述符。
    epoll 注册事件只需要一次拷贝(增量拷贝,依靠回调),另外返回就绪 fd,不需要遍历所有的。

运行模型

是否立即返回。

阻塞:空 cpu,IO 阻塞线程
非阻塞

是否由本线程执行

同步 IO
异步

1. 所有请求单进程 / 线程

2. 一个请求一个线程 / 进程

accept 后一个连接全部过程在一个进程 / 线程,结束后结束线程 / 进程,每次有新连接创建一个新的进程 / 线程去处理请求

  • 一个连接一个进程:父进程 fork 子进程 =》fork 代价大 百级别
  • prefork 多个进程会 accept 同一个 lsfd,linux2.6 已经支持就觉多进程同时 accept 一个时的惊群现象。
  • 一个连接一个线程 万级别限制,线程相互影响不稳定
  • prethread 多线程共享数据,可以直接 accept 后分配给线程,也可以多个线程共同 accept(accept 实现了线程安全?).

3. 一个进程 / 线程处理多个请求

线程池 / 进程池 + 非阻塞 +IO 多路复用(非阻塞 +IO 多路复用 少了哪个这种模型都没有意义)
reactor 监听所有类型事件,区分 accept 和业务处理

  • 单 reactor 单线程。reactor 是负责 IO 事件监听
  • 单 reactor 多线程 接收后 read 后给子线程处理,处理后返回发送;主线程负责所有 IO 事件处理

  • 多 reactor 多进程 nginx 没有 accept 后分配,而是子进程自己 listen,accept。
  • 多 reactor 多线程 主 accept 接收后把 fd 给子线程,子线程读 - 处理 - 写(更简单,无需传递读写数据)memchache、netty

    这个网上的图是错的。accept 后所有的读写处理都在一个线程中,无共享数据需要传递

总结下:

基本上是 accept 肯定要在一个线程中,因为只有一个 fd。
1)单 reactor 单线程 accept+read/process/send
2)单 reactor 多线程 accept+read/send =》多 process
3)多 reactor 多线程 accepct=> 多 read/process/send
4)另一种 accepct[0 号]=> 子多 read/send =》多 process 当只有一个时退化为单 reactor 多线程。线上就这个。

适用范围:

假设 4 个请求并发连接,2 个线程

  • 若 p 是瓶颈,比如 p1 占用 4 个格子
    1)单 reactor 多线程

    r1->r2->r3->r4->s1->s2->s3->s4
      p1w|w|w|w|->p3w|w|w|w|
          p2w|w|w|w|->p4w|w|w|w|

    2)多 reactor 多线程

    r1->p1w|w|w|w|->r3->p3w|w|w|w|->s1->s3
    r2->p2w|w|w|w|->r4->p4w|w|w|w|->s2->s4

    此时单 reactor 多线程更快。多 reactor 多线程编写简单

  • 若 r 是瓶颈(比如占 3 个,4 个换行了 =。=)
    1)单 reactor 多线程

    r1w|w|w|->r2w|w|w|->r3w|w|w|->r4w|w|w|->s1w|w|w|->s2w|w|w|->s3w|w|w|->s4w|w|w|
            p1->                p3
                      p2->                p4

    2)多 reactor 多线程

    r1w|w|w|->p1->r3w|w|w|->p3->s1w|w|w|->s3w|w|w|
    r2w|w|w|->p2->r4w|w|w|->p4->s2w|w|w|->s4w|w|w|

    此时多 reactor 多线程快

  • 最后一种模式分别

    r1->r3      ->s1        ->s3
      p1w|w|w|w|->p3w|w|w|w|
    r2->r4      ->s2        ->s4
      p2w|w|w|w|->p4w|w|w|w|
    
    r1w|w|w|->r3w|w|w|->s1w|w|w|->s3w|w|w|
            p1->      p3 
    r2w|w|w|->r4w|w|w|->s2w|w|w|->s4w|w|w|
            p2->      p4
  • 结论:
    若读写为瓶颈建议多 reactor 多线程。
    若处理为瓶颈建议单 reactor 多线程,
    用最后一种混合需要评估下,如果处理为瓶颈还可以考虑下,io 为瓶颈就不用了,因为并没有快多少,反而编程麻烦。

4.proactor

与 reactor 区别是 reactor 是同步读写,preactor 是异步读写

  • 在 Reactor 中实现读
    注册读就绪事件和相应的事件处理器。
    事件分发器等待事件。
    事件到来,激活分发器,分发器调用事件对应的处理器。
    事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。
  • 在 Proactor 中实现读:
    处理器发起异步读操作(注意:操作系统必须支持异步 IO)。在这种情况下,处理器无视 IO 就绪事件,它关注的是完成事件。
    事件分发器等待操作完成事件。
    在分发器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自定义缓冲区,最后通知事件分发器读操作完成。
    事件分发器呼唤处理器。
    事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分发器。

关于数据共享

早在 1973 年,actor 模式被提出(跟图灵机一个级别的模式,只是个模式),主要是对立于数据共享加锁的。一个是分布式计算中没办法很好的共享数据,另一个是共享数据 加锁会阻塞 线程(单机的话)/ 请求超时(分布式),还有个不重要的有时候锁不住(比如 cpu 的多级 cache,没个线程里的 cache 不同步),因此采用一种非阻塞和通信的方式,数据发送到 actor排队 后即返回不加锁,通过通信传递改变,actor 是整个数据 + 行为(对象)的组合 ,不仅仅负责数据的锁,actor 之间要做到无共享,发给 actor 的自己复制一份新的,actor 可以继续调 actor,所以要都 无共享 。(这么说非阻塞 IO,select 等都是借鉴的这个。)
actor 行为
1.Actor 将消息加入到消息队列的尾部。
2. 假如一个 Actor 并未被调度执行,则将其标记为可执行。
3. 一个(对外部不可见)调度器对 Actor 的执行进行调度。
4.Actor 从消息队列头部选择一个消息进行处理。
5.Actor 在处理过程中修改自身的状态,并发送消息给其他的 Actor。
为了实现这些行为,Actor 必须有以下特性:
● 邮箱(作为一个消息队列)
● 行为(作为 Actor 的内部状态,处理消息逻辑)
● 消息(请求 Actor 的数据,可看成方法调用时的参数数据)
● 执行环境(比如线程池,调度器,消息分发机制等)
● 位置信息(用于后续可能会发生的行为)
另外一种:CSP,不要通过共享内存来通信,而应该通过通信来共享内存的思想 ,Actor 和 CSP 就是两种基于这种思想的并发编程模型.Actor 模型的重点在于参与交流的实体, 而 CSP 模型的重点在于用于交流的通道.channel。channel 共享带锁,不再扩展了。
actor 的设计也要抽象好,比如
比如左右两个叉子,如果加锁。锁(左右)/ 为了并发,单独左右。actor 要左右组合在一起,每次判断左叉子和右叉子都有返回成功。
zmq 每个线程绑定一个 cpu,线程之间不会共享 session,不需要加锁。每个连接的操作都在一个 worker 中. 通信传递数据。

正文完
 0