socket通信服务端编程

目录
  1. 多进程
  2. 多线程
  3. 线程池(进程池)技术

当开发一个Unix服务器程序时,需要处理的1对多或者多对更多的问题,我们有如下类型的进程控制可供选择:

多进程

通过调用fork派生出一个子进程来处理客户端请求,但子进程的最大数目受限于由于操作系统对用户可以派生的子进程数的限制(这种情况可以使用预先派生子进程的进程池技术)。
缺点:执行fork是一个耗时操作,且最多数目有限制
整体流程图如下:
tcpip_sever_fork

编程注意点:

  1. 父进程需要处理子进程的SIGCHLD信号,做一些子进程结束后的清理工作,释放资源(调用waitpid等待子进程退出,防止出现僵尸进程及关闭accept的套接字)。

    1
    2
    3
    4
    5
    6
    7
    8
    void sig_chld(int signo)
    {
    pid_t pid;
    int status;
    while((pid=waitpid(-1, &status, WNOHANG)) > 0) //WNOHANG 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若结束,则返回该子进程的ID。
    printf("child %d terminated\n", pid);
    return;
    }
  2. 由于fork的写时复制特性,子进程执行fork后需要关闭父进程listen的描述符,然后再处理客户端的请求,处理完后关闭该socket

多线程

多线程技术相比多进程来说,由于线程是轻量级的进程,创建线程的速度和效率要高很多(需要指出的是,有的操作系统不支持多线程)。其流程和多进程一致(只是fork换成了pthread_create),代码框架如下(摘自unp源码):

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
47
48
49
50
/* include serv06 */
#include "unpthread.h"
int
main(int argc, char **argv)
{
int listenfd, connfd;
void sig_int(int);
void *doit(void *);
pthread_t tid;
socklen_t clilen, addrlen;
struct sockaddr *cliaddr;
if (argc == 2)
listenfd = Tcp_listen(NULL, argv[1], &addrlen);
else if (argc == 3)
listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
else
err_quit("usage: serv06 [ <host> ] <port#>");
cliaddr = Malloc(addrlen);
Signal(SIGINT, sig_int);
for ( ; ; ) {
clilen = addrlen;
connfd = Accept(listenfd, cliaddr, &clilen);
Pthread_create(&tid, NULL, &doit, (void *) connfd); }
}
void *
doit(void *arg)
{
void web_child(int);
Pthread_detach(pthread_self());
web_child((int) arg);
Close((int) arg);
return(NULL);
}
/* end serv06 */
void
sig_int(int signo)
{
void pr_cpu_time(void);
pr_cpu_time();
exit(0);
}

编程注意点:
子线程要调用pthread_detach,由自己释放资源,避免资源泄露。

线程池(进程池)技术

线程池技术的原理:类似于某企业的客服系统,假设共有10个客服(相当于线程池中有10个线程),当业务繁忙时(服务人数大于10),则后面来的只能等待,直到有客服人员处理完业务后才能处理

  • 进程池技术
    对应于多进程并发服务器,可以使用预先派生子进程的方法,然后将客户端的请求交给子进程处理。
    程序流程:父进程在调用listen后,fork出一一定数量的子进程,然后可以执行下面的1)或2) :
    1) 各个子进程在函数体内循环调用accept等待客户端连接,并处理客户端请求
    2) 由父进程监听描述符,然后传递给各个子进程
    方法1)存在accept惊群现象:当有客户端连接时,所有N个子进程均被唤醒(因为他们监听的同一个listen socket);尽管所有N个子进程均被唤醒,但只有最先运行的子进程获得此客户端连接,其余子进程继续恢复睡眠(accept执行过程中发现accept队列长度为0),可以通过对accept调用加锁或者信号量来减少该问题带来的性能影响
    方法2) 需要增加对子进程的管理和调度,监控各个子进程的状态,然后通过描述符传递机制(可以通过socketpair机制来实现)将该描述符传递给空闲的子进程(同时也涉及accept fd队列管理,毕竟可能存在所有子进程都满载的情况)

  • 线程池技术
    类似进程池技术,但由于线程可以访问进程的全局变量空间,主程序可以维持一个队列,然后各个子进程从队列中获取connfd然后进行处理(需要添加互斥变量控制),不用进行父子进程间的描述符传递操作,更加实用。

综上,如果系统支持多线程的话,使用多线程技术可以获得更高的效率。

tips:

  1. 如何修改用户能fork的最大子进程数?
  • 通过ulimit -a查看用户能fork的最大子进程数目:
    max user processes (-u) 3810
  • 通过修改/etc/sercurity/limits.conf,增加用户限制条目:
    @user hard nproc 65535
  • 由于操作系统对系统最多pid数有限制,如果要突破该限制的话需要更改kernel.pid_max参数
    sysctl kernel.pid_max #查看
    sysctl -w kernel.pid_max=num #修改,其中num为进程数
  1. 如何获取进程耗费的cpu时间?
    使用getrusage函数,设置第一个参数为RUSAGE_SELF和RUSAGE_CHILDREN分别获取父子进程的cpu使用情况,在分别统计用户态耗时(ru_utime结构)及内核态耗时(ru_stime结构)
本站总访问量