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

信号处理

父进程往往很繁忙,因此不能只调用waitpid函数以等待子进程终止。接下来讨论解决方案。

 

向操作系统求助

子进程终止的识别主体是操作系统,因此,若操作系统能把终止信息告诉正忙于工作的父进程,父进程将暂时放下工作,处理子进程终止相关事宜。这将有助于构建高效的程序。 

 

信号处理(Signal Handing)机制

 信号是在特定事件发生时,由操作系统向进程发送的消息。

 

信号与signal函数

信号注册函数:

 

函数返回类型:参数为int型,返回void型函数指针。

 

第一个参数signo为特殊情况信息,第二个参数为特殊情况下将要调用的函数的地址值(指针)。

 发生第一个参数代表的情况时,调用第二个参数所指的函数。

 

可在signal函数中注册的部分特殊情况和对应的常数:

SIGALRM:已到通过调用alarm函数注册的时间

SIGINT:输入CTRL+C

SIGCHLD:子进程终止

 signal(SIGCHLD,mychild); ​//子进程终止调用mychild函数
 signal(SIGALRM,timeout);   //已到alarm函数注册的时间,调用timeout函数
 signal(SIGINT,keycontrol); //输入CTRL+C时调用keycontrol函数

以上是信号注册过程,注册好信号后,发生注册信号时,操作系统将调用该信号对应的函数。

 

alarm函数:

 

传递给该函数一个正整数参数,相应时间后产生SIGALRM信号。 

 

 

 

信号处理示例:

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

void timeout(int sig)
{
	if (sig == SIGALRM)
		puts("Time out!");
	alarm(2);				//在信号处理器(Handler)中使用alarm函数,会每个2秒重复产生SIGALRM信号 
}

void keycontrol(int sig)
{
	if (sig == SIGINT)
		puts("CTRL+C pressed!");
}

int main(int argc, char *agrv[])
{
	int i;
	signal(SIGALRM,timeout);	//alarm产生SIGALRM信号,进入timeout函数
	signal(SIGINT,keycontrol);	//输入CTRL+Cc产生SIGINT信号,进入keycontrol函数
	alarm(2);

	for (i = 0; i < 5; i++)		//5次等待睡眠,产生信号唤醒进程,睡眠状态被打断。
	{
		puts("wait ...");
		sleep(100);
	}

	return 0;
}

运行结果:

1.SIGALRM信号:

 

2.SIGINT信号:

 

利用sigaction函数进行信号处理

sigaction函数,类似与signal函数,完全可以代替后者,也更稳定。

 

 声明并初始化sigaction结构体变量以调用上述函数,结构体定义如下:

struct sigaction
{
    void (*sa_handler)(int);
    sigset_t sa_mask;
    int sa_flags;
}

sa_handler成员保存信号处理函数的指针值。sa_mask,sa_flags的所有位均初始化为0即可。

 

 sigaction函数示例: 

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

void timeout(int sig)
{
	if (sig == SIGALRM)
		puts("Time out!");
	alarm(2);
}

int main(int argc, char *argv[])
{
	int i;
	struct sigaction act;		//声明sigaction结构体变量
	act.sa_handler = timeout;	//在sa_handler中保存处理函数指针值
	sigemptyset(&act.sa_mask);	//将sa_mask所有位初始化为0
	act.sa_flags = 0;		//sa_flags成员初始化为0
	sigaction(SIGALRM,&act,0);

	alarm(2);

	for (i = 0; i < 5; i++)
	{
		puts("wait ...");
		sleep(100);
	}
	return 0;
}

 

利用信号处理技术消灭僵尸进程

子进程终止时将产生SIGCHLD信号,接下来利用sigaction函数编写示例。

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

void read_childproc(int sig)
{
	int status;
	pid_t id = waitpid(-1,&status,WNOHANG);	//调用waitpid函数,若子进程正常终止,不会成为僵尸进程
	if (WIFEXITED(status))
	{
		printf("Removed proc id: %d \n",id);
		printf("Child send: %d \n",WEXITSTATUS(status));
	}
}

int main(int argc, char *argv[])
{
	pid_t pid;
	struct sigaction act;
	act.sa_handler = read_childproc;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;
	sigaction(SIGCHLD,&act,0);			//注册子进程终止(SIHCHLD)的信号,若子进程终止,调用read_childproc函数。

	pid = fork();
	if (pid == 0)		//子进程执行区域
	{
		puts("Hi! I'm child process one~");
		sleep(10);
		return 12;		//子进程终止,发出SIHCHLD信号,sigaction函数调用read_childproc处理函数
	}
	else				//父进程执行区域
	{	
		sleep(1);		//睡眠1s是为了延迟下一句printf的输出时间,让子进程先输出。下同
		printf("Child proc id: %d \n",pid);
		pid = fork();
		if (pid == 0)	        //另一子进程执行区域
		{
			puts("Hi! I'm child process two~");
			sleep(10);
			exit(24);	//子进程终止,发出SIHCHLD信号, sigaction函数调用read_childproc处理函数
		}
		else 
		{
			sleep(1);
			int i;
			printf("Child proc id: %d \n",pid);
			for (i = 0; i < 5; i++)
			{
				puts("wait ...");
				sleep(5);
			}
		}
	}
	return 0;
} 

运行结果:

 

可以看出,子进程并为变成僵尸进程,而是正常终止了。

 

基于多任务的并发服务器 

 利用fork函数编写并发服务器。

 

基于进程的并发服务器模型 

 扩展回声服务器端,使其可以同时向多个服务端提供服务。

