4、TCP预先派生子进程服务器程序,accept使用文件上锁保护
为什么我们要给accept上锁呢?因为不同的系统实现。
书中提到,没加锁的程序在4.4BSD上运行,实现允许多个进程在引用同一套接字的描述符上调用accept,然而这种做法也仅仅适用于在内核中实现的源自伯里克内核。相反,System V不支持这么做。在某些不支持的系统上运行不加锁的程服务器时,某个子进程accept会返回EPORTO错误
加锁就是解决的办法。
这里就不贴出代码了,仅仅是在accept调用前加锁和在处理完客户后解锁。
5、TCP预先派生子进程服务器,传递描述符
这里我们只让父进程调用accept,然后把所接收的已连接套接字传递给某个子进程。这么做绕过了为所有子进程的accept调用提供上锁保护的可能需求,不过需要从父进程到子进程的某种形式的描述符传递。这样多少会有点复杂,因为父进程必须跟踪子进程的忙闲状态,以便给空闲子进程传递新的套接字。
在之前的父进程预先派生子进程例子中,父进程无需考虑由哪个子进程接收一个客户端连接,由操作系统处理这个细节。
然而对于当前的预先派生子进程例子,我们必须为每个子进程维护一个信息结构以便管理,如下:
通过比较我们发现,父进程通过流管道把描述符传递到各个子进程,并且各个子进程通过流管道写回单个字节,无论是与使用共享内存区的互斥锁的服务器,还是与使用文件锁上锁解锁的服务器相比,都更费时
而且,越早派生的子进程调用的次数越多(实现相关)
6、TCP并发服务器,每个客户一个线程
这个版本很简单,就是主线程阻塞在accept上,每次有客户请求就创建线程处理。
从结果图得知,快于每一个派生子进程的服务器。
7、TCP预先创建线程,每个线程各自accept
这里我们使用互斥锁就可以了。例子不复杂
到此,几种范式都结束了,现在最重要的是总结。
1、当系统负载较轻时,每来一个客户就现场派生一个子进程为之服务的传统并发模型就足够了
2、相比传统的每个客户fork一次的设计范式, 预先创建一个进程池或线程池的设计范式能有效提高性能,不过要提的是,随着客户数量的改变而动态的改变子进程数或线程数是有必要的
3、某些系统实现允许多个子进程或线程阻塞在同一个accept上,另一些却要求在accept之前加锁
4、让所有子进程或线程自行调用accept 通常比 让父进程或主线程调用accept并把描述符传递给子进程或线程 来的简单而快速。
5、由于潜在的select冲突,让所有子进程或线程阻塞在accept更可取
6、 使用线程通常快于使用进程(但是到底用线程还是进程 取决于需求)
为什么我们要给accept上锁呢?因为不同的系统实现。
书中提到,没加锁的程序在4.4BSD上运行,实现允许多个进程在引用同一套接字的描述符上调用accept,然而这种做法也仅仅适用于在内核中实现的源自伯里克内核。相反,System V不支持这么做。在某些不支持的系统上运行不加锁的程服务器时,某个子进程accept会返回EPORTO错误
加锁就是解决的办法。
这里就不贴出代码了,仅仅是在accept调用前加锁和在处理完客户后解锁。
5、TCP预先派生子进程服务器,传递描述符
这里我们只让父进程调用accept,然后把所接收的已连接套接字传递给某个子进程。这么做绕过了为所有子进程的accept调用提供上锁保护的可能需求,不过需要从父进程到子进程的某种形式的描述符传递。这样多少会有点复杂,因为父进程必须跟踪子进程的忙闲状态,以便给空闲子进程传递新的套接字。
在之前的父进程预先派生子进程例子中,父进程无需考虑由哪个子进程接收一个客户端连接,由操作系统处理这个细节。
然而对于当前的预先派生子进程例子,我们必须为每个子进程维护一个信息结构以便管理,如下:
//child头文件
#include "unp.h"
typedef struct{
pid_t child_pid; //..
int child_count; //times used for sservice
int child_status; //0 == ready
int child_pipfd; //every child has a pipe
}Child;
Child *cptr;
//主函数
//进程池处理服务器
//父进程调用accept
#include "unp.h"
#include "child.h"
int main(int ac, char *av[])
{
int listenfd, i, nchildren, navail, maxfd, nready, connfd;
socklen_t addrlen;
fd_set rset, masterset;
ssize_t n;
if(ac != 3)
{
fprintf(stderr, "Usage : tcpserver host port childrens");
exit(1);
}
listenfd = tcp_listen(av[1], av[2], &addrlen);
FD_ZERO(&masterset); //添加监听套接字
FD_SET(listenfd, &masterset);
maxfd = listenfd;
nchildren = atoi(av[3]);
navail = nchildren;
cptr = calloc(nchildren, sizeof(Child));
if(cptr == NULL)
oops("calloc error");
for(i=0; i<nchildren; i++)
{
child_make(i,listenfd); //添加各子进程流管道套接字,进行描述符传递和进程状态通知
FD_SET(cptr[i].child_pipfd, &masterset);
if(maxfd < cptr[i].child_pipfd)
maxfd = cptr[i].child_pipfd;
}
if(signal(SIGINT, sig_int) == SIG_ERR)
oops("signal error");
for(;;)
{
rset = masterset;
if(navail <= 0) //添加一个计数器,防止在没有子进程可用的情况下调用accept。如此处理我们让客户请求在listen函数的队列里排队等待。
FD_CLR(listenfd, &rset);
if((nready=select(maxfd+1,&rset,NULL,NULL,NULL)) < 0)
oops("select error");
//有客户发来请求,我们查看哪个子进程是空闲着的,那么我们就把这个描述符发给它让他去处理
if(FD_ISSET(listenfd, &rset))
{
if((connfd=accept(listenfd,NULL,NULL)) < 0)
oops("accept error");
//这个循环会导致排在前面的进程会得到较多的处理客户的机会,这个从先前贴出的结果表格也可以看出来,当然我们也可以使每次循环从之前的断点继续,但是这样并没有什么优势,除非系统有特殊的要求
for(i=0; i<nchildren; i++)
if(cptr[i].child_status == 0)
break;
if(i == nchildren)
{
fprintf(stderr,"no avaiable child");
exit(1);
}
write_fd(cptr[i].child_pipfd, "", 1, connfd);
cptr[i].child_status = 1;
cptr[i].child_count++;
navail --;
close(connfd);
if(--nready <= 0)
continue;
}
//查看是否有子进程发来消息告诉我们它已经结束了
for(i=0; i<nchildren; i++)
{
if(FD_ISSET(cptr[i].child_pipfd,&rset))
{
//如果返回为0(因为在每个子进程处理完一个客户后都会向父进程发送一个字节),表明子进程的流管道那段异常终止了。
//这里根据书上说最好做一个登记,重新派遣一个子进程来取代之前意外终止子进程的位置.
if((n=Read(cptr[i].child_pipfd, &c, 1)) == 0)
{
fprintf(stderr,"Child terminate unexpectedly");
exit(1);
}
cptr[i].child_status = 0;
navail++;
if(--nready <= 0)
break;
}
}
}
return 0;
}
pid_t child_make(int i, int listenfd)
{
int sock[2];
pid_t pid;
//为每一个子进程建立一个流管道,用于传输描述符
if(socketpair(AF_LOCAL, SOCK_STREAM, 0, sock) < 0)
oops("socketpair error");
if((pid=fork()) < 0)
oops("fork error");
if(pid > 0)
{
//父进程得到子进程相关信息后初始化结构体以备管理
close(sock[1]);
cptr[i].child_pid = pid;
cptr[i].child_count = 0;
cptr[i].child_status = 0;
cptr[i].child_pipfd = sock[0];
return pid;
}
close(sock[0]);
close(sock[1]);
close(listenfd);
child_main(i, listenfd);
}
void child_main(int i, int listenfd)
{
int connfd;
char c;
ssize_t n;
printf("child %d is starting\n",i);
for(;;)
{
//首先,这里调用Read_fd函数,大写首字母表明已经过错误处理,其次这个函数在Unix域套接字使用中的描述符传递章节使用过,需要了解可以自行翻书,主要用途就是从流管道获取描述符,为什么要接收一个字节的字符该章也有讲,主要为了避免不必要的麻烦(到底是文件结束了还是本就没有东西可读),没有别的用途
//这里以后就是阻塞在read_fd上,而不是之前的accept上了
if((n=Read_fd(STDERR_FILENO, &c, 1, &connfd)) == 0)
{
fprintf(stdout,"read_fd return 0");
exit(1);
}
if(connfd < 0)
{
fprintf(stdout,"no descriptor is sent");
exit(1);
}
web_child(connfd);
close(connfd);
//完成客户处理后,子进程通过流管道写出一个字节,告知父进程本进程已可用
//如果需要传递描述符的话,那么我们就需要使用自己写的专属的read_fd和write_fd函数,然而普通的传递字节则可用write和read
write(STDERR_FILENO,"",1);
}
}
通过比较我们发现,父进程通过流管道把描述符传递到各个子进程,并且各个子进程通过流管道写回单个字节,无论是与使用共享内存区的互斥锁的服务器,还是与使用文件锁上锁解锁的服务器相比,都更费时
而且,越早派生的子进程调用的次数越多(实现相关)
6、TCP并发服务器,每个客户一个线程
这个版本很简单,就是主线程阻塞在accept上,每次有客户请求就创建线程处理。
从结果图得知,快于每一个派生子进程的服务器。
7、TCP预先创建线程,每个线程各自accept
这里我们使用互斥锁就可以了。例子不复杂
//pthread头文件
#include "unp.h"
typedef struct{
pthread_t thread_tid;
logn thread_count;
}Thread;
Thread *tptr;
int listenfd, nthreads;
pthread_mutex_t mlock = PTHREAD_MUTEX_INITIALIZER;
//main函数
#include "pthread_30.h"
#include "unp.h"
int main(int ac, char *av[])
{
int nthreads, i;
if(ac != 4)
{
fprintf(stderr, "Usage : tcpserver host port threads\n");
exit(1);
}
nthreads = atoi(av[3]);
tptr = calloc(nthreads, sizeof(Thread));
if(tptr == NULL)
oops("calloc error")
for(i=0; i<nthreads; i++)
thread_make(i);
if(signal(SIGINT, sig_int) == SIG_ERR)
oops("signal error");
for(;;)
pause();
return 0;
}
void thread_make(int i)
{
if(pthread_create(&tptr[i].thread_tid,NULL,&thread_main, (void*)&i) < 0)
oops("pthread_create error");
return;
}
void *thread_main(void *arg)
{
int i = *(int *)arg;
int connfd;
for(;;)
{
pthread_mutex_lock(&mlock); //调用锁来取代每次accept之间的争夺
if((connfd=accept(listenfd,NULL,NULL)) < 0)
oops("accept error");
pthread_mutex_unlock(&mlock);
tptr[i].thread_count++;
web_child(connfd);
close(connfd);
}
}
8、TCP预先创建线程,主线程统一accept
//头文件
#include "unp.h"
typedef struct
{
pthread_t thread_tid;
int thread_count;
}Thread;
Thread *tptr;
//因为是在一个进程中的,所以我们不再需要传递描述符等等复杂的操作,而是通过共享一个数组
//下面是能存放的最多的客户数,每次主线程accept到一个描述符就存进来。
#define MAXNCLI 32
//存在clifd数组中, 在主线程存放描述符的同时,每个子线程开始从这个数组里取描述符,取出来就去服务客户。iput和iget是两个游标。iput指当前已经存放的位置,iget指当前已经服务的位置。既然数组不是无限大的,那么iput在到达MAXNCLI的时候就从0开始,一旦iput和iget相等,说明存的速度大于取的速度,此时我们应该给数组扩容
int clifd[MAXNCLI], iget, iput;
pthread_mutex_t clifd_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t clifd_cond = PTHREAD_COND_INITIALIZER;
//main函数
#include "unp.h"
#include "pthread_30_2.h"
int main(int ac, char *av[])
{
int listenfd, i, nthreads, connfd;
socklen_t alen;
if(ac != 4)
{
fprintf(stderr,"Usage: tcpserver host nthreads\n");
exit(1);
}
listenfd = tcp_listen(av[1],av[2],&alen);
nthreads = atoi(av[3]);
tptr = calloc(nthreads, sizeof(Thread));
iget = iput = 0;
for(i=0; i<nthreads; i++) //创建线程池
thread_make(i);
if(signal(SIGINT,sig_int) == SIG_ERR) //人为结束
oops("signal error");
for(;;)
{
connfd = Accept(listenfd,NULL,NULL);
pthread_mutex_lock(&clifd_mutex);
clifd[iput] = connfd; //对数组的解释在头文件中
if(++iput == MAXNCLI)
iput = 0;
if(iput = iget)
{
fprintf(stderr,"The array is too small");
exit(1);
}
//这里还是比较推荐先signal然后释放互斥锁的,前面的文章有提到过
pthread_cond_signal(&clifd_cond);
pthread_mutex_unlock(&clifd_mutex);
}
retrun 0;
}
void thread_make(int i)
{
Pthread_create(&tptr[i].thread_tid, NULL, &thread_main, (void*)&i);
return;
}
void thread_main(void *arg)
{
int i = *(int *)arg;
int connfd;
for(;;)
{
pthread_mutex_lock(&clifd_mutex);
while(iget == iput) //之前提到过,可能会因为信号导致假唤醒,可依据条件变量阻塞条件来solve
pthread_cond_wait(&clifd_cond, &clifd_mutex); //条件变量阻塞的条件是:当前没有描述符可以被读取
connfd = clifd[iget++];
if(iget == MAXNCLI)
iget = 0;
pthread_mutex_unlock(&clifd_mutex);
tptr[i].thread_count++;
web_child(connfd);
close(connfd);
}
}
到此,几种范式都结束了,现在最重要的是总结。
1、当系统负载较轻时,每来一个客户就现场派生一个子进程为之服务的传统并发模型就足够了
2、相比传统的每个客户fork一次的设计范式, 预先创建一个进程池或线程池的设计范式能有效提高性能,不过要提的是,随着客户数量的改变而动态的改变子进程数或线程数是有必要的
3、某些系统实现允许多个子进程或线程阻塞在同一个accept上,另一些却要求在accept之前加锁
4、让所有子进程或线程自行调用accept 通常比 让父进程或主线程调用accept并把描述符传递给子进程或线程 来的简单而快速。
5、由于潜在的select冲突,让所有子进程或线程阻塞在accept更可取
6、 使用线程通常快于使用进程(但是到底用线程还是进程 取决于需求)