服务器多进程、多线程和事件驱动模型

多进程模型、多线程模型

  • 多进程模型
    • 每接受一个连接就fork一个子进程,在该子进程中处理该连接的请求
    • 特点是多进程占用系统资源多,进程切换的系统开销大,Linux下最大进程数有限制,不利于处理大并发
  • 多线程模型
    • 每接受一个连接就create一个子线程,利用子线程处理这个连接的请求
    • Linux下有最大线程数限制(进程虚拟地址空间有限),进程频繁创建和销毁造成系统开销,同样不利于处理大并发
  • 优缺点
    • 多进程
      • 编程相对容易;通常不需要考虑锁和同步资源的问题
      • 更强的容错性:比起多线程的一个好处是一个进程崩溃了不会影响其他进程
      • 有内核保证的隔离:数据和错误隔离
      • 进程切换开销大
    • 多线程
      • 创建速度快
      • 共享数据,多线程间可以共享同一虚拟地址空间,多进程间的数据共享就需要用到共享内存、信号量等IPC技术
      • 较轻的上下文切换开销
      • 一旦有一个线程挂掉,整个进程都可能会挂掉
      • 需要对共享资源的访问进行同步

事件驱动模型

简介

  • Linux下基于select、poll或epoll实现
  • 程序的基本结构是一个事件循环结合非阻塞IO,以事件驱动和事件回调的方式实现业务逻辑,目前在高性能的网络程序中,使用得最广泛的就是这种并发模型
  • 结合线程池,避免线程频繁创建和销毁的开销,能很好地处理高并发
  • 事件驱动模型使得我们可以在一个线程内处理多个客户端的事件,省去了频繁创建销毁进程或线程的开销,结合非阻塞IO能够做到不使程序阻塞在某个描述符的读写操作上

I/O多路复用

I/O多路复用使得程序能同时监听多个文件描述符,在一个或更多文件描述符就绪前始终处于睡眠状态。Linux下的I/O复用方案有select、poll和epoll

  • select
 #include 
 #include 
 #include 
 int select(int n, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
  • 监听的文件描述符分为三类,分别等待可读(readfds)、可写(writefds)和异常事件(exceptfds)
  • 成功返回时,每个集合只包含对应类型的I/O就绪的文件描述符,下次调用时需要重新填充readfds、writefds和exceptfds
  • 监听的文件描述符数量最大值是1024
  • poll
    #include 
    int poll(struct pollfd* fds, unsigned int nfds, int timeout);
    struct pollfd {
        int fd;
        short events;
        short revents;
    };
    
  • 与select使用的三个基于位掩码的文件描述符集合不同,poll使用一个简单的nfds个pollfd结构体构成的数组,fds指向该数组
  • 每个pollfd结构体指定监听单一的文件描述符。events是要监听的文件描述符事件的一组位掩码,revents字段则是发生在该文件描述符上的事件的位掩码。内核在poll返回时设置revents字段
  • 每次返回时,需要遍历nfds数组,寻找活跃的文件描述符并处理,与select相比,下次调用poll时不需要重新填充监听的文件描述符
  • epoll
     #include 
     int epoll_create(int size);
     int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
     int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
     struct epoll_event {
         __u32 events;
         union {
             void* ptr;
             int fd;
             __u32 u32;
             __u64 u64;
         } data;
     };
    
  • epoll_create用于创建epoll实例
  • epoll_ctl用于向指定的epoll实例加入或删除或修改文件描述符,op的有效值是EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL
  • epoll_wait等待给定epoll实例关联的文件描述符上的事件,通过events数组返回活跃的文件描述符,因此处理时不需要遍历全部的监听描述符

Reactor和Preactor

常见的事件驱动模型有同步非阻塞I/O模型Reactor,与Reactor相对应的是异步非阻塞I/O模型Preactor,这两种模型的工作步骤(读事件)如下

  • Reactor
    • 应用程序注册读就需事件和相关联的事件处理器。
    • 事件分离器等待事件的发生。
    • 当发生读就需事件的时候,事件分离器调用第一步注册的事件处理器。
    • 事件处理器首先执行实际的读取操作,然后根据读取到的内容进行进一步的处理。
  • Preactor
    • 应用程序初始化一个异步读取操作,然后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键。
    • 事件分离器等待读取操作完成事件。
    • 在事件分离器等待读取操作完成的时候,操作系统调用内核线程完成读取操作,并将读取的内容放入用户传递过来的缓存区中。这也是区别于Reactor的一点,在Proactor中,应用程序需要传递缓存区。
    • 事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区读取数据,而不需要进行实际的读取操作。
  • 可以看出,Reactor与Preactor的区别在于事件分离器通知的是I/O就绪事件还是I/O完成事件,这也是同步I/O模型与异步I/O模型的区别。Preactor模型需要操作系统的支持,并不常用。

你可能感兴趣的:(Linux,Linux系统编程)