利用之前所讲的内容,我们可以构建按序向第一个客户端到第一百个客户端提供服务的服务器端。当然第一个客户端不会抱怨服务器端,但如果每个客户端的平均服务时间为0.5秒,则第100个客户端会对服务器端产生相当大的不满。
1.“第一个连接请求的受理时间为0秒,第50个连接请求的受理时间为50秒,第100个连接请求的受理时间为100秒!但只要受理,服务只需1秒。”
2.“所有连接请求的受理时间不超过1秒,但平均服务时间为2~3秒。”
无需过多考虑到底哪一种好,只需假设一下自己再观看视频,而且自己是第100个,就能得出结论。接下来讨论如何提高客户端满意度平均标准。
即使有可能延长服务时间,也有必要改进服务器端,使其同时向所有发起请求的客户端提供服务,以提高平均满意度。而且,网络程序中数据通信时间比CPU运算时间占比更大,因此,向多个客户端提供服务是一种有效利用CPU的方式。接下来讨论同时向多个客户端提供服务的并发服务器端。下面列出的是具有代表性的并发服务器端实现模型和方法。
* 多进程服务器:通过创建多个进程提供服务
* 多路复用服务器:通过捆绑并统一管理I/O对象提供服务
* 多线程服务器:通过生成与客户端等量的线程提供服务
先讲解第一种方法:多进程服务器。这种方法不适合在Windows平台下(不支持),因此重点放在Linux平台。若各位不太关心基于Linux的实现,可以直接跳到第12章。不过下面内容有助于理解服务器端构建方法。
定义:占用内存空间的正在运行的程序。
CPU的核数和进程数:
拥有两个运算设备的CPU称作双核(Daul)CPU,拥有4个运算器的CPU称作4核(Quad)CPU。也就是说,1个CPU中可能包含多个运算设备(核)。核的个数与可同时运行的进程数相同。相反,若进程数超过核数,进程将分时使用CPU资源。但因为CPU运转速度极快,我们会感到所有进程同时运行。当然,核数越多,这种感觉越明显。
无论进程如何创建,所有进程都会从操作系统分配到ID。此ID称为“进程ID ”,其值为大于2的整数。1要分配给操作系统启动后的(用于协助操作系统)首个进程,因此用户进程无法得到ID值1。
#include
pid_t fork(void);
成功时返回进程ID,失败时返回-1.
fork函数将创建调用的进程(概念上略难)。也就是说,并非根据完全不同的程序创建进程,而是复制正在运行的、调用fork函数的进程。另外,两个进程都将执行fork函数调用后的语句(准确说是在fork函数返回后)。但因为通过同一个进程、复制相同的内存空间,之后的程序流根据fork函数的返回值加以区分。即利用fork函数的如下特点区分程序执行流程。
父进程:fork函数返回子进程ID。
子进程:fork函数返回0.
此处“父进程”指原进程,即调用fork函数的主体,而“子进程”是通过父进程调用fork函数复制出的进程。
示例fork.c
#include
#include
int gval = 10;
int main(int argc, char *argv[])
{
pid_t pid;
int lval = 20;
gval++, lval += 5;
pid = fork();
if(pid == 0)
gval += 2, lval += 2;
else
gval -= 2, lval -= 2;
if(pid == 0)
printf("Child Proc: [%d, %d] \n", gval, lval);
else
printf("Parent Proc:[%d %d] \n", gval, lval);
return 0;
}
文件操作中,关闭文件和打开文件同等重要。同样,进程销毁也和进程创建同样重要。如果未认真对待进程销毁,它们将变成僵尸进程困扰大家。
进程完成工作后(执行完main函数中的程序后)应被销毁,但有时这些进程将变成僵尸进程,占用系统的重要资源。这种状态下的进程就称作“僵尸进程”,这也是给系统带来负担的原因之一。
利用如下两个示例展示调用fork函数产生子进程的终止方式。
* 传递参数并调用exit函数。
* main函数中执行return语句。
向exit函数传递的参数值和main函数的return语句返回的值都会传递给操作系统。而操作系统不会销毁子进程,直到把这些值传递给产生该子进程的父进程。处在这种状态下的进程就是僵尸进程。也就是说,将子进程变成僵尸进程的正是操作系统。既然如此,此僵尸进程何时被销毁呢?
“应该向创建子进程的父进程传递子进程的exit参数值或return语句的返回值。”
如何向父进程传递这些值呢?操作系统不会主动把这些值传递给父进程。只有父进程主动发起请求(函数调用时),操作系统才会传递该值。换言之,如果父进程未主动要求获得子进程的结束状态值,操作系统将一直保存,并让子进程长时间处于僵尸进程状态。
如前所述,为了销毁子进程,父进程应主动请求获取子进程的返回值。发起请求的具体方法(幸好非常简单),共有2种,其中之一是调用如下函数:
#include
pid_t wait(int *statloc)
//成功时返回终止的子进程ID,失败时返回-1.
调用此函数时如果已有子进程终止,那么子进程终止时传递的返回值(exit函数的参数值、main函数的return返回值)将保存到该函数的参数所指内存空间。但函数参数指向的单元中还包含其他信息,因此需要通过下列宏进行分离。
WIFEXITED子进程正常终止时返回“真”(true)
WEXITSTATUS返回子进程的返回值。
也就是说,向wait函数传递变量status的地址时,调用wait函数后应编写如下代码:
if(WIFEXITED(status))
{
puts("Normal termination!");
printf(Child pass num: %d", WEXITSTATUS(status));
}
示例:
wait.c
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int status;
pid_t pid = fork();
if(pid == 0)
{
return 3;
}
else
{
printf("child PID: %d \n", pid);
pid = fork();
if(pid == 0)
{
exit(7);
}
else
{
printf("Child PID: %d \n", pid);
wait(&status);
if(WIFEXITED(status))
printf("Child send one: %d \n", WEXITSTATUS(status));
wait(&status);
if(WIFEXITED(status))
printf("Child send two: %d \n", WEXITSTATUS(status));
sleep(30);
}
}
return 0;
}
这就是通过调用wait函数消灭僵尸进程的方法。调用wait函数时,如果没有已终止的子进程,那么程序将阻塞(Blocking)直到有子进程终止,因此需要谨慎调用该函数。
wait函数会引起程序阻塞,还可以考虑调用waitpid函数。这是防止僵尸进程的第二种方法。也是防止阻塞的方法。
#include
pid_t waitpid(pid_t pid, int * statloc, int options);
//pid 等待终止的目标子进程的ID,若传递-1,则与wait函数相同,可以等待任意子进程终止。
//statloc 与wait函数的statloc参数具有相同含义
//options 传递头文件sys