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