僵尸进程及信号处理
- 多进程服务器:通过创建多个进程提供服务
- 多路复用服务器:通过捆绑并统一管理I/O对象提供服务 。
- 多线程服务器:通过生成与客户端等量的线程提供服务 。
进程:运行着的程序,被从磁盘加载到内存并有运行权限。
拥有 2 个运算设备的 CPU 称作双核( Daul ) CPU ,拥有 4 个运算器的 CPU称作 4 核(Quad )C PU 。也就是说, 1 个 CPU 中可能包含多个运算设备(核)。核的个数与可同时运行的进程数相同。采用多线程技术后,一个进程的线程可以在多核上运行。
进程ID
每一个进程都有一个唯一的PID。LINUX下可以通过
ps au
查看
fork函数创建进程
pid_t fork(void*);
//成功范围ID 失败-1
fork函数将创建调用的进程副本(子进程)。也就是说 ,并非根据完全不同的程序创建进程, 而是复制正在运行的、调用fork函数的进程 。 另外,两个进程都将执行fork函数调用后的语句(准确地说是在fork函数返回后)。 但因为通过同一个进程、复制相同的内存空间 ,之后的程序流要根据 fork函数的返回值加以区分。利用 fork函数的如下特点区分程序执行流程。
父进程 : fork函数数返回子进程ID 。
子进程 : fork函数返回 0 。
此处"父进程" ( Parent Process )指原进程, 即调用fork 函数的主体,而"子进程" (Child Process ) 是通过父进程调用 fork函数复制出的进程 。 接下来讲解调用 fork函数后的程序运行流程,
父进程调用 fork函数的同时复制出子进程,并分别得到fork函数的返回值。但复制前,父进程将全局变量gval增加到 11, 将局部变量lval的值增加到25 , 因此在这种状态下完成进程复制。复制完成后根据fork函数的返回类型区分父子进程 。 父进程程将lval的值加 1 ,但这不会影响子进程的 lval值 。 同样, 子进程将gval的值加1也不会影响到父进程的 gval。因为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();
if(pid==0) // if Child Process
gval+=2, lval+=2;
else // if Parent Process
gval-=2, lval-=2;
if(pid==0)
printf("Child Proc: [%d, %d] \n", gval, lval);
else
printf("Parent Proc: [%d, %d] \n", gval, lval);
return 0;
}
进程和僵尸进程
进程销毁也和进程创建同等重要 。 如果未认真对待进程销毁,它们将变成僵尸进程困扰各位 。
僵尸进程
进程完成工作后 ( 执行完main 函数中的程序后)应被销毁,但有时这些进程将变成僵尸进程,占用系统中的重要资源 。 这种状态下的进程称作"僵尸进程",这也是给系统带来负担的原因之一。
调用fork函数产生子进程的终止方式 。
传递参数并调用exit函数 。
main函数中执行return语句并返回值 。
向 exit函数传递的参数值和main函数的return语句返回的值都会传递给操作系统 。 而操作系统不会销毁子进程,直到把这些值传递给产生该子进程的父进程。 处在这种状态下的进程就是僵尸进程 。 也就是说,将子进程变成僵尸进程的正是操作系统 。
应该向创建子进程的父进程传递子进程的 exit参数值或return语句的返回值 。 从而销毁僵尸进程。如果父进程终止,处于僵尸状态的子进程将同时销毁。
如何向父进程传递这些值呢?操作系统不会主动把这些值传递给父进程 。只有父进程主动发起请求(函数调用)时,操作系统才会传递该值 。 换言之,如果父进程未主动要求获得子进程的结束状态值,操作系统将一直保存, 并让子进程长时间处于僵尸进程状态 。
为了销毁子进程,父进程应主动请求获取子进程的返回值。
销毁僵尸进程方法1:利用wait函数
pid_t wait(int * statloc);
// 成功时退回终止的子进程 ID, 失败时返回-1。
调用此函数时如果已有子进程终止 ,那么子进程终止时传递的返回值( exit函数的参数、main函数的 return返回值)将保存到该函数的参数所指内存空间。 但函数参数指向的单元中还包含其他信息,因此需要通过下列宏进行分离 。
WIFEXITED 子进程程正常终止时返回true
WEXITSTATUS 返回子进程的返回值 。
也就是说, 向wait函数传递变量status的地址时,调用wait函数后应编写如下代码 。
#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; //子进程1结束
}
else
{
printf("Child PID: %d \n", pid);
pid=fork();
if(pid==0)
{
exit(7);//子进程2结束
}
else
{
printf("Child PID: %d \n", pid);
wait(&status);
if(WIFEXITED(status))
printf("Child send one: %d \n", WEXITSTATUS(status));
wait(&status);
if(WIFEXITED(status))
printf("Child send two: %d \n", WEXITSTATUS(status));
sleep(30); // Sleep 30 sec.
}
}
return 0;
}
这就是通过调用 wait函数消灭僵尸进程的方法 。 调用wait函数时,如果没有己终止的子进程,那么程序将阻塞( Blocking ) 直到有子进程终止,因此需谨慎调用该函数。
使用 waitpid 函数
wait函数会引起程序阻塞,还 可以考虑调用 waitpid 函数 。 这是防止僵尸进程的第二种 方法也是防止阻塞的方法 。
pid_t waitpid(pid_t pid, int * statloc, int options);
// 成功时返回终止的子进程ID,(或0),失败时返回-1。
//PID: 等待终止的目标子进程的ID,若传递 - 1 ,则与wait函数相同,可以等待任意子进程终止。
//statloc: 与wait函数的 statloc参数具有相同含义。
//options:传递头文件sys/wait.h中声明的常量WNOHANG,即使没有终止的子进程也不会进入阻塞状态,而是返回O并退出函数 。
#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);
return 24;
}
else
{
while(!waitpid(-1, &status, WNOHANG))
{
sleep(3);
puts("sleep 3sec.");
}
if(WIFEXITED(status))
printf("Child send %d \n", WEXITSTATUS(status));
}
return 0;
}
父进程未阻塞。
信号处理
父进程不可能一直调用waitpid
函数等待子进程终止。可以让操作系统将子进程结束的消息传递给父进程,那么当收到消息之后父进程就可以终止子进程。这就是信号处理机制。此处的"信号"是在特定事件发生时由操作系统向进程发送的消息 。
进程:"嘿,操作系统!如果我之前创建的子进程终止,就帮我调用 zombie-handier函数。 "
OS: “好的!如果你的子进程终止,我会帮你调用 zombie-handler函数,你先把该函数要执 行的语句编好!”
上述对话中进程所讲的相 当于"注册信号"过程,即进程发现自己的子进程结束时,请求操作系统调用特定函数 。该请求通过如下函数调用完成(因此称此函数为信号注册函数)。
#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
// 为了在产生信号时调用 返回之前注册的函数指针。
/*
函数名:signal
参数: int signo, void (*func)(int)
返回类型:参数为int, 返回值为void的函数指针。
*/
调用上述函数时,第一个参数为特殊情况信息,第二个参数为特殊情况下将要调用的函数的地址值(指针)。 发生第一个参数代表的情况时,调用第二个参数所指的函数 。下面给出可以在signal函数中注册的部分特殊情况和对应的常数 。
SIGALRM: 已到通过调用alarm函数注册的时间 。
SIGINT: 输入CTRL+C 。
SIGCHLD: 子进程终止 。
例子: 子进程终止则调用 mychild函数 。
已到通过alarm函数注册的时间,请调用 timeout函数 。 "
输入CTRL +C时调用 keycontrol函数。
unsigned int alarm(unsigned int seconds);
// 返回0或以秒为单位的距SIGALRM信号发生所剩时间。
//如果调用该函数的同时向它传递一个正整型参数,相应时间后(以秒为单位)将产生SIGALRM信号。
signal(SIGCHLD,mychild)
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void timeout(int sig){//信号处理函数
if(sig==SIGALRM)
puts("Time out!");
alarm(2); //为了每隔2秒重复产生SIGALRM信号,在信号处理器中调用 alarm 函数 。
}
void keycontrol(int sig){
if(sig==SIGINT)
puts("CTRL+C pressed");
}
int main(int argc, char *argv[]){
int i;
signal(SIGALRM, timeout);//注册信号处理函数
signal(SIGINT, keycontrol);
alarm(2);//预约2秒后发生SIGALRM信号。
for(i=0; i<3; i++){
puts("wait...");
sleep(100);
}
return 0;
}
也就是说,再过300秒 、 约5分钟后终止程序,这是相当长的 一段时间,但实际执行时只需不到 10秒 。
发生信号时将唤醒由于调用 sleep函数而进入阻塞状态的进程。
调用函数的主体的确是操作系统,但进程处于睡眠状态时无法调用函数。 因此产生信号时,为了调用信号处理器,将唤醒由于调用sleep函数而进入阻塞状态的进程 。 而且,进程一旦被唤醒,就不会再进入睡眠状态 。即使还未到sleep函数中规定的时间也是如此 。所以上述示例运行不到10秒就会结束,连续输入CTRL+C则有可能 1秒都不到 。
利用sigaction函数进行信号处理
sigaction函数,它类似于signal函数,而且完全可以代替后者,也更稳定 。 之所以稳定,是因为如下原因: signal 函数在 UNIX 系列的不同操作系统中可能存在区别,但 sigaction函数完全相同 "
int sigaction(int signo, const struct sigaction * act, struct sigaction *oldact);
//成功时退回 θ ,失败时退回-1
/*
slgno 与 signal 函数相同,传递信号信息 。
act 对应于第一个参数的信号处理函数 ( 信号处理器 ) 信息 。
oldact 通过此参数获取之前注册的信号处理函数指针,若不需要则传递0 。
*/
struct sigaction
{
void (*sa_handler)(int);
sigset_t sa_mask;//0
int sa_flags;//0
}
#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;
act.sa_handler=timeout;
sigemptyset(&act.sa_mask);//调用 sigemptyset函数将sa_mask成员的所有位初始化为0。
act.sa_flags=0;
sigaction(SIGALRM, &act, 0);
alarm(2);
for(i=0; i<3; i++)
{
puts("wait...");
sleep(100);
}
return 0;
}