TCP/IP网络编程 (十):多进程服务器端 ---(1) 进程

进程概念及应用

 

两种类型的服务器端

 

服务器设计技术有很多,按使用的协议来分有TCP服务器和UDP服务器。

按处理方式来分有循环服务器并发服务器

目前最常用的服务器模型就这两种:
--循环服务器:服务器在同一时刻只能响应一个客户端的请求

--并发服务器:服务器在同一时刻可以响应多个客户端的请求

 

并发服务器端的实现方法

网络程序中数据通信时间比CPU运算时间占比更大,因此,向多个客户端提供服务是一种有效利用CPU的方式。

下面列出的是具有代表性的并发服务器端实现模型和方法:
--多进程服务器:通过创建多个进程提供服务

--多路复用服务器:通过捆绑同一管理I/O对象提供服务

--多线程服务器:通过生成与客户端等量的线程提供服务

 

 

理解进程

例如从网上下载一个游戏,此时的游戏只是程序,而不是进程。因为未进入运行状态。

运行程序后,游戏被加载到主内存并进入运行状态,这时才可称为进程。

进程是程序流的基本单位,若创建多个进程,则操作系统将同时运行。有时一个程序运行过程中也会产生多个进程,接下来要创建的多进程服务器就是其中代表。

 

提示:CPU核的个数与进程数

拥有2个运算设备的CPU称作双核CPU,4个运算器的CPU称作4核CPU。一个CPU可包含多个运算设备,核的个数与同时运行的进程数相同。若进程数超过核数,进程将分时使用CPU资源。

 

 

进程ID

无论进程是如何创建的,所有进程都会从操作系统分配到ID。此ID称为“进程ID”(PID),值为大于2的整数。

ps指令可以查看当前运行的所有进程。

 

通过调用fork函数创建进程

用于创建多进程服务器端的fork函数:

 

fork函数将创建调用的进程副本(子进程)。父进程和子进程都将执行fork函数调用后的语句。

程序流根据fork函数的返回值来区分父子进程:

(fork完毕后,父进程中pid为子进程进程ID,子进程中pid为0)

 

调用fork函数后的程序运行流程:

进程复制完成后,父进程将lval的值加1,子进程将gval的值加1。

因为fork函数调用后分成了完全不同的进程,所有父子进程不会相互影响。

 

示例:

#include<stdio.h>
#include<unistd.h>

int gval = 10;
int main(int argc,char *argv[])
{
	pid_t pid;
	int lval = 20;
	gval++, lval+=5;

	pid = fork();                           //创建子进程,子进程pid是0
	if (pid == 0)				//子进程
		gval += 2, lval += 2;
	else					//父进程
		gval -= 2, lval -= 2;
	
	if (pid == 0)				//子进程
		printf("Chile  Proc: [%d,%d] \n",gval,lval);
	else					//父进程
		printf("Parent Proc: [%d,%d] \n",gval,lval);
	return 0;
}

运行结果:

 

 

可以看出,调用fork函数后,父子进程拥有完全独立的内存结构。

 

进程和僵尸进程

进程创建和进程销毁同等重要。如果未认真对待进程销毁,它们将变成僵尸进程。

 

僵尸进程

僵尸进程是指完成执行(通过 exit 系统调用,或运行时发生致命错误或收到终止信号所致)但在操作系统的进程表中仍然有一个表项(进程控制块PCB),处于"终止状态"的进程。僵尸进程会占用系统中的重要资源。

 

产生僵尸进程的原因

调用fork函数产生子进程的终止方式:

--传递参数并调用exit函数

--main函数中执行return语句并返回值

 

向exit函数传递的参数值和main函数的return语句返回的值都会传递给操作系统。而操作系统不会销毁子进程,直到把这些值传递给产生该子进程的父进程。处在这种状态下的进程就是僵尸进程。将子进程变成僵尸进程的正是操作系统。

 

操作系统不会主动把这些值传递给父进程,只有父进程主动发送请求(函数调用)时,操作系统才会传递该值,否则操作系统将一直保存,并让子进程长时间处于僵尸进程状态。

 

示例:创建僵尸进程

#include<stdio.h>
#include<unistd.h>

