一.多进程服务器端
(一)进程概念及应用
利用之前学习到的内容,我们的服务器可以按照顺序处理多个客户端的服务请求。在客户端和服务时间增长的情况下,服务器就不足以满足需求了。
1.两种类型的服务器端
(1)普通服务器:当有100个客户端连接请求到来时,假设每个请求的受理时间为1s,那么第50个请求需要等待50s,第100个请求需要等待100s
(2)并发服务器:所有客户端的连接请求受理时间都不超过1s,单平均服务时间2-3s。
很明显并发服务器处理高并发量的情况效率更高。
2.并发服务器端的实现方法
多进程服务器:通过创建多个进程提供服务。
多路复用服务器:通过捆绑并统一管理 I/O 对象提供服务。
多线程服务器:通过生成与客户端等量的线程提供服务。
3.理解进程(Precoess)
进程(Process)是计算机中的一个术语,指的是正在运行中的程序实例。在操作系统中,每个进程都有自己独立的内存空间和资源,它们之间相互隔离,并且可以独立执行。
每个进程可以包含一个或多个线程,线程是进程内的执行单元,负责执行进程的指令。不同的进程之间可以并发执行,相互之间独立运行,彼此不会干扰。
进程有以下几个特点:
- 独立性:每个进程拥有自己的地址空间和资源,运行时相互独立,一个进程的崩溃不会影响其他进程。
- 并发性:多个进程可以同时运行,由操作系统进行调度和管理,利用多核处理器实现并行处理。
- 隔离性:不同进程之间的内存空间相互隔离,一个进程无法直接访问另一个进程的数据和资源,需要通过特定的机制进行通信和共享。
- 可抢占性:操作系统可以根据优先级和时间片轮转等策略,暂停当前进程的执行,并将CPU分配给其他进程,以实现公平调度和资源利用。
进程是操作系统中重要的概念,它为程序的执行提供了一个独立和可控的环境。通过进程,操作系统可以同时运行多个应用程序,实现资源的合理分配和管理。
生活中有许多例子可以说明进程的概念。下面是几个常见的例子:
-
煮饭过程:将烹饪一顿饭比作一个进程。在煮饭的过程中,你需要准备食材、洗切处理、点火、加热、炒煮等一系列步骤。每个步骤都是相对独立的,但又相互关联,最终完成一道美味的饭菜。
-
打印文件:当你要打印一个文件时,你会选择打印命令并发送给打印机。打印机会创建一个打印进程,它负责从计算机接收数据、解析文件格式、生成打印页面,并将页面发送到打印机进行输出。同时,你可以进行其他操作,如编辑文档或浏览网页,这些操作与打印进程并行执行。
-
路上的交通:将路上的车辆比作进程。在拥挤的道路上,每辆车都是一个独立的进程,它们之间相互独立运行,但也受到交通规则和信号灯的控制。每辆车根据自己的路径和目的地进行行驶,通过调度和协调,交通系统实现了车辆的并发运行和道路资源的合理利用。
-
整个工业生产过程:在一个工厂中,生产线上的各个环节可以看作是不同的进程。例如,原材料的采购、加工制造、装配、质量检测等环节都是相对独立的进程,它们按照一定的顺序和流程进行,并最终完成产品的制造。
这些例子说明了生活中进程的存在和应用。无论是在计算机系统中还是在日常生活中,进程都扮演着协调和管理任务的重要角色,实现了多个任务之间的并发执行和资源的合理利用。
4.进程ID
进程ID(Process ID),也称为PID,是操作系统中用来唯一标识一个正在运行的进程的数字标识符。每个进程在创建时都会被分配一个独特的PID。
- pa au 查看当前运行的所有进程
进程ID的作用有以下几个方面:
-
进程标识:通过PID,操作系统可以准确地标识和区分不同的进程。不同的进程具有不同的PID,使得操作系统可以对它们进行管理、调度和资源分配。
-
进程控制:操作系统可以使用PID来控制进程的创建、终止和暂停等操作。通过指定PID,可以准确地选择目标进程并执行相应的操作。
-
进程通信:在进程间进行通信时,PID常被用作目标进程的标识符。发送进程可以通过目标进程的PID将消息或数据传递给指定的进程。
-
资源管理:各个系统资源,如内存、文件、网络连接等,都与特定的进程相关联。通过PID,操作系统可以将资源与相应的进程关联起来,并进行有效的资源管理和保护。
需要注意的是,PID是动态分配的,当一个进程终止后,其PID可能会被重新分配给新创建的进程。因此,PID只在进程的生命周期内是唯一的。
5.通过 fork() 函数创建进程
在操作系统中,可以使用fork()函数来创建一个新的进程。fork()是一个系统调用,其功能是复制当前进程(称为父进程),创建一个新的进程(称为子进程)。子进程是父进程的副本,它继承了父进程的代码、数据和资源。
具体使用方法如下:
- 在程序中调用fork()函数。fork()函数没有参数,返回值为整型。
- fork()函数的返回值不同于父进程和子进程。在父进程中,fork()返回子进程的PID(大于0);在子进程中,fork()返回0;如果fork()调用失败,返回一个负值表示错误。
- 父进程和子进程之后的代码是完全独立执行的。根据fork()函数的返回值,可以在程序中使用条件语句或其他逻辑来区分父进程和子进程的执行路径。
- 子进程可以通过修改自己的代码和数据,执行不同的任务。父进程和子进程之间共享打开的文件描述符和某些系统资源,但是它们有各自独立的运行环境和内存空间。
#include<iostream>
#include<unistd.h>
int val=10;
int main(){
pid_t pid=fork();
int index=25;
val++,index+=5;
if(pid==-1){//fork调用失败
std::cout<<" fork调用失败"<<std::endl;
return 1;
}
else if(pid==0)
index+=10;
else
val+=2;
if(pid==0)
std::cout<<"子进程:"<<"val:"<<val<<" index:"<<index<<std::endl;
else
std::cout<<"父进程:"<<"val:"<<val<<" index:"<<index<<std::endl
;
return 0;
}
可以看出,父子进程的变量都是单独区分开的,修改并不会相互影响。
通过fork()函数创建的子进程继承了父进程的大部分状态,包括变量值、打开的文件、进程优先级等。子进程可以独立执行其他任务,这样就实现了并发执行多个进程的能力。
需要注意的是,fork()函数的调用可能会导致操作系统创建新的进程和分配额外的资源。因此,在使用fork()函数时应该注意合理使用系统资源,避免过多创建进程导致系统负载过重。
(二)进程与僵尸进程
进程是操作系统中正在运行的程序的实例,它具有独立的执行环境和资源。当一个进程完成了它的任务,并且终止了,但其父进程尚未通过wait()或waitpid()等系统调用来获取该子进程的状态信息时,这个已经终止但尚未被回收的进程就成为僵尸进程。
僵尸进程是一种特殊的进程状态,其主要特点包括:
- 僵尸进程处于终止状态:即进程已经执行完毕,但它的进程描述符仍然存在于系统中。
- 父进程尚未对其进行处理:父进程还没有使用wait()或waitpid()等系统调用来获取子进程的退出状态信息。
- 僵尸进程不再执行任何代码:僵尸进程不再占用CPU时间片,也不再占用其他系统资源。
产生僵尸进程的常见情况是,父进程在创建子进程后,没有及时处理子进程的终止状态。这可能是因为父进程疏忽、崩溃或者被其他任务所占用而没有处理子进程。
虽然僵尸进程本身并不会导致系统性能问题,但过多的僵尸进程可能会浪费系统资源。因此,需要及时清理僵尸进程。父进程可以通过以下方式处理僵尸进程:
- 使用wait()或waitpid()等系统调用:父进程可以主动调用wait()或waitpid()等系统调用来获取子进程的退出状态信息,从而使子进程成为"终止"状态,释放其占用的系统资源。
- 使用信号处理机制:父进程可以通过注册SIGCHLD信号处理函数,当收到这个信号时,处理僵尸进程的终止状态。
另外,操作系统也会提供一些机制来自动回收僵尸进程,例如Linux中的"init"进程(PID为1)会负责收养孤儿进程和回收僵尸进程。
在编写程序时,父进程应该及时处理子进程的退出状态,以避免过多的僵尸进程积累。
1.子进程的终止方式:
(1)正常退出:子进程可以在执行完任务后通过调用exit()函数来正常退出。exit()函数会终止当前进程,并将退出状态传递给父进程。
#include <stdlib.h>
int main() {
// 子进程执行任务
// ...
// 正常退出
exit(EXIT_SUCCESS);
}
(2)异常退出:子进程也可以通过调用abort()函数或触发一个信号来异常退出。abort()函数会立刻终止进程,触发SIGABRT信号,而信号处理程序则会默认终止进程。
#include <stdlib.h>
int main() {
// 子进程执行任务
// ...
// 异常退出
abort();
// 或者
raise(SIGABRT);
}
(3)返回值退出:子进程可以通过在main函数中返回一个整数值来退出。这个整数值会被传递给父进程作为退出状态码。
int main() {
// 子进程执行任务
// ...
// 返回值退出
return 0;
}
(4)exec()系列函数:子进程可以使用exec()系列函数来加载一个新的程序镜像,从而替换当前进程的内容。一旦调用exec()成功,子进程就会停止原有的代码执行,而是开始执行新程序的代码。这种方式不是直接终止进程,而是将子进程转变为新的程序。
#include <unistd.h>
int main() {
// 子进程执行任务
// ...
// 加载新程序
execl("/bin/ls", "ls", "-l", NULL);
}
无论子进程是通过哪种方式终止,父进程都可以通过使用 wait() 或 waitpid() 等系统调用来获取子进程的退出状态信息,并进行相应的处理。这样可以确保父进程及时清理僵尸进程,并释放相应的资源。
2.销毁僵尸进程
(1)wait
#include<iostream>
#include<unistd.h>
#include<sys/wait.h>
using namespace std;
int main(){
int status;
pid_t pid=fork();
if(pid==-1){
std::cout<<"父进程创建失败"<<std::endl;
return 1;
}
else if(pid==0)
return 3;//返回终止
else{
cout<<"child PID: "<<pid<<endl;
pid=fork();
if(pid==0)
exit(7);//exit函数终止
else{
cout<<"child PID: "<<pid<<endl;
wait(&status);//将之前终止的子进程相关信息保存到status变量,同时子进程完全销毁
if(WIFEXITED(status))//判断是否正常终止,如果正常退出,下面子进程返回值
cout<<"child send one:"<<WEXITSTATUS(status)<<endl;
wait(&status);//第二个终止的子进程
if(WIFEXITED(status))
cout<<"child send two:"<<WEXITSTATUS(status)<<endl;
sleep(30);
}
}
return 0;
}
return 或是 exit 都是把进程终止,但是子进程的系统资源还没有回收,父进程通过 wait 函数释放子进程所占据的资源。
注意:调用 wait 函数时,如果没有已终止的子进程,那么程序将阻塞(Blocking)直到子进程终止,因此需谨慎使用。
(2)waitpid
调用 waitpid 函数时,程序不会阻塞。
#include<iostream>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
using namespace std;
int main(){
pid_t pid=fork();//创建子进程
int status;
if(pid==0){//子进程
sleep(15);
return 24;
}
else{//父进程
while(!waitpid(-1,&status,WNOHANG)){//当没有子进程终止时,保持循环
sleep(3);//休眠三秒
cout<<"sleep 3sec"<<endl;
}//子进程终止后,资源被waitpid回收
if(WIFEXITED(status))//判断子进程是否正常退出
cout<<"child send :"<<WEXITSTATUS(status)<<endl;
}
return 0;
}