博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
多进程服务器端 ---(2) *
阅读量:2179 次
发布时间:2019-05-01

本文共 6937 字,大约阅读时间需要 23 分钟。

信号处理

父进程往往很繁忙,因此不能只调用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
#include
#include
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
#include
#include
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
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 30void 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
\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
#include
#include
#include
#include
#include
#define BUF_SIZE 30void 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
\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);}

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




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

你可能感兴趣的文章
Leetcode C++《热题 Hot 100-48》406.根据身高重建队列
查看>>
《kubernetes权威指南·第四版》第二章:kubernetes安装配置指南
查看>>
Leetcode C++《热题 Hot 100-49》399.除法求值
查看>>
Leetcode C++《热题 Hot 100-51》152. 乘积最大子序列
查看>>
Leetcode C++ 《第181场周赛-1》 5364. 按既定顺序创建目标数组
查看>>
Leetcode C++ 《第181场周赛-2》 1390. 四因数
查看>>
阿里云《云原生》公开课笔记 第一章 云原生启蒙
查看>>
阿里云《云原生》公开课笔记 第二章 容器基本概念
查看>>
阿里云《云原生》公开课笔记 第三章 kubernetes核心概念
查看>>
阿里云《云原生》公开课笔记 第四章 理解Pod和容器设计模式
查看>>
阿里云《云原生》公开课笔记 第五章 应用编排与管理
查看>>
阿里云《云原生》公开课笔记 第六章 应用编排与管理:Deployment
查看>>
阿里云《云原生》公开课笔记 第七章 应用编排与管理:Job和DaemonSet
查看>>
阿里云《云原生》公开课笔记 第八章 应用配置管理
查看>>
阿里云《云原生》公开课笔记 第九章 应用存储和持久化数据卷:核心知识
查看>>
linux系统 阿里云源
查看>>
国内外helm源记录
查看>>
牛客网题目1:最大数
查看>>
散落人间知识点记录one
查看>>
Leetcode C++ 随手刷 547.朋友圈
查看>>