Backend 服务器编程模型
服务器程序设计范式(UNP CHP3)
-
个请求1个进程.(one process per client)
-
增强型预先派生 N 个子进程(pre-fork)
减少进程创建开销.
注意惊群
现象:
多个进程同时 accept 同一个 listen fd, 因为 fork 进程时,如果没有 close_on_exec, 子进程增加父进程 fd 的引用。
当有连接到来时,进程同时唤醒,(但并不是 accept 返回)。
但只有最先运行的子进程获得 client 连接,其他继续睡眠,每次都这样也是有开销的。这就是“惊群”现象
- prefork accept 上锁
并不是所有内核的 accept 都能如此使用,安全的做法是给 accept 上锁。只有1个进程阻塞在 accept 中,其他阻塞在锁上。
上进程锁
能用文件锁,另外的技巧是创建文件锁的文件后,立即 unlink 掉,这样程序崩溃也不会有临时文件,但是进程还在,文件锁的功能可用
上进程共享的互斥锁
首先锁必须设置 PTHREAD_PROCESS_SHARED 属性。 然后锁必须在共享内存上,如用 mmap 创建一段共享内存,然后把锁创建到上面
- prefork 传递 fd
不同于在子进程中 accept,而是父进程 accept 后,把 fd 传递给“空闲”的子进程 具体是在父进程创建 socketpair 留一个管道,遍打通了父子的通道.
主进程用 select 做事件驱动,一旦有事件,查询到空闲子进程,便把数据转发到该进程处理。 这种方式比前面的 accept 多了很多 pipe 传递数据,效率比较低。
- 每个客户请求1个线程 (one thread per client)
最直白,用的最多,显然比 prefork 要快。 在客户后,现场生成子线程。 accept 在 main 中,也就是无连接时,主程序大部分时间都阻塞在 accept 里。
- pre-thread
先创建线程池,是主线程 accept,把新的请求 fd 提交到线程池中的某个可用线程,又快一点 因此2个动作少不了:
主线程的put提交new fd的动作
线程池里 get 获得活动的fd的动作
- lock+accept
先创建线程池,让每个线程自身 accept,用锁保护 accept,保证每个时刻只有一个线程在 accept 比方式 5 又要快一点