信号处理
父进程往往很繁忙,因此不能只调用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); }
运行结果与普通客户端相同。
基于多任务的服务器端实现方法讲解到此。