linux多进程可伸缩模型探索

来源:互联网 发布:c 编程艺术 源代码 编辑:程序博客网 时间:2024/04/30 02:41

最近,在工作中遇到了一个需求,希望网络框架可以动态的增加或者减少进程数。采用多进程模型,可以提高服务的并发性。云平台提供的服务都是多租户的,每个用户对并发性的需求是不一样的。如果接入层同时为多个用户提供服务,可能会出现相资源竞争、相互干扰的现象,定位和排查问题比较复杂。为了避免这个问题,那就一个接入层只服务一个客户,这样避免了资源竞争,相互干扰。随之而来的是,接入层部署的数量将会显著增长。接入层到底部署几个进程合适呢?和用户的并发性需求有直接关系。最好能做到,qps较低的用户分配的进程数较少,qps较高的客户分配的进程数较多。当用户购买服务的时候,我们并不知道客户的qps,就算知道,qps也是波动的,有波峰和波谷现象。如何能让服务器架构支持动态创建和杀死进程变得紧迫。

闲话少叙,最近研究了一下nginx多进程架构模型。我决定使用基于信号驱动实现一个可伸缩的多进程模型。该模型具备以下功能特点:

  1. 父进程接收到信号SIGUSR1,动态创建一个子进程
  2. 父进程接收到信号SIGUSR2,,动态杀死一个子进程
  3. 父进程接收到信号SIGCHLD,父进程回收子进程资源,避免僵尸进程。
  4. 父进程接收到信号SIGALRM,父进程定时向已创建的所有子进程广播消息
  5. 父进程接收到信号SIGTERM、SIGQUIT,父进程杀掉所有子进程
首先来看看linux信号,linux信号分为不可靠信号和可靠信号。不可靠信号可能会出现丢失,而可靠信号不会出现丢失。信号值小于SIGRTMIN的是不可靠信号,而位于SIGRTMIN和SIGRTMAX之间的信号是可靠信号。在linux平台下,可以使用kill -l命令查看系统支持的所有信号。
当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会再注册一次。因此,信号不会丢失,实时信号又叫“可靠信号“。
当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册,则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫“”不可靠信号“。”
内核处理一个进程接收到的信号是在该进程的上下文中。因此,进程必须处于运行态。当进程被信号唤醒或者正常的调度获得cpu时间片时,在其从内核态返回到用户态时,检测是否有信号等待处理。如果存在未决信号等待处理且该信号未被阻塞,则调用信号处理函数。
模型中采用的是不可靠信号,进程可能接受到同一个信号多次,但只执行一次信号处理函数的情况。信号处理函数的执行流与main函数的执行流是相互独立的,可以说成是“”并行“的。意识到这一点很重要,信号处理函数编写的不好,程序会隐藏隐患。一旦触发,将很难调试和定位。对信号处理函数的编写有几点注意事项:
  1. 避免使用系统调用。原本执行信号处理函数是进程从内核态返回用户态时,如果在信号处理函数中又调用系统调用,会使进程再次进入内核态,这会造成函数帧栈信息变得复杂和混乱,不利于调试和定位问题。在linux平台下,很多系统调用会影响errno全局变量,如何确保errno从信号处理函数返回后,恢复成调用信号处理函数之前的值,也是开发人员的责任。
  2. 避免使用库函数。很多库函数除了影响errno全局变量外,还是不可重入的函数。这些不可重入的函数使用了全局变量或者静态变量。这些变量成了两个执行流的竞争资源和临界资源,如果不做保护,将会造成服务异常。这也是为什么很多开源项目会自定义,信号安全的函数。
  3. 越简单越好原则。这样系统的可靠性和健壮性可以得到保证,排查问题和定位问题会容易很多。
在linux平台下,通过fork函数派生出来的子进程继承了父进程的信号处理函数和信号屏蔽字。信号屏蔽字就是一组需要被当前进程阻塞的信号集。如果父进程设置了信号屏蔽字,那么子进程同样阻塞这一组信号集。子进程如果不取消信号屏蔽,那么子进程将不会执行这些信号的处理函数。
在模型中,父进程通过sigsuspend系统调用,实现信号驱动。sigsuspend系统调用会临时替换当前进程的信号屏蔽字,并阻塞进程。当进程接收到信号后,sigsuspend函数会立刻返回,信号屏蔽字恢复成调用之前的值。父进程对一组感兴趣的信号做了屏蔽,这样进程只有执行sigsuspend函数的时候,才会取消对这组信号的屏蔽。如果这组信号有被递交,将会取消阻塞并递达,然后执行信号处理函数。当且仅当信号处理函数返回后,才会执行sigsuspend函数的下一行代码。父进程根据信号处理函数的执行结果,执行不同的处理逻辑。这个过程与epoll多路复用的处理过程比较相似,sigsuspend相当于epoll_wait函数。
在linux平台下,通过fork函数派生子进程,子进程除了继承父进程的信号处理函数、信号屏蔽字,还要继承父进程已经打开的文件描述符。父子进程共享同一个文件结构,并且引用计数增加为2,当且仅当文件引用计数等于0的时候,才会真正关闭已打开的文件。当fork后,子进程需要关闭那些已经在父进程中打开的,但自己不感兴趣的文件描述符。
在模型中,父、子进程通过socketpair套接字对进行ipc通信。该套接字对是全双工的,父、子进程可以进行双向通信。父进程向socketpair套接字对的一端写入数据,子进程从socketpair套接字对的另一端接收数据。在创建socketpair套接字对之后,就立刻设置成非阻塞。一旦socketpair套接字对不可写或者不可读时就会返回EAGAIN临时资源不可用的错误。子进程将socketpair套接字对的一端加入epoll,并注册了EPOLLIN事件。当该套接字可读时,epoll_wait立刻返回,子进程从该套接字中读出父进程写入的数据。
除了通过socketpair套接字对,进行父、子进程间通信外,父进程通过kill系统调用向子进程发送信号,子进程如果没有屏蔽父进程递交的信号,子进程将会产生中断,进入内核态,当从内核态返回用户态时,执行已经递达的信号所注册的信号处理函数。父进程注册了SIGCHLD信号,当子进程退出时,父进程将会接收到SIGCHLD信号。父进程对子进程做善后处理,避免产生僵尸进程。父、子进程通过信号同样实现了双向通信。父进程通过waitpid函数,对子进程所善后处理。SIGCHLD信号是不可靠信号,特别是多个子进程同时退出时,父进程几乎同时收到多个SIGCHLD信号,只要父进程没有在sigsuspend函数处睡眠,那么只有一次SIGCHLD信号会被注册,后续的信号将全部丢弃。也就是说SIGCHLD信号的处理函数只调用一次,但可能有多个子进程退出。调用一次waitpid,只会回收一个已退出的子进程的资源。因此,需要能够有效的处理这种情况,否则僵尸进程会越来越多,造成系统资源浪费。还要注意避免在waitpid函数处阻塞,因为父进程只允许在sigsuspend函数处阻塞睡眠。
父进程通过setitimer函数设置了定时器,定时触发SIGALRM信号。父进程从sigsuspend函数返回后,向所有子进程进行广播消息。
通过这个多进程伸缩模型,让我对linux信号处理机制、信号屏蔽字,子进程从父进程继承哪些信息有了更深的理解。认识到信号在多进程编程中的重要性。利用socketpair套接字对和信号进行父、子间通信,编写多进程网络框架并不困难。nginx就是一个基于信号驱动的多进程网络框架,我将继续研究nginx。



0 0