int main(int argc,char *argv[])
{
	pid_t pid = fork();

	if (pid == 0)		//if Child Process
	{
		puts("Hi,I am a child process!");
	}
	else 
	{
		printf("Child Process ID: %d \n",pid);	//父进程保存子进程pid
		sleep(30);		//父进程暂停30秒。	
	}

	if (pid == 0)
		puts("End child process!");
	else 
		puts("End parent process!");
	return 0;
}

运行结果:zombie.c

(其中父进程关闭是在30秒后)

 

运行结果:僵尸进程的验证 (在跳出父进程暂停30秒状态前,验证子进程是否为僵尸进程。)


可以看出,PID为10977的进程状态为僵尸进程。经过30秒的等待时间后,PID为10976的父进程和之前的僵尸子进程同时销毁。

 

提示:后台处理

后台处理是指将控制台窗口中的指令放到后台运行的方式。

例如上例:

 

 

即可在同一控制台输入ps au命令查看进程,无需再打开一个控制台窗口。

 

 

销毁僵尸进程1:利用wait函数

为了销毁子进程,父进程应主动请求获取子进程的返回值。

调用wait函数:

 

调用此函数时如果已有子进程终止,那么子进程终止时传递的返回值将保存到该函数的参数所指内存空间。但函数参数指向的单元还包含其他信息,因此需要通过下列宏进行分离:

--WIFEXITED子进程正常终止时返回“真”(true)

--WEXITSTATUS返回子进程的返回值。

 

也就是说,向wait函数传递变量status的地址时,调用wait函数后应编写如下代码。

if (WIFEXITED(status))      //是正常终止的吗?
{
    puts("Normal termination!");
    printf("Child pass num: %d",WEXITSTATUS(status)); 
}

编写示例,此示例中不会再让子进程变成僵尸进程:

//wait.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>

int main(int argc,char *argv[])
{
	int status;
	pid_t pid = fork();

	if (pid == 0)
	{
		return 3;			//终止第一个子进程,返回3
	}
	else 
	{
		printf("Child PID: %d \n",pid);
		pid = fork();
		if (pid == 0)
		{
			exit(7);		//终止第二个子进程,返回7
		}
		else 
		{
			printf("Child PID: %d\n",pid);
			wait(&status);	        //将之前终止的子进程相关信息保存到status变量,同时子进程完全销毁
			if (WIFEXITED(status))	//若正常终止,则调用WEXITSTATUS宏输出子进程的返回值
				printf("Child send one: %d \n",WEXITSTATUS(status));
		
			wait(&status);	        //因为之前创建了2个进程,所以再次调用wait函数和宏
			if (WIFEXITED(status))
				printf("Child send two: %d \n",WEXITSTATUS(status));
			sleep(30);		//休眠30秒
		}
	}
	return 0;
}

运行结果:

 

在sleep 30秒结束之前,通过ps au查看,系统中并无上述PID对应的子进程。这是因为调用wait函数完全销毁了进程。这就是通过wait函数消灭僵尸进程的方法。

如果没有已终止的子进程,那么程序将阻塞直到有子进程终止,因此需谨慎调用该函数。

 

 

销毁僵尸进程2:使用waitpid函数

 

wait函数会引起程序阻塞,还可以考虑调用waitpid函数。这是防止僵尸进程的第二种方法,也是防止阻塞的方法。

 

调用waitpid函数的示例,程序不会阻塞:

#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>

int main(int argc,char *argv[])
{
	int status;
	pid_t pid = fork();

	if (pid == 0)
	{
		sleep(15);		//推迟子进程的执行,这会导致程序延迟15秒
		return 24;
	}
	else 
	{
		while (!waitpid(-1,&status,WNOHANG))	//传递WHNOHANG参数,若没有终止的子进程将返回0
		{
			sleep(3);
			puts("sleep 3sec.");
		}

		if (WIFEXITED(status))		
			printf("Child send %d \n",WEXITSTATUS(status));	//WEXITSTATUS返回子进程的返回值	
	}
	return 0;
}

运行结果:



 

while循环执行了5次,直到子进程返回。这也证明了waitpid未阻塞。


猜你喜欢

转载自blog.csdn.net/amoscykl/article/details/80282366