基于多进程的并发回声服务器端的实现模型:

                        

 

每当有客户端请求服务时,回声服务器端都创建子进程以提供服务。

经过如下过程,这是与之前的回声服务器端的区别所在:

---第一阶段:回声服务器端(父进程)通过调用accept函数受理连接请求

---第二阶段:此时获取的套接字文件描述符创建并传递给子进程

---第三阶段:子进程利用传递来的文件描述符提供服务

 

实现并发服务器

/* 实现并发服务器端*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
#include<arpa/inet.h>
#include<sys/socket.h>

#define BUF_SIZE 30
void error_handling(char *message)
{
	fputs(message,stderr);
	fputc('\n',stderr);
	exit(1);
}

/* Handler */
void read_childproc(int sig)
{
	pid_t pid;
	int status;
	pid = waitpid(-1,&status,WNOHANG);
	printf("removed proc id: %d \n",pid);
}

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	struct sockaddr_in serv_adr, clnt_adr;

	pid_t pid;
	struct sigaction act;
	socklen_t adr_sz;
	int str_len, state;
	char buf[BUF_SIZE];
	if (argc != 2) {
		printf("Usage: %s <port> \n",argv[0]);
		exit(1);
	}
	act.sa_handler = read_childproc;			//设置信号处理函数
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;
	state = sigaction(SIGCHLD,&act,0);			//子进程终止时调用Handler
	serv_sock = socket(PF_INET,SOCK_STREAM,0);
	memset(&serv_adr,0,sizeof(serv_adr));
	serv_adr.sin_family = AF_INET;
	serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_adr.sin_port = htons(atoi(argv[1]));

	if (bind(serv_sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr)) == -1)
		error_handling("bind() error");
	if (listen(serv_sock,5) == -1)
		error_handling("listen() error");

	while (1) 
	{
		adr_sz = sizeof(clnt_adr);
		clnt_sock = accept(serv_sock,(struct sockaddr*)&clnt_adr,&adr_sz);
		if (clnt_sock == -1)
			continue;
		else 
			puts("new client connected...");
		
		pid = fork();
		if (pid == -1)
		{
			close(clnt_sock);  
			continue;
		}
		if (pid == 0)		//子进程运行区域
		{
			close(serv_sock);   //终止子进程中的服务端套接字(服务器端只存在于父进程)
			while((str_len = read(clnt_sock,buf,BUF_SIZE)) != 0)
				write(clnt_sock,buf,str_len);

			close(clnt_sock);
			puts("client disconnected...");
			return 0;		//调用Handler
		}
		else				//父进程运行区域
			close(clnt_sock);       //终止父进程中的客户端连接套接字(客户端只存在于子进程)
	}
	close(serv_sock);
	return 0;
} 

运行结果:

 

 启动服务端后,创建多个客户端并建立连接,可以验证服务器端同时向大多数客户端提供服务。

 

通过fork函数复制文件描述符

mpserv.c中的fork函数调用过程如图所示:调用fork函数后,2个文件描述符指向同一套接字

                                

为了将文件描述符整理成如图形式,mpserv.c中74,83行调用了close函数

 

 

分割TCP/IP的I/O程序

已实现的回声客户端传输数据后需要等待服务器端返回的数据,因为代码中重复调用了read和write函数。现在可以创建多个进程,因此可以分割数据收发过程。

                                       

客户端的父进程负责接收数据,子进程负责发送数据。这样,无论客户端是否从服务器端接收完数据都可以进行传输。

 

分割I/O的一个另一个好处是:可以提高频繁交换数据的程序性能

区别:

                         

 (右侧是分割I/O后的客户端数据传输方式)

 

回声客户端的I/O程序分割 

/* 分割I/O的回声客户端 */
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>

#define BUF_SIZE 30
void error_handling(char *message);
void read_routine(int sock,char *buf);
void write_routine(int sock,char *buf);

int main(int argc,char *argv[])
{
	int sock;
	pid_t pid;
	char buf[BUF_SIZE];
	struct sockaddr_in serv_adr;
	if (argc != 3) {
		printf("Usage: %s <IP> <port> \n",argv[0]);
		exit(1);
	}

	sock = socket(PF_INET,SOCK_STREAM,0);
	memset(&serv_adr,0,sizeof(serv_adr));
	serv_adr.sin_family = AF_INET;
	serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
	serv_adr.sin_port = htons(atoi(argv[2]));

	if (connect(sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr)) == -1)
		error_handling("connect() error!");

	pid = fork();
	if (pid == 0)
		write_routine(sock,buf);		//子进程发送
	else
	{	
		read_routine(sock,buf);			//父进程接收
	}
	close(sock);
	return 0;
}

void read_routine(int sock,char *buf)
{
	while(1)
	{
		int str_len = read(sock,buf,BUF_SIZE);
		if (str_len == 0)
			return ;
		
		buf[str_len] = 0;
		printf("Message from server: %s",buf);
	}
}

void write_routine(int sock,char *buf)
{
	while(1)
	{
		fgets(buf,BUF_SIZE,stdin);
		if (!strcmp(buf,"q\n") || !strcmp(buf,"Q\n"))
		{
			shutdown(sock,SHUT_WR);			//shutdown函数只断开一个流,SHUT_WR代表断开输出流
			return;
		}
		write(sock,buf,strlen(buf));
	}
}

void error_handling(char *message)
{
	fputs(message,stderr);
	fputc('\n',stderr);
	exit(1);
} 

运行结果与普通客户端相同。




基于多任务的服务器端实现方法讲解到此。

猜你喜欢

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