概述
本节主要探究并发服务器程序设计的另两类变体:
- 预先派生子进程:让服务器在启动阶段调用fork创建一个子进程池,每个客户请求由当前可用子进程池中的某个(闲置)子进程处理;
- 预先创建线程:让服务器在启动阶段创建一个线程池,每个客户由当前可用线程池中的某个(闲置)线程处理;
-
TCP客户程序设计范式
用于测试各个范式服务器的TCP客户程序
#include "unp.h"
#define MAXN 16384
int main(int argc, char **argv){
int i,j,fd,nchildren,nloops,nbytes;
pid_t pid;
ssize_t n;
char request[MAXLINE],reply[MAXN];
//服务器主机或IP地址、端口、fork子进程数、每个进程发送给服务器请求数、服务器返回字节数
if(argc != 6)
err_quit("usage: client <hostname or IPaddr> <port> <#children> "
"<#loop/child> <#bytes/request>");
nchildren = atoi(argv[3]);
nloops = atoi(argv[4]);
nbytes = atoi(argv[5]);
snprintf(request, sizeof(request), "%d\n", nbytes);
for(i=0;i<nchildren;i++){
if((pid = fork()) == 0){
for(j=0;j<nloops;j++){
fd = tcp_connect(argv[1], argv[2]);
write(fd, request, strlen(request));
if((n = readn(fd, reply, nbytes)) != nbytes)
err_quit("server returned %d bytes", n);
close(fd):
}
printf("child %d done\n",i);
exit(0);
}
}
while(wait(NULL) > 0);
if(errno != ECHILD)
err_sys("wait error");
exit(0);
}
-
TCP迭代服务器程序
这种服务器总是在完全处理某个客户请求之后才转向下一个客户。
-
TCP并发服务器程序:每个客户一个子进程
传统上并发服务器调用fork派生一个子进程来处理每个客户。这使得服务器能够同时为多个客户服务,每个进程一个客户。客户数目的唯一限制是操作系统对以其名义运行服务器的用户ID能够同时拥有多少子进程的限制。
#include "unp.h"
int main(int argc, char **argv){
int listenfd, connfd;
pid_t childpid;
void sig_chld(int), sig_int(int), web_child(int);
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: serv01 [ <host> ] <port#> ");
cliaddr = malloc(addrlen);
signal(SIGCHLD, sig_chld);
signal(SIGINT, sig_int);
for( ; ; ){
clilen = addrlen;
if((connfd = accept(listenfd, cliaddr, &clilen)) < 0){
if(errno == EINTR)
continue;
else
err_sys("accept error");
}
if((childpid = fork()) == 0){
close(listenfd);
web_child(connfd);
exit(0);
}
close(connfd);
}
}
//捕获由键入终端键产生的SIGINT信号
//在客户运行完毕之后我们键入该键以显示服务器程序运行所需的CPU时间
void sig_int(int signo){
void pr_cpu_time(void);
pr_cpu_time();
exit(0);
}
- 以下是SIGINT信号处理函数调用的pr_cpu_time函数
#include "unp.h"
#include <sys/resource.h>
#ifndef HAVE_GETRUSAGE_PROTO
int getrusage(int, struct rusage *)
#endif
//getrusage调用了两次,分别返回调用进程(RUSAGE_SELF)和它所有已终止子进程(RUSAGE_CHILDREN)
//的资源利用统计
void pr_cpu_time(void){
double user, sys;
struct rusage myusage, childusage;
if(getrusage(RUAGE_SELF, &myusage) < 0)
err_sys("getrusage error");
if(getrusage(RUSAGE_CHILDREN, &childusage) < 0)
err_sys("getrusage error");
user = (double) myusage.ru_utime.tv.sec +
myusage.ru_utime.tv.usec/1000000.0;
user += (double) childusage.ru_utime.tv.sec +
childusage.ru_utime.tv.usec/1000000.0;
sys= (double) myusage.ru_stime.tv.sec +
myusage.ru_stime.tv.usec/1000000.0;
sys+= (double) childusage.ru_stime.tv.sec +
childusage.ru_stime.tv.usec/1000000.0;
printf("\nuser time = %g, sys time = %g\n", user, sys);
}
- 处理每个客户请求的web_child函数
#include "unp.h"
#define MAXN 16384
void web_child(int sockfd){
int ntowrite;
ssize_t nread;
char line[MAXLINE], result[MAXN];
for( ; ; ){
if((nread = readline(sockfd, line, MAXLINE)) == 0)
return ;//connection closed bby other end
ntowrite = atoi(line);
if((ntowrite <= 0) || (ntowrite > MAXN))
err_quit("client request for %d bytes", ntowrite);
writen(sockfd, result, ntowrite);
}
}
-
TCP预先派生子进程服务器程序:accept无上锁保护
该技术的服务器不像传统意义的并发服务器那样为每个客户现场派生一个子进程,而是在启动阶段预先派生一定数量的子进程,当各个客户连接到达时,这些子进程立即就能为它们服务。
【优点】无须引入父进程执行fork的开销就能处理新到的客户。
【缺点】父进程必须在服务器启动阶段猜测需要预先派生多少子进程。
通过增加一些代码,服务器总能应对客户负载的变动。父进程必须做的就是持续监视可用(即闲置)子进程数,一旦该值降到低于某个阈值就派生额外的子进程。同样,一旦该值超过另一个阈值就终止一些过剩的子进程。
//预先派生子进程服务器程序main函数
#include "unp.h"
static int nchildren;
static pid_t *pids;
int main(int argc, char **argv){
int listenfd, i;
socklen_t addrlen;
void sig_int(int);
pid_t child_make(int, int, int);
if(argc == 3)
listenfd = tcp_listen(NULL, argv[1], &addrlen);
else if(argc == 4)
listenfd = tcp_listen(argv[1], argv[2], &addrlen);
else
err_quit("usage: serv02 [ <host> ] <port#> <#children> ");
nchildren = atoi(argv[argc-1]);
pids = calloc(nchildren, sizeof(pid_t));
for(i=0;i<nchildren;i++)
pids[i] = child_make(i, listenfd, addrlen);
signal(SIGINT,sig_int);
for( ; ; )
pause();
}
void sig_int(int signo){
int i;
void pr_cpu_time(void);
for(i=0;i<nchildren;i++)
kill(pids[i], SIGTERM);
while(wait(NULL) > 0);
if(errno != ECHILD)
err_sys("wait error");
pr_cpu_time();
exit(0);
}
pid_t child_make(int i, int listenfd, int addrlen){
pid_t pid;
void child_main(int, int, int);
//调用fork后只有父进程返回,子进程调用child_main函数
if((pid = fork()) > 0)
return pid;
child_main(i, listenfd, addrlen);
}
void child_main(int i, int listenfd, int addrlen){
int connfd;
void web_child(int);
socklen_t clilen;
struct sockaddr *cliaddr;
cliaddr = malloc(addrlen);
printf("child %ld starting\n",(long)getpid());
for( ; ; ){
clilen = addrlen;
connfd = accept(listenfd, cliaddr, &clilen);
web_child(connfd);
close(connfd);
}
}
-
TCP预先派生子进程服务器程序:accept使用文件上锁保护
【问题存在】上面的代码中多个进程引用同一个监听套接字的描述符上调用accept这种做法仅仅适用于内核中实现accept的源自Berkeley的内核。若是在SVR4的Solaris2.5内核上运行,则客户连接服务器不久后,某个子进程的accept就会返回EPROTO错误(表示协议有错)。
【解决办法】让应用进程在调用accept前后安置某种形式的锁(lock),这样任意时刻只有一个子进程阻塞在accept调用中,其他子进程则阻塞在试图获取用于被保护accept的锁上。
上述代码中,对main函数进行的修改如下:
my_lock_init("/tmp/lock.xxxxx");
for(i=0;i<nchildren;i++)
pids[i] = child_make(i, listenfd, addrlen);
同时,child_make函数的唯一改动是在调用accept之前获取文件锁,在accept返回之后释放文件锁。
for( ; ; ){
clilen = addrlen;
my_lock_wait();
connfd = accept(listenfd, cliaddr, &clilen);
my_lock_release();
web_child(connfd);
close(connfd);
}
- 以下是使用POSIX文件上锁功能的my_lock_init函数
#include "unp.h"
//两个flock结构,一个用于上锁,一个用于解锁
static struct flock lock_it, unlock_it;
static int lock_fd = -1;
void my_lock_init(char *pathname){
char lock_file]1024];
strncpy(lock_file, pathname, sizeof(lock_file));
//mkstemp函数根据该模板创建一个唯一的路径名
lock_fd = mkstemp(lock_file);
//通过从文件系统目录中删除该路径名,若程序崩溃,这个临时文件也会消失
//若是多个进程打开该文件,只要引用计数不变为0,则文件本身不会被删除
unlink(lock_file);
lock_it.l_type = F_WRLCK;
lock_it.l_whence = SEEK_SET;
lock_it.l_start = 0;
lock_it.l_len = 0;
unlock_it.l_type = F_UNLCK;
unlock_it.l_whence = SEEK_SET;
unlock_it.l_start = 0;
unlock_it.l_len = 0;
}
- 上锁函数my_lock_wait
void my_lock_wait(){
int rc;
while((rc = fcntl(lock_fd, F_SETLKW, &lock_it)) < 0){
if(errno == EINTR)
continue;
else
err_sys("fcntl error for my_lock_wait");
}
}
- 解锁函数my_lock_release
void my_lock_release(){
if(fcntl(lock_fd, F_SETLKW, &unlock_it) < 0)
err_sys("fcntl error for my_lock_release");
}
-
TCP预先派生子进程服务器程序,accept使用线程上锁被保护
为了使用线程上锁,main、child_makek和child_main函数都保持不变,唯一需要改动的是3个上锁函数。在不同进程之间使用线程上锁要求:
- 互斥锁变量必须存放在由所有进程共享的内存区中
- 必须告知线程函数库这是在不同进程之间共享的互斥锁
- my_lock_init函数
#include "unpthread.h"
#include <sys/mman.h>
static pthread_mutex_t *mptr;
void my_lock_init(char *pathname){
int fd;
pthread_mutexattr_t mattr;
fd = open("/dev/zero", O_RDWR, 0);
//内存映射
mptr = mmap(0, sizeof(pthread_mutex_t), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
//初始化锁
pthread_mutexattr_init(&mattr);
pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);
//初始化共享内存中的互斥锁
pthread_mutex_init(mptr, &mattr);
}
- my_lock_wait函数
void my_lock_wait(){
pthread_mutex_lock(mptr);
}
- my_lock_release函数
void my_lock_release(){
pthread_mutex_unlock(mptr);
}
-
TCP预先派生子进程服务器程序:传递描述符
此版本是只让父进程调用accept,然后把所接受的已连接套接字“传递”给某个子进程。这么做绕过了为所有子进程的accept调用提供上锁保护的可能需求,不过需要从父进程到子进程的某种形式的描述符传递。
对于当前的预先派生子进程例子,我们必须为每个子进程维护一个信息结构以便管理。
typedef struct{
pid_t child_pid;
int child_pidefd;
int child_status;
long child_count;
}Child;
首先修改child_make函数,在调用fork之前先创建一个字节流管道。它是一对Unix域字节流套接字。派生出子进程之后,父进程关闭其中一个描述符(sock[1]),子进程关闭另一个描述符(sockfd[0])。子进程还把流管道的自身拥有端(sock[1])复制到标准错误输出,这样每个子进程就通过读写标准错误输出和父进程通信。
#include "unp.h"
#include "child.h" //上面的代码
pid_t child_make(int i, int listenfd, int addrlen){
int sockfd[2];
pid_t pid;
void child_main(int, int, int);
socketpair(AF_LOCAL, SOCK_STREAM, 0, sockfd);
if((pid = fork()) > 0){
close(sockfd[1]);
cptr[i].child_pid = pid;
cptr[i].child_pipefd = sockfd[0];
cptr[i].child_status = 0;
return pid;
}
dup2(sockfd[1], STDERR_FILENO);
close(sockfd[0]);
close(sockfd[1]);
close(listenfd);
child_main(i, listenfd, addrlen);
}
所有子进程均派生之后的进程关系如下图。我们关闭每个子进程中的监听套接字,因为只有父进程才调用accept。父进程必须处理监听套接字以及所有字节流套接字,使用select多路选择它的所有描述符。
#include "unp.h"
#include "child.h"
static int nchildren;
int main(int argc, char **argv){
/*
naval:跟踪当前可用的子进程数,若为0,则从select读描述符集关掉与监听套接字对应的位
*/
int listenfd, i, navail, maxfd, nsel, connfd, rc;
socklen_t addrlen, clilen;
void sig_int(int);
pid_t child_make(int, int, int);
ssize_t n;
struct sockaddr *cliaddr;
if(argc == 3)
listenfd = tcp_listen(NULL, argv[1], &addrlen);
else if(argc == 4)
listenfd = tcp_listen(argv[1], argv[2], &addrlen);
else
err_quit("usage: serv05 [ <host> ] <port#> <#children> ");
FD_ZERO(&masterset);
FD_SET(listenfd, &masterset);
maxfd = listenfd;
cliaddr = malloc(addrlen);
nchildren = atoi(argv[argc-1]);
navail = nchildren;
cptr = calloc(nchildren, sizeof(child));
for(i=0;i<nchildren;i++){
child_make(i, listenfd, addrlen);
FD_SET(cptr[i].child_pipefd, &masterset);
maxfd = max(maxfd, cptr[i].child_pipefd);
}
signal(SIGINT,sig_int);
for( ; ; ){
rset = masterset;
if(navail <= 0)
FD_CLR(listenfd, &rset);
nsel = select(maxfd+1, 1, &rset, NULL, NULL, NULL);
if(FD_ISSET(listenfd, &rset)){
clilen = addrlen;
connfd = accept(listenfd, cliaddr, &clilen);
for(i=0;i<nchildren;i++)
if(cptr[i].child_status == 0)
break;
if(i == nchildren)
err_quit("no available children");
cptr[i].child_status = 1;
cptr[i].child_count++;
navail--;
//使用write_fd函数把就绪的已连接套接字传递给孩子进程
n = write_fd(cptr[i].child_pipefd, "", 1, connfd);
close(connfd);
if(--nsel == 0)
continue;
}
for(i=0;i<nchildren;i++){
if(FD_ISSET(cptr[i].child_pipefd, &rset)){
if((n = read(cptr[i].child_pipefd, &rc, 1)) == 0)
err_quit("child %d terminated unexpectedly", i);
cptr[i].child_status = 0;
navail++;
if(--nsel == 0)
break;
}
}
}
}
void sig_int(int signo){
int i;
void pr_cpu_time(void);
for(i=0;i<nchildren;i++)
kill(pids[i], SIGTERM);
while(wait(NULL) > 0);
if(errno != ECHILD)
err_sys("wait error");
pr_cpu_time();
exit(0);
}
pid_t child_make(int i, int listenfd, int addrlen){
pid_t pid;
void child_main(int, int, int);
//调用fork后只有父进程返回,子进程调用child_main函数
if((pid = fork()) > 0)
return pid;
child_main(i, listenfd, addrlen);
}
void child_main(int i, int listenfd, int addrlen){
cgar c;
int connfd;
void web_child(int);
ssize_t n;
printf("child %ld starting\n", (long)getpid());
for( ; ; ){
if((n = read_fd(STDERR_FILENO, &c, 1, &connfd)) == 0)
err_quit("read_Fd returned 0");
if(connfd < 0)
err_quit("no descriptor from read_fd");
web_child(connfd);
close(connfd);
write(STDERR_FILENO, "", 1);
}
}
-
TCP并发服务器程序:每个客户一个线程
//创建线程TCP服务器程序的main函数
#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;
}
-
TCP预先创建线程服务器程序:每个线程各自accept
本服务器的基本设计是预先创建一个线程池,并让每个线程各自调用accept。取代让每个线程都阻塞在accept调用之中的做法,我们改用互斥锁以保证任何时刻只有一个线程在调用accept。
以下是同于维护关于每个线程若干信息的Thread结构
typedef struct {
pthread_t thread_tid;
long thread_count;
}Thread;
Thread *tptr;
int listenfd, nthreads;
socklen_t addrlen;
pthread_mutex_t mlock;
- 预先创建线程TCP服务器程序
#include "unpthread.h"
#include "pthread07.h"
pthread_mutex_t mlockk = PTHREAD_MUTEX_INITIALIZER
int main(int argc, char **argv){
int i;
void sig_int(int), thread_make(int);
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: serv07 [ <host> ] <port#> <#thread>");
nthreads = atoi(argv[argc-1]);
tptr = calloc(nthreads, sizeof(Thread));
for(i=0;i<nthreads;i++)
thread_make(i);
signal(SIGINT, sig_int);
for( ; ; )
pause();
}
- thread_make函数
#include "unpthread.h"
#include "pthread07.h"
void thread_make(int i){
void *thread_main(void *);
pthread_create(&tptr[i].thread.tid, NULL, &thread_main, (void*) i);
return ;
}
- thread_main函数
void* thread_main(void *arg){
int connfd;
void web_child(int);
socklen_t clilen;
struct sockaddr *cliaddr;
cliaddr = malloc(addrlen);
printf("thread %d starting\n", (int)arg);
for( ; ; ){
clilen = addrlen;
pthread_mutex_lock(&mlock);
connfd = accept(listenfd, cliaddr, &clilen);
pthread_mutex_unlock(&mlock);
tptr[(int) arg].thread_count++;
web_child(connfd);
close(connfd);
}
}
-
TCP预先创建线程服务器程序:主线程统一accept
typedef struct {
pthread_t thread_tid;
long thread_count;
}Thread;
Thread *tptr;
#define MAXNCLI 32
int clifd[MAXNCLI], iget, iput;
pthread_cond_t chifd_cond;
pthread_mutex_t clifd_mutex;
#include "unpthread.h"
#include "pthread07.h"
pthread_mutex_t clifd_mutex = PTHREAD_MUTEX_INITIALIZER
pthread_cond_t clifd_cond = PTHREAD_COND_INITIALIZER
int main(int argc, char **argv){
int i, listenfd, connfd;
void sig_int(int), thread_make(int);
if(argc == 3)
listenfd = tcp_listen(NULL, argv[1], &addrlen);
else if(argc == 4)
listenfd = tcp_listen(argv[1], argv[2], &addrlen);
else
err_quit("usage: serv08 [ <host> ] <port#> <#thread>");
cliaddr = malloc(addrlen);
nthreads = atoi(argv[argc-1]);
tptr = calloc(nthreads, sizeof(Thread));
iget = iput = 0;
for(i=0;i<nthreads;i++)
thread_make(i);
signal(SIGINT, sig_int);
for( ; ; ){
clilen = addrlen;
connfd = accept(listenfd, cliaddr, &clilen);
pthread_mutex_lock(&clifd_mutex);
clifd[iput] = connfd;
if(++iput == MAXNCLI)
iput = 0;
if(iput == iget)
err_quit("iput = iget = %d", iput);
pthread_cond_signal(&clifd_cond);
pthread_mutex_unlock(&clifd_mutex);
}
}
- thread_make函数
void thread_make(int i){
void *thread_main(void*);
pthread_create(&tptr[i].thread_tid, NULL, &thread_main, (void *) i);
return ;
}
- thread_main函数
void* thread_main(void *arg){
int connfd;
void web_child(int);
printf("thread %d starting\n",(int) arg);
for( ; ; ){
pthread_mutex_lock(&clifd_mutex);
while(iget == iput)
pthread_cond_wait(&clifd_cond, &clifd_mutex);
connfd = clifd[iget];
if(++iget == MAXNCLI)
iget = 0;
pthread_mutex_unlock(&clifd_mutex);
tptr[(int) arg].thread_count++;
web_child(connfd);
close(connfd);
}
}