具有代表性的并发服务器端实现模型和方法:
多进程服务器:通过创建多个进程提供服务。✔
多路复用服务器:通过捆绑并统一管理I/O对象提供服务。
多线程服务器:通过生成与客户端等量的线程提供服务。
1. 进程的概念及应用
1.1 什么是进程?
进程:占用内存空间的正在运行的程序。
从操作系统的角度看,进程是程序流的基本单位。若创建多个进程,则操作系统将同时运行。有时一个程序运行过程中也会产生多个进程,多进程服务器端就是其中的代表。
CPU核的个数与进程数:
拥有2个运算设备的CPU称作双核CPU,拥有4个运算器的称作四核CPU。也就是说,1个CPU可能包含有多个运算设备(核),核的个数与可同时运行的进程数相同。如果进程数超过了核,那么进程将分时使用CPU资源,但因为CPU运算很快,所以我们会觉得所有进程都同时在运行。
1.2 创建进程
1.2.1 进程ID
每个进程都会从操作系统中分配到一个ID,这个ID就是“进程ID”,进程ID的值为大于2的整数,因为1要分配给操作系统启动后的首个进程(协助操作系统的进程),因此用户无法得到ID为1的进程。
Linux中查看所有进程的命令: ps au
1.2.2 fork函数(创建进程)
#include<unistd.h>
pid_t fork();
成功返回进程ID,失败返回-1
fork函数: 将创建基于当前运行的,调用fork函数的进程的副本(内存空间完全相同)。此进程为父进程,其副本为子进程。两个进程都将执行fork函数调用后的语句(准确来说,就是从这个fork函数的返回值开始),之后的程序流中,通过fork函数的返回值来区分,当前执行的是子进程还是父进程。
父进程:fork函数返回子进程ID
子进程:fork函数返回0
如图:
从复制发生点开始,父进程的所有变量的值,在复制发生点处时,是什么值,子进程也就会是什么值,之后两个进程之间的变量值互不影响,如图所示,最终结果:
父进程:gval=11,lval=26
子进程:gval=12,lval=25
1.2.3 僵尸进程(为什么要进行进程销毁)
如果在子进程创建后,没有销毁子进程,那么子进程就有可能会成为僵尸进程,僵尸进程会占用内存空间,造成资源消耗。
僵尸进程产生的原因:
子进程通过两种方式来终止,一是传递参数并调用exit函数,二是main函数找那个执行return语句并返回值。这两种方式返回的值都会传递给操作系统,但操作系统不会销毁子进程,而是直到把这些值传递给产生该子进程的父进程之后,才会进行销毁。在销毁之前,这期间状态下的进程,就是僵尸进程。所以也可得知,如果要终止子进程,那么就必须向父进程传递exit参数值或return语句的返回值。
那么子进程如何向父进程传递exit参数值或return语句的返回值,来终止自己?
答:通过父进程主动发起请求来调用。如果父进程没有主动要求获取子进程的结束状态值,那么操作系统将一直保存子进程。
1.2.4 wait函数(销毁进程,阻塞)
#include<sys/wait.h>
pid_t wait(int* statloc); //statloc指向的内存空间,存放有子进程的exit参数值或return语句的返回值等其它信息
成功返回终止的子进程ID,失败返回-1
statloc指向的内存空间,不仅仅存放有子进程的exit参数值或return语句的返回值,还有其它信息,需要通过如下宏进行分离:
WIFEXITED(int statue);
子进程正常终止返回1(真)。
WEXITSTATUES(int statue);
返回子进程的返回值。
所以,使用wait函数的标准流程:
int status;
wait(&status);
if(WIFEXITED(status)) //如果正常终止
{
std::cout<<"子进程正常终止!"<<std::endl;
std::cout<<"子进程返回值:"<<WEXITSTATUS(status)<<std::endl;
}
注意:调用wait函数时,如果没有子进程要终止,那么程序将阻塞住,直到有任意一个子进程终止。
1.2.5 waitpid函数(销毁进程,无阻塞)
#include<sys/wait.h>
pid_t waitpid(
pid_t pid, //如果为-1,那么和wait函数一样,等待任意一个子进程终止
int* statloc, //同wait函数一样
int options //传递头文件中声明的常量WNOHANG,表示即使没有子进程终止,
//也不会阻塞程序执行,只是返回0并退出函数
);
成功返回终止的子进程ID(或0),失败返回-1
注意:返回0的情况是没有子进程终止。